The Distributed BMS system uses CAN (Controller Area Network) bus for communication between daughter boards and the secondary board. This document specifies the CAN protocol implementation, message formats, and communication procedures.
- Standard: CAN 2.0B
- Bit Rate: 500 kbps
- Physical Layer: ISO 11898-2 (High-Speed CAN)
- Termination: 120Ω resistors at both ends of the bus
- Connector: Standard CAN bus connector (9-pin D-sub)
| Parameter | Value | Notes |
|---|---|---|
| Bit Rate | 500 kbps | Fixed rate for all nodes |
| Bus Length | Up to 100m | With proper termination |
| Node Count | Up to 8 | Daughter boards per secondary |
| Voltage Levels | 0V (Recessive), 3.3V (Dominant) | Differential signaling |
| Termination | 120Ω | Required at bus ends |
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ CAN ID │ DLC │ Type │ Data │
│ (11-bit) │ (4-bit) │ (1-byte) │ (7-bytes) │
└─────────────┴─────────────┴─────────────┴─────────────┘
- Range: 0x000 - 0x7FF
- Usage: Identifies the source daughter board
- Assignment: Each daughter board has a unique ID
- Example: Daughter board 1 uses ID 0x101
- Value: Always 8 (0x8)
- Purpose: Indicates 8-byte payload
- Fixed: All BMS messages use full 8-byte payload
- Purpose: Identifies the type of data in the message
- Values:
0x00: HIGH_TEMP0x01: VOLTAGE_EXTREMES0x02: AVERAGES
- Purpose: Contains the actual measurement data
- Format: Depends on message type (see below)
Purpose: Transmits highest temperature reading and sensor index
Format:
Byte 0: Message Type (0x00)
Byte 1-4: Temperature (float, little-endian)
Byte 5: Temperature sensor index (0-4)
Byte 6-7: Reserved (0x00)
Example:
// Temperature: 25.5°C, Sensor index: 2
uint8_t data[8] = {
0x00, // HIGH_TEMP message type
0x00, 0x00, 0xCC, 0x41, // 25.5f as float
0x02, // Sensor index 2
0x00, 0x00 // Reserved
};Encoding Function:
Frame encodeHighTemp(float highTemp, uint8_t highIndex) {
Frame f{};
f.data[0] = HIGH_TEMP;
std::memcpy(&f.data[1], &highTemp, 4);
f.data[5] = highIndex;
return f;
}Decoding Function:
bool decodeHighTemp(const uint8_t* data, float& temp, uint8_t& idx) {
if (data[0] != HIGH_TEMP) return false;
std::memcpy(&temp, &data[1], 4);
idx = data[5];
return true;
}Purpose: Transmits highest and lowest cell voltages with their indices
Format:
Byte 0: Message Type (0x01)
Byte 1-2: Highest voltage (uint16_t, little-endian)
Byte 3-4: Lowest voltage (uint16_t, little-endian)
Byte 5: Lowest voltage cell index (0-4)
Byte 6: Highest voltage cell index (0-4)
Byte 7: Reserved (0x00)
Example:
// High: 3720mV (cell 2), Low: 3650mV (cell 1)
uint8_t data[8] = {
0x01, // VOLTAGE_EXTREMES message type
0x88, 0x0E, // 3720 as uint16_t
0x44, 0x0E, // 3650 as uint16_t
0x01, // Low voltage cell index
0x02, // High voltage cell index
0x00 // Reserved
};Encoding Function:
Frame encodeVoltageExtremes(uint16_t highV, uint16_t lowV,
uint8_t lowIdx, uint8_t highIdx) {
Frame f{};
f.data[0] = VOLTAGE_EXTREMES;
f.data[1] = highV & 0xFF;
f.data[2] = highV >> 8;
f.data[3] = lowV & 0xFF;
f.data[4] = lowV >> 8;
f.data[5] = lowIdx;
f.data[6] = highIdx;
return f;
}Decoding Function:
bool decodeVoltageExtremes(const uint8_t* data, uint16_t& highV,
uint16_t& lowV, uint8_t& lowIdx, uint8_t& highIdx) {
if (data[0] != VOLTAGE_EXTREMES) return false;
highV = (data[2] << 8) | data[1];
lowV = (data[4] << 8) | data[3];
lowIdx = data[5];
highIdx = data[6];
return true;
}Purpose: Transmits average temperature, average voltage, and cell count
Format:
Byte 0: Message Type (0x02)
Byte 1-4: Average temperature (float, little-endian)
Byte 5-6: Average voltage (uint16_t, little-endian)
Byte 7: Number of cells (uint8_t)
Example:
// Avg temp: 26.0°C, Avg voltage: 3685mV, 4 cells
uint8_t data[8] = {
0x02, // AVERAGES message type
0x00, 0x00, 0xD0, 0x41, // 26.0f as float
0x65, 0x0E, // 3685 as uint16_t
0x04 // 4 cells
};Encoding Function:
Frame encodeAverages(float avgTemp, uint16_t avgVoltage, uint8_t numCells) {
Frame f{};
f.data[0] = AVERAGES;
std::memcpy(&f.data[1], &avgTemp, 4);
f.data[5] = avgVoltage & 0xFF;
f.data[6] = avgVoltage >> 8;
f.data[7] = numCells;
return f;
}Decoding Function:
bool decodeAverages(const uint8_t* data, float& avgTemp,
uint16_t& avgVoltage, uint8_t& numCells) {
if (data[0] != AVERAGES) return false;
std::memcpy(&avgTemp, &data[1], 4);
avgVoltage = (data[6] << 8) | data[5];
numCells = data[7];
return true;
}Each daughter board transmits three messages per cycle:
// Daughter board main loop
while (1) {
// ... data collection ...
// Build messages
auto f0 = CanFrames::make_high_temp(results);
auto f1 = CanFrames::make_voltage_extremes(results);
auto f2 = CanFrames::make_average_stats(results);
// Transmit messages
can.sendStd(0x07, f0.bytes, f0.dlc);
can.sendStd(0x07, f1.bytes, f1.dlc);
can.sendStd(0x07, f2.bytes, f2.dlc);
HAL_Delay(250); // 250ms cycle time
}The secondary board receives and processes messages from all daughter boards:
// Secondary board main loop
while (1) {
CanBus::Frame rx;
if (can.read(rx)) {
fleet.handle(rx, HAL_GetTick());
}
}The secondary board processes received messages based on type:
void BmsFleet::handle(const CanBus::Frame& rx, uint32_t now_ms) {
int mi = find_module_index(rx.id);
if (mi < 0) return; // Not registered
ModuleData& M = modules_[mi];
const uint8_t* b = rx.data;
switch (CanFrames::getType(b)) {
case CanFrames::HIGH_TEMP: {
float t; uint8_t idx;
if (CanFrames::decodeHighTemp(b, t, idx)) {
M.high_C = t;
M.high_temp_idx = idx;
M.last_ms = now_ms;
M.got_type0 = true;
}
} break;
case CanFrames::VOLTAGE_EXTREMES: {
uint16_t hv, lv; uint8_t li, hi;
if (CanFrames::decodeVoltageExtremes(b, hv, lv, li, hi)) {
M.high_mV = hv; M.low_mV = lv;
M.low_idx = li; M.high_idx = hi;
M.last_ms = now_ms;
M.got_type1 = true;
}
} break;
case CanFrames::AVERAGES: {
float at; uint16_t av; uint8_t nc;
if (CanFrames::decodeAverages(b, at, av, nc)) {
M.avg_C = at; M.avg_cell_mV = av; M.num_cells = nc;
M.last_ms = now_ms;
M.got_type2 = true;
}
} break;
}
}The system handles various CAN bus error conditions:
if (can.sendStd(0x07, data, 8) != CanBus::Result::Ok) {
faultManager.setFault(FaultManager::FaultType::CAN_TRANSMIT_ERROR, true);
} else {
faultManager.clearFault(FaultManager::FaultType::CAN_TRANSMIT_ERROR);
}// Check for dropped frames
if (can.rx_dropped() > 0) {
faultManager.setFault(FaultManager::FaultType::CAN_RECEIVE_ERROR, true);
}// Monitor bus status
if (HAL_CAN_GetError(&hcan1) != HAL_CAN_ERROR_NONE) {
// Handle bus errors
HAL_CAN_ResetError(&hcan1);
}All received messages are validated before processing:
bool validateMessage(const CanBus::Frame& frame) {
// Check DLC
if (frame.dlc != 8) return false;
// Check message type
uint8_t type = CanFrames::getType(frame.data);
if (type > 2) return false;
// Check reserved bytes
if (frame.data[6] != 0 || frame.data[7] != 0) return false;
return true;
}| Parameter | Value | Notes |
|---|---|---|
| Cycle Time | 250ms | Daughter board main loop |
| Message Interval | ~83ms | 3 messages per cycle |
| Transmission Time | <10ms | Per message |
| Bus Utilization | <5% | At 500 kbps |
| Parameter | Value | Notes |
|---|---|---|
| Processing Time | <1ms | Per received message |
| Queue Depth | 16 frames | Circular buffer size |
| Timeout | 1500ms | Module offline detection |
Each daughter board must have a unique CAN ID:
| Daughter Board | CAN ID | Description |
|---|---|---|
| Board 1 | 0x101 | First daughter board |
| Board 2 | 0x102 | Second daughter board |
| Board 3 | 0x103 | Third daughter board |
| ... | ... | ... |
| Board 8 | 0x108 | Eighth daughter board |
The secondary board uses ID 0x200 for any responses or status messages.
| ID Range | Purpose | Notes |
|---|---|---|
| 0x000-0x100 | System messages | Reserved for system use |
| 0x109-0x1FF | Future expansion | Available for additional boards |
| 0x201-0x7FF | Future use | Available for other purposes |
Daughter boards configure filters to receive system messages:
// Accept all messages (for system commands)
can.configureFilterAcceptAll();Secondary board configures filters to receive daughter board messages:
// Filter for daughter board messages (0x101-0x108)
can.configureFilterStdMask(0x100, 0x1F8); // Mask: 0x1F8 = 1111111000The protocol is designed to work with standard CAN bus analyzers:
// Log all transmitted messages
printf("TX: ID=0x%03X, DLC=%d, Data=", frame.id, frame.dlc);
for (int i = 0; i < frame.dlc; i++) {
printf("%02X ", frame.data[i]);
}
printf("\n");// Monitor CAN errors
uint32_t errors = HAL_CAN_GetError(&hcan1);
if (errors != HAL_CAN_ERROR_NONE) {
printf("CAN Error: 0x%08X\n", errors);
}The system collects communication statistics:
// Get transmission statistics
uint32_t tx_ok = can.tx_ok();
uint32_t tx_err = can.tx_err();
uint32_t rx_dropped = can.rx_dropped();
printf("CAN Stats: TX_OK=%d, TX_ERR=%d, RX_DROPPED=%d\n",
tx_ok, tx_err, rx_dropped);The protocol can be extended with additional message types:
enum MessageType : uint8_t {
HIGH_TEMP = 0,
VOLTAGE_EXTREMES = 1,
AVERAGES = 2,
// Future extensions
BALANCING_STATUS = 3,
FAULT_STATUS = 4,
CALIBRATION_DATA = 5,
// ... up to 255
};Future protocol versions can include version information:
// Extended message format with version
struct ExtendedFrame {
uint8_t version; // Protocol version
uint8_t message_type; // Message type
uint8_t data[6]; // Payload data
};- ISO 11898-1: CAN protocol specification
- ISO 11898-2: High-speed CAN physical layer
- ISO 11898-3: Low-speed CAN physical layer
- CAN 2.0B: Extended frame format support
- ISO 14229: UDS (Unified Diagnostic Services)
- ISO 15765: Diagnostic communication over CAN
- SAE J1939: Heavy-duty vehicle communication
- Use consistent data formats across all message types
- Include validation fields in message structure
- Design for extensibility and backward compatibility
- Use meaningful message type identifiers
- Implement comprehensive error detection
- Provide graceful degradation on communication failures
- Log all communication errors for debugging
- Implement automatic recovery procedures
- Minimize message payload size
- Use efficient encoding/decoding algorithms
- Implement proper buffering for high-throughput scenarios
- Monitor bus utilization and performance
- Validate all received messages
- Implement message authentication if required
- Protect against message injection attacks
- Use secure CAN ID assignment procedures
This CAN communication protocol provides a robust, scalable foundation for the distributed BMS system communication requirements.