DMA控制器架构深度解析:通道、信号与传输模式

各位同学,今天我们来啃一块硬骨头——DMA控制器的内部架构。说实话,我刚入行那会儿,觉得DMA就是个“后台搬运工”,配好源地址、目的地址、长度就完事了。直到有一次做高速数据采集,系统吞吐量死活上不去,我才意识到:不懂DMA的底层架构,你连问题出在哪都找不到。

这一节,我会把DMA控制器的核心模块拆开来讲。你跟着我走一遍,以后配DMA心里就有底了。

2.1 DMA通道:独立的数据搬运流水线

DMA控制器内部通常集成多个通道(Channel)。每个通道本质上是一条独立的数据搬运流水线。我习惯把通道想象成“专用车道”——每个车道都有自己的源地址寄存器、目的地址寄存器、传输计数器和控制逻辑。

关键认知:通道之间是硬件并行的。一个通道在搬运UART数据时,另一个通道可以同时搬运SPI数据,互不干扰。这是DMA能提升系统吞吐量的根本原因。

不同MCU的通道数量差异很大。我列个常见配置供你参考:

MCU系列 DMA控制器数量 每控制器通道数 典型应用
STM32F4 2个(DMA1/DMA2) 8通道 ADC、SPI、USART
i.MX RT1050 1个(eDMA) 16通道 以太网、LCD、SDIO
NXP LPC546xx 1个(DMA0) 24通道 多路传感器采集

这里有个坑:通道数不是越多越好。通道多了,仲裁逻辑变复杂,响应延迟反而可能增加。我在一个项目中用过32通道的DMA,结果通道间优先级配置不当,低优先级通道的请求被饿死了。嗯,这个后面讲优先级时会细说。

2.2 请求与应答信号:DMA的“握手协议”

DMA不是自己在那闷头搬数据。它需要外设告诉它“我有数据了”或者“我需要数据了”。这个过程靠的就是请求(Request)和应答(Acknowledge)信号。

典型的握手流程是这样的:

  1. 外设发起请求:比如UART接收寄存器非空,拉高DMA请求线。
  2. DMA控制器仲裁:如果有多个通道同时请求,DMA根据优先级决定先服务谁。
  3. DMA发送应答:选中通道后,DMA向外设发送应答信号,表示“我来接手了”。
  4. 外设释放请求:外设看到应答,把数据放到总线上,然后拉低请求线。
  5. DMA执行传输:DMA完成一次数据搬运,更新地址和计数器。

实战经验:我曾经调试过一个SPI DMA通信失败的问题。示波器一抓,发现外设的请求信号只维持了1个时钟周期,而DMA的应答信号需要2个时钟周期才能到达。这就是典型的“握手时序不匹配”。解决办法是在DMA配置里插入一个等待周期。

不同厂家的DMA,握手方式略有差异。我整理了一个对比表:

特性 STM32 DMA NXP eDMA TI uDMA
请求信号极性 高电平有效 可配置(高/低) 上升沿触发
应答方式 单次应答 单次/连续应答 单次应答
请求保持时间 至少1个HCLK 至少2个总线时钟 至少1个系统时钟
支持链式请求 否 是(通过TCD链接) 是(通过描述符链)

2.3 传输粒度:字节、字与突发

DMA一次搬多少数据?这就涉及到传输粒度(Transfer Width/Burst Size)。说白了,就是DMA每次“咬一口”咬多大。

  • 字节(8-bit):一次搬1个字节。适合UART、I2C这类按字节传输的外设。
  • 半字/字(16/32-bit):一次搬2或4个字节。适合SPI、ADC等并行数据接口。
  • 突发(Burst):连续搬多个数据单元。比如一次突发搬4个字(16字节)。

你可能会问:为什么不用最大粒度一直搬?原因有两个:

第一,总线占用。 突发传输会长时间霸占总线,其他外设(比如CPU取指令)就得等着。我在一个音频处理项目里,DMA突发设得太大,结果CPU取指令被频繁阻塞,系统响应延迟从10μs飙升到200μs。

第二,外设FIFO深度。 很多外设的FIFO只有几个字节深。你一次突发搬16个字,外设FIFO根本装不下,数据就丢了。

避坑指南:我曾经在配置SDIO DMA时,把突发大小设成了16字,结果SD卡返回的数据一直错位。查了两天才发现,SDIO的FIFO深度只有8字。突发大小超过FIFO深度,数据会覆盖。记住一个原则:突发大小 ≤ 外设FIFO深度。

不同传输粒度的适用场景,我建议你这样选:

传输粒度 总线占用时间 适用外设 注意事项
字节(8-bit) 短 UART、I2C、低速传感器 CPU干扰小,但吞吐量低
字(32-bit) 中等 SPI、ADC、DAC 注意外设数据对齐
突发(4-16字) 长 LCD、以太网、高速存储 必须检查FIFO深度

2.4 地址模式:线性、循环与链式

地址模式决定了DMA搬完一次数据后,地址怎么变。这是DMA配置里最容易出错的地方。我按自己的理解,把三种模式讲透。

2.4.1 线性模式(Linear Mode)

最简单也最常用。源地址和目的地址按固定步长递增。搬完N个数据后,DMA自动停止。

// 伪代码示例:线性模式配置
dma_channel_config ch_cfg;
ch_cfg.src_addr      = (uint32_t)adc_buffer;    // 源地址:ADC数据缓冲区
ch_cfg.dst_addr      = (uint32_t)ram_buffer;    // 目的地址:RAM缓冲区
ch_cfg.src_increment = 4;                       // 源地址每次+4(32位数据)
ch_cfg.dst_increment = 4;                       // 目的地址每次+4
ch_cfg.transfer_size = 1024;                    // 总共搬1024次
ch_cfg.mode          = DMA_MODE_LINEAR;         // 线性模式

这个模式适合“一次性搬完”的场景,比如把ADC采样结果搬到内存里做FFT。

2.4.2 循环模式(Circular Mode)

地址走到末尾后,自动回到起始地址。说白了,就是个环形缓冲区。

我特别喜欢这个模式。为什么?因为它可以无限循环搬运,不需要CPU反复重配DMA。比如音频播放:DMA从内存循环读取音频数据送到DAC,CPU只管往缓冲区里填新数据就行。

性能要点:循环模式下,DMA传输完成中断可以配置为“半传输完成中断”和“传输完成中断”。利用这两个中断,你可以实现双缓冲(Double Buffering)——CPU在DMA搬后半段时,处理前半段的数据。这样吞吐量直接翻倍。

2.4.3 链式模式(Linked List / Chain Mode)

这是最灵活、也最复杂的模式。DMA搬完一段数据后,自动加载下一个传输描述符(TCD),开始新的传输。多个描述符串成一个链表。

你想想看,这意味着什么?意味着你可以预定义一系列不同的传输任务,DMA自己按顺序执行,CPU完全不用管。

// 链式模式描述符示例(NXP eDMA TCD结构)
typedef struct {
    uint32_t src_addr;          // 源地址
    uint32_t dst_addr;          // 目的地址
    uint16_t nbytes;            // 本次传输字节数
    uint8_t  src_offset;        // 源地址偏移
    uint8_t  dst_offset;        // 目的地址偏移
    uint32_t *next_tcd;         // 指向下一个TCD的指针
} tcd_t;

// 定义三个传输任务
tcd_t tcd1 = { .src_addr = &adc_val1, .dst_addr = &buf_a, .nbytes = 4, .next_tcd = &tcd2 };
tcd_t tcd2 = { .src_addr = &adc_val2, .dst_addr = &buf_b, .nbytes = 4, .next_tcd = &tcd3 };
tcd_t tcd3 = { .src_addr = &adc_val3, .dst_addr = &buf_c, .nbytes = 4, .next_tcd = NULL };

// 启动链式传输
DMA_StartChain(&tcd1);

我在一个多通道数据采集系统里用过链式模式。32个传感器,每个传感器采集100个点,用链式模式把32个DMA任务串起来。CPU只在所有任务完成后收一次中断。吞吐量比逐通道配置高了将近10倍。

注意:链式模式对内存对齐有要求。TCD描述符通常需要16字节或32字节对齐。我见过有人因为描述符没对齐,DMA加载TCD时直接触发总线错误。嗯,这个坑我踩过。

2.5 三种地址模式对比总结

为了方便你选型,我把三种模式的核心差异整理成一张表:

特性 线性模式 循环模式 链式模式
地址回绕 否 是(自动回起始) 否(跳转到下一描述符)
CPU干预频率 每次传输结束需重配 无需重配,持续运行 仅需配置链表头
适用场景 单次批量传输 流式数据(音频/视频) 复杂多段传输
内存开销 低(仅需缓冲区) 低(仅需缓冲区) 高(需额外TCD空间)
配置复杂度 低 中 高

2.6 本章知识结构图

下面这张图把DMA控制器的核心模块和它们之间的关系画出来了。你对照着看,思路会更清晰。

DMA控制器架构核心模块关系图 DMA控制器架构核心模块关系图 DMA控制器 DMA通道(Channel 0 ~ Channel N) 通道 0 通道 1 通道 2 ...... 传输粒度 字节(8-bit) 字(32-bit)| 突发(Burst) 地址模式 线性 | 循环 | 链式 请求与应答 DMA_REQ / DMA_ACK 外设接口层 UART | SPI | I2C | ADC | DAC | SDIO | 以太网 系统内存(SRAM / Flash / 外设寄存器) 说明:DMA控制器通过通道连接外设和内存,传输粒度和地址模式决定数据搬运方式,请求应答信号控制传输时机