STM32解析SBUS协议:从反相电平到16通道解码的完整指南
2026/9/9 20:38:15 网站建设 项目流程

简介:面向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×8=200bit”,那是完全忘了校验位和停止位,写代码没问题,但算时序会差出25%。

1.2 一帧数据是怎么排布的

25字节分布非常紧凑:

字节偏移内容
0起始字节,固定0x0F
1~2216个通道数据,每个通道11bit,共176bit
23标志位
24结束字节,固定0x00

通道数据部分没有任何填充,16×11=176bit正好等于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; }

讲两行就明白规律了。通道0:buf[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下,一个字节占12bit,25字节共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三极管+上拉电阻成本低、电路简单边沿略缓,长线通信容易受干扰
反相器IC(NC7S04/74HC04等)波形干净、带载能力强多一颗芯片,占一点面积
MCU串口内部RXINV/TXINV省掉外部电路只有部分较新型号支持,F103这类老型号没有

选型上,如果只是开发板到接收机20cm内短连,一个三极管就够;如果是机架走线、旁边有电调和大电流,我建议直接用74HC04这类反相器,至少信号完整性有保证。不管用哪种方案,都要确认反相后的信号电平在MCU的VIH/VIL范围内,不要让5V直接灌进3.3V的引脚。

5.2 100000波特率的误差怎么控

SBUS串口对波特率误差的容忍度比一般UART要低一些,因为一帧有25字节×12bit=300bit,累积误差会放大。理论上72MHz主频给USART1分频到100000bps是能精确到720分频的,误差0%;但如果你的开发板在用内部RC振荡器,那就要小心,内部RC即便校准后也常有1%以上偏差,在一帧末尾就可能出现奇偶校验失败。

排查方法很简单:用示波器看SBUS反相后的波形,测量一个起始位到第一个停止位的时间。100000bps下一个bit是10us,8E2一个字节是12bit,所以一个完整字节约120us。如果测出来的脉宽和理论值偏差超过2%,先把时钟源换成外部晶振再说。别上来就调软件。

5.3 一次间歇性丢帧的完整排查链路

那是我调试一个六足机器人时发生的事。现象:主板通过SBUS读取遥控器,正常走动时偶发抖动,大约每十几秒会卡一下,像电机瞬间断了信号。初步怀疑是接收机问题,但拿着示波器去量接收机SBUS输出,波形规律、脉宽正常,于是把矛头转向主控板。

排查链路是这样的:

  1. 先用逻辑分析仪在MCU的RX引脚前、反相器之后抓波形。结果发现大部分帧正常,偶发在某字节的低电平中间出现一个窄的高电平毛刺,导致串口把这一位的采样值吃掉,进而整帧校验不过。
  2. 再做排除法:只接SBUS线,不启动电机,长时间观察,毛刺消失;一启动电机和电调,毛刺又出现。很明显是电机电调的开关噪声串进了信号线。
  3. 最后确认根因:SBUS线和电机电源线在机架里并行走线,反相器输出到MCU RX的这段又没有屏蔽,电调PWM斩波时产生的共模干扰直接耦合进来。
  4. 处理办法:把SBUS线换成双绞线并尽量远离电源线;反相器尽量靠近MCU摆放;在RX引脚对地加一个100pF小电容做高频滤波。改完之后,连续跑了两个小时没有丢帧。

这个案例给我的经验:SBUS本身是低速协议,但它工作在强干扰环境时,物理层的小问题会被放大成“丢帧、乱码、通道漂移”。排查顺序一定是先物理层后协议层,先示波器后改代码。

5.4 最后还是想叮嘱几句

给准备动手的朋友几条实在建议。第一,拿到接收机第一步是示波器或逻辑分析仪抓原始波形,确认极性和电平,不要一上来就写解析代码。第二,代码里要同时保留“原始字节流打印”和“解析后通道值打印”,排查问题时这两样缺一不可。第三,标志位不是可有可无的装饰,失控保护逻辑一定在控制逻辑之前处理。第四,做编码器时多用自环回测,把TX和RX通过反相器接在一起,通了再往外接设备。SBUS看着折腾人,但只要把物理层这几条规矩弄清,后面就顺了。

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

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

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

立即咨询