简介面向STM32嵌入式开发者资源提供完整SBUS协议双向解析与编码实现适用于无人机飞控、机器人遥控等需要高实时性多通道信号传输的场合要求读者具备UART、DMA及基本中断编程基础。压缩包内共2个文件sbus.h与sbus.c源码合计约4KB结构精简、便于阅读移植。目前已有7608人浏览学习。源码基于STM32F407实现250Kbps的UART配置、DMA自动接收与中断触发完成25bit数据帧的校验、反码解码将18路通道值提取到系统变量中同时支持将控制量反向编码为SBUS帧输出还包含丢帧检测与恢复策略。sbus.h对函数与结构体做了清晰声明可直接集成到飞控或其他控制模块也可快速适配其他STM32系列是学习通信协议栈与嵌入式实时处理的实用参考。 调过遥控车或无人机的人应该都体会过遥控接收机上密密麻麻排着十几根PWM信号线既占引脚又难走线。现在中高端接收机基本都支持SBUS一根线把16个通道串回来主控侧只需要一个UART。但我第一次在STM32上解析SBUS时以为改个波特率就能收结果示波器上波形有、数据全乱折腾半晚上才发现是反相电平的问题。这篇把完整流程写透SBUS协议到底怎么回事、25字节帧怎么拆出16个通道、怎么从零开始编码一帧SBUS以及我联调时踩过的各种坑。准备用STM32接遥控接收机或者想自己做SBUS模拟器/测试台的朋友可以直接按这篇文章来。1. 先看懂SBUS的“立规矩”反相电平、100000波特率、25字节帧结构SBUS看起来像个串口协议但专门跟习惯直觉对着来。它有四条硬规矩反相TTL电平、100000bps波特率、8E2帧格式、每帧固定25字节。四条里漏掉一条解析都跑不起来。1.1 收发双方的核心参数一张表最直观参数数值波特率100000 bps数据位8位校验方式偶校验Even停止位2位电平逻辑反相TTL空闲低电平单帧长度25字节典型发送周期14ms部分接收机支持7ms关于8E2多说一句它和常见8N1不一样物理线路上每个字节实际是“1起始位8数据1校验2停止”一共12个bit。SBUS在100000bps下每个bit是10us。网上有些文章把25字节算成“25×8200bit”那是完全忘了校验位和停止位写代码没问题但算时序会差出25%。1.2 一帧数据是怎么排布的25字节分布非常紧凑字节偏移内容0起始字节固定0x0F1~2216个通道数据每个通道11bit共176bit23标志位24结束字节固定0x00通道数据部分没有任何填充16×11176bit正好等于22字节所以解析时只能按11bit连续切分。标志字节各位含义如下Bit含义0~3ch17 ~ ch20遥控器上的开关量通道4Frame Lost无线链路丢帧标志5Failsafe失控保护激活标志6~7保留如果你只是拿4个通道玩小车通道17到20可以不管但Frame Lost和Failsafe务必要读后面有一节专门讲。1.3 为什么这个反相让不少人栽跟头普通UART串口空闲时是高电平起始位是一个下降沿。SBUS刚好反过来空闲是低电平起始位是一个上升沿。串口外设普遍靠下降沿启动接收你把接收机的SBUS输出直接用杜邦线接到STM32的RX最常见的表现就是串口完全收不到数据或者收到一堆错位乱码。我在调试时用示波器量过波形形状和串口一模一样但整体极性是反的。所以必须先把信号反相一次恢复成普通UART电平再进MCU。2. 接收解析把25字节还原成16个通道值2.1 串口初始化的三个关键设置SBUS物理层的“100000、8E2”要直接反应到串口配置里。用HAL库USART1初始化关键部分就是这几行huart1.Init.BaudRate 100000; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_2; huart1.Init.Parity UART_PARITY_EVEN;这里有个经典坑很多人看到8E2就以为是9位数据加偶校验把WordLength配成UART_WORDLENGTH_9B。在STM32的USART寄存器语义里8E2是8个数据位配上偶校验和2个停止位M位应配置为8位数据。配成9B之后串口会把校验位也当成数据收进来解析出来的通道值会整体偏移而且很难从现象上想到是这里的问题。信号反相如果在硬件上做MCU这边不需要特殊配置如果你的STM32型号串口外设带RXINV/TXINV也可以直接写寄存器开启接收反相省掉外部电路。没有这个位的型号——比如F103——老实加硬件反相器别在软件里硬扛。2.2 11bit通道数据的拼接与解码SBUS的16个通道每个通道11bit通道值范围0~2047遥控器摇杆一般输出172~1811之类的中位值飞控常用PPM值还得再换算。先把原始通道值解出来#define SBUS_START_BYTE 0x0F #define SBUS_END_BYTE 0x00 #define SBUS_FRAME_LEN 25 typedef struct { uint16_t ch[16]; uint8_t ch17; uint8_t ch18; uint8_t ch19; uint8_t ch20; uint8_t frame_lost; uint8_t failsafe; } SBUS_t; void SBUS_Parse(const uint8_t *buf, SBUS_t *sbus) { if (buf[0] ! SBUS_START_BYTE || buf[24] ! SBUS_END_BYTE) { return; } sbus-ch[0] ((uint16_t)buf[1] | ((uint16_t)buf[2] 8)) 0x07FF; sbus-ch[1] ((uint16_t)buf[2] 3 | ((uint16_t)buf[3] 5)) 0x07FF; sbus-ch[2] ((uint16_t)buf[3] 6 | ((uint16_t)buf[4] 2) | ((uint16_t)buf[5] 10)) 0x07FF; sbus-ch[3] ((uint16_t)buf[5] 1 | ((uint16_t)buf[6] 7)) 0x07FF; sbus-ch[4] ((uint16_t)buf[6] 4 | ((uint16_t)buf[7] 4)) 0x07FF; sbus-ch[5] ((uint16_t)buf[7] 7 | ((uint16_t)buf[8] 1) | ((uint16_t)buf[9] 9)) 0x07FF; sbus-ch[6] ((uint16_t)buf[9] 2 | ((uint16_t)buf[10] 6)) 0x07FF; sbus-ch[7] ((uint16_t)buf[10] 5 | ((uint16_t)buf[11] 3)) 0x07FF; sbus-ch[8] ((uint16_t)buf[12] | ((uint16_t)buf[13] 8)) 0x07FF; sbus-ch[9] ((uint16_t)buf[13] 3 | ((uint16_t)buf[14] 5)) 0x07FF; sbus-ch[10] ((uint16_t)buf[14] 6 | ((uint16_t)buf[15] 2) | ((uint16_t)buf[16] 10)) 0x07FF; sbus-ch[11] ((uint16_t)buf[16] 1 | ((uint16_t)buf[17] 7)) 0x07FF; sbus-ch[12] ((uint16_t)buf[17] 4 | ((uint16_t)buf[18] 4)) 0x07FF; sbus-ch[13] ((uint16_t)buf[18] 7 | ((uint16_t)buf[19] 1) | ((uint16_t)buf[20] 9)) 0x07FF; sbus-ch[14] ((uint16_t)buf[20] 2 | ((uint16_t)buf[21] 6)) 0x07FF; sbus-ch[15] ((uint16_t)buf[21] 5 | ((uint16_t)buf[22] 3)) 0x07FF; sbus-ch17 (buf[23] 0) 0x01; sbus-ch18 (buf[23] 1) 0x01; sbus-ch19 (buf[23] 2) 0x01; sbus-ch20 (buf[23] 3) 0x01; sbus-frame_lost (buf[23] 4) 0x01; sbus-failsafe (buf[23] 5) 0x01; }讲两行就明白规律了。通道0buf[1]的8bit加上buf[2]的8bit拼成16bit后取低11位所以是buf[1] | buf[2]8再0x07FF。通道1通道0已经把buf[2]的低3位用掉了通道1从buf[2]第4位开始先右移3把低3位丢掉再拼上buf[3]左移5位作为高6位同样0x07FF。后面每一行都是同一个逻辑从连续位流里按11bit切块。不要死背这几行建议理解规律后用脚本生成解析代码或者封装成位读取函数否则改一个通道定义就很容易改错。2.3 用状态机收帧而不是死等DMA接收侧我推荐状态机理由很简单SBUS一帧14ms根本不需要DMA那种高性能通道倒是丢帧恢复逻辑更重要。一个典型实现#define STATE_WAIT_START 0 #define STATE_RECEIVING 1 uint8_t sbus_state STATE_WAIT_START; uint8_t sbus_raw[25]; uint8_t sbus_cnt; void SBUS_OnByte(uint8_t byte) { if (sbus_state STATE_WAIT_START) { if (byte SBUS_START_BYTE) { sbus_raw[0] byte; sbus_cnt 1; sbus_state STATE_RECEIVING; } } else { sbus_raw[sbus_cnt] byte; if (sbus_cnt SBUS_FRAME_LEN) { sbus_state STATE_WAIT_START; if (sbus_raw[24] SBUS_END_BYTE) { SBUS_t sbus; SBUS_Parse(sbus_raw, sbus); /* 这里把sbus数据交给控制循环 */ } } } }这个状态机虽然短但一定要加超时复位。接收途中如果被干扰丢掉0x0F状态机会一直卡在STATE_RECEIVING后面的正常帧全被当成同一帧的延续收不到任何有效数据。我的做法是在定时器中断里维护一个毫秒计数普通14ms模式时超过20ms没有收到任何新字节就把状态强制复位回STATE_WAIT_START。这个细节很多人漏掉也是解析一段时间后突然“卡死”的常见原因。Futaba有些接收机支持7ms高速模式超时时间要相应缩短不能一个参数通吃所有设备。3. 解析之后别急着用标志位与掉线逻辑才是不失控的底线3.1 第24字节里藏着4个开关通道很多新手解析完16个通道就收工了其实第24字节下标23也很有用。bit0到bit3对应遥控器上的4个开关通道对应到OpenTX里就是你映射的SF、SE这类两段开关做自定义混控、切飞行模式、开关某些功能都靠它们。读取方法在解析代码里已经写了判断一下对应bit即可。3.2 Failsafe和Frame Lost在工程里怎么用bit4的Frame Lost表示无线链路上这一帧有没有丢包bit5的Failsafe表示接收机是否进入了失控保护状态。两个标志独立不要混在一起判断。如果你做一个遥控小车或者机器人底盘拿到通道值后直接驱动电机一旦遥控器关机或信号被遮挡通道值可能保持最后一帧机器会继续往前冲后果很严重。所以解析完成之后至少加一条防护逻辑if (sbus.failsafe || sbus.frame_lost) { // 进入安全模式电机停转、舵机回中、给出告警 motor_stop(); return; }还要考虑完全掉线的情况也就是连续一段时间没收到任何有效帧。比如在每次解析成功后记录last_sbus_tick HAL_GetTick();主循环里判断HAL_GetTick() - last_sbus_tick 200超过200ms没收到新帧就认为接收机掉线了同样要走停车逻辑。3.3 一个“通道值自动漂移”的真实案例我之前做过一个遥控小车底盘调试时发现车在直线行驶中会突然顿一下读取串口打印的通道值发现油门通道经常从1500跳到1200然后立刻恢复。第一反应是电位器抖动但换了遥控器还是一样。后来把原始帧和解析后的通道值同时打出来发现变化的那几帧其failsafe位为1也就是说不是摇杆抖动而是接收机在特定位置进入了失控保护输出的是我预设的安全油门。找到问题之后就清楚了任何failsafe置位的帧都不能作为正常控制数据使用。修正逻辑后小车恢复正常。4. 反向编码从STM32发出标准SBUS帧4.1 确定发送周期与帧间隔有时候我们需要做一个SBUS信号源比如飞控测试台、接收机模拟器、或者自己搞一套遥控链路。这时候就要会编码。标准SBUS帧周期约14ms也就是大约70Hz。100000bps、8E2下一个字节占12bit25字节共300bit一帧发完需要3ms。定时器周期设14ms串口在3ms内发完剩余11ms完全空闲对主循环几乎无压力。如果对接的设备支持高速模式可以用7ms周期但务必确认接收方兼容后再改。4.2 打包代码位流连续写入25字节编码是解码的反向把16个11bit数据按顺序压进22字节。我的做法是维护一个32位位缓冲每加入一个通道就累计11bit满8bit就向输出写一个字节。这样比逐通道手工移位更不容易错通道数增减也不影响核心逻辑。void SBUS_Encode(const SBUS_t *sbus, uint8_t *buf) { uint8_t *p buf 1; uint32_t bit_buf 0; uint8_t bit_cnt 0; int i; buf[0] SBUS_START_BYTE; memset(buf 1, 0, 22); for (i 0; i 16; i) { bit_buf | ((uint32_t)(sbus-ch[i] 0x07FF)) bit_cnt; bit_cnt 11; while (bit_cnt 8) { *p (uint8_t)(bit_buf 0xFF); bit_buf 8; bit_cnt - 8; } } if (bit_cnt 0) { *p (uint8_t)(bit_buf 0xFF); } buf[23] (sbus-ch17 ? 0x01 : 0) | (sbus-ch18 ? 0x02 : 0) | (sbus-ch19 ? 0x04 : 0) | (sbus-ch20 ? 0x08 : 0) | (sbus-frame_lost ? 0x10 : 0) | (sbus-failsafe ? 0x20 : 0); buf[24] SBUS_END_BYTE; }因为总bit数176正好等于22字节循环结束后bit_cnt必然小于8最后补一次即可。前置的memset保证了字节23之前没有残留数据。注意ch值先做0x07FF防止外部传入大于2047的值把位流顶穿。4.3 联调验证把TX和RX接到一起做自测编码器写完怎么验证我的习惯是直接自环把STM32的TX信号经反相器接到RX主循环里定时调用SBUS_Encode传入一组测试值比如ch[0]1024、ch[1]1500然后看解析结果是否一致。如果一致说明编码、解码两边的位拼接没有方向性错误如果不一致优先检查反相电路或串口配置而不是代码。通了之后再拿这个板子去冒充接收机给飞控供数调PID时不用在遥控器上手忙脚乱非常方便。5. 联调避坑反相电路、波特率误差和一次丢帧疑难杂症5.1 硬件反相的三种做法对比方案优点缺点NPN三极管上拉电阻成本低、电路简单边沿略缓长线通信容易受干扰反相器ICNC7S04/74HC04等波形干净、带载能力强多一颗芯片占一点面积MCU串口内部RXINV/TXINV省掉外部电路只有部分较新型号支持F103这类老型号没有选型上如果只是开发板到接收机20cm内短连一个三极管就够如果是机架走线、旁边有电调和大电流我建议直接用74HC04这类反相器至少信号完整性有保证。不管用哪种方案都要确认反相后的信号电平在MCU的VIH/VIL范围内不要让5V直接灌进3.3V的引脚。5.2 100000波特率的误差怎么控SBUS串口对波特率误差的容忍度比一般UART要低一些因为一帧有25字节×12bit300bit累积误差会放大。理论上72MHz主频给USART1分频到100000bps是能精确到720分频的误差0%但如果你的开发板在用内部RC振荡器那就要小心内部RC即便校准后也常有1%以上偏差在一帧末尾就可能出现奇偶校验失败。排查方法很简单用示波器看SBUS反相后的波形测量一个起始位到第一个停止位的时间。100000bps下一个bit是10us8E2一个字节是12bit所以一个完整字节约120us。如果测出来的脉宽和理论值偏差超过2%先把时钟源换成外部晶振再说。别上来就调软件。5.3 一次间歇性丢帧的完整排查链路那是我调试一个六足机器人时发生的事。现象主板通过SBUS读取遥控器正常走动时偶发抖动大约每十几秒会卡一下像电机瞬间断了信号。初步怀疑是接收机问题但拿着示波器去量接收机SBUS输出波形规律、脉宽正常于是把矛头转向主控板。排查链路是这样的先用逻辑分析仪在MCU的RX引脚前、反相器之后抓波形。结果发现大部分帧正常偶发在某字节的低电平中间出现一个窄的高电平毛刺导致串口把这一位的采样值吃掉进而整帧校验不过。再做排除法只接SBUS线不启动电机长时间观察毛刺消失一启动电机和电调毛刺又出现。很明显是电机电调的开关噪声串进了信号线。最后确认根因SBUS线和电机电源线在机架里并行走线反相器输出到MCU RX的这段又没有屏蔽电调PWM斩波时产生的共模干扰直接耦合进来。处理办法把SBUS线换成双绞线并尽量远离电源线反相器尽量靠近MCU摆放在RX引脚对地加一个100pF小电容做高频滤波。改完之后连续跑了两个小时没有丢帧。这个案例给我的经验SBUS本身是低速协议但它工作在强干扰环境时物理层的小问题会被放大成“丢帧、乱码、通道漂移”。排查顺序一定是先物理层后协议层先示波器后改代码。5.4 最后还是想叮嘱几句给准备动手的朋友几条实在建议。第一拿到接收机第一步是示波器或逻辑分析仪抓原始波形确认极性和电平不要一上来就写解析代码。第二代码里要同时保留“原始字节流打印”和“解析后通道值打印”排查问题时这两样缺一不可。第三标志位不是可有可无的装饰失控保护逻辑一定在控制逻辑之前处理。第四做编码器时多用自环回测把TX和RX通过反相器接在一起通了再往外接设备。SBUS看着折腾人但只要把物理层这几条规矩弄清后面就顺了。本文还有配套的精品资源点击获取