目录

1.  DMA概述

1.1  简介

1.2  存储器映像

1.3  DMA框图

1.4  基本结构

1.5  触发源选择

1.6  数据宽度与对齐

2.  工程配置

2.1  触发源

2.2  对应通道

2.3  传输方向

2.4  优先级

2.5  模式选择

2.6  地址是否自增

2.7  数据宽度

3.  函数介绍

3.1  HAL_UARTEx_ReceiveToIdle_DMA()函数

3.2  HAL_UART_Transmit_DMA()函数

3.3  HAL_UARTEx_RxEventCallback()函数

4.  代码编写


1.  DMA概述

1.1  简介

         DMA,全称Direct Memory Access,即直接存储器访问。

        DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。

        如果没有不通过DMA,CPU传输数据还要以内核作为中转站,例如将ADC采集的数据转移到SRAM中。

        而如果通过DMA的话,DMA控制器将获取到的外设数据存储到DMA通道中,然后通过DMA总线与DMA总线矩阵协调,将数据传输到SRAM中,期间不需内核参与。

主要特征:

  • 同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
  • 独立数据源和目标数据区的传输宽度(字节、半字、全字);
  • 可编程的数据传输数目:最大为65535;
  • 对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。

1.2  存储器映像

        计算机系统的五大组成部分:运算器、控制器、存储器、输入设备和输出设备。

        其中运算器和控制器合在一起叫CPU。

STM32所有类型的存储器:

类型 起始地址 存储器 用途
ROM 0x0800 0000 程序存储器Flash 存储C语言编译后的程序代码
0x1FFF F000 系统存储器 存储BootLoader,用于串口下载
0x1FFF F800 选项字节 存储一些独立于程序代码的配置参数
RAM 0x2000 0000 运行内存SRAM 存储运行过程中的临时变量
0x4000 0000 外设寄存器 存储各个外设的配置参数
0xE000 0000 内核外设寄存器 存储内核各个外设的配置参数

1.3  DMA框图

        在看之前我们先需要搞懂一个概念什么是寄存器,寄存器是一种特殊的存储器,寄存器的每一位后面连接着一根导线,可以操作外设电平的状态,完成如操作引脚电平,开关的打开或者关闭,切换数据选择器,当做计数器等的操作:

所以寄存器可以说是连接软件和硬件的桥梁,软件读写寄存器就相当于在控制硬件的执行。

        下面我们来看看DMA的框图:

①:DMA总线访问各个存储器;

②:DMA内部的多个通道进行独立的数据转运;

③:仲裁器用于管理多个通道,防止冲突;

④:DMA从设备用于配置DMA参数;

⑤:DMA请求用于硬件触发DMA的数据转运;

1.4  基本结构

        上面的图看不懂,没关系,我们总结一下:

1.5  触发源选择

        对于硬件触发我们需要根据不同的触发源,选择不同的通道,软件触发就随便了:

        其不同的外设,需要对应不同的通道,可以参考上下图:

        这里我们需要考虑一个问题:当多个DMA请求同时到来时,是如何工作呢?

        STM32的DMA仲裁器处理多请求时,其优先级判定分为两个明确的阶段:

软件优先级:这是你可以在程序中配置的。每个数据流(Stream)或通道(Channel)都可以被设置为以下四个等级之一:

  • 非常高(Very High)
  • 高(High)
  • 中(Medium)
  • 低(Low)

硬件优先级:当两个或更多请求的软件优先级相同时,仲裁器会转而依靠硬件规则来裁决:编号较小的数据流或通道,拥有更高的优先级。例如,DMA1的优先级高于DMA2。

1.6  数据宽度与对齐

        根据下表,简单来说就是右对齐,要是目标宽度不够,取最低位(可以参考第四行),要是目标宽度比源端宽度大则高位补零(可以参考第二或者三行):

2.  工程配置

        还是一样,重复的配置,这里不再一步一步演示步骤,详细可以参考:

STM32学习·HAL库速通篇(三)·如何在STM32CubeMX新建工程_stm32cubemx 配置cmsis库-CSDN博客

        只进行一些上面没有的配置,现在我们想要配置串口1,找到图示位置进行配置:

​​

  • Disable:禁用
  • Asynchronous:异步模式
  • Synchronous:同步模式
  • Single Wire (Half-Duplex):单线(半双工)模式
  • Multiprocessor Communication:多处理器通讯模式
  • IrDA:红外数据协会(红外通讯)模式
  • LIN:本地互连网络(LIN 总线)模式
  • SmartCard:智能卡模式

        然后配置数据通讯协议相关参数,这里直接使用默认参数:

​​

        波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,单位为波特。比特率指单位时间内传输的比特数,单位 bit/s (bps)。对于 USART 波特率与比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。

USART 的发送器和接收器使用相同的波特率。计算公式如下:

​​

        其中,fPLCK​ 为 USART 时钟,USARTDIV 是一个存放在波特率寄存器 (USART_BRR) 的一个无符号定点数。其中 DIV_Mantissa [11:0] 位定义 USARTDIV 的整数部分,DIV_Fraction [3:0] 位定义 USARTDIV 的小数部分。

        然后找到图示位置勾选上:

        找到图示位置对相关引脚进行 DMA 配置:

        其相关参数如下:

        这里来大概解释一下相关作用:

  • 触发源:就是靠什么触发,是发送还是接收数据进行触发;
  • 对应通道:主要看当前触发源对应的是哪个通道,详细可以参考上面的表格;
  • 传输方向:就是数据从哪里传到哪里;
  • 优先级:就是当前通道数据传输的优先级;
  • 模式选择:选择循环模式或者单次触发模式;
  • 地址是否自增:怎么理解呢,就是假如有三个数据传过来,如果地址不进行自增,那么后面的数据就会覆盖前面的数据,导致数据确实,不过对于数据的读取,我们例如串口我们都是从DR寄存器读取数据,因此不需要自增;
  • 数据宽度:指定一次转运要按多大的数据宽度来进行,其可以选择字节(uint8_t),半字(uint16_t),字(uint32_t)。


        详细一点的解释:

2.1  触发源

        触发源本质是哪个外设的哪个操作会启动 DMA 传输,比如 “USART1_RX” 作为触发源,意思是:当 USART1 的接收数据寄存器(DR)有新数据时,硬件自动触发 DMA 开始搬运这个字节。

        一些特殊事件比如:

  • UART_RX 触发:UART 接收寄存器非空(RXNE 事件)
  • UART_TX 触发:UART 发送寄存器空(TXE 事件)
  • ADC 触发:ADC 转换完成(EOC 事件)

        触发源是 “硬件级” 的,不需要 CPU 干预,外设事件直接唤醒 DMA 工作。

2.2  对应通道

        MCU 的 DMA 有多个 “通道(Channel)”,每个通道只能绑定一个 “触发源”(一对一映射),这是芯片硬件设计死的,不能改,比如 STM32F103 中:USART1_RX 只能用 DMA1 Channel5,USART2_RX 只能用 DMA1 Channel6。

        那么我们为什么要设置这么多通道呢?那是因为不同通道可以设置不同优先级,避免多个 DMA 同时工作时冲突,因此配置时必须选对通道,否则 DMA 收不到外设的触发信号,完全不工作。

2.3  传输方向

DMA 只有 3 种方向,且都是 “单向” 的:

外设→存储器(Peripheral To Memory):最常用(如 UART 接收、ADC 采样)
        UART 接收场景:UART 的 DR 寄存器(外设) → 内存缓冲区(数组 / 变量)

存储器→外设(Memory To Peripheral):常用(如 UART 发送、DAC 输出)
        UART 发送场景:内存缓冲区 → UART 的 DR 寄存器

存储器→存储器(Memory To Memory):极少用(纯内存数据搬运,不如 CPU 直接复制快)

2.4  优先级

        优先级是多个DMA 通道同时请求传输时,谁先工作,分为 4 级:Low < Medium < High < Very High。

注意:优先级只对 “同一 DMA 控制器(如 DMA1)” 的通道有效,DMA1 和 DMA2 之间的优先级由芯片硬件决定(一般 DMA2 > DMA1)。

2.5  模式选择

正常模式(Normal):

  • 传输完 “预设的字节数” 后,DMA 自动停止,不会再响应外设的触发信号;
  • 缺点:UART 接收时,若数据长度超过预设值,超出部分会丢失;
  • 适用场景:固定长度数据传输(比如每次传 8 个字节)。

循环模式(Circular):

  • 传输完预设字节数后,DMA 不停止,自动把 “内存地址” 重置到起始位置,继续接收;
  • 核心优势:配合 UART 空闲中断,实现 “无丢失接收任意长度数据”;
  • UART 接收必选:如果用 Normal 模式,缓冲区满了就停,后续数据会覆盖或丢失;用 Circular 模式,数据会循环存入缓冲区,直到空闲中断触发后,CPU 再去读有效数据。

2.6  地址是否自增

外设地址(Peripheral Address):指外设的寄存器地址(如 UART 的 DR 寄存器地址:0x40013804);
内存地址(Memory Address):指 RAM 中的缓冲区地址(如 uint8_t rx_buf [100] 的起始地址)。

2.7  数据宽度

        数据宽度是DMA 单次搬运的数据位数,必须和外设的数据格式匹配:

数据宽度 对应类型 适用场景
字节(Byte) uint8_t UART(默认 8 位数据)、I2C/SPI 字节传输
半字(Half Word) uint16_t ADC(12 位数据)、16 位传感器数据
字(Word) uint32_t 32 位寄存器数据搬运(极少用)

        这是为什么呢?例如:UART 默认传输 8 位数据(1 个字节),如果选 “半字”,DMA 会每次搬 2 个字节,但 UART 只给 1 个字节,导致数据错位(比如收到 0x12,DMA 会读成 0x0012),解析数据时全错。


        然后配置文件名称等信息,生成代码:

        可以看到这里已经初始化完成相关信息:

3.  函数介绍

        这里主要运行如下三个函数:

功能 HAL API 说明
空闲中断 + DMA 接收 HAL_UARTEx_ReceiveToIdle_DMA() 启动 DMA 接收,支持空闲中断(Idle Line),可解决不定长接收问题
发送数据(DMA 方式) HAL_UART_Transmit_DMA() 使用 DMA 方式发送数据
DMA 接收完成回调函数 HAL_UARTEx_RxEventCallback() 当接收完成或检测到空闲中断时自动调用,返回实际接收数据长度

3.1  HAL_UARTEx_ReceiveToIdle_DMA()函数

        该函数既可以接收满指定长度的数据,也可以在数据传输中断(触发 IDLE)时提前终止接收,适合处理 “不定长数据”(如串口透传、指令接收):

        简单翻译了解一下:

/**
  * @brief  以DMA模式接收一定量的数据,直到接收到预期数量的数据或触发IDLE空闲事件为止。
  * @note   接收过程由本函数调用触发。接收的后续过程由DMA服务完成:DMA会自动将接收到的
  *         数据元素传输到用户指定的接收缓冲区,并在接收过半/接收完成时调用已注册的回调函数。
  *         UART的IDLE空闲事件也会被用作判定接收阶段结束的依据。在所有情况下,回调函数执行时
  *         都会告知实际接收到的数据元素数量。
  * @note   当UART校验位使能时(PCE = 1),接收到的数据包含校验位(位于最高位MSB)。
  * @note   当UART校验位未使能时(PCE = 0),且字长配置为9位(M = 01),接收到的数据会被
  *         当作uint16_t类型的集合处理。这种情况下,Size参数必须指明pData缓冲区中可用的uint16_t元素数量。
  * @param  huart  UART外设句柄(包含UART配置、状态、回调函数等核心信息)。
  * @param  pData  指向数据缓冲区的指针(数据元素类型为uint8_t或uint16_t)。
  * @param  Size   期望接收的数据元素数量(单位:uint8_t或uint16_t,需与pData类型匹配)。
  * @retval HAL状态码(HAL_OK/HAL_ERROR/HAL_BUSY等)。
  */
// 定义函数返回值类型为HAL状态码,函数名:HAL_UARTEx_ReceiveToIdle_DMA
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  // 声明局部变量,存储函数最终返回的HAL状态
  HAL_StatusTypeDef status;

  /* 检查当前是否已有Rx(接收)过程在进行中 */
  if (huart->RxState == HAL_UART_STATE_READY)  // 若UART接收状态为“就绪”(无正在进行的接收)
  {
    // 合法性检查:缓冲区指针为空 或 期望接收的数量为0 → 非法参数
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;  // 返回“错误”状态
    }

    /* 将接收类型设置为“直到IDLE事件的接收模式” */
    huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;
    // 初始化接收事件类型为“传输完成(TC)”(后续可被IDLE事件覆盖)
    huart->RxEventType = HAL_UART_RXEVENT_TC;

    // 调用底层函数启动DMA接收,传入句柄、缓冲区、接收数量,返回启动状态
    status =  UART_Start_Receive_DMA(huart, pData, Size);

    /* 检查Rx(接收)过程是否已成功启动 */
    if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
    {
      // 清除UART的IDLE空闲标志位(防止之前的残留标志干扰)
      __HAL_UART_CLEAR_IDLEFLAG(huart);
      // 原子操作:置位UART CR1寄存器的IDLEIE位 → 使能IDLE空闲中断
      // (IDLE事件发生时会触发中断,进而终止DMA接收)
      ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
    }
    else
    {
      /* 若接收启动时已有错误待处理(例如溢出错误Overrun),
         中断可能已被触发并导致接收终止,此时接收类型会被重置为“标准接收模式” */
      status = HAL_ERROR;  // 接收启动失败,返回错误状态
    }

    return status;  // 返回最终的接收启动状态
  }
  else  // 若UART接收状态非“就绪”(已有接收过程在运行)
  {
    return HAL_BUSY;  // 返回“忙”状态
  }
}

3.2  HAL_UART_Transmit_DMA()函数

        函数提前注册了 “传输完成”“传输过半”“传输错误” 回调,这些回调会在 DMA 完成对应阶段后自动触发(如全部数据发送完成后调用UART_DMATransmitCplt),你可以在回调中处理发送完成后的逻辑(如标记发送状态、启动下一次发送):

        启动 UART 的 DMA 发送流程:初始化发送参数→配置 DMA 回调→启动 DMA 中断传输→使能 UART DMA 请求,全程无需 CPU 主动参与数据搬运:

/**
  * @brief  以DMA模式发送指定数量的数据。
  * @note   当UART校验位未使能时(PCE = 0),且字长配置为9位(M1-M0 = 01),
  *         待发送的数据会被当作uint16_t(简写u16)类型的集合处理。这种情况下,Size参数必须指明
  *         通过pData缓冲区提供的uint16_t元素数量。
  * @param  huart  指向UART_HandleTypeDef结构体的指针,该结构体包含指定UART模块的所有配置信息
  *                (如波特率、字长、校验位、DMA句柄、状态标志等)。
  * @param  pData  指向数据缓冲区的指针(数据元素类型为uint8_t(简写u8)或uint16_t(简写u16))。
  * @param  Size   待发送的数据元素数量(单位:uint8_t或uint16_t,需与pData类型匹配)。
  * @retval HAL状态码(HAL_OK/HAL_ERROR/HAL_BUSY等)。
  */
// 定义函数返回值为HAL状态码,函数功能:UART DMA模式发送数据
// 参数说明:huart-UART句柄,pData-发送缓冲区(const表示缓冲区只读,防止函数内篡改),Size-发送数据长度
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
{
  // 声明临时指针变量,用于处理不同数据类型(u8/u16)的地址转换
  const uint32_t *tmp;

  /* 检查当前是否已有Tx(发送)过程在进行中 */
  if (huart->gState == HAL_UART_STATE_READY)  // 若UART全局状态为“就绪”(无正在进行的发送/接收)
  {
    // 合法性校验:发送缓冲区为空 或 发送长度为0 → 参数非法
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;  // 返回“错误”状态码
    }

    // ========== 初始化UART句柄的发送相关参数 ==========
    huart->pTxBuffPtr = pData;       // 记录发送缓冲区的起始地址
    huart->TxXferSize = Size;        // 记录本次需要发送的总数据长度
    huart->TxXferCount = Size;       // 记录剩余待发送的数据长度(初始值=总长度)

    huart->ErrorCode = HAL_UART_ERROR_NONE;  // 清空错误码(初始化为“无错误”)
    huart->gState = HAL_UART_STATE_BUSY_TX;  // 将UART全局状态置为“忙-发送中”,防止重复启动

    // ========== 配置DMA回调函数 ==========
    /* 设置UART DMA发送完成回调函数 */
    huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;
    /* 设置UART DMA发送过半回调函数(发送到总长度一半时触发) */
    huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;
    /* 设置DMA错误回调函数(DMA传输出错时触发) */
    huart->hdmatx->XferErrorCallback = UART_DMAError;
    /* 设置DMA中止回调函数(此处置空,表示不处理DMA中止事件) */
    huart->hdmatx->XferAbortCallback = NULL;

    // ========== 启动DMA传输 ==========
    /* 类型转换:将pData(u8/u16指针)转为uint32_t类型指针,适配DMA地址参数要求
       (DMA的地址参数为uint32_t,需统一地址宽度) */
    tmp = (const uint32_t *)&pData;
    /* 启动DMA中断模式传输:
       参数1:UART的发送DMA句柄
       参数2:数据源地址(发送缓冲区首地址)
       参数3:数据目标地址(UART的数据寄存器DR地址,DMA直接将数据写入DR)
       参数4:传输数据长度
       注:启动后DMA会自动将缓冲区数据逐个写入UART_DR,无需CPU干预 */
    HAL_DMA_Start_IT(huart->hdmatx, *(const uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);

    /* 清除SR寄存器中的TC(传输完成)标志位(写入0清除)
       防止之前的残留TC标志干扰本次发送的状态判断 */
    __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC);

    /* 使能UART的DMA发送请求:置位UART CR3寄存器的DMAT位
       该位使能后,UART会在DR寄存器空时自动向DMA发起数据传输请求 */
    ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);

    return HAL_OK;  // 发送流程启动成功,返回“正常”状态码
  }
  else  // 若UART全局状态非“就绪”(已有发送/接收过程在运行)
  {
    return HAL_BUSY;  // 返回“忙”状态码
  }
}

3.3  HAL_UARTEx_RxEventCallback()函数

        该函数的核心作用是在接收完成 / IDLE 触发时告知用户实际接收的数据长度:

该回调函数会在以下两种场景被调用:
场景 1:DMA 接收满Size指定的长度(传输完成 TC 事件);
场景 2:接收过程中触发 UART IDLE 空闲事件(无数据传输超过 1 个字符时间)。
        两种场景下,Size参数都会传入实际接收到的字节数(场景 1 等于预设长度,场景 2 小于预设长度)。

/**
  * @brief  接收事件回调函数(使用高级接收服务后,用于通知Rx事件的回调函数)。
  * @note   这里的“高级接收服务”特指如HAL_UARTEx_ReceiveToIdle_DMA这类带IDLE事件终止的接收函数,
  *         区别于普通的HAL_UART_Receive_DMA。
  * @param  huart  UART外设句柄(标识发生接收事件的具体UART端口,如USART1/USART2等)。
  * @param  Size   应用层接收缓冲区中实际可用的数据数量(即缓冲区中已接收到的有效数据长度,
  *                指示缓冲区中数据有效到哪个位置)。
  * @retval 无返回值。
  */
// __weak:弱定义修饰符,核心作用是“用户可重定义”——用户在自己的代码中定义同名函数时,
// 编译器会优先使用用户定义的版本,覆盖这个默认的空实现
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  /* 防止未使用参数导致的编译警告 */
  // UNUSED:HAL库宏,本质是((void)参数),告诉编译器“该参数虽未使用,但并非疏忽”,屏蔽警告
  UNUSED(huart);
  UNUSED(Size);

  /* 注意:本函数不应被修改。当需要使用该回调功能时,
           应在用户代码文件中重新实现HAL_UARTEx_RxEventCallback函数。
   */
}

4.  代码编写

        首先我们先进行 printf () 的重定向,C 语言中的 printf () 默认输出到显示器,在嵌入式中没有显示器,需要重定向到串口通过重写 fputc () 函数,把 printf () 输出的字符通过串口发出去:

#include <stdio.h>
 
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

        直接将代码复制进去即可:

        然后找到图示位置勾选上,否则 printf () 可能无法正常使用:

        然后初始化,接收缓冲区:

#define LENGTH   64//接收缓冲区大小

uint8_t RxBuff[LENGTH];//接收缓冲区

        调用上方介绍函数,使能DMA接收事件:

	HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxBuff, LENGTH);//使能DMA接收事件
	printf("当前已开启接收中断,等待数据发送!!!\r\n");

        然后调用回到函数:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	if(huart -> Instance == USART1)
	{
		printf("已经接收到长度为%d字节的数据,数据数据如下:",Size);

		HAL_UART_Transmit_DMA(&huart1, RxBuff, Size);//回显发送数据

		HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxBuff, LENGTH);//使能DMA接收事件
	}
}

        通过串口调试助手看一下:

完整工程:

基于STM32实现串口(USART)DMA收发数据(HAL库版本)资源-CSDN下载

HAL库学习笔记_时光の尘的博客-CSDN博客

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐