stm32_mpu6050_滤波教程
🚁 STM32 + MPU6050 从零到四轴:一套让你真的"看懂"飞控的教程
一句话说清这篇教程在干嘛:我们用一块 STM32、一个 MPU6050(小陀螺+小加速度计),一步步做出一个"知道自己在哪、怎么保持平衡、怎么飞稳"的四轴飞控核心。从读取传感器原始数据,到算出姿态角,到用 PID 去控制电机,再到用光流传感器实现室内定点——每一步都讲清楚"为什么这么做",而不是只丢给你一堆代码。
🧭 目录
- 先认识传感器:一个"瞎子"和一个"醉汉"
- 让 STM32 和它"聊上天":I2C 读数据
- 把乱码变成人话:单位换算与校准
- 姿态到底怎么描述:欧拉角、矩阵、四元数
- 姿态解算:怎么把两个传感器"合成"一个准的
- 进阶滤波:卡尔曼 和 DMP(偷懒神器)
- PID:让姿态"听话"的控制魔法
- 电机输出:PID 算出的数怎么变成"转多快"
- 光流传感器:室内没 GPS 时靠"看图"定位
- 飞控全貌:把前面串成一条完整闭环
- 新手学习路线 & 参考资源
1. 先认识传感器:一个"瞎子"和一个"醉汉"
这就是我们这篇教程的"主角"——一块巴掌大的 MPU6050 模块,以及承载算法的 STM32 单片机:

▲ 图 1 · MPU6050 六轴(3 轴加速度 + 3 轴陀螺)传感器模块,通过 I2C 与单片机通信
▲ 图 2 · STM32 开发板,作为飞控的"大脑",负责读数据、算姿态、输出 PWM
想象 MPU6050 其实装了两个性格完全相反的小家伙:
🥴 陀螺仪(Gyroscope)——一个"醉汉"
- 它负责测角速度,也就是"我转得有多快"(单位 °/s)。
- 优点:反应极快,你刚转一点点它立刻就知道。
- 缺点:它只能测"转动的速度",要得到角度必须积分(把速度累加起来)。可它有个坏毛病——会悄悄"跑偏"(零偏漂移)。就像醉汉走路,走几步方向就歪了,时间越久错得越离谱。
- 结论:短时间超准,长时间必漂。
🧘 加速度计(Accelerometer)——一个"坐得稳的老实人"
- 它负责测加速度(单位 g),静止时能"感觉"出重力朝哪边。
- 优点:长期超级稳定,你放平它就知道"地面在这边",永远不漂。
- 缺点:它特别"怕抖"。飞机一震、一加速,它测出的方向就全是乱颤的噪声,短时间里根本没法信。
- 结论:长期稳,短时间抖。
💡 核心思想:取长补短
这俩一个"快但漂",一个"稳但抖"——正好互补。姿态解算(姿态融合)的全部秘密,就是想办法让它俩"互相当靠山",合成一个既快又稳的角度。
🎯 这就是"互补滤波"名字的来源:把两个传感器的长处互补起来。
接线图(就这么简单,一共 4 根线):
STM32 MPU6050 模块
┌────────────┐ ┌────────────┐
│ 3.3V ───┼── 红色 ────────▶│ VCC │
│ GND ───┼── 黑色 ────────▶│ GND │
│ PB6·SCL ──┼── 黄色 ────────▶│ SCL │
│ PB7·SDA ──┼── 绿色 ────────▶│ SDA │
└────────────┘ └────────────┘
(SCL / SDA 都需要接 4.7kΩ 上拉电阻到 3.3V)
⚠️ 新手常踩的三个坑
- MPU6050 的
AD0引脚接地 → 地址是0x68;接 3.3V → 地址变0x69。地址写错 = 通信失败,最最常见的坑! - 别把模块上的 5V 直接怼到 STM32 引脚上(I/O 是 3.3V 电平)。
- I2C 必须接上拉电阻,否则信号不稳定、时好时坏。
2. 让 STM32 和它"聊上天":I2C 读数据
📬 它是怎么沟通的?
MPU6050 内部有一排"邮箱"(寄存器),STM32 只要用 I2C 协议"写个纸条进去、或取一张纸条出来"就能沟通。芯片底层的电路连接大概是这样:

▲ 图 3 · MPU6050 内部电路原理示意(MCU 通过 I2C 数据线 SDA/SCL 与片内寄存器读写通信)
数据都排好队放在从地址 0x3B 开始的一连串邮箱里,顺序是:
加速度X → 加速度Y → 加速度Z → 温度 → 陀螺X → 陀螺Y → 陀螺Z
┌────────┐┌────────┐┌────────┐┌──────┐┌──────┐┌──────┐┌──────┐
│ 2字节 ││ 2字节 ││ 2字节 ││ 2字节││ 2字节││ 2字节││ 2字节│
└────────┘└────────┘└────────┘└──────┘└──────┘└──────┘└──────┘
0x3B 0x3D 0x3F 0x41 0x43 0x45 0x47
所以我们一次突发读取 14 个字节,就把 6 个轴 + 温度全拿回来了,效率最高。
🔌 初始化代码(STM32 HAL 库)
void MPU6050_Init(void){
HAL_Delay(100); // 等待传感器内部供电稳定
uint8_t reg = 0x01; // 唤醒并选择时钟源 PLL with X Gyro
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x6B, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
reg = 0x10; // 设陀螺仪量程 ±1000°/s
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x1B, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
reg = 0x00; // 设加速度量程 ±2g (16384 LSB/g)
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x1C, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
}
📖 读取 14 字节原始数据
uint8_t buf[14];
int16_t ax, ay, az, gx, gy, gz, tmp;
void MPU6050_ReadRaw(void){
// 从 0x3B 开始,一口气读 14 个字节
HAL_I2C_Mem_Read(&hi2c1, 0x68<<1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100);
// 每个数据是16位有符号数,高字节在前,低字节在后,拼起来:
ax = (buf[0] << 8) | buf[1];
ay = (buf[2] << 8) | buf[3];
az = (buf[4] << 8) | buf[5];
tmp= (buf[6] << 8) | buf[7]; // 温度
gx = (buf[8] << 8) | buf[9];
gy = (buf[10]<< 8) | buf[11];
gz = (buf[12]<< 8) | buf[13];
}
✅ 完成后打开串口打印
ax, ay, az, gx, gy, gz,轻轻晃动模块,看数值会不会跟着变。能变 = 你成功了!这是飞控之路的第一个里程碑 🎉。
3. 把乱码变成人话:单位换算与校准
上一步读到的只是原始 ADC 数字(比如 16384、-5000),像一堆没单位的乱码。要变成有物理意义的值,得除以"灵敏度":
物理值 = 原始值 ÷ 灵敏度
MPU6050 芯片内部是一个 16 位的 ADC(模数转换器)。它测到的物理信号(重力、旋转)最终会被转换成一个 16 位有符号整数(int16_t)。
16 位有符号数的最大范围是:
−32768∼+32767
−32768∼+32767。
满量程对应的最大数字就是 32768。
你选择了 ±2g±2*g* 量程(gg 是地球重力加速度,约等于 9.8 m/s2)。
换算推导:
- 芯片感受到的加速度范围是:−2g∼+2g
- 芯片 ADC 输出的数字范围是:−32768∼+32767
- 两者一一对应:
- +2g+2g 对应数字 +32768
- +1g+1g 对应数字 +16384(即 32768÷2)
- 0g0g 对应数字 0
- −1g−1g 对应数字 −16384
- −2g−2g 对应数字 −32768
| 量程 | 加速度灵敏度 (LSB/g) | 陀螺灵敏度 (LSB/°/s) |
|---|---|---|
| ±2g / ±250°/s | 16384 | 131 |
| ±4g / ±500°/s | 8192 | 65.5 |
| ±8g / ±1000°/s | 4096 | 32.8 |
| ±16g / ±2000°/s | 2048 | 16.4 |
// 加速度:÷16384 → 单位 g(静止时重力 1g 会落在某个轴上)
float accelX = ax / 16384.0f;
float accelY = ay / 16384.0f;
float accelZ = az / 16384.0f;
// 陀螺仪:÷32.8 → 单位 °/s
float gyroX = gx / 32.8f;
float gyroY = gy / 32.8f;
float gyroZ = gz / 32.8f;
🧊 零偏校准:帮"醉汉"先站直
还记得那个会跑偏的陀螺仪吗?它静止时明明没转,却会输出一个固定的"假速度"(零偏)。如果不扣掉,积分会越来越偏。
解决办法:上电后让它静止几秒,多采几次平均,就得到这个"假速度",以后每个读数都减掉它。
//
// Created by 31962 on 2026/8/30.
//
#include "Tmpu6050.h"
#include "debug_config.h"
// 存储校准偏移量(全部使用原始有符号 16 位整数 LSB 尺度,避免单位混乱)
static int16_t ax_offset = 0;
static int16_t ay_offset = 0;
static int16_t az_offset = 0;
static int16_t gx_offset = 0;
static int16_t gy_offset = 0;
static int16_t gz_offset = 0;
uint8_t buf[14];
// ===== 初始化 MPU6050 =====
void MPU6050_Init(void){
HAL_Delay(100); // 等待传感器内部供电稳定
uint8_t reg = 0x01; // 唤醒并选择时钟源 PLL with X Gyro
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x6B, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
reg = 0x10; // 设陀螺仪量程 ±1000°/s
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x1B, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
reg = 0x00; // 设加速度量程 ±2g (16384 LSB/g)
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS<<1, 0x1C, I2C_MEMADD_SIZE_8BIT, ®, 1, 100);
}
// ===== I2C 扫描 =====
void I2C_Scan(void) {
printf("Scanning I2C bus...\r\n");
for (uint8_t i = 1; i < 128; i++) {
if (HAL_I2C_IsDeviceReady(&hi2c1, (i << 1), 1, 10) == HAL_OK) {
printf("Found I2C device at address: 0x%02X (8-bit: 0x%02X)\r\n", i, (i << 1));
return;
}
}
printf("No I2C device found!\r\n");
}
// ===== 自动校准(开机时水平静止放桌上) =====
void MPU6050_Calibrate(void) {
int32_t sum_ax = 0, sum_ay = 0, sum_az = 0;
int32_t sum_gx = 0, sum_gy = 0, sum_gz = 0;
const int samples = 200;
debug_printf("Calibrating... Keep MPU6050 still and flat!\r\n");
for (int i = 0; i < samples; i++) {
if (HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDRESS<<1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100) == HAL_OK) {
// 必须强制转换为 (int16_t),否则负数会出错!
sum_ax += (int16_t)((buf[0] << 8) | buf[1]);
sum_ay += (int16_t)((buf[2] << 8) | buf[3]);
sum_az += (int16_t)((buf[4] << 8) | buf[5]);
sum_gx += (int16_t)((buf[8] << 8) | buf[9]);
sum_gy += (int16_t)((buf[10] << 8) | buf[11]);
sum_gz += (int16_t)((buf[12] << 8) | buf[13]);
}
HAL_Delay(5);
}
// 计算偏差:
ax_offset = sum_ax / samples; // X 轴静止理论上是 0
ay_offset = sum_ay / samples; // Y 轴静止理论上是 0
// Z 轴静止理论上是 1g,对应 16384 LSB 刻度!用刻度减刻度!
az_offset = (sum_az / samples) - 16384;
// 陀螺仪静止理论上全为 0
gx_offset = sum_gx / samples;
gy_offset = sum_gy / samples;
gz_offset = sum_gz / samples;
}
// ===== 输出最终校准并换算后的物理量数据 =====
void MPU6050_GetCalibratedData(float *mpu_value) {
if (HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDRESS<<1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100) == HAL_OK) {
// 1. 读取带符号的原始 16 位整数
int16_t raw_ax = (int16_t)((buf[0] << 8) | buf[1]);
int16_t raw_ay = (int16_t)((buf[2] << 8) | buf[3]);
int16_t raw_az = (int16_t)((buf[4] << 8) | buf[5]);
int16_t raw_temp = (int16_t)((buf[6] << 8) | buf[7]);
int16_t raw_gx = (int16_t)((buf[8] << 8) | buf[9]);
int16_t raw_gy = (int16_t)((buf[10] << 8) | buf[11]);
int16_t raw_gz = (int16_t)((buf[12] << 8) | buf[13]);
// 2. 减去 LSB 刻度偏差
int16_t cal_ax = raw_ax - ax_offset;
int16_t cal_ay = raw_ay - ay_offset;
int16_t cal_az = raw_az - az_offset;
int16_t cal_gx = raw_gx - gx_offset;
int16_t cal_gy = raw_gy - gy_offset;
int16_t cal_gz = raw_gz - gz_offset;
// 3. 统一除以灵敏度,转换为物理 float 单位
mpu_value[0] = (float)cal_ax / 16384.0f; // 加速度 X (g)
mpu_value[1] = (float)cal_ay / 16384.0f; // 加速度 Y (g)
mpu_value[2] = (float)cal_az / 16384.0f; // 加速度 Z (g)
mpu_value[3] = ((float)raw_temp / 340.0f) + 36.53f; // 温度 (℃) 不校准直接换算
mpu_value[4] = (float)cal_gx / 32.8f; // 陀螺仪 X (°/s)
mpu_value[5] = (float)cal_gy / 32.8f; // 陀螺仪 Y (°/s)
mpu_value[6] = (float)cal_gz / 32.8f; // 陀螺仪 Z (°/s)
}
}
💡 三轴都这样处理。加速度计一般不用大校准(它本身够稳),除非你要很高精度。
我的最后的结果:

4. 姿态到底怎么描述:欧拉角、矩阵、四元数
🤔 一个深刻的问题:怎么告诉别人"一个物体现在朝哪"?
先看这张图,一架四轴在空中能绕三个方向转,这就是它的"三个自由度":

▲ 图 4 · 四轴的三个姿态角:Roll(横滚)、Pitch(俯仰)、Yaw(偏航)——它们定义了飞机"朝哪、歪多少"
有三种主流"语言":
① 欧拉角(Roll / Pitch / Yaw)——最直观
- Roll 横滚:像飞机压翅膀往左/往右歪。
- Pitch 俯仰:像飞机抬头/低头。
- Yaw 偏航:像飞机在原地转圈(左转/右转)。
- 致命缺点:当 Pitch 转到接近 ±90°(仰头到天顶)时,会陷入"万向锁"(Gimbal Lock)——Roll 和 Yaw 突然分不清了,丢失一个自由度,算法直接崩溃。就像两个铰链叠在一起卡死。
② 旋转矩阵(3×3)——精确但啰嗦
- 信息完整、数学清楚,但有 9 个元素、信息冗余,而且每次运算后数值会"漂移",要反复做正交化校正,很麻烦。
③ 四元数(Quaternion)——飞控界的"普通话" ⭐
- 只用 4 个数字,没有万向锁,运算量小、稳定性好。
- 飞控和姿态解算的工业标准,几乎所有飞控都用它。
🧮 四元数长什么样?
它可以理解为"绕一个轴 n 转一个角度 θ"的浓缩编码:
q = q0 + q1·i + q2·j + q3·k
= cos(θ/2) + sin(θ/2)·(nx·i + ny·j + nz·k)
并且它必须满足归一化(模长为 1):
q0² + q1² + q2² + q3² = 1
🔄 四元数 → 欧拉角(解算结果最终要给人看的)
// 由四元数反解出三个欧拉角
Roll = atan2f(2*(q0*q1 + q2*q3), 1 - 2*(q1*q1 + q2*q2));
Pitch = asinf (2*(q0*q2 - q1*q3));
Yaw = atan2f(2*(q0*q3 + q1*q2), 1 - 2*(q2*q2 + q3*q3));
⚙️ 四元数的"心脏":姿态怎么随时间更新?
知道了角速度 ω,就能推出四元数怎么变。这是一条微分方程:
dq/dt = ½ · q ⊗ ω
在代码里,我们用"一阶积分"把它离散化(halfT = dt/2):
// 这是 Mahony / Madgwick 都用的四元数一阶积分核心
q0 += (-q1*gx - q2*gy - q3*gz) * halfT;
q1 += ( q0*gx + q2*gz - q3*gy) * halfT;
q2 += ( q0*gy - q1*gz + q3*gx) * halfT;
q3 += ( q0*gz + q1*gy - q2*gx) * halfT;
// 归一化,保证模长为 1
float norm = sqrtf(q0*q0+q1*q1+q2*q2+q3*q3);
q0/=norm; q1/=norm; q2/=norm; q3/=norm;
📌 一句话记住:欧拉角直观但会"卡死"(万向锁),四元数没有这个毛病又省算力,所以飞控都用四元数。
以上是数学研究生需要研究的领域,我们知道学会用现成的算法库或则ai处理收到的数据就行
5. 姿态解算:怎么把两个传感器"合成"一个准的
🧠 先懂互补滤波思想(一维版本)
陀螺仪积分出来的角度:快但会漂。
加速度计算出的角度:稳但会抖。
互补滤波就是给它俩各上一道"滤镜",然后加权相加:
angle = α · (angle + gyro·dt) + (1−α) · angle_acc
└───── 陀螺仪(权重大,占 0.98)─────┘ └─加速度计(权重小,占 0.02)─┘
α通常取 0.95~0.98:大部分信任"快"的陀螺仪,- 用剩下一点点"稳"的加速度计,偷偷把漂移拉回来。
陀螺仪 ω ──▶ [高通·α] ┐
├──▶ 加权求和 ──▶ 姿态角
加速度计 ──▶ [低通·1−α] ┘
🚁 三维版:Mahony 算法(四元数互补滤波)
一维互补滤波一次只能算一个轴。把同样的思想推广到三维四元数,就是大名鼎鼎的 Mahony 算法。它流程就三步,非常好懂:
步骤1 由当前四元数,反推"理论重力方向" v
步骤2 拿加速度计实测的重力方向 a,与 v 做叉积 → 得到误差 e = v × a
(注意:陀螺仪若完全准,a 应该等于 v;差得越多,e 越大)
步骤3 用误差 e 通过 PI 修正陀螺仪:ω = ω + Kp·e + Ki·∫e
再用修正后的 ω 去积分更新四元数
一句话:加速度计是"老师",负责挑陀螺仪的毛病(误差),通过 PI 一点点纠正它的漂移。
💻 Mahony 完整代码(可直接用)
// 输入:加速度(单位g)、角速度(rad/s,注意要转成弧度)、采样周期dt
void MahonyAHRSupdate(float gx, float gy, float gz,
float ax, float ay, float az,
float dt){
float halfT = dt/2.0f;
float norm, ex, ey, ez, vx, vy, vz;
// 1. 归一化加速度计(只关心方向,不关心大小)
norm = sqrtf(ax*ax+ay*ay+az*az);
ax/=norm; ay/=norm; az/=norm;
// 2. 由当前四元数推算"理论重力方向"v(机体坐标系下)
vx = 2*(q1*q3 - q0*q2);
vy = 2*(q0*q1 + q2*q3);
vz = q0*q0 - q1*q1 - q2*q2 + q3*q3;
// 3. 叉积得到姿态误差 e = v × a
ex = (ay*vz - az*vy);
ey = (az*vx - ax*vz);
ez = (ax*vy - ay*vx);
// 4. 误差积分 + 比例,修正陀螺仪(Kp比例、Ki积分)
exInt += ex * Ki * dt;
eyInt += ey * Ki * dt;
ezInt += ez * Ki * dt;
gx += Kp*ex + exInt;
gy += Kp*ey + eyInt;
gz += Kp*ez + ezInt;
// 5. 四元数微分积分更新 + 归一化
q0 += (-q1*gx - q2*gy - q3*gz)*halfT;
q1 += ( q0*gx + q2*gz - q3*gy)*halfT;
q2 += ( q0*gy - q1*gz + q3*gx)*halfT;
q3 += ( q0*gz + q1*gy - q2*gx)*halfT;
norm = sqrtf(q0*q0+q1*q1+q2*q2+q3*q3);
q0/=norm; q1/=norm; q2/=norm; q3/=norm;
}
🎛️ 参数经验(先记个大概,后面再调)
Kp(纠偏力度):越大响应越快,但太大引入噪声。常用起步值 2.0。Ki(消除陀螺零偏):用于消除长时间漂移。常用起步值 0.005。- 采样周期
dt建议 1~5ms(主循环里跑)。 - 调参方法:把角度通过串口画出来,看响应快不快、会不会震荡,再微调。
6. 进阶滤波:卡尔曼 和 DMP(偷懒神器)
🧠 卡尔曼滤波:会"自己学习怎么加权"的高手
互补滤波的权重 α 是写死的。卡尔曼滤波更聪明:它有一个预测 + 校正的两步循环,会根据噪声自动调整权重,是"线性系统的最优估计器"。
┌──────────────┐ ┌──────────────┐
预测 │ 角度=角度+ │─────────▶│ 校正 │
│ (gyro-bias)dt│ │ 角度 += K·(...)│
│ P = APAT+Q │ │ K = 最优增益 │
└──────────────┘ └──────────────┘
▲ │
└────────────────────────┘ (循环)
公式(不用背,理解即可):
预测:x̂k = A·x̂k-1 + B·uk , Pk = A·Pk-1·Aᵀ + Q
校正:K = Pk·Hᵀ(H·Pk·Hᵀ+R)⁻¹ , x̂k = x̂k + K·(zk − H·x̂k)
- 优点:更优、能显式建模噪声。
- 缺点:算得多一点,要调 3 个噪声参数(
Q_angle、Q_gyro、R_angle),调起来比互补滤波费劲。 - 早期在 51/STM32 上很流行做单轴姿态(一次算一个轴)。
😴 DMP:MPU6050 自带的"偷懒神器"
重点来了!MPU6050 芯片内部自带一个数字运动处理器 DMP,可以直接在硬件里完成姿态解算,输出现成的四元数,都不用你自己写 Mahony/卡尔曼!
// 从 DMP 的 FIFO 读出四元数(q30 定点格式)
long quat[4];
dmp_read_fifo(gyro, accel, quat, ×tamp, &sensors, &more);
// 除以 2^30 转成浮点
q0 = quat[0] / 1073741824.0f;
q1 = quat[1] / 1073741824.0f;
q2 = quat[2] / 1073741824.0f;
q3 = quat[3] / 1073741824.0f;
// 再用上章的公式把四元数换算成欧拉角即可
💡 官方移植包
MPU6050 + dmp6Axis(Jrowberg)是大家最常用的。移植好就能直接拿角度,非常适合快速上手或算力低的 MCU。
⚖️ 四种方案怎么选?(一张表看清)
| 方案 | 原理 | 计算量 | 稳定性 | 适合场景 |
|---|---|---|---|---|
| 一阶互补滤波 | 固定权重加权 | 很小 | 一般 | 平衡小车、入门理解 |
| 卡尔曼滤波 | 最优估计(预测+校正) | 中 | 较好,要调参数 | 单轴角度、要平滑 |
| Mahony/Madgwick | 四元数互补滤波 | 小 | 好、无万向锁 | 飞控姿态解算(最推荐) |
| 硬件 DMP | 芯片内部硬件解算 | 最小(MCU) | 好,但采样率固定 | 快速上手、低算力 MCU |
以下是对roll和pitch角进行滤波,因为 MPU6050 只有 6 个轴,没有“地磁计”(指南针),所以它在物理上根本无法对 Yaw(偏航角)进行滤波校正
以上都有ai和算法库实现,直接调用api就行,接下来我会对比没有滤波和滤波以后的效果,通过vofa展示
5.1 一阶互补滤波
1. 滤波器头文件 mpu6050_filter.h
创建如下的文件,粘贴复制:
#ifndef __MPU6050_FILTER_H
#define __MPU6050_FILTER_H
#include "main.h"
#include "Tmpu6050.h"
#include <math.h>
// ==================== 姿态角结构体 ====================
typedef struct {
float pitch; // 俯仰角 (绕Y轴,抬头低头)
float roll; // 翻滚角 (绕X轴,左右倾斜)
float yaw; // 偏航角 (绕Z轴,左右转向,仅陀螺仪积分)
} Attitude_t;
// ==================== 卡尔曼滤波器状态结构体 ====================
typedef struct {
float angle; // 当前最优角度估计值
float bias; // 当前陀螺仪零偏估计值
float P[2][2]; // 误差协方差矩阵 (2x2)
float Q_angle; // 角度过程噪声 (越小越信任陀螺仪)
float Q_bias; // 零偏过程噪声
float R_measure; // 测量噪声 (越小越信任加速度计)
} Kalman_t;
// ==================== 函数声明 ====================
// --- 通用工具 ---
void MPU6050_GetAccelAngles(float *mpu_data, float *accel_pitch, float *accel_roll);
// --- 方法①:一阶互补滤波 ---
void Filter_Complementary1st_Init(Attitude_t *att);
void Filter_Complementary1st_Update(Attitude_t *att, float *mpu_data, float dt);
// --- 方法②:二阶互补滤波 ---
void Filter_Complementary2nd_Init(Attitude_t *att);
void Filter_Complementary2nd_Update(Attitude_t *att, float *mpu_data, float dt);
// --- 方法③:卡尔曼滤波 ---
void Filter_Kalman_Init(Kalman_t *kf);
float Filter_Kalman_Update(Kalman_t *kf, float accel_angle, float gyro_rate, float dt);
void Filter_Kalman_GetAttitude(Kalman_t *kf_pitch, Kalman_t *kf_roll,
Attitude_t *att, float *mpu_data, float dt);
// --- VOFA+ 输出 (JustFloat 协议) ---
void VOFA_SendData(float *data, uint8_t count);
#endif
2. 滤波器实现 mpu6050_filter.c
#include "mpu6050_filter.h"
#include <string.h>
#include <stdio.h>
extern UART_HandleTypeDef huart1; // 根据你的实际串口修改
// ================================================================
// 通用工具:加速度计算倾角
// ================================================================
// 用 atan2 从加速度直接算出静态角度(只在静止/缓慢运动时准确)
void MPU6050_GetAccelAngles(float *mpu_data, float *accel_pitch, float *accel_roll) {
float ax = mpu_data[0]; // 单位: g
float ay = mpu_data[1];
float az = mpu_data[2];
// Pitch: 绕Y轴旋转(抬头低头)
*accel_pitch = atan2f(ax, sqrtf(ay * ay + az * az)) * 57.2957795f; // 弧度转角度
// Roll: 绕X轴旋转(左右倾斜)
*accel_roll = atan2f(ay, sqrtf(ax * ax + az * az)) * 57.2957795f;
}
// ================================================================
// 方法①:一阶互补滤波 (First-Order Complementary)
// ================================================================
// 原理:
// angle = α * (angle + gyro * dt) + (1-α) * accel_angle
// ↑ 高通滤波(信任陀螺仪短期变化) ↑ 低通滤波(信任加速度计长期趋势)
// α 越大 → 越信任陀螺仪(响应快但会漂移)
// α 越小 → 越信任加速度计(稳定但有震动噪声)
// ================================================================
#define COMP1_ALPHA 0.98f // 互补系数,通常取 0.95 ~ 0.99
void Filter_Complementary1st_Init(Attitude_t *att) {
att->pitch = 0.0f;
att->roll = 0.0f;
att->yaw = 0.0f;
}
void Filter_Complementary1st_Update(Attitude_t *att, float *mpu_data, float dt) {
// 1. 从加速度计算静态角度
float accel_pitch, accel_roll;
MPU6050_GetAccelAngles(mpu_data, &accel_pitch, &accel_roll);
// 2. 读取陀螺仪角速度 (°/s)
float gyro_x = mpu_data[4]; // Roll 方向的角速度
float gyro_y = mpu_data[5]; // Pitch 方向的角速度
float gyro_z = mpu_data[6]; // Yaw 方向的角速度
// 3. 一阶互补滤波核心公式
// 短期相信陀螺仪积分,长期用加速度计修正漂移
att->pitch = COMP1_ALPHA * (att->pitch + gyro_y * dt) + (1.0f - COMP1_ALPHA) * accel_pitch;
att->roll = COMP1_ALPHA * (att->roll + gyro_x * dt) + (1.0f - COMP1_ALPHA) * accel_roll;
att->yaw += gyro_z * dt; // Yaw 只能靠陀螺仪积分(加速度计无法测偏航)
}
// ================================================================
// 方法②:二阶互补滤波 (Second-Order Complementary)
// ================================================================
// 原理:在一阶互补的基础上,引入加速度计角度的变化率作为第二个修正项,
// 相当于多了一个"微分校正"环节,让滤波器对突变更敏感且更平滑。
//
// x1 = (accel_angle - angle) * k1
// x2 += (accel_angle - angle) * k2 * dt
// angle += (gyro_rate + x1 + x2) * dt
// ================================================================
// 二阶互补滤波内部状态
static float comp2_x1_pitch = 0, comp2_x2_pitch = 0;
static float comp2_x1_roll = 0, comp2_x2_roll = 0;
#define COMP2_K1 0.02f // 比例修正系数(越大越信加速度计)
#define COMP2_K2 0.005f // 积分修正系数(消除稳态误差)
void Filter_Complementary2nd_Init(Attitude_t *att) {
att->pitch = 0.0f;
att->roll = 0.0f;
att->yaw = 0.0f;
comp2_x1_pitch = 0; comp2_x2_pitch = 0;
comp2_x1_roll = 0; comp2_x2_roll = 0;
}
void Filter_Complementary2nd_Update(Attitude_t *att, float *mpu_data, float dt) {
float accel_pitch, accel_roll;
MPU6050_GetAccelAngles(mpu_data, &accel_pitch, &accel_roll);
float gyro_x = mpu_data[4];
float gyro_y = mpu_data[5];
float gyro_z = mpu_data[6];
// --- Pitch 二阶互补 ---
float err_pitch = accel_pitch - att->pitch; // 加速度计与当前估计的偏差
comp2_x1_pitch = err_pitch * COMP2_K1; // 比例修正
comp2_x2_pitch += err_pitch * COMP2_K2 * dt; // 积分修正(消除稳态偏差)
att->pitch += (gyro_y + comp2_x1_pitch + comp2_x2_pitch) * dt;
// --- Roll 二阶互补 ---
float err_roll = accel_roll - att->roll;
comp2_x1_roll = err_roll * COMP2_K1;
comp2_x2_roll += err_roll * COMP2_K2 * dt;
att->roll += (gyro_x + comp2_x1_roll + comp2_x2_roll) * dt;
// --- Yaw 仅陀螺仪积分 ---
att->yaw += gyro_z * dt;
}
// ================================================================
// 方法③:卡尔曼滤波 (Kalman Filter)
// ================================================================
// 原理(简化版):
// 预测阶段:用陀螺仪积分预测下一时刻的角度
// 更新阶段:用加速度计的测量值修正预测结果
// 卡尔曼增益 K 会自动调整:
// - 如果加速度计噪声大 → K 变小 → 更信任陀螺仪
// - 如果陀螺仪漂移大 → K 变大 → 更信任加速度计
// 最终得到"数学上最优"的角度估计
// ================================================================
void Filter_Kalman_Init(Kalman_t *kf) {
kf->angle = 0.0f;
kf->bias = 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;
// 噪声参数(可根据实际传感器噪声调整)
kf->Q_angle = 0.001f; // 角度过程噪声(越小 → 越信任陀螺仪预测)
kf->Q_bias = 0.003f; // 零偏过程噪声
kf->R_measure = 0.03f; // 测量噪声(越小 → 越信任加速度计测量)
}
float Filter_Kalman_Update(Kalman_t *kf, float accel_angle, float gyro_rate, float dt) {
// ========== 第一步:预测 (Predict) ==========
// 用陀螺仪角速度预测当前角度(减去估计的零偏)
kf->angle += dt * (gyro_rate - kf->bias);
// 更新误差协方差矩阵(不确定性会随时间增大)
kf->P[0][0] += dt * (dt * kf->P[1][1] - kf->P[0][1] - kf->P[1][0] + kf->Q_angle);
kf->P[0][1] -= dt * kf->P[1][1];
kf->P[1][0] -= dt * kf->P[1][1];
kf->P[1][1] += kf->Q_bias * dt;
// ========== 第二步:更新 (Update) ==========
// 计算卡尔曼增益 K
float S = kf->P[0][0] + kf->R_measure; // 创新协方差
float K[2]; // 卡尔曼增益向量
K[0] = kf->P[0][0] / S;
K[1] = kf->P[1][0] / S;
// 用加速度计测量值修正预测
float y = accel_angle - kf->angle; // 创新(测量残差)
kf->angle += K[0] * y;
kf->bias += K[1] * y;
// 更新误差协方差矩阵(不确定性因修正而减小)
float P00_temp = kf->P[0][0];
float P01_temp = kf->P[0][1];
kf->P[0][0] -= K[0] * P00_temp;
kf->P[0][1] -= K[0] * P01_temp;
kf->P[1][0] -= K[1] * P00_temp;
kf->P[1][1] -= K[1] * P01_temp;
return kf->angle;
}
// 封装:一次性计算 Pitch 和 Roll
void Filter_Kalman_GetAttitude(Kalman_t *kf_pitch, Kalman_t *kf_roll,
Attitude_t *att, float *mpu_data, float dt) {
float accel_pitch, accel_roll;
MPU6050_GetAccelAngles(mpu_data, &accel_pitch, &accel_roll);
att->pitch = Filter_Kalman_Update(kf_pitch, accel_pitch, mpu_data[5], dt);
att->roll = Filter_Kalman_Update(kf_roll, accel_roll, mpu_data[4], dt);
att->yaw += mpu_data[6] * dt;
}
// ================================================================
// VOFA+ JustFloat 协议输出
// ================================================================
// VOFA+ 的 JustFloat 协议:
// 直接发送 float 数据的二进制原始字节(小端序)
// 每组数据结尾追加 4 个字节的帧尾:0x00 0x00 0x80 0x7F
// VOFA+ 会自动识别并画出波形
// ================================================================
void VOFA_SendData(float *data, uint8_t count) {
// 帧尾标志(固定为 0x007F8000 的小端表示)
static const uint8_t tail[4] = {0x00, 0x00, 0x80, 0x7F};
// 发送所有 float 数据(每个 float 占 4 字节)
HAL_UART_Transmit(&huart1, (uint8_t *)data, count * sizeof(float), 10);
// 发送帧尾
HAL_UART_Transmit(&huart1, (uint8_t *)tail, 4, 10);
}
3. 主函数 main.c — 四种滤波对比验证
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "debug_config.h"
#include "Tmpu6050.h"
#include "mpu6050_filter_all.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
// 四种滤波方法的姿态角结果
Attitude_t att_comp1; // 一阶互补
Attitude_t att_comp2; // 二阶互补
Attitude_t att_kalman; // 卡尔曼
// 卡尔曼滤波器实例(Pitch 和 Roll 各一个)
Kalman_t kf_pitch, kf_roll;
// 时间步长(10ms = 100Hz)
#define DT 0.01f
// 选择当前验证的滤波方法(改这个数字切换 1/2/3)
#define FILTER_MODE 1 // 1=一阶互补, 2=二阶互补, 3=卡尔曼
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
float mpu_value[7]={0};
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
MPU6050_Init();
// 保持静止平放 1 秒钟进行自动校准
MPU6050_Calibrate();
// 3. 初始化所有滤波器
Filter_Complementary1st_Init(&att_comp1); // 一阶互补滤波器
Filter_Complementary2nd_Init(&att_comp2); // 二阶互补滤波器
Filter_Kalman_Init(&kf_pitch); // pitch 卡尔曼滤波器
Filter_Kalman_Init(&kf_roll); // roll 卡尔曼滤波器
att_kalman.pitch = 0;
att_kalman.roll = 0;
att_kalman.yaw = 0;
float accel_pitch_raw, accel_roll_raw;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 获取校准后的数据
MPU6050_GetCalibratedData(mpu_value);
// ===== 同时运行三种软件滤波算法 =====
Filter_Complementary1st_Update(&att_comp1, mpu_value, DT);
Filter_Complementary2nd_Update(&att_comp2, mpu_value, DT);
Filter_Kalman_GetAttitude(&kf_pitch, &kf_roll, &att_kalman, mpu_value, DT);
// ===== 加速度计直接算出的原始角度(未滤波,作为参考基准线)=====
MPU6050_GetAccelAngles(mpu_value, &accel_pitch_raw, &accel_roll_raw);
// ===== VOFA+ 可视化输出 =====
// 发送 8 个通道的数据到 VOFA+ 同时显示对比:
// CH1: 加速度计原始 Pitch(有噪声,作为参考)
// CH2: 一阶互补 Pitch
// CH3: 二阶互补 Pitch
// CH4: 卡尔曼 Pitch
// CH5: 加速度计原始 Roll
// CH6: 一阶互补 Roll
// CH7: 二阶互补 Roll
// CH8: 卡尔曼 Roll
float vofa_buf[8] = {
accel_pitch_raw, // CH1 原始加速度计 Pitch
att_comp1.pitch, // CH2 一阶互补 Pitch
att_comp2.pitch, // CH3 二阶互补 Pitch
att_kalman.pitch, // CH4 卡尔曼 Pitch
accel_roll_raw, // CH5 原始加速度计 Roll
att_comp1.roll, // CH6 一阶互补 Roll
att_comp2.roll, // CH7 二阶互补 Roll
att_kalman.roll, // CH8 卡尔曼 Roll
};
VOFA_SendData(vofa_buf, 8);
// ===== 也可以切换为 printf 文字输出调试 =====
// (如果用 VOFA+ 的 FireWater 协议,取消下面注释)
// debug_printf("%.2f,%.2f,%.2f,%.2f\r\n",
// accel_pitch_raw, att_comp1.pitch, att_comp2.pitch, att_kalman.pitch);
// debug_printf("%.2f,%.2f,%.2f,%.2f\r\n",accel_roll_raw,att_comp1.roll,att_comp2.roll,att_kalman.roll);
HAL_Delay(10); // 10ms = 100Hz 采样率,与 DT=0.01f 对应
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
4. VOFA+ 配置方法
步骤 1:打开 VOFA+ 软件
步骤 2:配置串口
- 选择你的 COM 口
- 波特率设置为你 CubeMX 里配置的值(常见
115200)
步骤 3:选择协议
- 点击右上角协议选择:JustFloat

步骤 4:开始接收
- 点击"开始"按钮,就能看到 8 条曲线实时滚动
步骤 5:通道命名(便于对比观察)
在 VOFA+ 的通道列表中,按顺序重命名为:

| 通道 | 名称 | 含义 |
|---|---|---|
| CH1 | Raw Pitch | 加速度计原始角度(有毛刺噪声) |
| CH2 | Comp1 Pitch | 一阶互补滤波 |
| CH3 | Comp2 Pitch | 二阶互补滤波 |
| CH4 | Kalman Pitch | 卡尔曼滤波 |
| CH5 | Raw Roll | 加速度计原始 Roll |
| CH6 | Comp1 Roll | 一阶互补 Roll |
| CH7 | Comp2 Roll | 二阶互补 Roll |
| CH8 | Kalman Roll | 卡尔曼 Roll |
我将单片机水平放置后,可以看出:Pitch滤波


5. 四种滤波方法对比总结
不是绝对的,在我们的场景中,每个mpu6050的环境以及温度补偿都不一样,所以会有偏差,但是算法没有好坏,只有适合。还有参数的调节好坏
| 特性 | 一阶互补 | 二阶互补 | 卡尔曼 | DMP |
|---|---|---|---|---|
| 复杂度 | ⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐(硬件自动) |
| CPU 占用 | 极低 | 低 | 中等 | 几乎为零 |
| 调参难度 | 只需调 α | 调 K1, K2 | 调 Q, R | 无需调参 |
| 抗震动 | 一般 | 较好 | 最优 | 好 |
| 长期漂移 | 小 | 更小 | 最小 | 无 |
| 适用场景 | 入门学习 | 平衡车 | 无人机/机器人 | 快速出成品 |
7. PID:让姿态"听话"的控制魔法
🎯 从"怎么把飞机摆平"说起
姿态解算告诉我们"飞机现在歪了多少",但光知道不行,得让它自己摆正。这就是 PID 的活。
PID 是个"负反馈闭环":不停地拿"期望值"和"当前值"比,算出差距(误差),再用差距去指挥电机:

▲ 图 5 · PID 负反馈闭环:期望值 − 测量值 = 误差 → PID → 被控对象 → 输出,输出再反馈回来比较
期望角度 ──→ [误差e] ──→ [PID控制器] ──→ [电机/机体] ──→ 输出
▲ │
└───────────── 负反馈 ◀───────────────┘
(姿态解算测出当前角度)
🎭 三个字母各是什么脾气?
- P(比例):误差越大推得越猛,反应快。但光靠 P 消除不了最后那一点"稳态误差",而且 P 太大容易"抖得像筛糠"。
- I(积分):把误差攒起来,专门消灭那点"死活消不掉"的稳态偏差(比如飞机老往一边歪,靠 I 一直给个补偿力)。
- D(微分):看误差变化得多快,有"预见性",能刹车——抑制超调、让系统更快稳下来(就像开车快撞上前提前松油门)。
u(t) = Kp·e(t) + Ki·∫e(t)dt + Kd·(de(t)/dt)
比例 积分累计 变化率
🧱 位置式 vs 增量式
// 位置式 PID:一次直接输出一个"绝对控制量"
out = Kp*err + Ki*errSum + Kd*(err - lastErr);
// 增量式 PID:输出"变化量",再累加(天然带积分效果、更安全)
dOut = Kp*(err - lastErr)+ Ki*err+ Kd*(err - 2*lastErr + lastLastErr);
out += dOut;
| 特性 | 位置式 PID | 增量式 PID |
|---|---|---|
| 输出含义 | 最终绝对控制量 | 本次输出变化量 Δout |
| 积分存储 | 保存全部历史误差总和 errSum | 仅保存最近 3 次误差,无总累加 |
| 积分饱和风险 | 高 | 低 |
| 故障冲击 | 大,异常时输出直接跳变 | 小,只会小幅改动 |
| 变量数量 | 少 | 略多 |
| 典型用途 | 温控、阀门、定值控制 | 电机、小车、无人机运动控制 |
🥪 飞控用的是"串级 PID"(双层嵌套的三明治)
一个 PID 直接控角度通常不够稳。四轴飞控普遍用串级 PID:
期望角度 ─▶ [外环·角度PID] → 期望角速度 ─▶ [内环·角速度PID] → 电机控制量
▲ ▲
姿态解算角度 陀螺仪实测角速度
- 外环(角度环):管"稳",输入角度误差,需要从加速度计积分出来,比较慢,输出"期望角速度"。
- 内环(角速度环):管"快",输入角速度误差(陀螺仪直接给),输出电机量。
- 内环快、外环稳,套在一起就又快又稳。
🔧 整定口诀:先内环,后外环。
① 内环只加 P → 加到出现高频抖动再退一点;② 加 D 抑制超调;③ 内环稳了再加外环 P;④ 最后加 I 消稳态误差。每次只改一个参数,用串口画波形判断。
8. 电机输出:PID 算出的数怎么变成"转多快"
📶 PWM:靠"占空比"说话
四轴电机(无刷电机 + 电调 ESC)靠 PWM 信号调速。PWM 就像用"手电筒快速闪灯":高电平占的比例(占空比)越大,电机转得越快。
低速 (占空比25%) ██░░░░░░██░░░░░░ 高电平短 → 转速低
高速 (占空比75%) ██████░░██████░░ 高电平长 → 转速高
- 常见电调"油门脉宽"范围:1000~2000 μs。
- STM32 用定时器输出 PWM 即可,占空比就是"控制量 / 满量程"。
🔀 动力分配(Mixer):一个控制量怎么分给 4 个电机?
PID 算出来的是"修正量",得叠加到 4 个电机上。以 X 型机架为例(符号取决于机架和转向约定):
// thr=基础油门;roll/pitch/yaw = 三个姿态环的输出
M1 = thr + pidRoll.out + pidPitch.out + pidYaw.out;
M2 = thr - pidRoll.out + pidPitch.out - pidYaw.out;
M3 = thr - pidRoll.out - pidPitch.out + pidYaw.out;
M4 = thr + pidRoll.out - pidPitch.out - pidYaw.out;
// 最后限幅到 [油门下限, 上限],防止某路越界
🔄 为什么相邻电机要反着转?
先看真实四轴的机架布局(X 型),4 个电机分布在四角:

▲ 图 6 · X 型四轴机架布局示意:相邻电机反方向旋转,对角电机同向,用于抵消反扭矩
M1(顺) M3(逆)
\ /
飞控/IMU
/ \
M2(逆) M4(顺)
因为要让 4 个电机的"反扭矩"互相抵消,飞机才不会自己原地打转。所谓"偏航"控制,正是靠"故意打破这种平衡"——让对角电机一增一减,飞机就转起来了。
9. 光流传感器:室内没 GPS 时靠"看图"定位
🖱️ 它其实就是个"倒着放的光学鼠标"
GPS 在室内没信号,那怎么知道飞机有没有水平漂移?答案:光流传感器。

▲ 图 7 · PMW3901 光流传感器模块(常搭配激光测距 VL53L1X 一起用),面向地面安装,原理类似光学鼠标
光流传感器 = 朝下的微型摄像头 + 图像协处理器,原理跟光学鼠标一模一样:
光流传感器(朝下) ──▶ 连续拍地面 ──▶ 比较相邻两帧的"花纹位置"
│
▼
像素位移 Δx, Δy(花纹动了几格)
📐 像素位移怎么变成"速度"?
关键公式(一定要理解):
线速度 ≈ 像素位移 × 对地高度 × 比例因子 ÷ 帧间隔
为什么要有高度? 因为光流测的其实是"角变化"。同样一个像素位移,飞机飞得越高,实际移动得越远。所以光流模块几乎都要搭配测距传感器(TOF/激光),否则测出的速度会有"高度误差被放大"的问题。
🤝 为什么还要融合 IMU(陀螺仪)?
单看光流,数据很"抖",而且有个大麻烦:飞机晃动/倾斜时,地面画面也会"假动"(机体原地摆,地面明明没动,图像却变了)。
所以标准做法是把三者融合(互补滤波或 EKF):
光流 Δx,Δy ──┐
测距 高度 h ──┼──▶ [融合: 抵消晃动 + 尺度换算] ──▶ 水平速度 vx,vy ──▶ [位置环PID] ──▶ 定点悬停
IMU 角速度 ──┘
- 用陀螺仪角速度抵消机身晃动带来的虚假光流;
- 用高度把像素换算成真实速度;
- 得到稳定的水平速度/位置后,喂给位置环 PID,实现室内"定点悬停"。
📊 主流光流传感器对比(据 PX4 文档)
| 传感器 | 分辨率 | 帧率 | 有效高度 | 接口 | 特点 |
|---|---|---|---|---|---|
| PMW3901 | 30×30 | ~10000fps | 80mm~∞ | SPI/UART | 低功耗、最流行,DJI Tello 同系 |
| ADNS-9800 | 30×30 | ~6400fps | 2~30mm | SPI | 低空近距离 |
| OV7725+MCU | 640×480 | ~500fps | 10cm~5m | SPI/I2C | 高度范围大、需计算 |
10. 飞控全貌:把前面串成一条完整闭环
把 1~9 章串起来,就是一条完整的飞控数据链路:
┌─────────────┐ ┌──────────────────┐ ┌────────────────────┐ ┌─────────────┐
│ MPU6050 │ │ 姿态解算·滤波 │ │ 串级PID │ │ 动力分配 │
│ (加速度+陀螺)│──▶│ Mahony/DMP/卡尔曼 │──▶│ 角度环+角速度环+位置环│──▶│ Mixer→4路PWM│
└─────────────┘ └──────────────────┘ └────────────────────┘ └─────────────┘
┌─────────────┐ ▲ │ │
│ 光流+测距 │──────────┘ ▼ ▼
└─────────────┘ 机体姿态/位置/速度 ◀──────── 4个电机
│ │
└─────────────── 反馈回传感器 ◀──────────────────┘
闭环一句话:传感器感受状态 → 解算/滤波算出姿态 → 串级 PID 决定怎么修正 → 动力分配把修正量分给 4 个电机 → 电机改变机体 → 传感器再感受新状态……如此循环,飞机才稳得住。
11. 新手学习路线 & 参考资源
🗺️ 由浅入深的学习顺序
- 先点亮 MPU6050:I2C 读出原始数据,串口打印,确认通信 OK(第 2 章)。
- 标定 + 换算:做零偏校准,把数据变成 g 和 °/s(第 3 章)。
- 先算单轴角度:加速度算角度、陀螺仪积分,做一阶互补/卡尔曼,先出 Pitch 或 Roll(第 5、6 章)。
- 上四元数解算:跑通 Mahony,或直接 DMP,拿到完整 Pitch/Roll/Yaw(第 4、6 章)。
- 上 PID:先单环调稳一个角度,再升级串级 PID(第 7 章)。
- 接电机:PWM 驱动电调,理解 Mixer 和电机转向(第 8 章)。
- 加光流:读像素位移,与 IMU 融合出速度/位置,室内定点悬停(第 9 章)。
📚 参考的优秀网上教程
- STM32Cube HAL 库 MPU6050 DMP 姿态解算(CSDN)— 寄存器读取与 DMP 移植
https://blog.csdn.net/weixin_52916289/article/details/135142374 - STM32 驱动 MPU6050 及四元数姿态解算(Zeeklog)— 驱动与读取代码
https://zeeklog.com/stm32-wan-zhuan-iiczhi-qu-dong-mpu6050ji-zi-tai-jie-suan - MPU6050 姿态解算——Mahony 互补滤波(CSDN)— 算法原理推导
https://blog.csdn.net/lqj11/article/details/107423334 - MPU6050 滤波、姿态融合(一阶互补、卡尔曼)(博客园)— 单轴滤波代码
https://www.cnblogs.com/qsyll0916/p/8030379.html - 谈谈 MPU6050 的数据融合:一阶滤波与卡尔曼滤波(CSDN)— 三种融合对比
https://blog.csdn.net/zsn15702422216/article/details/52223799 - STM32 MPU6050 姿态解算 Mahony 互补滤波算法(CSDN)— C 语言实现
https://blog.csdn.net/weixin_44821644/article/details/116893943 - 无人机算法之 PID(CSDN)— 单级/串级 PID 与调试
https://blog.csdn.net/u012320127/article/details/104579607 - 四轴无人机飞行控制原理(PID)(CSDN)— 电机 PWM 与动力分配
https://blog.csdn.net/weixin_46697509/article/details/133017111 - 四轴 PID 控制算法详解(单环 PID、串级 PID)(CSDN)— 参数整定
https://blog.csdn.net/lovely_yoshino/article/details/92789955 - PX4 官方文档:PMW3901 光流传感器 — 原理、参数与接线
https://docs.px4.io/main/zh/sensor/pmw3901 - PX4 光流技术实战教学(openvela/CSDN)— 光流与 IMU/EKF 融合
https://openvela.csdn.net/694b91185b9f5f31781a3769.html - 无人机光流模块 Optical Flow 设置(CSDN)— PMW3901 配置
https://blog.csdn.net/weixin_60324241/article/details/147270687
⚠️ 安全提醒:涉及飞行器的实验,请在合规场地、确保安全的前提下进行,并遵守当地无人机法规。教程中的代码与原理图为教学示意,正式项目请以芯片官方数据手册(MPU6050 / PMW3901 / STM32 参考手册)为准。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)