CAN Map and CAN Manager
Purpose
Define the message specification, generator workflow and deterministic serialization strategy for the CAN-FD backbone.
The CAN map is the single source of truth.
Design Goals
Declarative message specification (YAML)
No manual byte indexing
No manual bit shifting in application code
Deterministic layout independent of compiler ABI
Compile-time enforcement of allowed message types
Scaled float support
Enum support
Strict DLC validation
DBC export generation
CAN Map Specification (YAML)
Messages are defined declaratively and code is generated.
Example:
can: can0
version: 1
namespaces:
- name: SensorNode
messages:
- name: StrainData
id: 0x201
dlc: 8
fields:
- { name: fx, type: int32, bits: 16 }
- { name: fy, type: int32, bits: 16 }
- { name: fz, type: int32, bits: 16 }
- { name: valid, type: uint8, bits: 1 }
The YAML file defines:
Output header name
Version
Namespaces
Message ID
DLC (bytes)
Field names
Bit width
Type
Optional scale (float)
Optional enum values
Strict DLC Rule
Total bits are tightly packed.
Required bytes:
required_bytes = ceil(total_bits / 8)
Validation rule:
required_bytes == dlc
Unused bits inside the final byte are allowed. Unused bytes are not allowed.
Nodes and Subscribers
The CAN map defines all nodes explicitly.
YAML must contain:
nodeslistsubscriberslist per message
Example:
nodes:
- ECU
- SensorNode
- Diagnosis
namespaces:
- name: SensorNode
messages:
- name: StrainData
id: 0x201
dlc: 7
subscribers:
- ECU
- Diagnosis
Rules:
subscribersis mandatory.Publisher is NOT implicitly a subscriber.
If no subscribers are defined, generation fails.
Subscriber names must exist in
nodeslist.Generation validates spelling and consistency.
Future generator extension will use this to produce:
Node-specific filter tables
Node-specific dispatch specialization
Compile-time elimination of unused message handlers
Generated C++ Message
Generator produces can0.hpp.
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 handled separately.
Scaled Float Support
YAML:
- name: temperature
type: float
bits: 16
scale: 0.1
Generated struct:
float temperature; // packed: 16 bits, scale: 0.1
static constexpr float temperature_scale = 0.1f;
Serialization:
int32_t raw =
static_cast<int32_t>(
std::round(msg.temperature / temperature_scale));
packSigned<offset,bits>(data, raw);
Float is never transmitted as IEEE-754. It is transported as scaled integer.
Enum Support
YAML:
- name: state
type: uint8
bits: 3
enum:
0: Idle
1: Running
2: Error
Generated struct:
enum class State : uint32_t {
Idle = 0,
Running = 1,
Error = 2,
};
State state;
Serialization uses underlying integer value.
Serialization Model
Generator produces serialize() and deserialize().
template<>
inline void serialize(const Message& msg, uint8_t* data);
template<>
inline void deserialize(Message& msg, const uint8_t* data);
All packing uses pack_utils.hpp.
Properties:
Width ≤ 32 bits per field
Offset + Width ≤ 64
Little-endian deterministic layout
No reinterpret_cast
No compiler bitfields
DBC Generation
Generator also produces:
can0.dbc
Includes:
Message definitions
Signal scaling
Enum value mappings
CAN map version comment
Used for SavvyCAN or other tools.
CAN Manager
The CAN manager is the transport layer between application logic and FDCAN.
It is strictly transport-only.
It does not:
Construct payloads
Interpret physical values
Perform bit shifting
Access individual fields
Transmit Path
Public API:
template<CanMessage Message>
void send(const Message& msg);
Transmit sequence:
Compile-time check:
MessagesatisfiesCanMessageconcept.Allocate modm CAN frame.
Set frame ID to
Message::id.Set frame length to
Message::dlc.Call generated
serialize(msg, frame.data).Submit frame to FDCAN driver.
Conceptual implementation:
template<CanMessage Message>
void CanManager::send(const Message& msg)
{
modm::can::Message frame(
static_cast<uint32_t>(Message::id),
Message::dlc);
frame.setExtended(false);
serialize(msg, frame.data);
transmit(frame);
}
The CAN manager never touches payload fields directly.
Receive Path
Reception is type-safe and compile-time constrained.
Conceptually:
FDCAN interrupt receives raw frame.
CAN manager matches frame ID against generated message list.
Matching message type is instantiated.
deserialize()reconstructs typed message.Typed message is dispatched.
Future implementation will use compile-time node specialization:
template<typename Node>
struct NodeDispatch;
template<>
struct NodeDispatch<Nodes::ECU>
{
static void handle(const SensorNode::StrainData& msg);
static void handle(const Diagnosis::Request& msg);
};
CAN manager will forward deserialized messages to:
NodeDispatch<Node>::handle(msg);
This avoids:
Runtime switch statements
Dynamic dispatch
Branch-heavy receive logic
Only messages subscribed by the node will generate handlers.
Dispatching is type-safe. No raw buffer handling is exposed to application layer.
Reception Filtering
Only CAN IDs defined in the CAN map are accepted.
Unknown IDs:
Ignored
Or counted as diagnostic error
Never forwarded as raw frame
Responsibilities
Initialize FDCAN
Configure filters
Handle TX queue
Handle RX interrupt
Perform serialize/deserialize
Monitor bus state (error passive / bus-off)
Provide error counters for diagnosis
Raw frame APIs must remain private.
Determinism
No dynamic allocation in send/receive path.
Bounded execution time.
No blocking in interrupt context.
No protocol logic inside CAN manager.