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

2ème partie : la programmation

. Par : Dominique, Jean-Luc. URL : https://www.locoduino.org/spip.php?article148

Dans la première partie de cet article, Mise en oeuvre du Bus CAN, nous vous avons présenté le Bus CAN comme un des meilleurs choix (je crois même que c’est le meilleur !) pour faire communiquer entre elles plusieurs cartes Arduino dans le cadre de nos projets ferroviaires.

Ce choix est d’autant plus raisonnable qu’il est facile de construire ses propres cartes CAN sur la base de la carte Can Locoduino ou à partir de cartes que l’on trouve maintenant facilement dans le commerce, pour peu que l’on fasse attention à leur compatibilité avec la carte Locoduino et le logiciel utilisé.

L’implémentation qui suit concerne uniquement les cartes Arduino à base de processeur Atmel (328, 2560), donc les Uno, Nano, Mega pour ne citer que les principaux.

Pour le Due, qui contient 2 interface CAN intégrées, c’est possible aussi mais avec une bibliothèque différente et du matériel différent mais plus simple.

Il va sans dire que le Due comme gestionnaire et quelques Mega et Nano pour s’occuper de la traction, les aiguilles, les occupations, les signaux et le décor, le tout relié sur un ou deux bus Can, constituent une informatique ferroviaire certainement haut de gamme, mais réalisable par nous autres amateurs.

Pour se fixer les idées, on va commencer par décrire les éléments logiciels qui font partie d’une carte de commande d’aiguilles, par exemple à base d’un Mega2560, sans entrer dans le détail de la façon de commander les moteurs d’aiguille (juste un petit peu), par les échanges CAN.

Ces éléments sont présentés dans l’ordre logique d’un programme Arduino. Il vous suffira de "copier-coller" les morceaux de code dans votre projet pour que les communications CAN soient immédiatement opérationnelles.

Mise en place de l’interface CAN : version "recette de cuisine"

Cette première partie dite "recette de cuisine" n’a pour but que de vous livrer les ingrédients à mettre en place dans votre programme, sans explication précise sur le bus CAN et son fonctionnement qui a été présenté dans l’article précédent. Ensuite viendront ces explications (patience !).

Aussi, si vous êtes impatient, le code important est disponible dès le début de l’article.

Le matériel se compose d’une carte Arduino (par exemple ici un Mega2560) et d’une carte CAN Locoduino. Si vous n’avez pas cette carte, mais une autre équipée d’un MCP2515 fréquencé à 16 MHz, ça doit marcher également.

On commence par relier à la carte CAN les broches du bus SPI, ainsi que le +5V et le 0V (Gnd).
Ajoutons une liaison entre la broche INT (interruption) de la carte CAN et la broche 2 (Interruption 0) de l’Arduino.l

Les branchements

Bus SPI de l’Arduino

Module CANArduino Uno/Pro Mini/Nano
SCK 13 (SCK)
SO 12 (MISO)
SI 11 (MOSI)
CS 10 (SS)
INT 2 (INT0)
Module CANArduino Mega
SCK 52 (SCK)
SO 50 (MISO)
SI 51 (MOSI)
CS 53 (SS)
INT 2 (INT0)

Alimentation

Module CANArduino
GND GND
VDD 5V

La bibliothèque

Il faut télécharger une bibliothèque qui se trouve ici : https://github.com/Seeed-Studio/CAN.... Puis il faut placer le dossier téléchargé dans le dossier des autres bibliothèques. Voir l’article Installer une bibliothèque.

Ensuite on doit placer ces 2 lignes en tête de programme pour bénéficier de la bibliothèque :

#include <SPI.h>                 // pour la bibliothèque CAN
#include "mcp_can.h"             // bibliothèque CAN

Puis il faut créer l’objet CAN comme le permet la bibliothèque :

// variables globales pour l'interface CAN
MCP_CAN CAN(53);   // Definition du CS (chip select) pin 53 (SS du bus SPI)
volatile byte Flag_Recv = 0;   // variable d'échange avec l'interruption IRQ

On voit ainsi qu’une variable globale Flag_Recv servira à faire savoir à la LOOP qu’un ou plusieurs messages sont arrivés sous interruption. Attention, quand cette interruption fait monter l’indicateur Flag_Recv, il faut bien prendre soin de vider TOUT le tampon du MCP2515 (sinon, il n’y aura plus d’autre IRQ et tout se bloque !).

Cette variable est positionnée par la routine d’interruption suivante :

/* 
 *  ISR CAN (Routine de Service d'Interruption)
 *  le flag IRQ monte quand au moins un message est reçu
 *  le flag IRQ ne retombe QUE si tous les messages sont lus
 */ 

void MCP2515_ISR()
{
     Flag_Recv = 1;
}

Après avoir placé ces lignes de code en tête de programme, dans la zone des définitions de variables, abordons le SETUP dans lequel on insère les lignes suivantes :

  /* -----------------------------------------------------
  *                       SETUP
  * -----------------------------------------------------
  */

  /////////////// INIT CAN /////////////////
  
while(true)
{
  if (CAN_OK == CAN.begin(CAN_500KBPS))      
   // initialisation du can bus : baudrate = 500k
  {
    Serial.println(F("CAN BUS init ok!"));
    break; // on sort du while.
  }
  else
  {
    Serial.println(F("CAN BUS init echec !"));
    Serial.println(F("Init CAN BUS a nouveau"));
  }
  delay(200);
}

On comprend bien ici que l’instruction CAN.begin(baudrate) démarre l’interface, avec un compte-rendu CAN_OK, sinon cela se répète car, à ce stade de l’initialisation, si le bus CAN ne démarre pas, il est inutile d’aller plus loin.

Personnellement je n’ai jamais vu d’échec sauf si la carte CAN n’est pas (ou est mal) branchée, coté Arduino.

Ensuite il faut attacher l’interruption 0 à la routine MCP2515_ISR() précédente :

attachInterrupt(0, MCP2515_ISR, FALLING); // interrupt 0 (pin 2)

Enfin on définit les filtres CAN qui limiteront les messages reçus à seulement ceux qui intéressent notre carte :

  /*
   * set mask & filter 
   */
   
  CAN.init_Mask(0, 0, 0x7F0);  // Il y a 2 masques à initialiser dans le mcp2515
  CAN.init_Mask(1, 0, 0x7F0);  // on teste tous les bits sauf les 4 de poids faible
   
  CAN.init_Filt(0, 0, 0x40);        // Reception possible : Id 40 à 4F (hex) 
  CAN.init_Filt(1, 0, 0x40);        // idem
  CAN.init_Filt(2, 0, 0x40);        // Reception possible : Id 40 à 4F (hex) 
  CAN.init_Filt(3, 0, 0x40);        // idem
  CAN.init_Filt(4, 0, 0x00);        // Reception possible : Id 00 à 0F
  CAN.init_Filt(5, 0, 0x00);        // Idem

A titre d’exemple, ces filtres sont initialisés ici pour ma carte de commande d’aiguilles.

Le setup ayant mis en place tous les acteurs, la loop peut commencer son travail répétitif !

/*-----------------------------------------------------
 *                        LOOP
 *-----------------------------------------------------                       
 */

void loop()
{

  if (Flag_Recv)  {
    Flag_Recv = 0;  // Flag MCP2515 prêt pour un nouvel IRQ
    CAN_recup();    // récupération du ou des messages CAN reçus
  }
  ...

Cette fonction CAN_recup() se charge de lire tous les messages reçus par le MCP2515 et les sauvegarder dans une mémoire tampon circulaire. Notre programme aura alors le loisir d’exploiter ces messages au rythme de son choix. Par exemple, on peut ne traiter qu’un seul message par tour de LOOP, mais j’ai constaté rapidement des pertes de messages. Il vaut mieux tout traiter d’un coup.

Tout d’abord il faut ajouter quelques variables globales :

// Variables globales pour la gestion des Messages reçus et émis
byte IdR;                       // Id pour la routine CAN_recup()
unsigned char lenR = 0;         // Longueur "    "       "
unsigned char bufR[8];          // tampon de reception      "
unsigned char bufS[8];          // tampon d'emission

// Variable globale Mémoire circulaire pour le stockage des messages reçus
unsigned char _Circule[256];    // récepteur circulaire des messages CAN sous IT
int _indexW, _indexR, _Ncan;    // index d'écriture et lecture, nb d'octets a lire
byte _CANoverflow = 0;          // flag overflow (buffer _Circule plein)

Voici la fonction CAN_recup() qui se charge du boulot :

/*
 * Routine de récupération des messages CAN dans la mémoire circulaire _Circule
 * appelée par LOOP lorsque Flag_Recv = 1;
 */
 
void CAN_recup()
{
  unsigned char len = 0;  // nombre d'octets du message
  unsigned char buf[8];   // message
  unsigned char Id;   // Id (on devrait plutôt utiliser un int car il y a 11 bits)

  while (CAN_MSGAVAIL == CAN.checkReceive())  {
    CAN.readMsgBuf(&len, buf);        // read data, len: data length, buf: data buf
    Id = CAN.getCanId();
    if ((_Ncan+len+2) < sizeof(_Circule))  { // il reste de la place dans _Circule
      _Circule[_indexW] = Id;         // enregistrement de Id
      _indexW++;
      _Ncan++;
      if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      _Circule[_indexW] = len;        // enregistrement de len
      _indexW++;
      _Ncan++;
      if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      for (byte z = 0; z<len; z++)  {
        _Circule[_indexW] = buf[z];    // enregistrement du message
        _indexW++;
        _Ncan++;
        if (_indexW == sizeof(_Circule))  {_indexW = 0;}
      }
    } else {
      _CANoverflow = 1;  // dépassement de la capacite de Circule
                            // le message est perdu
    }
  } 
}

A ce stade, on a juste reçu et sauvegardé les messages CAN qui viennent d’arriver.

Pour exploiter les messages, de façon indépendante de leur réception en temps réel, j’utilise le code suivant :

  byte RId;  // variables pour le traitement des messages lus dans _Circule
  byte Rlen;
  byte Rbuf[8];
  
  // traitement des messages stockés dans la mémoire circulaire _Circule
    
  while (_Ncan > 2)  {    // chaque message dans _Circule occupe au moins 3 octets
    _Ncan--;
    RId = _Circule[_indexR];        // recup Id
    _indexR++;
    if (_indexR == sizeof(_Circule))  {_indexR = 0;}
    _Ncan--;
    Rlen = _Circule[_indexR];       // recup longueur
    _indexR++;
    if (_indexR == sizeof(_Circule))  {_indexR = 0;}
    if (_dumpCan)  {     	 // _dumpCan est un boolean a déclarer en globale 
      Serial.print("CAN id ");	// si on veut conditionner l'affichage des message
      Serial.print(RId);
      Serial.print(", data ");
    }
    for (int k = 0; k < Rlen; k++)  {
      _Ncan--;
      Rbuf[k] = _Circule[_indexR];  // recup octets message
      _indexR++;
      if (_indexR == sizeof(_Circule))  {_indexR = 0;}
      if (_dumpCan)  {  
      Serial.print("0x");
      Serial.print(Rbuf[k], HEX);
      }
    }
    if (_dumpcan) Serial.println();
   // le message est maintenant dans les globales RId, Rlen et Rbuf[..]
  // ---> Votre traitement du message à ajouter ici <---
}

La suite du code est maintenant personnelle, selon la signification donnée à l’identifiant RId et aux octets de Rbuf.
On pourra avantageusement utiliser l’instruction switch pour traiter les différents cas de RId.

switch (Rid) {
  case xxx:
  // votre code
  break;
}

On pourra également utiliser les test de bits comme :

if (bitRead(Rbuf[0], 7) {
  // votre code
}

Et l’émission de messages ?

Mais c’est très simple :
J’envoie par exemple des messages ultra simples contenant un seul octet avec la fonction suivante :

void CANMessage(byte Message)
{
  unsigned char bufS[8];
  bufS[0] = Message;
  CAN.sendMsgBuf(0x30, 0, 1, bufS);  
  // Id = 30H (Aiguille), message standard avec 1 seul octet
}

Tous les éléments de base sont maintenant en place.
Voici quelques explications complémentaires.

Retour sur la fonction CAN_recup()

Pourquoi ne pas traiter directement les messages reçus par le MCP2515 dans la routine d’interruption ?
Il y a plusieurs raisons à cela :

  • il faut libérer cette routine le plus vite possible pour ne pas perdre de nouveaux messages
  • il faut éviter d’être obligé d’utiliser un grand nombre de variables globales qui seraient spécifiques aux traitements.

Le meilleur moyen de libérer le MCP2515 est de récupérer ce qui se trouve dans son tampon et le transférer en mémoire. Comme il est impossible de prévoir combien de messages arriveront, à quelle fréquence et avec quelle taille, un tableau ne convient pas du tout car il pourrait conduire au gaspillage de la mémoire.

J’utilise donc une mémoire circulaire, c’est à dire une série d’octets (un tableau unsigned char _Circule[256] précisément ici) qui est gérée comme une FIFO (1er entré, 1er sorti) : Les messages composés de leur Id (ramené à 1 octet), leur longueur len et leurs données (len octets) sont stockés à la queue leu leu au moyen d’un pointeur d’écriture _indexW qui avance d’un cran à chaque octet écrit. Lorsque ce pointeur arrive en bout de tableau, le pointeur est remis à zéro et ça continue.
Evidemment la lecture des messages se fait concurremment ailleurs dans la LOOP, à l’aide d’un pointeur de lecture _indexR répondant au même principe.

Pour savoir s’il y a des messages dans la mémoire circulaire, un compteur Ncan est incrémenté par la fonction CAN_recup() et est décrémenté par la fonction qui les traite.

Au fur et à mesure de la lecture des messages, le pointeur _indexR avance et Ncan diminue, ce qui libère de la place dans la mémoire.

Ncan = 0 signifie qu’il n’y a pas de message. Un message existe quand Ncan > 2 (un octet Id, un octet longueur, un octet de donnée).

Si Ncan = 256, cela veut dire que la mémoire circulaire est pleine, ce qui ne doit jamais arriver (il faudrait alors augmenter sa taille), ou que la fonction de traitement est en panne quelque part (c’est un bug !).

A vous de jouer !

Maintenant vous pouvez mettre du CAN dans vos projets ferroviaires !.

Yes you CAN !