18650锂电池电量估算实战:从电压曲线到SOC算法
1. 从一颗18650说起为什么电压能“猜”出电量18650锂电池大概是电子圈里最不陌生的东西了。直径18mm、长度65mm的圆柱形电芯标称电压3.6V或3.7V满电4.2V截止放电2.5V到3.0V——这些数字几乎每个玩过电池包的人都背得出来。但真正做过BMS电池管理系统的人都知道知道这些数字是一回事拿这些数字去算准剩余电量SOC完全是另一回事。我最早接触电量估算是在一个手持设备项目上当时想得很简单ADC采一下电池电压查个表线性映射一下不就完了结果实测直接翻车——设备显示还剩40%电量实际用不到十分钟就关机了。后来才明白锂电池的电压-容量关系根本不是线性的中间有一段平台区电压变化极小但容量变化巨大靠简单线性映射必然出问题。这篇内容就是把我这些年从放电曲线采集、ADC采样电路设计、到最终电量估算算法落地的完整过程梳理一遍。核心围绕18650锂电池的电压-容量关系展开涉及BMS的基本原理、ADC采样的工程细节、嵌入式系统里的数据处理方法。适合正在做电池管理、便携设备电量计、或者单纯想搞清楚“为什么手机电量显示不准”的嵌入式工程师和硬件爱好者。不管你是刚入门还是已经做过几版BMS这里面的实操细节和踩坑经验应该都能对上你的胃口。2. 电压-容量关系的本质放电曲线到底在说什么2.1 锂电池放电曲线的三个阶段18650锂电池的放电曲线如果拿恒流放电数据画出来横轴是放出的容量mAh纵轴是端电压V你会看到一条非常典型的曲线。这条曲线可以分成三段来理解。第一段是初始下降区。从4.2V满电开始放电电压会快速下降到3.9V左右大概放出5%到10%的容量。这一段电压下降快是因为电池内部极化效应和欧姆内阻导致的压降在起作用。刚充满的电池表面电荷分布不均匀一加载就开始重新分布电压自然掉得快。第二段是平台区也是最有意思的一段。电压从3.9V缓慢下降到3.5V左右但放出的容量可能占到总容量的60%到70%。这一段电压变化非常平缓典型18650电芯在0.5C放电倍率下平台区电压斜率大概只有0.1V/1000mAh左右。这意味着什么意味着你ADC采到10mV的电压误差对应的容量误差可能高达100mAh。这就是为什么单纯靠电压估电量在平台区几乎不可用。第三段是快速下降区。电压从3.5V迅速跌到截止电压2.5V或3.0V放出剩余15%到20%的容量。这一段电压对容量又变得敏感了但问题是留给你的反应时间不多了设备随时可能因为欠压保护而关机。注意不同厂家、不同批次、甚至同一批次不同循环次数的18650电芯放电曲线都有差异。标称容量2600mAh的电芯实际可能从2500到2700不等平台区电压位置也可能差50mV以上。2.2 为什么不能只看电压内阻和温度的影响电压-容量关系之所以复杂根本原因在于电池端电压不等于开路电压OCV。端电压等于开路电压减去内阻压降而内阻压降又等于电流乘以内阻。这个内阻不是常数它随温度、SOC、循环次数变化。温度的影响特别明显。零下10度时18650的内阻可能是常温25度时的3到5倍。同样放出1000mAh容量低温下端电压可能比常温低100mV以上。如果你在低温环境下用常温的电压-容量曲线去查表估算出来的SOC会偏低很多设备可能提前关机。内阻还随SOC变化。低SOC时内阻增大大电流放电时压降更明显。我实测过一颗2600mAh的18650在3.5V平台区0.2C放电时端电压3.48V换成1C放电端电压直接掉到3.35V差了130mV。这130mV如果按平台区斜率换算差不多对应300mAh的容量偏差。所以真正做电量估算不能只采一个电压就完事。你需要知道电流、温度最好还能在设备静置时采开路电压来校准。这就是为什么BMS芯片里通常都有库仑计Coulomb Counter配合电压检测一起用。2.3 开路电压法与库仑计法的取舍开路电压法OCV法的思路很简单电池静置足够长时间后端电压约等于开路电压而开路电压和SOC有确定的对应关系。查表就能得到SOC。优点是原理简单、不需要累积误差。缺点是电池必须静置通常要2小时以上才能达到稳定设备运行中根本没法用。库仑计法则是实时积分电流SOC变化量等于电流对时间的积分除以额定容量。优点是实时性好运行中也能估算。缺点是累积误差电流采样有偏差、积分时间有偏差时间长了SOC就飘了。而且电池老化后实际容量下降如果不更新容量参数库仑计也会算错。实际工程中通常是两者结合设备运行时用库仑计设备关机或静置时用开路电压法校准。BMS芯片如TI的BQ系列、ADI的LTC系列内部都是这套逻辑。但如果你用的是通用MCU加外部ADC的方案这些逻辑就得自己写。3. 硬件基础ADC采样电路怎么搭才靠谱3.1 分压电路的设计与计算18650满电4.2V截止2.5V而大多数MCU的ADC参考电压是3.3V或2.5V。所以直接采电池电压会超量程必须分压。分压电阻的选择有几个讲究。假设你用3.3V参考电压的12位ADC希望4.2V时分压后不超过3.3V分压比至少是4.2/3.31.27。但为了留余量通常取1.5到2之间。比如上电阻100kΩ、下电阻100kΩ分压比24.2V分压后2.1V3.3V量程下占用64%的码值范围。这样12位ADC在4.2V时的分辨率是4.2V/4096≈1.02mV看起来够用。但这里有个坑分压电阻会持续消耗电池电量。100kΩ100kΩ的串联电阻4.2V下电流是21μA一年就是184mAh。对于2600mAh的电池来说不算大但对于低功耗设备来说不能忽略。所以实际设计中分压电阻通常取大一些比如1MΩ1MΩ电流降到2.1μA。但电阻大了之后ADC采样时的输入阻抗和采样保持电容会影响精度。提示STM32系列MCU的ADC输入阻抗要求通常建议信号源阻抗小于10kΩ。如果你用1MΩ分压必须在ADC输入端加电压跟随器运放缓冲否则采样误差会很大。我早期省掉运放采出来的电压比万用表低80mV查了半天才发现是输入阻抗不匹配。3.2 参考电压的选择与噪声抑制ADC的参考电压直接决定测量精度。如果你用MCU的VDDA作为参考而VDDA又来自LDO那LDO的噪声和温漂都会影响测量。比如一个普通LDO温漂100ppm/°C温度变化20度3.3V参考就漂了6.6mV对应到电池电压就是13mV误差。更稳的做法是用专用电压基准芯片比如REF30333.3V或REF30252.5V初始精度0.05%温漂25ppm/°C。价格不贵但精度提升明显。我对比过用LDO参考时SOC估算在温度变化时能飘5%以上换基准芯片后降到1%以内。噪声方面电池电压本身比较干净但分压电阻的热噪声和ADC的量化噪声还是要处理。硬件上在ADC输入端并一个0.1μF电容到地和分压电阻组成低通滤波截止频率大概1/(2πRC)。比如1MΩ和0.1μF截止频率1.6Hz能滤掉大部分高频噪声。软件上再做多次采样取平均或中值滤波。3.3 采样时机与电流同步如果你只采电压不采电流那只能做开路电压法。但设备运行时电池有负载电流端电压不等于开路电压。所以要么在设备休眠时采样要么同步采电流做补偿。同步采电流需要电流检测电路通常是在电池负极串一个采样电阻比如10mΩ到100mΩ用差分ADC或运放放大后采样。采样电阻的选择要平衡功耗和信噪比。10mΩ电阻在1A电流下压降10mV功耗10mW对18650来说可以接受。但10mV信号对12位ADC来说只占3个码值3.3V参考分辨率太差。所以通常需要运放放大比如放大50倍到500mV占用62个码值分辨率好很多。采样时机上电压和电流最好同时采或者至少在同一时刻触发。如果MCU的ADC有多通道扫描模式可以配置成电压通道和电流通道交替采样。但要注意采样保持时间通道切换后需要足够的建立时间。我遇到过通道切换太快导致串扰的问题电压通道采到了电流通道的残留电荷后来把采样时间从7.5周期加到71.5周期才解决。4. 从原始数据到SOC算法实现与参数标定4.1 放电曲线的采集与拟合要建立电压-容量关系首先得有数据。最靠谱的方法是自己测。拿一颗18650用电子负载做恒流放电比如0.5C1300mA每隔一定时间记录电压和放出的容量。电子负载要支持数据记录或者用万用表和计时器手动记。我一般用可编程电子负载配上位机软件每10秒记一个点放到截止电压2.5V为止。测出来的数据是一堆容量电压点。接下来要拟合。简单的方法是分段线性插值把曲线分成若干段每段用直线近似。段数越多越准但存储和计算量也越大。对于嵌入式系统通常分16到32段就够了。比如每100mAh一个点2600mAh就是26个点存一个数组查表时做线性插值。更精细的做法是用多项式拟合比如5阶多项式。但多项式在两端容易振荡而且计算量大。我试过用三次样条插值效果不错但代码复杂。对于大多数应用分段线性加中值滤波已经够用。注意放电曲线受放电倍率影响。0.2C和1C的曲线在平台区能差100mV以上。所以如果你设备工作电流变化大最好测多条曲线按电流大小选择。或者用更复杂的模型比如Shepherd模型或PNGV模型但这些模型参数多标定麻烦。4.2 库仑计积分与误差补偿库仑计的核心公式很简单SOC(t) SOC(0) - ∫I(t)dt / Q_rated。其中Q_rated是额定容量I(t)是放电电流放电为正。离散化后就是每个采样周期累加SOC - I * Δt / Q_rated。看起来简单但误差来源很多。首先是电流采样误差如果采样电阻精度1%、运放增益误差1%、ADC参考误差0.5%综合起来电流误差可能到2%。2%的电流误差在完整放电周期里就是2%的SOC误差。其次是时间误差如果定时器不准Δt有偏差积分也会偏。最后是Q_rated本身电池老化后实际容量下降如果还用标称值SOC会越算越偏。补偿方法有几个。一是定期用开路电压校准设备静置时如果电压稳定就查表修正SOC。二是温度补偿低温下可用容量下降Q_rated要打折。三是老化补偿记录循环次数或累计放电量估算容量衰减。我一般会在固件里存一个Q_rated参数出厂时标定用户使用中通过开路电压校准慢慢修正。4.3 分段线性查表的代码实现嵌入式里查表最常用的就是分段线性插值。假设你有一个电压数组v_table[]和对应的容量数组c_table[]按电压从高到低排列。查表时先找到电压落在哪个区间然后线性插值算容量。// 假设v_table和c_table按电压降序排列长度为N float voltage_to_capacity(float v) { if (v v_table[0]) return 0.0f; // 满电 if (v v_table[N-1]) return c_table[N-1]; // 空电 for (int i 0; i N-1; i) { if (v v_table[i] v v_table[i1]) { float ratio (v_table[i] - v) / (v_table[i] - v_table[i1]); return c_table[i] ratio * (c_table[i1] - c_table[i]); } } return 0.0f; }这段代码简单直接但要注意几个点。一是v_table必须单调递减否则查表会出错。二是浮点运算在低端MCU上可能慢可以改成定点数。三是如果电压在平台区相邻两个表点之间电压差很小插值对电压误差很敏感。所以表点在平台区要密一些比如每20mV一个点在快速下降区可以稀一些每100mV一个点。我实际用的表是32个点平台区3.9V到3.5V占了16个点每25mV一个其他区域16个点。这样在平台区电压误差10mV对应的容量误差大概40mAh可以接受。4.4 温度补偿与老化修正的工程处理温度补偿最简单的做法是查温度-容量修正表。比如25度时Q_rated2600mAh0度时修正系数0.9零下10度时0.8。这个系数也是实测出来的把电池放在温箱里不同温度下做完整放电记录实际放出容量。老化修正更麻烦。锂电池循环500次后容量可能降到80%。如果你知道循环次数可以按经验公式修正。但循环次数不好准确统计充放电一次算一次循环部分充放电怎么算我一般用累计放电量来估算每放出2600mAh算一次等效循环。然后按容量衰减曲线修正Q_rated。提示温度补偿和老化修正都会引入误差如果修正过头反而更糟。我的经验是温度补偿系数保守一点比如实测0度容量是90%修正系数取0.93宁可高估一点也不要低估导致提前关机。老化修正也是容量衰减估算偏保守避免SOC跳变。5. 实测案例从零搭建一个18650电量计5.1 硬件选型与电路搭建我最近做的一个小项目用STM32G474做控制器ADC是12位参考电压用外部REF3033。电池是松下NCR18650B标称3400mAh。分压电阻用1MΩ1MΩ中间加了一个OPA333做电压跟随。电流采样用50mΩ电阻加INA240差分放大增益50倍。电路搭出来之后先不急着写代码先用万用表测分压后的电压和电池实际电压对比。我测了10颗电池分压后电压和实际电压的偏差在2mV以内说明电阻精度和运放缓冲没问题。然后测电流采样用电子负载拉1AINA240输出2.5V理论值1A×50mΩ×502.5V实测2.48V误差0.8%可以接受。ADC配置上采样时间设成71.5周期12位分辨率单次转换模式。电压通道和电流通道交替采用DMA搬运数据。采样率设成100Hz每10ms采一对电压电流值。这个速率对电池电量估算来说绰绰有余因为电池电压变化很慢。5.2 固件流程与数据处理固件的主循环很简单每10ms读一次ADC电压值做中值滤波加滑动平均电流值同样处理。然后每1秒做一次库仑计积分每10秒做一次开路电压校准判断。开路电压校准的逻辑是如果电流小于10mA视为静置且持续超过30分钟就认为端电压接近开路电压查表修正SOC。修正时不是直接覆盖而是渐进修正比如每次修正10%避免SOC跳变。这个渐进系数我试过0.1、0.2、0.5最后用0.2比较稳既不会修正太慢也不会让用户看到电量突然跳。库仑计积分用定点数电流单位mA时间单位秒累加值单位mAs。每3600秒换算成mAh。Q_rated初始值3400mAh温度补偿系数查表老化修正按累计放电量每3400mAh减0.5%容量。5.3 实测数据与误差分析实测下来常温25度0.5C放电从满电放到2.5V库仑计估算的SOC和实际放出容量对比最大误差在3%以内。平台区误差稍大因为电压查表在平台区不敏感主要靠库仑计。快速下降区误差小因为电压查表敏感能及时修正。低温0度时如果不做温度补偿SOC估算偏低8%左右设备在显示15%时就关机了。加了温度补偿后误差降到3%以内。高温45度时容量略增补偿系数1.02误差也在3%以内。最大的误差来源其实是电池个体差异。同一批买的NCR18650B实测容量从3250到3450不等差了6%。如果Q_rated用3400那容量3250的那颗就会在显示5%时还有100mAh容量3450的那颗会在显示0%时还有50mAh。所以如果设备对电量精度要求高最好每颗电池单独标定或者至少按批次标定。6. 常见问题与排查技巧实录6.1 电压采样跳动大怎么办这是最常见的问题。ADC采出来的电压值跳来跳去有时候跳几十mV。原因可能有几个一是分压电阻太大ADC输入阻抗不匹配加电压跟随器能解决。二是参考电压不稳换专用基准芯片。三是地线干扰模拟地和数字地要分开单点接地。四是ADC配置问题采样时间太短增加采样周期。我遇到过一次电压跳动特别大查了半天发现是ADC输入端的滤波电容没焊好虚焊了。补焊之后立刻稳定。所以硬件问题先查焊接再查配置。6.2 SOC显示跳变怎么处理SOC跳变通常发生在开路电压校准的时候。如果静置后电压查表和当前SOC差很多直接覆盖就会跳。解决办法是渐进修正每次修正一小部分。另外校准条件要严格电流必须足够小静置时间足够长。如果设备频繁启停可能永远等不到静置条件那就只能靠库仑计误差慢慢累积。还有一种跳变是温度突变引起的。比如设备从空调房拿到室外温度变化导致电压变化SOC跟着变。这种情况可以在温度变化率超过阈值时暂停校准等温度稳定再说。6.3 电池老化后估算不准的应对电池老化后容量下降内阻增大放电曲线也会变。如果固件里Q_rated不更新SOC会越算越偏。应对方法一是定期做完整充放电校准但用户不一定配合。二是用累计放电量估算老化每放出一定电量就修正Q_rated。三是检测内阻变化内阻增大到一定程度就认为老化严重修正容量。我一般用第二种简单可靠。累计放电量存在Flash里每3400mAh减0.5%容量上限减到70%。这样即使不校准SOC误差也不会无限增大。6.4 常见问题速查表问题现象可能原因排查方法解决措施电压采样跳动大分压电阻太大、参考不稳、地线干扰万用表对比、换基准、查地线加运放缓冲、换基准、单点接地SOC显示跳变开路电压校准过猛、温度突变查校准日志、监控温度渐进修正、温度稳定后再校准平台区SOC误差大电压查表不敏感、电流采样误差对比库仑计和实际容量加密平台区表点、校准电流采样低温提前关机温度补偿不足、内阻增大低温放电测试加温度补偿系数、降低截止电压老化后SOC偏Q_rated未更新查累计放电量按累计放电量修正容量ADC采样值偏输入阻抗不匹配、参考偏差测分压后电压、测参考电压加电压跟随器、校准参考提示排查问题时先用万用表确认硬件再查固件配置最后看算法逻辑。我踩过的坑里硬件问题占一半固件配置占三成算法问题只占两成。所以别一上来就怀疑算法先拿万用表量一量。7. 一些实操心得和后续扩展方向做电量估算这些年最大的体会是没有一劳永逸的方案。电池特性会变使用场景会变用户预期也会变。你能做的是在精度、成本、复杂度之间找平衡。如果设备对电量精度要求不高比如只是显示个大概那简单的电压查表加滤波就够了。如果要求高比如医疗设备或电动工具那就得上库仑计加定期校准。另一个体会是标定很重要。再好的算法参数不对也白搭。Q_rated、温度补偿系数、老化修正曲线这些都要实测。我一般会花一整天时间做电池标定把不同温度、不同倍率、不同循环次数的数据都测一遍。虽然费时间但后面省心。后续如果想做得更精细可以考虑几个方向。一是用阻抗跟踪算法TI的Impedance Track就是这类通过测内阻来估算SOC精度更高但算法复杂。二是用机器学习方法把电压、电流、温度、历史数据喂给模型让模型学出SOC映射。三是多节电池串联时的均衡和SOC估算这个更复杂因为每节电池特性不同还要考虑均衡电流的影响。不过对于大多数18650应用来说把电压查表和库仑计做好加上合理的温度补偿和老化修正已经能满足90%的需求了。剩下的10%要么是极端环境要么是超高精度要求那就得用专用BMS芯片省心但成本高。自己用MCU做灵活但费功夫看你怎么取舍了。