我第一次把遥控器接收机的SBUS输出接到STM32串口时,串口助手刷出来的不是整齐的0x0F开头帧,而是一堆间距随机的0x00、0x0F和大量错位字节。折腾到后半夜才意识到,SBUS根本不和标准UART电平兼容,信号是反相的,先解决硬件反转,再去谈协议解析才有意义。
这篇文章把两块内容串在一起讲:先拆透SBUS协议本身——帧结构、25字节里16个通道怎么映射、为什么用115200/8E2的串口参数;然后给出硬件反相器的两种落地设计,并基于STM32 HAL库的DMA+空闲中断写一套能直接用的解析链路。适合正在做飞控、舵机控制板、机器人遥控底盘,或者单纯想搞懂SBUS底层原理的读者。看完之后你不仅能跑通F103和F4系列,还能自己画一块带反相器的转接板。
1. SBUS这条总线到底在传什么:帧格式与电气特点
1.1 帧结构拆解:25字节里有176个有效位
SBUS是一个单线半双工串行协议,实际物理层就是UART,只不过参数比较特殊:波特率115200,数据位8位,偶校验,2个停止位,即8E2。每一帧固定25个字节,接收机以14ms周期发送一帧,部分接收机支持7ms快速模式。这25个字节的用途非常紧凑:
| 字节位置 | 长度 | 内容 |
|---|---|---|
| 第0字节 | 1 | 帧头,固定0x0F |
| 第1至22字节 | 22 | 16个通道数据,每通道11位 |
| 第23字节 | 1 | 标志位,包含ch17、ch18、failsafe、frame lost |
| 第24字节 | 1 | 帧尾,固定0x00 |
16个通道每个占11位,16乘以11等于176位,正好塞进22个字节里,也就是176位。这里的映射方式和传统PPM逐通道独立编码完全不一样,SBUS用的是连续位段方式:第一个通道从第0位开始取11位,第二个通道从第11位开始取11位,以此类推。也就是说,通道数据是跨字节边界的,一个字节里可能同时包含两个通道的部分数据。
数值范围方面,11位能表示0到2047。在遥控器协议里,这个值不会直接当成速度或角度,而是对应舵机脉宽:0x000对应1000微秒,0x7FF对应1500微秒中位,0xFFF对应2000微秒。所以拿到原始值以后,通常要换算成脉宽或者油门百分比。换算公式很简单:
pulse_us = (uint32_t)sbus.channels[i] * 1000 / 2047 + 1000;这基本就是SBUS给控制逻辑层的信息,其他一切交给姿态算法或者电机控制处理。我想强调一个容易忽略的点:SBUS本身不携带CRC校验,整帧只有帧头0x0F和帧尾0x00两个标记。接收端能做的最基本校验就是检查帧头帧尾和25字节长度,更高层的完整性依赖failsafe标志和协议时序。
1.2 为什么它是反相串口:电气层设计的来龙去脉
SBUS最大的迷惑点就是反相。标准UART在空闲时是高电平,起始位是一个下降沿到低电平。而SBUS物理层反过来:空闲时是低电平,起始位为高电平,数据位也是反逻辑的。
要理解为什么这么做,得从接收机内部电路说起。接收机射频前端输出后,协议芯片给出的SBUS信号经过一个PNP三极管或者反相驱动级,目的是为了兼容部分传统设备。另一个常见说法是,采用反相设计后,如果SBUS线上出现干扰或短路,默认状态是低电平,不会让舵机误触发。不管历史原因如何,实际结果就是:把SBUS输出直接接到普通STM32串口,主控会把空闲低电平识别为持续的发送状态,收到的数据必然乱掉。
反相信号还有一个特点,它不兼容TTL电平判断。STM32 TTL串口要求高电平大于0.7倍VDD才算是逻辑1,标准UART空闲高电平。SBUS的“空闲低、起始高”会让UART外设在引脚上没有跳变沿时认为总线忙,第一帧往往就丢了。所以实际工程里,不能指望写代码把RX引脚拉高来解决问题,必须在硬件上把信号再做一次反相,恢复成标准UART极性,或者利用STM32某些系列的RXINV硬件反相功能绕过外部电路。
技术验证上,最简单的方法是用逻辑分析仪抓SBUS引脚波形。标准UART的一个字节,波形是先低后高,起始位低电平,停止位高电平;而SBUS抓出来的字节波形正好相反,起始位高电平,停止位低电平。看到这个波形,你就知道必须添加反相处理,否则后续工作全白费。这里也顺带提一句,接收机的SBUS输出通常和遥控器的固件设置有关,有些接收机支持设置成SBUS、iBus、PPM或SBUS Output反转,但设置成非反相或“inverted off”并不常见,多数固件默认就是反相输出。
2. 反相器电路设计:从原理图到实际焊接
2.1 三极管反相方案:成本最低、改动最灵活
硬件反相器的第一种做法是单个NPN三极管,非常适合手头临时调试和飞控转接板。原理其实很简单:把接收机SBUS输出当作控制信号,控制三极管的导通与截止,集电极输出经过上拉电阻的相反电平。
电路可以这样搭:
- 接收机SBUS信号经过一个1kΩ电阻接到NPN三极管基极
- 三极管发射极接地
- 集电极接一个4.7kΩ到10kΩ的上拉电阻到3.3V
- 集电极同时接到STM32的RX引脚
当SBUS为低电平时,三极管基极电压低于开启阈值,处于截止状态,集电极被上拉电阻拉到3.3V,输出高电平,对应标准UART空闲状态。当SBUS为高电平时,基极电流让三极管饱和导通,集电极被拉到接近0V,输出低电平,也就是UART的起始位有效状态。这样就把“空闲低、起始高”的SBUS信号反相回“空闲高、起始低”的标准串口信号。
选型方面,常见的小信号三极管2N2222、S8050、BC547都可以。基极电阻1kΩ到2.2kΩ,上拉电阻4.7kΩ到10kΩ就行。我不建议用太大的基极电阻,因为SBUS波特率115200,一个位时间大约8.7微秒,基极电阻太大导致上升沿变缓,会直接吃掉半个位的有效窗口,严重时解出来的字节全是错的。
这个电路还有个隐藏优点:它自带电平转换能力。如果接收机工作在5V电平,而STM32是3.3V,三极管反相电路天然能把5V信号转换成3.3V逻辑,不需要额外加电平转换芯片。很多航模接收机的SBUS端口实际上是3.3V,但部分老款接收机内部可能是5V的,用这个方案都不用纠结。
2.2 单门逻辑芯片方案:SN74LVC1G04这类芯片怎么接
三极管方案虽然灵活,但分立元件多了以后,焊盘、走线、寄生电容都会影响信号质量,尤其是在飞机和高振动场景下。更稳妥的方案是用单路反相门芯片,比如SN74LVC1G04、MC74VHC1G04,封装通常是SOT-23-5,尺寸和一颗0805电阻差不多,但内部是标准逻辑门,输出波形非常干净。
SN74LVC1G04的引脚定义很固定:1脚VCC,2脚A输入,3脚GND,4脚Y输出,5脚NC或使能。实际接法比三极管方案还简单:
- VCC接3.3V,GND接地,靠近芯片放一个100nF去耦电容
- SBUS信号输入到A引脚
- Y引脚接到STM32的RX引脚
输入输出电平范围方面,SN74LVC1G04支持1.65V到5.5V供电,在3.3V供电下,输入高电平阈值大约1.7V左右,所以无论接收机输出3.3V还是5V信号,都能可靠识别。这类单门芯片传播延迟一般在几纳秒级别,对115200波特率的信号来说毫无压力。
如果手头只有多路反相器芯片,比如74HC04或者74HC14,也可以直接取其中一路,剩下的门闲置或者全部串联做缓冲。74HC14是施密特触发器输入,自带滞回特性,对付有噪声的长线SBUS信号反而更稳。不过74HC04和74HC14经常是SOIC-14封装,面积稍大,放在飞控板上不如单门芯片好布局。
2.3 布局与选型中的几个细节
反相器设计本身不复杂,但实际布线时有几个细节会影响可靠性。第一,反相器尽量靠近STM32的RX引脚,而不是靠近接收机。因为反相器输出的是标准UART信号,走线长了容易引入干扰,但SBUS反相信号本身抗干扰能力相对强。第二,如果接收机到STM32的线比较长,建议在反相器输入端并一个10kΩ下拉电阻,防止接收机未上电时输入端悬空导致输出乱跳。第三,无论是三极管方案还是逻辑门方案,输出端都不需要再接限流电阻,直接接到MCU RX即可,RX引脚本身是高阻抗输入。
这里还要提一个容易被忽略的坑:接收机内部输出级可能是开漏结构,必须在外部加一个上拉电阻。某些接收机不支持拉电流,如果你用三极管方案时发现低电平正常、高电平上不去,很可能就是少了一个上拉电阻。用万用表量一下接收机SBUS引脚对地的静态电压,如果空闲时接近0V,再用手动拉高测试能恢复,基本就是内部开漏输出。
3. STM32 HAL库侧配置:串口参数与DMA+IDLE接收链路
3.1 CubeMX中的配置参数,一个都不能错
硬件反相器做好后,接下来是STM32侧配置。我用的是STM32F103系列,但配置思路在F4、H7上完全一致。打开STM32CubeMX,选择对应型号,进入UART配置界面,关键参数如下:
| 参数项 | 设置值 |
|---|---|
| Mode | Asynchronous |
| Baud Rate | 115200 |
| Word Length | 8 Bits |
| Parity | Even |
| Stop Bits | 2 |
| Oversampling | 16 Samples |
| Enable DMA | RX, Circular Mode |
数据位这里要特别注意,HAL库里8位数据位+偶校验的组合,实际传输的总位数是8位数据加1位校验,但不包含停止位,所以通信双方要一致选择8E2。如果选成8N1或者8N2,收到的字节会大量出错,还会造成帧头对不齐。我遇到过很多新手直接把SBUS当成普通串口,配成8N1,结果逻辑分析仪显示波形完全正确,但STM32收到的数据就是不对,查到最后才发现是奇偶校验位不匹配。
DMA模式选择Circular循环模式,这样UART接收DMA会自动把数据写入环形缓冲区,不需要每次收到一帧后重新配置DMA。NVIC设置里,建议把USART全局中断使能打开,DMA中断也可以打开,但使用空闲中断的情况下,DMA中断不是必须的。如果使用HAL_UARTEx_ReceiveToIdle_DMA,空闲中断由HAL内部处理,我们只需要实现回调函数即可。
3.2 为什么用DMA+空闲中断,而不是逐字节接收
SBUS每帧25字节,每14ms来一帧。如果采用中断逐字节接收,每8.7微秒触发一次UART中断,对MCU来说虽然不至于崩溃,但在飞控这种高负载场景下,所有外设都在争抢CPU时间,逐字节中断会显著增加调度抖动。DMA+空闲中断的组合是更优解,DMA负责把UART接收寄存器里的数据搬进内存,CPU完全不用管,只有当一帧结束后出现总线空闲,UART空闲中断才会触发一次,通知CPU处理完整帧。
所谓空闲中断,就是串口在一段连续时间内没有收到新数据的标志。SBUS帧与帧之间有大约几毫秒的空闲间隔,这正好是检测帧边界的天然条件。用空闲中断比用定时器判断帧间隔更简单,因为它由UART硬件直接产生,不需要额外占用定时器资源。
代码初始化只有一行,但需要注意放置位置。我通常在main函数里先初始化UART,然后调用下面这行:
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, sbus_rx_buffer, SBUS_RX_BUF_SIZE);这个API在STM32CubeF1的1.8.0版本之后才提供,如果你用的是老版本HAL库,可能需要手动配置IDLE中断。手动方式其实也不复杂,在UART IRQHandler里判断__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE),然后清标志并调用HAL_UART_RxCpltCallback类似逻辑。但从新项目角度,我建议直接升级HAL库版本,用ReceiveToIdle这套API,代码会简洁很多。
3.3 接收缓存管理与帧到达回调
缓冲区定义需要注意,因为SBUS帧长25字节,但DMA是循环模式运行的,如果我设置缓冲区为64字节,接收时可能一帧数据跨缓冲区末尾,分成两段存放。比如帧的前10字节在缓冲区的偏移54处,后15字节回到偏移0处。处理这种情况最省事的方法是把缓冲区设得稍微大一点,并且从回调中获取实际接收长度,然后做环形缓冲区的拼接处理,或者简单点,直接把缓冲区设置为帧长25字节。
我推荐一种更实用的做法:缓冲区设成64字节,用HAL_UARTEx_ReceiveToIdle_DMA时指定接收长度SBUS_RX_BUF_SIZE,在回调函数中判断接收长度。代码如下:
#define SBUS_FRAME_SIZE 25 #define SBUS_RX_BUF_SIZE 64 uint8_t sbus_rx_buffer[SBUS_RX_BUF_SIZE]; volatile uint8_t sbus_frame_ready = 0; volatile uint16_t sbus_rx_size = 0;回调函数:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart1) { sbus_rx_size = Size; sbus_frame_ready = 1; HAL_UARTEx_ReceiveToIdle_DMA(&huart1, sbus_rx_buffer, SBUS_RX_BUF_SIZE); } }主循环里只需要检查sbus_frame_ready标志,然后把sbus_rx_buffer里的数据交给解析函数。如果接收到的Size小于25,说明可能是串口噪声或中间断帧,我会选择丢弃这一帧,等待下一帧完整数据。这里有个经验:由于SBUS每一帧之间是固定周期,偶尔丢一帧不影响大局,千万不要为了一次不完整帧去阻塞等待,那样会拖垮整个控制循环。
4. 从原始字节流到16通道数据:解析函数全实现
4.1 位段提取原理:11位通道在22字节中的分布
SBUS解析的核心难点在于从字节流中提取连续位段。先回顾一下通道分布规律:第1个通道占据数据区第0到第10位,第2个通道占据第11到第21位,第3个通道占据第22到第32位,依此类推。
把一个字节展开成8个bit,那么这16个通道在22字节中的排布并不对齐字节边界。例如第6通道的起始位可能在某个字节的第5位,数据会横跨两个甚至三个字节。提取方法通常有两个:一种是位运算逐位提取,另一种是先将连续字节数据复制到一个32位临时变量里,再通过移位和掩码取出目标通道。
位运算提取时,要小心C语言右移和字节序。SBUS是LSB first,即先发送低位,所以通道的11位在内存中的排列就是低字节在前。直接用uint8_t数组按索引读取是安全的,不需要考虑大小端问题,因为数据本身就是在内存中线性存放的。
提取通道的代码可以写成下面这样,索引i从0到15:
static uint16_t sbus_extract_channel(const uint8_t *data, uint8_t index) { uint16_t start_bit = index * 11; uint16_t byte_index = start_bit / 8; uint8_t bit_offset = start_bit % 8; uint32_t value = 0; value |= ((uint32_t)data[byte_index]) << 0; value |= ((uint32_t)data[byte_index + 1]) << 8; value |= ((uint32_t)data[byte_index + 2]) << 16; return (uint16_t)((value >> bit_offset) & 0x07FF); }这里用了连续三个字节组合成24位无符号数,再右移并掩码11位,能覆盖所有16个通道的任意位偏移。最后一个通道即第16个,它的11位最多延伸到数据区的第21和第22字节,所以读取data[byte_index+2]时,实际上是在读取数据区中超出该通道范围的部分,需要确保缓冲区里有一块安全内存。这就是为什么前面把DMA缓冲区设置成64字节,而不是刚好25字节的一个原因。
4.2 解析代码与状态结构体
我习惯定义这样一个SBUS状态结构体,把原始通道、脉宽、信号质量信息统一封装,方便其他模块直接调用。
typedef struct { uint16_t channels[16]; uint16_t pulse_us[16]; uint8_t ch17; uint8_t ch18; uint8_t frame_lost; uint8_t failsafe; uint8_t valid; uint32_t last_frame_tick; } sbus_t;完整的帧解析函数如下:
uint8_t sbus_parse_frame(uint8_t *buf, uint16_t size, sbus_t *sbus) { if (size < SBUS_FRAME_SIZE) { return 0; } if (buf[0] != 0x0F || buf[SBUS_FRAME_SIZE - 1] != 0x00) { return 0; } for (uint8_t i = 0; i < 16; i++) { sbus->channels[i] = sbus_extract_channel(&buf[1], i); sbus->pulse_us[i] = (uint16_t)((uint32_t)sbus->channels[i] * 1000 / 2047 + 1000); } sbus->ch17 = (buf[23] & 0x01) ? 1 : 0; sbus->ch18 = (buf[23] & 0x02) ? 1 : 0; sbus->frame_lost = (buf[23] & 0x04) ? 1 : 0; sbus->failsafe = (buf[23] & 0x08) ? 1 : 0; sbus->valid = 1; sbus->last_frame_tick = HAL_GetTick(); return 1; }解析原理不复杂,但我要提醒几个细节。首先是帧头和帧尾校验,不能只检查帧头0x0F,因为数据区里也可能出现0x0F字节,单靠帧头无法确认帧边界。帧尾0x00同样不是绝对可靠,但配合25字节定长和帧头检测,误判概率已经很低。其次,标志字节buf[23]的含义在不同版本的SBUS文档里略有差异,但最常见的定义是:bit0表示ch17的开关量,bit1表示ch18的开关量,bit2为Frame Lost,bit3为Failsafe激活。有些飞控开源代码还会用到bit4以上,不过目前主流接收机基本都只用前四位。
4.3 信号丢失与failsafe处理
解析出failsafe和frame_lost之后,必须在控制逻辑里针对这些状态做处理,而不是直接忽略。SBUS接收机失去遥控器信号后,会根据接收机的故障保护设置输出上限值或者保持最后一次有效值,同时置位failsafe标志。如果你在做舵机或电机控制,等解析数据再做决策就晚了。
我通常在主循环里维护一个超时判断:如果距离上一帧有效数据的tick超过30ms,就认为接收机掉线,把所有通道输出设置为安全值。具体代码如下:
if (HAL_GetTick() - sbus.last_frame_tick > 30) { sbus.valid = 0; for (uint8_t i = 0; i < 16; i++) { sbus.channels[i] = 0; sbus.pulse_us[i] = 1500; } }这样即使解析函数因为偶发噪声丢掉一帧,控制逻辑也不会立刻爆表。对于飞控这类安全攸关的场景,我的建议是把超时阈值设到50ms左右,给姿态解算留一点缓冲,但对舵机控制来说30ms以内比较合适。同样要注意,SBUS的failsafe标志位有时和中位值混淆,不能单独靠标志位判断,还是得结合帧间隔和接收机配置一起看。
整体跑通后,我习惯在调试串口里打印通道0、通道1、通道2的值,用手掰动遥控器摇杆观察数值变化。如果你能看到0x7FF左右的中位值随摇杆变化,说明整条链路已经通了。如果数值固定或乱跳,先检查反相器电路,再检查串口参数和解析位偏移,这是大概率出问题的地方。
5. 实测中的几个坑:时序、printf、以及反相器带来的异常
5.1 波形验证永远先于代码调试
写代码之前,强烈建议先用逻辑分析仪或者示波器把SBUS信号测一遍。这个过程看似多余,但实际上能一次性排除掉很多硬件问题。我个人的流程是:把逻辑分析仪的地线夹到接收机地,通道夹到SBUS信号引脚,采样率设到1MHz以上,然后给接收机上电。
抓波形时重点看两件事:一是空闲电平是低还是高,确认是否反相;二是检查一个字节的停止位数量。SBUS是8E2,所以波形是一个字节内部有8个数据位,数据位后面跟着两个明显的高电平位,那是两个停止位。如果波形显示只有一个停止位高电平,说明接收机配置可能不是严格的SBUS标准,或者你抓错了信号源。
有一次我碰到一个奇怪现象,反相器做完了,串口也配成115200 8E2,但接收数据始终多一个字节或者少一个字节。后来用示波器看才发现,接收机的SBUS输出波形在帧尾后多了一段毛刺,导致UART认为还有半个字节。这个问题的根源是接收机输出级的上拉电阻值太大,边沿过缓,叠加干扰后在停止位后面出现毛刺。解决办法是给反相器输入端加一个100pF到1nF的小电容,把高频毛刺滤掉。
5.2 偶校验和两个停止位:最容易弄错的串口参数
SBUS采用8E2,但很多教程会把它简单类比成普通串口,实际写代码时还是用8N1,这是最常见的错误之一。在标准UART帧格式里,校验位即使不检查,也会占用一个位的时间。如果发送端是8E2,接收端配成8N1,数据位长度对不上,会导致整个字节错位,表现就是帧头0x0F偶尔能对上,但通道数据全是乱码。
解决方法是严格按8E2配置。在使用STM32 HAL库时,如果CubeMX里Parity选Even,Word Length选8 Bits,Stop Bits选2,那么发送端和接收端就一致了。这里需要补充一点,8E2实际占用的位时间等于1个起始位、8个数据位、1个校验位、2个停止位,总共12位,比8N1的10位多20%。这意味着每个字节的传输时间更长,但115200波特率下,25字节一帧的总时间大约2.6毫秒左右,对14ms帧周期来说完全没问题。
有些接收机的SBUS口可以通过配置改成非反相输出,但即使改了,8E2这个参数也不会变。所以协议层解析要按8E2来,电气层再根据是否反相决定要不要加反相器。这两件事千万别混在一起。
还有一个小技巧:如果用USB转TTL模块直接调试SBUS,务必确认模块是否支持8E2。很多便宜的CP2102、CH340模块在8E2配置下也能工作,但部分模块的驱动对偶校验支持不完善,收发数据会出现偶发错位。遇到这种情况,别在MCU端浪费时间,换个USB转串口模块试试,往往立刻就能解决。
5.3 DMA接收粘包和处理不完整帧
在DMA循环模式下,如果缓冲区比帧长,接收过程可能跨两个DMA周期。空闲中断触发时,Size可能大于25,因为缓冲区里残留着上一帧的一部分。比如第一次收到26字节,其中前25字节是完整帧,多出1字节是下一帧的数据;这时如果主循环只按25字节解析,下一帧开头就少了一个字节,后续帧头检测就会失败。
处理这种粘包,我建议有两种方法。第一种是每次解析前先扫描帧头0x0F,然后以0x0F所在位置为起始,取25字节作为一帧;如果扫描不到帧头,就把整包丢弃。第二种是帧头检查加帧尾检查,同时确认Size在25到某个上限之间,比如32,超过32就清空缓冲区重新等待。
实际工程中我倾向于扫描帧头的方式,因为SBUS每14ms只来一帧,一帧里出现多个0x0F的概率很低,扫描帧头定位成本也很低。示例代码如下:
if (sbus_frame_ready) { sbus_frame_ready = 0; for (uint16_t i = 0; i <= sbus_rx_size - SBUS_FRAME_SIZE; i++) { if (sbus_rx_buffer[i] == 0x0F) { sbus_parse_frame(&sbus_rx_buffer[i], SBUS_FRAME_SIZE, &sbus); break; } } }这种方法在偶发错位后能自动重新同步,不会因为一次坏帧影响后续所有帧。DMA循环模式下,如果多次接收后解析一直失败,还要检查是不是DMA传输方向配置反了,或者缓冲区地址没有用Cortex-M3的SRAM段。部分DMA控制器不能访问特定内存区域,这在旧版F103上要格外注意。
5.4 串口打印调试时的坑:别让printf拖垮接收时序
飞控开发中调试SBUS最常用的手段是串口打印。接收机连着STM32的USART2,调试信息用USART1打印,两个串口互不干扰。但有一个问题经常被忽视:printf默认是阻塞式的,在115200波特率下打印一个字符大约耗时87微秒,打印几十个字符就会占用好几毫秒,正好覆盖SBUS的一整个帧周期。如果控制循环里直接printf打印大量调试信息,SBUS的接收中断和DMA回调可能被阻塞,导致帧超时。
我的建议是,调试信息不要直接在中断回调里打印,也不要在主循环里大量打印。可以维护一个环形打印缓冲区,把要调试的数据先写入内存缓冲,再异步发送。或者更简单点,只在按键触发或者调试模式下才打印一帧数据,打印完马上退出。
我自己做过一个对比测试:连续打印16个通道原始值,每帧打印一次,使用标准printf,结果帧间隔从14ms变成了20ms以上,明显影响控制实时性。后来改成只打印通道0到通道3,帧间隔恢复到了14ms左右。如果你的控制逻辑对实时性要求高,打印内容务必精简,或者干脆用J-Link RTT这类不占用串口总线的方式调试。
写在最后的实操体会
如果让我总结整个SBUS移植过程,最重要的一句话就是:先把波形看明白,再写代码。反相器和STM32 HAL库配置都不难,难的是排查那些看似随机、实际上有规律的问题。我见过不少人卡在一开始,串口配置了、反相器也焊了,但接收机到手没先测波形,折腾一天发现其实是接收机固件里SBUS输出被设置成了非反相。
另外,代码层面建议把解析和硬件解耦。反相器只是负责信号极性转换,UART只负责字节流搬运,解析函数只负责从字节流里提取通道。三层各自独立,将来换接收机、换MCU、换协议时,只需要替换其中一层,大部分代码都能复用。
最后分享一个我常用的测试方法:拿到新接收机,先不做任何控制逻辑,只写个极简程序,把SBUS解析结果通过串口发到电脑,用地面站或者串口助手观察通道值。如果四个通道摇杆方向、数值范围都正常,再往上加舵机控制、姿态解算这些功能。这样做能确保每一层都验证过,后续排查范围会小很多。SBUS算是遥控通信里最实用、也最值得手写一遍的协议,跑通一次之后,你对串口、DMA、位运算和硬件电路的理解都会扎实不少。