FlexCAN Deep Dive — Mailboxes & Filtering¶
Level 1's module 7 gave you the CAN protocol and a working TX/RX pair. That is enough for a demo node and nowhere near enough for an ECU sharing a 500 kbit/s bus with twenty other modules. Production firmware answers harder questions: which mailbox catches which frames, what happens when frames arrive faster than the CPU drains them, and what the node does when the bus turns hostile. This module walks the FlexCAN peripheral mailbox-by-mailbox and register-by-register, then wires the error path that keeps a faulty node from taking the whole bus down with it.
The message buffer RAM¶
FlexCAN's core is a block of RAM organized as message buffers (MBs).
On the S32K144, CAN0 has 32 MBs; CAN1 and CAN2 have 16 — and the count
depends on payload size, because the RAM is fixed. With 8-byte payloads an
MB costs 16 bytes; switch to CAN FD 64-byte payloads and one MB costs 72
bytes, so the same RAM holds far fewer. Choosing payload in the driver
config is a budgeting decision, not a cosmetic one.
Each MB is four-plus words:
word 0 : CS — CODE, SRR, IDE, RTR, DLC, TIME STAMP
word 1 : ID — 11-bit standard ID (bits 28:18) or 29-bit extended
word 2+: DATA — payload, big-endian (data[0] is first byte on the wire)
The CODE field in CS is the whole state machine. The values you will actually meet:
| CODE | Direction | Meaning |
|---|---|---|
0b0000 |
RX | INACTIVE — MB is not participating |
0b0100 |
RX | EMPTY — armed and waiting for a match |
0b0010 |
RX | FULL — a frame landed; read it and re-arm |
0b0110 |
RX | OVERRUN — a second frame arrived before you read the first |
0b1010 |
RX | RANSWER — auto-answer a remote frame (rarely used) |
0b1000 |
TX | INACTIVE — MB is free for a new transmission |
0b1100 |
TX | DATA — transmit this frame once the bus is free |
0b1001 |
TX | ABORT — cancel a pending transmission |
Two hardware rules bite people who go straight to registers:
- Reading CS locks the MB. Hardware will not overwrite a locked MB.
You unlock it by reading the free-running TIMER register, or by
locking a different MB. Forget the unlock and that mailbox quietly stops
receiving.
FLEXCAN_DRV_Receivedoes this for you. - Writing CODE last commits the MB. Fill ID and data first, then write CS — otherwise the engine can transmit a half-built frame.
Filtering: masks that actually match¶
An RX mailbox matches an incoming ID through a mask, and the convention is the one people get backwards:
mask bit = 1 → this ID bit MUST match the mailbox's ID
mask bit = 0 → "don't care" — accept either value
FlexCAN offers several mask scopes, selected by configuration:
| Register | SDK call | Scope |
|---|---|---|
RXMGMASK |
FLEXCAN_DRV_SetRxMbGlobalMask |
One mask for all RX MBs |
RX14MASK / RX15MASK |
— | Private masks for MB14 and MB15 |
RXIMR[n] |
FLEXCAN_DRV_SetRxIndividualMask |
One mask per mailbox |
RXFGMASK |
FLEXCAN_DRV_SetRxFifoGlobalMask |
Mask for RX FIFO filter elements |
Individual masks require MCR[IRMQ]; the driver sets it when you select
the mask type. A worked example — one mailbox accepting the whole 0x31x
block of our Level 1 node family:
#include "flexcan_driver.h"
#define INST_CAN0 0u
#define MB_CMD_RX 1u
void can_filters_init(void)
{
/* Per-mailbox masks instead of one global mask */
FLEXCAN_DRV_SetRxMaskType(INST_CAN0, FLEXCAN_RX_MASK_INDIVIDUAL);
/* Standard IDs sit in bits 28:18 of the mask word.
0x7F0 << 18 => match the top 7 bits, ignore the low 4 =>
IDs 0x310..0x31F all land in this mailbox. */
FLEXCAN_DRV_SetRxIndividualMask(INST_CAN0, FLEXCAN_MSG_ID_STD,
MB_CMD_RX, (0x7F0u << 18));
flexcan_data_info_t rxInfo = {
.msg_id_type = FLEXCAN_MSG_ID_STD,
.data_length = 8u,
.is_remote = false,
};
FLEXCAN_DRV_ConfigRxMb(INST_CAN0, MB_CMD_RX, &rxInfo, 0x310u);
}
The shift is not optional
Standard IDs live in the upper bits of the ID word. A mask written
as plain 0x7F0 masks nothing useful and you will receive either
everything or nothing. Write (mask << 18) for 11-bit IDs; extended
29-bit IDs use the word unshifted.
RX FIFO: when frames outrun the CPU¶
A mailbox holds exactly one frame. If your node consumes ten different IDs
arriving in bursts, dedicating ten mailboxes and servicing ten interrupts
is wasteful. The RX FIFO is the answer: a six-deep hardware queue
occupying the MB0–MB5 region, fed by an ID filter table starting at
MB6 and growing with CTRL2[RFFN] (8, 16, … up to 128 elements — each
extra step consumes more mailbox RAM).
Filter element formats:
| Format | Elements per word | Matches |
|---|---|---|
| A | 1 | One full standard or extended ID + IDE/RTR bits |
| B | 2 | Two partial IDs (full 11-bit standard, or upper bits of extended) |
| C | 4 | Four 8-bit ID slices — coarse, mask-like |
| D | — | Reject all |
static flexcan_state_t canState;
static flexcan_id_table_t rxFifoFilters[8] = {
{ .isRemoteFrame = false, .isExtendedFrame = false, .id = 0x310u },
{ .isRemoteFrame = false, .isExtendedFrame = false, .id = 0x311u },
{ .isRemoteFrame = false, .isExtendedFrame = false, .id = 0x320u },
{ .isRemoteFrame = false, .isExtendedFrame = false, .id = 0x321u },
/* Unused slots: repeat a used ID rather than leaving 0x000 — an
accidental 0x000 filter accepts the highest-priority ID on the
bus and floods your FIFO. */
{ false, false, 0x321u }, { false, false, 0x321u },
{ false, false, 0x321u }, { false, false, 0x321u },
};
void can_fifo_init(void)
{
flexcan_user_config_t cfg;
FLEXCAN_DRV_GetDefaultConfig(&cfg);
cfg.max_num_mb = 16u;
cfg.is_rx_fifo_needed = true;
cfg.num_id_filters = FLEXCAN_RX_FIFO_ID_FILTERS_8;
cfg.flexcanMode = FLEXCAN_NORMAL_MODE;
cfg.payload = FLEXCAN_PAYLOAD_SIZE_8;
FLEXCAN_DRV_Init(INST_CAN0, &canState, &cfg);
FLEXCAN_DRV_ConfigRxFifo(INST_CAN0, FLEXCAN_RX_FIFO_ID_FORMAT_A,
rxFifoFilters);
FLEXCAN_DRV_SetRxFifoGlobalMask(INST_CAN0, FLEXCAN_MSG_ID_STD,
(0x7FFu << 18)); /* exact match */
}
The FIFO raises three separate flags in IFLAG1: bit 5 frames
available, bit 6 FIFO warning (nearly full), bit 7 overflow — a
frame was lost. Treat bit 7 as a defect, not a status: it means software
missed a deadline. Count it, export it, and treat it as evidence during
timing analysis.
Interrupt-driven handling¶
Polling FLEXCAN_DRV_GetTransferStatus is fine for a 100 ms status frame
and hopeless on a busy bus. Install a callback:
static volatile uint32_t g_rxFifoOverflows;
static flexcan_msgbuff_t g_rxMsg;
static void can_event_cb(uint8_t instance, flexcan_event_type_t eventType,
uint32_t buffIdx, flexcan_state_t *state)
{
(void)state;
switch (eventType) {
case FLEXCAN_EVENT_RXFIFO_COMPLETE:
app_queue_frame(&g_rxMsg); /* copy out fast */
FLEXCAN_DRV_RxFifo(instance, &g_rxMsg); /* re-arm the FIFO */
break;
case FLEXCAN_EVENT_RXFIFO_OVERFLOW:
g_rxFifoOverflows++; /* a frame was lost */
break;
case FLEXCAN_EVENT_RX_COMPLETE:
app_mailbox_ready(buffIdx);
break;
case FLEXCAN_EVENT_TX_COMPLETE:
app_tx_done(buffIdx);
break;
default:
break; /* nothing to do */
}
}
void can_irq_init(void)
{
FLEXCAN_DRV_InstallEventCallback(INST_CAN0, can_event_cb, NULL);
FLEXCAN_DRV_RxFifo(INST_CAN0, &g_rxMsg); /* first arm */
}
The callback runs in interrupt context. Do the minimum: copy the frame into an application queue, set a flag, return. Parsing signals, running control logic, or anything blocking belongs in the task that consumes the queue — module 4's FreeRTOS lesson makes that split explicit.
Error counters and bus-off¶
Every CAN node maintains two counters in ECR: TXERRCNT and
RXERRCNT. Transmit errors cost 8, receive errors cost 1, successes
decrement. The thresholds are protocol-defined, not vendor-specific:
| State | Condition | Behaviour |
|---|---|---|
| Error active | both counters < 128 | Sends active (dominant) error flags |
| Error passive | either counter ≥ 128 | Sends passive (recessive) error flags — cannot destroy other nodes' frames |
| Bus off | TXERRCNT > 255 | Node disconnects itself from the bus |
ESR1[FLTCONF] reports the current state and ESR1[BOFFINT] fires on the
transition. Recovery from bus-off requires 128 occurrences of 11
consecutive recessive bits — about 1408 bit times, roughly 2.8 ms of
quiet bus at 500 kbit/s. MCR[BOFFREC] controls whether that happens
automatically: cleared = automatic recovery enabled, set = software
must intervene. The double negative catches everyone once.
static volatile uint32_t g_busOffCount;
static void can_error_cb(uint8_t instance, flexcan_event_type_t eventType,
flexcan_state_t *state)
{
(void)state;
if (eventType == FLEXCAN_EVENT_ERROR) {
uint32_t esr1 = FLEXCAN_DRV_GetErrorStatus(instance);
if ((esr1 & CAN_ESR1_BOFFINT_MASK) != 0u) {
g_busOffCount++;
app_set_dtc(DTC_CAN_BUS_OFF); /* module 9: report it */
app_enter_can_safe_state(); /* consumers time out */
}
}
}
The practice around bus-off is worth spelling out, because it appears in most OEM software specifications:
- Never spin waiting for recovery. The rest of the ECU must keep running its safe-state logic while CAN is down.
- Rate-limit re-initialization. A node that resets its CAN controller in a tight loop against a shorted bus becomes exactly the babbling idiot the protocol was designed to contain. Back off progressively — 10 ms, 100 ms, 1 s — then latch a fault.
- Distinguish "bus is broken" from "I am broken." Persistent TX errors alongside a healthy RX counter point at your own transceiver, stub, or termination, not at the network.
- Abort, do not orphan. If a periodic frame is superseded before it
ever wins arbitration,
FLEXCAN_DRV_AbortTransfer(requiresMCR[AEN]) cancels it so the bus never carries stale data. - Disable self-reception (
MCR[SRXDIS]) unless you genuinely want to hear your own frames — otherwise every transmission also consumes an RX slot and confuses timeout logic.
Cheat sheet¶
| Item | Notes |
|---|---|
| MB count | S32K144: CAN0 = 32 MBs, CAN1/CAN2 = 16 — fewer with 64-byte FD payloads |
| MB layout | CS (CODE/IDE/RTR/DLC) · ID · DATA; write CODE last |
| RX CODEs | EMPTY 0b0100 → FULL 0b0010 → OVERRUN 0b0110 if not drained |
| Lock rule | Reading CS locks the MB; read TIMER to unlock |
| Mask polarity | 1 = must match, 0 = don't care |
| Standard IDs | Shift by 18 in ID and mask words (0x7FF << 18) |
| Mask scopes | RXMGMASK global · RXIMR[n] individual (MCR[IRMQ]) · RXFGMASK FIFO |
| RX FIFO | 6 deep, occupies MB0–5, filter table from MB6, size via CTRL2[RFFN] |
| FIFO flags | IFLAG1 bit 5 available · bit 6 warning · bit 7 overflow = frame lost |
| Error counters | ECR TXERRCNT/RXERRCNT; ≥128 error-passive, TX >255 bus-off |
| Bus-off recovery | 128 × 11 recessive bits; MCR[BOFFREC] cleared = automatic |
| Callback discipline | Copy and return — no parsing, no blocking, no logging in the ISR |
How It Actually Works¶
FlexCAN's message buffers (MBs) aren't a software queue abstraction — each MB is a fixed region of dedicated RAM inside the FlexCAN module itself, with a hardware matching engine that continuously compares incoming frame IDs against every RX MB's ID/mask filter in parallel, in the same clock cycle a frame's identifier finishes arriving off the wire. This parallel-compare-against-all-filters architecture is why FlexCAN can accept or reject frames at full bus speed without the CPU examining every ID that appears on the bus — filtering is done by dedicated comparator logic per buffer (or per group, when using the Rx FIFO's ID-table filtering), not a software if chain.
The MB CODE field is a tiny hardware state machine, not just a status flag: CODE=0x4 (EMPTY) tells the matching engine this RX buffer is available to be filled; the instant a matching frame is fully received (including passing CRC), hardware atomically writes the payload, timestamp, and ID into the buffer and flips CODE to 0x2 (FULL) — and it does this as one indivisible hardware transaction specifically so a CPU reading a buffer never sees a torn/half-updated frame, even if a second matching frame starts arriving on the bus in the very next bit.
For transmit, arbitration among your own pending TX buffers (before they even reach bus arbitration against other nodes) is handled by FlexCAN's internal priority logic — when multiple local MBs are marked CODE=0xC (ready to transmit), the module's own arbitration picks the lowest-ID one first, matching how the CAN bus itself would arbitrate, so local buffer scheduling and network-level arbitration follow the same priority rule end to end.
(Described from the S32K reference manual's FlexCAN chapter; not measured on physical silicon in this course.)
Exercise¶
Extend your Level 1 capstone node to use the RX FIFO instead of dedicated
mailboxes, and give it a real error path. Specifically: (1) configure an
eight-element format-A filter table for four IDs your node consumes, and
prove with candump that a fifth, unlisted ID never reaches your
dispatcher; (2) deliberately leave one filter entry as 0x000 on a busy
bus, observe what floods in, then fix it — that is a mistake worth making
once, cheaply; (3) instrument FLEXCAN_EVENT_RXFIFO_OVERFLOW with a
counter exported in your status frame, then cause an overflow on purpose
by stalling your dispatcher for 5 ms during a burst; (4) implement bus-off
handling with the three-strikes backoff above, and test it by disconnecting
the second node's termination. Finally, write one paragraph explaining how
your design distinguishes a broken bus from a broken node — that paragraph
is the part a reviewer will actually read.