1. DMA 基础概念:数据搬运的“隐形管家”

大家好,欢迎来到这门实战课的第一章。今天咱们聊聊 DMA——这个在嵌入式系统里默默干活、却经常被新手忽略的家伙。

DMA 的全称是 Direct Memory Access,直接存储器访问。说白了,它就是一个专门负责数据搬运的硬件模块。你想想看,CPU 本来要处理各种计算、控制逻辑,结果还得花大量时间把数据从外设搬到内存,或者从内存搬到外设——这多浪费啊。DMA 就是来解放 CPU 的。

1.1 什么是 DMA?

DMA 是一个独立于 CPU 的硬件引擎。它可以在不占用 CPU 核心的情况下,在内存和外设之间、或者内存的不同区域之间传输数据。

我刚开始做嵌入式开发时,对 DMA 的理解就是“一个能自己搬数据的东西”。后来在项目里踩过坑才明白,它的价值远不止于此。

核心要点:DMA 传输的发起者可以是软件(CPU 配置后启动),也可以是硬件(外设触发)。传输过程中,CPU 可以去做其他事情。

1.2 DMA 与 CPU 的数据搬运对比

咱们直接上表格,一目了然:

对比维度 CPU 直接搬运 DMA 搬运
占用核心 全程占用 CPU 仅配置和完成时占用
传输速度 受限于 CPU 指令执行速度 硬件级,通常更快
并发能力 无法同时处理其他任务 CPU 可并行处理其他任务
适用场景 小数据量、简单操作 大数据量、周期性传输
功耗 CPU 高负载运行,功耗高 专用硬件,功耗更低
编程复杂度 简单,直接读写寄存器 需要配置 DMA 控制器

举个例子。假设你要从一个 ADC 外设读取 1024 个采样数据。用 CPU 轮询的话,每个数据都要 CPU 去读寄存器、存内存——这期间 CPU 啥也干不了。用 DMA 的话,你配好源地址、目的地址、数据长度,然后 DMA 自己就把活干完了,CPU 可以去做 FFT 计算或者处理其他外设。

我的经验:在 STM32 上做音频采集时,我试过用 CPU 轮询 I2S 接口。结果 CPU 占用率飙到 80% 以上,系统几乎没法响应其他中断。换成 DMA 后,CPU 占用率直接降到 5% 以下。差距就是这么明显。

1.3 DMA 在嵌入式系统中的角色

DMA 在嵌入式系统里扮演什么角色?我总结了三句话:

  • 性能加速器:让 CPU 从繁重的数据搬运中解脱出来
  • 实时性保障:配合外设触发,实现低延迟的数据传输
  • 功耗优化器:CPU 可以更早进入休眠模式

你想想看,现在的嵌入式系统越来越复杂。传感器数据采集、显示刷新、网络通信、音频处理……哪个不需要搬数据?如果没有 DMA,CPU 就得疲于奔命。

我记得有个项目是做多通道数据采集系统,8 个 ADC 同时采样,每个通道 10kHz 的采样率。如果用 CPU 中断方式,每秒要处理 80,000 次中断——这还不算数据处理的时间。后来用 DMA 的循环模式和双缓冲技术,CPU 只需要每隔一段时间来处理一次完整的数据块。嗯,这才是正确的做法。

1.4 DMA 传输的基本流程

咱们用一张 SVG 图来展示 DMA 传输的典型流程:

DMA 传输典型流程 CPU 配置 DMA 设置源/目的地址 设置传输长度 启动 DMA 传输 传输完成 → 中断通知 配置内容 • 源地址(外设/内存) • 目的地址(内存/外设) • 传输长度(字节/半字/字) • 传输模式(单次/循环) 传输过程中 CPU 可以执行其他任务 DMA 独立完成数据搬运 完成处理 CPU 响应 DMA 中断 处理传输完成的数据

这个流程看起来简单,但实际项目中每个环节都有坑。比如配置源地址时,外设地址寄存器是否支持自增?目的地址是内存缓冲区,要不要考虑对齐?传输长度是字节还是字?

避坑提醒:我曾经在一个项目中,DMA 配置了循环模式,但忘记设置缓冲区大小与传输长度的匹配关系。结果数据覆盖了后面的内存区域,导致系统随机崩溃。排查了两天才找到问题。所以,DMA 配置一定要仔细核对地址和长度。

1.5 DMA 的传输模式

DMA 控制器通常支持以下几种传输模式:

  • 单次传输:传输一次后停止,需要重新配置才能再次启动
  • 循环传输:传输完成后自动重新开始,适合连续数据流
  • 双缓冲传输:两个缓冲区交替使用,一个用于 DMA 写入,一个供 CPU 处理
  • 链表传输:通过描述符链表实现多个不连续内存块的传输

我个人最常用的是循环模式和双缓冲模式。比如做音频播放,用循环模式让 DMA 持续往 DAC 送数据;做数据采集,用双缓冲模式让 CPU 处理一个缓冲区的同时,DMA 往另一个缓冲区写数据——这样就不会丢数据。

1.6 为什么在 Rust 中学习 DMA?

你可能会问:用 C 也能做 DMA 驱动,为什么非要用 Rust?

原因很简单:安全。DMA 操作涉及内存的直接访问,在 C 里很容易出现缓冲区溢出、野指针、数据竞争等问题。Rust 的所有权系统和借用检查器,能在编译时就帮你发现这些问题。

举个例子。在 C 里,你可能会不小心把 DMA 缓冲区的地址传给了另一个线程,然后两个地方同时读写——这就是数据竞争。在 Rust 里,编译器会强制你使用合适的同步原语,或者通过所有权转移来避免这种情况。

而且,Rust 的嵌入式生态越来越成熟。像 embedded-dma 这样的 crate,提供了类型安全的 DMA 抽象。咱们后面的章节会深入讲解。

本章小结:DMA 是嵌入式系统中不可或缺的组件。它通过硬件方式实现数据搬运,解放 CPU,提升系统性能和实时性。理解 DMA 的基本概念、传输流程和模式,是后续编写 Rust DMA 驱动的基础。