资讯动态

STM32四轴飞控从零实现:MPU6050姿态解算与串级PID完整指南

发布时间:2026/10/16 4:14:17 来源:尧图企业网站定制
简介压缩包是一份基于STM32微控制器实现的无人机飞控工程面向嵌入式飞控开发初学者完整展示从底层外设驱动到上层控制算法的代码组织方式。文件共212个以C源程序与头文件为主40个C源文件、42个头文件同时包含标准外设库的启动、配置与中断处理文件以及编译过程中生成的中间文件和最终烧录文件整个资源包约4.53MB。从内容预览可见工程基于STM32F10x系列涉及通用输入输出、串口、串行外设接口、I2C总线、定时器、复位与时钟控制等外设驱动并含有独立的control控制模块便于理解传感器数据读取、姿态解算、遥控指令解析和PWM电机控制等关键环节。压缩包内还保留了编译产物与调试信息方便对照源码检查初始化流程和排错。目前已有2427人学习下载适合希望从零搭建飞控系统并研究STM32工程结构的开发者参考。1. STM32飞控从零自己写一套四轴姿态控制硬件选型到代码落地的完整复现路径说实话一提到“飞控”很多人第一反应是 Pixhawk 或者匿名飞控这类成熟开源项目觉得几千行的代码、复杂的卡尔曼滤波、各种传感器融合才是门槛。但如果你只是想搞明白姿态控制原理、想自己从底层写一套能在室内稳定悬停的四轴用 STM32 做个飞控反而是最合适的学习路径——它把你的注意力强制拉回到“传感器怎么读、姿态怎么解算、电机怎么转”这三件事上而不是迷失在 Linux、ROS、Mavlink 这些外围生态里。这套用 STM32 做的飞控核心价值就是给你一份能编译、能烧录、能起飞的最小可行飞控工程从 MPU6050 读取原始数据、互补滤波解算姿态、串级 PID 输出、PWM 控制电调全链路都有清晰的代码对应关系。适合至少会点 C 语言、懂一点 STM32 外设定时器、I2C、ADC基础的人。2. 传感器架构与硬件选型先解决数据从哪来、准不准的问题2.1 为什么选 MPU6050 而不是更贵的 ICM 系列飞控第一步不是写 PID而是确认传感器能给你足够可靠的数据。这套资源里默认用的是 MPU6050六轴三轴加速度计 三轴陀螺仪I2C 接口工作在 400kHz 快速模式下最高 1kHz 输出速率。选它不是因为它性能最好而是因为数据手册明确、寄存器结构简单、网上踩坑资料多调试起来不玄学。如果你在淘宝上买模块会发现大部分是 GY-521 或者 MPU6050 模块板上自带 LDO 稳压和上拉电阻。需要注意的一个细节是模块的 I2C 地址默认是 0x68但也有极少数厂家焊了 AD0 跳线使地址变成 0x69。资源里的驱动代码写死了 0x68如果你读数全是 0xFF先检查地址而不是怀疑代码。MPU6050 还有一个被称为水果机模式的数字运动处理器DMP官方库可以直接输出四元数很多初学者喜欢用 DMP 省掉自己写姿态解算。但这份资源里没有用 DMP而是用裸寄存器读取加速度、角速度原始数据然后自己在 MCU 上跑互补滤波解算。理由很直接DMP 把最关键的解算过程变成黑盒出了问题你根本不知道是传感器还是算法的问题也无法调试。真正自己写过一遍四元数更新和互补滤波之后你再回头去看 DMP 输出的四元数会清楚得多。2.2 I2C 初始化与原始数据读取的四个关键寄存器配置在代码层面I2C 的初始化时序要比大多数人想象得严格。MPU6050 上电后需要至少 30ms 的稳定时间然后主控要先写 PWR_MGMT_1地址 0x6B唤醒芯片接着设置陀螺仪量程再设置加速度计满量程范围。// mpu6050_init.c - 部分关键初始化代码 #define MPU6050_ADDR 0x68 #define PWR_MGMT_1 0x6B #define GYRO_CONFIG 0x1B #define ACCEL_CONFIG 0x1C #define ACCEL_XOUT_H 0x3B void MPU_Init(void) { // 步骤1: 延时确保上电稳定 HAL_Delay(50); // 步骤2: 唤醒芯片并选择时钟源为PLL X轴陀螺 // 寄存器地址0x6B, 值0x01表示用X轴陀螺作为参考时钟 uint8_t pwr_reg 0x01; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR 1, PWR_MGMT_1, I2C_MEMADD_SIZE_8BIT, pwr_reg, 1, 100); // 步骤3: 陀螺仪量程设置为±2000dps寄存器值0x03 // 对应灵敏度16.4 LSB/(dps)代码里后面换算弧度要用这个值 uint8_t gyro_reg 0x03; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR 1, GYRO_CONFIG, I2C_MEMADD_SIZE_8BIT, gyro_reg, 1, 100); // 步骤4: 加速度计量程设置为±16g寄存器值0x03 // 对应灵敏度2048 LSB/g注意量程越大精度越低 // 飞控上用±16g不合理建议改回±2g寄存器值0x00 uint8_t accel_reg 0x03; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR 1, ACCEL_CONFIG, I2C_MEMADD_SIZE_8BIT, accel_reg, 1, 100); }上面这段代码里的量程设置其实留了一个常见的坑加速度计满量程 16g 时灵敏度只有 2048 LSB/g意味着 1g 的重力加速度在垂直方向上的读数大约是 2048 左右浮动而你需要从这件读数里解算出俯仰角和横滚角精度会明显不够。我一般会把加速度计改成 ±2g寄存器值 0x00灵敏度直接变成 16384 LSB/g静态情况下角度分辨率明显改善。陀螺仪量程 ±2000dps 反而合理因为四轴飞行时角速度经常超过 500dps量程小会直接饱和导致数据削顶。数据读取的顺序也有讲究从 ACCEL_XOUT_H 开始连续读 14 个字节一次性把加速度三轴、温度、陀螺仪三轴全部取出。分两次读容易在高转速时读到前后两拍不一致的中间状态。// 连续读取14字节原始数据 uint8_t buf[14]; uint8_t reg ACCEL_XOUT_H; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR 1, reg, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); // 组装16位有符号数寄存器高位在前需要左移8位再或上低位 int16_t ax (int16_t)(buf[0] 8 | buf[1]); int16_t ay (int16_t)(buf[2] 8 | buf[3]); int16_t az (int16_t)(buf[4] 8 | buf[5]); int16_t gx (int16_t)(buf[8] 8 | buf[9]); int16_t gy (int16_t)(buf[10] 8 | buf[11]); int16_t gz (int16_t)(buf[12] 8 | buf[13]);这里需要特别解释一下为什么读地址只写了 ACCEL_XOUT_H 就能一次拿完所有数据MPU6050 的寄存器地址是按轴顺序连续排列的ACCEL_XOUT_H 是 0x3B往后数 13 个寄存器正好到陀螺仪的 Z 轴低位。I2C 的连续读特性允许你在一个事务里从起始地址依次读完主控只需要提供起始地址和读取长度。资源里的 I2C 读取频率是 1kHz也就是主循环每 1ms 进一次采样。3. 姿态解算与数据融合用互补滤波替代卡尔曼的实战选型逻辑3.1 加速度计和陀螺仪各自的死穴与互补原理传感器融合是整个飞控代码里最值得自己写一遍的部分。加速度计的问题在于它能测重力方向但机体只要有平移加速度加速度计的读数就不再单纯反映重力方向快速加减速时机体会得到一个错误的角度这正是电机转速突然变化时机体会猛偏的来源。陀螺仪没有这个问题积分出来的角度在短时间内非常平滑可靠但积分会漂移长时间后角度会缓慢偏离真实值并且没有任何参照来纠正。互补滤波就是在这两者之间做一个可变权重的折中让低速时陀螺仪的积分角度作为主信号但用加速度计算出的角度作为缓慢修正项把长期漂移拉回来。这种方案在四轴这种短时间机动、长时间悬停平稳的场景下效果非常接近卡尔曼滤波而计算量却少一个数量级。资源里用的是 Mahony 互补滤波框架它的核心思想是用 PI 反馈修正四元数偏差而不是简单地对欧拉角做一阶低通。为什么要绕这个远路因为直接对欧拉角做加权平滑会出现万向锁问题而且在 90 度俯仰角附近角度跳变会让平滑完全失效。四元数没有奇异的数学性质适合做全球面姿态表达。3.2 四元数与 Mahony 滤波的 C 实现逐行解读这一段是资源里值得反复读的核心代码我把每一步的数学含义拆开讲// mahony.c - Mahony互补滤波核心更新, 采样周期dt0.001s void Mahony_Update(float ax, float ay, float az, float gx, float gy, float gz, float dt) { float norm; float vx, vy, vz; float ex, ey, ez; float halfT 0.5f * dt; // 归一体化加速度计读数 // 这是必要的后续向量叉积需要比较方向向量长度无关紧要 norm sqrtf(ax*ax ay*ay az*az); ax / norm; ay / norm; az / norm; // 从当前四元数预测重力方向向量机体坐标系下 // 这个向量的数学来源是四元数旋转矩阵的第三列 vx 2.0f * (q1*q3 - q0*q2); vy 2.0f * (q0*q1 q2*q3); vz q0*q0 - q1*q1 - q2*q2 q3*q3; // 加速度计实测方向与预测方向做叉积 // 叉积结果就是误差偏转角方向指向需要修正的方向 ex (ay*vz - az*vy); ey (az*vx - ax*vz); ez (ax*vy - ay*vx); // PI补偿器比例项快速消除偏差 // 积分项消除稳态误差Ki必须很小太大容易引起震荡 exInt ex * Ki * dt; eyInt ey * Ki * dt; ezInt ez * Ki * dt; // 修正陀螺仪角速度再进行四元数一阶积分更新 gx gx Kp * ex exInt; gy gy Kp * ey eyInt; gz gz Kp * ez ezInt; 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 norm sqrtf(q0*q0 q1*q1 q2*q2 q3*q3); q0 / norm; q1 / norm; q2 / norm; q3 / norm; }整段代码的骨架就是“预测 - 测量 - 修正”的经典闭环结构。陀螺仪提供短周期的角速度预测加速度计作为外部测量叉积算出误差后套一个 PI 控制器来修正角速度最后用一个四元数微分方程做积分。有一个重要的工程细节这里四元数更新的积分步长用了半采样周期也就是 0.5ms这是四元数微分方程的数值积分结构决定的dt 传错会导致姿态更新频率不对。Kp 和 Ki 的取值直接影响滤波表现。资源里推荐的默认值是 Kp1.0Ki0.02头几次调试时直接用这两个值跑然后在静止状态下观察角度输出的噪声水平。如果你发现静止时角度仍然有小幅高频抖动说明 Kp 太大在放大陀螺仪噪声如果发现角度明显滞后于实际动作说明 Kp 太小响应慢。Ki 积分项主要是为了消除加速度计在持续倾斜时的稳态误差取太大在暴力机动后会振荡好几秒才收敛。3.3 从四元数到欧拉角的换算与边界处理姿态解算的最终输出通常需要给人看这时候要把四元数转成欧拉角。如果你直接在飞控代码里用 atan2 和 asin 换算有一个隐藏边界问题// quaternion_to_euler.c - 四元数转欧拉角单位弧度 #define RAD_TO_DEG 57.2957795f float roll atan2f(2.0f*(q0*q1 q2*q3), 1 - 2.0f*(q1*q1 q2*q2)) * RAD_TO_DEG; // asin的输入必须限幅到[-1, 1]之间 float pitch asinf(2.0f*(q0*q2 - q1*q3)) * RAD_TO_DEG; float yaw atan2f(2.0f*(q0*q3 q1*q2), 1 - 2.0f*(q2*q2 q3*q3)) * RAD_TO_DEG;pitch 计算时如果四元数在归一化之前没有处理干净2.0f*(q0q2 - q1q3) 这个值理论上应该在 [-1, 1] 区间内但当飞行器接近垂直状态时数值会靠近这个区间边界浮点误差可能导致它变成 1.000001此时 asinf 直接返回 NaN。这就是为什么要在转换之前强制限幅float val 2.0f*(q0*q2 - q1*q3); if (val 1.0f) val 1.0f; if (val -1.0f) val -1.0f; float pitch asinf(val) * RAD_TO_DEG;这个 bug 平时根本不会出现只有在你大机动把四轴拉到接近 90 度仰角时才触发而且触发后表现为屏幕上角度突然变成乱码。资源里这段代码直接把限幅写进转换函数属于血泪经验之后的改进。4. PID 控制闭环内环角速率加外环角度的串级结构怎么调参4.1 串级 PID 的内外环设计逻辑飞控的核心控制策略是串级 PID结构上分内外两个环路。外环是角度环输入是期望姿态角输出是期望角速度内环是角速度环输入是期望角速度输出是电机修正量。为什么要串级而不直接让角度环输出 PWM原因很简单四轴的动力学本质上是角速度被电机扭矩直接控制角度是角速度的积分如果你直接对角度做 PID相当于你把一个积分环节白送给了被控对象系统的响应迟滞和震荡倾向都会变大。外环的输出是内环的给定内环能更快地消除扰动这在阵风或螺旋桨气流冲击时比单环结构有明显优势。这里有一个绕不开的换算问题MPU6050 陀螺仪原始数据是 LSB需要先除以灵敏度换算成 °/sPID 的输出也不能直接当成 PWM 占空比。如果你使用的 STM32 定时器是 16 位分辨率、计数周期 10000那 PWM 的增量可以映射到 0% 到 100% 的部分区间。资源里做了一个粗略但实用的映射将 PID 输出钳位到 [-400, 400]再映射到 500 到 2500 微秒的脉宽范围。// pid.c - 串级PID核心, 外环角度环输出限幅後作为内环给定 typedef struct { float kp, ki, kd; float integral, last_error; float output_limit; } PID_t; // 外环角度PID: 输入期望角度与实际角度, 输出期望角速度 float PID_Angle_Update(PID_t *pid, float target, float actual, float dt) { float error target - actual; // 对角度误差做限幅防止大误差时积分饱和 float error_clamped fmaxf(-45.0f, fminf(45.0f, error)); pid-integral error_clamped * dt; // 积分限幅, 防止长时间超差后积分过大导致输出饱和 pid-integral fmaxf(-20.0f, fminf(20.0f, pid-integral)); float output pid-kp * error pid-ki * pid-integral pid-kd * (error - pid-last_error) / dt; pid-last_error error; // 外环输出作为内环给定限幅在合理角速度范围 return fmaxf(-200.0f, fminf(200.0f, output)); } // 内环角速度PID: 输入期望角速度(来自外环)与实际角速度 float PID_Rate_Update(PID_t *pid, float target, float actual, float dt) { float error target - actual; pid-integral error * dt; pid-integral fmaxf(-50.0f, fminf(50.0f, pid-integral)); float output pid-kp * error pid-ki * pid-integral pid-kd * (error - pid-last_error) / dt; pid-last_error error; // 内环直接输出的是油门的修正量微秒级别 return fmaxf(-300.0f, fminf(300.0f, output)); }内环和外环的 PID 参数是分开标定的。资源里给出的默认值是外环 P4.5I0.02D0.8内环 P2.5I0.05D0.3。从经验上说内环的 P 值可以一点点加直到用手轻轻掰动机架能感觉到明显的回正力且不震荡为止外环参数则负责最终的跟随效果。至于为什么外环的 I 那么小是因为角度环本身就有积分特性I 的作用只是消掉静态偏差给大了反而在转动过程中引入超调。4.2 电机混控输出十字布局的 PWM 分配矩阵有了 PID 输出的修正量后还必须把三轴修正量映射到四个电机上这个过程叫混控。资源里用的是经典十字飞行模式四个电机分别负责两组相对方向的力矩组合。如果你手里是 X 型机架映射关系要重新推导X 型每个电机同时承担俯仰和横滚的分量不能用同一个矩阵。// motor_mix.c - X型四轴混控矩阵 // motor 1前右, motor 2后左, motor 3前左, motor 4后右 #define MOTOR_MIN 1000 #define MOTOR_MAX 2000 uint32_t throttle_cmd 1500; // 遥控器油门通道转换为微秒脉宽值 int32_t m1 throttle_cmd roll_output - pitch_output yaw_output; int32_t m2 throttle_cmd - roll_output pitch_output yaw_output; int32_t m3 throttle_cmd roll_output pitch_output - yaw_output; int32_t m4 throttle_cmd - roll_output - pitch_output - yaw_output; // 限幅必须在混控之后做, 而且不能简单的截断 // 需要做“差分限幅”如果超出范围, 将所有电机等量平移回范围 int32_t max_m fmax(fmax(m1, m2), fmax(m3, m4)); int32_t min_m fmin(fmin(m1, m2), fmin(m3, m4)); if (max_m MOTOR_MAX) { int32_t shift max_m - MOTOR_MAX; m1 - shift; m2 - shift; m3 - shift; m4 - shift; } if (min_m MOTOR_MIN) { int32_t shift MOTOR_MIN - min_m; m1 shift; m2 shift; m3 shift; m4 shift; }混控里最容易翻车的点是限幅的顺序。很多人直接对每个电机单独 clamp 到 [1000, 2000]这样会导致一个严重问题当你做了大幅横滚操作一侧电机被截断到最小值 1000同时另一侧也被截断到最大值 2000但如果两者都需要再增大呢单独截断会丢失差分关系导致倾斜角度无法继续增加。正确做法是一致平移保持所有电机之间的差值不变这样至少保持了姿态控制的线性关系。另一个容易被忽视的细节是油门映射如果你用的是普通遥控器油门通道占空比映射到 [1000, 2000]但要给 PID 修正留出余量混控前最好把映射范围限制到 [1100, 1900]这样即使 PID 修正量有 ±300 的冲击也不会瞬间撞到上下限。4.3 电子调速器和电机校准的同步时机代码写完后如果不校准电调就直接上电大概率会碰到电机转不起来或者油门曲线严重不均匀的问题。绝大多数便宜的电调在首次上电时需要进入校准模式这一步不在 STM32 代码里而是在上电时序里上电时油门信号必须保持最高 2000 微秒 3 秒以上电调会发出一段确认音然后把油门信号降到最低 1000 微秒电调再次提示后完成校准。但如果直接用飞控的 PWM 输出启动电调可能处于识别模式导致校准失败。资源里建议先用一个独立的信号源校准完所有电调再接入飞控输出。5. 姿态环调试避坑采样率、滤波参数与飞控供电的三个隐蔽陷阱5.1 现象姿态角静止时漂移越来越快这个现象出现的原因是陀螺仪零飘在作祟。很多初学者在代码里直接用了 MPU6050 的原始角速度数据做四元数更新没做任何去零飘处理。MPU6050 在静止时角速度读数并不是 0而是某个几十甚至几百 LSB 的偏置这个偏置在积分过程中不断累积导致姿态角缓慢但不可逆地漂移。解决的办法是在起飞前采集一段静止数据求平均把偏置存下来运行时减去偏置再参与解算。资源里的做法是上电后强制飞控静止 2 秒采集 1000 组陀螺仪数据求平均值作为零漂补偿值。在代码里注意一点补偿值要在姿态解算开始前就放到全局变量里而且不能每帧都更新否则会把真实角速度信息也抹掉。5.2 现象电机转速提高后IMU 数据出现周期性尖峰这种尖峰基本都是电机换向时的高频干扰通过地线耦合进了 I2C 总线。飞控和电调共用一个电源的时候特别容易出现电调是开关负载工作时会产生很大的电流波动如果 IMU 模块离电调近或者接地阻抗偏高I2C 上的数据位就会被干扰翻转。解决手段按顺序试第一信号线尽量短且远离电机供电线第二在 MPU6050 电源引脚附近加一个 10uF 钽电容和 0.1uF 陶瓷电容做去耦第三I2C 的 SDA/SCL 串一个 100 欧姆串联电阻抑制振铃最后如果还不行就把 I2C 速率从 400kHz 降到 100kHzFlySky 接收机就是这么干的。5.3 现象同一个 PID 参数换一块电池后飞行手感完全不同这个问题的根源在于电池电压变化导致电机响应不再一致。新电池充满时是 12.6V空载后电压可能落到 11V 以下同样的 PWM 占空比在不同电压下对应的电机转速差异很大而你 PID 输出里的修正量无法感知这种差异。所以在混控代码中应该用当前电池电压做油门补偿把油门通道的微秒值乘以一个电流电压比例系数低电压时自动增加脉宽来维持相同的电机转速。实现上ADC 采样电池电压后常见做法是约定一个参考电压比如 12V 对应油门不变低于 12V 按比例增油门。切记油门补偿只作用于油门基值不要作用在 PID 修正量上否则控制环路增益会被电压波动直接干扰。5.4 现象用手掰机架时角度响应很快但一飞起来就振荡这是调参的一个经典矛盾。手掰机架时你施加的是外部强迫运动飞机的回正力和阻尼是正常反馈回路但在空中螺旋桨的气流是持续扰动且四个电机的响应时延会让环路的相位裕度变小。解决思路是用记录飞行数据的方式来调参而不是单靠手感和眼睛。资源里加了一段飞行日志功能把目标角度、实际角度、PID 输出三个值以 200Hz 频率记录到串口利用匿名上位机的绘图窗口观察曲线如果实际角度跟随目标角度时出现过冲并反向就是 P 太高如果在目标角度附近缓慢爬向设定值、伴有持续的余差就是积分不够。6. 从稳住到能飞用飞行日志调参的实战收尾PID 参数最终要达到的效果是阻尼适中的临界状态角度环的阶跃响应大约 200 到 300 毫秒收敛到目标角度的 90%过程中没有超过目标角度 5% 的超调。我从第一次起飞到现在总结出一条核心习惯不要相信手感和记忆每次试飞都开日志。具体操作是在代码里把 IMU 姿态角、外环输出、内环输出、电机混控油门值打包成二进制帧通过串口以 115200 波特率发到地面站飞行结束后把日志拉出来看曲线用数据而不是感觉做决定。一个非常实用的调参顺序是先调内环角速度 P让飞行器手动模式下能快速回中但不抖动如果回中后手臂左右摆动超过两次才停下说明 P 太大或 D 太小再调外环角度 P目标是悬停时姿态角的波动幅度不超过 2 度最后加一点外环 D 用来抑制悬停时的高频抖动D 值过大的表现是电机声音变得尖锐摸机架能感到高频振手这时候必须降 D。还有一点容易被忽略飞行日志里的采样率不要太贪。如果同一时间点把 12 个通道数据全发出去串口带宽不够数据会被截断出现错误的尖峰反而掩盖真实波形。我一般只记录姿态内环相关的六到八个变量采样率设为 200Hz足够看到振荡的细节又不至于数据溢出。把那套 STM32 飞控的代码从头到尾重新拉通之后我现在每做一个新的飞控项目都会沿用这套调试流程上电静止两秒采集零漂校准、开串口日志、强制掰机架看响应、试飞拉日志、根据曲线关掉一个多余的 D 再飞一次。这个过程听起来慢但它能让你每次迭代都看到具体的改进方向而不是陷入反复盲调参数的死循环。希望这份资源里的代码和调试思路能帮你少走一圈弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑