如果你用 STM32 接过 MPU6050,大概率见过这样的画面:串口助手把原始加速度和陀螺仪数据打出来,静止放在桌面上,数据依然在上下跳。我第一次做平衡小车的时候,把 ACC_Y 直接当倾角用,结果电机还没启动,数据已经抖得跟心电图一样。后来才知道,传感器不是坏,是没有做滤波。
MPU6050 的原始数据里有真实姿态信息,也混着大量噪声。陀螺仪积分会漂,加速度计会被振动“带偏”,如果不做滤波就进 PID 或者其他控制算法,轻则波形毛糙,重则直接失控。这篇文章整理的是我在 STM32 上做 MPU6050 数据处理的完整思路,针对的是刚把传感器调通、却不知道后续怎么处理的开发者。除了滤波算法的代码,还包括原始数据的含义、硬件层面的噪声预防、零点校准,以及我实际调试中踩过的一堆坑。
1. MPU6050 的原始数据为什么不能直接拿去用
1.1 加速度计和陀螺仪的噪声来源
MPU6050 内部其实集成了一个三轴加速度计和一个三轴陀螺仪,这两个传感器的物理原理完全不同,所以噪声特征也完全不一样。
加速度计测量的是比力,也就是物体受到的加速度和重力加速度的矢量和。它最怕的是高频振动,电机转动、齿轮咬合、车架共振,这些都会叠加到加速度输出上。静止的时候把模块放桌上,串口打印出来的 ACC_X、ACC_Y 也会有 ±0.02g 甚至更大的跳动,这不是传感器坏了,而是环境里细微振动和电气噪声一起混了进来。
陀螺仪测量的是角速度,它相对安静,但问题出在“积分”上。我们要得到角度,必须对角速度做时间积分,哪怕零点只偏了 0.1°/s,积分 10 秒就漂出 1°,时间越长偏得越离谱。这也就是大家常说的“陀螺仪温漂”和“随机游走”,前者由温度变化引起,后者是积分过程中噪声被不断累积的结果。
两者还有个本质矛盾:加速度计长期稳定,短期受振动影响大;陀螺仪短期准确,长期积分会漂。所以滤波的任务,不是简单地把数据弄平滑,而是要想办法取长补短。
1.2 读到的原始值到底是什么
MPU6050 通过 I2C 输出的是一串 16 位有符号整数,范围是 -32768 到 32767,它本身并不是我们熟悉的 “角度” 或者 “g”,而是 ADC 量化之后的 LSB 原始值。
要换算成物理单位,必须对应量程看灵敏度。我常用的是加速度计 ±16g、陀螺仪 ±2000°/s,换算关系如下:
| 传感器 | 量程 | 灵敏度 | 换算公式 |
|---|---|---|---|
| 加速度计 | ±2g | 16384 LSB/g | acc = raw / 16384 |
| 加速度计 | ±4g | 8192 LSB/g | acc = raw / 8192 |
| 加速度计 | ±8g | 4096 LSB/g | acc = raw / 4096 |
| 加速度计 | ±16g | 2048 LSB/g | acc = raw / 2048 |
| 陀螺仪 | ±250°/s | 131 LSB/(°/s) | gyro = raw / 131 |
| 陀螺仪 | ±500°/s | 65.5 LSB/(°/s) | gyro = raw / 65.5 |
| 陀螺仪 | ±1000°/s | 32.8 LSB/(°/s) | gyro = raw / 32.8 |
| 陀螺仪 | ±2000°/s | 16.4 LSB/(°/s) | gyro = raw / 16.4 |
举个例子,加速度计输出 raw 值是 2048,在 ±16g 量程下就是 2048 / 2048 = 1.0g,正好对应静止时 Z 轴感受到的重力。陀螺仪 raw 输出是 164,在 ±2000°/s 量程下就是 164 / 16.4 = 10°/s,代表当前绕某个轴的角速度是每秒 10 度。
很多新手直接拿 raw 值去做 PID 或者互补滤波,参数怎么调都不对,就是因为单位没有统一。我建议一上来就先把原始值换算成 g 和 °/s,后面所有算法都基于物理单位,调参的时候脑子才清楚。
2. 滤波算法怎么选:滑动平均、一阶低通还是互补滤波
2.1 先给滤波定个“目标”:你要的是平滑还是实时
选滤波算法之前,先想清楚一个核心矛盾:滤波强度越高,数据越平滑,但延迟也越大。做静态角度测量,比如电子水平仪,你可以用很大的滤波窗口,延迟几百毫秒都无所谓。但做平衡小车、飞控、云台,这些系统实时性要求极高,滤波延迟太大就会让控制环“反应迟钝”,严重时直接震荡。
我一般这样划分需求:
- 静态采集或者离线分析:滑动窗口滤波 + 一阶低通都可以,追求极致的平滑。
- 控制周期 100Hz 以上的实时系统:一阶低通或者互补滤波,延迟控制在 10ms 级别。
- 需要完整姿态角:互补滤波或者 DMP,直接输出横滚、俯仰、偏航。
- 高精度导航或者对相位要求苛刻:卡尔曼滤波,但前提是你真能调好它。
2.2 三种常用滤波器方案的对比
我用的最多的就是滑动窗口滤波、一阶低通滤波和互补滤波,三者各有明显特点:
| 算法 | 计算量 | 内存占用 | 延迟 | 特点 |
|---|---|---|---|---|
| 滑动窗口(滑动平均) | 低 | 需要 N 个样本缓冲 | 约 (N-1)/2 个采样周期 | 简单粗暴,削峰效果好,适合预处理 |
| 一阶低通 | 极低 | 只需要两个变量 | 取决于 alpha 和采样周期 | 轻量、实时性好,适合嵌入式实时系统 |
| 互补滤波 | 低 | 两三个角度变量 | 取决于时间常数 | 融合加速度计和陀螺仪,适合姿态解算 |
| 卡尔曼滤波 | 高 | 矩阵变量多 | 取决于模型参数 | 理论上最优,但调参坑很深 |
滑动窗口滤波的思路是缓存最近的 N 个数据,每次取平均作为输出。N 越大越平滑,但相移也越大。它本质上是一个有限冲激响应低通滤波器,对周期性噪声和高频毛刺非常有效。C 语言里这个东西还能同时拿去处理 ADC 采样值,比如稳压电源的电流采样、热敏电阻的温度读取,都是一回事。
一阶低通滤波本质上是模拟 RC 低通滤波器的数字化版本,公式是:
y[n] = α * x[n] + (1 - α) * y[n-1]
其中 α 是新数据的权重。α 越大,跟随越快,滤波越弱;α 越小,数据越平滑,延迟越大。这个滤波器在 STM32 上跑起来只占极少资源,实时性很好。
互补滤波则是专门给姿态解算设计的,它把陀螺仪积分出来的角度经过高通,把加速度计算出来的角度经过低通,两个结果加起来。陀螺仪负责动态部分,加速度计负责长期校准,互相弥补。
2.3 为什么卡尔曼滤波不是万能的
网上很多教程一上来就贴卡尔曼滤波代码,好像不用卡尔曼就不够专业。我五年前第一次做飞控也是这个心态,去网上找了一版卡尔曼滤波代码直接往工程里一丢,结果波形不但没变好,反而连续发散,角度直接跑到几万度。
卡尔曼滤波本身没有错,但它的效果严重依赖系统模型和噪声协方差矩阵 Q、R 的取值。MPU6050 的姿态模型有加速度计和陀螺仪两个输入,实际模型比单变量低通复杂得多,Q 和 R 设得不对,滤波器就会“自信地给出错误结果”。而且它的计算量在 MCU 上也不算小,STM32F103 跑一次标准卡尔曼大概要几十微秒,如果控制周期很紧张,还会挤压其他任务的执行时间。
我现在的习惯是:除非做产品级惯性导航,否则先上互补滤波,效果足够,代码量还少。真到了卡尔曼不可替代的时候,再回过来研究数学模型也不迟。
3. 先从硬件和寄存器把“数据源”弄干净
3.1 接线和供电:很多噪声是硬件带来的
软件写得再好,硬件一塌糊涂,滤波也救不回来。MPU6050 模块常见的是 3.3V 供电,但很多电机驱动板上只有 5V,直接供电可能会让模块工作不稳定。我通常这样做:
- VCC 接 3.3V,如果板上没有 3.3V,就用 AMS1117 之类的稳压芯片单独转一路。
- SDA 和 SCL 都要接上拉电阻,模块上一般已经有 10kΩ 上拉,如果你用杜邦线延长,最好再并联 4.7kΩ 上拉,保证上升沿足够快。
- 靠近模块的 VCC 和 GND 之间加一个 100nF 陶瓷电容,再并一个 10uF 电解电容,用来滤掉电源里的高频纹波。
- AD0 引脚决定 I2C 地址,接 GND 时地址是 0x68,接 3.3V 时是 0x69。多模块共用总线的时候用来区分设备,单模块默认接 GND。
我踩过最典型的坑是:MPU6050 和电机共用一个电源,电机一转,I2C 数据就出现全 0xFF 或者 CRC 错误。后来把供电分开,再在模块旁边补了滤波电容,问题立刻消失。这个现象看起来像是软件 bug,实际上电源噪声把通信波形彻底破坏了。
3.2 关键寄存器和初始化顺序
MPU6050 的寄存器分布在 0x00 到 0x75,我只需要关注几个关键寄存器,用 STM32 的 HAL 库操作 I2C 就行。初始化顺序建议是:先唤醒芯片,再配置采样率,然后设量程,最后启用数字低通滤波器。
下面是我在 STM32F103 上用的初始化代码,HAL 库版本:
#define MPU6050_ADDR 0x68 #define MPU6050_DEV_ADDR (MPU6050_ADDR << 1) uint8_t mpu6050_init(void) { uint8_t val = 0; // 1. 电源管理寄存器,唤醒芯片,关闭睡眠模式 val = 0x00; HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x6B, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); // 2. 采样率分频器,陀螺仪输出频率 1kHz / (1 + 0) = 1kHz val = 0x00; HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x19, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); // 3. CONFIG 寄存器,启用 DLPF,设置数字低通带宽约 44Hz val = 0x03; HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x1A, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); // 4. 陀螺仪量程 ±2000 dps,对应灵敏度 16.4 LSB/(dps) val = 0x18; HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x1B, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); // 5. 加速度计量程 ±16g,对应灵敏度 2048 LSB/g val = 0x18; HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x1C, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); return 0; }这里有个容易被忽略的点:CONFIG 寄存器的 DLPF 配置。启用 DLPF 之后,加速度计和陀螺仪的输出都会经过内置的低通滤波器,可以滤掉一部分高频噪声。MPU6050 的 DLPF 带宽可选很多档位,0x03 对应大概 44Hz,比较适合平衡车和云台这类动态场景。如果你后面还要做软件滤波,可以把 DLPF 档位调高一点,软件滤波再来承担主要降噪任务。
寄存器配置完之后,还要确认数据读取地址。加速度计和陀螺仪的数据从 0x3B 开始连续排列,一次 I2C 突发读 14 个字节就能把三轴加速度、温度、三轴陀螺仪全部读回来。我习惯用下面的方式读取并转换:
void mpu6050_read_raw(int16_t *acc_x, int16_t *acc_y, int16_t *acc_z, int16_t *gyro_x, int16_t *gyro_y, int16_t *gyro_z) { uint8_t buf[14]; HAL_I2C_Mem_Read(&hi2c1, MPU6050_DEV_ADDR, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); *acc_x = (int16_t)((buf[0] << 8) | buf[1]); *acc_y = (int16_t)((buf[2] << 8) | buf[3]); *acc_z = (int16_t)((buf[4] << 8) | buf[5]); // buf[6] / buf[7] 是温度寄存器 *gyro_x = (int16_t)((buf[8] << 8) | buf[9]); *gyro_y = (int16_t)((buf[10] << 8) | buf[11]); *gyro_z = (int16_t)((buf[12] << 8) | buf[13]); }读取之后,再根据量程换算成物理量。比如陀螺仪量程是 ±2000°/s,那就 raw / 16.4f;加速度计量程是 ±16g,那就 raw / 2048.0f。单位不统一,后面滤波代码写得再漂亮,参数也调不明白。
3.3 零点校准不能省
我在做第一次平衡小车的时候就吃过亏,陀螺仪静止时原始输出居然是几十甚至上百,直接积分算角度,一会儿就歪到一边去了。这个偏差来源于芯片制造误差和贴片应力,每颗芯片都不一样,必须做零点校准。
最简单的校准方法是上电后让模块保持静止,连续采样 200 次,分别求三个轴加速度和三个轴陀螺仪的平均值,然后存成偏移量。正常工作时,用每次读到的原始值减去这个偏移量,再除以灵敏度,就得到“校准后”的物理值。
typedef struct { float acc_bias[3]; float gyro_bias[3]; } mpu6050_bias_t; mpu6050_bias_t mpu6050_calibrate(void) { mpu6050_bias_t bias = {0}; int16_t ax, ay, az, gx, gy, gz; for (int i = 0; i < 200; i++) { mpu6050_read_raw(&ax, &ay, &az, &gx, &gy, &gz); bias.acc_bias[0] += ax; bias.acc_bias[1] += ay; bias.acc_bias[2] += az; bias.gyro_bias[0] += gx; bias.gyro_bias[1] += gy; bias.gyro_bias[2] += gz; HAL_Delay(5); } for (int i = 0; i < 3; i++) { bias.acc_bias[i] /= 200.0f; bias.gyro_bias[i] /= 200.0f; } return bias; }加速度计在静止时,理论上 X、Y 轴的输出应该接近 0,Z 轴接近 1g,但现实中模块不可能完全水平,所以加速度计偏移只做减法,不强行把 X、Y 拉成 0。校准完成之后,陀螺仪静止输出应该在 0 附近跳动,跳动幅度就是噪声底,后续滤波强度就是针对这个跳动幅度来定的。
4. STM32 上三种滤波代码的落地实测
4.1 滑动窗口滤波:简单直接地削峰
滑动窗口滤波也叫滑动平均滤波,实现思路非常直白:把最近 N 次采样值放进一个环形的缓冲区,每次都输出这 N 个值的平均值。它的作用是让突变的毛刺被周围的数据“稀释”掉,适合处理周期性噪声比较大的信号。
我的代码里用一个环形数组实现,避免每次重新求和,效率高一些:
#define FILTER_WINDOW 10 typedef struct { float buf[FILTER_WINDOW]; uint8_t index; float sum; } sliding_average_t; float sliding_average_update(sliding_average_t *f, float input) { f->sum -= f->buf[f->index]; f->buf[f->index] = input; f->sum += input; f->index = (f->index + 1) % FILTER_WINDOW; return f->sum / FILTER_WINDOW; }用到的时候,每个轴单独建一个 sliding_average_t 变量就行。窗口长度选多少,要结合采样频率来定。假设采样频率是 1kHz,窗口长度 10,那滤波输出滞后大约 5 个采样周期,也就是 5ms,这个延迟对大多数控制场景都可接受。窗口长度加到 50,滞后变成 25ms,控制环的相位裕量会被吃掉不少,调试平衡小车时手感会明显变得“肉”。
这个滤波器的缺点也很明显:无法从根本上解决陀螺仪积分漂移,因为它只是对数据做平均,并不会融合加速度计的长时间信息。所以我在实际项目中,通常先用它做 ADC 采样或者按键扫描这类场景,姿态解算还是交给互补滤波。
4.2 一阶低通滤波:内存和算力最优选
一阶低通滤波在嵌入式中是出场率最高的软件滤波器,没有之一。它的计算量只有一个乘法和一个加法,内存只需要两个 float,特别适合 STM32 这类资源不算富裕的 MCU。
核心公式再写一次:
y[n] = α * x[n] + (1 - α) * y[n-1]
这里的 α 不是随便拍脑袋定的,它由采样周期 T 和截止频率 fc 决定:
α = (2π × fc × T) / (1 + 2π × fc × T)
如果采样周期 T 是 0.01s,也就是 100Hz 采样,想要截止频率 5Hz,那 α ≈ (0.314) / (1.314) ≈ 0.24。截止频率越低,滤波越强,输出越平滑,但延迟越大。
我的实现:
typedef struct { float alpha; float prev; } lowpass_t; float lowpass_update(lowpass_t *f, float input) { f->prev = f->alpha * input + (1.0f - f->alpha) * f->prev; return f->prev; }使用的时候,对加速度计的每个轴分别建 lowpass_t 变量,陀螺仪如果噪声也很大,同样可以做低通。要注意的是,低通滤波会带来滞后,如果后面有 PID 控制器,PID 的微分项会放大高频噪声,低通滤波先压一遍噪声反而能减少微分项的抖动。
很多人会问,一阶低通和滑动窗口到底哪个好?我的答案是看实时性要求。滑动窗口在抑制周期性噪声上更直接,但需要 N 个样本缓冲区;一阶低通响应更快,参数也更直观,用一颗 STM32 的主频跑一阶低通,完全不用担心性能。
4.3 互补滤波:姿态解算的入门标配
互补滤波解决的是“长期漂移”问题。它把陀螺仪积分的角度通过高通滤波保留短期动态,把加速度计计算出的角度通过低通滤波保留长期趋势,然后相加。通俗点说,就是平时主要信陀螺仪,但每隔一段时间,加速度计会把漂移拉回来一次。
先看加速度计怎么算角度。以横滚角 Roll 为例,静止时重力加速度在 Y 轴和 Z 轴的分量满足:
roll = atan2(acc_y, acc_z) × 57.3
这里转换成度数,直觉上也很好理解:模块倾斜时,重力从 Z 轴往 Y 轴“分走”一部分,根据比例就能反推出倾斜角。俯仰角 Pitch 就用 acc_x 和 acc_z 算。
有了加速度计角度之后,和陀螺仪积分角度做融合:
#define COMP_ALPHA 0.98f static float roll; static float pitch; void complementary_update(float gyro_rate_x, float gyro_rate_y, float acc_roll, float acc_pitch, float dt) { roll = COMP_ALPHA * (roll + gyro_rate_x * dt) + (1.0f - COMP_ALPHA) * acc_roll; pitch = COMP_ALPHA * (pitch + gyro_rate_y * dt) + (1.0f - COMP_ALPHA) * acc_pitch; }参数 0.98 表示 98% 信陀螺仪,2% 信加速度计。这个比例决定了陀螺仪积分漂移多久才能被加速度计“拉回来”。我理解它的方式,是看时间常数:
τ = (α × T) / (1 - α)
如果 α = 0.98,采样周期 T = 0.01s,那 τ ≈ 0.49 秒。意思是加速度计角度大约每 0.5 秒校正一次积分漂移,这个量级对平衡小车和云台都挺合适。α 越接近 1,短期越信任陀螺仪,但长期漂移会更大;α 太小,动态性能变差,姿态角会被加速度计的振动“带偏”。
4.4 实测对比:曲线拉直后的真实差距
我在静态和动态两种场景下都测过这三组数据。静态下,MPU6050 平放在桌面,陀螺仪积分 30 秒后不滤波的角度能漂到 3° 到 8°,而经过互补滤波的角度始终在 ±0.5° 以内,这个差距不需要示波器,串口绘图一看就明白。
动态下,用手拿着模块快速转动,再用串口波形去看角度输出。滑动窗口和低通滤波后的加速度波形明显平滑,但快速转动时会有几十毫秒的延迟感;互补滤波的 Roll 角跟随手感最自然,停车后也不会像纯积分那样继续“飘”。
三者的选择逻辑再总结一句:想要简单平滑就滑动窗口,想要均匀降噪和一阶动态响应就低通,想要直接输出姿态角且抗漂移就互补滤波。没有绝对最好的滤波器,只有最适合当前系统的滤波器。
5. 调参、调试中躲不开的坑与排查思路
5.1 I2C 读不到数据?先查这三件事
我在 STM32 上调试 MPU6050,最常遇到的现象是 HAL_I2C_Mem_Read 返回 HAL_ERROR,或者读回来的数据全是 0xFF。这种情况通常绕不开三个原因。
第一个是地址错误。MPU6050 的 7 位地址是 0x68,但 HAL 库的地址参数需要左移一位,所以我代码里写的是 0x68 << 1。如果你直接把 0x68 塞进去,I2C 通信时发送的地址就少了一位,设备永远不会应答。很多新手踩坑都踩在这里。
第二个是上拉电阻缺失。I2C 总线的 SDA 和 SCL 是开漏输出,必须靠上拉电阻拉高。某些裸模块或者自制 PCB 上没画上拉电阻,总线一直处于低电平,通信自然超时。这个可以用示波器或者逻辑分析仪测波形确认,如果没有仪器,先量 SDA、SCL 在空闲时是不是高电平。
第三个是电源问题。模块供电不稳或者供电先于 STM32 掉电,I2C 总线可能被芯片内部钳位。解决方式是在主循环里做一次复位重试,连续三次都失败,再对 MPU6050 执行一次复位寄存器写操作:
val = 0x80; // DEVICE_RESET HAL_I2C_Mem_Write(&hi2c1, MPU6050_DEV_ADDR, 0x6B, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); HAL_Delay(100);千万不要一上来就怀疑 MCU 的问题,我遇到过好几次,代码换到另一个板子上好了,回来又坏了,最后发现是模块和板子的接线接触不良。杜邦线用久了氧化,单独测是通的,一震动就断,非常隐蔽。
5.2 滤波后数据“反应迟钝”怎么办
滤波效果太好,数据变成一条直线,同时模块都转过去 30° 了,输出角度还慢吞吞追不上来。这是滤波延迟过大导致的。
我先给一个排查顺序。滑动窗口滤波,先检查窗口长度 N 是否过大,如果采样率只有 100Hz,窗口又设了 50,那延迟就是 245ms,当然钝。一阶低通滤波,检查 α 是否太小,α 从 0.2 调高到 0.6,反应会明显变快。互补滤波,检查 α 是否太接近 1,比如 0.995,时间常数超过 1 秒,动态姿态自然跟不上。
另外,采样率本身也要确认。MPU6050 内部 DLPF 开启时,默认陀螺仪输出频率是 1kHz,如果 I2C 实际读取频率只有 50Hz,那滤波器的时间常数就要按 20ms 采样周期重新计算,否则参数不是偏超前就是偏滞后。先用定时器把采样周期固定,再来调滤波参数,不然永远是盲调。
5.3 数字滤波器救不了的“硬伤”
有些“数据异常”不是滤波能解决的。比如模块安装位置靠近电机电刷,电磁干扰直接串进加速度计输出,这时候波形会高频抖动,表现为几 kHz 的毛刺,靠低通滤波能压一部分,但响应也会被拖慢。更好的办法是从硬件下手:缩短连接线、使用屏蔽线、把传感器放到远离电机的位置、给电源做 π 型滤波。
还有一种是总线数据偶发错误导致的“跳变”,比如某次 I2C 读取因为干扰得到了一个明显偏离正常范围的 raw 值,滤波窗口再大也会被这一瞬间的异常值拉出尖峰。这类问题最有效的处理方式是做数据有效性判断,最简单的方法是把原始值限制在合理范围,突变超过一定阈值就丢弃或者沿用上一次值。
int16_t limit_value(int16_t raw, int16_t prev, int16_t max_delta) { if (abs(raw - prev) > max_delta) { return prev; } return raw; }这个手段在实际项目中帮了我大忙。有一版云台控制,电机转动时 I2C 经常被干扰,姿态角偶尔跳 5° 以上,加上限幅后整个波形立刻稳定了。
5.4 MCU 算力与资源占用问题
MPU6050 滤波本身计算量不大,但很多开发者忽略了它所在的整个数据链路。I2C 用阻塞方式读取时,100kHz 波特率下一次读 14 个字节大概要几毫秒,如果控制周期是 10ms,这一步就占了三分之一以上的时间。我建议用定时器触发读取,或者配合 DMA 提高效率,不要在中断里做长时间阻塞读取。
浮点运算在 STM32F103 这种不带 FPU 的 Cortex-M3 上消耗较大。一个互补滤波每轴也就几次浮点乘加,问题不大;但如果用到卡尔曼,矩阵运算频繁,建议上 STM32F4 或降采样。用 C 语言写 ADC 值滤波函数时也一样,能合并到整数运算就合并到整数运算,浮点只在最后转换时用一次。
滤波缓冲区如果每个轴都开大数组,STM32F103 的内存也会吃紧。滑动窗口 128 个样本、3 个轴,就是 384 个 float,1.5KB 内存,在 20KB RAM 的芯片上还能接受,但要小心别开太多。优化方式是用 uint16_t 或者 int16_t 存原始值,只在求均值时转成 float。
6. 再往前走一步:滤波做完之后还能做什么
数据滤波只是第一步,真正的产品级工程还需要在后续做不少优化。比如把姿态数据用环形队列缓存起来,配合 FreeRTOS 做面向任务的数据分发;或者在 STM32F4 上用 DMP 直接读取四元数,省掉一部分 MCU 算力。如果你想继续深入,可以先试着把当前滤波后的角度接到 PID 上,做一个小自平衡小车,那样就会真正体会到,滤波参数和控制参数之间到底是怎么互相牵制的。
我做这个项目养成的习惯是:每次改滤波参数,都用串口助手把原始数据和滤波后数据一起打印,用量化看到的效果来判断改动是否有效,而不是“感觉好多了”就完事。数据说话,比什么都靠谱。