STM32HAL 快速入门(二十):UART 中断改进 —— 环形缓冲区解决数据丢失

前言

大家好,这里是 Hello_Embed 。上一篇我们用中断方式实现了 UART 收发,但发现一个关键问题:若 CPU 在处理其他任务时未及时重新使能接收中断,新数据会覆盖旧数据,导致丢失。本篇的核心改进方案是 ------"中断接收 + 环形缓冲区":中断中实时将接收的数据存入缓冲区,主程序从缓冲区按需读取,彻底解决 "接收不及时" 的痛点。下一篇我们将进一步学习更高效的 DMA 方式,现在先聚焦这个经典的中断优化方案。

本篇笔记所提及的环形缓冲区相关知识可在同系列笔记14-15中找到

一、改进核心思路

要解决数据丢失,关键是让 "接收" 和 "处理" 解耦 ------ 接收端用中断快速存数据,处理端(主程序)慢慢读数据,无需同步等待。具体思路有两点:

  1. 提前使能接收中断:程序启动时就开启 UART 接收中断,确保任何时候有数据都能被捕获;
  2. 中断中存环形缓冲区:每次接收中断触发时,立即将数据存入环形缓冲区,避免数据在寄存器中被覆盖,主程序从缓冲区读取数据时不影响接收。
二、代码实现:从缓冲区定义到中断处理

我们基于上一篇的工程修改,核心是在usart.c中集成环形缓冲区,实现 "中断存数据、主程序读数据" 的流程。

1. 准备工作:包含头文件与定义核心变量

首先在usart.c的开头包含环形缓冲区头文件(需确保路径正确),并定义接收相关的变量:

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函数,作用是初始化环形缓冲区提前使能接收中断------ 程序启动时调用一次,后续无需手动开启中断。

c 复制代码
/* 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(接收完成回调函数),核心逻辑是:将暂存的字节存入环形缓冲区,并重新使能接收中断(确保下一个数据能被捕获)。

c 复制代码
/* 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,对应缓冲区空)。

c 复制代码
/* 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. 沿用发送相关函数

若需要保留中断发送功能,可沿用上篇的发送完成回调和等待函数(确保发送流程正常):

c 复制代码
/* 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中定义的函数:

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. 主程序核心逻辑
c 复制代码
/* 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"------ 证明环形缓冲区成功暂存了所有数据,即使主程序在处理发送,也不会丢失接收的数据。

五、核心流程梳理(为什么能解决丢失?)

整个改进方案的闭环流程如下,关键是 "接收" 和 "处理" 的解耦:

  1. 启动阶段StartUART1Recv初始化缓冲区→使能接收中断;
  2. 接收阶段 :电脑发数据→RXNE 中断触发→HAL_UART_RxCpltCallback将数据存入缓冲区→重新使能中断(准备下一次接收);
  3. 处理阶段 :主程序通过UART1GetChar从缓冲区读数据→加 1 后返回→即使主程序耗时,缓冲区也会暂存新数据,不会被覆盖。
结尾

"中断 + 环形缓冲区" 是 UART 通信中解决数据丢失的经典方案,它兼顾了中断的高效性和缓冲区的可靠性,适合中低速、数据量不大的场景。下一篇笔记,我们将学习更高级的 "DMA 方式"------ 让 DMA 硬件替 CPU 完成 "数据搬运",彻底解放 CPU,适合高速、大数据量的通信场景。
Hello_Embed 继续带你探索 UART 通信的高效实现方式,敬请期待~

相关推荐
咸甜适中2 小时前
rust语言 (1.88) 学习笔记:客户端和服务器端同在一个项目中
笔记·学习·rust
Grassto3 小时前
RAG 从入门到放弃?丐版 demo 实战笔记(go+python)
笔记
Magnetic_h3 小时前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa
研梦非凡4 小时前
ICCV 2025|从粗到细:用于高效3D高斯溅射的可学习离散小波变换
人工智能·深度学习·学习·3d
矢志不移7924 小时前
裸机开发 时钟配置,EPIT
单片机·嵌入式硬件
清风6666665 小时前
基于STM32的APP遥控视频水泵小车设计
stm32·单片机·mongodb·毕业设计·音视频·课程设计
limengshi1383925 小时前
机器学习面试:请介绍几种常用的学习率衰减方式
人工智能·学习·机器学习
知识分享小能手5 小时前
React学习教程,从入门到精通,React 组件核心语法知识点详解(类组件体系)(19)
前端·javascript·vue.js·学习·react.js·react·anti-design-vue
周周记笔记6 小时前
学习笔记:第一个Python程序
笔记·学习