ARTICLE DETAIL

资讯详情

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

STM32 HAL库DMA+IDLE中断解析SBUS协议实战

STM32 HAL库DMA+IDLE中断解析SBUS协议实战 1. 项目缘起与方案选型思考SBUS 是遥控接收机领域非常常见的一种串行总线协议玩航模、做飞控、搞机器人底盘的朋友应该都不陌生。它用一根信号线就能把十几路遥控通道数据打包发出来接线极简抗干扰也不错。但它有两个让人又爱又恨的特点一是反相电平二是固定 25 字节帧、100000 波特率、偶校验、2 位停止位。很多新手第一次用普通串口去接收到的全是乱码原因就出在这两个地方。我这次的项目需求很明确用 STM32 的 HAL 库稳定地接收并解析 SBUS 数据最终输出 16 路通道值给上层控制逻辑用。难点在于SBUS 是连续不断以 14ms 左右为周期发送的如果每来一个字节就进一次中断CPU 会被频繁打断稍微复杂一点的主循环就会被拖慢。所以我的方案是DMA 循环接收 IDLE 空闲中断 状态机解析三件套组合。为什么选这个组合先说 DMA。串口接收用 DMA本质上是让数据搬运这件事从 CPU 手里接过去CPU 只在“一帧收完”的时候被通知一次而不是每个字节都被打扰。循环模式Circular的好处是缓冲区自动回绕不用手动重置 DMA 指针特别适合 SBUS 这种永不停歇的数据流。再说 IDLE 中断。串口在总线上没有数据时会进入空闲状态硬件会置位 IDLE 标志。SBUS 帧与帧之间有明显的间隔正好可以用 IDLE 中断来判定“这一帧结束了”。最后是状态机因为 DMA 缓冲区里可能一次攒了好几帧也可能只攒了半帧必须靠状态机去逐字节判断帧头、校验、提取通道才能保证解析的健壮性。这套方案相比“单字节中断接收”的优势非常明显CPU 占用低、丢帧率低、代码结构清晰。相比“纯 DMA 定时器超时判断”IDLE 中断的响应更及时也更省一个定时器资源。对于 STM32F103、F407、G070 这些常见型号这套思路都能直接套用区别只是 DMA 通道和外设实例的映射不同。适合谁来参考如果你已经会用 CubeMX 配置串口能看懂基本的 HAL 库收发函数但一遇到“不定长数据接收”就头疼那这篇内容就是给你准备的。我会把配置、代码、踩坑点全部摊开讲尽量做到你照着抄就能跑起来。2. 核心细节解析与实操要点2.1 SBUS 协议帧结构拆解先把协议本身说透不然后面代码就是空中楼阁。SBUS 一帧固定 25 个字节结构如下字节位置内容说明0帧头 0x0F固定起始字节1-2216 路通道数据每路 11 bit共 22 字节23标志位失联、失控等状态24帧尾 0x00固定结束字节通道数据的打包方式是最容易搞错的地方。16 个通道每个 11 位总共 176 位正好 22 字节。它不是按字节对齐的而是像流水一样把 11 位一段一段拼进去。比如通道 1 占 byte1 的全部 8 位加上 byte2 的低 3 位通道 2 占 byte2 的高 5 位加上 byte3 的低 6 位以此类推。解析时必须用位操作去“抠”不能简单地按字节读。我一般用下面这种写法来提取逻辑清晰且不容易错ch[0] ((int16_t)buf[1] | ((int16_t)buf[2] 8)) 0x07FF; ch[1] ((int16_t)buf[2] 3 | ((int16_t)buf[3] 5)) 0x07FF; ch[2] ((int16_t)buf[3] 6 | ((int16_t)buf[4] 2) | ((int16_t)buf[5] 10)) 0x07FF;注意每个通道最后都要 0x07FF因为 11 位最大值就是 2047。这一步如果漏了高位会串进来通道值就会乱跳。提示SBUS 的 11 位原始值范围通常是 172 到 1811对应遥控器的 1000 到 2000 微秒。做映射时不要直接拿 0 到 2047 去算否则中位点会偏。2.2 为什么必须用偶校验和 2 位停止位SBUS 规定波特率 100000数据位 8偶校验Even2 位停止位。这三个参数一个都不能错。我见过有人只改了波特率校验位没动结果收到的数据帧头都对不上。原因是校验位参与帧格式配置错了硬件会直接把校验失败的字节丢掉或者标错。在 CubeMX 里配置 USART 时Parity 选 EvenStop Bits 选 2。这里有个隐藏的坑开启校验位后HAL 库会把数据位当成 9 位处理也就是说你实际收到的字节最高位可能是校验位。不过对于 SBUS 来说因为我们只关心低 8 位数据而且帧头 0x0F 的校验位是 0所以通常不影响。但如果你发现收到的帧头变成了 0x8F 之类就要检查是不是校验位被算进数据里了。2.3 电平反相问题的两种处理思路SBUS 信号是反相的也就是空闲时是低电平起始位是高电平。普通串口空闲是高电平所以直接接上去收不到正确数据。解决办法有两个第一种是硬件反相用一个三极管或者专用反相电路把信号翻过来再接到 MCU 的 RX 上。这是最稳妥的做法不占 CPU也不依赖外设特性。第二种是软件反相利用 STM32 部分型号 USART 支持的 TX/RX 引脚互换功能再配合内部反相配置。但这个方法因芯片型号而异F1 系列基本不支持F4、G0 部分型号可以。我个人的建议是如果你画板子直接加一个反相电路省心如果是临时飞线调试可以用一个 NPN 三极管搭个简易反相成本几毛钱。注意反相电路的三极管基极电阻别选太大否则上升沿会变缓100000 波特率下容易误码。我一般用 1k 到 4.7k 之间。2.4 DMA 循环模式与缓冲区大小的权衡DMA 接收缓冲区开多大这是个经验活。SBUS 一帧 25 字节14ms 一帧。如果你主循环处理够快开 50 字节两帧就够。但实际项目中主循环可能有其他耗时任务比如 OLED 刷新、PID 计算偶尔会延迟几十毫秒。这时候如果缓冲区太小DMA 指针会绕回来覆盖还没处理的数据导致丢帧。我的做法是开64 字节或 128 字节并且配合一个“读指针”来跟踪处理进度。DMA 的写指针可以通过__HAL_DMA_GET_COUNTER反推出来。这样即使主循环偶尔卡顿只要缓冲区没绕满一圈数据就不会丢。64 字节大约能存 2.5 帧128 字节能存 5 帧对于大多数场景足够了。另外DMA 模式一定要选Circular不要选 Normal。Normal 模式下 DMA 传完设定长度就停了SBUS 是连续流停了就再也收不到必须手动重启非常麻烦。3. 实操过程与核心环节实现3.1 CubeMX 配置步骤详解打开 CubeMX选好你的芯片型号比如 STM32F103C8T6。第一步配置时钟树保证 USART 的时钟源正确。F103 的 USART1 挂在 APB2 上通常 72MHz。第二步配置 USART1Mode 选 AsynchronousBaud Rate 填 100000Word Length 选 8 Bits带校验时 CubeMX 可能显示 9 Bits正常Parity 选 EvenStop Bits 选 2DMA Settings 里点 Add选 USART1_RXMode 选 CircularData Width 都是 Byte第三步开中断。在 NVIC Settings 里勾选 USART1 global interrupt。注意IDLE 中断和接收中断共用同一个中断向量所以只要开了 global interrupt 就行。第四步生成代码。生成前确认一下 DMA 的优先级一般设 Medium 或 High 都可以SBUS 数据量不大不会跟其他 DMA 抢得太厉害。3.2 初始化代码与 IDLE 中断开启CubeMX 生成的MX_USART1_UART_Init和MX_DMA_Init会处理好大部分初始化。但有一件事 CubeMX 不会自动做开启 IDLE 中断。你需要在初始化完成后手动加一句__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);同时启动 DMA 接收HAL_UART_Receive_DMA(huart1, sbus_rx_buf, SBUS_BUF_SIZE);这两句一般放在main函数里MX_USART1_UART_Init()之后。顺序不能反先启动 DMA 再开 IDLE 中断否则可能错过第一帧。3.3 IDLE 中断回调的写法HAL 库默认的中断处理函数HAL_UART_IRQHandler会处理各种标志但 IDLE 标志它不会自动清也不会调用回调。所以我们需要在stm32f1xx_it.c的USART1_IRQHandler里手动判断void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); sbus_idle_callback(); } HAL_UART_IRQHandler(huart1); }这里有个细节清 IDLE 标志的推荐做法是“先读 SR 再读 DR”HAL 库的__HAL_UART_CLEAR_IDLEFLAG宏已经帮你做了这件事。但如果你用的是老版本 HAL可能需要手动写volatile uint32_t tmp; tmp huart1.Instance-SR; tmp huart1.Instance-DR; (void)tmp;提示清标志一定要在回调之前做否则中断会反复触发CPU 直接跑飞。3.4 状态机解析逻辑设计IDLE 回调里不要做太多事只记录当前 DMA 写指针位置然后置一个标志位让主循环去解析。这样中断响应快也不容易出竞态问题。状态机我设计成三个状态等待帧头、接收数据、校验帧尾。具体流程是从上次读指针开始逐字节扫描 DMA 缓冲区直到当前写指针位置。遇到 0x0F 就认为可能是帧头进入接收状态把后续 24 字节存入临时帧数组。收满 25 字节后检查第 24 字节是否为 0x00是则解析通道否则丢弃重新找帧头。解析完成后更新读指针继续扫描剩余数据。这里的关键是处理缓冲区回绕。DMA 写指针可能小于读指针说明绕了一圈。我的处理方式是分两段扫描先扫读指针到缓冲区末尾再扫缓冲区开头到写指针。这样逻辑清晰不会漏数据。typedef enum { STATE_HEADER, STATE_DATA, STATE_TAIL } sbus_state_t; static sbus_state_t state STATE_HEADER; static uint8_t frame[25]; static uint8_t frame_idx 0; void sbus_parse(uint8_t *buf, uint16_t start, uint16_t end) { for (uint16_t i start; i ! end; i (i 1) % SBUS_BUF_SIZE) { uint8_t byte buf[i]; switch (state) { case STATE_HEADER: if (byte 0x0F) { frame[0] byte; frame_idx 1; state STATE_DATA; } break; case STATE_DATA: frame[frame_idx] byte; if (frame_idx 24) state STATE_TAIL; break; case STATE_TAIL: frame[frame_idx] byte; if (byte 0x00) { sbus_decode(frame); } state STATE_HEADER; frame_idx 0; break; } } }这段代码里i ! end的循环条件配合取模天然处理了回绕。注意end是 DMA 当前写指针不是缓冲区末尾。3.5 通道值映射与失联处理解析出 11 位原始值后通常要映射到 1000 到 2000 的脉宽范围。线性映射公式是pwm (raw - 172) * 1000 / (1811 - 172) 1000;但实际遥控器输出范围可能略有不同我一般会做限幅防止超出 1000 到 2000。另外SBUS 第 23 字节的标志位要检查如果失联标志置位应该把通道值置为安全值比如中位 1500 或者油门最低。这个逻辑在飞控里尤其重要不做的话失控时电机会保持最后指令非常危险。4. 常见问题与排查技巧实录4.1 收到的数据全是乱码或帧头对不上这是最常见的问题九成出在配置上。排查顺序我一般是这样现象可能原因排查方法完全无数据接线反了、电平未反相用示波器看 RX 引脚波形帧头偶尔对偶尔错波特率偏差大检查时钟树确认 USART 时钟数据整体偏移校验位或停止位配错核对 Even 和 2 Stop Bits帧头变成 0x8F校验位被算进数据检查 HAL 的 9 位处理我踩过最坑的一次是时钟树配错USART 时钟不是预期的 72MHz导致实际波特率差了 3% 左右短帧还能凑合25 字节的长帧就频繁出错。后来用示波器量了位宽才定位到。4.2 IDLE 中断不触发或反复触发不触发通常是忘了__HAL_UART_ENABLE_IT或者中断优先级被其他中断屏蔽了。反复触发则是标志没清干净一定要按“读 SR 再读 DR”的顺序清。还有一种情况是 DMA 没启动串口根本没收到数据自然也不会进 IDLE。注意如果你在中断里调用了HAL_UART_Receive_DMA重新启动接收要小心它可能会关闭再打开 DMA导致 IDLE 中断配置丢失。我的做法是只在初始化时启动一次 DMA循环模式不需要重启。4.3 通道值跳动或偶尔丢帧通道值跳动一般是位操作写错比如移位方向反了或者掩码没加。丢帧则多半是缓冲区太小或者主循环处理太慢。我建议在解析函数里加一个帧计数器跑一段时间看看计数是否稳定在 70 帧每秒左右14ms 一帧。如果明显偏低就要查缓冲区大小和处理耗时。另外DMA 和 CPU 同时访问缓冲区会有竞态。虽然读指针和写指针分开但极端情况下 CPU 正在读的字节可能被 DMA 覆盖。解决办法是解析前先记录写指针解析时只处理到写指针之前的数据并且解析速度要快于 DMA 写入速度。SBUS 每 14ms 才 25 字节CPU 解析一帧也就几微秒完全跟得上。4.4 不同 STM32 型号的移植要点F103 和 F407 的 DMA 通道映射不同CubeMX 会自动处理但手动改代码时要注意。G0 系列的 USART 配置略有差异IDLE 标志的清除方式一样但 DMA 请求映射要看参考手册。我移植到 G070 时发现它的 DMA 通道更灵活但 CubeMX 生成的代码里 DMA 初始化顺序有变化需要确认MX_DMA_Init在MX_USART1_UART_Init之前调用否则 DMA 时钟没开接收会失败。还有一个通用建议把 SBUS 解析封装成独立的 .c/.h 文件只暴露一个sbus_get_channel(uint8_t ch)接口。这样换芯片时只需要改底层初始化和中断部分解析逻辑完全不用动。我在几个项目里都这么干移植成本极低。最后分享一个小技巧调试阶段可以用一个 LED 指示帧接收状态每收到一帧翻转一次。如果 LED 闪烁均匀说明接收正常如果闪烁不规则或者不闪问题一定在接收环节不用去查解析代码。这个笨办法帮我省了很多时间。
返回列表