3. DMA控制器架构:通道、请求映射与传输配置

DMA控制器,说白了就是个硬件搬运工。但它不是傻搬,你得告诉它:从哪搬、搬到哪、搬多大块、什么时候搬。我刚开始接触DMA时,总觉得配置起来很繁琐,后来摸清了它的架构,才发现其实就那么几件事。

这一节,我们重点拆解DMA控制器的核心组成。你搞懂了通道映射、地址配置、传输宽度和突发传输,DMA驱动就拿下了一半。

3.1 DMA通道与请求映射

先说说通道。DMA控制器内部有多个通道,每个通道独立处理一条数据传输任务。比如STM32的DMA2有7个通道,每个通道可以绑定一个外设请求。

通道是什么? 你可以把它想象成一条专用车道。每个通道有自己的源地址寄存器、目的地址寄存器、传输计数器和控制配置。通道之间互不干扰,可以同时跑不同的传输任务。

请求映射呢? 外设想用DMA,得先发个请求信号。这个请求信号要路由到哪个通道,就是映射关系。我见过不少新手直接把外设请求随便挂到一个通道上,结果数据错乱——因为通道优先级和请求触发时机没配对。

核心要点: 每个外设的DMA请求都有固定的映射关系。比如USART1_TX通常映射到DMA2的通道7。查芯片参考手册的DMA请求映射表,是第一步。

举个例子,我曾在某款MCU上调试SPI DMA传输。SPI的TX请求默认映射到DMA1的通道3,但我为了省事,把它改到了通道5。结果SPI死活不发数据。查了半天,发现通道5的请求线根本没连到SPI模块。嗯,硬件映射是固定的,软件改不了。

外设请求 DMA控制器 通道 说明
USART1_TX DMA2 通道7 发送数据请求
USART1_RX DMA2 通道5 接收数据请求
SPI1_TX DMA2 通道3 SPI发送请求
ADC1 DMA2 通道0 ADC转换完成请求

个人习惯: 我在做HAL层封装时,会把通道映射表做成一个结构体数组。外设ID作为索引,直接查表拿到通道号和DMA控制器编号。这样上层驱动不用关心底层映射,改起来也方便。

3.2 源地址与目的地址配置

地址配置是DMA的核心。你得告诉DMA:数据从哪读,写到哪去。这里有两个关键点:地址递增模式和地址宽度对齐。

地址递增模式:

  • 递增模式: 每次传输后,地址自动加1(或加传输宽度)。适合内存到内存、内存到外设的连续数据块。
  • 固定模式: 地址不变。适合外设到内存,比如ADC采集,每次数据都写到同一个寄存器,但内存地址要递增。

我遇到过一个问题:用DMA从USART接收数据,目的地址设成了递增模式,但USART的数据寄存器地址是固定的。结果每次接收,DMA都从同一个外设地址读数据,但写到内存的不同位置。这没问题。但反过来,如果源地址是递增,目的地址固定,那就成了从内存不同位置读数据,写到同一个外设寄存器——这通常用于发送数据块。

我曾经踩过的坑: 配置源地址和目的地址时,没注意数据宽度对齐。比如源地址是8位数据,目的地址是32位数据,DMA会按32位搬运,导致数据错位。记住:源和目的的数据宽度必须一致,或者DMA支持自动打包/解包。

地址对齐规则:

  • 8位传输:地址可以是任意地址
  • 16位传输:地址必须2字节对齐
  • 32位传输:地址必须4字节对齐

你想想看,如果源地址是0x20000001,你配置了32位传输,DMA会尝试从非对齐地址读4字节。有些MCU会触发硬件错误,有些则默默读错数据。所以,我建议在驱动初始化时加一个地址对齐检查。

// 地址对齐检查示例
if ((uint32_t)src_addr % transfer_width != 0) {
    // 报告错误,或者做对齐处理
    return HAL_ERROR;
}

3.3 传输宽度与突发传输

传输宽度,就是DMA一次搬运多少字节。常见的有8位、16位、32位。突发传输,则是连续搬运多个数据单元,中间不释放总线。

传输宽度选择:

  • 8位: 适合字节流,比如UART、I2C
  • 16位: 适合半字数据,比如音频采样
  • 32位: 适合整字数据,比如图像处理、内存拷贝

突发传输: 说白了就是一次申请总线,连续搬好几笔数据。突发长度可以是4、8、16个数据单元。突发传输的好处是减少总线仲裁开销,提高吞吐量。

我做过一个测试:用DMA搬运1KB数据,单次传输模式耗时约120μs,而16字突发传输只用了45μs。性能提升很明显。但突发传输也有代价——它会长时间占用总线,影响CPU和其他外设的访问。

传输模式 总线占用 吞吐量 适用场景
单次传输 低 低 实时性要求高,外设频繁请求
4字突发 中 中 一般数据流
8字突发 高 高 大数据块,如LCD刷新
16字突发 极高 极高 内存拷贝,图像处理

我的建议: 突发长度不是越大越好。如果系统中有多个高优先级外设(比如USB、以太网),突发太长会导致其他外设响应延迟。我一般从4字突发开始调,用示波器看总线占用情况,再逐步增加。

配置突发传输时,还要注意FIFO深度。DMA控制器内部有个FIFO,用来暂存数据。突发长度不能超过FIFO深度,否则数据会溢出。比如FIFO深度是4字,你配了8字突发,那前4个字存进FIFO,后4个字还没地方放,就丢了。

// DMA配置示例(STM32 HAL库风格)
DMA_InitTypeDef dma_config;
dma_config.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;   // 外设8位
dma_config.MemDataAlignment     = DMA_MDATAALIGN_BYTE;   // 内存8位
dma_config.Mode                 = DMA_NORMAL;            // 普通模式
dma_config.Priority             = DMA_PRIORITY_HIGH;
dma_config.FIFOMode             = DMA_FIFOMODE_ENABLE;
dma_config.FIFOThreshold        = DMA_FIFO_THRESHOLD_FULL; // FIFO满触发
dma_config.MemBurst             = DMA_MBURST_INC4;       // 内存侧4字突发
dma_config.PeriphBurst          = DMA_PBURST_INC4;       // 外设侧4字突发

这里要注意:外设侧突发和内存侧突发可以不同。比如外设是慢速SPI,用单次传输;内存是快速SRAM,用8字突发。DMA会自动协调两边的速率。

3.4 DMA传输流程总览

说了这么多,我们来画个图,把整个DMA传输流程串起来。

DMA传输流程与配置关系图 DMA传输流程与配置关系 外设请求触发 通道映射与优先级 源/目的地址配置 传输宽度 & 突发 传输完成 / 中断 配置要点 • 查手册确认映射 • 优先级避免冲突 • 地址对齐检查 • 递增/固定模式 • 宽度匹配 • 突发长度 ≤ FIFO • 总线占用评估 • 中断使能配置 • 错误处理回调 • 传输完成标志 • 半传输中断 • 传输错误中断 • FIFO错误处理 • 直接模式 vs FIFO

这个图把DMA传输的五个关键步骤串起来了。从外设请求触发,到通道映射,再到地址配置、传输参数设置,最后传输完成。每一步都有对应的配置要点,我在右侧列出来了。

实际开发中,我习惯先配通道映射和优先级,再配地址,最后调传输参数。顺序别搞反了,否则调试时很难定位问题。

总结一下: DMA控制器架构的核心就是通道、地址、宽度和突发。通道决定谁干活,地址决定从哪干到哪,宽度决定一次干多少,突发决定干多快。把这四个点吃透,DMA驱动就稳了。