结合之前几篇的进度,蓝鸟V3.0 已经解决了机架、动力和传感器融合的问题,整机勉强能飞,但“能飞”和“好开”之间还差着一条重要链路——飞控如何听懂遥控器的指令。这一篇就专注于通信协议和航模遥控通道的完整落地:从协议选型、接收机接线、串口 DMA 接收、SBUS 帧解析,到通道映射、行程校准和失控保护。适合正在自研飞控、或者想搞明白开源飞控遥控链路底细的朋友,看完就能自己复现一套稳定的遥控接入方案。
我最早做蓝鸟V1.0、V2.0时,遥控接入用的是最传统的 PWM 直连:接收机每个通道一根信号线接到飞控 IO 口,代码里开一堆输入捕获定时器。那时候通道少、功能简单,勉强能跑,但通道一多、信号线一长,问题就全冒出来了。V3.0 在规划时就把通信链路重写了一遍,前后踩了不少坑,这篇就当是一次经验记录和复盘。
1. 从“能飞”到“好开”:通信架构的选型思路
1.1 飞控内外通信的整体划分
飞行器上的通信链路,一般分两大部分:一个是飞控芯片与传感器之间的片内总线通信,另一个是飞控与外界(遥控接收机、GPS、地面站、图传)之间的对外数据交换。V3.0 的通信架构设计,就是从这两个层面分别做取舍。
片内传感器通信,直接关系到 IMU 数据的输出速率和稳定性。我选用的是 SPI 总线挂载 6 轴 IMU,I2C 总线挂载气压计。原因很直接:IMU 需要高频读取,SPI 没有地址握手和 ACK 等待,速率可以稳定跑到几十兆,而且不容易被飞控上的电调 PWM 信号干扰;气压计对速率要求不高,I2C 足够,还能省几个引脚。早期用过 I2C 读 IMU,遇到过一个很典型的问题:当油门拉高、四个电调同时换相时,I2C 总线上偶发出现 NACK 甚至总线挂死,因为电调换相产生的噪声叠到了信号线上。换成 SPI 后,这个现象基本没再出现。
对外通信就要复杂得多。V3.0 的对外接口分配大致是这样的:UART1 接遥控接收机,UART2 接 GPS 模块,UART3 接地面站数传,UART4 保留给调试日志。之所以每个外设独立占用一个串口,而不是简单共用一个总线,是为了隔离故障域——任何一个外设异常都不会拖垮其他外设的数据链路。
1.2 为什么果断抛弃 PWM 直连方案
V2.0 时代用 PWM 直连,每个遥控通道需要一根独立信号线,8 通道就要 8 根线外加电源和地线,飞控这边要准备至少 8 个输入捕获引脚,每个引脚还得配一个定时器通道。接线噩梦还是其次,真正的问题在于信号质量:长距离多根 PWM 信号线在机架内穿行时,会不断耦合电调和电机产生的高频噪声,导致脉宽测量偶尔跳变,飞控误判为摇杆大幅移动,飞机会猛然点头或横滚。
V3.0 直接改成单线串行协议。遥控器把所有通道数据打包成串行帧,通过一根信号线送到飞控。线束少、抗干扰强、解析方式统一,而且可以非常方便地增加通道数量,为以后扩展云台控制、独立模式开关留了余量。整套遥控链路的信号处理全部收拢到一个串口中断+DMA 的机制里,代码结构也清爽很多。
2. 遥控协议选型与解析:从 PWM、PPM 到 SBUS 再到 CRSF
2.1 PPM 的局限与 SBUS 的优势
在单线化改造之前,很多人会先想到 PPM。PPM 确实是单线,但它本质上是把多个通道的脉冲宽度在时间上串在一起,每一帧约 20ms,依靠电平跳变间隔来表示各个通道值。PPM 有个固有短板:帧内所有通道共用一条时间轴,任何一个通道的脉宽抖动都会污染后续通道的测量;而且它没有数据校验、没有帧结束的强标志,接收端解析逻辑对时序要求极高,受温度漂移和干扰影响明显。
SBUS 是在串口物理层上跑的协议,波特率 100000,数据位 8、偶校验、2 停止位,每帧固定 25 字节。22 字节用于打包 16 通道数据,每个通道占 11 比特,另外包含 1 字节标志位和 1 字节结束符。因为是数字通信,只要帧格式正确、校验结果对,通道值就是稳定的,不会因为脉宽漂移产生解析误差。SBUS 还能传递接收机是否信号丢失的标志,这点对失控保护特别有用。
现在的航模遥控器,高性价比的遥控系统基本都支持 SBUS;更高端一些的还有 CRSF 协议,波特率 420000,帧率最高可到 500Hz,延迟更低,且支持双向遥测。V3.0 优先用了 SBUS,因为它的生态最成熟、兼容性最好,光一个接收机就同时支持 PWM、PPM、SBUS 三种输出,方便我串口调试时来回切换验证。
协议横向对比:
| 协议 | 物理层 | 通道数 | 刷新率 | 硬件接线 | 抗干扰能力 | 适用场景 |
|---|---|---|---|---|---|---|
| PWM | 多根信号线脉宽 | 8 | 50-400Hz | 1通道/线 | 差 | 调试、小机 |
| PPM | 单线脉宽序列 | 8 | 50Hz | 1线 | 一般 | 入门单线方案 |
| SBUS | 单线串口 100000bps | 16 | 100Hz | 1线 | 强 | 主流飞控选型 |
| CRSF | 单线串口 420000bps | 16+ | 最高500Hz | 1线 | 强 | 高频竞技、双向遥测 |
2.2 SBUS 电平反转和帧结构细节
SBUS 最容易被忽略的点:接收机输出的信号电平是反相的。它设计初衷是和双向串口传输兼容,所以对外输出的是反向信号。直接用普通 UART 去读,无论如何都解析不出有效帧,因为你读到的每个字节正好是逻辑取反后的结果。
解决方式有三个:一是用三极管或 MOSFET 搭一个反相器,把信号反回来再接单片机 RX 引脚;二是用硬件本身就支持信号反相的串口外设,比如有些 STM32 系列提供 UART 的反相接收选项,可以在寄存器里直接配置;三是买一块现成的 SBUS 转串口小板,不过这样一来多一个器件,反而增加了排查复杂度。V3.0 我选择的是第一种,自己搭了一级反相器,成本不到一元,原理也最可控。
SBUS 帧结构里,第一个字节固定为 0x0F,最后一个字节固定为 0x00,中间 22 个字节按位依次排列 16 个通道值。每个通道 11 位,无符号整数,范围大约是 0~2047,中位对应 1000 附近的换算,标准信号范围对应遥控器设计值通常是 172~1811 的区间,具体因遥控器而异。解析时要把帧头、帧尾和 flags 字节分清楚,再把 22 字节按位切割重组成 16 个 11 位的无符号数。
3. 接收机接入飞控:硬件接线、串口 DMA 与帧解析实现
3.1 硬件接线与电平匹配细节
接线图不复杂,但有几个细节必须做对:接收机的 SBUS 输出引脚接到反相器的输入端,反相器输出接飞控 UART1 的 RX;接收机供电我用的是飞控的稳压输出,电压 5V,地线必须和飞控共地。如果接收机飞控各用一坨电源、没有共地,串口电平会飘得离谱,我见过最夸张的情况是帧解析成功率只有两成。
共地这件事很多人觉得无所谓,实测下来是大坑。飞控和接收机如果各自用独立 BEC 供电,地电位会有小幅度偏移,SBUS 的 3.3V 逻辑电平在这种偏移下很容易被误判。更稳妥的做法是接收机供电直接在飞控电源模块上取电,保证参考地完全一致。
反相器电路我用的是 NPN 三极管方案,输入高电平时输出低,输入低电平时输出高,逻辑刚好反相一次,接在 UART RX 前面的效果就是还原接收机原始信号。注意三极管的基极限流电阻和高电平输入电压不能超过单片机电源域,我用 3.3V 供电,这样不会对单片机的 RX 引脚产生超额电压。
3.2 串口参数配置与 DMA 接收
SBUS 的波特率是 100000,数据格式 8 个数据位、偶校验、2 个停止位,这个组合写成串口配置参数时和常见的 8N1 不一样,必须留意。配置错误非常隐蔽,尤其是偶校验设错,平时大概率一个字节都解析不对,但偶尔又像有信号,很容易让人怀疑硬件坏了。
我更推荐用 DMA 加串口空闲中断来接收 SBUS 帧。因为 SBUS 一帧正好 25 字节,等在一段时间内没有新的数据到达时就认为一帧结束;DMA 会把 25 字节连续搬到缓冲区,期间不需要 CPU 干预,只有帧结束才进入中断。比逐字节中断接收省很多 CPU 时间,而且不容易丢字节。
// 以STM32 HAL库为例配置串口底层 sbus_uart.Instance = USART1; sbus_uart.Init.BaudRate = 100000; sbus_uart.Init.WordLength = UART_WORDLENGTH_8B; sbus_uart.Init.StopBits = UART_STOPBITS_2; sbus_uart.Init.Parity = UART_PARITY_EVEN; sbus_uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&sbus_uart); // 使能DMA接收,环形缓冲区或固定缓冲区均可 HAL_UART_Receive_DMA(&sbus_uart, sbus_rx_buf, SBUS_FRAME_SIZE);开启串口空闲中断后,每次空闲中断代表一帧接收完毕,此时从 DMA 缓冲区里取数据。取完要注意把 DMA 缓冲区重新装填,否则下一次 DMA 写入会覆盖旧数据。
3.3 SBUS 帧解析与通道值还原
拿到 25 字节原始帧后,第一步校验帧头 0x0F、帧尾 0x00,第二步看 flags 字节里的信号丢失位。通道值还原是位操作,要把 22 个字节当成一个连续的比特流,每 11 位切一段,重组成 16 个 uint16_t。这一过程看起来简单,写起来却容易因为字节序搞乱,尤其是中间几组通道跨越字节边界的时候。
#define SBUS_CHANNEL_BITS 11 #define SBUS_CHANNEL_COUNT 16 void sbus_parse_frame(uint8_t *frame, uint16_t *channels, uint8_t *lost_frame) { if (frame[0] != 0x0F || frame[24] != 0x00) { return; // 帧头帧尾非法 } *lost_frame = (frame[23] >> 2) & 0x01; uint16_t bit_buf = 0; uint16_t mask = 0x07FF; // 11位掩码 uint8_t bit_pos = 0; uint16_t byte_idx = 1; for (uint8_t ch = 0; ch < SBUS_CHANNEL_COUNT; ch++) { while (bit_pos + 11 > 16) { bit_buf |= (frame[byte_idx++] << bit_pos); bit_pos += 8; } channels[ch] = bit_buf & mask; bit_buf >>= 11; bit_pos -= 11; } }这段代码是按位拼接的思路:每次从帧字节里取一个字节左移到当前位置然后拼进 bit_buf,当 bit_buf 里至少有 11 个有效位时,就切出一个通道。切完之后 bit_buf 右移 11 位,bit_pos 同步减 11,继续下一组。实测下来只要 bit_buf 和 bit_pos 的加法减法拉齐,解析一次 25 字节帧只需几十微秒,远远小于 10ms 的遥控帧周期,性能完全够用。
解析出的通道值还需要归一化,再映射成控制系统的期望输入。比如实际得到的数值范围是 [200, 1800],中位约 1000,我会把它转换成标准的 [1000, 2000] 油门比例值,或转换成 [-1, 1] 的摇杆偏转量。每个遥控器本身的输出范围存在差异,这个归一化参数要通过标定写入存储区。
4. 遥控通道映射、校准与保护逻辑
4.1 通道映射与模式开关设计
V3.0 的通道规划:前四个通道是基本的副翼、升降、油门、方向,后面四个是功能开关和保留扩展位。第 5 通道接一个三段开关用来切换飞行模式,自稳模式、定高模式、手动模式各占一档;第 6 通道做解锁/上锁;第 7 通道预留给了未来可能会加的云台俯仰;第 8 通道接蜂鸣器状态。
通道映射要写一个统一的映射表,让控制层代码只读映射后的通道语义,不直接接触原始 SBUS 序号。这么做的价值在于:万一遥控器预设通道顺序和预期不一致,只需要修改映射表,不需要动控制层逻辑。平时切换遥控器型号后也能快速适配,不至于改代码改到晕头转向。
模式开关还有一个细节:三段开关在拨动瞬间经常会产生中间抖动,直接读通道值判断档位,可能在切换时出现目标状态抖动若干毫秒。我用的是滞回判断加消抖延时,只有连续多次读到同一档位才认定开关状态变更。这样切换手感很干净,不会出现莫名闪断。
typedef struct { int8_t ch_index; uint16_t min_val; uint16_t mid_val; uint16_t max_val; } channel_cfg_t; static const channel_cfg_t channel_map[8] = { {0, 172, 992, 1811}, // ROLL {1, 172, 992, 1811}, // PITCH {2, 172, 992, 1811}, // THROTTLE {3, 172, 992, 1811}, // YAW {4, 172, 992, 1811}, // MODE_SWITCH {5, 172, 992, 1811}, // ARM_SWITCH {6, 172, 992, 1811}, // RESERVED {7, 172, 992, 1811}, // BUZZER };4.2 中位与行程校准的完整流程
不同遥控器的中位点、输出行程都存在差异,出厂标称 992 不一定准。我在调试菜单里做了个原地校准模式:逐个拨动摇杆打到最左、最右、中位,把读到的原始 SBUS 数值记录下来,写入 Flash 参数区。以后更换遥控器或者重新绑定接收机,只需要重新做一次校准,不需要重新编译固件。
校准的逻辑是:先读 50 次原始值取平均,排除抖动;然后把记录的 min/mid/max 写入参数存储。控制层读取时按下式归一化:
normalized = (raw_value - mid_val) * 2 / (max_val - min_val)这样输出大约在 [-1, 1] 区间,正负表示方向,绝对值表示行程占比。油门通道不需要正负方向,我单独映射成 0~100% 的比例。
实操心得:校准过程要在上电后执行,且保证摇杆全部回中,否则容易把错误的中位点写进去。一个不起眼但容易栽跟头的细节,就是接收机刚上电的前几秒 SBUS 帧可能不稳定,不要在这个时间段做采样,我让系统先等 2 秒再进入校准状态,避免采样到异常值。
4.3 失控保护逻辑与帧丢失判定
失控保护是航模环节里最不该省的一个功能。V3.0 里同时实现了两级保护:第一级是接收机端自己上报的失联标志,第二级是飞控端在固定时间内没有收到任何一帧 SBUS 数据,进入独立的超时保护。
SBUS 的 flags 字节第 3 位(bit 2)表示 frame lost,第 4 位(bit 3)表示 failsafe 激活。当接收机在遥控器信号丢失状态时,会将所有通道输出保存在接收机内部预设的失效值,同时置位相应标志。飞控解析到 failsafe 标志后,会立刻把飞行模式切换到自稳倾斜模式,并自动把油门降到安全怠速值,让飞机原地降高而不是自由落体。
飞控端超时保护是最后一道防线。我在主循环里每 10ms 检查一次“最后一帧 SBUS 的到达时间戳”,如果超过 250ms 没有新帧,就直接执行保护动作。这个时间阈值是根据遥控帧率 100Hz 得出的,正常情况下一帧都不会漏,超过两帧都没有就说明链路已经断了。保护动作要可预测,不要让飞控继续以最后一次摇杆指令执行,否则接收机突然断开后飞机会一直按照旧的油门方向飞出去,那比失控本身还危险。
还有一个容易忽略的细节:遥控器的油门通道默认是“保持最后值”还是“回落安全值”,最好在遥控器端设置成回落安全值。如果和飞控端的保护逻辑联动起来,整个链路在信号丢失时的行为才是确定性的。
5. 实测中的几个坑与排查方法
5.1 常见通信故障速查
实测阶段我整理了一张故障速查表,每次遇到通信异常直接按表排查,效率比瞎猜高一倍。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 通信完全没数据 | 电平未反相、接线断开、波特率错误 | 示波器量 SBUS 引脚电平波形 |
| 能收到帧但通道值全乱 | 偶校验配错、停止位配错 | 查看 UART 配置是否 8E2 |
| 偶发丢帧、间歇约等 | 供电不稳定、共地不良 | 检查接收机供电电源波纹 |
| 某个通道数值固定不动 | 遥控器通道未映射或输出异常 | 遥控器监视器逐通道检查 |
| 解锁后频繁自检失败 | 油门行程校准不对、最小值高于安全阈值 | 重新做行程校准 |
我遇到的第一个通信问题就在电平反相上。当时波形长这样:接收机输出一帧信号,示波器看到每个字节的高电平时间和低电平时间完全反了,UART 自然什么也解不出来。把三极管反相器补上后,波形瞬间恢复了规整的串口电平,解析立刻正常。这一课让我记住了:拿到不认识的“数字协议”,第一件事永远是拿示波器看信号极性,第二才是看波特率和帧格式。
5.2 偶发丢帧与抖动处理
正常 100Hz 的 SBUS 帧率下,一帧都不该丢。测试时发现情况不是这样:手里捏着天线和外壳时,解算出的通道值偶尔会出现一次跳变,频率倒不高,几十帧出现一次。排查过程很曲折,最后定位到是接收机天线附近有几根电机电源线,油门拉高时电磁干扰叠加到接收信号上,接收端偶发一个错误帧。
处理方式分了三步:第一步,接收机天线尽量挪到机架尾部外侧,和电机电源线保持至少 5cm 距离;第二步,飞控到接收机的 SBUS 信号线改成双绞线,绕在电源地线旁边,形成共模抑制回路;第三步,解析侧加了帧合法性过滤,同一帧如果头尾不对直接丢弃,不送进通道数据更新逻辑。三步完成后,丢帧率从几十分之一降到几乎为零。
通道值抖动是另一个层次的问题。来自遥控器电路本身的摇杆电位器磨损和 ADC 量化,都会带来小幅抖动量。控制层不能直接拿最原始值去控制,否则飞控姿态会一直小幅度震荡。我只做了一个轻量级滤波:对每一帧的通道值取上一次值和本次值的线性插值,相当于一阶低通,截止频率根据摇杆操作频率调的。注意不要过度滤波,否则操作感会严重发肉,飞机反应像隔着一层棉被,新手容易越飞越慌。
5.3 布线布出来的通信问题
通信系统还有一个容易被忽视的主因,那就是机架内部布线。V3.0 早期为了好看,把信号线和动力线绑在同一个线束里,结果发现 SBUS 信号偶尔被电调换相噪声打乱。后来我把线束重新梳理:动力线走机臂内侧、信号线走机架外侧,两者呈十字交叉而不是平行并排;接收机电源线也单独走,不和 SBUS 信号线并排。重新布线后,串口接收中断次数显著下降,很多以前“偶尔冒出来”的通信异常自己消失了。
对还在调机的朋友,我的建议是布线的时候多花十分钟做分离处理,省下的是之后无数个排查的夜晚。另外,飞控安装时别让单片机晶振靠接收机天线太近,否则射频信号会直接耦合到主控时钟上,出现比丢帧更难查的偶发复位问题。
写在最后的几点体会
这套 SBUS 遥控链路在蓝鸟 V3.0 上跑了快两个月,最大的感受是:通信协议本身不复杂,复杂的是它周边的环境适配。电平反转、共地、天线布局、配置参数,每一处看起来都特别基础,但任何一处偷懒,最终都会在某次试飞时给你颜色看。通信链路出问题不像姿态解算错误那么直观,它常常是“偶尔抖一下”“偶尔断一帧”,排查起来非常磨人。
我个人的经验是:一定要在第一轮调试时就配好示波器和逻辑分析仪,在通信协议这个环节,光靠日志去反推问题效率极低。现在这套 SBUS 解析代码已经稳定运行,也为后面的 CRSF 协议预留了接口。等我把 CRSF 双向遥测跑通之后,再来分享第二轮的测试数据。对于刚起步的朋友,建议不要一上来就追求最新协议,先把 SBUS 的链路吃透,它对理解串行遥控通信的底层逻辑很有帮助。