news 2026/9/19 19:04:43

STM32H7串口DMA在RT-Thread上的适配与优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32H7串口DMA在RT-Thread上的适配与优化

1. 为什么STM32H7的串口DMA在RT-Thread上会翻车

STM32H7这颗芯片在嵌入式圈子里算是明星级别的存在,480MHz的主频、双精度浮点、大容量RAM,拿来做工业网关、音频处理、高速数据采集都很合适。但很多从F4、F7转过来的朋友第一次在H7上跑RT-Thread的串口DMA时,大概率会遇到一个很尴尬的情况:代码编译通过、串口能打印、但DMA就是不动,或者发一次就卡死,再或者接收数据永远进不了回调。

我最早踩这个坑是在一个Modbus网关项目上,用STM32H743配RT-Thread 4.1.x,串口1挂DMA发送,结果rt_device_write返回成功但示波器上量不到波形。查了两天才发现,问题根本不在我的应用代码,而是RT-Thread默认的STM32串口驱动对H7系列的DMA支持是不完整的——准确地说,是H7的DMAMUX架构和F4/F7的DMA控制器差异太大,默认驱动里那套基于DMA1_StreamX的写法在H7上根本对不上号。

这篇文章就是把这个坑从头到尾讲清楚:H7的DMA到底和以前有什么不一样、RT-Thread默认驱动为什么接不上、怎么改才能让串口DMA真正跑起来。内容适合已经会用RT-Thread、但被H7的DMA卡住的嵌入式工程师,也适合正在选型阶段想提前了解H7开发难点的朋友。我会把寄存器层面的原因、驱动修改的具体位置、实测验证的方法都写出来,代码可以直接抄。

1.1 STM32H7的DMA架构到底变了什么

先说清楚H7和F4/F7在DMA上的本质区别,这是理解后面所有问题的前提。

F4/F7时代的DMA叫DMA1DMA2,每个控制器有若干Stream(流),每个Stream有若干Channel(通道),外设请求通过Channel映射到Stream上。比如USART1_TX在F4上固定映射到DMA2_Stream7的Channel4,这个映射关系是硬编码在芯片里的,查参考手册的DMA请求映射表就能找到。

H7完全换了一套架构。它用的是DMAMUX(DMA request multiplexer),DMA控制器本身叫DMA1DMA2,但每个控制器有8个Stream,每个Stream不再直接绑定外设,而是先连到DMAMUX,由DMAMUX来决定这个Stream响应哪个外设的请求。DMAMUX相当于一个"请求路由器",每个Stream对应DMAMUX的一个通道,通道里写一个请求编号(Request ID),就能把任意外设请求接到任意Stream上。

这个变化带来的直接后果是:H7上不存在"USART1_TX固定用DMA2_Stream7"这种说法了。你可以把USART1_TX接到DMA1_Stream0,也可以接到DMA2_Stream5,只要DMAMUX配置对就行。灵活性大大提高,但代价是所有依赖固定映射的代码全部失效。

还有一个坑是H7的DMA请求编号。比如USART1_RX的请求编号是DMA_REQUEST_USART1_RX,这个宏在H7的HAL库头文件里定义,值跟F4完全不同。如果你直接把F4的驱动移植过来,请求编号写错了,DMA会一直等一个永远不会来的请求,表现就是"配置成功但不动"。

1.2 RT-Thread默认驱动的实现假设

RT-Thread的STM32串口驱动在bsp/stm32/libraries/HAL_Drivers/drv_usart.c里,核心逻辑是:打开DMA模式时,根据串口实例去查一张表,找到对应的DMA控制器和Stream,然后调用HAL库的HAL_DMA_Init配置。

问题就出在这张表上。默认驱动里的DMA配置表是按F4/F7的架构写的,结构体大概是这样的:

struct stm32_uart_dma { DMA_Stream_TypeDef *Instance; rt_uint32_t channel; ... };

注意那个channel字段。在F4上它是DMA的Channel编号,在H7上这个概念已经不存在了,H7需要的是DMAMUX的请求编号。默认驱动在H7上编译时,要么因为找不到对应的宏而报错,要么勉强编译通过但配置出来的DMA根本不工作。

更麻烦的是,很多BSP里的drv_usart.c是直接从F4的BSP复制过来的,里面的DMA实例名、请求编号、中断向量全是F4的。你在H7的工程里用这套代码,编译器可能不报错(因为HAL库把一些宏做了兼容定义),但运行时DMA就是不动。

我实测过,在RT-Thread Studio里新建一个H743的工程,默认生成的drv_usart.c里DMA部分基本是空的或者注释掉的,需要自己补。这就是为什么很多人说"RT-Thread在H7上串口DMA用不了"——不是用不了,是默认没给你配好。

2. 动手改造:让串口DMA在H7上真正跑起来

搞清楚原因之后,改造思路就很清晰了:把默认驱动里那套F4风格的DMA配置,换成H7的DMAMUX风格。下面我按实际操作的顺序来讲,每一步都说明为什么这么做。

2.1 第一步:确认你的H7型号和DMA请求编号

不同H7型号(H743、H750、H723、H7A3等)的DMA请求编号表是不一样的,这个必须查对应型号的参考手册。以H743为例,USART1的请求编号在手册的"DMAMUX1 request mapping"表里能查到:

外设请求请求编号说明
USART1_RX41DMAMUX1
USART1_TX42DMAMUX1
USART2_RX43DMAMUX1
USART2_TX44DMAMUX1
USART3_RX45DMAMUX1
USART3_TX46DMAMUX1

这些编号在HAL库里有对应的宏,比如DMA_REQUEST_USART1_RX。你可以在stm32h7xx_hal_dma.h里搜一下确认。千万不要凭记忆写编号,我见过有人把TX和RX的编号写反了,结果发送时DMA去等接收请求,自然一动不动。

提示:H7的DMAMUX1和DMAMUX2是两套,DMA1/DMA2连的是DMAMUX1,BDMA连的是DMAMUX2。串口一般用DMA1或DMA2,所以查DMAMUX1的表。

2.2 第二步:重写DMA配置表

默认驱动里那张表要整个换掉。我在项目里是这么改的,在drv_usart.c里定义一个H7专用的配置结构:

struct stm32_h7_uart_dma { DMA_TypeDef *dma_ctrl; /* DMA1 或 DMA2 */ rt_uint32_t stream_index; /* 0~7 */ rt_uint32_t request; /* DMAMUX 请求编号 */ IRQn_Type irq; /* DMA 中断向量 */ }; static const struct stm32_h7_uart_dma uart1_tx_dma = { .dma_ctrl = DMA1, .stream_index = 0, .request = DMA_REQUEST_USART1_TX, .irq = DMA1_Stream0_IRQn, }; static const struct stm32_h7_uart_dma uart1_rx_dma = { .dma_ctrl = DMA1, .stream_index = 1, .request = DMA_REQUEST_USART1_RX, .irq = DMA1_Stream1_IRQn, };

这里Stream的选择有讲究。H7的DMA1和DMA2各有8个Stream,理论上任意Stream都能接任意请求,但实际选的时候要考虑两点:一是避开已经被其他外设占用的Stream,二是尽量让TX和RX用不同的DMA控制器以分担总线压力。我上面把TX和RX都放在DMA1,是因为这个项目里DMA2被SPI和ADC占了,如果资源充足,TX放DMA1、RX放DMA2是更优的选择。

2.3 第三步:初始化时正确配置DMAMUX

HAL库的HAL_DMA_Init在H7上会自动处理DMAMUX配置,但前提是你传进去的DMA_HandleTypeDefInit.Request字段填对了。默认驱动往往漏了这一步。正确的初始化代码:

static rt_err_t h7_uart_dma_init(struct stm32_h7_uart_dma *cfg, DMA_HandleTypeDef *hdma) { hdma->Instance = (DMA_Stream_TypeDef *)((rt_uint32_t)cfg->dma_ctrl + 0x10 + 0x18 * cfg->stream_index); hdma->Init.Request = cfg->request; hdma->Init.Direction = DMA_MEMORY_TO_PERIPH; hdma->Init.PeriphInc = DMA_PINC_DISABLE; hdma->Init.MemInc = DMA_MINC_ENABLE; hdma->Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma->Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma->Init.Mode = DMA_NORMAL; hdma->Init.Priority = DMA_PRIORITY_HIGH; hdma->Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(hdma) != HAL_OK) { return -RT_ERROR; } return RT_EOK; }

那个Instance的计算看着有点绕,是因为H7的DMA Stream寄存器是连续排列的,基地址加偏移就能算出每个Stream的地址。你也可以直接用DMA1_Stream0这种宏,但用计算的方式在配置表里更灵活。

Init.Request这一行是关键,它告诉HAL库这个Stream要响应哪个外设请求,HAL库内部会去写DMAMUX的CCR寄存器。如果这一行漏了或者填错,DMA就是不动。

2.4 第四步:中断向量和回调的对接

DMA中断向量在H7上也和F4不同。F4的DMA2_Stream7中断叫DMA2_Stream7_IRQHandler,H7虽然名字类似,但向量号变了,而且如果你用了DMAMUX的同步模式,还可能有额外的中断。在RT-Thread里,需要在stm32h7xx_it.c里把中断处理函数接到HAL库的回调上:

void DMA1_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&uart1_tx_dma_handle); }

然后在驱动里注册发送完成回调,通知RT-Thread的串口框架发送完成:

static void uart_dma_tx_cplt(DMA_HandleTypeDef *hdma) { struct stm32_uart *uart = (struct stm32_uart *)((char *)hdma - offsetof(struct stm32_uart, dma_tx.handle)); rt_hw_serial_isr(&uart->serial, RT_SERIAL_EVENT_TX_DONE); }

这个offsetof的用法是从HAL句柄反推回RT-Thread的设备结构体,是RT-Thread串口驱动的标准套路。如果你不接这个回调,表现就是第一次发送成功,第二次发送卡死——因为RT-Thread在等TX_DONE事件才会发下一包。

3. 实测验证:怎么确认DMA真的在工作

改完代码不代表就成功了,必须实测验证。我一般用三个层次来确认。

3.1 寄存器层面:看DMAMUX和DMA的使能位

最直接的方法是在调试器里看寄存器。发送一包数据后,暂停程序,检查:

  • DMAMUX1_ChannelX_CCR:应该等于你配置的请求编号
  • DMA1_StreamX_CR:EN位应该被置1(传输中)或传输完成后被硬件清零
  • DMA1_StreamX_NDTR:剩余传输数量,发送过程中应该递减

如果CCR是0,说明DMAMUX没配上,回去检查Init.Request。如果CR的EN位一直是0,说明HAL_DMA_Start没被调用或者调用失败。

3.2 逻辑分析仪层面:量TX引脚波形

寄存器对了不代表波形对。我用逻辑分析仪抓USART1_TX引脚,发送"Hello"五个字节,应该看到清晰的串口波形。如果波形只有第一个字节然后停住,多半是DMA传输完成中断没触发,或者NDTR配置错了。

这里有个H7特有的坑:H7的DMA在传输完成后,如果配置成循环模式,NDTR会自动重载;如果是普通模式,NDTR归零后需要软件重新配置才能再发。RT-Thread默认用的是普通模式,每次发送都要重新HAL_DMA_Start,这个在驱动里要处理好。

3.3 应用层面:连续发送压力测试

最后在应用层做压力测试,连续发送1000包数据,每包256字节,看是否有丢包或卡死。我实测下来,改好的驱动在H743上跑115200波特率连续发送,CPU占用率不到3%,比中断方式发送低了将近20个百分点,这就是DMA的价值。

测试项中断方式DMA方式(改后)
115200连续发送CPU占用22%2.8%
1000包丢包数00
最大无阻塞波特率115200921600
代码复杂度

4. 常见问题速查与避坑经验

这一节是我在实际项目中踩过的坑的汇总,按问题现象来组织,方便你对照排查。

4.1 问题速查表

现象可能原因排查方法
编译报错找不到DMA_REQUEST_USART1_TXHAL库版本太老或型号选错确认stm32h7xx_hal_dma.h里有该宏
配置成功但DMA不动DMAMUX请求编号错误查参考手册DMAMUX表,核对CCR寄存器
第一次发送成功第二次卡死TX_DONE回调没接检查rt_hw_serial_isr是否被调用
接收数据进不了回调接收DMA没开循环模式或IDLE中断没配H7接收建议用DMA循环+IDLE中断
高速率下数据错位FIFO模式或对齐配置错误检查MemDataAlignmentPeriphDataAlignment
中断进不去中断向量没接或优先级配置错误检查stm32h7xx_it.c和NVIC配置

4.2 几个容易忽略的细节

Cache一致性问题。H7有D-Cache,DMA直接访问内存时如果Cache没处理好,会出现"内存里数据是对的但DMA发出去的是旧数据"这种诡异现象。发送前要SCB_CleanDCache_by_Addr,接收后要SCB_InvalidateDCache_by_Addr。这个问题在F4/F7上不突出,在H7上是必踩的坑。我建议把DMA缓冲区放到非Cache区域,或者用MPU配置成Write-Through模式,省去手动维护Cache的麻烦。

DMA传输完成中断和串口中断的优先级。如果DMA中断优先级比串口中断低,可能出现串口中断里等DMA完成导致死锁。我一般把DMA中断优先级设得比串口中断高一级。

RT-Thread的rt_device_write返回值。DMA模式下这个函数返回的是实际写入的字节数,但如果DMA还在传输中,新的写入会被阻塞或返回0。应用层要做好重试逻辑,不要假设一次write就能全部发出去。

注意:H7的DMA1和DMA2不能同时访问同一个内存区域,如果TX和RX缓冲区有重叠,会出现数据竞争。建议TX和RX用独立缓冲区,并且按Cache line对齐(32字节)。

4.3 一个实用的调试技巧

如果你不确定DMA到底有没有在工作,可以在DMA传输完成回调里翻转一个GPIO,用示波器看这个GPIO的翻转频率。这比看寄存器直观得多,而且能直接反映出DMA的实际吞吐。我在调试阶段经常这么干,一个LED或者一个空闲引脚就够了。

另外,RT-Thread Studio的调试器里可以实时查看变量,把hdma->Instance->NDTR加到watch窗口,发送时看它递减,是最快的确认方法。

5. 关于H7串口DMA的一些延伸思考

改完这个驱动之后,我对H7的DMA架构有了更深的理解。DMAMUX这套设计虽然初期学习成本高,但用熟了之后灵活性确实强。比如你可以把多个外设的请求通过DMAMUX的同步模式做联动,实现"ADC转换完成自动触发DMA搬运"这种硬件级流水线,完全不占CPU。这在F4上是做不到的。

还有一个值得关注的方向是H7的BDMA(Basic DMA)。BDMA挂在DMAMUX2上,专门用于低功耗域的外设,比如LPUART。如果你做低功耗产品,串口用LPUART+BDMA,可以在STOP模式下保持接收,功耗比用DMA1/DMA2低一个数量级。这个我还在验证中,等有完整数据了再单独写一篇。

最后说个实际体会:RT-Thread的BSP质量参差不齐,H7这种新芯片的BSP往往是从老芯片复制过来改的,DMA这种架构变化大的模块最容易出问题。遇到驱动不工作,先别怀疑自己的应用代码,去翻BSP里的驱动实现,大概率问题就在那。我现在的习惯是拿到新BSP先看drv_usart.cdrv_spi.c里的DMA部分,确认是H7原生实现还是F4移植过来的,这能省下大量调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 19:01:19

GitHub热榜项目筛选与运行指南:从趋势解读到实践部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 19:01:16

Linux DRM drmModeSetCrtc底层原理与纯色显示实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华