The problem
In an EV the BMS does not drive the dashboard directly; it sends its data over CAN to other ECUs that use and display it. Getting this right needs a clear message definition, correct bit timing, filtering, and handling of missing or invalid data. This project builds that link end to end between two control units, as on a vehicle.
Objectives
- Define a small message set for BMS status with IDs, cycle times and signal scaling.
- Configure bxCAN bit timing, filters and interrupts on both nodes.
- Pack battery data on the BMS node and decode it on the dashboard node.
- Detect message timeouts and invalid values on the dashboard side.
- Measure bus behaviour and document the interface.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- 1S battery with connector and T-sense input
- CAN bus interface with screw terminal
- 16×2 LCD with contrast control
- RGB LEDs and a buzzer
- Load section: three switchable resistive loads with LEDs
Software
- STM32CubeIDE
- Embedded C with STM32 HAL
Extra parts
- A second DIYguru EV Embedded Development Kit — The board has one CAN node. A second kit acts as the dashboard ECU with its own LCD, giving a real two-node bus; a USB-CAN adapter on a PC can stand in for early testing.
- Twisted-pair cable with two 120 Ω termination resistors — To connect the two screw terminals as a short, properly terminated CAN bus, unless termination is already fitted on the boards.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- CAN identifier and priority
- The 11-bit standard ID labels the message and sets its priority: a lower ID wins arbitration. Safety-relevant messages get lower IDs.
- Bit timing
- Each bit is divided into time quanta; the sample point near the end of the bit must match on all nodes. With APB1 at 36 MHz, prescaler 4 and 18 quanta gives 500 kbit/s.
- Acceptance filters
- bxCAN hardware filters let only wanted IDs reach the receive FIFO, saving CPU time on a busy bus.
- Signal timeout
- If an expected periodic message stops arriving, the receiver must treat its data as invalid rather than showing the last value forever.
Step-by-step guide
- 1
Define the message set
Write an interface document. For example ID 0x100 every 100 ms: cell voltage in millivolts (16 bits), temperature in 0.1 °C with an offset (16 bits), SOC in 0.5 per cent steps (8 bits), status bits (8 bits) and a rolling counter (4 bits). ID 0x101 every 1 s: limits and firmware version. State byte order, scaling, offset, range and the value meaning invalid for each signal.
Checkpoint: An interface table both team halves agree on before coding.
- 2
Wire the bus
Connect the CAN screw terminals of the two kits with twisted pair: CAN_H to CAN_H, CAN_L to CAN_L and a common ground. Check whether each board already has a 120 Ω termination; the bus needs about 60 Ω between CAN_H and CAN_L with both boards unpowered. Enable the CAN block DIP switches on both boards. Keep the cable short and twisted, and label which end is which board.
Checkpoint: A multimeter reads about 60 Ω between CAN_H and CAN_L with both boards unpowered.
- 3
Configure bxCAN
In CubeMX enable CAN on the pins your kit routes to the transceiver; on the STM32F103 the CAN peripheral shares RAM with USB, so the two cannot be used together. Set 500 kbit/s: prescaler 4, time segment 1 of 15 quanta, time segment 2 of 2 quanta and SJW 1. Enable automatic bus-off management. Before connecting nodes, test each board alone in loopback mode, where it receives its own frames.
Checkpoint: Each board in loopback mode receives the frame it transmits.
- 4
Write the BMS node
On the BMS node, read calibrated cell voltage and temperature as in the voltage-monitor project and compute a simple SOC from rested voltage or a placeholder for now. Pack signals into the 8-byte payload with explicit shifts and masks, not structure overlays. Transmit with HAL_CAN_AddTxMessage from a 100 ms timer flag, checking HAL_CAN_GetTxMailboxesFreeLevel first and incrementing the rolling counter each frame.
Checkpoint: In loopback the packed bytes match a hand calculation of the expected payload.
- 5
Write the dashboard node
On the dashboard kit, configure a filter bank in ID-mask mode to accept only 0x100 and 0x101, start with HAL_CAN_Start and enable CAN_IT_RX_FIFO0_MSG_PENDING. In HAL_CAN_RxFifo0MsgPendingCallback, read with HAL_CAN_GetRxMessage, decode, range-check and store values with a timestamp. Update the LCD from the main loop, not the interrupt. Show voltage and temperature on line 1 and SOC with a status word on line 2. Count received frames per second and show the count on a debug screen selected from a key, so you can see straight away if frames are being lost.
Checkpoint: The dashboard LCD shows the BMS node's voltage and temperature and they change when a load is switched on the BMS kit.
- 6
Handle timeouts and invalid data
On the dashboard, if 0x100 has not arrived for three cycles (300 ms), show dashes in place of values and light the amber LED with a 'BMS LOST' message. Check the rolling counter for repeated or skipped values and the signal ranges for impossible numbers. Show a red LED and buzzer for BMS fault status bits. Log each timeout event with a timestamp so its cause can be investigated later.
Checkpoint: Unplugging the CAN cable makes the dashboard show 'BMS LOST' within about 300 ms; reconnecting restores values.
- 7
Handle bus errors
Enable error interrupts and implement HAL_CAN_ErrorCallback to count errors. Read the transmit and receive error counters from the CAN error status register and show them on a debug screen. Test by disconnecting one wire while transmitting, then reconnecting: with automatic bus-off management the node should recover. Watch what the dashboard shows while the BMS node is in an error state: it should report lost data, not stale values. Record how long recovery takes after reconnection, and explain the role of the 128 occurrences of 11 recessive bits that bus-off recovery waits for.
Checkpoint: Error counters rise when the bus is broken and the link recovers without a reset after reconnection.
- 8
Test and measure
Log 10 minutes of operation on the dashboard: number of frames received, missed frames from the counter, and timing jitter between arrivals measured with a microsecond timer. Calculate the bus load from your message set (a standard 8-byte frame is up to about 135 bits with worst-case stuffing). Check the BMS values on the dashboard against a multimeter on the BMS cell.
Checkpoint: A test log with zero missed frames, measured jitter and calculated bus load.
- 9
Document the interface
Finalise the interface document with the as-built details, filter configuration, bit timing, timeout rules and test results. Add a wiring diagram and photos of both kits. Keep both firmware projects in one repository with a shared header for message definitions so the nodes cannot drift apart. Add a version number to the interface document and to the 0x101 message, so a dashboard can detect a BMS running an incompatible message definition. Record every change to the message set with the reason in a change log.
Checkpoint: A shared message header used by both nodes and a complete interface document.
Testing & validation
- Loopback self-test on each node.
- Bus resistance about 60 Ω with correct termination.
- Decoded values on the dashboard match the BMS measurements.
- Timeout detection within the specified time.
- Recovery after bus disconnection without reset.
Expected results
A working BMS-to-dashboard CAN link between two control units, with a documented message set, timeout and error handling, and measured bus behaviour.
Take it further
- Add a checksum signal for end-to-end protection.
- Add the dashboard's keypad to request a BMS reset over CAN.
- Log all CAN traffic to SPI flash with RTC timestamps.
- Move the message definitions into a DBC file and generate the pack and unpack code.
Viva and review questions
Why must both nodes use the same bit timing?
CAN nodes sample each bit at a configured point; if bit rates or sample points differ, nodes misread bits and generate errors.
What is the 120 Ω termination for?
It matches the cable's characteristic impedance at both ends to stop reflections, and defines the recessive state of the bus.
Why add a rolling counter?
It lets the receiver detect lost or repeated frames and a transmitter that has frozen while still sending.
Why decode in the interrupt but update the LCD in the main loop?
LCD writes are slow; keeping them out of the interrupt keeps receive latency low and avoids missing frames.