Après la théorie et ses jolis dessins, la pratique et l’âpreté du langage...
Un décodeur d’accessoires universel
Un décodeur d’accessoires universel (3)
.
Par :
DIFFICULTÉ :★★☆
Résumé des épisodes précédents...
Universal Accessory Decoder, UAD pour les amis, est une bibliothèque destinée à piloter des accessoires via des circuits de puissances commandés par un Arduino. Les ordres peuvent être reçus par des codes DCC en écoutant les rails, ou par des boutons à actionner manuellement, comme un TCO (Tableau de contrôle optique). Le côté universel tient à la possibilité de piloter tous les types d’accessoires, via tous les types d’alimentation, commandés par tous les types de donneurs d’ordre. Bien sûr, la promesse est alléchante et difficile à tenir... Je compte sur la communauté pour m’aider à approcher l’universalité au plus près !
Le fichier Full.ino
L’exemple Full.ino, livré avec la bibliothèque, pilote quelques accessoires, deux servos et trois leds. Ce n’est qu’une petite partie de ce dont la bibliothèque est capable, mais je compte sur vous pour construire des croquis utilisant UAD avec des cas concrets impliquant plein d’accessoires ! Voyons maintenant comment le source de ce croquis d’exemple est organisé.
Première partie : les déclarations :
/*************************************************************
project: <DCC Accessory Decoder>
author: <Thierry PARIS>
description: <2 servos and 3 leds driven by dcc arduino>
*************************************************************/
#include "UniversalAccessoryDecoder.h"
UniversalAccessoryDecoder.h
est le fichier include principal de la bibliothèque.
#define kDCC_INTERRUPT 3
Nous entrons dans le vif du sujet : le décodage DCC a besoin d’un vecteur d’interruption. En clair, il faut lui dire sur quelle pin l’Arduino va recevoir les paquets. Mais évidemment, il faut le faire indirectement sinon ce serai trop simple ! Selon le modèle, chaque numéro de vecteur utilise une pin différente :
Board | int 0 | int 1 | int 2 | int 3 | int 4 | int 5 |
---|---|---|---|---|---|---|
Uno, Ethernet | 2 | 3 | ||||
Mega2560 | 2 | 3 | 21 | 20 | 19 | 18 |
Leonardo | 3 | 2 | 0 | 1 | 7 |
Source : http://arduino.cc/en/Reference/atta...
J’ai ici utilisé la pin 20 de l’Arduino Mega parce que le shield l293d utilisé pour certains tests me cache les pins 0 à 13 !
#define AccessoryServoNumber 2
#define SERVO1 0
#define SERVO2 1
#define SERVO1_PORT 0
#define SERVO2_PORT 1
#define AccessoryMotorLightNumber 3
#define LIGHT1 2
#define LIGHT2 3
#define LIGHT3 4
#define LIGHT1_PORT 0
#define LIGHT2_PORT 1
#define LIGHT3_PORT 2
Il y a une liste d’accessoires avec deux servos (SERVO1
, SERVO2
pour 0 et 1) et trois leds (LIGHT1,2,3
qui sont les accessoires 2, 3 et 4).
Comme chaque accessoire va être un élément dans une liste, pour éviter de faire des erreurs et devoir me rappeler que l’accessoire 4, c’est la led verte, je préfère créer des defines pour chaque objet de la liste. Ça me permettra ensuite de traiter l’accessoire LIGHT3
plutôt que 4 ! J’aurais pu être encore plus précis et mettre LED_VERTE
ou LIGHT_GREEN
...
Rappelons que des defines sont juste des ordres donnés au compilateur de remplacer la définition LIGHT3
par sa valeur ’4’. Aucune mémoire n’est consommée, tout juste un petit temps supplémentaire de compilation est requis, mais le jeu en vaut la chandelle !
// Accessories
Accessories accessories;
DccCommander dccCommander;
ButtonsCommander buttonsCommander;
AccessoryGroup groupServos;
AccessoryGroup groupLights;
// Drivers
DriverArduino *arduino;
La liste d’accessoire est déclarée, les commanders utilisés aussi. Suivent les deux groupes ainsi que le driver Arduino dont j’aurai besoin pour cet exemple.
Rentrons dans le Setup.
/////////////////////////////////////////////
// Setup
//
void setup()
{
UAD_StartSetup();
La fonction UAD_StartSetup()
doit être lancée en premier dans le setup()
de votre programme. Elle commence par initialiser la console à 115200 bauds pour afficher les messages de debug. Elle mémorise aussi l’état de la mémoire au départ du setup()
pour pouvoir vous dire à la fin combien vous en avez consommé...
Initialisation des commanders
dccCommander.Setup(0x00, 0x00, kDCC_INTERRUPT);
dccCommander.SetStatusLedPin(13);
buttonsCommander.Setup(3,
new ButtonsCommanderPush(2),
new ButtonsCommanderSwitch(2),
new ButtonsCommanderPotentiometer(321, 0, 20, 145) // Link it to SERVO1,
// from 20 to 145 degrees
);
Le commander DCC initialise le numéro d’interruption décrit plus haut, puis définit la led qui clignotera pour témoigner de la présence du signal DCC : 13.
Vient ensuite le commander des boutons. Le premier argument de son Setup()
, c’est le nombre de boutons que l’on va utiliser. Viennent ensuite chaque bouton.
Le ButtonCommanderPush
est un bouton poussoir, à qui on déclare qu’il aura deux codes DCC à gérer, qu’il enverra en alternance à chaque appui.
Le ButtonCommanderSwitch
est un interrupteur, à qui on déclare aussi qu’il aura deux codes DCC à gérer. Chaque position physique de l’interrupteur enverra un code DCC.
Le ButtonCommanderPotentiometer
est le potentiomètre. On lui donne le code DCC qui va lui permettre d’identifier l’accessoire à déplacer, puis la plage de valeurs à envoyer à l’accessoire. La valeur brute lue sur un port analogique de l’Arduino est comprise entre 0 et 1023, elle sera mappée sur les valeurs données ici. C’est à dire qu’au minimum, le potentiomètre enverra 20 à l’accessoire, et 145 lorsqu’il sera à son maximum. C’est curieux comme ça ressemble à des angles de servo...
PUSH(buttonsCommander, 0)->AddDccId(320, 1);
PUSH(buttonsCommander, 0)->AddDccId(320, 0);
PUSH(buttonsCommander, 0)->Setup(26);
SWITCH(buttonsCommander, 1)->AddDccId(319, 0, 24);
SWITCH(buttonsCommander, 1)->AddDccId(319, 1, 25);
SWITCH(buttonsCommander, 1)->Setup();
POTENTIOMETER(buttonsCommander, 2)->Setup(8);
Une fois construite la liste des boutons, il faut les initialiser avec leur propre Setup().
Pour le bouton poussoir, le bouton de la liste, on donne avec AddDccId
les deux codes DCC dont il va s’occuper, puis on fait le Setup()
en lui donnant la pin Arduino sur lequel sera raccordé le bouton poussoir physique.
Idem pour le switch, le bouton 1
, qui déclare ses deux codes DCC, chacun étant lié à une pin. Avec ce type d’initialisation, on pourrait utiliser un switch à 10 positions de la même manière...
Enfin le potentiomètre déclare la pin qu’il utilise et éventuellement la précision de lecture de la valeur. Une nouvelle valeur doit être en dehors de l’intervalle valeur-précision / valeur+précision pour être prise en compte. Ici la valeur par défaut 1 est utilisée, donc rien à ajouter dans l’appel du Setup
...
// Drivers setups
arduino = new DriverArduino(AccessoryMotorLightNumber, AccessoryServoNumber);
arduino->Setup();
arduino->SetupPortServo(SERVO1_PORT, 2);
arduino->SetupPortServo(SERVO2_PORT, 3);
arduino->SetupPortMotor(LIGHT1_PORT, 9, ANALOG);
arduino->SetupPortMotor(LIGHT2_PORT, 10, ANALOG);
arduino->SetupPortMotor(LIGHT3_PORT, 11, ANALOG);
Voilà l’initialisation du driver Arduino
Ce driver dispose d’un nombre variable de ports moteur/lumière et de port servos. C’est pourquoi il faut lui dire dans son constructeur combien on veut en utiliser.
La fonction Setup()
est ensuite appelée, puis les setups de chaque port. Chaque port utilise une pin et déclare, lorsque que c’est nécessaire, que l’on va les utiliser en analogique/PWM. Les servos utilise la modulation de tension entre 0 et 5v pour savoir à quelle position se placer et doivent donc impérativement utiliser des pins PWM (entre 0 et 13 sur le Mega). Les leds ont aussi besoin de moduler la valeur de la tension pour gérer le fading.
// Accessories setups
accessories.Setup(
AccessoryMotorLightNumber + AccessoryServoNumber,
new AccessoryServo(316, 0, 20),
new AccessoryServo(314, 0, 314, 1, 20),
new AccessoryLight(1, 0),
new AccessoryLight(1, 1),
new AccessoryLight(2, 0)
);
Là on remplit la liste des accessoires. Le constructeur réclame en premier argument le nombre total d’accessoires gérés. C’est la somme de AccessoryMotorLightNumber
et AccessoryServoNumber
. Viennent ensuite les constructeurs des accessoires. Chacun déclare son ou ses codes DCC. Le dernier argument, optionnel, est la plupart du temps une durée en millisecondes. Par contre, la signification de ce temps change selon le type d’accessoire. Pour un moteur, mettez 0 pour un fonctionnement continu type moulin à vent. toute autre valeur donne le temps d’activation du moteur. Pour un aiguillage à solénoïde, mettez le moins possible, entre 50 et 300ms. A peu près 1000ms pour un dételeur. Pour une lumière, 0 signifie aussi fonctionnement continu, tandis qu’une autre valeur fixera la fréquence d’un clignotement. Enfin pour un servo, ce temps qui représente le délai entre deux mouvements d’un degré va décider de la vitesse du mouvement. A 0ms le mouvement se fait au maximum possible de la vitesse. Entre 1 et 5 millisecondes le mouvement sera plus lent, mais pendant ce temps rien d’autre ne pourra fonctionner, aucun autre accessoire ne bougera, et les ordres passés en DCC ou par les boutons seront perdus. Entre 6 et 20ms, c’est la même chose, mais le temps est suffisant pour mémoriser les ordres donnés par les boutons ou le DCC. Ces ordres seront traités dès que le mouvement du servo sera terminé. Enfin, plus de 20ms le mouvement est lent, traité degré par degré, mais entre deux micro mouvements, les autres accessoires ont le temps de s’activer !
SERVO(accessories, SERVO1)->Setup(arduino, SERVO1_PORT, 20, 145);
SERVO(accessories, SERVO2)->Setup(arduino, SERVO2_PORT, 10, 150, 4);
SERVO(accessories, SERVO2)->AddDccPosition(315, 0, 45);
SERVO(accessories, SERVO2)->AddDccPosition(315, 1, 135);
SERVO(accessories, SERVO2)->SetPowerCommand(49);
LIGHT(accessories, LIGHT1)->SetFading(20, 10);
LIGHT(accessories, LIGHT2)->SetFading(20, 10);
//LIGHT(accessories, LIGHT3)->SetFading(20, 10);
LIGHT(accessories, LIGHT1)->Setup(arduino, LIGHT1_PORT);
LIGHT(accessories, LIGHT2)->Setup(arduino, LIGHT2_PORT);
LIGHT(accessories, LIGHT3)->Setup(arduino, LIGHT3_PORT);
Il faut dire à UAD qui fait bouger quoi. C’est à dire sur quel port de quel driver chaque accessoire est branché. Tous les Setup()
commencent par dire sur quel driver, puis sur quel port le dispositif est branché. Mais là encore, selon le type d’accessoire les arguments suivants n’ont pas la même signification. Dans le cas d’un moteur, on va donner entre 0 et 255 la valeur de la tension utilisée pour le faire bouger. A 255 on est au maxi de ce que fournit l’alimentation, à 0 c’est 0v... Pour une lumière, le même principe est appliqué ce qui va permettre de régler l’intensité lumineuse ! Si rien n’est précisé, on est à 255, le maximum possible. Et pour un servo, il faut donner les valeurs mini et maxi de mouvement en degrés. Éventuellement comme ici, on peut ajouter des positions intermédiaires et leur affecter un code DCC avec AddDccPosition
. Sur SERVO2
, on ajoute une pin qui va commander l’alimentation du servo et ne s’activera que très peu de temps avant le premier mouvement (délai réglable avec le second argument optionnel), et se désactivera peu de temps aussi après la fin du mouvement. On évite ainsi les mouvements erratiques du servo au repos. Le fading est aussi fixé ici sur les leds, avec la valeur de chaque pas de la progression vers l’intensité voulue, entre 0 et celle donnée plus haut, et en millisecondes le temps à rester sur cette intensité avant de passer au pas suivant.
GroupState *pServo1 = new GroupState(320, 0, false);
pServo1->Setup(2,
new GroupStateItem(accessories[SERVO1], MINIMUM, 500),
new GroupStateItem(accessories[SERVO2], MINIMUM, 500));
GroupState *pServo2 = new GroupState(320, 1, false);
pServo2->Setup(2,
new GroupStateItem(accessories[SERVO1], MAXIMUM, 500),
new GroupStateItem(accessories[SERVO2], MAXIMUM, 500));
groupServos.Setup(2, pServo1, pServo2);
GroupState *pLight1 = new GroupState(319, 0, true);
pLight1->Setup(3,
new GroupStateItem(accessories[LIGHT1], LIGHTON),
new GroupStateItem(accessories[LIGHT2], LIGHTOFF),
new GroupStateItem(accessories[LIGHT3], LIGHTON));
GroupState *pLight2 = new GroupState(319, 1, true);
pLight2->Setup(3,
new GroupStateItem(accessories[LIGHT1], LIGHTOFF),
new GroupStateItem(accessories[LIGHT2], LIGHTON),
new GroupStateItem(accessories[LIGHT3], LIGHTOFF));
groupLights.Setup(2, pLight1, pLight2);
Finissons les déclarations avec les groupes. Un groupe est une liste d’états, des GroupState
, et chacun d’eux est une liste d’accessoires avec leur état/position.
Pour le groupe des servos, on a besoin de deux états. Le premier, pServo1
, sera activé par le code DCC 320/0 et n’autorise pas les mouvement simultanés. Il comprend une position MINIMUM
pour le SERVO1
, et une position MINIMUM
aussi pour le SERVO2
. Le dernier argument est un délai à attendre avant de commencer à bouger l’accessoire suivant, ceci pour éviter un éventuel chevauchement de fonctionnement de deux accessoires consécutifs qui ne serait pas voulu. Le second état, pServo2
, est activé par le code 320/1 et conduit les deux servos à leur position maximum. Enfin on fait le Setup()
du groupe, en donnant le nombre d’états (2), et les états (pServo1
et pServo2
).
Le groupe des leds est lui aussi créé sur le même modèle en utilisant deux états, pLight1
et pLight2
activés par les codes 319/0 et 319/1 et allumant ou éteignant les trois leds...
UAD_EndSetup()
}
Fin du setup ! L’essentiel est fait, et la mémoire dépensée est affichée en mode debug grâce à UAD_EndSetup()
.
void loop()
{
if (dccCommander.Loop())
{
accessories.Loop();
groupServos.Loop();
groupLights.Loop();
buttonsCommander.Loop();
}
}
La fonction loop()
du sketch est répétée, rappelons le, inlassablement par le micro-contrôleur. Elle est très simple. Le plus sensible est la fonction Loop()
du DccCommander
qui doit être appelé aussi souvent que possible pour ne pas perdre de paquets. Lorsque lui a fini son travail, sa fonction Loop()
retourne true
et permet aux autres Loop()
de commencer le leur. Les accessoires et les groupes sont appelés pour le traitement des mouvements déjà commencés, des lumières à faire clignoter, ou des groupes à faire passer à l’accessoire suivant...
Ensuite les Loop()
des autres commanders qui vont activer les accessoires si nécessaire, et c’est tout !
On en a fini avec l’exemple Full. Si vous voulez voir à quoi ça ressemble en fonctionnement, je vous invite à regarder la vidéo :
https://www.youtube.com/watch?v=qOB...
Voilà. Le codage du fichier .ino est aussi direct que possible, mais vous voyez des améliorations ou des simplifications à faire pour rendre son accès encore plus facile, n’hésitez pas à me le dire !
Merci à l’équipe LOCODUINO pour leur aide pour la rédaction de cet article long et compliqué !