Skip to content

CAN

CAN is how the node hears the rest of the car. It's also how it talks back. The subsystem is three cooperating threads in can_app.c, a driver in can_driver.c, and the frame decode/encode logic in telemetry.c and can_frames.c.

The three threads

ThreadWakes onDoes
CAN RXRX semaphore (from ISR)Drains the RX ring buffer, ingests every frame
CAN TXTX semaphorePushes queued frames into the FDCAN hardware
CAN Periodic TX20 Hz timerSnapshots telemetry, packs derived values, queues them

The RX and TX threads are purely event-driven: the FDCAN ISR copies frames into a ring buffer and posts a semaphore, keeping the interrupt short. The periodic thread is the only one on a clock.

Receiving: ingesting the bus

The hardware filter accepts every standard ID into FIFO0 (extended and remote frames are rejected): the node is meant to hear the whole bus, so "filtering" is just the ingest switch ignoring unknown IDs.

When the RX thread wakes it calls can_driver_process_rx(), which pops frames and hands each to telemetry_ingest_can(). Each frame's ingest is bracketed by telemetry_write_begin/_end, so readers never see a half-decoded frame. The ingest itself is a big switch on the CAN identifier: each known ID decodes its payload (using per-sensor decoders and Motorola/Intel bit extractors) straight into the matching telemetry_data fields.

What gets decoded

The switch handles the whole car: front and rear gateways, the BMS, both inverters (GI left/right), the ECU (state machines, UKF and torque-vectoring outputs), LVSOC, the dashboard, and the IMU. Each frame's payload lands directly in the matching telemetry_data fields.

The exact identifiers and byte layouts are the car's CAN database: the authoritative list is the switch in telemetry_ingest_can(), not this page. If you need the full map, read the source or the shared DBC.

IMU sub-framing

The IMU frame carries an accelerometer, gyro, or Euler-angle payload depending on a function code in the data bytes. process_imu_message() demultiplexes it and applies the sensor's fixed scale factors.

Transmitting: re-broadcasting derived values

The periodic thread takes a snapshot and calls pack_and_send_tx_can_frames(), which the node uses to publish values it computed back onto the bus for other boards: currently the GPS-derived speed, course, and yaw (speed scaled ×100, angles wrapped to [-π, π], all packed as little-endian int16).

Packed frames go into the TX ring buffer via can_driver_tx_enqueue(); the TX thread drains that into the FDCAN peripheral.

Robustness

  • Bus-off self-recovery: on a bus-off error the driver clears the FDCAN INIT bit and rejoins the bus automatically (with a brief LED blip so it's visible). No power cycle needed.
  • Drop counters: the driver keeps RAM counters for RX ring overflows, TX enqueue drops, TX FIFO failures, and bus-off events (s_rx_drops, s_tx_enqueue_drops, s_tx_add_failures, s_bus_off_count). Readable over a debugger when diagnosing bus problems.

Lap frames

The lap subsystem also emits CAN frames: send_lap_completed_can_frame() (lap count) and send_lap_reset_can_frame(), declared in can_frames.h.

Work in progress

These two emitters are declared and called by the lap thread but not yet defined on the feature/lap-tracking branch, so their CAN IDs and layouts aren't fixed yet. See Lap Tracking.

Released under the MIT License.