DMA 硬件架构:控制器内部结构、通道与请求线、关键寄存器
做 DMA 驱动,第一件事不是翻手册看寄存器列表,而是先把硬件框图吃透。我见过不少同事上来就对着寄存器表一个个配,结果 DMA 要么不工作,要么传错地址。说白了,你得先知道 DMA 控制器长什么样,它的手脚(通道、请求线)连到哪里,脑子(寄存器组)怎么用。
这一章,我们就拆开 DMA 控制器的黑盒子,看看里面到底有什么。
2.1 DMA 控制器内部结构
一个典型的 DMA 控制器,内部大致可以分成这几个模块:
- 控制逻辑单元:核心大脑,负责解析配置、仲裁优先级、产生控制信号。
- 通道寄存器组:每个通道都有一组独立的寄存器,包括源地址、目的地址、传输计数、控制配置等。
- 地址生成器:根据配置的地址模式和传输宽度,自动计算下一次读写的地址。
- 数据缓冲/FIFO:临时存放从源端读出的数据,等待写入目的端。大小通常 4~32 字节。
- 仲裁器:当多个通道同时请求时,决定谁先获得总线控制权。
- 中断控制单元:传输完成、传输错误等事件触发中断给 CPU。
我早期调试一个音频 DMA 驱动时,发现数据总是有杂音。查了半天,原来是 FIFO 深度设置太小,加上总线仲裁延迟,导致数据断流。嗯,从那以后我每次看 datasheet,都会先确认 FIFO 大小和仲裁策略。
核心要点:DMA 控制器本质上是一个“智能搬运工”,它不需要 CPU 参与数据搬运,但需要 CPU 告诉它“从哪里搬、搬到哪里、搬多少、怎么搬”。
2.2 通道与请求线
通道是 DMA 控制器的基本工作单元。每个通道可以独立配置,服务于一个外设或一段内存传输。你想想看,如果只有一个通道,那所有外设都得排队等,效率太低了。所以芯片设计者会放多个通道,比如 4 个、8 个甚至 16 个。
每个通道都对应一条或多条请求线。外设(比如 UART、SPI、ADC)通过请求线告诉 DMA:“我有数据准备好了,快来搬!”
经验之谈:我建议你在看芯片手册时,先画一张“外设 → 请求线 → 通道”的映射表。很多坑都出在这里——比如你配了 UART 的 DMA 通道,但实际 UART 的请求线连的是另一个通道号。
请求线的类型通常有两种:
- 硬件请求:由外设硬件自动触发,比如 UART 接收 FIFO 非空时拉高请求信号。
- 软件请求:由 CPU 写寄存器手动触发,常用于内存到内存的拷贝。
我记得有一次调试一个 SPI 从设备的 DMA 接收,数据总是少几个字节。查了三天,最后发现是 SPI 的请求线极性配反了——外设请求是高电平有效,我配成了低电平。嗯,这种细节手册上往往只写一句话,但能让你折腾好几天。
2.3 源地址与目的地址寄存器
这两个寄存器是 DMA 传输的核心。源地址寄存器存放数据来源的地址,目的地址寄存器存放数据要写往的地址。
地址模式通常有几种:
| 模式 | 说明 | 典型场景 |
|---|---|---|
| 固定地址 | 每次传输后地址不变 | 外设数据寄存器(如 UART_DR) |
| 递增地址 | 每次传输后地址 + 传输宽度 | 内存缓冲区 |
| 递减地址 | 每次传输后地址 - 传输宽度 | 某些特殊 FIFO 或栈操作 |
| 循环/回环 | 地址到达边界后回到起始值 | 音频环形缓冲区 |
注意:源地址和目的地址的宽度必须与传输宽度匹配。比如你配了 32 位传输,但源地址没有 4 字节对齐,轻则传输错误,重则触发总线错误异常。我曾经在一个项目中因为结构体成员对齐问题,导致 DMA 从非对齐地址读数据,结果系统直接挂死。
2.4 传输计数寄存器
这个寄存器告诉 DMA:“这次总共要搬多少数据。” 每次完成一次传输(一个数据单元),计数寄存器就减 1。当减到 0 时,DMA 认为本次传输完成,可以触发中断或自动重载。
这里有个容易混淆的点:传输计数是数据单元个数,不是字节数。比如你配了传输宽度为 32 位(4 字节),计数设为 100,那么总共传输 400 字节。
我个人的习惯是,在驱动初始化时,把传输计数和传输宽度的关系用宏定义写清楚:
/* 传输宽度定义 */
#define DMA_WIDTH_BYTE 0
#define DMA_WIDTH_HALFWORD 1
#define DMA_WIDTH_WORD 2
/* 计算传输计数 */
#define DMA_BUF_SIZE 1024 /* 总字节数 */
#define DMA_WIDTH DMA_WIDTH_WORD
#define DMA_TRANSFER_COUNT (DMA_BUF_SIZE / (1 << DMA_WIDTH))
这样写,别人看代码时一目了然,也避免了自己算错。
传输计数寄存器还有一个常见特性——自动重载。当计数减到 0 时,如果使能了自动重载模式,硬件会自动把初始值重新加载进去,适合做周期性的数据传输,比如音频播放的 ping-pong 缓冲。
避坑指南:我曾经在做一个高速数据采集项目时,传输计数寄存器设成了 65535,结果发现 DMA 传输到一半就停了。查手册才发现,这个控制器的传输计数寄存器只有 16 位,最大只能设 65535。如果你要传输超过 65535 个数据单元,要么用链表/描述符模式,要么分多次启动 DMA。