☰
无迹卡尔曼滤波(UKF)在电池SOC估计中的Matlab实现与调参经验
2026/10/15 5:14:42 网站建设 项目流程

电池管理系统里的SOC估计,听起来是个老话题,但真正动手用无迹卡尔曼滤波算法(UKF)把它做稳,其实有不少细节值得记一笔。我之前在Matlab里写过一套注释清晰的电池SOC估计脚本,核心就是UKF,花了很多精力保证代码可读性——滤波器这类算法,一旦逻辑绕起来,过两周再打开,如果没注释基本等于看天书。这篇文章就把电池建模、UKF原理、Matlab脚本实现和调参经验完整过一遍,适合正在做毕设、搞算法预研,或者单纯想把UKF从公式落到代码的读者。

1. 为什么电池SOC要用无迹卡尔曼滤波

1.1 传统SOC估算方法的瓶颈

SOC(荷电状态)没法直接测量,只能通过外特性去估计。工程上最常用的两个基础方法是安时积分法和开路电压法,但二者都有明显短板。

安时积分法的原理很简单:SOC(k+1) = SOC(k) - η·I·Δt / Q。只要电流采样准确,短时间内精度不错,计算量也小。可问题是电流传感器有零漂,采样有噪声,积分误差会一直累积且无法自我修正。初始SOC给错,后续一路错下去。这就像手机电量显示,如果长期不做完整充放电校准,百分比会慢慢漂移,最后充满瞬间突然跳变。

开路电压法靠的是OCV-SOC静态曲线查表,电池静置足够久之后,端电压等于开路电压,反查SOC自然准确。但动态工况下电池始终带有极化电压和欧姆压降,端电压根本不是OCV;而且很多电芯在SOC中间平台区电压变化非常平缓,特别是磷酸铁锂材料,稍微一点电压噪声就能造成几个百分点的SOC误差。

所以在实际项目中,不能只依赖单一方法,而是要把电流积分、开路电压特性和端电压观测融合起来。卡尔曼滤波正好做这件事,UKF又是其中很适合电池这类强非线性系统的选择。

1.2 EKF线性化的短板

扩展卡尔曼滤波(EKF)是卡尔曼滤波处理非线性系统的经典方案,思路是先对状态方程和观测方程求偏导,得到雅可比矩阵,然后套用标准卡尔曼滤波框架。

听起来简单,但落到电池SOC估计上就有几个头疼的地方:

  • OCV-SOC曲线不是线性关系,尤其平台区斜率接近零,一阶线性化之后误差容易被放大;
  • 电池模型的状态方程里有指数项,还有电流和状态的耦合,求偏导麻烦,万一模型从一阶RC换成二阶RC,所有偏导都要重推一遍;
  • 雅可比矩阵的离散化写法容易出错,一旦算错,滤波结果不是发散就是收敛到错误值。

EKF不是不能用,只是“维护成本”偏高。尤其当系统的非线性强度难以量化时,一阶近似到底够不够,谁也不敢打包票。

1.3 无迹变换在干什么

UKF的核心不是“把非线性函数线性化”,而是用“确定性采样”来传播统计特性。你可以这样理解:状态变量本身是个随机变量,只知道它的均值和协方差。现在要让它通过一个非线性函数,想知道输出的均值和协方差。

传统做法是找出函数切线,然后计算切线的传播;而无迹变换的做法是先在状态分布附近“撒”一组代表性点,这组点叫sigma点,让它们分别通过真实非线性函数,再对映射结果加权求均值和协方差。整个过程不需要求导,也不需要大规模蒙特卡洛采样,计算量可控,理论精度却能达到三阶泰勒展开的水平,强非线性下一般比EKF更稳。

对电池SOC估计来说,这意味着我只需要写清楚状态方程和观测方程,UKF自己就能完成统计量传递。模型从一阶RC改到二阶RC,也不需要重新推雅可比,维护体验好很多。

2. UKF核心原理与电池等效电路模型

2.1 电池模型选型:一阶RC戴维南等效电路

UKF是个滤波器框架,真正决定滤波精度的还有电池模型。我在这套脚本里用的是最常见的一阶RC戴维南等效电路,模型参数不算复杂,但描述动态电压基本够用。状态向量取二维:SOC和极化电压U1,系统输入是电流I,观测量是端电压V。

状态方程写成离散形式:

SOC(k+1) = SOC(k) - η·I(k)·Δt / (Q_cap·3600)

U1(k+1) = exp(-Δt/(R1·C1))·U1(k) + R1·(1 - exp(-Δt/(R1·C1)))·I(k)

观测方程:

V(k) = OCV(SOC(k)) - U1(k) - R0·I(k)

其中Q_cap是最大可用容量,单位Ah;η是库伦效率,放电时取1左右;R1、C1分别是一阶RC网络的极化电阻和极化电容;R0是欧姆内阻;OCV(SOC)通过离线标定数据插值得到。

有人会问为什么不用二阶RC?工程上二阶RC能更精细模拟浓差极化,但状态维度从2变成3,sigma点数从5变成7,计算量增加,还需要多一组RC参数的标定数据。一阶RC在中低倍率工况下,配合UKF已经能把端电压和SOC估计做得不错。先把一阶跑通,后续想扩到二阶,就是往状态向量里加一个U2的问题,代码结构不需要伤筋动骨。

2.2 无迹变换的数学步骤

无迹变换是UKF的地基。对于n维状态向量,我生成2n+1个sigma点,每个点带一个权重,分均值权重Wm和协方差权重Wc。

有两个关键变量需要先配置:

λ = α²(n + κ) - n

其中alpha控制sigma点在均值周围的散布,一般取范围1e-3到1之间;kappa是辅助缩放因子,通常取0或3-n;beta跟状态分布假设有关,高斯分布下取2效果最好。

sigma点生成方式:

X⁰ = x̄
Xⁱ = x̄ + sqrt(n+λ)·sqrt(P)的第i列
Xⁱ⁺ⁿ = x̄ - sqrt(n+λ)·sqrt(P)的第i列

P的平方根用Cholesky分解求下三角矩阵,Matlab里就是chol(P, 'lower')。

权重如下:

Wm⁰ = λ/(n+λ)
Wc⁰ = λ/(n+λ) + (1-α²+β)
Wmⁱ = Wcⁱ = 1/(2(n+λ)),i = 1..2n

这组权重在后面计算预测均值和协方差时会反复用到。代码里最好不要手写死,而是初始化时统一算好。

2.3 UKF主循环的四个环节

UKF滤波循环可以拆成四步:

第一步,根据当前状态均值和协方差生成sigma点。第二步,时间更新:把所有sigma点代入状态方程向前传播,然后加权得到预测均值x_pred和预测协方差P_pred,最后叠加过程噪声Q。第三步,量测更新:把预测的sigma点代入观测方程,得到预测观测值z_pred、观测协方差Pzz、互协方差Pxz。第四步,状态校正:用Pxz除以Pzz得到卡尔曼增益K,然后用实际量测电压残差去修正状态和协方差。

这套流程跟标准卡尔曼滤波是同一套骨架,只不过EKF用雅可比矩阵传递协方差,UKF用sigma点直接传递。

3. Matlab脚本框架与关键代码实现

3.1 脚本整体结构与注释规范

标题里既然写了“注释清晰”,这部分我就多说几句自己的习惯。我会把脚本分成五大块:文件头注释、电池参数区、OCV-SOC曲线数据、UKF滤波循环、结果绘图。

文件头注释里写清楚三件事:这段脚本解决什么问题、状态向量和观测向量是什么、主要单位和采样时间是多少。这样无论谁拿到文件,一眼就能建立上下文。再往下,每个函数前面都有一行注释说明输入输出;每一段滤波循环前,用注释标出“时间更新”还是“量测更新”;关键公式旁边标注对应论文或教科书里的公式编号。变量命名也别偷懒,SOC_est、P_pred、z_pred这种名字远远好过a、b、c。

内部实现我用了两个子函数,一个是状态方程f_func,一个是观测方程h_func。这样主循环代码非常干净,模型想换的时候只需要改这两个函数。

3.2 电池参数与OCV曲线初始化

以一套演示用的2.6Ah三元电芯参数为例:

% ============ 电池与滤波参数设置 ============ Q_cap = 2.6; % 最大可用容量,Ah dt = 1; % 滤波周期,秒 eta = 1.0; % 库伦效率,放电取1 R0 = 0.02; % 欧姆内阻,欧姆 R1 = 0.005; % 极化电阻,欧姆 C1 = 2000; % 极化电容,法拉 tau = R1 * C1; % 一阶RC时间常数,秒 % OCV-SOC 插值表 soc_pts = 0:0.1:1; ocv_pts = [3.20 3.28 3.34 3.39 3.48 3.58 3.71 3.85 3.96 4.06 4.16];

这里要强调单位。SOC更新公式里Q_cap单位是Ah,而dt单位是s,所以换算成小时需要除以3600。这个细节很多第一次写的人容易漏,漏掉之后SOC会在几秒内直接冲到负数或超过1。

OCV-SOC曲线实际项目中通过HPPC或低倍率充放电实验获得,我这里用一条示例插值表代替。注意不要在SOC接近0或1的两个端点外推,否则查表会得到奇怪电压。

3.3 状态方程与观测方程的实现

状态方程写成独立函数,输入当前状态、电流和模型参数,输出下一时刻状态:

function x_next = f_func(x, I, dt, eta, Q_cap, R1, C1) SOC = x(1); U1 = x(2); % 安时积分更新SOC SOC_next = SOC - eta * I * dt / (Q_cap * 3600); % 一阶RC网络极化电压更新 tau = R1 * C1; U1_next = exp(-dt / tau) * U1 + R1 * (1 - exp(-dt / tau)) * I; x_next = [SOC_next; U1_next]; end

观测方程稍微复杂一点,需要先根据SOC查OCV,再减去极化电压和欧姆内阻压降:

function V_est = h_func(x, I, R0, soc_pts, ocv_pts) SOC = max(0, min(1, x(1))); % 防止SOC越界 U1 = x(2); % 插值得到当前SOC对应的开路电压 OCV = interp1(soc_pts, ocv_pts, SOC, 'pchip'); V_est = OCV - U1 - R0 * I; end

观测方程里的SOC限幅很有必要。滤波过程中,如果SOC暂时被修正到负值或超过1,OCV插值函数会返回NaN,整个滤波循环直接崩掉。用max和min钳制可以保证稳定性。

3.4 sigma点生成与权重初始化

n=2时,总共生成2n+1=5个sigma点。权重初始化代码如下:

n = 2; % 状态维度 alpha = 0.01; % sigma点散布系数 beta = 2; % 高斯分布推荐值 kappa = 0; % 辅助缩放因子 lambda = alpha^2 * (n + kappa) - n; % 均值权重 Wm = zeros(2*n + 1, 1); % 协方差权重 Wc = zeros(2*n + 1, 1); Wm(1) = lambda / (n + lambda); Wc(1) = lambda / (n + lambda) + (1 - alpha^2 + beta); for i = 2 : 2*n + 1 Wm(i) = 1 / (2 * (n + lambda)); Wc(i) = 1 / (2 * (n + lambda)); end

alpha取0.01时,sigma点离均值较近,适合SOC状态变化不太剧烈的情况。如果发现滤波结果对噪声太敏感,可以往0.1左右调,让sigma点更分散一些。

主循环里生成sigma点和时间更新的写法是:

% 生成当前时刻的sigma点 sqrtP = chol(P, 'lower'); X = zeros(n, 2*n + 1); X(:, 1) = x; for i = 1 : n X(:, 1 + i) = x + sqrt(n + lambda) * sqrtP(:, i); X(:, 1 + n + i) = x - sqrt(n + lambda) * sqrtP(:, i); end % 时间更新:所有sigma点通过状态方程 X_pred = zeros(n, 2*n + 1); for i = 1 : 2*n + 1 X_pred(:, i) = f_func(X(:, i), I(k), dt, eta, Q_cap, R1, C1); end x_pred = X_pred * Wm; P_pred = (X_pred - x_pred) * diag(Wc) * (X_pred - x_pred)' + Q;

3.5 量测更新与滤波主循环

量测更新部分是把传播后的sigma点继续通过观测方程,然后算出卡尔曼增益:

Z_pred = zeros(1, 2*n + 1); for i = 1 : 2*n + 1 Z_pred(i) = h_func(X_pred(:, i), I(k), R0, soc_pts, ocv_pts); end z_pred = Z_pred * Wm; Pzz = (Z_pred - z_pred) * diag(Wc) * (Z_pred - z_pred)' + R; Pxz = (X_pred - x_pred) * diag(Wc) * (Z_pred - z_pred)'; K = Pxz / Pzz; % 状态校正 x = x_pred + K * (V_meas(k) - z_pred); P = P_pred - K * Pzz * K';

注意我这里用的是传播后的sigma点直接做量测预测。理论上还有一种标准做法是时间更新后从x_pred重新生成sigma点再走观测方程,两种方式在实际SOC估计里差别很小,但直接传播法的代码少一段,阅读起来更清爽。我在脚本注释里会标注这一点,避免后来的人误以为写漏了。

Pzz是标量(因为观测是电压),所以K = Pxz / Pzz就是普通的标量除法。

整个循环跑完后,SOC估计值就是x(1)的轨迹。如果想看滤波不确定性,还可以把P(1,1)开根号,得到SOC估计标准差,用来判断当前估计是否可信。

4. 仿真验证:从收敛速度到误差分析

4.1 仿真工况与量测数据生成

滤波器的效果需要放到工况里检验。我先用状态方程生成一组带噪声的“真实电压观测”,再把初始SOC故意设偏,观察UKF能不能把SOC修正回来。

N = 3600; % 仿真长度,1小时 t = (0 : N-1)'; % 电流工况:前600秒恒流放电,之后加入脉冲电流 I = 0.5 * ones(N, 1); % 0.5C放电约1.3A I(601:1200) = 1.0; % 1C脉冲 I(1201:1800) = 0.2; % 小电流 I(1801:2400) = -0.5; % 充电脉冲 I(2401:end) = 0.3; % 真实SOC与U1轨迹 SOC_true = zeros(N, 1); U1_true = zeros(N, 1); SOC_true(1) = 0.8; U1_true(1) = 0; for k = 1 : N-1 x_tmp = f_func([SOC_true(k); U1_true(k)], I(k), dt, eta, Q_cap, R1, C1); SOC_true(k+1) = x_tmp(1); U1_true(k+1) = x_tmp(2); end % 量测电压 = 观测方程输出 + 传感器噪声 V_noise = 0.005 * randn(N, 1); V_meas = zeros(N, 1); for k = 1 : N V_meas(k) = h_func([SOC_true(k); U1_true(k)], I(k), R0, soc_pts, ocv_pts) + V_noise(k); end

这里故意让真实SOC起点是0.8,而滤波器初始值是0.9,用来验证收敛能力。

4.2 典型滤波结果:看收敛速度和误差

实际跑下来的典型结果是这样几组数字:初始SOC误差约10个百分点,UKF大概在几十秒之内把误差压到2个百分点以内。收敛速度取决于OCV-SOC曲线的斜率,斜率大的SOC区间收敛快,平台区收敛相对慢。原因是UKF主要靠电压残差去修正估计,而平台区电压对SOC不敏感,可提供的信息量本来就少。

稳态阶段,SOC估计误差能稳定在1到2个百分点左右,滤波后的电压残差基本在正负几个毫伏范围内,比原始量测噪声明显平滑很多。

这一切的前提是模型参数基本准确。如果内部模型参数与“真实”参数差异过大,滤波器的电压残差会出现某种系统性偏差,比如持续为正或持续为负,这时候光调Q和R没有用,得回头校准模型参数。

4.3 与EKF对比的直观感受

我并不是说UKF在所有指标上都完胜EKF,但在电池SOC这个场景里,直观对比确实有几条明显优势:

对比维度EKFUKF
非线性处理方式一阶线性化,需要求雅可比矩阵确定性采样,不需求导
模型修改成本改模型要重推雅可比只改状态方程和观测方程
强非线性工况平台区容易失真精度相对更稳
计算量较小,差异约在可接受范围每个周期多了5次函数传播

对BMS这种嵌入式环境,UKF的计算量会比EKF高一截,但5个sigma点对现代MCU并不算大负担。要是资源特别紧张的场合,也有简化或降维方案。

5. 调参经验:我踩过的坑与排查思路

5.1 协方差初值怎么设才不震荡

P0代表对初始状态的信任程度。如果手里SOC初值来自开路电压查表,误差大概正负5个百分点,那P0(1,1)可以取0.05的平方,也就是0.0025。U1初始值一般设为0,但不确定度可能到0.1V左右,所以P0(2,2)取0.01。

P0设得太小,滤波器会过于相信初始状态,SOC收敛很慢;设得太大,滤波初期容易震荡,甚至需要很久才能稳定。实操中我会先给一个偏大的初始协方差,让滤波器快速利用量测修正,到稳态之后协方差再慢慢降下来。这比一开始就锁得很死的效果好。

5.2 Q和R的整定心得

Q是过程噪声协方差,R是测量噪声协方差。整定核心是理解它们各自的物理含义。

Q(1,1)对应SOC的模型不确定度。电流测量噪声σ_I会通过安时积分进入SOC,一个合理起步值是(η·dt/(Q_cap·3600)·σ_I)的平方。Q(2,2)对应极化电压模型误差,因为一阶RC本身是近似,这个值不用算得太精确,取一个小量即可。

R对应电压传感器噪声方差。好的电压采样大概有3到5mV噪声,R可以取(0.005)²到(0.01)²之间。R调小了,滤波输出会追着电压噪声跑,SOC曲线毛糙;R调大了,滤波器反应迟钝,工况切换时SOC跟不上真值。

经验速查表:

现象倾向的调整方向
SOC估计曲线毛刺明显R适当调大,或Q调小
SOC响应太慢,跟不上工况切换Q适当调大,或R调小
电压残差长期同号先查模型参数,而不是继续调噪声
端电压拟合很好但SOC偏检查OCV-SOC曲线是否标定准确

5.3 常见错误与排查速查表

写这套脚本过程中,我遇到过几个比较典型的坑,列出来给后来者参考。

错误现象可能原因排查与解决
chol(P)报矩阵不是正定滤波后P可能失去对称性每次更新前加 P = (P+P')/2 强制对称
interp1返回NaNSOC越界,查表点超出端点观测函数里用max/min把SOC钳制在0到1
SOC发散到负值或大于1Q或P0设置过大先调小P0和Q,同时在输出端限幅
滤波结果噪声大R太小,或模型参数有偏差先调R,再核查模型参数
代码运行慢sigma点循环写法太笨重把模型函数写成一次性向量运算,尽量少用循环

还有一个容易被忽略的问题:矩阵求逆。当Pzz是标量时,直接K = Pxz / Pzz最稳。如果以后扩展到多维观测,例如加入温度观测,Pzz会变成矩阵,这时优先用K = Pxz / Pzz的右除写法,或者pinv(Pzz),不要用自己手动算inv再乘。原因很简单,数值上除法比显式求逆更稳定,这个习惯可以避免很多数值灾难。

5.4 注释清晰背后的调试收益

最后说回“注释清晰”这件事,它不只是给别人看的。UKF主循环里每一行都对应一个数学公式,如果注释不写清楚,三周后再想查“P_pred为什么减K Pzz K'”,又得从UT推导开始重新看一遍。

我习惯在时间更新、量测更新、增益计算的关键行上方,各写一行与公式对应的注释,比如“Pzz = 观测预测协方差 + 测量噪声R”,这样整个循环的逻辑链条是完整的。调试的时候,如果结果不对,可以先人工打印中间变量,对照注释确认是哪个环节出了问题。这种可追溯性,是滤波器代码里最值钱的东西。

6. 写在最后:一点真实体会

如果只让我留一条建议,我会说:不要一上来就把模型、滤波、数据生成全塞进一个巨型脚本。先把状态方程和观测方程单独写成函数,再用最简单的恒流工况验证滤波器的收敛性,最后才加动态工况。这个顺序能省掉一半的调试时间。

还有一点,UKF虽然不需要求雅可比矩阵,但不代表可以忽略模型准确性。模型本身错了,滤波器再精巧也无济于事。遇到滤波结果不对,先问模型对不对,再问参数调没调,最后才去怀疑算法框架。

这套带注释的Matlab UKF脚本跑顺之后,后续可以往自适应噪声整定、强跟踪UKF、二阶RC模型扩展、甚至卡尔曼增益的遗忘因子方向继续做。每一步现有代码框架都能复用。希望这篇经验能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询