2、DMA硬件架构:DMA控制器内部结构、通道与请求线、源地址与目的地址寄存器、传输计数器
好,咱们今天来聊聊DMA的硬件架构。说实话,很多做嵌入式开发的朋友,对DMA的理解就停留在“能搬数据”这个层面。但真要你调一个DMA驱动,或者排查一个数据错乱的问题,光知道它能搬数据是远远不够的。你得钻进它的内部,看看它到底是怎么工作的。
我个人习惯,在讲任何外设之前,先看它的框图。DMA控制器也不例外。你把它拆开来看,其实核心部件就那么几个:通道、请求线、地址寄存器、传输计数器。咱们一个一个来啃。
2.1 DMA控制器内部结构:一个多通道的搬运工
DMA控制器,说白了就是一个专门负责数据搬运的微型处理器。它不需要CPU去执行指令,它自己有一套状态机,能自动完成从源地址读数据,写到目的地址的整个过程。
它的内部结构,我习惯把它想象成一个“多路复用器”加上“多个独立的搬运通道”。
- 控制逻辑单元:这是大脑。负责解析配置、管理通道优先级、处理仲裁。
- 地址生成单元:每个通道都有自己的地址生成器。它能自动递增、递减或者固定地址。嗯,这里要注意,不同芯片的地址增减模式可能不一样,有的支持“回环模式”,有的不支持。
- 数据缓冲器:通常是FIFO(先进先出)。为什么需要FIFO?因为总线的速度和外设的速度可能不匹配。FIFO能起到一个“蓄水池”的作用,防止数据丢失。
- 中断控制单元:传输完成、传输错误、半传输完成,这些事件都会触发中断。我建议你在调试阶段,把“半传输完成中断”打开,这样你能看到数据搬了一半时的状态,对排查问题很有帮助。
核心要点:DMA控制器内部,每个通道都是独立的。这意味着你可以同时配置通道0做内存到内存的拷贝,通道1做UART的接收,通道2做ADC的采集。它们互不干扰,只要总线带宽够用。
2.2 通道与请求线:谁在叫DMA干活?
通道,你可以把它理解成DMA控制器里的一个“虚拟搬运工”。每个通道都有自己的寄存器组,包括源地址、目的地址、传输计数、控制配置等。
那“请求线”是什么?说白了,就是外设向DMA控制器喊话的“电话线”。
举个例子:UART接收到一个字节,它想把这个字节搬到内存里。UART不会直接去找CPU,它会拉高一根硬件信号线——这根线就是DMA请求线。DMA控制器检测到这根线被拉高,就知道“哦,UART有活干了”。
这里有个常见的坑,我曾经在调试一个I2S音频播放时遇到过:
我曾经把DMA的请求极性配置反了。外设是“高电平请求”,我配成了“低电平请求”。结果DMA一直不工作,音频就是不出声。查了两天才发现是寄存器里一个bit配反了。所以,一定要仔细看数据手册里关于请求信号极性的描述。
通道和请求线的映射关系,不同芯片差异很大。有的芯片是固定映射:比如通道0只能接UART1,通道1只能接SPI2。有的芯片是“可编程映射”:你可以把任意一个外设的请求线,连接到任意一个通道上。我个人更喜欢后者,灵活性高,布线也方便。
| 特性 | 固定映射 | 可编程映射 |
|---|---|---|
| 配置复杂度 | 简单,无需配置路由 | 稍复杂,需要配置请求选择寄存器 |
| 灵活性 | 低,外设必须用指定通道 | 高,可任意分配 |
| 典型芯片 | STM32F1系列 | STM32F4/H7系列、i.MX RT系列 |
2.3 源地址与目的地址寄存器:数据从哪来,到哪去?
这两个寄存器,是DMA配置里最核心的部分。你想想看,DMA要搬数据,它总得知道“从哪读”和“写到哪”吧?
源地址寄存器(Source Address Register)存放的是数据来源的地址。目的地址寄存器(Destination Address Register)存放的是数据要写往的地址。
这里有个关键点:地址的增长模式。
- 固定模式:地址不变。适用于外设的数据寄存器。比如UART的DR寄存器,你每次读它,它都是同一个地址。
- 递增模式:每次传输后,地址自动增加一个数据宽度(1字节、2字节、4字节)。适用于内存到内存的拷贝,或者内存到外设FIFO的批量传输。
- 递减模式:用得少,但在某些图像处理或栈操作中会用到。
我记得有一次,一个同事在做摄像头数据采集。他把源地址配成了递增,但摄像头的数据输出寄存器是固定的。结果每次DMA读完第一个像素后,地址就跑到下一个内存地址去了,读到的全是垃圾数据。这就是典型的“地址模式配错了”。
我的调试技巧:在配置完DMA后,先不要启动传输。用调试器读一下源地址寄存器和目的地址寄存器的值,确认它们指向了你期望的地址。这一步能帮你过滤掉90%的配置错误。
2.4 传输计数器:搬多少,心里要有数
传输计数器(Transfer Counter),也叫“传输长度寄存器”。它告诉DMA控制器:“这次你要搬多少个数据单元”。
注意,我说的是“数据单元”,不是“字节”。为什么?因为DMA可以配置不同的数据宽度:8位(字节)、16位(半字)、32位(字)。
举个例子:
- 如果你配置数据宽度为8位,传输计数器设为100,那么总共传输100字节。
- 如果你配置数据宽度为16位,传输计数器设为100,那么总共传输200字节。
- 如果你配置数据宽度为32位,传输计数器设为100,那么总共传输400字节。
这个计数器通常是递减的。每传输完一个数据单元,计数器就减1。当它减到0时,DMA就知道“活干完了”,然后可以触发完成中断,或者自动重新加载初始值(循环模式)。
这里有个容易忽略的点:源和目的的数据宽度可以不一致。比如,源是8位的(从ADC读数据),目的是32位的(写到内存缓冲区)。DMA控制器会自动进行数据打包或解包。但要注意,这种情况下,传输计数器是以“源”的数据宽度为准,还是以“目的”为准?不同芯片的实现不一样。我建议你查一下芯片手册里的“FIFO打包规则”。
避坑指南:我曾经在一个项目中,把传输计数器设成了0。你猜怎么着?DMA直接不工作,或者传输了2^32次(取决于芯片实现)。所以,永远不要将传输计数器初始化为0,除非你想触发一个未定义行为。
好了,DMA的硬件架构核心部件就这些。你掌握了通道、请求线、地址寄存器、传输计数器这四个概念,基本上就能看懂大部分DMA控制器的数据手册了。下一节,咱们会把这些部件串起来,看看一次完整的DMA传输流程到底是怎么走的。