STM32串口UART实战-串口接收超时解析法模板


上一节单片机发送了数据,本节将讲解超时解析法让单片机完成数据接收

一,超时解析引入

1,什么是数据解析

电脑的数据源源不断地通过 RX 线进入单片机,但问题是:我怎么知道一帧完整的数据什么时候结束呢?
比如,电脑发送了 “Hello” 这个字符串,单片机是一个字节一个字节接收的(‘H’, ‘e’, ‘l’, ‘l’, ‘o’)。单片机怎么知道收到 ‘o’ 之后就表示 “Hello” 发送完了,而不是后面还有其他字符呢?
这就是数据解析要解决的问题。方法有很多,比如:

  1. 固定长度: 双方约定好每次都发送固定长度的数据,比如每次 10 个字节。收满 10 个字节就算一帧。
  2. 特定结束符: 双方约定好用一个特殊的字符或字符串作为结束标志,比如每次发送都以回车换行符 \r\n 结尾。收到这个标志就算一帧结束。
  3. 超时解析法: 利用数据传输的间歇时间来判断。如果两个字节之间的时间间隔超过某个阈值(比如 10ms),就认为上一帧数据已经结束了。

其核心思想:
1.设置一个接收缓冲区(就是一个数组),用来存放收到的字节。
2.启动 UART 接收(通常使用中断方式,每收到一个字节就触发一次中断)。
3.在中断服务函数里:
将收到的字节存入缓冲区。
记录当前收到字节的时间(或者说重置一个计时器)。
再次启动下一次接收。
4.在主循环(或一个定时任务)里,不断检查:当前时间距离上次收到字节的时间,是否超过了预设的超时时间?
5.如果超过了超时时间,并且缓冲区里有数据,就说明一帧数据接收完毕!可以对缓冲区里的数据进行处理了。处理完后,清空缓冲区,准备接收下一帧。

也就是说,超时解析是一种处理接收到的数据的方法

二,全局变量与常量

要让串口接收数据,首先得有地方存放数据、记录状态。我们定义以下变量去存放数据,记录存放时间和存放数量

uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];

“货架” (缓冲区): 一个 uint8_t 类型的数组,用于存放串口接收到的每一个字节(货物)。UART_RX_BUFFER_SIZE (值为 128) 定义了货架的大小,防止货物堆积如山导致溢出。

uint16_t uart_rx_index;

“计数器”: 记录当前货架上放了多少件货(收到了多少字节)。当新货到达,计数器加一;当一批货处理完毕,计数器清零。

uint32_t uart_rx_ticks;

“计时器” (时间戳): 记录最后一件货物放到货架上的准确时间。我们用它来判断货物之间的时间间隔。

extern volatile uint32_t uwTick;

“系统时钟”: 这是由 STM32 HAL 库提供的一个全局变量,像一个精准的秒表,每毫秒自动递增。我们用它来获取当前时间,与 uart_rx_ticks 比较。

#define UART_TIMEOUT_MS 100

“超时规则”: 定义了一个常量,表示我们能容忍的货物到达的最大时间间隔(100毫秒)。如果超过这个时间没新货来,我们就认为这一批货送完了。

extern UART_HandleTypeDef huart1;

“串口控制器”: 这是 HAL 库中代表具体串口硬件(如 USART1)的结构体。我们需要通过它来操作串口,比如启动接收、发送数据等。

#include <string.h> 

“工具库”: 引入标准库头文件,提供诸如 memset (清空货架)、vsnprintf (打包打印信息) 等常用工具。

我们将这些变量定义在函数外部,称为全局变量,这样在中断处理函数和主任务函数中都可以访问和修改它们,共享状态信息。

三,中断回调函数HAL_UART_RxCpltCallback

当串口硬件接收到一个字节的数据时,它会向 CPU 发送一个"中断"信号,CPU 暂停当前工作,转而执行一个特定的函数来处理这个事件。在 HAL 库中,这个函数就是 HAL_UART_RxCpltCallback。
那这个函数里写什么呢:目前串口已经接收到数据,但是还没有存放到缓冲区uart_rx_buffer,我们在这个函数中先判断串口号,再记录数据入库时间,并让数据地址加1,然后用HAL_UART_Receive_IT函数将数据存放到缓冲区

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 1. 核对身份:是 USART1 的快递员吗?
	if (huart->Instance == USART1)
	{
        // 2. 更新收货时间:记录下当前时间
		uart_rx_ticks = uwTick;
        // 3. 货物入库:将收到的字节放入缓冲区(HAL库已自动完成)
        //    并增加计数器
        //    (注意:实际入库由 HAL_UART_Receive_IT 触发,这里只更新计数)
		uart_rx_index++;
        // 4. 准备下次收货:再次告诉硬件,我还想收一个字节
		HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[uart_rx_index], 1);
	}
}

中断函数流程:

  1. 核对身份 (if 判断): 确认是目标串口 USART1 发来的中断。
  2. 更新时间戳 (uart_rx_ticks = uwTick;): 立刻记录下当前收到字节的时间。这是超时判断的基础。
  3. 更新计数 (uart_rx_index++;): 记录已接收字节数的"计数器"加一。HAL 库在调用这个回调之前,已经默默地把接收到的那个字节放到了 uart_rx_bufferuart_rx_index 指向的位置。
  4. “预订下一个包裹” (HAL_UART_Receive_IT(…)): 这是精髓所在!我们告诉 UART 硬件:“好的,这个字节我收到了,请继续监听,如果再来一个字节,请再次通知我(触发中断),并把它放到缓冲区的下一个位置 (&uart_rx_buffer[uart_rx_index])”。如果不重新启动接收,UART 就只会接收这一个字节,然后就"关门谢客"了。

还有一个关键的问题,中断函数中我们只是让接收到一个数据就让数据地址加1,但是数据的首地址是什么?
在这里插入图片描述

四,数据处理-uart_task()

判断数据是否送完,并进行处理的任务,交给uart_task 函数。这个函数需要在调度器中不断地被调用。

void uart_task(void)
{
    // 1. 检查货架:如果计数器为0,说明没货或刚处理完,休息。
	if (uart_rx_index == 0)
		return;

    // 2. 检查手表:当前时间 - 最后收货时间 > 规定的超时时间?
	if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS) // 核心判断
	{
        // --- 3. 超时!开始理货 --- 
        // "uart_rx_buffer" 里从第0个到第 "uart_rx_index - 1" 个
        // 就是我们等到的一整批货(一帧数据)
		my_printf(&huart1, "uart data: %s\n", uart_rx_buffer);
        // (在这里加入你自己的处理逻辑,比如解析命令控制LED)
        // --- 理货结束 --- 

		// 4. 清理现场:把处理完的货从货架上拿走,计数器归零
		memset(uart_rx_buffer, 0, uart_rx_index);
		uart_rx_index = 0;
	    huart1.pRxBuffPtr = uart_rx_buffer;
	}
    // 如果没超时,啥也不做,等下次再检查
}

代码逻辑:

  1. 检查是否有货 (if (uart_rx_index == 0)): 如果缓冲区是空的 (uart_rx_index 是 0),说明没有待处理的数据,直接返回,不浪费时间。
  2. 判断是否超时 (if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS)): 这是超时法的核心!用当前系统时间 uwTick 减去最后一次收到字节的时间 uart_rx_ticks,得到时间差。如果这个差值大于我们设定的 UART_TIMEOUT_MS(100毫秒),就意味着在100毫秒内没有新的字节到来。
  3. 回显数据 (超时后的代码块): 一旦超时条件满足,我们就认为 uart_rx_buffer 中从第 0 个元素到第 uart_rx_index - 1 个元素构成了一帧完整的数据。代码中简单地用 my_printf 将数据显示出来。这是你需要根据实际应用替换成自己数据处理逻辑的地方,比如解析 JSON、执行命令等。
  4. 清空状态 (memset(…) 和 uart_rx_index = 0;): 数据处理完毕后,必须清理现场!使用 memset 将缓冲区内容清零(好习惯),并将 uart_rx_index 置 0,表示货架空了,可以接收下一批新货了。
  5. 关于 uwTick 回卷: uwTick 是一个 32 位无符号整数,它会一直增加,最终会溢出回零。但是, (uint32_t)currentTime - (uint32_t)lastTime 这种计算方式在 C 语言中能正确处理回卷(只要两次时间差不超过 uint32_t 最大值的一半,对于毫秒计时这几乎不可能发生),所以直接相减是安全的。
  6. 重要修正:关于注释掉的 huart1.pRxBuffPtr = ...: 原始代码注释中认为在简单中断场景下此行可选,但经过实践验证,在此实现中,这行代码是必需的! 如果注释掉 huart1.pRxBuffPtr = uart_rx_buffer; 这一行,很可能会导致接收指针混乱,引发不可预期的错误。具体原因涉及到 HAL 库内部状态管理,我们将在下节课开头进行详细的 Debug 演示来深入剖析这个问题。请务必保留这行代码!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值