1. DMA 基础概念与工作原理

咱们这门课,上来就得聊清楚一个东西——DMA。

DMA,全称 Direct Memory Access。说白了,就是给 CPU 找了个专职的数据搬运工。

我刚开始做嵌入式的时候,老觉得这玩意儿可有可无。直到有一次做高速数据采集,CPU 被中断折腾得够呛,我才意识到:没有 DMA,很多活儿根本干不了。

1.1 什么是 DMA?

CPU 平常忙着跑操作系统、做算法、响应中断。但数据搬运这种体力活——把外设的数据拷到内存,或者从内存搬到外设——如果都让 CPU 一手包办,那它就没空干正事了。

DMA 就是专门干这个的。

举个例子:ADC 采样完 1024 个点,如果不走 DMA,CPU 得一个一个字节去读。如果走 DMA,CPU 只需要告诉 DMA:「嘿,帮我把这批数据搬到内存地址 0x20001000,搬完通知我。」然后 CPU 就可以去忙别的事了。

你琢磨琢磨,效率差出多少。

我的习惯:凡是外设数据传输量超过 16 字节,我基本都会考虑 DMA。尤其是定时采样、音频流、视频帧这类实时性要求高的场景,没有 DMA 基本跑不动。

1.2 DMA 的工作原理

那 DMA 到底是怎么干活的?我画个流程图帮大家理解。

DMA 传输流程——从发起请求到完成任务 CPU 配置 DMA 源/目的地址 + 传输计数 外设发起 DMA 请求 DREQ 信号置高 DMA 控制器仲裁 优先级裁决 + 总线仲裁 从源地址读取数据 外设 FIFO / 内存 写入目的地址 内存 / 外设数据寄存器 传输计数 - 1 比较是否传输完成 传输完成 → 发中断 通知 CPU 数据处理完毕 关键:CPU 只参与启动和结束 搬运过程完全由 DMA 控制器完成

1.3 关键角色

一个完整的 DMA 传输,有四个关键角色。

角色 干什么的 我踩过的坑
DMA 控制器 传输过程的「大脑」,负责仲裁、地址生成、传输控制 不同芯片的 DMA 控制器差异很大,移植代码时别想当然
源地址寄存器 存放数据来源的地址。可以是内存地址,也可以是外设地址 外设地址通常是固定的,但内存地址别忘了设置自增模式
目的地址寄存器 数据要送到哪里去 目的和源的宽度要匹配,否则数据会错位
传输计数寄存器 记录还要搬多少数据。每次传输完一个单元,自动减 1 有些芯片计数到 0 会自动 reload,有些不会 —— 别搞混
控制寄存器 配置模式、优先级、中断使能等 优先级设得太高,可能把 CPU 总线访问饿死,我吃过这个亏

1.4 DMA 传输的几种模式

不同场景下,DMA 的工作模式也不一样。我总结一下常见的三种:

  1. 单次传输(Single Transfer)
    每次 DMA 请求只传输一个数据单元。适合数据量小、间隔不固定的场景。比如按键扫描、外部中断触发。
  2. 块传输(Block Transfer)
    一次请求传输一整块数据,直到计数完毕才释放总线。适合大数据量连续传输,比如文件读写、图像数据搬运。
  3. 请求传输(Demand Transfer)
    外设通过信号持续请求,DMA 就一直传输,直到外设撤销请求或计数完毕。适合 FIFO 型外设,比如 UART 接收。

避坑指南:我之前在一个项目中,用块传输做 SPI Flash 读取。结果外设还没准备好数据,DMA 就一股脑开始搬了,读到一堆乱码。后来改成请求传输模式,加上外设的「数据就绪」信号握手,问题就解决了。

1.5 几种常见的 DMA 架构

不同芯片厂商的 DMA 实现不太一样。我挑三个有代表性的说说:

架构 特点 典型芯片
简单 DMA 只有一个 DMA 通道,每次只能处理一个传输任务。资源简单,但效率有限 早期 ARM7、8051 扩展 DMA
多通道 DMA 多个通道并行工作。每个通道有独立的源/目的/计数配置。这是主流方案 STM32 系列 (DMA1/DMA2)
链表 DMA(Scatter-Gather) 用链表描述多个不连续的内存块。DMA 自动按链表顺序完成所有传输。特别适合网络包处理 i.MX RT 系列、Xilinx Zynq

你想想看,如果你的应用要同时从 ADC 采数据、往 DAC 送波形、还要和 PC 通过 USB 通信——那单通道 DMA 根本不够用。这时候就得选多通道甚至链表 DMA。

1.6 传输数据宽度与对齐

这里有个细节很多人容易忽视:数据宽度。

DMA 一次传一个字节(8 位)、两个字节(16 位)、还是四个字节(32 位)?

  • 源宽度:外设数据寄存器的位宽。比如 UART 一般是 8 位,SPI 可能是 8/16 位。
  • 目的宽度:内存地址的位宽。一般是 32 位。
  • 对齐问题:如果源是 8 位、目的是 32 位,DMA 控制器会怎么处理?有的自动打包,有的会报错。

我的建议:除非特别清楚芯片的行为,否则尽量让源宽和目的宽保持一致。不一致时,先看芯片手册里对「字节重组」的描述。我遇到过一款芯片,8 位到 32 位传输时,数据放到了错误的内存偏移,排查了一整天。

1.7 中断与回调

DMA 传输完成后,通常会触发中断。

CPU 在中断服务程序里完成后续操作:比如处理数据、开启下一轮传输、或者唤醒等待的任务。

写 ISR 的时候有个原则:能干的活尽量少。

举个例子:DMA 搬完 1024 个采样点。ISR 里就干一件事——设置一个标志位。真正的数据处理放到主循环或任务里去做。这样能减少中断延迟,也避免在 ISR 里调耗时函数。

// 伪代码示例:DMA 传输完成中断
void DMA_IRQHandler(void) {
    if (DMA_GetITStatus(DMA1_FLAG_TC)) {
        // 1. 清除中断标志
        DMA_ClearITPendingBit(DMA1_FLAG_TC);
        
        // 2. 设置标志位(别在这做复杂处理)
        dma_transfer_done = 1;
        
        // 3. 如果有环形缓冲,可以重新加载配置
        // DMA_Config(...);
    }
}

// 主循环中处理数据
void main_loop() {
    while (1) {
        if (dma_transfer_done) {
            dma_transfer_done = 0;
            process_data(buffer, 1024);  // 真正的处理放在这里
        }
        // ... 其他任务
    }
}

这套路我用在好几个项目里,从来没出过问题。

1.8 本章小结

DMA 其实不复杂。就是让专门的硬件去搬数据,CPU 腾出手来干正事。

但实际项目中,每款芯片的 DMA 轮子都不太一样。踩过几次坑,你就知道关键在哪里了:地址配置、宽度匹配、优先级、中断处理——把这几个点抓住,DMA 就能用得顺溜。