在使用 STM32F407 的 HAL 库进行串口开发时,HAL_UART_Receive 是常用的阻塞接收函数。很多开发者会认为它就是一个简单的“接收指定数量字节”的函数,但在实际应用中,当上位机发送的数据量不等于我们期望接收的数据量时,程序会表现出一些值得注意的行为。本文通过一个简单的测试,带大家深入理解 HAL_UART_Receive 的内部逻辑及处理边界情况的方式。
一、函数基础回顾
测试基于 STM32F407 的 UART1,配置为 115200-8-N-1,使用 STM32CubeMX生成的HAL 库。

HAL_UART_Receive 是阻塞式串口接收函数,快速过一下 HAL_UART_Receive函数 的核心源码:
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
// ... 初始化代码省略 ...
/* 核心循环:等待接收 Size 个数据 */
while (huart->RxXferCount > 0U)
{
// 等待 RXNE 标志(接收数据寄存器非空),超时则退出
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
{
huart->RxState = HAL_UART_STATE_READY;
return HAL_TIMEOUT; // 超时返回
}
// 读取数据到缓冲区(处理 9位/8位 情况)
if (pdata8bits == NULL) { /* 9位数据处理 */ }
else { /* 8位数据处理,存入 pData */ }
huart->RxXferCount--; // 每收到一个字节,计数减 1
}
huart->RxState = HAL_UART_STATE_READY;
return HAL_OK; // 成功接收到 Size 个字节
}
关键逻辑提炼:
- 函数通过
while循环尝试接收 恰好Size个字节; - 每收到一个字节,存入
pData缓冲区,并将剩余计数RxXferCount减 1; - 若在超时时间内收到
Size个字节 → 返回HAL_OK; - 若等待超时(未收满
Size个) → 返回HAL_TIMEOUT。
二、测试场景与代码
设定:期望接收长度 nUartRxSize = 5,超时时间 nTimeout 为固定值(非无限等待)。测试三种场景:
- 上位机发送 3 个字节(小于设定长度 5)
- 上位机不发送任何数据
- 上位机发送 7 个字节(大于设定长度 5)
测试核心代码:
// 定义变量
uint8_t rxUartBuff[50] = {0}; // 接收缓冲区,固定长度5
uint16_t nUartRxSize = 5; // 设定接收长度:5字节
uint16_t nTimeout = 500; // 设定超时时间:500ms
uint16_t nUartRxFrame = 0; // 记录接收到的帧数
uint8_t nFor = 0;
HAL_StatusTypeDef uart_rx_stat;
// 阻塞接收:期望收5个字节
uart_rx_stat = HAL_UART_Receive(&huart1, rxUartBuff, nUartRxSize , nTimeout );
// 打印整个接收缓冲区的所有数据(不管函数返回值)
printf("nRx1=%03d:",nUartRxFrame);
for(nFor=0;nFor<nUartRxSize ;nFor++)
{
printf("%d=%02x,",nFor,rxUartBuff[nFor]);
}
printf("\r\n");
// 只有返回HAL_OK才会执行的逻辑
if(uart_rx_stat == HAL_OK)
{
printf("nRx2=%03d:",++nUartRxFrame);
// 业务处理代码(实测:数据不匹配时,这里永远不会执行)
}
三、实验结果与现象分析
设定 nUartRxSize = 5,即期望接收 5 个字节。下面是三种典型情况的测试日志。
场景 1:上位机发送 3 个字节(31 32 33)
[2026-04-15 11:00:29.660]# SEND HEX/3 >>>
31 32 33
[2026-04-15 11:00:29.966]# RECV ASCII/36 <<<
nRx1=000:0=31,1=32,2=33,3=00,4=00,
✅ 现象分析:
- 函数返回值:不是 HAL_OK,而是 HAL_TIMEOUT
- 接收缓冲区:前 3 个字节是正确的接收数据,后 2 个字节保持默认 0
if(uart_rx_stat==HAL_OK)内的代码不会执行
场景 2:上位机不发送任何数据
[2026-04-15 11:00:30.470]# RECV ASCII/36 <<<
nRx1=000:0=00,1=00,2=00,3=00,4=00,
✅ 现象分析:
- 函数直接超时,返回
HAL_TIMEOUT - 接收缓冲区全部为默认值 0
- 业务处理代码依旧不执行
场景 3:上位机发送 7 个字节(31 32 33 34 35 36 37)
[2026-04-15 11:00:31.020]# SEND HEX/7 >>>
31 32 33 34 35 36 37
[2026-04-15 11:00:31.053]# RECV ASCII/82 <<<
nRx1=000:0=31,1=32,2=33,3=34,4=35,
nRx2=001:0=31,1=32,2=33,3=34,4=35,
[2026-04-15 11:00:31.561]# RECV ASCII/36 <<<
nRx1=001:0=36,1=00,2=00,3=00,4=00,
✅ 现象分析:
-
第一次调用
HAL_UART_Receive(期望 5 字节)成功接收了 5 个字节(31~35),因此返回HAL_OK,并执行了if分支内的打印(nRx2)。 -
上位机剩余的 2 个字节(36、37)留在了串口接收缓冲区(硬件 FIFO 或移位寄存器)中。
-
紧接着的第二次调用(31.561)试图再次接收 5 个字节,此时从硬件中取出了仅有的 2 个字节(36、37),随后超时返回,缓冲区显示
0=36,1=00,2=00,3=00,4=00(实际 37 并未出现在缓冲区中,为何?)
四、核心原理:为什么会出现这种现象?
结合 HAL 库源码,可以总结出关键执行流程:
1、检查 RxState 是否就绪,若忙则返回 HAL_BUSY。
2、设置接收状态为 HAL_UART_STATE_BUSY_RX,初始化计数变量。
3、循环接收,直至 RxXferCount == 0:
- 调用
UART_WaitOnFlagUntilTimeout等待RXNE标志(接收数据寄存器非空),若超时则提前退出并返回HAL_TIMEOUT。 - 读取数据寄存器
DR,根据数据位宽和校验设置存入pData缓冲区,指针递增。 RxXferCount递减。
4、若循环正常结束(即收够了 Size 个字节),状态恢复为 READY,返回 HAL_OK。
因此:
-
数据量不足:函数会在超时后返回
HAL_TIMEOUT,此时已接收的数据已经存入缓冲区,未接收部分不会被主动清零,但后续再次调用会覆盖它们。 -
数据量超额:函数只取前
Size个字节,返回HAL_OK,多余的字节留在硬件中,可能在下一次接收时被读到(若无溢出)。
五、关键结论(开发必看)
-
不要仅依赖返回值判断接收是否完整
若通信协议帧长不固定,应结合超时机制(如空闲中断、DMA 半满中断)或使用HAL_UART_Receive_IT(中断模式)来实现不定长接收。 -
注意缓冲区的残留数据
当HAL_UART_Receive因超时返回时,未填满的区域可能包含上一次接收的旧数据。在解析前,建议用接收计数变量(如实际接收长度)来标识有效数据,而非假定缓冲区全为有效。 -
避免数据溢出
如果上位机连续发送大量数据,而 MCU 调用HAL_UART_Receive的间隔过长,硬件 FIFO 可能溢出导致丢包。适当提高主循环频率、使用 DMA 接收或合理配置硬件 FIFO 可缓解此问题。 -
阻塞接收的适用场景
HAL_UART_Receive适合接收固定长度、对实时性要求不高的命令帧(如每次固定 8 字节的查询指令)。对于交互式或变长协议,更推荐中断或 DMA 方式。
六、总结
通过本次实验,验证了 HAL_UART_Receive 在面临数据量不匹配时的具体行为:
-
接收数据不足:超时返回,已收数据存入缓冲区,后续调用会覆盖未收满部分。
-
接收数据超额:返回成功,但仅读取指定数量,剩余数据留待下次处理(需注意溢出风险)。
理解这些细节有助于我们编写更健壮的 STM32 串口通信代码,避免因对 HAL 库行为的误解而产生奇怪的 bug。希望这篇心得对正在使用 STM32 HAL 库的朋友有所帮助!
长度不匹配时的现象&spm=1001.2101.3001.5002&articleId=160181718&d=1&t=3&u=a3ca574de9bf4bd6a2c6283d9c3d68f3)
6万+

被折叠的 条评论
为什么被折叠?



