3. Mouvement et alimentation des aiguillesa) MatérielComme expliqué en introduction, les aiguilles du réseau sont mues par des petits servos 4G, fournis avec le matériel Tam Valley. La polarité des cœurs étaient à l'origine manœuvrées par les cartes-filles.
Celles-ci sont dotées d'un relai DPDT qui n'autorise qu'une gestion 'binaire' de la polarité du cœur (elle correspond à l'un OU l'autre des rails) sans pouvoir totalement l'isoler pendant le mouvement, d'où les fréquents court-circuits.
J'ai donc acheté des cartes relais 2 relais − solution court-terme − sur un site chinois bien connu. Elles me permettent de gérer la polarité des cœurs en toute liberté :
- j'alimente le relai 1 pour polariser le cœur sur le rail 1
- j'alimente le relai 2 pour polariser le cœur sur le rail 2
- si aucun des deux relais n’est alimenté, le coeur est isolé
Pour gérer les servos, j'avais une carte
driver 12 servos Adafruit sous le coude. Cette carte communique avec l'Arduino par le bus I2C. C'est surdimensionné pour 3 servos, mais je comptais m'en servir pour gérer des éclairages sur les ports inutilisés.
b) LogicielC'est là que j'ai commencé à me frotter aux Classes, en suivant la série d'articles "
Le monde des objets" de Locoduino bien sûr ! Soyez indulgents sur la mise en œuvre
Je suis preneur des conseils pour améliorer le code. J’ai aussi pas mal joué sur les manipulations de bits pour la gestion des registres d’état.
Le fichier .h déclare une classe
turnoutDriver avec ses propriétés, fonctions privées et publiques.
Il déclare aussi quelques fonctions “générales” pour initialiser la librairie servo, initialiser les objets, mettre à jour l’ensemble des objets ou encore configurer les objets pendant l'exécution.
Le rôle de cette librairie est de mouvement des aiguilles et l’alimentation des cœurs. A terme, elle doit également communiquer avec le gestionnaire des façades de contrôle, le gestionnaire de détecteurs infrarouges (qui vont également donner des ordres selon le sens de marche et la position des aiguilles) et enfin avec un automate (qui va générer des itinéraires aléatoires).
Pour cela, la librairie crée plusieurs registres globaux :
Le registre d’état
turnoutDriver_states : c’est un byte qui représente la position des aiguilles. Chaque bit représente une aiguille, un 1 si elle est déviée, un 0 si elle est fermée. Lorsqu’une aiguille est en cours de mouvement, son état représente sa position en fin de mouvement : si nécessaire (un train arrive sur une aiguille qui va être talonnée), on peut lui donner un contre-ordre même si la manœuvre n’est pas terminée.
Ce registre est utilisé en lecture / écriture par le gestionnaire d’aiguilles, et en lecture uniquement par les autres librairies qui ont besoin de connaître la position des aiguilles.
Le registre de progression
turnoutDriver_progress : encore un byte, qui mémorise les aiguilles en cours de manœuvre. Chaque bit représente une aiguille, un 1 si elle est en cours de mouvement, un 0 si elle est immobile.
Ce registre est utilisé en lecture/écriture par le gestionnaire d’aiguilles, et en lecture uniquement par le gestionnaire des façades (pour gérer le clignotement des LEDs).
Le registre d’ordres
turnoutController_buttons, toujours un byte, sert à transmettre les boutons de contrôle activés les ordres d’inversion des aiguilles, qu’ils viennent de la détection des boutons des façades de contrôle, de l’automate, des détecteurs IR… Là encore, chaque bit représente une aiguille. Un bit à 1 signifie qu’il faut inverser la position de l’aiguille correspondante.
Ce registre est utilisé en lecture/écriture par à peu près tout le monde ! Le gestionnaire d’aiguilles lit les bits et les réinitialise un fois les ordres traités. Les autres librairies positionnent les bits à 1 pour transmettre un ordre de manœuvre.
La taille de ces registres limitent l’utilisation à 8 aiguilles (ou 8 servos).
La librairie se charge d’instancier le gestionnaire de servos (ici c’est la librairie Adafruit_PWMServoDriver qui pilote le PCA9685 de la carte Adafruit) et la fonction
turnoutDriverStart() va l’initialiser :
Adafruit_PWMServoDriver servos = Adafruit_PWMServoDriver();
void turnoutDriverStart()
{
servos.begin();
servos.setPWMFreq(60);
}
L’objet
turnoutDriver possède les propriétés suivantes :
- le numéro de l’aiguille (qui correspond également au numéro de port sur la carte driver des servos)
- la valeur numérique des positions fermée/déviée
- la vitesse du mouvement
- s’il y a un cœur à alimenter ou non
- les pins sur lesquels les relais sont connectés
- le signal pour activer les relais (0 ou 5v : HIGH ou LOW)
- la valeur numérique de la position courante du servo
- le timestamp du dernier mouvement (également utilisé pour gérer le mouvement lent)
La fonction
TurnoutDriverInit() va prendre en charge le processus d’instanciation et d’initialisation des aiguilles :
void turnoutDriverInit(int positionClosed, int positionThrown, byte pinFeederClosed, byte pinFeederThrown, bool feederOnLevel, int tempo)
{
turnoutDriver_register[turnoutDriver_count] = new turnoutDriver (positionClosed, positionThrown, pinFeederClosed, pinFeederThrown, feederOnLevel, tempo);
turnoutDriver_register[turnoutDriver_count]->begin();
turnoutDriver_count++;
}
Elle instancie d’abord l’objet
turnoutDriver, et s’assure que le courant est coupé dans le coeur :
turnoutDriver::turnoutDriver(int positionClosed, int positionThrown, byte pinFeederClosed, byte pinFeederThrown, bool feederOnLevel, int tempo)
{
_driverId = turnoutDriver_count;
_closed = positionClosed;
_thrown = positionThrown;
if (_closed > _thrown)
{
_closed *= -1;
_thrown *= -1;
}
_tempo = tempo;
_lastMove = 0;
if (pinFeederClosed == 255)
_hasFeeder = false;
else
{
_hasFeeder = true;
_pinFeederClosed = pinFeederClosed;
_pinFeederThrown = pinFeederThrown;
_feederOnLevel = feederOnLevel;
pinMode(_pinFeederClosed, OUTPUT);
pinMode(_pinFeederThrown, OUTPUT);
power(0);
}
}
La fonction récupère la référence de l’objet créé et le stocke dans le tableau
turnoutDriver_register. Ainsi, il est possible de créer successivement les aiguilles depuis le programme principal et de sous-traiter la mise à jour de l’ensemble des objets. C’est ce que fait la fonction
turnoutDriverUpdate() en parcourant ce tableau et invoquant successivement la fonction
update() de chaque objet :
void turnoutDriverUpdate()
{
for (byte i = 0; i < turnoutDriver_count; i++)
turnoutDriver_register[i]->update();
}
Le programme principal peut donc ressembler donc à ça :
#include "turnoutDriver.h"
void setup()
{
turnoutDriverStart();
turnoutDriverInit(394, 334, 8, 12, HIGH, 40);
turnoutDriverInit(347, 375, 8, 12, HIGH, 75);
turnoutDriverInit(357, 329, 8, 12, HIGH, 95);
}
void loop()
{
turnoutDriverUpdate();
}
La fonction d’initialisation va ensuite initialiser l’aiguille en position fermée :
void turnoutDriver::begin()
{
_position = _closed;
servos.setPWM(_driverId, 0, abs(_closed));
turnoutDriver_states &= ~(1 << _driverId);
turnoutDriver_progress &= ~(1 << _driverId);
power(1);
}
A chaque tour de programme, la fonction
update() regarde si un ordre a été transmis à l’aiguille via le registre
turnoutController_buttons. Si c’est le cas, on appelle la fonction d’inversion (
toggle()) puis on réinitialise le bit du registre d’ordre :
if (readButton())
{
toggle();
turnoutController_buttons &= ~(1 << _driverId);
}
Tous les mouvements (inversion, ouverture, fermeture) sont traités de la même façon.
D’abord on initialise le mouvement :
- on renseigne le registre d’état avec la position de destination (ouverte ou fermée)
- on renseigne le registre de progression, pour indiquer que l’aiguille est en mouvement
- on coupe de courant dans le cœur
void turnoutDriver::initiateMove(bool _direction)
{
if (_direction)
turnoutDriver_states |= 1 << _driverId;
else
turnoutDriver_states &= ~(1 << _driverId);
power(0);
turnoutDriver_progress |= 1 << _driverId;
}
A chaque tour de programme, on met à jour la position des aiguilles en mouvement. Voici donc la fonction
update() complète :
void turnoutDriver::update()
{
if (readButton())
{
toggle();
turnoutController_buttons &= ~(1 << _driverId);
}
if (watchProgress())
move();
}
La fonction
move() évalue la position réelle des aiguilles en mouvement par rapport à la position de destination.Si l’aiguille doit bouger et si l’intervalle de temps souhaité entre deux mouvements est atteint (pour gérer la vitesse), on incrémente/décrémente la position d’une unité. Et ainsi de suite.
void turnoutDriver::move()
{
if (millis() - _lastMove > _tempo)
{
if (state())
if (_position < _thrown)
{
_position++;
servos.setPWM(_driverId, 0, abs(_position));
}
else
terminateMove();
else
if (_position > _closed)
{
_position--;
servos.setPWM(_driverId, 0, abs(_position));
}
else
terminateMove();
_lastMove = millis();
}
}
Lorsque la position de destination est atteinte, on réinitialise le bit de progression et on remet le courant.
void turnoutDriver::terminateMove()
{
turnoutDriver_progress &= ~(1 << _driverId);
power(1);
}
Voilà pour l’essentiel.