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

Comment générer un signal DCC

. Par : Dominique. URL : https://www.locoduino.org/spip.php?article17

Dans cet article, on va aborder la programmation de l’Arduino pour produire un signal DCC de 2 manières différentes.

La première méthode oblige à considérer le fonctionnement du Timer et des interruptions, ainsi que les routines d’interruption. C’est une application pratique des articles généraux publiés dans ce site, notamment « Les interruptions (1) » et « Les Timers (I) » et suivants. Elle a pour but de permettre de comprendre les tripes de la technologie, pour ceux qui le souhaitent.

La deuxième méthode fait appel à une bibliothèque toute faite qui permet de cacher ces tripes et aussi de proposer plus de fonctions. Ce sera certainement celle que vous choisirez pour votre projet !

Une première méthode pour produire un signal DCC simple avec Arduino

On a dit précédemment, voir « L’Arduino et le système de commande numérique DCC » que l’envoi des commandes DCC impose de faire travailler notre Arduino en tâche de fond, sous interruption. On va donc utiliser un des timers existants.

  • Le Timer0 est utilisée par les fonctions delay(), millis(), entre autres.
  • Le Timer1 est utilisée par la librairie Servo et est le plus précis (16 bits).
  • La Timer2 est utilisée par la fonction Tone().

On va, dans cet exemple, se servir de Timer2 et, par conséquent, on devra se passer de la production des sons avec Tone() car notre exemple va modifier les valeurs de temps pour satisfaire la norme DCC et donc dérégler la fonction Tone() ;

Le jeu va consister à programmer le timer pour qu’il envoie une interruption à chaque fois que le signal DCC doit changer. Pour les 0, ceci correspond à 50µs et pour les 1, à 29µs. Nous n’allons pas entrer dans les détails de cette programmation car elle fera l’objet d’un article séparé. Tout d’abord, le timer est programmé de manière à ce qu’il s’incrémente toutes les 0,5µs, on appelle ceci un tick du timer. Ensuite, le déclenchement d’interruption sur débordement est validé. L’explication de ce qu’est un débordement est dans l’article « Types, constantes et variables. Il est enfin initialisé à une valeur telle qu’il faut 100 ticks (TIMER_LONG dans le programme qui suit), pour un 0 et 58 ticks (TIMER_SHORT) pour un 1 pour déborder.

Utilisation de l’interruption du timer pour compter la demi période du signal DCC.
Le timer est chargé avec une valeur telle que le temps, en nombre de ticks du timer, avant de déborder est égal au temps désiré. Le tick est programmé à 0,5µs. Pour 50µs, cela représente 100 ticks. Le timer déborde lorsqu’il repasse à 0. La valeur à charger pour 100 ticks est donc égale à 256 - 100 = 156. Pour 29µs, cela représente 58 ticks. La valeur à charger pour 58 ticks est donc égale à 256 - 58 = 198.

Cela correspond à l’initialisation suivante :

/* ------------------- Setup Timer2. ------------------- 
 * Configure le Timer2 de 8-Bit pour générer une interruption
 * toutes les 58 ou 100 micro-secondes.
 * La durée doit être chargée dans TCNT2 par la routine
 * d'interruption ISR(TIMER2_OVF_vect).
 */
 
#define TIMER_LONG  156
#define TIMER_SHORT 198
 
void SetupTimer2()
{
  //Timer2 Settings: Timer Prescaler /8, mode 0
  //Timer clock = 16MHz/8 = 2MHz ou 0,5usec
  TCCR2A = 0;
  TCCR2B = 0 << CS22 | 1 << CS21 | 0 << CS20; 

  //Timer2 Overflow Interrupt Enable   
  TIMSK2 = 1 << TOIE2;

  //load the timer for its first cycle
  TCNT2 = TIMER_SHORT; 
}

Cette fonction est appelée une fois seulement dans la fonction setup().
Grâce à elle, la routine d’interruption ISR(TIMER2_OVF_vect) que l’on verra plus loin, sera appelée automatiquement à chaque fois qu’une transition 0 vers 1 ou 1 vers 0 est nécessaire.

Auparavant on devra déclarer quelques constantes et variables globales qui sont utilisées par cette routine d’interruption :

#define DCC_PIN    4 // Port Arduino qui délivre le signal DCC
#define PREAMBLE   0 // état "envoi du préambule"
#define SEPARATOR  1 // état "envoi du bit start de chaque octet"
#define SENDBYTE   2 // état "envoi des bits de chaque octet"
unsigned char last_timer = TIMER_SHORT; // valeur du Timer en cours   
unsigned char flag = 0;                 // soit bit 1 (court) ou 0 (long)
unsigned char every_second_isr = 0;     // demi-impulsion : haut ou bas
unsigned char state = PREAMBLE;
unsigned char preamble_count = 16;
unsigned char outbyte = 0;
unsigned char cbit = 0x80;

ainsi qu’une zone en mémoire qui contient les commandes DCC à envoyer les unes après les autres, en recommençant au début lorsque la dernière est émise.

// -------------------  buffer for command ------------------- 
struct Message {
  unsigned char data[7]; // taille de la plus grande commande possible
  unsigned char len;     // taille réelle de la commande
} ;

#define MAXMSG  5

struct Message msg[MAXMSG] = { 
  {{0xFF, 0, 0xFF, 0, 0, 0, 0}, 3},      // message idle
  {{locoAdr, 0x3F,  0, 0, 0, 0, 0}, 4},  // vitesse (128 pas) et direction (bit 7) 
  {{locoAdr2, 0x3F,  0, 0, 0, 0, 0}, 4}, // vitesse (128 pas) et direction (bit 7) 
  {{locoAdr, 0x80,  0, 0, 0, 0, 0}, 3},  // lumière FL : 0x80 éteinte
  {{locoAdr2, 0x90,  0, 0, 0, 0, 0}, 3}  // lumière FL : 0x90 allumée
}; // chaque message doit être complété avec les valeurs et le XOR
                                
int msgIndex=0;  
int byteIndex=0;

La routine d’interruption

Voici maintenant la routine d’interruption qui effectue l’envoi de chaque bit d’un message DCC et recharge le timer avec la valeur correspondant au délai avant la prochaine transition du signal DCC. Il est nécessaire de fournir quelque explication sur ce rechargement. Entre le moment où l’interruption est déclenchée et le moment ou le timer est rechargé, le temps continue de s’écouler et le timer de s’incrémenter. Par conséquent si le timer était rechargé avec la valeur brute, le prochain délai serait trop important. Il est donc nécessaire d’ajuster cette valeur en lui ajoutant la valeur du timer au moment du rechargement. C’est le rôle de la variable latency qui reçoit valeur du timer et est ajoutée à la valeur brute.

// ------------------- Timer2 overflow interrupt vector handler ------------------- 
ISR(TIMER2_OVF_vect) {
  //Capture la valeur courante de TCTN2. 
  //Permet de corriger le temps de latence de l'interruption.
  //Recharge le Timer.  
  
  // Une interruption sur 2, inversion seulement du signal (seconde moitié du bit)
  if (every_second_isr)  {
     digitalWrite(DCC_PIN,1);
     every_second_isr = 0;    
     
     // mise à jour du Timer
     latency=TCNT2;
     TCNT2=latency+last_timer; 
     
  }  else  {  // != passage au bit ou état suivant
     digitalWrite(DCC_PIN,0);
     every_second_isr = 1; 
     
     switch(state)  {
       case PREAMBLE:
           flag=1; // bit court
           preamble_count--;
           if (preamble_count == 0)  {  // vers état suivant
              state = SEPARATOR;
              // message suivant
              msgIndex++;
              if (msgIndex >= MAXMSG)  {  msgIndex = 0; }  
              byteIndex = 0; //index 0 du premier octet
           }
           break;
        case SEPARATOR:
           flag=0; // bit long
           // état suivant
           state = SENDBYTE;
           // octet suivant
           cbit = 0x80;  // bit à envoyer le coup suivant         
           outbyte = msg[msgIndex].data[byteIndex];
           break;
        case SENDBYTE:
           if (outbyte & cbit)  { 
              flag = 1;  // bit court
           }  else  {
              flag = 0;  // bit long
           }
           cbit = cbit >> 1;
           if (cbit == 0)  {  // dernier bit, vers octet suivant
              byteIndex++;
              if (byteIndex >= msg[msgIndex].len)  {
                 // fin du message
                 state = PREAMBLE;
                 preamble_count = 16;
              }  else  {
                 // envoi du bit 0 séparateur avant octet suivant
                 state = SEPARATOR ;
              }
           }
           break;
     }   
 
     if (flag)  {  // 1 bit court
        latency=TCNT2;
        TCNT2=latency+TIMER_SHORT;
        last_timer=TIMER_SHORT;
     }  else  {   // 0 bit long
        latency=TCNT2;
        TCNT2=latency+TIMER_LONG; 
        last_timer=TIMER_LONG;
     }  
  }
}

Génération de la trame DCC

D’abord, chaque bit comprend une demi-période à l’état bas (0) et une demi-periode à l’état haut (1). A la fin de cette demi-période, on doit passer au bit suivant en parcourant la zone mémoire de message, octet par octet (chargé dans la variable outbyte = msg[msgIndex].data[byteIndex]).

Pour organiser cette promenade, on utilise un automate avec switch (state) et 3 états possibles pour state :

  • PREAMBLE : tant qu’on ne tombe pas sur un 0, on envoie des 1
  • SEPARATOR : on envoie le 0 qui marque le début de l’octet suivant
  • SENDBYTE : on envoie les bits de chaque octet du message

A la fin d’un message on passe au suivant. A la fin du dernier, on recommence au premier.

Les 5 messages dans l’exemple sont :

  • Un message IDLE ;
  • 2 commandes de vitesse ;
  • 2 commandes de lumière.

donc pour piloter 2 locomotives.

Cette méthode a l’avantage d’être assez simple, mais elle a l’inconvénient de nécessiter la connaissance au bit près des commandes DCC.

J’ai vite trouvé cette méthode extrêmement fastidieuse et j’ai cherché une autre méthode.

Je suis finalement tombé sur le site de Railstar [1] qui propose une librairie disponible librement en Open Source.

La librairie CmdrArduino

Il s’agit maintenant d’une librairie, ce qui signifie qu’il n’est plus nécessaire de copier/coller du code dans votre application mais seulement d’appeler des fonctions, Nous verrons cela dans un prochain article.

Lorsque la librairie est installée, il suffit d’aller dans le menu Fichier/Exemples/CmdrArduino et de choisir l’exemple CmdrArduino_minimum :

/********************
 * Centrale DCC minimum avec un potentiomètre de vitesse connecté
 * sur le port analogique 0,
 * un bouton poussoir connecté entre le 0V et l'entrée digitale 4
 * Le signal DCC est délivré sur la Pin 9, et est capable de piloter
 * un booster à base de LMD18200 directement.
 ********************/
 
#include <DCCPacket.h>
#include <DCCPacketQueue.h>
#include <DCCPacketScheduler.h>
 
DCCPacketScheduler dps;
unsigned int analog_value;
char speed_byte, old_speed = 0;
byte count = 0;
byte prev_state = 1;
byte F0 = 0;
 
void setup() {
  Serial.begin(9600);
  dps.setup();  // initialisation de la librairie
 
  // Bouton sur la pin 4
  pinMode(4, INPUT_PULLUP);
}
 
void loop() {
  // Lecture de l'état du bouton pour la commande de lumière F0
  byte button_state = digitalRead(4); //high == relaché; low == appuyé
  if(button_state && (button_state != prev_state))
  {
    // inversion de l'état
    F0 ^= 1;
    Serial.println(F0,BIN);
    dps.setFunctions0to4(3,DCC_SHORT_ADDRESS,F0);
  }
  prev_state = button_state;

  // Potentiomètre de vitesse
  analog_value = analogRead(0);
  speed_byte = (analog_value >> 2)-127 ; 
  // Ramène la gamme 0-1023 à +126-126, l'arrêt étant le point milieu du potentiomètre
  if(speed_byte != old_speed)
  {
    if(speed_byte == 0) // On évite l'arrêt brutal (e_stop) en remplaçant le 0 par 1
    {
      if(old_speed > 0) speed_byte = 1;
      else speed_byte = -1;
    }
    Serial.print("analog = ");
    Serial.println(analog_value, DEC);
    Serial.print("digital = ");
    Serial.println(speed_byte, DEC);
    dps.setSpeed128(3,DCC_SHORT_ADDRESS,speed_byte);
    old_speed = speed_byte;
  }
  // Cet appel est impératif pour permettre à la librairie de faire son travail
  dps.update();
}

Explications :

On réalise le montage suivant avec :

  • Un Arduino Uno
  • Un module LMD18200
  • Un potentiomètre de 10K
  • Un bouton poussoir
  • Une alimentation 12 volts

On supposera que l’Arduino est alimenté via le câble USB relié à l’ordinateur

Ensuite on installe la librairie CmdrArduino, on charge le programme ci-dessus et cela doit marcher du premier coup, si la loco placée sur les rails est bien configurée avec l’adresse DCC 3.

Il faut placer le potentiomètre au milieu avant d’alimenter le circuit.
La loco avance quand on pousse le potentiomètre d’un coté, et recule de l’autre coté.
Un appui sur le bouton allume les phares de la loco. Un autre appui les éteint.

Bien entendu cet exemple est le plus simple qu’il soit possible de réaliser. Il démontre vite que nous devons aller plus loin. Par exemple :

  • Il serait plus pratique d’avoir un inverseur de Direction et utiliser toute l’excursion du potentiomètre pour la vitesse.
  • On voudrait piloter plusieurs locomotives.
  • On voudrait commander d’autres fonctions de la machine et des accessoires.
  • Etc..

Rassurez-vous, on va y arriver !

Tout d’abord nous allons explorer la librairie CmdrArduino, les fonctions qu’elle propose. Ce sera l’objet de l’article suivant.

Puis nous reviendrons sur la construction d’une centrale plus complète pour conclure cette série d’articles.