DMA传输基础:DMA工作原理、DMA与CPU的协同、DMA传输模式
各位同学,咱们今天聊聊DMA。这东西在嵌入式系统里,尤其是做低功耗和高速数据传输时,绝对是个核心角色。我刚开始做项目那会儿,总觉得DMA是个黑盒子,配置好就能用,结果踩了不少坑。后来才明白,搞懂它的工作原理和协同方式,才能真正用好它。
DMA工作原理:说白了就是“数据搬运工”
DMA的全称是Direct Memory Access,直接存储器访问。它的核心任务就是——在不占用CPU的情况下,把数据从一个地方搬到另一个地方。你想想看,如果没有DMA,CPU得亲自去搬数据,比如从外设寄存器搬到内存,或者从内存搬到外设。这活儿虽然简单,但特别费时间。CPU本来可以处理更复杂的逻辑,结果被这些重复的搬运工作拖累了。
DMA的工作原理其实不复杂。它内部有一个专门的控制器,包含几个关键部分:
- 源地址寄存器:告诉DMA数据从哪来。
- 目的地址寄存器:告诉DMA数据搬到哪去。
- 传输计数器:记录要搬多少数据。
- 控制寄存器:配置传输模式、触发方式等。
当CPU配置好这些寄存器后,DMA就开始干活了。它自己控制总线,从源地址读取数据,再写入目的地址。每完成一次传输,计数器减1,直到计数器归零,DMA才停下来,然后给CPU发个中断信号:“活干完了,你接着忙吧。”
关键点:DMA传输过程中,CPU可以继续执行其他任务,或者进入低功耗模式。这就是DMA能提升系统效率和降低功耗的根本原因。
DMA与CPU的协同:谁说了算?
DMA和CPU共享系统总线,这就涉及到一个问题——谁先用总线? 我遇到过不少新手,配置DMA后系统跑飞了,其实就是总线冲突没处理好。
常见的协同方式有两种:
| 协同方式 | 工作原理 | 适用场景 |
|---|---|---|
| 周期窃取 | DMA在CPU不访问总线的空闲周期里“偷”一个总线周期来传输数据 | 低速外设、对CPU性能影响要求严格的场景 |
| 突发传输 | DMA一次性占用总线,连续传输多个数据,传输完才释放总线 | 高速外设、大数据块传输(如显示刷新、音频流) |
我个人习惯是:对于显示刷新这类需要持续高速传输的场景,用突发传输;对于传感器数据采集这种偶尔来一次的场景,用周期窃取。这样既能保证传输效率,又不会让CPU卡死。
避坑指南:我曾经在一个项目中,用突发传输搬运大量数据,结果CPU被“饿死”了——因为DMA一直占着总线,CPU连中断都响应不了。后来我加了总线仲裁优先级控制,才解决这个问题。记住:DMA再快,也不能让它把CPU“憋死”。
DMA传输模式:三种常见玩法
DMA的传输模式,说白了就是“怎么搬数据”。常见的模式有三种,我分别说说。
1. 内存到外设
这是最常用的模式。比如你要把内存中的图像数据发送到LCD显示接口。CPU只需要配置好源地址(内存缓冲区)、目的地址(LCD数据寄存器)和传输长度,然后启动DMA。DMA会自动把数据从内存搬到LCD,CPU可以去做其他事情。
2. 外设到内存
反过来,从外设读数据到内存。比如ADC采集数据,每次转换完成后,DMA自动把结果搬到内存缓冲区。这样CPU不用频繁去读ADC寄存器,等DMA传输完一整批数据后,CPU一次性处理就行。
3. 内存到内存
这个模式比较特殊,两个地址都在内存空间。比如你要把一个大数组从一个位置复制到另一个位置。用DMA做这件事,比CPU用memcpy快得多,尤其是数据量大的时候。不过要注意,有些低端MCU不支持内存到内存的DMA传输,选型时要看清楚。
小技巧:我习惯把DMA传输模式和外设的触发方式结合起来配置。比如SPI发送,可以配置为“SPI发送缓冲区空”触发DMA传输;UART接收,可以配置为“UART接收缓冲区非空”触发DMA传输。这样DMA和外设配合得天衣无缝。
DMA传输的完整流程
为了让你更直观地理解,我画了一张流程图,展示DMA从配置到完成的完整过程。
DMA配置实战:一个简单的例子
说了这么多理论,咱们来点实际的。下面是一个STM32上配置DMA传输的代码片段,实现从内存到外设(USART发送)的DMA传输。
// DMA配置结构体
DMA_InitTypeDef DMA_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址:USART数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer; // 内存地址:发送缓冲区
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:内存→外设
DMA_InitStructure.DMA_BufferSize = 256; // 传输数据长度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度:字节
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 正常模式(非循环)
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式
// 初始化DMA通道
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能DMA通道
DMA_Cmd(DMA1_Channel4, ENABLE);
经验之谈:配置DMA时,我建议先把所有寄存器清零,再逐个配置。有些MCU的DMA寄存器有默认值,不清零的话容易出问题。另外,传输长度(BufferSize)一定要和外设的预期数据量匹配,否则要么数据没传完,要么传多了覆盖了不该覆盖的内存。
DMA传输模式对比总结
最后,我把三种传输模式的特点整理成一张表,方便你对比选择。
| 传输模式 | 源地址 | 目的地址 | 典型应用 | 注意事项 |
|---|---|---|---|---|
| 内存→外设 | 内存缓冲区 | 外设数据寄存器 | LCD显示、DAC输出、SPI发送 | 外设地址一般不递增 |
| 外设→内存 | 外设数据寄存器 | 内存缓冲区 | ADC采集、UART接收、I2C读取 | 注意缓冲区大小和溢出处理 |
| 内存→内存 | 内存区域A | 内存区域B | 数据拷贝、图像处理、协议栈 | 部分MCU不支持,需查手册 |