3. 轮询式串口驱动:阻塞发送与接收的实现,及其在RTOS中的问题

轮询式(Polling)串口驱动是最基础的实现方式。CPU 通过循环检查状态寄存器,等待硬件完成一个字节的发送或接收。这种“忙等待”机制在裸机编程中简单直接,但引入 RTOS 后,会引发严重的多任务互斥与实时性问题。

3.1 阻塞发送的实现原理

阻塞发送的核心逻辑:应用程序调用发送函数后,函数不会立即返回,而是持续轮询串口状态寄存器,直到当前字节发送完成(TX 缓冲区空/发送完成标志置位),再发送下一个字节。整个过程 CPU 被完全占用。

典型实现代码(STM32 HAL 风格):
// 轮询式阻塞发送一个字节
void UART_Polling_SendByte(UART_HandleTypeDef *huart, uint8_t data)
{
    // 等待上一次发送完成(TXE 标志)
    while (!(huart->Instance->SR & USART_SR_TXE))
    {
        // 空循环,CPU 100% 占用
    }
    // 写入数据寄存器
    huart->Instance->DR = data;
}

// 轮询式阻塞发送多字节
void UART_Polling_SendBuffer(UART_HandleTypeDef *huart,
                             uint8_t *pData, uint16_t Size)
{
    for (uint16_t i = 0; i < Size; i++)
    {
        UART_Polling_SendByte(huart, pData[i]);
    }
    // 等待最后一个字节发送完成(TC 标志)
    while (!(huart->Instance->SR & USART_SR_TC))
    {
        // 空循环
    }
}
关键点: 轮询发送的“阻塞”体现在函数返回前,调用线程无法执行任何其他任务。发送 100 字节 @ 115200bps 约耗时 8.7ms,期间 CPU 完全被该线程独占。

3.2 阻塞接收的实现原理

阻塞接收同样采用忙等待:应用程序调用接收函数后,循环检查 RXNE(接收数据寄存器非空)标志,直到收到一个字节才返回。若没有数据到达,线程将永久卡死在轮询循环中。

// 轮询式阻塞接收一个字节
uint8_t UART_Polling_ReceiveByte(UART_HandleTypeDef *huart)
{
    // 等待接收数据到达
    while (!(huart->Instance->SR & USART_SR_RXNE))
    {
        // 空循环,CPU 100% 占用
    }
    // 读取数据寄存器
    return (uint8_t)(huart->Instance->DR & 0xFF);
}

// 轮询式阻塞接收多字节(带超时)
uint8_t UART_Polling_ReceiveBuffer(UART_HandleTypeDef *huart,
                                   uint8_t *pData, uint16_t Size,
                                   uint32_t Timeout)
{
    uint32_t tickstart = HAL_GetTick();
    for (uint16_t i = 0; i < Size; i++)
    {
        while (!(huart->Instance->SR & USART_SR_RXNE))
        {
            // 超时检查
            if ((HAL_GetTick() - tickstart) > Timeout)
            {
                return 1; // 超时错误
            }
        }
        pData[i] = (uint8_t)(huart->Instance->DR & 0xFF);
    }
    return 0; // 成功
}
注意: 轮询接收若不实现超时机制,一旦对端停止发送数据,当前线程将永久挂起,导致整个系统假死。在 RTOS 中,这种无限制的忙等待会阻塞所有低优先级任务。

3.3 轮询驱动在RTOS中的核心问题

将轮询式串口驱动直接移植到 RTOS 环境,会暴露以下四个关键问题:

问题 描述 影响等级
CPU 资源浪费 轮询等待期间 CPU 100% 空转,无法执行其他任务 严重
任务优先级反转 低优先级任务持有串口锁,高优先级任务被阻塞等待 严重
中断响应延迟 长时间轮询关闭中断或屏蔽调度器,导致高优先级中断丢失 中等
多任务互斥困难 多个任务同时调用轮询发送/接收,导致数据错乱 严重
轮询式串口驱动在RTOS中的问题示意图 轮询串口驱动 while(!TXE) { } while(!RXNE) { } while(!TC) { } CPU 100% 空转 RTOS 环境问题 任务优先级反转 中断响应延迟 多任务互斥失败 系统实时性下降 轮询驱动直接移植到 RTOS 会破坏任务调度与实时性

3.4 多任务互斥问题详解

当两个或以上任务同时访问串口硬件时,轮询驱动缺乏原子性保护,会导致数据交叉错乱。以下是一个典型竞态场景:

竞态场景示例:
// 任务A:发送 "Hello"
void TaskA(void *arg)
{
    while (1)
    {
        UART_Polling_SendBuffer(&huart1, "Hello", 5);
        vTaskDelay(100);
    }
}

// 任务B:发送 "World"
void TaskB(void *arg)
{
    while (1)
    {
        UART_Polling_SendBuffer(&huart1, "World", 5);
        vTaskDelay(100);
    }
}

由于轮询发送函数内部没有临界区保护,任务A发送到一半时可能被任务B抢占,导致串口输出变成 "HeWorllldo" 等乱序数据。这种问题在裸机中不会出现(单任务),但在 RTOS 多任务环境下是致命缺陷。

3.5 轮询驱动与RTOS调度器的冲突

轮询等待循环会阻止 RTOS 调度器运行。若轮询期间关闭了中断或调度锁,系统将完全失去响应能力。

场景 轮询行为 调度器状态 后果
轮询发送 100 字节 ~8ms 空循环 无法调度 高优先级任务延迟 8ms
轮询接收无超时 永久等待 永久阻塞 系统假死
轮询中关中断 空循环 中断被屏蔽 丢失串口中断及其他中断
经验法则: 在 RTOS 中,任何形式的忙等待(while 空循环)都应被视为设计缺陷。应使用信号量、消息队列或事件标志组来替代轮询等待。

3.6 轮询驱动的适用场景

尽管轮询驱动在 RTOS 中存在诸多问题,但在以下受限场景中仍可谨慎使用:

  • 系统启动阶段: RTOS 内核尚未启动,或调度器未运行,此时只能使用轮询方式输出调试信息。
  • 临界错误处理: 系统发生致命错误(HardFault),需要立即输出错误码,此时中断可能已不可用。
  • 极低波特率调试: 发送单个字节耗时极短(如 1200bps 下约 8ms),且系统无其他实时任务。
  • 单任务裸机过渡: 从裸机向 RTOS 迁移的初期阶段,临时保留轮询驱动用于验证硬件。
强烈建议: 在产品级 RTOS 项目中,轮询式串口驱动仅用于调试输出(printf),不应作为正式通信接口。后续章节将介绍中断驱动和 DMA 驱动的正确实现方式。

3.7 本章小结