Skip to content

CAN

FDCAN1, classic CAN frames, 500 kbps, on PA11 (RX) / PA12 (TX). Configuration is in Core/Src/fdcan.c; the shared ring-buffer/scheduler driver is in Drivers/common/drivers/can/generic_can_driver.c (identical across the ARTTU firmware repos, don't fork it here); board-specific message packing/unpacking is in Core/Src/App/Communication/can_comm.c.

RX path

The RX ISR copies the frame into a ring buffer and sets a thread flag; canRxTask wakes, drains the buffer, and process_can_frames() decodes each recognized ID into the matching field of can_input_data. Unrecognized IDs are silently ignored, so only frames matching the FDCAN hardware filters (see below) even reach this switch.

Filters

Three dual filters (StdFiltersNbr = 3) admit six standard IDs to RX FIFO0; everything else is rejected at the hardware level (FDCAN_REJECT on both non-matching standard and extended, and on remote frames):

FilterIDs
0BMS_SoC_Voltage (17), BMS_Current (21)
1BMS_Errors_Temps (16), GPS_Angles (240)
2GIL_Inv_Data_2 (33), GIR_Inv_Data_2 (49)

Adding a new RX message means adding both a filter entry here and a case in process_can_frames(), in the same PR.

Message table (RX)

DBC messageIDFields usedNotes
BMS_SoC_Voltage17Adaptive_SOC, Pack_Inst_VoltageSee the byte-offset gotcha below
BMS_Current21Pack_CurrentSame gotcha
BMS_Errors_Temps16High_Temperature
GPS_Angles240GPS_SpeedLittle-endian on the wire, see below
GIL_Inv_Data_233Motor_Temp_L, Inverter_Temp_LLeft-side motor/inverter temps, feeds the fan hysteresis
GIR_Inv_Data_249Motor_Temp_R, Inverter_Temp_RRight-side, same purpose

TX path

canTxTask wakes on whichever comes first: a CAN_TX_WAKE_FLAG thread flag (set whenever a button changes, and also from the FDCAN TX-empty/TX-complete ISRs to drain the queue promptly), or a 1 ms timeout. Every wake it calls set_can_frames() to repack the current ui_data state into DASH_Buttons (ID 0x0D0 / 208), sends it immediately if the buttons changed, runs the periodic scheduler for any periodic frames, and drains the TX ring buffer into the hardware FIFO.

Message table (TX)

DBC messageIDPayloadNotes
DASH_Buttons208 (0x0D0)Byte 0: bitmask of apps_recalibrate, steer_recalibrate, suspension_recalibrate, imu_recalibrate, activate_cooling, slip_control_on_off, sd_logging_on_off, telemetry_on_off. Byte 1: slip_control_mode_enum (1-12)Non-periodic; sent on change, plus once after the delayed startup timer

Delayed startup frame

init_can_tasks() (can_tasks.c) arms a one-shot FreeRTOS software timer (osTimerNew(..., osTimerOnce, ...), 3000 ms) instead of sending the flash-loaded button state onto the bus the instant the RTOS boots. On expiry, its callback sets ui_data.buttons_updated_flag and wakes canTxTask, which sends the first real DASH_Buttons frame. This was a deliberate change: firing a frame the instant the dashboard's own tasks exist, before the rest of the vehicle network is necessarily up, is not useful and was moved out of flash_config.c's boot-time load path into this timer.

Two real bugs found on this board

Both were found and fixed during bring-up of this exact firmware and are worth knowing about, since they don't announce themselves as CAN bugs at first glance.

The silent NULL task handle bug

Symptom: HAL_FDCAN_RxFifo0Callback was confirmed (via breakpoint) to fire on every incoming frame, but osThreadFlagsSet(canRxTaskHandle, ...) was never actually reached, because canRxTaskHandle was NULL, permanently, not intermittently. The same was true of canTxTaskHandle. Received CAN data never updated the UI, and touching a button never produced a TX frame, at the same time, because both tasks had silently failed to be created.

Root cause: configTOTAL_HEAP_SIZE in Core/Inc/FreeRTOSConfig.h was set to 10000 bytes. GUI_Task alone requests an 8192-byte stack; add defaultTask's 512 bytes and there was nowhere near enough heap left for canRxTask and canTxTask's 2048-byte stacks each, created last in MX_FREERTOS_Init(). osThreadNew() on a CMSIS-RTOS2/FreeRTOS heap_4 port returns NULL on allocation failure. No crash, no log, just a task that silently never exists.

Fix: raised configTOTAL_HEAP_SIZE to 49152 bytes (also updated the matching field in the .ioc so a CubeMX regeneration doesn't revert it). The MCU has 640 KB of RAM and the board was using roughly 94 KB of it at the time, so there was ample room.

If you add a new task, thread, timer, or RTOS object

Check the heap budget. osThreadNew/osTimerNew/osSemaphoreNew/osMessageQueueNew all draw from the same configTOTAL_HEAP_SIZE pool, and none of them fail loudly if it runs out.

Byte-offset and endianness bugs in the manual DBC decode

This project decodes CAN payloads by hand (can_comm.c) rather than from generated DBC code, using small U16/S16 macros over raw payload bytes. Three real decode bugs were found this way:

  • Pack_Inst_Voltage (BMS_SoC_Voltage, bit 23 in the DBC, meaning bytes 2-3) was being read from bytes 0-1, which is actually a different signal in the same message (Pack_Open_Voltage). The value looked plausible, since it was a real voltage-shaped number, just the wrong one.
  • Pack_Current (BMS_Current, bit 23, bytes 2-3) had the same class of bug, reading Average_Current's byte offset instead.
  • Adaptive_SOC (BMS_SoC_Voltage, bit 63, byte 7) was being read from byte 6, landing on Pack_SOC instead.
  • GPS_Speed (GPS_Angles) is 0|16@1- in the DBC: the @1 means little-endian (Intel) byte order, unlike every other signal decoded in this file, which is @0 (big-endian/Motorola). The decode had the two payload bytes swapped.

None of these produced an obviously broken value (0, garbage, out of range); they produced a real, plausible-looking number for the wrong signal, or a wrong-but-not-absurd number from swapped bytes. That's what made them worth documenting: a manual byte-level DBC decode does not fail loudly when the offset or endianness is wrong, it just quietly reads a neighboring signal or a byte-reversed value.

When adding a new signal to can_comm.c

Always check both the start_bit and the @0/@1 byte-order marker in MAIN_DBC.dbc for that exact signal, not just the message. Signals within the same message can have different byte orders (as GPS_Angles shows), and a start bit of N in Motorola/big-endian numbering means byte N / 8, not byte N / 8 rounded the way you might assume from the field's position in the struct.

Released under the MIT License.