2026-06-09 · Davide Carrese

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.

ScaleModeConfiguration Bits per BankFilters per Bank
32-bitMask1 identifier + 1 mask (32 bits each)1
32-bitList2 identifiers (32 bits each)2
16-bitMask2 identifier/mask pairs (16 bits each)2
16-bitList4 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;
⚠ Common Pitfall

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

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:

  1. Map every CAN ID in the system with a spreadsheet — each node, each message, periodicity, DLC, data format.
  2. 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).
  3. Reserve bank 0 as a "monitor" filter in early bring-up: mask = 0, accept all. Replace it with strict filters before production.
  4. 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.
  5. Add an error counter watchdog: read CAN_ESR periodically. Rising error counts indicate a failing transceiver, unterminated bus, or baud-rate mismatch. Log TEC and REC somewhere accessible (serial console, memory for post-mortem).
  6. 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

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.