基于RTOS信号量实现多优先级串口打印缓冲池

1. 问题背景与痛点

在嵌入式系统中,串口打印是调试和运行监控的常用手段。然而,传统直接使用printfHAL_UART_Transmit(阻塞模式)存在两大问题:

  • 中断阻塞:若在中断服务函数(ISR)中调用阻塞打印,会长时间占用CPU,导致其他中断无法及时响应,甚至丢失实时事件。
  • 任务优先级反转:低优先级任务频繁打印时,可能阻塞高优先级任务的执行,破坏实时调度。

解决方案的核心是异步化:将打印操作从任务/中断中解耦,通过缓冲池和RTOS信号量实现安全的数据传递。

2. 设计原理

2.1 信号量角色

  • 互斥信号量(Mutex):保护缓冲池的读写操作,防止多任务并发访问导致数据竞争。
  • 二值信号量(Binary Semaphore):通知后台打印任务有数据可处理,实现任务间同步。

2.2 多优先级缓冲池架构

我们设计一个环形缓冲池,支持不同优先级的日志(如ERROR、WARN、INFO)。每个优先级对应独立的缓冲区,避免高优先级日志被低优先级日志淹没。后台任务(prvUARTPrintTask)根据优先级顺序处理缓冲区,确保关键日志优先输出。

// 缓冲区结构定义
typedef struct {
    uint8_t *data;      // 数据指针
    uint16_t head;      // 写索引
    uint16_t tail;      // 读索引
    uint16_t size;      // 缓冲区大小
} RingBuffer_t;

// 优先级定义
#define LOG_LEVEL_ERROR  0
#define LOG_LEVEL_WARN   1
#define LOG_LEVEL_INFO   2
#define LOG_LEVEL_MAX    3

static RingBuffer_t logBuffers[LOG_LEVEL_MAX];
static SemaphoreHandle_t mutex;          // 互斥信号量
static SemaphoreHandle_t binSem;         // 二值信号量,通知打印任务

3. FreeRTOS配置步骤

  1. 启用信号量支持:在FreeRTOSConfig.h中确保configUSE_MUTEXESconfigUSE_COUNTING_SEMAPHORES为1。
  2. 创建信号量:在初始化代码中创建互斥信号量和二值信号量。
  3. 创建打印任务:任务优先级设为中等(如tskIDLE_PRIORITY+2),栈大小足够(建议512字)。
  4. 配置UART:使用中断方式发送(如HAL_UART_Transmit_IT),并在发送完成回调中释放二值信号量,实现流控。

4. 完整代码示例

4.1 初始化函数

void Log_Init(void) {
    // 分配静态缓冲区(也可动态分配)
    static uint8_t bufError[128];
    static uint8_t bufWarn[128];
    static uint8_t bufInfo[256];
    
    RingBuffer_Init(&logBuffers[LOG_LEVEL_ERROR], bufError, sizeof(bufError));
    RingBuffer_Init(&logBuffers[LOG_LEVEL_WARN], bufWarn, sizeof(bufWarn));
    RingBuffer_Init(&logBuffers[LOG_LEVEL_INFO], bufInfo, sizeof(bufInfo));
    
    mutex = xSemaphoreCreateMutex();
    binSem = xSemaphoreCreateBinary();
    
    xTaskCreate(prvUARTPrintTask, "UARTPrint", 512, NULL, 2, NULL);
}

4.2 日志写入函数(可被中断调用)

void Log_Write(uint8_t level, const char *fmt, ...) {
    char buffer[128];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buffer, sizeof(buffer), fmt, args);
    va_end(args);
    
    // 从ISR或任务调用,使用带中断保护的互斥
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (xPortIsInsideInterrupt()) {
        xSemaphoreTakeFromISR(mutex, &xHigherPriorityTaskWoken);
    } else {
        xSemaphoreTake(mutex, portMAX_DELAY);
    }
    
    // 写入对应优先级缓冲区
    RingBuffer_Write(&logBuffers[level], (uint8_t*)buffer, strlen(buffer));
    
    if (xPortIsInsideInterrupt()) {
        xSemaphoreGiveFromISR(mutex, &xHigherPriorityTaskWoken);
        xSemaphoreGiveFromISR(binSem, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    } else {
        xSemaphoreGive(mutex);
        xSemaphoreGive(binSem);
    }
}

4.3 后台打印任务

static void prvUARTPrintTask(void *params) {
    uint8_t data;
    while (1) {
        // 等待数据通知
        xSemaphoreTake(binSem, portMAX_DELAY);
        
        // 按优先级顺序处理(ERROR -> WARN -> INFO)
        for (int level = 0; level < LOG_LEVEL_MAX; level++) {
            while (RingBuffer_Read(&logBuffers[level], &data, 1) == 1) {
                // 使用中断发送,避免阻塞
                HAL_UART_Transmit_IT(&huart1, &data, 1);
                // 等待发送完成(由UART完成回调释放信号量)
                xSemaphoreTake(uartTxSem, portMAX_DELAY);
            }
        }
    }
}

4.4 UART发送完成回调

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        xSemaphoreGiveFromISR(uartTxSem, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

5. 关键注意事项

  • 中断安全:在ISR中调用Log_Write时,必须使用FromISR版本的信号量函数,并检查是否需要上下文切换。
  • 缓冲区溢出:环形缓冲区需处理写满情况,可丢弃最旧数据或丢弃新数据,根据应用选择。
  • 优先级反转:互斥信号量可能引起优先级继承问题,FreeRTOS的互斥量已内置优先级继承机制,但需确保所有任务使用同一互斥量。
  • 内存开销:每个缓冲区大小需权衡,过小导致日志丢失,过大浪费RAM。建议根据日志频率动态调整。
  • 任务栈大小:打印任务中调用vsnprintf可能占用较大栈空间,需合理配置。

6. 总结

通过RTOS信号量实现的多优先级串口打印缓冲池,成功将日志输出从任务和中断中解耦,既保证了中断响应的实时性,又通过优先级调度确保关键日志优先输出。此方案在资源受限的嵌入式系统中尤为实用,是构建可靠日志系统的有效实践。