第三章 扩展卡尔曼滤波(EKF):非线性系统线性化
说实话,上一章讲的标准卡尔曼滤波,在BMS领域里其实有点「水土不服」。
为什么?因为电池系统压根不是线性的。你想想看,SOC和电压之间的关系,那是一根弯曲的曲线,不是直线。OCV-SOC曲线你肯定见过,中间那段平缓得跟什么似的,两端又陡得吓人。这种非线性关系,标准KF根本处理不了。
我刚开始做SOC估计时,就踩过这个坑。拿着标准KF往上怼,结果误差大得离谱,尤其是电池在中间SOC区间的时候,估计值飘得我都不敢看。后来才明白——得用扩展卡尔曼滤波,也就是EKF。
3.1 非线性系统线性化:泰勒展开
EKF的核心思想,说白了就四个字:以直代曲。
怎么代?用泰勒展开。把非线性函数在当前估计点附近展开,只保留一阶项,忽略高阶项。这样就把非线性系统近似成了线性系统,然后就可以套用标准KF的框架了。
数学上长这样:
假设状态方程:xk = f(xk-1, uk-1) + wk-1
观测方程:zk = h(xk) + vk
对f和h分别求偏导,得到雅可比矩阵:
Fk-1 = ∂f/∂x |x̂k-1
Hk = ∂h/∂x |x̂k|k-1
嗯,这里要注意:泰勒展开只在展开点附近有效。如果非线性特别强,或者你的初始估计离真实值太远,一阶近似就不够用了。我在项目中遇到过电池在低温下OCV曲线特别扭曲的情况,那时候EKF的收敛速度明显变慢,就是因为线性化误差太大了。
3.2 雅可比矩阵计算:手撕还是自动微分?
雅可比矩阵,是EKF里最让人头疼的部分。你得对每个状态变量求偏导,而且还得在每一步重新计算。
我个人习惯手撕雅可比。为什么?因为电池模型的结构相对固定,手撕出来的代码效率高,而且你能清楚知道每个偏导的物理意义。比如对于一阶RC模型:
// 状态方程:x = [SOC, V1]^T
// SOC(k) = SOC(k-1) - (η*Δt/Cn)*I(k-1)
// V1(k) = V1(k-1)*exp(-Δt/τ) + R1*(1-exp(-Δt/τ))*I(k-1)
// 雅可比矩阵 F = ∂f/∂x
F[0][0] = 1.0; // ∂SOC(k)/∂SOC(k-1)
F[0][1] = 0.0; // ∂SOC(k)/∂V1(k-1)
F[1][0] = 0.0; // ∂V1(k)/∂SOC(k-1)
F[1][1] = exp(-Δt/τ); // ∂V1(k)/∂V1(k-1)
// 观测方程:Vt = OCV(SOC) - V1 - R0*I
// 雅可比矩阵 H = ∂h/∂x
H[0][0] = dOCV_dSOC(SOC); // ∂Vt/∂SOC
H[0][1] = -1.0; // ∂Vt/∂V1
避坑指南:我曾经在dOCV_dSOC的计算上吃过亏。OCV-SOC曲线通常用查表加插值实现,但插值方法选不好,导数会剧烈跳变。我建议用三次样条插值,导数平滑,EKF的稳定性会好很多。
当然,现在也有自动微分工具。如果你用的是Python,JAX或者PyTorch都能自动算雅可比。但嵌入式C代码里,还是老老实实手撕吧。
3.3 EKF在SOC估计中的实战
好了,理论说完了,咱们直接上实战。下面是一个完整的EKF-SOC估计流程,我用的是二阶RC模型:
// EKF-SOC估计主循环
void ekf_soc_estimate(EKF_State *ekf, float current, float voltage) {
// 1. 时间更新(预测)
// 状态预测:用上一时刻的状态和当前电流
predict_state(ekf, current);
// 协方差预测:P = F * P * F^T + Q
float F[2][2] = {{1, 0}, {0, exp(-dt/tau2)}};
float P_pred[2][2];
matrix_multiply(F, ekf->P, P_pred); // F * P
matrix_multiply_transpose(P_pred, F, ekf->P); // * F^T
ekf->P[0][0] += Q_soc; // 加过程噪声
ekf->P[1][1] += Q_v2;
// 2. 测量更新(校正)
// 计算卡尔曼增益:K = P * H^T * (H * P * H^T + R)^(-1)
float H[1][2] = {dOCV_dSOC(ekf->soc), -1};
float S = H[0][0]*ekf->P[0][0]*H[0][0]
+ H[0][1]*ekf->P[1][1]*H[0][1]
+ 2*H[0][0]*ekf->P[0][1]*H[0][1]
+ R_measure;
float K[2] = {
(ekf->P[0][0]*H[0][0] + ekf->P[0][1]*H[0][1]) / S,
(ekf->P[1][0]*H[0][0] + ekf->P[1][1]*H[0][1]) / S
};
// 状态更新:x = x_pred + K * (z - h(x_pred))
float V_pred = ocv_lookup(ekf->soc) - ekf->v2 - R0*current;
float innovation = voltage - V_pred;
ekf->soc += K[0] * innovation;
ekf->v2 += K[1] * innovation;
// 协方差更新:P = (I - K*H) * P
// 实际用Joseph形式保证对称正定
update_covariance_joseph(ekf->P, K, H);
}
重要提醒:协方差更新一定要用Joseph形式!直接用(I - K*H)*P在数值上容易失去对称性,导致协方差矩阵不正定,然后EKF就发散了。我调试时遇到过这个问题,折腾了两天才找到原因。
3.4 参数调优:Q和R怎么设?
EKF里有两个关键参数:过程噪声协方差Q和测量噪声协方差R。这两个参数设不好,EKF性能直接拉胯。
| 参数 | 物理含义 | 设大了会怎样 | 设小了会怎样 |
|---|---|---|---|
| Q(过程噪声) | 模型的不确定性 | 滤波响应快,但噪声大 | 滤波平滑,但响应慢 |
| R(测量噪声) | 传感器的噪声水平 | 更相信模型,响应慢 | 更相信测量,易受噪声干扰 |
我个人习惯这样调:先根据传感器手册确定R的初始值,比如电压测量精度±5mV,那R就设(0.005)²。然后Q从较小的值开始试,看SOC估计的收敛速度和稳态误差。
我曾经在一个项目中,电池电流传感器有偏置误差,导致SOC估计长期偏离。后来我把Q适当调大,让EKF更相信电压测量,才把偏差拉回来。嗯,这就是所谓的「自适应」思想——但那是后话了。
3.5 EKF的局限性
EKF虽然好用,但也不是万能的。我总结了几点:
- 线性化误差:强非线性下,一阶近似不够用。这时候可以考虑UKF或者粒子滤波。
- 雅可比计算:模型复杂时,手撕雅可比容易出错。我建议写单元测试验证。
- 初值敏感:初始SOC偏差太大,EKF可能不收敛。实际中我会先用开路电压法给个初值。
一个小技巧:如果你发现EKF在某个SOC区间老是发散,可以试试在每次测量更新后加一个状态约束。比如SOC强制限制在0~100%之间,V1限制在合理范围内。这虽然不是严格的数学处理,但工程上非常有效。
好了,EKF的内容就到这里。下一章咱们聊聊无迹卡尔曼滤波(UKF),那个不用算雅可比,对付非线性更有一套。