ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 DMA+IDLE中断实现SBUS稳定解析方案

STM32 DMA+IDLE中断实现SBUS稳定解析方案 1. 为什么SBUS解析值得单独拎出来讲SBUS这玩意儿在航模和机器人圈子里太常见了一根线就能传16个通道接线简单、抗干扰也不错很多接收机、飞控、舵机控制器都在用。但真到自己用STM32去接它的时候问题就来了波特率是100000、8位数据、偶校验、2位停止位这组参数跟标准串口配置完全不一样很多人第一次配就卡在这儿。更麻烦的是SBUS一帧25个字节帧头0x0F、帧尾可选、中间22字节塞了16个通道的11位数据还有标志位和失控保护位解析逻辑如果写得随意通道值就会跳来跳去。我见过太多项目里用“串口接收中断里逐字节判断帧头”的写法跑起来看着能用但一旦主循环里有耗时操作或者波特率稍微有点偏差就开始丢帧、错位。后来我把这套方案换成了DMA循环接收 IDLE空闲中断 状态机解析的组合实测在F103、F407、G0、H7上都跑得很稳CPU占用几乎可以忽略。这篇文章就把这套方案的完整实现思路、配置细节、状态机设计、踩过的坑以及怎么验证数据正确性一次性讲透。不管你是刚接触HAL库的新手还是已经用过标准库想迁移过来的老手只要你的项目里需要稳定接收SBUS信号这套方案都能直接抄作业。我会从CubeMX配置开始到DMA缓冲区设计、IDLE中断处理、状态机逐字节解析、通道值还原、失控保护判断再到实测中遇到的帧错位、DMA半满中断干扰、串口溢出等问题全部展开讲。看完你至少能少走两三个晚上的弯路。2. SBUS协议本身的几个关键细节2.1 电气特性和串口参数为什么这么特殊SBUS是Futaba搞出来的串行总线物理层是反相UART也就是说它的电平逻辑跟普通串口是反的。接收机出来的SBUS信号空闲时是高电平起始位是低电平但经过反相器之后才变成标准UART能识别的波形。所以如果你直接把接收机的SBUS线接到STM32的RX上大概率什么都收不到需要加一个反相电路常见的是用三极管或者74HC14这类反相器。有些接收机已经内置了反相输出买的时候要看清楚是“SBUS”还是“SBUS inverted”。串口参数方面SBUS固定是100000波特率、8位数据位、偶校验、2位停止位。注意这里没有流控也不需要硬件流控。在CubeMX里配置UART的时候波特率填100000Word Length选8位Parity选EvenStop Bits选2。很多人会忽略偶校验和2位停止位结果收出来的数据全是乱的。我一开始也犯过这个错用默认的115200、无校验、1位停止位去收示波器上看波形都对但串口数据就是不对后来查了手册才发现参数完全不对。还有一个细节SBUS帧与帧之间的间隔大约是14ms也就是每14毫秒发一帧25字节。这个间隔对于DMA循环接收来说非常友好因为你有充足的时间去处理上一帧数据不用担心下一帧马上就来覆盖缓冲区。2.2 25字节帧结构逐字节拆解SBUS一帧固定25字节结构如下字节位置内容说明00x0F帧头固定值1-22通道数据16个通道每个11位共176位正好22字节23标志位bit0失控保护bit1信号丢失bit2故障保护激活24帧尾0x00或0x04部分接收机固定0x00通道数据的打包方式是这样的16个通道各11位总共176位按小端顺序连续排列在22个字节里。具体来说第1个通道占byte1的bit0-bit7和byte2的bit0-bit2第2个通道占byte2的bit3-bit7和byte3的bit0-bit5以此类推。手动去移位拼接很容易出错我后面会给出一个经过验证的解析函数。标志位字节里bit0是失控保护标志当接收机失去发射机信号时置1bit1是信号丢失标志bit2是故障保护激活。实际项目中我们最关心的是bit0一旦置1就要让飞机或机器人进入安全状态。帧尾字节有些接收机是0x00有些是0x04解析的时候可以不做强校验但帧头0x0F必须校验否则状态机会跑飞。2.3 为什么不能用普通串口接收中断逐字节处理很多人第一反应是用HAL_UART_Receive_IT逐字节接收然后在回调里判断帧头、拼数据。这个方案在小数据量、低波特率下能用但SBUS有两个特点让它变得很脆弱一是波特率100000字节间隔只有100微秒左右中断频率很高二是每帧25字节连续到来如果主循环里有其他中断或者耗时操作很容易在某个字节上错过中断导致整帧错位。更致命的是HAL库的逐字节接收中断在每次接收完成后需要重新调用HAL_UART_Receive_IT如果调用不及时下一个字节就丢了。我实测过在主循环里加一个OLED刷新就会开始丢帧。所以SBUS这种连续多字节、固定帧长的协议最适合用DMA把整帧数据搬到内存再用IDLE中断判断一帧结束CPU只需要在帧结束后处理一次数据效率高且稳定。3. CubeMX配置与DMA缓冲区设计3.1 串口和DMA的具体配置项打开CubeMX选好芯片型号后先配置时钟树保证UART时钟源正确。以F103为例UART1挂在APB2上时钟72MHz。然后配置USART1为异步模式参数如下Baud Rate: 100000Word Length: 8 Bits (including Parity)Parity: EvenStop Bits: 2Data Direction: Receive Only如果只接收可以省一个引脚Over Sampling: 16 Samples接着到DMA Settings标签页添加USART1_RX的DMA请求。模式选Circular循环模式这是关键因为SBUS是连续不断发送的循环模式可以让DMA自动回绕不需要每次重新启动。数据宽度都选Byte优先级Medium或High都可以。NVIC里使能USART1全局中断优先级根据你的系统来定建议不要低于系统滴答定时器。注意DMA循环模式下缓冲区大小建议设为50字节25的2倍这样即使一帧还没处理完下一帧也不会立刻覆盖。但SBUS帧间隔14ms处理时间通常远小于这个值所以25字节也够用。我习惯用50留点余量。3.2 缓冲区大小和半满中断的取舍DMA循环接收有个特性当传输了一半数据时会触发半传输中断HT全部传输完触发传输完成中断TC。很多人会同时使能这两个中断然后在回调里判断。但对于SBUS来说我们真正需要的是IDLE中断因为帧与帧之间有间隔IDLE能准确标识一帧的结束。半满中断在这里反而可能添乱。假设缓冲区50字节DMA搬到第25字节时触发HT如果这时候你误以为一帧结束了去处理就会拿到半帧数据。所以我的做法是只使能IDLE中断不使能DMA的HT和TC中断。CubeMX里DMA配置的NVIC中把DMA的全局中断关掉只保留USART1的全局中断。这样IDLE中断触发时DMA已经把所有到达的字节都搬完了我们通过计算当前DMA剩余计数来得到这一帧的长度。3.3 启动DMA接收的正确姿势初始化完成后在main函数里调用一次uint8_t sbus_rx_buf[50]; HAL_UART_Receive_DMA(huart1, sbus_rx_buf, 50); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);这两行是核心。第一行启动DMA接收第二行手动使能IDLE中断。注意HAL库默认不会自动使能IDLE中断必须手动加。很多人忘了这一句然后发现IDLE回调根本不进查半天以为是DMA配置问题。另外HAL_UART_Receive_DMA只需要调用一次因为DMA是循环模式它会一直搬数据。不要在IDLE回调里重新调用否则会打断DMA的循环导致数据错乱。我见过有人在回调里重新启动DMA结果每帧都丢几个字节就是这个原因。4. IDLE中断处理与状态机解析4.1 IDLE中断里到底该做什么IDLE中断触发时说明串口总线空闲了一帧数据接收完毕。这时候我们要做三件事第一清除IDLE标志第二计算这一帧接收了多少字节第三把数据拷贝出来或者置个标志让主循环处理。清除IDLE标志不能直接用__HAL_UART_CLEAR_IDLEFLAG因为HAL库的这个宏在某些系列上不生效需要先读SR寄存器再读DR寄存器。我通常这样写void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t remain __HAL_DMA_GET_COUNTER(huart1.hdmarx); uint16_t recv_len 50 - remain; // 处理recv_len和sbus_rx_buf } HAL_UART_IRQHandler(huart1); }这里有个细节__HAL_DMA_GET_COUNTER返回的是DMA还剩多少没搬用总长度减去它就是已经搬了多少字节。但因为是循环模式如果一帧25字节DMA可能已经绕了一圈remain可能比总长度还大这时候需要做取模处理。不过SBUS帧间隔14ms50字节缓冲区在100000波特率下搬完只需要5ms左右所以正常情况下不会绕圈。但为了保险我还是会判断recv_len是否合理。4.2 状态机逐字节解析的设计思路拿到一帧数据后不要直接按固定偏移去取通道值因为可能因为干扰导致帧头错位。我习惯用一个简单的状态机来解析typedef enum { SBUS_STATE_HEADER, SBUS_STATE_DATA, SBUS_STATE_FLAGS, SBUS_STATE_FOOTER } sbus_state_t; typedef struct { sbus_state_t state; uint8_t data[25]; uint8_t index; uint16_t channels[16]; uint8_t failsafe; uint8_t frame_lost; } sbus_t;状态机从HEADER开始收到0x0F进入DATA连续收22字节后进入FLAGS收1字节后进入FOOTER收完最后1字节后判断帧尾然后回到HEADER。如果中途收到不符合预期的字节直接重置状态机。这样即使某一帧错位下一帧也能自动恢复。解析通道值的函数我用了位操作经过多个项目验证void sbus_decode_channels(uint8_t *data, uint16_t *channels) { channels[0] ((data[1] | data[2]8) 0x07FF); channels[1] ((data[2]3 | data[3]5) 0x07FF); channels[2] ((data[3]6 | data[4]2 | data[5]10) 0x07FF); channels[3] ((data[5]1 | data[6]7) 0x07FF); channels[4] ((data[6]4 | data[7]4) 0x07FF); channels[5] ((data[7]7 | data[8]1 | data[9]9) 0x07FF); channels[6] ((data[9]2 | data[10]6) 0x07FF); channels[7] ((data[10]5| data[11]3) 0x07FF); channels[8] ((data[11]8| data[12] | data[13]8) 0x07FF); channels[9] ((data[13]3| data[14]5) 0x07FF); channels[10] ((data[14]6| data[15]2 | data[16]10) 0x07FF); channels[11] ((data[16]1| data[17]7) 0x07FF); channels[12] ((data[17]4| data[18]4) 0x07FF); channels[13] ((data[18]7| data[19]1 | data[20]9) 0x07FF); channels[14] ((data[20]2| data[21]6) 0x07FF); channels[15] ((data[21]5| data[22]3) 0x07FF); }这段代码看起来有点绕但它是按照SBUS的位打包顺序严格推导出来的。每个通道11位跨字节拼接最后与0x07FF做掩码保证结果在0-2047之间。实际通道值范围通常是172-1811对应遥控器的1000-2000微秒脉宽。4.3 失控保护和信号丢失的判断逻辑标志位字节的第0位是失控保护第1位是信号丢失。正常情况下这两个位都是0。当接收机失去信号时bit0会置1同时通道值会跳到预设的失控保护值。我们在主循环里每帧检查一次if (sbus.failsafe || sbus.frame_lost) { // 进入安全状态比如关闭电机、舵机回中 }这里有个坑有些接收机在失控时会把所有通道值设为固定值但标志位不一定置1。所以除了看标志位还要看通道值是否长时间不变。我一般会加一个超时判断如果连续500ms没有收到有效帧也进入安全状态。5. 实测中遇到的坑和排查过程5.1 帧头错位导致通道值乱跳第一次跑通的时候发现通道值偶尔会跳变尤其是油门通道突然从1000跳到1800。用逻辑分析仪抓波形发现串口数据本身没问题但解析出来的值不对。后来在状态机里加了一个计数器发现有时候一帧里会出现两个0x0F导致状态机提前进入DATA把后面的数据当成了通道数据。原因是SBUS数据里通道值也可能出现0x0F如果状态机只靠帧头判断就会误判。解决办法是状态机在HEADER状态时不仅要判断0x0F还要结合帧间隔。因为IDLE中断已经保证了一帧的边界所以我在IDLE回调里直接把整帧数据传给状态机状态机从索引0开始解析不再逐字节喂。这样就不会因为数据里出现0x0F而错位。5.2 DMA半满中断和IDLE中断打架有一次我手贱把DMA的HT中断也打开了结果发现IDLE回调里拿到的recv_len有时候是25有时候是12。排查后发现HT中断在DMA搬到一半时触发如果这时候恰好串口空闲了IDLE也会触发两个中断嵌套导致DMA计数器被读走两次。后来把HT和TC中断全部关掉只留IDLE问题消失。提示DMA循环接收时除非你明确需要半满处理否则不要使能HT中断。IDLE中断足够应对SBUS这种帧间隔明显的协议。5.3 串口溢出错误导致接收停止在长时间运行测试中偶尔会出现接收突然停止重启后才恢复。查SR寄存器发现ORE溢出错误置位了。原因是DMA虽然能搬数据但如果DMA被更高优先级中断打断太久串口接收寄存器溢出就会置OREHAL库默认会停止接收。解决办法是在错误回调里清除ORE标志并重新启动DMAvoid HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE)) { __HAL_UART_CLEAR_OREFLAG(huart); HAL_UART_Receive_DMA(huart, sbus_rx_buf, 50); __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); } }这个坑在长时间运行的设备上很容易遇到尤其是系统里还有其他高优先级中断的时候。5.4 波特率偏差导致的偶发校验错误SBUS要求100000波特率但STM32的UART时钟分频不一定能精确得到100000。比如72MHz时钟分频系数算下来可能有微小误差。如果误差超过2%偶校验就会偶尔出错。我实测F103在72MHz下100000波特率的实际误差大约0.16%完全没问题。但如果你用的是内部RC振荡器误差可能到3%以上就会频繁校验错误。所以建议用外部晶振并且在CubeMX里检查一下波特率误差提示。6. 通道值映射与上层应用对接6.1 从11位原始值到PWM脉宽的换算SBUS通道原始值是0-2047对应遥控器摇杆的极限位置。实际舵机和电调需要的是1000-2000微秒的PWM信号。换算公式很简单uint16_t sbus_to_pwm(uint16_t sbus_val) { // 典型范围172-1811对应1000-2000us return (uint16_t)((sbus_val - 172) * 1000 / (1811 - 172) 1000); }但不同接收机的范围可能略有差异有的中位是992有的范围是170-1810。我一般会在初始化时让用户把摇杆打到中位和极限自动校准一次这样最准。6.2 失控保护状态下的安全策略一旦检测到failsafe或frame_lost上层应用必须立即进入安全状态。对于多旋翼通常是关闭电机或让电机怠速对于固定翼可能是舵机回中、油门收到底对于地面机器人就是停止运动。我习惯在状态机里维护一个last_valid_tick如果超过500ms没有有效帧就强制置failsafe标志这样即使接收机没发标志位也能兜底。6.3 多路SBUS接收的扩展思路有些项目需要接两路SBUS做冗余比如主接收机和备份接收机。这时候可以用两个UART各自配DMA和IDLE中断状态机独立运行。主循环里优先使用主接收机的数据如果主接收机failsafe就切换到备份。注意两个UART的DMA通道不能冲突CubeMX里会自动分配但你要检查一下优先级避免高优先级的那路把低优先级的DMA请求堵死。7. 验证数据正确性的几种手段7.1 用逻辑分析仪抓SBUS波形最直接的办法是用逻辑分析仪接在SBUS信号线上设置波特率100000、偶校验、2停止位解码出来的字节流跟STM32接收缓冲区里的数据对比。如果一致说明串口配置和DMA接收没问题如果不一致就要查反相电路和串口参数。我用的是一款几十块钱的USB逻辑分析仪配合开源软件解码SBUS很方便。7.2 串口打印通道值做动态观察把解析出来的16个通道值通过另一个UART打印到串口助手观察摇杆动作时数值是否线性变化。如果某个通道跳变或者不变化就重点查那个通道的位拼接。我一般会打印原始值和PWM值两列方便对比。7.3 用已知固定值做单元测试在没有接收机的情况下可以手动构造一帧SBUS数据比如所有通道设为992中位标志位0帧尾0x00然后喂给状态机看解析结果是否全部为992。这个方法在调试状态机逻辑时非常有效可以排除硬件干扰。8. 几个容易被忽略的优化点8.1 用DMA双缓冲进一步降低CPU占用如果你的芯片支持DMA双缓冲模式比如F4、F7、H7系列可以把缓冲区分成两半DMA搬一半的时候CPU处理另一半进一步降低延迟。不过SBUS帧间隔14ms单缓冲50字节已经绰绰有余双缓冲更多是锦上添花。我只有在同时处理多路SBUS且主循环很忙的时候才会用。8.2 把解析放在主循环而不是中断里IDLE中断里只做最轻量的工作记录长度、置标志、拷贝数据到另一个缓冲区。真正的状态机解析和通道值计算放在主循环里根据标志位触发。这样中断执行时间极短不会影响其他中断的响应。我实测中断里只花不到2微秒主循环里解析一帧也就几十微秒。8.3 超时检测和自动恢复除了串口错误回调里的恢复我还会在主循环里加一个超时计数器。如果连续1秒没有收到任何IDLE中断就重新初始化UART和DMA。这个兜底逻辑在电磁环境复杂的场合很有用比如电机干扰导致串口锁死。8.4 通道值滤波的必要性SBUS本身已经比较稳定但如果你发现通道值有轻微抖动可以加一个简单的滑动平均滤波。不过要注意滤波会引入延迟对于穿越机这种需要快速响应的场景不建议滤波。对于航拍机或者机器人加3点滑动平均就够了。9. 移植到不同STM32系列时的注意事项这套方案在F103、F407、G030、H743上都跑过整体逻辑一致但有几个系列差异要注意。F0和G0系列的DMA控制器跟F1/F4不太一样__HAL_DMA_GET_COUNTER的宏定义可能不同需要查对应系列的HAL库头文件。H7系列的UART时钟源更复杂CubeMX里要确认时钟树配置正确否则波特率误差会偏大。另外H7的DMA缓存一致性需要注意如果开了D-CacheDMA缓冲区要放在非缓存区域或者手动做cache维护否则CPU读到的可能是旧数据。还有一个通用注意点HAL_UART_Receive_DMA在循环模式下如果中途调用了HAL_UART_DMAStop再重新启动时DMA计数器会重置但串口可能还在接收导致第一帧数据错位。所以除非必要不要中途停止DMA。如果一定要停停完之后先清空缓冲区再重启。我在实际项目里把这套代码封装成了一个sbus.c和sbus.h对外只暴露初始化、获取通道值、获取failsafe状态三个接口上层应用完全不用关心底层是DMA还是中断。这样换芯片的时候只需要改CubeMX配置和少量宏定义业务代码一行不用动。如果你也在做SBUS相关的项目建议一开始就把接口抽象好后面会省很多事。
返回列表