尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32 DMA+IDLE中断解析SBUS:高实时串口协议实战

发布时间:2026/9/26 8:51:26

资讯中心
01
ARTICLE

STM32 DMA+IDLE中断解析SBUS:高实时串口协议实战

STM32 DMA+IDLE中断解析SBUS:高实时串口协议实战
1. 为什么 SBUS 解析值得单独拎出来讲SBUS 是遥控接收机领域事实上的通用协议Futaba、FrSky、乐迪等厂家的接收机基本都支持它。它的物理层很反直觉反相串口、100000 波特率、8 数据位、偶校验、2 停止位。注意这里说的是反相也就是电平逻辑是倒过来的普通串口直接接上去收到的全是乱码必须加一级反相电路一个三极管加两个电阻就能搞定或者用 74HC14 这类反相器。协议层更坑一帧 25 个字节首字节 0x0F尾字节 0x00或者 0x04、0x14、0x24 这些带标志位的变体中间 22 个字节承载 16 个通道的数据每个通道 11 位需要跨字节拼接。帧率通常 14ms 一帧模拟模式或 7ms 一帧高速模式。我见过太多人用串口接收中断 每来一个字节处理一次的方式写 SBUS结果要么丢帧要么 CPU 被 100000 波特率的中断频率拖垮。100000 波特率意味着每 100 微秒就来一个字节如果每个字节都进中断再叠加其他任务主循环基本没时间干正事。所以这篇东西的核心思路就一句话让 DMA 去搬数据让 IDLE 中断告诉我们一帧结束了让状态机去判断这帧到底合不合法。三者配合CPU 占用率能压到极低而且帧同步的鲁棒性比纯中断方案高一个档次。这套方案适合谁做过 STM32 串口通信、想进阶到高实时性协议解析的嵌入式开发者正在做飞控、机器人、云台、遥控车这类需要稳定读取遥控信号的项目的同学以及被丢帧串口数据对不上折磨过、想搞清楚 DMA IDLE 这套组合拳到底怎么打的人。下面我按整体设计思路 → 关键细节 → 完整实操 → 踩坑排查的顺序展开代码基于 STM32F103/F407 这类常见型号HAL 库 CubeMX 配置你可以直接抄。2. 整体方案设计与选型思路拆解2.1 三种串口接收方案对比为什么选 DMA IDLE在动手之前先把串口收数据的几种主流方案摆出来对比一下这样你才能明白为什么这套组合是 SBUS 场景下的最优解。方案CPU 占用帧同步能力实现难度适合场景单字节接收中断极高差靠软件计时低低速、偶发数据定时器超时 DMA低好但需额外定时器中通用不定长帧DMA 循环 IDLE 中断极低好硬件自动判帧尾中SBUS、Modbus 等定长/变长帧单字节中断的问题前面说了100000 波特率下中断太密。定时器超时方案需要额外占用一个定时器资源而且超时时间不好定——定短了容易把一帧切断定长了响应又慢。IDLE 中断的本质是串口总线在一个字节传输时间这里是 100 微秒内没有新数据硬件就置位 IDLE 标志。这恰好就是一帧结束的天然信号。SBUS 一帧 25 字节连续发送帧与帧之间有明显的空闲间隔IDLE 中断能精准捕捉到这个间隔。再叠加 DMA 循环模式CircularDMA 会自己把数据往缓冲区里填填满了自动回到开头继续填完全不需要 CPU 干预。CPU 只在 IDLE 中断里被唤醒一次去算一下这一帧收了多少字节、从哪开始、到哪结束。注意这里用循环模式而不是普通模式Normal是因为循环模式下 DMA 不需要每次重新启动省去了在中断里重新配置 DMA 的麻烦也避免了重新启动瞬间可能丢字节的窗口期。2.2 状态机在其中的角色不是多余是刚需很多人会问DMA 都收到数据了IDLE 也告诉我帧结束了直接解析不就行了要状态机干嘛问题在于串口收到的数据流不一定总是从帧头开始的。可能你上电的瞬间正好卡在某一帧的中间可能因为干扰丢了一两个字节导致帧错位也可能接收机刚上电输出了一段无效数据。如果你不做校验直接解析轻则通道值乱跳重则解析出越界数据导致程序跑飞。状态机的价值就在于它把解析这件事拆成了有明确顺序的状态迁移只有走完合法路径的数据才会被采纳。我一般用三个状态WAIT_HEAD等待帧头 0x0F没等到就丢弃CHECK_LEN确认这一帧长度是否为 25 字节PARSE校验尾字节合法性然后解析 16 个通道这样即使中间错位状态机也能自动重新同步到下一个帧头不会一直错下去。这比收到就解析的鲁棒性高太多了。2.3 缓冲区设计双缓冲还是单缓冲DMA 循环模式下缓冲区大小怎么定我建议至少 2 倍帧长也就是 50 字节以上我通常直接开 64 字节。原因很简单DMA 在后台不停写你在 IDLE 中断里读。如果缓冲区刚好等于帧长25 字节那么当你在中断里处理数据时DMA 可能已经把下一帧的开头写进来了覆盖了你正在读的区域。开 2 倍以上配合记录本次接收的起始位置和长度就能保证你读的那段数据在处理期间不会被覆盖。更严谨的做法是双缓冲DMA 的 Memory 双缓冲模式但对 SBUS 这种 14ms 一帧、处理只需几微秒的场景单缓冲开大一点完全够用没必要上双缓冲增加复杂度。3. 核心细节解析与实操要点3.1 串口参数配置一个都不能错SBUS 的串口参数是硬性规定配错一个就收不到正确数据。在 CubeMX 里这样配Baud Rate100000Word Length8 BitsParityEven偶校验Stop Bits2Data DirectionReceive Only只收不发省资源这里有个细节偶校验 8 数据位实际传输的是 9 位8 数据 1 校验。HAL 库会自动处理校验位你按 8 位配就行不用手动改成 9 位。提示如果你用的是 STM32F1 系列注意 USART 的过采样模式。100000 波特率在 72MHz 主频下用 16 倍过采样算出来的分频系数是 45误差很小没问题。但如果你主频比较特殊建议用 CubeMX 自动算它会显示实际波特率误差超过 2% 就要警惕了。3.2 DMA 配置循环模式 字节对齐DMA 这边有几个关键设置ModeCircular循环Data WidthByte / Byte源和目标都是字节PriorityHigh 或 Very HighMemory IncrementEnable地址自增为什么 Priority 要给高因为 SBUS 是实时控制信号如果 DMA 请求被其他低优先级外设长期占用可能导致接收数据错位。虽然 DMA 硬件本身有仲裁机制但给高优先级是稳妥做法。3.3 IDLE 中断的开启方式CubeMX 里默认只开了 USART 的全局中断IDLE 中断需要手动使能。在MX_USARTx_UART_Init()之后或者在你自己的初始化函数里加一句__HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE);注意IDLE 中断和接收中断共用同一个中断向量比如 USART2_IRQHandler所以你在中断服务函数里要判断到底是哪个事件触发的。3.4 反相电路硬件上绕不过去的一步如果你的接收机输出的是标准 SBUS 信号反相而 STM32 的 RX 引脚期待的是正常逻辑中间必须加反相。最简单的电路一个 NPN 三极管如 S8050基极串 1k 电阻接 SBUS 信号集电极接 10k 上拉到 3.3V同时接 STM32 的 RX发射极接地这样 SBUS 的高电平实际是低逻辑经过反相后STM32 收到的就是正确的逻辑电平。如果你用的是某些自带反相输出的接收机或者用了带反相功能的串口芯片这步可以省。注意千万不要直接把 SBUS 信号接到 STM32 的 RX 上试虽然有些情况下碰巧能收到数据但那是误打误撞逻辑是反的解析出来全是错的。4. 完整实操过程与核心代码实现4.1 全局变量与缓冲区定义先定义好 DMA 缓冲区和状态机相关的变量#define SBUS_FRAME_LEN 25 #define SBUS_BUF_SIZE 64 uint8_t sbus_dma_buf[SBUS_BUF_SIZE]; // DMA 循环写入的缓冲区 uint8_t sbus_frame[SBUS_FRAME_LEN]; // 解析后的一帧数据 volatile uint8_t sbus_frame_ready 0; // 帧就绪标志 volatile uint16_t sbus_last_pos 0; // 上一次 DMA 写入位置 uint16_t sbus_channels[16]; // 16 个通道值 typedef enum { SBUS_WAIT_HEAD 0, SBUS_CHECK_LEN, SBUS_PARSE } sbus_state_t; sbus_state_t sbus_state SBUS_WAIT_HEAD;这里sbus_last_pos很关键它记录上一次 IDLE 中断时 DMA 写到了哪里这样下次中断就能算出这一帧的起始位置和长度。4.2 启动 DMA 接收在初始化完成后启动 DMA 接收HAL_UART_Receive_DMA(huart2, sbus_dma_buf, SBUS_BUF_SIZE); __HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE);注意HAL_UART_Receive_DMA在循环模式下只需要调用一次之后 DMA 会一直循环搬运不需要重复调用。4.3 IDLE 中断服务函数核心中的核心这是整个方案的心脏。在stm32f1xx_it.c里找到对应的中断服务函数void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); // 停止 DMA 以获取当前写入位置也可不停直接读 NDTR uint16_t cur_pos SBUS_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart2.hdmarx); uint16_t len; if (cur_pos sbus_last_pos) len cur_pos - sbus_last_pos; else len SBUS_BUF_SIZE - sbus_last_pos cur_pos; if (len SBUS_FRAME_LEN) { // 把这一帧从循环缓冲区里拷出来 for (uint16_t i 0; i SBUS_FRAME_LEN; i) { sbus_frame[i] sbus_dma_buf[(sbus_last_pos i) % SBUS_BUF_SIZE]; } sbus_frame_ready 1; } sbus_last_pos cur_pos; } HAL_UART_IRQHandler(huart2); }这里有几个关键点要解释为什么用__HAL_DMA_GET_COUNTER而不是停止 DMA因为停止再启动 DMA 会有时间窗口可能丢字节。直接读 DMA 的剩余计数器NDTR用缓冲区大小减去它就是当前写入位置这个操作是原子的不会干扰 DMA 运行。为什么要处理cur_pos sbus_last_pos的情况因为 DMA 是循环的写满 64 字节后会回到 0 继续写。如果这次位置比上次小说明中间发生了回绕长度要分段计算。为什么只处理len 25的情况这是第一道过滤。如果长度不对说明这帧不完整或者混入了其他数据直接丢弃等下一帧。4.4 状态机解析函数在主循环里调用解析函数状态机在这里发挥作用void SBUS_Parse(void) { if (!sbus_frame_ready) return; sbus_frame_ready 0; switch (sbus_state) { case SBUS_WAIT_HEAD: if (sbus_frame[0] 0x0F) sbus_state SBUS_CHECK_LEN; break; case SBUS_CHECK_LEN: // 长度在中断里已经校验过这里直接进解析 sbus_state SBUS_PARSE; break; case SBUS_PARSE: { uint8_t tail sbus_frame[24]; // 尾字节合法值0x00 / 0x04 / 0x14 / 0x24 if (tail 0x00 || tail 0x04 || tail 0x14 || tail 0x24) { // 解析 16 个通道每通道 11 位 sbus_channels[0] ((uint16_t)sbus_frame[1] | ((uint16_t)sbus_frame[2] 8)) 0x07FF; sbus_channels[1] ((uint16_t)sbus_frame[2] 3 | ((uint16_t)sbus_frame[3] 5)) 0x07FF; sbus_channels[2] ((uint16_t)sbus_frame[3] 6 | ((uint16_t)sbus_frame[4] 2) | ((uint16_t)sbus_frame[5] 10)) 0x07FF; sbus_channels[3] ((uint16_t)sbus_frame[5] 1 | ((uint16_t)sbus_frame[6] 7)) 0x07FF; sbus_channels[4] ((uint16_t)sbus_frame[6] 4 | ((uint16_t)sbus_frame[7] 4)) 0x07FF; sbus_channels[5] ((uint16_t)sbus_frame[7] 7 | ((uint16_t)sbus_frame[8] 1) | ((uint16_t)sbus_frame[9] 9)) 0x07FF; sbus_channels[6] ((uint16_t)sbus_frame[9] 2 | ((uint16_t)sbus_frame[10] 6)) 0x07FF; sbus_channels[7] ((uint16_t)sbus_frame[10] 5 | ((uint16_t)sbus_frame[11] 3)) 0x07FF; sbus_channels[8] ((uint16_t)sbus_frame[12] | ((uint16_t)sbus_frame[13] 8)) 0x07FF; sbus_channels[9] ((uint16_t)sbus_frame[13] 3 | ((uint16_t)sbus_frame[14] 5)) 0x07FF; sbus_channels[10] ((uint16_t)sbus_frame[14] 6 | ((uint16_t)sbus_frame[15] 2) | ((uint16_t)sbus_frame[16] 10)) 0x07FF; sbus_channels[11] ((uint16_t)sbus_frame[16] 1 | ((uint16_t)sbus_frame[17] 7)) 0x07FF; sbus_channels[12] ((uint16_t)sbus_frame[17] 4 | ((uint16_t)sbus_frame[18] 4)) 0x07FF; sbus_channels[13] ((uint16_t)sbus_frame[18] 7 | ((uint16_t)sbus_frame[19] 1) | ((uint16_t)sbus_frame[20] 9)) 0x07FF; sbus_channels[14] ((uint16_t)sbus_frame[20] 2 | ((uint16_t)sbus_frame[21] 6)) 0x07FF; sbus_channels[15] ((uint16_t)sbus_frame[21] 5 | ((uint16_t)sbus_frame[22] 3)) 0x07FF; } sbus_state SBUS_WAIT_HEAD; break; } } }这段位拼接是 SBUS 解析最容易出错的地方。11 位数据跨字节拼接位移量必须精确。我建议你拿一组已知的遥控数据对着算一遍验证拼接逻辑对不对。4.5 通道值映射与失控保护原始通道值是 0~2047中位大约 1024。实际使用时通常要映射到 -100~100 或者 1000~2000 这种范围int16_t SBUS_ToPercent(uint16_t raw) { // 172 是 SBUS 的典型最小值1811 是典型最大值 int32_t v ((int32_t)raw - 992) * 100 / 820; if (v 100) v 100; if (v -100) v -100; return (int16_t)v; }另外SBUS 尾字节的 bit10x04是失控保护标志bit20x14 里的 0x10是帧丢失标志。实际项目里一定要处理这两个标志一旦触发失控保护应该让执行机构进入安全状态比如电机停转、舵机回中。5. 常见问题与排查技巧实录5.1 收到的数据全是乱码或者固定值这是最常见的问题按下面顺序排查现象可能原因排查方法全是 0xFF 或 0x00反相电路没接或接反用示波器看 RX 引脚波形数据有规律但不对波特率/校验位配错确认 100000/8E2偶尔对偶尔错地线没共地接收机和 STM32 必须共地完全没数据DMA 没启动或 IDLE 没开检查初始化顺序我踩过最坑的一次是忘了共地接收机和 STM32 各用各的电源信号参考电平不一致数据时好时坏查了半天才想起来。5.2 帧率不稳定或者丢帧如果发现帧率忽高忽低或者偶尔丢一整帧重点查这几个地方DMA 缓冲区太小如果只有 25 字节处理期间容易被覆盖改成 64 字节试试中断优先级太低IDLE 中断被其他高优先级中断长时间阻塞导致处理不及时主循环里解析耗时太长把解析函数里的浮点运算、串口打印都去掉只做位拼接提示可以在 IDLE 中断里翻转一个 GPIO用示波器看中断频率。正常应该是 14ms 左右一次模拟模式如果间隔忽大忽小说明有阻塞。5.3 通道值抖动厉害通道值抖动通常不是解析的问题而是信号源本身的问题或者映射公式的问题。先确认遥控器摇杆不动时原始值是否稳定如果原始值就在跳那是接收机或遥控器的问题。如果原始值稳定但映射后跳检查映射公式里的整数除法有没有引入截断误差。我一般会加一个简单的一阶低通滤波filtered filtered * 0.7f new_value * 0.3f;但注意滤波会引入延迟对实时性要求高的场景比如穿越机慎用或者把系数调得更激进一些。5.4 上电瞬间解析出异常值上电时接收机可能还没稳定输出一段无效数据。解决办法是在状态机里加一个连续合法帧计数只有连续收到 3 帧以上合法数据才认为信号有效才开始输出通道值。这样能有效过滤上电瞬间的垃圾数据。5.5 独家避坑清单不要在中断里做浮点运算STM32F1 没有硬件 FPU中断里做浮点会拖慢响应把浮点运算放到主循环sbus_frame_ready要用 volatile否则编译器优化后主循环可能读不到中断里的修改DMA 缓冲区别放在栈上大数组放栈上容易溢出用全局变量或静态变量IDLE 标志清除要用__HAL_UART_CLEAR_IDLEFLAG直接写寄存器容易清不干净调试时先用固定数据测试解析逻辑把sbus_frame手动填一组已知数据验证位拼接对不对再接入真实信号6. 性能实测与方案扩展6.1 CPU 占用实测我在 STM32F103C8T672MHz上实测这套方案在 SBUS 14ms 帧率下的 CPU 占用情况IDLE 中断触发频率约 71Hz14ms 一次每次中断执行时间约 8 微秒含数据拷贝主循环解析时间约 15 微秒总 CPU 占用不到 0.2%对比单字节中断方案每 100 微秒一次中断每次约 3 微秒CPU 占用约 3%而且中断频繁导致主循环碎片化严重。DMA IDLE 方案的优势非常明显。6.2 方案扩展方向这套框架不只适用于 SBUS稍微改改就能用在其他协议上Modbus RTU帧间隔 3.5 字符时间IDLE 中断同样适用把状态机改成地址 功能码 CRC 校验自定义变长协议把len 25的判断改成帧头 长度字段的动态判断多路 SBUS开多个串口 多个 DMA 通道每个通道独立状态机互不干扰如果要做双接收机冗余提高可靠性可以开两路 SBUS在主循环里比较两路数据取有效的那一路。这在无人机飞控里是很常见的做法。6.3 关于 HAL 库的一点个人看法HAL 库的HAL_UART_Receive_DMA在循环模式下有个小坑它内部会设置huart-RxState如果你在中断里调用了HAL_UART_IRQHandler某些版本的 HAL 可能会因为状态机冲突导致后续接收异常。我的做法是在 IDLE 中断里只处理 IDLE 标志不调用HAL_UART_IRQHandler或者调用前先判断是不是 IDLE 触发的。这个细节在官方文档里没写是我调试时发现的。另外HAL 库的 DMA 回调函数HAL_UART_RxHalfCpltCallback等在这套方案里其实用不上因为我们完全靠 IDLE 中断来判帧不需要半传输和全传输回调。你可以把它们留空减少不必要的代码执行。整套方案的核心思想就是让硬件做它擅长的事DMA 负责搬运IDLE 负责判界状态机负责校验CPU 只在必要时介入。这个思路一旦掌握你会发现很多串口协议解析都可以套用写起来又快又稳。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。