电池管理与 SOC 估算实战:磷酸铁锂显示还剩 40%,三分钟后直接关机——我把”电压查表”换成”库仑计 + OCV 修正”的全过程

2026年10月4日 0 By admin

引子:一块显示”还剩 40%”的电表,三分钟后自己关机了

事情出在一块用 磷酸铁锂(LiFePO4)4 串电池组供电的离网监测终端上。客户投诉很直接:屏幕上电量条还显示 40%,设备工作十几分钟后突然欠压关机,拆下来用充电器一充,又能跑很久。

我们自己的实验室复现结果更离谱:满电开机,电量从 100% 掉到 60% 几乎是断崖式的——跑两个多小时,电量条纹丝不动地卡在 80%,然后”哗”一下跳到 65%。

原因其实不复杂:这块板子的 SOC 估算是纯电压查表——ADC 采电池电压,查一张”电压→百分比”的表完事。这套方案在三元锂上勉强能用,但磷酸铁锂的电压曲线是出了名的”躺平”,查表法在这上面等于闭眼开枪。

这一篇就把整个改造过程写下来:为什么电压查表在磷酸铁锂上能差出 30%、库仑计怎么落地、OCV 修正和内阻补偿怎么配合,最后给一张可复用的自查清单。


先看病灶:磷酸铁锂的电压曲线有多”躺平”

我把 4 串电池组(标称 12.8V)的静置开路电压(OCV)和 SOC 的对应关系实测了一轮(25°C,静置 1 小时后读数):

SOC (%) 单节 OCV (V) 4 串组 OCV (V)
100 3.40 13.60
95 3.35 13.40
90 3.33 13.32
70 3.30 13.20
50 3.28 13.12
30 3.25 13.00
20 3.22 12.88
10 3.15 12.60
5 3.05 12.20
0 2.50 10.00

看出问题了吗?从 30% 到 90% 这 60 个百分点的 SOC,OCV 只从 3.25V 涨到 3.33V——总共 80mV,平均每 1% SOC 只有约 1.3mV。

而我的 ADC 是 STM32 内置 12 位,VREF 3.3V 经过分压电阻采 4 串电压。算一笔账:

  • 分压比 1/5(13.6V → 2.72V),12 位 ADC 一步 = 3.3V / 4096 × 5 ≈ 4.0mV(组电压)
  • 折算回单节,一步 ≈ 1.0mV
  • ADC 本身还有 ±2~3 LSB 的失调与噪声,实际有效分辨率只有 ±5mV 左右

也就是说,ADC 的测量误差(±5mV)已经跟 30%~90% 整个平台的电压跨度(80mV)处于同一量级。查表法在这个区间里,本质上是在用尺子量头发丝。

更糟的是负载。电池组内阻(4 节电芯 + 保护板 + 线缆)实测约 50mΩ,设备工作电流 1.5A:

负载压降 = 1.5A × 50mΩ = 75mV

75mV,比整个平台段还宽。 负载一重,电压直接被 IR 压降拖到查表结果”还剩 30%”——而真实 SOC 可能还有 70%。这就是为什么客户看到的电量会”突然跳水”:负载变化一扰动,查表结果在几个百分点之间乱跳。

三宗罪总结

  1. 平台段太平:LiFePO4 在 20%~90% SOC 区间电压变化 <100mV,电压法分辨率天然不足;
  2. IR 压降污染:负载电流 × 内阻造成的压降可达几十到上百 mV,比平台段跨度还大;
  3. 回弹要等:想要真实的 OCV,得断负载静置 30 分钟以上——设备不可能为测个电量歇半小时。

三元锂(NCM)的曲线斜率大(3.5~3.9V 跨度覆盖 10%~90%),电压法”能用”;磷酸铁锂直接把这条路堵死了。换电池化学体系,估算策略必须跟着换。


三种 SOC 方法怎么选

动手前先把选型表摆出来:

方案 原理 精度 成本 硬件要求 适用场景
电压查表(OCV 直查) 电压→查表→SOC 差(LiFePO4 上 ±20~30%) 零 仅 ADC NCM 锂电/铅酸的粗略显示
纯库仑计 电流积分 中(漂移累积) 低 采样电阻 + 电流采样 负载平稳、有满点校准机会
库仑计 + OCV 修正 积分为主,静置时用 OCV 锚定 良(±5% 内可达) 中 采样电阻 + ADC + 静置检测 电池供电设备的通用解
专用电量计芯片(如 ModelGauge 类) 芯片内部融合算法 良~优 中~高 换芯片 预算够、不想自己调模型
卡尔曼滤波融合 电池模型 + 状态估计 优 高 需要建模与调参 动力电池/高价值设备

我的选择是第三档:库仑计为主体,OCV 修正兜底,内阻补偿把负载下的显示拉直。不换硬件、不加芯片,纯固件改造——这也是大多数存量项目最现实的路。


第一步:把库仑计搭起来

库仑计的原理一句话:对电流做时间积分,SOC 变化量 = ∫I dt / 满容量。

硬件上我们没动板子——这块设备本来就有低边采样电阻(1mΩ,原本只做过流保护),把它兼做电流采样即可。ADC 用差分模式采采样电阻两端,PGA 放大 32 倍后,1mΩ × 1.5A = 1.5mV 的满载信号够用。

采样电阻的账要算清

采样电阻是库仑计的命根子,三个约束取交集:

  • 压降损耗:R × I_max 不能太大。1mΩ × 3A = 3mV,损耗 9mW,可以忽略;
  • 信噪比:R 越小、信号越小,ADC 噪声占比越高。睡眠时的 μA 级漏电流在这个阻值下采不出来(0.1mA × 1mΩ = 0.1μV),小电流段靠 OCV 修正兜,不要硬拉分辨率;
  • 温漂:普通厚膜电阻温漂 ±100~250ppm/℃,满载温升 20°C 时误差能到 0.5%。预算够就上 合金采样电阻(±25ppm),不够就在校准表里补温度系数。

固件实现(整数运算,零浮点)

按照我一贯的规矩,固件里不碰浮点(原因说过很多次了)。电流单位用 μA,电荷用 μAh,全程 int64:

/* 库仑计核心:对采样电流做积分
 * cur_ua  : 本次采样电流(有符号,充电为正),uA
 * dt_ms   : 两次采样间隔,ms
 * soc_x100: SOC,放大 100 倍(0 ~ 10000)
 * cap_uah : 电池实际可用容量,uAh(由满点校准动态更新)
 */
void coulomb_step(int32_t cur_ua, uint32_t dt_ms)
{
    /* 电荷增量 uAh = cur_ua * dt_ms / 3600000
     * 用 int64 防溢出:3A=3e6 uA * 60000ms / 3.6e6 = 50000 uAh */
    int64_t dq = (int64_t)cur_ua * (int64_t)dt_ms;   /* uA*ms */
    dq /= 3600000;                                    /* -> uAh */

    soc_acc_uah += dq;

    /* SOC = 剩余电量 / 实际容量,放大 10000 倍避免整数截断 */
    if (cap_uah > 0) {
        int32_t soc = (int32_t)(((int64_t)soc_acc_uah * 10000LL) / cap_uah);
        if (soc < 0)   soc = 0;
        if (soc > 10000) soc = 10000;
        soc_x100 = (uint16_t)soc;
    }
}

采样节奏不用快:1Hz 足够。库仑计关心的是长周期积分,不是捕捉电流尖峰(尖峰对 SOC 的贡献本来就小)。1Hz 下每秒一次 64 位乘除,对主循环的负担可以忽略。


第二步:积分会漂——三处误差源逐个堵

纯库仑计最大的问题不是精度,是误差只涨不跌。我把三个主要漂移源逐一处理:

1. 电流采样失调

ADC 前端有零点失调,表现为”设备明明没干活,SOC 也在慢慢掉”。实测我们板上失调折算约 8mA,看着不多:

24 小时漂移 = 8mA × 24h = 192mAh
电池 6000mAh → 每天 3.2%,一周就是 22%

处理:开机后采 100 次平均,若均值 < 15mA 且设备确认空闲,把该均值记为零点偏置,之后每次采样减掉。失调校零后,日漂移从 3.2% 压到 0.4% 以下。

2. 睡眠期积分断档

这台设备有休眠(Tickless 那套,之前写过),深度睡眠时主 ADC 和库仑计逻辑都停了,睡眠期间的漏电流(约 40μA)没有被积分。一晚睡 10 小时,少积 0.4mAh,不多,但如果睡眠占比高(IoT 常态),这个账会越欠越多。

处理分两层:

  • 睡前把 soc_acc_uah 存入带电保持的 SRAM/备份域,唤醒后接着积,保证不重置;
  • 睡眠电流用预先标定好的常数按睡眠时长补积分:sleep_charge = I_sleep × t_sleep。常数标一次(万用表 μA 档串进去量)就能用,误差远好于完全丢弃。

3. 容量基准不准

cap_uah 若直接写 datasheet 标称值(6000mAh),会踩两个坑:实际容量比标称低 5~10%(出厂差异+老化),而且容量随温度缩水。基准错了,SOC 从第一天起就整体偏移。

处理:容量基准动态校准——每次从”满电”跑到”0% 关机”,把这段实际积分出的电荷量存为新的 cap_uah(乘个学习速率系数 0.7 做平滑,避免单次异常污染)。这就是电量计芯片里 SOH(State of Health)学习的简化版。跑三五次完整循环后,容量基准就和真实值贴住了。


第三步:OCV 修正——给漂移的积分”上锚”

库仑计漂移只能被”锚”住,锚点就是 OCV。策略:

什么时候信电压?

只在静置时信。 判据:

  • 电流绝对值 < 20mA(接近空载);
  • 该状态持续 ≥ 5 分钟(让电池表面极化衰减掉大半);
  • 温度在标定表覆盖范围内。

满足后,用当前端电压近似 OCV(残余极化再打个小折扣),查 OCV-SOC 表得到参考 SOC,与库仑计当前 SOC 做加权融合:

/* ocv_soc: 查表得到参考 SOC(x100)
 * soc_x100: 库仑计当前 SOC(x100)
 * k      : 修正速率 0~100,每次 tick 向 OCV 靠拢 k/256
 */
void soc_blend(int32_t ocv_soc, uint32_t k)
{
    int32_t diff = ocv_soc - (int32_t)soc_x100;
    /* 限幅:单次最多修正 2%,防止查表异常把积分结果一拳打飞 */
    if (diff >  200) diff =  200;
    if (diff < -200) diff = -200;
    soc_acc_uah += (int64_t)diff * cap_uah / 10000 * k / 256;
}

关键设计是”小步慢走”:每次修正不超过 2%,且向 OCV 方向缓慢收敛。这样即使 OCV 查表偶尔被异常扰动(比如刚拔掉大负载、电压还没回弹完),SOC 显示也只是缓缓滑动,不会跳变。

静置判定别踩”伪静置”

我们的第一版判据只看电流 < 20mA,结果白天设备周期性待机(每 30 秒醒一次发心跳),每次醒都拉 200mA 一下,电池永远处在”刚被扰动”的状态,5 分钟静置计时反复清零——OCV 修正一整天都没机会执行,漂移照旧。

改法:静置计时器不在”电流小”时清零,而是记录最后一次大电流(>100mA)的时间戳,now - last_heavy > 5min 且当前电流小,才算真静置。改完之后,设备一晚上的睡眠期就是天然的修正窗口,每天至少锚定 1~2 次。

满点和空点:两个免费的 100% 锚

  • 充电末端:充电器转涓流/截止(电流 < C/50 且电压到截止值),此刻 SOC 置 100%、soc_acc_uah = cap_uah。每次充满都是一次免费校准;
  • 放电末端:欠压保护触发前,记录当时积分电量,作为 0% 锚点参与容量学习。

有了这两个端点锚 + 睡眠期 OCV 修正,积分漂移被压在一个循环内不超过 3~5% 的水平。


第四步:内阻补偿——把负载下的显示”拉直”

库仑计 + OCV 修正解决了” SOC 准不准”,但还有一个观感问题:负载大的时候,设备端电压被 IR 压降拖低,欠压检测会提前触发。1.5A × 50mΩ = 75mV,我们的欠压阈值设在单节 2.8V,相当于负载时比空载提前约 15% SOC 就关机。

处理思路:欠压判断不用端电压,用补偿后的虚拟 OCV:

V_ocv_est = V_term + I × R_int(T)

内阻 R_int 的获取两条路:

  1. 静态填表:datasheet 典型内阻 + 温度系数(LiFePO4 电芯 25°C 约 20~40mΩ,-10°C 翻 2~3 倍),出厂按电池规格填表;
  2. 在线估计:利用负载电流的自然波动,取相邻两段”电流变化 ≥ 0.5A 且间隔 < 1s”的样本对,R = ΔV / ΔI,滑动平均滤噪。只在高 SOC 平台段(此时 OCV 对 SOC 不敏感、ΔV 主要反映 IR)才采纳样本。

我们的板子两路都做了:静态表兜底,在线估计在 30%~90% SOC 区间刷新表值。上线一个月,内阻表从 50mΩ 慢慢学到 62mΩ(电池在老化),欠压关机点也随之自动右移——这其实就是 SOH 的另一个观测窗口。

温度补偿别忘了:内阻-温度曲线比容量-温度还陡。查表插值还是整数运算:

/* R_int 查表:温度点 -20/-10/0/25/45 °C -> mΩ×100 */
static const uint16_t r_tbl[] = {1800, 1100, 750, 500, 480};

uint16_t r_int_lookup(int8_t temp_c)
{
    /* 线性插值,返回 mΩ×100 */
    ...
}

温度这件事:容量和内阻都在缩水

单列一节是因为这块板上吃过亏:冬天客户反馈”电量 30% 就关机”,寄回来测,环境 0°C。

LiFePO4 的可用容量随温度缩水明显:

温度 (°C) 可用容量(相对 25°C)
45 ~100%
25 100%
0 ~80~85%
-10 ~70%
-20 ~55~60%

如果不做温度补偿,0°C 时”电量条 30%”对应的真实可用能量只有常温 30% 的八成,再叠加内阻翻倍导致欠压提前,实际”还剩 10 分钟”的显示就是骗人。

处理:cap_uah 除以温度容量系数后再参与 SOC 归一化,内阻按上节查表。两条曲线都做成 5 点折线表插值,代码量不到 100 行。


改造前后的效果

同一台设备、同一组电池,循环测试对比:

指标 改造前(电压查表) 改造后(库仑计+OCV+补偿)
平台段 SOC 误差 ±20~30% ±4%
电量显示波动 负载扰动下乱跳 平滑单调
“剩 10 分钟”预测误差 完全不可用 ±20%(够用)
满充后起点 误差带入下个循环 100% 强制锚定
一周静置漂移 不可统计 < 3%
客户投诉”突然关机” 频繁 一个月零投诉

踩坑清单(8 条)

  1. OCV 表用别人的:网上找的 LiFePO4 OCV 表和我们实际电芯差了 20mV,直接导致修正方向错。OCV 表必须拿自己这批电芯实测,至少静置法标 10 个点;
  2. 静置判据只看电流:如前述,周期性唤醒的设备永远”伪静置”,要按”最后一次大电流时间戳”判定;
  3. 修正步长不限幅:早期版本 OCV 修正一步到位,某次 ADC 受扰读错,SOC 从 70% 直接跳到 45%,客户以为设备坏了。改成单次 ≤2% 渐进收敛后不会再跳;
  4. 容量基准用标称值:实际容量低 8%,SOC 显示永远虚高,最后 20% 断崖。动态容量学习是必须的;
  5. 睡眠期积分丢弃:睡眠占比 80% 的产品,库仑计等于只在工作时段生效,账面漂移全攒在睡眠里。睡眠电流常数补积分 + 备份域保存累计值;
  6. 内阻补偿只在满电附近做:低 SOC 段 OCV 斜率变陡,ΔV 里分不清是 IR 还是 SOC 变化,在线估 R 会乱飞,限制在 30%~90% 平台段采纳样本;
  7. 满充判定太松:转灯即算 100%,但充电器实际还有一段补充电,导致每次”满点”都不在同一状态,锚点漂。判据用”截止电流 < C/50 持续 2 分钟”;
  8. 温度传感器贴错位置:NTC 贴在电芯支架上而不是电芯表面,读数比电芯实际温度低 8°C,容量补偿方向反了。NTC 必须贴电芯大面。

落地清单(12 项)

  • ☐ OCV-SOC 表用本批电芯静置法实测,≥10 个 SOC 点,注明温度
  • ☐ 库仑计用有符号电流积分,充电/放电统一处理,SOC 限幅 0~100%
  • ☐ 电流采样开机零点校准(空闲均值 <15mA 记失调)
  • ☐ 满充锚点:截止电流 <C/50 持续 2 分钟 → SOC=100% 并更新容量基准
  • ☐ 放电末端锚点:欠压前记录积分量参与容量学习(学习速率 0.5~0.7 平滑)
  • ☐ 静置检测按”最后一次大电流 >5min”判定,睡眠期即天然修正窗口
  • ☐ OCV 修正单次限幅 ≤2%,渐进收敛,禁止跳变
  • ☐ 睡眠前 SOC 累计值存备份域,睡眠漏电流按标定常数补积分
  • ☐ 内阻补偿:静态表兜底 + 30%~90% SOC 段在线估计(ΔV/ΔI 滑动平均)
  • ☐ 欠压判断用 V + I×R(T) 补偿后的虚拟 OCV,不用端电压
  • ☐ 容量与内阻均带温度查表补偿(≥5 个温度点折线插值)
  • ☐ 上报数据带原始诊断量(累计电荷/零点偏置/内阻表值/最近 OCV 修正量),远程可判读

相关阅读


下一篇切回软件赛道,讲 Zephyr 与 FreeRTOS 的迁移对比:同一块板子,把一个 FreeRTOS 工程搬到 Zephyr 上要动哪些地方、哪些坑是文档里不会写的。