STM32F4陀螺仪姿态解算无人机控制

你有没有试过亲手“托起”一架四旋翼,看着它在空中微微颤抖、左右摇晃,却始终不肯平稳起飞?😅 很多初学者都卡在这一步——不是电机不转,也不是遥控失灵,而是 飞控系统“看不懂”自己的姿态 。这时候,真正的大脑才该上线:IMU传感器 + 高性能MCU + 精巧的姿态融合算法。

今天我们就来拆解这个“飞行大脑”的核心逻辑:如何用 STM32F4 + MPU6050 + Mahony算法 ,让一台DIY无人机稳稳地悬停在空中 ✈️。


为什么是STM32F4?

说白了,飞控就像一个高速反应的体操运动员——每秒要做几百次“感知-判断-调节”的闭环动作。普通单片机(比如STM32F1或Arduino)跑个PID可能还行,但一旦加上浮点运算密集的姿态解算,立马就“头晕眼花”。

而STM32F4系列,尤其是像 STM32F407VG 这样的型号,简直就是为这类任务量身定制的:

  • 主频高达 168MHz
  • 内置 FPU(浮点单元) ,三角函数、开方不再靠查表硬扛
  • 支持DSP指令集,矩阵运算更高效
  • 多达 192KB SRAM ,足够缓存滤波状态和中间变量
  • 多路高级定时器(TIM1/TIM8),轻松输出6路互补PWM驱动无刷电调

最关键的是,它能在 500Hz甚至1kHz的控制频率下稳定运行复杂滤波算法 ,这是实现自稳飞行的前提!

想象一下:每次姿态更新间隔只有2ms,你得在这短短时间内完成I²C读取、数据校准、四元数更新、PID计算、PWM刷新……没有FPU和足够RAM?基本别想流畅跑通整套流程。


MPU6050:小身材,大能量 🧠

MPU6050可能是目前最流行的六轴IMU之一,集成三轴陀螺仪+三轴加速度计,通过I²C就能通信,价格亲民,适合教学和原型开发。

但它真能胜任飞控任务吗?关键看两点:

1. 数据质量够不够稳?
  • 分辨率:16位ADC
  • 更新率:最高可达1kHz(通过配置采样分频器)
  • 默认I²C地址: 0x68 (AD0接地时)

虽然它是消费级MEMS传感器,噪声比工业级高一些,但在合理滤波和校准后,完全能满足入门级四旋翼的需求。

2. 怎么读数据才不拖后腿?

这里推荐使用 HAL库 + DMA批量读取 ,避免频繁中断影响实时性。不过为了清晰起见,先来看基础版I²C读取代码:

uint8_t MPU6050_ReadReg(uint8_t reg) {
    uint8_t data;
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
    return data;
}

void MPU6050_GetRawData(int16_t *ax, int16_t *ay, int16_t *az,
                        int16_t *gx, int16_t *gy, int16_t *gz) {
    uint8_t buf[14];
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR << 1, MPU6050_REG_ACCEL_XOUT_H, 
                     I2C_MEMADD_SIZE_8BIT, buf, 14, 100);

    *ax = (int16_t)(buf[0] << 8 | buf[1]);
    *ay = (int16_t)(buf[2] << 8 | buf[3]);
    *az = (int16_t)(buf[4] << 8 | buf[5]);
    *gx = (int16_t)(buf[8] << 8 | buf[9]);
    *gy = (int16_t)(buf[10] << 8 | buf[11]);
    *gz = (int16_t)(buf[12] << 8 | buf[13]);
}

💡 小贴士:高位字节在前,记得左移8位再合并!

初始化也不能马虎,至少要做两件事:
1. 关闭睡眠模式
2. 设置合适的量程(太小会饱和,太大损失精度)

void MPU6050_Init(void) {
    HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR << 1, MPU6050_REG_PWR_MGMT_1, 
                      I2C_MEMADD_SIZE_8BIT, (uint8_t[]){0x00}, 1, 100);

    // ±2000°/s 角速度,±2g 加速度
    HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR << 1, MPU6050_REG_GYRO_CONFIG, 
                      I2C_MEMADD_SIZE_8BIT, (uint8_t[]){0x18}, 1, 100);
    HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR << 1, MPU6050_REG_ACCEL_CONFIG, 
                      I2C_MEMADD_SIZE_8BIT, (uint8_t[]){0x00}, 1, 100);
}

姿态解算:别再只用陀螺仪积分了!🌀

新手最容易犯的错误就是:“我用陀螺仪测角速度,然后积分一下不就知道角度了吗?”
听起来很合理,对吧?但现实很残酷—— 陀螺仪有零偏漂移 ,哪怕每天只漂0.1°/s,飞30秒误差就能累积到3°以上,飞机早就翻了。

那怎么办?加速度计来救场!

加速度计怎么看姿态?

当飞行器静止时,加速度计感受到的主要是重力。根据重力在XYZ轴上的投影,可以反推出俯仰(pitch)和横滚(roll)角:

$$
\text{roll} = \arctan\left(\frac{a_y}{\sqrt{a_x^2 + a_z^2}}\right), \quad
\text{pitch} = \arctan\left(\frac{-a_x}{\sqrt{a_y^2 + a_z^2}}\right)
$$

但它也有问题: 一动就不准了! 飞行中的振动、加减速都会干扰测量。

所以聪明的做法是—— 融合两者优点 :用陀螺仪做短期预测,用加速度计做长期修正。这就是所谓的“传感器融合”。


Mahony算法:轻量级王者登场 👑

在嵌入式平台上,EKF(扩展卡尔曼滤波)虽然精度高,但计算复杂;互补滤波简单但难以处理非线性误差。而 Mahony算法 正好居中:基于四元数表示,利用PI控制器动态修正陀螺仪偏置,既准确又高效。

它的核心思想其实很简单:

  1. 用陀螺仪积分预测下一时刻的姿态(四元数更新)
  2. 用加速度计测量当前重力方向
  3. 计算“预测重力”与“实测重力”的误差向量
  4. 把这个误差反馈回去,调整陀螺仪读数(相当于在线校正零偏)
  5. 再用修正后的角速度重新积分

整个过程像一个闭环控制系统,把姿态估计变成了一个“姿态跟踪问题”。

下面是精简后的核心实现(带注释):

float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f;
float exInt = 0.0f, eyInt = 0.0f, ezInt = 0.0f;
float Kp = 2.0f;   // 比例增益
float Ki = 0.0f;   // 积分项可设为0以简化

void MadgwickAHRSupdate(float gx, float gy, float gz, 
                        float ax, float ay, float az, float dt) {
    float recipNorm;
    float halfvx, halfvy, halfvz;
    float halfex, halfey, halfez;

    // 步骤1:归一化加速度计数据(单位向量)
    recipNorm = invSqrt(ax*ax + ay*ay + az*az);
    ax *= recipNorm; ay *= recipNorm; az *= recipNorm;

    // 步骤2:根据当前四元数,推算“理论重力”在机体坐标系的分量
    halfvx = q1*q3 - q0*q2;
    halfvy = q0*q1 + q2*q3;
    halfvz = q0*q0 - 0.5f + q3*q3;

    // 步骤3:误差 = 实测 × 理论 的叉积(即垂直于两个向量的方向)
    halfex = ay*halfvz - az*halfvy;
    halfey = az*halfvx - ax*halfvz;
    halfez = ax*halfvy - ay*halfvx;

    // 步骤4:PI反馈修正陀螺仪输入
    exInt += halfex * Ki * dt;
    eyInt += halfey * Ki * dt;
    ezInt += halfez * Ki * dt;

    gx += Kp * halfex + exInt;
    gy += Kp * halfey + eyInt;
    gz += Kp * halfez + ezInt;

    // 步骤5:四元数微分方程积分更新
    q0 += (-q1*gx - q2*gy - q3*gz) * 0.5f * dt;
    q1 += ( q0*gx + q2*gz - q3*gy) * 0.5f * dt;
    q2 += ( q0*gy - q1*gz + q3*gx) * 0.5f * dt;
    q3 += ( q0*gz + q1*gy - q2*gx) * 0.5f * dt;

    // 步骤6:归一化四元数(保持单位长度)
    recipNorm = invSqrt(q0*q0 + q1*q1 + q2*q2 + q3*q3);
    q0 *= recipNorm; q1 *= recipNorm; q2 *= recipNorm; q3 *= recipNorm;
}

其中 invSqrt() 是著名的“魔法数字”牛顿迭代法快速逆平方根:

float invSqrt(float x) {
    float halfx = 0.5f * x;
    float y = x;
    long i = *(long*)&y;
    i = 0x5f3759df - (i >> 1);
    y = *(float*)&i;
    y = y * (1.5f - (halfx * y * y));
    return y;
}

🤓 这个常数 0x5f3759df 曾震惊无数程序员,出自《雷神之锤III》源码,堪称经典优化!


四元数 → 欧拉角:给PID看懂的语言

Mahony输出的是四元数,但PID控制器通常需要欧拉角(Roll/Pitch/Yaw)。转换也很直接:

void QuaternionToEuler(float q0, float q1, float q2, float q3, 
                       float *roll, float *pitch, float *yaw) {
    *roll  = atan2(2*(q0*q1 + q2*q3), 1 - 2*(q1*q1 + q2*q2));
    *pitch = asin(2*(q0*q2 - q3*q1));
    *yaw   = atan2(2*(q0*q3 + q1*q2), 1 - 2*(q2*q2 + q3*q3));

    *roll  *= 180.0f / M_PI;
    *pitch *= 180.0f / M_PI;
    *yaw   *= 180.0f / M_PI;
}

注意: Yaw角无法仅由加速度计获得 ,因为它不随航向变化。若需航向稳定,必须引入磁力计或视觉/GPS辅助。


整体系统怎么搭?🔧

典型的飞控架构如下:

[遥控接收机] → [STM32F4]
                    ↓
             [MPU6050] ←→ 姿态解算 → PID控制器 → [PWM输出] → [电调] → [电机]
                    ↑
                [电源管理]

工作流程建议放在 定时器中断 中执行(如TIM6,周期2ms):

void TIM6_IRQHandler(void) {
    if (TIM6->SR & TIM_SR_UIF) {
        TIM6->SR &= ~TIM_SR_UIF;

        // 1. 读取原始数据
        MPU6050_GetRawData(&ax, &ay, &az, &gx, &gy, &gz);

        // 2. 单位转换 + 零偏补偿
        ax_scaled = ax / 16384.0f;  // 转为g
        gx_scaled = gx / 131.0f * M_PI/180.0f;  // 转为rad/s,并去零偏

        // 3. 更新姿态
        MadgwickAHRSupdate(gx_scaled, gy_scaled, gz_scaled,
                           ax_scaled, ay_scaled, az_scaled, 0.002f);

        // 4. 转欧拉角
        QuaternionToEuler(q0, q1, q2, q3, &roll, &pitch, &yaw);

        // 5. PID控制
        roll_output  = pid_calculate(&pid_roll,  target_roll,  roll);
        pitch_output = pid_calculate(&pid_pitch, target_pitch, pitch);
        yaw_output   = pid_calculate(&pid_yaw,   target_yaw,   yaw);

        // 6. 更新PWM占空比
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, base_throttle + roll_output - pitch_output - yaw_output);
        // ...其他通道类似
    }
}

这样就能维持 500Hz 的控制频率 ,确保系统响应迅速且稳定。


实战经验分享 ⚙️

别以为写完代码就万事大吉,实际调试才是重头戏。以下几点至关重要:

项目 经验之谈
传感器安装 必须紧贴重心,尽量水平放置,避免因杠杆效应放大振动
零偏校准 上电静置500ms,采集平均值作为初始偏移,后续持续在线校正
抗振处理 在PCB上加橡胶垫,或对加速度计数据做滑动平均滤波(窗口3~5点)
Kp参数调节 初始设为1.0,逐步增大直到出现高频抖动,再回调一点即可
电源去耦 MPU6050电源引脚务必并联0.1μF陶瓷电容,否则I²C容易丢包
编译优化 开启 -O2 -mfpu=fpv4-sp-d16 -mfloat-abi=hard ,发挥FPU全部性能

如果你发现飞机总是慢慢“抬头”或“侧倾”,大概率是加速度计没校准好;如果剧烈震荡,则可能是PID中D项过大或姿态更新延迟太高。


结语:从自稳到自主的起点 🚀

这套基于 STM32F4 + MPU6050 + Mahony + PID 的方案,看似简单,却是理解现代无人机控制的绝佳入口。它不仅让你搞懂“飞机是怎么知道自己歪了”的,更为后续升级打下坚实基础:

  • 加个HMC5883L磁力计?→ 实现航向锁定
  • 接入GPS模块?→ 做定点悬停和航线飞行
  • 换成EKF融合气压计、光流?→ 构建小型VIO系统
  • 移植到FreeRTOS?→ 实现多任务调度,支持地面站通信

每一步都不难,关键是先把底层姿态估计算清楚。毕竟,连自己都“站不稳”的机器人,谈何智能?

所以,下次当你看到一架无人机安静地悬停在空中,请记住:那不是魔法,是 四元数、PI反馈和精确计时共同编织的平衡艺术 ❤️。

现在,轮到你动手试试了 —— 你的飞行器,准备好起飞了吗?🚁

Logo

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

更多推荐