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控制器架构的核心就是通道、地址、宽度和突发。通道决定谁干活,地址决定从哪干到哪,宽度决定一次干多少,突发决定干多快。把这四个点吃透,DMA驱动就稳了。