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 阻塞式收发的实现流程

阻塞式收发的流程其实很简单,我画个图你就明白了:

阻塞式收发流程图 开始 调用 HAL_UART_Transmit/Receive 操作完成? 否 超时? 是 返回 HAL_TIMEOUT 否,继续等待 是 返回 HAL_OK 结束 说明 CPU全程等待 不处理其他任务 直到操作完成或超时

从流程图可以看出,阻塞式收发的核心就是一个循环等待的过程。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 实际项目中的注意事项

最后,我总结几个实际项目中容易踩的坑:

  1. 超时时间别设0。设0意味着不等待,函数会立即返回。这在某些场景下有用,但初学者容易搞混。
  2. 接收缓冲区要够大。如果设了Size=100,但只收到50个字节,函数会一直等,直到超时。
  3. 注意数据类型。pData是uint8_t*,传字符串时要强转。
  4. 不要在中断里用轮询模式。中断里调用阻塞函数,会导致系统卡死。