4. 轮询模式发送与接收:HAL_UART_Transmit()与HAL_UART_Receive()
轮询模式,说白了就是CPU死等。你发一个字节,CPU就站在那盯着状态寄存器,直到硬件说“发完了”,它才挪步干下一件事。接收也一样——没数据来,CPU就卡在函数里不动弹。
很多初学者觉得这太浪费了。没错,是浪费。但在某些场景下,轮询反而是最稳、最不容易出幺蛾子的方式。我做过一个工业控制项目,中断优先级配得乱七八糟,最后调试阶段全靠轮询模式保底。
4.1 阻塞发送:HAL_UART_Transmit()
先看函数原型:
HAL_StatusTypeDef HAL_UART_Transmit(
UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout
);
四个参数:
- huart:串口句柄指针,比如 &huart1
- pData:要发送的数据缓冲区指针
- Size:发送的字节数
- Timeout:超时时间,单位ms
函数内部干了什么?它先把数据写到发送数据寄存器(DR),然后轮询等待TXE(发送寄存器空)标志位。每发完一个字节,再等TC(发送完成)标志位。全部发完才返回。
关键点:HAL_UART_Transmit()返回HAL_OK才算发送成功。返回HAL_TIMEOUT说明超时了,返回HAL_ERROR说明参数有问题。
我习惯在发送前先检查huart->gState状态,确保串口不在忙状态。否则可能触发断言失败。
4.2 阻塞接收:HAL_UART_Receive()
接收函数长这样:
HAL_StatusTypeDef HAL_UART_Receive(
UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout
);
参数含义和发送函数完全对称。它等待RXNE(接收寄存器非空)标志位,每收到一个字节就存到pData缓冲区,直到收满Size个字节。
注意:HAL_UART_Receive()必须收到指定数量的字节才会返回。如果只来了3个字节但你要求收5个,它会一直等下去,直到超时。
我曾经踩过这个坑:上位机发不定长数据包,我用固定Size去收,结果串口卡死在那。后来改成先收包头再收数据体,分两次调用。
4.3 超时机制与HAL_MAX_DELAY
超时参数怎么设?看场景:
| 超时值 | 含义 | 适用场景 |
|---|---|---|
| 0 | 不等待,立即返回 | 查询模式,配合循环使用 |
| 1~0xFFFF | 指定毫秒数 | 大多数通信场景 |
| HAL_MAX_DELAY | 无限等待,永不超时 | 调试、回环测试 |
HAL_MAX_DELAY定义在stm32f1xx_hal_def.h里,值是0xFFFFFFFF。传入这个值后,HAL库内部的滴答计时器溢出判断永远不成立,函数会一直阻塞。
我个人建议:产品代码里别用HAL_MAX_DELAY。万一硬件出问题,程序就死锁了。调试时可以用,但正式发布前一定要改成合理的超时值。
4.4 轮询模式适用场景与性能分析
轮询模式到底什么时候用?我列个表:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 调试阶段 | 轮询 | 简单、可控、容易定位问题 |
| 低速率通信(<115200bps) | 轮询 | CPU占用可以接受 |
| 高波特率(>460800bps) | 中断/DMA | 轮询会严重拖慢系统 |
| 实时性要求高的系统 | 中断/DMA | 轮询阻塞影响任务调度 |
| 单次发送少量数据 | 轮询 | 代码简洁,延迟可预测 |
性能上,轮询模式在115200bps下发送100字节大约耗时8.7ms。如果主频72MHz,这期间CPU白白转了约62万条指令。你想想看,如果系统里还有别的任务要跑,这个开销就有点大了。
4.5 实战:串口回环测试
回环测试是验证串口收发通路最直接的方法。把TX和RX短接,发什么就收什么。
硬件连接:
- STM32F103的PA9(USART1_TX)接PA10(USART1_RX)
- 共地
代码实现:
#include "stm32f1xx_hal.h"
UART_HandleTypeDef huart1;
uint8_t tx_buffer[] = "Hello STM32!";
uint8_t rx_buffer[32];
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
int main(void)
{
HAL_Init();
MX_USART1_UART_Init();
while (1)
{
// 发送数据
HAL_UART_Transmit(&huart1, tx_buffer, sizeof(tx_buffer)-1, 1000);
// 延时一会儿,等数据回环
HAL_Delay(10);
// 接收数据(收同样长度)
HAL_UART_Receive(&huart1, rx_buffer, sizeof(tx_buffer)-1, 1000);
// 比较收发是否一致
if (memcmp(tx_buffer, rx_buffer, sizeof(tx_buffer)-1) == 0)
{
// 回环成功,可以在这里点个灯
}
else
{
// 回环失败,数据不一致
}
HAL_Delay(1000);
}
}
调试技巧:如果回环测试失败,先检查硬件连接。我遇到过好几次是杜邦线接触不良,导致数据丢包。用示波器看TX脚波形是最快的方法。
回环测试通过后,说明串口硬件和HAL库配置都没问题。接下来就可以放心地做更复杂的通信协议了。
核心要点总结:
- 轮询模式是阻塞的,CPU在收发期间不干别的事
- 超时参数要合理设置,产品代码慎用HAL_MAX_DELAY
- 回环测试是验证串口通路最有效的手段
- 轮询适合调试、低速率、少量数据的场景