3. 串口发送

任务说明

本案例通过串口发送一串数据,对比两种实现方式:裸机轮询阻塞发送与RTOS队列+中断异步发送。核心目标是理解阻塞等待与异步通知在CPU利用率上的差异,并验证两种方式均能正确完成数据发送。

环境依赖

  • 硬件:任意集成UART外设的MCU(本例以STM32F103系列为参考)
  • RTOS:FreeRTOS(V10.0+),需开启队列API
  • 工具链:arm-none-eabi-gcc + STM32CubeMX生成的初始化代码(或等效HAL库)

裸机阻塞发送实现

// 裸机方式:轮询TXE标志,阻塞等待每个字节发送完成
void BareMetal_SendString(const char* str) {
    while (*str) {
        // 等待发送数据寄存器空(TXE置位)
        while (!(USART1->SR & USART_SR_TXE));
        USART1->DR = *str++;
    }
    // 等待最后一个字节发送完成(TC置位)
    while (!(USART1->SR & USART_SR_TC));
}

// 调用示例
int main_bare(void) {
    const char msg[] = "BareMetal blocking send\r\n";
    while (1) {
        BareMetal_SendString(msg);
        Delay_ms(1000);  // 简单延时,观察效果
    }
}

RTOS队列+中断异步发送实现

// RTOS方式:任务将数据放入队列,UART中断从队列取出并发送
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

QueueHandle_t uart_queue;
volatile uint8_t tx_busy = 0;

// UART发送中断服务函数(在stm32f1xx_it.c中调用)
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (USART1->SR & USART_SR_TXE) {
        uint8_t ch;
        if (xQueueReceiveFromISR(uart_queue, &ch, &xHigherPriorityTaskWoken)) {
            USART1->DR = ch;
        } else {
            // 队列空,关闭TXE中断
            USART1->CR1 &= ~USART_CR1_TXEIE;
            tx_busy = 0;
        }
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 发送任务:将字符串逐字节放入队列
void vUartSendTask(void* params) {
    const char msg[] = "RTOS async send via queue\r\n";
    while (1) {
        const char* p = msg;
        while (*p) {
            // 等待队列有空间
            xQueueSend(uart_queue, p++, portMAX_DELAY);
        }
        // 等待最后一个字节发送完成(简单延时模拟业务处理)
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 初始化:创建队列并启动任务
void RTOS_UART_Init(void) {
    uart_queue = xQueueCreate(16, sizeof(uint8_t));
    // 使能UART1时钟、配置GPIO和USART(使用HAL或寄存器均可)
    // 使能USART1中断:NVIC_EnableIRQ(USART1_IRQn);
    // 注意:初始不使能TXE中断,由任务首次发送时触发
    xTaskCreate(vUartSendTask, "UartSend", 128, NULL, 1, NULL);
}

运行与验证

  1. 裸机验证:编译烧录后,用串口助手连接UART1(默认115200-8-N-1),观察是否周期性收到BareMetal blocking send字符串。
  2. RTOS验证:编译烧录RTOS版本,观察是否周期性收到RTOS async send via queue字符串。
  3. 效率对比方法:在裸机发送循环中翻转一个GPIO,用示波器测量高电平持续时间(即阻塞等待时间);在RTOS发送任务中,在xQueueSend前后翻转GPIO,测量任务被阻塞的时间。对比两者,RTOS方式下任务等待队列的时间远小于裸机方式等待硬件发送完成的时间。

通过条件:两种方式均能正确、完整地输出预定字符串;示波器显示RTOS方式下任务阻塞时间明显缩短(通常降低50%以上),证明CPU在等待期间可被调度执行其他任务。

时间 裸机阻塞发送 CPU忙等发送 空闲 CPU忙等发送 空闲 阻塞等待 RTOS队列异步发送 入队 任务B运行 入队 任务B运行 入队 任务B运行 UART中断后台逐字节发送(不占任务时间) CPU可调度其他任务

关键差异说明

  • 裸机方式:CPU在while循环中空转等待硬件标志位,期间无法执行其他任务,CPU利用率低。
  • RTOS方式:任务仅将数据放入队列即让出CPU(xQueueSend内部可能阻塞),实际发送由中断逐字节完成,任务可被调度执行其他工作。
  • 队列作用:作为任务与中断之间的数据缓冲区,实现生产者和消费者的解耦,同时利用中断机制避免CPU轮询等待。