直接存储器访问 DMA
基于 STM32F103 · 零基础讲解 DMA 原理、通道配置、标准库与 HAL 库编程。
🎯学习目标
- 了解 DMA 的基本概念、用途和优势;
- 理解 DMA 与 CPU 的关系,明白为什么需要 DMA;
- 理解 STM32 的 DMA 模块结构和工作原理;
- 熟悉 DMA 通道、优先级、传输方向等核心概念;
- 掌握 DMA 配置流程和关键参数设置;
- 熟悉 STM32 标准库和 HAL 库中有关 DMA 的库函数;
- 掌握用标准库开发 DMA 应用程序的方法;
- 掌握用 HAL 库开发 DMA 应用程序的方法;
- 能够独立完成 ADC+DMA、串口+DMA 等常见应用。
1DMA 概述
DMA(Direct Memory Access,直接存储器访问)是一种在存储器和外设之间、存储器和存储器之间进行高速数据传输的方式。
想象你要把一箱书从房间 A 搬到房间 B。没有 DMA:你(CPU)必须自己一本一本搬,搬的时候什么都干不了。有 DMA:你叫一个搬运工(DMA),告诉他从哪搬、搬到哪、搬多少本,然后你就可以去学习了。搬运工搬完会告诉你(中断)。
好处:CPU 不被数据搬运占用,可以同时处理其他任务,系统效率大大提高。
把 CPU 想象成公司老板,DMA 是快递员。
没有 DMA 的公司:老板(CPU)亲自搬货,累得要死,没时间做决策(处理业务逻辑)。
有 DMA 的公司:老板(CPU)告诉快递员(DMA):从仓库A(外设)搬到办公室B(内存),搬100箱。然后老板就可以去开会了。快递员搬完后打电话(中断)告诉老板:"搬完了!"
关键点:DMA 搬运数据时,CPU 完全"解放",可以去执行其他代码。这就是为什么 DMA 能大幅提高系统效率。
- 高速传输:DMA 直接控制总线,传输速度可达总线最高速度,不经过 CPU 中转;
- CPU 解放:CPU 只需配置一次,之后 DMA 自动搬运,CPU 可以处理其他任务;
- 实时性好:对于 ADC 采样、串口通信等实时性要求高的场景,DMA 能保证数据及时搬运。
- ADC 连续采集:ADC 每转换完一次,数据自动存入内存数组,CPU 不用每次都去读;
- 串口大批量发送:把内存数组通过串口发出,CPU 不用一个字节一个字节地发;
- 串口接收数据:串口收到数据自动存入内存,CPU 不用每次都去读寄存器;
- 存储器拷贝:把一块内存数据快速拷贝到另一块内存;
- SPI/I2C 通信:高速数据传输场景。
2DMA 工作原理
DMA 传输不需要 CPU 干预,DMA 控制器直接控制总线,完成数据搬运。传输过程:
- CPU 配置 DMA:源地址、目标地址、传输数量、传输方向等;
- DMA 请求:外设发出请求(如 ADC 转换完成、串口收到数据);
- DMA 响应:DMA 控制器获得总线控制权;
- DMA 传输:自动搬运数据,每搬一个数据,地址和计数器自动更新;
- 传输完成:DMA 释放总线,可触发中断通知 CPU。
- 外设 → 存储器:如 ADC 采集数据存入内存;
- 存储器 → 外设:如内存数据通过串口发送;
- 存储器 → 存储器:如内存数据拷贝到另一块内存。
外设→存储器(收快递):数据从外部设备(如 ADC、串口)送到内存。比如温度传感器采集数据,通过 DMA 自动存到数组里。
存储器→外设(寄快递):数据从内存送到外部设备(如串口)。比如要把一个数组通过串口发出去,DMA 自动从数组取数据发给串口。
存储器→存储器(搬家):数据从一块内存搬到另一块内存。比如把数组 A 的内容拷贝到数组 B。
DMA 传输的关键参数
| 参数 | 说明 | 举例 |
|---|---|---|
| 源地址 | 数据从哪里来 | ADC 数据寄存器地址 |
| 目标地址 | 数据送到哪里去 | 内存数组首地址 |
| 传输方向 | 外设→存储器、存储器→外设、存储器→存储器 | 外设→存储器 |
| 传输数量 | 要搬运多少个数据 | 100 个 |
| 数据宽度 | 每次搬运多大的数据(字节/半字/字) | 半字(16位) |
| 地址自增 | 每搬完一个数据,地址是否自动+1 | 存储器地址自增 |
| 循环模式 | 搬完一轮后是否自动重新开始 | 循环模式 |
| 优先级 | 多个通道同时请求时谁先搬 | 高优先级 |
3STM32 DMA 结构
STM32F103 有 2 个 DMA 控制器,DMA1 有 7 个通道,DMA2 有 5 个通道(仅大容量产品有 DMA2)。
| DMA 控制器 | 通道数 | 请求源 |
|---|---|---|
| DMA1 | 7 个通道 | ADC1、USART1/2/3、SPI1/2、I2C1/2、TIM1/2/3/4 等 |
| DMA2 | 5 个通道 | ADC3、SPI/I2S3、UART4/5、TIM5/6/7/8 等 |
DMA 优先级
当多个 DMA 通道同时请求传输时,需要仲裁器决定谁先传输。STM32 DMA 优先级分为 4 个等级:
| 优先级 | 说明 |
|---|---|
| 非常高(Very High) | 最高优先级,最先响应 |
| 高(High) | 第二优先级 |
| 中(Medium) | 第三优先级 |
| 低(Low) | 最低优先级,最后响应 |
- 优先级高的通道先传输;
- 如果优先级相同,通道号小的先传输(CH1 > CH2 > CH3 ...);
- 优先级在配置时设置,运行时不能改变。
DMA 传输模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 正常模式 | 传输完成后停止,需要重新使能才能再次传输 | 单次数据传输 |
| 循环模式 | 传输完成后自动重新开始,循环往复 | ADC 连续采集、音频播放 |
正常模式:搬完一轮就停止,适合"一次性搬运"场景,比如发送一帧数据。
循环模式:搬完一轮自动重头开始,适合"持续采集"场景,比如 ADC 不断采样。循环模式下,DMA 会自动覆盖之前的数据(除非你及时处理)。
4DMA 配置流程
使用 DMA 的一般步骤:
- 使能 DMA 时钟;
- 配置 DMA 通道参数:外设地址、存储器地址、传输方向、数据量、地址自增、循环模式等;
- 使能 DMA 通道;
- 使能外设的 DMA 请求(如 ADC_DMACmd、USART_DMACmd);
- (可选)配置 DMA 中断,处理传输完成事件。
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&adc_buf; // 存储器地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 方向:外设→存储器
DMA_InitStructure.DMA_BufferSize = 100; // 传输数量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不自增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非存储器到存储器
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
- PeripheralBaseAddr:外设数据寄存器地址,如 ADC1->DR、USART1->DR;
- MemoryBaseAddr:内存数组首地址,如 adc_buf[0] 的地址;
- DIR:传输方向,DMA_DIR_PeripheralSRC(外设→存储器)、DMA_DIR_PeripheralDST(存储器→外设);
- BufferSize:要传输的数据个数,不是字节数;
- PeripheralInc:外设地址是否自增,一般外设只有一个数据寄存器,不需要自增;
- MemoryInc:存储器地址是否自增,存到数组时需要自增;
- PeripheralDataSize / MemoryDataSize:数据宽度,字节(8位)、半字(16位)、字(32位);
- Mode:DMA_Mode_Normal(正常)、DMA_Mode_Circular(循环);
- Priority:优先级,Low/Medium/High/VeryHigh;
- M2M:是否为存储器到存储器模式,ENABLE/DISABLE。
① 先使能 DMA 时钟(RCC_AHBPeriphClockCmd);
② 再配置 DMA 通道参数(DMA_Init);
③ 然后使能 DMA 通道(DMA_Cmd);
④ 最后使能外设的 DMA 请求(如 ADC_DMACmd)。
注意:如果先使能外设 DMA 请求,再使能 DMA 通道,可能会丢失第一次请求。
5标准库函数
常用 DMA 标准库函数(源码在 stm32f10x_dma.c):
| 函数 | 功能 | 参数说明 |
|---|---|---|
DMA_Init() | 初始化 DMA 通道 | DMA_Channelx, DMA_InitStruct |
DMA_DeInit() | 恢复默认值 | DMA_Channelx |
DMA_Cmd() | 使能/禁用 DMA 通道 | DMA_Channelx, ENABLE/DISABLE |
DMA_SetCurrDataCounter() | 设置传输数量 | DMA_Channelx, DataNumber |
DMA_GetCurrDataCounter() | 获取剩余传输数量 | DMA_Channelx |
DMA_GetFlagStatus() | 获取 DMA 标志位 | DMA_FLAG_xxx |
DMA_ClearFlag() | 清除 DMA 标志位 | DMA_FLAG_xxx |
DMA_ITConfig() | 配置 DMA 中断 | DMA_Channelx, DMA_IT_xxx, ENABLE/DISABLE |
DMA_GetITStatus() | 获取中断状态 | DMA_IT_xxx |
DMA_ClearITPendingBit() | 清除中断挂起位 | DMA_IT_xxx |
// 初始化 DMA 通道
void DMA_Init(DMA_Channel_TypeDef* DMA_Channelx, DMA_InitTypeDef* DMA_InitStruct);
// 使能/禁用 DMA 通道
void DMA_Cmd(DMA_Channel_TypeDef* DMA_Channelx, FunctionalState NewState);
// 设置传输数量(可在运行时修改)
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMA_Channelx, uint16_t DataNumber);
// 获取剩余传输数量
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMA_Channelx);
// 获取标志位状态
FlagStatus DMA_GetFlagStatus(uint32_t DMA_FLAG);
// 清除标志位
void DMA_ClearFlag(uint32_t DMA_FLAG);
// 配置中断
void DMA_ITConfig(DMA_Channel_TypeDef* DMA_Channelx, uint32_t DMA_IT, FunctionalState NewState);
- DMA1_FLAG_TC1:DMA1 通道 1 传输完成标志;
- DMA1_FLAG_HT1:DMA1 通道 1 半传输完成标志;
- DMA1_FLAG_GL1:DMA1 通道 1 全局标志(传输完成或半传输);
- DMA1_FLAG_TE1:DMA1 通道 1 传输错误标志。
6HAL 库函数
HAL 库中 DMA 相关函数(源码在 stm32f1xx_hal_dma.c):
| 函数 | 功能 | 参数说明 |
|---|---|---|
HAL_DMA_Init() | 初始化 DMA | hdma(DMA_HandleTypeDef 指针) |
HAL_DMA_Start() | 启动 DMA 传输(无中断) | hdma, SrcAddress, DstAddress, DataLength |
HAL_DMA_Start_IT() | 启动 DMA 传输(带中断) | hdma, SrcAddress, DstAddress, DataLength |
HAL_DMA_Abort() | 中止 DMA 传输 | hdma |
HAL_DMA_GetState() | 获取 DMA 状态 | hdma |
HAL_DMA_GetError() | 获取 DMA 错误 | hdma |
// DMA 句柄
DMA_HandleTypeDef hdma_adc1;
// 配置 DMA
hdma_adc1.Instance = DMA1_Channel1; // DMA1 通道 1
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设→存储器
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不自增
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 存储器地址自增
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 外设数据半字
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // 存储器数据半字
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; // 高优先级
HAL_DMA_Init(&hdma_adc1);
// 启动 DMA 传输(带中断)
HAL_DMA_Start_IT(&hdma_adc1, (uint32_t)&ADC1->DR, (uint32_t)adc_buf, 100);
HAL 库提供了 DMA 传输完成回调函数,可以在中断中自动调用:
// 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// DMA 传输完成,处理数据
}
// 半传输完成回调(循环模式下很有用)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
// 半传输完成,可以处理前半部分数据
}
⭐重点例题
例题1:ADC+DMA 采集(标准库)
// 1. 配置 ADC(省略详细配置)
// 2. 配置 DMA
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能 DMA1 时钟
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 100;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE);
// 3. 使能 ADC 的 DMA 请求
ADC_DMACmd(ADC1, ENABLE);
// 4. 启动 ADC 转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
关键点:① 必须先使能 DMA 时钟;② ADC 的 DMA 请求必须在 DMA 使能之后;③ 循环模式下 DMA 会自动重新开始。
例题2:ADC+DMA 采集(HAL 库)
// CubeMX 自动生成的代码
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adc_buf[100];
// 启动 ADC+DMA 采集
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 100);
// 传输完成回调(自动调用)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// 100 个数据采集完成,可以处理数据
}
// 半传输完成回调
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
// 前 50 个数据采集完成
}
关键点:① CubeMX 配置更简单;② HAL 库自动处理中断;③ 回调函数自动调用。
例题3:串口+DMA 发送(标准库)
// 1. 配置 USART1(省略详细配置)
// 2. 配置 DMA1 通道 4(USART1_TX)
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 存储器→外设
DMA_InitStructure.DMA_BufferSize = 100;
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_Init(DMA1_Channel4, &DMA_InitStructure);
// 3. 使能 USART1 的 DMA 发送请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// 4. 启动 DMA 传输
DMA_Cmd(DMA1_Channel4, ENABLE);
关键点:① 串口发送用 DMA1 通道 4;② 方向是存储器→外设;③ 正常模式(发完就停)。
例题4:串口+DMA 接收(标准库)
// 1. 配置 USART1(省略详细配置)
// 2. 配置 DMA1 通道 5(USART1_RX)
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→存储器
DMA_InitStructure.DMA_BufferSize = 100;
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_Init(DMA1_Channel5, &DMA_InitStructure);
// 3. 使能 USART1 的 DMA 接收请求
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
// 4. 启动 DMA 传输
DMA_Cmd(DMA1_Channel5, ENABLE);
关键点:① 串口接收用 DMA1 通道 5;② 方向是外设→存储器;③ 可以配置循环模式持续接收。
例题5:存储器到存储器拷贝(标准库)
uint8_t src_buf[100] = {1, 2, 3, ...};
uint8_t dst_buf[100];
// 配置 DMA1 通道 1(存储器到存储器模式)
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)src_buf;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dst_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→存储器(这里的"外设"是源存储器)
DMA_InitStructure.DMA_BufferSize = 100;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // 源地址自增
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_Enable; // 存储器到存储器模式
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 启动传输(存储器到存储器模式不需要外设请求)
DMA_Cmd(DMA1_Channel1, ENABLE);
// 等待传输完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
关键点:① M2M 必须设置为 ENABLE;② 两个地址都要自增;③ 存储器到存储器模式不需要外设请求,使能 DMA 通道就开始传输。
🎯自测(点击展开)
DMA 的全称是什么?
DMA 传输有哪三个方向?
STM32F103 有几个 DMA 控制器?
DMA 传输完成后如何通知 CPU?
DMA 优先级有几个等级?
循环模式和正常模式有什么区别?
ADC+DMA 采集时,DMA 的源地址是什么?
串口发送用 DMA1 的哪个通道?
存储器到存储器模式需要设置哪个参数?
使能 DMA 时钟的函数是什么?
📝强化题库
选择题点选即时判分;填空题输入后"检查"或"显示答案"。