4. HAL库USART轮询模式:阻塞式收发与超时处理
这一章我们聊点实在的——怎么用HAL库的轮询模式,让STM32F407老老实实地把数据发出去、收进来。说白了,就是HAL_UART_Transmit和HAL_UART_Receive这两个函数怎么用,以及它们背后的阻塞机制和超时处理。
我个人习惯把轮询模式叫做“傻瓜模式”。为什么?因为CPU全程盯着,数据没发完就不干别的事。这在简单场景下非常可靠,但你要是搞复杂系统,就得小心了。
4.1 轮询模式的核心思想
轮询模式,也叫阻塞模式。CPU发起一次收发操作后,就一直在那等,直到操作完成或超时。这期间CPU不能干别的。
你想想看,这像不像你去银行办业务——柜台叫到你的号,你就一直坐在那等,直到办完才能走。中间不能去干别的事。
轮询模式的特点:
- 代码简单,逻辑清晰
- 适合数据量小、实时性要求不高的场景
- CPU利用率低,因为大部分时间在空等
- 容易理解,适合初学者入门
我在项目中遇到过一种情况:用轮询模式给蓝牙模块发AT指令,每条指令就几个字节,收发完就完事。这种场景用轮询模式再合适不过了。
4.2 HAL_UART_Transmit 函数详解
先看发送函数。它的原型长这样:
HAL_StatusTypeDef HAL_UART_Transmit(
UART_HandleTypeDef *huart, // 串口句柄
uint8_t *pData, // 要发送的数据缓冲区
uint16_t Size, // 发送的数据长度(字节)
uint32_t Timeout // 超时时间(ms)
);
四个参数,一个返回值。我来逐个说说。
huart:串口句柄。你在CubeMX里配好串口后,它会生成一个huart1或huart2这样的变量。传进去就行。
pData:数据缓冲区指针。注意是uint8_t*类型,也就是字节数组。你要发字符串?记得强转一下。
Size:要发送的字节数。不是字符串长度,是实实在在的字节数。
Timeout:超时时间,单位毫秒。这个参数很关键,后面我会专门讲。
返回值:HAL_OK表示成功,其他值表示出错。
我的习惯:发送字符串时,我一般用strlen()获取长度,或者直接用sizeof()——前提是你知道数组的实际大小。
来个例子:
uint8_t txBuffer[] = "AT+RESET\r\n";
HAL_UART_Transmit(&huart1, txBuffer, strlen((char*)txBuffer), 1000);
这行代码的意思:通过串口1发送"AT+RESET\r\n",超时时间1秒。如果1秒内没发完,函数返回超时错误。
4.3 HAL_UART_Receive 函数详解
接收函数和发送函数是对称的:
HAL_StatusTypeDef HAL_UART_Receive(
UART_HandleTypeDef *huart, // 串口句柄
uint8_t *pData, // 接收数据缓冲区
uint16_t Size, // 期望接收的字节数
uint32_t Timeout // 超时时间(ms)
);
注意一个关键点:这个函数必须收满Size个字节才会返回。少一个字节都不行。
为什么会这样?因为轮询模式的设计思路就是“要么收满,要么超时”。它不会收到一个字节就返回。
我曾经踩过的坑:刚开始用HAL_UART_Receive时,我想收一个字节就处理,结果函数一直卡在那,因为蓝牙模块只发了一个字节,而我设的Size是10。后来才明白,轮询模式不适合这种不定长接收。
来个接收例子:
uint8_t rxBuffer[10];
HAL_StatusTypeDef status;
status = HAL_UART_Receive(&huart1, rxBuffer, 5, 2000);
if(status == HAL_OK) {
// 成功收到5个字节,可以处理了
} else {
// 超时或出错,做错误处理
}
这段代码的意思是:等串口1收到5个字节,最多等2秒。2秒内没收满,就返回超时。
4.4 阻塞式收发的实现流程
阻塞式收发的流程其实很简单,我画个图你就明白了:
从流程图可以看出,阻塞式收发的核心就是一个循环等待的过程。CPU在函数内部不断检查硬件状态,直到满足条件才退出。
4.5 超时处理机制
超时处理是轮询模式里最容易出问题的地方。我见过不少新手在这里翻车。
超时时间怎么设?
这取决于你的波特率和数据量。我一般这样估算:
| 波特率 | 每字节传输时间 | 10字节建议超时 | 100字节建议超时 |
|---|---|---|---|
| 9600 | 约1.04ms | 50ms | 200ms |
| 115200 | 约0.087ms | 10ms | 50ms |
| 921600 | 约0.011ms | 5ms | 20ms |
注意,这只是纯传输时间。实际还要考虑系统调度、中断延迟等因素。我建议在这个基础上加50%的余量。
我的经验:给蓝牙模块发AT指令时,我一般设500ms超时。因为蓝牙模块的响应时间不稳定,有时候要等几百毫秒才回复。设太短容易超时,设太长又影响系统响应。
超时后的处理:
函数返回HAL_TIMEOUT后,你该怎么办?
HAL_StatusTypeDef status;
uint8_t cmd[] = "AT\r\n";
uint8_t resp[64];
status = HAL_UART_Transmit(&huart1, cmd, strlen((char*)cmd), 500);
if(status != HAL_OK) {
// 发送失败,重试或报错
Error_Handler();
}
status = HAL_UART_Receive(&huart1, resp, 10, 1000);
if(status == HAL_TIMEOUT) {
// 超时了,可能是蓝牙模块没响应
// 我一般会重发一次指令
HAL_UART_Transmit(&huart1, cmd, strlen((char*)cmd), 500);
status = HAL_UART_Receive(&huart1, resp, 10, 1000);
} else if(status != HAL_OK) {
// 其他错误
Error_Handler();
}
这段代码展示了一个简单的重试机制。超时后先重发指令,再等一次响应。如果还超时,那就真的出问题了。
4.6 轮询模式的典型应用场景
轮询模式虽然简单,但用对地方效果很好:
- 初始化配置:上电后给蓝牙模块发AT指令,配置工作模式
- 简单命令交互:发一条指令,等一条回复,不涉及大量数据
- 调试输出:用
printf重定向到串口,打印调试信息 - 低速率通信:数据量小,对实时性要求不高
注意:轮询模式不适合高速、大数据量的通信。比如你要通过蓝牙传音频数据,用轮询模式CPU就废了。那种场景得用DMA或中断。
4.7 实际项目中的注意事项
最后,我总结几个实际项目中容易踩的坑:
- 超时时间别设0。设0意味着不等待,函数会立即返回。这在某些场景下有用,但初学者容易搞混。
- 接收缓冲区要够大。如果设了Size=100,但只收到50个字节,函数会一直等,直到超时。
- 注意数据类型。
pData是uint8_t*,传字符串时要强转。 - 不要在中断里用轮询模式。中断里调用阻塞函数,会导致系统卡死。