CAN Bus Communication Module
Overview
This document describes the CAN (Controller Area Network) communication subsystem used for vehicle-level diagnostics, command distribution, and bootloader integration. The implementation uses the FDCAN (Flexible Data-rate CAN) peripheral on the STM32L562 microcontroller, supporting standard 11-bit identifiers with interrupt-driven transmission and reception.
File Locations
- Headers:
Core/Inc/App/Communication/can_comm.h,Core/Inc/App/Communication/can_driver.h - Source:
Core/Src/App/Communication/can_comm.c,Core/Src/App/Communication/can_driver.c
Physical Layer
- Peripheral: FDCAN (STM32L562xx)
- Protocol: CAN 2.0B (Standard frame format)
- Identifiers: 11-bit standard IDs (0x000 - 0x7FF)
- Frame Format: Classic CAN (not CAN-FD)
- Payload: Up to 8 bytes per frame
- Error Recovery: Automatic bus-off recovery
CAN Network Topology:
Message Identifiers and Frame Organization
Status Frames (Periodic Telemetry)
Status frames are transmitted periodically at 50 Hz (20ms period) from each inverter device:
Periodic Broadcast Timeline:
Left Inverter Status (Device ID 0x004)
| Message ID | Content | Period | Rate |
|---|---|---|---|
| 0x020 | PWM%, Voltage, Current, RPM | Periodic | 50 Hz |
| 0x021 | Motor Temp, Inverter Temp, Aux, Throttle | Periodic | 50 Hz |
Right Inverter Status (Device ID 0x005)
| Message ID | Content | Period | Rate |
|---|---|---|---|
| 0x030 | PWM%, Voltage, Current, RPM | Periodic | 50 Hz |
| 0x031 | Motor Temp, Inverter Temp, Aux, Throttle | Periodic | 50 Hz |
Status Frame Payload Structure
Frame 0x020 / 0x030 (Motor Metrics):
Byte Layout (Big-Endian):
┌─────┬─────┬──────┬──────┬──────┬──────┬───────┬───────┐
│ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ B6 │ B7 │
├─────┴─────┼──────┴──────┼──────┴──────┼───────┴───────┤
│ PWM % │ Voltage │ Current │ RPM │
│ (0-100) │ (×10) │ (×10) │ rev/min │
│ │ 48.2V → │ 15.3A → │ │
│ │ 0x01E6 │ 0x0099 │ │
└───────────┴─────────────┴─────────────┴───────────────┘
Example: PWM=45%, U=48.2V, I=15.3A, RPM=2450
Bytes: [00][2D][01][E6][00][99][09][9A]Frame 0x021 / 0x031 (Temperature & Inputs):
Byte Layout (Big-Endian):
┌─────┬─────┬──────┬──────┬──────┬──────┬───────┬───────┐
│ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ B6 │ B7 │
├─────┴─────┼──────┴──────┼──────┴──────┼───────┴───────┤
│Motor Temp │Inv. Temp │ Aux │ Throttle │
│ (°C) │ (°C) │ (×1000) │ (×1000) │
│ Signed │ Unsigned │ 0.12V → │ 2.45V → │
│ │ │ 0x0078 │ 0x0099 │
└───────────┴─────────────┴─────────────┴───────────────┘
Example: mot=35°C, con=42°C, aux=0.12V, throttle=2.45V
Bytes: [00][23][00][2A][00][78][00][99]Byte Order: Big-endian (High byte first)
Debug Frames (Non-periodic)
Debug frames are sent on-demand and do not follow a periodic schedule:
| Message ID | Purpose | Format |
|---|---|---|
| 0x022 | Left inverter debug | Device-specific |
| 0x032 | Right inverter debug | Device-specific |
Control Frames (ECU to Inverter)
The ECU broadcasts control commands and mode settings:
Command Frame (0x0E0)
Byte 0: Motor Enable/Disable/Regen (0=Disable, 1=Enable, 2=Regen)
Byte 1: Left inverter regen braking percentage (0-100%)
Byte 2: Right inverter regen braking percentage (0-100%)
Byte 3: Left inverter speed percentage (0-100%)
Byte 4: Right inverter speed percentage (0-100%)
Byte 5-6: Calculated vehicle speed (16-bit, big-endian)
Byte 7: ReservedBootloader & Programming Frames
Bootloader Request (0x4F0)
Byte 0: Device ID (0x04 = Left Inverter, 0x05 = Right Inverter)
Byte 1-7: ReservedTriggers the target inverter to enter bootloader mode.
Programming Command (0x4F1)
Byte 0-7: Raw programming data (8 bytes)Passes programming commands from ECU to inverter via CAN. Typically contains firmware update packets or configuration commands.
Data Reception & Processing
Interrupt-Driven RX Pipeline
CAN RX Processing Architecture:
When CAN frames arrive on FIFO 0:
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) {
// Drain FIFO with batch size limit to prevent ISR overrun
while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO0) > 0 &&
drained_count < CAN_RX_ISR_BATCH_SIZE) {
// Extract frame from hardware FIFO
HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &can_rx_hdr, data);
// Push into application ring buffer for deferred processing
CAN_driver_rx_callback(&can_driver, data, &can_rx_hdr,
can_rx_hdr.Identifier, can_rx_hdr.DataLength,
HAL_GetTick());
}
}Key Features:
- Batch processing limit prevents ISR overrun
- Software ring buffer decouples hardware and application processing
- Timestamps captured for debug and synchronization
Ring Buffer Processing
Messages are extracted from the ring buffer in the main task loop:
void process_can_frames(CAN_Driver_t* driver) {
while (driver->rx_ring_buffer.head != driver->rx_ring_buffer.tail &&
processed_count < CAN_RX_PROCESS_BUDGET_PER_CALL) {
CAN_Rx_Message_Frame_t current_frame =
driver->rx_ring_buffer.frame[driver->rx_ring_buffer.tail];
driver->rx_ring_buffer.tail =
(driver->rx_ring_buffer.tail + 1) % driver->rx_ring_buffer.size;
// Dispatch to handler based on message ID
switch(current_frame.msg_id) {
case 0x0E0: // ECU status
case 0x4F0: // Bootloader request
case 0x4F1: // Programming data
// ... handle each message type
}
}
}Ring Buffer Configuration:
- Size: 128 frames (
NR_OF_CAN_RX_BUFFER_FRAMES) - Prevents message loss during high traffic periods
CAN Passthrough Mode
Passthrough mode allows UART data to be bridged across the CAN network for remote debugging or over-the-air programming:
Passthrough Message Flow:
Passthrough Frame Structure
UART messages are fragmented and wrapped in special CAN frames:
Start Marker (0x0023 + 1 or 0x0033 + 1)
CAN Frame Structure:
┌──────────────────────────────────────────────────┐
│ CAN ID: 0x024 (Left) or 0x034 (Right) │
├──────────────────────────────────────────────────┤
│ Byte 0: 0x3C '<' │
│ Byte 1: 0x50 'P' │
│ Byte 2: 0x54 'T' │
│ Byte 3: 0x53 'S' (START marker) │
│ Byte 4: len_lo (Total payload length & 0xFF) │
│ Byte 5: len_hi (Total payload length >> 8) │
│ Byte 6: 0x3E '>' │
│ Byte 7: 0x00 (Reserved) │
└──────────────────────────────────────────────────┘
Example: UART message "S=2.45" (6 bytes)
Payload: {0x3C, 0x50, 0x54, 0x53, 0x06, 0x00, 0x3E, 0x00}Data Frames (0x0023 or 0x0033)
CAN Frame Structure:
┌──────────────────────────────────────────────────┐
│ CAN ID: 0x023 (Left) or 0x033 (Right) │
├──────────────────────────────────────────────────┤
│ Byte 0-7: Raw UART data (up to 8 bytes) │
│ Padded with 0x00 if < 8 bytes │
└──────────────────────────────────────────────────┘
Example: "S=2.45\r\nU="
Payload: {0x53, 0x3D, 0x32, 0x2E, 0x34, 0x35, 0x0D, 0x0A}
('S', '=', '2', '.', '4', '5', '\r', '\n')End Marker (0x0023 + 2 or 0x0033 + 2)
CAN Frame Structure:
┌──────────────────────────────────────────────────┐
│ CAN ID: 0x025 (Left) or 0x035 (Right) │
├──────────────────────────────────────────────────┤
│ Byte 0: 0x3C '<' │
│ Byte 1: 0x50 'P' │
│ Byte 2: 0x54 'T' │
│ Byte 3: 0x45 'E' (END marker) │
│ Byte 4: len_lo (Total payload length & 0xFF) │
│ Byte 5: len_hi (Total payload length >> 8) │
│ Byte 6: 0x3E '>' │
│ Byte 7: 0x00 (Reserved) │
└──────────────────────────────────────────────────┘
Example: End marker for 14-byte message
Payload: {0x3C, 0x50, 0x54, 0x45, 0x0E, 0x00, 0x3E, 0x00}Passthrough Data ID Selection
Data ID is determined by device type:
- Left Inverter (DEVICE_LEFT_INVERTER): Base ID 0x023
- Right Inverter (DEVICE_RIGHT_INVERTER): Base ID 0x033
Each passthrough sequence uses sequential IDs:
base_id + 0→ Data framesbase_id + 1→ Start markerbase_id + 2→ End marker
Passthrough Transmission Flow
static void send_uart_passthrough_chunks(const uint8_t *data, uint16_t len) {
// 1. Send START marker with total payload length
send_can_passthrough_marker(PASSTHROUGH_MARKER_START, len);
HAL_Delay(1);
// 2. Fragment and send data in 8-byte chunks
while (offset < len) {
uint8_t payload[8] = {0};
uint16_t chunk_len = ((len - offset) > 8) ? 8 : (len - offset);
memcpy(payload, &data[offset], chunk_len);
send_8_byte_can_passthrough_payload(payload);
offset += chunk_len;
HAL_Delay(1); // Inter-frame spacing
}
// 3. Send END marker
send_can_passthrough_marker(PASSTHROUGH_MARKER_END, len);
}Passthrough Sequence Diagram:
Characteristics:
- 1ms delay between frames prevents CAN bus congestion
- Receiver can reconstruct full message using START/END markers and length field
- Supports variable-length payloads
- Allows round-trip debugging over CAN network
Data Transmission
CAN TX Pipeline Flow:
Periodic Status Updates
Status frames are scheduled and transmitted at regular intervals:
void set_can_frames(CAN_Driver_t* driver) {
// Extract and pack telemetry from inverter UART data
uint16_t voltage_x10 = (uint16_t)(inverter_data.RS232_Message.U * 10.0f);
uint16_t current_x10 = (uint16_t)(inverter_data.RS232_Message.I * 10.0f);
// Pack into CAN payload (big-endian)
driver->tx_message_frames[0].payload[0] = HIGH_BYTE(inverter_data.RS232_Message.PWM);
driver->tx_message_frames[0].payload[1] = LOW_BYTE(inverter_data.RS232_Message.PWM);
driver->tx_message_frames[0].payload[2] = HIGH_BYTE(voltage_x10);
driver->tx_message_frames[0].payload[3] = LOW_BYTE(voltage_x10);
driver->tx_message_frames[0].payload[4] = HIGH_BYTE(current_x10);
driver->tx_message_frames[0].payload[5] = LOW_BYTE(current_x10);
driver->tx_message_frames[0].payload[6] = HIGH_BYTE(inverter_data.RS232_Message.RPM);
driver->tx_message_frames[0].payload[7] = LOW_BYTE(inverter_data.RS232_Message.RPM);
// Frame 0x021/0x031: Temperature & Inputs
driver->tx_message_frames[1].payload[0] = HIGH_BYTE(motor_temp_raw);
driver->tx_message_frames[1].payload[1] = LOW_BYTE(motor_temp_raw);
driver->tx_message_frames[1].payload[2] = HIGH_BYTE(inverter_temp_raw);
driver->tx_message_frames[1].payload[3] = LOW_BYTE(inverter_temp_raw);
driver->tx_message_frames[1].payload[4] = HIGH_BYTE(aux_x1000);
driver->tx_message_frames[1].payload[5] = LOW_BYTE(aux_x1000);
driver->tx_message_frames[1].payload[6] = HIGH_BYTE(throttle_x1000);
driver->tx_message_frames[1].payload[7] = LOW_BYTE(throttle_x1000);
}Interrupt-Driven TX Processing
CAN controller signals when TX FIFO is empty or buffer complete:
void HAL_FDCAN_TxFifoEmptyCallback(FDCAN_HandleTypeDef *hfdcan) {
if (hfdcan == (FDCAN_HandleTypeDef*)can_driver.hfdcan) {
can_driver.tx_queue_drain_requested = 1;
}
}
void HAL_FDCAN_TxBufferCompleteCallback(FDCAN_HandleTypeDef *hfdcan,
uint32_t BufferIndexes) {
if (hfdcan == (FDCAN_HandleTypeDef*)can_driver.hfdcan) {
can_driver.tx_queue_drain_requested = 1;
}
}TX Processing:
- Main task checks
tx_queue_drain_requestedflag - Calls
CAN_process_tx_queue()to send buffered frames - Supports up to 128 queued TX frames (
NR_OF_CAN_TX_BUFFER_FRAMES)
TX Message Configuration
typedef struct {
uint32_t Identifier; // 11-bit CAN ID
uint32_t IdType; // FDCAN_STANDARD_ID
uint32_t TxFrameType; // FDCAN_DATA_FRAME
uint32_t DataLength; // FDCAN_DLC_BYTES_8
uint32_t ErrorStateIndicator; // FDCAN_ESI_ACTIVE
uint32_t BitRateSwitch; // FDCAN_BRS_OFF (classic CAN)
uint32_t FDFormat; // FDCAN_CLASSIC_CAN
} FDCAN_TxHeaderTypeDef;Error Handling & Recovery
Error State Machine:
Bus-Off Recovery
When CAN controller enters bus-off state (excessive errors):
void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef *hfdcan,
uint32_t ErrorStatusITs) {
if((ErrorStatusITs & FDCAN_IT_BUS_OFF) != 0) {
// Recover from bus-off by disabling and re-enabling INIT mode
hfdcan->Instance->CCCR &= ~FDCAN_CCCR_INIT;
}
}Message Loss Protection
- Ring buffer size (128 frames) provides buffering for burst traffic
- ISR batch processing limit prevents interrupt overrun
- Application processing budget prevents starvation of other tasks
Data Structure References
typedef struct {
can_board_ECU_t ecu_msg; // ECU control messages (0x0E0)
can_msg_PRG_Bootloader_t bootloader_msg; // Bootloader frames (0x4F0)
can_msg_PRG_Inverter_t prg_inverter_msg; // Programming frames (0x4F1)
} CAN_input_data_t;
extern volatile CAN_input_data_t can_input_data;Integration Notes
- CAN frames are generated from UART telemetry in
set_can_frames() - Incoming CAN commands update global control variables
- Bootloader entry is triggered via FSM state machine (
board_fsm_mode_bootloader_request()) - Passthrough mode is gated by
pass_through_prog_flagglobal state - CAN timestamp tracking in
board_fsm_update_can_timestamp()for FSM timeout logic - FDCAN peripheral must be initialized via STM32CubeMX with interrupt callbacks enabled
Key Takeaways
Architecture Strengths:
- Periodic telemetry at 50 Hz for real-time vehicle control
- Ring buffer buffering (128 frames each) handles burst traffic
- Batch ISR processing prevents interrupt overrun
- Time-budgeted task processing maintains system responsiveness
- Multi-frame passthrough enables complex diagnostic and programming workflows
- Automatic bus-off recovery for robust error handling
- Device-independent framing supports left/right inverters transparently
