卡尔曼滤波不好懂?单片机非线性滤波天花板,看完直接上手写代码!

你有没有过这种疑惑?无人机在天上翻来覆去却从不迷路,电机没装传感器却能精准转圈圈,甚至雷达盯着高速移动的目标跑,再灵活也不会跟丢——这些看似“开挂”的操作背后,其实都藏着一个叫“扩展卡尔曼滤波(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^k1+,uk1)
翻译:根据上一次的精准结果(x^k−1+\hat{x}_{k-1}^{+}x^k1+)和控制信号(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=FkPk1+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=PkHkT(HkPkHkT+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(zkh(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+=(IKkHk)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=θk1+ωk1Ts+wθωk=ωk1+J1(TeTL)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];  // 得到估算的转速
        // 这里可以添加后续处理代码(比如电机控制、数据输出等)
    }
}

五、关键注意事项:避开这些“坑”,滤波效果翻倍

  1. 雅可比矩阵是“命根子”:EKF的线性化全靠它,一定要严格根据状态方程和观测方程推导,瞎写的话滤波会“发疯”,结果完全不靠谱;
  2. Q/R参数要“精调”:Q增大→系统跟踪速度变快,但抗噪声能力变弱(容易被干扰带偏);R增大→更信任预测值,不信任观测值(传感器不准时可以调大);反之则更信任观测值,多试几次找到平衡点就行;
  3. 数值稳定性要注意:矩阵求逆时一定要检测行列式(避免分母为0),协方差矩阵要保持正定(简单说就是对角线数值不能太小或为负),不然会出现计算溢出;
  4. 模型别瞎简化:实际应用中,状态方程和观测方程要贴合系统特性(比如电机模型要用上精准的电阻、电感参数),简化太多会导致滤波精度下降。

六、EKF的“用武之地”:不止于电机控制

EKF可不是只用来估算电机位置的“偏科生”,它的应用范围广到你想象不到:

  • 无人机姿态估计:把IMU(惯性测量单元)的加速度、角速度数据和GPS数据融合,让无人机飞得更稳、不迷路;
  • 目标跟踪:雷达、视觉传感器的数据融合,不管目标跑多快、变向多灵活,都能牢牢锁定;
  • 机器人导航:结合激光雷达、摄像头数据,让机器人在复杂环境中精准定位、避障;
  • 电池状态估算:预测电池的剩余电量(SOC)、健康状态(SOH),让新能源设备续航更靠谱。

其实EKF的核心逻辑一点都不复杂,就是“猜-算-修-更”的循环:先根据历史数据猜一猜,再算一算猜得靠谱不,然后用实测数据修正猜测,最后更新参数为下一轮做准备。只要把状态方程、观测方程和雅可比矩阵根据实际场景改一改,这个“万能滤波工具”就能为你的项目打工!

看完这篇,是不是觉得EKF再也不是遥不可及的“学术大佬”了?赶紧把代码抄过去,结合自己的项目改一改,体验一把“精准滤波”的快乐吧!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐