DMA工作原理:控制器结构、传输流程与总线仲裁

讲DMA,绕不开它的“大脑”——DMA控制器。说白了,它就是系统里专门负责搬数据的一个小硬件引擎。CPU只要告诉它“从哪搬到哪、搬多少”,它就能自己跑完整个流程,完全不用CPU插手。我很多刚入行的朋友觉得这玩意儿玄乎,其实拆开来看,就三个核心模块:控制逻辑、地址发生器、数据缓冲。咱们一个一个说。

DMA控制器内部结构

一个典型的DMA控制器,你可以把它想象成一个智能搬运工。它自己有一套寄存器,用来存放源地址、目的地址、传输长度和传输模式。我习惯把控制器结构分成以下几部分:

  • 主设备接口(Master Interface): 负责向总线发起读写请求。数据传输时,DMA控制器就是总线主设备。
  • 从设备接口(Slave Interface): CPU通过这个接口来配置DMA寄存器。我记得有次调试STM32的DMA,发现传输方向一直不对,最后查出来是配置寄存器时写错了地址偏移量。
  • 通道控制器(Channel Controller): 每个通道管理一个独立的传输任务。通道里存着配置好的源地址、目的地址、传输计数和优先级。
  • 地址发生器(Address Generator): 根据配置的模式(增量、递减、固定)自动更新地址。这一点很容易出错,我很早以前遇到过数据传输到一半地址没更新,结果数据全写到了同一个地方。
  • 传输计数器(Transfer Counter): 记录了还要搬多少数据。每次传输完成一个单位,计数器减一。减到零就触发完成中断。
  • 仲裁逻辑(Arbitration Logic): 当多个DMA通道同时请求总线时,由它来决定谁先用。

你想想看,这结构其实很清晰:CPU负责“下命令”,DMA负责“干活儿”,仲裁逻辑负责“排顺序”。

DMA传输流程详解

DMA的一次完整传输,我总结为“五步搞定”。这个流程我建议你背下来,因为后面所有的配置都是围绕这五个步骤展开的。

  1. CPU配置DMA: 设置源地址、目的地址、传输长度、传输模式(循环、单次、块传输)等参数。同时要指定哪个外设(比如UART、SPI、ADC)作为请求源。
  2. 外设发起请求: 当外设准备好数据(比如UART接收到一个字节),它会拉高DMA请求信号(REQ)。DMA控制器接收到请求后,开始判断当前通道的优先级。
  3. 总线仲裁: 如果多个通道同时请求,仲裁逻辑会按优先级最高的通道去争夺总线控制权。等DMA拿到总线后,它暂时把CPU挂起(或让CPU走另一个总线桥,看系统设计)。
  4. 数据传输: DMA根据源地址读数据,再根据目的地址写数据。每次传输完成后,地址发生器和计数器自动更新。这里需要注意的是,如果设置的是块传输,会一次性占用总线直到整个块搬完;如果是单次传输,每搬一个单位就释放一次总线。
  5. 传输完成: 计数器减到零后,DMA发出中断信号(可选),通知CPU数据搬完了。然后清空请求标志,等待下一次触发。

嗯,这里要注意:很多新手分不清“单字传输”和“块传输”的区别。我个人实战中用的最多的是块传输模式,因为效率高。但在高实时性要求场景下(比如音频播放),我会选择单次传输,避免长时间占用总线导致CPU任务卡顿。

一次典型的DMA配置代码示例(以STM32为例)

// 启动UART1接收的DMA传输
// 假设已经配置好UART1, DMA1通道5
// 源地址: UART1的数据寄存器
// 目的地址: 全局数组 rx_buffer
// 传输长度: 100个字节

__HAL_RCC_DMA1_CLK_ENABLE();

hDma.Instance = DMA1_Channel5;
hDma.Init.Direction = DMA_PERIPH_TO_MEMORY;      // 外设到内存
hDma.Init.PeriphInc = DMA_PINC_DISABLE;           // 外设地址固定
hDma.Init.MemInc = DMA_MINC_ENABLE;               // 内存地址递增
hDma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 8位
hDma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;     // 8位
hDma.Init.Mode = DMA_NORMAL;                      // 单次传输模式
hDma.Init.Priority = DMA_PRIORITY_HIGH;

HAL_DMA_Init(&hDma);
HAL_DMA_Start(&hDma, (uint32_t)&USART1->DR, (uint32_t)rx_buffer, 100);

我的一点建议:配置DMA之前,务必先确认外设的时钟已经使能。我在做电机驱动项目时就吃过亏,明明是UART没时钟,结果去查DMA寄存器,浪费了好几个小时。

总线仲裁与带宽保障

DMA虽然是“搬砖工”,但它不是无限资源。系统总线的带宽就那么多,CPU、DMA、外设都在抢。总线仲裁就是负责分配这个“使用权”。

常见的仲裁方式有两种:固定优先级和循环优先级。固定优先级就是给每个通道或每个主设备定死优先级,高优先级的永远先走。循环优先级则相对公平,大家轮流来。

我这里有一个对比表格,可以帮你快速理解:

仲裁方式 特点 适用场景 我踩过的坑
固定优先级 实现简单、低延迟 高实时性外设(如高速ADC) 低优先级通道可能被饿死
循环优先级 带宽分配均匀 多个外设吞吐量相近时 高优先级任务可能抖动
带权循环 可配置每次传输的份额 混合实时与吞吐场景 配置复杂,寄存器多

我曾经在一个多通道数据采集系统里,因为用了固定优先级,导致低速的温湿度传感器几乎抢不到总线,数据一直丢失。后来换成循环优先级,问题就解决了。所以你看,仲裁策略选不对,硬件再强也没用。

总线仲裁还有一个关键点叫突发传输。所谓突发,就是一次请求总线后,连续传输多个数据单位,而不是每次传完就释放总线再重新请求。突发长度(Burst Length)可以配置为4、8、16等等。突发越长,总线利用率越高,但CPU被挂起的时间也越长。我建议你在做低功耗项目时,把突发长度调小一些;如果追求吞吐量,可以适当增大。

警告:有些MCU(比如早期的STM32F1系列)的DMA不支持SDRAM等慢速设备的突发传输。我记得我第一次用DMA传LCD数据显示,死活不出画面,后来发现是因为没关突发模式。如果你用的外设时钟比较慢,建议先关闭突发,等调通后再优化。

DMA工作原理核心流程 CPU配置DMA寄存器 外设发出DMA请求 总线仲裁(优先级判定) DMA传输数据 传输完成(中断触发) 配置要点 1. 源/目的地址对齐 2. 传输计数与数据宽度 3. 模式选择(普通/循环) 仲裁考虑因素 1. 固定 vs 循环优先级 2. 突发长度设置 3. 多通道同时请求

你看这流程图,从左到右,从上到下。我个人觉得最难把握的就是仲裁环节,因为它是动态的,跟系统里其他冲突强相关。比如CPU正在处理中断,DMA又来抢总线,这时候如果是固定优先级,CPU的实时响应就会受影响。

总结一下核心:DMA控制器结构就是“接口+通道+地址生成+计数器+仲裁”。传输流程是“配置→请求→仲裁→传输→完成”。总线仲裁要重点关注优先级策略和突发长度。嗯,掌握了这些,基本就理解了DMA的工作骨架。