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.

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!