Custom State of Charge Algorithm
Goal: A weighted fusion of coulomb counting SOC and OCV SOC. A high accuracy and low-cost implementation.
Why? Coulomb counting on its own can accumulate errors due to repeated integration. We also cannot predict the current SOC using only coulomb counting, which is why we must incorporate Voltage mapping.
System inputs:
- Sampling time = dt
- Pack current = I[n]
- Previous Pack current = I[n-1]
- Pack voltage (under load) = V[n]
- Previous Pack voltage (under load) = V[n-1]
- SOC = SOC[n]
- Previous SOC = SOC[n-1]
Coulomb Counting:
This is the easier algorithm to implement of the two. The equation for SOC using coulomb counting is:
Taken into discrete-time for embedded systems, the new equation is:
SOC[n] = SOC[n-1] + (I[n] * dt) / C
UPDATE: The code uses the trapezoidal rule for better approximation.
Integrated current = 0.5 * (current[n] + current[n-1]) * (time)
float integrated_current = 0.5f * (float)(bms->pack_current + s_storage.last_current) * (d_time);
s_storage.i_soc = s_storage.averaged_soc + (integrated_current / bms->config.pack_capacity);
Voltage Mapping (Simpler version implemented currently):
We will use a 0.2C discharge curve. We plan on doing this with the Chroma 6200D HV power supply. Once we have the discharge curve of the pack we can map voltage values to specific SOC values. The voltage value will represent the voltage UNDER LOAD. There will be inaccuracy due to varying voltages under load.
A 0.2C curve is roughly ~7.76A, which is what we expect our car to be at during cruising speed. Also, our implementation will use a function to control the voltage weighting.
My reasoning for this is that at high and low SOCs the discharge curve is significantly steeper. It is also non-linear in these regions. Coulomb counting is only effective for linear regions in the discharge curve, which is why voltage mapping is superior at the extreme ends. When we are less than 20% or greater than 80%, voltage weight will begin to ramp up.
Note: The current implementation as of September 22, 2024 does not use a 0.2C discharge curve. It instead uses the discharge curve found on this website https://lygte-info.dk/review/batteries2012/LG%2021700%20M50%205000mAh%20%28Grey%29%20UK.html I have taken the data off of the 0.2A per cell. Which is ~1.6A on the pack. That is why we have a predict_voltage() function, that accounts for the voltage lost by the internal resistance.
The general math behind this is:
Use a lookup table to find 2 voltage values such that VOLTAGE_LOW <= Pack_Voltage <= VOLTAGE_HIGH.
VOLTAGE_LOW and VOLTAGE_HIGH will map to a SOC %. We can interpolate between the two values to find a better approximation of the current SOC %.
EVERYTHING BELOW THIS DID NOT HAPPEN YET:
OCV SOC FUTURE EXPLORATION:
This algorithm raises issues that will be addressed through empirical data collection. OCV mapping allows us to predict the current SOC when there is no load on the pack.
Issue #1 = How do we map OCV - SOC?
Discharge testing.
To extract the OCV-SOC curve you can fully charge the battery, and then by 1% SOC decrements, discharge the battery and wait for 30min-1 hr for the battery to rebalance. Measure the voltage and map it, then repeat by 1% decrements until you reach min cell/pack voltage. To calculate the current to discharge at:
For example, if you wanted to discharge a 5000mAH battery, at a rate of 5% per hour, you would need to discharge 250mA:
A great visual on how to discharge test from TechLanz:
I plan on discharging at 5% SOC increments per 30 minutes, with 1 hour to allow pack stabilization. This means it will take 90 * 100/5 = 1800 minutes or 30 hours to complete a full discharge test (Likely automating this). The MS15 pack has 9 modules in series, with cells arranged 8P4S. I will run discharge tests on 2 modules, and if time permits on 2 cells.
MS15 uses INR21700 cells which have a capacity of 4850 mAH. For 5% SOC increments per 30 minutes I will have to discharge a cell at 485 mA. For an entire module it will be 8 * 4850 mAH = 38800 mAH. So for 5% increments, I will discharge the module at 3880 mA.
Issue #2 = The pack is always under load, how do we get the OCV?
Battery modelling!
TLDR: Lots of math I had fun with, we use this equation for the RC circuit:
We can model our battery pack with either a 2RC or RC model. For simplicity, I will stick to an RC model.
The circuit can be modelled with the equation:
Two ideas to model this equation for the firmware:
1. First-order Taylor series approximation of ex
So OCV voltage can be estimated using this:
2. Transfer function with Inverse Laplace Transform (S domain → Time domain)
4 Ways to Implement a Transfer Function in Code | Control Systems in Practice
The transfer function for the RC circuit (eq 1) can be converted into the time domain using reverse Laplace transform (eq 2). Notice that we are multiplying the s-domain function by Ipack. This is the convolution of the RC transfer-function and pack current.
Discretized for embedded systems, the equation can be written as:
This is what will be implemented in our firmware; it is more accurate than the first-order Taylor series approx.
Cool now that we have the mathematical implementation, how do we get R1, C1 and Rcell? We can do something called HPPC battery testing.
Building Better Batteries: Characterize Battery Parameters for Simulation
Resources:
A Closer Look at State of Charge (SOC) and State of Health (SOH) Estimation Techniques for Batteries | Analog Devices
Li-ion Battery RC Modeling, What it is and How it is done. Part-1: Testing