The problem
A BMS can only protect and estimate what it measures correctly. In an EV pack, a few millivolts of cell-voltage error moves the state-of-charge estimate noticeably on the flat part of the Li-ion curve, and a wrong temperature reading can let a cell charge when it is too cold or too hot. This project teaches calibrated, filtered voltage and temperature measurement on a real cell, the first job of any BMS front end.
Objectives
- Measure the 1S cell voltage through the ADC with a calibrated error target of ±10 mV.
- Convert the T-sense input to degrees Celsius with a target error of ±1 °C at room temperature.
- Filter the readings so the display is stable but still follows a load step within half a second.
- Show voltage, temperature and a colour-coded status on the 16×2 LCD and RGB LEDs.
- Document the measurement chain, its error sources and a calibration procedure another team could repeat.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- 1S battery with connector and T-sense input
- Load section: three switchable resistive loads with LEDs
- Battery charging unit with its own switch
- 16×2 LCD with contrast control
- RGB LEDs and a buzzer
- BMP280 pressure and temperature sensor
Software
- STM32CubeIDE
- Embedded C with STM32 HAL
- STM32CubeMX pin and clock configurator (inside CubeIDE)
- Spreadsheet or Python for the calibration table
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
- ADC resolution and reference
- The STM32F103 ADC is 12-bit, so with a 3.3 V reference one count is about 0.8 mV at the pin. A voltage divider scales the 4.2 V cell into range and multiplies that step size by the divider ratio.
- VREFINT
- An internal reference of about 1.20 V that the ADC can read on channel 17. Measuring it lets firmware correct for changes in the 3.3 V supply that would otherwise shift every reading.
- NTC thermistor conversion
- A thermistor's resistance falls as temperature rises. The Beta equation or a lookup table converts the measured resistance to temperature; a table avoids slow software logarithms on a chip with no FPU.
- Two-point calibration
- Measuring two known inputs gives a gain and an offset correction. It removes most of the error caused by divider resistor tolerance and ADC offset.
- Moving-average filtering
- Averaging the last N samples reduces random noise by roughly the square root of N, at the cost of a delay of about N/2 samples.
Step-by-step guide
- 1
Write the measurement specification
Before touching the board, list what you will measure and how well. The cell works between about 2.5 V and 4.2 V; temperature of interest is roughly 0 to 60 °C. Set targets: voltage error ±10 mV, temperature error ±1 °C, display update twice a second, and a status rule (green normal, amber warning, red limit). Work out the theoretical step size: 3.3 V / 4096 is about 0.8 mV at the ADC pin, multiplied by your divider ratio at the cell. This tells you whether the targets are realistic before you write code.
Checkpoint: A one-page specification with ranges, accuracy targets, update rate and status thresholds, reviewed by the team.
- 2
Set up the board safely
Check the cell is seated with the correct polarity at its connector and never bridge its terminals. Using the DIP switches, enable the battery-sense, T-sense, LCD and RGB LED blocks and route them to the controller you are programming, following the kit's block labels and pin map. Leave the charging unit and loads switched off for now. Connect the ST-Link to the programming header, power the board from the master switch and flash a simple LED-blink program to prove the toolchain works.
Checkpoint: An LED blinks at the rate you programmed and the debugger can halt and resume the code.
- 3
Configure clocks and the ADC in CubeMX
Set the 8 MHz HSE through the PLL to a 72 MHz system clock and set the ADC prescaler to divide by 6 so the ADC clock is 12 MHz, inside the 14 MHz limit. Enable ADC1 in scan mode with three channels: cell-voltage divider, T-sense, and VREFINT. Choose a long sampling time (239.5 cycles) because divider and thermistor sources have high impedance. Use circular DMA so the three results land in a small array without CPU effort, and start it with HAL_ADC_Start_DMA.
Checkpoint: In the debugger's Live Expressions view the three raw counts update continuously and change when you touch nothing but switch a load on.
- 4
Calibrate the ADC and correct for supply drift
Call HAL_ADCEx_Calibration_Start once after power-up and before starting conversions; it removes the ADC's internal offset. Then use VREFINT to estimate the real analog supply: VDDA is about 1.20 V × 4095 / VREFINT_count. The internal reference itself has a spread given in the datasheet, so measure the board's 3.3 V rail with a multimeter once and adjust the constant. From here on, convert every count to millivolts using the measured VDDA rather than assuming exactly 3.3 V.
Checkpoint: The computed VDDA agrees with the multimeter reading of the 3.3 V rail within about 10 mV.
- 5
Scale and calibrate the cell voltage
Measure the divider ratio rather than trusting nominal resistor values: read the cell with a multimeter at its terminals and compare with the ADC result. Record readings at two states of charge (for example a rested cell near 3.6 V and after a short supervised charge near 4.1 V) and compute gain and offset. Store them as integers so the conversion is millivolts = (counts × gain_num) / gain_den + offset, which is fast on a Cortex-M3 with no FPU.
Checkpoint: At three different cell voltages the firmware reading is within ±10 mV of the multimeter.
- 6
Convert T-sense to temperature
Find the thermistor and bias resistor values in the kit documentation (an NTC is typical). Compute the thermistor resistance from the divider reading, then convert to temperature. Instead of calling log() in software floating point, build a lookup table of resistance against temperature every 5 °C from the Beta or Steinhart–Hart equation on your PC and interpolate linearly between entries in firmware. Use the BMP280's temperature reading or a reference thermometer next to the cell as a room-temperature check.
Checkpoint: With the cell at rest, cell temperature and the reference agree within ±1 °C; holding the cell lightly with fingers raises the reading within a minute.
- 7
Filter the readings
Raw readings jitter by several counts. Apply a 16-sample moving average or a first-order IIR filter (y += (x − y) / 8) in integer arithmetic. Compare the noise before and after by logging 200 samples in the debugger. Check the trade-off: the filter must not hide a real load step. Switch on one resistive load and confirm the filtered value settles within half a second. Add a separate, slower filter for temperature because it changes slowly.
Checkpoint: Displayed voltage varies by no more than 2 mV with no load, and a load step shows up on the display within 0.5 s.
- 8
Build the LCD screen and status LEDs
Write the 16×2 LCD driver with initialisation, cursor positioning and string output. Line 1 shows voltage and temperature, for example V:3.912 T:28.4C; line 2 shows status text. Overwrite characters in place instead of clearing the screen each update, which avoids flicker. Drive the RGB LEDs from the status rule, adding hysteresis (for example 30 mV and 2 °C) so the colour does not chatter at a threshold. Sound the buzzer once when the status first turns red.
Checkpoint: Switching loads moves the voltage reading and the LED changes colour at the specified thresholds without flicker or chatter.
- 9
Test under rest, load and charge
Run a test matrix: cell at rest, each of the three loads, all loads together and a short supervised charge with the charging unit switch. Never leave the cell charging unattended. At each point record the multimeter voltage, the displayed voltage, the reference temperature and the displayed temperature. Calculate the error at every point and check it against the specification. If errors are larger under load, check the sense wiring and sampling time before blaming the calibration.
Checkpoint: A filled test table showing every voltage error within ±10 mV and temperature within ±1 °C, or a written explanation where it is not.
- 10
Document the design
Write up the measurement chain from cell to display: divider, ADC configuration, calibration constants, filter design and conversion tables. Include the error budget you predicted in step 1 against what you measured, a photo of the board with DIP-switch positions, and the calibration procedure as a numbered list so another team could repeat it. Keep the code in a version-controlled repository with the CubeMX .ioc file so the configuration can be regenerated.
Checkpoint: A report and repository that a reviewer can use to rebuild and recalibrate the monitor without asking you questions.
Testing & validation
- Voltage error against a calibrated multimeter at no load, each single load and during charge, target ±10 mV.
- Temperature error against a reference thermometer or the BMP280 at room temperature, target ±1 °C.
- Noise: standard deviation of 200 filtered voltage samples at rest.
- Step response: time for the display to settle after a load is switched on, target under 0.5 s.
- Status thresholds and hysteresis verified by slowly varying the input.
Expected results
A working cell monitor that shows calibrated voltage and temperature on the LCD with colour-coded status, a measured error table, and a documented calibration method that later BMS projects can reuse.
Take it further
- Log readings to the SPI flash with RTC timestamps.
- Send readings over UART to a PC and plot them live in Python.
- Add a cold-charge warning that blocks charging below a set temperature.
- Move the readings onto the BMS OLED and use the six navigation buttons for a settings menu.
Viva and review questions
Why do you need a voltage divider to measure the cell?
A full cell is about 4.2 V but the ADC input must stay within its 3.3 V supply. The divider scales the voltage into range, and its ratio must be calibrated because resistor tolerance adds error.
What does VREFINT correct for?
The ADC result is relative to its supply voltage. Reading the internal reference lets the firmware compute the actual supply and remove errors when the 3.3 V rail drifts.
Why use a lookup table for the thermistor?
The STM32F103 has no floating-point unit, so logarithms are slow in software. A small interpolated table is fast, deterministic and accurate enough over the working range.
What is the cost of heavier filtering?
More averaging lowers noise but delays the response. Too much filtering would hide a sudden voltage drop that protection logic needs to see quickly.