☰
STM32四旋翼飞控程序设计全解析:从姿态解算到串级PID
2026/10/21 18:00:02 网站建设 项目流程

飞控程序在很多人眼里是个神秘的东西:传感器数据进来,电机转速出去,中间就像黑盒。但实际上,把四旋翼飞行程序设计摊开来看,就是一个典型的嵌入式实时控制任务——读传感器、解算姿态、计算控制量、输出PWM,循环往复,而且每个环节都有清晰到近乎刻板的规矩。这篇文章我从头到尾梳理一遍基于STM32的四旋翼飞行程序设计,包含物理原理、硬件选型逻辑、姿态解算和串级PID的代码骨架,以及真机调试中那些文档里不会写的坑。适合正在做毕设或准备入门飞控的读者,也适合想从应用层往底层看一眼的开发爱好者。

1. 四旋翼的飞行动力学基础:程序设计的物理前提

1.1 两种机架布局:十字型与X型

四旋翼之所以叫四旋翼,是因为它靠四个独立转速的电机提供升力。但"四个电机怎么排"直接决定程序里动力分配方程长什么样。

最常见的两种布局是十字型和X型。十字型的前后左右各一个电机,机头方向和某一个电机臂对齐;X型则是机头方向在两个电机臂的角平分线上。从程序设计角度看,X型布局更常用,因为前置摄像头、云台等设备的安装空间不受电机臂阻挡,而且在横滚和俯仰方向上,每个动作都有两个电机共同参与,响应更平滑。

布局差异带来的直接后果是控制分配矩阵不同。同样是"想要俯仰前倾",十字型需要降低后方电机转速、提高前方电机转速;X型则需要同时调整四个电机的转速组合。这部分在程序中就是一个坐标映射关系,后续算出的期望推力、期望横滚力矩、俯仰力矩、偏航力矩,都会通过这个映射关系换算成4个电机的期望转速。

1.2 四种基本运动:升降、俯仰、横滚、偏航

四旋翼没有传统的舵面,一切运动都靠转速差实现。搞清楚这四种基本运动的力矩来源,程序里的控制目标才写得明白。

  • 垂直升降:四个电机同时增加或减小转速,总升力变化,机体垂直移动。
  • 俯仰(前后倾斜):前后两个电机产生转速差,机体会绕Y轴(横轴)旋转,产生前后方向的水平分力。
  • 横滚(左右倾斜):左右两个电机产生转速差,机体会绕X轴(纵轴)旋转,产生左右方向的水平分力。
  • 偏航(机头转向):利用旋翼反扭矩。四个电机同向旋转时,机身会受到与旋翼旋转方向相反的扭矩。如果让对角线上两个电机(逆时针旋)和另外两个电机(顺时针旋)产生转速差,反扭矩不平衡,机身就会绕Z轴转动。

这里有一个初学者非常容易忽略的点:电机转向是固定的,逆时针和顺时针各占两个,不能乱接。程序里动力分配是按"电机顺序+转向方向"写死的,如果接线时把转向搞错,飞控的所有校正都会失效,飞机会原地打转甚至直接翻。

1.3 从期望姿态到电机转速:控制链路的完整闭环

整段飞行程序的本质是三个闭环嵌套:角度环、角速度环、电机转速环(由电调完成)。传感器采集到角速度和加速度,姿态解算模块算出当前欧拉角,控制器把期望姿态和当前姿态的误差转换为期望角速度,再把角速度误差转换为期望力矩,最后通过动力分配模块,把期望力矩换算成4个PWM占空比。

用一句话概括:期望姿态 → 实况姿态=差 → 期望角速度 → 实况角速度=差 → 期望力矩 → 期望转速 → PWM。控制程序就是沿着这条链路跑的,每一环的输出频率和优先级都有讲究,后面第三节和第四节展开讲。

2. STM32在飞控中的任务分工与硬件选型

2.1 为什么是STM32而不是更高性能的处理器

飞控程序对主控的要求并不苛刻,但很全面:要有足够的定时器输出多路PWM,要能快速读取传感器(SPI或I2C),要在中断里完成姿态解算和控制算法,还有裕量去处理遥控信号和日志。

STM32F103系列在多数入门飞控里完全够用——72MHz主频、多个通用定时器、硬件SPI/I2C、丰富的中断引脚,价格也低。到了F405/F427这一档,主频升到168MHz,带了FPU(浮点运算单元),做姿态解算时浮点运算速度有明显优势,这也是很多商品飞控选择F4系列的原因。如果说F1是"刚刚好完成任务",F4就是"算起来更从容"。

我个人的建议是:入门阶段用F103完全没问题,但如果你准备在飞控上跑卡尔曼滤波、加光流、加视觉处理的前级数据融合,直接上带FPU的F4系列,省得后面换平台重写底层驱动。选型从来不是"性能越高越好",而是"外设够不够、生态熟不熟、调试方不方便"。

2.2 飞控硬件拓扑:传感器、电调、接收机还在哪里

手写飞控的硬件链路其实很简洁:主控板、惯性测量单元(IMU)、气压计、电调、接收机、电压检测,可能再加一个空速计或GPS。

典型的接口分配如下:

外设接口方式STM32外设资源
惯性测量单元(六轴/九轴)I2C 或 SPI硬件I2C / SPI1
气压计I2C 或 SPI与IMU共用总线或独立总线
4路电调信号PWM信号定时器CH1~CH4
接收机串口/IO(PPM或SBUS)USART(带DMA)
电压检测ADCADC1 或 ADC2
蜂鸣器/LEDGPIO任意GPIO

有一个经验之谈:IMU和气压计尽量走SPI,并挂在独立的片选引脚上。虽然I2C也能跑,但I2C速率上限低,而且IMU数据更新率较高,SPI总线能让传感器采集更从容。如果主控资源有限,IMU走SPI,气压计走I2C,分总线挂载,也能稳定跑。

2.3 中断优先级与实时性:姿态环为什么不能被"卡顿"

飞行程序对实时性要求最高的部分是角速度环。角速度环的输出直接决定电机转速,如果这个环路的周期抖动变大,电机响应就会忽快忽慢,飞机会出现肉眼可见的抖动甚至振荡。

所以中断优先级的设计原则是:IMU数据就绪中断要比串口接收中断优先级高,比日志存储任务优先级高。一个通用的做法是设置IMU中断为最高抢占优先级(硬件),PWM输出更新在定时器更新中断里完成,接收机信号解析放在普通优先级,日志和状态指示灯放在主循环后台跑。

在F103这类没有FPU的芯片上,姿态解算里的浮点运算会对中断产生一定的占用时间。如果主循环里同时做大量浮点操作,IMU中断响应可能被推迟。常见优化手段是把姿态解算放到IMU中断里做,或者用一个高优先级定时器中断触发解算——总之,不能让解算关键路径被阻塞。

3. 姿态解算:让飞控"知道"自己在空中什么姿势

3.1 欧拉角、四元数、方向余弦矩阵,我为什么选四元数

姿态描述有三种主流方式:欧拉角、方向余弦矩阵(DCM)、四元数。

欧拉角最直观——横滚角、俯仰角、偏航角,人一看就懂。但它有个致命问题:万向节死锁。当俯仰角接近±90度时,横滚和偏航的旋转轴会重合,姿态方程出现奇异,计算出的姿态不稳定。四旋翼飞行中俯仰角接近90度的场景很少,但特技飞行动作里确实可能遇到,而且使用欧拉角做姿态更新,涉及大量三角函数,计算量大。

四元数则用四个分量描述三维旋转,没有奇异点,计算量也小,只用乘法和加法就能完成姿态更新。它的缺点是"不直观",调试时需要把四元数转成欧拉角来看。

我的建议很直接:姿态解算内部一律用四元数做递推,只在数据输出层转成欧拉角,既保证了避开了万向锁的坑,调试也方便。如果只用DCM,在传感器噪声比较明显时,正交化修正的复杂度反而更高。

3.2 互补滤波的核心思想:用加速度计"纠偏"陀螺仪

陀螺仪测量角速度,积分得到角度。它的优点是动态响应快、不受加速度干扰,缺点是积分会漂移——陀螺仪零偏随时间累积,几秒钟后角度就偏得离谱。

加速度计测量重力方向,可以算出横滚角和俯仰角。它的优点是长期稳定,没有漂移,缺点是动态响应慢,而且飞行器加速或振动时会引入大量干扰。

互补滤波的做法就是"短期信陀螺仪,长期信加速度计":把陀螺仪的角度作为主体,把加速度计算出的角度当作修正量,通过一个比例系数慢慢拉回漂移。这个系数就是滤波器的时间常数,决定了"多快修正过来"。太大会让漂移难以抑制,太小又会让振动噪声渗透进来。

3.3 一段可用的姿态解算代码骨架

以MPU6050这类六轴传感器为例,实现一个Mahony互补滤波算法的核心步骤:

// 读取IMU原始数据(假设已经完成SPI/I2C读取) float gx = imu.gyro.x * DEG2RAD; // 陀螺仪,转为弧度/秒 float gy = imu.gyro.y * DEG2RAD; float gz = imu.gyro.z * DEG2RAD; float ax = imu.accel.x; // 加速度计,单位g float ay = imu.accel.y; float az = imu.accel.z; // 1. 根据四元数算出理论上的重力方向(这是关键) float halfvx = q1 * q3 - q0 * q2; float halfvy = q0 * q1 + q2 * q3; float halfvz = q0 * q0 - 0.5f + q3 * q3; // 2. 把加速度计实际测量的重力方向归一化 float norm = sqrt(ax * ax + ay * ay + az * az); ax /= norm; ay /= norm; az /= norm; // 3. 用向量叉积算出误差——实际与理论的偏差 float ex = (ay * halfvz - az * halfvy); float ey = (az * halfvx - ax * halfvz); float ez = (ax * halfvy - ay * halfvx); // 4. 误差通过比例积分,修正陀螺仪零偏 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) * 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. 再归一化四元数,防止累积误差导致非单位四元数 norm = sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm;

这段代码每执行一次,就是一次姿态更新。配合定时器中断,以500Hz或1kHz的速率跑解算,完全能保证控制环有足够的状态输入。

关键参数Kp和Ki的作用:Kp是加速度计的交叉修正比例,Ki用来消除陀螺仪常值偏置。Kp太大会导致振动噪声被放大,Kp太小则姿态漂移明显。通常Kp取值从0.1到0.5开始调,Ki取0.005到0.02,没有通吃所有的固定值,要拿实际数据说话。

3.4 传感器数据预处理:那些没人提醒你的细节

直接拿IMU原始数据进来解算,姿态基本上是要抖的。几个必须做的前置处理:

  • 陀螺仪单位统一:把原始ADC值除以灵敏度(如131、65.5等),换算成度/秒或弧度/秒。
  • 加速度计归一化:在解算里已经做了归一化,但要确保读数单位一致。
  • 零偏标定:陀螺仪静置时的平均输出就是零偏,首次上电后取100次采样求平均,存下来再减去。
  • 低通滤波:对加速度计数据做一阶低通,截止频率大约30Hz到50Hz,可以有效滤掉电机高频振动。

有一个容易忽略但影响巨大的点:IMU的安装朝向。MPU6050的芯片坐标轴不一定会和机架坐标轴对齐,尤其当你把IMU模块颠倒或转90度安装时,解算结果会整体偏转。程序里要有坐标轴交换和正负号校正的逻辑,把IMU坐标系映射到机体坐标系,这一步错了,后面整个控制环全白搭。

4. 双环PID控制:角度环和角速度环的配合

4.1 为什么是双环,而不是直接把角度误差映射成电机输出

如果只用角度误差做PID控制,会遇到一个典型问题:阻尼不足。角度误差很大,PID输出很大,电机猛烈加速,机体快速回中,但当机体回到水平位置时,角速度已经很大了,还会继续冲过去,于是来回振荡。你需要加入角速度反馈来提供"阻尼",让机身停在目标姿态附近。

这个"阻尼"正是角速度环干的事。所以标准的姿态控制结构是串级双环:

  • 外环(角度环):输入期望角度,输出期望角速度。频率通常在250Hz~500Hz。
  • 内环(角速度环):输入期望角速度,输出期望力矩/电机控制量。频率要求更高,建议500Hz~1kHz。

内环是"管刹车"的,外环是"管方向"的。角速度环响应快,角度环响应稍慢,两级串起来,系统的鲁棒性和手感都会比单环好很多。

4.2 串级PID的代码实现骨架

// 外环:角度环,输出期望角速度 float pid_angle_roll_outer(float target_roll, float current_roll, float dt) { float err = target_roll - current_roll; // 只使用P项就够了,积分容易造成超调 return Kp_angle * err; } // 内环:角速度环,输出控制量 float pid_rate_roll_inner(float target_rate, float current_rate, float dt) { float err = target_rate - current_rate; float output = Kp_rate * err + Ki_rate * rate_err_integral + Kd_rate * (err - last_rate_err) / dt; last_rate_err = err; return output; }

实际参数整定顺序:先调内环的P,让角速度能快速响应,然后再加D抑制振荡,最后才调外环的P。如果一上来就动外环,很容易出现"看起来响应很快,实际上藏着内环谐振"的假象。

积分项需要特别处理的场景是风扰和重心偏移。如果机体重心偏了,它总会往一边漂,单靠P和D拉不回来,这时需要积分项慢慢积累修正量。但积分对振动也敏感,所以积分要做限幅,并且只有在角速度误差比较小时才允许积分累积,否则一刹车就是猛烈的回弹。

4.3 动力分配:从三个期望力矩到四个电机转速

姿态控制输出是三个期望力矩(横滚、俯仰、偏航),加上期望推力,总共四个量,刚好对应四个电机。以X型布局为例,典型的动力分配公式:

motor_1 = throttle + roll + pitch - yaw; // 右前,顺时针 motor_2 = throttle - roll + pitch + yaw; // 左后,逆时针 motor_3 = throttle + roll - pitch + yaw; // 右后,逆时针 motor_4 = throttle - roll - pitch - yaw; // 左前,顺时针

这里的roll、pitch、yaw就是角速度环输出的直接控制量,正负号要和实际机架转向匹配。最后做一次限幅,确保输出在最小油门和最大油门之间,并且即使摇杆最小也有一个最低怠速值,防止电机停转导致姿态失控。

动力分配是所有上层控制器和物理世界的"最后握手",也是最容易因为机架转向划分不同而错的环节。我调试时习惯在电机架上贴上标签纸,每一步焊接后核对一次转向,等上电再验证一次,养成习惯能省很多排障时间。

4.4 油门曲线与电机响应

还有一个容易被忽略的问题:PWM占空比和电机实际升力之间的关系不是线性的。电调输出和转速基本线性,但转速和升力近似是二次关系。如果在程序里不加补偿,姿态控制器在悬停点附近的增益会偏弱,在高速爬升时增益会偏强,飞起来手感很奇怪。

简单做法是做悬停中点标定:在飞机绑住或手持的状态下,微调油门让姿态输出归零,找到悬停油门附近的值作为控制量映射的基准点,让PID输出围绕这个基准点做加减。这样即使没有精确的二次映射,控制效果也远超直接输出的方式。

5. 真机调试的安全设计与踩坑实录

5.1 解锁逻辑、紧急关停与电压保护

飞控程序里必须有一套"保险丝"逻辑,这比所有炫酷算法都重要。

我常用的解锁条件是:

  • 遥控油门在最低位。
  • 偏航摇杆在某个方向摆到最大并保持1~2秒。
  • 且当前没有报错。

满足这三个条件才允许电机响应PWM输出。同时设计一个紧急关停:油门在最低位且偏航反方向最大,电机立即停转。以前遇到过手忙脚乱收油门但飞机还是继续飞的情况,就是因为解锁逻辑没做紧急状态分支。

电压保护方面,用ADC持续检测电池电压,低于阈值三级报警——一级降低最大输出限制,二级发出持续蜂鸣,三级强制降落或直接切断动力。这个阈值要留余量,锂电池的空载电压和带载电压差很多,不能按空载算。用程序的滤波平滑电压读数,避免电机瞬间大电流导致的误报警。

5.2 调试工具链:串口日志、波形观察和飞行数据回放

飞控调试没有一套靠谱的数据观察手段,等于闭着眼睛开飞机。我用下来最有效的几样:

  • 串口日志:姿态角度、角速度环输出、电机输出值、遥控数据这些关键量,用DMA发送到地面站,即使不用专用地面站软件也能用串口终端观察。
  • 波形工具:把串口数据转成曲线,实时看姿态响应和PID输出是否顺滑、有没有极限振荡。这个比看数字直观得多。
  • 飞行回放:在飞行中把全部关键变量写入外部Flash或SD卡,落地后逐帧回放。炸机后唯一能还原现场的就是记录的数据。

调试时最优价值的事情是给每个PID输出加一个"后台记录标志位",记录几秒钟的超限次数和限幅触发次数。如果某一轴频繁触限,说明这一轴的响度能力不足或者期望值给得不合理。

5.3 最容易让飞控"抽风"的三个问题

第一是振动。桨叶轻微不平衡、电机轴承老化、机架共振,都会让加速度计读入大量高频噪声。表现是姿态解算结果在高频抖,内环还没反应,外环就已经开始乱动了。我的经验是:先做机械层面的减震,给IMU加减震棉垫或减震球,再配合程序里的低通滤波,不要指望滤波解决一切。

第二是数据时序。IMU数据和角度解算、PID更新没有做好同步,偶尔"错位"一次还好,错配频繁会让控制量出现毛刺。最典型的是I2C读取中断嵌套,导致某一次读取到半新半旧的数据。解决办法是把传感器采集放在一个确定的中断上下文里,用DMA+标志位保证每次读完再进入解算。

第三是电调行程校准问题。不同厂家的电调对PWM行程定义不同,有的完全从电机转动才开始响应,有的在油门最低时还在低速转动。如果不做一次"全行程校准",电机推力分配会严重不均。校准方法:解锁后将油门推到最高,等电调发出确认音,再拉回最低,完成行程范围学习。我每次换电调或重刷固件后都会重新校准一次。

5.4 现场调试的一个备查清单

真机试飞前,强制过一遍以下检查项,缺一个都可能有性命之忧:

  • 螺旋桨转向正确:同一对角线上两个电机转向一致,另一对转向相反。
  • 螺旋桨安装方向:正反桨别装反,装反了升力直接反着来。
  • 重心位置:电池尽量压在中心,偏得太多飞控的积分项可能拉不回来。
  • 飞控安装方向:程序里期望的姿态方向和传感器坐标轴方向一致。
  • 解锁测试:先不上桨,解锁推油门,确认电机转速平滑变化,没有异常抖动。
  • 上电顺序:先开发射机,再开接收机供电,防止接收机端的失控状态被捕捉。

这些检查项看着琐碎,但你可以在任何一次炸机事故里找到下意识漏掉的某一项。程序做得再完美,物理世界的一次疏忽就会让它变成碎片。

写在最后

从原理到代码,从选型到试飞,四旋翼飞行程序设计其实没有哪一步是"高不可攀"的。它考验的是把嵌入式外设、传感器数据处理、控制理论和真实机械约束串到一起的能力。我最初做这个项目时,光是调整IMU坐标轴映射就折腾了一个礼拜,等到第一次成功解锁、推油门、离地悬停的那几秒钟,一切都值了。

现在回头复盘,我会建议每个想做飞控的人先想清楚一件事:你是在做软件框架,还是在做算法,还是在做整机系统?同一个标题下,这三个方向的学习路径完全不同。先确定目标,再决定在哪个环节深挖,比如想做算法就去研究所和仿真,想做产品就多花时间在可靠性、安全策略和调试工具链上。

最后分享一个小工具经验:动量记录仪的数据里,最能反映飞控状态的是角速度曲线。如果角速度曲线平滑得像正弦波,说明PID参数舒服;如果出现密密麻麻的小毛刺,传感器数据还有问题需要处理。在真正把姿态解算的输入质量做好之前,调PID参数都是白费力气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询