卡尔曼滤波不好懂?单片机非线性滤波天花板,看完直接上手写代码!
卡尔曼滤波不好懂?单片机非线性滤波天花板,看完直接上手写代码!
你有没有过这种疑惑?无人机在天上翻来覆去却从不迷路,电机没装传感器却能精准转圈圈,甚至雷达盯着高速移动的目标跑,再灵活也不会跟丢——这些看似“开挂”的操作背后,其实都藏着一个叫“扩展卡尔曼滤波(EKF)”的硬核技术!
说出来你可能不信,这个听起来满是公式和术语的“大佬级算法”,本质上就是个“聪明的猜谜高手”:面对非线性系统里满是干扰的数据,它能一步步修正猜测,把杂乱的信号“捋顺”,精准揪出我们想要的核心信息。今天就抛开晦涩的学术腔调,用唠嗑的方式带你吃透EKF的原理,还附上能直接抄的C语言代码,新手也能轻松拿捏!
一、先搞懂:EKF到底是干啥的?
卡尔曼滤波(KF)大家可能听过,它是线性系统的“数据美颜师”——比如给匀速运动的小车测速,能把传感器的噪声过滤得干干净净。但现实世界哪有那么多“规规矩矩的线性系统”?电机的电压电流关系、无人机的姿态变化,全是弯弯绕绕的非线性模型,KF遇到这些就直接“卡壳”了。
这时候EKF就登场了!它相当于KF的“升级版外挂”,核心操作是用“泰勒级数一阶线性化”把非线性模型“掰直”——简单说就是把复杂的曲线在某一点附近,用一条直线近似代替,这样KF就能接着干活了。就像我们看远处的曲线,凑近了看局部其实和直线差不多,EKF就是利用这个小技巧,搞定了非线性系统的滤波难题。
二、EKF的“五步魔法”:从猜测到精准的全过程
EKF的核心逻辑其实超简单,就五个步骤,像玩闯关游戏一样一步步推进,咱们逐个拆解:
1. 先明确:系统的“基本设定”
要让EKF干活,得先告诉它三个关键信息:
- 状态向量x:你想知道的核心数据(比如电机的转子位置θ、转速ω,相当于游戏里要找的“宝藏”);
- 输入向量u:系统的控制信号(比如电机的d/q轴电压,相当于给宝藏设定的“引导线索”);
- 观测向量z:传感器的实测数据(比如电机的d/q轴电流,相当于寻宝路上的“现场线索”)。
还有两个“调皮的干扰项”得提前说明:
- 过程噪声w:系统运行中不可避免的小误差(比如电机转动时的轻微抖动),用协方差矩阵Q描述它的“捣乱程度”;
- 观测噪声v:传感器测量时的误差(比如电流采样的微小偏差),用协方差矩阵R描述它的“不靠谱程度”。
对应的两个核心方程,咱们用“人话”翻译一下:
- 非线性状态方程:xₖ = f(xₖ₋₁, uₖ₋₁) + wₖ₋₁ → 下一秒的状态 = 基于上一秒状态和控制信号的猜测 + 小干扰;
- 非线性观测方程:zₖ = h(xₖ) + vₖ → 传感器实测值 = 基于当前状态的预测观测值 + 测量误差。
2. 五步核心操作:EKF的“闯关流程”
(1)状态预测:猜一猜下一秒会咋样?
公式:x^k−=f(x^k−1+,uk−1)\hat{x}_{k}^{-}=f\left(\hat{x}_{k-1}^{+}, u_{k-1}\right)x^k−=f(x^k−1+,uk−1)
翻译:根据上一次的精准结果(x^k−1+\hat{x}_{k-1}^{+}x^k−1+)和控制信号(uₖ₋₁),预测当前的状态(x^k−\hat{x}_{k}^{-}x^k−,带“-”表示这是猜测值)。比如知道电机上一秒的位置和转速,就能猜这一秒的位置大概是多少。
(2)协方差预测:算一算这个猜测靠谱吗?
公式:Pk−=FkPk−1+FkT+QkP_{k}^{-}=F_{k} P_{k-1}^{+} F_{k}^{T}+Q_{k}Pk−=FkPk−1+FkT+Qk
翻译:预测完状态,还得给这个猜测打个“靠谱分”(协方差矩阵Pk−P_{k}^{-}Pk−)。这里的Fₖ是状态方程的雅可比矩阵,相当于“线性化翻译官”,把非线性的状态变化转换成线性关系;FkTF_{k}^{T}FkT是Fₖ的转置,最后加上过程噪声Qₖ,就得到了猜测值的“不靠谱程度”。
(3)卡尔曼增益计算:该听预测的还是听观测的?
公式:Kk=Pk−HkT(HkPk−HkT+Rk)−1K_{k}=P_{k}^{-} H_{k}^{T}\left(H_{k} P_{k}^{-} H_{k}^{T}+R_{k}\right)^{-1}Kk=Pk−HkT(HkPk−HkT+Rk)−1
翻译:这一步是EKF的“智慧核心”,计算一个“权重”Kₖ。Hₖ是观测方程的雅可比矩阵(另一个“线性化翻译官”),通过一堆矩阵运算,最终决定:到底该多相信预测值(x^k−\hat{x}_{k}^{-}x^k−),还是多相信传感器的观测值(zₖ)。比如传感器很精准(Rₖ小),Kₖ就会偏向观测值;系统预测很靠谱(Pk−P_{k}^{-}Pk−小),Kₖ就会偏向预测值。
(4)状态更新:修正猜测,得到精准结果
公式:x^k+=x^k−+Kk(zk−h(x^k−))\hat{x}_{k}^{+}=\hat{x}_{k}^{-}+K_{k}\left(z_{k}-h\left(\hat{x}_{k}^{-}\right)\right)x^k+=x^k−+Kk(zk−h(x^k−))
翻译:用卡尔曼增益Kₖ,把“预测值”和“观测值与预测观测值的差值”(也就是观测残差)结合起来,修正出更精准的当前状态(x^k+\hat{x}_{k}^{+}x^k+,带“+”表示这是优化后的结果)。相当于猜完之后,对照现场线索调整答案,让结果更准。
(5)协方差更新:更新靠谱分,为下一轮做准备
公式:Pk+=(I−KkHk)Pk−P_{k}^{+}=\left(I-K_{k} H_{k}\right) P_{k}^{-}Pk+=(I−KkHk)Pk−
翻译:修正完状态后,也得更新“靠谱分”(Pk+P_{k}^{+}Pk+)。I是单位矩阵,通过计算得到新的协方差矩阵,作为下一轮预测的基础。就像闯关成功后,更新自己的经验值,为下一关做准备。
三、典型应用:电机转子位置估算(一看就懂的实例)
光说理论太枯燥,咱们拿永磁同步电机(PMSM)举例,看看EKF是怎么“干活”的:
1. 设定核心参数
- 状态向量x = [θ, ω]ᵀ:θ是转子位置,ω是转速(这俩是我们要精准估算的“宝藏”);
- 输入向量u = [U_d, U_q]ᵀ:U_d和U_q是电机的d/q轴电压(控制信号);
- 观测向量zₖ = [I_d, I_q]ᵀ:I_d和I_q是电机的d/q轴电流(传感器实测数据)。
2. 状态方程(电机的“运动规律”)
{θk=θk−1+ωk−1⋅Ts+wθωk=ωk−1+1J(Te−TL)⋅Ts+wω\left\{\begin{array}{l} \theta_{k}=\theta_{k-1}+\omega_{k-1} \cdot T_{s}+w_{\theta} \\ \omega_{k}=\omega_{k-1}+\frac{1}{J}\left(T_{e}-T_{L}\right) \cdot T_{s}+w_{\omega} \end{array}\right.{θk=θk−1+ωk−1⋅Ts+wθωk=ωk−1+J1(Te−TL)⋅Ts+wω
翻译:
- 转子位置θₖ = 上一秒位置θₖ₋₁ + 上一秒转速ωₖ₋₁ × 采样周期Tₛ + 位置干扰w_θ;
- 转速ωₖ = 上一秒转速ωₖ₋₁ + (电磁转矩Tₑ - 负载转矩T_L)/ 转动惯量J × 采样周期Tₛ + 转速干扰w_ω。
简单说就是:电机的位置是转速累积来的,转速是转矩差决定的,过程中会有一点点干扰。
3. 观测方程(传感器的“测量逻辑”)
zₖ = [IdIq]\left[\begin{array}{c} I_{d} \\ I_{q} \end{array}\right][IdIq] = h(xₖ, uₖ) + vₖ
翻译:传感器测到的电流I_d、I_q,是基于当前电机状态xₖ和控制电压uₖ的非线性函数h计算出来的,再加上一点点测量误差vₖ。EKF就是通过这个方程,把电流数据转换成对位置和转速的修正依据。
四、C语言实现:直接抄的通用框架(附详细解释)
看完原理,最关键的就是上手写代码!下面是适配电机位置/转速估算的通用EKF框架,每个部分都加了通俗注释,新手也能跟着改:
1. 头文件和宏定义(先搭好“舞台”)
#include <math.h>
#include <string.h>
// 矩阵维度定义:根据实际需求调整,这里是电机的2维状态、2维观测、2维输入
#define STATE_DIM 2 // 状态维度:[转子位置theta, 转速omega]
#define OBSERVE_DIM 2 // 观测维度:[d轴电流I_d, q轴电流I_q]
#define INPUT_DIM 2 // 输入维度:[d轴电压U_d, q轴电压U_q]
2. EKF结构体(存放所有“关键数据”)
typedef struct {
float x[STATE_DIM]; // 优化后的状态估计值(最终要的结果)
float P[STATE_DIM][STATE_DIM]; // 优化后的协方差矩阵(靠谱分)
float Q[STATE_DIM][STATE_DIM]; // 过程噪声协方差(干扰的捣乱程度)
float R[OBSERVE_DIM][OBSERVE_DIM];// 观测噪声协方差(传感器的不靠谱程度)
float F[STATE_DIM][STATE_DIM]; // 状态雅可比矩阵(线性化翻译官1)
float H[OBSERVE_DIM][STATE_DIM]; // 观测雅可比矩阵(线性化翻译官2)
float K[STATE_DIM][OBSERVE_DIM]; // 卡尔曼增益(权重)
float x_pred[STATE_DIM]; // 预测的状态值(初步猜测)
float P_pred[STATE_DIM][STATE_DIM];// 预测的协方差矩阵(猜测的靠谱分)
} EKF_TypeDef;
3. 矩阵运算工具函数(必备“小工具”)
矩阵运算听起来吓人,其实就是固定套路,这些函数直接抄就行:
// 矩阵乘法:C = A*B(A是m×n矩阵,B是n×p矩阵,结果C是m×p矩阵)
void matrix_mult(float A[][STATE_DIM], float B[][STATE_DIM],
float C[][STATE_DIM], int m, int n, int p) {
memset(C, 0, sizeof(float)*m*p); // 先把结果矩阵清零
for(int i=0; i<m; i++) {
for(int j=0; j<p; j++) {
for(int k=0; k<n; k++) {
C[i][j] += A[i][k] * B[k][j]; // 按规则相乘累加
}
}
}
}
// 矩阵转置:B = A^T(A是m×n矩阵,结果B是n×m矩阵)
void matrix_transpose(float A[][STATE_DIM], float B[][STATE_DIM], int m, int n) {
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
B[j][i] = A[i][j]; // 行变列、列变行
}
}
}
// 2×2矩阵求逆:B = A^-1(专门针对2×2矩阵,简单高效)
void matrix_inv_2x2(float A[2][2], float B[2][2]) {
float det = A[0][0]*A[1][1] - A[0][1]*A[1][0]; // 计算行列式
if(fabs(det) < 1e-6) return; // 行列式太小,矩阵奇异,没法求逆
float inv_det = 1.0f / det; // 行列式的倒数
// 按2×2矩阵求逆公式计算
B[0][0] = A[1][1] * inv_det;
B[0][1] = -A[0][1] * inv_det;
B[1][0] = -A[1][0] * inv_det;
B[1][1] = A[0][0] * inv_det;
}
4. EKF初始化(给系统“开个好头”)
void EKF_Init(EKF_TypeDef *ekf) {
// 初始状态:默认转子位置0,转速0(可以根据实际情况调整)
ekf->x[0] = 0.0f; // theta:转子位置初始值
ekf->x[1] = 0.0f; // omega:转速初始值
// 初始协方差矩阵:刚开始不确定性大,对角线设为1.0(靠谱分低)
ekf->P[0][0] = 1.0f; ekf->P[0][1] = 0.0f;
ekf->P[1][0] = 0.0f; ekf->P[1][1] = 1.0f;
// 过程噪声协方差:根据系统特性调整,数值越小越信任系统
ekf->Q[0][0] = 1e-4f; ekf->Q[0][1] = 0.0f;
ekf->Q[1][0] = 0.0f; ekf->Q[1][1] = 1e-3f;
// 观测噪声协方差:根据传感器精度调整,数值越小越信任传感器
ekf->R[0][0] = 1e-3f; ekf->R[0][1] = 0.0f;
ekf->R[1][0] = 0.0f; ekf->R[1][1] = 1e-3f;
}
5. 状态方程模型(实现“猜测逻辑”)
// 输入:上一次的状态x_prev、控制信号u、采样周期Ts;输出:预测的状态x_pred
void EKF_State_Model(float x_prev[], float u[], float x_pred[], float Ts) {
// 预测转子位置:上一次位置 + 上一次转速 × 采样周期
x_pred[0] = x_prev[0] + x_prev[1] * Ts;
// 预测转速:上一次转速 + (电磁转矩Te - 负载转矩TL)/ 转动惯量J × 采样周期
// 简化处理:假设负载转矩TL=0,电磁转矩Te设为1.0(实际要根据电机模型计算)
float Te = 1.0f; // 电磁转矩(示例值,需替换为实际计算逻辑)
float J = 0.01f; // 电机转动惯量(根据实际电机参数调整)
x_pred[1] = x_prev[1] + (Te / J) * Ts;
}
6. 计算状态雅可比矩阵F(“翻译官1上岗”)
void EKF_Calc_F(float x[], float Ts, float F[][STATE_DIM]) {
// F是df/dx,也就是状态方程对状态向量的偏导数矩阵
// 第一行:位置对位置的偏导=1,位置对转速的偏导=Ts
F[0][0] = 1.0f; F[0][1] = Ts;
// 第二行:转速对位置的偏导=0(简化模型),转速对转速的偏导=1
F[1][0] = 0.0f; F[1][1] = 1.0f;
}
7. 观测方程模型(实现“预测观测值”)
// 输入:预测的状态x_pred、控制信号u;输出:预测的观测值z_pred
void EKF_Observe_Model(float x_pred[], float u[], float z_pred[]) {
float theta = x_pred[0]; // 预测的转子位置
float U_d = u[0], U_q = u[1]; // 控制电压U_d、U_q
// 电机参数:根据实际电机调整(定子电阻、电感、反电动势系数)
float R = 0.5f; // 定子电阻
float L_d = 0.001f; // d轴电感
float L_q = 0.001f; // q轴电感
float Ke = 0.1f; // 反电动势系数
// 简化的电流观测模型(实际需根据PMSM电流方程迭代计算)
z_pred[0] = (U_d) / (R + L_d / Ts); // 预测d轴电流I_d
z_pred[1] = (U_q - Ke * x_pred[1]) / (R + L_q / Ts); // 预测q轴电流I_q
}
8. 计算观测雅可比矩阵H(“翻译官2上岗”)
void EKF_Calc_H(float x_pred[], float H[][STATE_DIM]) {
// H是dh/dx,也就是观测方程对状态向量的偏导数矩阵
// 简化模型:I_d对位置和转速都没偏导,所以前两行都是0
H[0][0] = 0.0f; H[0][1] = 0.0f;
// I_q对位置偏导=0,对转速偏导根据公式计算(示例值,需按实际模型推导)
H[1][0] = 0.0f; H[1][1] = -0.1f / (0.5f + 0.001f / 1e-4f);
}
9. EKF主函数(核心“闯关流程”)
// 输入:EKF结构体指针、控制信号u、观测值z、采样周期Ts;输出:更新后的状态和协方差
void EKF_Update(EKF_TypeDef *ekf, float u[], float z[], float Ts) {
// -------------------------- 1. 状态预测:猜一猜当前状态 --------------------------
EKF_State_Model(ekf->x, u, ekf->x_pred, Ts);
// -------------------------- 2. 协方差预测:算猜测的靠谱分 --------------------------
EKF_Calc_F(ekf->x, Ts, ekf->F); // 计算状态雅可比矩阵F
float F_T[STATE_DIM][STATE_DIM];
matrix_transpose(ekf->F, F_T, STATE_DIM, STATE_DIM); // 求F的转置F_T
float F_P[STATE_DIM][STATE_DIM];
matrix_mult(ekf->F, ekf->P, F_P, STATE_DIM, STATE_DIM, STATE_DIM); // F*P
matrix_mult(F_P, F_T, ekf->P_pred, STATE_DIM, STATE_DIM, STATE_DIM); // F*P*F_T
// 加上过程噪声Q,得到预测协方差P_pred
for(int i=0; i<STATE_DIM; i++) {
for(int j=0; j<STATE_DIM; j++) {
ekf->P_pred[i][j] += ekf->Q[i][j];
}
}
// -------------------------- 3. 计算卡尔曼增益K:确定权重 --------------------------
EKF_Calc_H(ekf->x_pred, ekf->H); // 计算观测雅可比矩阵H
float H_T[OBSERVE_DIM][STATE_DIM];
matrix_transpose(ekf->H, H_T, OBSERVE_DIM, STATE_DIM); // 求H的转置H_T
float H_P[OBSERVE_DIM][STATE_DIM];
matrix_mult(ekf->H, ekf->P_pred, H_P, OBSERVE_DIM, STATE_DIM, STATE_DIM); // H*P_pred
float H_P_Ht[OBSERVE_DIM][OBSERVE_DIM];
matrix_mult(H_P, H_T, H_P_Ht, OBSERVE_DIM, STATE_DIM, OBSERVE_DIM); // H*P_pred*H_T
// 加上观测噪声R
for(int i=0; i<OBSERVE_DIM; i++) {
for(int j=0; j<OBSERVE_DIM; j++) {
H_P_Ht[i][j] += ekf->R[i][j];
}
}
// 求(H*P_pred*H_T + R)的逆矩阵
float H_P_Ht_inv[OBSERVE_DIM][OBSERVE_DIM];
matrix_inv_2x2(H_P_Ht, H_P_Ht_inv);
// 计算卡尔曼增益K = P_pred * H_T * 逆矩阵
float P_Ht[STATE_DIM][OBSERVE_DIM];
matrix_mult(ekf->P_pred, H_T, P_Ht, STATE_DIM, STATE_DIM, OBSERVE_DIM);
matrix_mult(P_Ht, H_P_Ht_inv, ekf->K, STATE_DIM, OBSERVE_DIM, OBSERVE_DIM);
// -------------------------- 4. 状态更新:修正猜测,得到精准结果 --------------------------
float z_pred[OBSERVE_DIM];
EKF_Observe_Model(ekf->x_pred, u, z_pred); // 计算预测观测值z_pred
float z_error[OBSERVE_DIM];
for(int i=0; i<OBSERVE_DIM; i++) {
z_error[i] = z[i] - z_pred[i]; // 计算观测残差(实测值 - 预测观测值)
}
// 修正状态:x^+ = x^- + K*(z - h(x^-))
for(int i=0; i<STATE_DIM; i++) {
ekf->x[i] = ekf->x_pred[i];
for(int j=0; j<OBSERVE_DIM; j++) {
ekf->x[i] += ekf->K[i][j] * z_error[j];
}
}
// -------------------------- 5. 协方差更新:更新靠谱分 --------------------------
float I_KH[STATE_DIM][STATE_DIM];
float K_H[STATE_DIM][STATE_DIM];
matrix_mult(ekf->K, ekf->H, K_H, STATE_DIM, OBSERVE_DIM, STATE_DIM); // K*H
// 计算I - K*H(I是单位矩阵,对角线为1,其余为0)
for(int i=0; i<STATE_DIM; i++) {
for(int j=0; j<STATE_DIM; j++) {
I_KH[i][j] = (i==j ? 1.0f : 0.0f) - K_H[i][j];
}
}
// 更新协方差:P^+ = (I - K*H)*P^-
matrix_mult(I_KH, ekf->P_pred, ekf->P, STATE_DIM, STATE_DIM, STATE_DIM);
}
10. 使用示例(怎么调用这个EKF?)
int main() {
EKF_TypeDef ekf; // 定义EKF结构体变量
EKF_Init(&ekf); // 初始化EKF
float Ts = 1e-4f; // 采样周期:100微秒(根据实际系统调整)
float u[INPUT_DIM] = {0.0f, 1.0f}; // 控制电压:U_d=0V,U_q=1V
float z[OBSERVE_DIM] = {0.1f, 2.0f};// 传感器实测电流:I_d=0.1A,I_q=2.0A
while(1) { // 循环迭代,持续滤波
EKF_Update(&ekf, u, z, Ts); // 调用EKF主函数更新
float theta_est = ekf.x[0]; // 得到估算的转子位置
float omega_est = ekf.x[1]; // 得到估算的转速
// 这里可以添加后续处理代码(比如电机控制、数据输出等)
}
}
五、关键注意事项:避开这些“坑”,滤波效果翻倍
- 雅可比矩阵是“命根子”:EKF的线性化全靠它,一定要严格根据状态方程和观测方程推导,瞎写的话滤波会“发疯”,结果完全不靠谱;
- Q/R参数要“精调”:Q增大→系统跟踪速度变快,但抗噪声能力变弱(容易被干扰带偏);R增大→更信任预测值,不信任观测值(传感器不准时可以调大);反之则更信任观测值,多试几次找到平衡点就行;
- 数值稳定性要注意:矩阵求逆时一定要检测行列式(避免分母为0),协方差矩阵要保持正定(简单说就是对角线数值不能太小或为负),不然会出现计算溢出;
- 模型别瞎简化:实际应用中,状态方程和观测方程要贴合系统特性(比如电机模型要用上精准的电阻、电感参数),简化太多会导致滤波精度下降。
六、EKF的“用武之地”:不止于电机控制
EKF可不是只用来估算电机位置的“偏科生”,它的应用范围广到你想象不到:
- 无人机姿态估计:把IMU(惯性测量单元)的加速度、角速度数据和GPS数据融合,让无人机飞得更稳、不迷路;
- 目标跟踪:雷达、视觉传感器的数据融合,不管目标跑多快、变向多灵活,都能牢牢锁定;
- 机器人导航:结合激光雷达、摄像头数据,让机器人在复杂环境中精准定位、避障;
- 电池状态估算:预测电池的剩余电量(SOC)、健康状态(SOH),让新能源设备续航更靠谱。
其实EKF的核心逻辑一点都不复杂,就是“猜-算-修-更”的循环:先根据历史数据猜一猜,再算一算猜得靠谱不,然后用实测数据修正猜测,最后更新参数为下一轮做准备。只要把状态方程、观测方程和雅可比矩阵根据实际场景改一改,这个“万能滤波工具”就能为你的项目打工!
看完这篇,是不是觉得EKF再也不是遥不可及的“学术大佬”了?赶紧把代码抄过去,结合自己的项目改一改,体验一把“精准滤波”的快乐吧!
更多推荐
所有评论(0)