ARTICLE DETAIL

资讯详情

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

STM32 HAL库SBUS解析实战:DMA+IDLE中断+状态机实现零丢帧

STM32 HAL库SBUS解析实战:DMA+IDLE中断+状态机实现零丢帧 1. 为什么 SBUS 解析值得单独拿出来讲SBUS 这个协议在航模、机器人、工业遥控领域出镜率极高但真正自己从零写解析的人不多大多数人第一次接触都是被它那套反相串口 25 字节定长帧 11 位打包的组合拳打懵。我最早做 SBUS 接收是在一块 F103 上当时用标准库轮询主循环里塞了个while死等结果遥控一抖舵机就跟着抽风后来换成 HAL 库 DMA IDLE 中断才彻底稳住。这篇内容就是把我这几年在 STM32 上用 HAL 库做 SBUS 解析的完整思路摊开讲。核心方案是DMA 循环接收 串口 IDLE 空闲中断 状态机解析三者配合能实现零丢帧、低 CPU 占用、可扩展性强的接收链路。适合正在做飞控、机器人遥控、云台控制、工业遥控接收端的同学也适合已经会点 HAL 库但被 SBUS 帧格式卡住的开发者。读完你应该能直接把这套代码搬到自己的工程里改改串口号和波特率就能跑。先说清楚 SBUS 的几个硬性特征这是后面所有设计的出发点物理层反相SBUS 是反相串口空闲电平为低起始位为高。所以硬件上必须加一个反相电路一个 NPN 三极管 两个电阻就能搞定或者用带反相功能的接收芯片。软件层面 HAL 库没法直接处理反相这点必须先解决。波特率 1000008E2100k 波特率、8 数据位、偶校验、2 停止位。注意是 100k 不是 115200很多人第一次配错就是这里。25 字节定长帧帧头 0x0F帧尾根据故障标志位可能是 0x00、0x04、0x14、0x24 等。16 通道 2 个数字通道22 字节承载 16 个 11 位通道数据剩下的是标志位。把这四点记住后面状态机的设计就顺理成章了。2. 整体方案设计与选型考量2.1 为什么不用轮询和普通中断先说说为什么放弃轮询。SBUS 一帧 25 字节100k 波特率下每字节约 100 微秒一帧大约 2.5 毫秒帧间隔通常 7 毫秒14ms 周期里发两帧或按厂商不同。如果用轮询主循环必须每 100 微秒回来查一次这对任何有实时任务的系统都是灾难。我试过在 F103 上轮询主循环里稍微加点浮点运算就开始丢字节。普通接收中断RXNE的问题是中断太频繁。25 字节就是 25 次中断如果同时还有别的串口、定时器中断CPU 会被打断得七零八落。而且每进一次中断都要读 DR、清标志开销不小。DMA 循环接收的好处是CPU 完全不参与搬运DMA 自己把串口数据往缓冲区里塞我们只需要在合适的时候去读缓冲区。配合 IDLE 空闲中断一帧数据接收完毕总线空闲一个字节时间触发一次中断中断频率从 25 次/帧降到 1 次/帧CPU 占用直接降一个数量级。2.2 为什么用循环模式而不是普通模式DMA 有两种模式Normal 和 Circular。Normal 模式下 DMA 搬完指定长度就停需要手动重启Circular 模式下缓冲区满了自动回到起点继续搬永不停止。SBUS 是连续流式数据帧与帧之间没有明显间隔或者说间隔很短如果用 Normal 模式每次接收完都要在中断里重新配置 DMA一旦配置慢了就丢数据。Circular 模式让 DMA 一直转我们通过计算当前写指针位置来判断收到了多少字节这才是流式协议的正确打开方式。缓冲区大小我一般设成 50 字节两帧这样即使 IDLE 中断响应慢了一点也不会覆盖掉还没处理的数据。设太小容易覆盖设太大浪费 RAM 且增加处理延迟。2.3 状态机在其中的角色有了 DMA 缓冲区和 IDLE 中断我们拿到的是一段连续的字节流但这段字节流里可能包含半帧、一帧、一帧半甚至多帧。怎么从字节流里准确切出完整的 25 字节帧这就是状态机要干的事。状态机的核心思路是以帧头 0x0F 为锚点逐字节推进凑够 25 字节且帧尾合法才认为是一帧有效数据。这样即使中间丢了几个字节状态机也能自动重新同步不会一直错位下去。我见过有人直接在 IDLE 中断里判断收到 25 字节就解析这种做法在数据干净时能用但一旦出现丢字节或粘包就彻底崩。状态机虽然多写几十行代码但鲁棒性完全不是一个级别。3. 核心细节解析与实操要点3.1 硬件反相电路不能省这是最容易被忽略的一步。STM32 的 USART RX 引脚默认空闲是高电平而 SBUS 信号空闲是低电平。如果不做反相你会看到接收到的数据全是 0x00 或者乱码怎么调软件都没用。最简单的反相电路一个 2N3904 或 S8050 NPN 三极管基极串 1k 电阻接 SBUS 信号集电极串 10k 上拉到 3.3V 并接 STM32 RX发射极接地。这样 SBUS 低电平时三极管截止RX 被上拉到高SBUS 高电平时三极管导通RX 被拉低。逻辑正好反相。注意有些接收机输出的是已经反相过的信号标注为非反相 SBUS或SBUS2这种可以直接接。买之前一定确认清楚否则白折腾半天。3.2 串口参数配置的坑在 CubeMX 里配置 USART 时参数必须严格按下面来参数值说明Baud Rate100000不是 115200Word Length8 Bits8 数据位ParityEven偶校验Stop Bits22 停止位Data DirectionReceive Only只收不发Over Sampling16默认即可偶校验这一项特别容易漏。SBUS 用偶校验如果配成无校验接收到的数据会错位状态机永远找不到正确的帧头。3.3 DMA 配置要点DMA 配置里几个关键项ModeCircularData WidthBytePeripheral 和 Memory 都是 BytePriorityHigh 或 Very HighSBUS 是实时数据优先级低了容易被别的 DMA 抢占Memory IncrementEnable缓冲区地址要递增Peripheral IncrementDisable串口 DR 寄存器地址固定缓冲区定义成uint8_t sbus_buf[50]全局变量别放栈上。3.4 IDLE 中断的开启方式HAL 库默认不开启 IDLE 中断需要手动调用__HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE);这一句通常放在MX_USART2_UART_Init()之后或者放在HAL_UART_Receive_DMA()之后。注意顺序先启动 DMA 接收再开 IDLE 中断否则可能第一个 IDLE 中断来时 DMA 还没准备好。启动 DMA 接收的调用HAL_UART_Receive_DMA(huart2, sbus_buf, 50);3.5 状态机的状态划分状态机我设计成三个状态WAIT_HEAD等待帧头 0x0FRECV_DATA收到帧头后持续收集后续字节CHECK_FRAME凑够 25 字节后校验帧尾实际实现时我更喜欢用一个frame_index计数器配合状态标志而不是写三个 case。因为 SBUS 是定长帧逻辑上更接近找头 数够 25 个。4. 实操过程与核心环节实现4.1 中断回调里的处理逻辑HAL 库的 IDLE 中断不会自动进HAL_UART_IRQHandler的回调需要自己在stm32f1xx_it.c的USART2_IRQHandler里判断void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); sbus_idle_callback(); } HAL_UART_IRQHandler(huart2); }sbus_idle_callback()里做两件事计算本次 DMA 写了多少字节然后把这些字节喂给状态机。计算已接收字节数的方法uint16_t recv_len SBUS_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart2.hdmarx);__HAL_DMA_GET_COUNTER返回的是 DMA 剩余待搬运的字节数用缓冲区总大小减去它就是已经写入的字节数。这是 Circular 模式下判断数据量的标准做法。4.2 状态机解析代码下面是我实际用的状态机核心逻辑去掉了业务相关部分#define SBUS_FRAME_LEN 25 #define SBUS_HEAD 0x0F static uint8_t frame_buf[SBUS_FRAME_LEN]; static uint8_t frame_idx 0; static uint8_t in_frame 0; void sbus_parse_byte(uint8_t byte) { if (!in_frame) { if (byte SBUS_HEAD) { frame_buf[0] byte; frame_idx 1; in_frame 1; } return; } frame_buf[frame_idx] byte; if (frame_idx SBUS_FRAME_LEN) { uint8_t tail frame_buf[SBUS_FRAME_LEN - 1]; if ((tail 0x00) || (tail 0x04) || (tail 0x14) || (tail 0x24)) { sbus_decode(frame_buf); } in_frame 0; frame_idx 0; } }这段代码的关键点只有帧尾合法才解码。如果帧尾不对说明这 25 字节里有错位直接丢弃等下一个 0x0F 重新开始。这样即使中间丢字节最多丢一帧下一帧立刻恢复。4.3 通道数据解码SBUS 的 16 个通道是 11 位打包的22 字节承载 16 个通道解码公式如下void sbus_decode(uint8_t *buf) { uint16_t ch[16]; ch[0] ((uint16_t)buf[1] | ((uint16_t)buf[2] 8)) 0x07FF; ch[1] ((uint16_t)buf[2] 3 | ((uint16_t)buf[3] 5)) 0x07FF; ch[2] ((uint16_t)buf[3] 6 | ((uint16_t)buf[4] 2) | ((uint16_t)buf[5] 10)) 0x07FF; ch[3] ((uint16_t)buf[5] 1 | ((uint16_t)buf[6] 7)) 0x07FF; ch[4] ((uint16_t)buf[6] 4 | ((uint16_t)buf[7] 4)) 0x07FF; ch[5] ((uint16_t)buf[7] 7 | ((uint16_t)buf[8] 1) | ((uint16_t)buf[9] 9)) 0x07FF; ch[6] ((uint16_t)buf[9] 2 | ((uint16_t)buf[10] 6)) 0x07FF; ch[7] ((uint16_t)buf[10] 5 | ((uint16_t)buf[11] 3)) 0x07FF; ch[8] ((uint16_t)buf[12] | ((uint16_t)buf[13] 8)) 0x07FF; ch[9] ((uint16_t)buf[13] 3 | ((uint16_t)buf[14] 5)) 0x07FF; ch[10] ((uint16_t)buf[14] 6 | ((uint16_t)buf[15] 2) | ((uint16_t)buf[16] 10)) 0x07FF; ch[11] ((uint16_t)buf[16] 1 | ((uint16_t)buf[17] 7)) 0x07FF; ch[12] ((uint16_t)buf[17] 4 | ((uint16_t)buf[18] 4)) 0x07FF; ch[13] ((uint16_t)buf[18] 7 | ((uint16_t)buf[19] 1) | ((uint16_t)buf[20] 9)) 0x07FF; ch[14] ((uint16_t)buf[20] 2 | ((uint16_t)buf[21] 6)) 0x07FF; ch[15] ((uint16_t)buf[21] 5 | ((uint16_t)buf[22] 3)) 0x07FF; /* 故障标志位 */ uint8_t failsafe (buf[23] 0x08) ? 1 : 0; uint8_t frame_lost (buf[23] 0x04) ? 1 : 0; /* 这里把 ch[] 和标志位交给上层业务 */ }这段解码看着吓人其实规律很清晰每 11 位一个通道跨字节边界时用移位和或运算拼接。 0x07FF是取低 11 位防止高位脏数据。实操心得这段代码建议直接抄别自己推。我见过太多人自己推的时候把某个通道的移位方向写反结果通道 3 和通道 4 数据互换调半天才发现。抄完用遥控器逐个通道推杆验证一遍确认无误再往下做。4.4 缓冲区指针管理Circular 模式下有个细节要注意DMA 写指针会绕回。如果一次 IDLE 中断里收到的数据跨越了缓冲区末尾直接按recv_len从缓冲区头部读会读到旧数据。我的处理方式是维护一个last_pos记录上次处理到的位置每次计算本次新增数据时考虑绕回static uint16_t last_pos 0; void sbus_idle_callback(void) { uint16_t curr_pos SBUS_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart2.hdmarx); while (last_pos ! curr_pos) { sbus_parse_byte(sbus_buf[last_pos]); last_pos; if (last_pos SBUS_BUF_SIZE) last_pos 0; } }这样无论 DMA 怎么绕状态机拿到的都是连续的字节流。这是整个方案里最容易被写错的地方很多人只处理了不绕回的情况跑一段时间就出问题。5. 常见问题与排查技巧实录5.1 收不到数据或全是乱码按下面顺序排查现象可能原因排查方法完全无数据反相电路没做或接反用示波器看 RX 引脚空闲电平应为高全是 0x00波特率配错确认是 100000 不是 115200数据错位校验位配错确认是 Even 偶校验偶尔乱码DMA 优先级低被抢占提高 DMA 通道优先级帧头找不到停止位配错确认是 2 停止位5.2 IDLE 中断不触发最常见的原因是忘了调__HAL_UART_ENABLE_IT。另一个原因是HAL_UART_Receive_DMA没调用DMA 没启动串口根本没在收数据自然不会有 IDLE。还有一种情况是中断优先级配置冲突IDLE 中断被别的更高优先级中断一直压着。检查 NVIC 里 USART 的抢占优先级别设得太低。5.3 帧尾校验一直失败如果帧头能找到但帧尾总是不对八成是中间丢了字节。可能原因DMA 缓冲区太小一帧还没处理完就被新数据覆盖IDLE 中断处理太慢比如在中断里做了浮点运算或打印串口波特率实际偏差太大接收机晶振和 STM32 晶振不匹配避坑技巧IDLE 中断里只做搬运 喂状态机所有业务处理比如把通道值转成 PWM、发到上位机都放到主循环里做。中断里越短越好这是铁律。5.4 通道值抖动如果通道值稳定但偶尔跳变检查两点一是遥控器本身的中位校准二是解码时的 0x07FF有没有漏。漏了掩码会把相邻通道的高位带进来表现为某个通道值突然变大。5.5 多路 SBUS 同时接收有些项目需要接两路甚至三路 SBUS比如双遥控冗余。这时候每路都要独立的缓冲区、独立的状态机变量、独立的 IDLE 回调。别想着复用一套变量会串数据。DMA 通道也要分开STM32 的 DMA 通道数量有限规划好再分配。6. 性能优化与扩展思路6.1 CPU 占用实测在 F103C8T672MHz上实测这套方案跑单路 SBUSCPU 占用大约 1.5% 到 2%。其中 IDLE 中断本身开销很小主要消耗在状态机逐字节处理和通道解码上。如果把解码也放到中断里占用会升到 5% 左右所以还是建议解码放主循环。6.2 从单路扩展到多路多路扩展时我习惯把状态机封装成结构体typedef struct { uint8_t buf[SBUS_FRAME_LEN]; uint8_t idx; uint8_t in_frame; uint16_t last_pos; uint16_t ch[16]; uint8_t failsafe; uint8_t frame_lost; } sbus_ctx_t;每个串口对应一个sbus_ctx_t实例解析函数接收sbus_ctx_t *ctx参数。这样代码干净扩展方便也不会出现变量串扰。6.3 和 OTA、Modbus 等协议共存很多项目里 SBUS 只是其中一个串口另外还有 Modbus 或 OTA 升级用的串口。这时候要注意 DMA 通道分配和中断优先级。我的经验是SBUS 用最高优先级实时性要求最高Modbus 次之OTA 最低升级时其他功能可以暂停。DMA 通道如果不够用可以考虑用 DMAMUX部分型号支持或者把低优先级串口改回中断接收。6.4 数据校验的加强标准 SBUS 只有帧尾校验没有 CRC。如果应用场景对可靠性要求极高比如工业遥控可以在应用层加一层校验比如连续两帧通道值差异超过阈值就判定为异常帧丢弃。这个逻辑放在解码之后、业务处理之前成本很低但效果明显。7. 我踩过的几个真实坑第一个坑是反相电路用了 PNP 三极管。当时手头没有 NPN想着 PNP 也能反相结果电平逻辑完全反了调了一下午才发现。反相电路必须用 NPN 或者专用反相芯片别想着用 PNP 凑合。第二个坑是DMA 缓冲区设成 25 字节。想着正好一帧省内存。结果 IDLE 中断稍微晚一点第二帧的头就覆盖了第一帧的尾帧尾校验永远失败。后来改成 50 字节两帧就稳了。缓冲区至少留一帧的余量这是血的教训。第三个坑是在 IDLE 中断里调用了printf。调试时想看看收到多少字节就在中断里加了个打印结果串口输出把 SBUS 接收时序全打乱了数据疯狂丢。中断里绝对不能做阻塞操作调试信息要么用变量记录后主循环打印要么用 GPIO 翻转配合逻辑分析仪看。第四个坑是忘了清 IDLE 标志。__HAL_UART_CLEAR_IDLEFLAG这一句漏了中断会反复触发CPU 直接跑飞。这个标志必须清而且要在读数据之前清。8. 代码组织建议最后说说工程结构。我一般把 SBUS 相关代码拆成三个文件sbus.h结构体定义、函数声明、宏定义sbus.c状态机、解码、上下文管理sbus_port.c和 HAL 库的对接层包括 IDLE 回调、DMA 启动、串口初始化后的钩子这样分层的好处是换芯片型号时只需要改sbus_port.c核心解析逻辑完全不用动。我有个项目从 F103 换到 F407只改了 port 层的几行代码半天就迁移完了。中断服务函数里保持极简void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); sbus_port_idle_handler(huart2, sbus_ctx[0]); } HAL_UART_IRQHandler(huart2); }sbus_port_idle_handler里只做指针计算和喂状态机不碰业务。业务层通过查询sbus_ctx[0].ch[]拿数据或者用回调通知。这种中断收数据、主循环处理的模式是我在多个量产项目里验证过最稳的结构。如果你正在做基于 STM32 的遥控接收、飞控或者机器人项目这套 SBUS 解析方案可以直接拿去用。先把反相电路焊对再把串口参数配准剩下的就是抄代码调通的事。真正花时间的从来不是解析逻辑本身而是那些硬件和配置上的细节坑。
返回列表