STM32学习·HAL库速通篇(十)·串口(USART)通过DMA收发不定长数据

目录
3.1 HAL_UARTEx_ReceiveToIdle_DMA()函数
3.3 HAL_UARTEx_RxEventCallback()函数
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接收事件
}
}
通过串口调试助手看一下:

完整工程:


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


所有评论(0)