当前位置: 首页 > news >正文

STM32HAL 飞快入门(十九):UART 编程(二)—— 中断方式实现收发及局限分析

STM32HAL 飞快入门(十九):UART 编程(二)—— 中断方式实现收发及局限分析

前言

大家好,这里是 Hello_Embed。上一篇我们用查询方式实现了 UART 收发,但存在 “数据不及时读取易丢失” 的问题。本篇将介绍中断方式—— 通过硬件中断主动通知 CPU 处理收发,减少 CPU 资源占用,同时分析其在复杂场景下的局限性,为下一篇 “中断 + 环形缓冲区” 的改进方案铺垫。

一、中断方式的核心优势与函数

中断方式的核心是 “硬件触发中断,CPU 仅在需要时处理”,无需轮询状态寄存器,显著提升效率。HAL 库中 UART 中断相关的核心函数如下:

功能函数作用回调函数(中断完成后触发)
中断发送HAL_UART_Transmit_IT启动中断发送,配置后立即返回HAL_UART_TxCpltCallback(发送完成)
中断接收HAL_UART_Receive_IT启动中断接收,配置后立即返回HAL_UART_RxCpltCallback(接收完成)
二、CubeMX 配置:使能 UART 中断

在查询方式配置的基础上,只需额外使能 UART 中断(NVIC),步骤如下:

  1. 进入 “Connectivity→USART1→NVIC Settings”,勾选 “Enabled” 使能 USART1 中断:
    请添加图片描述

  2. 其他配置(波特率 115200、8 位数据位等)与查询方式一致,生成代码。

三、中断发送:从启动到完成的流程

中断发送的核心是 “CPU 启动发送后即可处理其他任务,发送完成后通过回调函数通知”,具体流程如下:

1. 启动中断发送:HAL_UART_Transmit_IT

函数定义与核心逻辑:

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
{
if (huart->gState == HAL_UART_STATE_READY) // 检查UART是否空闲
{
huart->pTxBuffPtr = pData;
// 记录发送数据地址
huart->TxXferSize = Size;
// 总长度
huart->TxXferCount = Size;
// 剩余长度(初始等于总长度)
huart->gState = HAL_UART_STATE_BUSY_TX;
// 标记为发送中
__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);
// 使能TXE中断(TDR寄存器空)
return HAL_OK;
}
return HAL_BUSY;
// 若UART忙碌,返回忙状态
}

关键:函数仅配置参数并使能中断,不直接发送数据,实际发送由 TXE 中断完成。

2. 中断服务函数:数据发送的核心执行

当 TDR 寄存器为空(数据已转移到移位寄存器)时,触发 TXE 中断,执行流程如下:

  1. 中断入口:USART1_IRQHandler(异常向量表中的串口 1 中断入口);
    请添加图片描述

  2. 跳转至 HAL 库通用处理函数:HAL_UART_IRQHandler(&huart1)

  3. 核心发送逻辑(UART_Transmit_IT):

// 从缓冲区取1字节写入TDR寄存器
huart->Instance->DR = (uint8_t)(*huart->pTxBuffPtr++ &
0x00FF);
huart->TxXferCount--;
// 剩余长度减1
if (huart->TxXferCount == 0) // 若所有数据发送完成
{
__HAL_UART_DISABLE_IT(huart, UART_IT_TXE);
// 关闭TXE中断
__HAL_UART_ENABLE_IT(huart, UART_IT_TC);
// 使能TC中断(发送完成)
}
  • TXE 中断:每发送 1 字节触发一次(除最后 1 字节),共触发Size-1次;
  • TC 中断:最后 1 字节从移位寄存器发送完成后触发,标记整个发送流程结束。
3. 发送完成回调:HAL_UART_TxCpltCallback

TC 中断触发后,HAL 库会调用发送完成回调函数(需用户重定义,默认是weak弱函数),用于通知 “发送已完成”。
usart.c中重定义回调函数:

/* USER CODE BEGIN 1 */
static volatile int g_tx_cplt = 0;
// 发送完成标志(volatile确保中断与主程序可见)
// 发送完成回调:置位标志
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1) // 确认是USART1
g_tx_cplt = 1;
}
// 等待发送完成:主程序中调用,避免轮询
void Wait_Tx_Complete(void)
{
while (g_tx_cplt == 0);
// 等待标志置位
g_tx_cplt = 0;
// 复位标志
}
/* USER CODE END 1 */
4. 主程序调用:中断发送示例

main.c中发送字符串,通过Wait_Tx_Complete等待发送完成:

/* USER CODE BEGIN PV */
extern void Wait_Tx_Complete(void);
// 声明等待函数
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
char *str = "Hello_Embed (Interrupt Tx)!\r\n";
/* USER CODE END 2 */
while (1)
{
// 启动中断发送
HAL_UART_Transmit_IT(&huart1, (uint8_t *)str, strlen(str));
Wait_Tx_Complete();
// 等待发送完成(不占用CPU,仅在完成后继续)
}

实验结果:串口工具可稳定接收字符串,证明中断发送成功:
请添加图片描述

四、中断接收:从启动到完成的流程

中断接收的逻辑与发送类似:启动接收后,CPU 可处理其他任务,接收完成后通过回调函数通知。

1. 启动中断接收:HAL_UART_Receive_IT

函数会使能 RXNE 中断(RDR 寄存器非空),等待数据到来:

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
// 类似发送函数,记录接收缓冲区地址、长度,使能RXNE中断
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
// 关键:使能接收非空中断
return HAL_OK;
}
2. 中断服务函数:数据接收的核心执行

当 RDR 寄存器有数据(接收完成 1 字节)时,触发 RXNE 中断,执行流程如下:

  1. 中断入口同样是USART1_IRQHandler,跳转至HAL_UART_IRQHandler
  2. 核心接收逻辑(UART_Receive_IT):
// 从RDR寄存器读取1字节到缓冲区
*huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->DR &
0x007F);
huart->RxXferCount--;
// 剩余接收长度减1
if (huart->RxXferCount == 0) // 若接收完成
{
__HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);
// 关闭RXNE中断
HAL_UART_RxCpltCallback(huart);
// 调用接收完成回调
}
3. 接收完成回调:HAL_UART_RxCpltCallback

需用户重定义,用于标记接收完成:

/* USER CODE BEGIN 1 */
static volatile int g_rx_cplt = 0;
// 接收完成标志
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1) // 确认是USART1
g_rx_cplt = 1;
}
// 等待接收完成
void Wait_Rx_Complete(void)
{
while (g_rx_cplt == 0);
// 等待标志置位
g_rx_cplt = 0;
// 复位标志
}
/* USER CODE END 1 */
4. 主程序调用:中断接收示例

目标:接收电脑发送的字符,加 1 后返回:

/* USER CODE BEGIN 2 */
char *str1 = "Please enter a char : \r\n";
char c;
// 存储接收的字符
extern void Wait_Tx_Complete(void);
extern void Wait_Rx_Complete(void);
/* USER CODE END 2 */
while (1)
{
// 发送提示信息
HAL_UART_Transmit_IT(&huart1, (uint8_t *)str1, strlen(str1));
Wait_Tx_Complete();
// 启动中断接收1字节
HAL_UART_Receive_IT(&huart1, (uint8_t *)&c, 1);
Wait_Rx_Complete();
// 等待接收完成
// 字符加1后返回
c += 1;
HAL_UART_Transmit_IT(&huart1, (uint8_t *)&c, 1);
Wait_Tx_Complete();
HAL_UART_Transmit_IT(&huart1, (uint8_t *)"\r\n", 2, 1000);
Wait_Tx_Complete();
}
五、中断方式的局限性:仍存在数据丢失

上述代码在简单场景下可工作,但快速发送多字节时,仍会出现数据丢失,原因如下:

  • 接收完成后,需重新调用HAL_UART_Receive_IT才能继续接收下一字节;
  • 若 CPU 正在执行耗时操作(如HAL_UART_Transmit_IT发送返回数据),未及时重新使能接收中断,新数据会覆盖 RDR 寄存器中的旧数据,导致丢失。
    例如,电脑快速发送 “123”,单片机仅收到 “1” ,只返回 “2”:
    请添加图片描述
总结

中断方式通过 “硬件触发 + 回调通知” 减少了 CPU 轮询的资源占用,比查询方式更高效,但单纯的中断接收仍存在缺陷 ——未及时重新使能中断会导致数据丢失。因此需要改进中断方式的接收函数,这也是我下一篇笔记的内容,或者使用DMA的方式。

结尾

本文介绍了 UART 中断方式的收发流程,理解了回调函数的作用及 HAL 库中断处理的逻辑,同时明确了当前实现的局限性。下一篇我们将学习改进中断方式的方法,彻底解决 UART 数据丢失问题。
Hello_Embed 继续带你深入 UART 编程的进阶技巧,敬请期待~

http://www.hskmm.com/?act=detail&tid=13630

相关文章:

  • 贪心算法应用:多重背包启发式疑问详解
  • 划重点|云栖大会「AI 原生应用架构论坛」看点梳理
  • 君子如水,心中有火:vivo本心而为30周年
  • Margin 塌陷问题如何解决?触发BFC。BFC的概念和触发条件
  • 9.22
  • 数字统计
  • 火速收藏!2025 云栖大会 AI 中间件议程看点全公开(附免费报名通道)
  • 第二次软工作业——个人项目 - LXJ
  • WinForm引入项目资源文件
  • 第二次作业
  • 训练集,验证集,测试集
  • Android 项目:画图白板APP开发(六)——分页展示 - 教程
  • ESP32 读取旋转编码器
  • mysql/oracle LEFT JOIN 取时间最大的数据
  • 6月6日证书 - 工信部人才交流中心PostgreSQL中级PGCP高级PGCM认证
  • 基于遗传算法与非线性规划的混合优化算法在电力系统最优潮流中的实现
  • 【下一款产品】
  • 数1的个数
  • 通过ML.Net调用Yolov5的Onnx模型
  • Java-如何在Eclipse开发-数组
  • 常用数据生成器
  • 基于RSSI修正的定位算法分析
  • c# 反射动态添加Attribute
  • MyBatis-Plus 全方位深度指南:从入门到精通
  • 鸿蒙项目实战(十):web和js交互
  • 【9.24 直播】集群数据管理实战:时序数据库 IoTDB 数据分区、同步与备份详解
  • 函数计算进化之路:AI 应用运行时的状态剖析
  • 01_进程与线程
  • 第六届医学人工智能国际学术会议(ISAIMS 2025)
  • redis 6.0 多线程