ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32实战:SBUS协议解析与反相器电路设计

STM32实战:SBUS协议解析与反相器电路设计 我第一次把遥控器接收机的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字节2216个通道数据每通道11位第23字节1标志位包含ch17、ch18、failsafe、frame lost第24字节1帧尾固定0x0016个通道每个占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脚VCC2脚A输入3脚GND4脚Y输出5脚NC或使能。实际接法比三极管方案还简单VCC接3.3VGND接地靠近芯片放一个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库侧配置串口参数与DMAIDLE接收链路3.1 CubeMX中的配置参数一个都不能错硬件反相器做好后接下来是STM32侧配置。我用的是STM32F103系列但配置思路在F4、H7上完全一致。打开STM32CubeMX选择对应型号进入UART配置界面关键参数如下参数项设置值ModeAsynchronousBaud Rate115200Word Length8 BitsParityEvenStop Bits2Oversampling16 SamplesEnable DMARX, 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到15static 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_index2]时实际上是在读取数据区中超出该通道范围的部分需要确保缓冲区里有一块安全内存。这就是为什么前面把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 Lostbit3为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选EvenWord Length选8 BitsStop 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、位运算和硬件电路的理解都会扎实不少。
返回列表