Chapter 24 — Data Flow Walkthrough
This chapter traces messages through the library end-to-end, from CAN frame arrival to protocol handler response and back out to the bus. Use these traces as a map when debugging or understanding how the pieces fit together.
24.1 The Five State Machines
| State Machine | Layer | File | Purpose |
|---|---|---|---|
| CAN Main SM | CAN Transport | can_main_statemachine.c | CAN frame dispatch, alias conflict, CAN login orchestration |
| CAN Login SM | CAN Transport | can_login_statemachine.c | Per-node CAN alias allocation (CID/RID/AMD) |
| OpenLCB Login SM | Core | openlcb_login_statemachine.c | Per-node OpenLCB login (Init Complete, event announcements) |
| OpenLCB Main SM | Core | openlcb_main_statemachine.c | Protocol message dispatch to handlers |
| CAN RX SM | CAN Transport | can_rx_statemachine.c | CAN frame classification and assembly (runs in ISR context) |
24.2 Main Loop Calling Order
flowchart TD
ML["OpenLcb_run()"] --> A["CanMainStateMachine_run()"]
A --> B["OpenLcbLoginMainStatemachine_run()"]
B --> C["OpenLcbMainStatemachine_run()"]
C --> D["_run_periodic_services()"]
subgraph CAN_SM["CAN Main SM Priority"]
A1["1. Handle duplicate aliases"]
A2["2. Send outgoing CAN frame"]
A3["3. Send login CAN frame"]
A4["4. Enumerate first/next node for login"]
end
subgraph OLCB_SM["OpenLCB Main SM Priority"]
C1["1. Send outgoing OpenLCB message"]
C2["2. Re-enumerate (multi-response)"]
C3["3. Pop incoming from FIFO"]
C4["4. Enumerate first node"]
C5["5. Enumerate next node"]
end
A --> CAN_SM
C --> OLCB_SM
style ML fill:#f3e5f5,stroke:#7b1fa2
style A fill:#fff3e0,stroke:#e65100
style B fill:#e3f2fd,stroke:#1565c0
style C fill:#e3f2fd,stroke:#1565c0
style D fill:#fce4ec,stroke:#c62828
24.3 Trace 1: Incoming CAN Frame to Protocol Response
This trace follows a single-frame Verify Node ID (Global) message from arrival to response.
sequenceDiagram
participant HW as CAN Hardware
participant ISR as CAN RX ISR
participant FIFO as CAN RX FIFO
participant CANSM as CAN Main SM
participant RXSM as CAN RX SM
participant OLCB_FIFO as OpenLCB FIFO
participant OLCB_SM as OpenLCB Main SM
participant Handler as Protocol Handler
participant TXSM as CAN TX SM
participant Bus as CAN Bus
HW->>ISR: CAN interrupt fires
ISR->>FIFO: Push raw can_msg_t
Note over ISR: Returns to main loop
CANSM->>FIFO: Pop can_msg_t
CANSM->>RXSM: Classify frame
Note over RXSM: Single frame, OpenLCB message
RXSM->>RXSM: Allocate openlcb_msg_t (BASIC)
RXSM->>RXSM: Extract MTI, aliases, Node IDs
RXSM->>OLCB_FIFO: Push openlcb_msg_t
OLCB_SM->>OLCB_FIFO: Pop openlcb_msg_t
Note over OLCB_SM: MTI = 0x0490 (Verify Node ID Global)
OLCB_SM->>OLCB_SM: Enumerate first node
OLCB_SM->>Handler: ProtocolMessageNetwork_handle_verify_node_id_global()
Handler->>Handler: Build Verified Node ID response
Handler->>OLCB_SM: Response in worker buffer
OLCB_SM->>TXSM: CanTxStatemachine_send_openlcb_message()
TXSM->>TXSM: Convert openlcb_msg_t to CAN frame(s)
TXSM->>Bus: Transmit CAN frame
Note over OLCB_SM: Enumerate next node (repeat for all nodes)
24.4 Trace 2: Multi-Frame Datagram Assembly
sequenceDiagram
participant Bus as CAN Bus
participant RX as CAN RX Handler
participant List as Buffer List
participant FIFO as OpenLCB FIFO
participant SM as OpenLCB Main SM
Bus->>RX: Datagram First frame (8 bytes)
RX->>RX: Allocate openlcb_msg_t (DATAGRAM)
RX->>RX: Set state.inprocess = true
RX->>RX: Copy payload bytes, set timer.assembly_ticks
RX->>List: Store in Buffer List (in-flight)
Bus->>RX: Datagram Middle frame (8 bytes)
RX->>List: Find matching entry (by source alias)
RX->>RX: Append payload bytes
Bus->>RX: Datagram Final frame (2-8 bytes)
RX->>List: Find matching entry
RX->>RX: Append final bytes
RX->>RX: Set state.inprocess = false
RX->>FIFO: Push completed openlcb_msg_t
SM->>FIFO: Pop completed datagram
Note over SM: Process as complete message
24.5 Trace 3: Application Sends Event
sequenceDiagram
participant App as Application
participant API as OpenLcbApplication API
participant Store as Buffer Store
participant SM as OpenLCB Main SM
participant TX as CAN TX SM
participant Bus as CAN Bus
App->>API: Send PCER (event_id)
API->>Store: Allocate openlcb_msg_t (BASIC)
API->>API: Fill MTI=0x05B4, source_alias, event_id
API->>SM: CanTxStatemachine_send_openlcb_message()
SM->>TX: Convert to CAN frame
TX->>TX: Build CAN identifier with Frame Type 1
TX->>TX: Insert event_id in 8-byte payload
TX->>Bus: Transmit single CAN frame
TX->>Store: Free openlcb_msg_t
24.6 Trace 4: Node Login (Power-On to RUN)
stateDiagram-v2
[*] --> INIT: Power on
INIT --> GENERATE_ALIAS: seed = Node ID
GENERATE_ALIAS --> CID7: alias = LFSR(seed)
CID7 --> CID6: Send CID7 frame
CID6 --> CID5: Send CID6 frame
CID5 --> CID4: Send CID5 frame
CID4 --> WAIT: Send CID4, snapshot tick
WAIT --> RID: elapsed > 200ms
RID --> AMD: Send RID
AMD --> INIT_COMPLETE: Send AMD, permitted=true
state "OpenLCB Login" as OL {
INIT_COMPLETE --> CONSUMER_EVENTS: Send Init Complete
CONSUMER_EVENTS --> PRODUCER_EVENTS: Enumerate all consumer events
PRODUCER_EVENTS --> LOGIN_COMPLETE: Enumerate all producer events
LOGIN_COMPLETE --> RUN: on_login_complete callback
}
note right of WAIT
CAN Login SM handles
INIT through AMD.
end note
note right of OL
OpenLCB Login SM handles
INIT_COMPLETE through RUN.
end note
24.7 Trace 5: Datagram Round-Trip
sequenceDiagram
participant A as Node A (sender)
participant Bus as CAN Bus
participant B as Node B (receiver)
A->>Bus: Datagram Only/First/Middle/Final
Bus->>B: Datagram assembled
B->>B: Protocol handler processes datagram
B->>Bus: Datagram Received OK (with Reply Pending flag)
Bus->>A: Datagram OK received
Note over A: Start reply timeout
B->>B: Handler prepares reply datagram
B->>Bus: Reply datagram (Only/First/Middle/Final)
Bus->>A: Reply received
A->>Bus: Datagram Received OK
Bus->>B: OK received
24.8 CAN RX Frame Classification
flowchart TD
A["Incoming CAN Frame"] --> B{Bit 27 set?}
B -->|Yes| C["OpenLCB Message Frame"]
B -->|No| D["Control Frame"]
C --> E{Frame Type bits 26-24}
E -->|001| F["Standard OpenLCB Message"]
E -->|010| G["Datagram Only"]
E -->|011| H["Datagram First"]
E -->|100| I["Datagram Middle"]
E -->|101| J["Datagram Final"]
E -->|111| K["Stream Data"]
D --> L{Control Frame Type}
L -->|CID 7-4| M["Check ID Frame"]
L -->|RID| N["Reserve ID Frame"]
L -->|AMD| O["Alias Map Definition"]
L -->|AME| P["Alias Map Enquiry"]
L -->|AMR| Q["Alias Map Reset"]
L -->|Error| R["Error Info Report"]
24.9 CAN TX Message Routing
flowchart TD
A["openlcb_msg_t to send"] --> B{Message type?}
B -->|"Global (no dest)"| C["Unaddressed frame handler"]
B -->|"Addressed"| D["Addressed frame handler"]
B -->|"Datagram"| E["Datagram frame handler"]
B -->|"Stream"| F["Stream frame handler"]
C --> G["Single CAN frame:
MTI in variable field"] D --> H{Payload fits in 1 frame?} H -->|Yes| I["Single frame with dest alias in bytes 0-1"] H -->|No| J["Multi-frame: First/Middle/Last
with framing bits"] E --> K{Payload <= 8 bytes?} K -->|Yes| L["Datagram Only frame"] K -->|No| M["Datagram First + Middle(s) + Final"]
MTI in variable field"] D --> H{Payload fits in 1 frame?} H -->|Yes| I["Single frame with dest alias in bytes 0-1"] H -->|No| J["Multi-frame: First/Middle/Last
with framing bits"] E --> K{Payload <= 8 bytes?} K -->|Yes| L["Datagram Only frame"] K -->|No| M["Datagram First + Middle(s) + Final"]
24.10 Thread Safety at ISR Boundary
flowchart LR
subgraph ISR["ISR Context"]
I1["CAN RX interrupt"]
I2["Push to CAN FIFO"]
I3["Set is_duplicate flag"]
I4["Set listener alias"]
end
subgraph MAIN["Main Loop Context"]
M1["Pop from CAN FIFO"]
M2["Register/unregister aliases"]
M3["Allocate/free buffers"]
M4["Process messages"]
end
LOCK["lock_shared_resources()"]
UNLOCK["unlock_shared_resources()"]
ISR ---|"FIFO is the boundary"| MAIN
MAIN --> LOCK
LOCK --> M1
M1 --> UNLOCK
style ISR fill:#fce4ec,stroke:#c62828
style MAIN fill:#e3f2fd,stroke:#1565c0
24.11 Buffer Lifecycle
stateDiagram-v2
[*] --> Free: Pool initialized
Free --> Allocated: allocate_buffer(type)
Allocated --> InFIFO: Push to FIFO
InFIFO --> Processing: Pop from FIFO
Processing --> SharedRef: inc_reference_count()
SharedRef --> Processing: dec via free_buffer()
Processing --> Free: free_buffer() when ref_count=0
Allocated --> Free: free_buffer() (no FIFO)
note right of SharedRef
Multiple consumers can
hold references via
reference counting.
end note
24.12 Debugging Checklist for Each Trace Step
| Step | What to Check | Common Failure |
|---|---|---|
| CAN frame arrives | Is CAN RX interrupt firing? Check FIFO push. | CAN hardware not initialized, wrong baud rate |
| FIFO pop | Is main loop calling OpenLcb_run() fast enough? | FIFO overflow (too slow main loop) |
| Frame classification | Is the 29-bit identifier correct? | Standard (11-bit) frame sent to extended-only handler |
| Buffer allocation | Is the correct pool type being allocated? | Pool exhaustion -- increase USER_DEFINED_*_BUFFER_DEPTH |
| Multi-frame assembly | Do all frames arrive within timeout? | 3-second assembly timeout expired |
| MTI dispatch | Is the handler wired in the interface struct? | NULL handler pointer -- sends OIR automatically |
| Response TX | Is is_tx_buffer_clear() returning true? | CAN TX hardware busy, frames queued but not sent |
| Buffer free | Is reference count reaching zero? | Buffer leak -- forgot to free after processing |