STM32 bxCAN: Configurazione dei Filtri
e Gestione dei Messaggi
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.
| Scala | Modalità | Bit di Configurazione per Banco | Filtri per Banco |
|---|---|---|---|
| 32-bit | Mask | 1 identificatore + 1 maschera (32 bit ciascuno) | 1 |
| 32-bit | List | 2 identificatori (32 bit ciascuno) | 2 |
| 16-bit | Mask | 2 coppie identificatore/maschera (16 bit ciascuna) | 2 |
| 16-bit | List | 4 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;
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
- Transceiver esterno necessario: bxCAN è un controller di protocollo, non un dispositivo di livello fisico. Servono TJA1050, SN65HVD230 o il transceiver integrato sulla scheda Nucleo/Discovery.
- Terminazione bus: 120 Ω a ogni estremità del bus CAN. La maggior parte delle Nucleo include una resistenza attivabile via ponticello.
- Baud rate corrispondente: Tutti i nodi devono usare la stessa temporizzazione. bxCAN usa il clock APB1. Per APB1 a 42 MHz su STM32F401: BS1=11, BS2=4, Prescaler=14 → 125 kbit/s.
- Ordine attivazione filtro: Disattivate sempre un banco prima di modificarne la configurazione, poi riattivatelo. Modificare un banco attivo ha risultati indefiniti.
- Priorità mailbox: bxCAN arbitra i mailbox TX per priorità dell'identificatore (ID più basso = priorità più alta) quando multipli sono in sospeso simultaneamente.
- Overflow FIFO: Se la FIFO di ricezione va in overflow, i nuovi messaggi vanno persi. Monitorate
CAN_RF0R_FOVR0e assicuratevi che la vostra ISR sia abbastanza veloce. - Modalità silent: Usate
CAN_MCR_SILMdurante il bring-up per verificare che il nodo trasmetta senza acknowledge — utile per debug del bus. - Modalità loop-back: Per test su singolo nodo, impostate
CAN_MCR_LBKM. Il periferico riceve i propri frame trasmessi internamente senza bus fisico.
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:
- Mappatura di ogni ID CAN del sistema in un foglio di calcolo — ogni nodo, ogni messaggio, periodicità, DLC, formato dati.
- 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).
- Riserva del banco 0 come filtro "monitor" durante il bring-up iniziale: maschera = 0, accetta tutto. Lo sostituisco con filtri stretti prima della produzione.
- 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. - Watchdog contatore errori: leggere
CAN_ESRperiodicamente. Conteggi di errore crescenti indicano un transceiver guasto, bus non terminato o baud rate non corrispondente. RegistrareTECeRECda qualche parte accessibile (console seriale, memoria per post-mortem). - 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
- SO: STM32F4Discovery configurazione filtro CAN (17k+ visualizzazioni)
- SO: STM32F con libreria HAL_CAN (10k+ visualizzazioni)
- RM0368: STM32F401 Reference Manual — capitolo bxCAN
- RM0440: STM32G4 Reference Manual — capitolo bxCAN/FDCAN
- RM0468: STM32H723 Reference Manual — capitolo FDCAN
- AN3154: Utilizzo base del periferico CAN STM32 (Application Note ST)
- ISO 11898-1:2015 — strato data link CAN

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