STM32串口UART实战-串口接收超时解析法模板
上一节单片机发送了数据,本节将讲解超时解析法让单片机完成数据接收
一,超时解析引入
1,什么是数据解析
电脑的数据源源不断地通过 RX 线进入单片机,但问题是:我怎么知道一帧完整的数据什么时候结束呢?
比如,电脑发送了 “Hello” 这个字符串,单片机是一个字节一个字节接收的(‘H’, ‘e’, ‘l’, ‘l’, ‘o’)。单片机怎么知道收到 ‘o’ 之后就表示 “Hello” 发送完了,而不是后面还有其他字符呢?
这就是数据解析要解决的问题。方法有很多,比如:
- 固定长度: 双方约定好每次都发送固定长度的数据,比如每次 10 个字节。收满 10 个字节就算一帧。
- 特定结束符: 双方约定好用一个特殊的字符或字符串作为结束标志,比如每次发送都以回车换行符
\r\n
结尾。收到这个标志就算一帧结束。 - 超时解析法: 利用数据传输的间歇时间来判断。如果两个字节之间的时间间隔超过某个阈值(比如 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);
}
}
中断函数流程:
- 核对身份 (if 判断): 确认是目标串口
USART1
发来的中断。- 更新时间戳 (uart_rx_ticks = uwTick;): 立刻记录下当前收到字节的时间。这是超时判断的基础。
- 更新计数 (uart_rx_index++;): 记录已接收字节数的"计数器"加一。HAL 库在调用这个回调之前,已经默默地把接收到的那个字节放到了
uart_rx_buffer
中uart_rx_index
指向的位置。- “预订下一个包裹” (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;
}
// 如果没超时,啥也不做,等下次再检查
}
代码逻辑:
- 检查是否有货 (if (uart_rx_index == 0)): 如果缓冲区是空的 (
uart_rx_index
是 0),说明没有待处理的数据,直接返回,不浪费时间。 - 判断是否超时 (if (uwTick - uart_rx_ticks > UART_TIMEOUT_MS)): 这是超时法的核心!用当前系统时间
uwTick
减去最后一次收到字节的时间uart_rx_ticks
,得到时间差。如果这个差值大于我们设定的UART_TIMEOUT_MS
(100毫秒),就意味着在100毫秒内没有新的字节到来。 - 回显数据 (超时后的代码块): 一旦超时条件满足,我们就认为
uart_rx_buffer
中从第 0 个元素到第uart_rx_index - 1
个元素构成了一帧完整的数据。代码中简单地用my_printf
将数据显示出来。这是你需要根据实际应用替换成自己数据处理逻辑的地方,比如解析 JSON、执行命令等。 - 清空状态 (memset(…) 和 uart_rx_index = 0;): 数据处理完毕后,必须清理现场!使用
memset
将缓冲区内容清零(好习惯),并将uart_rx_index
置 0,表示货架空了,可以接收下一批新货了。 - 关于
uwTick
回卷:uwTick
是一个 32 位无符号整数,它会一直增加,最终会溢出回零。但是,(uint32_t)currentTime - (uint32_t)lastTime
这种计算方式在 C 语言中能正确处理回卷(只要两次时间差不超过uint32_t
最大值的一半,对于毫秒计时这几乎不可能发生),所以直接相减是安全的。 - 重要修正:关于注释掉的
huart1.pRxBuffPtr = ...
: 原始代码注释中认为在简单中断场景下此行可选,但经过实践验证,在此实现中,这行代码是必需的! 如果注释掉huart1.pRxBuffPtr = uart_rx_buffer;
这一行,很可能会导致接收指针混乱,引发不可预期的错误。具体原因涉及到 HAL 库内部状态管理,我们将在下节课开头进行详细的 Debug 演示来深入剖析这个问题。请务必保留这行代码!