2. 轮询模式串口通信实现:阻塞式收发的真相

轮询模式,也叫阻塞式收发。说白了就是CPU死等。你调用HAL_UART_Transmit,它就把数据一个字节一个字节往外送,送完才返回。这期间CPU啥也干不了。

我刚开始用STM32时,觉得这模式简单啊,直接调API就行。后来项目一复杂,才发现这里面坑不少。今天咱们就把这块掰开揉碎了讲。

2.1 阻塞式收发的基本流程

先看最常用的发送函数:

HAL_StatusTypeDef HAL_UART_Transmit(
    UART_HandleTypeDef *huart,
    uint8_t *pData,
    uint16_t Size,
    uint32_t Timeout
);

四个参数:串口句柄、数据指针、发送长度、超时时间。超时单位是毫秒,设为HAL_MAX_DELAY就是无限等。

接收函数类似:

HAL_StatusTypeDef HAL_UART_Receive(
    UART_HandleTypeDef *huart,
    uint8_t *pData,
    uint16_t Size,
    uint32_t Timeout
);

用法示例:

uint8_t tx_buffer[] = "Hello STM32\r\n";
HAL_UART_Transmit(&huart1, tx_buffer, sizeof(tx_buffer), 1000);

uint8_t rx_buffer[10];
HAL_UART_Receive(&huart1, rx_buffer, 5, 500);

发送时,函数内部会轮询TXE(发送寄存器空)标志位。每发一个字节,就等硬件把数据从DR寄存器移到移位寄存器,然后才能发下一个。接收同理,轮询RXNE(接收非空)标志位。

核心问题:整个等待过程,CPU一直在跑while循环检查标志位。没有中断,没有DMA,就是硬等。

2.2 CPU占用率实测——数据说话

我拿STM32F103C8T6做了个测试,主频72MHz,串口波特率115200。用逻辑分析仪抓引脚电平变化,同时用示波器看一个GPIO翻转来标记CPU忙闲状态。

数据量(字节) 发送耗时(μs) CPU占用率 可执行其他任务
1 约87 100% 否
10 约870 100% 否
100 约8700 100% 否
1000 约87000 100% 否

看到了吗?不管发多少数据,CPU占用率都是100%。因为它在while循环里死等。哪怕只发1个字节,这87μs里CPU也干不了别的。

为什么会这样?因为115200波特率下,1个字节(含起始位、8数据位、停止位)大约耗时87μs。这87μs里CPU就在那轮询TXE标志位。

我的经验:如果你系统里只有一个串口任务,且数据量很小,轮询模式其实够用。但一旦有多个外设需要同时处理,轮询模式就是灾难。

2.3 实时性瓶颈分析

轮询模式有三个致命问题:

  1. CPU被完全占用——发数据期间无法响应其他中断
  2. 无法处理突发数据——如果接收时数据来得快,你还在发上一包,接收缓冲区就溢出了
  3. 超时机制不可靠——Timeout参数是基于SysTick的,如果SysTick被其他高优先级中断阻塞,超时就不准了

我遇到过最坑的一次:一个设备用轮询模式收GPS数据,波特率9600。本来好好的,后来加了传感器采集任务,结果GPS数据经常丢包。查了半天,发现是传感器采集占用了太长时间,导致接收超时返回HAL_TIMEOUT,数据没读完就被丢弃了。

避坑指南:千万不要在主循环里用HAL_UART_Receive接收不定长数据。一旦设置Size=10,Timeout=1000,而对方只发了5个字节,你就得等1000ms超时才能返回。这期间系统完全卡死。

2.4 什么时候该用轮询模式?

虽然我吐槽了这么多,但轮询模式不是一无是处。它适合以下场景:

  • 调试阶段——快速验证串口是否正常工作
  • 简单系统——只有一个串口任务,没有其他实时要求
  • 低数据量——每次只发几个字节,且间隔时间长
  • 同步通信——比如和某个传感器一问一答,必须等回复

我个人习惯:项目初期先用轮询模式把功能跑通,确认硬件没问题后,再切换到中断或DMA模式。这样调试效率高,出问题也好定位。

2.5 轮询模式下的性能调优技巧

如果你不得不用轮询模式,这几个技巧能帮你省点CPU:

  1. 降低波特率——9600波特率下1字节约1ms,CPU占用时间更长。但如果你CPU有其他事要做,反而应该提高波特率,让发送更快结束
  2. 合并发送——把多个小数据包拼成一个大包一次发送,减少函数调用次数
  3. 缩短超时——接收时Timeout设小一点,比如10ms,超时后返回再重试
  4. 使用DMA——嗯,这个后面章节会详细讲

举个例子,如果你要发10个字节,分10次调用HAL_UART_Transmit,每次1字节,总耗时是10×87μs + 10×函数调用开销。如果一次发10字节,总耗时还是约870μs,但省了9次函数调用开销。

2.6 轮询模式知识体系

下面这张图总结了轮询模式的核心要点:

轮询模式串口通信知识体系 HAL_UART_Transmit/Receive CPU 100% 占用 阻塞等待标志位 超时机制不可靠 调试阶段 简单系统 低数据量 提高波特率 合并数据包 缩短超时 核心结论 轮询模式适合调试和简单系统,复杂项目建议用中断或DMA

2.7 本章小结

轮询模式串口通信,说白了就是CPU亲自下场干苦力。每发一个字节,CPU就在那盯着标志位,啥也干不了。优点是代码简单、逻辑清晰,缺点是CPU利用率极低、实时性差。

我建议你记住三点:

  • 调试阶段放心用轮询模式,快速验证功能
  • 正式项目中,如果串口数据量大或需要并发处理,果断换中断或DMA
  • 如果非要用轮询,记得合并数据包、提高波特率、缩短超时