当前位置: 首页 > news >正文

一文说清DMA存储器到外设传输工作原理

一文讲透DMA存储器到外设传输:从原理到实战

你有没有遇到过这样的场景?

在做一个音频播放项目时,为了让DAC输出连续的波形,你用定时器每几十微秒触发一次中断,CPU从中断里把下一个采样点写进DAC寄存器。结果系统一跑起来,CPU占用率直接飙到80%以上,UI卡顿、通信延迟,连个简单的按键响应都变得迟钝。

问题出在哪?不是你的代码写得不好,而是你在“用人扛沙袋”——明明可以靠传送带自动送料,却非得让工人一趟趟搬。

这个“传送带”,就是DMA(Direct Memory Access)

今天我们就来彻底搞清楚:当数据要从内存送到外设时,DMA到底是怎么工作的?它是如何解放CPU、实现高效传输的?


为什么需要DMA?

先回到那个音频例子。

假设你要播放一个44.1kHz采样率的音频,意味着每秒要向DAC写入44,100次数据。如果每次都要CPU亲自出手:

  • 每次中断至少消耗几十个时钟周期;
  • 频繁上下文切换带来额外开销;
  • CPU根本没时间干别的事。

这就像让你一边炒菜,一边每隔3毫秒去开门拿快递——饭还能做好吗?

而DMA的作用,就是把这个“拿快递”的任务交给门卫。你只负责告诉他:“这里有256个包裹,地址是DAC门口,按顺序送,送完叫我。” 然后就可以专心炒菜了。

关键价值一句话总结:

让CPU专注思考,让DMA负责跑腿。


DMA控制器是怎么干活的?

我们以STM32这类常见MCU为例,拆解一下DMA在“内存→外设”模式下的工作流程。

它不是魔法,而是一套精密的自动化流水线

想象一下工厂里的装配线:

  • 原材料放在某个货架上(内存缓冲区);
  • 成品接收口固定在一个工位(外设寄存器);
  • 传送带(DMA控制器)知道从哪取料、送到哪、送多少、怎么送。

这套系统要正常运转,必须提前设定好以下参数:

参数说明
源地址内存中数据起始位置,比如&buffer[0]
目标地址外设的数据寄存器地址,如&DAC->DHR12R1
数据宽度每次传8位、16位还是32位?需和外设匹配
地址增量源地址是否自动+1(是),目标地址是否+1(否)
传输数量总共传多少个数据单元
触发源谁说了算才能开始传?通常是外设发出请求

这些信息统称为DMA通道配置,由CPU在启动前设置好。

工作流程四步走

  1. 准备阶段
    CPU配置DMA通道:告诉它起点、终点、搬多少、怎么搬。就像给门卫发任务清单。

  2. 等待信号
    DMA进入待机状态,静静监听目标外设是否“准备好收货”。比如DAC完成上次转换后,会主动发出一个硬件信号:“我可以接下一个数据了!”

  3. 启动搬运
    一旦收到请求,DMA立刻接管总线控制权(通过总线仲裁),从内存读出一个数据,写入外设寄存器。整个过程不经过CPU。

  4. 循环执行直到结束
    每传一次,DMA自动递增源地址指针(比如指向buffer[1]),目标地址保持不变(始终是DAC的那个寄存器)。重复上述过程,直到所有数据传完。

最后,DMA可以发一个中断通知CPU:“活干完了。”


外设是如何“喊”DMA来帮忙的?

很多人误以为DMA是自己主动跑的,其实不然——它更像是一个听命行事的快递员。

真正发起动作的是外设本身

仍以DAC为例:

  1. DAC内部有一个数据保持寄存器(DHR),用于存放待转换的数字值;
  2. 当前数据完成D/A转换后,硬件逻辑检测到DHR可被重写;
  3. 此时,DAC自动拉高其DMA Request信号线;
  4. 这个信号连接到DMA控制器的请求输入端;
  5. DMA感知到请求,立即执行一次传输,将新数据写入DHR;
  6. 写完后DAC自动开始下一次转换,同时释放请求信号;
  7. 等转换再次完成,流程重复……

这就形成了一个闭环流水线:

[内存] → (DMA) → [DAC DHR] → [模拟输出] ↑_________| 完成反馈

整个过程完全由硬件驱动,无需软件干预,节奏精准、延迟极低。

✅ 小贴士:这种机制叫做硬件握手(Hardware Handshake),比软件轮询或中断更高效、更可靠。


支持哪些外设有资格“叫”DMA?

并不是所有外设都能发起DMA请求。只有那些高频交互、对实时性要求高的设备才配备这项能力。

常见的支持DMA的外设有:

外设应用场景
DAC音频输出、波形生成
ADC高速采样、传感器采集
SPI / I2C / USART大量数据收发
TIMPWM输出、编码器接口
SAI多通道音频传输

它们都有一个共同特点:需要持续不断地与内存交换数据

如果你查看芯片手册中的外设框图,会发现这些模块通常多了一条名为DMA_REQTXE/TXE_DMAEN的控制路径,专门用来对接DMA控制器。


实战代码:用DMA驱动DAC播放正弦波

下面我们看一段真实的STM32 HAL库代码,演示如何使用DMA将内存中的波形数据发送给DAC。

#include "stm32f4xx_hal.h" DAC_HandleTypeDef hdac; uint16_t sine_wave[256]; // 存储一个周期的正弦波样本 // DAC初始化 void MX_DAC_Init(void) { __HAL_RCC_DAC_CLK_ENABLE(); hdac.Instance = DAC; HAL_DAC_Init(&hdac); DAC_ChannelConfTypeDef sConfig = {0}; sConfig.DAC_Trigger = DAC_TRIGGER_NONE; // 不使用外部触发 sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1); } // 启动DMA传输 void Start_Audio_Playback(void) { // 填充正弦波数据(略) Generate_Sine_Wave(sine_wave, 256); // 启动DMA传输:内存 → DAC HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave, 256, DAC_ALIGN_12B_R); // 12位右对齐 }

这段代码的关键在于这一行:

HAL_DAC_Start_DMA(...)

它背后做了什么?

  1. 自动配置DMA通道;
  2. 设置源地址为sine_wave数组首地址;
  3. 设置目标地址为 DAC 的数据寄存器;
  4. 设置传输方向为内存→外设;
  5. 开启DMA请求使能;
  6. 启动第一个传输。

从此以后,只要DAC完成一次转换,就会自动请求下一个数据,DMA立即响应并送上新样本。整个过程CPU全程“躺平”。


高级技巧:双缓冲实现无缝播放

上面的例子只能播256个点,播完就停了。实际应用中我们希望连续播放,怎么办?

答案是:双缓冲(Double Buffer)或Ping-Pong缓冲

原理很简单:

  • 准备两块内存区域:Buffer A 和 Buffer B;
  • 初始DMA从A读数据;
  • 当A快传完时,DMA触发“半传输中断”;
  • CPU趁机填充B的数据;
  • 传完A后,DMA自动切换到B继续传;
  • 同时CPU填充A,准备下一轮;
  • 如此往复,形成无限循环。

这样就能做到边传边填,避免断音,特别适合音频流、视频帧等连续数据场景。

STM32的DMA控制器原生支持该功能,只需启用Circular ModeDouble Buffer Mode即可。


工程实践中必须注意的坑

再强大的技术,用不好也会翻车。以下是几个常见陷阱及应对策略:

1. 内存未对齐导致传输失败

某些DMA控制器要求源地址按数据宽度对齐。例如:

  • 32位传输 → 起始地址必须是4的倍数;
  • 否则可能触发总线错误或静默失败。

✅ 解法:使用编译器指令强制对齐

__attribute__((aligned(4))) uint16_t sine_wave[256];

2. Cache导致数据不一致(Cortex-M7/M4F等带缓存的芯片)

如果你在高速RAM中生成了数据,但Cache没刷新,DMA可能读到的是旧数据!

✅ 解法:手动清理Cache

SCB_CleanDCache_by_Addr((uint32_t*)&sine_wave, sizeof(sine_wave));

否则你会发现:明明写了新数据,DMA送出去的却是上周的……

3. 低功耗模式下DMA失效

进入Stop模式后,主时钟关闭,DMA和外设也都歇菜了。

✅ 解法:选择合适的唤醒源,或使用低功耗定时器+DMA组合。

4. 忘记开启DMA时钟

很基础,但也最容易犯错。

✅ 解法:检查RCC配置,确保DMA时钟已使能

__HAL_RCC_DMA1_CLK_ENABLE(); // 根据所用通道选择

5. 外设未开启DMA请求

即使DMA配好了,如果外设没打开DMA输出使能,照样没人“叫门”。

✅ 解法:确认外设侧也开启了DMA请求

DAC->CR |= DAC_CR_DMAEN1; // 手动置位DMA使能位

它不只是“搬运工”,更是系统性能的放大器

DMA的价值远不止省点CPU那么简单。它带来的是一种系统级的能力跃迁

对比项中断方式DMA方式
CPU占用高(频繁中断)极低(仅初始化/结束)
数据抖动明显(中断延迟)极小(硬件同步)
最大吞吐受限于中断响应速度接近总线极限
实时性
功耗表现差(频繁唤醒)优(长时间休眠)

特别是在以下场景中,DMA几乎是唯一可行方案:

  • 🔊 音频回放/录音(>16kHz采样率)
  • 📹 图像传感器数据采集
  • 📡 高速串口通信(如UART 1Mbps以上)
  • 🎛️ 精确PWM波形生成(如电机控制)

没有DMA,这些应用要么无法实现,要么成本极高。


结语:掌握DMA,才算真正入门嵌入式系统设计

当你第一次成功用DMA驱动DAC输出平稳的正弦波,而CPU占用率几乎为零时,你会有一种“打通任督二脉”的感觉。

这不是简单的功能实现,而是一种思维方式的转变:

不再把CPU当作万能调度中心,而是把它视为系统的决策大脑,把重复性劳动交给专用硬件去完成。

DMA正是这种思想的最佳体现之一。

未来随着边缘计算、实时AI推理、多传感器融合的发展,设备内部的数据流动只会越来越复杂。届时,不仅要用好DMA,还要学会协调多个DMA通道、优化传输优先级、甚至使用链表式DMA(LLI)构建动态数据流。

但一切的基础,都始于理解清楚:数据是如何从内存安静地流向外设的。

如果你正在学习嵌入式开发,不妨现在就动手试一试:写一个数组,用DMA把它送到DAC或SPI,看看示波器上的波形是否稳定流畅。

那一刻,你会真正体会到——什么叫“让硬件为自己工作”。

欢迎在评论区分享你的DMA实战经验,或者提问踩过的坑,我们一起交流进步。

http://www.jsqmd.com/news/140620/

相关文章:

  • 从ADB到fastboot:驱动切换机制图解说明
  • 图解说明电路板PCB设计基本步骤(适合零基础)
  • 多线程竞争资源导致crash的通俗解释
  • 通过OpenMV实现农作物计数:快速理解方案
  • Dify平台主题与UI自定义能力:打造品牌专属界面
  • nmodbus零基础教程:一步步实现寄存器读取
  • DUT接口匹配技术详解:手把手教程(从零实现)
  • Dify平台能否替代传统后端开发?边界在哪里?
  • nanopb + MQTT 在嵌入式端的整合示例
  • Dify与Zapier类工具集成前景:无代码自动化再升级
  • Dify平台能否用于舆情监控?新闻聚合与情感分析实践
  • Dify与LangChain对比:谁更适合企业级AI应用开发?
  • 为工业4.0赋能:Vivado注册2035系统级设计全面讲解
  • 一文说清USB3.0主机控制器工作模式
  • Dify平台缓存机制详解:减少重复Token调用降低成本
  • Packet Tracer汉化全面讲解:支持语言包加载方法
  • 基于OpenMV的实时人脸识别完整指南
  • Dify平台社区活跃度分析:开源力量推动AI平民化
  • MicroPython与云平台通信项目应用实例
  • Dify在金融领域的应用尝试:自动化报告生成系统搭建
  • Dify + GPU集群:构建高并发AI服务的终极解决方案
  • 物品协同过滤实战案例:从零实现推荐引擎
  • Dify平台国际化支持现状与多语言处理能力评估
  • Dify平台数据导出功能评测:便于后续分析与审计
  • Dify平台冷启动问题解决方案:首次加载优化建议
  • Dify如何支持多租户架构?SaaS化部署可行性探讨
  • 2025年云南大学计算机考研复试机试真题(附 AC 代码 + 解题思路)
  • 基于SpringBoot+Vue的金帝豪斯健身房管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • 零基础学习如何在Multisim14中绘制原理图
  • 教学资源共享平台信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】