The problem
Many first BMS programs run everything in one while-loop. When the display or serial logging gets slow, protection checks are delayed. Production BMS firmware splits the work into prioritised tasks, and students need to see the difference in measured numbers.
Objectives
- Build a baseline super-loop BMS: sampling, protection, display and UART logging.
- Port it to FreeRTOS with tasks, a queue, a mutex and software timers.
- Measure protection response time and sampling jitter in both versions.
- Size stacks and heap correctly within 20 KB RAM.
What you need
From the EV Embedded Development Kit
- Smart BMS unit STM32F103C8T6
- 1S battery with connector and T-sense input
- Load section: three switchable resistive loads with LEDs
- Charge/discharge control relay
- BMS power switch, OLED and six navigation buttons
- Serial UART interface
Software
- STM32CubeIDE with HAL drivers
- FreeRTOS through the CubeMX CMSIS-RTOS v2 wrapper
- Embedded C
- Python 3 with pyserial and matplotlib for timing analysis
Extra parts
None. Everything this project needs is on the kit.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- Super-loop
- A single endless loop that calls each function in turn. Any slow function delays every other one.
- Pre-emptive priority scheduling
- FreeRTOS always runs the highest-priority ready task, so a protection task can interrupt a slow display update.
- Queue and mutex
- A queue passes data between tasks safely; a mutex protects shared resources such as the UART or a shared measurement structure, with priority inheritance to limit blocking.
- Jitter
- Variation in the time between consecutive runs of a periodic task. Low jitter gives consistent sampling for filters and SOC estimation.
- Stack high-water mark
- uxTaskGetStackHighWaterMark reports the least free stack a task has had, used to size stacks without wasting RAM.
Step-by-step guide
- 1
Set requirements and safety rules
Specify the BMS functions: sample voltage and temperature every 10 ms, check over-voltage, under-voltage and over-temperature and open the relay within 20 ms of a violation, update the OLED every 200 ms and log over UART every 100 ms. Write lithium safety rules for the team: correct polarity, never short the terminals, supervised charging only. Protection limits are tested by moving software thresholds, not by abusing the cell.
Checkpoint: A requirements table with periods and deadlines, and a signed safety sheet.
- 2
Configure the BMS unit in CubeMX
Confirm the pin mapping for the cell voltage, T-sense, load switches, relay, OLED and buttons from the kit schematic. Enable ADC1 with DMA, I2C for the OLED, GPIO for relay and loads, a USART at 115200 baud and two spare GPIOs as timing probes. Set the DIP switches for the battery and load section and the BMS OLED. Keep a copy of the .ioc file for both versions.
Checkpoint: The OLED shows a test screen and a raw ADC value that changes when a load is switched on.
- 3
Build the super-loop baseline
Write the loop: start an ADC conversion, read and convert voltage and temperature, run protection, redraw the whole OLED, format and send a UART log line with blocking HAL_UART_Transmit, then poll buttons. Toggle a timing GPIO at each sample and another when protection acts. This is the reference design to beat. Keep this baseline in its own git branch so it can be measured again later under exactly the same conditions as the RTOS version.
Checkpoint: The baseline works: values update, loads switch and lowering the software over-voltage limit opens the relay.
- 4
Measure the baseline
Record sample timestamps with the DWT cycle counter and log them, or measure the GPIO with a timer input capture on a spare pin. Measure protection response: inject a threshold crossing by changing the limit from a button and record the time to relay command. Then make the display redraw deliberately slow (full-screen redraw every loop) and measure again. Collect at least a few thousand samples per condition.
Checkpoint: A table of sampling period mean, minimum, maximum and jitter, and protection response time, under normal and slow-display conditions.
- 5
Enable FreeRTOS
Enable FreeRTOS in CubeMX with CMSIS-RTOS v2, change the HAL timebase from SysTick to a spare timer (for example TIM4) as CubeMX recommends, set configTOTAL_HEAP_SIZE to fit the RAM budget and enable stack overflow checking (configCHECK_FOR_STACK_OVERFLOW 2) and uxTaskGetStackHighWaterMark. Start with a heap of around 8 KB and adjust after measuring. Implement vApplicationStackOverflowHook and vApplicationMallocFailedHook so they force the relay open and light a fault LED, because a memory error in a BMS must end in a safe state, not a silent hang.
Checkpoint: A blinking test task runs and the stack overflow hook is reached when a test task overflows deliberately.
- 6
Design the task set
Create tasks: Sample (highest priority, 10 ms using vTaskDelayUntil), Protect (high, woken by a notification from Sample), Estimate (medium, 100 ms, simple SOC by voltage lookup), Display (low, 200 ms) and Log (low, 100 ms). Pass measurements from Sample to the others through a queue or a mutex-protected structure, and guard the UART with a mutex. Check that the total CPU load estimate leaves comfortable headroom.
Checkpoint: A task diagram with priorities, periods, communication paths and stack sizes.
- 7
Port the code
Move each function from the super-loop into its task without changing its logic. Replace blocking UART calls with DMA transmission completed by a callback that gives a semaphore. Make the Protect task act directly on the relay GPIO. Keep the button handling in a low-priority task or an EXTI interrupt that sends a notification. Avoid calling blocking HAL functions from the high-priority tasks.
Checkpoint: The RTOS version reproduces the baseline's displayed values and protection behaviour.
- 8
Measure the RTOS version
Repeat exactly the baseline measurements, including the slow-display condition. Check stack high-water marks for each task and free heap with xPortGetFreeHeapSize, and trim stacks to keep a safety margin of at least 25 per cent. Use the same timing probes, the same injected threshold crossings and the same slow-display setting as for the baseline, so differences come only from the architecture. Plot histograms of sampling period for both versions on the same axes for the report.
Checkpoint: A side-by-side table showing jitter and protection response for both designs under both conditions, plus stack and heap usage.
- 9
Test failure cases
Make a low-priority task hog the CPU in a busy loop and confirm protection still meets its deadline. Hold the UART mutex for a long time and observe priority inheritance. Starve the Display task and show only the display slows. Record what happens if a high-priority task never blocks, as a lesson in task design. Repeat each test in the super-loop build where it applies, to show the contrast.
Checkpoint: A failure-case log describing each test, the observed behaviour and the explanation.
- 10
Document the port
Write up the requirements, both designs, measurement method, results, RAM budget and lessons learnt, including when a super-loop is still acceptable. Include the task diagram, priority rationale, stack and heap table, measurement method, side-by-side results and failure-case log. Explain how a reviewer could check that protection always meets its deadline, and which parts of the design would need more work for a production BMS, such as a task-health watchdog and CAN reporting.
Checkpoint: A report with timing histograms for both versions and the task diagram.
Testing & validation
- Protection response time meets the 20 ms requirement in the RTOS version even with a slow display.
- Sampling jitter measured and compared with the baseline.
- Stack high-water marks show at least 25 per cent margin for every task.
- Functional results match between baseline and RTOS versions.
Expected results
A FreeRTOS BMS on the Smart BMS unit with measured improvements in protection response and sampling jitter over a super-loop baseline, and a documented RAM budget.
Take it further
- Add CAN transmission of BMS status as a separate task.
- Use FreeRTOS run-time statistics to measure CPU load per task.
- Add a task-health watchdog that feeds the IWDG only when all tasks check in.
Viva and review questions
Why move the HAL timebase off SysTick?
FreeRTOS uses SysTick for its tick; giving HAL its own timer avoids conflicts and keeps HAL_Delay and timeouts working.
Why does the RTOS version react faster?
Protection runs in a high-priority task that pre-empts the slow display, instead of waiting for the loop to come round.
What is priority inversion and how is it limited?
A high-priority task waits on a resource held by a low-priority one while a medium task runs. FreeRTOS mutexes use priority inheritance to shorten this.
How did you size the stacks?
Started generous, ran all scenarios, read uxTaskGetStackHighWaterMark, then trimmed while keeping a 25 per cent margin.