STM32 bxCAN: Configurazione dei Filtri
e Gestione dei Messaggi

2026-06-09 · Davide Carrese

Una guida completa al periferico bxCAN degli STM32: modalità di scaling dei banchi filtro, configurazione mask/list, HAL_CAN e accesso diretto ai registri, trasmissione e ricezione messaggi, con un esempio reale di comunicazione tra due schede per F4, G4 e H7.

Il periferico bxCAN (basic extended CAN) di STM32 è uno dei controller CAN più diffusi nell'ecosistema ARM. Implementa il protocollo CAN 2.0B ed è presente su praticamente tutte le serie STM32F, G e H. Tuttavia, la sua architettura a 28 banchi filtro configurabili in quattro modalità diverse genera regolarmente confusione tra chi proviene da software-CAN o protocolli seriali più semplici.

Questa guida esplora ogni aspetto della configurazione bxCAN: le due modalità di scaling (filtraggio identificatore a 32 e 16 bit), le modalità mask e list, la gestione dei buffer di messaggi e la ricezione interruzioni-driven. Include codice sia per HAL_CAN che a livello di registri, così da poterlo adattare sia che utilizziate CubeMX sia che preferiate l'accesso diretto.

Architettura dei Filtri bxCAN

bxCAN fornisce 28 banchi filtro (banchi 0–27) sulla maggior parte delle serie STM32F e STM32G. Alcuni STM32H7 ne hanno di più o di meno — verificate sempre il reference manual. Ogni banco può essere configurato indipendentemente in una di quattro modalità, selezionate combinando i bit FBMx (modalità) e FBBy (scala) nei registri CAN_FMx e CAN_FSx.

ScalaModalitàBit di Configurazione per BancoFiltri per Banco
32-bitMask1 identificatore + 1 maschera (32 bit ciascuno)1
32-bitList2 identificatori (32 bit ciascuno)2
16-bitMask2 coppie identificatore/maschera (16 bit ciascuna)2
16-bitList4 identificatori (16 bit ciascuno)4

I registri CAN_FiR1 e CAN_FiR2 contengono i valori di configurazione effettivi. Come vengono interpretati dipende esclusivamente dalle impostazioni di scala e modalità per quel banco.

Modalità Mask a 32 bit

CAN_FiR1 contiene l'identificatore filtro a 32 bit (ID). CAN_FiR2 contiene la maschera a 32 bit. Per ID CAN standard a 11 bit, l'identificatore va nei bit ID[28:18] e la maschera in MASK[28:18]; i bit rimanenti vengono ignorati. Per ID estesi a 29 bit, tutti i 32 bit sono utilizzati. Un messaggio ricevuto supera il filtro se (rx_id & maschera) == (filtro_id & maschera). Impostate un bit della maschera a 1 per richiedere una corrispondenza esatta su quel bit, o a 0 per accettare sia 0 che 1.

Modalità List a 32 bit

CAN_FiR1 contiene il primo identificatore e CAN_FiR2 il secondo. Un messaggio ricevuto passa se il suo ID corrisponde esattamente a uno dei due. Questo offre due ID CAN specifici per banco filtro.

Modalità Mask a 16 bit

Ogni banco contiene due gruppi filtro indipendenti. CAN_FiR1[31:16] è il primo ID, CAN_FiR1[15:0] è la prima maschera; CAN_FiR2[31:16] è il secondo ID, CAN_FiR2[15:0] è la seconda maschera. Ogni campo a 16 bit contiene STID[10:3] nei bit [15:8] e STID[2:0]+IDE+RTR nei bit [7:0]. Utile principalmente quando servono solo ID standard e si vuole massimizzare la densità dei filtri.

Modalità List a 16 bit

Quattro identificatori a 16 bit per banco: CAN_FiR1[31:16], CAN_FiR1[15:0], CAN_FiR2[31:16], CAN_FiR2[15:0]. Fino a 112 (28×4) ID standard distinti possono essere accettati su un STM32F4.

Sequenza di Inizializzazione dei Filtri

I banchi filtro devono essere configurati mentre il CAN è in modalità inizializzazione. Il periferico CAN entra in modalità inizializzazione quando il bit INRQ in CAN_MCR viene impostato e l'hardware conferma con INAK in CAN_MSR. Per i filtri stessi, disattivate ogni banco azzerando il bit FACTx in CAN_FA1R prima di scrivere nei suoi registri, poi impostate FACTx per attivarlo.

// Entrata in modalità inizializzazione
CAN1->MCR |= CAN_MCR_INRQ;
while (!(CAN1->MSR & CAN_MSR_INAK));

// Disattivazione banco filtro 0
CAN1->FA1R &= ~(1UL << 0);

// Configurazione banco 0: 32-bit, modalità mask, accetta tutto
CAN1->FM1R &= ~(1UL << 0);   // FM = 0 → modalità mask
CAN1->FS1R |= (1UL << 0);    // FS = 1 → scala 32-bit

// ID = 0x00000000, maschera = 0x00000000 (accetta qualsiasi)
CAN1->sFilterRegister[0].FR1 = 0;
CAN1->sFilterRegister[0].FR2 = 0;

// Assegnazione FIFO (FIFO0), attivazione banco 0
CAN1->FFA1R &= ~(1UL << 0);  // Assegnazione FIFO0
CAN1->FA1R |= (1UL << 0);    // Attivazione

// Uscita dalla modalità inizializzazione
CAN1->MCR &= ~CAN_MCR_INRQ;
while (CAN1->MSR & CAN_MSR_INAK);

Configurazione Filtri con HAL_CAN

Usando HAL, la configurazione dei filtri si gestisce con HAL_CAN_ConfigFilter(). La struttura CAN_FilterTypeDef astrae la selezione di modalità e scala:

CAN_FilterTypeDef filter = {
    .FilterIdHigh      = 0x0000,
    .FilterIdLow       = 0x0000,
    .FilterMaskIdHigh  = 0x0000,
    .FilterMaskIdLow   = 0x0000,
    .FilterFIFOAssignment = CAN_FILTER_FIFO0,
    .FilterBank        = 0,
    .FilterMode        = CAN_FILTERMODE_IDMASK,
    .FilterScale       = CAN_FILTERSCALE_32BIT,
    .FilterActivation  = ENABLE
};
HAL_CAN_ConfigFilter(&hcan1, &filter);

Per accettare un ID specifico, popolate i campi FilterId e FilterMaskId con l'identificatore CAN nel formato corretto (nibble-swapped). Per ID standard 11 bit 0x321:

// ID standard 0x321 su filtro mask 32-bit
filter.FilterIdLow    = (uint16_t)((0x321 << 21) & 0xFFFF);
filter.FilterIdHigh   = (uint16_t)((0x321 << 21) >> 16);
filter.FilterMaskIdLow  = 0xFFF0;  // maschera tutti i bit ID
filter.FilterMaskIdHigh = 0xFFFF;
⚠ Insidia Comune

HAL si aspetta l'identificatore in formato nibble-swapped specifico del silicio bxCAN — la disposizione del registro filtro a 32 bit non è semplicemente l'ID CAN shiftato a sinistra. Se il vostro filtro non accetta nulla, provate la modalità CAN_FILTERSCALE_32BIT con valori noti via registri prima di incolpare l'hardware.

Trasmissione Messaggi

bxCAN ha tre mailbox di trasmissione. Per inviare un messaggio, scegliete un mailbox libero, popolatene i registri e richiedete la trasmissione:

// Livello registri: invio ID standard 0x321, 8 byte dati
CAN_TxHeaderTypeDef tx_header = {
    .StdId = 0x321,
    .ExtId = 0,
    .IDE   = CAN_ID_STD,
    .RTR   = CAN_RTR_DATA,
    .DLC   = 8,
    .TransmitGlobalTime = DISABLE
};

uint32_t mailbox;
HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &mailbox);

Equivalente a livello di registri:

// Attesa mailbox libero
while (!(CAN1->TSR & (CAN_TSR_TME0|CAN_TSR_TME1|CAN_TSR_TME2)));

uint32_t mailbox;
if (CAN1->TSR & CAN_TSR_TME0) mailbox = 0;
else if (CAN1->TSR & CAN_TSR_TME1) mailbox = 1;
else mailbox = 2;

CAN1->sTxMailBox[mailbox].TIR = (0x321 << 21);
CAN1->sTxMailBox[mailbox].TDTR = 8;
CAN1->sTxMailBox[mailbox].TDLR = data[0] | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);
CAN1->sTxMailBox[mailbox].TDHR = data[4] | (data[5]<<8) | (data[6]<<16) | (data[7]<<24);
CAN1->sTxMailBox[mailbox].TIR |= CAN_TIxR_TXRQ;  // Richiesta TX

while (!(CAN1->TSR & (CAN_TSR_RQCP0 << (mailbox*8))));
if (CAN1->TSR & (CAN_TSR_TXOK0 << (mailbox*8))) {
    // Trasmissione riuscita
}

Ricezione Messaggi con Interrupt

Per la ricezione interrupt-driven, abilitate l'interrupt di messaggio pendente sulla FIFO e gestitelo nella callback:

// Abilitazione interrupt FIFO0
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef rx_header;
    uint8_t rx_data[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);

    if (rx_header.StdId == 0x321) {
        // Elaborazione dati in rx_data[0..7]
    }
}

Ricezione a livello di registri:

// Verifica conteggio FIFO0 pendente
while (CAN1->RF0R & CAN_RF0R_FMP0) {
    uint32_t rir = CAN1->FIFOMailbox[0].RIR;
    uint32_t rdtr = CAN1->FIFOMailbox[0].RDTR;
    uint32_t rdlr = CAN1->FIFOMailbox[0].RDLR;
    uint32_t rdhr = CAN1->FIFOMailbox[0].RDHR;

    uint16_t std_id = (rir >> 21) & 0x7FF;
    uint8_t dlc = rdtr & 0x0F;
    uint8_t data[8];
    data[0] = rdlr & 0xFF;       data[1] = (rdlr >> 8) & 0xFF;
    data[2] = (rdlr >> 16) & 0xFF; data[3] = (rdlr >> 24) & 0xFF;
    data[4] = rdhr & 0xFF;       data[5] = (rdhr >> 8) & 0xFF;
    data[6] = (rdhr >> 16) & 0xFF; data[7] = (rdhr >> 24) & 0xFF;

    // Rilascio mailbox FIFO0
    CAN1->RF0R |= CAN_RF0R_RFOM0;
}

Esempio pratico: Comunicazione CAN tra Due Schede

Consideriamo un setup a due schede: Board A (STM32F401RE, NUCLEO-F401RE) invia una lettura sensore a 4 byte ogni 100 ms sull'ID CAN 0x100. Board B (STM32G474RE) riceve il messaggio, applica un fattore di scala e rimanda una risposta sull'ID 0x200.

Board A — Trasmettitore (STM32F401RE, a livello di registri):

void CAN_SendSensor(uint32_t raw_adc) {
    uint32_t mb;
    while (!(CAN1->TSR & (CAN_TSR_TME0|CAN_TSR_TME1|CAN_TSR_TME2)));
    if (CAN1->TSR & CAN_TSR_TME0) mb = 0;
    else if (CAN1->TSR & CAN_TSR_TME1) mb = 1;
    else mb = 2;

    CAN1->sTxMailBox[mb].TIR = (0x100 << 21);
    CAN1->sTxMailBox[mb].TDTR = 4;
    CAN1->sTxMailBox[mb].TDLR = raw_adc;
    CAN1->sTxMailBox[mb].TIR |= CAN_TIxR_TXRQ;
    while (!(CAN1->TSR & (CAN_TSR_RQCP0 << (mb*8))));
}

int main(void) {
    HAL_Init();
    MX_CAN1_Init();  // CubeMX: 125 kbit/s, modalità normal

    // Filtro: accetta solo ID 0x200
    CAN1->MCR |= CAN_MCR_INRQ;
    while (!(CAN1->MSR & CAN_MSR_INAK));
    CAN1->FA1R &= ~(1UL << 0);
    CAN1->FM1R &= ~(1UL << 0);
    CAN1->FS1R |= (1UL << 0);
    CAN1->sFilterRegister[0].FR1 = 0x200 << 21;
    CAN1->sFilterRegister[0].FR2 = 0x7FF << 21;
    CAN1->FFA1R &= ~(1UL << 0);
    CAN1->FA1R |= (1UL << 0);
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while (CAN1->MSR & CAN_MSR_INAK);

    CAN1->IER |= CAN_IER_FMPIE0;
    HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);

    while (1) {
        CAN_SendSensor(ADC1->DR);
        HAL_Delay(100);
    }
}

Board B — Ricevitore (STM32G474, risposta su ID 0x200):

uint32_t last_sensor_value = 0;

void CAN1_RX0_IRQHandler(void) {
    if (CAN1->RF0R & CAN_RF0R_FMP0) {
        uint32_t rir = CAN1->FIFOMailbox[0].RIR;
        uint32_t std_id = (rir >> 21) & 0x7FF;

        if (std_id == 0x100) {
            last_sensor_value = CAN1->FIFOMailbox[0].RDLR;
            uint32_t scaled = last_sensor_value * 3300 / 4096;  // mV

            while (!(CAN1->TSR & (CAN_TSR_TME0|CAN_TSR_TME1|CAN_TSR_TME2)));
            uint32_t mb;
            if (CAN1->TSR & CAN_TSR_TME0) mb = 0;
            else if (CAN1->TSR & CAN_TSR_TME1) mb = 1;
            else mb = 2;
            CAN1->sTxMailBox[mb].TIR = (0x200 << 21);
            CAN1->sTxMailBox[mb].TDTR = 4;
            CAN1->sTxMailBox[mb].TDLR = scaled;
            CAN1->sTxMailBox[mb].TIR |= CAN_TIxR_TXRQ;
        }
        CAN1->RF0R |= CAN_RF0R_RFOM0;
    }
}

Checklist Pratica

Come lo Affronterei su un Progetto Cliente

Su un sistema CAN di produzione con nodi multipli (ad esempio un controllore industriale con sensori, attuatori e un gateway), seguo un budget di filtri rigoroso:

  1. Mappatura di ogni ID CAN del sistema in un foglio di calcolo — ogni nodo, ogni messaggio, periodicità, DLC, formato dati.
  2. Allocazione banchi filtro per nodo a partire dal banco 0. Uso la modalità list a 32 bit per piccoli insiemi di ID specifici (match più veloce, debug più semplice). Passo alla modalità mask a 32 bit solo quando serve accettare intervalli (es. tutti i frame diagnostici in range 0x700–0x7FF).
  3. Riserva del banco 0 come filtro "monitor" durante il bring-up iniziale: maschera = 0, accetta tutto. Lo sostituisco con filtri stretti prima della produzione.
  4. Configurazione filtri "pinnata" nel codice — mai fare affidamento sullo stato di default dei banchi dopo il reset (le impostazioni di default di CubeMX cambiano tra versioni). Scrivere ogni banco in una funzione CAN_FilterConfig() chiamata una sola volta.
  5. Watchdog contatore errori: leggere CAN_ESR periodicamente. Conteggi di errore crescenti indicano un transceiver guasto, bus non terminato o baud rate non corrispondente. Registrare TEC e REC da qualche parte accessibile (console seriale, memoria per post-mortem).
  6. Thread monitor bus CAN (o poll nel loop principale) che logga il recupero da bus-off: quando il periferico entra in stato bus-off (CAN_ESR_BOFF), abortire tutti i mailbox TX pendenti, resettare il periferico CAN e reinizializzare i filtri. La sequenza di recupero da bus-off è ben documentata nella RM0390 sezione 32.7.8.

Il guasto più comune che ho visto in produzione è un filtro mal configurato che accetta silenziosamente tutto, sovraccaricando il microcontrollore con frame irrilevanti e rubando tempo CPU ai task real-time. Iniziate con filtri list-mode stretti, poi allargate incrementalmente durante i test — mai il contrario.

Riferimenti

Commenti

Avete esperienze con insidie dei filtri bxCAN? Scrivetemi un'email — aggiorno l'articolo con i riscontri reali.