news 2026/8/4 11:52:07

DMA技术详解:从原理到STM32串口DMA实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA技术详解:从原理到STM32串口DMA实战应用

1. 先别被缩写吓到,DMA解决的是CPU“打杂”问题

DMA,全称Direct Memory Access,直接存储器访问。这个名字听起来很技术,但它的核心目标非常直接:让CPU从繁重的数据搬运工作中解放出来

想象一个场景:你的单片机(比如STM32)需要通过串口(USART)接收一长串数据。如果没有DMA,CPU需要像一个“快递员”一样,不断地从串口的数据寄存器里把一个个字节“搬”到内存里。每搬一个字节,CPU都要停下手中的计算工作(比如处理传感器数据、更新屏幕显示),去处理这个“搬砖”的杂活。数据量一大,CPU就几乎被“打杂”占满了,真正要干的“正事”(算法、逻辑)就卡住了。

DMA就是一个专门负责“搬砖”的“小工”。你只需要告诉它:从哪个地方(外设寄存器,如USART->DR)搬数据,搬到哪个地方(内存数组),搬多少。然后CPU就可以去干别的了,DMA会默默地在后台完成所有数据的搬运工作。搬完了,它还可以“举手”(触发中断)告诉CPU:“老板,活干完了,数据都放好了,你来处理吧。”

所以,对于新手来说,理解DMA最关键的一点是:它不帮你计算,它帮你搬运。它的价值在于提升系统效率,让CPU专注于核心计算任务,尤其适合处理大量、连续的数据流,比如:

  • 串口(UART/USART)通信:接收GPS模块的NMEA长报文,或者发送大量的调试信息。
  • ADC采样:连续采集音频、电压信号,需要将海量采样点存入内存。
  • SPI/I2C通信:读写大容量Flash(如W25Q128)、驱动TFT屏幕(如LVGL使用DMA加速图形刷新)。
  • I2S音频:播放或录制音频数据流。

很多新手纠结于DMA的配置细节,却忽略了它要解决的“为什么”的问题。先建立这个“CPU解放者”的认知,后面的通道、传输模式、中断就都好理解了。

2. 理解DMA的“工作流程”:控制器、通道与传输模式

在动手配置代码之前,我们需要把DMA这个“小工”的工作机制搞清楚。这能让你在遇到问题时,知道该检查“工作指令”的哪一部分。

2.1 DMA控制器与通道:谁在干活,走哪条路?

以STM32为例,它内部有一个或多个DMA控制器。每个控制器管理着多条“通道”(Channel)。你可以把控制器想象成一个“劳务公司”,通道就是它派出去的“不同专业的工人”。

  • 通道与请求:每个通道通常被“固定分配”或“可配置”给一个或多个外设。例如,STM32F4的DMA1的通道4,可能被分配给USART1的发送请求。这意味着,当USART1需要发送数据时,它会向DMA控制器“下单”,控制器就会指派通道4这个“专送USART1快递的工人”去干活。在CubeMX或标准库配置中,为外设选择正确的DMA通道是第一步,选错了,DMA就收不到“订单”,不会工作。

  • 仲裁器:如果一个控制器有多个通道同时接到“订单”(比如USART1要收数据,ADC1要传数据),谁先干?这由仲裁器根据优先级(软件可配置)决定。对于新手,通常保持默认优先级即可。

2.2 DMA的“工作清单”:源、目标、数量和模式

配置DMA,本质上就是给这个“工人”写一份清晰的“工作清单”。这份清单主要包含:

  1. 源地址(Source Address):数据从哪里搬?是外设的数据寄存器地址(如&USART1->DR),还是内存中的一个数组地址(&sourceBuffer)?
  2. 目标地址(Destination Address):数据搬到哪里去?是内存地址,还是外设寄存器地址?
  3. 传输数量(Data Number):一共要搬多少“件”?这个“件”可以是字节(Byte)、半字(HalfWord,2字节)或字(Word,4字节),取决于外设数据宽度。
  4. 传输方向(Direction)
    • 外设到内存(Peripheral-to-Memory):最常见,如串口接收。源是USART->DR,目标是内存数组。
    • 内存到外设(Memory-to-Peripheral):也很常见,如串口发送。源是内存数组,目标是USART->DR。
    • 内存到内存(Memory-to-Memory):这是DMA一个强大的功能,可以在不占用CPU的情况下,在内存内部大规模拷贝数据。很多芯片(如STM32)的DMA都支持此模式。

2.3 核心工作模式:单次、循环与双缓冲

这是理解DMA高效处理数据流的关键。

  • 单次模式(Normal Mode):DMA按照你指定的数量搬完一次后,就自动停止,需要CPU重新配置才能启动下一次。适合处理确定长度的单次数据块。
  • 循环模式(Circular Mode)这是处理连续数据流的利器。DMA搬完指定数量的数据后,会自动将源/目标地址和传输计数器重置为初始值,然后重新开始搬运,周而复始。比如用于ADC连续采样,采样数据会被源源不断地填入一个固定大小的内存缓冲区(数组)中,覆盖旧数据。CPU只需要定期来缓冲区里读取最新数据即可。
  • 双缓冲模式(Double Buffer Mode):可以看作是循环模式的“增强版”。它使用两个缓冲区(Buffer0和Buffer1)。当DMA正在向Buffer0填充数据时,CPU可以安全地处理Buffer1中的数据;当Buffer0填满,DMA会自动切换到向Buffer1填充,同时CPU可以切换到处理Buffer0。这完美避免了CPU和DMA同时访问同一块内存区域可能带来的数据一致性问题,在高速数据流处理(如I2S音频、摄像头数据)中至关重要。STM32的很多外设DMA支持直接配置双缓冲。

注意:很多新手在配置串口不定长接收时,喜欢用“单次模式+DMA传输完成中断”,但这要求你知道数据的确切长度。更常见的做法是“循环模式+串口空闲中断(IDLE)”,DMA一直开着,当串口总线空闲一段时间(即一帧数据结束)时,触发中断,此时通过计算DMA已搬运的数据量,就能知道这一帧不定长数据有多长。这就是“HAL库串口空闲中断加DMA”这个热词的典型应用场景。

3. 从零配置:以STM32 HAL库串口DMA收发为例

理论说再多,不如动手调一遍。我们以STM32CubeMX和HAL库为环境,配置USART1使用DMA进行发送和接收。这是最经典、最常用的场景。

3.1 环境准备与CubeMX图形化配置

  1. 硬件:任意一款STM32开发板(如STM32F103C8T6,STM32F407VET6等),确保USART1引脚(PA9/PA10)连接正确。
  2. 软件:安装STM32CubeMX和对应的IDE(Keil MDK或STM32CubeIDE)。
  3. CubeMX配置步骤
    • 选择芯片:创建新工程,选择你的具体型号。
    • 开启USART1:在Pinout & Configuration标签页,找到USART1,选择Asynchronous(异步模式)。引脚PA9(TX)和PA10(RX)会自动配置。
    • 配置DMA
      • 切换到DMA设置页。
      • 点击Add添加DMA请求。
      • 对于USART1_TX(发送)
        • DMA Request: 选择USART1_TX
        • Mode: 选择Normal(单次模式)。因为发送通常由CPU主动触发一次。
        • Direction:Memory To Peripheral(内存到外设)。
        • Priority: 默认LowMedium
      • 对于USART1_RX(接收)
        • DMA Request: 选择USART1_RX
        • Mode:强烈建议选择Circular(循环模式),为不定长接收做准备。
        • Direction:Peripheral To Memory(外设到内存)。
        • Priority: 可以比TX高,确保数据不丢失。
    • 生成代码:配置时钟树(通常用内部或外部晶振,保持默认即可),在Project Manager设置好工程名、路径和IDE,然后点击GENERATE CODE

3.2 代码编写:启动、发送与接收处理

CubeMX生成的代码搭建了框架,我们还需要添加业务逻辑。

// 1. 定义缓冲区 #define RX_BUFFER_SIZE 256 uint8_t txBuffer[] = "Hello DMA!\r\n"; uint8_t rxBuffer[RX_BUFFER_SIZE]; // DMA循环接收缓冲区 volatile uint16_t rxLen = 0; // 接收到的数据长度 uint8_t rxDataTemp[RX_BUFFER_SIZE]; // 用于临时存放处理的数据 // 2. 在main()初始化后,启动串口DMA接收 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); // 启动串口DMA循环接收,数据会持续填入rxBuffer HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE); // 3. 开启串口空闲中断(IDLE),用于检测一帧数据接收完成 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); while (1) { // 主循环处理其他任务,接收完全由DMA+中断在后台完成 // 例如,可以定时发送数据 HAL_UART_Transmit_DMA(&huart1, txBuffer, sizeof(txBuffer) - 1); // 发送字符串(不含结尾\0) HAL_Delay(1000); } } // 4. 重写串口空闲中断回调函数 void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 暂时关闭DMA,安全地计算接收数据长度 __HAL_DMA_DISABLE(huart->hdmarx); // 计算本次接收到的数据长度 // 公式:设定的缓冲区大小 - DMA当前剩余未传输的数据量 rxLen = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx); if (rxLen > 0) { // 1. 将DMA缓冲区数据拷贝到临时处理缓冲区 memcpy(rxDataTemp, rxBuffer, rxLen); // 2. 处理数据 (例如,解析指令、打印等) // user_handle_data(rxDataTemp, rxLen); // 3. 处理完后,可以将处理结果通过DMA发送出去 // HAL_UART_Transmit_DMA(&huart1, response, responseLen); } // 重新配置DMA接收计数器,并启动DMA,准备接收下一帧数据 // 注意:因为之前关闭了DMA,需要重新设置传输数量并启动 __HAL_DMA_SET_COUNTER(huart->hdmarx, RX_BUFFER_SIZE); __HAL_DMA_ENABLE(huart->hdmarx); // 清除空闲中断标志位 __HAL_UART_CLEAR_IDLEFLAG(huart); } }

代码关键点解析

  • 启动接收HAL_UART_Receive_DMA启动了DMA循环接收,数据会源源不断写入rxBuffer,写满后从头覆盖(循环模式)。
  • 空闲中断UART_IT_IDLE中断在一帧数据结束后(总线空闲)触发。在回调函数里,我们计算DMA已经搬运了多少数据到缓冲区,这就是一帧数据的长度。
  • 安全操作:在IdleCallback中,先__HAL_DMA_DISABLE再计算长度,是为了防止在计算过程中DMA仍在修改缓冲区导致数据不一致。计算处理完后,必须重新设置计数器并启用DMA。
  • 发送HAL_UART_Transmit_DMA非阻塞,调用后立即返回,CPU可以去干别的,DMA负责发送完成。

3.3 验证与调试:如何判断DMA在工作?

  1. 发送验证:连接串口助手,上电后应能每秒收到一次“Hello DMA!”。同时,用调试器单步运行,会发现HAL_UART_Transmit_DMA调用后程序立刻继续执行,不会卡住,证明发送是DMA在后台完成的。
  2. 接收验证:在串口助手向单片机发送一串数据(如“12345”)。在HAL_UART_IdleCallback函数内设置断点,或者通过printfrxDataTemprxLen打印出来。你应该能看到正确接收的数据和长度。
  3. 资源监控:在调试模式下,观察CPU利用率(如果IDE支持)。在进行大量数据收发时,对比使用DMA和不用DMA(轮询或中断方式)的CPU负载差异,能直观感受到DMA的优势。

4. 进阶、排错与不同平台的DMA思想

当你掌握了基础用法,就会遇到更复杂的需求和问题。同时,DMA的思想在各种平台(如GD32、FPGA、Zynq)上是相通的。

4.1 常见问题排查链路(避坑指南)

DMA不工作,或者数据不对,别急着怀疑人生,按这个顺序查:

  1. 第一步:查“订单”和“工人”

    • 通道映射对吗?这是最常出错的地方。回头检查CubeMX或你的初始化代码,USART1_RX/TX是否分配到了正确的DMA和通道上?查对应芯片的参考手册《DMA请求映射表》。
    • DMA时钟开了吗?在STM32的RCC配置中,必须使能对应的DMA时钟(如__HAL_RCC_DMA1_CLK_ENABLE())。CubeMX通常会自动生成。
  2. 第二步:查“工作清单”

    • 地址对齐对吗?如果外设数据宽度是16位(如某些ADC),那么内存缓冲区地址最好2字节对齐。HAL库通常能处理,但自己分配缓冲区时要注意。使用__ALIGNED(4)等关键字修饰缓冲区数组。
    • 传输数量(Counter)重置了吗?在单次模式发送完成后,或像上面例子中在空闲中断里重新启动接收前,必须重新设置传输数量HAL_DMA_Start__HAL_DMA_SET_COUNTER
    • 缓冲区溢出?循环接收模式下,如果CPU处理数据的速度跟不上DMA接收的速度,新数据会覆盖旧数据。确保你的处理逻辑足够快,或者使用双缓冲(Double Buffer)
  3. 第三步:查“完成报告”

    • 中断开了吗?如果你使用了DMA传输完成中断(HAL_UART_TxCpltCallback/HAL_UART_RxCpltCallback),需要在CubeMX中开启DMA的全局中断(NVIC Settings),并实现对应的回调函数。
    • 中断标志清除了吗?在中断服务函数或回调函数中,要确保清除了相应的DMA或UART中断标志位,否则会连续进入中断。
    • “发送完成”判断对吗?这是热词“DMA传输最后一个字节后 怎么判断串口已发送完成?”的答案。对于UART,DMA传输完成只意味着数据从内存搬到了UART的发送数据寄存器(TDR)。但UART硬件还需要时间把TDR里的数据一位一位地通过TX线发出去。所以,DMA传输完成 != 串口发送完成。如果需要精确知道串口物理发送完毕(例如才能关闭RS485的发送使能),需要等待UART的TC(传输完成)标志位置位,而不是DMA完成中断。
  4. 第四步:查“数据一致性”

    • 内存访问冲突?CPU和DMA同时访问同一块内存区域,如果没有正确处理缓存(Cache)或使用双缓冲,会导致数据错乱。对于Cortex-M7内核(如STM32H7)或高性能平台,需要特别注意数据缓存(D-Cache)的一致性问题,在DMA传输前后可能需要调用SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr函数。

4.2 不同平台的DMA实现思想

  • GD32 / RT-Thread:GD32作为STM32的兼容/替代品,DMA使用方式几乎一样。在RT-Thread等RTOS中,使用DMA通常结合信号量、消息队列。例如,在DMA完成中断中释放信号量,通知RT-Thread的线程去处理数据,实现高效的多任务通信。
  • FPGA实现DMA:在FPGA中,DMA控制器通常需要你用硬件描述语言(如Verilog)自己设计。它包括状态机控制、地址生成器、总线主设备接口等。FPGA的DMA更底层,灵活性极高,可以实现定制化的高速数据通路,比如直接从摄像头传感器接口(如DVP/MIPI)搬运数据到DDR内存。
  • Zynq AXI DMA:Xilinx Zynq SoC的DMA基于AXI总线协议。它通常包含两个独立的通道(MM2S:内存到流, S2MM:流到内存),通过VDMA(Video DMA)还能处理视频帧。在FreeRTOS下,你需要调用Xilinx提供的XDMA库函数进行配置,并处理好中断与任务同步。它的配置比单片机复杂,但吞吐能力也强得多。
  • Linux下的DMA:在如RZ/N2L这类MPU上,DMA驱动通常由内核提供。应用层通过设备文件(如/dev/dma)或特定的驱动API(如dmaengine)来申请和配置DMA通道,实现用户空间缓冲区与设备间的高效数据传输。

4.3 性能考量与高级应用

  • DMA测速:如何衡量DMA带来的性能提升?可以对比测试:用for循环CPU拷贝1MB数据的时间 vs 使用内存到内存DMA拷贝的时间。同时用逻辑分析仪或示波器监测IO引脚翻转,可以精确测量DMA传输耗时。
  • 与其它技术结合
    • LVGL + DMA:在刷新TFT屏幕时,使用DMA将显存(Frame Buffer)中的数据搬运到LCD的GRAM或SPI接口,可以极大降低CPU负载,实现流畅的图形界面。
    • SPI + DMA:读写SPI Flash(W25Q128)时,使用DMA可以大幅提升连续读写速度。配置时注意SPI的数据帧格式(8位/16位)与DMA的宽度匹配。
    • ADC + DMA + 双缓冲:实现高速连续采样无遗漏的经典方案。DMA循环模式配合双缓冲,ADC永不停止,CPU总有一个完整、稳定的缓冲区可供处理。

DMA不是一个孤立的技术点,它是嵌入式系统优化“数据流”的核心工具。从理解“解放CPU”这个初衷开始,通过标准外设(如UART)上手实践,再逐步深入到内存管理、缓存一致性、多任务同步和不同硬件平台的实现,你就能真正掌握这把提升系统性能的利器。记住,配置时多想想你给DMA的“工作清单”是否清晰完整,出问题时按照“订单-清单-报告-数据”的链路去排查,大部分难题都能迎刃而解。

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

C++函数封装进阶:从参数设计到模板实战的工程实践

1. 从“能用”到“好用”:为什么函数封装是C进阶的必经之路如果你写过一些C代码,可能已经习惯了把功能一股脑塞进main函数,或者随意定义几个函数来处理特定任务。这当然能跑起来,但代码很快就会变得像一团乱麻——修改一个地方&am…

作者头像 李华
网站建设 2026/8/4 11:50:38

Godot引擎RPG开发全流程:从架构设计到核心系统实现

1. 项目概述:为什么选择Godot来构建你的RPG? 如果你正在寻找一个既能让你天马行空地构思世界观,又不会在技术实现上把你卡死的游戏引擎来制作一款RPG,那么Godot引擎绝对是一个值得你投入时间研究的选项。我最初接触Godot&#xff…

作者头像 李华
网站建设 2026/8/4 11:49:40

Linux中文字体安装指南:从乱码到完美显示

1. 为什么在Linux上安装中文字体库是个“刚需”? 如果你在Linux上打开一个中文文档、浏览一个中文网页,或者运行一个带中文界面的软件,看到的却是一堆“口口口”或者乱码方块,那感觉就像对着满汉全席却只能闻味儿。这十有八九是系…

作者头像 李华
网站建设 2026/8/4 11:49:10

Ubuntu 22.04安装NS3网络模拟器:从依赖配置到编译运行的完整指南

1. 为什么在Ubuntu上安装NS3依然是个“技术活”?如果你正在学习网络协议、准备进行网络仿真研究,或者想复现一篇顶会论文里的实验,NS3(Network Simulator 3)大概率是你绕不开的一个工具。作为一个开源的、离散事件驱动…

作者头像 李华
网站建设 2026/8/4 11:48:56

企业级Jenkins Pipeline共享库架构设计与实践

1. 企业级Jenkins Pipeline共享库实施概述 在大型企业持续集成/持续交付(CI/CD)实践中,Jenkins Pipeline共享库已成为标准化建设的核心基础设施。我曾在三个不同行业的头部企业主导过共享库实施,发现它能将构建效率提升40%以上,同时显著降低维…

作者头像 李华
网站建设 2026/8/4 11:47:33

3步搞定网盘限速!LinkSwift直链解析工具完整实战指南

3步搞定网盘限速!LinkSwift直链解析工具完整实战指南 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云…

作者头像 李华