基于RTOS信号量实现多优先级串口打印缓冲池
1. 问题背景与痛点
在嵌入式系统中,串口打印是调试和运行监控的常用手段。然而,传统直接使用printf或HAL_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配置步骤
-
启用信号量支持:在
FreeRTOSConfig.h中确保configUSE_MUTEXES和configUSE_COUNTING_SEMAPHORES为1。 - 创建信号量:在初始化代码中创建互斥信号量和二值信号量。
-
创建打印任务:任务优先级设为中等(如
tskIDLE_PRIORITY+2),栈大小足够(建议512字)。 -
配置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信号量实现的多优先级串口打印缓冲池,成功将日志输出从任务和中断中解耦,既保证了中断响应的实时性,又通过优先级调度确保关键日志优先输出。此方案在资源受限的嵌入式系统中尤为实用,是构建可靠日志系统的有效实践。