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.
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
}
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 !