西门子——UART——超时解析

基本概念

 串行 vs. 并行

想象一下排队过安检:

  • 串行 (Serial): 就像大家一个接一个排队通过安检门,每次只能过一个人(一个比特)。UART 就是这种方式,数据一位一位地在单根线上发送。优点:省线,适合长距离;缺点:速度相对较慢。
  • 并行 (Parallel): 就像同时开了好几个安检门,大家可以同时通过。数据同时在多根线上发送。优点:速度快;缺点:需要更多线,成本高,不适合长距离

 波特率 (Baud Rate)

波特率就好比两个人打电话时说话的速度。你说话快,对方也要能听得快;你说话慢,对方也要能听得慢。只有双方速度(波特率)约定一致,才能正确理解对方的信息。

常见的波特率有 9600, 115200 等,单位是 bps (bits per second),即每秒传输多少比特。

数据帧 (Data Frame)

为了确保数据正确传输,UART 通信时会把数据打包成一个个"数据帧"来发送。就像写信需要信封、地址、邮票一样,数据帧也有固定格式

重要:通信双方必须约定好相同的数据帧格式(数据位、校验位、停止位),否则就会"鸡同鸭讲",无法解析数据。

TX 和 RX

UART 通信至少需要两根线:

  • TX (Transmit): 发送数据线。设备的 TX 连接到另一个设备的 RX。
  • RX (Receive): 接收数据线。设备的 RX 连接到另一个设备的 TX。

记住:TX 要连接对方的 RX,RX 要连接对方的 TX,交叉连接,就像打电话一样,你的话筒对着别人的听筒。

通常还需要一根 GND (Ground) 线,即地线,用来统一双方的参考电平,确保信号稳定。

硬件连接

  1. 找到单片机上的 UART 引脚: 通常在开发板原理图或丝印上会标明 UART1_TX, UART1_RX 等。不同单片机可能叫 USART、SCI 等,但原理类似。
  2. 准备 USB 转 TTL 模块: 这是一个小模块,一边是 USB 口插电脑,另一边引出 TXD, RXD, GND, VCC 引脚。
  3. 连接:
    • 单片机的 TX 连接到 USB 转 TTL 模块的 RXD
    • 单片机的 RX 连接到 USB 转 TTL 模块的 TXD
    • 单片机的 GND 连接到 USB 转 TTL 模块的 GND
    • (可选)如果模块需要供电,根据情况连接 VCC。
  4. 插入电脑: 将模块的 USB 口插入电脑。电脑会识别到一个虚拟串口(COM口)。

注意: 一定要交叉连接 TX 和 RX!GND 必须连接!

连接好后,你就可以在电脑上使用串口助手软件(如 XCOM、Putty 等)来收发数据了。

 超时解析法:简单高效的数据接收

好了,现在硬件连上了,单片机也配置好了 UART(这部分通常由 STM32CubeMX 或类似工具生成初始化代码),数据源源不断地通过 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

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_buffer` 中 `uart_rx_index` 指向的位置。
  4. "预订下一个包裹" (HAL_UART_Receive_IT(...)): 这是精髓所在!我们告诉 UART 硬件:"好的,这个字节我收到了,请继续监听,如果再来一个字节,请再次通知我(触发中断),并把它放到缓冲区的下一个位置 (`&uart_rx_buffer[uart_rx_index]`)"。如果不重新启动接收,UART 就只会接收这一个字节,然后就"关门谢客"了。

💡

首次"开门": 别忘了,第一次接收需要手动启动。通常在 `main` 函数初始化代码中,调用一次 `HAL_UART_Receive_IT(&huart1, &uart_rx_buffer[0], 1);` 来等待第一个字节的到来,相当于第一次给门口的传感器通电。

"理货员":超时处理任务 uart_task

"收货员"(中断)只负责快速接收字节并记录时间,它不关心数据是否完整。判断一批货(一帧数据)是否送完,并进行处理的任务,交给了我们的"理货员"——uart_task 函数。这个函数需要在主循环 `while(1)` 中不断地被调用。

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;
	}
    // 如果没超时,啥也不做,等下次再检查
}

  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,表示货架空了,可以接收下一批新货了。

⚠️

关于 `uwTick` 回卷: `uwTick` 是一个 32 位无符号整数,它会一直增加,最终会溢出回零。但是, `(uint32_t)currentTime - (uint32_t)lastTime` 这种计算方式在 C 语言中能正确处理回卷(只要两次时间差不超过 `uint32_t` 最大值的一半,对于毫秒计时这几乎不可能发生),所以直接相减是安全的。

⚠️

重要修正:关于注释掉的 `huart1.pRxBuffPtr = ...`: 原始代码注释中认为在简单中断场景下此行可选,但经过实践验证,在此实现中,这行代码是必需的! 如果注释掉 `huart1.pRxBuffPtr = uart_rx_buffer;` 这一行,很可能会导致接收指针混乱,引发不可预期的错误。具体原因涉及到 HAL 库内部状态管理,我们将在下节课开头进行详细的 Debug 演示来深入剖析这个问题。请务必保留这行代码! 

 "广播员":辅助函数 my_printf

为了方便我们在开发过程中查看接收到的数据或打印调试信息,代码提供了一个 `my_printf` 函数,它能像标准 `printf` 一样工作,但输出目标是 UART 串口。

int my_printf(UART_HandleTypeDef *huart, const char *format, ...)
{
	char buffer[512]; // 临时存储格式化后的字符串
	va_list arg;      // 处理可变参数
	int len;          // 最终字符串长度

	va_start(arg, format);
	// 安全地格式化字符串到 buffer
	len = vsnprintf(buffer, sizeof(buffer), format, arg);
	va_end(arg);

	// 通过 HAL 库发送 buffer 中的内容
	HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
	return len;
}
  • 使用 C 语言的 `stdarg.h` 库处理不定数量的参数(就像 `printf` 可以接受不同数量和类型的参数一样)。
  • 调用 `vsnprintf`,这是一个安全的函数,它会将格式字符串 (`format`) 和后续参数组合成最终的字符串,并存入 `buffer` 中,同时防止缓冲区溢出。
  • 最后调用 `HAL_UART_Transmit` 函数,将 `buffer` 中的内容通过指定的 `huart` (比如 `&huart1`) 发送出去。`0xFF` 是发送超时时间(这里设为一个相对宽松的值)。

💡

这个函数非常适合在 `uart_task` 中打印接收到的数据,或者在代码其他地方输出调试日志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值