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:

  • nodes list

  • subscribers list per message

Example:

nodes:
  - ECU
  - SensorNode
  - Diagnosis

namespaces:
  - name: SensorNode
    messages:
      - name: StrainData
        id: 0x201
        dlc: 7
        subscribers:
          - ECU
          - Diagnosis

Rules:

  • subscribers is mandatory.

  • Publisher is NOT implicitly a subscriber.

  • If no subscribers are defined, generation fails.

  • Subscriber names must exist in nodes list.

  • 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:

  1. Compile-time check: Message satisfies CanMessage concept.

  2. Allocate modm CAN frame.

  3. Set frame ID to Message::id.

  4. Set frame length to Message::dlc.

  5. Call generated serialize(msg, frame.data).

  6. 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:

  1. FDCAN interrupt receives raw frame.

  2. CAN manager matches frame ID against generated message list.

  3. Matching message type is instantiated.

  4. deserialize() reconstructs typed message.

  5. 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.