简介:一套面向航模与飞控开发者的STM32 SBUS接收机解析完整工程,覆盖UART配置、中断接收、SBUS协议解析以及C#上位机串口通信。资源共618个文件,压缩包6.31MB,以338个c源码、106个头文件等单片机端工程为主,另含9个cs上位机源码与sln工程文件,以及uvprojx、icf等工具链配置,便于直接用Keil/VS打开编译与二次开发。已有2527人学习下载。代码包含SBUS数据帧同步、10位通道提取、失锁标志判断、校验处理及上位机实时显示逻辑,可帮助理解从接收机信号到上位机界面的完整数据链路。整体目录结构清晰,适合无人机、航模等嵌入式学习者参考,也可作为飞控项目协议解析模块的开发模板。 我入坑航模和嵌入式有一段时间了,第一次被 SBUS 折腾到怀疑人生,是在做一台自平衡小车的时候:遥控器、接收机都通电了,示波器也看得到信号,可 STM32 串口收过来全是乱码。后来才明白,SBUS 接收机输出的不是普通串口信号,它是一套带着反相、非标准波特率和奇偶校验的“特殊串口协议”。这篇笔记我把从硬件接线到协议解析的完整过程整理出来,给想用 STM32 读取 SBUS 接收机的朋友做个参考,尤其适合已经在用 PPM/普通串口、想进一步降低延时提升通道数的开发者。
1. SBUS协议拆解:为什么STM32不能直接拿串口例程来读
SBUS 的底层确实是串口,但它和平时用的 115200-8-N-1 差得很远。如果直接把接收机的 SBUS 引脚连到 STM32 的 RX,然后拿一个标准串口例程去读,大概率什么也读不出来,或者读出来一堆错位数据。原因主要有三点:负逻辑、100000bps 波特率、8E2 帧格式。
1.1 一帧SBUS数据到底长什么样
SBUS 每一帧固定 25 字节。第 1 字节是帧头 0x0F,第 25 字节是帧尾 0x00,中间 22 字节用于打包 16 个通道的数据。每个通道不是单独的 8 位或者 16 位,而是按 11 位连续排列,也就是说 16 个通道总共 176 位,刚好塞满这 22 字节。
第 24 字节是状态位,常见的定义是:
- bit7:第 17 通道数字量
- bit6:第 18 通道数字量
- bit5:丢帧标志(frame lost)
- bit4:Failsafe 激活标志
这里我要提醒一句:0x00 作为帧尾并不是唯一的,数据区里面完全可能出现 0x00,所以不能简单通过“找到 0x00 就认为是帧尾”来判断。更稳的做法是先找 0x0F 帧头,再按 25 字节定长读取,最后检查第 25 字节是否为 0x00。
1.2 负逻辑、100000bps、8E2三个硬门槛
普通串口空闲时 TX 是高电平,起始位是低电平;SBUS 正好相反,空闲时为低电平,起始位是高电平。如果用普通串口逻辑去采样,整帧高低都会被翻转,解出来的通道值完全不对。
波特率也不是常用的 115200,而是 100000bps。很多调好的串口助手没有这个选项,需要手动输入。帧格式是 8 个数据位、偶校验、2 个停止位,也就是常说的 8E2。注意这里的“8 位数据”和 STM32 HAL 库里的配置要匹配:打开奇偶校验后,串口的字长逻辑要选 8 位,而不是在 9 位模式下再叠加校验位。
这三个门槛缺少任何一个都会翻车。刚开始调试时,我建议先用逻辑分析仪在 SBUS 引脚上看一眼:如果你看到空闲电平是低,起始位是向上跳的,那就说明接收机输出的确实是负逻辑 SBUS,必须经过反相之后才能进 STM32 的 RX。
2. 硬件连接与信号反相:最容易忽视的第一道坎
2.1 接收机的SBUS引脚输出的是反相信号,怎么办
很多开源飞控板子上会印着 SBUS 和一个倒置的标记,比如“SBUS/INV”,这就是因为板子上已经有一颗反相器。如果你是在自己做板子或者用开发板调试,就需要自己处理反相。
用 NPN 三极管做最实用,成本也最低。电路大致是:接收机 SBUS 输出通过一个 1kΩ 电阻接到三极管基极,发射极接地,集电极接 STM32 的 RX,同时集电极通过 10kΩ 上拉电阻到 3.3V。这样接收机输出高电平时三极管导通,STM32 收到低电平;接收机输出低电平时三极管截止,STM32 收到高电平,正好把负逻辑翻转过来。
也可以用 74HC04、74LVC1G04 这样的单门反相器。选 74LVC1G04 的好处是它支持 3.3V 供电,输入耐压也够 5V,接线更简单。如果你手头没有反相器,千万不要直接把 SBUS 接到 STM32,轻则收不到数据,重则烧引脚。
2.2 电平匹配与共地细节
航模接收机一般由 5V 或电池电压供电,SBUS 输出高电平可能是 3.3V,也可能是 5V。STM32 的 RX 引脚是否耐受 5V,需要查对应型号数据手册。大部分 STM32 引脚标注为 FT(5V tolerant),可以用 5V 电平直接输入,但保险起见,建议在接收机 SBUS 和反相器之间串联一个 1kΩ 电阻,配合反相器输入端自带上拉或者下拉,既能限流也能分压。
共地是我反复强调的一点:接收机的 GND 和 STM32 的 GND 必须连接,否则参考电平不一致,SBUS 信号会出现噪声甚至完全乱跳。实际接线顺序建议是:先接 GND,再接电源,最后接 SBUS 信号线。带电插拔 SBUS 线偶尔会触发接收机重启,调试时养成断电接线的习惯,能省不少麻烦。
2.3 上电后先用逻辑分析仪确认波形
拿到一套新接收机,我建议先别写代码,直接用逻辑分析仪抓一下 SBUS 引脚波形。不需要解协议,只看两点:一是空闲电平是不是低电平,二是波特率对不对。逻辑分析仪里如果能看到一串约 3ms 间隔的脉冲串,基本就说明接收机在输出 SBUS。
有些廉价逻辑分析仪支持协议解码,可以直接选 SBUS 或者自定义 100000bps 8E2 来解码。如果没有协议解码,就用测量功能看一位的宽度:100000bps 下每一位是 10μs。看到一个起始位之后跟着 11 位(8 位数据 + 1 位校验 + 2 位停止)的情况,说明信号格式也正常。这里看波形比直接上代码更快,能直接排除接线问题。
3. 基于HAL库的SBUS接收实现:DMA+空闲中断最稳
3.1 CubeMX里串口参数怎么填
用 STM32CubeMX 初始化串口,参数这样配:
- Baud Rate:100000
- Word Length:8 Bits(加上 Parity 后实际是 9bit 传输,但 HAL 里的配置要选 8 Bits)
- Parity:Even
- Stop Bits:2
- 方向:Receive Only 或者收发都行,如果用单线接法,只开接收更干净
很多人会在 Word Length 里选 9 Bits,认为奇偶校验算一位所以是 9 位,这是错的。STM32 的硬件里当 Parity 使能时,数据字长必须选 8 Bits,校验位会自动追加到帧里。选 9 Bits 会让接收到的数据变成 9 位用户数据 + 校验位,解析时移位全乱。
串口时钟的选择也影响波特率。比如 STM32F103 在 72MHz 主频下,USART1 挂在 APB2 上(72MHz),100000bps 的 DIV 是 720,正好是整数,误差为 0;如果某个型号的串口时钟不是 100MHz 的整数倍,要打开计算器确认一下误差,一般误差在 1% 以内勉强能跑,但不如整数倍稳。
3.2 接收缓冲与空闲中断的处理思路
SBUS 一帧大约 3ms,帧与帧间隔大约 11ms。如果只做 25 字节定长接收,最怕的是起始位置不对导致整帧错位。我推荐的做法是:DMA 循环接收 + 串口空闲中断。
原理很简单:DMA 把串口数据持续搬运到一个环形缓冲,空闲中断在总线空闲时触发,这时我们去读取 DMA 剩余计数,知道这一帧结束的位置,再从缓冲区里找 0x0F 帧头。伪代码如下:
#define SBUS_RX_BUF_SIZE 64 static uint8_t sbus_rx_buf[SBUS_RX_BUF_SIZE]; void sbus_uart_init(UART_HandleTypeDef *huart) { HAL_UARTEx_ReceiveToIdle_DMA(huart, sbus_rx_buf, SBUS_RX_BUF_SIZE); __HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT); }这里禁用 DMA 半传输中断,是因为 SBUS 帧太短,半满中断会频繁打断主循环,而且容易在没有完整帧时就去解析。空闲中断只在串口线上安静下来时触发,天然对应一帧结束。
在空闲中断回调里,要重新调用一次接收函数,让下一次 DMA 继续接收。很多新手只收到第一帧,就是因为回调里没有重新启动接收。
3.3 通道解包函数:从25字节里抠出16个11bit通道
拿到一帧完整数据后,核心工作是把 22 字节里的 16 个 11bit 通道拆出来。我自己常用的解析函数是:
#define SBUS_FRAME_SIZE 25 #define SBUS_CHANNEL_NUM 16 void sbus_parse(const uint8_t *frame, uint16_t channels[SBUS_CHANNEL_NUM], uint8_t *flags) { const uint8_t *data = frame + 1; for (int ch = 0; ch < SBUS_CHANNEL_NUM; ch++) { uint32_t bitpos = ch * 11; uint32_t byte_index = bitpos / 8; uint32_t bit_offset = bitpos % 8; uint32_t bits; bits = data[byte_index] | (data[byte_index + 1] << 8); if (bit_offset > 5) { bits |= (uint32_t)data[byte_index + 2] << 16; } channels[ch] = (bits >> bit_offset) & 0x07FF; } *flags = frame[23]; }这个函数的思路是按“位流”去理解 22 字节数据区:通道从第 0 位开始,每个通道占 11 位,所以第 ch 个通道的起始位就是 ch * 11。取出跨越的 2~3 个字节,再右移 offset,就能拿到 11 位有效值。
解析之后,channels[0]到channels[15]就是遥控器上的 16 个通道值,范围一般 0~2047。需要特别说明:不同遥控器的中立点不一定在 1024,有的在 992,有的在 1024,后面做 PWM 输出时最好做一次校准,不要直接拿原始值去驱动舵机。
4. 调试中踩过的三个坑:从没数据到乱数据
4.1 忘加反相,收到的全是0xFF或0x00
我第一次调试时,以为 SBUS 就是普通的 TTL 串口,直接把接收机信号线接到了 STM32 的 RX,结果串口助手打到 100000 波特率,收进来的数据不是 0xFF 就是 0x00,偶尔有几个看似正常的字节但连不上帧头。
问题就出在负逻辑。接收机空闲时输出低电平,STM32 没有收到起始位;当有数据时,原本应该是起始位的低电平,反而成了高电平,整帧被当成噪声。后来我在信号线上加了一颗三极管反相,再打开串口助手,立刻就能看到以 0x0F 开头、0x00 结尾的数据流。
给个很直观的判断方法:用逻辑分析仪看波形,如果空闲电平是低,起始位是向上的,就必须反相。反相之后,空闲电平变成高,起始位变成向下,此时 SBUS 才符合 STM32 串口外设的采样习惯。
4.2 波特率误差和帧错位
SBUS 对时序要求比普通遥控协议严格。100000bps 的每一位是 10μs,如果串口时钟分频出来不是恰好 100000,长时间接收之后会出现位偏移。表现是:偶尔能解出帧头,但通道值总有几个在跳,甚至帧尾 0x00 找不到。
排查方法很简单,先确认串口时钟频率,再用公式算分频值。STM32 很多系列支持小数分频,但如果配置的是整数分频,就尽量选一个能用外部晶振倍频到整数倍 100000 的主频。比如 STM32F103 用 72MHz 主频,USART1 分频值 720,零误差;如果用了不常用的 PLL 配置导致 APB2 时钟 71.5MHz,波特率误差就会超过 0.5%,SBUS 还能勉强跑,但长时间大量数据后偶发错位免不了。
另外,帧错位和波特率误差是两码事。如果你在 DMA 缓冲里看到的数据流是连续多个 0x0F,说明接收机每帧都被正确接收,但解析起点没对齐。解决办法是搜索滑窗:从 DMA 缓冲中逐字节找 0x0F,一旦找到就认为这是一帧的起点,等待后续 24 字节,再检查第 25 字节是否为 0x00。如果帧尾不对,就丢弃当前帧,继续往后找,直到帧头和帧尾同时满足条件。
4.3 空闲中断回调后没重新开启接收,只收到第一帧
这是 HAL 库开发时最经典的“只进一次”问题。HAL_UARTEx_ReceiveToIdle_DMA在接收完一帧并触发空闲中断后,DMA 传输就结束了,如果不重新启动,串口后续数据不会再进缓冲区。
解决方法是把重新接收写在空闲中断回调里:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart1) { // 处理当前帧,这里要把 Size 对应的数据复制到 sbus_frame_buf process_sbus_frame(sbus_rx_buf, Size); // 重新启动 DMA 接收 HAL_UARTEx_ReceiveToIdle_DMA(&huart1, sbus_rx_buf, SBUS_RX_BUF_SIZE); } }注意:Size是在这次空闲事件中接收到的字节数,不一定等于 25。SBUS 接收机正常工作时每帧固定 25 字节,但刚上电或丢帧时可能多出半个字节。回调里最好做一次长度判断:Size >= 25才解析,否则只保留数据不执行通道提取。
回调里处理数据要快,不要做耗时操作。SBUS 帧间隔大约 14ms,如果回调里执行了比如打印日志、延时、大量浮点运算,就可能错过下一帧。比较稳妥的做法是回调里只拷贝到一个全局数组并置位标志,由主循环去解析。
5. 让读到的SBUS真正可用:帧有效性、滤波和映射
5.1 丢帧、Failsafe怎么判断
SBUS 协议里有两个状态位对实际控制非常重要:丢帧标志和 Failsafe 标志。解析时可以从flags的第 5 位和第 4 位读出来:
uint8_t frame_lost = (flags & 0x20) >> 5; uint8_t failsafe = (flags & 0x10) >> 4;如果frame_lost为 1,说明接收机与遥控器之间的射频信号不稳定,当前通道值不可靠。如果failsafe为 1,说明接收机已经进入失控保护状态,此时不能把解析出的通道值直接用于执行机构,应该让系统进入安全模式,比如停止电机或让舵机回到中位。
除了状态位,还可以在应用层做超时判断。每一帧 SBUS 的间隔大约是 14ms 或 7ms(不同厂家、不同发射模式可能不同)。你可以在主循环里记录最后一次收到有效帧的时间,如果超过 50ms 没有新帧,就可以认为 SBUS 链路已断开。这个时间阈值不要设得太小,因为遥控器在振动或电磁干扰下偶尔会丢一帧,50ms 已经足够灵敏也不会误判。
5.2 通道值怎么映射成PWM占空比
SBUS 解析出来的通道值范围是 0~2047,而遥控器摇杆中位通常对应 1024 左右。如果要用这个值去控制舵机,需要先转换成 1000us~2000us 的脉宽。
最简单的线性映射是:
uint32_t pulse_us = (channels[0] * 1000UL) / 2047UL + 1000UL;但实际使用中我发现这个公式太理想化。有些遥控器在摇杆两端输出的不是 0 和 2047,而是 32 和 2015 之类的边界值。建议在调试时先把遥控器打到最左、最右,记录实际读到的最大值和最小值,再做线性映射。否则你会发现舵机还没打满,PWM 就已经超限了。
另外,SBUS 的遥控器行程设置也会影响输出。有些遥控器可以设置 End Point(EPA),改了这个值之后,接收机输出的通道范围会跟着变。做飞控或者小车时,最好在代码里加一个 PWM 限幅环节,把计算出来的脉宽钳位到 1000us~2000us 之间,防止误操作导致舵机反向堵转。
5.3 进阶:从SBUS到SBUS2,通道路数不够时怎么办
常规 SBUS 已经提供了 16 个比例通道加 2 个数字通道,对绝大多数航模和遥控车够用。但如果你用的是 FrSky 的某些新接收机,它可能输出的是 SBUS2 协议。SBUS2 在帧结构上更复杂,数据部分除了通道信息之外,还会插入传感器数据、RSSI 信息等,不能直接用上面的 25 字节固定长度解析。
如果你确实需要在 SBUS2 上做传感器数据读取,建议先去查对应接收机的官方协议文档。平时的应用层控制,还是优先选普通 SBUS 输出,因为实现起来稳定、资料也多。
如果 16 个通道还不够,我见过一些项目的做法是同时读取接收机的 SBUS 和 PPM 输出,把 PPM 的通道作为扩展输入。这种方案可行,但需要占用两路定时器输入捕获或两路串口,软件复杂度会增加不少。我自己做的遥控机械臂项目里,16 个通道已经能把每个关节的角度、夹爪开合、云台俯仰全部覆盖,所以我最终没有继续扩展 SBUS2,而是把精力放在了对通道值做一阶低通滤波和死区设置上。
最后分享一个调试技巧:用逻辑分析仪同时抓 SBUS 引脚和 STM32 的 RX 引脚,对比反相前后的波形,能非常直观地确认硬件是否正常。比用万用表猜电平省事太多。SBUS 解析这事,看着是串口通信,实际上硬件、协议、软件三方面都得对得上,把前面几步都理顺之后,后面再加云台控制、姿态解算就顺手多了。
本文还有配套的精品资源,点击获取