STM32 bxCAN: Filter Configuration
and Message Handling
A complete walkthrough of the STM32 bxCAN peripheral: filter bank scaling modes, mask vs list configuration, HAL_CAN and register-level setup, transmission/reception, and a real dual-board communication example for F4, G4, and H7 series.
The STM32 bxCAN (basic extended CAN) peripheral is one of the most widely deployed CAN controllers in the ARM MCU ecosystem. It implements the CAN 2.0B protocol and is present on practically every STM32F, G, and H series device. However, its filter bank architecture — with 28 configurable banks supporting four different filter modes — regularly confuses engineers migrating from software-CAN or simpler serial protocols.
This guide walks through every aspect of bxCAN configuration: from the two scaling modes (32-bit and 16-bit identifier filtering) through mask and list modes, message buffer management, and interrupt-driven receive. It includes both HAL_CAN and register-level code so you can adapt it whether you use CubeMX or prefer direct register access.
The bxCAN Filter Architecture
bxCAN provides 28 filter banks (banks 0–27) on most STM32F and STM32G series. Some STM32H7 derivatives have more or fewer; always check the reference manual. Each filter bank can be independently configured in one of four modes, selected by combining the FBMx (mode) and FBBy (scale) bits in the CAN_FMx and CAN_FSx registers.
| Scale | Mode | Configuration Bits per Bank | Filters per Bank |
|---|---|---|---|
| 32-bit | Mask | 1 identifier + 1 mask (32 bits each) | 1 |
| 32-bit | List | 2 identifiers (32 bits each) | 2 |
| 16-bit | Mask | 2 identifier/mask pairs (16 bits each) | 2 |
| 16-bit | List | 4 identifiers (16 bits each) | 4 |
The filter bank registers CAN_FiR1 and CAN_FiR2 hold the actual configuration values. How they are interpreted depends entirely on the scale and mode settings for that bank.
32-bit Mask Mode
CAN_FiR1 holds the 32-bit filter identifier (ID). CAN_FiR2 holds the 32-bit mask. For standard 11-bit CAN IDs, the identifier is placed in bits ID[28:18] and the mask in MASK[28:18]; the remaining bits are ignored. For extended 29-bit IDs, all 32 bits are used. A received message passes the filter if (rx_id & mask) == (filter_id & mask). Set a mask bit to 1 to require an exact match on that bit, or 0 to accept both 0 and 1.
32-bit List Mode
CAN_FiR1 holds the first identifier and CAN_FiR2 holds the second. A received message passes if its ID matches either entry exactly. This gives you two specific CAN IDs per filter bank.
16-bit Mask Mode
Each bank holds two independent filter groups. CAN_FiR1[31:16] is the first ID, CAN_FiR1[15:0] is the first mask; CAN_FiR2[31:16] is the second ID, CAN_FiR2[15:0] is the second mask. Each 16-bit field contains STID[10:3] in bits [15:8] and STID[2:0]+IDE+RTR in bits [7:0]. This is primarily useful when you only need standard IDs and want to maximise filter density.
16-bit List Mode
Four 16-bit identifiers per bank: CAN_FiR1[31:16], CAN_FiR1[15:0], CAN_FiR2[31:16], CAN_FiR2[15:0]. Up to 112 (28×4) distinct standard IDs can be accepted on an STM32F4.
Filter Initialisation Sequence
Filter banks must be configured while they are in initialisation mode. The CAN peripheral enters initialisation mode when the INRQ bit in CAN_MCR is set and the hardware confirms with INAK in CAN_MSR. For the filters themselves, deactivate each bank by clearing the FACTx bit in CAN_FA1R before writing to its registers, then set FACTx to activate it.
// Enter initialisation mode CAN1->MCR |= CAN_MCR_INRQ; while (!(CAN1->MSR & CAN_MSR_INAK)); // Deactivate filter bank 0 CAN1->FA1R &= ~(1UL << 0); // Configure bank 0: 32-bit, mask mode, accept all (identity filter) CAN1->FM1R &= ~(1UL << 0); // FM = 0 → mask mode CAN1->FS1R |= (1UL << 0); // FS = 1 → 32-bit scale // Set ID = 0x00000000, mask = 0x00000000 (match any) CAN1->sFilterRegister[0].FR1 = 0; CAN1->sFilterRegister[0].FR2 = 0; // Set FIFO assignment (FIFO0), activate bank 0 CAN1->FFA1R &= ~(1UL << 0); // FIFO0 assignment CAN1->FA1R |= (1UL << 0); // Activate // Exit initialisation mode CAN1->MCR &= ~CAN_MCR_INRQ; while (CAN1->MSR & CAN_MSR_INAK);
HAL_CAN Filter Configuration
If you use HAL, the filter setup is handled through HAL_CAN_ConfigFilter(). The CAN_FilterTypeDef struct abstracts mode and scale selection:
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);
To accept a specific ID, populate the FilterId and FilterMaskId fields with the CAN identifier in the correct nibble-swapped format. For standard 11-bit ID 0x321:
// Standard ID 0x321 on 32-bit mask filter // STID occupies bits [28:18] of the 32-bit word // In HAL, this translates to: filter.FilterIdLow = 0x321 << 5; // STID[10:3] → bits[15:8], STID[2:0] → bits[7:5] filter.FilterMaskIdLow = 0x7FF << 5; // mask all 11 bits // Or, for a specific ID with exact match: uint32_t id_reg = (0x321 << 21) | (CAN_ID_STD << 2); // EXID=0, IDE=0 filter.FilterIdLow = (uint16_t)(id_reg & 0xFFFF); filter.FilterIdHigh = (uint16_t)(id_reg >> 16); filter.FilterMaskIdLow = 0xFFF0; // mask all ID bits filter.FilterMaskIdHigh = 0xFFFF;
The HAL expects the identifier in a nibble-swapped format specific to the bxCAN silicon — the 32-bit filter register layout is not simply the CAN ID shifted left. If your filter accepts nothing, try CAN_FILTERSCALE_32BIT mode with the helper macro CAN_FILTER_IDMASK(id, mask) or use the register-level approach with known working values.
Message Transmission
bxCAN has three transmit mailboxes. To send a message, pick an empty mailbox, populate its registers, and request transmission:
// Register-level: send standard ID 0x321, 8 data bytes
CAN_TxHeaderTypeDef tx_header = {
.StdId = 0x321,
.ExtId = 0,
.IDE = CAN_ID_STD,
.RTR = CAN_RTR_DATA,
.DLC = 8,
.TransmitGlobalTime = DISABLE
};
// Find empty mailbox and send
uint32_t mailbox;
HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &mailbox);
// tx_data is uint8_t[8]
Register-level equivalent:
// Wait for an empty mailbox
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->TxMailbox[mailbox].TIR = (0x321 << 21); // Standard ID, IDE=0
CAN1->TxMailbox[mailbox].TDTR = 8; // DLC
CAN1->TxMailbox[mailbox].TDLR = data[0] | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);
CAN1->TxMailbox[mailbox].TDHR = data[4] | (data[5]<<8) | (data[6]<<16) | (data[7]<<24);
CAN1->TxMailbox[mailbox].TIR |= CAN_TIxR_TXRQ; // Request transmission
// Poll for completion
while (!(CAN1->TSR & (CAN_TSR_RQCP0 << (mailbox*8))));
if (CAN1->TSR & (CAN_TSR_TXOK0 << (mailbox*8))) {
// Transmission successful
}
Message Reception with Interrupts
For interrupt-driven reception, enable the FIFO message-pending interrupt and handle it in the callback:
// Enable FIFO0 message pending interrupt
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
// In the callback:
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);
// Check which ID was received
if (rx_header.StdId == 0x321) {
// Process data in rx_data[0..7]
}
}
Register-level reception:
// Check FIFO0 pending count
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;
// Release FIFO0 output mailbox
CAN1->RF0R |= CAN_RF0R_RFOM0;
}
Practical example: Dual-Board CAN Communication
Consider a two-board setup: Board A (STM32F401RE, NUCLEO-F401RE) sends a 4-byte sensor reading every 100 ms on CAN ID 0x100. Board B (STM32G474RE) receives the message, applies a scaling factor, and echoes a response on ID 0x200.
Board A — Transmitter (STM32F401RE, register-level):
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); // Standard ID 0x100
CAN1->sTxMailBox[mb].TDTR = 4; // 4 data bytes
CAN1->sTxMailBox[mb].TDLR = raw_adc; // 32-bit value
CAN1->sTxMailBox[mb].TIR |= CAN_TIxR_TXRQ; // Request TX
while (!(CAN1->TSR & (CAN_TSR_RQCP0 << (mb*8)))); // Wait
}
int main(void) {
// System clock, GPIO, CAN1 init with 125 kbit/s (typical for CAN)
// Configure PB8=CAN1_RX, PB9=CAN1_TX as alternate function
HAL_Init();
MX_CAN1_Init(); // CubeMX or manual: 125 kbit/s, normal mode
// Filter: accept ID 0x200 only
CAN1->MCR |= CAN_MCR_INRQ;
while (!(CAN1->MSR & CAN_MSR_INAK));
CAN1->FA1R &= ~(1UL << 0);
CAN1->FM1R &= ~(1UL << 0); // Mask mode
CAN1->FS1R |= (1UL << 0); // 32-bit
CAN1->sFilterRegister[0].FR1 = 0x200 << 21;
CAN1->sFilterRegister[0].FR2 = 0x7FF << 21; // Mask: keep ID bits
CAN1->FFA1R &= ~(1UL << 0);
CAN1->FA1R |= (1UL << 0);
CAN1->MCR &= ~CAN_MCR_INRQ;
while (CAN1->MSR & CAN_MSR_INAK);
// Enable RX interrupt for FIFO0
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); // Send latest ADC reading
HAL_Delay(100);
}
}
Board B — Receiver (STM32G474, response on 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
// Echo scaled value as response on ID 0x200
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; // Release mailbox
}
}
Practical Checklist
- Transceiver required: bxCAN is a protocol controller, not a physical-layer device. You need an external CAN transceiver (TJA1050, SN65HVD230, or built-in on your Nucleo/Discovery board).
- Bus termination: A 120 Ω resistor at each end of the CAN bus is mandatory. Most Nucleo boards include one that can be enabled via a solder bridge.
- Baud rate matching: All nodes must use the same bit timing. STM32 bxCAN uses the APB1 clock as time quanta source. For 42 MHz APB1 on STM32F401, BS1=11, BS2=4, Prescaler=14 gives 125 kbit/s.
- Filter activation order: Always deactivate a bank before changing its configuration, then re-activate. Changing an active bank has undefined results.
- Mailbox priority: bxCAN arbitrates TX mailboxes by identifier priority (lowest ID = highest priority) when multiple are pending simultaneously.
- FIFO overflow: If the receive FIFO overflows, new messages are lost. Monitor
CAN_RF0R_FOVR0and ensure your ISR is fast enough. - Silent mode: Use
CAN_MCR_SILMduring bring-up to verify your node transmits without acknowledging — useful for bus debugging. - Loop-back mode: For single-node testing, set
CAN_MCR_LBKM. The peripheral receives its own transmitted frames internally without a physical bus.
How I Would Approach This on a Client Project
On a production CAN system with multiple nodes (e.g., an industrial controller with sensors, actuators, and a gateway), I follow a strict filter budget:
- Map every CAN ID in the system with a spreadsheet — each node, each message, periodicity, DLC, data format.
- Allocate filter banks per node starting from bank 0. Use 32-bit list mode for small sets of specific IDs (fastest match, simplest debug). Switch to 32-bit mask mode only when you need to accept ranges (e.g., all diagnostic frames in range 0x700–0x7FF).
- Reserve bank 0 as a "monitor" filter in early bring-up: mask = 0, accept all. Replace it with strict filters before production.
- Pin the filter configuration in code — never rely on default bank state after reset (CubeMX defaults change between versions). Hard-code every bank in a
CAN_FilterConfig()function called exactly once. - Add an error counter watchdog: read
CAN_ESRperiodically. Rising error counts indicate a failing transceiver, unterminated bus, or baud-rate mismatch. LogTECandRECsomewhere accessible (serial console, memory for post-mortem). - Write a CAN bus monitor thread (or poll in the main loop) that logs bus-off recovery: when the peripheral enters bus-off state (
CAN_ESR_BOFF), issue an abort of all pending TX mailboxes, reset the CAN peripheral, and re-initialise the filters. The bus-off recovery sequence is well documented in RM0390 section 32.7.8.
The most common failure I've seen in production is a misconfigured filter that silently accepts everything, overloading the MCU with irrelevant frames and starving real-time tasks of CPU time. Start with strict list-mode filters, then widen incrementally under test — never the other way around.
References
- SO: STM32F4Discovery CAN filter configuration (17k+ views)
- SO: STM32F using HAL_CAN Library (10k+ views)
- RM0368: STM32F401 Reference Manual — bxCAN chapter
- RM0440: STM32G4 Reference Manual — bxCAN/FDCAN chapter
- RM0468: STM32H723 Reference Manual — FDCAN chapter
- AN3154: STM32 CAN peripheral basic usage (ST Application Note)
- ISO 11898-1:2015 — CAN data link layer
Comments
Have experience with bxCAN filter pitfalls? Send me an email — I update the article with real-world findings.

Comments
Have comments? Send me an email.