前言
大家好,这里是 Hello_Embed。上一篇我们用中断方式实现了 UART 收发,但发现一个关键问题:若 CPU 在处理其他任务时未及时重新使能接收中断,新数据会覆盖旧数据,导致丢失。本篇的核心改进方案是 ——“中断接收 + 环形缓冲区”:中断中实时将接收的数据存入缓冲区,主程序从缓冲区按需读取,彻底解决 “接收不及时” 的痛点。下一篇我们将进一步学习更高效的 DMA 方式,现在先聚焦这个经典的中断优化方案。
本篇笔记所提及的环形缓冲区相关知识可在同系列笔记14-15中找到
一、改进核心思路
要解决数据丢失,关键是让 “接收” 和 “处理” 解耦 —— 接收端用中断快速存数据,处理端(主程序)慢慢读数据,无需同步等待。具体思路有两点:
- 提前使能接收中断:程序启动时就开启 UART 接收中断,确保任何时候有数据都能被捕获;
- 中断中存环形缓冲区:每次接收中断触发时,立即将数据存入环形缓冲区,避免数据在寄存器中被覆盖,主程序从缓冲区读取数据时不影响接收。
二、代码实现:从缓冲区定义到中断处理
我们基于上一篇的工程修改,核心是在usart.c
中集成环形缓冲区,实现 “中断存数据、主程序读数据” 的流程。
1. 准备工作:包含头文件与定义核心变量
首先在usart.c
的开头包含环形缓冲区头文件(需确保路径正确),并定义接收相关的变量:
#include <circle_buffer.h> // 包含环形缓冲区头文件
/* USER CODE BEGIN 1 */
// 1. 发送完成标志(沿用上篇)
static volatile int g_tx_cplt = 0;
// 2. 接收暂存变量:每次中断接收1字节,先存在这里
static uint8_t g_RecvChar;
// 3. 环形缓冲区存储数组:容量100字节,可存100个接收数据
static uint8_t g_RecvBuf[100];
// 4. 环形缓冲区结构体:管理读写指针和长度
static circle_buf g_uart1_rx_bufs;
/* USER CODE END 1 */
2. 步骤 1:初始化环形缓冲区 + 启动接收中断
定义StartUART1Recv
函数,作用是初始化环形缓冲区并提前使能接收中断—— 程序启动时调用一次,后续无需手动开启中断。
/* USER CODE BEGIN 1 */
// 启动UART1接收:初始化缓冲区+使能接收中断
void StartUART1Recv(void)
{
// 初始化环形缓冲区:绑定结构体、容量100、存储数组g_RecvBuf
circle_buf_init(&g_uart1_rx_bufs, 100, g_RecvBuf);
// 使能接收中断:接收1字节到g_RecvChar,触发中断后进入回调函数
HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1);
}
/* USER CODE END 1 */
circle_buf_init
参数说明:&g_uart1_rx_bufs
(缓冲区结构体)、100
(容量)、g_RecvBuf
(存储数组);HAL_UART_Receive_IT
:使能 RXNE 中断(RDR 寄存器非空时触发),接收的 1 字节暂存到g_RecvChar
。
3. 步骤 2:接收中断回调 —— 数据存入缓冲区
重写HAL_UART_RxCpltCallback
(接收完成回调函数),核心逻辑是:将暂存的字节存入环形缓冲区,并重新使能接收中断(确保下一个数据能被捕获)。
/* USER CODE BEGIN 1 */
// 接收完成回调函数:中断触发后执行
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1) // 确认是USART1的中断
{
// 1. 将接收的字节(g_RecvChar)写入环形缓冲区
circle_buf_write(&g_uart1_rx_bufs, g_RecvChar);
// 2. 重新使能接收中断:准备接收下一个字节(关键!避免中断断连)
HAL_UART_Receive_IT(&huart1, &g_RecvChar, 1);
}
}
/* USER CODE END 1 */
- 为什么要 “重新使能中断”?
HAL_UART_Receive_IT
是 “一次性” 的 —— 接收 1 字节后会自动关闭中断,必须重新调用才能继续接收下一字节。
4. 步骤 3:封装缓冲区读取函数
定义UART1GetChar
函数,供主程序调用,从环形缓冲区读取 1 字节数据(成功返回 0,失败返回 - 1,对应缓冲区空)。
/* USER CODE BEGIN 1 */
// 从环形缓冲区读取1字节数据
int UART1GetChar(uint8_t *pVal)
{
// 调用环形缓冲区读函数,将数据存入pVal指向的地址
return circle_buf_read(&g_uart1_rx_bufs, pVal);
}
/* USER CODE END 1 */
pVal
:主程序传入的 “数据存储地址”,读取成功后,缓冲区的数据会存在这里;- 返回值:0 表示读取成功(有数据),-1 表示缓冲区空(无数据)。
5. 沿用发送相关函数
若需要保留中断发送功能,可沿用上篇的发送完成回调和等待函数(确保发送流程正常):
/* USER CODE BEGIN 1 */
// 发送完成回调函数(沿用)
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
g_tx_cplt = 1;
}
// 等待发送完成(沿用,主程序调用)
void Wait_Tx_Complete(void)
{
while (g_tx_cplt == 0); // 等待发送完成标志置位
g_tx_cplt = 0; // 复位标志
}
/* USER CODE END 1 */
三、主程序调用:实现 “接收 - 处理 - 返回” 完整流程
在main.c
中,先启动接收中断,再通过 “发送提示→读取缓冲区→数据加 1 返回” 的逻辑,验证改进方案是否有效。
1. 声明外部函数
在main.c
的/* USER CODE BEGIN PV */
区域,声明usart.c
中定义的函数:
/* USER CODE BEGIN PV */
// 声明外部函数:启动接收中断、等待发送完成、读取缓冲区数据
extern void StartUART1Recv(void);
extern void Wait_Tx_Complete(void);
extern int UART1GetChar(uint8_t *pVal);
/* USER CODE END PV */
2. 主程序核心逻辑
/* USER CODE BEGIN 2 */
// 1. 启动UART1接收:初始化缓冲区+使能中断(程序启动时执行一次)
StartUART1Recv();
// 2. 定义发送的提示信息和接收变量
char *str1 = "Please enter a char : \r\n";
uint8_t c; // 存储从缓冲区读取的字节
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE BEGIN 3 */
// 步骤1:发送提示信息(中断方式)
HAL_UART_Transmit_IT(&huart1, (uint8_t *)str1, strlen(str1));
Wait_Tx_Complete(); // 等待发送完成
// 步骤2:从环形缓冲区读取1字节(循环等待,直到有数据)
while (0 != UART1GetChar(&c)); // 返回0表示读取成功,退出循环
// 步骤3:数据加1后返回(查询方式,简单场景可用)
c += 1;
HAL_UART_Transmit(&huart1, &c, 1, 1000); // 发送加1后的字符
HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n", 2, 1000); // 换行
}
/* USER CODE END 3 */
四、实验验证:数据丢失问题解决
烧录程序后,用串口工具一次性发送 “12345”(模拟快速连续发送),结果如下:
可以看到,单片机正确接收了所有字符,并返回 “23456”—— 证明环形缓冲区成功暂存了所有数据,即使主程序在处理发送,也不会丢失接收的数据。
五、核心流程梳理(为什么能解决丢失?)
整个改进方案的闭环流程如下,关键是 “接收” 和 “处理” 的解耦:
- 启动阶段:
StartUART1Recv
初始化缓冲区→使能接收中断; - 接收阶段:电脑发数据→RXNE 中断触发→
HAL_UART_RxCpltCallback
将数据存入缓冲区→重新使能中断(准备下一次接收); - 处理阶段:主程序通过
UART1GetChar
从缓冲区读数据→加 1 后返回→即使主程序耗时,缓冲区也会暂存新数据,不会被覆盖。
结尾
“中断 + 环形缓冲区” 是 UART 通信中解决数据丢失的经典方案,它兼顾了中断的高效性和缓冲区的可靠性,适合中低速、数据量不大的场景。下一篇笔记,我们将学习更高级的 “DMA 方式”—— 让 DMA 硬件替 CPU 完成 “数据搬运”,彻底解放 CPU,适合高速、大数据量的通信场景。
Hello_Embed 继续带你探索 UART 通信的高效实现方式,敬请期待~