首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
STM32G071 USART3不定长收发:DMA+空闲中断与环形缓冲设计
📅 2026/9/7 13:25:08
✍️ 爱科研究院
👁 阅读 3,247
简介STM32G071CBT6微型开发板串口3不定长可变长数据报文收发程序是一份面向嵌入式开发者、围绕STM32G071CBT6的USART3通信实践而整理的源码工程。资源以Keil MDK工程为主体包含663个C源文件、261个头文件以及IAR/STM32CubeMX相关配置与编译脚本涵盖HAL库驱动、串口中断、环形缓冲与报文分包解析等关键实现压缩包共1278个文件、约35.09MB。已有67人学习下载。资料中附带的链接指向pda2002.com与adixm.com相关参考资源同时包含编译生成的hex、axf、map等产物与工程备份文件便于直接烧录验证和二次移植。对正在调试STM32G0串口不定长收发、需要参考HAL库工程结构的开发者这份资源能提供较为完整的代码框架与排错切入点。 拿到一块STM32G071CBT6核心板我最先做的不是点灯而是把串口3调通做成一条能收发不定长数据报文的通道。串口不定长收发听着基础真正下手做的时候会发现一堆细节怎么判断一帧结束、DMA怎么配合才能不丢数据、环形缓冲区怎么设计才不容易踩坑。这篇文章把我这套基于STM32G071CBT6的USART3不定长收发程序拆开讲从方案选型、接收机制、协议设计到CubeMX配置和实际调试问题一次说清楚。这套程序解决的核心问题很简单上位机通过串口发送长度不固定的数据报文ST32端能准确区分每一帧的边界完整接收、缓存、解析并且能按协议回包。适合正在做单片机串口通信、想用DMA空闲中断处理不定长数据的朋友参考也适合刚接触STM32G0系列、想找一份能直接套用的串口驱动模板的人。G071CBT6虽然是个Cortex-M0内核的小芯片但外设搭配相当齐全串口资源也不少USART3只是其中一个搞清楚一套之后其他串口照葫芦画瓢就能迁移。1. 项目概述与方案选型1.1 为什么选STM32G071CBT6做串口报文处理STM32G071CBT6是意法半导体G0系列里性价比很高的一颗料Cortex-M0内核主频能到64MHzFlash 128KBRAM 36KB。从资源上看它不算强但处理串口报文这种工作完全够用。更关键的是这颗芯片的USART外设做了不少增强比如支持自动波特率检测、8倍过采样、以及红外和智能卡模式串口通信相关的硬件功能很完善对于做工业控制、传感器采集这类需要长时间稳定跑串口协议的场景非常合适。我这次选它还有一个实际原因项目里需要用串口3而不是默认的串口1。很多开发板例程都优先用USART1但实际产品中USART1往往被占用比如接了调试打印、Bootloader跳转或者其他外设这时候USART3反而是更合理的通道。G071CBT6提供多个串口可以从容切换这也是选这颗料的一个加分项。核心板上的USART3引脚引出比较合理不用飞线直接就能接USB转串口模块。从功耗角度看G0系列本身是低功耗定位在做电池供电的设备时串口收发的功耗优化空间比老F1系列好很多。如果你的设备需要在低功耗模式下通过串口唤醒G071CBT6这颗芯片的表现也会更从容。1.2 不定长报文的三条路我为什么走DMA空闲中断处理不定长报文圈子里最常见的方案有三种。第一种是固定帧长每帧固定N个字节判断接收长度达到N就算一帧完整。这种最简单但灵活性太差工业协议里帧长经常变化不适合做通用收发。第二种是帧头帧尾匹配比如规定帧头0xAA 0x55帧尾0x0D 0x0A接收时逐字节搜索帧头帧尾。这种方案通用性强但逐字节中断接收在高波特率下占CPU资源严重而且如果数据内容里恰好出现帧头帧尾字节还得做转义处理协议复杂度会上升。第三种就是我现在用的方案串口空闲中断IDLE配合DMA接收空闲中断硬件会自动检测总线上的空闲状态一帧数据传完总线空闲时间够了硬件就触发中断CPU在中断里把DMA收到的数据取走。三种方案对比下来固定帧长适合数据格式单一的内部通信帧头帧尾适合协议本来就定义好帧边界的场景而IDLEDMA是芯片资源允许时的最优解硬件帮我们完成了断帧核心工作CPU负担极轻代码也不复杂。我这次选择IDLEDMA还有一个原因是STM32G071的串口接收FIFO和DMA请求映射都做得比较完善用起来很顺手实测在115200波特率下连续收发几千帧都没有丢数据。当然IDLEDMA不是没有缺点。如果发送端在帧内部字节之间存在过大间隔比如用某个上位机软件逐字节延时发送硬件会把这帧数据误判成多帧。这个问题我后面在调试部分会详细讲解决思路也有比如搭配帧头帧尾双保险。2. 接收机制核心拆解2.1 空闲中断IDLE到底做了什么很多刚接触的朋友会把IDLE中断理解为“没数据就触发一次中断”这个说法不准确。IDLE中断是串口接收线在一个字节传输完成后继续保持空闲电平超过一个完整字节时间时触发的换句话说它标志的是“一个连续传输过程结束了”这正是我们判断一帧数据结束的好时机。举个例子上位机通过串口发送一帧报文假设内容是14个字节这14个字节如果通过DMA连续发送字节与字节之间几乎没有缝隙那么USART接收完最后一个字节后接收线进入空闲状态大约一个字节时间后IDLE标志置位触发中断。在中断里我只需要从DMA接收缓冲区里读出“本次一共收到了多少个字节”这就是当前帧的长度。在STM32G071上用HAL库实现并不复杂核心是把接收函数切换成HAL_UARTEx_ReceiveToIdle_DMA这个函数在G0系列上被支持得很好。它内部把IDLE中断和DMA接收结合起来收到一帧后回调HAL_UARTEx_RxEventCallback回调参数Size就是本次接收到的字节数。我在这套程序里就是用这个回调作为帧接收完成的入口。如果你不愿意用HAL库想直接操作寄存器也是可行的。在USART3的IRQHandler里读ISR寄存器判断IDLE标志位然后清标志再用DMA当前计数值反算出接收长度。两种方式我都试过HAL库方式代码更简洁寄存器方式更适合理解底层原理。我在这篇文章里以HAL库为主但在关键位置会补一句寄存器层面的原理方便有需要的朋友深入。2.2 环形缓冲区让中断和主循环互不干扰接收中断得到一帧数据后如果直接在中断服务函数里做协议解析、CRC校验、逻辑处理会带来两个问题。第一中断服务时间太长可能影响下一次串口接收造成丢字节第二如果解析逻辑比较重比如要查表、要处理业务状态把CPU长时间占用在中断里非常危险。正确做法是中断只负责把原始数据丢进缓冲区主循环再从缓冲区取出来做协议解析。这里我就用了一个经典的环形缓冲队列。缓冲区分成读写两个指针中断写入时只动写指针主循环读取时只动读指针只要保证单生产者单消费者模型读写指针本身不会冲突。缓冲区大小设为256字节如果单帧数据超过这个长度就需要做分帧或者加大缓冲区这个要根据实际协议帧的最大长度来定不能盲目塞一个很大的数组浪费RAM。环形队列的关键代码并不复杂但有几个细节要特别注意。写指针和读指针的更新要放在临界区保护下最稳妥的办法是先保存全局中断状态再操作指针操作完恢复。另外同时记录当前队列里的数据量取帧时先判断数据量够不够一帧避免读出一半数据造成解析错乱。我这套程序里环形队列操作和协议解析是拆开的队列只负责存取字节不关心帧格式后续想换协议只要动解析层就行。typedef struct { uint8_t *buf; uint16_t size; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t; int16_t ring_write(ring_buf_t *rb, const uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { rb-buf[rb-head] data[i]; rb-head (rb-head 1) % rb-size; if (rb-head rb-tail) { return -1; } } return 0; } int16_t ring_read(ring_buf_t *rb, uint8_t *data, uint16_t len) { if (ring_used(rb) len) { return -1; } for (uint16_t i 0; i len; i) { data[i] rb-buf[rb-tail]; rb-tail (rb-tail 1) % rb-size; } return len; }2.3 为什么DMA在这里是刚需串口接收有两种模式一种是逐字节中断收到一个字节进一次中断CPU把数据搬到内存另一种就是DMA方式串口硬件收到数据后由DMA控制器自动搬运到内存缓冲区收满指定长度或者发生空闲时才通知CPU。前者代码简单但每收一个字节就要进一次中断115200波特率下大约86微秒就有一个字节意味着CPU有相当比例的时间在响应串口中断会挤压主循环的运行时间。DMA方式下CPU全程不参与字节搬运收到一帧数据后CPU只需要知道“这帧有多长”然后处理整块数据效率高一个量级。用DMA接收不定长数据缓冲区长度一般设为单帧可能的最大长度DMA计数器会随着接收自动递减触发空闲中断后用缓冲区总长度减去DMA剩余计数器值就是实际收到的字节数。这里有个容易踩的坑DMA接收缓冲区大小和环形队列大小要匹配。如果DMA缓冲区设成256字节环形队列也建议至少256字节否则一帧数据到达时DMA缓冲区装得下但环形队列放不下就会出现数据被丢弃的情况。我在第一版程序里就吃过这个亏DMA缓冲区256环形队列只开了128上位机一次发长帧时逻辑就出错了。3. 报文协议设计与代码框架3.1 帧格式设计只看字节怎么识别一帧IDLE中断帮忙解决了物理层面的断帧问题但实际应用中只靠IDLE还是不够稳。比如前面提到的如果上位机发送时字节间有较大间隔IDLE会把一帧拆成多帧这时就需要协议层有二次校验能力。所以这套程序在IDLE断帧的基础上仍然在报文格式里保留了帧头帧尾和长度字段形成双保险。我定义了一套简单实用的帧格式帧头0xAA 帧头0x55 LEN CMD DATA[0..n] CRC8LEN字段表示从CMD开始到CRC之前的字节数CRC8是对LEN、CMD和DATA做异或和计算的结果。接收端拿到一帧数据后先检查帧头是否匹配再看LEN和实际长度是否一致最后校验CRC三层检查通过后才认为这是一帧有效数据进入业务处理。这种帧格式在电力、工业通信里很常见简单直接适合大多数场景。如果你的项目对安全要求更高可以把CRC8换成CRC16或CRC32或者干脆用芯片自带的硬件CRC外设来计算。我用CRC8主要是考虑报文长度都不大异或和的计算量极小解析起来也快。应答机制方面设备收到一帧合法指令后处理完再回一帧ACK报文ACK里带上原始指令的CMD和一个小序号字段上位机靠这个判断指令是否执行成功。如果上位机发出指令后一段时间内没收到ACK就重发。这套机制是很多串口协议的基本形态实现成本很低但对排查丢帧非常有帮助。3.2 程序结构与关键代码实现整个程序结构上分成三层。第一层是uart_driver负责串口和DMA的初始化以及帧接收回调这是和硬件强相关的一层第二层是ring_buffer提供环形队列读写接口这层不关心数据内容只做存取第三层是protocol负责从环形队列里取出原始字节流校验帧头、长度、CRC然后分发到业务处理函数。串口接收的核心代码不长主要是配置好DMA和IDLE中断然后在回调里把数据存入环形队列并立刻重新开启下一次接收。这里一定要记住回调函数返回之前必须重新调用HAL_UARTEx_ReceiveToIdle_DMA否则后续数据不会再触发接收程序会“假死”在等待状态。我之前漏过一次体现在上位机那边就是只能收一次数据之后怎么发都不回包。void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART3) { ring_write(rx_ring, rx_dma_buf, Size); HAL_UARTEx_ReceiveToIdle_DMA(huart3, rx_dma_buf, RX_DMA_BUF_SIZE); __HAL_UART_CLEAR_IDLEFLAG(huart3); } }主循环里的解析逻辑则是从环形队列不断取字节用状态机识别帧头然后积累数据到本地缓冲区长度够了做校验。这个状态机写法看起来简单但比一次取一帧判断要稳因为它能应对环形队列里残留半帧数据的情况。我把协议解析做成独立的process_uart_frame函数业务逻辑只在这个函数里处理以后要加新指令改这一个位置就行。void protocol_parse(ring_buf_t *rb) { while (ring_used(rb) 0) { uint8_t b; ring_read(rb, b, 1); switch (parse_state) { case WAIT_AA: if (b 0xAA) parse_state WAIT_55; break; case WAIT_55: if (b 0x55) parse_state WAIT_LEN; else parse_state WAIT_AA; break; case WAIT_LEN: frame_len b; frame_index 0; parse_state WAIT_DATA; break; case WAIT_DATA: frame_buf[frame_index] b; if (frame_index frame_len) { parse_state WAIT_AA; protocol_handle_frame(frame_buf, frame_len); } break; } } }4. CubeMX配置与工程落地4.1 时钟和串口引脚配置的注意事项STM32CubeMX配置这种工程基本是固定套路但有几个地方值得单独说。时钟树方面我建议把系统时钟配到64MHz也就是G071CBT6的最高主频同时注意USART3挂载的时钟树分支在CubeMX里它会自动处理但保存生成工程后最好确认一下USART3的时钟源是不是我们预期的频率这直接影响波特率误差。波特率我选的是115200这也是最常用的调试波特率。引脚方面USART3的TX和RX复用功能需要手动指定我用的是PC4和PC5这对引脚初始化时把GPIO模式设置成AF_PP即复用推挽输出速度选High。这里有个细节RX引脚也需要配置成复用模式而不是输入模式很多新手在这里会卡住CubeMX里USART3选择异步模式后会自动配置好引脚但如果你手改过引脚一定要重新检查GPIO的复用AF号是否正确。另外不要忘记打开USART3和DMA对应的中断以及设置好中断优先级。串口空闲中断和DMA中断优先级不需要太高放在比系统滴答低一级的位置即可但一定要保证它们能被CPU及时响应否则高负载下可能出现帧接收超时的问题。4.2 DMA通道选择和中断分组STM32G071的DMA请求映射USART3_TX和USART3_RX分别对应DMA1的某两个通道具体通道号打开CubeMX配置时界面上会直接列出来。有一点必须注意STM32G0系列和F1系列不太一样外设的DMA请求映射方式有差别老项目代码里的DMA配置不能直接套用需要对照数据手册里的DMA request table确认。这也是很多老工程师第一次用G0系列时容易翻车的地方。DMA配置项里数据方向选择PeripheralToMemory外设地址不需要手动填HAL库会根据串口句柄自动处理内存地址就是我们的接收缓冲区。传输模式选择Circular循环模式这样一轮DMA接收完成后如果IDLE中断没有及时触发也不会停在那里数据还能继续循环接收。虽然我们靠IDLE断帧但循环模式是一个保护垫能防止硬件DMA停在缓冲区末尾造成后续数据丢失。中断分组上串口全局中断和DMA中断都要在NVIC设置里勾选使能优先级可以都给为2实测没有问题。如果系统中还有定时器、外部中断等其他任务建议串口中断优先级不要设成最高避免长时间占用中断环境但也不要太低否则接收时序会受影响。这个数值没有绝对标准要根据项目实际情况试出来。4.3 工程落地后发现的几处细节坑第一处是清IDLE标志的时序。使用HAL_UARTEx_ReceiveToIdle_DMA时在RxEventCallback里最好再调用一次__HAL_UART_CLEAR_IDLEFLAG虽然某些版本HAL库内部处理过但我遇到过一次回调连续触发两次的异常手动清一次标志之后就稳定了。第二处是缓冲区大小和DMA传输长度的匹配。RX_DMA_BUF_SIZE我定义为256这是协议允许的最大单帧长度。如果上位机发来超过256字节的帧DMA循环模式会覆盖前面未取走的数据数据自然就是错的。所以要么把DMA缓冲区定义得比协议最大帧还大要么在协议层对超长帧做丢弃处理。第三处是低功耗和串口的联动。G071主打低功耗但如果你开启了低功耗模式串口接收前要注意配置唤醒源。我在调低功耗模式时发现停止模式下串口中断不能直接唤醒需要在RTC或者外部中断先唤醒再继续处理串口数据。这个坑不涉及具体代码但要是做到了低功耗部分几乎必踩。5. 调试实录与常见问题5.1 用串口调试助手造帧测试程序烧录之后调试阶段我主要用串口调试助手来模拟上位机。先把USB转串口模块插到电脑上确认驱动正常这一步我在Windows上遇到过CH340驱动没安装好的情况设备管理器里能识别到COM口但打开就报错重新安装驱动后解决。打开串口助手配置好波特率115200、8位数据、1位停止位、无校验就能开始发数据了。测试的第一步是验证下位机回包。用串口助手按十六进制方式发送一帧报文比如AA 55 05 01 11 22 33 C9其中AA 55是帧头05是长度01是命令11 22 33是数据C9是CRC异或和。正常的话串口助手数据接收区里应该在几毫秒内出现ACK回包这能确认中断接收、DMA传输、协议解析整条链路是通的。测试不定长功能时可以准备几种不同长度的帧从3字节到100字节分别发几组每一组都确认回包正确。串口调试助手有定时发送功能可以设置每隔50毫秒或100毫秒自动发送连续跑几分钟看统计有没有丢包。实测115200波特率下50毫秒发一次100字节长的报文连续跑了10分钟丢包率为零。这个压力测试结果说明这套方案在常规应用场景下稳定性是被验证过的。5.2 调试中踩过坑的几个现象第一个坑是只能收到第一帧后续全部无响应。排查后确认是回调函数里没有重新调用HAL_UARTEx_ReceiveToIdle_DMA。这个函数在触发一次RxEvent回调后DMA接收就停止了如果不重新启动串口不会再进入接收状态。所以每次回调里重新启动接收是固定的不能漏。第二个坑是收到的数据总是多出几个字节或者断成两截。这个典型的IDLE断帧过度问题经常出现在用串口助手的手动发送功能时因为手动发送通常是软件循环写入字节间可能被系统调度打断产生超过一个字节的空隙硬件就认为帧结束了。解决思路是上位机用DMA或整包发送方式如果协议本身无法保证连续发送就要在协议层增加帧头帧尾检查把IDLE判断作为辅助手段。第三个坑是长时间运行后偶发一帧数据校验错误。排查后发现环形队列的缓冲区在极端情况下存在被覆盖的风险比如上位机连续快速发帧主循环还没来得及取走队列数据下一帧又写入进来了。解决办法是把环形队列容量扩大一倍同时在协议解析层加入长度字段校验校验不过的直接丢弃不影响后续帧。这套组合做下来长时间运行再没有出现校验错误。5.3 常见问题速查表现象可能原因解决方案串口助手打不开COM口CH340驱动未装或端口被占用重装驱动关闭占用端口的其他软件一帧都不收无任何回包回调里没重启ReceiveToIdle回调末尾重新调用HAL_UARTEx_ReceiveToIdle_DMA只能收固定长度变长就错DMA缓冲区被写满覆盖增大DMA接收缓冲区到协议最大帧以上数据断成两截IDLE断帧过于灵敏上位机整包发送或在协议层增加帧头帧尾校验偶发校验错误环形队列覆盖扩大环形队列容量丢弃异常帧后继续工作接线正确但收不到数据TX/RX接反或GND没接交叉连接TTL电平确保共地波特率不准导致乱码时钟树配置或外置晶振选择有误检查CubeMX时钟树和串口时钟源实测波特率误差调试时还有一个实用技巧串口调试助手里打开时间戳显示每个回包都带时间这样能直接看到指令发出到回包的时间差。正常情况应该在几毫秒内如果超过10毫秒说明主循环任务比较多或者协议解析效率有优化空间。我调程序时习惯把这个时间差作为性能指标记录方便后续优化。这套代码我后来也移植到了同系列的STM32G030和G070上只改了一句时钟配置和引脚映射串口部分完全复用说明G0系列串口外设的通用性很强。如果有朋友想把USART3换成其他串口只需要复制一份驱动修改外设基地址、IRQHandler名字和DMA通道请求即可其余逻辑不用变。这套驱动框架现在还在项目里跑着稳定性经过了一轮又一轮的考验。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/7 13:25:08
激光测距传感器VL53L0X实战:原理、校准与应用经验
2026/9/7 13:25:08
2026百元内硬盘盒选购指南:SATA/NVMe主控与避坑全解析
2026/9/7 13:25:08
爱沙尼亚大件货运物流公司怎么选?一份按需求决策的选购指南
2026/9/7 13:55:11
MATLAB t-tide工具箱详解:潮汐调和分析原理与实战
2026/9/7 13:55:11
高级过拟合的伪装:数据泄漏与验证集陷阱全解析
2026/9/7 13:55:11
华为流程管理精髓:组织力如何靠流程长出来
2026/9/7 13:55:11
无锡爱彼劳力士二手表上门回收靠谱吗?收藏级名表怎么选渠道?
2026/9/7 13:55:11
Agent-shell:在Emacs中打造AI Agent中立层与多后端工作流
2026/9/7 13:50:10
2026鹤岗化工产品成分分析检测排名 TOP5 CMA 资质提供含量检测、纯度检测、元素分析 联系方式推荐
2026/9/7 0:03:59
基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现
2026/9/7 0:03:59
UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南
2026/9/7 0:03:59
BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析
2026/9/7 0:22:31
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/7 0:44:48
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/7 1:55:33
基于CNN的调制信号识别:MATLAB实现时频图分类实战