Les lecteurs des articles et du forum Locoduino qui s’intéressent au DCC connaissent forcément DCC++, la perle de Gregg E. Berman déjà maintes fois utilisée dans nos projets. Pourtant, à chaque utilisation, les sources de DCC++ sont copiés puis adaptés dans le nouveau projet. Si une évolution intervient dans le code de DCC++, il faut recommencer la copie et l’adaptation dans chaque projet qui l’utilise et qui veut bénéficier des nouveautés...
C’est exactement pour faciliter cela que l’IDE Arduino a créé les bibliothèques !
Bibliothèque DCCpp
Encore un DCC++ ? Mais à quoi ça sert ?
.
Par :
DIFFICULTÉ :★★☆
Pourquoi une bibliothèque ?
Nous sommes devant le cas typique d’un projet dont une bonne partie du code doit pouvoir être ré-utilisée dans d’autres projets. C’est le but d’une bibliothèque, et c’est ce que j’ai décidé de faire après avoir vu tout le monde ici s’en servir, et l’avoir moi-même utilisé dans au moins deux projets différents : Bibliothèque DcDccNanoController et sa grande sœur encore en gestation...
Une bibliothèque comme celle-ci doit répondre à plusieurs contraintes :
- Respecter le travail du véritable auteur et minimiser les modifications apportées.
Lorsque vous êtes l’auteur d’une bibliothèque, vous n’avez personne à qui rendre des comptes à propos du contenu ou de la forme de cette bibliothèque. Ce n’est pas le cas ici, puisque je suis parti du remarquable travail de Gregg. Il a donc fallu reproduire intégralement les copyrights, les documentations du source et conserver autant que possible l’API, c’est à dire les noms des classes et des fonctions exposées aux utilisateurs. - Fournir une boite noire
Une bibliothèque, c’est un truc obscur que les utilisateurs ne voient qu’à travers la liste des fonctions exposées dans le fichier .h principal. C’est ce que l’on appelle une boite noire : on l’installe, et on l’oublie. Pas question de changer quoi que ce soit dedans. Tout le paramétrage doit se faire à partir du croquis de l’utilisateur. C’est la grande différence avec la version originale de DCC++ qui imposait la modification de plusieurs fichiers .h pour régler son fonctionnement. On va malgré tout voir que je n’ai malheureusement pas pu tout régler aussi élégamment que ce que j’aurai voulu... - Fournir une API claire et simple
L’API, c’est un peu la signature d’une bibliothèque. C’est à la fois la liste des fonctions et/ou des classes et de ses fonctions membres. Si l’on veut qu’une bibliothèque soit utilisée, son rôle doit être parfaitement défini, et le nombre de choses à implémenter dans le croquis doit être aussi logique et limité que possible.
Logique par le nommage et le rôle de chaque objet et fonction, et limité à ce qui est vraiment nécessaire et suffisant pour ne pas noyer l’utilisateur dans des choses d’usage rare ou incompréhensibles, ou dans les rouages internes du fonctionnement dont il n’a probablement rien à faire. - Fournir une documentation et des exemples
Expliquer à un utilisateur dans une documentation à quoi sert une bibliothèque est le meilleur moyen de fixer une API, parce que l’on se rend compte des besoins et des attentes. Fournir des exemples est encore plus utile, puisque l’on s’aperçoit en l’écrivant de la cohérence et de la simplicité ou pas de l’interface. C’est souvent le moment d’une remise en cause de certaines choses que l’on croyait simples et évidentes !
Rôle de la bibliothèque
Laissons Gregg parler pour nous expliquer à quoi sert DCC++ (extrait d’un texte traduit par mes soins, le readme de Github, lien tout en haut de l’article...) :
DCC++ BASE STATION est un programme C++ pour l’Arduino Uno et l’Arduino Mega utilisant l’Arduino IDE 1.6.6.
Il permet à un Arduino Uno ou Mega standard équipé d’un Arduino Motor Shield (ou d’un autre) d’être utilisé comme une centrale DCC complète, conforme aux standards DCC du NMRA, pour un réseau de modélisme ferroviaire digital.
Cette version de DCC++ BASE STATION supporte :
- Adresse de décodeur sur deux (adresse courte) et quatre octets (adresse longue).
- Contrôle simultané de plusieurs locomotives.
- Commande à 128 pas de vitesse.
- Fonctions F0 à F28
- Activation d’accessoires utilisant 512 adresses, chacune avec 4 sous adresses (norme NMRA complète).
- Est incluse la possibilité de stocker l’état courant d’aiguillages connectés à cette centrale.
- Programmation d’un décodeur sur la voie principale
- Ecriture de CV par octets
- Ecriture de CV par mise à jour de certains bits individuellement.
- Programmation sur une voie dédiée de programmation
- Ecriture de CV par octets
- Ecriture de CV par mise à jour de certains bits individuellement.
- Lecture des données émises par le décodeur en cours de programmation.
DCC++ BASE STATION est contrôlé par des commandes textes recues par l’interface série de l’Arduino. Elles peuvent être envoyées par la console série de l’IDE Arduino, ou par des interfaces ou des programmes extérieurs.
Sur un Arduino Mega, un shield Ethernet peut être utilisé pour recevoir des ordres via un réseau informatique plutôt que par la liaison série.
Le rôle de la nouvelle bibliothèque sera grosso-modo le même, avec quelques adaptations pour rendre son usage un petit peu plus facile et mieux répondre à des besoins généraux.
Adaptations
Arduino
Grâce à l’intervention de Dominique, le code de DCC++ a été rendu compatible avec un Arduino Nano, qui est quasiment identique à un Uno avec quelques broches de plus (A6 et A7). C’est ce qui m’a permis de l’utiliser pour la Bibliothèque DcDccNanoController. La bibliothèque a aussi été rendue compatible avec les ESP32, Arduino plus puissants et multi-coeurs. Au passage, l’IDE 1.8.13 a été testé et validé pour la bibliothèque.
Commandes texte
Dans la version originale de DCC++, les trains sont uniquement pilotés à partir de commandes texte. J’ai voulu y ajouter un moyen plus direct, des commandes envoyées par des fonctions membres d’une classe dédiée DCCpp. Elle donne accès à toutes les commandes utiles pour piloter un ou plusieurs trains et des accessoires. La partie commande texte devient optionnelle, mais elle reste disponible si besoin.
Shields moteur
Le choix des shields compatibles avec le programme original résulte sans doute de ce que Gregg a pu trouver au moment de l’écriture de son code. Il s’agit de l’Arduino Motor shield V3 et du Pololu MC33926 Motor shield. A noter que pour le premier, j’ai trouvé un compatible en chine pour la moitié du prix, et il marchait très bien... jusqu’à ce qu’il grille ! Sans doute une mauvaise manip de ma part sur un shield probablement plus fragile que l’original. Les shields originaux permettent aussi la mesure de courant pour la remontée d’information des décodeurs. Pour ma propre centrale, mon choix s’est porté sur le fameux LMD18200 dont nous parlons beaucoup ici... Après avoir écarté la mesure de courant imprécise fournie par le LMD lui-même, c’est le petit circuit Max471 qui remplit ce rôle. Cette configuration est bien connue puisque déjà utilisée dans cet article et bien d’autres. Bien sûr, pas question de nous brider à ces seuls matériels, il faut que l’interface soit aussi ouverte que possible, sans perdre son héritage !
Shields Ethernet
Là encore Gregg a utilisé plusieurs shields : Arduino Ethernet Shield v1, Arduino Ethernet Shield v2, et le Seeed Studio Ethernet Shield qui semble avoir disparu. De mon côté, je me suis procuré des shields et des petits modules (chinois toujours...) à base du circuit ENC28J60.
La nouvelle bibliothèque
DCCpp.h
Il s’agit du fichier include principal de la bibliothèque. Comme dans mes autres bibliothèques, c’est le fichier à modifier si l’on veut exclure des parties inutiles pour le croquis. C’est aussi le seul fichier à modifier si besoin. Ceux qui connaissaient la version classique de DCC++ doivent oublier les config.h et autres comm.h ! Les classes Turnout, Output, Sensor et EEStore (sauvegarde de l’état général des autres classes dans l’EEPROM) peuvent être totalement ou partiellement exclues de la compilation, évitant ainsi de polluer l’espace mémoire programme de votre application. Par défaut tout est désactivé, et il suffit de retirer le ’//’ de début de commentaire devant la ligne #define que l’on veut pour activer une partie de la bibliothèque.
////////////////////////////////////////////////////////
// Add a '//' at the beginning of the line to be in release mode.
//#define DCCPP_DEBUG_MODE
///////////////////////////////////////////////////////
// Verbose mode lets you see all actions done by the
// library, but with a real flood of text to console...
// Has no effect if DCCPP_DEBUG_MODE is not activated.
//#define DCCPP_DEBUG_VERBOSE_MODE
///////////////////////////////////////////////////////
// The function DCCpp::printConfiguration()
// is very heavy in program memory. So to avoid problems
// you can make this function available by uncomment the next line.
//#define DCCPP_PRINT_DCCPP
// Inclusion area
//
/**Comment this line to avoid using and compiling Turnout.*/
//#define USE_TURNOUT
/**Comment this line to avoid using and compiling EEPROM saving.*/
//#define USE_EEPROM
/**Comment this line to avoid using and compiling Outputs.*/
//#define USE_OUTPUT
/**Comment this line to avoid using and compiling Sensors.*/
//#define USE_SENSOR
/**Comment this line to avoid using and compiling Serial commands.*/
//#define USE_TEXTCOMMAND
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5100 chip.*/
//#define USE_ETHERNET_WIZNET_5100
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5500 chip.*/
//#define USE_ETHERNET_WIZNET_5500
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5200 chip.*/
//#define USE_ETHERNET_WIZNET_5200
/**Comment this line to avoid using and compiling Ethernet shield using ENC28J60 chip.*/
//#define USE_ETHERNET_ENC28J60
On peut également décider d’utiliser ou non la partie commande par texte, baptisée SerialCommand dans la version originale, mais rebaptisée TextCommand ici, vu que la liaison Ethernet utilise aussi ce canal... Si dans les petites classes précédemment citées, l’IDE était probablement capable tout seul d’éliminer les sources dont le code n’est pas utilisé, le problème est différent avec TextCommand qui comme son nom l’indique contient beaucoup de texte qui va forcément remplir la mémoire SRAM, la mémoire vive de l’Arduino, et ce même s’il n’est pas utilisé. Dans l’esprit de ne pas modifier plus que nécessaire l’oeuvre de Gregg, je ne suis pas repassé partout pour transférer ces textes en mémoire programme avec des F("")...
Comme d’habitude chez moi, des macros DCCPP_DEBUG_MODE
et DCCPP_PRINT_DCCPP
existent aussi, qui permettent respectivement d’activer un mode d’affichage qui va envoyer quantité d’informations sur la console afin de mieux comprendre ce qui se passe, de voir les erreurs éventuelles, et permettre d’imprimer un rapport complet de la configuration utilisée si besoin.
Enfin, comme je l’ai un peu dévoilé plus haut, je n’ai pas pu laisser le créateur de croquis choisir librement son shield Ethernet sans l’obliger à passer par la modification de ce fichier et l’activation ou non de l’une des macros USE_ETHERNET_WIZNET_5100
(Shield v1), USE_ETHERNET_WIZNET_5500
(shield v2), USE_ETHERNET_WIZNET_5200
(Seeed) ou USE_ETHERNET_ENC28J60
.
Fonctionnement général de DCCpp/DCC++ : les registres
Exactement comme la version originale de Gregg, la bibliothèque dispose par défaut de 12 registres pour la voie principale, et 3 pour la voie de programmation. Un registre sert à stocker des ordres DCC à envoyer. Le moteur de DCC++ va scanner tous les registres en séquence et envoyer ces ordres dès qu’il le peut. Le registre 0 est destiné aux ordres transitoires, ceux que l’on envoie qu’une seule fois, comme l’activation d’accessoire ou l’arrêt d’urgence.
Tous les ordres DCC placés dans les autres registres seront répétés à l’infini. Par exemple la commande de vitesse est répétée en permanence, permettant ainsi à une loco qui subit une micro coupure de courant de retrouver immédiatement son ordre de vitesse et sa direction. Si l’ordre n’avait été envoyé qu’une seule fois, la loco se serait arrêtée et attendrait un ordre. Par défaut, le code de Gregg pousse l’activation de fonction dans le registre 0, en faisant ainsi un ordre unique, potentiellement perdu à la première coupure venue. J’ai choisi pour ma part de laisser le choix à l’utilisateur de la bibliothèque.
Si l’on retire le registre 0 qui est réservé pour les ordres uniques, il reste onze registres. Donc en gros 11 locomotives pilotables simultanément. Grâce à sa mémoire vive plus vaste, on pourrait aller plus loin sur un Mega en allouant plus de registres et ainsi piloter plus de locos...
Classe DCCpp
C’est la classe principale. Elle contient quelques fonctions que l’on peut classer en quatre groupes :
- celles du
setup
avecbegin()
,beginMain()
pour la voie principale,beginProg()
pour la voie de programmation, etbeginEthernet()
pour le shield Ethernet. A noter que ces trois dernières fonctions sont toutes facultatives. Par exemple sibeginProg
n’est pas appelée, cela signifiera pour la bibliothèque que la partie voie de programmation sera complètement ignorée, les broches ne seront évidemment pas réservées, et le moteur d’envoi de paquet ne se préoccupera pas de cette partie. Bien sûr, si à la foisbeginMain
etbeginProg
ne sont pas appelées, la partie DCC n’aura plus beaucoup de travail...
LesbeginMain
etbeginProg
réclament essentiellement des numéros de broche à activer ou interroger, mais toutes sont facultatives ! J’ai utilisé ici une astuce de programmation : un numéro de broche est stocké dans un ’byte’ dont la valeur peut varier entre 0 et 255. Aucun Arduino aujourd’hui n’a plus de broches que le Mega, et celui-ci en compte 69 en comptant les broches analogiques. Je peux donc utiliser toutes les valeurs au-dessus de 69 pour dire autre chose à la fonction. Pour ce genre de choses, mieux vaut utiliser des valeurs frappantes, comme 0 ou 255, et comme le 0 est un numéro de broche valide, j’ai décidé de lui dire que cette broche n’était pas utile avec le numéro 255. Une constanteUNDEFINED_PIN
a été définie à 255 pour que le stratagème soit plus évident à comprendre...
Afin de simplifier l’accès aux shields moteurs historiques, j’ai ajouté les fonctionsbeginMainMotorShield()
etbeginProgMotorShield()
pour le shield Arduino Motor Shield, etbeginMainPololu()
etbeginProgPololu()
pour le shield Pololu. Ces fonctions ne réclament aucun arguments et sont strictement équivalentes à un beginMain ou un beginProg avec les bons arguments. - les fonctions générales
loop()
à appeler forcément dans laloop
du croquis,panicStop(true/false)
à appeler en cas de besoin. Il y a aussipowerOn()
etpowerOff()
pour mettre et couper le courant, etsetAccessory()
pour envoyer un ordre DCC d’activation/désactivation d’un accessoire. - les fonctions de pilotage pour la voie principale :
setSpeedMain()
pour le sens et la direction,setFunctionsMain()
pour les fonctions de la loco,readCvMain()
pour la lecture de variables de configuration du decodeur etwriteCvMain()
pour les écrire. Une fonctiongetCurrentMain()
est aussi disponible pour consulter une valeur pondérée du courant consommé. - et les mêmes fonctions de pilotage pour la voie de programmation
setSpeedProg()
,setFunctionsProg()
,readCvProg()
,writeCvProg()
etgetCurrentProg()
. Vous noterez que c’est une amélioration puisque dans le code original, impossible de déplacer une loco ou d’utiliser une de ses fonctions sur la voie de programmation ! D’ailleurs la classe TextCommand ne permet pas de fixer la vitesse, la direction ou l’état d’une fonction d’une loco sur la voie de programmation par une commande texte.
Les fonctions
Pour gérer les fonctions d’une loco, il faut envoyer sur la voie un paquet DCC qui utilise l’adresse du décodeur concerné et deux octets qui correspondent à un champ de bits correspondant à un petit ensemble de fonctions. La norme DCC défini cinq ensembles : F0 à F4, F5 à F8, F9 à F12, F13 à F20 et enfin F21 à F28. Il n’est pas possible d’activer séparément telle ou telle fonction , la norme réclame l’envoi de l’état de toutes les fonctions actives du même ensemble dans ce paquet. Il faut donc tenir une comptabilité de l’état des fonctions actives pour une loco donnée. Et en fonction de ce qui a été activé, envoyer les bons groupes...
Pour simplifier cette gestion, j’ai ajouté une classe FunctionsState
. Il doit y avoir une instance de cette classe pour chaque loco pilotée. Les méthodes setFunctionsMain
et Prog reçoivent une instance de classe FunctionsState
en argument et en déduisent ce qu’il faut envoyer en DCC...
Les classes annexes
Ce que j’appelle les classes annexes sont celles qui ne sont pas directement liées au fonctionnement de la centrale DCC, mais qui peuvent y ajouter des fonctionnalités, comme manœuvrer un accessoire ou surveiller un capteur. D’ailleurs je dis classe, mais en réalité ce sont des structures. Et là on redécouvre une particularité du C++ : les structures sont des classes publiques dont tous les membres sont publics ! A tel point qu’il est possible d’y implanter des fonctions membre, et même des constructeurs ! La suite de l’article parlera de classes, mais ce sont bien des structures...
Tout comme le reste de DCC++, ces classes étaient exclusivement prévues pour une gestion par la ligne de commande. Cette possibilité est évidemment conservée si USE_TEXTCOMMAND
est activé, mais un fonctionnement plus ’objet’ a été ajouté.
Dans le cas d’utilisation de TextCommand
, rien n’est à faire dans le croquis. Sans TextCommand
, il faut déclarer les objets utilisés, puis les initialiser avec un begin()
. Ceci doit impérativement être fait avant DCCpp::begin()
, parce que si la sauvegarde EEPROM est activée (avec USE_EEPROM
) alors c’est pendant ce begin
que les configurations des différents objets annexes seront rechargées depuis l’EEPROM.
Classe Turnout
DCCpp est capable de gérer des accessoires en envoyant la commande DCC qui va bien pour en changer l’état, et en mémorisant l’état courant dans l’EEPROM de l’Arduino. Chaque accessoire doit être une instance de la classe Turnout
. Le fichier source a été renommé pour mieux coller au nom de la classe, l’original s’appelait Accessories.c . Le nom de la classe n’a pas changé, c’était déjà Turnout
.
Classe Sensor
DCCpp est capable aussi de vérifier à intervalle régulier l’état d’une broche de l’Arduino. Lorsqu’une broche change d’état, alors un message est envoyé sur l’interface série ou Ethernet utilisée pour les commandes textuelles. Sans la commande textuelle, une fonction isActive()
permet de tester l’état de cette broche à n’importe quel moment. Chaque broche scannée doit être gérée par une instance de la classe Sensor
.
Classe Output
DCCpp est capable d’activer ou de désactiver des broches. Chaque broche gérée doit être déclarée avec une instance de la classe Output
. La classe permet l’activation avec un état haut ou bas de la broche associée, selon le besoin de ce qui y est raccordé.
Classe EEStore
Si l’EEprom est activée, alors les autres classes annexes sauveront leur état lorsque la fonction store()
sera appelée. Le chargement avec load()
se fait tout seul pendant le setup()
, mais le store()
n’est pas automatique. Votre croquis doit l’appeler de temps à autre pour tenir compte des changements qui ont pu intervenir pendant l’exécution... Une fonction clear()
est disponible pour réinitialiser la mémoire. Enfin le stockage par EEStore commence à l’octet 0 de la mémoire EEPROM, et sur une longueur qui dépend des nombres de Output, Sensor et Turnout. Donc si vous avez d’autres données à sauver derrière, laissez une place suffisante !
Un peu de pratique
Le matériel
Savoir quel matériel brancher, et comment, sont des sujets déjà parfaitement traités dans l’article Réalisation de centrales DCC avec le logiciel libre DCC++ (3) de Christophe que je vous recommande chaudement...
Exemple Série
L’exemple le plus simple d’utilisation de la bibliothèque est celui qui passe par une liaison série pour envoyer des ordres, par exemple en se servant de la console de l’IDE lorsque l’Arduino est connecté à l’ordinateur :
/*************************************************************
project: <Dc/Dcc Controller>
author: <Thierry PARIS>
description: <Dcc Serial Controller sample>
*************************************************************/
#include "DCCpp.h"
#ifndef USE_TEXTCOMMAND
#error To be able to compile this sample,the line #define USE_TEXTCOMMAND must be uncommented in DCCpp.h
#endif
void setup()
{
Serial.begin(115200);
DCCpp::begin();
// Configuration for my LMD18200. See the page 'Configuration lines' in the documentation for other samples.
#if defined(ARDUINO_ARCH_ESP32)
DCCpp::beginMain(UNDEFINED_PIN, 33, 32, 36);
#else
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 11, A0);
#endif
}
void loop()
{
DCCpp::loop();
}
Pas grand-chose dans cet exemple. On commence par l’inclusion de la bibliothèque, puis vient un message de sécurité réclamant l’ouverture de l’utilisation de la classe TextCommand
obligatoire pour cet exemple avec la définition de USE_TEXTCOMMAND
dans DCCpp.h .
Le setup()
lance DCCpp avec le begin
, et configure la voie principale via beginMain
. L’appel à beginMain est différent selon que l’on compile pour un Uno ou un Mega, ou un ESP32. Cette fonction comprend quatre arguments :
- Le premier argument est la broche qui sert de renvoi du signal sur un Arduino Mega équipé d’un shield moteur Arduino. Dans le cas de cet exemple, c’est un LMD18200 qui est utilisé, il n’y a donc pas besoin de cette broche et
UNDEFINED_PIN
est donné en argument. Pour les shields permis par le DCC++ original, il faut utiliserMOTOR_SHIELD_DIRECTION_MOTOR_CHANNEL_PIN_A
ouPOLOLU_DIRECTION_MOTOR_CHANNEL_PIN_A
. - Le deuxième argument est la broche de signal, généralement dépendante du fonctionnement des interruptions de l’Arduino utilisé. Dans les cas de Uno, Nano ou Mega, le DCC++ original fixait déjà la bonne valeur dans une constante
DCC_SIGNAL_PIN_MAIN
que j’ai juste ré-utilisée ici. Inutile de dire que mettreUNDEFINED_PIN
ici est possible, mais supprime tout traitement DCC pour cette voie... - Le troisième argument correspond à la broche ’PWM’ du LMD18200 et sert à activer ou pas la sortie DCC. Avec un PWM à 0, le courant est coupé sur la voie ! Pour les cas de shield Arduino ou Pololu, cette broche est donnée avec la constante
MOTOR_SHIELD_SIGNAL_ENABLE_PIN_MAIN
ouPOLOLU_SIGNAL_ENABLE_PIN_MAIN
. Il est aussi possible de raccorder cette broche ’PWM’ directement au 5V, et laisserUNDEFINED_PIN
en argument. Dans ce cas, DCCpp ne pourra pas couper le courant sur la voie, et il faudra prévoir un autre mécanisme pour les cas d’urgence... - Le quatrième argument correspond à la broche de mesure de courant pour la programmation des CVs. Dans le cas des shields habituels, sont à utiliser
MOTOR_SHIELD_CURRENT_MONITOR_PIN_MAIN
ouPOLOLU_CURRENT_MONITOR_PIN_MAIN
. Il est aussi possible de ne pas utiliser de mesure de courant en mettant UNDEFINED_PIN. Dans ce cas on peut toujours programmer les CVs, mais pas lire leur contenu, ni être sûr que la programmation s’est bien passée...
Pour beginProg
, les arguments ont la même signification, mais le nom des constantes à utiliser se terminent par _PROG au lieu de _MAIN.
Pour mieux comprendre, voici ce qu’il faut mettre dans le setup()
pour les cas décrits dans l’article de Christophe :
Arduino Uno + LMD18200 + MAX471
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, A0);
Arduino Uno + 2 LMD18200 + 2 MAX471
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, A0);
DCCpp::beginProg(UNDEFINED_PIN, DCC_SIGNAL_PIN_PROG, 5, A1);
Arduino Mega2560 + LMD18200 + MAX471
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, A0);
Arduino Mega2560 + 2 LMD18200 + 2 MAX471
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, A0);
DCCpp::beginProg(UNDEFINED_PIN, DCC_SIGNAL_PIN_PROG, 11, A1);
Arduino Uno ou Mega2560 + Arduino Motor Shield
DCCpp::beginMainMotorShield();
DCCpp::beginProgMotorShield();
Ce qui est strictement équivalent à :
DCCpp::beginMain(MOTOR_SHIELD_DIRECTION_MOTOR_CHANNEL_PIN_A, DCC_SIGNAL_PIN_MAIN, MOTOR_SHIELD_SIGNAL_ENABLE_PIN_MAIN, MOTOR_SHIELD_CURRENT_MONITOR_PIN_MAIN);
DCCpp::beginProg(MOTOR_SHIELD_DIRECTION_MOTOR_CHANNEL_PIN_B, DCC_SIGNAL_PIN_PROG, MOTOR_SHIELD_SIGNAL_ENABLE_PIN_PROG, MOTOR_SHIELD_CURRENT_MONITOR_PIN_PROG);
Malgré les différences de câblage entre un Uno et un Mega, ce sont les définitions des constantes qui changent automatiquement au moment de la compilation.
Arduino Uno ou Mega2560 + Pololu Motor Shield
DCCpp::beginMainPololu();
DCCpp::beginProgPololu();
Ce qui là encore est strictement équivalent à :
DCCpp::beginMain(POLOLU_DIRECTION_MOTOR_CHANNEL_PIN_A, DCC_SIGNAL_PIN_MAIN, POLOLU_SIGNAL_ENABLE_PIN_MAIN, POLOLU_CURRENT_MONITOR_PIN_MAIN);
DCCpp::beginProg(POLOLU_DIRECTION_MOTOR_CHANNEL_PIN_B, DCC_SIGNAL_PIN_PROG, POLOLU_SIGNAL_ENABLE_PIN_PROG, POLOLU_CURRENT_MONITOR_PIN_PROG);
On voit que pour les shields, toutes les broches sont en dur dans le code, forcées par des constantes. Dans les cas de LMD18200, les deux dernières broches sont libres et ne sont pas imposées par le matériel. A vous de choisir ce qui convient le mieux. Il suffit de se rappeler que la broche de mesure de courant est forcément analogique !
Attention aux deux premières broches sur des circuits AVR (Uno, Mega, Leonardo, Micro, Mini...) : sur ces micro-contrôleurs seules quelques broches sont capables d’être pilotées par des interruptions. Et c’est ce que fait DCC++/DCCpp. Donc si vous choisissez de changer ces broches pour d’autres, assurez vous qu’elles soient capables de ça ! La consultation du datasheet de l’Arduino, ou plus simplement des articles dédiés aux interruptions sur Locoduino est fortement conseillée..
Pour terminer l’examen du code de l’exemple, la fonction loop
ne fait que lancer le loop
de DCCpp !
Si tout va bien et que vous êtes branchés correctement, alors les ordres saisis en texte dans la console de l’IDE sont transmis à l’Arduino, qui les transmet à DCC++, qui les transforme en signal DCC, qui est lu par un décodeur de loco qui fait ce qu’on lui dit. Ouf !
Exemple Ethernet
/*************************************************************
project: <Dc/Dcc Controller>
author: <Thierry PARIS>
description: <Dcc Ethernet Controller sample>
*************************************************************/
#include "DCCpp.h"
#if !defined(USE_TEXTCOMMAND) || !defined(USE_ETHERNET)
#error To be able to compile this sample,the lines #define USE_TEXTCOMMAND and #define USE_ETHERNET must be uncommented in DCCpp.h
#endif
// the media access control (ethernet hardware) address for the shield:
uint8_t mac[] = { 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF };
//the IP address for the shield:
uint8_t ip[] = { 192, 168, 1, 200 };
EthernetServer DCCPP_INTERFACE(2560); // Create and instance of an EthernetServer
void setup()
{
Serial.begin(115200);
DCCpp::begin();
// Configuration for my LMD18200. See the page 'Configuration lines' in the documentation for other samples.
#if defined(ARDUINO_ARCH_ESP32)
DCCpp::beginMain(UNDEFINED_PIN, 33, 32, 36);
#else
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 3, A0);
#endif
DCCpp::beginEthernet(mac, ip, EthernetProtocol::TCP);
}
void loop()
{
DCCpp::loop();
}
Par rapport à l’exemple par la liaison série la différence réside dans le beginEthernet
qui réclame des arguments classiques pour une liaison réseau : adresse MAC de l’appareil (six octets : notez le texte que représente les valeurs : BEEF BEEF BEEF ! C’est juste pour le fun...), et adresse Ip (quatre octets en IPv4) du serveur. Le port du serveur (ici 2560, comme le Méga !) est quant à lui fixé par la déclaration du serveur elle-même au début du source :
// Serveur de l'Arduino, avec un numéro de port.
EthernetServer DCCPP_INTERFACE(2560);
Pour que ce code puisse compiler, il faut quand même modifier DCCpp.h dans le répertoire de la bibliothèque et activer la bonne interface réseau :
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5100 chip.*/
//#define USE_ETHERNET_WIZNET_5100
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5500 chip.*/
//#define USE_ETHERNET_WIZNET_5500
/**Comment this line to avoid using and compiling Ethernet shield using Wiznet 5200 chip.*/
//#define USE_ETHERNET_WIZNET_5200
/**Comment this line to avoid using and compiling Ethernet shield using ENC28J60 chip.*/
#define USE_ETHERNET_ENC28J60
Dans mon cas, c’est la dernière interface, celle pour un shield ou un circuit équipé d’un ENC28J60. N’oubliez pas qu’il faut aussi décommenter la ligne #define USE_TEXTCOMMAND
un peu plus tôt dans ce même fichier, parce que l’interface Ethernet ne fonctionne qu’à travers des commandes texte.
Exemple MiniDcc
Le but de cet exemple est de se passer de liaison série ou Ethernet, et d’utiliser de vrais boutons physiques. Comme d’habitude, c’est Commanders qui s’y colle :
/*************************************************************
project: <Dc/Dcc Controller>
author: <Thierry PARIS>
description: <Minimalist Dcc Controller sample>
*************************************************************/
#include "Commanders.h"
#include "DCCpp.h"
#define EVENT_NONE 0
#define EVENT_MORE 1
#define EVENT_LESS 2
#define EVENT_SELECT 3
#define EVENT_CANCEL 4
#define EVENT_MOVE 5
#define EVENT_START 6
#define EVENT_END 7
#define EVENT_EMERGENCY 8
#define EVENT_FUNCTION0 9
#define EVENT_FUNCTION1 10
#define EVENT_ENCODER 11
ButtonsCommanderPush buttonSelect;
ButtonsCommanderEncoder buttonEncoder;
ButtonsCommanderPush buttonCancel;
ButtonsCommanderPush buttonEmergency;
ButtonsCommanderSwitchOnePin buttonF0;
ButtonsCommanderSwitchOnePin buttonF1;
// in this sample, only one loco is driven...
int locoId; // DCC id for this loco
int locoStepsNumber; // 14, 28 or 128
int locoSpeed; // Current speed
bool locoDirectionForward; // current direction.
FunctionsState locoFunctions; // Current functions
void setup()
{
Serial.begin(115200);
buttonSelect.begin(EVENT_SELECT, A0);
buttonEncoder.begin(EVENT_ENCODER, 14, 8, 2);
buttonCancel.begin(EVENT_CANCEL, A3);
buttonEmergency.begin(EVENT_EMERGENCY, A4);
#if defined(ARDUINO_ARCH_ESP32)
buttonF0.begin(EVENT_FUNCTION0, A5);
buttonF1.begin(EVENT_FUNCTION1, A6);
#else
buttonF0.begin(EVENT_FUNCTION0, A1);
buttonF1.begin(EVENT_FUNCTION1, A2);
#endif
DCCpp::begin();
// Configuration for my LMD18200. See the page 'Configuration lines' in the documentation for other samples.
#if defined(ARDUINO_ARCH_ESP32)
DCCpp::beginMain(UNDEFINED_PIN, 33, 32, 36);
#else
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 11, A5);
#endif
locoId = 3;
locoStepsNumber = 128;
locoSpeed = 0;
locoDirectionForward = true;
//locoFunctions.Clear(); // Already done by the constructor...
}
void loop()
{
DCCpp::loop();
unsigned long event = Commanders::loop();
switch (event)
{
case EVENT_ENCODER:
{
int data = Commanders::GetLastEventData();
if (data > 0)
{
if (locoStepsNumber >= 100)
locoSpeed += 10;
else
locoSpeed++;
if (locoSpeed > locoStepsNumber)
locoSpeed = locoStepsNumber;
DCCpp::setSpeedMain(1, locoId, locoStepsNumber, locoSpeed, locoDirectionForward);
}
if (data < 0)
{
if (locoStepsNumber >= 100)
locoSpeed -= 10;
else
locoSpeed--;
if (locoSpeed < 0)
locoSpeed = 0;
DCCpp::setSpeedMain(1, locoId, locoStepsNumber, locoSpeed, locoDirectionForward);
}
break;
}
case EVENT_FUNCTION0:
if (locoFunctions.isActivated(0))
locoFunctions.inactivate(0);
else
locoFunctions.activate(0);
DCCpp::setFunctionsMain(2, locoId, locoFunctions);
break;
case EVENT_FUNCTION1:
if (locoFunctions.isActivated(1))
locoFunctions.inactivate(1);
else
locoFunctions.activate(1);
DCCpp::setFunctionsMain(2, locoId, locoFunctions);
break;
}
}
La déclaration, puis la configuration des différents boutons correspond à ce qui est décrit dans l’article sur Commanders, je ne reviens pas dessus. La partie intéressante pour nous est à deux endroits : DCCpp.h qui a certainement vu commenté sa ligne #define USE_TEXTCOMMAND
puisque l’interface texte ne sera pas utilisée ici, et le grand switch de loop
dans lequel chaque changement important provoqué par les boutons (vitesse, fonctions...) donne lieu à un appel direct d’une fonction de la classe DCCpp.
Comme expliqué plus haut, activer ou désactiver une fonction nécessite de renvoyer en DCC l’état de toutes les fonctions du même ensemble ! Le code associé à EVENT_FUNCTION0
et EVENT_FUNCTION1
(lignes 103 à 117) montre comment procéder dans DCCpp. Lorsqu’une fonction doit changer d’état, on informe la classe locoFunctions
correspondant à la loco concernée de son activation (activate
) ou pas (inactivate
), puis on demande à cette classe d’envoyer l’ordre DCC correspondant avec setFunctionsMain()
. En argument, le numéro de registre pour les fonctions de cette loco, son adresse DCC, et la classe que l’on va interroger pour savoir à l’instant ’t’ quels sont les fonctions activées et en déduire quel(s) ensemble(s) et quels octets sont à envoyer sur les rails.
Exemple étendu aux classes annexes
/*************************************************************
project: <Dc/Dcc Controller>
author: <Thierry PARIS>
description: <Dcc++ Controller sample with all options>
*************************************************************/
#include "Commanders.h"
#include "DCCpp.h"
#define EVENT_NONE 0
#define EVENT_MORE 1
#define EVENT_LESS 2
#define EVENT_SELECT 3
#define EVENT_CANCEL 4
#define EVENT_MOVE 5
#define EVENT_START 6
#define EVENT_END 7
#define EVENT_EMERGENCY 8
#define EVENT_FUNCTION0 9
#define EVENT_FUNCTION1 10
#define EVENT_ENCODER 11
#define EVENT_TURNOUT1 20
#define EVENT_TURNOUT2 21
ButtonsCommanderPush buttonSelect;
ButtonsCommanderEncoder buttonEncoder;
ButtonsCommanderPush buttonCancel;
ButtonsCommanderPush buttonEmergency;
ButtonsCommanderSwitchOnePin buttonF0;
ButtonsCommanderSwitchOnePin buttonF1;
ButtonsCommanderSwitchOnePin buttonTurnout1;
ButtonsCommanderSwitchOnePin buttonTurnout2;
// in this sample, only one loco is driven...
int locoId; // DCC id for this loco
int locoStepsNumber; // 14, 28 or 128
int locoSpeed; // Current speed
bool locoDirectionForward; // current direction.
FunctionsState locoFunctions; // Current functions
Turnout turn1, turn2;
Output output1, output2;
Sensor sensor1, sensor2;
void setup()
{
Serial.begin(115200);
buttonSelect.begin(EVENT_SELECT, A0);
buttonEncoder.begin(EVENT_ENCODER, 14, 8, 2);
buttonCancel.begin(EVENT_CANCEL, A3);
buttonEmergency.begin(EVENT_EMERGENCY, A4);
#if defined(ARDUINO_ARCH_ESP32)
buttonF0.begin(EVENT_FUNCTION0, A5);
buttonF1.begin(EVENT_FUNCTION1, A6);
#else
buttonF0.begin(EVENT_FUNCTION0, A1);
buttonF1.begin(EVENT_FUNCTION1, A2);
#endif
buttonTurnout1.begin(EVENT_TURNOUT1, 30);
buttonTurnout2.begin(EVENT_TURNOUT2, 31);
DCCpp::begin();
// Configuration for my LMD18200. See the page 'Configuration lines' in the documentation for other samples.
#if defined(ARDUINO_ARCH_ESP32)
DCCpp::beginMain(UNDEFINED_PIN, 33, 32, 36);
DCCpp::beginProg(UNDEFINED_PIN, 21, 22, 23);
#else
DCCpp::beginMain(UNDEFINED_PIN, DCC_SIGNAL_PIN_MAIN, 11, A0);
DCCpp::beginProg(UNDEFINED_PIN, DCC_SIGNAL_PIN_PROG, 3, A1);
#endif
locoId = 3;
locoStepsNumber = 128;
locoSpeed = 0;
locoDirectionForward = true;
//locoFunctions.Clear(); // Already done by the constructor...
turn1.begin(1, 100, 1);
turn2.begin(2, 200, 2);
output1.begin(1, 5, B110);
output2.begin(2, 6, B110);
sensor1.begin(1, 7, 0);
sensor2.begin(2, 9, 1);
}
void loop()
{
DCCpp::loop();
// activate first output from first sensor state.
bool active = sensor1.isActive();
if (active != output1.isActivated())
output1.activate(active);
// activate second output from second sensor state.
active = sensor2.isActive();
if (active != output2.isActivated())
output2.activate(active);
unsigned long event = Commanders::loop();
switch (event)
{
case EVENT_ENCODER:
{
int data = Commanders::GetLastEventData();
if (data > 0)
{
if (locoStepsNumber >= 100)
locoSpeed += 10;
else
locoSpeed++;
if (locoSpeed > locoStepsNumber)
locoSpeed = locoStepsNumber;
DCCpp::setSpeedMain(1, locoId, locoStepsNumber, locoSpeed, locoDirectionForward);
}
if (data < 0)
{
if (locoStepsNumber >= 100)
locoSpeed -= 10;
else
locoSpeed--;
if (locoSpeed < 0)
locoSpeed = 0;
DCCpp::setSpeedMain(1, locoId, locoStepsNumber, locoSpeed, locoDirectionForward);
}
}
case EVENT_FUNCTION0:
if (locoFunctions.isActivated(0))
locoFunctions.inactivate(0);
else
locoFunctions.activate(0);
DCCpp::setFunctionsMain(2, locoId, locoFunctions);
break;
case EVENT_FUNCTION1:
if (locoFunctions.isActivated(1))
locoFunctions.inactivate(1);
else
locoFunctions.activate(1);
DCCpp::setFunctionsMain(2, locoId, locoFunctions);
break;
case EVENT_TURNOUT1:
if (turn1.isActivated())
turn1.inactivate();
else
turn1.activate();
break;
case EVENT_TURNOUT2:
if (turn2.isActivated())
turn2.inactivate();
else
turn2.activate();
break;
}
}
Parce qu’il en fallait un, un exemple d’utilisation de ces fameuses classes annexes. Pour lui, il faut activer les parties concernées dans DCCpp.h : USE_TURNOUT
, USE_SENSOR
et USE_OUTPUT
. L’exemple est exactement identique au précédent. La seule chose qui change, ce sont les Turnout
, Sensor
et Output
déclarés (lignes 43 à 45). En mode ligne de commande, ces objets sont créés dynamiquement en mémoire. Sans la ligne de commande, il faut les déclarer comme n’importe quel objet... Une fois déclarés, les begin
(lignes 81 à 88) vont fixer les broches utilisées, et les identifiants si nécessaire. Enfin dans le loop
, les fonctions de gestion (lignes 95 à 105 et 153 à 165) seront utilisées pour changer l’état des sorties (activate
, inactivate
...) ou tester les entrées (isActivated
, isActive
).
Exemple de test
Un exemple Autotest est aussi présent. Il permet de vérifier que la centrale marche bien, en tout cas en mode texte. Il faudra peut être changer dans le code des commandes l’adresse DCC fixée à 3 par défaut. Ensuite placez une loco configurée à cette adresse sur la voie et lancez l’Arduino. La loco décrira une petite séquence avant/arrière/clignotement des feux qui peut être modifiée à loisir.
Où ça se passe...
La bibliothèque est disponible sur la forge Locoduino.
Notez que dans le répertoire ’extras/Doc’ se trouve une documentation html visible sur n’importe quel système avec n’importe quel navigateur un peu moderne. Cette documentation reprend en Anglais l’ensemble des informations données ici et détaille plus précisément les rôles des classes, des fonctions et de leurs arguments. Sous Windows pour la lancer, il suffit de double cliquer sur le fichier StartDoc.bat présent dans le répertoire de la bibliothèque, sinon il faut manuellement double-cliquer sur le fichier ’extras/Doc/index.html’ . La page ’Revision History’ donne la liste des versions et les changements apportés en français et en anglais !
La bibliothèque DCCpp est destinée à remplacer tous les usages des sources de DCC++ dans vos projets. Elle est bien plus configurable que son originale, et possède un peu plus de fonctionnalités. De plus, bien configurée elle devrait dans certains usages prendre beaucoup moins de mémoire. Vous gagnerez au change !
Article mis à jour le 10/11/2020