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
Sensors return CAN message types defined in the CAN map.
Only CAN-map messages may be transmitted.
Any CAN map change affecting message layout must break compilation.
No raw CAN frame sending.
Strict separation of sensor acquisition and transport.
Prefer static allocation for fixed-size objects.
No dynamic allocation in real-time paths.
CAN map is the single source of truth for message layout.
Serialization must be deterministic and compiler-independent.
Software Stack
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:
sensor.update();
if(sensor.hasDataChanged())
{
auto msg = sensor.getProcessed();
canManager.send(msg);
}
Transport Layer
CAN manager:
Uses
Message::idUses
Message::dlcCalls 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:
using Node = Nodes::ECU;
CanManager<Node> 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.
template<typename T>
concept CanMessage = IsCanMessage<T>::value;
CAN manager API:
template<CanMessage Message>
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 ECUnamespace SensorNodenamespace Diagnosis
Each message is generated as a struct containing:
static constexpr idstatic constexpr dlcTyped payload members
Optional enum types
Optional scaling constants
Optional debug name
Example:
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:
CanMessageconcept restrictionMessage 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:
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:
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