The problem
Open-circuit voltage is one of the few direct windows into a cell's state of charge, and nearly every BMS uses an OCV–SOC table to set or correct its SOC estimate. Typical tables copied from the internet do not match a specific cell. This project measures the curve for the actual kit cell, shows where it is flat and steep, and turns it into an embedded lookup table.
Objectives
- Run an automated pulse-and-rest discharge test that records OCV at roughly 5 per cent SOC intervals.
- Estimate the charge removed in each pulse from measured voltage and calibrated load resistance.
- Store the OCV–SOC table in SPI flash with a header and checksum.
- Estimate SOC from rested voltage by interpolation and quantify the error in flat regions.
- Explain the limits of OCV-based SOC: rest time, temperature and hysteresis.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- 1S battery with connector and T-sense input
- Battery charging unit with its own switch
- Charge/discharge control relay
- Load section: three switchable resistive loads with LEDs
- SPI flash memory
- Serial UART interface
- 16×2 LCD with contrast control
Software
- STM32CubeIDE
- Embedded C with STM32 HAL
- Python with pyserial, pandas, NumPy and matplotlib
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
- OCV–SOC relationship
- A rested Li-ion cell's voltage rises with SOC. The curve is steep near empty and full and relatively flat in the middle, where a small voltage error means a large SOC error.
- Pulse-and-rest test
- Discharging a small amount of charge, then resting until the voltage settles, gives one OCV point per pulse. It is the low-cost version of the lab GITT method.
- Relaxation
- After current stops, the voltage keeps recovering for many minutes as concentration gradients relax, so OCV must be read after a fixed, adequate rest.
- Hysteresis
- OCV measured after charging is slightly higher than after discharging at the same SOC. This project measures the discharge branch only, so it must be used consistently.
Step-by-step guide
- 1
Plan the test
Pick a discharge load and pulse length so each pulse removes about 5 per cent of the cell's nominal capacity, giving around 20 points. Choose a rest time per point; start with 30 minutes and check later whether the voltage has stopped moving. Calculate the total test time, which can be a full day, and plan supervision. Define the cut-off voltage above the cell's rated minimum as the 0 per cent point of your table.
Checkpoint: A test plan with pulse length, rest time, expected number of points and total duration.
- 2
Set up and calibrate
Check cell polarity. Enable DIP switches for battery sense, T-sense, relay, loads, SPI flash, LCD and UART. Calibrate cell voltage against a multimeter at two points, because the OCV table is only as accurate as the voltage reading. Measure the chosen load resistance with the board unpowered and the cell disconnected. Configure the ADC with DMA and VREFINT correction and heavy averaging for rest readings.
Checkpoint: Rest voltage reading within ±5 mV of the multimeter with a 64-sample average.
- 3
Bring up the SPI flash
Configure SPI1 in master mode with a modest clock and a GPIO chip select. Send the JEDEC ID command (0x9F) and decode the manufacturer and capacity bytes to identify the part. Implement write enable, sector erase, page program and read, waiting on the status register busy bit after each erase and program. Reserve one sector for the OCV table. Respect the page size when programming so writes do not wrap within a page.
Checkpoint: A pattern written to a sector reads back identically after a power cycle.
- 4
Estimate charge per pulse
During each pulse, sample voltage every 100 ms and add (V / R_load) × 0.1 s to the charge removed. Add the board's quiescent consumption over the whole test, including rests, using a value measured once with a multimeter in series. Record the cumulative charge at the start of each rest. State the uncertainty from load resistance and heating; this sets the uncertainty of the SOC axis.
Checkpoint: Logged charge per pulse is consistent from pulse to pulse within your stated uncertainty.
- 5
Automate pulse, rest and record
Write a state machine: FULL_REST, PULSE, REST, RECORD, DONE. Start from a supervised full charge followed by a long rest. In RECORD, take a heavily averaged voltage and temperature and send the point over UART. Stop when the loaded voltage reaches the cut-off. Abort on temperature limits or implausible readings, opening the relay. Show progress on the LCD. Because the test runs for many hours, stream every point over UART as well so nothing is lost if the board resets.
Checkpoint: A shortened dry run with five pulses produces five recorded points and stops correctly.
- 6
Check rest-time adequacy
For three points spread across the SOC range, log the voltage every 10 seconds throughout the rest and plot it. If it is still rising noticeably at the end, lengthen the rest or fit an exponential to extrapolate the settled value. Report how much the OCV would change with a shorter rest, because a BMS in a vehicle rarely gets a long rest.
Checkpoint: Relaxation plots showing the voltage change in the last five minutes of rest is below a few millivolts.
- 7
Build the table
After the test, assign SOC to each point: 100 per cent at the start, 0 per cent at cut-off, with the cumulative charge in between scaled to the total. In Python, plot OCV against SOC, check it is monotonic, and resample at fixed SOC steps. Generate a compact table of millivolts and SOC in tenths of a per cent and send it back to the board for storage.
Checkpoint: A smooth, monotonic OCV–SOC plot and a table of 21 points ready for flash.
- 8
Store and look up in firmware
Write the table to flash with a header (version, number of points, temperature at test, cut-off voltage) and a simple checksum. At start-up, read and verify it; if the checksum fails, fall back to a built-in default and say so on the LCD. Implement lookup by binary search and linear interpolation in integer arithmetic. Show SOC on the LCD only when the cell has rested; otherwise show OCV not valid.
Checkpoint: After a power cycle the table loads with a valid checksum and the rested SOC is displayed.
- 9
Validate and quantify error
Partly discharge a fresh full charge by a known estimated charge, rest, and compare the lookup SOC with the SOC from charge counting. Repeat at a high, mid and low SOC. Calculate the SOC error caused by a 5 mV voltage error at each region using the local slope of the curve. Discuss why the flat mid-region is the hardest and why lookup is combined with coulomb counting in practice.
Checkpoint: A table of lookup SOC against reference SOC at three points, with the voltage-sensitivity analysis.
- 10
Report
Include the procedure, relaxation plots, OCV–SOC curve, table format, flash layout, validation results and limitations: discharge branch only, one temperature, estimated current and a single cell. Provide the Python scripts and raw logs. Add a section on how the table should be used in a BMS: when a reading counts as rested, how long the rest must be and how lookup SOC should be blended with a running coulomb counter. Include the flash layout as a table of offsets, sizes and meanings so later projects can read the stored data.
Checkpoint: A reproducible report that another team could follow to characterise their own kit cell.
Testing & validation
- Voltage calibration at two points and rest-reading noise.
- Relaxation curves showing the chosen rest time is adequate.
- Monotonic OCV–SOC curve built from about 20 points.
- Flash storage verified by checksum after power cycling.
- Lookup SOC compared with charge-counted SOC at three states.
Expected results
A measured OCV–SOC curve for the kit cell, stored in flash and used by firmware for rested-voltage SOC, with a sensitivity analysis showing where the method is reliable and where it is weak.
Take it further
- Measure the charge branch with an INA219 add-on to quantify hysteresis.
- Repeat at a second ambient temperature and interpolate between tables.
- Combine the lookup with coulomb counting for continuous SOC.
- Fit a smooth analytical OCV model and compare its error with the table.
Viva and review questions
Why must OCV be measured after a rest?
Under or just after current, the terminal voltage includes ohmic drop and polarisation. Only after relaxation does the voltage reflect the equilibrium state that maps to SOC.
Why is SOC from OCV inaccurate in the middle range?
The curve is flat there, so a few millivolts of measurement error corresponds to a large change in SOC.
What is OCV hysteresis?
The rested voltage depends slightly on whether the cell was last charged or discharged, so a table from one direction gives a biased estimate after the other.