CANopen sous LabVIEW avec un Raspberry Pi : NMT, SDO, PDO et variateur DS402

Image de couverture : CANopen sous LabVIEW avec un Raspberry Pi : NMT, SDO, PDO et variateur DS402

Écrit par

dans

Un Raspberry Pi, une interface CAN à quelques euros et le runtime LabVIEW suffisent pour piloter un variateur de moteur au profil CANopen DS402. Cet article décrit la chaîne complète : configuration du bus sous Linux, passerelle entre LabVIEW et les sockets du noyau, puis deux façons de parler CANopen, l’une en s’appuyant sur la pile CANopenNode, l’autre en construisant soi-même les trames NMT, SDO et PDO dans des classes LabVIEW.

Pour décoder les trames citées au fil du texte ou en construire de nouvelles, le décodeur de trames CANopen fait le travail dans le navigateur.

L’architecture

  • Le Raspberry Pi exécute le programme LabVIEW grâce à LINX, qui installe le moteur d’exécution LabVIEW dans un chroot (voir Le Raspberry Pi se propulse vers LabVIEW 2019).
  • L’interface CAN est vue par Linux comme une interface réseau SocketCAN, can0 : carte d’extension à contrôleur MCP2515 sur le bus SPI ou adaptateur USB reconnu par le noyau.
  • Un démon fait le lien entre le bus et une socket Unix qui accepte des commandes texte.
  • Une petite bibliothèque C, appelée par un nœud d’appel de bibliothèque (Call Library Function Node), permet à LabVIEW d’ouvrir cette socket, d’écrire une commande et de lire la réponse.

LabVIEW ne voit donc jamais le bus directement : il échange des chaînes de caractères avec un processus qui, lui, dispose de toute l’API SocketCAN. Cette séparation simplifie énormément le code LabVIEW et permet de déboguer le bus avec les outils Linux habituels pendant que l’application tourne.

Configurer l’interface can0

Le débit doit être identique sur tous les nœuds du bus ; ici 1 Mbit/s, le maximum de CAN classique. Le paramètre restart-ms relance automatiquement le contrôleur après un bus-off, faute de quoi l’interface reste muette jusqu’au prochain redémarrage. Pour régler les registres de temps de bit d’un microcontrôleur à l’autre bout du câble, le calculateur de débit CAN donne les valeurs de prédiviseur et de segments.

# /etc/network/interfaces.d/can0 : interface montée au démarrage, 1 Mbit/s
auto can0
iface can0 can static
    bitrate 1000000

# équivalent à la main (utile pour essayer un autre débit)
sudo ip link set can0 down
sudo ip link set can0 type can bitrate 1000000 restart-ms 100
sudo ip link set can0 up

# observer le bus (paquet can-utils)
candump -td can0
# envoyer une trame : NMT « start » vers tous les nœuds
cansend can0 000#0100

La bibliothèque partagée pour LabVIEW

LabVIEW sait parler TCP et UDP, mais pas aux sockets Unix. Quatre fonctions C suffisent : Open, Close, Write et Read. Le point important est la compilation : le runtime LabVIEW tournant dans un chroot, la bibliothèque doit être compilée dans ce chroot (sudo schroot --run-session -c lv, puis installation des outils de compilation avec opkg) et copiée dans son /usr/lib. Dans le nœud d’appel, le nom seul de la bibliothèque suffit alors, et l’option « exécuter dans n’importe quel thread » évite de bloquer le thread d’interface pendant une lecture.

/* unix_socket.c : accès à une socket Unix (AF_UNIX) depuis LabVIEW,
 * via un nœud d'appel de bibliothèque (Call Library Function Node).
 * Compilation dans le chroot du runtime LabVIEW :
 *   gcc -shared -fPIC -o libunixsocket.so unix_socket.c
 *   puis copie dans /usr/lib du chroot (/srv/chroot/labview/usr/lib vu de l'extérieur)
 */
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

/* Ouvre une connexion sur la socket ; renvoie le descripteur ou une valeur négative */
int Open(const char *socketPath)
{
    struct sockaddr_un addr;
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1)
        return -1;

    memset(&addr, 0, sizeof addr);
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, socketPath, sizeof addr.sun_path - 1);

    if (connect(fd, (struct sockaddr *)&addr, sizeof addr) == -1) {
        close(fd);
        return -2;
    }
    return fd;
}

int Close(int fd)
{
    return close(fd);
}

/* Écrit une commande texte ; 0 si tout est parti */
int Write(int fd, const char *command, size_t length)
{
    return write(fd, command, length) == (ssize_t)length ? 0 : -1;
}

/* Lit la réponse (appel bloquant) ; renvoie le nombre d'octets reçus ou -1 */
int Read(int fd, char *replyBuf, int bufSize)
{
    ssize_t n = read(fd, replyBuf, (size_t)bufSize);
    return n < 0 ? -1 : (int)n;
}

/* Commande puis réponse en un seul appel */
int SendCommand(int fd, const char *command, size_t length, char *replyBuf, int bufSize)
{
    if (Write(fd, command, length) != 0)
        return -1;
    return Read(fd, replyBuf, bufSize);
}

CANopen en quelques notions

CANopen ajoute au bus CAN un adressage et des services. Chaque nœud porte un numéro de 1 à 127 ; l’identifiant d’une trame (le COB-ID) combine un code de fonction sur 4 bits et ce numéro : COB-ID = code × 0x80 + nœud. Le tableau ci-dessous reprend la répartition par défaut des identifiants.

ServiceCOB-IDRôle
NMT0x000Commandes de gestion du réseau envoyées par le maître (démarrer, arrêter, réinitialiser)
SYNC0x080Top de synchronisation qui déclenche les PDO synchrones
EMCY0x080 + nœudMessage d’urgence émis par un nœud en défaut
TPDO1 à 40x180, 0x280, 0x380, 0x480 + nœudDonnées de process émises par le nœud
RPDO1 à 40x200, 0x300, 0x400, 0x500 + nœudDonnées de process reçues par le nœud
SDO0x580 + nœud (réponse), 0x600 + nœud (requête)Lecture et écriture du dictionnaire d’objets
Heartbeat0x700 + nœudÉtat NMT du nœud, émis périodiquement
  • Le dictionnaire d’objets rassemble tous les paramètres d’un nœud, repérés par un index sur 16 bits et un sous-index sur 8 bits : 0x1017 pour la période du heartbeat, 0x6040 pour le mot de contrôle d’un variateur, 0x6064 pour sa position…
  • Les SDO lisent ou écrivent une entrée du dictionnaire, avec accusé de réception. Lents mais sûrs, ils servent au paramétrage.
  • Les PDO transportent jusqu’à 8 octets de données sans protocole, sur un identifiant dédié. Ils ne circulent que lorsque le nœud est en état Operational. Attention au point de vue : un RPDO est reçu par le nœud, donc émis par le maître.
  • NMT fait passer les nœuds de Pre-operational (après le démarrage) à Operational ou Stopped, et le heartbeat permet au maître de vérifier qu’ils sont toujours là.

Première approche : la pile CANopenNode

Le projet libre CANopenNode fournit, avec canopend, un nœud CANopen complet qui s’exécute sous Linux et peut jouer le rôle de maître. Lancé avec l’option -c, il ouvre une socket Unix de commandes : on lui envoie des lignes de texte, il se charge des trames, des délais et des transferts segmentés.

# LaunchCanOpen.sh : démarre la pile CANopenNode (maître) et sa passerelle de commandes,
# puis affiche le trafic du bus dans le terminal
CANopenSocket/canopend/app/canopend can0 -c $HOME/canopen.socket &
candump can0

# Depuis un autre terminal, la même socket accepte des commandes texte :
#   ./canopencomm [1] 4 read 0x1017 0 u16          -> lecture SDO du heartbeat du nœud 4
#   ./canopencomm [2] 4 write 0x1017 0 u16 1000    -> écriture SDO
#   ./canopencomm [3] 4 start                      -> NMT : passage en Operational

Côté LabVIEW, une classe encapsule la référence de la socket et propose une méthode par commande : elle formate la ligne ([séquence] nœud read index sous-index type), l’envoie, lit la réponse et la convertit. Une réponse commence par le même numéro de séquence suivi de la valeur ou de OK, ou d’un message ERROR: contenant le code d’abandon SDO. L’approche est rapide à mettre en place et très robuste ; elle convient bien au paramétrage et à la supervision. Elle devient moins naturelle dès qu’il faut produire des PDO à cadence fixe ou réagir à chaque trame reçue.

Seconde approche : construire les trames dans LabVIEW

La seconde approche ne délègue au démon que l’accès au bus, avec le protocole texte de socketcand. Une trame s’envoie avec send, une émission périodique se programme avec add et se met à jour avec update, et les trames reçues arrivent sous la forme < frame … >. Tout le reste, c’est-à-dire le CANopen, est écrit en LabVIEW, dans une bibliothèque de classes : NMT, SDO, SYNC, RPDO, TPDO et une classe DS402 pour le variateur.

# Protocole texte de socketcand (mode « BCM ») : une commande entre < >
< open can0 >                              # rattache la connexion à l'interface
< add 0 100000 201 2 0F 00 >               # émission cyclique : 0 s + 100 000 µs, ID 0x201, 2 octets
< update 0 100000 201 2 06 00 >            # change le contenu de la trame cyclique
< delete 201 >                             # arrête l'émission cyclique
< send 601 8 40 41 60 00 00 00 00 00 >     # trame unique : lecture SDO de 0x6041 sur le nœud 1
< subscribe 0 0 181 >                      # reçoit les trames 0x181 (TPDO1 du nœud 1)
# réponse reçue : < frame 181 1697551234.123456 37 02 >

L’émission cyclique est le point fort de cette méthode : c’est le noyau Linux (le gestionnaire de diffusion, ou BCM) qui envoie la trame à la période demandée, avec une régularité qu’une boucle LabVIEW sur Raspberry Pi ne garantit pas. Le VI ci-dessous convertit la période en secondes et microsecondes, met en forme l’identifiant et les octets en hexadécimal, puis écrit la commande sur la socket.

Ajout d’une trame cyclique (commande socketcand « add ») sous LabVIEW
Programmation d’une trame cyclique : la période est découpée en secondes et microsecondes, puis la commande < add … > est écrite sur la socket.

Lire un objet par SDO

Une lecture SDO dite expédiée tient en deux trames. La requête, envoyée sur 0x600 + nœud, contient l’octet de commande 0x40, l’index en petit-boutiste (poids faible d’abord), le sous-index et quatre octets à zéro. Le nœud répond sur 0x580 + nœud avec 0x4F, 0x4B, 0x47 ou 0x43 selon que la donnée fait 1, 2, 3 ou 4 octets, suivis de la donnée, elle aussi poids faible en premier. Une réponse 0x80 signale un abandon, avec un code d’erreur sur 32 bits.

Dans le VI, la trame est assemblée à partir de l’index et du sous-index, envoyée, puis la réponse est décodée : les bits 2 et 3 de l’octet de commande donnent le nombre d’octets inutilisés, et une boucle recompose la valeur en décalant chaque octet de 8 bits.

Lecture SDO expédiée : construction de la requête et assemblage de la réponse sous LabVIEW
Lecture SDO : requête 0x40 + index + sous-index, puis décodage de la longueur et de la donnée petit-boutiste.

Exemple, pour lire la période de heartbeat (0x1017) du nœud 4 : la requête est 604#4017100000000000 et la réponse 584#4B171000E8030000, soit 0x03E8 = 1000 ms. L’onglet « Construire un SDO » du décodeur CANopen produit ces trames pour n’importe quel objet.

Piloter un variateur DS402

Le profil DS402 normalise le comportement des variateurs : quelle que soit la marque, le mot de contrôle (0x6040) fait évoluer une machine d’états et le mot d’état (0x6041) indique où l’on se trouve. Pour mettre un moteur sous couple, la séquence est toujours la même : Shutdown (0x0006) vers Ready to switch on, Switch on (0x0007) vers Switched on, puis Enable operation (0x000F) vers Operation enabled. Après un défaut, un front montant sur le bit 7 (0x0080) acquitte l’erreur.

L’état se lit en comparant certains bits du mot d’état à un tableau de référence : pour chaque état, un masque garde les bits significatifs et une valeur indique ce qu’ils doivent valoir. Le VI applique les huit masques d’un coup et cherche la première correspondance.

Décodage du mot d’état DS402 par masques et valeurs sous LabVIEW
Décodage du mot d’état : un ET logique avec chaque masque, une comparaison avec la valeur attendue, puis la conversion de l’index trouvé en état.
ÉtatMasqueValeurExemple de mot d’état
Not ready to switch on0x4F0x000x0000
Switch on disabled0x4F0x400x0250
Ready to switch on0x6F0x210x0231
Switched on0x6F0x230x0233
Operation enabled0x6F0x270x0237
Quick stop active0x6F0x070x0217
Fault reaction active0x4F0x0F0x021F
Fault0x4F0x080x0218

L’onglet DS402 du décodeur CANopen affiche la machine d’états complète : un clic sur un état ou une transition charge le mot d’état et le mot de contrôle correspondants.

Le programme de test

Le VI de test réunit toutes les briques. À gauche, il crée les objets : variateur DS402, NMT, SYNC, un RPDO pour recevoir le mot d’état et un TPDO pour envoyer le mot de contrôle. Le TPDO est démarré en émission cyclique à 100 ms, si bien que la boucle principale n’a plus qu’à mettre à jour son contenu. Dans la boucle, les SDO lisent le mode de fonctionnement, la position et la vitesse, écrivent le mode demandé et la consigne de position ; le RPDO reçu est décodé pour afficher l’état du variateur. À droite, tout est arrêté et détruit proprement, et les erreurs sont fusionnées.

Diagramme de test : maître CANopen et variateur DS402 sous LabVIEW
Programme de test : création des objets CANopen, mot de contrôle en PDO cyclique, mot d’état reçu en PDO, paramètres et consignes par SDO.

Les pièges classiques

  • Aucune trame ne passe : vérifier les deux résistances de terminaison de 120 Ω (60 Ω mesurés entre CAN_H et CAN_L, alimentation coupée) et le débit de chaque nœud. Un seul nœud au mauvais débit suffit à générer des erreurs pour tout le monde.
  • Les PDO restent muets : le nœud est probablement encore en Pre-operational. Envoyer la commande NMT Start (000#0100 pour tous les nœuds) et vérifier le heartbeat, qui doit passer de 0x7F à 0x05.
  • Le variateur refuse de passer en Operation enabled : la séquence doit être respectée pas à pas, et un défaut actif bloque tout. Lire le mot d’état avant chaque commande plutôt que d’envoyer 0x000F d’emblée.
  • Des valeurs incohérentes : l’ordre des octets. CANopen transmet tout en petit-boutiste, alors que LabVIEW aplatit les données en gros-boutiste par défaut.
  • Un SDO sans réponse : prévoir un délai d’attente et un abandon propre, sinon une lecture bloquante fige la boucle.

Pour aller plus loin

Commentaires

Laisser un commentaire