第2章 DMA控制器架构概述

说实话,很多人一上来就调中断延迟,却连DMA控制器长什么样都没搞明白。我带你从最核心的三个模块说起——源地址、目标地址,还有控制寄存器。搞懂这些,后面优化才有底气。

2.1 核心模块:源地址、目标地址与控制寄存器

每一个DMA传输,说白了就是“从哪里搬到哪里,搬多少,怎么搬”。这三个信息分别存在三个寄存器里:

  • 源地址寄存器(SRC):存放数据来源的地址,可以是内存地址,也可以是外设地址。
  • 目标地址寄存器(DST):存放数据要写到的位置。
  • 控制寄存器(CTL):定义传输方向、数据宽度、单次传输大小、是否产生中断等等。

我记得刚入行时调试一块带DMA的SoC,配置了SRC和DST,数据就是不动。查了半天发现控制寄存器的“增量模式”没设——外设地址固定,内存地址应该递增,我搞反了。结果数据都覆盖到同一个地址,整个缓冲乱了。所以配置时一定先确认地址是否自动递增。

关键点: DST和SRC寄存器通常支持每次传输后自动加减,减量模式虽然少用,但做环形队列时特别管用。

2.2 描述符与链表:灵活传输的基石

只用三个寄存器搞不定复杂场景。你想啊,一次传输1024字节,每字节触发一次中断?太浪费了。所以有了描述符(Descriptor)。

描述符是一个小结构体,把一次传输所需的SRC、DST、CTL、下一个描述符指针打包在一起。多个描述符串起来就是链表。每完成一个描述符,硬件自动取链表中下一个,继续传输,直到链表尾。

// 一个典型的DMA描述符结构(C语言)
typedef struct {
    uint32_t src_addr;      // 源地址
    uint32_t dst_addr;      // 目标地址
    uint32_t control;       // 控制位(宽度、中断使能等)
    uint32_t next_desc;     // 下一个描述符指针(NULL表示结束)
} dma_desc_t;

我曾经在一个音频采集项目里,用描述符链表把采样数据分成16块缓冲。DMA一块一块搬,搬完一块就触发一次“半缓冲中断”,主核在另一块里处理数据。延迟直接降到原来的一半。嗯,这里要注意:描述符必须对齐到16字节,有些控制器还要求描述符在专用的内存区域,否则会触发总线错误。

小技巧: 用链表做循环缓冲时,把最后一个描述符的next_ptr指向第一个,硬件就会一直循环下去。我常用的手法是初始化时把所有描述符串成环,再单独设一个“传输完成”中断标志寄存器。

2.3 常见DMA控制器架构对比

不同厂商的DMA控制器细节差别不小。下面这张表是我自己总结的,方便你选型时快速比对:

特性 基本寄存器型 描述符链表型 多通道/智能型
典型芯片 STM32F1 基础DMA NXP i.MX RT 系列 eDMA Xilinx Zynq AXI DMA
配置方式 每次传输前都要填寄存器 一次性构建描述符链表 硬件自动加载描述符,支持多通道分时
中断触发次数 每块传输都触发 可配置每N块或链表结束触发 每通道独立中断,可合并
适合场景 简单、低频、小传输 中等频率、多缓冲、流水处理 高带宽、多路并发、大数据量

避坑提醒: 我曾经在只用基本寄存器型的DMA上硬做环形缓冲区,每传输一字节就去重配寄存器,结果中断响应延迟比CPU轮询还高。后来换了描述符链表,一次配置完,中断次数降了两个数量级。所以选型时一定算清楚:你的传输频率和中断频率是否匹配。

DMA控制器核心模块与描述符链表关系 DMA控制器核心架构 CPU 内存(SRAM) 外设(UART/SPI) DMA控制器引擎 SRC寄存器 | DST寄存器 | CTL寄存器 描述符指针 + 链表遍历硬件 描述符链表(内存中) Desc0 → Desc1 → ... → DescN 总线 DMA传输 读取描述符 注意 描述符需对齐到16字节 链表尾指针标记为NULL

你看这张图,DMA控制器引擎内部握着三个关键寄存器,从内存的描述符链表中取任务,然后直接搬数据到外设或内存。整个过程CPU只需要在链表构建时干预一次,之后DMA自己跑,直到完成中断通知CPU。这个结构决定了中断响应延迟的底层上限——我们在后续章节会逐一拆解。

个人习惯: 我总会在初始化时把描述符链表的第一个描述符的地址写到DMA控制器的“链表基址”寄存器,同时使能“链表结束中断”。这样即使出了问题,也能通过观察描述符链表的遍历状态快速定位。