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.

mercoledì 23 agosto 2023

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

Stakar Ogord: Uniformi della OrgoCorp. Dovrete indossarle per muovervi nell'Orgosfera senza attirare l'attenzione.
Drax: Questo non è il mio colore.
Stakar Ogord: Cosa hai detto?
Drax: Stona con i miei occhi.
Ed eccoci alla quarta (e ultima!) parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima, la seconda e la terza parte, please…). Nel dialogo qui sopra c'è, di nuovo, tutta la vena comica del film, che viaggia come al solito in parallelo alla parte seria. Mi ripeterò: è po’ come il CAN bus, che è così ben studiato ed efficace che rende divertente scrivere Software che lo usa, salvo quando devi risolvere un problema molto complicato e allora entri nella fase seria e drammatica... In questa quarta parte scopriremo gli ultimi segreti che ci mancano per diventare dei veri e propri Guardians of the CAN bus!

...ma anche il CAN bus stona coi tuoi occhi?...

Come ho fatto notare nella chiusura dell'ultimo articolo oramai ci manca solo il CAN FD per ultimare il nostro viaggio nei segreti del CAN bus. E allora veniamo subito al dunque: cosa è il CAN FD? Il CAN FD (CAN Flexible Data Rate) è un'espansione del CAN bus "classico" (Classical CAN) ed è definito dalla norma ISO 11898-1:2015. Il CAN FD facilita non solo l'invio di volumi di dati più grandi, visto che il limite di 8 byte del CAN classico è stato aumentato a 64 byte. Inoltre, offre la possibilità di una velocità di trasmissione più elevata nella fase dei dati, raggiungendo agevolmente i 5 Mbit/s. Anche del CAN FD esiste una versione extended, quindi, alla fin fine, abbiamo quattro tipi di frame, quelli mostrati in questa figura riassuntiva:

can bus versions

Si può commentare che, con questa versione FD, il CAN bus si snatura un poco, passando da semplice e affidabilissimo "bus di campo" a un rango superiore, quasi quello dei protocolli di rete sofisticati (tipo Ethernet) che permettono di trasferire notevoli moli di dati: comunque, anche se "snaturato", è assolutamente pronto all'uso e quando è il caso si può/deve usare, perché offre delle grandi prestazioni.

Ok, con ben quattro tipi disponibili sembrerebbe abbastanza complicato scrivere Software CAN "compatibile" (o quasi) passando da un tipo all'altro senza fare troppi cambi... Ma per questo c'è il nostro amico SocketCAN! Come abbiamo già visto nell'articolo precedente, passare da CAN classic a CAN extended è decisamente semplice, e sono felice di comunicarvi che, grazie alla flessibilità di SocketCAN, anche il CAN FD si usa in maniera molto semplificata. Vediamo come: non so se ricordate, ma nel primo articolo del ciclo avevo mostrato come è descritto (nell'header can.h) un frame CAN di SocketCAN, ve lo ripropongo:

/**
* 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)));
};

Ecco, questa sopra è una versione abbastanza datata (Linux 2.6) che non trattava ancora CAN FD. In Linux 3.7 troviamo la prima versione "moderna" che include il CAN FD:

/**
* struct can_frame - basic CAN frame structure
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @can_dlc: frame payload length in byte (0 .. 8) aka data length code
* N.B. the DLC field from ISO 11898-1 Chapter 8.4.2.3 has a 1:1
* mapping of the 'data length code' to the real payload length
* @data: CAN frame payload (up to 8 byte)
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

/**
* struct canfd_frame - CAN flexible data rate frame structure
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @len: frame payload length in byte (0 .. CANFD_MAX_DLEN)
* @flags: additional flags for CAN FD
* @__res0: reserved / padding
* @__res1: reserved / padding
* @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte)
*/
struct canfd_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 len; /* frame payload length in byte */
__u8 flags; /* additional flags for CAN FD */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8)));
};

Poi, in Linux 5.11, il can_frame è diventato così (mentre il canfd_frame è rimasto invariato):

/**
* struct can_frame - Classical CAN frame structure (aka CAN 2.0B)
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @len: CAN frame payload length in byte (0 .. 8)
* @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8)
* @__pad: padding
* @__res0: reserved / padding
* @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length
* len8_dlc contains values from 9 .. 15 when the payload length is
* 8 bytes but the DLC value (see ISO 11898-1) is greater then 8.
* CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver.
* @data: CAN frame payload (up to 8 byte)
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
union {
/* CAN frame payload length in byte (0 .. CAN_MAX_DLEN)
* was previously named can_dlc so we need to carry that
* name for legacy support
*/
__u8 len;
__u8 can_dlc; /* deprecated */
};
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};

Come si nota il campo can_dlc è stato deprecato (ma non cancellato) e il nome è stato allineato a quello usato nel CAN FD, proprio per permettere la scrittura semplificata di Software per entrambi i tipi senza troppi cambi!. Insomma, alla fine della fiera, ora è possibile riscrivere gli esempi del primo articolo (cansend.c e canrecv.c) per usare "Classic" o FD definendo una semplice variabile di compilazione (che ho chiamato CAN_FD). Cominciamo con canfdsend.c, 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>

// definizione del tipo CanFrame
#ifdef CAN_FD
typedef struct canfd_frame CanFrame; // CAN FD
#else
typedef struct can_frame CanFrame; // CAN classic
#endif

// canfdsend - 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 ioctl() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

// 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
CanFrame frame;
memset(&frame, 0, sizeof(CanFrame));
frame.can_id = 0x100U;
frame.len = 8;
snprintf(frame.data, sizeof(frame.data), "0123456");

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

close(sockfd);
return EXIT_SUCCESS;
}

ed ora passiamo a canfdrecv.c!

#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>

// definizione del tipo CanFrame
#ifdef CAN_FD
typedef struct canfd_frame CanFrame; // CAN FD
#else
typedef struct can_frame CanFrame; // CAN classic
#endif

// canfdrecv - 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 ioctl() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

// 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
CanFrame frame;
if (read(sockfd, &frame, sizeof(CanFrame)) == -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.len);
for (int i = 0; i < frame.len; i++)
printf("%c ", frame.data[i]);

printf("\n");

close(sockfd);
return EXIT_SUCCESS;
}

Visto come è facile? Nel codice ci sono gli appositi "#ifdef CAN_FD" che decidono che parte di codice usare. Quindi per ottenere la versione "Classic" si può compilare con:

aldo@Linux $ gcc canfdsend.c -o canfdsend
aldo@Linux $ gcc canfdrecv.c -o canfdrecv

Mentre, per ottenere la versione FD si deve compilare con:

aldo@Linux $ gcc canfdsend.c -o canfdsend -DCAN_FD
aldo@Linux $ gcc canfdrecv.c -o canfdrecv -DCAN_FD

E i cambi nel codice sono veramente limitatissimi, grazie alla flessibilità dell'interfaccia SocketCAN: basta definire un nuovo tipo (che ho chiamato CanFrame) da usare al posto della struct can_frame (o della struct canfd_frame), così i cambi nel codice sono minimi, anche grazie al fatto che il campo can_dlc ora si chiama len in entrambe le strutture, e quindi, in pratica, l'unico vero cambio nel codice è questo:

#ifdef CAN_FD
// set del modo CAN_FD
int enable_canfd = 1;
if ( setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable_canfd, sizeof(enable_canfd)) == -1) {

// errore setsockopt()
printf("%s: errore setsockopt() (%s)\n", argv[0], strerror(errno));
close(sockfd);
return EXIT_FAILURE;
}
#endif

che forza l'uso, nel socket aperto precedentemente, dei frame di tipo FD. Insomma, come avevo promesso è veramente semplice scrivere codice "compatibile" con entrambi i tipi "Classic" e FD. E il CAN FD extended? Vabbé, ve lo lascio come utile esercizio, basti sapere che è sufficiente modificare il codice per FD con le minime modifiche che avevo mostrato nello scorso articolo al riguardo del CAN extended.

Ok, penso che con questo possa bastare. Il nostro viaggio nei misteri del CAN bus e del suo uso attraverso SocketCAN è terminato, ora possiamo qualificarci come Guardians of the CAN bus! Giuro che non ci sarà (per il momento...) una quinta parte, quindi nel prossimo articolo parleremo d'altro. E non trattenete il fiato nell'attesa, mi raccomando!

Ciao, e al prossimo post!

sabato 15 luglio 2023

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

Mantis: È sbagliato manipolare i sentimenti degli amici.
Drax: E quella volta che mi hai fatto innamorare del mio calzino?
Mantis: Beh, è stato divertente.

Ed eccoci alla terza parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima e la seconda parte, please...). Nel dialogo qui sopra c'è tutta la vena comica del film, che viaggia sempre in parallelo alla vena seria e drammatica. Un po' come il CAN bus, che è così ben studiato ed efficace che rende divertente scrivere Software che lo usa, salvo quando devi risolvere un problema molto complicato e allora entri nella fase seria e drammatica... ma siamo qui per questo! In questa terza parte scopriremo gli ultimi segreti che ci mancano per diventare dei veri e propri Guardians of The CAN bus!

...peró il CAN bus è ancora più divertente!...

Nelle parti una e due abbiamo affrontato un bel po' di argomenti: la struttura del CAN bus e dei frame, il protocollo SocketCAN, esempi elementari di send e receive, il can_frame di SocketCAN, le can-utils, il setup di un CAN device sotto Linux, il sistema di filtraggio... E cosa ci manca allora? Direi che per completare l'opera bisogna descrivere: il trattamento degli errori, il CAN extended e, dulcis in fundo, il CAN FD. Uh, è molta roba e il tempo stringe: cominciamo!

E cominceremo dal trattamento degli errori: nel caso che qualcosa vada male nel protocollo è prevista la generazione di messaggi di errore che, opportunamente decodificati, conducono alla identificazione del problema: ossia, la mia funzione di ricezione riceverà de messaggi auto-generati dal controller che sta nel dispositivo fisico con cui comunico, e questi messaggi mi diranno cosa c'è che non va. È un sistema di auto-diagnostica molto efficace, quindi. Vediamo allora una versione di canrecv.c che realizza questo punto: vai col codice di canrecverr.c!

#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>
#include <linux/can/error.h>

// canrecverr - 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;
}

// set errori da testare (in questo caso 3 ma se ne possono aggiungere a piacere)
can_err_mask_t err_mask = (CAN_ERR_TX_TIMEOUT | CAN_ERR_LOSTARB | CAN_ERR_PROT);
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask));

// 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;
}

// analizzo se è un error frame
if ((frame.can_id & CAN_ERR_FLAG) != 0) {
printf("%s: messaggio di errore\n", argv[0]);
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? Usando la onnipresente system-call  setsockopt(2) si attiva la gestione degli errori usando il flag CAN_RAW_ERR_FILTER e passandole la maschera err_mask  degli errori che vogliamo vedere: io ne ho messi tre, ma se ne possono mettere altri a piacere, o addirittura aggiungerli tutti assegnando a err_mask  la maschera speciale CAN_ERR_MASK. Per attivare questa gestione l'unico altro cambio da effettuare nel codice è il test della presenza del flag CAN_ERR_FLAG nel can_id del messaggio ricevuto: se il flag è preesente non è un messaggio normale ma è un messaggio di errore da analizzare per estrarre l'informazione contenuta. L'analisi dell'errore si fa cercando nel can_id quali sono i bit attivi, seguendo questo esempio:

// errore: "TX timeout (by netdevice driver)"
if (frame->can_id & CAN_ERR_TX_TIMEOUT)
printf("TX timeout (by netdevice driver)");

// errore: "arbitration lost in bit: (data[0])"
if (frame->can_id & CAN_ERR_LOSTARB)
printf("arbitration lost in bit: 0x%02x", frame->data[0]);

// errore: "protocol violations: type (data[2]) location (data[3])"
if (frame->can_id & CAN_ERR_PROT)
printf("protocol violations: type 0x%02x location 0x%02x", frame->data[2], frame->data[3]);

L'esempio evidentemente non è completo, visto che gli errori trattabili sono veramente molti, ma per completarlo basta aiutarsi con le molte #define contenute in linux/can/error.h, è una attività relativemente semplice. Notare che la gestione degli errori è compatibile con quella dei filtri: nessuno impedisce di usarle (con oculatezza) allo stesso tempo.

Chiuso il capitolo errori possiamo passare al CAN extended, identificato dal protocollo CAN2.0(B) (mentre il CAN standard è CAN2.0(A)). Come si può intuire dal nome è una versione (quasi identica) del bus che usa un can_id esteso, e il risultato lo possiamo vedere nella seguente figura, dove (a) el il tipo normale e (b) è l'extended:

Frame--Standard-Extended

Con l'extended, praticamente, possiamo trasferire gli stessi dati, ma abbiamo una capacità di indirizzamento superiore. Dal punto di vista che ci interessa, quello del codice, questo cambio implica poche differenze visto che il "lavoro sporco" lo effettua il protocollo SocketCAN che, come gia visto nella primo articolo del ciclo, usa un can_id di 32 bit, quindi è compatibile con il CAN extended. La differenza fondamentale è nell'uso delle maschere definite nell'header can.h, di cui vi passo un estratto (utile anche per il punto precedente, quello degli errori):

/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
#define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
#define CAN_ERR_FLAG 0x20000000U /* error message frame */

/* valid bits in CAN ID for frame formats */
#define CAN_SFF_MASK 0x000007FFU /* standard frame format (SFF) */
#define CAN_EFF_MASK 0x1FFFFFFFU /* extended frame format (EFF) */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */

quindi, ad esempio, nel codice per la gestione degli errori visto sopra (canrecverr.c) si dovrebbe cambiare una piccola parte del codice per usare CAN extended (vi mostro solo il frammento modificato):

// set di 2 filtri (ma possono essere di più) e attivazione con setsockopt(2)
struct can_filter rfilter[2];
rfilter[0].can_id = 0x100;
rfilter[0].can_id |= CAN_EFF_FLAG;
rfilter[0].can_mask = (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_EFF_MASK);
rfilter[1].can_id = 0x200;
rfilter[1].can_id |= CAN_EFF_FLAG;
rfilter[1].can_mask = (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_EFF_MASK);
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

E, analogamente, quando inviamo o riceviamo un messaggio in modo CAN extended dobbiamo fare questo:

// preparo la spedizione di un messaggio
...
send_frame.can_id = 0x100;
send_frame.can_id |= CAN_EFF_FLAG; // per usare CAN extended
...

// esamino un messaggio ricevuto
...
printf("recvd: 0x%03X\n", frame->can_id & CAN_EFF_MASK);
...

Tutto qua!

E adesso dovremmo concludere in bellezza con il CAN FD. Dovremmo, ma l'atmosfera pre-vacanziera mi sta travolgendo, e poi non voglio buttare troppa carne al fuoco a Luglio inoltrato (scrivo questo pensiero in tempo reale: mentre scrivevo quello che avete appena letto sopra non avevo ancora questa intenzione. Giuro!)... quasi quasi lo rimando al prossimo articolo. Si, lo rimando... termineremo con una quarta parte in Agosto, tutta sul CAN FD, sperando che qualcuno la legga in spiaggia, ah ah ah.

Ciao, e al prossimo post!

lunedì 19 giugno 2023

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

Nebula: Drax, siediti! [a Drax che è disteso]
Drax: È qui per questo!
Star-Lord: Drax, si chiama divano. Non è un letto.
Drax: Perché è così lungo allora?
Star-LordMa che...
Drax: Beh, trovo difficile credere che non abbia scopi multipli. È così allettante.

Ed eccoci alla seconda parte di Guardians of the CAN bus, un ciclo di articoli ispirati al bellissimo Guardiani della Galassia Vol.3 (ripassatevi la prima parte, please...). Nel dialogo qui sopra si può evidenziare lo spirito del film, con Drax il Distruttore sempre pronto a scatenare equivoci esilaranti... Eppure non è affatto un film comico, è molto più profondo, esattamente come questo articolo che potrebbe sembrare semi-serio ma è (spero) una concreta guida introduttiva ai misteri del CAN bus (e vabbé, io spero che lo sia...). In questa seconda parte parleremo di utilities, setup e filtri... uh, molta roba, cominciamo allora!

...secondo me il CAN bus si usa meglio da distesi...

Come promesso nell'articolo precedente il primo argomento che tratteremo riguarda la maniera di eseguire e testare Software per CAN bus (come i due esempi proposti nella prima parte) su una macchina Linux. In questo siamo abbastanza fortunati: il CAN bus oltre ad essere perfettamente integrato (attraverso SocketCAN) nel kernel  Linux, è anche ben supportato a livello utente (anzi, a livello programmatore), visto che fornisce un utilissimo set di utilità, le can-utils, pensate proprio per il testing, e dispone anche di un dispositivo virtuale che è perfetto per provare il Software sviluppato anche senza disporre di un dispositivo fisico "reale". Con le can-utils possiamo fare un sacco di operazioni, tra cui queste:

  • candump: visualizza, filtra e registra i dati CAN su file
  • canplayer: riproduce i file di log del CAN.
  • cansend: invia un singolo frame.
  • cangen: genera un traffico CAN casuale.
  • cansequence: invia e controlla una sequenza di frame CAN con payload crescente.
  • cansniffer: visualizza le differenze di contenuto dei dati CAN.

E queste sono solo le funzioni principali! Ce ne sono anche molte secondarie! Ma, visto che in rete ci sono già delle buone guide descrittive di can-utils, non voglio riscoprire l'acqua calda, e quindi vi consiglio di leggere, ad esempio, direttamente la descrizione su GitHub.

E passiamo al lato pratico: una volta scritto il codice vorremo provarlo, no? (credo che questa sia sempre una attività raccomandabile...) E allora dobbiamo chiedere al nostro amico Linux di attivare il dispositivo CAN, che può essere uno vero o, come detto sopra, uno virtuale che si comporta esattamente come uno reale (questo virtual device è un vero gioiellino che aiuta moltissimo il lavoro di programmazione). Per attivare la comunicazione CAN possiamo usare un semplice Bash Script di setup come il seguente:

#!/bin/bash

# aggiunge il nuovo device
sudo ip link add dev vcan0 type vcan

# abilita il nuovo device
sudo ip link set vcan0 up

Come indicato nei commenti (che devono essere sempre presenti e chiari sia nel C che nel Bash) il primo comando aggiunge un device di tipo rete (stiamo usando SocketCAN) che abbiamo chiamato vcan0 ed è di tipo virtuale (è un "vcan"). Il seguente comando usa anche lui l'interfaccia di rete e dice semplicemente a Linux di attivare il device. Tutto qua!

È semplicissimo, no? E se il dispositivo è "reale" che faremo? Useremo un tipo "can" invece di "vcan" e, per coerenza, lo chiameremo can0 invece di vcan0. Ricordatevi che vcan0 e can0 sono solo nomi (e possiamo usarne anche "di fantasia"), bisogna solo tenere presente che il nostro codice dovrà, poi, riferirsi al nome assegnato in setup al dispositivo. E il numero 0? Si mette per ricordare che possiamo trattare più bus alla volta, quindi mettere un numero progressivo è una buona idea.

Adesso che abbiamo installato e attivato il nostro device CAN possiamo compilare ed eseguire gli esempi dell'altra volta (canrecv.c e cansend.c), ottenendo questi risultati:

aldo@Linux $ gcc canrecv.c -o canrecv
aldo@Linux $ ./canrecv
id:0x100 dlc:8 data: 0 1 2 3 4 5 6
aldo@Linux $

Qui sopra canrecv si era messo in attesa, e il risultato (id:0x100 dlc:8 data: 0 1 2 3 4 5 6) è apparso, ovviamente, solo quando in un altro terminale ho eseguito cansend :

aldo@Linux $ gcc cansend.c -o cansend
aldo@Linux $ ./cansend
aldo@Linux $

sono soddisfazioni...

E adesso possiamo passare al prossimo punto, che, direi, si potrebbe chiamare "Funzioni avanzate di SocketCAN: filtri ed errori".  L'argomento che mi preme trattare per primo è quello dei filtri, ossia di come limitare la ricezione dei messaggi a "ricevo solo i can_id che mi interessano", anche perché dobbiamo ricordare che CAN bus è, per l'appunto, un bus, quindi potrebbe esserci un certo affollamento di dispositivi e, quindi, di messaggi che circolano.

Il metodo di filtraggio è relativamente semplice, e si basa sull'attributo CAN_RAW_FILTER del socket in uso, e si deve settare usando la system-call setsockopt(2) a cui bisogna passare la lista dei filtri da applicare, usando la apposita struttura can_filter descritta in uno degli header file di SocketCAN, e cioè linux/can.h. Ma forse è meglio fornire un semplice esempio facendo cantare il codice: di seguito vi mostro canrecvfilt.c, che, come indica il nome, è una canrecv.c con i filtri. 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>

// canrecvfilt - 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;
}

// set di 2 filtri (ma possono essere di più) e attivazione con setsockopt(2)
struct can_filter rfilter[2];
rfilter[0].can_id = 0x100;
rfilter[0].can_mask = CAN_SFF_MASK;
rfilter[1].can_id = 0x200;
rfilter[1].can_mask = CAN_SFF_MASK;
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

// 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? È identica alla canrecv.c dello scorso articolo però ha alcune linee aggiunte tra la bind(2) e la read(2) (ma non era obbligatorio metterle proprio li, eh!): si riempie un array di filtri (in questo caso ne ho messi 2 ma se ne possono aggiungere a piacere) e si passa l'array alla setsockopt(2). Ogni struttura can_filter dell'array contiene il can_id che si vuole far passare e la maschera da applicare al filtro (serve per evitare di usare anche i bit non significativi per il filtraggio): nell'esempio ho usato CAN_SFF_MASK, che è la maschera di default del Standard CAN, mentre per il Extended CAN  bisogna usare un altra maschera (ma questo lo vedremo nella terza parte del ciclo di articoli).

Per verificare il funzionamento dell'esempio basta compilare (ed eseguire) canrecvfilt.c e modificare cansend.c per inviare, ad esempio, un messaggio con can_id=0x300, e, magicamente, succederà che la canrecvfilt non riceve nulla! Poi si può provare a spedire messaggi con can_id=0x100 o 0x200 per verificare, invece, la corretta ricezione dei messaggi. È semplicissimo!

L'esempio che ho scritto è del tipo "classico" , ma sono possibili anche delle varianti: ad esempio si può filtrare al contrario, e cioè: "questi can_id non li voglio ricevere", ma questi sono dettagli che vi lascio il piacere di approfondire a parte.

Ok, la seconda parte del ciclo la chiudiamo qui. Nella terza parte (ebbene si! Ci sarà una terza parte, e l'ho deciso in corsa...) parleremo della gestione degli errori e, poi, di Extended CAN e CAN FD ovvero le due varianti del tipo "Classical": sono meno usate, ma hanno caratteristiche interessanti, e possono essere necessarie in alcuni progetti, quindi è meglio essere preparati all'uopo. Tenetevi pronti ma non siate impazienti: sicuramente il nuovo articolo non arriverà domani (e neanche dopodomani, ah ah ah).

Ciao, e al prossimo post!