Hal库的使用

目录

1.HAL库核心思想

[1. 句柄(Handle)为中心](#1. 句柄(Handle)为中心)

[2. 三种编程模式](#2. 三种编程模式)

[3. 弱函数回调机制](#3. 弱函数回调机制)

2.快速上手

[1. 使用STM32CubeMX初始化](#1. 使用STM32CubeMX初始化)

[2. 常用HAL函数命名规律](#2. 常用HAL函数命名规律)

[3. 重要的回调函数](#3. 重要的回调函数)

1.必须调用HAL_Init()

2.中断处理函数要重定向

3.接收中断需不断重启

2.HAL+DMA+空闲中断

[1. 宏定义配置](#1. 宏定义配置)

​编辑

[2. 初始化函数](#2. 初始化函数)

[1. HAL库的DMA接收机制](#1. HAL库的DMA接收机制)

2.接收中断和空闲中断

3.函数

[3. 中断处理链](#3. 中断处理链)

1.HAL库的中断处理流程

[4. 用户中断处理 - 空闲中断检测](#4. 用户中断处理 - 空闲中断检测)

[5. 核心:空闲中断回调](#5. 核心:空闲中断回调)

[1. DMA传输计数器的使用](#1. DMA传输计数器的使用)

2.关闭的是DMA

实例:完整数据传输流程图解

步骤1:初始化配置

步骤2:数据接收过程(以接收"Hello"为例)

步骤3:空闲中断触发

步骤4:数据处理

步骤5:数据处理回调


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+空闲中断

串口接收不定长数据

工作流程:

  1. DMA自动把串口数据搬运到缓冲区(无需CPU干预)

  2. 空闲中断表示"数据包结束"

  3. 计算实际接收长度,处理数据

  4. 重新准备接收下一包

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);  // 这里应该是你的业务逻辑
}
相关推荐
羽获飞2 小时前
从零开始学嵌入式之STM32——13.使用STM32自带硬件模块实现IIC协议通讯
单片机·嵌入式硬件
单片机设计星球2 小时前
51单片机的【智能婴儿床】仿真设计
单片机·嵌入式硬件·51单片机
San_a dreamer fish4 小时前
STM32开发入门基础篇知识要点
stm32·单片机·嵌入式硬件
安庆平.Я5 小时前
STM32——FreeRTOS - 移植
stm32·单片机·嵌入式硬件
HIZYUAN6 小时前
AG32 MCU可以替代STM32+CPLD吗(一)
stm32·单片机·嵌入式硬件
Lester_11016 小时前
STM32 定时器驱动电机时,定时器编码器输入通道引脚模式为什么设置为输出开漏,不应该是输入模式吗
stm32·单片机·嵌入式硬件
逐步前行6 小时前
STM32_GPIO_HAL库操作
stm32·单片机·嵌入式硬件
深圳元器猫6 小时前
无源声表谐振器 - 智能物联网声表滤波器解决方案
单片机·嵌入式硬件
脱离语言7 小时前
51单片机开发——运行Demo
单片机·51单片机