基于STM32的无线四轴飞行器飞控与遥控器系统设计指南
2026/9/16 13:41:26 网站建设 项目流程

简介:基于STM32单片机设计的无线四轴飞行器与飞控遥控器系统,分成飞行器和遥控器两大部分,面向电子竞赛、无人机入门及嵌入式开发者。以STM32为核心,配合NRF24L01无线模块实现遥控指令传输,采用MPU6050姿态传感器完成飞行姿态检测与稳定控制,并附有详细操作说明,涵盖螺旋桨安装、飞控初始化、遥控器解锁及IMU校准等实际使用环节。资源包共673个文件,约10.96MB,主要包含208个.h头文件、195个.c源文件、原理图、PCB图及hex固件等,既有完整工程源码,也有硬件设计参考,适合从软硬件两方面学习。目前已有172人浏览学习。通过这套资料,可快速搭建四轴飞行器原型,理解STM32在无人机飞控、无线通信和传感器融合中的典型应用,也为后续扩展遥控器功能提供良好基础。

1. 基于 STM32 单片机实现无线四轴飞行器飞控与遥控器系统的设计骨架

很多人在四轴飞行器项目里栽跟头,并不是因为 PID 调不出来,而是被"飞控"和"遥控器"这两个字带偏了方向。基于 STM32 单片机设计的无线四轴飞行器、飞控遥控器系统,拆开看其实是两套独立的嵌入式设备:机身上的飞控板负责姿态解算和电机混控,手持的遥控器板负责摇杆采集和无线数据下发,两者靠一条 2.4G 无线链路维持 50Hz 到 100Hz 的控制周期。对刚接触四轴的工程师来说,最常踩的坑是先把姿态解算写得天花乱坠,结果电机混控输出和无线帧结构完全没对齐,飞机在地面就翻肚皮。这个项目的正确做法是从系统顶层画两条数据流:遥控器 → 无线模块 → 飞控 → 电机,以及飞控 → 无线模块 → 遥控器(回传姿态和电量),再顺着数据流把硬件原理图、PCB 和源码逐层填满。适合的读者是准备毕业设计、电子竞赛,或者第一次接触 IMU 传感器和串级 PID 的嵌入式从业者。

2. 系统架构与核心器件选型:在原理图阶段定死通信和供电边界

2.1 飞控板与遥控器的 MCU 选型:为什么都用 STM32 而不是 STC 或 51

常见做飞控遥控器系统的选型方案是机载飞控用 STM32F103C8T6,遥控器端也用同型号,因为两者需要的资源基本对等:飞控需要读取 MPU6050(I2C)、接收遥控数据(SPI 接 NRF24L01)、输出 4 路 PWM;遥控器需要读取 4 到 6 路摇杆 ADC、按键输入、SPI 无线收发、OLED 显示。F103C8T6 的 64KB Flash 和 20KB RAM 放过姿态解算和通信协议完全够用,而且两边的工程可以共用一套 NRF24L01 驱动和通信帧代码,仓库里维护成本直接减半。

提示:如果你的设计里还想加超声波定高、光流定点或者 OSD 图传叠加,建议把飞控换成 STM32F405RGT6,预算多 10 元左右,但浮点运算和 DMA 资源会宽裕很多。

选 STM32 而不是 51 单片机还有一个核心原因:硬件除法器和乘法器对 PID 控制的实时性影响。四轴飞控的内环控制周期通常做到 1kHz,也就是 1 毫秒内要读完 IMU、执行完互补滤波加上串级 PID 计算。51 单片机在 24MHz 主频下跑浮点运算会明显吃紧,而 STM32F103 在 72MHz 下用硬件浮点配合整型优化的 PID 可以轻松跑满 1kHz 甚至 2kHz,留出余量给无线数据接收和电机更新。

2.2 原理图设计的三个关键节点:MPU6050 去耦、NRF24L01 电源滤波、电机供电隔离

原理图设计常犯的错误是把 MPU6050 直接挂在 MCU 的 3.3V 上不处理。MPU6050 内部是 MEMS 振动结构,对电源纹波非常敏感,纹波大了数据会随机漂移,飞控会表现为"地面不动,但姿态角自己在缓慢旋转"。我一般做法是给 MPU6050 的 VDD 单独加一颗 10Ω 电阻和 10uF + 0.1uF 电容组成 π 型滤波,模拟地和数字地在主芯片下方单点汇合。NRF24L01 的峰值电流在发射瞬间会跳到 110mA 左右,如果和 MCU 共用一条细走线,射频发射时刻会造成 MCU 供电跌落,导致无线数据收发的关键时序被干扰。正确做法是在 NRF24L01 模块座子旁边放一颗 100uF 电解电容和 0.1uF 陶瓷电容,并且电源线从 3.3V LDO 单独引出,不要和 MCU 的电源走线共用一条低压差路径。

电机供电隔离是原理图里最容易被忽视的:四个直流空心杯电机在启动瞬间电流是稳态的 2 到 3 倍,如果飞控板和电机驱动共用电源层,大电流冲击会直接串进传感器和 MCU。常见的设计是 1S 锂电池(3.7V)先经过主电源开关进入电机驱动 MOS 管,同时分一路经过 LDO(如 ME6211)降到 3.3V 给 MCU 和传感器用,LDO 输入端的 100uF 电容要贴着电池接口放。电机的 VCC 走线要用 1mm 以上宽度或铺铜,避免过孔限制电流导致压降过大,飞机大油门时出现复位重启。

2.2.1 PCB 布局:IMU 位置决定飞控的机械噪声

PCB 图的布局核心是 MPU6050 必须放在 PCB 物理中心附近,而且要避开电机座螺丝孔位。电机高速旋转时,螺旋桨的机械振动会从机臂传导到机身,如果 MPU6050 离螺丝孔太近或者处于 PCB 边缘,加速度计采集的振动噪声会被放大,配合互补滤波的高通特性会引入明显的姿态抖动。另外 MCU 的 8MHz 晶振要靠近 BOOT0 和 OSC_IN/OSC_OUT 引脚放置,晶振下方铺地铜并打过孔包围,这是配套源代码能在硬件上稳定运行的前提——晶振不起振,软件层面的工作全都白做。

2.2.2 8MHz 晶振电容计算:匹配负载电容而不是随便选 20pF

热词里经常出现stm32 晶振电容计算,这里给一个可以直接套的公式。ST 官方规范中,STM32F103 的 8MHz 晶振要求并联负载电容,常见匹配公式是:

CL = (C1 * C2) / (C1 + C2) + Cs

其中CL是晶振规格书里标的负载电容(常见 8MHz 晶振标 12pF 或 20pF),C1C2是晶振两脚对地的匹配电容,Cs是 PCB 引脚的寄生电容,经验值取 2 到 5pF。要让晶振工作在标称频率附近,应当使:

  • 若晶振CL = 12pF,取Cs = 3pF,则C1 = C2 = 2 * (CL - Cs) = 18pF,取容值相近的 18pF 贴片电容。
  • 若晶振CL = 20pF,取Cs = 3pF,则C1 = C2 = 2 * (20 - 3) = 34pF,取 33pF 或 33pF 与 1pF 并联。

电容偏差会导致实际频率偏移,偏太多会造成串口波特率误差和定时器时基漂移,飞控解锁后电机会出现间歇性抽搐,这是典型的"原理图看着没问题但飞不起来"的隐性原因。

2.3 无源器件选型:MOS 管、LDO、上拉电阻的容差控制

电机驱动部分用 SI2302 这类 N-MOS 管是常见做法,但要注意 SI2302 在 3.3V 栅极电压下的导通电阻范围。选型时重点看 VGS(th) 阈值电压,确保 MCU 的 3.3V GPIO 能完全打开 MOS 管,否则电机在大电流下 MOS 管发热严重,会反向影响机身的散热和 IMU 温度漂移。I2C 上拉电阻选择 4.7kΩ 是保守值,如果 MPU6050 通信偶尔失败,可以换成 2.2kΩ 增加上升沿速率。飞控的 SDA 和 SCL 走线尽量等长且远离 PWM 输出线,避免电机调速的 PWM 信号对 I2C 产生串扰。

3. 无线链路协议设计:nRF24L01 六通道遥控帧格式与收发程序实现

3.1 通信帧结构定义:数据对齐比数据内容更关键

STM32 飞控接收遥控器的数据,最忌讳的是定义一个大结构体直接内存拷贝。不同编译器的字节对齐规则不一致,而且 STM32 和遥控器如果用了不同的优化等级,结构体内部 padding 会悄悄改变,数据大概率错位。我一般会定义一个固定长度的数组作为协议层载体,再用 memcpy 逐字段映射:

// 六通道遥控帧,固定 10 字节长度 #pragma pack(1) typedef struct { uint8_t head; // 帧头 0xAA uint8_t cmd; // 0x01=遥控数据, 0x02=解锁/锁定, 0x03=紧急停机 uint8_t throttle; // 油门 0-255 uint8_t yaw; // 偏航 0-255, 中点约 127 uint8_t pitch; // 俯仰 0-255 uint8_t roll; // 横滚 0-255 uint8_t aux1; // 辅助通道 1(模式切换) uint8_t aux2; // 辅助通道 2(LED/蜂鸣器) uint8_t crc_low; // CRC16 低字节 uint8_t crc_high; // CRC16 高字节 } rc_frame_t; #pragma pack()

代码逻辑说明:#pragma pack(1)强制结构体按 1 字节对齐,保证无论接收端单片机优化选项怎么变,字段偏移量始终固定,这是跨平台无线通信的基本功。帧头0xAA用于接收端找同步起始位,摇杆通道都用 0 到 255 表示,中点 127 左右通过遥控器代码里的中位校准修正。

提示:CRC 校验不能省。NRF24L01 虽然硬件自带 CRC,但只能检出射频传输错误,无法识别两帧数据之间的粘包和丢帧。在 payload 里再算一次 CRC16 是极便宜的保险。

3.2 NRF24L01 收发最小代码:SPI 驱动和 Enhanced ShockBurst 配置

NRF24L01 收发代码可以直接复用到飞控和遥控器两端,只做一个发送端和接收端的条件编译。初始化时要特别注意:模块的 CE 引脚拉高 130 微秒以上才进入发送模式,10 微秒内的短脉冲只会让模块进入待机状态,初学者常在这里卡死,英语注释里写的 "CE high for at least 10us" 经常被误解成 10 微秒即可。

// NRF24L01 发送模式最小初始化代码 void nrf24_tx_init(void) { // 配置发送端地址和射频参数 nrf24_write_reg(REG_CONFIG, 0x0E); // CRC 使能, 2字节CRC, 上电 nrf24_write_reg(REG_EN_AA, 0x01); // 仅数据管道0开启自动应答 nrf24_write_reg(REG_SETUP_RETR, 0x1A); // 自动重发500us间隔, 最多重发10次 nrf24_write_reg(REG_RF_CH, 40); // 2.480GHz 频点 nrf24_write_reg(REG_RF_SETUP, 0x26); // 2Mbps, 0dBm 发射功率 nrf24_write_reg(REG_RX_PW_P0, 10); // 管道0接收长度10字节 // 设置发送和接收地址为相同的 5 字节地址 uint8_t addr[5] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7}; nrf24_write_reg_burst(REG_TX_ADDR, addr, 5); nrf24_write_reg_burst(REG_RX_ADDR_P0, addr, 5); }

发送函数内部要处理MAX_RT中断标志位,否则重发次数耗尽后模块会一直报错,导致后续发送全部失败:

uint8_t nrf24_send(uint8_t *data, uint8_t len) { CE_LOW(); // 清空发送中断标志 nrf24_write_reg(REG_STATUS, (1 << 5) | (1 << 4) | (1 << 3)); // 写入发送缓冲区 nrf24_cmd_write_burst(CMD_W_TX_PAYLOAD, data, len); // CE 拉高至少 15us 触发发送 CE_HIGH(); delay_us(20); CE_LOW(); // 等待发送完成或超时 uint32_t timeout = 10000; while ((nrf24_read_reg(REG_STATUS) & (1 << 5)) == 0 && timeout--) { delay_us(10); } if (timeout == 0) return 0; // 发送超时 uint8_t status = nrf24_read_reg(REG_STATUS); if (status & (1 << 4)) { // MAX_RT 重发耗尽 nrf24_write_reg(REG_STATUS, (1 << 4)); // 清标志后重新发送 return 0; } return 1; }

这个函数的参数说明:REG_STATUS里 bit5 是 TX_DS(发送完成),bit4 是 MAX_RT(重发次数耗尽)。判断发送成功要看两个条件:TX_DS 必须置位,并且 MAX_RT 不能同时置位。只判断 TX_DS 会漏掉一种情况——重发 10 次后刚好某个数据包被应答了,模块会同时置位 TX_DS 和 MAX_RT,这帧数据虽然发出去了,但延迟已经超过 50ms,对飞控来说是失效数据,网络热词里说的"stm32和变频器通讯"踩坑模型虽然是指工业场景,但和这里的超时语义一致——通讯协议里不能只看最终成功标志,必须看时序约束。

3.3 无线频点和速率的选择:2Mbps 速率下的数据噪声边界

NRF24L01 支持 250kbps、1Mbps、2Mbps 三档速率,飞控场景我建议用 2Mbps。理由很直接:飞行器控制链路对延迟敏感,对绝对误码率不敏感。四轴在 80km/h 左右飞行时,多径效应和同频干扰很常见,2Mbps 下单包传输时间短,被 WiFi 或者蓝牙跳频信号干扰的概率窗口更小。缺点是接收灵敏度从 -94dBm 降到 -82dBm 左右,但对室内 30 到 50 米遥控距离来说完全可用。频点选 40(2.480GHz)附近是业界默认的"打架区",2.4G WiFi 的信道 11 到 13 也在这附近。更稳妥的做法是在遥控器上加一个频道偏移代码,上电时自动扫描 RSSI,避开噪声最大的频点再建立链路,这个功能本项目的遥控器端源码里通常会做成自动跳频的简版。

4. 飞控端源码核心:MPU6050 数据采集、姿态解算与串级 PID 输出

4.1 MPU6050 原始数据读取:I2C 时序和量程配置

飞控源代码第一步是初始化 MPU6050 的陀螺仪和加速度计量程。四轴飞行器常飞的动作范围里,陀螺仪用 ±500°/s 量程、加速度计用 ±4g 量程比较合适。±250°/s 在快速翻转时会削顶,导致姿态解算瞬变,飞控会给出错误的电机修正量;±2000°/s 虽然不会削顶,但量化噪声被放大到 16.4 LSB/°/s,静止状态下陀螺仪数据波动会明显增大。以下是 I2C 初始化代码:

void mpu6050_init(void) { // 退出睡眠模式: 设置 PWR_MGMT_1 寄存器为 0x00 uint8_t pwr_data = 0x00; i2c_write_reg(MPU6050_ADDR, 0x6B, &pwr_data, 1); // 陀螺仪量程 ±500°/s: 对应 GYRO_CONFIG 寄存器值为 0x08 uint8_t gyro_cfg = 0x08; i2c_write_reg(MPU6050_ADDR, 0x1B, &gyro_cfg, 1); // 加速度计量程 ±4g: 对应 ACCEL_CONFIG 寄存器值为 0x08, 灵敏度 8192 LSB/g uint8_t accel_cfg = 0x08; i2c_write_reg(MPU6050_ADDR, 0x1C, &accel_cfg, 1); // 配置数字低通滤波带宽 94Hz: 对应 CONFIG 寄存器值为 0x03 uint8_t dlpf_cfg = 0x03; i2c_write_reg(MPU6050_ADDR, 0x1A, &dlpf_cfg, 1); }

代码里的参数逻辑:DLPF(数字低通滤波)设为 94Hz 是要在保留飞行操控频段信号的同时滤掉电机和桨叶的高频振动。四轴飞行时姿态变化的核心频段在 5Hz 到 30Hz,94Hz 低通足够保持响应,又不会把 200Hz 以上的电机噪声传递进解算环路。读取原始数据时要注意帧同步,MPU6050 的加速度计和陀螺仪各自输出 6 个字节,I2C 多字节读取时从 0x3B 地址一次性连续读 14 个字节最稳妥,分多次单字节读取会引入中间采样时刻的时间漂移。

4.2 四元数互补滤波:用加速度计修正陀螺仪积分漂移

姿态解算选择互补滤波而不是完全用四元数梯度下降法的原因是运算量:F103C8T6 在 72MHz 下跑 Mahony 互补滤波大约耗时 200 微秒,而完整跑一遍 Madgwick 梯度下降法要 500 微秒以上,这会挤压后续 PID 和无线通信的时间预算。互补滤波的思路是把陀螺仪积分后的高频姿态和加速度计计算出的低频姿态按权重融合,飞控专用代码如下:

void attitude_update(float gx, float gy, float gz, float ax, float ay, float az, float dt) { // 归一化加速度计向量 float norm = sqrtf(ax * ax + ay * ay + az * az); ax /= norm; ay /= norm; az /= norm; // 当前四元数计算出的重力方向参考向量 float vx = 2.0f * (q1 * q3 - q0 * q2); float vy = 2.0f * (q0 * q1 + q2 * q3); float vz = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3; // 叉积误差: 测量向量与参考向量之间的偏差 float ex = ay * vz - az * vy; float ey = az * vx - ax * vz; float ez = ax * vy - ay * vx; // 误差积分项, 消除静态偏差 (Kp=1.5, Ki=0.3) ex_int += ex * Ki * dt; ey_int += ey * Ki * dt; ez_int += ez * Ki * dt; // 陀螺仪角速度补偿 gx += Kp * ex + ex_int; gy += Kp * ey + ey_int; gz += Kp * ez + ez_int; // 一阶龙格库塔法更新四元数 float q0_dot = -q1 * gx - q2 * gy - q3 * gz; float q1_dot = q0 * gx + q2 * gz - q3 * gy; float q2_dot = q0 * gy - q1 * gz + q3 * gx; float q3_dot = q0 * gz + q1 * gy - q2 * gx; q0 += q0_dot * dt; q1 += q1_dot * dt; q2 += q2_dot * dt; q3 += q3_dot * dt; // 四元数归一化, 防止数值累积误差 float q_norm = sqrtf(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); q0 /= q_norm; q1 /= q_norm; q2 /= q_norm; q3 /= q_norm; // 由四元数转欧拉角, 用于 PID 控制和上位机显示 roll_angle_out = atan2f(2.0f * (q0 * q1 + q2 * q3), 1.0f - 2.0f * (q1 * q1 + q2 * q2)); pitch_angle_out = asinf(2.0f * (q0 * q2 - q1 * q3)); yaw_angle_out = atan2f(2.0f * (q0 * q3 + q1 * q2), 1.0f - 2.0f * (q2 * q2 + q3 * q3)); }

这段代码里最值得说明的是KpKi的含义:Kp是加速度计对陀螺仪的交叉修正强度,越大则姿态跟踪越快但噪声越容易被引入;Ki是积分修正项,负责消除陀螺仪零偏在积分过程中造成的累积漂移。飞行调试时如果发现姿态角在悬停时缓慢偏向一个方向,优先调大Ki;如果机身发抖且姿态角跳变,优先调小Kp。初始值Kp=1.5, Ki=0.3是我从大量调试记录里抽出来的适用手抛型小四轴的中位值,可以从这个基础上再微调。

提示:atan2fasinf在 F103 上编译后大约各耗 3 到 5 微秒,完整转换一次在 20 微秒内可以完成。如果你的姿态解算频率是 1kHz,欧拉角输出耗时会吃掉 2% 的 CPU 时间,对整体实时性没有实质影响。

4.3 串级 PID 控制:内环角速度、外环角度与电机混控映射

飞控常用的控制结构是串级 PID,外环是角度环,内环是角速度环。外环输出直接作为内环的期望值输入,这样设计可以让内环以更快的频率抑制扰动。一般外环频率跑 250Hz,内环频率跑 1kHz。以下是最小可用的外环加内环实现:

// 外环角度环 PID, 输入期望角度和目标角度, 输出期望角速度 float angle_pid_outer(float expected, float measured, float dt) { float err = expected - measured; float p_out = kp_angle * err; angle_integral[0] += ki_angle * err * dt; // 积分限幅, 防止 windup if (angle_integral[0] > ANGLE_I_LIMIT) angle_integral[0] = ANGLE_I_LIMIT; if (angle_integral[0] < -ANGLE_I_LIMIT) angle_integral[0] = -ANGLE_I_LIMIT; return p_out + angle_integral[0]; } // 内环角速度环 PID, 输出是电机 PWM 修正量 float rate_pid_inner(float expected_rate, float measured_rate, float dt) { float err = expected_rate - measured_rate; float p_out = kp_rate * err; rate_integral[0] += ki_rate * err * dt; if (rate_integral[0] > RATE_I_LIMIT) rate_integral[0] = RATE_I_LIMIT; if (rate_integral[0] < -RATE_I_LIMIT) rate_integral[0] = -RATE_I_LIMIT; float d_out = kd_rate * (err - prev_err[0]) / dt; prev_err[0] = err; return p_out + rate_integral[0] + d_out; }

参数逻辑说明:内环的kd_rate是对角速度变化率的阻尼,调大了飞机会变得僵硬,调小了会振荡。内环输出经过一个限幅函数后,再和遥控器油门值混合成四个电机的 PWM 输出。混合是 x 型布局的固定映射:电机 1 和电机 4 是一对对角线,电机 2 和 3 是另一对,横滚和俯仰命令分别以差分形式叠加。差分叠加的符号方向必须和 MPU6050 的安装方向一致,否则飞控会越修越偏,飞机解锁后直接原地自旋或者翻机。

5. 遥控器端源码与交互逻辑:摇杆采样、按键消抖与 OLED 回传显示

5.1 摇杆 ADC 采样和原点校准:中点漂移对飞控的影响

遥控器端的核心输入是两颗二维摇杆,内部是两个电位器,输出模拟电压到 STM32 的 ADC 引脚。F103 的 ADC 是 12 位分辨率,摇杆量程的物理范围通常只能用到 10 位左右,原因在于电位器碳膜两端的死区。我一般把 ADC 原始值映射到uint8_t的 0 到 255,再进行中位校准。

摇杆中位偏移是四轴飞控最隐蔽的问题来源:电位器用久了碳膜磨损,机械中心对应的 ADC 值不会是理论上的 2048,如果拿原始值直接当作 127 发给飞控,飞控外环会一直收到一个固定的非零期望角度,飞机解锁后就会向某个方向缓缓漂移。处理方式是在遥控器初始化时进行 200 次 ADC 采样平均,把每次采样的 ADC 值减去校准值后再映射到 -127 到 +127 的输出范围:

// 摇杆通道校准: 上电时检测当前摇杆位置并保存为中心值 uint16_t stick_center[6]; void stick_calibrate(void) { for (uint8_t ch = 0; ch < 6; ch++) { uint32_t sum = 0; for (uint8_t i = 0; i < 200; i++) { sum += adc_read_channel(ch); delay_us(100); } stick_center[ch] = sum / 200; } }

参数说明:这里的delay_us(100)是为了让 ADC 采样避开同一时刻 PWM 电机信号或 OLED 刷新带来的电源干扰。200 次采样取平均能消除大部分白噪声,但需要注意的是校准必须在遥控器开机后 2 秒内完成,期间用户不能碰摇杆,否则中心值会被污染。如果后续发现摇杆回中后飞控端仍有漂移,可以把校准时间延长到 3 秒并在 OLED 上显示"校准中"字样。

5.2 按键消抖与模式切换逻辑:用状态机替代延时消抖

遥控器上一般有解锁、锁定和模式切换三个按键。初学者最容易在按键处理上犯的错是直接用delay_ms(20)消抖,这会阻塞遥控器主循环,导致 NRF24L01 的发送周期被拖长到 30ms 以上,飞行器的手感会变肉。正确的做法是用定时器扫描按键,配合状态机消抖:

// 按键状态机: 等待按下 -> 确认按下 -> 等待释放 typedef enum { KEY_STATE_IDLE = 0, // 空闲状态 KEY_STATE_PRESSED, // 已检测到按下 KEY_STATE_CONFIRM // 消除抖动后确认 } key_state_t; void key_scan_10ms(void) { static key_state_t state[4]; static uint8_t key_dbnc_cnt[4]; for (uint8_t i = 0; i < 4; i++) { uint8_t level = gpio_read(key_pin[i]); switch (state[i]) { case KEY_STATE_IDLE: if (level == 0) { // 检测到低电平按下 state[i] = KEY_STATE_PRESSED; } break; case KEY_STATE_PRESSED: if (level == 0) { key_dbnc_cnt[i]++; if (key_dbnc_cnt[i] >= 3) { // 连续 3 次扫描确认按下 key_event_callback(i); // 触发对应的模式切换 state[i] = KEY_STATE_CONFIRM; } } else { key_dbnc_cnt[i] = 0; state[i] = KEY_STATE_IDLE; } break; case KEY_STATE_CONFIRM: if (level == 1) { // 等待释放 key_dbnc_cnt[i] = 0; state[i] = KEY_STATE_IDLE; } break; } } }

这个状态机在定时器中断里每 10ms 执行一次,三个连续采样周期保持一致才判定按键有效,相当于 30ms 的硬件消抖时间,不会阻塞主循环。按键回调里做的工作很轻,只是翻转一个system_mode全局变量,然后往无线发送缓冲区写一帧命令数据,真正的发送动作由无线发送周期决定。

5.3 OLED 回传显示:用串口双工把飞控姿态和电量拉回遥控器

遥控器端通过 NRF24L01 的 ACK 载荷机制可以在接收遥控命令的同时带回飞控的遥测数据。NRF24L01 的增强型 ShockBurst 模式支持在自动应答时附带 1 到 32 字节的数据,这意味着飞控收到遥控器数据包后,把姿态角和电池电压填写到 ACK 载荷里返回给遥控器,通信链路不需要额外的回传包,省时省带宽。

OLED 显示的内容一般包括当前的飞行模式(自稳/定高/手动)、油门输出百分比、飞控回传的横滚角和俯仰角,以及飞控端的电池电压。对四轴飞行器来说,电池电压回传是最常用的功能,因为机载电池电压如果低于 3.5V 单片电压,大油门时电压会瞬间跌穿 LDO 的最低工作电压导致飞控重启。常见方案是飞控的 ADC 脚直接分压测量电池电压,分压电阻用 10kΩ 和 10kΩ 各一颗,ADC 结果乘以 2 再乘以 3.3V/4096 得到单节电压,回传到遥控器端后,OLED 的电压值低于 3.5V 时切换成大字报警显示。

6. 串级 PID 的滤波陷阱与参数整定:用手抛法验证飞控姿态响应

串级 PID 参数整定是整套 STM32 飞控系统里最耗时的环节,而最容易被忽略的是内环微分项的滤波。内环角速度环的微分项放大了高频噪声,电机 PWM 变化和 IMU 采样抖动都会通过 D 项引入输出抖动,飞控会出现小幅度高频颤振。常见做法是先对陀螺仪原始的角速度数据做一阶低通滤波,再送入微分环节:

// 陀螺仪角速度低通滤波, 截止频率约 42Hz float gyro_filtered[3]; float gyro_lpf(float raw, uint8_t idx, float dt) { float alpha = dt / (dt + 1.0f / (2.0f * 3.14159f * 42.0f)); gyro_filtered[idx] += alpha * (raw - gyro_filtered[idx]); return gyro_filtered[idx]; }

这个滤波器的参数逻辑:alpha是平滑系数,dt是采样周期(0.001 秒对应 1kHz 采样率),42Hz 截止频率保留了人类操控所需的姿态响应带宽,同时过滤掉了高频机械噪声。如果把截止频率提到 100Hz,滤波延时变小但噪声变大;降到 20Hz 后飞控的响应会明显迟钝,快速打舵时飞机会有"黏滞感"。

参数整定的实际操作,我建议遵循先内环后外环,先 P 后 D 再 I的原则。保持外环禁用(期望角度直接置为 0),先把内环的kp_rate从小到大加,直到用手拿起飞机旋转机身可以感觉到明显的对抗力,继续增加直到出现高频振荡,然后回退 50% 作为安全初值。接着加kd_rate,每次增加 20%,同时观察机身是振荡收敛还是持续振荡。内环调完再开外环,kp_angle从 2.0 起步,用手抛法验证:手拿飞机轻轻向某个方向偏转 20 度然后释放,如果飞机能在 1 秒内回到水平且没有明显回弹,说明外环 P 够用了,否则继续加大。

提示:手抛法验证比绑在桌子上试车安全得多,操作要领是把飞机举在胸口高度,手部动作要轻微,避免螺旋桨打到手。电机解锁状态下手不要贴近桨叶平面,只抓机臂中心附近的开阔区域。

参数整定的最终落地路径是写进飞控的常量区而不是每次重新编译烧录。正式发货或做演示前,把一组稳定的参数固化到config.h里,并且把这个配置区做成可以在遥控器上调整的调试模式:按下遥控器的某个组合按键进入参数调节菜单,用摇杆把 PID 参数实时写入 EEPROM,重启后生效。这个做法在反复调参过程中省掉了大量烧录等待时间,效率比每次用 ST-LINK 重新下载高一个数量级。至此,从系统架构、原理图 PCB 到无线通信和飞控源码的实际参数整定已经形成一条完整闭环。

本文还有配套的精品资源,点击获取

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

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

立即咨询