拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32H743串口DMA高效接收:环形队列与空闲中断实战指南

简介面向嵌入式开发者的STM32H743高效串口通信工程围绕直接存储器访问与通用异步收发器结合空闲中断的完整实现展开重点解决变长数据帧的实时接收、CPU负载优化以及多外设并行处理等工程问题适合中高级嵌入式工程师学习参考。压缩包共192个文件以C源码文件与头文件为主同时包含Keil工程配置文件、汇编启动文件、hex烧录文件以及一键清理脚本等整体体积约1.36MB目录结构完整便于直接导入编译。工程完整展示了从串口参数初始化、直接存储器访问通道配置、空闲中断触发到数据搬运与处理的实现链路并扩展到定时器、FDCAN、I2C、ADC等常用外设驱动可作为后续项目开发的底层参考。目前已有3429人学习下载代码结构清晰、模块划分明确既能帮助入门者理解硬件搬运与中断协同的工作原理也能为工程师扩展多通道串口通信提供可复用的工程模板。1. 为什么偏偏是H743这颗芯片的DMA串口组合强在哪先说个我实际遇到的场景。之前做一款多路传感器采集设备需要用四路串口同时接收不同波特率的数据主控跑着RTOS还要做实时解析。最开始用F103的方案串口中断一多CPU占用率直接飙到60%以上偶尔还会因为中断响应不及时丢字节查起来非常头疼。后来换成STM32H743搭配DMA做搬运中断频率大幅下降同样的逻辑CPU占用率压到了10%以内串口数据零丢失。这个对比让我彻底明白了一件事在H743这个级别的芯片上还靠逐个字节进中断去处理串口数据纯属浪费硬件能力。STM32H743搭载的Cortex-M7内核最高跑到480MHz带双精度浮点单元和L1缓存算力相当富裕。但真正让我推荐它做串口DMA应用的原因是它把“数据搬运”这件事从CPU手里彻底解放了。H743的DMA控制器有多个数据流Stream每个数据流可以绑定不同的外设请求支持内存到外设、外设到内存、内存到内存三种传输方向而且可以配置为循环模式、双缓冲模式、突发传输等。配合UART的FIFO和硬件流控这基本就是为高吞吐串口通信准备的完整方案。适合谁来参考这篇文章如果你正在做多串口数据采集、大流量串口日志传输、通过USB转串口芯片做高速通信或者需要在串口收发的间隙让CPU去处理其他高负载任务那么H743DMAUART这套组合非常值得认真设计。这篇文章从原理到工程配置再到我踩过的坑系统讲一遍。2. DMA和UART是怎么协同工作的底层机制梳理很多初学者会把DMA理解成一个“自动帮你搬数据的功能”这个理解没错但太模糊了。真正用起来你会发现DMA只是一个被动的执行者它所有的工作都靠外设发出的请求信号来触发。UART每接收到一个字节或者发送寄存器为空都会产生一个事件信号这个信号会连接到DMA控制器的某个通道请求线上。DMA收到请求后从设定的源地址读一个字节或半字、字写入目标地址然后计数器减一直到计数归零触发传输完成中断。这个过程中CPU只在最后收到一个中断中间整段数据流没有任何CPU参与。这是DMA的本质价值把高频、机械、重复的数据搬运工作交给专用硬件让CPU专心做逻辑判断和协议解析。2.1 串口接收的两种主流DMA模式第一种是普通模式Normal Mode配置DMA接收缓冲区设定接收字节数NDMA搬运N个字节后触发一次完成中断之后停止。这种方式适合长度固定的报文帧。比如我做过一个设备它的RS485通信协议每条帧固定8字节那么DMA配置成接收8字节触发一次中断CPU在中断里直接解析一帧逻辑非常干净。第二种是循环模式Circular ModeDMA缓冲区是一个环硬件不断接收数据写入缓冲区写到末尾自动绕回开头始终不停止。CPU可以随时检查当前写指针到了哪里自己决定什么时候读取数据。这种模式下完全没有“接收完成”的概念你是在“消费”一个持续流入的缓冲池。这种方式最适合不定长、高频、持续性的数据流比如GPS模块的NMEA语句输出、串口调试日志等。我在H743上最常用的就是循环模式串口空闲中断IDLE Line Interrupt的组合。基本原理是串口每收到一帧数据在帧结束后的空闲期内会产生一个IDLE事件这个事件既可以触发串口中断也可以在DMA循环模式下配合判断DMA当前写指针算出这一帧数据的起始位置和长度。这样CPU只在“一整帧数据到达”时被唤醒一次效率非常高。后面实战部分会展开讲这段逻辑。2.2 发送方向的DMA看似简单其实有细节串口发送用DMA比接收更直观你准备好一个内存缓冲区把数据填好然后触发DMA从内存缓冲区搬运到UART的发送数据寄存器TDRUART硬件负责把寄存器里的数据逐个移位发出去。DMA搬运完所有数据后会产生发送完成中断。有个细节经常被忽略UART外设有一个“发送完成”TC标志和DMA的传输完成DMA TC标志两者不是一回事。DMA把最后一个字节写入UART的TDR之后DMA侧的传输完成中断就触发了但此时这个字节可能还没完全从引脚上移出去。如果这时你立刻操作串口外设比如关闭串口、修改波特率、或者切换RS485的发送/接收方向就会导致最后一个字节被截断。判断数据真正发完要等待串口外设的TC标志位置位。在HAL库中HAL_UART_Start_DMA调用之后DMA完成中断触发时数据还在移位寄存器里你需要再等一下TC标志。对于RS485这种需要切换方向的总线这一步做错会导致帧尾丢失对端设备就收不到完整帧。提示使用HAL_UART_Start_DMA_IT发送时如果发送长度小于等于缓冲区某个阈值HAL库可能不会真正启用DMA而是直接以轮询方式发送。这个阈值在部分版本的HAL库中存在如果你的发送长度很短比如几个字节实际效率可能没有想象中高。3. 工程配置实战CubeMX到HAL库的完整链路在STM32CubeMX里配置H743的串口DMA看起来只是勾选几个选项但里面有几个关键参数直接影响工程质量。我从一个完整项目的角度来拆解。3.1 CubeMX里的关键配置项首先是串口本身的参数波特率、数据位、停止位、校验位这些按你的协议来即可。接着是DMA设置。H743每个UART有独立的DMA发送和DMA接收请求线分别映射到DMA控制器的不同Stream和Channel。CubeMX会自动分配但有一个参数需要手动确认。一个是DMA传输方向。UART_RX对应的DMA方向是PeripheralToMemory外设到内存UART_TX对应MemoryToPeripheral内存到外设。如果你选反了DMA不会工作数据一动不动。一个是数据宽度。UART寄存器的数据寄存器是8位的所以内存端和外设端的数据宽度都应该配置为Byte。有人习惯把内存端配置成Word这会造成数据错乱——每4个字节才搬一次而且只搬第一个字节到UART后面3个字节全丢了。这个坑我见过不止一次。还有一个重要参数是外设地址是否递增。串口数据寄存器是一个固定地址两边都不能递增内存缓冲区的地址当然是递增的。对于接收方向外设地址固定、内存地址递增发送方向则相反。3.2 H743相比F103的额外配置项H743的DMA配置中有两个在F1上没有的增强选项值得说明。Burst突发传输模式。配置为Single单次最稳定配置为INCR4/INCR8/INCR16时DMA可以连续搬运多个数据单元后再释放总线。突发模式能提升总线利用率但如果内存地址没有按相应倍数对齐会触发错误。我的建议是串口这种低速外设单次传输模式完全够用不需要刻意追求突发模式。FIFO模式与阈值。H743 DMA支持内部FIFO可以设置FIFO阈值1/4、1/2、3/4、Full。FIFO模式对内存到内存的搬运很有用但对串口这种外设我实测下来用Direct Mode直接模式更省心。直接模式下DMA不经过FIFO缓冲数据一到就搬运延迟更低不会因为阈值配置不合适导致数据滞留。3.3 时钟频率和DMA带宽的匹配配置完参数还有一个容易忽略的点DMA的时钟。H743的DMA1和DMA2挂在不同总线时钟域上具体时钟频率要看RCC配置。理论上DMA的搬运能力远大于串口的吞吐需求UART在几Mbps波特率下不过每秒几十万字节对DMA来说是小菜一碟。所以在这个场景下你不需要担心DMA带宽不足重点反而在于中断优先级和内存访问冲突。H743有两个DMA控制器如果同时使用多个外设的DMA可以把串口分配到DMA1另一个高吞吐外设比如ADC或SDMMC分配到DMA2合理分散总线压力。RTOS环境中DMA中断优先级建议设置为高于任务切换的临界值避免低优先级中断被长任务推迟导致DMA缓冲区长时间未被消费造成覆盖。4. 接收缓冲区设计环形队列是串口DMA的核心工程配置完DMACPU确实不用管数据搬运了但怎么高效、安全地消费DMA缓冲区里的数据是一个需要认真设计的工程问题。最常见的方案就是设计一个环形队列Ring Buffer作为接收缓存。4.1 为什么需要环形队列DMA在循环模式下持续写入一个固定大小的缓冲区。如果CPU只在一个时间点去读取这个缓冲区那么读取前后新到的数据就会有问题——可能数据还没被处理就被新的数据覆盖或者CPU读取时缓冲区某一部分正在被DMA写入造成半新半旧的数据。环形队列解决的就是“生产者DMA硬件持续生产、消费者CPU软件间歇消费”这个典型异步问题。一个基本的串口环形队列需要记录三个关键信息缓冲区基地址、缓冲区总长度、最新的DMA写位置。DMA的NDTR寄存器当前剩余传输计数可以算出当前写指针位置。HAL库中HAL_UART_Ex_ReceiveToIdle_DMA这类接口也提供了从DMA缓冲区读取已接收数据的方法。我自己的实现方式更简单清晰CubeMX生成的代码中串口接收DMA会配置一个aRxBuffer数组DMA循环写入这个数组。每次进入串口空闲中断时读取hhuart.hdmarx-Instance-NDTR寄存器用RxBufferSize - NDTR就能得到当前DMA写到了哪个位置再结合上一次处理时的写位置就计算出了这一帧数据的长度和位置。4.2 一个实用的环形队列实现思路伪代码如下#define RX_BUF_SIZE 256 uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint16_t last_processed_pos 0; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { uint16_t current_pos RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart-hdmarx); // 这里 current_pos 即当前DMA写指针位置 // Size 是本次空闲中断时新收到的数据长度 // 处理 rx_buffer 中 [last_processed_pos, last_processed_pos Size) 的数据 process_rx_data(rx_buffer[last_processed_pos], Size); // 注意处理环形回绕情况如果 last_processed_pos Size 超过缓冲区长度 // 则需要分成两段处理 last_processed_pos current_pos; } }这段代码的核心逻辑在CubeMX生成代码的基础上补充回调只要串口产生空闲中断说明一帧数据结束了此时DMA缓冲区里新到的数据区间就是上次处理位置到当前位置之间的距离。这里有几个细节需要特别注意。缓冲区大小要和最大帧长匹配。如果缓冲区太小而DMA循环模式下一帧数据还没被CPU处理就已经被新数据覆盖那就丢数据了。串口DMA的缓冲区越大丢失的可能性越低但内存占用和缓存一致性开销也越大。H743的RAM很充裕内部SRAM高达512KB以上给串口接收分配256字节甚至1KB都没有压力不必像F103那样小心翼翼。内存对齐问题。H743的Cortex-M7核心有数据缓存D-Cache如果DMA写入的内存区域被D-Cache缓存了CPU读取时可能读到旧数据因为DMA是直接写内存不经过缓存。解决方式有两个一是将DMA缓冲区定义在非缓存的RAM区域H743的SRAM默认可配缓冲属性二是使用SCB_InvalidateDCache_by_Addr在读取前失效缓存。我习惯在CubeMX生成的分散加载文件中将DMA缓冲区放到指定内存区同时配置MPU将该区域设置为不可缓存Device或Non-cacheable这样全程不需要手动操作缓存省心很多。4.3 双缓冲是升级方案如果你对实时性要求更高不希望CPU在空闲中断里花太多时间处理数据而耽误接收下一帧可以考虑DMA双缓冲模式Double Buffer Mode。H743的DMA支持通过寄存器切换两个内存缓冲区DMA在缓冲区A写满后自动切换到缓冲区B继续写同时产生一个传输完成中断通知CPU处理缓冲区A的数据。这样CPU处理数据和DMA接收数据可以完全并行互不干扰。双缓冲的实现比循环模式稍复杂但一些关键配置在CubeMX里无法直接生成需要手动操作寄存器或调用LL库函数。我的建议是如果你的数据流是“突发到达需要及时处理”双缓冲是非常理想的方案如果数据流相对平稳、帧间隔足够CPU完成处理循环模式已经够用不用增加复杂度。双缓冲之后单独写一篇这里点到为止。5. 实战中那些容易栽跟头的坑逐个踩过才算数这一部分是我最想分享的。串口DMA这套方案配置流程和示例代码到处都是但真正在工程里稳定跑起来要避开的坑还真不少。5.1 接收数据不完整空闲中断和DMA完成中断的配合第一次用循环模式做串口接收时我发现一个问题短帧数据一切正常但长帧数据经常会丢末尾几个字节或者干脆丢一整段。排查了很久才发现问题出在中断的选择上。如果只依赖DMA传输完成中断在循环模式下DMA永远不“完成”除非缓冲区满了所以短帧根本不会触发中断如果只依赖串口空闲中断那么当数据刚好在缓冲区边界处连续到达时DMA的NDTR计算容易出错。更准确的做法是同时开启DMA接收完成中断和串口空闲中断在两种中断中分别处理DMA完成中断时说明缓冲区一整圈满了这时可以按一整段处理数据空闲中断时说明一帧结束处理剩余数据。两套逻辑共同覆盖边界情况。这是我在H743上调试时注意到的细节HAL_UARTEx_ReceiveToIdle_DMA这个HAL函数内部已经同时开启了IDLE中断和DMA传输完成中断并在回调里区分了事件类型。不要用旧版的HAL_UART_Receive_DMA来配合空闲中断用新版HAL库提供的Ex版本接口就是为了解决这个场景设计的。5.2 偶发的CRC校验错误缓存一致性之前有一段时间我做的H743设备在持续运行几个小时后偶尔出现串口收到的数据CRC校验错误。用逻辑分析仪看波形完全正常波特率也准但数据就是偶尔坏一两个字节。这个现象非常诡异因为如果是硬件干扰应该是随机字节错误但实际错误总是出现在固定偏移位置。最后定位到是D-Cache的问题。H743的CPU核心有D-Cache默认情况下CPU读内存优先从Cache读。DMA写内存不经过Cache所以当DMA更新了某段缓冲区内容而Cache里还保留着旧的这一段的副本时CPU读到的就是旧数据。这种问题在设备刚启动时几乎不出现因为当时Cache未命中运行过程中随着Cache的填充和老化策略变化偶发错误就出现了。解决办法前面提到过给DMA缓冲区所在的RAM区域配置MPU为non-cacheable或者在读取之前手动SCB_InvalidateDCache_by_Addr失效对应地址的缓存行。这里给你一个具体配置MPU的小片段MPU_Region_InitTypeDef MPU_InitStruct {0}; MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress (uint32_t)rx_buffer; MPU_InitStruct.Size MPU_REGION_SIZE_256B; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_REGION_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_REGION_NOT_SHAREABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_CTRL_PRIVILEGED_DEFAULT);注意MPU配置必须在对应内存区域被访问之前生效建议在系统初始化阶段完成。而且Cacheable/Not Cacheable的设置要和链接脚本中该区域的属性一致否则行为不可预期。5.3 RS485方向切换TC标志的等待做RS485通信时发送完成后需要立即把收发器切换到接收模式。如果切早了最后一个字节可能只发出一半总线上的其余节点就收不到完整帧。我用过几版代码最后稳定方案是发送完成中断里不直接切方向而是等待串口的TC标志位置位后再切// DMA发送完成中断回调 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { // 等待TC标志 while (__HAL_UART_GET_FLAG(huart, UART_FLAG_TC) RESET) {} // 切换RS485方向为接收 RS485_DE_RE(0); } }这里还有一个细节如果发送缓冲区很大比如一次发几百字节DMA把最后一字节写入TDR后TC标志要等这一字节完全从移位寄存器移出才置位。等待过程很短1个字节的时间用while等没问题。如果你的发送或者接收状态机不允许长时间等待可以在发送启动前就把TC标志清零然后在发送完成中断里读取逻辑更稳妥。5.4 DMA句柄和串口句柄的关联还有一个看起来不起眼但很容易踩的问题CubeMX生成的代码里huart.hdmatx和huart.hdmarx这两个指向DMA句柄的指针一定要正确关联。如果你在CubeMX中修改了DMA配置重新生成代码后发现串口DMA不正常优先检查这两个指针是否还指向正确的DMA句柄。我有一个项目就是因为手动添加了多个DMA句柄后串口代码里还引用着旧的句柄名编译不报错运行不工作查了半天才发现。6. 定位问题的三板斧串口DMA出问题时怎么快速排查串口DMA出了问题现象通常很典型但根因千奇百怪。分享一套我总结的排查顺序帮助大家少走弯路。6.1 先确认DMA有没有在跑问题发生的第一时间先看DMA的NDTR寄存器数值是否在变化。在调试器里暂停程序查看huart.hdmarx-Instance-NDTR如果这个值一直停在初始值附近说明DMA根本没被触发。此时检查三件事DMA的使能位是否置位、串口的请求信号是否使能UART_CR3_DMAR位、CubeMX中的DMA请求映射是否正确。如果NDTR值在持续减小接收模式下说明DMA正在正常工作问题大概率在软件读取逻辑或缓存一致性上。6.2 再查中断标志有没有被清除如果DMA在工作、数据也确实写到了缓冲区但CPU就是没反应大概率是中断标志未清除导致中断无法再次进入。HAL库的HAL_UART_IRQHandler会处理大部分标志清除但某些情况下比如同时使用IDLE中断和DMA中断IDLE标志需要手动清除__HAL_UART_CLEAR_IDLEFLAG(huart1);建议在IDLE中断回调中处理完数据后立即清除标志避免频繁重入或标志残留导致后续中断丢失。6.3 最后看数据是“没到”还是“脏了”如果数据收到了但是内容和发送端不一致那就是数据完整性问题。先看字节偏移是否固定如果固定偏移总出错多半是数据宽度配置错误或缓冲区起始地址未对齐如果随机出错优先怀疑D-Cache或硬件干扰如果是头部丢字节可能是DMA从半程开始接管而你没有清空缓冲区——启动DMA接收前先把缓冲区全部清零并把NDTR手动重置。这个排查顺序我在至少五个项目里用过成功率很高。串口DMA这个技术本身不难难的是工程环境里各种因素交织产生的复合问题。核心思路始终是先确认硬件在动再确认软件在读最后才怀疑数据和环境。7. 一些实测数据和最终建议最后给一些我实测的环境配置作为参考。H743主频480MHzUART波特率921600DMA循环模式接收缓冲区1KBD-Cache开启且缓冲区内存区域配置为non-cacheable。在这种配置下持续接收几十KB数据无丢失、无CRC错误CPU占用率增加不到2%。换成同样波特率用中断方式接收CPU占用率需要15%-20%差异非常明显。对于发送我用DMA发送一个2KB的日志缓冲区从调用HAL_UART_Transmit_DMA到最终TC标志置位耗时约18ms921600波特率下2KB理论发送时间约17.4msCPU在启动DMA后就可以做其他事情实测对主循环响应速度没有任何影响。一个小建议如果你的串口通信协议允许尽量给每帧数据增加帧头帧尾和CRC校验。DMA模式接收的最大风险是“缓冲区中混入错误数据但系统不自知”有了帧校验即使硬件极端情况下出错也能及时踢掉坏帧不至于污染上层协议状态机。我个人的经验是H743的串口DMA配置一旦跑顺稳定性相当可观。它会强迫你把缓冲区管理、中断优先级、内存属性这些底层问题都想清楚而这些能力几乎可以平移到任何带DMA的MCU平台上。如果你现在正用F103之类的芯片做多路串口应用遇到性能瓶颈不妨试试H743这套方案——它值得你去折腾一次。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门