2、DMA控制器架构与工作原理归纳总结
DMA(Direct Memory Access)说白了,就是让硬件自己搬数据,CPU只负责下指令。我刚开始做嵌入式时,总觉得CPU搬数据也挺快,直到一个项目里要同时处理摄像头图像和音频流,CPU被打到100%还掉帧……嗯,从那以后我再也不敢小看DMA了。
2.1 DMA控制器的核心组成
一个典型的DMA控制器,内部其实就是一个微型的状态机+数据通路。我个人习惯把它拆成几个模块看:
- 通道(Channel):每个通道对应一个独立的传输任务。有些芯片有8个通道,有些有16个。我记得第一次用STM32F4的DMA2,里面8个通道我都用满了,还要来回切换优先级。
- 请求仲裁器(Arbiter):多个外设同时请求DMA时,谁先谁后?仲裁器根据优先级决定。我在项目中遇到过两个高速外设抢通道,结果传输乱掉——后来把其中一个降了优先级才解决。
- 地址发生器(Address Generator):负责生成源地址和目标地址。支持递增、递减、固定三种模式。你想想看,如果外设FIFO只读一个固定地址,而内存地址必须递增,地址发生器就得灵活处理。
- 传输计数器(Transfer Counter):记录还剩多少数据没搬完。减到0时产生中断或自动重载。
- 控制/状态寄存器:配置传输方向、数据宽度、突发长度,以及查看当前状态、错误标志。
知识点:通道本身不存储数据,它只是“资源分配单元”。真正干活的是总线主控接口。
2.2 DMA传输的工作流程
一次完整的DMA传输,走下面这几步:
- 初始化:软件配置通道参数——源地址、目标地址、数据长度、方向、模式。
- 使能:启用DMA通道,等待外设请求或软件触发。
- 请求:外设(如UART、ADC)发出DMA请求信号。
- 仲裁:仲裁器判断是否允许当前通道占用总线。
- 传输:地址发生器和传输计数器配合,从源读取数据写到目标。总线宽度和字节对齐是坑,我后面说。
- 完成:计数器归零,产生传输完成中断(可选)。
我曾经在一个音频项目中,忘记配置传输完成中断,结果CPU不知道数据已就绪,一直读旧数据——听了一下午沙沙声才反应过来。
2.3 三种常见传输模式
| 模式 | 特点 | 典型应用 | 注意 |
|---|---|---|---|
| 单次传输(Basic) | 一次配置搬完固定长度的数据,搬完即止 | ADC单次采集、SPI一次性收发 | 每次都要重新配置,CPU开销较大 |
| 循环传输(Circular) | 搬完自动重置地址和计数器,无限循环 | 连续音频流、环形缓冲区 | 小心地址回卷时的数据覆盖 |
| 链式传输(Linked List / Descriptor) | 多个传输描述符链接,每个完成自动跳到下一个 | 复杂批量传输、双缓冲乒乓 | 描述符结构体需对齐到指定边界 |
我的经验:链式传输是双缓冲乒乓DMA的基石。你配置两个描述符,一个指向Buffer A,一个指向Buffer B,传输完成后自动切换——CPU几乎零介入。
2.4 关键参数与避坑指南
实操中容易踩的坑,我列几个:
- 地址对齐:DMA通常要求源地址和目标地址按数据宽度对齐。比如一次搬32位,地址必须是4的倍数。我曾经在某个MCU上没对齐,结果DMA搬出乱码,查了一下午手册才发现。
- 缓存一致性:如果DMA和CPU同时访问同一块内存,尤其在有Cache的Cortex-M7或A系列芯片上,必须做Cache维护(Clean/Invalidate)。我记得有一次没刷Cache,CPU看到的数据始终是旧的——典型的“缓存一致性问题”。
- 突发长度(Burst Length):设得太长可能导致总线占用太久,影响其他主设备;设得太短传输效率偏低。我一般根据外设FIFO深度来决定,比如FIFO有4个word,突发长度就设4。
- 传输完成中断频率:如果每传一个字节都中断,那还不如用CPU搬。所以通常配置成“块传输完成”或“半传输完成”中断——一半完成你开始处理,另一半DMA继续搬,这就是双缓冲的思想。
注意:在某些MCU上,DMA传输中修改配置是危险的。最好在禁用通道后再改,否则可能引起总线错误或传输错乱。
2.5 DMA控制器架构示意
下面这张图是我根据多个平台(STM32、NXP i.MX、TI C2000)总结的通用DMA控制器内部结构。你看一眼就能明白数据怎么走。
从图上你能看到:CPU只管配置寄存器,外设发出请求后,仲裁器决定哪个通道干活,地址发生器和传输控制器协同完成总线事务,最终把数据从外设搬到内存(或反向)。这个过程中,CPU可以继续跑其他任务——效率就是这么来的。
2.6 一个实际案例:SPI+DMA接收音频数据
我以前做过一个音频采集项目,用SPI读取ADC,数据率1.5Mbps。如果用CPU中断逐字节搬,系统直接被淹死。后来换成DMA:
// 伪代码演示SPI DMA配置
void spi_dma_init(void) {
// 1. 使能DMA时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
// 2. 配置DMA通道
DMA1_Stream0->PAR = (uint32_t)&SPI1->DR; // 外设地址:SPI数据寄存器
DMA1_Stream0->M0AR = (uint32_t)audio_buffer; // 内存地址
DMA1_Stream0->NDTR = BUFFER_SIZE; // 传输次数
DMA1_Stream0->CR = DMA_SxCR_CHSEL_0 // 通道选择
| DMA_SxCR_DIR_0 // 外设到内存
| DMA_SxCR_MINC // 内存地址递增
| DMA_SxCR_PSIZE_0 // 外设宽度8位
| DMA_SxCR_MSIZE_0 // 内存宽度8位
| DMA_SxCR_TCIE; // 传输完成中断
// 3. 使能DMA通道
DMA1_Stream0->CR |= DMA_SxCR_EN;
// 4. 使能SPI的DMA接收请求
SPI1->CR2 |= SPI_CR2_RXDMAEN;
}
注意:配置时一定要先配置DMA参数,最后打开外设的DMA请求。否则可能发生“请求先于准备”的竞态问题。我吃过这个亏,数据前几个字节全是0xFF。
总结一下:搞懂DMA控制器的工作原理,本质上就是搞懂“谁请求、谁仲裁、谁搬、谁完成通知”。把这四个角色搞清楚,后面学双缓冲乒乓就是水到渠成的事。