屏东县网站建设_网站建设公司_VS Code_seo优化
2026/1/9 20:29:17 网站建设 项目流程

DMA与CPU如何“分头行动,默契配合”?揭秘硬件级数据搬运的底层逻辑

你有没有遇到过这样的场景:单片机接了一个高速ADC,采样率一上来,CPU立马满载,连最基础的LED闪烁都卡顿了?或者串口收数据时稍快一点,就频频丢包,调试半天发现是中断处理不过来?

这背后,其实藏着一个古老而关键的设计哲学——别让CPU干搬砖的活儿

在现代嵌入式系统中,真正决定性能上限的,往往不是主频多高、核心几个,而是数据能不能高效地流动。而实现这一点的核心技术之一,就是我们今天要深挖的主角:DMA(Direct Memory Access)

它不像CPU那样“思考”,却能在后台默默完成海量数据的搬运,像一位不知疲倦的快递员,把外设的数据一箱箱送到内存仓库里。整个过程,几乎不打扰正在忙算法、控逻辑的CPU。

那么问题来了:
- DMA到底是怎么绕开CPU直接读写内存的?
- 它和CPU之间会不会“抢总线”打架?
- 实际编程时该怎么用才能不出错?

别急,这篇文章我们就从硬件交互流程的角度,彻底讲清楚DMA与CPU是如何协同工作的。没有空洞术语堆砌,只有真实可感的技术脉络。


为什么CPU不能一直当“搬运工”?

先回到最初的问题:既然CPU能读寄存器、写内存,为什么不自己动手搬数据?

答案很简单:太慢、太累、还耽误正事

举个例子。假设你要通过UART接收1KB的数据:

  • 如果用轮询方式,CPU得反复检查状态寄存器,每收到一个字节就手动读出来再存进数组;
  • 即使用中断,也是每来一个字节触发一次中断,进入ISR读取并保存。

看起来好像也没啥,但算笔账你就明白了:

方式每字节开销(指令周期)总耗时(9600bps下)CPU占用率
轮询~50>10ms接近100%
中断驱动~30 + 中断上下文切换~8ms70%-90%
DMA仅初始化+结束介入0%运行中占用<5%

看到区别了吗?同样是完成任务,DMA把99%的工作甩给了硬件,CPU只在开始前配置一下、结束后处理一下,中间完全可以去跑控制算法、响应用户输入,甚至睡觉省电。

这就是分工的价值:让擅长计算的专注计算,让擅长传输的负责传输。


DMA控制器到底是个什么角色?

我们可以把DMA控制器理解为一个独立的小型处理器,但它不执行通用程序,只干一件事:按指令搬数据

它的“指令”不是代码,而是由CPU预先设置好的一组参数:

struct dma_config { void *src_addr; // 数据从哪儿来(比如 ADC->DR) void *dst_addr; // 数据往哪儿放(比如 buffer[]) size_t data_size; // 搬多少次 uint8_t width; // 每次搬多宽(8/16/32位) uint8_t increment; // 地址是否自动递增 uint8_t mode; // 单次?循环?双缓冲? };

一旦启动,DMA就会根据这些配置,自动生成地址、发起总线请求、完成读写操作,全程无需CPU插手。

它是怎么“绕过”CPU访问内存的?

关键就在于——它也能当“总线主人”

在典型的微控制器架构中(如STM32、NXP Kinetis),系统总线(如AHB)支持多个主设备(Bus Master):

  • CPU 是主设备 #1
  • DMA 控制器是主设备 #2
  • 有些SoC还有加密引擎、GPU等其他主设备

它们通过一个叫总线仲裁器(Bus Arbiter)的模块协调谁能在什么时候使用总线。

所以DMA并不是真的“绕过”CPU,而是和CPU平起平坐,轮流使用总线资源。就像两个人共用一条高速公路,交警(仲裁器)决定谁先上道。


DMA与CPU协作的真实时序:谁在什么时候干活?

我们来看一个典型的数据采集场景下的时间线:

时间轴: [ CPU 初始化 DMA ] → [ DMA 抢占总线传输 ] → [ CPU 执行其他任务 ] ←→ ... ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ (突发传输,持续几微秒)

具体分为三个阶段:

阶段一:CPU主导 —— 配置通道

此时DMA还在待命,CPU做以下工作:

  • 启用对应外设的DMA请求功能(如 USART_CR3 |= DMAR)
  • 设置DMA源地址(外设数据寄存器)、目标地址(内存缓冲区)
  • 配置传输方向、数据宽度、数量、是否开启中断
  • 启动DMA传输(写DMA_CCR.EN = 1)

完成后,CPU就可以转身去做别的事了。

阶段二:DMA接管 —— 自动搬运

当外设准备好数据(例如UART接收到一个字节),会拉高DMA Request信号线,通知DMA控制器:“有数据啦!”

DMA立刻向总线仲裁器申请使用权。一旦获批,它便:

  1. 锁定总线,CPU暂时挂起对外存的访问(但缓存命中仍可执行指令)
  2. 发出读操作,从外设寄存器取数据
  3. 发出写操作,将数据存入指定内存位置
  4. 自动递增地址或计数器
  5. 重复上述步骤直到传输完成

这个过程可以是单次传输(传完一次就停),也可以是循环模式(不断覆盖缓冲区),甚至是双缓冲交替(AB区切换,无缝衔接)。

📌 小知识:很多高端MCU支持“突发传输”(Burst Transfer),即DMA一次性连续搬4个、8个甚至16个字,极大减少总线切换开销,提升效率。

阶段三:事件交接 —— 回归CPU

当预设的数据量全部搬完,DMA会:

  • 清除使能位(或保持使能在循环模式下)
  • 触发一个中断(DMA Complete Interrupt)
  • 释放总线控制权

这时CPU收到中断,跳转到回调函数处理数据,比如:

  • 解析协议
  • 进行滤波或FFT
  • 打包发送到网络
  • 或重新启动下一轮DMA接收,形成流水线

整个过程中,CPU只参与了开头和结尾,中间完全解放。


真实代码长什么样?以STM32为例

下面这段基于STM32 HAL库的代码,展示了如何用DMA实现UART后台接收:

#define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; UART_HandleTypeDef huart2; DMA_HandleTypeDef hdma_usart2_rx; void uart_dma_init(void) { // 1. 初始化UART外设 huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_RX; HAL_UART_Init(&huart2); // 2. 启动DMA接收(关键一步!) HAL_UART_Receive_DMA(&huart2, rx_buffer, RX_BUFFER_SIZE); } // 3. 当DMA传输完成时自动调用 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart2) { // 处理已接收到的一整包数据 process_received_data(rx_buffer, RX_BUFFER_SIZE); // 可选:重新启动DMA,继续监听下一包 HAL_UART_Receive_DMA(huart, rx_buffer, RX_BUFFER_SIZE); } }

就这么几行代码,就建立了一个零拷贝、低负载、高可靠的串口通信通道。

重点在于HAL_UART_Receive_DMA()这个函数——它背后做了大量配置工作:

  • 开启USART的DMA接收使能位
  • 绑定DMA通道到USART_RX
  • 设置内存地址为rx_buffer
  • 设置外设地址为USART2->DR
  • 配置为内存递增、外设固定、32位对齐优化
  • 使能传输完成中断

然后一切交给硬件,CPU再也不用操心每一个字节是怎么来的。


常见应用场景:哪些地方离不开DMA?

1. 高速ADC采样(如音频、传感器)

要求:48kHz采样率,双通道,16位精度 → 每秒约192KB数据。

若用中断方式,意味着每20.8μs就要进一次中断,对CPU压力极大。而使用DMA配合定时器触发ADC,可实现精准定时采集,并通过循环缓冲+半传输中断机制实现前后台流水处理:

  • 前半缓冲填满 → CPU处理前半部分
  • 后半缓冲填满 → CPU处理后半部分
  • 同时DMA持续填充,无间隙

完美解决实时性与CPU负载之间的矛盾。

2. I2S音频播放/录音

I2S是典型的流式接口,数据源源不断。DMA在这里几乎是标配:

  • 内存中的音频帧 → 经DMA送入I2S寄存器 → DAC输出声音
  • 麦克风采集 → I2S → DMA写入内存缓冲 → CPU编码压缩

整个链路全由硬件打通,CPU只需管理缓冲区切换和格式转换。

3. 内存间大数据复制

比如图形显示中刷新Framebuffer,或者FPGA与MCU间批量交换数据。传统memcpy()可能耗时毫秒级,而DMA可在几微秒内完成,且不影响系统响应。

4. SDIO/USB/ETH等高速接口

在支持SD卡、USB设备或以太网通信的系统中,DMA更是必不可少。否则根本无法达到MB/s级别的吞吐能力。


实战避坑指南:那些年踩过的DMA陷阱

尽管DMA强大,但用不好也会带来麻烦。以下是开发者常遇到的几个“坑”及应对策略:

❌ 坑点1:缓存不一致(Cache Coherency)

现象:DMA把新数据写进了内存,但CPU读出来的还是旧值。

原因:在带Cache的处理器(如Cortex-A系列)中,CPU可能从L1 Cache读取数据,而DMA直接改的是物理内存。

解决方案
- 在DMA写入后,执行__DSB(); SCB_InvalidateDCache_by_Addr()清除对应区域Cache
- 或使用非缓存映射内存段(Uncached Region)存放DMA缓冲区

❌ 坑点2:地址未对齐导致总线错误

现象:程序运行到HAL_DMA_Start()直接HardFault。

原因:某些总线(如AHB)要求32位访问必须4字节对齐,64位需8字节对齐。

解决方案
- 使用__attribute__((aligned(4)))强制对齐缓冲区
- 检查DMA手册中关于“自然对齐”的要求

uint8_t rx_buffer[256] __attribute__((aligned(4)));

❌ 坑点3:忘记清中断标志,导致反复进中断

现象:DMA传输完成后,中断服务函数被无限调用。

原因:没有正确清除DMA中断标志位(如DMA_ISR_TCIFx)。

解决方案
- 确保在中断处理末尾调用__HAL_DMA_CLEAR_FLAG()或由HAL库自动处理
- 检查是否多个中断源同时触发

❌ 坑点4:多个外设争抢同一DMA通道

现象:某个外设DMA突然失效,查了半天发现被别的模块占用了。

解决方案
- 查阅芯片参考手册的“DMA请求映射表”
- 设计初期规划好各外设使用的DMA通道
- 必要时动态分配或禁用非关键通道


总结:DMA的本质是什么?

说到最后,我们可以给DMA下一个更本质的定义:

DMA是一种将“确定性、重复性、大批量”的数据搬运任务从软件迁移到硬件的机制

它不是魔法,也不是替代CPU的存在,而是系统级资源调度的艺术体现

它的价值体现在三个层面:

  1. 性能提升:释放CPU,提高吞吐;
  2. 实时保障:确定性延迟,避免漏采;
  3. 功耗优化:CPU可深度睡眠,仅靠外设+DMA维持数据采集。

掌握DMA,不只是学会调API,更是建立起一种硬件思维

“这个问题能不能交给硬件自动完成?有没有现成的外设联动机制?”

当你开始这样思考时,就已经迈入了高级嵌入式开发的大门。

未来,在边缘AI、工业控制、智能传感等领域,随着数据速率不断提升,DMA与其他硬件模块(如DMAC、PDMA、AXI互连、MPU保护单元)的协同将更加复杂而精密。

但万变不离其宗——让合适的硬件做合适的事,永远是构建高效系统的底层法则。

如果你正在做一个需要稳定收发数据的项目,不妨试试加上DMA。也许你会发现,原来那个“卡死”的系统,突然变得丝滑流畅了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询