Back to Technology

STM32 Part 12: CAN Bus

March 31, 2026 Wasil Zafar 27 min read

CAN Bus powers automotive ECUs, industrial field buses, and robotics — master STM32 bxCAN and FDCAN configuration, hardware message filtering, error frame handling, and multi-node network design.

Table of Contents

  1. CAN Protocol Overview
  2. Bit Timing Calculation
  3. Message Filters
  4. Transmitting & Receiving
  5. Error Handling
  6. FDCAN (G4, H7, U5)
  7. Multi-Node CANopen-Style Network
  8. Exercises
  9. CAN Design Tool
  10. Conclusion & Next Steps
Series Overview: This is Part 12 of our 18-part STM32 Unleashed series. We cover STM32 bxCAN and FDCAN from bit timing configuration through hardware message filtering, error state machines, and multi-node network design — the essential skills for any automotive or industrial embedded project.

STM32 Unleashed: HAL Driver Development

Your 18-step learning path • Currently on Step 12
1
Architecture & CubeMX Setup
STM32 family, clock tree, HAL vs LL, CubeMX workflow, first project
Completed
2
GPIO & Button Debounce
GPIO modes, pull-up/down, EXTI, software debounce, HAL_GPIO_ReadPin
Completed
3
UART Communication
Polling, interrupt, DMA modes, printf retargeting, ring buffers
Completed
4
Timers, PWM & Input Capture
TIM basics, PWM generation, input capture, encoder mode
Completed
5
ADC & DAC
Single/continuous conversion, DMA, injected channels, DAC waveforms
Completed
6
SPI Protocol
SPI master/slave, full-duplex, DMA transfers, sensor drivers
Completed
7
I2C Protocol
I2C master, 7/10-bit addressing, DMA, multi-master, error handling
Completed
8
DMA & Memory Efficiency
DMA streams, circular mode, memory-to-memory, zero-copy patterns
Completed
9
Interrupt Management & NVIC
Priority grouping, preemption, ISR design, HAL callbacks, latency
Completed
10
Low-Power Modes
Sleep, Stop, Standby modes, RTC wakeup, LP UART, power profiling
Completed
11
RTC & Calendar
RTC configuration, alarms, backup registers, calendar subseconds
Completed
12
CAN Bus
FDCAN/bxCAN, filters, message frames, error handling, automotive use
You Are Here
13
USB CDC Virtual COM Port
USB FS/HS, CDC class, virtual serial, control transfers, descriptors
14
FreeRTOS Integration
Tasks, queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, stack sizing
15
Bootloader Development
Custom IAP bootloader, UART/USB DFU, flash programming, jump-to-app
16
External Storage: SD & QSPI Flash
FATFS on SD card, QSPI NOR flash, memory-mapped execution, wear levelling
17
Ethernet & TCP/IP Stack
LwIP integration, DHCP, TCP server, HTTP, MQTT, Ethernet DMA descriptors
18
Production Readiness
Watchdog, HardFault handler, flash option bytes, code signing, CI/CD

CAN Protocol Overview

The Controller Area Network (CAN) protocol was developed by Bosch in the 1980s and has since become the dominant serial bus for automotive ECUs, industrial automation, robotics, and medical devices. Its key properties — differential signaling, multi-master arbitration, non-destructive collision handling, and hardware error detection — make it uniquely suited for real-time embedded networks operating in electrically noisy environments.

CAN uses a differential pair (CAN_H and CAN_L) with a characteristic impedance of 120 Ω. Dominant bits (logic 0) drive CAN_H high and CAN_L low (differential 2 V); recessive bits (logic 1) allow both lines to float to roughly 2.5 V (differential ≈ 0 V). Any node transmitting a dominant bit overrides a recessive bit — this is the physical basis for arbitration.

Multi-master CSMA/CD with non-destructive arbitration means every node can attempt transmission simultaneously. The node transmitting the higher-priority (lower numeric) ID wins; the losing node backs off and retries. Unlike Ethernet, no data is lost — the winner's frame is received correctly by all nodes.

CAN Variants

  • CAN 2.0A — 11-bit standard identifier (2048 unique IDs), maximum 8-byte data field. Still by far the most common in automotive body electronics.
  • CAN 2.0B — 29-bit extended identifier (536 million IDs), maximum 8-byte data field. Used in J1939 (trucks, agriculture), CANopen, DeviceNet.
  • CAN FD (Flexible Data-Rate) — retains the 11/29-bit arbitration phase but switches to a higher bit rate for the data phase and allows up to 64-byte payload. Standardised as ISO 11898-1:2015.

Bit Timing Fundamentals

A CAN bit period is divided into four segments: Sync Seg (1 tq, fixed), Prop Seg (1–8 tq), Phase Seg 1 (1–8 tq), and Phase Seg 2 (1–8 tq), where tq is the time quantum derived from the peripheral clock divided by the prescaler. The sample point occurs at the end of Phase Seg 1 — typically at 75–87.5% of the total bit period. Maximum bit rate is determined by cable length; propagation delay on a CAN bus is roughly 5 ns/m.

CAN Variant Max Bit Rate Max Data Bytes STM32 Families FD Support
bxCAN 1 Mbit/s 8 F0, F1, F3, F4, F7 No
FDCAN 8 Mbit/s (data phase) 64 G0, G4, H7, L5, U5 Yes
CAN 2.0A 1 Mbit/s 8 Both No
CAN 2.0B 1 Mbit/s 8 Both (bxCAN 2.0B passive on F0) No
ISO CAN FD 8 Mbit/s data / 1 Mbit/s arb 64 FDCAN families Yes

The following bxCAN initialisation configures CAN1 at 500 kbit/s on a STM32F4 with APB1 at 42 MHz. The prescaler, BS1, and BS2 values produce exactly 500 kbit/s with a sample point of 87.5%.

/* bxCAN at 500 kbit/s on STM32F4, APB1 = 42 MHz
 * Bit rate = APB1 / (Prescaler * (1 + BS1 + BS2))
 * 500 000 = 42 000 000 / (6 * (1 + 11 + 2)) = 42 000 000 / (6 * 14)
 * Sample point = (1 + BS1) / (1 + BS1 + BS2) = 12/14 = 85.7%
 */
CAN_HandleTypeDef hcan1;

void MX_CAN1_Init(void)
{
    hcan1.Instance                  = CAN1;
    hcan1.Init.Prescaler            = 6;
    hcan1.Init.Mode                 = CAN_MODE_NORMAL;
    hcan1.Init.SyncJumpWidth        = CAN_SJW_1TQ;
    hcan1.Init.TimeSeg1             = CAN_BS1_11TQ;
    hcan1.Init.TimeSeg2             = CAN_BS2_2TQ;
    hcan1.Init.TimeTriggeredMode    = DISABLE;
    hcan1.Init.AutoBusOff           = DISABLE;   /* Manual bus-off recovery */
    hcan1.Init.AutoWakeUp           = DISABLE;
    hcan1.Init.AutoRetransmission   = ENABLE;    /* Retransmit on error/arbitration loss */
    hcan1.Init.ReceiveFifoLocked    = DISABLE;
    hcan1.Init.TransmitFifoPriority = DISABLE;   /* Mailbox priority by ID */

    if (HAL_CAN_Init(&hcan1) != HAL_OK)
    {
        Error_Handler();
    }
}

Bit Timing Calculation

Getting bit timing right is the most common source of CAN bring-up failures. The formula is straightforward but requires careful attention to your APB1 clock frequency — CAN1 and CAN2 both hang off APB1 on STM32F4/F7.

The nominal bit rate is: Bit Rate = f_APB1 / (Prescaler × (1 + BS1 + BS2)). The time quantum (tq) is tq = Prescaler / f_APB1. The total number of tq per bit is N = 1 + BS1 + BS2 where 1 is the fixed Sync Seg. The sample point percentage is SP% = (1 + BS1) / N × 100.

CAN specifications generally require the sample point between 75% and 87.5% of the bit period. For bus lengths above 100 m or noisier environments, a lower sample point (75%) gives more margin for propagation delay; for short, clean buses at high bit rates, a higher sample point (87.5%) is typical.

Bit Rate APB1 Clock Prescaler BS1 (tq) BS2 (tq) Total tq Sample Point
125 kbit/s42 MHz211321687.5%
250 kbit/s42 MHz121121485.7%
500 kbit/s42 MHz61121485.7%
1 Mbit/s42 MHz31121485.7%
500 kbit/s36 MHz41521888.9%
500 kbit/s45 MHz51521888.9%

The following code shows how to configure bxCAN for three common bit rates on a STM32F4 with APB1 = 42 MHz. All three share the same BS1/BS2 values — only the prescaler changes. This consistency simplifies network-wide changes.

/* CAN bit timing configurations for APB1 = 42 MHz
 * All achieve ~85.7% sample point (BS1=11, BS2=2, N=14 tq)
 */

/* 250 kbit/s: 42 MHz / (12 * 14) = 250 000 bit/s */
static void CAN_SetBitRate_250k(CAN_HandleTypeDef *hcan)
{
    hcan->Init.Prescaler = 12;
    hcan->Init.TimeSeg1  = CAN_BS1_11TQ;
    hcan->Init.TimeSeg2  = CAN_BS2_2TQ;
    hcan->Init.SyncJumpWidth = CAN_SJW_1TQ;
}

/* 500 kbit/s: 42 MHz / (6 * 14) = 500 000 bit/s */
static void CAN_SetBitRate_500k(CAN_HandleTypeDef *hcan)
{
    hcan->Init.Prescaler = 6;
    hcan->Init.TimeSeg1  = CAN_BS1_11TQ;
    hcan->Init.TimeSeg2  = CAN_BS2_2TQ;
    hcan->Init.SyncJumpWidth = CAN_SJW_1TQ;
}

/* 1 Mbit/s: 42 MHz / (3 * 14) = 1 000 000 bit/s */
static void CAN_SetBitRate_1M(CAN_HandleTypeDef *hcan)
{
    hcan->Init.Prescaler = 3;
    hcan->Init.TimeSeg1  = CAN_BS1_11TQ;
    hcan->Init.TimeSeg2  = CAN_BS2_2TQ;
    hcan->Init.SyncJumpWidth = CAN_SJW_1TQ;
}

/* Re-initialise CAN with new bit rate (requires bus to be idle) */
HAL_StatusTypeDef CAN_ChangeBitRate(CAN_HandleTypeDef *hcan,
                                    uint32_t bitRateKbps)
{
    HAL_CAN_Stop(hcan);
    switch (bitRateKbps)
    {
        case 250:  CAN_SetBitRate_250k(hcan); break;
        case 500:  CAN_SetBitRate_500k(hcan); break;
        case 1000: CAN_SetBitRate_1M(hcan);   break;
        default:   return HAL_ERROR;
    }
    return HAL_CAN_Init(hcan);
}

Message Filters

bxCAN has 28 filter banks on STM32F4 (14 on the connectivity line STM32F105/107). These banks are shared between CAN1 and CAN2 — by default all 28 are assigned to CAN1, but you can split them with CAN_SlaveStartFilterBank. Each filter bank is configured in one of four modes based on two orthogonal settings:

  • Filter Mode — Mask Mode: the bank holds one ID + one mask. Any incoming ID that matches the masked pattern is accepted. A mask bit of 1 means "this bit must match"; a 0 bit is a "don't care". This is the most flexible mode — a single bank can accept entire ranges of IDs.
  • Filter Mode — List Mode: the bank holds a list of exact IDs. Only messages whose ID exactly matches one of the listed IDs are accepted.
  • Filter Scale — 16-bit: each bank stores two 16-bit filter entries, allowing 2 ID+mask pairs (mask mode) or up to 4 IDs (list mode) per bank.
  • Filter Scale — 32-bit: each bank stores one 32-bit filter entry — 1 ID+mask pair (mask mode) or 2 exact IDs (list mode) per bank.

Every accepted message is routed to either FIFO0 or FIFO1, each of which has 3 message slots. Separating traffic categories across the two FIFOs (e.g. safety-critical to FIFO0, diagnostic to FIFO1) keeps high-priority processing independent.

/* Filter bank 0: accept ONLY ID 0x123 (32-bit mask mode, FIFO0)
 * Filter bank 1: accept IDs 0x200–0x2FF (mask 0x700, FIFO1)
 */
void CAN_ConfigureFilters(CAN_HandleTypeDef *hcan)
{
    CAN_FilterTypeDef f;

    /* ── Bank 0: exact match on 0x123 ─────────────────────────────── */
    f.FilterBank           = 0;
    f.FilterMode           = CAN_FILTERMODE_IDMASK;
    f.FilterScale          = CAN_FILTERSCALE_32BIT;
    /* For standard 11-bit ID in 32-bit format, ID is left-shifted by 21 */
    f.FilterIdHigh         = (0x123U << 5);  /* High 16 bits of ID register */
    f.FilterIdLow          = 0x0000;         /* Low  16 bits (no extension bit) */
    f.FilterMaskIdHigh     = (0x7FFU << 5);  /* All 11 ID bits must match */
    f.FilterMaskIdLow      = 0x0000;
    f.FilterFIFOAssignment = CAN_RX_FIFO0;
    f.FilterActivation     = ENABLE;
    f.SlaveStartFilterBank = 14;             /* Bank 14–27 reserved for CAN2 */
    HAL_CAN_ConfigFilter(hcan, &f);

    /* ── Bank 1: accept IDs 0x200–0x2FF (mask 0x700) ───────────────── */
    f.FilterBank           = 1;
    f.FilterIdHigh         = (0x200U << 5);  /* Match 0x2xx range */
    f.FilterIdLow          = 0x0000;
    f.FilterMaskIdHigh     = (0x700U << 5);  /* Top 3 bits of 11-bit ID checked */
    f.FilterMaskIdLow      = 0x0000;
    f.FilterFIFOAssignment = CAN_RX_FIFO1;
    f.FilterActivation     = ENABLE;
    HAL_CAN_ConfigFilter(hcan, &f);
}

Transmitting & Receiving

bxCAN provides three transmit mailboxes. When all three are busy the HAL returns HAL_ERROR — a common surprise for developers who assume blocking behaviour. Always check the return value of HAL_CAN_AddTxMessage and implement a retry or queue if your application sends at high rates.

Received messages are stored in FIFO0 or FIFO1 according to whichever filter bank matched. Each FIFO holds up to 3 messages. If the FIFO fills and a fourth message arrives, the oldest message is discarded by default (or the newest if ReceiveFifoLocked = ENABLE). Read from the FIFO as fast as possible — always from interrupt context.

The recommended pattern is: enable CAN_IT_RX_FIFO0_MSG_PENDING notification, read in HAL_CAN_RxFifo0MsgPendingCallback, dispatch by message ID to a handler table. TX completion notification (CAN_IT_TX_MAILBOX_EMPTY) is optional unless you need precise timing of when a frame left the wire.

/* ────────────────────────────────────────────────────────────────────
 * CAN transmit + receive example — heartbeat producer, dispatcher consumer
 * ──────────────────────────────────────────────────────────────────── */

#define CAN_ID_HEARTBEAT   0x001U  /* Node heartbeat every 100 ms */
#define CAN_ID_SENSOR_ADC  0x123U  /* ADC reading */
#define CAN_ID_SENSOR_TEMP 0x124U  /* Temperature */

static uint32_t g_txMailbox;       /* Mailbox reference returned by HAL */

/* Send a heartbeat frame */
void CAN_SendHeartbeat(CAN_HandleTypeDef *hcan, uint8_t nodeId,
                       uint8_t state)
{
    CAN_TxHeaderTypeDef txHdr = {
        .StdId              = CAN_ID_HEARTBEAT,
        .ExtId              = 0,
        .IDE                = CAN_ID_STD,
        .RTR                = CAN_RTR_DATA,
        .DLC                = 2,
        .TransmitGlobalTime = DISABLE
    };
    uint8_t txData[2] = { nodeId, state };

    if (HAL_CAN_AddTxMessage(hcan, &txHdr, txData, &g_txMailbox) != HAL_OK)
    {
        /* All three mailboxes busy — handle or queue */
        CAN_HandleTxBusy();
    }
}

/* FIFO0 receive callback — called from USB/CAN IRQ handler */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rxHdr;
    uint8_t rxData[8];

    while (HAL_CAN_GetRxFifoFillLevel(hcan, CAN_RX_FIFO0) > 0)
    {
        if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0,
                                 &rxHdr, rxData) != HAL_OK)
        {
            break;
        }
        CAN_Dispatch(rxHdr.StdId, rxData, rxHdr.DLC);
    }
}

/* Simple ID-based dispatcher */
static void CAN_Dispatch(uint32_t id, const uint8_t *data, uint8_t dlc)
{
    switch (id)
    {
        case CAN_ID_HEARTBEAT:
            App_OnHeartbeat(data[0], data[1]);
            break;
        case CAN_ID_SENSOR_ADC:
            App_OnAdcReading(data);
            break;
        case CAN_ID_SENSOR_TEMP:
            App_OnTemperature(data);
            break;
        default:
            /* Unknown or unfiltered frame — log and discard */
            break;
    }
}

Error Handling

CAN defines a rigorous error-state machine based on two 8-bit error counters: the Transmit Error Counter (TEC) and the Receive Error Counter (REC). Understanding this state machine is critical for diagnosing field failures and writing robust recovery logic.

  • Error Active (TEC/REC < 128): normal operation. The node participates fully and sends active error frames when it detects an error.
  • Error Passive (TEC/REC ≥ 128): the node is degraded. It sends passive error frames (no dominant bit stuffing) and must wait 8 more consecutive recessive bits before retransmitting — effectively reducing its bus load automatically.
  • Bus-Off (TEC ≥ 256): the node is electrically disconnected from the bus by the CAN controller. It cannot transmit or receive until recovery. Recovery requires detecting 128 × 11 consecutive recessive bits (either automatically or manually triggered).

The ESR register on STM32 contains a snapshot of error type flags: stuff error, form error, acknowledgement error, bit recessive error, bit dominant error, CRC error, as well as TEC and REC values. Always log the full ESR in your error callback for post-mortem analysis.

/* CAN Error Callback — reads ESR register, logs over UART, initiates recovery */
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
    uint32_t err  = HAL_CAN_GetError(hcan);
    uint32_t esr  = hcan->Instance->ESR;
    uint8_t  tec  = (uint8_t)((esr >> 16) & 0xFF);
    uint8_t  rec  = (uint8_t)((esr >> 24) & 0xFF);

    /* Log error details over UART */
    char buf[128];
    int len = snprintf(buf, sizeof(buf),
        "[CAN ERR] err=0x%08lX ESR=0x%08lX TEC=%u REC=%u\r\n",
        err, esr, tec, rec);
    HAL_UART_Transmit(&huart2, (uint8_t *)buf, (uint16_t)len, 10);

    if (err & HAL_CAN_ERROR_ACK)
    {
        /* No node acknowledged our frame — check termination resistors */
        Log_Error("CAN: ACK error — verify bus termination");
    }
    if (err & HAL_CAN_ERROR_STUFF)
    {
        Log_Error("CAN: Bit stuff error — check bit rate mismatch");
    }
    if (err & HAL_CAN_ERROR_CRC)
    {
        Log_Error("CAN: CRC error — possible electrical noise");
    }

    /* Bus-off recovery: manual restart after 128 x 11 recessive bits */
    if (err & HAL_CAN_ERROR_BOF)
    {
        Log_Error("CAN: Bus-Off — scheduling recovery");
        /* Set a flag; recovery is handled in main loop to avoid
         * calling HAL_CAN_Start from interrupt context */
        g_canBusOffPending = 1;
    }
}

/* Call periodically from main loop */
void CAN_ServiceBusOff(CAN_HandleTypeDef *hcan)
{
    if (g_canBusOffPending)
    {
        g_canBusOffPending = 0;
        /* Wait ~130 ms (>= 128 * 11 bits at 1 Mbit/s + margin) */
        HAL_Delay(130);
        if (HAL_CAN_Start(hcan) == HAL_OK)
        {
            Log_Info("CAN: Bus-Off recovery successful");
        }
    }
}

FDCAN (G4, H7, U5)

STM32 devices from the G0/G4/H7/L5/U5 families feature the Bosch M_CAN IP block, which ST calls FDCAN. Its architecture differs significantly from bxCAN: instead of fixed hardware mailboxes, it uses a message RAM — a block of SRAM partitioned at initialisation time into Tx queue, Tx FIFO, Rx FIFO0, Rx FIFO1, and Rx Buffers. This gives you fine-grained control over buffer sizes.

CAN FD adds two key capabilities to classical CAN:

  • Larger payload: up to 64 bytes per frame (vs 8 bytes in classical CAN).
  • Bit Rate Switch (BRS): the arbitration phase runs at the nominal (lower) bit rate for compatibility and arbitration correctness. After the arbitration phase, if the BRS flag is set, the data phase runs at a higher bit rate — up to 8 Mbit/s on STM32G4/H7.

The FDCAN_HandleTypeDef init requires you to specify both the nominal (arbitration) bit timing and the data bit timing separately when BRS is used. The data prescaler is independent of the nominal prescaler, allowing precise fractional speed ratios.

/* FDCAN on STM32G4 — 1 Mbit/s arbitration + 4 Mbit/s data (BRS)
 * FDCAN1 kernel clock = 170 MHz (PLL Q output on STM32G4)
 * Nominal: 170 MHz / (5 * (1 + 27 + 6)) = 1 000 000 bit/s
 * Data:    170 MHz / (1 * (1 + 37 + 4))  = 4 047 619 bit/s (~4 Mbit/s)
 */
FDCAN_HandleTypeDef hfdcan1;

void MX_FDCAN1_Init(void)
{
    hfdcan1.Instance                  = FDCAN1;
    hfdcan1.Init.ClockDivider         = FDCAN_CLOCK_DIV1;
    hfdcan1.Init.FrameFormat          = FDCAN_FRAME_FD_BRS;

    /* Nominal bit timing (arbitration phase) */
    hfdcan1.Init.NominalPrescaler     = 5;
    hfdcan1.Init.NominalSyncJumpWidth = 1;
    hfdcan1.Init.NominalTimeSeg1      = 27;  /* Prop + PhSeg1 */
    hfdcan1.Init.NominalTimeSeg2      = 6;

    /* Data bit timing (high-speed phase) */
    hfdcan1.Init.DataPrescaler        = 1;
    hfdcan1.Init.DataSyncJumpWidth    = 4;
    hfdcan1.Init.DataTimeSeg1         = 37;
    hfdcan1.Init.DataTimeSeg2         = 4;

    /* Message RAM allocation */
    hfdcan1.Init.StdFiltersNbr        = 8;
    hfdcan1.Init.ExtFiltersNbr        = 2;
    hfdcan1.Init.TxFifoQueueMode      = FDCAN_TX_FIFO_OPERATION;
    hfdcan1.Init.RxFifo0ElmtsNbr      = 8;
    hfdcan1.Init.RxFifo0ElmtSize      = FDCAN_DATA_BYTES_64;
    hfdcan1.Init.TxElmtsNbr           = 4;
    hfdcan1.Init.TxElmtSize           = FDCAN_DATA_BYTES_64;

    if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) { Error_Handler(); }
    HAL_FDCAN_Start(&hfdcan1);
}

/* Transmit a 64-byte CAN FD frame with BRS */
void FDCAN_SendLargeFrame(FDCAN_HandleTypeDef *hfdcan,
                          uint32_t id, const uint8_t *payload)
{
    FDCAN_TxHeaderTypeDef txHdr = {
        .Identifier          = id,
        .IdType              = FDCAN_STANDARD_ID,
        .TxFrameType         = FDCAN_DATA_FRAME,
        .DataLength          = FDCAN_DLC_BYTES_64,
        .ErrorStateIndicator = FDCAN_ESI_ACTIVE,
        .BitRateSwitch       = FDCAN_BRS_ON,
        .FDFormat            = FDCAN_FD_CAN,
        .TxEventFifoControl  = FDCAN_NO_TX_EVENTS,
        .MessageMarker       = 0
    };
    HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &txHdr, payload);
}

Multi-Node CANopen-Style Network

In real-world systems, CAN carries structured traffic between multiple nodes. The CANopen application-layer standard (CiA 301) defines a message taxonomy that is directly applicable even when you are not using a full CANopen stack:

  • NMT (0x000) — network management: start, stop, reset commands from master to nodes.
  • SYNC (0x080) — synchronisation pulse: triggers simultaneous PDO transmission across all nodes.
  • Emergency (0x080 + NodeID) — node-specific error broadcast.
  • PDO (0x180–0x5FF) — process data objects: real-time I/O, sensor readings, setpoints.
  • SDO (0x580–0x67F) — service data objects: configuration parameter read/write.
  • Heartbeat (0x700 + NodeID) — periodic node alive indication.

The node ID scheme ensures unique CAN IDs per node within a message class without requiring a central allocator. A simple dispatch table — an array of function pointers indexed by ID range — efficiently routes messages in a resource-constrained MCU.

/* CANopen-inspired dispatch table for a 3-node network
 * Master node ID = 1, Sensor A = 2, Sensor B = 3
 */
#define CANOPEN_HEARTBEAT_BASE  0x700U
#define CANOPEN_PDO1_TX_BASE    0x180U
#define CANOPEN_SDO_RX_BASE     0x600U
#define CANOPEN_SDO_TX_BASE     0x580U

typedef void (*CanMsgHandler_t)(uint8_t nodeId,
                                const uint8_t *data, uint8_t dlc);

typedef struct {
    uint32_t        baseId;
    uint32_t        mask;       /* 0x780 = top 4 bits of 11-bit ID */
    CanMsgHandler_t handler;
} CanRouteEntry_t;

static const CanRouteEntry_t s_routes[] = {
    { CANOPEN_HEARTBEAT_BASE, 0x780U, OnHeartbeat      },
    { CANOPEN_PDO1_TX_BASE,   0x780U, OnPdo1Received   },
    { CANOPEN_SDO_RX_BASE,    0x780U, OnSdoRequest     },
    { CANOPEN_SDO_TX_BASE,    0x780U, OnSdoResponse    },
};
#define NUM_ROUTES  (sizeof(s_routes) / sizeof(s_routes[0]))

/* Route an incoming message to the correct handler */
void CAN_RouteMessage(uint32_t id, const uint8_t *data, uint8_t dlc)
{
    for (size_t i = 0; i < NUM_ROUTES; i++)
    {
        if ((id & s_routes[i].mask) == s_routes[i].baseId)
        {
            uint8_t nodeId = (uint8_t)(id & ~s_routes[i].mask);
            s_routes[i].handler(nodeId, data, dlc);
            return;
        }
    }
    /* No route found */
    CAN_LogUnknownFrame(id, data, dlc);
}

/* Heartbeat handler — update node liveness table */
static void OnHeartbeat(uint8_t nodeId, const uint8_t *data, uint8_t dlc)
{
    if (nodeId > 0 && nodeId <= MAX_NODES)
    {
        s_nodeTable[nodeId].lastHeartbeatTick = HAL_GetTick();
        s_nodeTable[nodeId].state            = data[0];
    }
}

Exercises

Exercise 1 Beginner

Two-Board CAN Heartbeat

Configure bxCAN at 500 kbit/s between two STM32 boards (or one STM32 and a USB-CAN adapter such as PEAK PCAN-USB or Canable). Board A sends an 8-byte heartbeat frame on ID 0x001 every 100 ms containing a 4-byte tick counter and a 4-byte sequence number. Board B receives the frame, validates the sequence number for continuity, and toggles an LED on each valid reception. Connect a CAN bus analyser (candump on Linux, PCAN-View on Windows) to verify frame timing and data integrity.

bxCAN Bit Timing candump
Exercise 2 Intermediate

Multi-Sensor CAN Producer-Consumer

Implement a producer-consumer CAN system. Three virtual sensor tasks (ADC reading on 0x123, temperature on 0x124, encoder count on 0x125) each publish their reading every 50 ms using bxCAN TX mailboxes. A master node configures filter bank 0 in mask mode to accept IDs 0x120–0x12F (mask 0x7F0) into FIFO0. The master receives all three sensor IDs in the FIFO0 callback, assembles a combined 8-byte packet, and forwards it over UART as a JSON string. Verify with a terminal emulator at 115200 baud.

CAN Filters FIFO UART Bridge
Exercise 3 Advanced

CAN FD Bootloader Protocol on STM32G4

Build a CAN FD firmware update protocol on STM32G4 (NUCLEO-G474RE). The master PC-side tool reads a binary file and sends it as 48-byte firmware pages over CAN FD frames (ID 0x100, BRS enabled, 4 Mbit/s data phase). Each page includes a 2-byte page index, 2-byte CRC-16, and 44 bytes of code. The node validates the CRC, writes the page to internal flash using HAL_FLASH_Program in word mode, and replies with an ACK (0x101, 1 byte: 0=OK, 1=CRC fail) over CAN FD. After all pages are received, the node verifies the complete image SHA-256 hash and sends a final JUMP command acknowledgement before jumping to the new application.

CAN FD FDCAN Bootloader Flash Programming

CAN Design Tool

Use this tool to document your CAN network design — peripheral instance, bit rate, filter configuration, and message map. Download as Word, Excel, PDF, or PPTX for project documentation and team reviews.

STM32 CAN Design Generator

Document your CAN network configuration — peripheral, bit rate, filter banks, and message map. Download as Word, Excel, PDF, or PPTX.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Next Steps

In this article we have built a complete picture of CAN Bus on STM32:

  • Protocol fundamentals: differential signalling, multi-master arbitration, and the three CAN variants (2.0A, 2.0B, FD) that you will encounter across automotive, industrial, and robotics applications.
  • Bit timing: calculating prescaler, BS1, and BS2 from APB1 clock to achieve exact bit rates with an 85–87% sample point — the most common bring-up failure point.
  • Hardware message filtering: configuring bxCAN's 28 filter banks in mask and list modes, at 16-bit and 32-bit scales, routed to FIFO0 or FIFO1 — allowing the CPU to completely ignore traffic it does not need.
  • Transmit and receive: mailbox management, interrupt-driven FIFO reading in HAL_CAN_RxFifo0MsgPendingCallback, and ID-based dispatch tables.
  • Error handling: the TEC/REC error state machine, ESR register analysis, and manual vs. automatic bus-off recovery strategies.
  • FDCAN: message RAM partitioning, dual bit rate (BRS), and 64-byte frames on the STM32G4/H7/U5 families.
  • Multi-node design: CANopen-inspired ID assignment, heartbeat monitoring, and dispatch tables that scale to dozens of nodes.

Next in the Series

In Part 13: USB CDC Virtual COM Port, we turn the STM32 into a native USB serial device — covering USB enumeration, CDC bulk data transfer, baud rate change callbacks, and building a high-throughput virtual COM port that outperforms traditional UART bridges.

Technology