双扩展卡尔曼滤波在锂电池SOC估计中的Matlab实现与避坑指南

发布时间:2026/10/28 8:35:57
双扩展卡尔曼滤波在锂电池SOC估计中的Matlab实现与避坑指南

简介:面向锂电池SOC估计研究者的MATLAB代码包,完整实现基于双扩展卡尔曼滤波的荷电状态估计算法。代码附带锂离子电池实验数据,包含SOC-OCV曲线与实验室实测电流电压,无需自行准备数据即可直接复现结果。算法采用两个扩展卡尔曼滤波器协同估计SOC,并与单一卡尔曼滤波方案进行对比,清晰展示不同方法的精度差异。资源共135个文件,压缩包大小约58.58MB,内有64个.m源码文件、58个.mat实验数据文件,以及少量PDF、Word说明文档和辅助脚本;代码结构清楚,从电池等效模型建立、参数识别到双滤波器实现均有详细中文注释,便于阅读和二次开发。此外还提供仿真验证脚本,可观察滤波收敛过程,支持按需调整模型参数并对比估计效果。运行前须先将实验数据导入MATLAB工作空间。已有3162人学习下载,适合电池管理系统、状态估计算法方向的学生与工程师作为课程设计或科研验证的参考实现。

1. 双扩展卡尔曼滤波的锂电池SOC估计matlab代码:为什么两条卡尔曼通道比一条更可靠

做锂电池SOC估计的matlab代码时,我最早只写一个扩展卡尔曼滤波器,直到在内阻随温度明显漂移的工况下连续翻车,才开始认真把双扩展卡尔曼滤波(DEKF)落实到代码里。所谓双扩展卡尔曼滤波,是把电池的状态量和模型参数拆成两个EKF并行运行:一个快滤波器估计SOC和极化电压,一个慢滤波器跟踪内阻等缓慢变化的参数。对BMS算法、电化学储能系统或毕业设计来说,这套代码解决的核心问题是:即使电池老化、温度变化导致模型参数不再准确,SOC估计仍能保持稳定。适合那些已经会搭一阶RC模型,但被单EKF参数失配、估计漂移困扰的工程师。下面直接给出一份可运行思路和matlab代码结构,并把我踩过的初始化、噪声协方差和可观测性坑一起说清楚。

2. 从电池等效电路到双EKF结构:两个滤波器各自在看什么

2.1 一阶RC等效电路模型:状态量与输出方程

做DEKF之前,先要把电池模型固定下来。常见做法是用一阶RC等效电路,因为它比纯库仑积分多出极化电压的响应,又比二阶RC少两个待辨识参数。对SOC估计来说,一阶RC在动态工况下的端电压拟合精度已经够用,参数也不至于多到让双滤波器都陷入不可观测。

状态量选两个:SOC和极化电压V1。离散状态方程写成:

SOC_{k+1} = SOC_k - (η·Δt / Cn) · I_k

V1_{k+1} = a·V1_k + b·I_k

其中 a = e^{-Δt/τ},b = R1·(1 - a),τ = R1·C1。η是库仑效率,放电时取1.0;Cn是电池容量,单位安时;Δt是采样周期。输出方程是:

V_t = OCV(SOC) - V1 - R0·I

这里OCV与SOC的关系不是简单解析式,而是查表。很多电池厂家直接提供电压容量对照表,磷酸铁锂的曲线比三元锂更平,平台区的dOCV/dSOC很小,查表点必须加密;三元锂的4.2V截止附近斜率很陡,查表点可以稀疏一点,但也要保证插值单调。

我在代码里一般把OCV表放在一个函数里,用interp1做线性插值。千万不要直接把离散表当阶梯函数用,否则EKF的增益会在平台区来回震荡,后面避坑章节会细讲。

2.2 状态EKF与参数EKF的耦合:谁给谁喂估计值

双扩展卡尔曼滤波的核心是两个EKF交替工作,但互相喂结果。状态EKF负责快速变化量,也就是SOC和极化电压;参数EKF负责慢变量,通常先只估内阻R0,因为R0直接出现在输出方程里,和端电压残差的关系最干净。

两个滤波器的分工可以用下面这个表概括:

滤波器估计量更新频率主要输入输出给谁
状态EKFSOC、V1每个采样周期电流、电压、当前R0给参数EKF提供SOC和V1估计
参数EKFR0每个采样周期或条件触发端电压残差、电流、状态估计给状态EKF提供最新R0

运行时序是:先用上一拍的R0做状态预测,再用电压测量更新SOC和V1;然后把更新后的SOC和V1当成已知量,用同一拍端电压残差去更新R0。不要让两个EKF同时改同一个变量,否则两个滤波器都会以为自己已经把误差消掉了,实际是互相掩盖。

参数EKF的随机游走模型一般写成θ_{k+1}=θ_k+w,w的方差Qθ决定参数跟踪速度。θ变化慢,Qθ就该很小。如果Qθ给得大,R0会追着电压噪声抖,反而让SOC估计更差。

2.3 为什么SOC和参数不能放进同一个状态向量

不少新手会问:与其开两个EKF,不如把SOC、V1、R0拼成一个三维状态向量,一个EKF全估了。理论可行,但实际跑起来问题很多。

首先是时间尺度差异。SOC和V1是毫秒到秒级动态,R0是分钟到小时级缓慢漂移。联合状态EKF必须用同一个过程噪声矩阵去描述这三者,快变量需要Q大,慢变量需要Q小,折中之后协方差矩阵容易病态。双EKF把两个时间尺度分开,各自给各自的Q,调试起来直观很多。

其次是线性化误差。EKF对非线性函数做一阶线性化,OCV曲线本来就是强非线性,如果状态向量里再混入参数,雅可比矩阵的维度变高,增益计算容易出现互相干扰的项。实际表现就是滤波器不发散但也不收敛,SOC和R0一起缓慢漂移。

第三是排错。联合EKF一旦发散,很难判断是SOC更新错了还是参数更新错了。双EKF可以分别看两个残差:状态残差大说明模型动态不准,参数残差大说明参数更新条件有问题。这个工程上的优势比理论上的“最优性”更值钱。

3. 在Matlab落地可运行的DEKF代码:模型函数、主循环和初始化

3.1 电池模型函数:输入电流返回端电压

先写模型函数。这个函数只做一件事:给一个状态向量x、电流I、采样周期dt、参数R0,返回下一拍状态和端电压估计。

function [Vt, x_next] = batteryModel(x, I, dt, R0) % 一阶RC模型 % x = [SOC; V1],SOC单位1,V1单位V % R0是内阻,容量Cn固定为3.0Ah SOC = x(1); V1 = x(2); Cn = 3.0; % 电池容量,Ah eta = 1.0; % 库仑效率 R1 = 0.0015; % 极化电阻,欧姆 C1 = 1000; % 极化电容,F tau = R1 * C1; % 时间常数,秒 a = exp(-dt / tau); b = R1 * (1 - a); SOC_next = SOC - eta * dt / Cn * I; V1_next = a * V1 + b * I; Vt = OCV_interp(SOC_next) - V1_next - R0 * I; x_next = [SOC_next; V1_next]; end

这个函数把容量Cn写死成3.0Ah,是刻意的。第一版DEKF只让R0做参数,容量先固定,否则参数EKF容易发散。如果你要估计容量,需要额外加一个观测方程,到避坑章节再展开。

OCV_interp函数单独写,用查找表加interp1。SOC点是0:0.05:1,电压点按电池厂家给的电压容量对照表填。线性插值比polyfit稳,polyfit在高次时容易在端点振荡。

3.2 核心DEKF循环:状态滤波与参数滤波怎么串起来

主循环是整个代码的核心。下面这段代码可以直接粘到Matlab脚本里跑,前提是你已经有电流序列I和电压测量序列Vt_meas,并且都按相同采样周期dt对齐。

N = length(I); dt = 1; % 采样周期1秒 x = [1; 0]; % SOC初值1,V1初值0 Px = diag([1e-3, 0.1]); % 状态协方差初值 Qx = diag([5e-6, 1e-5]);% 状态过程噪声 Rx = 1e-4; % 电压测量噪声方差,对应10mV rms theta = 0.005; % R0初值,5mΩ Ptheta = 1e-4; % 参数协方差初值 Qtheta = 1e-8; % 参数随机游走噪声 soc_est = zeros(N, 1); r0_est = zeros(N, 1); for k = 1:N % ---- 状态EKF预测 ---- [~, x_pred] = batteryModel(x, I(k), dt, theta); a = exp(-dt / (0.0015 * 1000)); Fx = [1, 0; 0, a]; Px_pred = Fx * Px * Fx' + Qx; % ---- 状态EKF更新 ---- Hx = [dOCV_dSOC(x_pred(1)), -1]; Vt_hat = OCV_interp(x_pred(1)) - x_pred(2) - theta * I(k); Kx = Px_pred * Hx' / (Hx * Px_pred * Hx' + Rx); x = x_pred + Kx * (Vt_meas(k) - Vt_hat); Px = (eye(2) - Kx * Hx) * Px_pred; % ---- 参数EKF预测与更新 ---- Ptheta_pred = Ptheta + Qtheta; Htheta = -I(k); Vt_hat_theta = OCV_interp(x(1)) - x(2) - theta * I(k); Ktheta = Ptheta_pred * Htheta / (Htheta * Ptheta_pred * Htheta + Rx); theta = theta + Ktheta * (Vt_meas(k) - Vt_hat_theta); Ptheta = (1 - Ktheta * Htheta) * Ptheta_pred; soc_est(k) = x(1); r0_est(k) = theta; end

这段代码里,状态EKF先工作,得到x后参数EKF再工作。参数EKF的Htheta只有一项,因为端电压对R0的偏导就是-I。电流放电台I为负值,所以Htheta为正,残差为正时会把R0调大。

dOCV_dSOC需要实现成OCV曲线的数值导数。可以用差分:在SOC附近取±0.001,算两遍OCV再求斜率。不要用polyfit的解析导数,因为查表插值后的曲线导数本身不光滑,数值差分更符合EKF的离散框架。

3.3 初始化与数据结构:Q、R和协方差矩阵该给多大

初始化看似只是拍脑袋填几个数,实际上是DEKF能不能收敛的第一道关口。下面的初始化参数表是我在1秒采样、5A恒流放电工况下常用的起点:

参数典型初值说明
Px(1,1)1e-3对应SOC初始不确定性约3%
Px(2,2)0.1极化电压的初始不确定度
Qx(1,1)5e-6主要由库仑积分误差决定
Qx(2,2)1e-5模型简化误差
Rx1e-4电压传感器噪声,10mV rms时Rx=1e-4
Ptheta1e-4R0初始不确定度约10mΩ
Qtheta1e-8R0慢变,给太大会抖动

Q和R的绝对大小不重要,比例才重要。如果Qx(1,1)/Rx偏大,SOC会积极跟随电压测量,噪声大;偏小则SOC只信任库仑积分,电压观测几乎没有修正作用。调试时先固定Rx,再逐个调Q,不要同时动多个旋钮。

还有一个常见坑:Ptheta初值给很大,比如1,表示对R0毫无先验。这样前几步参数EKF增益会特别大,R0可能一步就从0.005跳到0.02。正确的做法是先跑一段离线数据,把R0初值设到肉眼可见的合理区间,Ptheta只给初值附近的不确定度。

4. 跑通后怎么验证和整定:SOC误差、放电曲线和噪声协方差

4.1 用模拟数据闭环:从25℃恒流放电看估计效果

DEKF写完后,第一件事不是接真电池,而是用已知模型生成模拟数据。这样真值在手,能直接判断算法本身有没有写错。下面这段代码生成一组5A恒流放电数据,电流序列是负数,持续1800秒。

dt = 1; N = 1800; I = -5 * ones(1, N); % 5A恒流放电 x_true = [1; 0]; R0_true = 0.005; % 真实内阻5mΩ Vt_true = zeros(1, N); for k = 1:N a = exp(-dt / (0.0015 * 1000)); x_true(1) = x_true(1) - dt / 3.0 * I(k); x_true(2) = a * x_true(2) + 0.0015 * (1 - a) * I(k); Vt_true(k) = OCV_interp(x_true(1)) - x_true(2) - R0_true * I(k); end Vt_meas = Vt_true + 0.003 * randn(1, N);

这里真实模型和DEKF里的batteryModel用的是同一个一阶RC结构,属于算法自洽验证。它能确认DEKF的增益计算、协方差更新和参数更新逻辑没有符号错误,但不能验证模型本身是否符合真实电池。真机验证换一批数据,模型误差会作为过程噪声进入Qx。

跑完DEKF后,把soc_est和x_true的第一行画在同一张图上,再把残差画出来。如果残差里还有明显趋势,比如SOC估计始终领先或落后0.5%,先不要调Q,去看OCV查表是否偏了。

4.2 SOC误差的两个判据:RMSE与最大绝对误差

验证DEKF不能只盯着曲线“看起来像”,要用两个数字说话。我一般固定用RMSE和最大绝对误差MAE。

err = soc_est - x_true(1, :)'; RMSE = sqrt(mean(err.^2)); MAE = max(abs(err)); fprintf('RMSE = %.4f, MAE = %.4f\n', RMSE, MAE);

RMSE反映整体精度,MAE抓极端情况。恒流放电工况下,一阶RC模型加DEKF的RMSE一般能压到0.01以内;动态工况因为有极化瞬态,RMSE会到0.02甚至0.03。这不是算法不行,而是模型阶数不够。

另外注意SOC低段误差容易放大。SOC低于20%时,OCV曲线斜率变陡,电压对SOC的敏感度高,如果查表点不够密,增益会被放大,MAE往往出现在这个区间。所以看误差时要按SOC区间分段统计,不要被总RMSE掩盖问题。

4.3 现场调试三件套:Q/R比例、OCV查表、电流噪声

真机调试时,最常改的三处是Q/R比例、OCV查表密度和电流噪声。

Q/R比例的症状很典型:SOC估计曲线平滑到看不出电压修正,说明Qx给太小;SOC跟着电压测量剧烈抖动,说明Qx给的相对Rx太大。调法固定:先把Rx按电压传感器手册里的噪声方差填好,再调Qx(1,1),每次乘或除10试一轮。

OCV查表的问题主要出现在磷酸铁锂。它的放电曲线平台区很平,dOCV/dSOC在SOC 30%~70%可能只有三元锂的十分之一,此时状态EKF的Hx变小,SOC几乎完全靠库仑积分撑住。如果这时OCV表本身还有台阶,电压残差会在平台区反复变号,SOC估计就会出现锯齿。解决方法是把电压容量对照表加密到SOC每1%一个点,并且用平滑后的曲线算数值导数。

电流噪声是很多工程实现容易忽略的点。DEKF只处理电压测量噪声,电流偏置会直接进入SOC状态方程做积分,等价于一个巨大的过程噪声。如果电流传感器存在固定偏置,DEKF无法从电压测量里完全恢复SOC。解决方式是在参数EKF里增加一个电流偏置参数,或者在硬件层面做零漂校准。这个扩展会带来可观测性问题,需要更多工况激励,不推荐第一版就做。

5. 避坑清单:双EKF在锂电池SOC估计上最容易翻车的5个细节

5.1 现象:SOC在电压平台区来回跳变

恒流放电跑到磷酸铁锂平台区,SOC估计本该平滑下降,实际却在5%范围内振荡。原因几乎都是OCV查表不平滑,或者dOCV_dSOC数值差分步长太大。电压平台区斜率接近0,残差里的微小电压波动会被放大成SOC修正。

解决方法是把OCV表离散点加密,插值方式从linear改成pchip,保证一阶导数连续。数值差分求dOCV_dSOC时,步长不要固定,可以用中心差分,步长取SOC表间隔的一半。调完再看SOC曲线,锯齿一般会消失。

5.2 现象:参数EKF把R0越估越大,甚至发散

恒流放电时R0估计值一路往上飘,SOC也跟着偏。原因是激励不足:恒流工况下,端电压的变动大部分来自SOC下降和极化电压缓慢建立,R0和V1等参数存在混叠。参数EKF在这种工况下没有新的信息可以区分它们,增益不减反增。

解决方法是给参数更新加触发条件。只有电流变化幅度超过阈值时才更新R0,否则只做预测,不更新参数。代码里加一个判断:

if abs(I(k) - I(k-1)) > 0.5 % 电流激励充分,更新参数 Ktheta = Ptheta_pred * Htheta / (Htheta * Ptheta_pred * Htheta + Rx); theta = theta + Ktheta * (Vt_meas(k) - Vt_hat_theta); Ptheta = (1 - Ktheta * Htheta) * Ptheta_pred; else % 激励不足,保留预测协方差 theta = theta; Ptheta = Ptheta_pred; end

同时对theta做边界限制,比如R0只能落在0.001到0.02之间。卡尔曼滤波本身不关心物理边界,但真值一定在合理区间内,边界裁剪是工程上最后的防线。

5.3 现象:状态EKF和参数EKF同时漂移

表现在SOC和R0都缓慢偏离真值,但两个残差都不大。原因通常是初始协方差Px和Ptheta给得过大,前几十步增益过高,把错误的修正量灌进了滤波器,之后协方差收缩,错误被“焊死”在状态里。

解决方法是故意跑一个错数据:把当前修好的状态当成真值重新初始化,然后人为把R0设偏50%,看滤波器能否在100步内拉回来。如果拉不回来,说明增益已经收缩到不更新的状态,需要适当增大Qtheta,让参数EKF保持“永远能接受新信息”的活力。

5.4 现象:想估计电池容量,结果端电压观测量不响应

有人把容量Cn也放进参数EKF,用端电压残差更新它,结果Cn几乎不动。这不是代码写错,而是可观测性问题:端电压对Cn没有直接偏导,Cn只通过SOC状态方程的积分间接影响电压,单靠瞬时电压残差根本喂不进去。

解决方法是把参数EKF的观测量改成SOC增量。先用状态EKF输出的SOC估计值做差分,再和安时积分公式做残差:

ΔSOC_est ≈ -η·Δt / Cn · I

这样Cn就能直接从SOC变化率里被观测出来。实际工程里我习惯把容量估计拆出来做另一个滤波器,而不是硬塞进同一套电压观测方程。顺带说一句,这个方向再往前走一步,就是电池剩余寿命预测的入口。

5.5 现象:Matlab脚本跑一遍要几分钟,无法实时

代码里每次循环都调用OCV_interp和dOCV_dSOC,数值差分每个SOC点要算两遍OCV,数据量一大就慢。解决方法是把OCV曲线和导数曲线预先算成一个高分辨率数组,循环里只做索引查表,不用插值。

更彻底的做法是把DEKF主循环封装成函数,用Matlab Coder生成C代码,或者直接在Simulink里跑。对BMS原型验证来说,先把OCV表向量化,已经能把速度提升一个数量级。

6. 把DEKF封装成一键重跑的matlab脚本:一个值得养成的工程习惯

前面所有代码都是平铺在脚本里,换一组数据就要改一堆变量。我后来养成的习惯是把DEKF封装成一个函数,输入电流、电压、采样周期和初始SOC,输出结果结构体。这样不同工况的数据,比如25℃恒流、动态脉冲、低温放电,都能一键重跑。

函数头可以这样写:

function result = run_DEKF(I, Vt_meas, dt, soc0) % 输入:电流序列I(A),电压序列Vt_meas(V),采样周期dt(s),初始SOC % 输出:result.soc, result.R0, result.RMSE, result.MAE % 内部完成初始化、DEKF循环、误差统计 end

这么做的好处是,验证新数据时不用再去翻初始化代码,只需要关心数据本身。另一个好处是,当你想对比单EKF和双EKF时,只要再写一个run_EKF函数,两个结果放在一起比较,说服力比空口解释强得多。我的血泪经验是:最早做DEKF时参数全写死在脚本里,换工况十几处要改,结果某一次漏改容量,整个验证结论都是错的。封装之后,初始化和循环逻辑只写一次,后面所有坑都在数据侧,排查速度明显提升。如果你准备把DEKF用到实际项目里,建议也从这个函数入口开始,先跑仿真数据,再跑实测数据,最后再考虑代码生成。希望帮到你。

本文还有配套的精品资源,点击获取