Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 26 maggio 2023

Guardians of the CAN bus
come usare il CAN bus in C - pt.1

Drax: E uccideremo chiunque si metta sulla nostra strada!
Star-Lord: No! non uccideremo nessuno.
Drax: Uccidiamo un po' di gente...
Star-Lord: Non uccidiamo un po' di gente.
DraxUccidiamo solo una persona. Una stupida persona che nessuno ama...
Star-Lord: Ora stai solo rendendo la cosa triste...

In questo articolo prendo spunto dal bellissimo Guardiani della Galassia Vol.3 che ho visto da poco e con gran piacere. Questo gruppo di guerrieri abituati a grandi battaglie interstellari spesso si perdono (con notevole humor) in discussioni surreali come quella sopra, che rendono divertente un film dalla forte carica drammatica: un mix che il bravissimo James Gunn maneggia alla perfezione. 

E cosa centra con tutto questo il Controller Area Network (CAN bus per gli amici) che è il protagonista del post di oggi? Centra, centra... il CAN bus è un protocollo che unisce una apparente semplicità (e un bus seriale su doppino intrecciato + GND) a una molteplicità di prestazioni tipiche dei protocolli sofisticati (tipo Ethernet): è orientato ai messaggi, è insensibile ai disturbi elettromagnetici (è un ottimo bus di campo), è multiplexato (più dispositivi sullo stesso bus), è protetto dal Data Collision, è veloce (1 Mbit/s)... insomma, il CAN bus è un vero Guardiano della Galassia! Tra l'altro è nato per uso Automotive, che è uno degli usi più restrittivi esistenti, dove "l'affidabilità è prima di tutto": e grazie a queste caratteristiche l'uso si è allargato anche in molti altri settori. Viva il CAN bus!

...qualcuno si è ricordato di portare il CAN bus?...

E qui ci vorrebbe una breve descrizione di come funziona il CAN bus a livello elettrico, però dubito che riuscirei a farne una veramente esaustiva, visto che non è proprio il mio campo (a meno di non fare copia-e-incolla, ah ah ah), per cui vi rimando alle decine di descrizioni che si trovano in rete (ad esempio questa). 

Quello che conta per noi è come si può rappresentare questo protocollo con il Software e, osservando la disposizione dei dati "elettrici", salta all'occhio che il tipo di trasmissione è basato sul concetto di "frame", quindi di piccoli pacchetti di dati che contengono un identificatore unico ID di 11 bit che permette di "dare un nome" al pacchetto, un campo DLC che specifica la lunghezza del campo dati contenuto nel pacchetto e, infine, il vero e proprio campo Data di 0-8 byte che è l'unico campo a larghezza variabile (controllata dal campo DLC). Gli altri bit del pacchetto di dati non è necessario descriverli in questa sede, comunque tra di loro troviamo alcuni nomi famigliari come il SOF, il EOF, il CRC, ecc. Vi mostro una immagine qualsiasi delle mille che si trovano in rete:

Ecco, come ben visibile nel disegno, il frame ha una piccola sezione dati, di massimo 8 byte (per la versione classica, mentre per la versione estesa CAN FD sono 64 byte), e quindi rispetto a altri protocolli come Ethernet, il formato dati del CAN bus è più, per modo di dire "ristretto", basandosi su pacchetti di dimensioni ridotte (i frame, per l'appunto) che permettono di scambiare pochi dati alla volta, quindi non è fatto per trasmettere rapidamente "qualunque cosa" (pensate a un un file transfer con TCP/IP sotto Ethernet), ma piccoli gruppi di dati in maniera super-sicura e super-efficiente: è un vero bus industriale, perfetto per trasferire misure di sensori, setpoint, comandi, ecc.

E come si usa il CAN bus a livello Software? Riferendoci alla struttura del frame descritta sopra si può dire che un driver Software per CAN può limitarsi a isolare le tre parti "utili" del frame, e cioè: ID, DLC e Data, che sono sufficienti per gestire la comunicazione. Sotto Linux funziona così, e abbiamo a disposizione ben due maniere di operare (e due driver):

  • SocketCAN che è, come dice il nome, un network driver  inserito nella famiglia BSD socket.
  • can4linux che è un classico device driver a caratteri.

In questo articolo parleremo di SocketCAN, che ha vari vantaggi, tra cui:

  • È inserito di default nel Kernel Linux, quindi è perfettamente integrato nel sistema.
  • Permette di usare la classica interfaccia socket, che è veramente il prezzemolino della programmazione low-level.
  • È quello che conosco meglio, perché lo uso normalmente, mentre l'altro no... (e questo è l'attributo fondamentale, ah ah ah).

E veniamo a SocketCAN: considerato il tipo di protocollo socket-like c'è da aspettarsi che il Software di comunicazione abbia un aspetto familiare, tipo i notissimi Client/Server TCP o UDP (tipo quelli visti qui e qui), e in effetti è così, a parte un piccolo dettaglio: qui non ci sono Client e Server, tutti i dispositivi leggono e scrivono senza bisogno di instaurare una connessione, quindi è come se fossero tutti Server. Per mostrarvi un caso reale semplificato ho scritto due semplici esempi specializzati, in cui uno scrive (cansend.c) e l'altro legge (canrecv.c). E vediamo gli esempi... vai col codice!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// cansend - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// compongo il frame
struct can_frame frame;
memset(&frame, 0, sizeof(struct can_frame));
frame.can_id = 0x100U;
frame.can_dlc = 8;
snprintf(frame.data, sizeof(frame.data), "0123456");

// invio il frame
if (write(sockfd, &frame, sizeof(struct can_frame)) != sizeof(struct can_frame) == -1) {
// errore write()
printf("%s: errore write() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

close(sockfd);
return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

// canrecv - funzione main()
int main(int argc, char *argv[])
{
// creo il socket
int sockfd;
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) == -1) {
// errore socket()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// set degli attributi di i/o
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", "vcan0");
if (ioctl(sockfd, SIOCGIFINDEX, &ifr) == -1) {
// errore ioctl()
printf("%s: errore socket() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// set attributi dell'indirizzo
struct sockaddr_can addr;
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

// assegna l'indirizzo al socket
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
// errore bind()
printf("%s: errore bind() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// ricevo il frame
struct can_frame frame;
if (read(sockfd, &frame, sizeof(struct can_frame)) == -1) {
// errore read()
printf("%s: errore read() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

// mostro il frame ricevuto
printf("id:0x%x dlc:%d data: ", frame.can_id, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++)
printf("%c ", frame.data[i]);

printf("\n");

close(sockfd);
return EXIT_SUCCESS;
}

Visto? Sembrano due Server UDP, ma ancora più semplificati. Compilando ed eseguendo (poi vedremo come...) si nota che cansend si blocca in attesa che canrecv gli invii un frame, e una volta ricevuto lo mostra ed esce. Nella realtà un applicazione SocketCAN tipicamente scrive e legge, per cui è opportuno gestire azioni di read/write in maniera non-blocking, ma questi dettagli dipendono molto dal tipo di oggetto che si vuole realizzare. Ovviamente per testare questi programmi bisogna disporre di un CAN bus attivo nel sistema: nel prossimo articolo vedremo come si può (sotto Linux) lavorare e testare il Sotfware pur non disponendo di veri dispositivi Hardware CAN da provare (giurin giuretta che sarà veramente il tema del prossimo articolo). Già che ci siamo vi mostro anche un piccolo estratto dell'header can.h di Linux, dove si nota come è stato tradotto in Sofware il formato del frame Hardware:

/**
* struct can_frame - basic CAN frame structure
* @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above.
* @can_dlc: the data length field of the CAN frame
* @data: the CAN frame payload.
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* data length code: 0 .. 8 */
__u8 data[8] __attribute__((aligned(8)));
};

Visto? Vengono trattati solo i campi ID, DLC e Data del frame, e sono stati rinominati, rispettivamente, can_id, can_dlc e data. Il campo can_id è lungo 32 bit, di cui 11 sono dedicati al vero e proprio ID del frame, mentre gli altri hanno usi un po' speciali (gestione degli errori, ecc.).

Ok, per oggi può bastare. Nella seconda parte dell'articolo vi spiegherò come testare gli esempi mostrati usando un device CAN virtuale, e se avanza tempo magari parlerò anche dei filtri e degli errori. E, come vi ho già raccomandato altre volte, non trattenete il respiro nell'attesa! (che potrebbe risultare dannoso per la salute, ah ah ah).

Ciao, e al prossimo post!