Software Architecture ===================== Define firmware structure for distributed sensor nodes and CAN-FD transport. This section specifies architectural boundaries and compile-time constraints. This section is work in progress. Code snippets are conceptual and for reference only. Design Invariants ----------------- 1. Sensors return CAN message types defined in the CAN map. 2. Only CAN-map messages may be transmitted. 3. Any CAN map change affecting message layout must break compilation. 4. No raw CAN frame sending. 5. Strict separation of sensor acquisition and transport. 6. Prefer static allocation for fixed-size objects. 7. No dynamic allocation in real-time paths. 8. CAN map is the single source of truth for message layout. 9. Serialization must be deterministic and compiler-independent. Software Stack -------------- .. graphviz:: digraph SoftwareStack { rankdir=TB; node [shape=box, style=filled, fillcolor=lightgray]; // ========================= // Application Layer // ========================= subgraph cluster_application { label="Application Layer"; style=filled; fillcolor="#e6f2ff"; AppLogic [label="Application Logic\n(Control / Estimation / Supervision)"]; SensorFramework[label="Sensor Framework\n(Generic Sensor PCB Architecture)"]; DiagnosisLayer [label="Diagnosis Layer\n(Request Handling / Responses)"]; } // ========================= // Transport Layer // ========================= subgraph cluster_transport { label="Transport Layer"; style=filled; fillcolor="#fff2e6"; CanManager [label="CAN Manager\n(Transport Only)"]; GeneratedMap [label="Generated CAN Map\n(Message Structs\nserialize()/deserialize()\nEnums / Concepts)", fillcolor="#ffe0cc"]; { rank=same; CanManager; GeneratedMap; } } // ========================= // Low-Level Drivers // ========================= subgraph cluster_drivers { label="Low-Level Drivers (modm)"; style=filled; fillcolor="#f2ffe6"; FDCANDriver [label="FDCAN Driver"]; SPIDriver [label="SPI / I2C / ADC Drivers"]; TimerDriver [label="Timers / GPIO / DMA"]; } // ========================= // Hardware Layer // ========================= subgraph cluster_hardware { label="Hardware"; style=filled; fillcolor="#f9f9f9"; MCU [label="STM32G4 MCU"]; Peripherals [label="Peripherals\n(FDCAN, SPI, ADC, Timers)"]; } // ========================= // Dependencies // ========================= // Application Flow AppLogic -> SensorFramework; AppLogic -> DiagnosisLayer; // Transport Flow SensorFramework -> CanManager; DiagnosisLayer -> CanManager; CanManager -> GeneratedMap; // Drivers CanManager -> FDCANDriver; SensorFramework -> SPIDriver; SensorFramework -> TimerDriver; // Hardware Access FDCANDriver -> Peripherals; SPIDriver -> Peripherals; TimerDriver -> Peripherals; Peripherals -> MCU; } Layer Responsibilities ---------------------- Application Layer ^^^^^^^^^^^^^^^^^ - Control logic - State estimation - Supervision - Diagnosis handling - No knowledge of serialization Sensor Framework ^^^^^^^^^^^^^^^^ Shared across all sensor PCBs. Responsibilities: - Call ``init()`` - Periodically call ``update()`` - Poll ``hasDataChanged()`` - Forward typed CAN message to CAN manager - Node supervision - Heartbeat handling Example: .. code-block:: cpp sensor.update(); if(sensor.hasDataChanged()) { auto msg = sensor.getProcessed(); canManager.send(msg); } Transport Layer ^^^^^^^^^^^^^^^ CAN manager: - Uses ``Message::id`` - Uses ``Message::dlc`` - Calls generated ``serialize()`` - Calls generated ``deserialize()`` - Never accesses individual fields - Never performs bit shifting - Never constructs payload manually Node-Specific CAN Manager (Concept) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The CAN manager will later be templated on node identity: .. code-block:: cpp using Node = Nodes::ECU; CanManager canManager; Node identity will be defined in the CAN map YAML and code-generated. Future specialization will allow: - Compile-time filter configuration - Compile-time dispatch selection - Elimination of unused message handling - No runtime branching based on node type Implementation is deferred. Low-Level Drivers ^^^^^^^^^^^^^^^^^ - Hardware abstraction via modm - FDCAN peripheral configuration - DMA / timers / SPI access Compile-Time Enforcement ------------------------ Only message types registered in the CAN map may be transmitted. .. code-block:: cpp template concept CanMessage = IsCanMessage::value; CAN manager API: .. code-block:: cpp template void send(const Message& msg); If a message is not registered, compilation fails. Any change to field layout in YAML modifies the generated header. Downstream code must recompile. CAN Map ^^^^^^^ Single source of truth for all messages on the CAN-FD bus. Messages are declared in YAML and code is generated. Messages are grouped by logical node namespace. Example structure: - ``namespace ECU`` - ``namespace SensorNode`` - ``namespace Diagnosis`` Each message is generated as a struct containing: - ``static constexpr id`` - ``static constexpr dlc`` - Typed payload members - Optional enum types - Optional scaling constants - Optional debug name Example: .. code-block:: cpp namespace SensorNode { struct StrainData { static constexpr uint16_t id = 0x201; static constexpr uint8_t dlc = 8; #ifdef CANMAP_INCLUDE_MESSAGE_NAMES static constexpr const char* name = "SensorNode::StrainData"; #endif int32_t fx; // packed: 16 bits int32_t fy; // packed: 16 bits int32_t fz; // packed: 16 bits uint8_t valid; // packed: 1 bit }; } The struct represents physical values only. Packing is deterministic and generated separately. Diagnosis --------- Diagnosis messages are part of the CAN map. Design principles: - Central controller requests diagnostic data - No ad-hoc diagnostic frames - Deterministic bounded execution - No dynamic allocation in real-time path Version reporting: - Firmware embeds Git commit hash - CAN map version embedded - Board version embedded - Returned via diagnosis request Memory Strategy --------------- - Prefer static allocation - No dynamic allocation in real-time paths - Heap usage allowed during initialization - Allocation failures must be handled explicitly Software Tests -------------- Testing is mandatory for serialization correctness and protocol stability. Testing is split into: - Compile-time validation - Host-side unit tests - Integration tests Compile-Time Validation ^^^^^^^^^^^^^^^^^^^^^^^ The generator enforces: - Unique CAN IDs - Strict DLC rule (ceil(bits/8) == dlc) - Enum value range validity - Float requires scale C++ enforces: - ``CanMessage`` concept restriction - Message registration - Struct regeneration on CAN map change Any change to YAML must trigger full recompilation. Serialization Unit Tests (Host) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Host-side tests verify: - Bit packing correctness - Bit offsets - Signed extension correctness - Scaled float conversion - Enum conversion - Boundary conditions (min/max values) - Zero initialization of unused bits Test pattern: .. code-block:: cpp Message msg{}; msg.fx = 123; uint8_t buffer[Message::dlc]; serialize(msg, buffer); Message decoded{}; deserialize(decoded, buffer); assert(decoded.fx == msg.fx); Round-trip serialization must be lossless within scaling precision. Edge Case Tests ^^^^^^^^^^^^^^^ For each field: - Minimum value - Maximum value - Zero - Negative (if signed) - Float rounding boundary Float tests must verify: .. code-block:: text abs(original - decoded) <= scale Integration Tests ^^^^^^^^^^^^^^^^^ Integration test stub verifies: - Message ID matches expected - DLC matches expected - Serialization matches DBC definition - No padding beyond required bytes - CAN manager rejects non-registered types Optional: - Loopback FDCAN test - Stress test at full bus rate - Bus-off recovery behavior Regression Safety ^^^^^^^^^^^^^^^^^ The CAN map is versioned. Any change to: - Bit width - Field order - Scaling - Enum definition changes generated headers and forces rebuild. Host tests must run in CI before deployment. Test Philosophy ^^^^^^^^^^^^^^^ - Deterministic serialization - No undefined behavior - No compiler-dependent layout - Explicit validation of protocol contract