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库配置都没问题。接下来就可以放心地做更复杂的通信协议了。

轮询模式串口通信流程 主循环开始 HAL_UART_Transmit() 等待TXE/TC标志 HAL_UART_Receive() 等待RXNE标志 循环 超时机制 Timeout=0 → 立即返回 Timeout=N → 等待N ms HAL_MAX_DELAY → 无限等

核心要点总结:

  • 轮询模式是阻塞的,CPU在收发期间不干别的事
  • 超时参数要合理设置,产品代码慎用HAL_MAX_DELAY
  • 回环测试是验证串口通路最有效的手段
  • 轮询适合调试、低速率、少量数据的场景