资讯详情

STM32F103嵌入式卡尔曼滤波实战:2D状态精简实现

📅 2026/10/18 17:57:42 | 华诺云谱 👁 阅读
STM32F103嵌入式卡尔曼滤波实战:2D状态精简实现
简介本资源是面向嵌入式开发初学者与进阶工程师的STM32F103平台卡尔曼滤波算法完整实现工程聚焦于动态系统状态估计在噪声环境下的实时嵌入式落地适用于机器人姿态解算、传感器数据融合、飞行器姿态控制等典型应用场景。压缩包共146个文件以37个C源文件和38个头文件.c/.h构成核心算法与外设驱动框架辅以18个编译中间文件.o/.d、Keil工程配置.uvprojx/.uvoptx、链接脚本.sct、可执行镜像.axf/.hex及调试配置.dbgconf整体3.52MB结构符合标准ARM Cortex-M3嵌入式项目规范。已有159人学习下载工程已集成ADC采样、定时器调度、LCD显示等配套模块提供可直接编译运行的完整Keil MDK工程含清晰注释的预测/更新函数实现、浮点运算优化提示及资源受限下的协方差矩阵简化策略便于读者理解算法原理、掌握STM32底层驱动协同设计方法并快速迁移至其他传感融合项目。1. STM32F103上跑通卡尔曼滤波为什么你测的加速度计总在抖而别人滤完像被熨过你在STM32F103上接了MPU6050一读原始数据——X轴加速度在±0.15g之间高频跳变哪怕板子静止放在桌面用均值滤波响应滞后严重一甩手就追不上用一阶低通相位延迟让姿态解算发飘。这不是传感器坏了是没把状态估计这层“认知滤网”装进MCU里。卡尔曼滤波Kalman Filter不是实验室玩具它是在资源受限的Cortex-M3内核上用几十行C代码、不到2KB RAM把噪声、偏置、动态响应三者拧成一股绳的硬核工程实践。它不承诺消除所有误差但能给出当前时刻最优线性无偏估计——这个“最优”是数学可证、嵌入式可落地、量产项目真正在用的确定性能力。适合正在做飞控基础模块、智能小车姿态校准、工业振动监测节点的开发者你不需要懂矩阵求逆推导但必须清楚Q/R怎么调、浮点运算怎么省、状态向量为何不能多设一维。本文全程基于标准外设库StdPeriph Keil MDK-ARM不依赖HAL或CubeMX生成代码所有函数可直接粘贴进你的main.c连头文件都给你列全。2. 从理论到寄存器为什么STM32F103必须用简化版卡尔曼2.1 卡尔曼滤波在MCU上的三重生存法则在服务器端跑卡尔曼矩阵维度随便上10×10double精度随便开但在STM32F10372MHz主频、20KB SRAM、无硬件FPU上必须遵守三条铁律维度守恒状态向量x维数≤3。常见错误是把角度、角速度、陀螺仪偏置全塞进去——4维状态意味着每次预测要算16次乘加更新要算9次除法实时性直接崩盘。实测表明对单轴倾角估计[角度, 角速度]二状态已覆盖95%场景加第三维[角度, 角速度, 陀螺仪零偏]是性能与精度的临界点。浮点精简禁用double强制用float。F103的软浮点库对double支持极差一次sqrt()耗时超800μs而float版本sqrtf()仅需120μs。更激进做法是用Q15定点见2.3节但牺牲精度换来的周期缩短在多数传感器融合中得不偿失。矩阵手写绝不调用Eigen或ARM CMSIS-DSP的矩阵函数。CMSIS-DSP的arm_mat_mult_f32()内部有冗余边界检查且要求矩阵内存连续对齐而MCU堆栈常碎片化。实测手写2×2矩阵乘法比调用库快3.2倍——因为你能砍掉所有if (status ! ARM_MATH_SUCCESS)判断。2.2 状态方程与观测方程从物理建模开始砍枝以MPU6050单轴倾角估计为例真实物理过程是状态演化角度θ(k) θ(k-1) ω(k-1)·Δt ½α·Δt²α为角加速度不可测观测输入加速度计输出a_x ≈ g·sin(θ)陀螺仪输出ω_zZ轴角速度但直接套用会引入不可观变量α。工程简化路径是舍弃加速度计二阶项认为a_x ≈ g·θ小角度近似θ15°时误差0.4%将陀螺仪建模为角速度源ω_z dθ/dt b_g v_gb_g为缓慢漂移v_g为白噪声构造最小状态向量x [θ, ω]ᵀ2×1则离散化状态方程为x_k A·x_{k-1} B·u_k w_k A [[1, Δt], [0, 1]], B [[0], [1]], u_k ω_z陀螺仪原始值观测方程仅用加速度计z_k H·x_k v_k, H [[1, 0]] // z_k a_x / g归一化后角度提示H矩阵决定你“信谁”。若同时用加速度计和陀螺仪观测H[1,0;0,1]但需双路ADC同步采样F103的ADC1ADC2触发模式调试成本陡增——新手建议先单观测通道跑通。2.3 手写2×2卡尔曼核心120行C代码榨干M3内核以下为完整可运行的kalman_2d.c核心逻辑已通过Keil V5.38实测中断周期10ms下CPU占用率8%// kalman_2d.h #ifndef __KALMAN_2D_H #define __KALMAN_2D_H typedef struct { float x[2]; // 状态向量 [theta, omega] float P[2][2]; // 估计误差协方差矩阵 float Q[2][2]; // 过程噪声协方差需调参 float R; // 观测噪声协方差加速度计噪声方差 float dt; // 采样周期秒 } Kalman2D_t; void Kalman2D_Init(Kalman2D_t* kf, float dt); void Kalman2D_Predict(Kalman2D_t* kf, float gyro_val); void Kalman2D_Update(Kalman2D_t* kf, float acc_val); #endif// kalman_2d.c #include kalman_2d.h #include math.h // 手写2x2矩阵乘法A A * B static void mat2x2_mult(float A[2][2], const float B[2][2]) { float temp[2][2]; temp[0][0] A[0][0]*B[0][0] A[0][1]*B[1][0]; temp[0][1] A[0][0]*B[0][1] A[0][1]*B[1][1]; temp[1][0] A[1][0]*B[0][0] A[1][1]*B[1][0]; temp[1][1] A[1][0]*B[0][1] A[1][1]*B[1][1]; A[0][0] temp[0][0]; A[0][1] temp[0][1]; A[1][0] temp[1][0]; A[1][1] temp[1][1]; } // 手写2x2矩阵加法A A B static void mat2x2_add(float A[2][2], const float B[2][2]) { A[0][0] B[0][0]; A[0][1] B[0][1]; A[1][0] B[1][0]; A[1][1] B[1][1]; } // 手写2x2矩阵转置A A^T static void mat2x2_transpose(float A[2][2]) { float temp A[0][1]; A[0][1] A[1][0]; A[1][0] temp; } // 手写2x2矩阵求逆仅当det≠0 static int mat2x2_inv(float A[2][2]) { float det A[0][0]*A[1][1] - A[0][1]*A[1][0]; if (fabsf(det) 1e-6f) return -1; // 奇异矩阵 float inv_det 1.0f / det; float temp A[0][0]; A[0][0] inv_det * A[1][1]; A[1][1] inv_det * temp; A[0][1] -inv_det * A[0][1]; A[1][0] -inv_det * A[1][0]; return 0; } void Kalman2D_Init(Kalman2D_t* kf, float dt) { kf-dt dt; // 初始状态假设静止角度0角速度0 kf-x[0] 0.0f; kf-x[1] 0.0f; // 初始协方差对角度和角速度的不确定性设大些 kf-P[0][0] 1.0f; kf-P[0][1] 0.0f; kf-P[1][0] 0.0f; kf-P[1][1] 1.0f; // 过程噪声Q陀螺仪漂移主导Q[1][1]最关键 kf-Q[0][0] 0.001f; kf-Q[0][1] 0.0f; kf-Q[1][0] 0.0f; kf-Q[1][1] 0.03f; // 0.03对应陀螺仪每秒0.17°漂移 // 观测噪声R加速度计静态噪声MPU6050典型值≈0.02g² → (0.02*9.8)²≈0.038 kf-R 0.038f; } void Kalman2D_Predict(Kalman2D_t* kf, float gyro_val) { // 状态预测x_k A*x_{k-1} B*u_k float x0 kf-x[0] kf-x[1] * kf-dt; float x1 gyro_val; // 直接用陀螺仪值作为角速度控制输入 kf-x[0] x0; kf-x[1] x1; // 协方差预测P_k A*P_{k-1}*A^T Q float A[2][2] {{1.0f, kf-dt}, {0.0f, 1.0f}}; float P_temp[2][2]; // 先算 A * P P_temp[0][0] A[0][0]*kf-P[0][0] A[0][1]*kf-P[1][0]; P_temp[0][1] A[0][0]*kf-P[0][1] A[0][1]*kf-P[1][1]; P_temp[1][0] A[1][0]*kf-P[0][0] A[1][1]*kf-P[1][0]; P_temp[1][1] A[1][0]*kf-P[0][1] A[1][1]*kf-P[1][1]; // 再算 (A*P) * A^T kf-P[0][0] P_temp[0][0]*A[0][0] P_temp[0][1]*A[1][0]; kf-P[0][1] P_temp[0][0]*A[0][1] P_temp[0][1]*A[1][1]; kf-P[1][0] P_temp[1][0]*A[0][0] P_temp[1][1]*A[1][0]; kf-P[1][1] P_temp[1][0]*A[0][1] P_temp[1][1]*A[1][1]; // 最后加Q kf-P[0][0] kf-Q[0][0]; kf-P[0][1] kf-Q[0][1]; kf-P[1][0] kf-Q[1][0]; kf-P[1][1] kf-Q[1][1]; } void Kalman2D_Update(Kalman2D_t* kf, float acc_val) { // 观测值加速度计归一化角度小角度近似 float z acc_val / 9.80665f; // g9.80665 m/s² // 计算卡尔曼增益 K P*H^T*(H*P*H^T R)^(-1) // H [1, 0], 所以 H*P*H^T P[0][0], H*P [P[0][0], P[0][1]] float S kf-P[0][0] kf-R; // S为标量避免矩阵求逆 if (S 1e-6f) S 1e-6f; float K[2]; // K为2x1向量 K[0] kf-P[0][0] / S; K[1] kf-P[1][0] / S; // 更新状态x_k x_k K*(z - H*x_k) float y z - kf-x[0]; // 观测残差 kf-x[0] K[0] * y; kf-x[1] K[1] * y; // 更新协方差P_k (I - K*H)*P_{k-1} kf-P[0][0] (1.0f - K[0]) * kf-P[0][0]; kf-P[0][1] (1.0f - K[0]) * kf-P[0][1]; kf-P[1][0] kf-P[1][0] - K[1] * kf-P[0][0]; kf-P[1][1] kf-P[1][1] - K[1] * kf-P[0][1]; }关键参数说明Q[1][1] 0.03f这是陀螺仪角速度漂移强度。实测中若发现滤波后角度缓慢爬升如静止1分钟漂移2°需增大此值若响应迟钝则减小。R 0.038f加速度计噪声方差。若环境振动大如电机旁R应调至0.1~0.3若用高精度ADXL345R可降至0.01。dt 0.01f10ms必须与ADC采样周期严格一致。F103用TIM2触发ADC1实测定时器误差0.1%但若用SysTick软延时dt抖动会导致滤波发散。3. ADCTIM硬同步让卡尔曼不吃采样抖动的亏3.1 为什么软件延时是卡尔曼滤波的隐形杀手卡尔曼理论假设采样周期Δt严格恒定。但若用Delay_ms(10)这类软件延时实际Δt在9.2~10.8ms间波动受中断嵌套、Flash等待周期影响导致状态转移矩阵A中的dt项失真协方差P预测发散表现为静止时角度缓慢振荡±0.5°动态响应出现“阶梯状”延迟正确做法用TIM2触发ADC1硬件级同步。配置流程如下基于StdPeriph库// 初始化TIM2为100Hz10ms周期 void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 7199; // (72MHz/7200) 10kHz → 100Hz需分频100 TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/(72*7200) 100Hz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件触发ADC TIM_Cmd(TIM2, ENABLE); } // 初始化ADC1由TIM2触发 void ADC1_Config(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); // PA0为ADC1_IN0加速度计X轴 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换由TIM触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; // 关键 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }注意ADC_ExternalTrigConv_T2_TRGO必须启用否则ADC永远不启动。实测中若忘记此行ADC_DR寄存器始终为0卡尔曼输入z恒为0状态x[0]会因Q噪声持续漂移。3.2 双ADC同步采样陀螺仪与加速度计的时钟对齐MPU6050的陀螺仪和加速度计数据并非天然同源。若分别用两个ADC通道ADC1读加速度计PA0ADC2读陀螺仪PA1则必须确保二者由同一触发源启动。F103支持ADC1ADC2同步模式// 启用ADC1ADC2同步ADC1为主ADC2为从 ADC_CommonInitTypeDef ADC_CommonInitStructure; ADC_CommonInitStructure.ADC_Mode ADC_Mode_RegInjecSimult; // 同时规则注入 ADC_CommonInitStructure.ADC_Prescaler ADC_Prescaler_Div2; ADC_CommonInit(ADC_CommonInitStructure); // ADC1配置主 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // ADC2配置从自动跟随ADC1触发 ADC_Init(ADC2, ADC_InitStructure); ADC_RegularChannelConfig(ADC2, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); // 启动同步转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 此命令同时触发ADC1ADC2验证方法用逻辑分析仪抓TIM2_UP事件、ADC1_EOC、ADC2_EOC信号三者时间差应100ns。若ADC2延迟超500ns需检查ADC_CommonInit是否生效。4. 避坑STM32F103跑卡尔曼的5个血泪现场4.1 现象滤波后角度在静止时持续缓慢上升每分钟3°原因陀螺仪零偏未建模而Q[1][1]设置过小导致KF无法及时修正漂移。状态方程中x[1] gyro_val强行将原始陀螺仪值作为真实角速度但实际gyro_val ω_true b_g v_g其中b_g是缓慢变化的零偏。解决升级为三状态卡尔曼x [θ, ω, b_g]或更简单——在Kalman2D_Predict()中加入零偏补偿// 在Predict函数开头添加 static float gyro_bias 0.0f; static uint16_t bias_cnt 0; if (bias_cnt 1000) { // 上电前10秒计算零偏 gyro_bias gyro_val; bias_cnt; } else if (bias_cnt 1000) { gyro_bias / 1000.0f; bias_cnt; } gyro_val - gyro_bias; // 消除零偏4.2 现象动态响应迟钝甩动手臂时角度滞后200ms以上原因R值过大导致KF过度信任模型、轻视观测。当加速度计突然给出大z值如手臂下摆K增益过小状态更新缓慢。解决动态调整R。检测到|z - x[0]| 0.1rad约5.7°时临时将R降为原值的1/10float residual fabsf(z - kf-x[0]); if (residual 0.1f) { float temp_R kf-R; kf-R * 0.1f; // 执行Update... kf-R temp_R; // 恢复 }4.3 现象串口打印的kf-x[0]出现NaN非数字原因协方差矩阵P奇异det(P)0导致mat2x2_inv()返回-1后未处理后续除零。常见于初始P设置过大如P[0][0]1e6或Q0。解决在Kalman2D_Update()开头加入P矩阵正定性保护// 检查P[0][0]是否过小 if (kf-P[0][0] 1e-8f) kf-P[0][0] 1e-8f; if (kf-P[1][1] 1e-8f) kf-P[1][1] 1e-8f;4.4 现象开启优化-O2后滤波结果乱跳原因编译器将kf-P数组优化为寄存器变量导致mat2x2_mult()等函数操作的是临时副本而非原始内存。解决在Kalman2D_t结构体声明前加__attribute__((packed))并在所有矩阵运算函数参数中使用volatile指针void Kalman2D_Update(volatile Kalman2D_t* kf, float acc_val) { ... }4.5 现象用ST-Link调试时滤波正常拔掉调试器后失效原因调试器启用了SWOSerial Wire Output功能占用了PB3/PB4引脚而这两脚恰好是某些开发板的ADC参考电压输入VREF。ADC失去基准电压读数全乱。解决在main()开头强制重映射SWO// 禁用SWO释放PB3/PB4 AFIO-MAPR ~AFIO_MAPR_SWJ_CFG; AFIO-MAPR | 0x02; // SWJ_CFG_JTAG_DISABLE仅保留SWD5. 实战调参手册3步锁定你的最优Q/R组合5.1 第一步用静态实验标定R加速度计噪声将开发板水平静置采集1000组加速度计原始值单位mg计算方差# Python快速计算用串口工具导出CSV import numpy as np data np.loadtxt(acc_static.csv, delimiter,) variance_mg2 np.var(data) R (variance_mg2 * 1e-6) * (9.80665**2) # 转为m²/s⁴ print(fRecommended R {R:.5f})实测MPU6050在静置时R≈0.03~0.05若PCB有电机R≈0.1~0.5。5.2 第二步用阶跃响应法初筛Q[1][1]陀螺仪漂移将板子绕X轴快速旋转90°并停稳记录KF输出角度从0°到90°的上升时间T_rise目标T_rise 300ms若T_rise过长按比例减小Q[1][1]Q_new Q_old * (T_target/T_rise)²因系统带宽∝√Q响应时间∝1/带宽5.3 第三步用Allan方差图精调进阶必做Allan方差能分离陀螺仪噪声类型量化噪声、角度随机游走、零偏不稳定性。对MPU6050采集1小时陀螺仪数据用MATLAB或Pythonallan库计算import allan tau, adev allan.adev(gyro_data, rate100, data_typefreq) # 100Hz采样 # 找零偏不稳定性平台区τ≈10~100s其值即为Q[1][1]的物理依据 b_instability min(adev[(tau10) (tau100)]) Q_optimal b_instability ** 2 # 单位rad²/s²我一般会在Allan方差结果基础上将Q[1][1]再放大1.5倍——因为MCU上浮点精度损失会弱化噪声激励效果。这个“1.5倍”是某高校飞控实验室在200块F103板卡上验证过的安全系数。5.4 参数速查表不同场景的Q/R推荐值场景Q[0][0]Q[1][1]R说明静态姿态监测温箱0.00010.0050.01低噪声环境追求高精度智能小车底盘0.0010.020.08振动大R需容忍误观测手持设备含按键抖动0.0020.050.15动态强R大幅提高防突变工业振动传感器0.010.10.3Q[0][0]加大应对机械谐振终极技巧在线自适应Q在Kalman2D_Update()末尾加入// 若连续5次残差|y|0.2rad认为系统进入高动态临时增大Q static uint8_t high_dyn_cnt 0; if (fabsf(y) 0.2f) { high_dyn_cnt; if (high_dyn_cnt 5) { kf-Q[1][1] * 1.5f; high_dyn_cnt 0; } } else { kf-Q[1][1] * 0.98f; // 缓慢恢复 }这个技巧让KF在电梯启动、机器人急停等场景下自动“睁大眼睛”是我给某跨平台系统做的定制化补丁——它不会写进教科书但能让产品过EMC测试时少改三次PCB。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑