第2章:DMA控制器硬件架构:DMA通道、请求线、握手信号、传输粒度
好,咱们直接进入正题。DMA控制器,说白了就是个硬件搬运工。CPU把搬运的活交给它,自己该干嘛干嘛去。但你要让这个搬运工干活,得先搞清楚它的内部结构。
我个人习惯,看一个DMA控制器,先看四个东西:通道、请求线、握手信号、传输粒度。这四个搞明白了,驱动代码怎么写心里就有数了。
2.1 DMA通道——搬运工的专用车道
DMA通道,你可以想象成一条专用车道。每个通道独立工作,互不干扰。我见过最多的是8通道的DMA控制器,也有16通道的,甚至32通道的。
每个通道都有自己的一套寄存器。比如源地址寄存器、目的地址寄存器、传输计数寄存器、控制寄存器。嗯,这里要注意:不同厂家的DMA控制器,通道数量可能不一样,但基本思路是一样的。
关键点:通道是独立的,但共享总线。如果多个通道同时请求,仲裁器会决定谁先走。
我在项目中遇到过一个问题:两个通道同时往内存写数据,结果数据乱了。后来发现是通道优先级没配好。所以,多通道场景下,优先级配置一定要小心。
2.2 请求线——谁需要搬运服务?
DMA通道不会自己主动干活。得有外设告诉它:「嘿,我有数据要搬!」这个「告诉」的动作,就是通过请求线完成的。
请求线,说白了就是一根物理信号线。外设拉高这根线,DMA控制器就知道有活干了。举个例子:UART接收了一个字节,它拉高请求线,DMA控制器就把这个字节搬到内存里。
你想想看,如果外设没有请求线,DMA怎么知道什么时候该干活?轮询?那还不如CPU自己搬呢。
| 外设类型 | 请求线作用 | 典型场景 |
|---|---|---|
| UART | 接收数据就绪 | 串口接收中断替代 |
| SPI | 发送/接收缓冲区空/满 | 高速SPI数据传输 |
| ADC | 转换完成 | 连续采样数据搬运 |
| 定时器 | 定时触发 | 周期性数据搬运 |
我曾经调试过一个SPI DMA的问题,数据死活不对。查了半天,发现是请求线的极性配反了。外设拉高请求,DMA却等拉低。这种坑,踩过一次就记住了。
2.3 握手信号——一次完整的交易
请求线只是开始。一次完整的DMA传输,需要一套握手协议。就像两个人握手,得你伸我也伸,节奏要对。
典型的握手信号包括:
- DREQ(DMA Request):外设发出的请求信号
- DACK(DMA Acknowledge):DMA控制器给外设的应答信号
- DONE/TC(Terminal Count):传输完成信号
流程是这样的:外设拉高DREQ → DMA控制器看到请求 → 拉高DACK告诉外设「收到」 → 开始搬数据 → 搬完拉高DONE。
小技巧:有些DMA控制器支持「突发传输」,一次握手可以搬多个数据。这在高速外设上特别有用,比如高速ADC。
我记得有一次,客户说DMA传输偶尔丢数据。我抓了波形一看,发现DACK信号时序有问题。外设收到DACK后,数据还没准备好,DMA就开始读了。这种时序问题,示波器一抓一个准。
2.4 传输粒度——一次搬多少?
传输粒度,就是DMA一次操作搬多少数据。常见的有:
- 字节(8位):一次搬1个字节
- 半字(16位):一次搬2个字节
- 字(32位):一次搬4个字节
- 双字(64位):一次搬8个字节
为什么要有粒度?说白了,就是效率问题。你搬一个32位的数据,用字节粒度要搬4次,用字粒度一次搞定。但粒度不是越大越好,得看外设和内存的对齐要求。
警告:粒度不匹配会导致数据错位!比如外设是8位,你配成32位粒度,数据顺序就乱了。我见过有人把音频数据搬得乱七八糟,就是因为这个。
举个例子:一个8位的UART,你配成32位粒度。DMA一次读4个字节,但UART只准备好了1个字节,剩下的3个字节是垃圾数据。嗯,这种问题在调试时特别隐蔽。
我个人习惯,外设是什么位宽,DMA粒度就配成什么位宽。除非有特殊需求,否则别乱改。
2.5 实战中的坑与经验
讲了这么多理论,说点实战的。我在做嵌入式Linux驱动时,设备树里配DMA节点,经常遇到这些问题:
- 通道冲突:两个外设用了同一个DMA通道。设备树里配的时候,一定要确认通道号不重复。
- 请求线映射错误:外设的请求线没连到正确的DMA通道。这得看芯片手册的引脚复用表。
- 粒度不匹配:外设和DMA的粒度不一致,导致数据错位。
- 握手信号时序:DREQ和DACK的时序不满足外设要求。
避坑指南:我曾经因为DMA通道号配错,导致系统启动就死机。后来养成习惯,每次配DMA节点,先看芯片手册的DMA通道分配表,再写设备树。
最后说一句:DMA控制器的硬件架构,不同厂家差异很大。但通道、请求线、握手信号、传输粒度这四个概念,是通用的。搞懂了它们,换什么芯片都不怕。