Skip to content

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 IDContentPeriodRate
0x020PWM%, Voltage, Current, RPMPeriodic50 Hz
0x021Motor Temp, Inverter Temp, Aux, ThrottlePeriodic50 Hz

Right Inverter Status (Device ID 0x005)

Message IDContentPeriodRate
0x030PWM%, Voltage, Current, RPMPeriodic50 Hz
0x031Motor Temp, Inverter Temp, Aux, ThrottlePeriodic50 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 IDPurposeFormat
0x022Left inverter debugDevice-specific
0x032Right inverter debugDevice-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: Reserved

Bootloader & Programming Frames

Bootloader Request (0x4F0)

Byte 0: Device ID (0x04 = Left Inverter, 0x05 = Right Inverter)
Byte 1-7: Reserved

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

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

c
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 frames
  • base_id + 1 → Start marker
  • base_id + 2 → End marker

Passthrough Transmission Flow

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

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

c
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_requested flag
  • 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

c
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):

c
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

c
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_flag global 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

Released under the MIT License.