第三章 扩展卡尔曼滤波(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 |k-1

Hk = ∂h/∂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),那个不用算雅可比,对付非线性更有一套。