4、卡尔曼滤波入门:状态空间模型、预测与更新步骤、一维卡尔曼滤波实现
各位同学,欢迎来到第四讲。
前面我们聊了电池建模和开路电压法。说实话,那些方法在静态场景下还行。但电动牙刷一开机,电机嗡嗡转起来,电压瞬间波动,传统方法就抓瞎了。
这时候,卡尔曼滤波就该登场了。
我个人觉得,卡尔曼滤波是电池电量估算里最优雅的工具之一。它不直接告诉你电量是多少,而是通过「预测」和「修正」两个步骤,一步步逼近真实值。有点像你闭着眼睛走路,先迈一步(预测),然后睁开眼看看偏了多少(更新),再调整方向。
4.1 状态空间模型:把问题装进数学框子里
要理解卡尔曼滤波,先得搞懂状态空间模型。说白了,就是把我们的系统用数学语言描述出来。
一个典型的离散时间系统,包含两个核心方程:
- 状态方程:描述系统内部状态如何随时间演变。
- 观测方程:描述我们如何通过传感器测量到系统状态。
拿电动牙刷的电池来说,我们关心的状态是电池的荷电状态(SOC)。但SOC不能直接测量,我们只能测到电压和电流。
状态空间模型长这样:
x(k) = A * x(k-1) + B * u(k-1) + w(k-1)
z(k) = H * x(k) + v(k)
这里:
- x(k):k时刻的系统状态(比如SOC)
- u(k):控制输入(比如电流)
- z(k):观测值(比如电压)
- A:状态转移矩阵
- B:控制输入矩阵
- H:观测矩阵
- w(k):过程噪声(系统模型的不确定性)
- v(k):观测噪声(传感器的测量误差)
嗯,这里要注意。w和v都是高斯白噪声,均值是0。这个假设很重要,卡尔曼滤波就是建立在噪声是高斯分布的基础上的。
我在项目中遇到过一个问题:有同事把过程噪声设得太小,结果滤波器对测量值过度信任,反而放大了噪声。后来我建议他把w的协方差矩阵调大一些,效果立马好了。
4.2 预测与更新:卡尔曼滤波的两步舞
卡尔曼滤波的核心就两个步骤:预测和更新。你想想看,这就像你每天早上出门前,先预估一下今天会不会下雨(预测),然后到了外面感受一下实际天气(更新),再调整你的判断。
4.2.1 预测步骤(时间更新)
预测步骤,就是根据上一时刻的状态,推算当前时刻的状态。公式如下:
// 状态预测
x_pred = A * x_est + B * u
// 误差协方差预测
P_pred = A * P_est * A^T + Q
这里:
- x_pred:预测的状态
- P_pred:预测的误差协方差(表示我们对预测的信任程度)
- Q:过程噪声协方差矩阵
Q值越大,说明我们对模型越不信任,预测的误差范围就越大。
4.2.2 更新步骤(测量更新)
更新步骤,就是用实际测量值来修正预测值。公式如下:
// 卡尔曼增益
K = P_pred * H^T * (H * P_pred * H^T + R)^(-1)
// 状态更新
x_est = x_pred + K * (z - H * x_pred)
// 误差协方差更新
P_est = (I - K * H) * P_pred
这里:
- K:卡尔曼增益(核心中的核心)
- R:观测噪声协方差矩阵
- z:实际测量值
卡尔曼增益K决定了我们更相信预测值还是测量值。如果K大,说明我们更相信测量值;如果K小,说明我们更相信预测值。
我曾经调试过一个项目,电池电压测量噪声特别大。一开始我把R设得很小,结果滤波器跟着测量值乱跳。后来我把R调大了,滤波器就平滑多了。说白了,R和Q的调参,就是卡尔曼滤波的「艺术」所在。
4.3 一维卡尔曼滤波实现:从理论到代码
好了,理论讲完了,咱们直接上代码。一维卡尔曼滤波是最简单的形式,状态只有一个变量(比如SOC)。
我习惯用C语言写嵌入式代码,这里给一个完整的实现:
#include <stdio.h>
#include <math.h>
// 一维卡尔曼滤波器结构体
typedef struct {
float x; // 状态估计值
float P; // 误差协方差
float Q; // 过程噪声协方差
float R; // 观测噪声协方差
float K; // 卡尔曼增益
} KalmanFilter1D;
// 初始化滤波器
void Kalman_Init(KalmanFilter1D *kf, float init_x, float init_P, float Q, float R) {
kf->x = init_x;
kf->P = init_P;
kf->Q = Q;
kf->R = R;
kf->K = 0;
}
// 预测步骤
void Kalman_Predict(KalmanFilter1D *kf, float u) {
// 状态预测:x = x + u(假设A=1,B=1)
kf->x = kf->x + u;
// 误差协方差预测:P = P + Q
kf->P = kf->P + kf->Q;
}
// 更新步骤
void Kalman_Update(KalmanFilter1D *kf, float z) {
// 计算卡尔曼增益
kf->K = kf->P / (kf->P + kf->R);
// 状态更新
kf->x = kf->x + kf->K * (z - kf->x);
// 误差协方差更新
kf->P = (1 - kf->K) * kf->P;
}
// 使用示例
int main() {
KalmanFilter1D kf;
// 初始化:初始SOC=50%,初始误差=10,Q=0.01,R=1
Kalman_Init(&kf, 50.0f, 10.0f, 0.01f, 1.0f);
// 模拟测量数据(带噪声的SOC值)
float measurements[] = {49.5f, 48.8f, 48.2f, 47.6f, 47.0f};
float control_input = -0.5f; // 每次放电0.5%
printf("原始测量值\t滤波后值\n");
for(int i = 0; i < 5; i++) {
// 预测
Kalman_Predict(&kf, control_input);
// 更新
Kalman_Update(&kf, measurements[i]);
printf("%.2f\t\t%.2f\n", measurements[i], kf.x);
}
return 0;
}
这段代码虽然简单,但已经包含了卡尔曼滤波的全部精髓。你想想看,每次调用Predict和Update,滤波器都在做一次「预测-修正」的循环。
4.4 参数调优:Q和R的博弈
卡尔曼滤波的性能,很大程度上取决于Q和R的取值。我总结了一个经验表格:
| 参数 | 物理含义 | 调大效果 | 调小效果 |
|---|---|---|---|
| Q(过程噪声) | 模型的不确定性 | 更相信测量值,响应快但噪声大 | 更相信模型,响应慢但平滑 |
| R(观测噪声) | 传感器的噪声水平 | 更相信预测值,平滑但滞后 | 更相信测量值,灵敏但易受干扰 |
对于电动牙刷的电池SOC估算,我建议:
- Q:取0.001~0.01(因为SOC变化很慢,模型相对准确)
- R:取0.5~2(取决于电压测量电路的噪声水平)
嗯,这里要注意。实际项目中,Q和R往往需要根据实验数据来标定。我习惯的做法是:先采集一段真实数据,然后用不同的Q和R跑一遍滤波器,看哪个组合的误差最小。
好了,这一讲就到这里。卡尔曼滤波入门其实不难,关键是要理解「预测-更新」这个循环,以及Q和R的物理含义。下一讲,我们会把一维卡尔曼滤波扩展到二维,真正用于电池SOC和电压的联合估算。
记住:理论是死的,应用是活的。多动手调参,你就能找到感觉。