目录
[1. 句柄(Handle)为中心](#1. 句柄(Handle)为中心)
[2. 三种编程模式](#2. 三种编程模式)
[3. 弱函数回调机制](#3. 弱函数回调机制)
[1. 使用STM32CubeMX初始化](#1. 使用STM32CubeMX初始化)
[2. 常用HAL函数命名规律](#2. 常用HAL函数命名规律)
[3. 重要的回调函数](#3. 重要的回调函数)
[1. 宏定义配置](#1. 宏定义配置)
[2. 初始化函数](#2. 初始化函数)
[1. HAL库的DMA接收机制](#1. HAL库的DMA接收机制)
[3. 中断处理链](#3. 中断处理链)
[4. 用户中断处理 - 空闲中断检测](#4. 用户中断处理 - 空闲中断检测)
[5. 核心:空闲中断回调](#5. 核心:空闲中断回调)
[1. DMA传输计数器的使用](#1. DMA传输计数器的使用)
1.HAL库核心思想
1. 句柄(Handle)为中心
cs
// 每个外设都有一个句柄结构体
UART_HandleTypeDef huart2; // 串口句柄
TIM_HandleTypeDef htim3; // 定时器句柄
ADC_HandleTypeDef hadc1; // ADC句柄
// 包含该外设的所有配置和状态
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart2);
2. 三种编程模式
cs
// ① 轮询模式 - 最简单,阻塞
HAL_UART_Transmit(&huart2, data, size, timeout);
// ② 中断模式 - 非阻塞,需中断处理
HAL_UART_Transmit_IT(&huart2, data, size); // 启动传输
// 完成后自动调用回调 HAL_UART_TxCpltCallback()
// ③ DMA模式 - 最高效,CPU干预少
HAL_UART_Transmit_DMA(&huart2, data, size);
3. 弱函数回调机制
cs
// 在stm32f1xx_hal_uart.c中定义的是弱函数
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
/* 用户可重写此函数 */
}
// 在你的代码中重写
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART2) {
// 处理USART2接收完成
HAL_UART_Receive_IT(&huart2, buffer, 1); // 重新开启接收
}
}
2.快速上手
1. 使用STM32CubeMX初始化
cs
// CubeMX会为你生成:
// - 所有时钟配置
// - 外设初始化代码
// - 引脚复用设置
// 这是最快的方式,推荐先用CubeMX生成框架
2. 常用HAL函数命名规律
cs
// 前缀规律
HAL_xxx_Init // 初始化
HAL_xxx_DeInit // 反初始化
HAL_xxx_MspInit // 底层初始化(引脚/时钟/RCC)
HAL_xxx_Transmit/Receive // 数据传输
HAL_xxx_Start/Stop // 启动/停止
HAL_xxx_IRQHandler // 中断处理入口
// 后缀规律
_IT // 中断模式
_DMA // DMA模式
_Ex // 扩展功能
3. 重要的回调函数
cs
// 初始化回调
void HAL_xxx_MspInit(XXX_HandleTypeDef* hxxx) // 引脚、时钟配置
// 完成回调
void HAL_xxx_TxCpltCallback(XXX_HandleTypeDef* hxxx) // 发送完成
void HAL_xxx_RxCpltCallback(XXX_HandleTypeDef* hxxx) // 接收完成
// 错误回调
void HAL_xxx_ErrorCallback(XXX_HandleTypeDef* hxxx) // 错误处理
示例
cs
// 标准库
USART_SendData(USART2, 'A');
while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
// HAL库 - 轮询
HAL_UART_Transmit(&huart2, (uint8_t*)"A", 1, 1000);
// HAL库 - 中断
uint8_t data[] = "Hello";
HAL_UART_Transmit_IT(&huart2, data, strlen(data));
// 在HAL_UART_TxCpltCallback中检查完成
注意
1.必须调用HAL_Init()
初始化systick和分组
2.中断处理函数要重定向
cs
void USART2_IRQHandler(void) {
HAL_UART_IRQHandler(&huart2); // 必须调用这个
}
3.接收中断需不断重启
cs
// 在接收回调中重新开启接收
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
HAL_UART_Receive_IT(&huart2, rx_buffer, 1); // 再次开启
}
2.HAL+DMA+空闲中断
串口接收不定长数据
工作流程:
DMA自动把串口数据搬运到缓冲区(无需CPU干预)
空闲中断表示"数据包结束"
计算实际接收长度,处理数据
重新准备接收下一包
1. 宏定义配置
cs
#define hdma_usartx_rx hdma_usart2_rx
#define IndefLenUsart huart2
#define IndefLenUsartName USART2
这是为了方便移植,如果你想换到USART1,只需要改这几行宏定义即可。
cs
// 这三个宏分别代表:
// 1. hdma_usartx_rx → DMA句柄(用于操作DMA)
// 2. IndefLenUsart → UART句柄(用于HAL库函数)
// 3. IndefLenUsartName → UART外设(用于判断中断来源)
// 它们在代码中的使用场景:
void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 使用 IndefLenUsartName 判断中断来源
if (huart->Instance == IndefLenUsartName) // → USART2
{
if (RESET != __HAL_UART_GET_FLAG(&IndefLenUsart, UART_FLAG_IDLE))
{ // → &huart2
__HAL_UART_CLEAR_IDLEFLAG(&IndefLenUsart); // → &huart2
USAR_UART_IDLECallback(huart);
}
}
}
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
HAL_UART_DMAStop(&IndefLenUsart); // → &huart2
// 使用 hdma_usartx_rx 获取DMA计数器
uint8_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
// → &hdma_usart2_rx
Data_Handle(receive_buff, data_length);
memset(receive_buff, 0, data_length);
// 重新启动DMA
HAL_UART_Receive_DMA(&IndefLenUsart, receive_buff, 255); // → &huart2
}
2. 初始化函数
cs
void USAR_IndefLenUsart_Init(void)
{
// 启用串口的空闲中断
__HAL_UART_ENABLE_IT(&IndefLenUsart, UART_IT_IDLE);
// 启动DMA接收,目标地址是receive_buff,最大255字节
HAL_UART_Receive_DMA(&IndefLenUsart, (uint8_t *)receive_buff, 255);
}
HAL库特色:
__HAL_UART_ENABLE_IT()这种__HAL_前缀的宏用于直接操作寄存器HAL_UART_Receive_DMA()是HAL库的DMA接收函数
1. HAL库的DMA接收机制
cs
// 启动DMA接收时:
HAL_UART_Receive_DMA(&huart2, buffer, size);
// 这意味着:DMA会在后台自动把串口数据写入buffer
// 直到:
// - 接收满size字节 → 触发传输完成中断
// - 出现错误 → 触发错误中断
// - 用户主动停止(如本代码的空闲中断)
2.接收中断和空闲中断
cs
// 接收中断 (RXNE - Read Data Register Not Empty)
// 每来一个字节就中断一次
字节1到达 → 中断 → 处理 → 返回
字节2到达 → 中断 → 处理 → 返回
字节3到达 → 中断 → 处理 → 返回
...
// CPU频繁进出中断,效率很低
// 空闲中断 (IDLE - Bus Idle)
// 只在总线空闲时中断一次
字节1 字节2 字节3 ... 字节n → 总线空闲 → 中断 → 一次性处理所有数据
// 一次中断处理整包数据,效率高
3.函数
// 函数原型:
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size)
// 参数1: &IndefLenUsart - 串口句柄(指向huart2)
// 参数2: receive_buff - 接收缓冲区(数据存放地址)
// 参数3: 255 - 要接收的字节数
3. 中断处理链
cs
// 在stm32f1xx_it.c中的中断服务函数
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart2); // HAL库的标准中断处理
USER_UART_IRQHandler(&huart2); // 用户自定义处理
}
这是HAL库中断处理的标准模式:
- 必须先调用
HAL_UART_IRQHandler()处理基本的HAL库中断 - 再调用用户函数处理特殊需求
中断出发后完整的执行流程
cs
/* 硬件检测到串口事件 → 触发中断 → 以下函数依次执行 */
// ===== 第1层:中断向量表(硬件固定)=====
void USART2_IRQHandler(void) // 这个名字是固定的,在启动文件中定义
{
// 这是中断的入口,硬件自动调用
}
// ===== 第2层:用户中断处理(你写的代码)=====
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart2); // 调用HAL库的中断处理
// 可能还有其他用户代码
}
// ===== 第3层:HAL库中断处理(ST写的代码)=====
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 检查各种中断标志
if(RXNE中断) {
// 处理接收
HAL_UART_RxCpltCallback(huart); // 调用回调
}
if(TXE中断) {
// 处理发送
}
if(错误中断) {
HAL_UART_ErrorCallback(huart); // 调用回调
}
// 注意:空闲中断不处理!
}
// ===== 第4层:回调函数(你重写的代码)=====
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 你写的接收完成后的处理代码
// 当HAL库处理完接收中断后,会自动调用这里
}
1.HAL库的中断处理流程
cs
// HAL库的中断处理函数内部大致是这样的:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 检查各种中断标志
if(/* 接收完成中断 */) {
HAL_UART_RxCpltCallback(huart); // 用户可重写
}
if(/* 发送完成中断 */) {
HAL_UART_TxCpltCallback(huart); // 用户可重写
}
// 但注意:空闲中断需要用户自己处理!
}
cs
/* HAL库的设计原则:提供基础,不限制应用 */
// HAL库提供的回调:
// - TxCpltCallback → 发送完成(硬件事件)
// - RxCpltCallback → 接收完成(硬件事件)
// - ErrorCallback → 错误发生(硬件事件)
// HAL库不提供的:
// - IdleCallback → 空闲状态(应用层概念)
// - FrameCallback → 帧完成(协议层概念)
// - CommandCallback → 命令解析(应用层概念)
// 因为硬件事件是确定的,应用层事件是变化的
4. 用户中断处理 - 空闲中断检测
cs
void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 判断是否是我们的目标串口
if (huart->Instance == IndefLenUsartName)
{
// 检查空闲中断标志
if (RESET != __HAL_UART_GET_FLAG(&IndefLenUsart, UART_FLAG_IDLE))
{
__HAL_UART_CLEAR_IDLEFLAG(&IndefLenUsart); // 必须清除标志
USAR_UART_IDLECallback(huart); // 处理数据
}
}
}
5. 核心:空闲中断回调
cs
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
// 1. 停止DMA传输(防止继续接收)
HAL_UART_DMAStop(&IndefLenUsart);
// 2. 关键点:计算实际接收了多少字节
// BUFFER_SIZE是设置的255 - DMA剩余计数 = 已接收字节数
uint8_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
// 3. 处理接收到的数据
Data_Handle(receive_buff, data_length);
// 4. 清空缓冲区,准备下一次接收
memset(receive_buff, 0, data_length); // 只清空使用的部分
// 5. 重新启动DMA接收
HAL_UART_Receive_DMA(&IndefLenUsart, (uint8_t *)receive_buff, 255);
}
1. DMA传输计数器的使用
cs
// __HAL_DMA_GET_COUNTER 这个宏很关键!
// 初始值:255
// 每接收1字节,计数器减1
// 实际接收长度 = 255 - 当前计数器值
uint8_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
2.关闭的是DMA
cs
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
// 这里关闭的是 DMA,不是 USART!
HAL_UART_DMAStop(&IndefLenUsart); // 停止DMA传输
// 注意:USART2本身还在工作!
// → USART2的DMA请求被禁止了 ✅
// - 如果有新数据来,USART2的DR寄存器仍然会更新
// - 只是DMA不再自动搬运数据到内存了
关闭前后状态:

实例:完整数据传输流程图解
┌─────────────────────────────────────┐
│ 数据传输完整流程图 │
└─────────────────────────────────────┘
发送端发送数据: "Hello STM32" (12字节)
↓
【硬件层面】USART2 RX引脚接收数据
↓
【DMA传输】自动将数据存入receive_buff
↓
【空闲中断】检测到总线空闲
↓
【数据处理】CPU介入处理数据
步骤1:初始化配置
cs
void USAR_IndefLenUsart_Init(void)
{
// 使能空闲中断 - 当串口空闲时触发中断
__HAL_UART_ENABLE_IT(&IndefLenUsart, UART_IT_IDLE);
// 启动DMA传输,设置传输目标地址和大小
// DMA会在后台自动将USART2->DR寄存器的数据搬运到receive_buff
HAL_UART_Receive_DMA(&IndefLenUsart, (uint8_t *)receive_buff, 255);
}
// 初始化后状态:
// DMA通道已开启,等待数据
// receive_buff[255] = {0,0,0,...} 全部为0,没有实现❌️
// DMA计数器 = 255 (剩余可接收字节数)
步骤2:数据接收过程(以接收"Hello"为例)
cs
// 假设发送端发送: 'H' 'e' 'l' 'l' 'o' (5个字节)
/* 字节1: 'H' 到达 */
USART2->DR = 'H' // 硬件自动将数据存入数据寄存器
DMA检测到DR有数据 → 自动搬运 → receive_buff[0] = 'H'
DMA计数器减1 → 254
/* 字节2: 'e' 到达 */
USART2->DR = 'e'
DMA自动搬运 → receive_buff[1] = 'e'
DMA计数器减1 → 253
/* 字节3: 'l' 到达 */
USART2->DR = 'l'
DMA自动搬运 → receive_buff[2] = 'l'
DMA计数器减1 → 252
/* 字节4: 'l' 到达 */
USART2->DR = 'l'
DMA自动搬运 → receive_buff[3] = 'l'
DMA计数器减1 → 251
/* 字节5: 'o' 到达 */
USART2->DR = 'o'
DMA自动搬运 → receive_buff[4] = 'o'
DMA计数器减1 → 250
// 此时内存状态:
receive_buff = ['H','e','l','l','o',0,0,0,...]
DMA计数器 = 250
步骤3:空闲中断触发
cs
// 发送完"Hello"后,总线上没有新数据(空闲状态)
// 硬件检测到空闲,触发空闲中断
void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 判断是USART2的空闲中断
if (huart->Instance == IndefLenUsartName)
{
if (RESET != __HAL_UART_GET_FLAG(&IndefLenUsart, UART_FLAG_IDLE))
{
// 清除标志,防止重复进入
__HAL_UART_CLEAR_IDLEFLAG(&IndefLenUsart);
// 调用数据处理函数
USAR_UART_IDLECallback(huart);
}
}
}
步骤4:数据处理
cs
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
// 1. 停止DMA传输(防止新数据覆盖当前数据)
HAL_UART_DMAStop(&IndefLenUsart);
// 2. 计算实际接收长度
// 初始值255 - 当前剩余250 = 已接收5字节
uint8_t data_length = 255 - __HAL_DMA_GET_COUNTER(&hdma_usartx_rx);
// data_length = 5
// 3. 处理数据
// receive_buff = ['H','e','l','l','o',0,0,0...]
// data_length = 5
Data_Handle(receive_buff, data_length);
// 4. 清空已处理的数据
memset(receive_buff, 0, data_length);
// receive_buff = [0,0,0,0,0,0,0,0,...]
// 5. 重新启动DMA,准备接收下一包
HAL_UART_Receive_DMA(&IndefLenUsart, (uint8_t *)receive_buff, 255);
// DMA计数器重新变为255
}
步骤5:数据处理回调
cs
void Data_Handle(uint8_t *buff, uint8_t size)
{
uint8_t rx_buff[255] = {0};
// 1. 拷贝数据到本地缓冲区(防止原始数据被修改)
memcpy(rx_buff, buff, size);
// rx_buff = ['H','e','l','l','o',0,0,0...]
// 2. 处理数据(这里转发到串口1作为测试)
// HAL_UART_Transmit(&huart1, rx_buff, size, 0x200);
// 3. 调用上层应用处理函数
ProcessRx(rx_buff, size); // 这里应该是你的业务逻辑
}

