Contents
Define the implementation architecture for the type-safe CAN transport layer.
This section specifies the CanManager design, template architecture, and integration with the modm hardware abstraction layer.
The CanManager implementation achieves:
Complete abstraction of FDCAN hardware from application code.
Zero-cost compile-time type checking of message permissions.
Automatic hardware initialization via template parameters.
Automatic filter configuration from CAN map metadata.
No dynamic allocation in transmission or reception paths.
Deterministic bounded execution time.
Separation of transport logic from application logic.
digraph CanManagerStack { // Graph settings rankdir=TB; splines=polyline; nodesep=1.2; ranksep=1.0; bgcolor="#FAFAFA"; node [fontname="Helvetica", fontsize=13]; edge [fontname="Helvetica", fontsize=11]; // ================================================================ // MAIN STACK // ================================================================ // USER CODE UserCode [ label=<<table border="0" cellborder="0" cellspacing="8"> <tr><td><font point-size="16"><b>π€ User Application</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td align="left"><font face="monospace" point-size="11">SensorNode::StrainData msg;</font></td></tr> <tr><td align="left"><font face="monospace" point-size="11">msg.fx = 123;</font></td></tr> <tr><td align="left"><font face="monospace" color="#E74C3C"><b>can.send(msg);</b></font></td></tr> <tr><td height="5"></td></tr> <tr><td align="left"><font face="monospace" color="#3498DB"><b>can.processMessages();</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td><font point-size="10" color="#7F8C8D">Work with CAN map structs only</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#EBF5FB:#D6EAF8", gradientangle=90, penwidth=2, color="#3498DB", width=4, height=2 ]; // CANMANAGER CanManager [ label=<<table border="0" cellborder="0" cellspacing="8"> <tr><td><font point-size="16"><b>π§ CanManager</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td align="left"><font point-size="12"><b>send<Message>(msg)</b></font></td></tr> <tr><td align="left"><font point-size="10">β’ Type checking β’ Serialization</font></td></tr> <tr><td height="8"></td></tr> <tr><td align="left"><font point-size="12"><b>processMessages()</b></font></td></tr> <tr><td align="left"><font point-size="10">β’ Poll FIFOs β’ Deserialize β’ Dispatch</font></td></tr> <tr><td height="8"></td></tr> <tr><td align="left"><font point-size="12"><b>Constructor</b></font></td></tr> <tr><td align="left"><font point-size="10">β’ Init hardware β’ Configure filters</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#FEF5E7:#FCE4B6", gradientangle=90, penwidth=2, color="#F39C12", width=4, height=2.2 ]; // GENERATED CODE Generated [ label=<<table border="0" cellborder="0" cellspacing="8"> <tr><td><font point-size="16"><b>βοΈ Generated from YAML</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td align="left"><font point-size="11"><b>can0.hpp</b> - Message structs</font></td></tr> <tr><td align="left"><font point-size="11"><b>can0_serialize.hpp</b> - Pack/unpack</font></td></tr> <tr><td align="left"><font point-size="11"><b>NodeTraits</b> - Permissions</font></td></tr> <tr><td height="10"></td></tr> <tr><td><font point-size="10" color="#27AE60">β Single source of truth</font></td></tr> <tr><td><font point-size="10" color="#27AE60">β Compile-time validation</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#E8F8F5:#D5F5E3", gradientangle=90, penwidth=2, color="#27AE60", width=4, height=2 ]; // MODM Modm [ label=<<table border="0" cellborder="0" cellspacing="8"> <tr><td><font point-size="16"><b>π modm HAL</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td align="left"><font point-size="11">Fdcan1::sendMessage()</font></td></tr> <tr><td align="left"><font point-size="11">Fdcan1::getMessage()</font></td></tr> <tr><td align="left"><font point-size="11">Fdcan1::initialize()</font></td></tr> <tr><td height="10"></td></tr> <tr><td><font point-size="10" color="#7F8C8D">STM32G4 hardware drivers</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#F4ECF7:#EBDEF0", gradientangle=90, penwidth=2, color="#8E44AD", width=4, height=1.8 ]; // HARDWARE Hardware [ label=<<table border="0" cellborder="0" cellspacing="8"> <tr><td><font point-size="16"><b>π© Hardware</b></font></td></tr> <tr><td height="10"></td></tr> <tr><td><font point-size="12">STM32G4 FDCAN Peripheral</font></td></tr> <tr><td><font point-size="12">CAN Bus (1M/5M bit/s)</font></td></tr> <tr><td height="5"></td></tr> <tr><td><font point-size="10" color="#95A5A6">Physical layer</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#EAECEE:#D5D8DC", gradientangle=90, penwidth=2, color="#5D6D7E", width=4, height=1.5 ]; // ================================================================ // SIDE ELEMENTS // ================================================================ // YAML SOURCE YAML [ label=<<table border="0" cellborder="0" cellspacing="6"> <tr><td><font point-size="14"><b>π can_map.yaml</b></font></td></tr> <tr><td height="8"></td></tr> <tr><td align="left"><font point-size="10" face="monospace">messages:</font></td></tr> <tr><td align="left"><font point-size="10" face="monospace"> - name: StrainData</font></td></tr> <tr><td align="left"><font point-size="10" face="monospace"> id: 0x201</font></td></tr> <tr><td align="left"><font point-size="10" face="monospace"> fields: [...]</font></td></tr> </table>>, shape=note, style="filled", fillcolor="#FFFACD", penwidth=1.5, color="#F39C12", width=2.5 ]; // TX FLOW TxFlow [ label=<<table border="0" cellborder="0" cellspacing="4"> <tr><td><font point-size="13" color="#E74C3C"><b>π€ Send Flow</b></font></td></tr> <tr><td height="5"></td></tr> <tr><td align="left"><font point-size="10">1. Create struct</font></td></tr> <tr><td align="left"><font point-size="10">2. send(msg)</font></td></tr> <tr><td align="left"><font point-size="10">3. Serialize</font></td></tr> <tr><td align="left"><font point-size="10">4. Hardware TX</font></td></tr> <tr><td align="left"><font point-size="10">5. CAN bus</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#FADBD8", penwidth=0, width=1.8 ]; // RX FLOW RxFlow [ label=<<table border="0" cellborder="0" cellspacing="4"> <tr><td><font point-size="13" color="#3498DB"><b>π₯ Receive Flow</b></font></td></tr> <tr><td height="5"></td></tr> <tr><td align="left"><font point-size="10">1. CAN bus</font></td></tr> <tr><td align="left"><font point-size="10">2. Hardware RX</font></td></tr> <tr><td align="left"><font point-size="10">3. processMessages()</font></td></tr> <tr><td align="left"><font point-size="10">4. Deserialize</font></td></tr> <tr><td align="left"><font point-size="10">5. onMessage(msg)</font></td></tr> </table>>, shape=box, style="filled,rounded", fillcolor="#D6EAF8", penwidth=0, width=1.8 ]; // KEY PRINCIPLE Principle [ label=<<table border="1" cellborder="1" cellspacing="0" cellpadding="8" bgcolor="#FFFEF0"> <tr><td bgcolor="#27AE60" colspan="2"><font color="white" point-size="13"><b>β¨ Key Abstraction</b></font></td></tr> <tr> <td bgcolor="#D5F5E3"><font point-size="11"><b>User Sees β</b></font></td> <td bgcolor="#FADBD8"><font point-size="11"><b>Hidden β</b></font></td> </tr> <tr> <td align="left"> <font point-size="10">β’ CAN map structs<br/>β’ Normal C++ types<br/>β’ send(), processMessages()</font> </td> <td align="left"> <font point-size="10">β’ modm::can::Message<br/>β’ Bit shifting<br/>β’ Hardware registers</font> </td> </tr> </table>>, shape=plaintext ]; // ================================================================ // CONNECTIONS // ================================================================ // Main vertical flow with large arrows UserCode -> CanManager [ penwidth=3, color="#E74C3C:#3498DB", arrowsize=1.2, label=<<font point-size="11"><b> API calls </b></font>> ]; CanManager -> Generated [ penwidth=2, color="#7F8C8D", style=dashed, arrowsize=1, label=<<font point-size="10"> uses </font>> ]; CanManager -> Modm [ penwidth=3, color="#8E44AD", arrowsize=1.2, label=<<font point-size="11"><b> calls </b></font>> ]; Modm -> Hardware [ penwidth=3, color="#5D6D7E", arrowsize=1.2, label=<<font point-size="11"><b> controls </b></font>> ]; // YAML generation flow YAML -> Generated [ penwidth=2.5, color="#F39C12", style=dashed, arrowsize=1.2, label=<<font point-size="11" color="#F39C12"><b>generates</b></font>> ]; // ================================================================ // LAYOUT // ================================================================ // Position side elements {rank=same; UserCode; TxFlow;} {rank=same; CanManager; YAML;} {rank=same; Hardware; RxFlow;} {rank=min; Principle;} // Invisible edges for layout TxFlow -> RxFlow [style=invis]; YAML -> Principle [style=invis]; }
The user works exclusively with CAN map structs and normal C++ types.
Key characteristics:
Direct field access: msg.fx = 123
msg.fx = 123
Type-safe API: can.send(msg)
can.send(msg)
Automatic message processing: can.processMessages()
can.processMessages()
No bit manipulation required
No hardware knowledge needed
Template-based transport layer providing:
send<Message>(msg) - Type-checked message transmission with automatic serialization
processMessages() - Polls hardware FIFOs, deserializes, and dispatches to handlers
Constructor - Automatic hardware initialization and filter configuration
All hardware complexity is hidden from the user.
Auto-generated from YAML source:
can0.hpp - Message struct definitions with metadata
can0_serialize.hpp - Deterministic pack/unpack functions
NodeTraits - Compile-time permissions (TxMessages, RxMessages)
Provides:
Single source of truth
Compile-time validation
Type safety guarantees
Hardware abstraction layer providing:
FDCAN peripheral drivers
GPIO configuration
Interrupt handling
Platform-specific implementations
The CanManager calls modm APIs but hides them from the user.
Physical layer:
STM32G4 FDCAN peripheral
CAN bus interface
1 Mbit/s nominal bitrate
5 Mbit/s data phase (CAN-FD)
User creates message struct
Calls can.send(msg)
CanManager checks compile-time permissions
Automatic serialization via generated code
modm transmits frame
Hardware sends to CAN bus
CAN bus receives frame
Hardware stores in RX FIFO
User calls can.processMessages()
CanManager polls FIFO
Matches ID to message type
Automatic deserialization
Calls userβs onMessage(msg) handler
onMessage(msg)
Single constructor call handles:
GPIO pin connection
FDCAN peripheral initialization
CAN-FD mode configuration
Reads RxMessages from NodeTraits
Configures hardware filters automatically
Ready to send/receive!
CAN map structs (SensorNode::StrainData)
SensorNode::StrainData
Normal C++ types (int32_t, float, uint8_t)
int32_t
float
uint8_t
Simple API (send(), processMessages())
send()
processMessages()
modm::can::Message
Bit shifting and masking
Hardware register access
Filter configuration
Serialization logic
The user works at a high level of abstraction, never touching low-level details.