LOCODUINO

Forum de discussion
Dépôt GIT Locoduino

vendredi 24 février 2017

8 visiteurs en ce moment

Réalisation de centrales DCC avec le logiciel libre DCC++ (1)

Comment adapter ce très bon logiciel à ses besoins propres

. Par : Dominique

Nous avons découvert ce logiciel libre (merci Denis) en Mars 2016 par la visualisation de quelques vidéos très bien faites (si on comprend l’anglais) sur le DCC   en général, et le projet en particulier.
En y regardant de plus près, la qualité de son écriture nous a amené à l’étudier en profondeur et à l’utiliser dans divers environnements matériels et applicatifs.

Les résultats sont excellents et les discussions et réalisations présentées sur le Forum Locoduino ici et par Tanguy, PAT95, msport, et bobyAndCo, méritent de rassembler l’essentiel du savoir-faire dans quelques articles, pour en faciliter l’utilisation et l’adaptation à vos propres projets.

Ce premier article, en français, est une présentation de ce qu’il offre comme possibilités au niveau de la partie "BaseStation" avec quelques exemples d’adaptations.

Dans les articles suivant nous aborderons le coté "Contrôleur" et nous présenterons des réalisations parmi lesquelles vous trouverez probablement tout ce qu’il faut pour vous lancer !

Présentation de DCC  ++

Le projet DCC  ++ a pour auteur un américain : Gregg E. Berman, qui y a travaillé depuis 2013 et jusqu’en 2016, avec la livraison, au printemps de cette année, de sa dernière version sur GitHub.

C’est un très bon programmeur C++ à qui je rend hommage pour cet excellent projet (s’il lit ce fil, ce qui ne manquera pas d’arriver).

Ce projet comprend deux parties :
- une centrale DCC dite "BaseStation" qui se charge de la génération du courant de traction selon la norme NMRA DCC. Cette centrale est construite sur la base d’un Arduino UNO ou d’un MEGA, augmenté d’une carte "Motor Shield  " pour la partie puissance (on verra plus loin comment élargir la base matérielle)
- une application "Processing" dite "Controller" qui se charge du pilotage de la "BaseStation" soit via l’interface USB entre le PC et l’Arduino, soit via l’interface TCP/IP en ethernet ou wifi mais sur Mega seulement (on verra plus loin comment élargir les possibilités de "contrôle" notamment en HTML.

Présentée comme cela, la BaseStation semble ne pas pouvoir se passer du Controller, comme une centrale SPROG a besoin d’un logiciel comme JMRI ou RocRail.
En réalité, d’une part, c’est vrai et il est possible de piloter la BaseStation par JMRI à partir de la version 4.1.16 ou RocRail (récent avec la bibliothèque dccpp) qui contiennent donc une interface spécifique pour DCC++.
Mais, d’autre part, ce n’est pas vrai, la centrale peut fonctionner de façon autonome à condition d’en modifier la programmation, ce qui fera l’objet d’exemples de réalisation décrits plus loin et dans les articles suivants.

Car Gregg a réalisé un logiciel Open Source admirable en C++ avec force commentaires et explications, et un découpage des fonctions en plusieurs onglets qui rendent la compréhension du logiciel beaucoup plus facile.

Voici ce qu’on obtient en quelques minutes en superposant un Arduino Mega, une carte Ethernet et une carte pour 2 moteurs (j’ai ajouté un radiateur sur le double pont en H L298 pour obtenir près de 2 A).

Avec cet ensemble qui revient à 2 ou 3 dizaines d’Euros, on n’est pas très loin d’une Z21 ! Mais il faut maitriser les logiciels (il n’y a pas encore d’application pour tablette, mais ça finira par arriver :), donc avoir une certaine expérience. Je vais tenter de vous aider dans cette voie.

Pour ceux qui voudraient se documenter sur le Web, en langue américaine, voici les liens utiles qui vous diront tout :

  1. Le site de référence de DCC++
  2. Le Wiki sur GitHub
  3. Le téléchargement sur GitHub
  4. La page des commandes possibles de DCC++ via l’interface série ou ethernet
  5. Le Forum américain Trainboard avec ses 76 pages de discussions :
  6. Une page d’introduction sur Trainboard
  7. La page de support sur le site JMRI
  8. La page de support sur le site RocRail

Pour vous faciliter la tâche, le logiciel DCC++ a été recopié dans le GIT Locoduino.

Il faut reconnaitre que Gregg à voulu particulièrement respecter la norme NMRA pour que sa centrale fournisse les meilleures caractéristiques :
- adressage des locos sur 2 ou 4 octets (de 1 à 10293)
- vitesses sur 128 crans
- conduite simultanée de plusieurs locos (un dizaine)
- commande des fonctions des locos (F0 à F28)
- gestion des accessoires d’adresse 0 à 2048
- programmation sur la voie principale, avec l’adresse DCC mais sans confirmation de la loco (écriture de CVs et de bits dans les CVs)
- programmation sur la voie de programmation sans adresse DCC mais avec confirmation de la (seule) loco (lecture et écriture de CVs et de bits dans les CVs).

La centrale peut donc se connecter à la voie principale ET à la voie de programmation. C’est une configuration qui ressemble assez bien à la SPROG (sans vouloir les comparer).

J’ai rapidement découvert qu’on pouvait ajouter d’autres fonctions facilement comme expliqué dans ce fil du Forum : DCC++ est plein de promesses.

Dans la suite de cet exposé, je vais détailler d’abord le logiciel de la BaseStation en montrant comment on pourrait tirer partie des différents mécanismes qui le composent.

Il s’agit du sketch DCpp_Uno.

Ce sketch est accompagné de 17 onglets, certains toujours nécessaires, d’autres optionnels (ce qui ne veut pas dire que si on les enlève, ça va encore compiler, mais l’adaptation est facile à faire) :

D’abord les onglets nécessaires :

  • DCCpp_Uno.h contient les choix de configuration matériels principaux : le type d’Arduino (Uno ou Mega, -> mais en changeant quelques mots-clé, on peut faire tourner le logiciel aussi sur un Nano ou un Pro Mini s’ils utilisent tous un ATMega328 à 16Mhz, comme le démontre le fil http://forum.locoduino.org/index.php?topic=203), le type de motor shield   (Arduino ou Polulu, -> qui peut facilement être détourné vers un LMD18200 par exemple), et le type d’interface de communication (Série/USB ou Ethernet), le numéro de version (1.2.1+) et le mode diagnostic qui affiche les registres (non activé par défaut) qu’on va détailler plus loin.
  • Comm.h qui définit 3 types de cartes Ethernet si le choix "Ethernet" est retenu. On n’est plus très loin de la Z21 !
  • Config.h qui contient les réglages de la configuration choisie parmi celles possibles : le motor shield, le nombre maximum de registres (dont on va parler plus loin car c’est l’élément clé de DCC++), l’interface de communication, l’adresse IP, le port et l’adresse Mac (si Ethernet). Il est doc possible de connecter la BaseStation en Wifi avec une carte Ethernet Wifi si on le souhaite (je ne l’ai pas testé).
  • PacketRegister.h (les déclarations) et PacketRegister.cpp (le code) : c’est lui qui gère les contenus binaires des registres qui sont transmis bit à bit, répétitivement vers le circuit de puissance (motor shield) pour générer le signal DCC.
  • CurrentMonitor.h (les déclarations) et CurrentMonitor.cpp (le code) : c’est lui qui gère la mesure de courant pour éviter les court-circuits, bien-sûr, mais surtout pour récupérer les réponses du décodeur en cours de programmation. C’est une fonction que je n’avais pas encore trouvée toute faite et ça tombe bien !

Ensuite les onglets dont on peut se passer éventuellement :

  • Accessories.h (les déclarations) et Accessories.cpp (le code) qui gère les commandes des accessoires
  • EEStore.h (les déclarations) et EEStore.cpp (le code) qui gère la sauvegarde des paramètres en EEPROM
  • Outputs.h (les déclarations) et Outputs.cpp (le code) qui pilote des sorties (commandes d’aiguille) de l’Arduino
  • Sensor.h (les déclarations) et Sensor.cpp (le code) qui gère des entrées (rétrosignalisation) de l’Arduino

L’architecture générale du logiciel DCC++

Cette collections d’onglets colle parfaitement à l’architecture du logiciel.

La génération du signal DCC

C’est là où réside tout l’intérêt de ce logiciel :

Tout d’abord, il n’est pas inutile de rappeler le principe du DCC : transmettre un courant "alternatif" sur les rails, qui serve à la fois d’alimentation pour les trains et de transmission de commandes pour les décodeurs installés dans les locos.

Ce principe est illustré sur les figures suivantes :

Le bits "0" durent à peu près 2 fois plus longtemps que les bits "1".

Une commande DCC est une suite de "1" et de "0" qui sont, en fait, dans une suite d’octets pour chaque commande. Comme il faut pouvoir transmettre plusieurs commandes les unes après les autres et répéter cela sans arrêt, on place ces commandes dans des tableaux d’octets qui seront lus, bit par bit, par une tâche de fond automatique.

Dans le 1er article de la collection Comment piloter trains et accessoires en DCC avec un Arduino, j’explique comment créer des tableaux d’octets correspondant à des commandes DCC à émettre de façon répétitive par une routine d’interruption (donc en tâche de fond). Le plus compliqué, que je traite de façon probablement trop simplifiée dans cet article, consiste à remplir ces tableaux d’octets correctement et élégamment.

Et bien c’est ce que fait DCC++ de façon admirable !

DCC++ utilise donc des tableaux d’octets dits "Packet" organisés en "Register" qui sont gérés dans une liste "RegisterList". Ceci est décrit dans l’onglet PacketRegister.h. Les définitions de base sont :

  1. struct Packet{
  2. byte buf[10];
  3. byte nBits;
  4. }; // Packet
  5.  
  6. struct Register{
  7. Packet packet[2];
  8. Packet *activePacket;
  9. Packet *updatePacket;
  10. void initPackets();
  11. }; // Register
  12.  
  13. struct RegisterList{
  14. int maxNumRegs;
  15. Register *reg;
  16. Register **regMap;
  17. Register *currentReg;
  18. Register *maxLoadedReg;
  19. Register *nextReg;
  20. Packet *tempPacket;
  21. byte currentBit;
  22. byte nRepeat;
  23. int *speedTable;
  24. int addressDccToDiscover; // ajout Dominique
  25. static byte idlePacket[];
  26. static byte resetPacket[];
  27. static byte bitMask[];
  28. RegisterList(int);
  29. void loadPacket(int, byte *, int, int, int=0) volatile;
  30. void setThrottle(char *) volatile;
  31. void setFunction(char *) volatile;
  32. void setAccessory(char *) volatile;
  33. void writeTextPacket(char *) volatile;
  34. void readCV(char *) volatile;
  35. void readCV_Main(char *s) volatile; // ajout Dominique
  36. void writeCVByte(char *) volatile;
  37. void writeCVBit(char *) volatile;
  38. void writeCVByteMain(char *) volatile;
  39. void writeCVBitMain(char *s) volatile;
  40. void printPacket(int, byte *, int, int) volatile;
  41. };

Au passage, on voit dans RegisterList les méthodes disponibles dans DCC++, ainsi que celle que j’ai osé ajouter (readCV_Main). Mais nous y reviendrons plus loin.

Il peut y avoir 12 registres dans un Uno et probablement jusqu’à 50 dans un Mega. En tout cas c’est réglable dans la configuration (onglet Config.h).
Le registre N°0 est réservé pour les commandes à envoyer une seule fois (paquets Idle et Reset, commandes de programmation, commandes de fonctions, commandes d’accessoires). Les registres suivants N° 1 .. MAX_MAIN_REGISTERS sont réservés chacun à une machine sur le réseau. Il ne doit y avoir qu’un seul registre affecté à une même machine, sinon elle serait soumise à des ordres contradictoires et risque de s’agiter bizarrement.

Il y a un jeu de registres pour la voie principale et un autre, plus petit pour la voie de programmation car elles peuvent être commandées simultanément.

L’ensemble de ces Registres constitue donc un grand tableau de bits 0 et de bits 1 où chaque ligne est une trame DCC conforme à la norme NMRA et où chaque bit se traduit par une alternance de 200 microsecondes pour un bit ZERO ou 116 microsecondes pour un bit UN.

On se reportera à l’article L’Arduino et le système de commande numérique DCC pour se remémorer le principe de fonctionnement du bus DCC.

Pour produire le signal DCC automatiquement, en tâche de fond, il faut une routine sous interruption pilotée par les 2 compteurs OCR1A (pour les 2 alternances du bit) et OCR1B (pour chaque alternance, donc la moitié de la durée du bit) de l’ATMega. Il est évident que cette mécanique dépend du processeur utilisé et DCC++ ne s’applique dans son état actuel qu’au UNO et au MEGA. On peut évidemment le modifier légèrement pour qu’il fonctionne aussi sur NANO et PRO MINI car ils utilisent le même processeur que le UNO, un ATMega328. La modification serait plus importante sur le DUE mais la nécessité d’utiliser un DUE est très faible à mon avis, le MEGA étant largement suffisant.

Les 2 routines d’interruption, l’une pour la voie principale et l’autre pour la voie de programmation sont déclarées dans le programme principal DCCpp_Uno :

  1. ISR(TIMER1_COMPB_vect){ // set interrupt service for OCR1B of TIMER-1 which flips direction bit of Motor Shield Channel A controlling Main Track
  2. DCC_SIGNAL(mainRegs,1)
  3. }
  4.  
  5. #ifdef ARDUINO_AVR_UNO // Configuration for UNO
  6.  
  7. ISR(TIMER0_COMPB_vect){ // set interrupt service for OCR1B of TIMER-0 which flips direction bit of Motor Shield Channel B controlling Prog Track
  8. DCC_SIGNAL(progRegs,0)
  9. }
  10.  
  11. #else // Configuration for MEGA
  12.  
  13. ISR(TIMER3_COMPB_vect){ // set interrupt service for OCR3B of TIMER-3 which flips direction bit of Motor Shield Channel B controlling Prog Track
  14. DCC_SIGNAL(progRegs,3)
  15. }
  16. #endif

Cette écriture simplifiée vient du fait que Gregg utilise une "Macro" qui évite d’écrire 3 fois à peu prês la même chose. J’avoue que je me suis gratté la tête en tombant là dessus, mais Thierry et Jean-Luc m’ont éclairé à temps.

Dans mon adaptation à une petite centrale dédiée à un va et vient, j’ai limité le fonctionnement à la seule voie principale et à un ATMega328 (un Nano, mais ce serait possible sur un Uno ou Pro Mini)

Dans le code qui suit, j’ai "dé-macro-isé" la routine pour la voie principale :

  1. ISR(TIMER1_COMPB_vect){ // set interrupt service for OCR1B of TIMER-1 which flips direction bit of Motor Shield Channel A controlling Main Track
  2. //DCC_SIGNAL(mainRegs,1)
  3. if(mainRegs.currentBit==mainRegs.currentReg->activePacket->nBits){ /* IF no more bits in this DCC Packet */
  4. mainRegs.currentBit=0; /* reset current bit pointer and determine which Register and Packet to process next--- */
  5. if(mainRegs.nRepeat>0 && mainRegs.currentReg==mainRegs.reg){ /* IF current Register is first Register AND should be repeated */
  6. mainRegs.nRepeat--; /* decrement repeat count; result is this same Packet will be repeated */
  7. } else if(mainRegs.nextReg!=NULL){ /* ELSE IF another Register has been updated */
  8. mainRegs.currentReg=mainRegs.nextReg; /* update currentReg to nextReg */
  9. mainRegs.nextReg=NULL; /* reset nextReg to NULL */
  10. mainRegs.tempPacket=mainRegs.currentReg->activePacket; /* flip active and update Packets */
  11. mainRegs.currentReg->activePacket=mainRegs.currentReg->updatePacket;
  12. mainRegs.currentReg->updatePacket=mainRegs.tempPacket;
  13. } else{ /* ELSE simply move to next Register */
  14. if(mainRegs.currentReg==mainRegs.maxLoadedReg) /* BUT IF this is last Register loaded */
  15. mainRegs.currentReg=mainRegs.reg; /* first reset currentReg to base Register, THEN */
  16. mainRegs.currentReg++; /* increment current Register (note this logic causes Register[0] to be skipped when simply cycling through all Registers) */
  17. } /* END-ELSE */
  18. } /* END-IF: currentReg, activePacket, and currentBit should now be properly set to point to next DCC bit */
  19.  
  20. if(mainRegs.currentReg->activePacket->buf[mainRegs.currentBit/8] & mainRegs.bitMask[mainRegs.currentBit%8]){ /* IF bit is a ONE */
  21. OCR1A=DCC_ONE_BIT_TOTAL_DURATION_TIMER1; /* set OCRA for timer N to full cycle duration of DCC ONE bit */
  22. OCR1B=DCC_ONE_BIT_PULSE_DURATION_TIMER1; /* set OCRB for timer N to half cycle duration of DCC ONE but */
  23. } else{ /* ELSE it is a ZERO */
  24. OCR1A=DCC_ZERO_BIT_TOTAL_DURATION_TIMER1; /* set OCRA for timer N to full cycle duration of DCC ZERO bit */
  25. OCR1B=DCC_ZERO_BIT_PULSE_DURATION_TIMER1; /* set OCRB for timer N to half cycle duration of DCC ZERO bit */
  26. } /* END-ELSE */
  27.  
  28. mainRegs.currentBit++; /* point to next bit in current Packet */
  29. }

Rien qu’avec cette routine d’interruption, et après l’initialisation des variables qui doit être faite dans le Setup(), notre BaseStation est capable d’emblée de produire le signal d’alimentation DCC des voies.

Il reste à comprendre maintenant comment les Registres sont programmés (octets et bits), c’est à dire comment BaseStation peut envoyer des commandes DCC sur les rails.

Pour ce faire, il y a une méthode de RegisterList qui est très importante et qu’il ne faut absolument pas modifier : c’est la méthode loadPacket

  1. // LOAD DCC PACKET INTO TEMPORARY REGISTER 0, OR PERMANENT REGISTERS 1 THROUGH DCC_PACKET_QUEUE_MAX (INCLUSIVE)
  2. // CONVERTS 2, 3, 4, OR 5 BYTES INTO A DCC BIT STREAM WITH PREAMBLE, CHECKSUM, AND PROPER BYTE SEPARATORS
  3. // BITSTREAM IS STORED IN UP TO A 10-BYTE ARRAY (USING AT MOST 76 OF 80 BITS)
  4.  
  5. void RegisterList::loadPacket(int nReg, byte *b, int nBytes, int nRepeat, int printFlag) volatile {
  6.  
  7. nReg=nReg%((maxNumRegs+1)); // force nReg to be between 0 and maxNumRegs, inclusive
  8.  
  9. while(nextReg!=NULL); // pause while there is a Register already waiting to be updated -- nextReg will be reset to NULL by interrupt when prior Register updated fully processed
  10.  
  11. if(regMap[nReg]==NULL) // first time this Register Number has been called
  12. regMap[nReg]=maxLoadedReg+1; // set Register Pointer for this Register Number to next available Register
  13.  
  14. Register *r=regMap[nReg]; // set Register to be updated
  15. Packet *p=r->updatePacket; // set Packet in the Register to be updated
  16. byte *buf=p->buf; // set byte buffer in the Packet to be updated
  17.  
  18. b[nBytes]=b[0]; // copy first byte into what will become the checksum byte
  19. for(int i=1;i<nBytes;i++) // XOR remaining bytes into checksum byte
  20. b[nBytes]^=b[i];
  21. nBytes++; // increment number of bytes in packet to include checksum byte
  22.  
  23. buf[0]=0xFF; // first 8 bytes of 22-byte preamble
  24. buf[1]=0xFF; // second 8 bytes of 22-byte preamble
  25. buf[2]=0xFC + bitRead(b[0],7); // last 6 bytes of 22-byte preamble + data start bit + b[0], bit 7
  26. buf[3]=b[0]<<1; // b[0], bits 6-0 + data start bit
  27. buf[4]=b[1]; // b[1], all bits
  28. buf[5]=b[2]>>1; // b[2], bits 7-1
  29. buf[6]=b[2]<<7; // b[2], bit 0
  30.  
  31. if(nBytes==3){
  32. p->nBits=49;
  33. } else{
  34. buf[6]+=b[3]>>2; // b[3], bits 7-2
  35. buf[7]=b[3]<<6; // b[3], bit 1-0
  36. if(nBytes==4){
  37. p->nBits=58;
  38. } else{
  39. buf[7]+=b[4]>>3; // b[4], bits 7-3
  40. buf[8]=b[4]<<5; // b[4], bits 2-0
  41. if(nBytes==5){
  42. p->nBits=67;
  43. } else{
  44. buf[8]+=b[5]>>4; // b[5], bits 7-4
  45. buf[9]=b[5]<<4; // b[5], bits 3-0
  46. p->nBits=76;
  47. } // >5 bytes
  48. } // >4 bytes
  49. } // >3 bytes
  50.  
  51. nextReg=r;
  52. this->nRepeat=nRepeat;
  53. maxLoadedReg=max(maxLoadedReg,nextReg);
  54.  
  55. if(printFlag && SHOW_PACKETS) // for debugging purposes
  56. printPacket(nReg,b,nBytes,nRepeat);
  57.  
  58. } // RegisterList::loadPacket

C’est loadPacket qui va positionner les octets et les bits de chaque commande DCC dans un des registres.

LoadPacket contient aussi un mécanisme très important : une sorte de synchronisation avec la routine d’interruption ISR (pendant laquelle le programme principal est mis en attente) qui empêche la modification d’un registre tant que l’appel précédent de loadPacket n’a pas complètement terminé sont travail et que la routine ISR n’a pas envoyé le paquet DCC sur les rails.

C’est la ligne suivant qui fait cela :

  1. while(nextReg!=NULL); // pause while there is a Register already waiting to be updated -- nextReg will be reset to NULL by interrupt when prior Register updated fully processed

Cette astuce permet de simplifier considérablement l’écriture du programme dans les niveaux plus hauts en faisant abstraction des contraintes précitées.

Par exemple, dans une autre fonction de RegisterList, on trouve :

  1. loadPacket(0,resetPacket,2,3); // NMRA recommends starting with 3 reset packets
  2. loadPacket(0,bRead,3,5); // NMRA recommends 5 verify packets
  3. loadPacket(0,resetPacket,2,1); // forces code to wait until all repeats of bRead are completed (and decoder begins to respond)

On est certain, grâce à ce mécanisme, que les paquets seront bien transmis intégralement sur les rails dans l’ordre indiqué. C’est génial, moi j’aime !

Avant de conclure sur cette partie, voici les autres fonctions présentes dans RegisterList. Leurs arguments sont contenus dans un tableau de caractères :

  1. void setThrottle(char *); // commande de vitesse (0..126) et directions (1=avant, 0=arriere)
  2. void setFunction(char *); // commande de fonction loco F0..F28
  3. void setAccessory(char *); // commande d'accessoire (0..2048)
  4. void writeTextPacket(char *); //envoi d'un paquet libre
  5. void readCV(char *); // lecture d'un CV sur voie de programation
  6. void readCV_Main(char *s); // lecture d'un CV sur voie principale (ajout perso de Dominique)
  7. void writeCVByte(char *); // écriture d'un octet entier de CV sur voie de programation
  8. void writeCVBit(char *); // écriture d'un bit de CV sur voie de programation
  9. void writeCVByteMain(char *); // écriture d'un octet entier de CV sur voie principale (sans réponse du décodeur)
  10. void writeCVBitMain(char *s); // écriture d'un bit de CV sur voie principale (sans réponse du décodeur)

Si vous comparez avec le source original de BaseStation, vous trouverez que la fonction readCV_Main(char *s) n’existe pas. C’est un exemple de modification qui est facile à faire. Dans cet exemple, j’ai simplement adressé la commande aux registres de la voie principale et j’ai pris les mesures de courant sur la voie principale (en lieu et place de la voie de programmation). Il va de soi que cette fonction n’a d’intérêt que si une seule loco est présente sur la voie principale.

J’ai imaginé cette modification dans le but de récupérer l’adresse DCC de la loco posée sur les rails pour avoir un automatisme total de va et vient.

L’interface avec le Contrôleur par liaison série ou ethernet

Nous avons vu dans l’épisode précédent comment la BaseStation fabrique ses paquets DCC à partir de commandes sous forme textuelle qui sont passées à des fonctions de l’objet RegisterList (qui n’est pas une classe mais une structure, mais bon..).

Les onglets SerialCommand.h et SerialCommand.cpp vont se charger d’assurer l’interface entre le canal de communication (série ou ethernet) et les fonctions de RegisterList.

SerialCommand.h déclare une structure SerialCommand :

  1. struct SerialCommand{
  2. static char commandString[MAX_COMMAND_LENGTH+1];
  3. static volatile RegisterList *mRegs, *pRegs;
  4. static CurrentMonitor *mMonitor;
  5. static void init(volatile RegisterList *, volatile RegisterList *, CurrentMonitor *);
  6. static void parse(char *);
  7. static void process();
  8. }; // SerialCommand

On y trouve pas mal de choses importantes :

  • Un tableau de caractères commandString : c’est la commande
  • Deux RegisterList, l’une mRegs pour la voie principale et l’autre pRegs pour la voie de programmation. C’est ce qui permet d’appeler simplement les fonctions de RegisterList vues précédemment
  • Une structure CurrentMonitor qui permet d’avoir une mesure de courant
  • Une fonction parse qui se chargera du décodage des commandString et qui va donc appeler les fonctions de RegisterList
  • Une fonction process qui va lire l’interface série ou ethernet et va passer la commande reçue à la fonction parse

Le code d’exécution se trouve donc dans l’onglet SerialCommand.cpp :

La fonction process ne fait pas grand chose d’autre que d’enregistrer les caractères compris entre "<" et ">" et de passer le résultat à parse.

La fonction parse analyse le 1er caractère de la commande pour envoyer le reste des caractères (sans cette 1ère lettre) vers la bonne fonction de RegisterList

"t" pour setThrottle (commande DCC)
"f" pour setFunction (commande DCC)
"a" pour setAccessory (commande DCC)
"T" pour une commande d’aiguille directe (commande non DCC)
"Z" pour une commande d’une pin de sortie de l’Arduino (commande non DCC)
"S" pour lire l’état d’un capteur relié à une pin d’entrée de l’Arduino (commande non DCC)
"Q" pour lire l’état de tous les capteurs à la fois (commande non DCC)
"w" pour programmer un CV sur la voie principale en écriture seule sans vérification (commande DCC)
"b" pour programmer un bit particulier d’un CV sur la voie principale en écriture seule sans vérification (commande DCC)
"W" pour programmer un CV sur la voie de programmation avec vérification donc réponse du décodeur (commande DCC)
"B" pour programmer un bit particulier d’un CV sur la voie de programmation avec vérification donc réponse du décodeur (commande DCC)
"R" pour lire un CV sur la voie de programmation avec réponse du décodeur évidemment (commande DCC)
"1" pour appliquer le signal DCC sur les rails (power ON)
"0" pour couper le signal DCC sur les rails (power OFF)
"c" pour lire la valeur du courant sur la voie principale (commande non DCC)
"s" pour envoyer sur la liaison un état du système (commande non DCC)
"E" pour sauvegarder les valeurs des aiguilles et capteurs en EEPROM (commande non DCC)
"e" pour effacer les valeurs enregistrées dans l’EEPROM (commande non DCC)

A cela s’ajoute quelques utilitaires comme :
"M" pour envoyer un paquet DCC de 2 à 5 octets sur la voie principale (commande DCC)
"P" pour envoyer un paquet DCC de 2 à 5 octets sur la voie de programmation (commande DCC)

Si on s’arrête un instant pour regarder la loop() dans DCCp_Uno, on y lit :

  1. ///////////////////////////////////////////////////////////////////////////////
  2. // MAIN ARDUINO LOOP
  3. ///////////////////////////////////////////////////////////////////////////////
  4.  
  5. void loop(){
  6.  
  7. SerialCommand::process(); // check for, and process, and new serial commands
  8.  
  9. if(CurrentMonitor::checkTime()){ // if sufficient time has elapsed since last update, check current draw on Main and Program Tracks
  10. mainMonitor.check();
  11. progMonitor.check();
  12. }
  13.  
  14. Sensor::check(); // check sensors for activate/de-activate
  15.  
  16. } // loop

Ce qui veut dire que le programme BaseStation ne fait rien d’autre que de traiter les commandes ci-dessus venues de la liaison série ou ethernet, ainsi que de surveiller le courant en cas de court-circuit et de tester les capteurs, ce qui se traduit par l’envoi de messages vers la liaison série ou ethernet.

Il ne fait donc rien de lui-même, mais cela nous ouvre la voie vers des réalisations personnelles intéressantes et sophistiquées, avec une telle belle boite à outils.

Mais revenons à la fonction parse :

On constate qu’il y a deux familles de fonctions :
Celles qui sont conformes à la norme DCC NMRA : t, f, a, w, b, W, B, R, M et P
Celles qui sont spécifiques à BaseStation et ne concernent pas la norme DCC : T, Z, S, Q, 1, 0, c, s, E, e

Les fonctions conformes à la norme DCC sont réalisées par RegisterList comme on l’a vu précédemment. Mais RegisterList ne s’occupe que des fonctions propres au DCC puisqu’il se charge de la gestion des paquets DCC envoyés aux rails.

Alors, pour les autres fonctions, il existe des onglets spécifiques qui traitent des fonctions non DCC :

Les onglets Accessories.h et Accessories.cpp s’occupent des aiguilles en commande directe par l’Arduino (et non en DCC via un décodeur d’accessoire, ce que BaseStation supporte aussi via la fonction setAccessory). Elles gèrent les structures :

  1. struct TurnoutData {
  2. byte tStatus;
  3. byte subAddress;
  4. int id;
  5. int address;
  6. };
  7.  
  8. struct Turnout{
  9. static Turnout *firstTurnout;
  10. int num;
  11. struct TurnoutData data;
  12. Turnout *nextTurnout;
  13. void activate(int s);
  14. static void parse(char *c);
  15. static Turnout* get(int);
  16. static void remove(int);
  17. static void load();
  18. static void store();
  19. static Turnout *create(int, int, int, int=0);
  20. static void show(int=0);
  21. }; // Turnout

On y retrouve une fonction parse qui fait le lien avec la fonction parse de SerialCommand, pour permettre au contrôleur de commander les aiguilles de cette façon.

Les onglets Outputs.h et Outputs.cpp s’occupent des pins disponibles de l’Arduino qui peuvent être programmées en sortie. Elles gère les structures :

  1. struct OutputData {
  2. byte oStatus;
  3. int id;
  4. byte pin;
  5. byte iFlag;
  6. };
  7.  
  8. struct Output{
  9. static Output *firstOutput;
  10. int num;
  11. struct OutputData data;
  12. Output *nextOutput;
  13. void activate(int s);
  14. static void parse(char *c);
  15. static Output* get(int);
  16. static void remove(int);
  17. static void load();
  18. static void store();
  19. static Output *create(int, int, int, int=0);
  20. static void show(int=0);
  21. }; // Output

On y retrouve aussi une fonction parse qui fait le lien avec la fonction parse de SerialCommand, pour permettre au contrôleur de commander les pins de sortie de l’Arduino.

Les onglets Sensor.h et Sensor.cpp s’occupent des pins disponibles de l’Arduino qui peuvent être programmées en entrée pour lire des états de capteurs. Elles gèrent les structures :

  1. struct SensorData {
  2. int snum;
  3. byte pin;
  4. byte pullUp;
  5. };
  6.  
  7. struct Sensor{
  8. static Sensor *firstSensor;
  9. SensorData data;
  10. boolean active;
  11. float signal;
  12. Sensor *nextSensor;
  13. static void load();
  14. static void store();
  15. static Sensor *create(int, int, int, int=0);
  16. static Sensor* get(int);
  17. static void remove(int);
  18. static void show();
  19. static void status();
  20. static void parse(char *c);
  21. static void check();
  22. }; // Sensor

On y retrouve encore une fonction parse qui fait le lien avec la fonction parse de SerialCommand, pour permettre au contrôleur de lire les états des capteurs de rétrosignalisation.

De plus, la fonction check est scrutée par la loop de façon à informer le contrôleur au plus vite des changements d’état des capteurs.

Bien entendu je ne vais pas rentrer dans l’analyse des fonctions de ces 3 dernières familles, mais une série de conclusions d’imposent :

1) BaseStation est un logiciel très complet rédigé de façon propre et claire donc fiable (bien que je n’ai pas tout testé, loin s’en faut, mais cela fleure bon).

2) Sa modularité en fait un logiciel adaptable : on peut enlever et ajouter des fonctions, on peut réaliser certaines fonctions différemment, mais il convient de respecter le noyau principal qui est le moteur DCC.

3) BaseStation est principalement destiné à être piloté par un logiciel de contrôle sur PC. L’auteur Gregg a écrit DCC++Controleur en Processing (version récente) : on peut l’apprécier ou pas. D’autres ont réalisé les adaptations dans JMRI et RocRail (dernières versions seulement).
Presonnellement, j’ai réalisé une petite manette minimaliste en Processing, en une heure de débutant :

Le fond est rouge quand le DCC n’est pas envoyé sur les rails, vert dans le cas contraire. Des images représentent l’état des feux, du sens de déplacement, ...
Vous pouvez télécharger cet exemple sur le fil du Forum DCC++ sur Nano avec LMD18200

Et on trouvera bientôt dans Locoduino des réalisations de serveurs Web pour piloter DCC++ via votre réseau local.

4) Mais rien n’empêche d’ajouter d’autres fonctions, notamment un automate de gestion de circulation, une gestion de Bal, une gestion de signaux, etc..
Les modèles de programmation des onglets peuvent être une bonne base pour ces extensions. Un bel automate peut aussi trouver sa place dans la loop ou appelé depuis la loop.

La détection de courant de DCC++

Un des gros avantages de DCC++ par rapport aux autres logiciels connus qui génèrent du DCC est sa mesure de courant qui fonctionne bien sur divers matériels (shield Arduino Motor, ou Polulu ou LMD18200).

La mesure de courant dans la LOOP (protection contre les court-circuits)

La mesure de courant se fait par la lecture d’un port analogique à chaque tour de loop() et l’intérêt de cette mesure est qu’elle intègre une moyenne glissante ou "lissage exponentiel" avec les dernières mesures, c’est une sorte de filtre passe-bas : on n’est pas tous capable d’inventer cela, donc profitons en ;)

La formule utilisée est c_t = coeff x apin + c_t-1 x (1-coeff) où
- c_t est la valeur de courant calculée au tour t
- c_t-1 est la valeur de courant calculée au tour t-1 (précédent)
- apin est la valeur analogique lue sur la pin analogique utilisée pour la mesure
- coeff est le coefficient de pondération qui est ici de 1%

La valeur lue sur le port apin est donc réduite au 1/100 ème de sa valeur et ajoutée à 99/100 du résultat précédent (initialisé à 0 au départ). La valeur calculée converge donc progressivement vers une valeur moyenne du courant, avec un grande stabilité car les mesures affectent peu le résultat.

On trouve le code suivant dans l’onglet CurrentMonitor.cpp

  1. void CurrentMonitor::check(){
  2. #define CURRENT_SAMPLE_SMOOTHING 0.01
  3.  
  4. current=analogRead(pin)*CURRENT_SAMPLE_SMOOTHING+current*(1.0-CURRENT_SAMPLE_SMOOTHING); // compute new exponentially-smoothed current
  5. if(current>CURRENT_SAMPLE_MAX && digitalRead(SIGNAL_ENABLE_PIN_PROG)==HIGH){ // current overload and Prog Signal is on (or could have checked Main Signal, since both are always on or off together)
  6. digitalWrite(SIGNAL_ENABLE_PIN_PROG,LOW); // disable both Motor Shield Channels
  7. digitalWrite(SIGNAL_ENABLE_PIN_MAIN,LOW); // regardless of which caused current overload
  8. INTERFACE.print(msg); // print corresponding error message
  9. }
  10. } // CurrentMonitor::check

Ce code, appelé dans la loop permet de couper l’alimentation en cas de dépassement de seuil (ici 300), c’est à dire en cas de court-circuit.

La mesure de courant pour les commandes de programmation

C’est l’application la plus interessante de cette mesure de courant.

Nous avons tous remarqué que lors de la programmation du décodeur d’une loco (on commence toujours par son adresse DCC), celle-ci se met à bouger :-[
Cela vient du fait que le décodeur va provoquer des impulsions de consommation de courant sur les rails en activant le moteur pendant une fraction de seconde. Il répète plusieurs cycles de "consommation" pour transmettre une information qui est composée de bits 0 et de bits 1.

Pour récupérer cette information, notre logiciel BaseStation va d’abord mesurer une valeur de repos du courant : "base" c’est à dire quand la loco est au repos : ce courant n’est pas nul car le décodeur en consomme toujours un peu.
Puis quand la loco va transmettre ses "bits" le logiciel va mesurer la consommation pendant la durée du bit, rechercher le pic de consommation (la valeur la plus élévée pendant ce laps de temps) puis comparer le résultat avec la valeur de repos. Si le résultat dépasse la valeur de repos d’un seuil ACK_SAMPLE_THRESHOLD alors le bit est un "1", sinon c’est un "0"

Ce mécanisme se répète 8 fois pour récupérer les 8 bits d’un octet de réponse de la loco.

Les constantes utilisées dans cette mesure se trouvent dans l’onglet PacketRegister.h :

  1. // Define constants used for reading CVs from the Programming Track
  2.  
  3. #define ACK_BASE_COUNT 100 // number of analogRead samples to take before each CV verify to establish a baseline current
  4. #define ACK_SAMPLE_COUNT 500 // number of analogRead samples to take when monitoring current after a CV verify (bit or byte) has been sent
  5. #define ACK_SAMPLE_SMOOTHING 0.2 // exponential smoothing to use in processing the analogRead samples after a CV verify (bit or byte) has been sent
  6. #define ACK_SAMPLE_THRESHOLD 30 // the threshold that the exponentially-smoothed analogRead samples (after subtracting the baseline current) must cross to establish ACKNOWLEDGEMENT

On constate que les paramètres de lissage ne sont pas tout à fait les même que pour la mesure dans la loop. Cela vient du fait que les mesures dans la loop se font en fonction du temps d’éxécution de la loop toute entière, alors que dans la réponse lors d’une programmation, ce sont seulement quelques instructions qui s’éxécutent.

Et le code de récupération de chaque bit se trouve dans l’onglet PacketRegister.cpp, à l’intérieur de chaque fonction de programmation avec réponse, comme readCV, writeCVByte, writeCVBit sur la voie de programmation :

  1. c=0;
  2. d=0;
  3. base=0;
  4.  
  5. for(int j=0;j<ACK_BASE_COUNT;j++)
  6. base+=analogRead(CURRENT_MONITOR_PIN_PROG);
  7. base/=ACK_BASE_COUNT;
  8.  
  9. bRead[2]=0xE8+i;
  10.  
  11. loadPacket(0,resetPacket,2,3); // NMRA recommends starting with 3 reset packets
  12. loadPacket(0,bRead,3,5); // NMRA recommends 5 verfy packets
  13. loadPacket(0,resetPacket,2,1); // forces code to wait until all repeats of bRead are completed (and decoder begins to respond)
  14.  
  15. for(int j=0;j<ACK_SAMPLE_COUNT;j++){
  16. c=(analogRead(CURRENT_MONITOR_PIN_PROG)-base)*ACK_SAMPLE_SMOOTHING+c*(1.0-ACK_SAMPLE_SMOOTHING);
  17. if(c>ACK_SAMPLE_THRESHOLD)
  18. d=1;
  19. }

Il va de soi que ce code ne doit pas être modifié car il fonctionne bien : nous avons testé ce code sur plusieurs plateformes à base d’Uno, Nano et Mega.

Avec ce mécanisme, j’ai réalisé quelque chose qui ne semble pas exister jusqu’à présent : une demande automatique du numéro de CV d’un loco qui serait seule sur une voie unique, notamment pour automatiser un va et vient.

Le principe est le suivant : je teste le bit 5 du cv#29. S’il est égal à 1, c’est une adresse longue codée dans les CV 17 et 18, sinon, c’est une adresse courte codée dans le CV 1.

Je vous donne la méthode que j’ai écrite pour cela :

  1. int trouveAdresseDCC() {
  2. int areponse = -1;
  3. SerialCommand::getCV29();
  4. areponse = mainRegs.valeurCV;
  5. if ((areponse != -1) && (bitRead(areponse, 5))) {
  6. // adresse longue : lire CV#17 et CV#18
  7. SerialCommand::getCV18();
  8. areponse = mainRegs.valeurCV;
  9. if (areponse != -1) {
  10. SerialCommand::getCV17();
  11. if (mainRegs.valeurCV != -1) {
  12. areponse = areponse + ((mainRegs.valeurCV - 192)<<8);
  13. }
  14. }
  15. } else {
  16. // adresse courte : lire CV#1
  17. SerialCommand::getCV1();
  18. areponse = mainRegs.valeurCV;
  19. }
  20. return(areponse);
  21. }

Et je vous donne aussi les commandes GetCVxx() que j’ai ajoutées dans l’onglet PacketRegsiter :

  1. void SerialCommand::getCV29() {
  2. //char commandStr[14] = "r 29 129 129 ";
  3. //commandStr[12] = '\0';
  4. //parse(commandStr);
  5. char commandStr[12] = "29 129 129 ";
  6. mRegs->readCV_Main(commandStr);
  7. }
  8.  
  9. void SerialCommand::getCV1() {
  10. //char commandStr[12] = "r 1 101 101";
  11. //commandStr[11] = '\0';
  12. //parse(commandStr);
  13. char commandStr[12] = "1 101 101 ";
  14. mRegs->readCV_Main(commandStr);
  15. }
  16.  
  17. void SerialCommand::getCV17() {
  18. //char commandStr[14] = "r 17 117 117";
  19. //commandStr[12] = '\0';
  20. //parse(commandStr);
  21. char commandStr[12] = "17 117 117 ";
  22. mRegs->readCV_Main(commandStr);
  23. }
  24.  
  25. void SerialCommand::getCV18() {
  26. //char commandStr[14] = "r 18 118 118";
  27. //commandStr[12] = '\0';
  28. //parse(commandStr);
  29. char commandStr[12] = "18 118 118 ";
  30. mRegs->readCV_Main(commandStr);
  31. }

Ceci pour vous montrer à quel point on peut tirer profit de DCC++ à chacun sa manière.

Comme, par exemple, en réalisant une centrale minimal à base de Nano :

dont vous pouvez suivre la réalisation sur le fil du Forum DCC++ sur Nano avec LMD18200.

Mais ce n’est pas tout ...

Cet article vous laisse entrevoir de nombreuses possibilités de pilotage et de gestion de votre réseau, sans l’achat d’une centrale du marché, je l’espère.

Nous avons vu un exemple de modification de "l’interieur". Celui-ci sera mis en oeuvre prochainement dans une nouvelle version de mon va-et vient.

Mais nous verrons aussi bientôt un exemple de pilotage par l’interface Ethernet et les immenses possibilités que cela permet notamment pour gérer un parc de locos.

A bientôt donc...

BaseStation confirme donc son universalité en permettant de commander la voie principale, et aussi la voie de programmation avec un compatibilité NMRA bien étudiée.

Les suites de cet article seront consacrées à plusieurs projets :
- la commande par Ethernet et la réalisation d’un mini serveur Internet pour piloter un parc de machine à partir de son PC ou sa tablette.
- la réalisation d’une nouvelle version de mon va-et-vient automatique qui marche à merveille !

... et j’espère aussi : vos propres réalisations.

A suivre...

19 Messages

Réagissez à « Réalisation de centrales DCC avec le logiciel libre DCC++ (1) »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Projets »

Un chenillard de DEL

Enseigne de magasin

Feux tricolores

Multi-animations lumineuses

L’Arduino et le système de commande numérique DCC

Un décodeur d’accessoire DCC versatile basé sur Arduino

Un moniteur de signaux DCC

Une barrière infrarouge

Un capteur RFID

Un TCO xpressnet

Une animation sonore

L’Arduino au coeur des systèmes de pilotage analogiques ou numériques

Calcul de la vitesse d’un train miniature avec l’Arduino

La génèse d’un réseau 100% Arduino

Une horloge à échelle H0

Simulateur de soudure à arc

Un automatisme de Passage à Niveau

La rétro-signalisation sur Arduino

Décodeur pour aiguillage à solénoïdes sur Arduino

Un décodeur DCC pour les signaux à deux ou trois feux sur Arduino NANO/UNO

Etude d’un passage à niveau universel

Réalisation pratique d’un système de mesure de vitesse à l’échelle N

Une Passerelle entre le bus S88 et le bus CAN pour la rétro signalisation

Un décodeur DCC pour 16 feux tricolores

Réalisation d’un affichage de gare ARRIVEE DEPART

Comment piloter trains et accessoires en DCC avec un Arduino (1)

Comment piloter trains et accessoires en DCC avec un Arduino (2)

Comment piloter trains et accessoires en DCC avec un Arduino (3)

Comment piloter trains et accessoires en DCC avec un Arduino (4)

SGDD : Système de Gestion DD (1)

SGDD : Système de Gestion DD (2)

SGDD : Système de Gestion DD (3)

La PWM : Qu’est-ce que c’est ? (1)

La PWM : Qu’est-ce que c’est ? (2)

La PWM : Qu’est ce que c’est ? (3)

Mise en oeuvre du Bus CAN entre modules Arduino (1)

Mise en oeuvre du Bus CAN entre modules Arduino (2)

Un gestionnaire en C++ pour votre réseau (1)

Un gestionnaire en C++ pour votre réseau (2)

Un gestionnaire en C++ pour votre réseau (3)

Un gestionnaire en C++ pour votre réseau (4)

Contrôleur à télécommande infrarouge pour centrale DCC++

Gestion d’une gare cachée (1)

Gestion d’une gare cachée (2)

Gestion d’une gare cachée (3)

Les derniers articles

Réalisation d’un affichage de gare ARRIVEE DEPART


Gilbert

Contrôleur à télécommande infrarouge pour centrale DCC++


Daniel

La PWM : Qu’est ce que c’est ? (3)


Jean-Luc

Réalisation de centrales DCC avec le logiciel libre DCC++ (3)


bobyAndCo

Un gestionnaire en C++ pour votre réseau (4)


Pierre59

Un décodeur DCC pour 16 feux tricolores


Dominique, JPClaude, Thierry

Réalisation de centrales DCC avec le logiciel libre DCC++ (2)


bobyAndCo

Réalisation de centrales DCC avec le logiciel libre DCC++ (1)


Dominique

Une Passerelle entre le bus S88 et le bus CAN pour la rétro signalisation


JPClaude

Un gestionnaire en C++ pour votre réseau (3)


Pierre59

Les articles les plus lus

Réalisation d’un affichage de gare ARRIVEE DEPART

La PWM : Qu’est-ce que c’est ? (1)

Comment piloter trains et accessoires en DCC avec un Arduino (1)

L’Arduino et le système de commande numérique DCC

Comment piloter trains et accessoires en DCC avec un Arduino (3)

Décodeur pour aiguillage à solénoïdes sur Arduino

Mise en oeuvre du Bus CAN entre modules Arduino (2)

La rétro-signalisation sur Arduino

L’Arduino au coeur des systèmes de pilotage analogiques ou numériques

Calcul de la vitesse d’un train miniature avec l’Arduino