前言
大家好,这里是 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),步骤如下:
进入 “Connectivity→USART1→NVIC Settings”,勾选 “Enabled” 使能 USART1 中断:
其他配置(波特率 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 中断,执行流程如下:
中断入口:
USART1_IRQHandler
(异常向量表中的串口 1 中断入口);
跳转至 HAL 库通用处理函数:
HAL_UART_IRQHandler(&huart1)
;核心发送逻辑(
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 中断,执行流程如下:
- 中断入口同样是
USART1_IRQHandler
,跳转至HAL_UART_IRQHandler
; - 核心接收逻辑(
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 编程的进阶技巧,敬请期待~