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
| Thread | Wakes on | Does |
|---|---|---|
| CAN RX | RX semaphore (from ISR) | Drains the RX ring buffer, ingests every frame |
| CAN TX | TX semaphore | Pushes queued frames into the FDCAN hardware |
| CAN Periodic TX | 20 Hz timer | Snapshots 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.
