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 实时性瓶颈分析
轮询模式有三个致命问题:
- CPU被完全占用——发数据期间无法响应其他中断
- 无法处理突发数据——如果接收时数据来得快,你还在发上一包,接收缓冲区就溢出了
- 超时机制不可靠——Timeout参数是基于SysTick的,如果SysTick被其他高优先级中断阻塞,超时就不准了
我遇到过最坑的一次:一个设备用轮询模式收GPS数据,波特率9600。本来好好的,后来加了传感器采集任务,结果GPS数据经常丢包。查了半天,发现是传感器采集占用了太长时间,导致接收超时返回HAL_TIMEOUT,数据没读完就被丢弃了。
避坑指南:千万不要在主循环里用HAL_UART_Receive接收不定长数据。一旦设置Size=10,Timeout=1000,而对方只发了5个字节,你就得等1000ms超时才能返回。这期间系统完全卡死。
2.4 什么时候该用轮询模式?
虽然我吐槽了这么多,但轮询模式不是一无是处。它适合以下场景:
- 调试阶段——快速验证串口是否正常工作
- 简单系统——只有一个串口任务,没有其他实时要求
- 低数据量——每次只发几个字节,且间隔时间长
- 同步通信——比如和某个传感器一问一答,必须等回复
我个人习惯:项目初期先用轮询模式把功能跑通,确认硬件没问题后,再切换到中断或DMA模式。这样调试效率高,出问题也好定位。
2.5 轮询模式下的性能调优技巧
如果你不得不用轮询模式,这几个技巧能帮你省点CPU:
- 降低波特率——9600波特率下1字节约1ms,CPU占用时间更长。但如果你CPU有其他事要做,反而应该提高波特率,让发送更快结束
- 合并发送——把多个小数据包拼成一个大包一次发送,减少函数调用次数
- 缩短超时——接收时Timeout设小一点,比如10ms,超时后返回再重试
- 使用DMA——嗯,这个后面章节会详细讲
举个例子,如果你要发10个字节,分10次调用HAL_UART_Transmit,每次1字节,总耗时是10×87μs + 10×函数调用开销。如果一次发10字节,总耗时还是约870μs,但省了9次函数调用开销。
2.6 轮询模式知识体系
下面这张图总结了轮询模式的核心要点:
2.7 本章小结
轮询模式串口通信,说白了就是CPU亲自下场干苦力。每发一个字节,CPU就在那盯着标志位,啥也干不了。优点是代码简单、逻辑清晰,缺点是CPU利用率极低、实时性差。
我建议你记住三点:
- 调试阶段放心用轮询模式,快速验证功能
- 正式项目中,如果串口数据量大或需要并发处理,果断换中断或DMA
- 如果非要用轮询,记得合并数据包、提高波特率、缩短超时