Un moniteur de signaux DCC

Comment espionner les commandes émises par la centrale DCC

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

Dans l’article « L’Arduino et le système de commande numérique DCC », nous avons vu à quoi ressemble la norme DCC.

Dans l’article « Comment piloter trains et accessoires en DCC avec un Arduino (2) », nous avons étudié la bibliothèque CmdrArduino qui se charge de générer les signaux DCC pour nous en tâche de fond.

Dans cet article nous allons utiliser une autre bibliothèque pour visualiser sur notre ordinateur les trames DCC qui sont envoyées aux rails, pour vérifier le bon fonctionnement de notre projet.

Cette bibliothèque est celle de Minabay qui est aussi utilisée dans l’article Un décodeur d’accessoires universel (1).

Mais cette fois-ci, la bibliothèque va observer le signal DCC et afficher les paquets vus toutes les 2 secondes.

La trace ci-dessous correspond à ce que ma centrale "va-et-vient" envoie en mode automatique. On peut voir l’adresse 4 de la loco (premier octet 00000100) et les 2 commandes :

  • la lumière (fonction groupe 1) : 10010000 (allumée)
  • la vitesse : advanced operation instruction 00111111 + octet de vitesse (bits 0..6) et direction (bit 7)

Le dernier octet est le code de contrôle qui ne nous intéresse pas ici.

J’ai ajouté la signification des paquets à droite de la liste.

Premières constatations :

  • On compte bien entre 130 et 140 paquets par seconde comme calculé dans la Présentation de la norme DCC. Ici on a 272 paquets en 2 secondes.
  • Environ 90% des paquets sont des Idle 11111111 00000000 11111111
  • La bibliothèque répète bien automatiquement les paquets de vitesse et lumière, environ 4 fois par seconde

Réalisation matérielle du moniteur

J’ai utilisé un simple Nano complété par un petit circuit d’interface entre les rails (tension +/- 15 volts, insupportable pour l’Arduino) et la broche 2 programmée en entrée digitale sous interruption.

Ce circuit est celui du site Minabay : j’y ai ajouté un condensateur de 270 pF (une valeur inférieure, jusqu’à 30-50 pF peut convenir également).

Voici la liste des composants nécessaires :

  • un Arduino (j’ai choisi un Nano mais n’importe lequel doit convenir)
  • 2 résistances de 10kΩ
  • 1 résistance de 1kΩ
  • 1 Diode 1N4148
  • 1 condensateur de 270 picoFarad
  • 1 Opto-coupleur 6N137 (il en faut un rapide quand même)
  • 1 plaque d’essai pour assembler les composants

Plutôt que d’utiliser un Uno avec son bornier femelle, j’ai préféré le Nano sans bornier. J’ai inséré des barrettes à souder dans les trous à la place des barrettes à picots habituels : de cette façon les fils à raccorder au Nano sont soudés sur les barrettes ce qui procure un meilleur contact qu’un ensemble de prises mâle et femelle.

De ce fait, les barrettes du Nano ne sont soudées qu’aux 4 coins ce qui permet de récupérer l’Arduino si le projet n’est plus utilisé plus tard.

A l’arrière de la plaque d’essai, le nombre de soudures est minimal et on ne prend pas de risque da court-circuits.

Le circuit terminé ne m’a pris qu’une petite heure. Le connecteur 4 points permet de sortir une liaison I2C vers un afficheur LCD 4 lignes de 20 caractères qui devrait me permettre de me passer du PC/Mac, à condition d’ajouter une petite alimentation 5V. Pour la liaison I2C, ne pas oublier les 2 resistances de rappel au 5V, de 4,7kΩ chacune sur SDA et SDL respectivement.

Le logiciel qui suit ne tient pas compte de cette possibilité.

La mise en boite est un simple sandwich entre 2 planchettes de contreplaqué.

Le logiciel

Après installation de la bibliothèque, on ouvre simplement l’exemple DCC_Monitor qui se trouve dans le dossier DCC_Decoder.

// DCC_Monitor
// Based on DCC_Decoder Library Version 4
// Dump of DCC packets every 2 seconds
// Arduino Pin 2 is the DCC input. It drives interrupt 0.

#include <DCC_Decoder.h>

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
//
// Defines and structures
//
#define kDCC_INTERRUPT    0

typedef struct
{
    int count;
    byte validBytes;
    byte data[6];
} DCCPacket;

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
//
// The dcc decoder object and global data
//
int gPacketCount = 0;
int gIdlePacketCount = 0;
int gLongestPreamble = 0;

DCCPacket gPackets[25];

static unsigned long lastMillis = millis();
    
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
//
// Packet handlers
//

// ALL packets are sent to the RawPacket handler. Returning true indicates
// that packet was handled. DCC library starts watching for 
// next preamble. Returning false and library continue parsing packet
// and finds another handler to call.
boolean RawPacket_Handler(byte byteCount, byte* packetBytes)
{
    // Bump global packet count
    ++gPacketCount;
    
    int thisPreamble = DCC.LastPreambleBitCount();
    if( thisPreamble > gLongestPreamble )
    {
        gLongestPreamble = thisPreamble;
    }
    
    // Walk table and look for a matching packet
    for( int i=0; i<(int)(sizeof(gPackets)/sizeof(gPackets[0])); ++i )
    {
        if( gPackets[i].validBytes )
        {
            // Not an empty slot. Does this slot match this packet?
            // If so, bump count.
            if( gPackets[i].validBytes==byteCount )
            {
                char isPacket = true;
                for( int j=0; j<byteCount; j++)
                {
                    if( gPackets[i].data[j] != packetBytes[j] )
                    {
                        isPacket = false;
                        break;
                    } 
                }
                if( isPacket )
                {
                   gPackets[i].count++;
                   return false;
                }
            }
        }else{
                // Empty slot, just copy over data
            gPackets[i].count++;
            gPackets[i].validBytes = byteCount;
            for( int j=0; j<byteCount; j++)
            {
                gPackets[i].data[j] = packetBytes[j];
            }
            return false;
        }
    }    
    
    return false;
}

// Idle packets are sent here (unless handled in rawpacket handler). 
void IdlePacket_Handler(byte byteCount, byte* packetBytes)
{
    ++gIdlePacketCount;
}

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
//
// Setup
//
void setup() 
{ 
    Serial.begin(9600);
    
    DCC.SetRawPacketHandler(RawPacket_Handler);   
    DCC.SetIdlePacketHandler(IdlePacket_Handler);
            
    DCC.SetupMonitor( kDCC_INTERRUPT );   
}

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
void DumpAndResetTable()
{
    char buffer60Bytes[60];
    
    Serial.print("Total Packet Count: ");
    Serial.println(gPacketCount, DEC);
    
    Serial.print("Idle Packet Count:  ");
    Serial.println(gIdlePacketCount, DEC);
        
    Serial.print("Longest Preamble:  ");
    Serial.println(gLongestPreamble, DEC);
    
    Serial.println("Count    Packet_Data");
    for( int i=0; i<(int)(sizeof(gPackets)/sizeof(gPackets[0])); ++i )
    {
        if( gPackets[i].validBytes > 0 )
        {
            Serial.print(gPackets[i].count, DEC);
            if( gPackets[i].count < 10 )
            {
                Serial.print("        ");
            }else{
                if( gPackets[i].count < 100 )
                {
                    Serial.print("       ");
                }else{
                    Serial.print("      ");
                }
            }
            Serial.println(
            	DCC.MakePacketString(
                    buffer60Bytes,
            	    gPackets[i].validBytes,
            	    &gPackets[i].data[0]
                )
            );
        }
        gPackets[i].validBytes = 0;
        gPackets[i].count = 0;
    }
    Serial.println("============================================");
    
    gPacketCount = 0;
    gIdlePacketCount = 0;
    gLongestPreamble = 0;
}

////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
//
// Main loop
//
void loop()
{
    DCC.loop();
    
    if( millis()-lastMillis > 1000 )
    {
        DumpAndResetTable();
        lastMillis = millis();
    }
}

Une fois le programme compilé et installé dans l’Arduino, on relie l’interface aux rails, puis on lance le moniteur de l’environnement Arduino ou tout autre terminal de son choix.

J’ai testé le programme sur différents Arduino, y compris le Due.

Version LCD I2C

On trouve facilement, pour 10€ environ, des afficheurs 4 lignes de 20 caractères avec une interface I2C.

J’ai donc modifié l’exemple de Minabay ci-dessus pour obtenir un affichage décodé selon les besoins de mon projet (centrale va-et-vient).

Toutes les 2 secondes, l’afficheur présente :

  • Sur la ligne 1 : le nombre de paquets IDLE et le nombre de paquets de données valides ;
  • Sur les lignes suivantes : le nombre de chaque type de paquet de données et leur signification.

Par exemple :
1@4>3 signifie 1 paquet d’adresse DCC 4, direction avant et vitesse 3
4@4>5 signifie 4 paquet d’adresse DCC 4, direction avant et vitesse 5
15@4L0 signifie 15 paquets d’adresse DCC 4, lumière éteinte
4@4>7 signifie 4 paquet d’adresse DCC 4, direction avant et vitesse 7

On voit bien l’accélération de 3 à 11 crans DCC pendant l’interval de 2 secondes.

Le logiciel de cette version est ici :

/*  DCC_Monitor for LCD 4x20

  Based on DCC_Decoder Library Version 4 from http://www.mynabay.com/arduino
  Display DCC packets counts every 2 seconds (IDLE should be around 50%)
  Arduino Pin 2 is the DCC input. It drives interrupt 0.
  Arduino Pin A4 is SDA, Pin A5 is SLC (even on Nano !).
  To connect an LCD 20x4 display via I2C

  Don't forget I2C pull-up resistors (4,7K each) connected to + 5V
  Compilation on Arduino 1.5.x necessitate a minor change in the DCC_Decoder library:
  Replace "prog_char*" type by "char*"
  
  Display 1st line : Idle paquets count, valid data paquets count
  Display next lines : for each data type :
  - paquet count, DCC address, 
    - speed paquet : direction (< or >), speed value (0..127)
    - light function paquet (FL) : L0 (off) or L1 (on)

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation.
  
  Copyright (c) 2014 Dominique Bultez. */

#define VERSION "1.0b3 Oct 14"
#define D_USB // comment this line to cancel display to the computer's terminal

#include <DCC_Decoder.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,20,4);  // set the LCD address to 0x27 for a 20 chars and 4 line display

////////////////////////////////////////////////////////////////////////////////////////
//
// Defines and structures
//
#define kDCC_INTERRUPT            0

typedef struct
{
    int count;
    byte validBytes;
    byte data[6];
} DCCPacket;

////////////////////////////////////////////////////////////////////////////////////////
//
// The dcc decoder object and global data
//
int gPacketCount = 0;
int gIdlePacketCount = 0;
int gLongestPreamble = 0;

DCCPacket gPackets[25];

static unsigned long lastMillis = millis();
    
////////////////////////////////////////////////////////////////////////////////////////
//
// Packet handlers
//

// ALL packets are sent to the RawPacket handler. Returning true indicates
// that packet was handled. DCC library starts watching for 
// next preamble. Returning false and library continue parsing packet
// and finds another handler to call.
boolean RawPacket_Handler(byte byteCount, byte* packetBytes)
{
        // Bump global packet count
    ++gPacketCount;
    
    int thisPreamble = DCC.LastPreambleBitCount();
    if( thisPreamble > gLongestPreamble )
    {
        gLongestPreamble = thisPreamble;
    }
    
        // Walk table and look for a matching packet
    for( int i=0; i<(int)(sizeof(gPackets)/sizeof(gPackets[0])); ++i )
    {
        if( gPackets[i].validBytes )
        {
                // Not an empty slot. Does this slot match this packet? If so, bump count.
            if( gPackets[i].validBytes==byteCount )
            {
                char isPacket = true;
                for( int j=0; j<byteCount; j++)
                {
                    if( gPackets[i].data[j] != packetBytes[j] )
                    {
                        isPacket = false;
                        break;
                    } 
                }
                if( isPacket )
                {
                   gPackets[i].count++;
                   return false;
                }
            }
        }else{
                // Empty slot, just copy over data
            gPackets[i].count++;
            gPackets[i].validBytes = byteCount;
            for( int j=0; j<byteCount; j++)
            {
                gPackets[i].data[j] = packetBytes[j];
            }
            return false;
        }
    }    
    
    return false;
}

// Idle packets are sent here (unless handled in rawpacket handler). 
void IdlePacket_Handler(byte byteCount, byte* packetBytes)
{
    ++gIdlePacketCount;
}

////////////////////////////////////////////////////////////////////////////////////////
//
// Setup
//
void setup() 
{ 
   Serial.begin(115200);
   lcd.init();                      // initialize the lcd 
   lcd.backlight();

   lcd.print("DCC monitor Version ");
   lcd.print(VERSION);
   delay(1000);
   //lcd.clear();
   
   DCC.SetRawPacketHandler(RawPacket_Handler);   
   DCC.SetIdlePacketHandler(IdlePacket_Handler);
            
   DCC.SetupMonitor( kDCC_INTERRUPT );   
}

////////////////////////////////////////////////////////////////////////////////////////
void DumpAndResetTable()
{
    char buffer60Bytes[60];
    
    #ifdef D_USB
    Serial.print("Total Packet Count: ");
    Serial.println(gPacketCount, DEC);
    #endif
    
    lcd.clear();
    //lcd.setCursor(0, 0);    // 1e ligne
    lcd.print("Idle:");
    lcd.print(gIdlePacketCount, DEC);

    #ifdef D_USB
    Serial.print("Idle Packet Count:  ");
    Serial.println(gIdlePacketCount, DEC);
    #endif
    
    lcd.print(" Data:");
    lcd.print(gPacketCount - gIdlePacketCount, DEC);
    lcd.print("  ");    
    
    #ifdef D_USB
    Serial.print("Longest Preamble:  ");
    Serial.println(gLongestPreamble, DEC);    
    Serial.println("Count    Packet_Data");
    #endif
    
    lcd.setCursor(0, 1);    // 2e ligne
    int j=0;
    for( int i=0; i<(int)(sizeof(gPackets)/sizeof(gPackets[0])); ++i )
    {
        if( gPackets[i].validBytes > 0 )
        {
            if (gPackets[i].count != gIdlePacketCount)
            {
              #ifdef D_USB
              Serial.print(gPackets[i].count, DEC);                                
              if( gPackets[i].count < 10 )
              {
                Serial.print("        ");
              }else{
                if( gPackets[i].count < 100 )
                {
                    Serial.print("       ");
                }else{
                    Serial.print("      ");
                }
              }
              Serial.println(
                  DCC.MakePacketString(
                      buffer60Bytes,
                      gPackets[i].validBytes,
                      &gPackets[i].data[0]
                  )
              );
              #endif
              
              if (gPackets[i].data[0] != 255)
              {
                lcd.print(gPackets[i].count, DEC);
                lcd.print("@");
                lcd.print(gPackets[i].data[0], DEC);
                switch (gPackets[i].data[1]) 
                {
                  case 0x3F:  // Advanced Operation Instruction : speed & direction
                  if (gPackets[i].data[2] > 127)
                  {
                    lcd.print(">");
                    lcd.print(gPackets[i].data[2] - 128);
                  }else{
                    lcd.print("<");
                    lcd.print(gPackets[i].data[2]);
                  }
                  break;
                  case 0x90:  // Fonction Group One FL on
                  lcd.print("L1");
                  break;
                  case 0x80:  // Fonction Group One FL off
                  lcd.print("L0");
                  break;
                  default:
                  lcd.print(gPackets[i].data[1], HEX);
                  break;                  
                }
                j++;
                if (j>5) j=0;
                switch (j)
                {
                  case 0:
                  lcd.setCursor(0, 1);    // 2e ligne
                  break;
                  case 1:
                  lcd.setCursor(10, 1);    // 2e ligne
                  break;
                  case 2:
                  lcd.setCursor(0, 2);    // 2e ligne
                  break;
                  case 3:
                  lcd.setCursor(10, 2);    // 2e ligne
                  break;
                  case 4:
                  lcd.setCursor(0, 3);    // 2e ligne
                  break;
                  case 5:
                  lcd.setCursor(10, 3);    // 2e ligne
                  break;
                }
              }
            }
        }
        gPackets[i].validBytes = 0;
        gPackets[i].count = 0;
    }
    #ifdef D_USB
    Serial.println("============================================");
    #endif
    
    gPacketCount = 0;
    gIdlePacketCount = 0;
    gLongestPreamble = 0;
}

////////////////////////////////////////////////////////////////////////////////////////
//
// Main loop
//
void loop()
{
    DCC.loop();
    
    if( millis()-lastMillis > 2000 )
    {
        DumpAndResetTable();
        lastMillis = millis();
    }
}

Le code peut être téléchargé ici :

La bibliothèque peut être téléchargée ici :

Dernièrement j’ai ajouté un circuit d’alimentation à partir des rails : il contient un pont redresseur reliée au connecteur des rails et suivi d’une capacité de 22uF, puis d’un régulateur 7805 et d’une capacité de 100uF. La sortie est reliée directement au 5V de l’Arduino.