Mise en place d'un bus CAN
En cours de rédaction
Sommaire
Présentation et objectifs
L'objectif de ce tutoriel est de mettre en place un bus CAN entre plusieurs carte STM32 de catégorie NUCLEO-L4, grâce à l'environnement Atollic et les fonctions HAL. Le contexte ( boutons, LEDS, écran ) n'est qu'une manière d'illustrer le fonctionnement du bus CAN, et vous pouvez vous en passer si ce qui vous intéresse concerne uniquement le bus CAN. Si vous souhaitez voir une implémentation du bus CAN sur des cartes Arduino, vous pouvez en trouver ici.
Vous pouvez rajouter dans ce paragraphe des photos des croquis papier que vous avez fait pour mieux visualiser ce qu'il y avait à faire.
Pré-requis
Aucune compétence en programmation n'est requise pour mener à bien ce tutoriel, mais sera peut être nécessaire pour une implémentation du bus plus poussée.
Matériel
- 3 cartes NUCLEO-L4 ( Je me suis servi des modèles L476RG et L432KC)
- 3 drivers CAN MCP2551
- 2 résistances de 120 Ohms
Et si vous souhaitez le tester visuellement :
- des écrans OLED-091 sous ssd1306
Logiciels
- STM32CubeMX
- AtollicTRUESTUDIO
Tutoriel CAN
La version courte servira uniquement à mettre en place le bus CAN sans aucune implémentation autour. L'autre version proposera un petit contexte afin de mieux visualiser la connexion en CAN enter les cartes, et sera a effectuer en complément de la version courte.
Version courte : Bus CAN uniquement
Étape 1 : Configurer l'environnement de travail
Ouvrez STM32CubeMX et créez un nouveau projet. Dans l'onglet Board Selector, sélectionnez votre type de carte et son modèle (qui devra être STM32L4 pour ce tuto). Je me suis servi pour ma part du modèle Nucleo32 STM32L432KC et du modèle Nucleo64 STML476RG. Cliquez sur Start Project et acceptez que le projet soit initialisé par défaut.
Une fois dans le projet, dans l'onglet Pinout, ouvrez le menu CAN1 et sélectionnez Master Mode. Notez sur le schéma central le nom des Pins qui viennent d'apparaitre qui correspondent à CAN1_TX et CAN1_RX, ce sont ces pins dont vous devrez vous servir pour effectuer le branchement. Vérifiez ensuite dans le menu RCC, toujours dans l'onglet Pinout, que le LSE est sur la valeur Crystal/Ceramic Resonator.
Dans l'onglet Clock Configuration, entrez le nombre 48 dans l'encadré entouré de bleu nommé HCLK et appuyez sur Entrée, ce qui devrait changer la valeur de tous les autres encadrés sur 48 également.
Dans l'onglet Configuration, cliquez sur CAN1. Dans l'onglet Parameter Settings, mettez le "Prescaler" à 12, le "Time Quanta in Bit Segment 1" à 13, le "Time Quanta in Bit Segment 1" à 2 et le "ReSynchronization Jump Width" à 1. Le "Time Quantum" devrait se mettre à 250.0ns. Toujours dans cette fenêtre de configuration, dans l'onglet NVIC Settings, cochez toutes les cases pour autoriser les interruptions. Acceptez les modifications.
Vous pouvez maintenant aller le menu déroulant Project, puis Settings. Donnez un nom à votre projet et une location dédiée à vos projets que vous pourrez facilement retrouver sous Atollic. Dans le menu déroulant Toolchain / IDE, sélectionnez TrueSTUDIO. Acceptez les modifications.
Dans le menu déroulant Project, vous pouvez maintenant générer le code en cliquant sur Generate Code. Étant donné que toutes vos cartes qui serviront au bus CAN seront configurées de la même manière, vous pouvez pour plus de clarté en générer plusieurs fois le même code pour chaque carte ( à part si le modèle est différent). Sinon, vous pouvez aussi envoyer le même code dans les cartes différentes en changeant quelques ligne de code à chaque fois.
Étape 2 : Effectuer le branchement
Le montage à réaliser est le suivant :
Explications:
Les drivers CAN doivent tous être connectés entre eux via leur canaux CANH et CANL ( broches 6 et 7), c'est ce qui va constituer le bus.
La résistance de 10k sur la broche 8 (RS) est conseillée, mais le bus peux fonctionner simplement en la raccordant à la masse.
La broche 2 (VSS) doit être reliée à la masse et la broche et la broche 3 (VDD) au 5V.
La broche 1 doit être connectée à la pin CAN1_TX, et la broche 4 au CAN1_RX. Ces pins ont été configurées à la première étape dans le logiciel STM32CubeMX et pourront donc être différentes de ce schéma.
Les deux drivers en bout de ligne doivent comporter une résistance de 120 Ohms entre leurs broches CANH et CANL.
Étape 3 : Écrire le programme
Ouvrez Atollic, et sélectionnez comme workspace le dossier ou vous avez mit le projet STM32CubeMX s'il vous est demandé ou bien importez le manuellement dans le Project Explorer s'il n'y est pas. Dans le dossier Src du projet, ouvrez le fichier main.c. Vous pouvez des à présent tester avant de rajouter du code si le projet est bien configuré en le transversant dans la carte et en vérifiant qu'il n'y ai pas d'erreur de compilation ou d’exécution (A l'aide de l'outil Debug).
Vérifiez la présence de la déclaration de la variable can dans la partie Private variables de la forme
CAN_HandleTypeDef hcan1;
Nous nous servirons de cette variable dans toutes les configurations futures. Ajoutez y à la suite les variables suivantes :
CAN_TxHeaderTypeDef TxHeader; CAN_RxHeaderTypeDef RxHeader; uint8_t TxData[8]; uint8_t RxData[8]; uint32_t TxMailbox;
Créez ensuite une fonction CAN_Config() qu'il faudra appeler ensuite dans la fonction principale main() juste après l'initialisation du CAN. Dans cette fonction, nous allons commencer pour configurer le filtre :
CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x320 << 5; // Ici, 320 est l'adresse de la carte. Il peux être différent pour chaque carte. sFilterConfig.FilterIdLow = 0; sFilterConfig.FilterMaskIdHigh = 0xFFF << 5; // Le masque peux servir à accepter une plage d'adresse au lieu d'une adresse unique. sFilterConfig.FilterMaskIdLow = 0; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; sFilterConfig.SlaveStartFilterBank = 14; HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig); // Configure le filtre comme ci-dessus
Le filtrage est une notion importante du bus CAN car c'est ce qui décide quelles informations transitant sur le bus CAN un périphérique doit traiter ou non. Je vous met un lien ici de quelques exemples qui m'ont aider à mieux comprendre son fonctionnement.
A la suite, on peux ajouter :
HAL_CAN_Start(&hcan1); // Démarre le périphérique CAN HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); // Active le mode interruption
Nous reviendrons un peu plus tard sur la fonction d’interruption, et sur la possibilité de passer en mode "polling" ( Il faudra pour cela supprimer cette ligne).
Les prochaines lignes concernent la configuration de l'envoi de trames sur le bus.
TxHeader.StdId = 0x321; // Détermine l'adresse du périphérique au quel la trame est destiné. // Si plusieurs périphériques sur le bus comprennent cette adresse dans leur filtre, ils recevront tous la trame. TxHeader.ExtId = 0x01; // Adresse étendue, non utilisée dans note cas TxHeader.RTR = CAN_RTR_DATA; // Précise que la trame contient des données TxHeader.IDE = CAN_ID_STD; // Précise que la trame est de type Standard TxHeader.DLC = 2; // Précise le nombre d'octets de données que la trame transporte ( De 0 à 8 ) TxHeader.TransmitGlobalTime = DISABLE;
Vous pouvez également rajouter dans cette fonction la valeur des données envoyées si elles sont fixes, ou si vous en souhaitez par défaut, grâce à ligne suivante :
TxData[0] = valeur; // Vous pouvez changer toutes les valeurs de Txdata[0] à Txdata[TxHeader.DLC - 1] (TxHeader.DLC étant défini ci dessus)
La fonction CAN_Config est terminée, en voici un rappel complet :
void CAN_Config(void) { CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x320 << 5; sFilterConfig.FilterIdLow = 0; sFilterConfig.FilterMaskIdHigh = 0xFFF << 5; sFilterConfig.FilterMaskIdLow = 0; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; sFilterConfig.SlaveStartFilterBank = 14; HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig); HAL_CAN_Start(&hcan1); HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); TxHeader.StdId = 0x320; TxHeader.ExtId = 0x01; TxHeader.RTR = CAN_RTR_DATA; TxHeader.IDE = CAN_ID_STD; TxHeader.DLC = 2; TxHeader.TransmitGlobalTime = DISABLE; }
N'oubliez pas d'ajouter l'appel de cette fonction dans la fonction main(), après l'initialisation des périphériques, à cet endroit :
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); MX_CAN1_Init(); /* USER CODE BEGIN 2 */ CAN_Config();
Nous pouvons maintenant passer à la configuration de la réception des trames. Il y a pour cela deux façons de procéder, en interruption ou en "polling".
Si vous souhaitez fonctionner par interruption ( et que vous l'avez bien ajouté dans la fonction CAN_Config()), il faudra définir la fonction suivante :
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData); }
Vous pouvez ajouter dans cette fonction, après la première ligne, des vérifications sur la trame reçue et le traitement que vous voulez en faire. Par exemple :
if (RxHeader.IDE == CAN_ID_STD && RxHeader.DLC == 1 && RxData[0] == 4) { // Traitement des données }
Vous avez accès à toutes les informations que vous pouvez vous même définir dans les envois de trame.
Si vous souhaitez fonctionner en "polling", vous devez retirer la ligne de code qui active les notifications dans la fonction CAN_Config(), et vous ne devez pas définir la fonction précédente. Il faudra en revanche constamment vérifier dans votre boucle de la fonction main() (dans le while(1)) si un message est arrivé ou non, grâce à la fonction suivante :
HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0);
Cette fonction renvoie une valeur entre 0 et 3, qui correspond au nombre de trames reçues en attente. Vous pouvez donc ajouter une condition qui ne sera vraie que si la valeur de retour est strictement positive, et le contenu de cette condition sera identique au contenu dans la fonction d'interruption précédemment décrite. Voici un exemple :
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { if(HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0) > 0) { HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &RxHeader, RxData); if (RxHeader.IDE == CAN_ID_STD && RxHeader.DLC == 1 && RxData[0] == 4) { // Traitement des données } } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
Attention, en mode "polling", si plus de 3 trames sont reçues sans être traitées,les plus anciennes seront effacées pour laisser de la place aux nouvelles, et donc perdues.
Enfin, pour envoyer une trame selon les configurations décrites dans la fonction CAN_Config(), il faut faire appel à cette fonction :
HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox);
Afin que le programme soit plus flexible, on peut définir une fonction qui se chargera de l'envoi en prenant en paramètre l'adresse et les données de la trame :
uint8_t CAN_Transmit(uint32_t addr, uint32_t data_size, uint8_t * tab_data) { if(data_size > 8 || sizeof(tab_data) / sizeof(tab_data[0]) <= data_size) return 0; TxHeader.StdId = addr; TxHeader.DLC = data_size; for(int i = 0; i < data_size; i++) { TxData[i] = tab_data[i]; } HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox); return 1; }
Le programme est maintenant prêt à être téléversé dans les différentes cartes qui composent le bus CAN. Pour cela, il suffit de connecter une carte au PC et de cliquer sur l’icône Debug pour téléverser le programme dans la carte. Vous pouvez commencer votre propre implémentation du bus ou suivre la version longue du tutoriel pour donner un contexte au bus CAN et le voir fonctionner. Si vous rencontrez des problèmes à l'exécution du programme, rendez vous dans la partir Conseils du tutoriel ou je reviens sur plusieurs points qui peuvent être problématiques.
Version longue : Bus CAN dans un contexte simple
Étape 1 : Configurer l'environnement de travail
Sur le logiciel STM32CubeMX, refaire toutes les étapes de la version courte. Avant de générer le code, il faut ajouter le mode I2C. Dans l'onglet Pinout, ouvrez le menu I2C1 et sélectionnez I2C dans le menu déroulant. Notez les deux nouvelles pins apparues sur la puce au centre nommées I2C1_SDA et I2C1_SCL, il faudra les brancher à l'écran à la deuxième étape. Vous pouvez maintenant générer le code et ouvrir le projet sous Atollic pour préparer la troisième étape.
Étape 2 : Effectuer le branchement
Quatre pins sont présentes sur l'écran OLED. GND doit être relié à une pin GND de la carte, VCC doit être relié au 3V3, puis SDA et SCL doivent être reliées aux pins I2C1_SDA et I2C1_SCL correspondants sur la carte définies à l'étape précédente.
Étape 3 : Écrire le programme
Pour faire fonctionner cet écran, il faut inclure quelques fichiers dans le projet. Ouvrez votre dossier où est situé le projet. Dans le dossier "Src", déposez les fichiers fonts.c et ssd1306.c. Dans le dossier "Inc", déposez les fichiers fonts.h et ssd1306.h.
Sous Atollic, dans le fichier main.c, il faut tout d'abord inclure les fichiers header :
#include "ssd1306.h" #include "fonts.h"
Dans la fonction main, placez vous juste après l'initialisation de l'I2C, avant la boucle while(1). Il faut initialiser l'écran puis choisir la couleur du fond. Avant ça il est préférable de placer un délai d'au moins 2 secondes pour être sur que l'écran ai bien eu le temps de démarrer avant de l'initialiser, comme suit :
HAL_Delay(2000); ssd1306_Init(); ssd1306_Fill(Black); // White pour mettre un fond blanc ssd1306_UpdateScreen();
Ensuite, pour écrire sur l'écran il y a le choix. Vous pouvez écrire en noir ou blanc, et choisir entre trois tailles de police. Je déconseille la première, Font_7x10, très peu lisible. La deuxième, Font_11x18, peut être affichée sur trois lignes tandis que la plus grande, Font_16x26, ne peux être affichée que sur deux lignes. Il y a également une limite de caractères en longueur, qui est de 11 pour la Font_11x18 et 7 pour la Font_16x26. Pour changer de ligne, il faut placer le curseur en le décalant de quelques pixels vers le bas, idéalement quelques pixels de plus que la taille de police utilisée. A titre d'exemple, je décale de 20 pixels par ligne pour la Font_11x18 et de 30 pixels par ligne pour la Font_16x26. Voici un exemple de code pour écrire trois lignes sur l'écran :
ssd1306_SetCursor(0,0); ssd1306_WriteString(" LAB AIX ",Font_11x18,White); ssd1306_SetCursor(0,20); ssd1306_WriteString(" BIDOUILLE ",Font_11x18,White); ssd1306_SetCursor(0,40); ssd1306_WriteString("ECRAN OLED",Font_11x18,White); ssd1306_UpdateScreen();
Pour afficher un donnée qui a été reçue en CAN, vous pouvez utiliser la fonction sprintf() dont je parle dans les conseils en fin de Tutoriel.
sprintf(buffer,"Data : %d ", RxData[0]); // Remplacer 0 par le numéro de l'octet qui contient l'information à afficher ssd1306_SetCursor(0,40); // Placer sur la ligne de son choix ssd1306_WriteString(buffer,Font_11x18,White);// Choisir la taille et la couleur de la police ssd1306_UpdateScreen();
En plaçant ce morceau de code à l'endroit où on reçoit les données ( différent selon le mode par interruption ou polling ) les données s'affichent sur l'écran.
Conseils
- Changer les pins par défaut sur STM32CubeMX
- Pour des raisons pratiques, il est parfois possible de changer les pins attribuées par défaut par le logiciel. Attention toutefois, les pins ne peuvent pas servir à toutes les fonctions, mais souvent un rôle peut être rempli par au moins deux pins différentes. En cliquant sur une pin donnée, vous pouvez sélectionner son rôle manuellement. Pour ne pas avoir a chercher, par exemple, quelle autre pin est capable de gérer le CAN_TX, il suffit de rester appuyé sur Ctrl et cliquer sur la pin qui occupe déjà cette fonction. Toutes les pins qui sont capables de gérer cette fonction vont s'allumer en bleu, et il suffira de la sélectionner pour changer la fonction d'emplacement sur le microcontrôleur.
- Version de STM32CubeMX
- Je vous recommande fortement d'utiliser la dernière mise à jour du logiciel. Certaines versions, comme la 4.25.0, génèrent un code qui comporte des erreurs, notamment dans les fonctions de configuration de l'horloge interne.
- Une seule carte à la fois
- Vous serez amené dans ce tutoriel à téléverser des programmes sur plusieurs cartes. Le problème est que l'IDE Atollic par défaut ne sait pas sur quelle carte envoyer le programme si plusieurs sont branchées au PC. Le moyen le plus simple est de ne laisser que la carte où l'on souhaite envoyer les données branchée au PC pendant le téléversement. Cependant, si vous ne souhaitez pas avoir à débrancher et rebrancher sans arrêt les cartes, il y a un moyen de dire au debugger d'un projet sur quel carte il devra envoyer son programme, par contre, il ne se lancera pas du tout si cette carte précise n'est pas branchée au PC. Pour affilier une carte spécifique à un projet Atollic, il faut connaitre le numéro de série de la carte. On peux la trouver lorsque la carte est connectée au PC dans le Gestionnaire de Périphériques, dans le menu déroulant Universal Serial Bus devices, faite un clic droit sur le périphérique STLink et cliquez sur Propriétés. Ensuite, dans l'onglet Détails, il faut choisir dans le menu déroulant Propriété la valeur Parent. Vous devriez trouver une longue chaine de caractère, dont le numéro de série qui correspond à tous les caractère après le dernier anti-slash. Par exemple, dans "USB\VID_0483&PID_374B\0673FF504952857567165020", seule la partie soulignée correspond au numéro de série.
- Retours de fonctions
- La plupart des fonctions HAL utilisées dans ce tutoriel renvoient une valeur pour avertir si son exécution s'est bien déroulé ou non. Lorsque tout se passe comme prévu, ces fonctions renvoient la macro HAL_OK, et sinon elles ont des codes d'erreurs propres qu'ils faudra vérifier dans la documentation ou les fichiers header. Voici un exemple de code :
if(HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox) != HAL_OK) { // Error Handler }
- Ecriture sur le port série
- L'IDE Atollic comporte un debugger très puissant qui permet de mettre des points d'arrêt et de regarder l'état du programme en tout temps. Cependant, il peux être assez complexe à utiliser et le plus simple peux parfois rester d'envoyer des messages sur le port série pour récupérer certaines informations pendant l’exécution du programme. Pour cela, on peux créer une petite fonction qui assurera l'écriture sur le porte Série :
UART_HandleTypeDef huart2; // D'abord vérifier que cette variable est déclarée par défaut au début du fichier // Fonction à écrire dans la partie /* USER CODE BEGIN 0 */ void log(char* str) { HAL_UART_Transmit(&huart2, str, strlen(str), 1000); } log("Hello World"); // L'appel se fait simplement de cette manière.
Pour une valeur autre qu'une chaine de caractère, il faut utiliser une fonction pour la transformer pour l'envoyer sur le port série. Regardez la syntaxe de spécification de format sur ce lien
uint8_t buffer[100]; int val = 3; sprintf(buffer,"%d", val); log(buffer);
Vérifiez aussi dans la fonction de configuration de l'UART en fin de fichier de vous mettre sur le même Baud Rate que votre terminal sur PC, et que le "huart2.Init.WordLength" est sur "UART_WORDLENGTH_8B".
- La collision de données sur le bus
- Il faut faire attention sur les adresses des trames envoyées sur le bus simultanément. La procédure qui permet au bus CAN de prioriser des trames qui partent en même temps et de donner la priorité à l'adresse la plus petite. Pour cette raison, envoyer deux trames de même adresses simultanément peux rendre le bus instable.
- Analyseur de trames
- Si le programme a l'air correct mais que aucune donnée ne transite sur le bus, un des moyens de vérifier ce qui se passe physiquement est de placer un analyseur logique sur le circuit. Par exemple, les produits de IKALOGIC permettent de placer des sondes sur le circuit et récupérer les données par USB sur le logiciel Scana Studio. En précisant que les trames doivent suivre le protocole CAN, le logiciel pourra indiquer non seulement les valeurs que contiennent les trames, mais aussi la vitesse de la trame. Cette notion de vitesse est très importante, car si un périphérique attends une trame à une certaine vitesse, il ne recevra pas une trame à une vitesse différente.
Pour aller plus loin
Vous pouvez suivre la mise en œuvre d'un Démonstrateur du bus CAN réalisé au Lab.
Bibliographie
- Doc HAL
- une liste