做电池管理系统仿真的人大多有过这种经历新电池的SOC用扩展卡尔曼滤波EKF估计误差很小曲线也平滑等电池循环几百次、容量衰减到85%之后同样的滤波器就开始“叛逆”了。明明还有电SOC显示却提前掉底端电压预测误差被一点点放大再往后连卡尔曼增益都在跟一个错误的模型较劲。我最近复现了一篇讨论“考虑老化的修正EKF算法估计锂电池SOC”的论文在Simulink里把整套仿真搭通之后才彻底想明白问题的根源不在滤波算法本身而在于电池模型里的老化参数没有跟着电池一起变化。这篇博文把这次复现的关键思路、Simulink实现细节和踩过的坑完整记录下来。帖子面向两类人一类是刚接触SOC估计、想理解EKF和老化关系的同学另一类是已经在做BMS仿真、想复现类似论文结果的工程师。文章不刻意拔高理论只讲实际能跑通、能验证的做法所有参数和步骤都在我自己的仿真环境里实测过。1. 复现之前先把“老化让SOC失准”的根因想透1.1 标准EKF估计SOC用到了哪些模型假设EKF估计SOC几乎离不开一个等效电路模型最常见的是戴维南一阶RC模型。离散化之后的状态空间非常直观状态方程SOC(k1) SOC(k) - η·I(k)·Δt / Q极化电压递推U1(k1) exp(-Δt/τ)·U1(k) (1 - exp(-Δt/τ))·R1·I(k)其中τ R1·C1观测方程Ut(k) Uocv(SOC(k)) - I(k)·R0 - U1(k)这个方程组看着简单但里面几个关键参数全部跟老化挂钩。Q是当前时刻的实际可用容量不是出厂标称容量R0是欧姆内阻老化后明显增大RC环节的时间常数会漂移连Uocv与SOC的对应关系都会随着循环次数增加而整体变化。EKF最核心的运行前提是“模型足够准”。如果真实电池已经处于老化状态而滤波器里还装着新电池的参数那么从第一步先验预测开始误差就埋下了。更麻烦的是EKF本身没有自纠错能力来区分“模型参数错了”和“SOC状态错了”这两种情况它只能通过卡尔曼增益把新息分配到状态修正上于是错误被转移而不是被消除。1.2 容量衰减如何通过状态方程累积先验误差用一组量化数据感受一下容量偏差的影响。设标称容量Q0 100AhSOH掉到85%之后实际容量Q 85Ah。用100A放电放电10秒滤波器按Q0 100Ah计算ΔSOC 100 × 10 / 3600 / 100 0.278%真实按Q 85Ah计算ΔSOC 100 × 10 / 3600 / 85 0.327%单步偏差只有0.05%几乎可以被忽略。但连续放电300秒后两个SOC之间的差距会被拉开约1.5个百分点。如果放电时间更长、电流更大这个累积偏差会更明显。关键问题在于EKF很难通过电压纠正这个偏差。因为在放电平台段OCV随SOC的变化非常平缓一个1.5%的SOC偏差映射到电压上可能只有几毫伏淹没在噪声里卡尔曼增益几乎不会对SOC做有效修正。结果就是SOC曲线整体下移且越放越偏。1.3 内阻增大与OCV漂移怎样放大端电压新息老化的另一个核心表现是内阻抬升。新电池R0 20mΩ老化后涨到35mΩ在100A电流下端电压预测就会产生1.5V偏差。EKF看到预测端电压和实测端电压之间出现1.5V的新息它会按照观测矩阵H和增益矩阵把一部分“电压误差”折算成SOC修正。问题在于这个折算方向是完全错误的——真实原因不是SOC错了而是R0老化了。EKF强行把电压新息的一部分解释成SOC变化相当于往错误方向推了一把。这就是为什么老化电池的SOC估计往往不只是“精度变差”而是会出现明显漂移甚至“跳电”的原因。所以论文方案的切入点不是重新调一组卡尔曼增益而是让模型参数跟随老化状态在线修正。想清楚这一点后面所有设计都顺理成章了。2. 修正EKF的算法框架状态扩展法与在线辨识法我复现时选了后者2.1 状态扩展法的直观诱惑和实际问题最容易想到的“修正”方式是把老化参数塞进EKF状态向量里一起估计。比如状态向量变成[SOC, U1, R0, Q]SOC和U1走原状态方程R0和Q当作慢变随机游走模型处理。思路很直接但实际跑起来有三个问题第一可观测性很弱。Q在状态方程里只出现在SOC的变化率中观测方程里没有Q的直接映射R0虽然出现在端电压方程里但它与SOC、U1存在较强的耦合。EKF需要在一次线性化里同时估计数值尺度差异很大的状态量非常容易病态。第二数值条件差。SOC接近1的量级Q是几十到上百的量级雅可比矩阵各行之间的尺度差距可能有三个数量级。卡尔曼增益计算里的矩阵求逆对这种尺度差异极其敏感我试过协方差矩阵P很快就不正定了改归一化之后稳定性仍然一般。第三调参灾难。状态扩展意味着要额外设置R0和Q的噪声方差这两个方差的相对大小强烈影响收敛结果。模拟中偶然跑出不错的结果但换一组老化参数或者换工况就发散复现代价太高。这篇论文采用的也不是纯粹的状态扩展而是把老化参数的更新从EKF里拆出来单独做在线辨识。2.2 双时间尺度框架什么是修正EKF的工程化实现老化参数的变化速度比SOC慢几个数量级。SOC在放电过程中每分钟都会大幅度变化而R0和容量可能几十天才会明显变化。工程上把这个特点利用起来做双时间尺度快速时间尺度用EKF估计SOC慢速时间尺度用在线辨识算法更新模型参数再把辨识出的参数回传给EKF的观测方程和状态方程。这个设计有几个好处。高频更新老化参数没有必要只会把测量噪声放大成参数抖动拆开之后EKF的状态维度保持很低数值稳定性好FFRLS辨识和EKF状态估计各自可以使用匹配自身时间尺度的遗忘因子和噪声指标。我复现的框架就是这种双时间尺度结构EKF的采样周期Ts 0.1sFFRLS参数每一秒更新一次。参数更新后写回EKF模块下一拍的端电压预测就基于更新过的模型参数形成“辨识-修正”闭环。2.3 一阶RC模型的离散回归式与FFRLS实现在线辨识这一步论文里用带遗忘因子的递推最小二乘FFRLS辨识等效电路参数。一阶RC模型可以直接改造成线性回归形式。先把端电压减去开路电压定义中间量e(k) Ut(k) - Uocv(SOC(k))其中Uocv按当前SOC估计值查表得到在辨识中当作已知项。推导一阶RC离散差分方程后可以得到e(k) a·e(k-1) - R0·I(k) (a·R0 - (1-a)·R1)·I(k-1)这里a exp(-Δt/τ)。把它写成回归形式观测向量φ(k) [e(k-1), I(k), I(k-1)]^T参数向量θ(k) [a, -R0, a·R0 - (1-a)·R1]^T输出y(k) e(k)FFRLS递推式为增益K(k) P(k-1)·φ(k) / (λ φ^T(k)·P(k-1)·φ(k))误差ε(k) y(k) - φ^T(k)·θ(k-1)参数更新θ(k) θ(k-1) K(k)·ε(k)协方差更新P(k) (I - K(k)·φ^T(k))·P(k-1) / λ遗忘因子λ我取0.98。辨识出θ之后反推R0 -θ2a θ1R1 (θ1·R0 - θ3)/(1 - θ1)C1 -Δt/(R1·ln(a))。这里有个容易被忽略的坑如果不把OCV从回归式里剥离RLS会试图用电压数据同时辨识OCV和阻抗参数辨识结果会出现严重的耦合漂移。论文里的处理方式就是把Uocv查表作为前馈项只辨识偏差量工程上这叫“去偏置回归”复现的时候务必照做。2.4 参数限幅与更新节奏在线辨识不是无限自由的。我在仿真里给R0设置了0.005Ω到0.2Ω的上下限R1上限0.1ΩC1下限几百法拉。不设限幅的话在电流接近零、激励不足的时刻FFRLS可能辨识出负电阻或者几千法拉的离谱电容值这些值一旦回传给EKF整个估计立刻发散。更新节奏上FFRLS每1s触发一次EKF每0.1s运行一次。这样的安排既能跟踪老化参数的缓慢变化又不会让参数更新频率对EKF的动态响应产生影响。Simulink里用两个不同采样周期的模块实现信号流通过Rate Transition连接。3. Simulink仿真架构拆解五个子系统与信号流3.1 电池模型子系统用真值SOC不接收估计值整个仿真的核心原则是电池模型扮演“真实电池”只能接受真实物理输入绝对不能用估计器的输出去驱动它。我在电池模型里直接用积分器计算真值SOC初始SOC 80%积分输入为I/Q其中Q按当前老化程度设定。老化程度通过设置参数表来模拟比如SOH 85%时Q 85Ah并把R0、R1、C1切换到老化组参数。OCV查表同样用真值SOC作为输入通过Lookup Table得到Uocv。查表数据来自HPPC脉冲静置测试我按SOC从0到100%、每10%一个点的密度标定。注意OCV曲线在低SOC段和高SOC段斜率大中间段偏平缓这个特征对EKF的可观测性影响很大后面会专门讲。RC网络部分我用离散状态方程实现避免在Simulink里引入连续状态。U1的递推式写进一个MATLAB Function输出端电压Ut Uocv - I·R0 - U1。最终电池模型对外只输出三个信号端电压Ut、电流I、真值SOC其中真值SOC只用于误差统计和OCV查表不让估计器看到。3.2 EKF估计子系统预测-更新一步到位EKF模块内部是一个MATLAB Function输入为I、Ut、R0来自辨识模块、Q来自SOH设定输出为SOC估计值、U1估计值。状态向量x [SOC, U1]^T状态方程和观测方程已在前面给出。核心EKF递推代码结构如下function [soc_est, u1_est, P_out] ekf_update(I_meas, Ut_meas, R0, R1, C1, Q, dt, x_pred, P_pred) % 状态预测 soc_pred x_pred(1) - I_meas * dt / Q; a exp(-dt / (R1 * C1)); u1_pred a * x_pred(2) (1 - a) * R1 * I_meas; % Jacobian矩阵 A [1, 0; 0, a]; H [dUocv_dSOC(soc_pred), -1]; % dUocv_dSOC由查表斜率给出 % 预测协方差 Q_noise diag([1e-4, 1e-5]); P_pred A * P_pred * A Q_noise; % 观测预测与新息 Ut_pred OCV_lookup(soc_pred) - R0 * I_meas - u1_pred; innov Ut_meas - Ut_pred; % 卡尔曼增益与更新 R_meas 1e-3; K P_pred * H / (H * P_pred * H R_meas); x_upd x_pred K * innov; P_out (eye(2) - K * H) * P_pred; soc_est x_upd(1); u1_est x_upd(2); end注意不要试图在EKF函数内部自行迭代预测和更新Simulink里应该把x和P通过Memory或Unit Delay反馈给下一拍的函数输入。这样既避免代数环也让数据流清晰。我最初把x_pred放在函数内部用persistent变量保存结果Simulink报出代数环错误改成显式反馈后立刻正常。3.3 老化参数辨识子系统FFRLS与参数回写FFRLS模块是一个独立的MATLAB Function触发周期1s。它的输入是I、Ut以及当前SOC估计值输出R0、R1、C1。初始化时P矩阵取1000·Iθ取[0.99; -0.01; -0.005]这个初值不要求准确RLS会在前几十秒内收敛。function [R0_out, R1_out, C1_out] ffrls_update(I, Ut, soc_est, dt_id, lambda) persistent theta P if isempty(theta) theta [0.99; -0.01; -0.005]; P 1000 * eye(3); end Uocv OCV_lookup(soc_est); e_now Ut - Uocv; e_prev theta(1) * e_prev_buffer; % 注意需要延时变量 phi [e_prev; I; I_prev]; y e_now; K P * phi / (lambda phi * P * phi); theta theta K * (y - phi * theta); P (eye(3) - K * phi) * P / lambda; R0 -theta(2); a theta(1); R1 (a * R0 - theta(3)) / (1 - a); C1 -dt_id / (R1 * log(a)); R0_out min(max(R0, 0.005), 0.2); R1_out min(max(R1, 0.001), 0.1); C1_out max(C1, 500); end实际运转时e_prev和I_prev是在模块外部用Unit Delay实现的函数内部只维护θ和P。这样时序关系清晰不会因为函数输出顺序导致隐式依赖。3.4 工况、初始值与评价通道工况输入我用两种恒流1C放电用于验证基本收敛性然后是自定义动态工况模拟车辆循环中的大电流变化用于验证老化修正的实际价值。动态工况载入方式用一个Simulink的Signal Builder或者From Workspace导入时间-电流数组。初始值设置EKF初始SOC设70%真实SOC设80%故意制造10%初始误差来观察收敛初始U1 0初始P diag([0.01, 0.01])。如果初始P取得太小EKF会过度信任初始状态SOC收敛会很慢这个经验后面踩坑部分细说。求解器方面我选择fixed-step、discrete固定步长0.1s。这样EKF和FFRLS的采样周期都能精确同步也避免了变步长求解器在离散逻辑切换时引入额外误差。4. 复现路上的几个坑代数环、参数漂移与调参顺序4.1 代数环为什么老出现搭建时最容易遇到的Simulink报错就是代数环。根源在于EKF的观测方程里用了当前时刻的Ut而Ut的计算又依赖Uocv查表如果电池模型中的OCV查表用了估计SOC就形成了Ut → EKF → SOC估计 → 电池模型OCV → Ut的闭合回路。Simulink需要在同一个仿真步里解这个环要么报告代数环要么引入实际不存在的瞬时迭代。解决办法其实很朴素电池模型的OCV始终用真值SOC查表EKF看到的Ut是“真实电池”的输出而不是受估计值影响的自反馈信号。这样在信号流上是单向的电池模型产生Ut → EKF估计 → 误差统计没有反馈闭合。这不算仿真作弊因为真实系统里电池不会因为估计器的SOC而改变输出端电压滤波器本来就只能被动观测。4.2 Q/R矩阵初值整定经验EKF的调参绝对绕不开Q和R矩阵。Q描述状态方程的可信度R描述电压测量的可信度。Q设得太小滤波器会非常相信模型预测老化参数一变就反应迟钝Q设得太大SOC估计会跟着电压噪声抖动曲线很难看。我建议按以下顺序整定先固定R 1e-3把Q_SOC从1e-6到1e-3按十倍递增分别跑恒流工况看SOC误差的收敛速度和稳态波动然后固定Q_SOC把R从1e-4到1e-2扫一遍观察估计SOC的噪声水平。最终我选定的组合是Q diag([1e-4, 1e-5])、R 1e-3。这个组合在动态工况下既有足够的响应速度又不会把电压噪声直接放大成SOC毛刺。另外可以看新息序列的统计特性帮助调参。新息应该近似为零均值白噪声如果新息序列明显有偏或者强相关说明模型里还有系统性误差单纯调Q/R是压不下去的。4.3 FFRLS参数漂移与遗忘因子FFRLS在电流恒定时会遇到激励不足的问题。恒流放电过程中I(k)和I(k-1)几乎相等φ矩阵的列之间存在近似线性相关最小二乘解不唯一。这时候辨识出的R1和C1会随意漂移R0相对稳一些因为R0直接由电压突变量决定。我的处理方式是比较相邻时刻电流差值的绝对值若小于死区阈值就冻结参数更新。动态工况下这个死区几乎不会触发因为电流持续变化恒流工况下则能有效防止参数漫游。死区阈值我设为0.5A。遗忘因子λ的取值也值得注意。λ越接近1历史数据权重越大参数越平滑但跟踪老化变化的速度越慢λ取0.95时跟踪速度快但噪声放大明显R0可能辨识出负值。我最终用0.98并额外加了一个R0下限保护。对于SOH快速衰减场景可以适当降低到0.96但必须把参数限幅做得更严格。4.4 平台期可观测性弱的现实问题这是修正EKF也解决不了的物理局限。锂电池OCV曲线在中段SOC区间斜率很小这意味着H矩阵的第一列dUocv/dSOC接近零。此时端电压对SOC变化几乎不敏感EKF的可观测性矩阵接近奇异卡尔曼增益对SOC的修正作用非常微弱。复现过程中我做了个对比初始SOC误差20%在动态工况大电流段启动EKF大约200秒内能把误差收敛到2%以内但若在高SOC平台区段启动且电流很小收敛时间会成倍增加。这个现象不是算法缺陷而是系统本身在该工作点对SOC几乎不可观测。工程上的应对思路依然是“离线标定在线修正”结合电池静置足够长时间后用开路电压直接重置SOC初值放电末期或充电末期利用电压斜率大、可观测性强的区段进行修正。论文仿真里一般不强调这个但实际复现出结果之后你会发现平台期给初始SOC带来的误差远远大于老化修正能弥补的范围。5. 复现结果怎么验证评价指标与对比实验5.1 实验矩阵设计复现不能只看一条曲线。我设计了三种SOH状态100%、90%、85%对应Q分别取100Ah、90Ah、85AhR0分别取20mΩ、28mΩ、35mΩ。两种工况恒流1C放电和自定义动态工况。每组实验跑相同的EKF初始值、相同的Q/R参数唯一区别是电池模型内部的老化参数以及FFRLS是否在线回写。这样做既能验证修正EKF对老化的适应性也能暴露标准EKF在老化状态下的失效模式。5.2 结果对比在SOH 85%、动态工况、初始SOC误差10%的测试条件下我这组仿真配置得到的结果如下算法MAERMSE最大绝对误差标准EKF模型参数固定为SOH100%2.4%3.1%6.8%修正EKFFFRLS在线更新参数0.6%0.9%2.1%SOH 100%时两者差距很小因为模型参数本来就没有偏SOH 85%时差距拉开非常明显。标准EKF的最大误差超过6%已经会对BMS的充放电管理产生实际影响修正EKF的最大误差能压到2%左右这个量级在SOC估计领域是可以接受的。还要留意一个细节修正EKF并不是从第一拍就表现优异。FFRLS需要约30到50秒完成参数收敛这段时间里R0估计从初值逐渐逼向真实值SOC误差相比标准EKF并不会立刻拉开。所以复现时不要因为前几十秒误差大就否定算法关键看稳态和整个工况段的误差统计。5.3 下一步可以怎么扩展复现完成之后我试着把修正EKF输出的R0和容量再用来估计SOH效果也不错。思路是在线累计辨识出的容量衰减量配合低通滤波生成SOH趋势曲线。实际嵌入式部署时通常不会在每一秒都跑FFRLS而是每几十秒触发一次中间用上一次的参数这样能进一步降低计算负载。另外一个可以扩展的方向是OCV曲线的在线修正。目前FFRLS只辨识R0、R1、C1OCV表仍当作已知。如果电池老化导致OCV平台本身偏移这种框架仍然会有残余误差。更完整的方案是让Uocv-SOC映射也参与慢速自适应比如在低SOC区间根据充电曲线实时修正OCV表点。这部分我还没有完全跑通但已经有了一些可行思路等验证稳定了再单独写一篇。复现完之后的一点个人体会如果让我用一句话总结这次复现那就是修正EKF的可贵之处不在卡尔曼滤波本身而在于把老化参数当成系统辨识问题在线解决再回写给状态估计器。它真正修正的不是“滤波器的系数”而是“模型本身”。给想复现的朋友三个建议。第一先用脚本把EKF和FFRLS在纯数值环境里跑通观察数据和调试参数都比Simulink里方便确认递推稳定后再搬到Simulink搭子系统能省出大量排查时间。第二所有辨识参数必须设边界负电阻不是模型问题而是激励不足或遗忘因子太大限幅能救回大部分发散场景。第三算法解决不了平台期不可观测的物理事实工程系统里始终要给静置OCV校准留下位置。搭完这个仿真之后我回头再看电池管理系统里的SOC显示确实已经不再是“一个卡尔曼滤波搞定一切”的简单认知了。老化修正的意义是让算法在电池整个生命周期的不同阶段都能保持一致的可靠性。这套Simulink框架现在放在手边后面做SOH估计和OCV自适应的时候还能继续复用算是一次投入、长期回报的复现。