Identifier et localiser vos trains avec le RFID/NFC et un bus CAN.

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

Les capteurs RFID sont très intéressants pour identifier différentes locomotives ou des wagons sur un réseau ferroviaire. Nous en avons déjà parlé à plusieurs reprises sur Locoduino notamment dans les articles Un capteur RFID ou Annonces en gare avec la RFID, ainsi que dans le forum RFID 13.56 Mhz & 125 Khz.

Quant au bus CAN, vous savez qu’il a notre préférence à Locoduino pour sa simplicité de mise en œuvre, son faible coût et sa fiabilité.

Ce que je vous propose aujourd’hui, c’est « le mariage des deux » avec la disposition de plusieurs capteurs RFID à des points stratégiques du réseau, l’identification personnalisée et la transmission des informations détectées par ces capteurs au travers d’un bus CAN pour être utilisées par un gestionnaire par exemple.

Pour optimiser la taille des messages CAN et faciliter leur traitement ultérieur, je vous montrerai tout d’abord comment modifier le contenu des tags pour que le message délivré par le capteur RFID soit exactement adapté à notre besoin.

Dans un second temps, nous verrons comment relier plusieurs capteurs RFID sur une même carte Arduino, comment procéder à la lecture du tag modifié et comment envoyer cet identifiant personnalisé sur un bus CAN.

Ce projet entre dans la catégorie "rétrosignalisation" dans l’architecture globale d’un réseau ferroviaire. Il s’agit de présenter ce type de détecteur RFID/NFC, sans contact, de plus en plus populaire dans la vie courante, et le moyen de transmettre les données de détection aux organes décisionnaires du réseau : via le bus CAN. Pour faire l’interface avec ces organes, un Arduino se charge de la relation avec le ou les capteurs RFID et de la communication sur le bus CAN.
Je précise que la détection RFID ne remplace pas les autres types de détection car elle est "ponctuelle", c’est à dire qu’elle génère un événement (il se passe "ceci" à un instant donné, à un endroit donné). Elle complète les autres détections comme la consommation de courant qui informe sur un "état" (un train se trouve sur un canton, pendant toute la traversée de ce canton).
Mais l’avantage de la détection RFID est qu’elle renseigne sur l’identité du train qui provoque l’événement.
Tous les systèmes de suivi des trains ne sont pas 100% infaillibles et la RFID peut apporter un lever de doute très utile. Et il y a bien d’autres applications possibles.
Nous allons décrire successivement :

  • une présentation des étiquettes RC522 utilisées dans ce projet
  • comment programmer une étiquette
  • comment lire une étiquette au passage d’un train
  • comment transmettre les données sur le bus CAN
  • comment utiliser ces données.

Accrochez vos wagons !

Rappel sur les lecteurs et étiquettes

Nous ne nous intéresserons qu’à la fréquence 13,56 Mhz.
Deux types de lecteurs sont favoris depuis quelque temps, l’un en interface I2C :

Figure 1 : Détecteur RFID/NFC à interface I2C
Figure 1 : Détecteur RFID/NFC à interface I2C

et l’autre en interface SPI, accompagné d’une carte et d’un badge qui n’ont pas d’application ici dans ce projet (trop grands pour nos petites locos !).

Figure 2 : Lecteur RFID/NFC à interface SPI, accompagné d'une carte et d'un badge qui n'ont pas d'application dans ce projet.
Figure 2 : Lecteur RFID/NFC à interface SPI, accompagné d’une carte et d’un badge qui n’ont pas d’application dans ce projet.

Nous choisissons plutôt des étiquettes autocollantes NTAG213 de dimensions 12 x 19 mm que l’on peut coller sous une loco (en évitant les parties métalliques ou le moteur) ou sous un wagon ou une voiture. Ces étiquettes conviennent bien pour l’échelle N.

Figure 3 : étiquette autocollante NTAG213
Figure 3 : étiquette autocollante NTAG213

Comme l’indique la fiche produit de ces étiquettes [1] fabriquées par NXP et vendues moins d’1 € [2], elles disposent d’une zone EEPROM de 144 octets, d’un numéro d’identification unique inscrit à l’usine et d’un débit de transfert de 106 kbits/s ce qui permet une identification très rapide d’une étiquette qui passe au dessus d’un lecteur.

Pour utiliser ces lecteurs, une bibliothèque est nécessaire : En cherchant "MFRC522" dans le gestionnaire de bibliothèque de l’IDE on en trouve deux, l’une pour la version SPI et l’autre pour les deux interfaces SPI et I2C. Mais cette dernière a peut-être encore quelques limitations et je n’ai pas pu compiler correctement avec elle. Donc j’ai utilisé une autre bibliothèque pour l’I2C qui n’apparaît pas dans le gestionnaire : les liens sont donnés ci-après et elles sont téléchargeables à la fin de cet article !

Figure 4 : le gestionnaire de bibliothèques en recherche de "MFRC522"
Figure 4 : le gestionnaire de bibliothèques en recherche de "MFRC522"

La version SPI du module a 4 avantages :

  • l’antenne se glisse facilement sous les traverses de la voie (en N ou HO), sans creuser le plan de voie, mais l’électronique est voyante, à coté de l’antenne, hors voie. elle se prête mieux à une disposition en tunnel ou en gare cachée.
  • l’interface SPI permet d’utiliser plusieurs lecteurs pour un même Arduino, chaque lecteur devant disposer d’un port CS (sélection du circuit), les autres ports étant communs (MISO, MOSI, SCK).
  • l’interface SPI est la plus rapide.
  • la bibliothèque utilisée est sur Github [3] et dans le gestionnaire de bibliothèques de l’IDE.

La version I2C du module a aussi ses avantages et ses inconvénients :

  • le circuit est plus petit mais plus épais donc nécessite de creuser le support sous la voie (voir sur le forum).
  • tous les lecteurs ont la même adresse I2C ce qui restreint le nombre de lecteurs à 1 par Arduino (sauf à installer un multiplexeur I2C, comme le TCA9548A).
  • La bibliothèque utilisée est uniquement sur Github [4].
  • La vulnérabilité du bus I2C au parasites (ce bus est fait pour de très courtes distances).

Dans le projet présentement décrit j’utilise des lecteurs SPI par paire sur un Arduino équipé d’une interface CAN. Au total j’ai besoin de 2 lecteurs (un pour chaque sens de circulation) en amont et en aval d’une gare, soit 4 au total.
Mais pour programmer les étiquettes, j’utilise un lecteur I2C (parce que j’en ai quelques uns en stock !).

Mais avant, "c’est quoi ce truc ?"

Le principe des communications de proximité sans contact (NFC = Near Field Communication) est basé sur l’émission d’une onde radio à 13,56 MHz par le lecteur, transmettant donc de l’énergie radioélectrique à la puce contenue dans l’étiquette, via son antenne (quelques spires de cuivre). Lorsque la puce est alimentée et a emmagasinée suffisamment d’énergie , elle peut émettre à son tour des données contenues dans son EEPROM, selon un protocole de communication normalisé (ISO 14443 et Mifare).

Figure 5 : principe de communication radio entre étiquette et lecteur
Figure 5 : principe de communication radio entre étiquette et lecteur

Les étiquettes NTAG213 qui nous intéressent sont fabriquées par NXP (une spinoff de Philips). Elles sont dotées d’une mémoire de 180 octets organisée en 45 pages de 4 octets par page, de la façon suivante :
– 26 octets réservés au fabricant et la configuration, dans lesquels se trouve le numéro d’identification unique UID que nous n’utiliserons pas),
– 34 bits utilisés par la protection de lecture sécurisée (que nous n’utiliserons pas dans ce projet),
– 4 octets qui définissent certains types d’opérations (capability container),
– 144 octets programmables par l’usager (nous) (mémoire en lecture/écriture).

Figure 6 : organisation mémoire du NTAG213
Figure 6 : organisation mémoire du NTAG213

C’est dans cette dernière zone que nous allons intervenir. Elles sont identifiées comme les pages 4 à 39 inclus.
On verra aussi la notion de secteur qui comprend 4 pages, soit 16 octets.
Il y a donc 9 secteurs de mémoire disponible.

Voici comment nous allons utiliser cette mémoire programmable dans la chaîne de détection, de l’étiquette au récepteur (un gestionnaire ou un automate de réseau), en passant par le lecteur RFID, l’Arduino qui le gère et le bus CAN. Il s’agit d’un exemple simple mais concret qui peut être étendu selon vos besoins.

Nous allons enregistrer dans le secteur 1 (dans les pages 4 à 7, soit 16 octets seulement), des caractères ASCII qu’il faudra taper au clavier dans le moniteur de l’IDE pour programmer l’étiquette avant de la coller sous la loco ou un wagon du train :

  • un numéro d’ordre de loco, wagon ou train (0.. 99) sous forme ASCII, donc les caractères "00" à "99",
  • puis un espace séparateur,
  • puis le nom de la loco, du wagon ou du train. Les 7 premiers caractères sont importants : ils doivent décrire la loco (ou le train) le mieux possible car son nom sera tronqué à 7 caractères lors des transmissions CAN, dans un seul message.

Le but est de permettre l’émission d’un message CAN à chaque passage au dessus d’un détecteur, d’une étiquette qui contienne un identifiant spécifique et 8 octets de données représentés comme ceci :

Figure 7 : un message Can de détection de train ou loco.
Figure 7 : un message Can de détection de train ou loco.

Pour obtenir ce message, les 2 octets du numéro d’ordre (page 4 de l’étiquette) seront convertis en un nombre compris entre 0 et 99 sur le premier octet du message CAN et les 7 octets suivants (pages 4, 5, et 6 de l’étiquette) seront copiés dans les 7 octets restant du message CAN. D’où l’importance de caractériser le nom de la loco dans les 7 premiers caractères. Il serait possible évidemment de transmettre plus d’informations via le bus CAN, mais en envoyant d’autres messages CAN complémentaires, complication que j’évite ici pour simplifier.

Note1 : la description de ce projet est destinée à utiliser ces modules RFID avec succès, donc pour une application simple pour commencer. Il est ensuite possible, pour ceux qui veulent aller plus loin, de garnir l’EEPROM des étiquettes avec d’autres données, par exemple pour commander diverses animations, voire des appareils de voie. Un bel exemple est présenté dans le projet d’ annonce en gare décrit ici dans le forum Locoduino

.

Note2 : Comme indiqué plus haut, nous n’utilisons pas le numéro d’identification unique UID propre à chaque étiquette et défini en usine, non modifiable. Utiliser cet UID aurait nécessité de le comparer à toutes les valeurs présentes sur le réseau, donc faire intervenir un tableau de correspondance entre chaque UID et un numéro de loco/wagon/train. Gérer ce tableau dans chaque lecteur Arduino associé aux lecteurs RFID aurait été trop complexe. Reporter cette gestion dans un gestionnaire, également, d’autant qu’il faut mettre à jour ce tableau à chaque création/modification/suppression d’étiquette. Inscrire le numéro de matériel et son nom directement dans la mémoire utilisateur de chaque étiquette court-circuite cette complication et aucun tableau n’est plus nécessaire.

.

Première partie : le programmateur d’étiquettes

Un seul programmateur pour l’ensemble du réseau suffit pour préparer toutes les étiquettes possibles, et il n’a pas besoin d’être installé sous une voie. On peut ainsi programmer une étiquette avant de la coller sous la loco ou un wagon. Il ne nécessite qu’un Arduino Nano ou Uno (qui dispose d’une sortie 3,3V nécessaire au module RFID RC522) :

Figure 8 : un programmateur d'étiquettes RFID
Figure 8 : un programmateur d’étiquettes RFID

Le détecteur ci-dessus est de type RC522 I2C V1.1, nous ne l’utiliserons pas en détection sur le réseau, n’étant pas en inferface SPI.
Le schéma de branchement est très simple :

Figure 9 : Le schéma du programmateur d'étiquettes
Figure 9 : Le schéma du programmateur d’étiquettes

L’alimentation du module RFID doit être prise sur la broche 3,3V et la broche GND. Les broches SDA et SCL sont connectées respectivement sur A4 et A5, avec une indispensable résistance de 4,7kΩ en pull-up au 3,3V. La broche RST est choisie ici sur D6 mais on peut choisir n’importe quelle autre sortie digitale, en adaptant le programme en conséquence. La broche IRQ, enfin, n’est pas connectée.

Comme toujours, une fois que le câblage est réalisé et soigneusement testé, on branche le Nano sur le port USB pour l’alimenter et on règle l’IDE sur Arduino Nano et ATmega328P (Old Bootloader). Puis on charge l’unique exemple proposé dans la bibliothèque : mfrc522_i2c, on vérifie qu’il n’y a pas d’erreur, ni à la compilation, ni au chargement, puis on ouvre la fenêtre du moniteur série et on approche une carte, un badge ou une étiquette :

On doit lire ceci :

MFRC522 Software Version: 0x92 = v2.0
Scan PICC to see UID, type, and data blocks...
Card UID: B9 DD 9A 56
Card UID: B9 DD 9A 56
... etc...

Maintenant on peut entrer dans le détail du logiciel de programmation d’étiquettes.
Le principe que j’ai appliqué ici est le suivant :
Le setup du programme est utilisé pour saisir le numéro et le nom de la loco ou du train à l’aide du moniteur de l’IDE ou un émulateur de terminal réglé sur la vitesse de 115200 b/s. Lorsque cette opération est faite et vérifiée, la loop va attendre la présentation d’une étiquette et va la programmer dès qu’elle est détectée et bien du type Mifare Ultralight, comme les étiquettes NTAG213.

Entête : bibliothèques, globales

Ici on commence par déclarer les bibliothèques utilisées. L’adresse internet de la bibliothèque MFRC522_I2C est indiquée en commentaire.
Pour la création de l’instance de l’objet mfrc522, on a besoin de son adresse I2C et de la broche de sortie du Nano utilisée pour le reset du module (ici la pin 6).
Si l’adresse 0x28 ne fonctionne pas, on utilisera le programme i2c_scanner pour la découvrir (ce programme est donné à la fin de l’article Bibliothèque Wire : I2C.
Ensuite on déclare le bloc de 16 octets qui servira à la saisie des données, dataBlock qui est initialisé avec des "0".
pageAddr sert à indiquer le début d’enregistrement sur l’étiquette (la page 4).

#include <Wire.h>
#include "MFRC522_I2C.h"
//https://github.com/arozcan/MFRC522-I2C-Library

const int RST_PIN = 6;    // Arduino Nano Pin ou autre Pin au choix

// 0x28 est l'addresse i2c . Chercher avec i2c_scanner sinon.

MFRC522 mfrc522(0x28, RST_PIN);   // Creation d'une instance MFRC522 
MFRC522::StatusCode status;       // variable pour le statut des opérations 

  byte dataBlock[]    = {       //  saisie des données à enregistrer sur l'étiquette
        0x00, 0x00, 0x00, 0x00, //  0,0,0,0
        0x00, 0x00, 0x00, 0x00, //  0,0,0,0
        0x00, 0x00, 0x00, 0x00, //  0,0,0,0
        0x00, 0x00, 0x00, 0x00  //  0,0,0,0
    };

  uint8_t pageAddr = 0x04;  //Les données sont ecrites dans 4 pages : 4,5,6 et 7.
                            //Pages 0 to 3 sont réservées donc inutilisées.           

Routines d’affichage

Ce sont 2 petits utilitaires d’affichage de 16 octets, l’un en hexadécimal et l’autre en Ascii pour voir en clair les caractères entrés au clavier ou lus dans l’étiquette.
Ce sont des routines classiques utilisables dans bon nombre de programmes. Dans la version hexadécimale, on remarque l’addition d’un "0" devant chaque nombre compris entre 0 et F, de façon à formater tous les nombres sur 2 chiffres pour une meilleure présentation.

/**
 * Routines d'affichage d'un tableau d'octets en Hexa et en Ascii (char)
 */
void dump_byte_array(byte *buffer, byte bufferSize) {
    for (byte i = 0; i < bufferSize; i++) {
        Serial.print(buffer[i] < 0x10 ? " 0" : " ");
        Serial.print(buffer[i], HEX);
    }
}

void dump_char_array(byte *buffer, byte bufferSize) {
    for (byte i = 0; i < bufferSize; i++) {
        Serial.print("  ");
        Serial.print((char)buffer[i]);
    }
}

On remarquera que si les 2 routines sont appelées successivement, les caractères en Ascii tombent en face de ceux en Hexa.

Initialisation (setup)

Dans ce setup, on initialise les ingrédients de ce projet et on interroge l’utilisateur pour entrer les caractères (numéro et nom de loco/train) à écrire sur une étiquette.
Un caractère de fin de saisie (#) permet de récupérer les données immédiatement après validation. Si on l’oublie, une tempo de 10 secondes y supplée quand même (pour les étourdis !).
Le texte qui s’affiche est un mode d’emploi exhaustif !
Car le programme est tout petit et la mémoire disponible permet d’y loger tous ces textes. On note au passage que ces textes sont encadrés d’un F(...) qui demande au compilateur de les stocker en mémoire flash et de ne pas encombrer la RAM.
Une particularité de ce setup est qu’il y a 3 boucles while imbriquées. Pour la première on en sort grâce au booleen done. Pour la seconde on en sort à la validation de la saisie de l’utilisateur. Pour la troisième on en sort par un break.

/**
 * Initialisation.
 */
void setup() {
  Serial.begin(115200);     // Intilatise la connexion serie avec le moniteur
  Wire.begin();             // Initialise I2C
  mfrc522.PCD_Init();       // Initialise le module MFRC522
  Serial.println();
  Serial.println(F("Programmation d'une étiquette MIFARE Ultralight."));
  Serial.println(F("Attention: Ecriture de données dans le secteur #1 (pages 4 à 7)"));
  boolean done = false;
  while (!done) {
    Serial.println(F("entrer le numero de loco (2 cars) puis espace puis Nom (7 car maxi) puis '#' puis valider"));
    byte len;
    Serial.setTimeout(10000L) ;       // temps de saisie limitée à 10s si on oublie le #
    while (Serial.available() == 0);  // attente entrée clavier et validation quelque soit le choix de fin de ligne
    len = Serial.readBytesUntil('#', (char *) dataBlock, 16) ; // lecture numero de loco et nom, maxi 16.
    Serial.print(F("Vous avez entré le Numero de loco et le Nom suivant: "));
    Serial.println();
    dump_byte_array(dataBlock, 16); // version Hexa
    Serial.println();
    dump_char_array(dataBlock, 16); // version Ascii
    Serial.println();
    Serial.println(F("taper Y pour programmer une etiquette ou N pour recommencer."));
    Serial.println("---------");
    byte incomingByte;
    while(1) {  //permet de quitter le setup et d'aller dans la loop qui attend l'étiquette et la programme
      if (Serial.available() > 0) {
        // Attente un caractère:
        incomingByte = Serial.read();
        if ((incomingByte == 'Y')||(incomingByte == 'y')) {
          done = true;
          break; // sortie du while
        }
        if ((incomingByte == 'N')|| (incomingByte == 'n')) {
          Serial.println();
          break;  // sortie du while et retour à la saisie
        }
      }
    }
  }
  Serial.println(F("Présentez maintenant une etiquette NTAG213 pour la programmer."));
  Serial.println();
}  // fin du setup

Vous avez remarqué l’utilisation de l’instruction Serial.readBytesUntil('#', (char *) dataBlock, 16) qui permet de transférer le contenu de la ligne de saisie du moniteur dans la variable dataBlock, du premier caractère jusqu’au "#" (non compris) qui signale la fin de saisie, avec une limite de 16 caractères maximum. En cas d’oubli d’ "#" la tempo de 10 secondes termine automatiquement la saisie, si on s’endort...

A la fin de la saisie du numéro et nom, il faut valider cette saisie en vérifiant le contenu de dataBlock qui s’affiche. Taper "N" ou "n" permettra de recommencer la saisie si on s’est trompé et taper "Y" ou "y" passe le programme à la loop qui se charge de l’écriture sur une étiquette.

Maintenant la loop : d’abord l’attente de présentation d’une étiquette

Le programme détecte la présence d’une étiquette, affiche le type d’étiquette et son UID (le numéro unique programmé en usine). Il n’accepte que les étiquettes Mifare Ultralight comme le NTAG213 présenté dans cet article.
Si tout va bien la suite de l’écriture se poursuit.

/**
 *  Programmation de l'etiquette
 */
void loop() {
    // attente présentation d'une étiquette
    if ( ! mfrc522.PICC_IsNewCardPresent())
        return;
        
    // Selection de l'etiquette
    if ( ! mfrc522.PICC_ReadCardSerial())
        return;
        
    // Affichage de l'UID et du type d'etiquette/tag
    Serial.print(F("Tag UID:"));
    dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size);
    Serial.println();
    Serial.print(F("PICC type: "));
    MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
    Serial.println(mfrc522.PICC_GetTypeName(piccType));
    // verification de compatibilité
    if (piccType != MFRC522::PICC_TYPE_MIFARE_UL) {
        Serial.println(F("Ce programme ne fonctionne qu'avec des étiquettes MIFARE Ultralight"));
        return;
    }

Lecture du secteur avant écriture pour savoir ce qui sera remplacé

Si l’étiquette n’est pas vierge (ou même si elle l’est), on commence par lire et afficher ce qui se trouve dans les pages 4 à 7. Ces données seront écrasées de toute façon, mais l’affichage sur le moniteur permettra de les récupérer au cas où il ne fallait pas les écraser.
L’instruction MIFARE_Read() fonctionne avec tous les types d’étiquettes au cas où vous envisagez d’utiliser ce programme avec d’autres étiquettes que les Ultralight.

    // Nous utilisons les pages 4 à 7 inclus (4 octets/page donc 16 octets)
    byte sector         = 1;
    byte blockAddr      = pageAddr;
    byte trailerBlock   = pageAddr + 3;
    byte buffer[18];        // 16 caractères + CRC
    byte size = sizeof(buffer);
    
     // Affichage du contenu du secteur 
    Serial.print(F("Contenu initial des pages: "));Serial.print(blockAddr);Serial.print(" à ");Serial.println(trailerBlock);
    status = (MFRC522::StatusCode) mfrc522.MIFARE_Read(blockAddr, buffer, &size);
    if (status != MFRC522::STATUS_OK) {
        Serial.print(F("MIFARE_Read() Echec: "));
        Serial.println(mfrc522.GetStatusCodeName(status));
    }
    dump_byte_array(buffer, 16); Serial.println();
    dump_char_array(buffer, 16); Serial.println();
    Serial.println();
    

Ecriture des données

Puis les données situées dans le tableau dataBlock sont écrites dans la mémoire de l’étiquette.
Attention : l’instruction MIFARE_Ultralight_Write() est réservée aux étiquettes Ultralight, d’où le test en début de programme qui le restreint à ce type.

    // Ecriture des données
    Serial.println(F("Données à écrire :  ")); //Serial.print(blockAddr);
    memcpy(buffer,dataBlock,16);

    //Serial.println(F(" ..."));
    dump_byte_array(buffer, 16); Serial.println();
    dump_char_array(buffer, 16); Serial.println();
    
    Serial.println(F("Ecriture des données..."));
    if (piccType == MFRC522::PICC_TYPE_MIFARE_UL) {
      for (int i=0; i < 4; i++) {
        //les données sont écrites dans une page (4 bytes par page)
        status = (MFRC522::StatusCode) mfrc522.MIFARE_Ultralight_Write(blockAddr+i, &buffer[i*4], 4);
        if (status != MFRC522::STATUS_OK) {
          Serial.print(F("MIFARE_Write() Echec: "));
          Serial.println(mfrc522.GetStatusCodeName(status));
        }
      }
    } 
    Serial.println();

Relecture et vérification

Enfin, comme il se doit, la relecture des données écrites et leur vérification sont faites et affichées.

    Serial.println(F("Relecture des données pour vérification... ")); 
    status = (MFRC522::StatusCode) mfrc522.MIFARE_Read(blockAddr, buffer, &size); // &size
    if (status != MFRC522::STATUS_OK) {
        Serial.print(F("MIFARE_Read() Echec: "));
        Serial.println(mfrc522.GetStatusCodeName(status));
    }
    Serial.println(F("Données lues dans l'etiquette; "));
    dump_byte_array(buffer, 16); Serial.println();
    dump_char_array(buffer, 16); Serial.println();
    // On va compter le nombre d'octets identiques à ceux de l'original
    Serial.println(F("Verification..."));
    byte count = 0;
    for (byte i = 0; i < 16; i++) {
        // Compare buffer (= ce qu'on a lu) avec dataBlock (= ce qu'on a écrit)
        if (buffer[i] == dataBlock[i])
            count++;
    }
    Serial.print(F("Nombre d'octets corrects = ")); Serial.println(count);
    if (count == 16) {
        Serial.println(F("Succes :-)"));
    } else {
        Serial.println(F("Echec :-("));
    }
    Serial.println();

Fin du processus

Le module est alors prié de s’endormir, ou on recommence l’écriture si on a un doute, avec les mêmes données saisies.

    // Halt PICC
    mfrc522.PICC_HaltA();
    // Stop encryption on PCD
    mfrc522.PCD_StopCrypto1();
    Serial.println(F("taper Y pour refaire la programmation ou appuyer sur le bouton de RESET pour programmer une autre étiquette"));
    Serial.println();
    byte incomingByte;
    while(1) {  
      if (Serial.available() > 0) {
    // read the incoming byte:
    incomingByte = Serial.read();
    if ((incomingByte == 'Y')||(incomingByte == 'y')) break;
    }
  }
}      // fin de loop()

A l’exécution, on doit voir ceci dans le moniteur de l’IDE :
Ici j’ai programmé une locomotive électrique BB20158 en numéro 0 (qui tire le premier train sur mon réseau).

Figure 10 : résultat de la programmation d'une étiquette
Figure 10 : résultat de la programmation d’une étiquette

On voit apparaitre une CC258963 en numéro 15 : c’est un essai qui montre qu’on peut taper plus de 7 caractères pour le nom de machine, mais seuls les 7 premiers seront transmis via CAN.
Une autre remarque concerne l’espace séparateur qui pourrait être remplacé par un chiffre quand le numéro de machine est supérieur à 100 (il faudrait 3 chiffres). Ici je n’en ai pas tenu compte, mais c’est une option possible.

Le programme est accessible en téléchargement ici :

Programme d’écriture sur Etiquette avec module I2C

Deuxième partie : le détecteur d’étiquettes et l’envoi sur le bus CAN

Ce détecteur a donc pour but de récupérer le numéro de train ou loco et le nom de ce train ou loco au passage au dessus du lecteur.

Mon réseau est constitué de deux boucles, une boucle "intérieure" et une boucle extérieure", une dans chaque sens de circulation, avec des possibilités de communications entre ces boucles. Les deux gares permettent de stocker 4 trains chacune car chaque boucle s’y dédouble.
J’ai choisi de détecter mes trains en entrée et en sortie de la gare principale. De chaque coté de la gare il y a un détecteur en entrée pour un sens de circulation et un autre en sortie pour l’autre sens.

Ici, dans cet exemple, je dispose une paire de lecteurs sous les 2 voies situées coté EST, entre la gare principale et la gare cachée (sous les zones 25 et 16). Cette boucle est située sous une montagne donc les lecteurs ne sont pas visibles des spectateurs, comme la gare cachée située sous le village. J’ajoute aussi une autre paire de lecteurs de l’autre coté de la gare principale (coté OUEST, sous les zones 11 et 30).
Les cotés EST et OUEST étant distants de plusieurs mètres, j’utilise un Nano pour deux détecteurs coté EST et un autre Nano pour deux détecteurs coté OUEST.
Comme chaque Nano sera connecté au bus CAN, j’ai prévu un identifiant "EST" et un identifiant "OUEST" pour déterminer l’origine des messages de détection. Et comme chaque Nano supporte deux détecteurs, un élément des messages CAN permettra de préciser s’il s’agit du détecteur de la voie "intérieure" ou "extérieure".

Figure 11 : disposition des capteurs et zones concernées
Figure 11 : disposition des capteurs et zones concernées

Voici les détecteurs mis en place avant la pose de la montagne qui cache ces boucles et rendent les trajets des trains plus attractifs.

Figure 12 : Deux lecteurs glissés sous les voies
Figure 12 : Deux lecteurs glissés sous les voies

Pour tester ces lecteurs et développer le programme, les voici sur une boucle de test :

Figure 15 : deux modules SPI raccordés à une carte Nano avec interface Can
Figure 15 : deux modules SPI raccordés à une carte Nano avec interface Can

J’ai choisi ici des modules en interface SPI de façon à pouvoir en brancher deux sur une carte Nano ou Uno équipée d’une interface CAN. On pourrait d’ailleurs en brancher bien plus car il reste des broches disponibles pour les signaux CS d’autres lecteurs.

Voici le schéma du montage complet avec une carte CAN :

Figure 13 : schéma des connexions des lecteurs RFID en mode SPI avec une carte CAN
Figure 13 : schéma des connexions des lecteurs RFID en mode SPI avec une carte CAN

Dans mon projet, j’ai choisi d’utiliser une carte à base de Nano, déjà équipée d’une interface CAN. Cette carte est décrite sur le forum ici :.

Figure 14 : Une carte Locoduino contenant déjà une interface Can
Figure 14 : Une carte Locoduino contenant déjà une interface Can

J’en avais 2 en stock. Elle contient son alimentation (9V est l’idéal) et l’interface CAN. Par contre je n’utilise pas le décodeur DCC ni les interfaces servos.

Cette carte n’ayant pas de connecteurs SPI disponibles pour les cartes RC522, j’ai soudé directement une nappe sur les points de soudure du support du Nano et réalisé une petite plaque avec 2 connecteurs femelles pour les modules RFID, auxquels j’ai soudé une simple nappe de 8 fils. J’espère faire prochainement un circuit imprimé propre.
Il serait possible aussi d’envisager une version RFID/NFC dans le cadre du satellite V2 (voir ce sujet sur le forum).

Figure 16 : connexion du bus SPI.
Figure 16 : connexion du bus SPI.

Voici maintenant le logiciel à installer sur la carte

Pour mieux appréhender la programmation pour la RFID d’un coté et celle du CAN d’un autre coté, après les entêtes communes (bibliothèques et globales) qu’il est difficile d’éparpiller, le RFID est séparé du CAN. Mais tout se retrouve au complet dans le programme à télécharger à la fin ?

Entête : bibliothèques, globales

La liste des bibliothèques s’allonge avec celles du CAN et l’EEPROM.
Les lecteurs sont gérés en SPI et sont au nombre de deux.

Pour installer la bibliothèque acan2515 de pierre molinaro, on la téléchargera ici :
bibliothèque ACAN

Pour différencier les 2 cartes, j’ai créé la notion de carte OUEST et carte EST, chacune ayant un identifiant CAN différent : 0x1A pour la carte OUEST et 0x1B pour la carte EST.

Nous retrouvons bien évidemment toutes les déclarations nécessaires à la bibliothèque ACAN (voir l’article La carte servomoteurs CAN et DCC (2)).

#include <SPI.h>
#include <mcp_can.h>
#include <MFRC522.h>
#include <ACAN2515Settings.h> /* Pour les reglages  */
#include <ACAN2515.h>         /* Pour le controleur */
#include <CANMessage.h>       /* Pour les messages  */
#include <EEPROM.h>         

#define SS_1_PIN 8 
#define SS_2_PIN 7 
#define RST_PIN 9
#define NR_OF_READERS   2
byte ssPins[] = {SS_1_PIN, SS_2_PIN};

MFRC522 mfrc522[NR_OF_READERS];   // Create MFRC522 instances.
MFRC522::StatusCode status;       // variable pour le statut des opérations 

uint8_t pageAddr = 0x04;      //Les données sont ecrites dans 4 pages : 4,5,6 et 7.
                              //Pages 0 to 3 sont réservées donc inutilisées.       
const byte versionNb = 13;    // 0.1.3 du 22/06/20 Tout changement de version provoque une Raz de l"EEPROM 
const byte OUEST = 0x1A;      // 26
const byte EST = 0x1B;        // 27
const byte EstouOuest = EST;  // position des lecteurs

static const byte MCP2515_CS  = 10 ; // CS input of MCP2515
static const byte MCP2515_INT = 3 ; // INT output of MCP2515

ACAN2515 controleurCAN(MCP2515_CS, SPI, MCP2515_INT);
static const uint32_t FREQUENCE_DU_QUARTZ = 16ul * 1000ul * 1000ul; //16Mhz
static const uint32_t FREQUENCE_DU_BUS_CAN = 500ul * 1000ul;        //500kHz
CANMessage messageCANEmission;
CANMessage messageCANReception;

Utilitaires de gestion EEPROM et d’affichage

L’EEPROM du Nano va contenir peu de choses, étant donné qu’il n’y a pas de configuration en réseau CAN :

  • Un tableau de 4 caractères : "RFID"
  • Un numéro de version du programme (un octet)
  • l’identifiant des messages CAN courts émis à chaque détection
  • la position EST ou OUEST sous forme de booléen.
    Nous avons une fonction de sauvegarde et une fonction de récupération des paramètres en EEPROM.
    Enfin nous retrouvons nos fonctions d’affichage des données lues dans les étiquettes comme dans le programmateur d’étiquettes.
void EEPROM_sauvegarde() {
  int addr = 0;
  for ( int i = 0; i<4; i++) {
  EEPROM.update(addr++, EEPROM_ID[i]);
  }
  EEPROM.update(addr++, versionNb);
  EEPROM.update(addr++, detectId);
  EEPROM.update(addr++, isEstOuest);
}

bool EEPROM_chargement() {
  int addr = 0;
  char buf[4];
  byte vers;
  for (int i = 0; i < 4; i++) {
    buf[i] = EEPROM.read(addr++);
    if (buf[i] != EEPROM_ID[i]) return false;
  }
  vers = EEPROM.read(addr++);
  Serial.print("version EEPROM ");Serial.println(vers);
  if (vers != versionNb) return false;
  detectId = EEPROM.read(addr++);
  Serial.print("detectId EEPROM ");Serial.println(detectId);
  if (detectId == 27) isEstOuest = 1; else isEstOuest = 0;
  isEstOuest = EEPROM.read(addr++);
  Serial.print("isEstOuest EEPROM ");Serial.println(isEstOuest? "EST": "OUEST");
  return true;
}
/**
 * Routines d'affichage d'un tableau d'octets en Hexa et en Ascii (char)
 */
void dump_byte_array(byte *buffer, byte bufferSize) {
    for (byte i = 0; i < bufferSize; i++) {
        Serial.print(buffer[i] < 0x10 ? " 0" : " ");
        Serial.print(buffer[i], HEX);
    }
}
void dump_char_array(byte *buffer, byte bufferSize) {
    for (byte i = 0; i < bufferSize; i++) {
        Serial.print("  ");
        Serial.print((char)buffer[i]);
    }
}

Initialisations du programme (setup)

Dans l’ordre on trouve :

  • l’initialisation du port série ;
  • l’initialisation du bus SPI et des lecteurs RFID ;
  • la configuration du bus CAN ;
  • le démarrage du bus CAN ;
  • l’affichage de la version et de la version logicielle ;
  • la lecture des données en EEPROM
    Si ces données ne correspondent pas aux variables de position et de version, celles-ci sont mises à jour et rechargées en EEPROM. Ceci n’arrive qu’une seule fois au premier démarrage ou lorsque la version logicielle change.
void setup() { 
  Serial.begin(115200);
  while (!Serial);    // Do nothing if no serial port is opened (added for Arduinos based on ATMEGA32U4)
  
  SPI.begin(); // Init SPI bus
  
  for (uint8_t reader = 0; reader < NR_OF_READERS; reader++) {
    mfrc522[reader].PCD_Init(ssPins[reader], RST_PIN); // Init each MFRC522 card
    Serial.print(F("Reader "));
    Serial.print(reader);
    Serial.print(F(": "));
    mfrc522[reader].PCD_DumpVersionToSerial();
  }
  Serial.println("RC522 init OK");
  
  ACAN2515Settings configuration(FREQUENCE_DU_QUARTZ, FREQUENCE_DU_BUS_CAN);
  //const uint32_t codeErreur = controleurCAN.begin(configuration, [] { controleurCAN.isr(); }); // ni masque, ni filtre
  const uint16_t codeErreur = controleurCAN.begin(configuration, [] { controleurCAN.isr(); }, masque, filtres, 2 );
  if (codeErreur == 0) {
  Serial.println("controleurCAN configuration OK");
  } else {
    Serial.print("controleurCAN: Probleme de connexion: ");
    Serial.println(codeErreur);
  }
  isEstOuest = (EstouOuest == EST)? true : false;
  Serial.print("versionSW " ); Serial.print(versionNb); Serial.print(" Position " ); Serial.print(EstouOuest, HEX);
  Serial.print(" ->" ); Serial.println(isEstOuest? "EST" : "OUEST");
  Serial.println("chargement EEPROM" );
  
  if (EEPROM_chargement() == false) {
    EEPROM_sauvegarde();
  }
} // end of setup

Boucle principale du programme (loop)

Cette partie est répétitive comme dans tout programme Arduino.

Détection et lecture des étiquettes NTAG213

Nous retrouvons une partie de ce qui a été mis dans le programmateur d’étiquette, pour chaque lecteur d’index 0 et 1 :

  • détection de passage d’une étiquette devant le lecteur ;
  • vérification du type d’étiquette Mifare Ultralight ;
  byte nloco;
  byte loco;

  for (uint8_t reader = 0; reader < NR_OF_READERS; reader++) {
    // Look for new cards
    if (mfrc522[reader].PICC_IsNewCardPresent() && mfrc522[reader].PICC_ReadCardSerial()) {
      Serial.print("Detection ");Serial.print(isEstOuest? "EST " : "OUEST ");
      Serial.print((reader==0)? "Interieur: " : "Exterieur: "); // reader = 0 ou 1 (1 exterieur et 0 interieur)
      dump_byte_array(mfrc522[reader].uid.uidByte, mfrc522[reader].uid.size);
      Serial.println();
    
      Serial.print(F("PICC type: "));
      MFRC522::PICC_Type piccType = mfrc522[reader].PICC_GetType(mfrc522[reader].uid.sak);
      Serial.println(mfrc522[reader].PICC_GetTypeName(piccType));
      // verification de compatibilité
      if (piccType != MFRC522::PICC_TYPE_MIFARE_UL) {
        Serial.println(F("Ce programme ne fonctionne qu'avec des étiquettes MIFARE Ultralight"));
        return;
      }

Ensuite il y a la lecture des pages 4 à 7 dans un tableau buffer et l’affichage des données lues.

      // lecture numero et nom du train/loco
      // Nous utilisons les pages 4 à 7 inclus (4 octets/page donc 16 octets)
      byte sector         = 1;
      byte blockAddr      = pageAddr;
      byte trailerBlock   = pageAddr + 3;
      byte buffer[18];        // 16 caractères + CRC
      byte size = sizeof(buffer);
    
      // Affichage du contenu du secteur 
      Serial.print(F("Contenu des pages: "));Serial.print(blockAddr);Serial.print(" à ");Serial.println(trailerBlock);
      status = (MFRC522::StatusCode) mfrc522[reader].MIFARE_Read(blockAddr, buffer, &size);
      if (status != MFRC522::STATUS_OK) {
        Serial.print(F("MIFARE_Read() Echec: "));
        Serial.println(mfrc522[reader].GetStatusCodeName(status));
      }
      dump_byte_array(buffer, 16); Serial.println();
      dump_char_array(buffer, 16); Serial.println();
      Serial.println();

      // numero de loco
      nloco = (buffer[0] - '0')*10 + (buffer[1] - '0');
      Serial.print("Loco No ");Serial.print(nloco);
      // nom de loco
      Serial.print(" - Nom : ");
      dump_char_array(&buffer[3], 8);
      Serial.println();

A ce stade, nous avons le numéro dans la variable nloco, le nom dans le tableau buffer et le numéro du lecteur (intérieur ou extérieur) dans la variable reader

Nous allons construire chaque message CAN de la façon suivante :

Table 1 : construction des messages CAN
Position du détecteur Identifiant du message CAN 1er octet données 7 octets de données suivants
EST intérieur
0x1A
nloco
octets 1 à 7 de buffer
Est extérieur
0x1A
0x80 + nloco
octets 1 à 7 de buffer
OUEST intérieur
0x1B
nloco
octets 1 à 7 de buffer
OUEST extérieur
0x1B
0x80 + nloco
octets 1 à 7 de buffer

Composition et envoi d’un message CAN à chaque détection

La définition des masques et filtres ci-après s’insère avant le setup.

Definition des masques et des filtres CAN

Pour comprendre les masques et les filtres CAN, on se reportera à l’article La bibliothèque ACAN (2)
Cette partie est optionnelle car ces cartes ne reçoivent pas de commandes par le réseau CAN (pour le moment). Mais il serait possible de le faire pour des opérations de configuration par exemple.
La fonction messageCmd est appelée lorsqu’un message reçu est conforme aux filtres définis juste après.
Les deux filtres sur des identifiants longs n’acceptent que des identifiants se terminant par "7A" ou "7B" (cela fait partie de mon plan d’adressage globale du CAN de mon réseau, que je ne décrit pas ici).

void messageCmd (const CANMessage & inMessage) {
  uint32_t mId = inMessage.id;
  uint8_t cmdType = inMessage.data[0];
  if ((mId & 0x1F) == EstouOuest) { // test 1A (EST) ou 1B (OUEST)
  // traitement d'un message de configuration (non décrit ici)  
  }
}
/* 
 * Définition des masques et filtres.
 */
const ACAN2515Mask masque = extended2515Mask (0x1FFFFFFF) ; // For filter #0 and #1
  const ACAN2515AcceptanceFilter filtres [] = {
    {extended2515Filter (0x1FFFFF7A), messageCmd},
    {extended2515Filter (0x1FFFFF7B), messageCmd},
  } ;

Envoi du message CAN

Et maintenant la composition et l’envoi du message CAN correspondant aux données lues dans l’étiquette, qui s’insère dans la loop.
Nous allons utiliser le numéro dans la variable nloco, le nom se trouvant dans le tableau buffer et le numéro du lecteur (intérieur ou extérieur) dans la variable reader :

  • L’identifiant court des messages est EST (0x1A) ou OUEST (0x1B).
  • Le numéro de loco sera inscrit dans le premier octet du message. Comme il y a deux lecteurs, le bit 7 (poids le plus fort) est positionné selon ce lecteur (0 si voie intérieure ou 1 si voie extérieure).
  • Puis s’ajoutent les 7 caractères du nom de loco ou de train.
  • Enfin le message est émis.
     // preparation envoi message CAN
      if (isEstOuest) messageCANEmission.id = OUEST; else messageCANEmission.id = EST;
      messageCANEmission.ext = false ;
      messageCANEmission.len = 8 ;
      if (reader == 1) {loco = nloco + 0x80;} else {loco = nloco;}
      messageCANEmission.data[0] = loco;
      for (byte i = 1; i < 8; i++) {
        messageCANEmission.data[i] =buffer[i+2];
      }

      // envoi du message CAN
      const bool ok = controleurCAN.tryToSend(messageCANEmission);
      delay(50);
      if (ok) {
        Serial.print("Message train ");Serial.print(nloco);Serial.println(" transmis !");
      } else {
        Serial.println("Pb de transmission CAN !");
      }
      Serial.println();

Enfin, si nous utilisions les filtres, il faudrait ajouter dans la loop cette ligne de code qui permet à la bibliothèque Acan de dispatcher les messages reçus, c’est à dire qui passe la main à la fonction messageCmd() lors d’une réception de message détecté par les filtres. :

controleurCAN.dispatchReceivedMessage()

Fin du programme

Enfin, on retrouve la mise en sommeil du lecteur et toutes les fins de conditions ouvertes précédemment, ainsi que la fin de loop

     // Halt PICC
      mfrc522[reader].PICC_HaltA();
      // Stop encryption on PCD
      mfrc522[reader].PCD_StopCrypto1();
    } //if (mfrc522[reader].PICC_IsNewC...
  } //for(uint8_t reader)
} // end of loop

Vous aurez noté l’abondance de commentaires et de sorties Serial.print qui peuvent être conditionnées par une option "DEBUG".

Voici le programme de ces lecteurs :

Programme de lecture d’etiquettes RFID et transmission sur bus Can du numéro et nom du train ou loco

et les 2 bibliothèques

Les deux bibliothèques MFRC522 SPI et I2C

Exemple de détection d’étiquette et de messages CAN transmis

Si je reprend l’étiquette programmée précédemment, voici ce qui s’affiche sur le moniteur de l’IDE lorsque cette étiquette est approchée d’un des deux lecteurs :

Reader 0: Firmware Version: 0x92 = v2.0
Reader 1: Firmware Version: 0x92 = v2.0
RC522 init OK
controleurCAN configuration OK
versionSW 13 Position 1B ->OUEST
chargement EEPROM
version EEPROM 13
detectId EEPROM 26
isEstOuest EEPROM OUEST

Detection OUEST Exterieur:  04 ED 7E C2 99 58 80
PICC type: MIFARE Ultralight or Ultralight C
Contenu des pages: 4 a 7
 30 30 20 42 42 32 30 31 35 38 00 00 00 00 00 00
  0  0     B  B  2  0  1  5  8  

Tiens, nous retrouvons notre BB20158 !

Un exemple de messages CAN reçu par un moniteur maison est affiché sur la figure 17. Chaque message est décodé pour afficher les données en clair. :

Figure 17 - messages Can visualisés sur un testeur (maison) de réseau Can
Figure 17 - messages Can visualisés sur un testeur (maison) de réseau Can

Nous retrouvons bien notre BB20158 (N° 0) sur le lecteur EST puis sur le lecteur OUEST après un tour complet du circuit !
Au total 4 trains sont en mouvement (Numéros 0 à 3).

Une remarque concernant la position des lecteurs et des étiquettes : Il faut faire des essais, donc regarder les détections sur un moniteur ou un afficheur.
Placé sous la motrice, sous le moteur, cela ne convient pas le plus souvent. Il y a parfois aucune place possible sous la motrice : je colle alors l’étiquette sous un wagon. Mais parfois le dessous des wagons ne présente pas de surface plate : je colle alors un morceau d’adhésif double face épais.
Idem pour le lecteur, il doit être bien centré et la position la plus efficace se trouve après essais en variant la vitesse du train

Des application nombreuses de la détection RFID/NFC

Dans mon réseau j’utilise le numéro de loco ou de train et le nom de la loco dans le gestionnaire des circulations : le suivi des trains est réalisé en partant des occupations et des libérations donnés par les détecteurs de consommation. Cela permet d’assurer la sécurité des circulations. Mais parfois, un mauvais contact (en N) fait perdre un train. alors au passage sur un détecteur RFID, tout revient dans l’ordre. De façon certaine, chaque identification RFID s’impose dans le suivi des trains. Mais elle n’est pas nécessaire sur tous les cantons, d’où le faible nombre de détecteurs pour mes 40 zones.
Le nom de la loco est utilisé pour l’affichage du train sur le TCO graphique que j’ai développé en parallèle avec le gestionnaire.

Bien d’autres applications sont aussi possibles sans rapport avec un gestionnaire comme le mien. Voici quelques suggestions :

  • déclencher une animation visuelle ou sonore particularisée pour chaque train (annonce en gare de l’arrivée imminente d’un train).
  • avertisseur sonore spécifique à chaque loco diffusé par un haut-parleur près de la voie (plutôt que d’acheter des locomotives sonores hors de prix).
  • meuglement des vaches au passage de certains trains (elles ont leurs têtes parfois).
  • repérages de wagons en stock dans une gare de triage et aide à la compositions des trains.
  • insertion automatique de nouveaux trains dans une circulation de plusieurs trains.
  • utilisation des identifications de trains dans un automate de circulation simple (auquel je suis en train de réfléchir avec la petite centrale wifi à base d’ESP32 qui dispose d’une interface CAN).
  • On trouvera d’autres exemples dans le sujet à lire sur le forum annonce en gare

J’invite nos chers lecteurs à donner leurs avis sur le forum Locoduino dans le sujet "Vos Projets/RFID/NFC".