news 2026/10/13 12:27:01

DMA读写操作硬件流程:手把手教程(零基础)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA读写操作硬件流程:手把手教程(零基础)

从零开始搞懂DMA:让CPU“解放双手”的数据搬运术

你有没有遇到过这种情况——系统里接了个高速ADC,采样率一上来,CPU瞬间被中断“淹死”,连个简单的LED都闪不动了?或者想做个音频播放器,结果PCM数据还没处理完,下一帧就已经来了?

这其实是很多嵌入式开发者都会踩的坑。而解决这类问题的关键,并不是换颗更快的芯片,而是学会用好一个“隐形助手”:DMA(Direct Memory Access)。

今天我们就抛开晦涩术语和复杂框图,手把手带你走一遍DMA读写操作的真实硬件流程。哪怕你是零基础,也能看懂它是怎么让CPU“躺平”、自己默默搬完几万字节数据的。


为什么我们需要DMA?

先来想一个问题:如果没有DMA,CPU是怎么把外设的数据存到内存里的?

比如ADC完成一次转换后,数据存在它的寄存器里。传统做法是:

  1. CPU不断轮询或等待中断;
  2. 中断触发后,CPU跳转到服务程序;
  3. CPU从ADC寄存器读出一个值;
  4. CPU把这个值写进内存数组;
  5. 计数+1,判断是否结束……

听起来不难对吧?但当你需要每秒采集10万次数据时,就意味着CPU要响应10万次中断——每次中断至少几十条指令开销,光是进出中断上下文就能耗掉大半性能。

更糟的是,这些时间本该用来做控制算法、通信协议、用户交互等真正重要的事。

于是,工程师们就想:能不能让这个“搬砖”的活儿交给别人干?只要告诉它“从哪搬到哪,搬多少”,剩下的让它自己去跑?

DMA 就是为此而生的专职“搬运工”。


DMA到底是个啥?一句话讲清楚

DMA 是一块独立的硬件电路,它可以在不打扰CPU的情况下,自动把数据从一个地方搬到另一个地方。

这里的“地方”通常是:
- 外设的数据寄存器(如 ADC_DR、SPI_DR)
- 内存中的某个缓冲区(如 SRAM 里的数组)

整个过程就像快递员取件送货:外设说“我有货了!”→ DMA 接单 → 自动取走数据 → 存进指定地址 → 完事后打个报告。

CPU只需要在开头下个单、结尾签个收,中间完全可以去睡觉。


硬件层面发生了什么?三步拆解真实流程

我们以最常见的场景为例:用DMA实现ADC连续采样。

想象你现在坐在调试器前,按下复位键,系统启动。接下来会发生什么?

第一步:搭桥铺路 —— 初始化配置

在代码里你写了这么一段:

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 1024);

这一行看似简单,背后却完成了几个关键动作:

  1. 打开时钟电源
    - 给ADC模块供电:__HAL_RCC_ADC_CLK_ENABLE()
    - 给DMA控制器供电:__HAL_RCC_DMA2_CLK_ENABLE()

  2. 设定搬运规则
    - 搬运方向:外设 → 内存(DMA_PERIPH_TO_MEMORY)
    - 数据宽度:半字(16位,匹配ADC输出)
    - 地址变化:外设地址不变(始终读ADC_DR),内存地址递增(填满buffer)
    - 工作模式:循环模式(采集完1024点自动重头再来)

  3. 建立物理连接
    - 把ADC的“数据就绪”信号线接到DMA控制器的请求输入端(硬件连线)
    - 配置DMA通道优先级(避免和其他外设抢总线)

这时候,整个通路已经搭好,只等第一个数据到来。


第二步:自动搬运 —— 真实传输发生时

假设你用了定时器触发ADC,每10μs启动一次转换。

当第1次转换完成:
  • ADC内部状态机置位EOC(Conversion Complete)
  • 自动拉高DMA请求信号(DMA_REQ)
  • DMA控制器检测到请求,向总线仲裁器申请访问权
  • 获得AHB总线控制权后,返回确认信号(DMA_ACK)
  • 发起一次总线读操作:从ADC_DR读取数据
  • 同时发起写操作:写入adc_buffer[0]
  • 更新内存地址指针 +2(半字=2字节)
  • 传输计数器减1
第2次、第3次……直到第1024次:

一切重复进行,无需任何CPU干预。

整个过程中,CPU可能正在执行PID计算、处理UART命令、甚至进入了低功耗睡眠模式。


第三步:任务完成 —— 回报与处理

当第1024个数据搬完后,DMA控制器做了两件事:

  1. 触发中断
    - 设置传输完成标志位(TCIF)
    - 如果使能了中断,则产生DMA_TC_IRQHandler

  2. 自我复位
    - 清除状态标志
    - 进入空闲状态等待下次请求
    - 若为循环模式,则自动重载初始地址和计数器,准备下一周期

此时你在中断服务函数中可以做:

void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_adc1); // HAL库处理 // 此处可添加数据分析、滤波、上传等逻辑 }

注意:这个中断每1024次才来一次,而不是每1次!相比中断驱动方式,CPU负载直接降了两个数量级。


实战技巧:那些手册不会明说的坑

别以为配置完参数就万事大吉。实际项目中,DMA常常“悄无声息”地出问题。以下是几个高频踩坑点和应对策略。

坑点1:明明收到了数据,但内容总是错的?

原因:缓存一致性问题(Cache Coherency)

现代MCU如STM32H7、i.MX RT系列带有数据缓存(D-Cache)。当DMA往SRAM写数据时,如果该区域被缓存了,CPU读取时会命中缓存中的旧数据!

✅解决方案:
- 方法一:关闭相关内存区域的缓存(MPU配置)
- 方法二(推荐):在处理前执行缓存无效化

SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));

⚠️ 记住口诀:“DMA写,CPU读 → 先清缓存;DMA读,CPU写 → 先刷缓存”。


坑点2:程序跑着跑着突然HardFault?

原因:地址未对齐或越界访问

例如你设置传输宽度为32位,但目标缓冲区起始地址是奇数地址(非4字节对齐),就会引发BusFault。

✅检查清单:
- 半字传输 → 起始地址必须2字节对齐
- 全字传输 → 必须4字节对齐
- 缓冲区长度不能超过DMA最大支持(通常65535次)

可以用编译器关键字强制对齐:

uint16_t adc_buffer[1024] __attribute__((aligned(4)));

坑点3:多个外设同时用DMA,谁先谁后?

原因:总线竞争导致优先级混乱

比如同时开了ADC采集和UART发送,两者都走DMA,可能会出现串口卡顿或采样丢点。

✅解决方案:
合理分配通道优先级:

hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; // 关键实时任务 hdma_uart.Init.Priority = DMA_PRIORITY_MEDIUM; // 次要通信任务

必要时使用不同DMA控制器(如DMA1 vs DMA2)分流。


不止于ADC:DMA还能怎么玩?

DMA的能力远不止采集这么简单。掌握之后你会发现,很多原本棘手的问题变得轻松起来。

✅ 音频播放:I2S + DMA 实现无感播放

配置I2S为DMA源,内存音频缓冲区为目标,开启循环模式,就能实现背景音乐播放,CPU几乎零参与。

HAL_I2S_Transmit_DMA(&hi2s, (uint8_t*)audio_buf, SAMPLES_PER_BUF);

✅ 高速通信:UART + DMA 收发海量数据

接收不定长数据时启用DMA+空闲中断组合技:
- DMA持续将收到的数据搬进ring buffer
- 空闲线检测(IDLE Line)触发中断,表示一包数据结束
- CPU只需一次性处理整包,效率极高

✅ 图像采集:DCMI/OV摄像头+DMA直灌内存

配合FSMC或FMC接口,直接将摄像头原始图像帧搬进外部SRAM,为后续JPEG压缩或AI推理提供原材料。

✅ 双缓冲模式:无缝衔接采集与处理

启用双缓冲后,DMA会在两个缓冲区间交替传输:

Buffer A ← 正在传输 ← ADC ↑ Buffer B ← 已完成 ← CPU正在处理

当DMA切换到B时,CPU就可以安心处理A中的数据,真正做到“边采边算”。


如何验证你的DMA真的在工作?

有时候代码看着没问题,但DMA就是不动。别慌,试试这几个排查方法:

🔍 方法1:查标志位

if (__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_TCIF)) { // 传输完成中断已触发 }

🔍 方法2:用逻辑分析仪抓信号

测量以下关键信号:
-DMA_REQ:是否有脉冲?频率是否正确?
-DMA_ACK:是否被响应?
- 总线上的地址/数据变化是否符合预期?

🔍 方法3:观察电流变化

DMA运行时虽然CPU休眠,但总线活动会导致整体功耗轻微上升。对比开启前后电流曲线,也能间接判断是否在传输。


写在最后:DMA教会我们的系统思维

学习DMA,不只是学会配几个寄存器那么简单。它背后体现的是一种分层协作、各司其职的系统设计理念。

CPU擅长决策和调度,就不该让它去做重复性的体力劳动。就像公司老板不该亲自打印文件一样,我们应该把合适的工作交给合适的模块。

当你掌握了DMA,你就迈出了构建高性能嵌入式系统的第一步。下一步,你可以探索:
- MDMA(增强型DMA)在Linux设备树中的应用
- DMA与FFT加速器联动实现频谱分析
- 使用链式DMA(Linked List Mode)处理超大数据流

技术的世界层层递进,而DMA,正是那块最坚实的垫脚石。


如果你也在用DMA解决实际问题,欢迎留言分享你的应用场景或调试心得。我们一起把这块“看不见的搬运工”,变成手中真正的利器。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 20:32:43

Kafka Streams时间窗口配置陷阱:90%开发者都忽略的3个细节

第一章:Kafka Streams时间窗口机制概述在流处理应用中,时间是核心维度之一。Kafka Streams 提供了强大的时间窗口机制,用于对持续不断的数据流按时间区间进行聚合与计算。窗口将无限数据流切分为有限的片段,使得开发者可以执行诸如…

作者头像 李华
网站建设 2026/10/12 1:27:52

learning_rate2e-4是否最优?lora-scripts学习率调参经验

learning_rate2e-4是否最优?LoRA微调中的学习率调参实战指南 在如今动辄数十亿参数的大模型时代,全量微调(full fine-tuning)早已成为少数拥有算力巨头的专属游戏。对于大多数开发者和中小团队而言,如何用一块消费级显…

作者头像 李华
网站建设 2026/10/10 13:46:25

Bootstrap响应式布局适配移动端查看训练状态

Bootstrap响应式布局适配移动端查看训练状态 在模型训练的深夜,你是否曾因为无法及时查看Loss曲线而焦虑?当实验跑在远程服务器上,通勤路上掏出手机却发现TensorBoard页面挤作一团——这几乎是每个AI工程师都经历过的窘境。传统的训练监控工具…

作者头像 李华
网站建设 2026/10/10 17:58:57

通过JLink下载实现工控MCU批量烧录实战案例

从单片到量产:用J-Link打造高可靠工控MCU批量烧录系统你有没有经历过这样的产线场景?十几名工人围坐在一排电脑前,手里拿着开发板,一根根插上ST-LINK,点开烧录软件,手动选择固件、点击“编程”、等待进度条…

作者头像 李华
网站建设 2026/10/8 2:09:52

JLink烧录配合RT-Thread系统的应用实践

JLink烧录与RT-Thread系统的深度协同:从开发到量产的高效实践一场关于“稳定烧录”和“实时调度”的硬核对话在嵌入式开发的世界里,你是否经历过这样的夜晚?凌晨两点,产线反馈新一批板子烧录失败率高达30%;串口下载反复…

作者头像 李华