ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32微型无人机飞控实战:姿态解算与串级PID调参指南

STM32微型无人机飞控实战:姿态解算与串级PID调参指南 简介一套基于STM32单片机的微型无人机完整工程源代码与文档说明齐备。项目以FreeRTOS为操作系统进行重构覆盖无人机底层驱动、任务调度与遥控器代码移植等关键环节适合嵌入式、自动化、电子信息等专业学生作为毕设或课程设计参考也适合有一定基础的开发者学习RTOS在飞控中的实际应用。压缩包共118个文件主体为53个C源文件和49个头文件另有汇编启动文件、文本说明、工程配置uvprojx/uvoptx及README等整体体积仅660KB文件结构清晰便于按模块查阅。已有163人学习下载代码经测试可运行具备较高的参考价值。通过该工程可了解STM32外设配置、FreeRTOS任务创建与通信机制以及无人机项目从工程搭建到代码移植的思路。1. STM32凭什么成为微型无人机飞控的主流选择很多第一次做微型无人机的人会纠结同一个问题一块STM32F103C8T6主频72MHz连浮点运算单元都没有跑姿态解算和控制算法真的够用吗答案是可以而且在MCU选型这个环节STM32几乎是微型无人机最稳的起点。Arduino生态确实上手快但当你需要同时处理PWM输出、DMA传输、硬件I2C、定时器捕获和多路中断的时候STM32外设的丰富程度和低延迟中断响应才是四轴这种强实时任务里真正靠得住的东西。标题里的“源代码文档说明”说明你拿到的不只是一块裸板而是一套完整的飞控工程。本文要做的是三件事把硬件最小系统搭起来、把姿态解算和控制律跑通、把源码和文档看明白之后能改出属于你自己的参数。这套路径适合已经会点灯、写过GPIO的STM32入门者也适合想从模块化代码里看软件架构的嵌入式工程师。全文不绑定某个具体开发板STM32F1或F4系列都能按同样的思路迁移。2. 硬件最小系统选型、接线与底层驱动2.1 核心选型F103C8T6还是F411CEU6先看微型无人机场景下几颗常用芯片的对比型号主频SRAMFlash适用场景STM32F103C8T672MHz20KB64KB入门、资料多、成本低STM32F401CCU684MHz64KB256KB需要低功耗、带FPUSTM32F411CEU6100MHz128KB512KB跑FreeRTOS、扩展功能多F103C8T6没有FPU但Cortex-M3内核在500Hz控制频率下跑定点姿态解算绰绰有余。真正决定选型的往往是资料密度MPU6050的DMP库、NRF24L01的驱动、各类显示和通信例程几乎都基于F1系列你拿到的源代码大概率也是在这个平台上验证过的。F411的优势在于带硬件浮点单元Mahony算法里的sqrtf和四元数运算会快很多但代价是价格和功耗。我的建议是预算有限或第一次做选F103想留出余量跑光流传感器或者以后上RTOS直接上F411。2.1.1 为什么不用更便宜的51或者ESP3251单片机不是不能飞但PWM分辨率、定时器数量和中断响应速度都不适合做四轴这种需要精确同步的任务。ESP32主频高、Wi-Fi方便但它的实时性受FreeRTOS调度和Wi-Fi协议栈干扰不适合直接做飞控更适合做图传或地面站通信。2.2 最小系统接线与传感器连接微型无人机的基础传感器配置是MPU6050六轴IMU加上一颗气压计可选无线部分用NRF24L01或普通遥控接收机。I2C总线上挂IMU和气压计SPI接NRF24L01PWM输出到四路MOS管驱动空心杯电机。接线表功能引脚说明I2C1_SCLPB6接MPU6050 SCL4.7k上拉I2C1_SDAPB7接MPU6050 SDA4.7k上拉SPI1_SCKPA5NRF24L01时钟SPI1_MOSIPA7NRF24L01数据输入NRF_CEPA8片选使能NRF_CSNPA4SPI片选TIM2_CH1~CH4PA0~PA3四路PWM输出接线时特别注意CE和CSN不能复用很多人在这里栽跟头CE位接错导致无线模块一直处于待机模式收不到遥控数据解锁后电机没反应。2.2.1 供电与电机噪声微型无人机最容易忽略的是电源设计。空心杯电机启动瞬间电流尖峰很大如果IMU和MCU共用一条电源线MPU6050的I2C数据会产生毛刺姿态解算直接漂掉。常见做法是电池电压先过一个大电容470uF以上再分两路——一路经过LDO降到3.3V给MCU和传感器另一路直接给电机桥。MCU电源入口加100nF10uF陶瓷电容组合。如果出现下载失败或“No STM32 target found”这种报错先查SWD四根线再看电机电源是否干扰了调试口。2.3 底层驱动的分层方式拿到源代码后第一件事是看懂工程结构。我一般会把驱动分成三层HAL层GPIO、I2C、SPI初始化、传感器驱动层MPU6050读写、气压计读取、应用层姿态解算、控制律、混控输出。这种分层的好处是换传感器时只改中间层不会牵动控制逻辑。MPU6050的读取代码示例// mpu6050.c #define MPU6050_ADDR 0x68 // 从MPU6050指定寄存器连续读取len个字节到buf uint8_t MPU6050_ReadRegs(uint8_t reg, uint8_t *buf, uint16_t len) { if (HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR1, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100) ! HAL_OK) { return 1; // 读失败 } return 0; }这段代码的关键在HAL_I2C_Mem_Read接口。第二个参数reg是寄存器地址比如加速度计X轴高字节的地址是0x3BMPU6050支持连续读即从0x3B开始一次读6个字节就能拿到三轴加速度原始值。最后一个参数100是超时时间单位毫秒。如果总线上挂了多个I2C设备要确认地址不冲突MPU6050的ADR0引脚拉低时地址是0x68拉高时是0x69。文档说明里如果写了引脚映射表开发时先核对这张表远比追原理图高效。3. 姿态解算把原始数据变成欧拉角3.1 加速度计和陀螺仪各自的坑加速度计在静止或匀速运动时能精确给出重力方向误差不随时间累积但微型无人机一旦有平移加速度惯性力会叠加到重力上算出来的角度就会偏。陀螺仪测的是角速度积分后得到角度动态响应快且不受线加速度影响但积分误差会随着温度变化和时间推移不断累积。所以姿态解算的本质不是“选哪个传感器”而是“怎么把两个都不完美的信号融合成一个可信的角度”。这就是互补滤波和Mahony算法存在的意义。3.2 互补滤波的原理和代码互补滤波的思路在频域上很直观陀螺仪积分得到的角度信任高频部分动态变化快加速度计计算的角度信任低频部分长时间稳定。离散形式angle alpha * (angle gyro * dt) (1 - alpha) * accel_anglealpha通常取0.95到0.98dt是解算周期。alpha越大越信任陀螺仪动态响应好但长时间会漂移alpha越小越信任加速度计长时间稳定但动态会有滞后。// complementary_filter.c #define ALPHA 0.98f void Complementary_Filter(float *angle, float gyro_rate, float accel_angle, float dt) { // gyro_rate是当前轴的角速度accel_angle是加速度计算出的角度 *angle ALPHA * (*angle gyro_rate * dt) (1.0f - ALPHA) * accel_angle; }注意这里dt不能省略它决定了积分步长。如果你在500Hz的定时器中断里调用这个函数dt就是0.002秒。如果实际调用频率和传参不一致角度会在快速转动时出现明显的滞后或超前。3.2.1 Mahony算法为什么更适合四轴互补滤波对三轴是分别独立处理的roll、pitch、yaw各算各的。但无人机在三维空间里旋转时轴与轴之间是耦合的。Mahony算法用四元数表示姿态把三轴当成一个整体来更新天然避免了欧拉角的万向锁问题。Mahony的核心代码片段// mahony.c void Mahony_Update(float gx, float gy, float gz, float ax, float ay, float az, float dt) { float norm; float halfex, halfey, halfez; float q0q0, q0q1, q0q2, q0q3; float q1q1, q1q2, q1q3; float q2q2, q2q3, q3q3; float vx, vy, vz; float ex, ey, ez; // 加速度计读数归一化消除尺度影响 norm sqrtf(ax * ax ay * ay az * az); if (norm 0.0f) return; ax / norm; ay / norm; az / norm; // 用当前四元数估算重力方向 q0q0 q0 * q0; q0q1 q0 * q1; q0q2 q0 * q2; q0q3 q0 * q3; q1q1 q1 * q1; q1q2 q1 * q2; q1q3 q1 * q3; q2q2 q2 * q2; q2q3 q2 * q3; q3q3 q3 * q3; vx 2.0f * (q1q3 - q0q2); vy 2.0f * (q0q1 q2q3); vz q0q0 - q1q1 - q2q2 q3q3; // 叉积误差陀螺仪积分出的姿态和加速度计观测姿态的差 ex (ay * vz - az * vy); ey (az * vx - ax * vz); ez (ax * vy - ay * vx); // PI补偿陀螺仪零漂kP和kI需要按实际传感器调 gx kP * ex exInt; gy kP * ey eyInt; gz kP * ez ezInt; // 四元数一阶积分更新 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; // 四元数归一化防止累积误差导致模长偏离1 norm sqrtf(q0 * q0 q1 * q1 q2 * q2 q3 * q3); q0 / norm; q1 / norm; q2 / norm; q3 / norm; }这段代码里最需要理解的是“叉积误差”。陀螺仪积分出来的姿态给出一个预估的重力方向vx, vy, vz加速度计直接测得一个真实的重力方向ax, ay, az两者的叉积就是误差信号。这个误差通过PI补偿器修正陀螺仪角速度等效于实时估计并消除陀螺仪零漂。kP控制修正速度kI控制零漂估计的收敛速度。一般kP取2.0、kI取0.05到0.1起步太大飞机会高频抖动太小悬停姿态会缓慢漂移。3.3 量程设置与单位换算MPU6050加速度计量程选±2g时灵敏度是16384 LSB/g选±16g时变成2048 LSB/g。微型无人机正常飞行g值不会超过±2g所以选±2g能获得最高分辨率。陀螺仪同理±250°/s对应131 LSB/(°/s)足够覆盖微型四轴的大部分飞行动作。// mpu6050_init.c uint8_t val; // 加速度计量程 ±2g寄存器地址0x1C默认值0x00 val 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR1, 0x1C, I2C_MEMADD_SIZE_8BIT, val, 1, 100); // 陀螺仪量程 ±250°/s寄存器地址0x1B默认值0x00 val 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR1, 0x1B, I2C_MEMADD_SIZE_8BIT, val, 1, 100);这段配置里两个寄存器都有对应的位域ACCEL_CONFIG的FS_SEL位和GYRO_CONFIG的FS_SEL位。改成0x08就是±4g和±500°/s改成0x10就是±8g和±1000°/s。量程扩大一倍噪声也会被放大到同一个比例所以不是量程越大越好。代码里读取原始值后需要除以灵敏度才能得到物理量比如加速度计的原始值除以16384就是g值。4. 控制律串级PID让飞机站得住4.1 单级PID为什么不够用如果只用角度环做PID控制外界的瞬时扰动比如一阵风先改变角度角度差变大控制器才输出修正量响应天然慢半拍。串级PID在角度环里面多套了一个角速度环外环输出的是“目标角速度”内环专门管角速度追踪。风一吹角速度先被内环压住角度还没来得及大偏机身已经稳定住了。这个结构在飞控里非常标准外环角度环、内环角速度环。内环频率高1kHz外环频率可以降到500Hz因为角度变化比角速度变化慢得多。4.2 串级PID代码结构// pid.c typedef struct { float kP; float kI; float kD; float integral; float last_error; } PID_t; // 姿态环串级控制单位角度用度角速度用度/秒 float PID_AngleRate(float target_angle, float angle, float gyro_rate, float dt) { // 外环角度差转换成目标角速度 float err_angle target_angle - angle; float target_rate err_angle * pid_angle.kP; // 内环角速度差转换成PWM输出 float err_rate target_rate - gyro_rate; pid_rate.integral err_rate * dt; // 积分限幅防止Windup if (pid_rate.integral 100.0f) pid_rate.integral 100.0f; if (pid_rate.integral -100.0f) pid_rate.integral -100.0f; float d_term pid_rate.kD * (err_rate - pid_rate.last_error) / dt; pid_rate.last_error err_rate; return err_rate * pid_rate.kP pid_rate.kI * pid_rate.integral d_term; }代码分成明显的两段。外环只用P因为角速度内环会处理大部分动态误差外环加D容易放大噪声。内环的I项用来消除稳态误差——比如飞机重心稍微偏前内环需要一个恒定的输出才能维持姿态这个输出就是积分项给的。D项在这里用的是误差差分再除以dt得到一个近似的误差变化率用来抑制角速度突变。积分限幅是必须的如果不加无人机被压在桌上时角度误差一直存在积分不断累积一解锁就会猛冲出去。4.2.1 电机混控PID输出怎么变成四个电机的转速PID输出的其实是roll、pitch、yaw三个轴的修正量还需要叠加到油门基准值上再按照电机的几何布局分配下去。X型四轴的常见混控// motor_mix.c // motor[0]~[3]对应四个电机输出值是PWM占空比 motor[0] throttle output_roll output_pitch - output_yaw; motor[1] throttle - output_roll output_pitch output_yaw; motor[2] throttle output_roll - output_pitch output_yaw; motor[3] throttle - output_roll - output_pitch - output_yaw;这里的符号取决于电机布局和转向。标准X型四轴中左前和右后的电机顺时针旋转右前和左后的电机逆时针旋转。yaw修正量的符号就要配合这个转向否则遥控打方向时飞机会转反。混控输出需要做限幅比如限制在1000到2000us的PWM脉宽范围内超出部分直接截断。千万不要让混控输出有负值那会导致电机反转四轴直接失控。4.3 PID参数整定的顺序和方法整定顺序铁律先内环后外环。先不给外环目标角度直接把飞机固定在支架上用手给一个扰动观察角速度回稳的速度和超调。具体步骤内环kP从小往大加每次增加20%直到飞机出现高频抖振然后回调到抖振值的60%作为初始值加内环kI消除静态误差注意积分限幅不要太大内环稳定后解锁外环角度环同样kP从小往大加观察角度阶跃响应是否过冲最后调节外环D抑制角度回正时的过冲但D太大会放大传感器噪声现象导致原因处理方式高频抖振嗡嗡声内环kP过大减小内环kP低频摇摆内环kI过大或外环kP不足减小kI或增大外环kP电机发热严重kD过大减小kD悬停漂移内环kI不足或传感器零偏没校准增大kI或先做陀螺仪零偏校准一个容易踩的坑是拿三个轴的同一组PID参数去飞。roll和pitch由于飞机结构对称参数可以接近但yaw轴的转动惯量和响应特性完全不同需要单独整定。5. 源代码组织与文档说明从工程里快速找到你要的东西5.1 工程目录结构怎么看典型的STM32无人机工程长的样子UAV/ ├─ Core/ // 启动文件、系统时钟、中断服务 ├─ Drivers/ // HAL库、CMSIS ├─ App/ // 传感器驱动、姿态解算、控制律 ├─ Middlewares/ // 无线通信协议、气压计库 ├─ Docs/ // 文档说明、接线图、参数表 └─ MDK-ARM/ // Keil工程文件拿到源代码后不要从第一行逐行读先打开Docs目录下的文档说明找出三张表引脚映射表、PID初值表、遥控通道映射表。这三张表决定了这个工程能不能在你的硬件上跑起来。没有文档的情况下就从App目录下的main.c开始追调用链看是裸机定时器中断驱动还是FreeRTOS任务调度。5.2 裸机还是RTOS的影响微型无人机的控制任务必须是硬实时的。裸机方案里定时器中断里完成传感器读取、姿态解算和控制输出死循环里只做无线接收和状态机切换。这种结构的时序确定性强加上中断优先级设置——传感器读取优先级高于无线接收——控制抖动可以控制在微秒级别。FreeRTOS方案把每个功能拆成独立任务代码好读但任务调度和队列的延迟是不确定的需要用高优先级任务嵌套中断来保证控制频率。对于刚接触飞控源码的开发者裸机项目更容易读懂控制流。5.3 Keil工程编译的常见错误处理拿到源代码第一件事不是改代码而是先编译通过。STM32CubeIDE或Keil里打开工程后依次做三件事确认芯片型号宏定义如STM32F103xB检查Flash大小和SRAM配置是否匹配你的芯片确认调试器设置——使用ST-LINK时选SWD模式。如果下载时报“No STM32 target found”SWD接线和复位电路是排查重点。需要按住板子的复位键再点下载等开始擦除Flash的瞬间松手这是一个屡试不爽的土办法。5.4 文档说明里最容易被忽略的内容很多文档说明会在附录里写传感器校准方法和已知问题列表。MPU6050出厂自带一组陀螺仪偏移参数但实际焊接后每个轴的偏移都会不同需要上电静止状态下采集几千个样本取平均作为零偏。如果源代码里已经有了校准逻辑飞之前先在水平桌面上完成一次完整校准流程。已知问题列表通常写了作者踩过的坑——比如某一版本的I2C时序在某些F103芯片上会导致偶发丢数据这类信息在调试阶段能帮你省下大量时间。6. 进阶技巧用串口日志调PID而不是靠手感6.1 把实时姿态和控制输出打出来靠眼睛看飞机反应调PID只能判断“稳不稳”很难判断是哪一个环导致的振荡。常见做法是加一个调试输出目标角度、实际角度、PID输出三者同时打印// debug_printf.c // 用printf重定向到串口50Hz发送一次调试数据 printf(T:%d A:%d O:%d\r\n, target_angle, angle, pid_output);在PC端用串口助手查看只是最原始的方案想要看到曲线直接用Python的pyserial和matplotlibimport serial import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200, timeout1) targets, angles, outputs [], [], [] for line in ser: # 读取串口的一行文本 # 解析格式 T:20 A:18 O:30 parts line.decode().strip().split() targets.append(float(parts[0].split(:)[1])) angles.append(float(parts[1].split(:)[1])) outputs.append(float(parts[2].split(:)[1])) if len(angles) 500: break plt.plot(targets, labeltarget) plt.plot(angles, labelangle) plt.plot(outputs, labeloutput) plt.legend() plt.show()这段脚本实时读取串口数据并画成三条曲线。注意115200波特率下每秒最多传约11KB如果一行有20字节那最高只能传550行/秒实际用50Hz足够了。曲线里能直接看出目标角度变化后实际角度的延迟、过冲以及PID输出是否饱和。6.2 从日志判断PID状态的三个指标第一阶跃响应的过冲不能超过30%。给一个20度的阶跃指令角度冲到26度以上说明kP过大或外环D不够。第二角速度曲线不能出现20Hz以上的高频振荡如果有优先检查内环kD和控制频率是否匹配。第三悬停时roll和pitch的实际角度应与目标角度差在正负2度以内超过这个范围检查传感器零偏校准和重心位置而不是继续加kI。6.3 黑匣子记录要放在解锁前更可靠的做法是把数据写入STM32内部的Flash飞行结束后再通过串口导出。这样做的好处是飞行过程中不占用串口带宽也不受无线干扰缺点是Flash擦写寿命有限一般只记录最后30秒数据。记录的时机要设计好不要解锁后才开始记否则起飞瞬间的数据会丢失。正确做法是上电后一直记录到环形缓冲区解锁时打一个标记关机后导出带标记的段落。最后说一个文档说明里一般不会写但每个老手都会检查的点PID输出到PWM比较寄存器之前一定要做电机怠速判断。很多微型无人机用的是无刷电调解锁状态下如果油门值低于启动阈值电机应该完全停转而不是输出一个PWM值让它在那里“半转不转”。检查三个方面油门最小输出时PWM是否完全关闭、积分限幅有没有上限、解锁前PID输出是否被强制清零。这三个点没有确认之前不要在户外试飞。本文还有配套的精品资源点击获取
返回列表