ARTICLE DETAIL

资讯详情

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

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

STM32 HAL库DMA+IDLE中断实现SBUS解析实战 1. 为什么SBUS解析值得单独拿出来讲SBUS这个协议在航模和机器人圈子里太常见了接收机、飞控、舵机控制器之间几乎都用它通信。但很多刚接触STM32的朋友第一次拿到SBUS信号时往往会卡在几个地方串口参数怎么配、数据怎么收才不丢帧、帧头帧尾怎么判断、失控保护标志位怎么处理。更麻烦的是SBUS的电气特性和普通串口不一样它是反相的波特率也不是常见的9600或115200而是100000校验位还得设成偶校验、2位停止位。这些细节如果没搞对串口助手上一片乱码根本没法往下走。我这次做的项目核心目标就是用STM32的HAL库把SBUS接收做成一个稳定、不占CPU、能自动处理帧同步的模块。方案上选了DMA循环接收加串口IDLE中断再配一个轻量级状态机来解析。为什么这么选因为SBUS一帧25个字节如果每来一个字节就进一次中断CPU会被频繁打断尤其在飞控这种对实时性要求高的场景里主循环的负担会明显增加。DMA循环模式可以让串口自己把数据搬到内存里IDLE中断只在总线空闲时触发一次相当于“一帧收完了再通知我”效率高很多。状态机则负责把DMA缓冲区里的原始字节流按SBUS的帧格式拆成16个通道值、2个标志位和1个帧尾校验。这套方案适合谁如果你正在做航模接收机、机器人遥控器、舵机控制板或者任何需要从SBUS信号里提取多路PWM通道值的项目那这篇内容可以直接参考。即使你用的是F103、F407还是G0系列只要HAL库支持串口DMA和IDLE中断思路都是通用的。下面我会从硬件连接、CubeMX配置、代码实现、状态机设计、踩坑记录几个方面把整个流程拆开讲清楚。2. SBUS的电气特性与串口参数配置2.1 反相电平最容易忽略的第一道坎SBUS的物理层是反相的UART。普通串口空闲时是高电平起始位是低电平SBUS正好反过来空闲时是低电平起始位是高电平。如果你直接把SBUS信号接到STM32的RX引脚串口收到的数据全是反的解析出来必然是乱码。解决办法有两种一是加一个反相电路用一个NPN三极管或者74HC14施密特触发器做电平反相二是用软件方式在串口配置里把RX引脚的反相功能打开。STM32的某些系列比如F0、G0、H7的部分型号支持硬件反相在CubeMX里可以直接勾选“RX Pin Inverted”。如果你的芯片不支持那就老老实实加反相电路这是最稳的。我这次用的是F407硬件上没有反相功能所以在外围加了一个简单的三极管反相电路。具体接法SBUS信号经过一个1k电阻接到NPN三极管的基极发射极接地集电极通过10k上拉电阻接到3.3V同时集电极接到STM32的RX引脚。这样SBUS的高电平会让三极管导通集电极拉低SBUS的低电平会让三极管截止集电极被上拉电阻拉高。逻辑正好反了一次STM32收到的就是标准UART电平了。实测这个电路在100k波特率下波形很干净没有明显畸变。2.2 串口参数100000波特率、偶校验、2停止位SBUS的串口参数是固定的波特率100000数据位8位偶校验Even停止位2位。在CubeMX里配置USART时这几个参数必须严格对应。波特率100000不是标准值但STM32的USART支持任意波特率只要时钟源和分频系数算得出来就行。以F407的APB1时钟42MHz为例USART2挂在APB1上波特率计算公式是USARTDIV fCK / (16 * BaudRate)。代入42MHz和100000得到USARTDIV 26.25。整数部分是26小数部分是0.25对应BRR寄存器的值就是0x1A426 4 | 0.25 * 16 26 4 | 4。CubeMX会自动算这个值你只需要在界面上填100000就行。偶校验和2停止位在CubeMX的USART配置里直接选。这里有个细节HAL库的串口初始化结构体里WordLength要设成UART_WORDLENGTH_9B因为偶校验会占用第9位。如果你设成8BHAL库会报错或者校验位不生效。这个坑我在第一次配置时踩过串口助手收到的数据校验位全是错的后来查了参考手册才发现要选9B。2.3 DMA循环模式让串口自己搬数据DMA的配置是这套方案的核心。在CubeMX里给USART2的RX添加DMA通道模式选Circular循环模式数据宽度选Byte优先级可以设Medium或High。循环模式的意思是DMA缓冲区满了之后自动回到开头继续写不需要CPU干预。对于SBUS这种连续不断的25字节帧循环模式正好合适DMA一直在后台把串口数据搬到缓冲区我们只需要在IDLE中断里读取当前写指针的位置就能知道这一帧收到了多少字节。这里要注意DMA缓冲区的长度。SBUS一帧25字节但实际接收时可能会有帧间隔或者噪声所以缓冲区不能只设25。我一般设成50或64留出余量。如果设得太小DMA写指针可能会在一帧还没解析完就绕回去导致数据被覆盖。设成64的话即使连续收两帧也不会立刻覆盖第一帧的数据给状态机留出了处理时间。3. CubeMX工程配置与DMA缓冲区规划3.1 时钟树与串口外设的对应关系在CubeMX里新建工程选好芯片型号后第一步是配时钟树。以F407ZGT6为例外部晶振8MHz经过PLL倍频到168MHzAPB1分频系数设为4所以APB1时钟是42MHz。USART2挂在APB1上所以它的时钟源就是42MHz。如果你用的是USART1它挂在APB2上时钟是84MHz波特率计算的分频系数会不一样。这个细节在配置波特率时要注意CubeMX会自动根据时钟树算BRR值但你得确保时钟树配对了否则实际波特率会有偏差。我习惯在配置完时钟树后先看一眼USART的时钟频率确认是42MHz还是84MHz。然后配USART2Mode选AsynchronousBaudRate填100000WordLength选9BParity选EvenStopBits选2其他默认。NVIC里把USART2的全局中断打开优先级设成5或6不要设太高避免影响其他关键中断。DMA Settings里点Add选USART2_RXMode选CircularData Width选Byte其他默认。3.2 DMA缓冲区的长度与对齐问题DMA缓冲区我定义成一个uint8_t数组长度64。为什么是64而不是25前面说了留余量防止覆盖。另外64是2的幂DMA写指针回绕时的边界计算更方便用位与运算就能代替取模。缓冲区定义的时候最好加上__attribute__((aligned(4)))或者ALIGN_32BYTES之类的对齐属性虽然Byte宽度的DMA对对齐要求不高但对齐之后访问效率更好也避免某些编译器优化带来的奇怪问题。#define SBUS_DMA_BUF_SIZE 64 uint8_t sbus_dma_buf[SBUS_DMA_BUF_SIZE] __attribute__((aligned(4)));在main函数里启动DMA接收HAL_UART_Receive_DMA(huart2, sbus_dma_buf, SBUS_DMA_BUF_SIZE);这行代码执行后DMA就开始把USART2收到的数据往sbus_dma_buf里搬了。注意循环模式下不需要重复调用这行一次启动就行。如果你在运行过程中调用了HAL_UART_DMAStop那需要重新启动。3.3 IDLE中断的使能方式HAL库默认不开启IDLE中断需要手动使能。在启动DMA接收之后加一行__HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE);这行代码的作用是让USART在总线空闲时触发中断。IDLE中断的标志位在SR寄存器里HAL库的中断处理函数HAL_UART_IRQHandler会检查这个标志但默认情况下它不会调用用户回调所以我们需要在中断服务函数里自己判断和处理。具体做法是在stm32f4xx_it.c的USART2_IRQHandler里先调用HAL_UART_IRQHandler然后判断IDLE标志void USART2_IRQHandler(void) { HAL_UART_IRQHandler(huart2); if (__HAL_UART_GET_FLAG(huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart2); sbus_idle_callback(); } }注意清除IDLE标志的顺序很重要先读SR寄存器再读DR寄存器才能清除。HAL库的__HAL_UART_CLEAR_IDLEFLAG宏就是干这个的直接调用就行。如果你先清标志再读数据可能会丢帧。4. 状态机解析SBUS帧的完整实现4.1 SBUS帧格式拆解25字节里有什么SBUS一帧固定25字节格式如下字节位置内容说明0帧头固定0x0F1-22通道数据16个通道每个11位共22字节23标志位bit0失控保护bit1信号丢失bit2故障保护激活24帧尾固定0x0016个通道每个11位总共176位正好22字节。打包方式是小端序通道1的低8位在字节1高3位在字节2的低3位通道2的低5位在字节2的高5位高6位在字节3……以此类推。手动解包很容易出错我一般用位操作循环处理uint16_t channels[16]; uint8_t *p frame[1]; for (int i 0; i 16; i) { int bit_offset i * 11; int byte_offset bit_offset / 8; int bit_in_byte bit_offset % 8; uint32_t val 0; val | p[byte_offset] 0; val | p[byte_offset 1] 8; val | p[byte_offset 2] 16; channels[i] (val bit_in_byte) 0x7FF; }这段代码的逻辑是把三个连续字节拼成一个24位整数然后右移bit_in_byte位取低11位。因为11位跨字节最多涉及3个字节所以取3个字节就够了。实测这个解包方式比逐位拼接快很多而且不容易出错。4.2 状态机的三个状态等待帧头、收集中、校验帧尾状态机我设计了三个状态IDLE等待帧头、RECEIVING收集中、COMPLETE校验完成。在IDLE状态状态机扫描DMA缓冲区找到0x0F就进入RECEIVING同时记录起始位置。在RECEIVING状态继续往后数25个字节如果第25个字节是0x00就认为一帧完整进入COMPLETE如果不是就回到IDLE重新找帧头。COMPLETE状态负责解包通道数据、更新标志位然后回到IDLE等待下一帧。这里有个关键点DMA是循环写的写指针会不断回绕。所以状态机不能简单地用数组下标线性扫描必须处理回绕。我的做法是记录一个“已处理位置”变量last_pos每次IDLE中断触发时从last_pos开始扫描到当前DMA写指针位置。如果写指针小于last_pos说明回绕了需要分两段扫描。具体实现static uint16_t last_pos 0; uint16_t curr_pos SBUS_DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart2_rx); // 从last_pos扫描到curr_pos处理回绕__HAL_DMA_GET_COUNTER返回的是DMA剩余传输次数用缓冲区大小减去它就是当前写指针的位置。这个技巧在循环DMA里非常常用比直接读NDTR寄存器更直观。4.3 失控保护与信号丢失标志的处理SBUS帧的第23字节是标志位bit0表示失控保护Failsafebit1表示信号丢失Frame Lostbit2表示故障保护激活Failsafe Activated。这三个标志在实际使用中很重要如果接收机检测到遥控器信号丢失会把Failsafe置1飞控看到这个标志就应该执行返航或降落。我在状态机的COMPLETE状态里把这几个标志解析出来存到一个结构体里typedef struct { uint16_t channels[16]; uint8_t failsafe; uint8_t frame_lost; uint8_t failsafe_activated; } sbus_frame_t;然后在主循环里判断如果frame_lost连续出现多次就认为信号真的丢了触发保护逻辑。注意frame_lost偶尔出现一次是正常的可能是帧间隔或者噪声不要一出现就触发保护最好加一个计数器连续3次以上再动作。5. 实测中遇到的坑与排查过程5.1 串口乱码反相电路和波特率的双重检查第一次上电测试时串口助手收到的全是0xFF或0x00完全没有SBUS帧的样子。排查过程分两步先确认反相电路有没有问题用示波器看STM32 RX引脚的波形发现空闲时是高电平起始位是低电平说明反相电路工作正常。然后检查波特率用示波器测一个位的宽度发现是10us对应100kHz波特率也对。最后发现问题出在WordLength上我一开始设的是8B但偶校验需要9B导致校验位被当成了数据位整个帧错位。改成9B之后串口助手立刻能看到0x0F开头的帧了。这个坑很典型很多人在CubeMX里看到WordLength有8B和9B两个选项不知道什么时候用9B。记住只要开了校验位就选9B因为校验位占用第9位。如果不开校验选8B。5.2 DMA缓冲区覆盖为什么帧数据会突然跳变调通之后我发现通道数据偶尔会跳变比如油门通道突然从1000跳到2000。用逻辑分析仪抓DMA写指针和状态机处理的位置发现是DMA缓冲区太小导致的。当时缓冲区设的是32而SBUS帧间隔很短连续两帧之间DMA写指针已经绕了快一圈状态机还没处理完第一帧第二帧的数据就把第一帧覆盖了。把缓冲区改成64之后问题消失。所以缓冲区长度至少要是帧长的2倍以上最好2.5倍留出足够余量。5.3 IDLE中断触发过于频繁帧间隔与噪声的处理另一个问题是IDLE中断触发太频繁有时候一帧数据会触发好几次IDLE。原因是SBUS帧内字节之间的间隔虽然很短但在某些接收机上字节之间会有微小的时间间隙导致IDLE标志被置位。解决办法是在IDLE回调里加一个简单的去抖如果这次IDLE距离上次IDLE的时间小于某个阈值比如500us就忽略这次中断。或者更简单的方式在状态机里判断如果当前帧还没收满25字节就继续等待不要重置状态。我用的方法是后者状态机在RECEIVING状态时即使IDLE中断触发也不重置而是继续检查已收到的字节数。只有收满25字节并且帧尾正确才认为一帧完成。这样即使IDLE多次触发也不会影响帧的完整性。5.4 通道值范围与映射从11位到PWM的转换SBUS的通道值是11位范围0-2047对应舵机的PWM范围通常是1000-2000us。转换公式pwm 1000 (channel * 1000) / 2047。但实际使用中很多遥控器的通道中点是1024对应1500us。如果你直接按0-2047线性映射中点会偏到1500左右但两端可能达不到1000和2000。更准确的做法是用遥控器的实际输出范围来标定把摇杆打到最低和最高记录SBUS值然后做线性插值。我一般会在代码里留一个标定接口方便现场调整。6. 性能优化与扩展思路6.1 CPU占用率实测DMAIDLE比逐字节中断省多少我用一个GPIO翻转加示波器的方式粗略测了一下CPU占用率。在100k波特率下SBUS每14ms发一帧每帧25字节。如果用逐字节中断每字节进一次中断14ms内进25次中断每次中断大概2-3us总开销约75us占用率约0.5%。看起来不高但在飞控这种主循环里有很多任务要跑的场景0.5%也是宝贵的。用DMAIDLE之后每帧只进一次IDLE中断中断里只做指针计算和状态机扫描总开销约10us占用率降到0.07%。而且DMA搬运数据完全不占CPU主循环可以专心跑控制算法。6.2 多路SBUS接收DMA通道分配与优先级如果你需要同时接收多路SBUS信号比如双接收机冗余那就需要多个USART和多个DMA通道。STM32F4的DMA1和DMA2各有8个流每个流可以配不同的通道。USART2_RX通常用DMA1_Stream5USART3_RX用DMA1_Stream1USART1_RX用DMA2_Stream2。配置的时候注意DMA流不能冲突优先级可以设成一样的因为SBUS数据量不大不会互相抢占。多路接收时每路都要有自己的缓冲区和状态机实例不要共用全局变量否则会互相干扰。6.3 从SBUS到PWM输出如何驱动舵机解析出通道值之后下一步通常是驱动舵机。STM32的定时器可以输出PWM比如TIM1的CH1-CH4配置成50Hz、脉宽1000-2000us。把SBUS通道值映射到CCR寄存器就行。注意舵机的PWM周期是20ms而SBUS帧间隔是14ms所以SBUS更新比PWM周期快不会出现控制滞后。如果你用的是总线舵机那就把通道值打包成协议帧通过另一个串口发出去。6.4 状态机的可扩展性加入帧计数与超时检测现在的状态机只做了基本的帧解析实际项目中还可以加两个功能帧计数和超时检测。帧计数就是每收到一帧计数器加一主循环里定期检查计数器有没有变化如果超过100ms没变说明接收机挂了。超时检测可以用一个定时器每收到一帧就重置定时器定时器溢出就触发保护。这两个功能在飞控里很常见能显著提高系统的可靠性。7. 代码组织与工程结构建议7.1 把SBUS解析做成独立模块我习惯把SBUS相关的代码放在一个独立的.c和.h文件里比如sbus.c和sbus.h。对外只暴露三个接口sbus_init()、sbus_get_frame()、sbus_idle_callback()。sbus_init()负责启动DMA和使能IDLE中断sbus_get_frame()返回最新的解析结果sbus_idle_callback()在中断里调用负责状态机推进。这样主函数里只需要调用这几个接口不需要关心内部实现。模块化之后换芯片或者换串口只需要改sbus.c里的硬件相关代码上层逻辑不用动。7.2 中断服务函数与主循环的分工中断服务函数里只做最轻量的工作清除IDLE标志、记录DMA写指针、推进状态机。解包通道数据和更新标志位可以放在主循环里做因为解包需要循环16次放在中断里会延长中断时间。我的做法是状态机在中断里只负责找到完整的帧把帧数据拷贝到一个全局缓冲区然后置一个标志位。主循环里检查标志位如果置位了就解包并更新通道值。这样中断时间最短主循环也能控制解包频率。7.3 调试接口用串口打印通道值调试的时候我一般会加一个串口打印功能把16个通道值通过另一个串口发到电脑上用串口助手看。注意打印不要放在中断里也不要在主循环里每帧都打印否则会拖慢主循环。我的做法是每100ms打印一次只打印前4个通道够用就行。打印格式用简单的CSVch1,ch2,ch3,ch4\n方便用Excel或者Python画图分析。8. 写在最后的一些个人体会这套DMAIDLE状态机的方案我从F103一直用到F407和G0稳定性很好基本没出现过丢帧。唯一需要注意的是不同系列的STM32在DMA和USART的寄存器细节上有些差异比如F0系列的DMA通道映射和F4不一样G0系列的USART支持硬件反相但DMA请求映射又变了。移植的时候重点检查三个地方DMA通道映射、IDLE标志清除方式、串口时钟频率。这三个地方对了基本就能跑起来。另外SBUS的100k波特率在长距离传输时容易受干扰如果线缆超过30cm建议加屏蔽或者用双绞线。我遇到过因为线太长导致帧尾偶尔变成0x01的情况状态机校验不过就丢帧后来换了屏蔽线就稳了。如果你在调试时发现帧尾经常不对先检查硬件连接再检查波特率误差最后才怀疑代码。
返回列表