DMA解释作用及配置-stm32f107VC为例
什么是 DMA?
DMA 是“直接存储器访问”的缩写,负责处理繁杂数据,绕过CPU,提高效率
它专门负责把数据从一个地方移到另一个地方,全程不需要 CPU 插手,CPU 可以做其他更重要的事情。
//#ifdef USE_DMA // 条件编译:若定义了USE_DMA,则编译以下代码
void DMA_USART3_RX_Config(void) { // 定义DMA接收配置函数
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA1的AHB时钟
DMA_InitTypeDef DMA_InitStructure; // 定义DMA初始化结构体
DMA_DeInit(DMA1_Channel3); // 复位DMA1通道3的寄存器
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART3->DR); // 外设基地址:USART3数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART3_RxBuffer; // 内存基地址:接收缓冲区
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:外设作为源(外设→内存)
DMA_InitStructure.DMA_BufferSize = USART3_RX_BUFFER_SIZE; // 传输数据量:缓冲区大小
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模式:正常模式(单次传输)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;// 通道优先级:中
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止内存到内存模式
DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 初始化DMA1通道3
DMA_Cmd(DMA1_Channel3, ENABLE); // 使能DMA1通道3
}
void USART3_Enable_DMA_RX(void) { // 定义使能USART3 DMA接收的函数
USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE); // 使能USART3的DMA接收请求
}
void USART3_IRQHandler(void) // USART3中断服务函数
{
if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) { // 若空闲线路中断标志置位
volatile uint16_t temp = USART3->SR; // 读状态寄存器(清除中断标志的第一步)
temp = USART3->DR; // 读数据寄存器(清除空闲标志)
(void)temp; // 防止编译器警告未使用变量
DMA_Cmd(DMA1_Channel3, DISABLE); // 关闭DMA通道,停止传输
USART3_RxCount = USART3_RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3); // 计算已接收字节数
USART3_RxFinished = 1; // 设置接收完成标志
}
}
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能DMA1的AHB时钟
非要使用DMA1吗?有没有DMA2呢?
有的,但是一般在大容量芯片中才有
STM32F103C8T6(中等容量)只有 DMA1,没有 DMA2;
而 STM32F103ZET6(大容量)则同时具有 DMA1 和 DMA2。
我们的107是有的,但是通道数存在差异
-
DMA1:7 个通道
-
DMA2:5 个通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART3->DR); // 外设基地址:USART3数据寄存器
指定 DMA 传输的外设端基地址。这里设置为 &(USART3->DR),即 USART3 的数据寄存器地址。DMA 会从这个地址读取(或写入)数据。
-
DR:“数据寄存器”(Data Register)。串口收到一个字节时,这个字节就会自动出现在USART3->DR这个寄存器里。 -
为什么前面要加
(uint32_t)强制转换? :在 C 语言中,&(USART3->DR)是一个指针,它的类型是volatile uint32_t *。而 DMA 初始化结构体中的DMA_PeripheralBaseAddr成员要求的是一个 32 位的无符号整数(uint32_t)。所以用(uint32_t)把指针的地址值直接转成整数,方便 DMA 硬件识别。
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART3_RxBuffer; // 内存基地址:接收缓冲区
指定 DMA 传输的内存端基地址。这里设置为 `USART3_RxBuffer`(数组名),即接收缓冲区的起始地址。DMA 会将数据从外设传输到此内存区域。--内存传输更快,储存会很慢
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:外设作为源(外设→内存)
传输方向选择。
DMA_DIR_PeripheralSRC:外设作为源(外设 → 内存),适用于接收数据。 SRC 是 Source 的缩写,表示 源端
DMA_DIR_PeripheralDST :外设作为目标(内存 → 外设),适用于发送数据。DST =destination 目的地。
DMA_BufferSize// 传输数据量:缓冲区大小
指定 DMA 通道要传输的数据量(单位与数据宽度一致)。此处为 USART3_RX_BUFFER_SIZE,即缓冲区大小。当传输完该数量数据后,DMA 会产生传输完成中断(如果使能)或停止。
这里是256Byte
#define USART3_RX_BUFFER_SIZE 256
-
USART3_RX_BUFFER_SIZE定义为 256,它表示的是传输的数据项个数。 -
数据项的大小由
DMA_PeripheralDataSize和DMA_MemoryDataSize决定,这里都设置为DMA_PeripheralDataSize_Byte(8 位,即 1 字节)。 -
因此,
DMA_BufferSize = 256表示 DMA 会连续搬运 256 个字节,每搬运一个字节,内存地址自动增加(因为DMA_MemoryInc = Enable),直到完成 256 次搬运后停止。
举个直观例子:
如果串口收到了 100 个字节,DMA 会从 USART 数据寄存器将这 100 个字节依次存入接收缓冲区的前 100 个位置,还剩余 156 个位置的搬运容量没有使用。当收到第 257 个字节时,由于 DMA 已经完成了 256 次搬运(缓冲已满),不会继续存入,需要软件重新配置 DMA 才能接收后续数据(除非使用循环模式)。这里是in normal
注意:在代码中,空闲中断处理里通过 DMA_GetCurrDataCounter 计算剩余未传输的字节数,然后用 USART3_RX_BUFFER_SIZE - 剩余数 得出实际接收到的字节数,说明缓冲区大小就是按字节数设置的。
DMA_PeripheralInc
配置外设地址是否在每次传输后自动增加。DMA_PeripheralInc_Disable:外设地址不变。因为 USART 数据寄存器是单个寄存器,不需要自增。
Inc 是 Increment 的缩写,表示 地址自增。
DMA_PeripheralInc_Enable:外设地址递增,适用于外设是存储器阵列的情况。
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度:字节
#define DMA_MemoryDataSize_Byte ((uint32_t)0x00000000)
//8字节
#define DMA_MemoryDataSize_Byte ((uint32_t)0x00000000) // 8位,对应寄存器位 00
#define DMA_MemoryDataSize_HalfWord ((uint32_t)0x00000100) // 16位,实际是 0x01 << 8
#define DMA_MemoryDataSize_Word ((uint32_t)0x00000200) // 32位,实际是 0x10 << 8
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度:字节
此都设置为8字节
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // DMA模式:正常模式(单次传输)
DMA_Mode_Normal`:正常模式,完成设定的 `BufferSize` 次传输后停止。
DMA_Mode_Circular`:循环模式,传输完成后自动重新开始,适用于连续数据流。
DMA_Priority
DMA 通道的优先级。当多个 DMA 通道同时请求时,优先级高的先响应。
可选:`VeryHigh`、`High`、`Medium`、`Low`。此处设为中等优先级。
DMA_M2M
是否启用内存到内存模式。
DMA_M2M_Disable:禁止内存到内存模式。此时 DMA 只能在外设和内存之间传输。
DMA_M2M_Enable:允许内存到内存传输(需要软件触发),且此时外设地址和内存地址均可自增。
!! 注意事项
-
在内存到内存模式下,DMA 会占用总线,如果传输数据量大,可能影响 CPU 访问内存的速度(但 DMA 本身是硬件,仍比 CPU 逐个字节复制高效)。
-
内存到内存模式通常不支持循环模式(
DMA_Mode_Circular对此模式无效),因为循环模式是为连续外设请求设计的。 -
配置时,如果启用
DMA_M2M_Enable,那么原来的“外设地址”和“内存地址”都被视为普通内存地址,你需要将源地址放在DMA_PeripheralBaseAddr,目标地址放在DMA_MemoryBaseAddr,方向设为DMA_DIR_PeripheralSRC或DMA_DIR_PeripheralDST均可,因为此时 DMA 不再区分外设和内存。
|
特性 |
DMA_M2M_Disable | DMA_M2M_Enable |
|---|---|---|
| 传输方向 | 外设 ↔ 内存 | 内存 ↔ 内存 |
| 触发方式 | 硬件请求(外设事件) | 软件启动(使能后立即传输) |
| 外设地址 | 固定/自增(但通常固定) | 可自增(视为内存源地址) |
| 内存地址 | 可自增 | 可自增 |
| 典型用途 | 串口、ADC、SPI 等外设数据搬运 | 内存复制、数组初始化、图像处理 |
DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 初始化DMA1通道3
DMA_Cmd(DMA1_Channel3, ENABLE); // 使能DMA1通道3
void USART3_Enable_DMA_RX(void) { // 定义使能USART3 DMA接收的函数
USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE); // 使能USART3的DMA接收请求
}
作用:使能 USART3 的 DMA 接收请求,让串口硬件在收到数据时自动触发 DMA 传输。
-
USART_DMACmd()是标准外设库函数,用于控制 USART 的 DMA 请求功能。 -
第一个参数
USART3:指定要操作的串口。 -
第二个参数
USART_DMAReq_Rx:表示要配置的是接收 DMA 请求(另一个选项是USART_DMAReq_Tx,用于发送)。 -
第三个参数
ENABLE:使能该请求。
执行效果
使能后,当 USART3 接收到一个字节并存入数据寄存器时,硬件会自动向 DMA 控制器发出一个请求。DMA 控制器根据之前配置好的参数(通道、地址、数据量等),将这个字节从 USART3->DR 搬运到指定的接收缓冲区中,整个过程无需 CPU 干预。
与之前配置的关系
在之前的 DMA_USART3_RX_Config() 函数中,我们完成了:
-
DMA 通道的初始化(外设地址、内存地址、数据宽度、模式等)
-
DMA 通道使能(
DMA_Cmd(DMA1_Channel3, ENABLE))
记住:DMA 通道使能后并不会立即传输,它只是准备好接收请求。真正启动传输的是外设发出的 DMA 请求。这个 USART3_Enable_DMA_RX() 函数就是打开 USART3 的 DMA 请求开关,让串口在收到数据时去“敲门”触发 DMA。
-
DMA 初始化:告诉 DMA 搬运数据的起点、终点、方式。
-
DMA 通道使能:让 DMA 准备好干活。
-
USART DMA 请求使能:让串口在收到数据时通知 DMA 干活。
void USART3_IRQHandler(void) // USART3中断服务函数
{
if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) { // 若空闲线路中断标志置位
volatile uint16_t temp = USART3->SR; // 读状态寄存器(清除中断标志的第一步)
temp = USART3->DR; // 读数据寄存器(清除空闲标志)
(void)temp; // 防止编译器警告未使用变量
DMA_Cmd(DMA1_Channel3, DISABLE); // 关闭DMA通道,停止传输
USART3_RxCount = USART3_RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3); // 计算已接收字节数
USART3_RxFinished = 1; // 设置接收完成标志
}
}
-
使用
volatile关键字防止编译器优化,确保这条读操作确实执行。
DMA_Cmd(DMA1_Channel3, DISABLE);
-
禁止 DMA1 通道 3。
-
此时一帧数据已经全部通过 DMA 搬运到了接收缓冲区中,我们需要暂时关闭 DMA,防止下一帧数据覆盖当前帧,或者防止在软件处理数据的过程中 DMA 继续修改缓冲区。
整个流程总结
-
USART3 空闲中断触发 → 表示一帧数据接收完毕。
-
读取 SR 和 DR → 清除空闲中断标志,避免反复进入中断。
-
关闭 DMA → 防止后续数据干扰当前帧。
-
计算接收长度 → 通过 DMA 剩余计数器算出真实接收字节数。
-
设置完成标志 → 通知应用程序处理数据。
更多推荐


所有评论(0)