ARTICLE DETAIL

资讯详情

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

STM32 HAL库DMA+IDLE+状态机实现SBUS协议解析

STM32 HAL库DMA+IDLE+状态机实现SBUS协议解析 1. 项目背景与方案选型思考SBUS 是遥控接收机领域非常常见的一种串行总线协议玩航模、做机器人底盘、搞云台控制的朋友应该都不陌生。它本质上是一路反相串口信号波特率 100000、8 位数据位、偶校验、2 位停止位一帧 25 字节每 14 毫秒左右来一帧一帧里塞了 16 个通道的摇杆数据外加 2 个数字开关通道。听起来不复杂但真到 STM32 上用 HAL 库把它稳定收下来坑其实不少。我这次做的项目核心目标就一句话用 STM32 的 HAL 库通过 DMA 循环接收 串口 IDLE 空闲中断 状态机把 SBUS 协议解析成 16 路通道值并且保证长时间运行不丢帧、不卡死、CPU 占用极低。这套方案适合谁适合正在做飞控、机器人、遥控车、云台、机械臂这类需要读取遥控器信号的嵌入式开发者也适合刚学完 HAL 库串口收发、想找一个综合实战项目练手的朋友。为什么不用最简单的“串口接收中断逐字节收”因为 SBUS 一帧 25 字节14ms 一帧算下来每秒大约 1785 字节。如果每个字节都进一次中断CPU 会被频繁打断稍微复杂一点的主循环就会被拖慢。更麻烦的是逐字节中断里如果处理不及时很容易丢字节导致整帧错位。而 DMA 循环接收配合 IDLE 中断能让 DMA 在后台默默把数据搬进缓冲区CPU 只在“一帧收完”这个时间点被叫醒一次效率完全不是一个量级。那为什么还要加状态机因为 IDLE 中断只告诉你“总线空闲了可能一帧结束了”但它不保证你收到的就是完整、正确的一帧。实际使用中会有噪声、会有半帧、会有起始字节对不上的情况。状态机的作用就是做帧同步和合法性校验把“看起来像一帧”的数据真正变成“可信的一帧”。这三者组合起来才是一套能上天的稳定方案。我试过只用 DMA IDLE 不做状态机短时间跑没问题但遥控器开关机、信号抖动的时候偶尔会解析出离谱的通道值舵机直接抽风。后来加了状态机做校验连续跑几个小时都没再出现异常。所以这套组合不是炫技是被实际问题逼出来的。2. SBUS 协议核心细节与硬件层要点2.1 协议帧结构与校验逻辑SBUS 一帧固定 25 字节结构是这样的第 0 字节是帧头0x0F第 1 到第 22 字节是 16 个通道的数据第 23 字节是标志位第 24 字节是帧尾0x00。通道数据不是每通道 2 字节而是 16 个通道共 22 字节按 11 位一个通道紧凑排列这就意味着解析时必须做位操作拼接不能简单地按字节取。具体来说通道 1 占第 1、2 字节通道 2 占第 2、3 字节依次错位。每个通道 11 位范围 0 到 2047对应遥控器摇杆从最低到最高。标志字节里 bit0 和 bit1 通常表示两个开关通道bit2 表示丢帧标志bit3 表示失效保护标志。帧尾固定 0x00这个在状态机校验里非常有用。注意SBUS 是反相电平硬件上必须加一个反相电路常见做法是用一个 NPN 三极管或者专用反相芯片把接收机的 SBUS 信号反相后再接到 STM32 的 RX 引脚。如果你直接把 SBUS 信号接到串口 RX收到的全是反的解析必然失败。这一点新手特别容易忽略。2.2 串口参数配置的坑SBUS 的串口参数和普通串口不一样必须配成波特率 100000、8 位数据位、偶校验、2 位停止位。在 CubeMX 里配置的时候很多人会习惯性选 8N1结果就是收不到正确数据。偶校验和 2 位停止位是 SBUS 协议规定的必须严格匹配。还有一个细节STM32 的串口在偶校验模式下数据位实际是 9 位8 数据 1 校验HAL 库会自动处理你只需要在 CubeMX 里选 Even 和 2 Stop Bits 就行。但如果你用 LL 库或者寄存器操作就要注意校验位占用的位置别把校验位当成数据位读进来。波特率 100000 不是标准波特率CubeMX 里需要手动输入。STM32 的串口时钟分频能支持这个非标准波特率但你要确认一下实际误差。我实测 F103 和 F407 在 72MHz 和 168MHz 主频下100000 波特率的误差都在可接受范围内通信稳定。如果你用的是其他主频建议算一下分频系数误差超过 2% 就可能出问题。2.3 DMA 循环模式的选择理由DMA 接收有两种模式普通模式和循环模式。普通模式收完指定长度就停需要手动重启循环模式则自动回绕缓冲区满了从头覆盖。SBUS 是连续不断的数据流用循环模式最合适DMA 永远在后台搬数据CPU 完全不用管。缓冲区大小怎么定SBUS 一帧 25 字节我一般开 50 字节的缓冲区也就是两帧的容量。这样即使 IDLE 中断稍微延迟一点也不会因为缓冲区太小而覆盖掉正在处理的数据。缓冲区太小会导致帧数据被新数据冲掉太大则浪费 RAM 且增加处理延迟。50 字节是个比较稳妥的经验值。DMA 循环模式下你需要知道当前 DMA 搬到了哪个位置才能算出这一帧数据从哪到哪。HAL 库提供了__HAL_DMA_GET_COUNTER宏可以读取 DMA 剩余传输次数用缓冲区总大小减去剩余次数就是当前写指针位置。这个技巧是整套方案的关键后面状态机里会详细讲怎么用。3. 状态机设计与帧同步策略3.1 为什么需要状态机IDLE 中断触发的时候你只知道“总线空闲了”但缓冲区里可能是一帧完整数据也可能是半帧还可能是两帧粘在一起。如果没有状态机你直接按固定偏移去取数据遇到半帧就会解析出错误值遇到两帧粘一起就会漏掉一帧。状态机的作用就是逐字节扫描缓冲区找到帧头 0x0F然后往后数 25 字节检查帧尾是不是 0x00校验通过才认为这是一帧有效数据。如果中间发现帧头不对或者帧尾不对就丢弃重新找。这样即使数据流里有噪声或者半帧状态机也能自动恢复同步不会一直错下去。我用的状态机很简单就三个状态等待帧头、接收数据、校验帧尾。别看简单配合 DMA 写指针的滑动窗口效果非常稳。3.2 滑动窗口与写指针计算DMA 循环接收的缓冲区是一个环形缓冲区数据不断写入写指针不断前进。IDLE 中断来的时候我需要知道从上次处理位置到当前写指针之间有哪些新数据。这就需要一个“读指针”变量记录上次处理到哪了。具体做法是定义一个read_ptr初始为 0。每次 IDLE 中断里用__HAL_DMA_GET_COUNTER算出当前写指针write_ptr BUF_SIZE - remaining。然后从read_ptr开始逐字节扫描到write_ptr状态机在这个过程中运行。扫描完后把read_ptr更新为write_ptr。这里有个边界情况如果write_ptr小于read_ptr说明 DMA 已经回绕了需要分两段扫描先扫read_ptr到缓冲区末尾再扫 0 到write_ptr。这个逻辑必须处理否则回绕的时候会丢数据。提示read_ptr和write_ptr都是相对于缓冲区大小的索引不要把它们当成绝对地址。每次更新后如果超过缓冲区大小要取模回绕。3.3 帧校验的严格程度取舍状态机校验帧的时候可以只检查帧头和帧尾也可以额外检查标志字节的丢帧和失效保护位。我建议至少检查帧头和帧尾这两个字节是固定的校验成本极低但能过滤掉绝大部分无效帧。标志字节的 bit2 和 bit3 建议也读出来丢帧标志置位说明接收机没收到发射机信号失效保护置位说明接收机进入了保护模式。这两个标志对飞行安全很重要解析出来后可以传给上层做决策比如丢帧时让飞机悬停或者返航。至于通道值的范围校验SBUS 规定是 0 到 2047但实际遥控器输出可能会有轻微超出。我一般不做严格范围校验只在解析后做限幅把超出范围的值钳到 0 或 2047。这样既不会误丢帧又能保证上层拿到的值合法。4. 完整实操流程与关键代码实现4.1 CubeMX 配置步骤先在 CubeMX 里选好芯片型号我这次用的是 STM32F103C8T6也就是常说的蓝板。配置步骤如下第一步打开 USART1模式选 Asynchronous波特率填 100000数据位 8校验 Even停止位 2。第二步打开 USART1 的 DMA 请求方向选 Peripheral To Memory模式选 Circular数据宽度都选 Byte。第三步打开 USART1 的全局中断NVIC 里使能 USART1_IRQn。第四步配置时钟树F103 主频拉到 72MHzUSART1 挂在 APB2 上。第五步生成代码工具链选 MDK-ARM 或者 STM32CubeIDE 都行。生成代码后HAL 库会自动初始化串口和 DMA但 IDLE 中断需要手动使能。在main函数里串口初始化之后加一句__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);这样 IDLE 中断才会触发。4.2 DMA 缓冲区与状态机变量定义在main.c或者单独的sbus.c里定义以下变量#define SBUS_BUF_SIZE 50 #define SBUS_FRAME_LEN 25 uint8_t sbus_buf[SBUS_BUF_SIZE]; uint16_t sbus_read_ptr 0; uint16_t sbus_channels[16]; uint8_t sbus_flags 0; uint8_t sbus_frame_ok 0;sbus_buf是 DMA 目标缓冲区sbus_read_ptr是读指针sbus_channels存解析后的 16 通道值sbus_flags存标志字节sbus_frame_ok是一个标志表示最近是否解析到有效帧。启动 DMA 接收的代码HAL_UART_Receive_DMA(huart1, sbus_buf, SBUS_BUF_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);这两句放在串口初始化之后主循环之前。4.3 IDLE 中断处理与状态机实现IDLE 中断的处理函数需要自己写HAL 库的默认 IRQHandler 里不会自动处理 IDLE。在stm32f1xx_it.c里找到USART1_IRQHandler在HAL_UART_IRQHandler调用之前或之后加一段 IDLE 判断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); }sbus_idle_callback是核心函数实现状态机扫描void sbus_idle_callback(void) { uint16_t write_ptr SBUS_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); uint16_t pos sbus_read_ptr; while (pos ! write_ptr) { // 状态机逐字节处理 sbus_buf[pos] // 这里省略具体状态机逻辑见下文 pos (pos 1) % SBUS_BUF_SIZE; } sbus_read_ptr write_ptr; }状态机逻辑我单独封装成一个函数每次传入一个字节返回是否解析到完整帧。状态定义typedef enum { STATE_WAIT_HEADER, STATE_RECV_DATA, STATE_CHECK_TAIL } sbus_state_t; sbus_state_t sbus_state STATE_WAIT_HEADER; uint8_t sbus_frame[SBUS_FRAME_LEN]; uint8_t sbus_index 0;处理函数void sbus_state_machine(uint8_t byte) { switch (sbus_state) { case STATE_WAIT_HEADER: if (byte 0x0F) { sbus_frame[0] byte; sbus_index 1; sbus_state STATE_RECV_DATA; } break; case STATE_RECV_DATA: sbus_frame[sbus_index] byte; if (sbus_index SBUS_FRAME_LEN - 1) { sbus_state STATE_CHECK_TAIL; } break; case STATE_CHECK_TAIL: sbus_frame[sbus_index] byte; if (byte 0x00) { sbus_parse_frame(sbus_frame); sbus_frame_ok 1; } sbus_state STATE_WAIT_HEADER; break; } }这段代码的逻辑是等待 0x0F 帧头收到后进入接收状态收满 24 字节后进入校验状态检查最后一字节是不是 0x00。是就解析不是就丢弃重新等帧头。简单但有效。4.4 通道数据解析与位操作解析函数sbus_parse_frame负责把 22 字节的通道数据拆成 16 个 11 位值void sbus_parse_frame(uint8_t *frame) { sbus_channels[0] ((frame[1] | frame[2] 8) 0x07FF); sbus_channels[1] ((frame[2] 3 | frame[3] 5) 0x07FF); sbus_channels[2] ((frame[3] 6 | frame[4] 2 | frame[5] 10) 0x07FF); sbus_channels[3] ((frame[5] 1 | frame[6] 7) 0x07FF); sbus_channels[4] ((frame[6] 4 | frame[7] 4) 0x07FF); sbus_channels[5] ((frame[7] 7 | frame[8] 1 | frame[9] 9) 0x07FF); sbus_channels[6] ((frame[9] 2 | frame[10] 6) 0x07FF); sbus_channels[7] ((frame[10] 5 | frame[11] 3) 0x07FF); sbus_channels[8] ((frame[12] | frame[13] 8) 0x07FF); sbus_channels[9] ((frame[13] 3 | frame[14] 5) 0x07FF); sbus_channels[10] ((frame[14] 6 | frame[15] 2 | frame[16] 10) 0x07FF); sbus_channels[11] ((frame[16] 1 | frame[17] 7) 0x07FF); sbus_channels[12] ((frame[17] 4 | frame[18] 4) 0x07FF); sbus_channels[13] ((frame[18] 7 | frame[19] 1 | frame[20] 9) 0x07FF); sbus_channels[14] ((frame[20] 2 | frame[21] 6) 0x07FF); sbus_channels[15] ((frame[21] 5 | frame[22] 3) 0x07FF); sbus_flags frame[23]; }这段位操作是 SBUS 解析的核心也是最容易写错的地方。建议对照协议表逐通道核对写完后用已知数据测试。我当初就是通道 2 和通道 3 的移位写反了调了半天才发现。4.5 主循环中的使用方式主循环里不需要做任何接收相关的事情只需要检查sbus_frame_ok标志然后处理通道值while (1) { if (sbus_frame_ok) { sbus_frame_ok 0; // 使用 sbus_channels 和 sbus_flags // 比如控制电机、舵机、打印调试 } }这种设计的好处是接收完全在中断和 DMA 里完成主循环只管用数据耦合度极低。你甚至可以在主循环里做复杂的 PID 运算、通信协议处理都不会影响 SBUS 接收。5. 常见问题排查与实战避坑经验5.1 收不到数据或数据全错最常见的原因是硬件反相没做或者串口参数配错。先确认 SBUS 信号是否经过反相接到 RX再用示波器或者逻辑分析仪看一下波形确认波特率是不是 100000。如果波形对但数据错检查 CubeMX 里校验位和停止位是不是 Even 和 2。还有一个容易忽略的点有些接收机的 SBUS 输出是 3.3V 电平有些是 5V如果电平不匹配STM32 可能识别不到需要加电平转换。5.2 IDLE 中断不触发IDLE 中断需要手动使能CubeMX 不会自动帮你开。检查__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);有没有加。另外IDLE 标志清除要用__HAL_UART_CLEAR_IDLEFLAG不能和别的标志一起清否则可能清不掉。还有一点如果你在HAL_UART_IRQHandler之后才判断 IDLE可能标志已经被清了所以判断要放在前面。5.3 帧解析偶尔错位这是状态机没处理好回绕导致的。当 DMA 写指针回绕到缓冲区开头时如果读指针还在后面扫描逻辑必须分两段。我见过有人直接while (read_ptr ! write_ptr)然后read_ptr回绕的时候就会死循环或者漏数据。正确做法是用取模运算或者分两段处理。5.4 通道值跳动或抽风先检查位操作有没有写错特别是跨字节的通道。然后检查标志字节的丢帧位如果丢帧位置位说明接收机信号不好通道值本来就不稳定。最后检查电源SBUS 接收机对电源噪声比较敏感如果和电机共用电源电机启动时可能导致接收机输出异常建议接收机单独供电或者加滤波电容。5.5 常见问题速查表问题现象可能原因排查方法完全收不到数据反相电路缺失、串口参数错误检查硬件反相、确认 100000/Even/2Stop数据全错但波形对校验位或停止位配错CubeMX 里改 Even 和 2 Stop BitsIDLE 中断不触发未使能 IDLE 中断加__HAL_UART_ENABLE_IT帧偶尔错位DMA 回绕未处理检查读指针回绕逻辑通道值跳动位操作错误、丢帧核对位操作、检查标志字节运行一段时间死机中断里处理太久、缓冲区溢出缩短中断处理、增大缓冲区提示中断里只做状态机扫描和帧解析不要做浮点运算、打印、延时等耗时操作。把这些放到主循环里做中断才能快速返回。6. 性能优化与扩展思路6.1 CPU 占用与实时性实测我用 F103C8T6 在 72MHz 下实测这套方案 CPU 占用不到 1%。DMA 搬运数据完全不占 CPUIDLE 中断每 14ms 触发一次每次扫描最多 50 字节状态机处理时间在微秒级。主循环有充足的算力做其他事情比如跑 PID、驱动电机、处理上位机通信。如果你用 F407 或者更高端的芯片性能更是绰绰有余。这套方案的可扩展性很好你可以同时跑多路 SBUS、多路串口 DMA 接收互不干扰。6.2 双缓冲与帧率统计如果你需要更高的可靠性可以用双缓冲DMA 配两个缓冲区一个在收的时候另一个在处理。不过 SBUS 帧率只有 70Hz 左右单缓冲 50 字节完全够用双缓冲有点过度设计。但如果你要做帧率统计或者丢帧统计可以在解析成功后记录时间戳算一下帧间隔正常应该是 14ms 左右偏差太大说明信号有问题。6.3 移植到其他芯片的注意事项这套方案不绑定 F103移植到 F407、G0、H7 等系列都可以主要改三个地方一是 CubeMX 里重新配置串口和 DMA二是中断服务函数的名字要对应芯片型号三是 DMA 计数器的获取方式可能略有不同HAL 库的__HAL_DMA_GET_COUNTER大部分系列都通用。移植到 GD32 或者国产替代芯片时要确认 HAL 库的兼容性有些函数的宏定义不一样。6.4 与 RTOS 结合的使用方式如果你在跑 FreeRTOS可以把 SBUS 解析放在一个独立任务里IDLE 中断里只发一个信号量或者任务通知任务收到通知后再处理数据。这样中断处理时间更短任务优先级也好控制。不过要注意任务里处理数据的时候DMA 还在后台写缓冲区所以读指针的更新要保证原子性或者用临界区保护一下。我个人在实际操作中的体会是这套 DMA IDLE 状态机的组合核心价值不在于代码多复杂而在于它把“接收”和“处理”彻底解耦了。DMA 管搬运IDLE 管触发状态机管校验主循环管应用各司其职互不拖累。你把这套模板搭好之后换成其他串口协议比如 GPS、数传、上位机通信只需要改状态机和解析函数框架完全不用动。最后再分享一个小技巧调试的时候可以在状态机里加一个计数器统计收到多少帧、丢弃多少帧通过串口打印出来一眼就能看出通信质量比盲目猜问题高效得多。
返回列表