什么是 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 继续修改缓冲区。

整个流程总结

  1. USART3 空闲中断触发 → 表示一帧数据接收完毕。

  2. 读取 SR 和 DR → 清除空闲中断标志,避免反复进入中断。

  3. 关闭 DMA → 防止后续数据干扰当前帧。

  4. 计算接收长度 → 通过 DMA 剩余计数器算出真实接收字节数。

  5. 设置完成标志 → 通知应用程序处理数据。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐