日期:2026-10-19。
在电脑上画出一条平滑的 SOC 曲线,和让这条曲线长期运行在 BMS 里,是两件有明显距离的事。距离往往藏在很普通的地方:温度到底是摄氏度还是十分之一摄氏度,电流和电压是不是同一时刻的样本,所谓的"500 ms 任务"究竟由谁调度,重启以后又该信任哪一个 SOC。
本文沿着一套 BMS 工程和一份独立 EKF SOC 交付代码,讨论这段距离如何缩短。BMS 工程使用 FreeRTOS、NXP SDK 和 Processor Expert 生成配置,现有容量算法包含安时积分、端点修正、SOC/SOE 同步和持久化;独立 EKF 包则实现了带电压偏置的二阶 RC 模型,并提供面向 BMS 接口的适配层。
这里有一个影响全文判断的现状:BMS 已预留 500 ms EKF 调用槽,但该位置仍为空,独立算法包尚不能视为已集成运行。 下文沿着调用链和递推公式核对现状,再讨论接入方案。公式按交付 C 代码整理;文中没有把仿真结果包装成台架经历,也没有把测试报告的历史描述当成当前固件已经具备的功能。
一、先看现有 SOC 的位置,再决定 EKF 应该放在哪里
这套工程的 SOC 并非一个每隔几百毫秒刷新一次的百分比变量。底层电流采样进入积分链,积分结果形成充放电量变化,容量模块维护当前容量与剩余容量,随后才产生 SOC 和显示数据。校准、同步、历史统计和掉电保存又会反过来影响这些状态。E1E2
如果接入 EKF 后只改显示 SOC,旧容量仍按原路径推进,那么显示、电量统计、SOE 和重启恢复就可能产生分歧。反过来,如果每轮 EKF 更新都回写容量,又会与旧算法的校准和同步争夺状态所有权。接入之前,必须回答"哪个模块在什么条件下修改哪一种状态"。更新函数只是这份接口约定中的一环。
独立包已有一个适合早期验证的选择:适配层保存影子 SOC,没有回写旧算法的容量和保护状态。这样可以先回答基本问题:输入单位是否正确,模型参数是否匹配,充放电切换时会不会出现异常,计算是否影响原调度。只有这些问题有了证据,才值得讨论如何将估计结果接入正式业务。E9
工程还存在另一条需要单独理解的路径:诊断、故障滤波、工作状态、安全等级和保护执行分别在调度链中运行。SOC 可以参与能量管理,却不能因此让模型估计直接接管过压、欠压或异常采样的处理。集成时应先画清数据流和控制流;仅凭 SOC 曲线变平滑,无法判断保护动作是否仍然满足设计要求。
阅读工程时也要区分"目录里有代码"和"当前配置启用了功能"。例如当前配置选择磷酸铁锂、关闭电池堆并机管理;数组最多容纳多少单体,只代表软件容量,不能直接当作现场实装规模。算法的估计对象必须与实际采样配置核对,不能由文件夹名称推断。
二、把二阶 RC 模型变成可以逐行对照的递推
把 EKF 拆成一次递推来看,计算顺序很清楚:先根据电流和电池模型预测下一时刻,再用测量电压修正预测。二阶 RC 模型用两个具有不同时间常数的支路描述电压动态,用欧姆电阻描述电流变化带来的即时压降。选择这个模型,是在计算量与电压动态描述能力之间作折中。
交付代码的状态向量为:
xk=zk, U1,k, U2,k, bkT
其中, z 是范围为 0 到 1 的 SOC; U1、 U2 是两个 RC 支路的模型电压; b 是加在电压观测方程中的偏置项。 U1、 U2、 b 的单位都是 V, z 无量纲。输入电流 I 以 A 为单位,约定放电为正、充电为负; Qn 是以 A·s 表示的容量, η 表示模型使用的效率系数, Ts 是秒单位的步长。E6
在一个采样间隔内冻结参数,并将电流视为保持不变,可写出:
ϕjzk−Uj,k−bk−=exp(−RjCjTs),j=1,2=zk−1−QnηTsIk=ϕjUj,k−1+Rj(1−ϕj)Ik=bk−1
对应的实际 C 代码很短,位于 EKF/SocEst/ekf_bms.c:79--82:
c
float soc_minus = soc_old - (ekf->eta * ekf->Ts / ekf->Qn) * input->I;
float u1_minus = phi1 * u1_old + g1 * input->I;
float u2_minus = phi2 * u2_old + g2 * input->I;
float b_minus = b_old;
负号与电流方向是一对约定。放电为正时 SOC 下降;如果上层接口把充电定义为正,必须在边界处转换,否则看似合理的电流曲线会驱动完全相反的容量变化。两个 RC 状态既保留上一次极化,又接受本轮电流输入。因此它们依赖时间连续性,不能把每一行日志都当成互不相关的独立样本。
电压预测写成:
U^t,k−=OCV(zk−,Tk,Ik)+bk−−U1,k−−U2,k−−IkR0
这里将电流写进 OCV 的参数列表,是因为代码按照充放电方向选用不同的 OCV 表,而不是说开路电压本身由瞬时负载电流直接决定。实际端电压与预测电压之差称为创新: rk=Ut,k−U^t,k−。创新告诉我们"本轮预测与观测不一致多少",却不会自动告诉我们误差来自 SOC、温度、采样偏差还是参数失配。
四状态中的 b 为模型留下了吸收慢变电压误差的空间,但它没有独立测量支撑,不能被解释成已经识别出的电流传感器零偏,也不能据此证明两个极化状态等于电池内部真实极化。尤其在 LFP 平台区,多个状态可能用不同组合解释相似的端电压。模型输出有物理启发,不等于每一个内部状态都已被单独验证。
这一点决定了调试方式:看到电压拟合改善时,仍应检查 SOC 是否偏移、 b 是否持续贴近限幅、极化状态是否出现异常补偿。只看一条端电压曲线,容易把模型自由度增加带来的拟合能力误认为 SOC 识别能力提高。
三、协方差把"相信多少"写进计算,也带来了近似边界
状态预测以后,还要传播预测的不确定性。令 P 为状态误差协方差, Q 为过程噪声协方差,则常见的 EKF 递推为:
Pk−=AkPk−1AkT+Qk
交付实现使用的观测线性化结构为:
Hk=dzdOCV, −1, −1, 1
随后计算:
SkKkxkPk=HkPk−HkT+R=Pk−HkT/Sk=xk−+Kkrk=(I−KkHk)Pk−
这里协方差更新式中的 I 是单位矩阵,与前文表示输入电流的 I 含义不同。电压测量为标量,所以创新协方差 S 也是标量;四个状态分别获得一个修正增益。程序的主要任务由此变得具体:查参数、预测状态、传播协方差、算预测电压与创新、计算增益、修正状态和协方差。E7E8
公式里的雅可比需要与代码使用的近似区分。当前实现按 A=diag(1,ϕ1,ϕ2,1) 展开协方差传播。RC 参数随 SOC 和温度查表变化,但这里把本步参数当作常数,没有把参数随 SOC 变化产生的全部导数纳入。温度是本模型的外部输入,并不是被漏掉的一列状态。观测矩阵也使用 OCV 斜率,没有完整展开 R0 随 SOC 变化等项。这是一种局部近似实现;评价它是否足够,需要看对应工况下的预测误差和稳定性,不能称为包含全部参数依赖的严格雅可比。
LFP 的平台区使这个问题更明显:相当一段 SOC 变化只带来很小的 OCV 变化,电压对 SOC 的约束较弱。程序将 OCV 斜率下限设为 0.005 V/单位 SOC,即每 1 个 SOC 百分点至少对应 0.05 mV 的线性化灵敏度。这是人为设置的计算约束;若真实曲线更平,线性化就会偏离它,不能由此宣称恢复了真实观测信息。即便斜率变大,也不能简单推断 SOC 增益必然变大,因为 H 同时进入增益的分子和分母,协方差的交叉项也参与运算。
数值保护同样要按实际能力描述。代码对 SOC 和偏置作限幅,对 S 设置小量保护,协方差更新以后再对称化。对称化能消除浮点计算造成的部分非对称,却不能单独保证矩阵半正定;状态限幅以后,协方差也没有自动完成一致的约束修正。回放时可以先记录协方差对角项、有限性和创新统计,检查长时间递推后是否出现异常,再据此判断是否需要 Joseph 形式等改进。矩阵只有四阶,也仍然需要这类数值检查。
还有一个容易被注释带偏的数值:初始化中的 R=0.01 在计算 S 时作为方差相加。电压以 V 为单位,则标准差是 0.01 =0.1 V,即 100 mV,并非注释中的约 10 mV。E6E8 这足以要求核对标定依据,却还不足以直接修改参数。测量噪声、模型误差与工况不确定性如何分配,应该由数据支持;根据注释改一个小数点,同样可能破坏原有调参结果。
四、输入契约比更新函数的名字更值得审查
仿真脚本通常直接使用 A、V、℃,固件接口却常用整数缩放值。适配层负责把这些数值转换成模型输入,接口约定因而不能只列变量名,还要列出单位、符号、量程、失效标志和样本时间。
| 输入 | 本工程接口约定 | EKF 内部约定 |
|---|---|---|
| 电流 | 10 mA,放电正、充电负 | A,乘 0.01 |
| 平均单体电压 | mV | V,乘 0.001 |
| 平均温度 | 整数 ℃ | ℃,此版本无需缩放 |
| SOC | 0~1000 对应 0~100% | 0~1,乘 0.001 |
| 总容量 | mAh | 先转 Ah,再在初始化中转 A·s |
温度是本次代码对照里最直观的例子。BMS 的 GetGCellAvgTempAPI 返回整数摄氏度,而独立适配器读取后乘了 0.1。E10 适配器中的原文如下,分别位于 EKF/SocEst/ekf_adapter.c:21,82,这里的注释反映适配器自身假设,与目标 BMS 接口不一致:
c
#define EKF_TEMP_SCALE (0.1f) /* 温度 0.1℃ -> ℃ */
c
sEkfIn.temp = (float)temp * EKF_TEMP_SCALE;
直接连接这两个版本时,25 ℃ 会进入模型成为 2.5 ℃,随后又被参数边界夹到低温端。程序没有越界,结果甚至可能十分平滑,但参数已不再对应真实工况。查表的边界保护因此不能替代单位检查。
容量则容易出现另一种混淆:电芯名义容量、当前学习容量和算法归一化容量并不是同一个概念。适配器优先取总容量,异常时回退标称容量,再由 Ah 转为 A·s。若两个来源都无效,预测公式中的分母仍需被保护。不能认为"已经有回退"就意味着一定获得了有效容量。
用一个说明性算例检验数量级:假设容量 300 Ah、电流 150 A、步长 0.5 s、效率取 1,则单步 SOC 减少 150×0.5/(300×3600)=6.94444×10−5,也就是约 0.00694444 个百分点。这不是实测成绩,只用于检查接口和量纲。若恒定输入下的纯预测量偏离这个数量级,应优先检查千分 SOC、容量单位、秒与毫秒,而不是马上调 Q 和 R。
输入契约还应覆盖数据来源。单体平均电压与二阶单体模型组合时,估计对象接近一个等效平均单体;改用最高或最低单体电压,测量对象就变了。此时 SOC 究竟代表整组平均能量状态,还是更接近限制单体的可用边界,需要重新定义。不能把平均电压改成最低电压以后,只凭曲线更接近某一参考,就认为物理含义保持不变。
接口能读到数值,也不代表这组数据可以用于估计。温度异常哨兵、采样通信中断、旧电压配新电流、浮点 NaN,都可能通过普通赋值进入算法。一种可落地的做法是把快照定义为带时间、序号、有效标志和来源的结构体,在进入算法前集中校验。这样函数收到的才是一次可解释的测量,而不只是几个全局变量的瞬时读数。
五、FreeRTOS 中的 500 ms,不只是给 Ts 写上 0.5
这套工程同时存在硬件中断、独立任务、软件定时器和业务轮询槽。理解它们的嵌套关系,是把模型时间步长落实到运行时间的前提。
硬件 1 ms 中断调用电流积分;软件 10 ms 回调内有十个业务槽,因此每个槽的名义执行周期为 100 ms。容量计算放在其中一个槽,进入容量函数以后又交替处理两类容量,所以相应分支名义上每 200 ms 执行一次。不能把"积分 1 ms"理解为"全部容量状态每 1 ms 更新",也不能只看外层 10 ms 定时器就认定业务每 10 ms 执行。E1E2
采集更新率还要与这些周期分开。1 ms 中断执行积分,不能证明 ADC 或电流芯片每 1 ms 提供一帧独立新样本;在尚未追完驱动与数据发布时序之前,只能确认积分调用周期。采集更新率、积分步长、容量任务周期和 EKF 周期是四个不同问题,记录与验证时应分别命名。
50 ms 回调调用 SOC/SOE 业务,后者使用二十槽轮询;其中第 2 和第 12 槽预留 EKF,间隔十个 50 ms,才构成名义 500 ms 入口。E3 BMS 的实际代码是:
c
/*500ms周期*/
case 2:
case 12:
/*SOC、SOE扩展卡尔曼算法任务*/
;
break;
独立适配器把 Ts 固定为 0.5 s,与这个预留位置在数值上匹配,但预留代码仍为空。另一个头文件注释提到"定时器中断每秒调用",也不能据此决定实际集成上下文。注释、入口和调用链必须相互对照。
这段计算在哪个任务里执行,会直接影响调度判断。工程以 xTimerCreate 创建多个周期函数,传入第四参数的 RTOS_ID_TIMER* 是定时器 ID,虽然附近注释容易使人联想到优先级,却不构成多个独立的定时器任务。回调共享定时器服务任务;当前配置中它的优先级为 3。E4
FreeRTOS 官方手册说明,软件定时器回调在同一个服务任务中执行,回调不应调用会使该任务阻塞的接口。因此,若把完整 EKF、慢速外设访问或记录写入直接塞进回调,一个函数的耗时便会影响其他回调的完成时间。周期到了只意味着调度条件成立,不意味着采样和计算必然在那个时刻完成。
当前源码组合还暴露了一项配置矛盾:业务任务声明优先级 12~18,而 configMAX_PRIORITIES 为 8;所附内核创建任务时将超限值截到 7。E5 静态推论是这些任务的预期优先级差异会丢失。是否与设备实际部署一致,仍需核对构建配置和二进制。它提醒我们:不能只看任务宏的数字,就画出想象中的抢占关系。
是否为 EKF 建立独立任务,要由执行时间和调度抖动的测量结果决定。若采用独立任务,可让周期入口只提交快照或通知,由任务完成计算并发布影子结果;但消息积压、丢样策略和过期判断也要随之明确。新建任务本身不会自动解决时间一致性。
固定步长模型要求真实输入与该步长假设相符。短暂抖动可能可以接受,连续漏算却不能用下一轮的一次 0.5 s 更新默默覆盖。按真实时间调整步长是一种方案,但同时涉及 RC 离散系数、过程噪声和区间电流表达,不能只改一个 Ts。另一种方案是在有完整样本时补做多个固定步长预测,前提是每段输入确实存在,不能用最新电流回填未知历史。
六、参数表和编译宏,必须与模型一起交付
如果把 EKF 理解成"一个 C 文件加几组经验参数",复现实验会很困难。这份实现至少包含三类相互关联的内容:状态递推、RC 查表、OCV 样条。它们共同决定预测电压,任何一类换版本,都可能改变最终 SOC。
RC 参数使用 SOC 与温度网格双线性插值,当前表的 SOC 范围为 5%~95%,温度范围为 15~35 ℃;OCV 使用 SOC 方向的分段三次系数,温度方向插值,温度覆盖 10~45 ℃。越界夹到边界意味着继续使用边界模型,不等于模型已在边界以外通过验证。E11
充电和放电分别使用不同表,按电流正负切换,零电流选择放电表。这个选择在程序里很简单,在工况上却需要关注切换连续性。电流在零附近抖动时,模型可能反复改变表分支;预测电压出现的变化又可能被 EKF 分配给 SOC 或偏置。先把切表点的创新与状态变化画出来,才能判断是否需要经过标定的方向判定、滞回或过渡策略。
电芯分支也有明确差异:默认参数选择 CATL 285 Ah,定义 CELL_EVE_314 才切至 EVE 314 Ah;测试报告描述的对象为 EVE 314 Ah,而 BMS 默认项目注释又指向标准 280 Ah。E12 这些数值不能靠"都差不多三百安时"合并理解。容量缩放解决不了 OCV 曲线和动态参数差异,编译成功也证明不了选中了正确的参数表。
要复现一次运行,日志中至少需要留下源码提交、编译宏、电芯对象、RC 表版本、OCV 表版本、容量来源、初始状态和噪声参数。这些信息共同回答"同一段数据为什么两次结果不同"。只有固件文件名和一张曲线图,通常不足以定位差异。
参数辨识与在线估计也应该分工清楚。离线辨识为模型提供适用范围内的动态关系,在线 EKF 在这些关系与观测之间分配不确定性。如果温度单位错了,或型号表选错了,在线增加一个偏置状态可能暂时改善电压创新,却不会把错误模型自动变成正确模型。出现持续补偿时,应先排除基础输入与参数版本,再讨论滤波器调参。
七、异常处理要回答"下一步怎么办"
论文中的递推通常假设测量不断到来、数值始终有限、容量始终为正。工程需要补上每个假设失败以后的路径。这份适配器在首次 Ready 后初始化,读取旧 SOC 作为初值;容量无效时回退标称容量。它已经提供基本接入骨架,但当前函数内还不足以覆盖 Ready 丢失、无效温度、缺测和连续过期数据等情形。E9E13
最容易混淆的处理是"跳过一轮"。跳过测量修正而继续预测,与冻结整个滤波器,含义不同。前者仍需要可靠的区间电流和经过时间,后者则让内部状态停留在旧时刻。重新收到样本时,要知道自己距离哪个时刻有多远,否则一个普通的通信恢复事件就会成为不连续状态跳转。
以下流程是建议方案,尚未在当前工程实现:
恢复策略要考虑电池在失效期间是否带载。断电后重新上电,可以利用历史容量提供先验;足够静置并满足条件时,也可以考虑 OCV 初始化。可是带载电压并不等于开路电压,直接反查 SOC 会把欧姆压降和极化误差一起写入初值。初值来源和初始协方差应一起设计:来源不可靠却把协方差设得很小,会让滤波器对错误初值过分自信。
按这些职责拆分,任务结构可以写成下面的伪代码。它用于讨论接入设计,尚未接入,也不能直接编译:
text
每个估计周期:
取得带单调采样时间的电压、电流、温度快照
校验单位、有效位、年龄和容量来源
依据失效时长决定初始化、预测或暂停
执行一步模型与滤波计算
检查有限性、创新、状态边界与计算耗时
发布影子结果及原因码
由低优先级记录路径保存诊断数据
日志本身也有语义边界。当前影子结构中的 tick++ 是调用计数,不能当作真实时间戳测量抖动。输出 SOC、极化状态和偏置为本轮修正后的状态,而输出预测电压与 error_Ut 仍对应修正前的电压预测及创新。把它们画在一起时应明确前后关系,不能标成"后验拟合残差",否则会误读算法的即时修正能力。E8
八、从影子运行走向工程结果,需要一条能复查的验证链
现有报告给出了 1.79 个 SOC 百分点的 RMSE,但报告明确说明参考 SOC 字段无效,指标相对 BMS_SOC 计算。E14 它能描述两种估计之间的一致性,不能直接证明相对独立真值的绝对误差。如果 EKF 初值来自旧 BMS,两者又共享电流和容量信息,那么"接近旧算法"尤其需要谨慎解释。
报告还描述了输出限速和充放电选压策略,当前交付适配器却始终读取平均电压,所读核心代码也不包含报告所述输出速率限制。E15 这个差异说明需要找到实验对应版本,不能据此否认其他版本曾完成实验,也不能将报告效果迁移到当前快照。
验证可以从一份可重放的样本开始。保存输入原值及单位、样本时间、有效标志、参数版本和初始化条件,在桌面端按固件运算顺序运行相同模型,再让板端影子任务处理同一类数据。这样可以将模型差异、浮点差异和调度问题分开定位。只保留最终百分比,会让大多数问题失去上下文。
定位一次偏差时,最好能从该轮 SOC 回到输入快照,再回到预测状态、预测电压和创新。若桌面端与板端从某一轮开始分离,这条记录链能帮助缩小检查范围;若两端逐步结果一致却都偏离独立参考,问题就需要回到模型、参数及参考数据上继续查。这里需要的是可重复的定位过程,单张曲线无法提供同样的检查条件。
验证顺序应从便于定位的情形逐渐扩展。恒流段检查容量变化和电流符号,静置段观察极化衰减,电流阶跃检查动态响应,充放电切换检查表分支,温度边界检查夹值行为;再加入无效温度、缺测、重启和长期运行。每一种工况都应有明确判据,例如是否发布无效标志、是否出现非有限数、周期是否超时,而不只写"曲线正常"。
有独立参考以后,还要将误差按温度、SOC 区间、电流大小和方向分组。一个全程 RMSE 可能掩盖平台区长期偏移,也可能被长时间静置段稀释。对于储能系统,充放端点的可用容量、重启连续性和切换过程往往比总平均误差更直接影响使用体验。需要评价的既有估计数值,也有错误何时发生、持续多久、是否被系统识别。
调度验证应与算法验证并行。记录计算耗时、任务栈余量、通知积压和采样到发布的延迟,检查 EKF 运行以后原通信与保护任务的时间行为。配置中的主频、堆大小、定时器栈深只是起点,不能替代测量。尤其在共享软件定时器上下文中,平均耗时很短也不足以排除偶发长延迟。
走到正式接入前,还需要一次状态所有权设计评审:EKF 输出是否只供显示、是否修正容量、修正何时生效、端点校准怎样作用、SOE 如何同步、持久化保存什么、恢复后谁提供初值。把这些问题写成接口和状态转换,理论中的递推才有了稳定的工程边界。这套工程的下一步已经足够具体:先修正接口单位的冲突,核对调度配置,补齐影子日志,并复现报告对应的版本。完成这些工作后,才有依据决定 EKF 应该接管哪些状态。