Le bus CAN a été présenté à plusieurs reprises sur LOCODUINO mais la récente demande d’un des membres du forum m’a fait prendre conscience que la mise en œuvre manquait d’exemples illustratifs pour que les modélistes puisse prendre ce bus en main le plus facilement possible. De plus, depuis les premiers articles, « Mise en oeuvre du Bus CAN entre modules Arduino (1) » et « Mise en oeuvre du Bus CAN entre modules Arduino (2) », de l’eau a coulé sous les ponts et d’autres bibliothèques pour le MCP2515 ou un contrôleur CAN plus récent comme le MCP2518 sont apparues et sont à la fois plus riches et plus faciles à mettre en œuvre que la vénérable bibliothèque de SeeedStudio.
La bibliothèque ACAN (1)
Prise en main
. Par :
. URL : https://www.locoduino.org/spip.php?article268Rappelons rapidement que le bus CAN est particulièrement bien adapté au modélisme ferroviaire et est à la fois simple, robuste et économique. Sa mise en œuvre sur le réseau est aisée. S’agissant d’un bus, il suffit d’une simple paire de câbles torsadés pour relier les contrôleurs entre eux et étendre le réseau. On insère un nouveau contrôleur en coupant le câble existant et en le reliant au nouveau contrôleur.
En CAN, tous les appareils sont en capacité de recevoir l’ensemble des messages circulants sur le bus mais aussi de communiquer des messages à destination de tous les autres appareils. Il y a donc une économie en termes de matériel et d’infrastructure.
En contrepartie, il faut connaître et maîtriser la messagerie propre au CAN. Ce qui est l’objet de cette série d’articles.
Nous allons donc examiner la bibliothèque ACAN et notamment son incarnation pour le MCP2515 dont on trouve des modules facilement et à petit prix sur eBay ou Aliexpress [1]. Je veux parler des modules comme celui présenté à la figure 1 qui est sans doute la façon la moins chère (moins de 1€50) et la plus facile pour ajouter une interface CAN à un Arduino et je dirais même pour mettre des Arduino en réseau de manière générale. Ça sera également l’occasion de voir la façon de concevoir plusieurs petites applications exploitant le bus CAN.
- Figure 1 : Module CAN
- Ce module embarque un contrôleur CAN MCP2515 (U2) et un transceiver CAN TJA1050 (U1), il s’agit d’un circuit d’interface entre les niveaux logiques (0 correspond à 0V, 1 correspond à 5V) et le bus qui lui est en différentiel (0 correspond à V
H - VL = 5V et 1 correspond à VH = VL ). Le quartz (X1) est à 8MHz, ce qui limite la vitesse du bus à 512kbits/s.
Le MCP2515 est un contrôleur CAN qui communique avec l’Arduino via le bus SPI et une ligne d’interruption qui permet au MCP2515 de signaler à l’Arduino qu’un message vient d’arriver ou vient de partir, deux événements que l’Arduino doit prendre en charge. La connexion à l’Arduino est simple et est donnée à la table 1 et la figure 2 ci-dessous.
Broche du module | Nom de la broche Arduino | Broche Uno/Nano | Broche Mega |
---|---|---|---|
INT | - | 2 ou 3 | 2, 3, 18, 19, 20 ou 21 |
SCK | SCK | 13 | 52 |
SI | MOSI | 11 | 51 |
SO | MISO | 12 | 50 |
CS | SS | Au choix | Au choix |
GND | GND | GND | GND |
VCC | 5V | 5V | 5V |
Il faut noter que ce module CAN n’est pas adapté aux Arduino alimentés en 3,3V car le bus est alimenté par la même tension que le contrôleur et il est préférable d’avoir une tension de 5V sur le bus, d’autant plus que la documentation du TJA1050 recommande une tension comprise entre 4,75V et 5,25V.
- Figure 2 : Connexions entre l’Arduino Uno et le module CAN
- Ici, le CS est sur la broche 10 et INT sur la broche 2.
Pour ce premier article et pour les deux premiers programmes, un seul Uno et un seul module CAN suffisent car nous allons exploiter la capacité des contrôleurs CAN à recevoir ce qu’ils émettent, le loopback. Ceci permet de jouer avec le bus CAN sans déployer effectivement un réseau. En revanche, pour la dernière application de cet article ainsi que pour celles proposées dans les articles suivants, il vous faudra disposer de deux modules CAN et deux Arduino, voire 3 pour certains. Le bus en lui même est constitué de deux fils que l’on vissera dans le bornier du module en reliant les H aux H et les L aux L entre les modules CAN et en constituant une chaine. Les deux modules situés aux extrémités de cette chaine reçoivent un cavalier sur le connecteur J1 afin de mettre en œuvre les résistances de terminaison de 120Ω. Ces résistances servent à empêcher la réflexion du signal à l’extrémité de la ligne.
Démarrage rapide
Venons en a la bibliothèque ACAN, écrite par Pierre Molinaro, et plus précisément l’ACAN2515. La première étape est de l’installer. Comme elle est disponible dans le gestionnaire de bibliothèque, l’installation est très simple. Ouvrez le gestionnaire et tapez « ACAN » dans la case de recherche, identifiez celle pour le 2515 puis cliquez « installer ». On peut également la télécharger sur github.
Premier programme
Le premier programme ne nécessite donc qu’un seul Arduino Uno et un module MCP2515. Nous allons exploiter la capacité du MCP2515 de recevoir ce qu’il émet et par conséquent notre programme va donc à la fois émettre et recevoir. Le message que nous envoyons ne contient aucune donnée.
Tout d’abord il faut procéder à quelques déclarations en commençant par l’inclusion de la bibliothèque.
#include <ACAN2515Settings.h> /* Pour les reglages */
#include <ACAN2515.h> /* Pour le controleur */
#include <CANMessage.h> /* Pour les messages */
Il faut ensuite décider des broches pour le CS et l’INT. Ici nous avons choisi les broches 10 [2] et 2. Pour la broche INT, il est impératif qu’il s’agisse d’une broche d’interruption externe, la table 1 donne les broches valides.
static const uint8_t MCP2515_CS = 10;
static const uint8_t MCP2515_INT = 2;
Un objet de type ACAN2515 est instancié pour gérer le MCP2515, les arguments pour le construire sont la broche du CS, le SPI utilisé [3] et la broche INT.
ACAN2515 controleurCAN(MCP2515_CS, SPI, MCP2515_INT);
Tant qu’à faire, et comme il est toujours préférable de déclarer des constantes pour les valeurs numériques plutôt que de les noyer dans le code en dur, on déclare la fréquence du quartz du MCP2515, ici 8MHz et la fréquence du bus, ici 125kb/s. La fréquence du bus est choisie de manière parfaitement arbitraire et peut être différente sans excéder 512kb/s avec un quartz à 8MHz.
static const uint32_t FREQUENCE_DU_QUARTZ = 8ul * 1000ul * 1000ul;
static const uint32_t FREQUENCE_DU_BUS_CAN = 125ul * 1000ul;
Il y a ici une subtilité qui demande quelques explications.Pourquoi les constantes ci-dessus sont-elles suffixées parul
?Sur un Arduino à base d’AVR, donc un microcontrôleur 8 bits, les entiers font 16 bits, y compris lorsqu’ils sont spécifiés sous forme littérale. Par conséquent écrire125 * 1000
fait que le compilateur calcule cette multiplication sur 16 bits car 125 et 1000 sont tous deux des entiers 16 bits. Or 125000 est trop grand pour être codé sur 16 bits et la valeur serait fausse. Leul
indique au compilateur que ces constantes sont des entiers 32 bits non signés et le calcul est juste.
On pourra trouver curieux d’avoir besoin d’une fréquence pour un bus qui ne sert à rien puisque nous somme en loopback mais cette fréquence est nécessaire pour initialiser le contrôleur CAN.
Enfin, deux messages, l’un pour l’émission et l’autre pour la réception.
CANMessage messageCANEmission;
CANMessage messageCANReception;
C’est tout pour les déclarations. Passons maintenant à setup
où nous allons initialiser le MCP2515. Tout d’abord, il faut démarrer le SPI. En effet, la bibliothèque ACAN ne le fait pas elle même car le SPI peut être utilisé par plusieurs dispositifs et les bibliothèques conçus pour ces dispositifs feraient des initialisations redondantes du SPI. La connexion série est également démarrée pour pouvoir afficher des informations.
void setup()
{
SPI.begin();
Serial.begin(115200);
Ensuite, on construit un objet de type ACAN2515Settings
qui permet de fixer les paramètres de fonctionnement. Le constructeur de cet objet prend ici comme arguments la fréquence du quartz et la fréquence du bus. Appelons cet objet configuration
.
ACAN2515Settings configuration(FREQUENCE_DU_QUARTZ, FREQUENCE_DU_BUS_CAN);
Il faut ensuite se mettre en mode de fonctionnement loopback.
configuration.mRequestedMode = ACAN2515Settings::LoopBackMode;
Puis démarrer le contrôleur CAN. Le premier argument est la configuration que l’on vient de construire et le second une fonction qui prend en charge l’interruption du contrôleur. ACAN s’occupe presque de tout et cette fonction est toujours l’appel de la méthode isr()
du contrôleur. Ici nous utilisons une syntaxe particulière appelée closure qui permet d’insérer au vol le contenu de la fonction et de la passer comme argument.
const uint32_t codeErreur = controleurCAN.begin(configuration, [] { controleurCAN.isr(); });
codeErreur
sera différent de 0 si un problème est survenu. Il convient donc de se bloquer dans ce cas, ce que fait le while(1);
.
if (codeErreur != 0) {
Serial.print("Erreur : 0x");
Serial.println(codeErreur, HEX);
while (1);
}
else {
Serial.println("OK !");
}
}
Voilà, le CAN est prêt à fonctionner. Il faut ensuite, dans loop()
, l’exploiter. Commençons par traiter la réception.
void loop()
{
if (controleurCAN.receive(messageCANReception)) {
/* Message recu */
Serial.println("recu !");
}
receive
prend comme argument le message CAN destination. Il retourne true
si un message est arrivé et a été copié dans le message CAN passé en argument et false
sinon et, dans ce cas, le message passé en argument n’est pas modifié.
Enfin, l’envoi se fait toutes les 2 secondes, nous avons donc une variable permettant de mémoriser la date d’un envoi et une donnant un numéro d’ordre pour l’affichage.
static uint32_t dateDenvoi = 0;
static uint32_t compte = 0;
Puis l’envoi en lui-même.
uint32_t dateCourante = millis();
if (dateCourante - dateDenvoi >= 2000) {
if (controleurCAN.tryToSend(messageCANEmission)) {
Serial.print("envoi ");
Serial.println(compte++);
dateDenvoi = dateCourante;
}
}
}
tryToSend
effectue l’envoi du message et retourne true
en cas de succès. L’envoi peut échouer parce que la file d’attente d’émission est pleine. Dans ce cas, tryToSend
retourne false
.
Le code complet de ce premier programme est téléchargeable ci-dessous.
Second programme, ajoutons une donnée
Tout cela est très bien, mais le but de la communication est de véhiculer de l’information. Il faut donc pouvoir envoyer des messages comportant des données. Un message CAN peut inclure au maximum 8 octets de données.
La classe CANMessage
Nous avons jusqu’ici utilisé des objets de type CANMessage mais sans détailler ce que l’on y trouve. Cette classe regroupe tous les attributs d’un message :
-
id
: l’identifiant sur 11 ou 29 bits selon que le message est standard ou étendu.id
est initialisé à 0 ; -
ext
: un booléen qui est àfalse
pour une trame standard et àtrue
pour une trame étendue.ext
est initialisé àfalse
; -
rtr
: un booléen qui est àfalse
pour un message de données et àtrue
pour un message remote [4].rtr
est initialisé àfalse
; -
len
: un entier qui est le nombre d’octets de données du message.len
est initialisé à 0 ; -
data
: un tableau de 8 octets non signés, donc dedata[0]
àdata[7]
qui est également accessible en tant que tableau de 4 mots de 16 bits non signés viadata16[0]
àdata16[3]
, en temps que tableau de deux mots de 32 bits non signés viadata32[0]
etdata32[1]
et enfin en tant qu’entier de 64 bits non signé viadata64
. Tous les éléments du tableaudata
sont initialisés à 0.
Le programme avec loopback sans donnée est modifié pour envoyer la variable compte
. Tout d’abord, compte
est un entier de 32 bits, donc occupant 4 octets. La longueur du message messageCANEmission
est donc renseignées en conséquence à la fin de loop()
.
messageCANEmission.len = 4;
Il suffit ensuite, au moment de l’envoi du message de placer compte
dans data32[0]
, ligne 3 dans la portion de code qui suit.
uint32_t dateCourante = millis();
if (dateCourante - dateDenvoi >= 2000) {
messageCANEmission.data32[0] = compte;
if (controleurCAN.tryToSend(messageCANEmission)) {
Serial.print("envoi ");
Serial.println(compte++);
dateDenvoi = dateCourante;
}
}
En ce qui concerne la réception, il suffit de récupérer la donnée et la longueur et d’afficher le tout.
if (controleurCAN.receive(messageCANReception)) {
Serial.print("recu : ");
Serial.print(messageCANReception.data32[0]);
Serial.print(", longueur : ");
Serial.println(messageCANReception.len);
}
Le code complet est disponible en téléchargement ci-dessous.
Du concret
Terminé pour le loopback, passons à des choses plus concrètes. L’application que je vous propose d’examiner nécessite 2 Arduino et 2 modules CAN. Sur le premier Arduino nous mettons deux boutons poussoir et sur le second 2 LED. Un bouton est associé à une LED et appuyer sur le bouton change l’état de la LED correspondante via, bien entendu, un message CAN. La figure 3 donne les connexions à réaliser.
- Figure 3 : Connexion de l’émetteur et du récepteur via le bus CAN
- Les connecteurs J1 des modules CAN doivent recevoir un strap.
La base de départ est le programme 2 en séparant la réception de l’émission. L’émission prend place sur le premier Arduino et la réception sur le second. Par conséquent le CANMessage messageCANEmission;
se retrouve dans le programme de l’émetteur et le CANMessage messageCANReception;
se retrouve dans le programme du récepteur.
L’émetteur
Comme il s’agit de transmettre un numéro de LED, 0 ou 1, 1 octet de données suffira. Il nous faut également gérer nos deux boutons poussoir. Pour cela, nous utilisons la bibliothèque Bounce2 qui permet de mettre en œuvre un anti-rebond et de gérer les boutons simplement. Les deux boutons sont connectés sur les broches 3 et 4 et deux objets Bounce
. Le plus simple est d’utiliser des tableaux.
static const uint8_t NB_BOUTONS = 2;
static const uint8_t brocheBouton[NB_BOUTONS] = { 3, 4 };
Bounce poussoir[NB_BOUTONS];
Dans setup()
, les broches sont programmées et attachées aux objets Bounce
et la taille de données du message est fixée.
for (uint8_t bouton = 0; bouton < NB_BOUTONS; bouton++) {
pinMode(brocheBouton[bouton], INPUT_PULLUP);
poussoir[bouton].attach(brocheBouton[bouton]);
}
messageCANEmission.len = 1;
Dans loop()
, les deux boutons sont lus et si l’un d’entre eux ou les deux sont pressés, le message correspondant est envoyé.
for (uint8_t bouton = 0; bouton < NB_BOUTONS; bouton++) {
poussoir[bouton].update();
if (poussoir[bouton].fell()) {
messageCANEmission.data[0] = bouton;
const bool ok = controleurCAN.tryToSend(messageCANEmission);
if (ok) {
Serial.print("message ");
Serial.print(bouton);
Serial.println(" envoye !");
}
}
}
Le récepteur
Du côté du récepteur, il faut gérer 2 LED, ce qui est très simple. Tout d’abord les deux broches sont déclarées.
static const uint8_t NB_LED = 2;
const uint8_t brocheLED[NB_LED] = { 3, 4 };
Dans setup()
, les deux broches sont programmées en sortie.
for (uint8_t led = 0; led < NB_LED; led++) {
pinMode(brocheLED[led], OUTPUT);
}
Enfin, dans loop()
le message est reçu, la donnée est extraite et l’état de la LED correspondante est changé.
if (controleurCAN.receive(messageCANReception)) {
/* Un message CAN est arrive */
static uint32_t numero = 0;
controleurCAN.receive(messageCANReception) ;
Serial.print("Recepteur: ");
Serial.print(++numero);
Serial.print(" message recu : ") ;
Serial.println(messageCANReception.data[0]);
/* l'etat de la LED correspondante est change */
const uint8_t led = messageCANReception.data[0];
if (led < NB_LED) {
digitalWrite(brocheLED[led], !digitalRead(brocheLED[led]));
}
else {
Serial.println("donnee incorrecte");
}
}
Les deux programmes sont téléchargeables ci-dessous.
La prochaine fois nous verrons l’usage des identifiants et des filtres sur des applications de commande de dispositifs, LEDs, servo-moteurs, via le bus CAN.
[1] tapez MCP2515 TJA1050 pour trouver les revendeurs
[2] Il faut noter que sur les Uno/Nano, cette broche doit être programmée en sortie pour que le SPI soit en mode maître, ce qui est requis ici, c’est donc la broche à utiliser de manière privilégiée pour le CS du SPI.
[3] Certaines carte Arduino ou compatibles disposent de plusieurs SPI qui s’appellent alors SPI, SPI1, SPI2, etc. Sur le Uno, il n’y en a qu’un seul, SPI.
[4] Nous verrons les messages remote dans un prochain article.