Chapter 11 — CAN RX and TX Pipelines

The receive and transmit pipelines convert between raw CAN frames (8 bytes max) and OpenLCB messages (up to 512 bytes). The RX path classifies, assembles multi-frame messages, and pushes to the OpenLCB FIFO. The TX path fragments OpenLCB messages into CAN frames and transmits them.

Source files: can_rx_statemachine.c, can_rx_message_handler.c, can_tx_statemachine.c, can_tx_message_handler.c (all in src/drivers/canbus/)

11.1 RX: Frame Classification

CanRxStatemachine_incoming_can_driver_callback() is called directly from the CAN receive ISR (or receive thread). It first checks whether the frame is an OpenLCB message or a CAN control frame:

flowchart TD A["CAN frame received"] --> B{"is_openlcb_message?\n(bit 28 set)"} B -- "Yes" --> C{"Frame subtype\n(bits 27:24)"} B -- "No" --> D{"Sequence number\n== 0?"} C -- "Standard" --> E{"Addressed?\n(dest addr bit)"} C -- "Datagram Only" --> F["single_frame(BASIC)"] C -- "Datagram First" --> G["first_frame(DATAGRAM)"] C -- "Datagram Middle" --> H["middle_frame()"] C -- "Datagram Final" --> I["last_frame()"] C -- "Stream" --> J["stream_frame(STREAM)"] E -- "Yes" --> K["Check framing bits\nand dispatch"] E -- "No" --> L["Unaddressed handler"] D -- "Yes" --> M["Variable-field:\nRID/AMD/AME/AMR/Error"] D -- "No" --> N["CID frame\n(CID7..CID1)"] style A fill:#4a90d9,color:#fff
ISR context: The RX callback runs in interrupt context (or a high-priority thread). It must complete quickly. Buffer allocation and FIFO pushes are designed to be lock-free from the RX side.

11.2 RX: Multi-Frame Assembly

OpenLCB messages that exceed 8 bytes are split across multiple CAN frames. The RX path reassembles them using the BufferList as scratch space:

Framing Bits (addressed messages)

The high nibble of payload byte 0 carries framing bits for addressed standard frames:

ValueMeaningHandler
0x00Only frame (complete message)single_frame()
0x10First frame of multi-framefirst_frame()
0x20Middle framemiddle_frame()
0x30Last framelast_frame()

Assembly Flow

  1. first_frame: Allocate an OpenLCB buffer (sized by type: BASIC, SNIP, or DATAGRAM). Load the message header (source alias, dest alias, MTI). Copy payload bytes. Set state.inprocess = true, stamp timer.assembly_ticks, and add to the BufferList.
  2. middle_frame: Find the in-progress buffer in BufferList by source/dest/MTI. Check the timeout (30 ticks = 3 seconds). Append payload bytes.
  3. last_frame: Find the buffer, append final bytes, clear state.inprocess, release from BufferList, and push to the OpenLCB FIFO.

Assembly Timeout

If a middle or last frame arrives more than 30 ticks (3 seconds) after the first frame, the assembly is considered stale. The buffer is freed and a reject message is sent. The timeout constant is CAN_RX_INPROCESS_TIMEOUT_TICKS = 30.

11.3 RX: Datagram Assembly

Datagrams use a different framing mechanism than standard messages. Instead of framing bits in the payload, the CAN frame type field (bits 27:24) indicates the position:

The assembly logic is the same (first/middle/last handlers) but with offset = 0 since the destination alias is in the CAN identifier rather than in the payload.

11.4 RX: Legacy SNIP

Early SNIP implementations did not include framing bits. The handler CanRxMessageHandler_can_legacy_snip() uses null-byte counting to determine frame boundaries: a SNIP reply is complete when 6 null bytes have been accumulated across all frames.

11.5 TX: Message Routing

CanTxStatemachine_send_openlcb_message() is the TX entry point. It routes each outgoing OpenLCB message to the correct frame handler:

flowchart TD A["send_openlcb_message()"] --> V{"state.invalid?"} V -- "Yes" --> DONE["return true (discard)"] V -- "No" --> R{"dest_alias == 0\nand dest_id != 0?"} R -- "Yes" --> RL["Resolve via\nlistener_find_by_node_id"] R -- "No" --> TX RL -- "Found" --> TX RL -- "Not found" --> DONE TX{"is_tx_buffer_empty()?"} TX -- "No" --> BUSY["return false (retry)"] TX -- "Yes" --> B{"Addressed?"} B -- "Yes" --> C{"MTI type"} B -- "No" --> D["unaddressed_msg_frame()"] C -- "MTI_DATAGRAM" --> E["datagram_frame()"] C -- "Stream MTIs" --> F["stream_frame()"] C -- "Other" --> G["addressed_msg_frame()"] style A fill:#4a90d9,color:#fff

11.6 TX: Fragmentation

The TX handlers fragment messages into CAN frames, looping until the entire payload is sent as an atomic sequence:

Datagram Fragmentation

Datagrams can be up to 72 bytes. The frame type selection in CanTxMessageHandler_datagram_frame():

ConditionFrame Type
Total payload <= 8 bytesDatagram Only
payload_index < 8Datagram First
More data remains after this frameDatagram Middle
This frame completes the payloadDatagram Last

Addressed Message Fragmentation

Standard addressed messages carry the 12-bit destination alias in payload bytes 0-1, leaving only 6 bytes per frame for actual data. The framing bits are set in the high nibble of byte 0:

ConditionFraming Bits
Total payload <= 6 bytesMULTIFRAME_ONLY (0x00)
payload_index < 6MULTIFRAME_FIRST (0x10)
More data remainsMULTIFRAME_MIDDLE (0x20)
Last frameMULTIFRAME_FINAL (0x30)

11.7 TX: CAN Header Construction

CAN identifiers are built from templates. For example, a datagram-only frame:

identifier = RESERVED_TOP_BIT
           | CAN_OPENLCB_MSG
           | CAN_FRAME_TYPE_DATAGRAM_ONLY
           | (dest_alias << 12)
           | source_alias;

A standard addressed frame places the MTI (not the destination alias) in bits 23:12. The destination alias goes in payload bytes 0-1 instead.

11.8 TX: Retry Mechanism

Before transmitting, the TX state machine checks is_tx_buffer_empty(). If the hardware TX buffer is busy, the function returns false and the caller retries on the next main-loop iteration. For multi-frame sequences, the payload_index is only advanced on successful transmission, ensuring no data is lost.

11.9 RX: CAN Control Frame Handling

Control frames are handled by dedicated functions in can_rx_message_handler.c:

FrameHandlerAction
CIDcid_frame()If alias matches one of ours, reply with RID to defend the alias.
RIDrid_frame()Check for duplicate alias; flag if found.
AMDamd_frame()Check duplicate; update listener alias table; release held attach messages.
AMEame_frame()Check duplicate; respond with AMD for matching aliases.
AMRamr_frame()Check duplicate; scrub BufferList and FIFO for stale source alias; clear listener table.
Error Infoerror_info_report_frame()Check for duplicate alias.