第2章:DMA硬件架构:DMA控制器内部结构、通道与请求信号、地址与计数寄存器详解

大家好,我是你们的驱动开发讲师。今天咱们来聊聊DMA的硬件架构。说实话,很多做驱动的朋友,写DMA代码时都是照着参考手册抄,出了问题就抓瞎。我个人觉得,不理解硬件结构,写出来的驱动就像在走钢丝。

这一章,我会把DMA控制器的内部结构、通道机制、请求信号,还有那些让人头疼的地址和计数寄存器,掰开了揉碎了讲清楚。嗯,咱们开始吧。

2.1 DMA控制器内部结构:核心组件拆解

一个典型的DMA控制器,内部其实没那么复杂。我习惯把它想象成一个「智能搬运工」。它有几个核心部件:

  • 控制逻辑单元:大脑,负责解析CPU发来的指令,协调各个部件工作。
  • 状态寄存器:记录当前DMA的工作状态,比如忙不忙、有没有出错。
  • 地址寄存器组:存放源地址和目标地址。这个后面会细讲。
  • 计数寄存器组:记录还要传输多少数据。
  • 通道仲裁器:当多个DMA通道同时请求时,决定谁先干活。

核心要点:DMA控制器本质上是一个「专用处理器」。它不需要CPU干预,自己就能完成内存到外设、内存到内存的数据搬运。

我在项目中遇到过一种情况:某款芯片的DMA控制器内部没有FIFO,导致每次传输都要等待总线仲裁。后来我们换了一款带深度FIFO的控制器,性能直接翻倍。所以,选型时一定要看内部缓冲大小。

2.2 DMA通道:独立的数据搬运工

DMA通道,说白了就是控制器内部的一条「专用数据通路」。每个通道都有自己的寄存器组,可以独立配置。

为什么需要多个通道?你想想看,系统里可能有多个外设同时需要DMA服务。比如音频播放要DMA,网卡收包也要DMA。如果只有一个通道,那就得排队,实时性根本保证不了。

我建议你记住以下几点:

  • 通道优先级:高优先级通道可以打断低优先级通道的传输。我在调试一个视频采集驱动时,就因为音频通道优先级设得太高,导致视频帧丢失。后来把视频通道优先级调高一级,问题就解决了。
  • 通道绑定:有些DMA控制器,通道和外设是固定绑定的。比如通道0只能给UART用,通道1只能给SPI用。这种设计虽然简单,但不够灵活。
  • 通道复用:更高级的控制器支持通道复用,你可以把任意外设的请求信号映射到任意通道上。这给驱动开发带来了很大便利。

实战技巧:配置DMA通道时,记得先复位通道状态寄存器。我曾经因为忘记这一步,导致DMA启动后一直报「传输完成」中断,但实际上数据根本没动。排查了整整一个下午。

2.3 请求信号:DMA的「启动按钮」

DMA传输不会自己开始,它需要一个触发信号。这个信号就是DMA请求。

请求信号分两种:

  1. 硬件请求:由外设直接发出。比如UART接收到一个字节,就会拉高DMA请求引脚。DMA控制器检测到信号后,开始传输。
  2. 软件请求:由CPU写寄存器触发。这种模式常用于内存到内存的拷贝,或者测试场景。

我记得有一次,客户反馈说DMA传输偶尔会漏数据。我排查了很久,最后发现是外设的请求信号脉宽太短,DMA控制器没来得及采样到。解决方案很简单:在DMA控制器里配置信号滤波,或者让外设延长请求信号的持续时间。

避坑指南:我曾经在调试一个高速ADC驱动时,发现DMA传输总是丢包。查了三天,最后发现是ADC的DMA请求信号和时钟域不同步。解决方案是在DMA控制器里使能「请求信号同步」功能。这个坑,大家一定要记住。

2.4 地址寄存器:数据从哪来,到哪去

每个DMA通道都有两个地址寄存器:

  • 源地址寄存器(SAR):存放数据来源的地址。
  • 目标地址寄存器(DAR):存放数据要去的地址。

地址模式有几种:

模式 说明 典型应用
固定地址 地址不变,每次传输都访问同一个地址 外设数据寄存器(如UART DR)
递增地址 每次传输后地址自动加1、2或4 内存缓冲区
递减地址 每次传输后地址自动减1、2或4 栈操作、FIFO

我个人习惯把源地址设为递增模式,目标地址设为固定模式——这是最常用的「外设到内存」传输。反过来,如果是「内存到外设」,那就源地址固定,目标地址递增。

嗯,这里要注意:地址对齐问题。很多DMA控制器要求源地址和目标地址必须按传输宽度对齐。比如传输32位数据,地址必须是4字节对齐。如果不对齐,轻则性能下降,重则触发总线错误。

2.5 计数寄存器:还差多少没搬完

计数寄存器(通常叫NDT或CNDT)记录的是「还要传输多少数据」。它有两种工作模式:

  • 递减模式:每传输一次,计数器减1。减到0时,触发传输完成中断。
  • 循环模式:计数器减到0后自动重载初始值,继续传输。这种模式常用于音频播放、数据采集等需要连续传输的场景。

关键点:计数寄存器的初始值,决定了单次DMA传输的数据量。比如你要传输1024个字节,传输宽度是32位(4字节),那计数器的初始值就是256(1024/4)。

我在项目中遇到过一个问题:某个外设的FIFO深度只有8个字节,但我把DMA的传输长度设成了1024字节。结果外设的FIFO溢出,数据全丢了。后来我把传输长度改成和外设FIFO深度匹配,再配合DMA的「单次触发传输」模式,问题才解决。

2.6 实战:配置一个典型的DMA传输

说了这么多理论,咱们来点实际的。下面是一个典型的DMA初始化代码片段(伪代码):

// 配置DMA通道0
DMA_ChannelTypeDef *ch = &DMA1->Channel0;

// 1. 关闭通道
ch->CCR = 0;

// 2. 设置源地址(外设数据寄存器地址)
ch->CPAR = (uint32_t)&USART1->DR;

// 3. 设置目标地址(内存缓冲区地址)
ch->CMAR = (uint32_t)rx_buffer;

// 4. 设置传输数量(256个半字,即512字节)
ch->CNDTR = 256;

// 5. 配置控制寄存器
ch->CCR = DMA_CCR_MINC      // 内存地址递增
      | DMA_CCR_PSIZE_0      // 外设数据宽度:16位
      | DMA_CCR_MSIZE_0      // 内存数据宽度:16位
      | DMA_CCR_CIRC         // 循环模式
      | DMA_CCR_TCIE         // 传输完成中断使能
      | DMA_CCR_EN;          // 使能通道

// 6. 使能外设的DMA请求
USART1->CR3 |= USART_CR3_DMAR;

这段代码看起来简单,但有几个细节我提醒一下:

  • 配置顺序很重要:先配地址和计数,最后才使能通道。我曾经反过来写,结果DMA一启动就用了错误的地址。
  • 中断标志位一定要在使能中断前清除。否则一使能就会触发假中断。
  • 循环模式下,传输完成后计数器会自动重载。但如果你在传输过程中修改了计数器值,可能会引发不可预知的行为。

调试技巧:如果DMA传输不正常,先检查地址寄存器和计数寄存器的值。用调试器读一下CPAR、CMAR和CNDTR,看看是不是你期望的值。我80%的DMA问题,都是靠这招定位的。

2.7 小结

好了,这一章的内容就到这里。咱们回顾一下:

  • DMA控制器内部有控制逻辑、状态寄存器、地址/计数寄存器、通道仲裁器。
  • 每个通道是独立的数据通路,支持优先级和复用。
  • 请求信号分硬件和软件两种,注意信号同步问题。
  • 地址寄存器决定数据流向,计数寄存器决定传输长度。

下一章,我会讲DMA传输模式的选择——单次传输、块传输、链表传输,到底什么时候该用哪种?咱们到时候见。