基于 FreeRTOS Message Buffer 构建异步日志系统
2026/9/15 8:20:53 网站建设 项目流程

基于 FreeRTOS Message Buffer 构建异步日志系统

目录

  • 1. 概述
  • 2. FreeRTOS Message Buffer 简介
  • 3. 架构设计
    • 3.1 整体架构图
    • 3.2 三缓冲区设计
    • 3.3 线程安全策略
  • 4. 数据结构详解
  • 5. 核心流程
    • 5.1 初始化流程
    • 5.2 日志写入流程
    • 5.3 日志输出流程
  • 6. 使用指南
    • 6.1 基本使用
    • 6.2 配置说明
    • 6.3 过滤功能
  • 7. 中断上下文支持
  • 8. 注意事项
  • 9. 完整示例

1. 概述

本日志系统基于FreeRTOS Message Buffer实现,具备以下核心特性:

特性说明
异步输出日志格式化与物理输出分离,写入不阻塞
任务态 + 中断态同时支持任务上下文和 ISR 上下文调用
无锁 ISR 路径ISR 使用独立缓冲区,无需临界区保护
ANSI 颜色支持终端彩色输出,可按级别区分
日志过滤支持按 Tag 前缀过滤日志
级别控制运行时动态调整输出级别
静态内存全部使用静态分配,无动态内存开销

2. FreeRTOS Message Buffer 简介

Message Buffer是 FreeRTOS 提供的一种轻量级 IPC 机制,特点如下:

  • 流式传输:不保留消息边界,数据以字节流形式传递(类似管道)。
  • 变长消息:每条消息长度可变,适合日志这种不定长数据。
  • 零拷贝发送xMessageBufferSend()会将数据直接拷贝到内部环形缓冲区。
  • FromISR 版本:提供xMessageBufferSendFromISR(),可在中断中安全调用。
  • 静态创建:通过xMessageBufferCreateStatic()使用用户提供的内存,避免堆分配。
┌──────────────┐ xMessageBufferSend() ┌──────────────────┐ xMessageBufferReceive() ┌──────────────┐ │ 写入者 │ ──────────────────────▶ │ Message Buffer │ ──────────────────────────▶ │ 读取者 │ │ (任务/ISR) │ │ (环形缓冲区) │ │ (输出任务) │ └──────────────┘ └──────────────────┘ └──────────────┘

相比 FreeRTOS Queue:

  • Queue 要求每条消息固定大小,日志消息长度差异大,会造成内存浪费。
  • Message Buffer 按实际长度写入,空间利用率更高。

3. 架构设计

3.1 整体架构图

┌─────────────────────────────────┐ │ log_async_t │ │ │ ┌──────────┐ │ ┌────────────┐ ┌─────────────┐ │ │ 任务态 │──mutex──────▶│ │ input_buf │ │ isr_buf │ │◀──无锁─── ISR │ 调用者 │ │ │ (格式化) │ │ (格式化) │ │ └──────────┘ │ └─────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────┐ │ │ │ xMessageBufferSend() │ │ │ │ xMessageBufferSendFromISR │ │ │ └───────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ Message Buffer │ │ │ │ (storage 环形缓冲区) │ │ │ └───────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ xMessageBufferReceive() │ │ │ └───────────┬──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ output_buf │ │ │ │ (接收 → print回调) │ │ │ └───────────┬──────────────┘ │ └──────────────┼───────────────────┘ │ ▼ ┌──────────────┐ │ 物理输出 │ │ (UART/USB等) │ └──────────────┘

3.2 三缓冲区设计

系统使用三个独立缓冲区,各司其职:

缓冲区用途使用者并发保护
input_buf任务态日志格式化log_async_write_core()任务路径Mutex
isr_buf中断态日志格式化log_async_write_core()ISR 路径无(ISR 独占)
output_buf接收 Message Buffer 数据log_async_task()输出任务单任务独占

设计动机:如果格式化和接收共用同一个缓冲区,在输出任务执行xMessageBufferReceive()的同时,写入者正在格式化,会导致数据被破坏。三缓冲区彻底消除了这种竞态。

3.3 线程安全策略

任务态调用: 调用者 A ──mutex_lock──▶ format(input_buf) ──▶ xMessageBufferSend() ──▶ mutex_unlock 调用者 B ──等待 mutex──────────────────────────────────────────────────▶ mutex_lock ... 中断态调用: ISR ──▶ format(isr_buf) ──▶ xMessageBufferSendFromISR() ──▶ portYIELD_FROM_ISR() (无锁,isr_buf 由单个 ISR 独占,不会被其他 ISR 抢占)
路径同步机制原因
任务态写入Mutex多任务可能并发调用,Mutex 保证input_buf互斥访问
中断态写入无锁ISR 不会被同优先级中断抢占;isr_buf独立,不与任务态冲突
xMessageBufferSend()内部线程安全FreeRTOS 内部已做保护(任务级暂停调度或临界区)
xMessageBufferSendFromISR()内部线程安全FreeRTOS 内部已做保护(关中断)

注意log_async_lock()/log_async_unlock()使用的是 FreeRTOS Mutex(互斥锁),它不能在 ISR 中使用。这也是 ISR 路径必须设计为无锁的根本原因。


4. 数据结构详解

// 日志级别typedefenum{LOG_LEVEL_ERROR=0,// 错误LOG_LEVEL_WARN,// 警告LOG_LEVEL_INFO,// 信息LOG_LEVEL_DEBUG// 调试}log_level_t;// 日志系统句柄typedefstruct{// ── FreeRTOS 内核对象 ──MessageBufferHandle_t buffer;// Message Buffer 句柄TaskHandle_t output_task;// 输出任务句柄SemaphoreHandle_t mutex;// 任务态互斥锁StaticMessageBuffer_t static_buffer;// Message Buffer 静态控制块// ── 缓冲区 ──uint8_t*storage;// Message Buffer 环形缓冲区存储char*input_buf;// 任务态格式化缓冲区size_tinput_len;// input_buf 大小char*isr_buf;// ISR 专用格式化缓冲区(独立,无锁)size_tisr_buf_len;// isr_buf 大小char*output_buf;// 输出任务接收缓冲区(独立)size_toutput_len;// output_buf 大小// ── 控制字段 ──log_level_toutput_level;// 当前输出级别uint8_tenable_color;// 是否启用 ANSI 颜色uint8_tfilter_type;// 过滤类型(0=关闭, 1=白名单, 2=全部通过)charfilters[16];// 过滤关键字(Tag 前缀匹配)// ── 输出回调 ──int(*print)(char*log,size_tsize);// 物理输出函数指针}log_async_t;

5. 核心流程

5.1 初始化流程

// 1. 静态分配所有缓冲区staticuint8_tgLogStorage[LOG_BUFFER_SIZE];// Message Buffer 存储区staticchargLogInput[LOG_LINE_MAX_LEN];// 任务态格式化缓冲区staticchargLogIsrBuf[LOG_ISR_BUFFER_SIZE];// ISR 专用格式化缓冲区staticchargLogOutput[LOG_LINE_MAX_LEN];// 输出任务接收缓冲区// 2. 调用初始化log_async_init(&gLogAsync,gLogStorage,sizeof(gLogStorage),// Message BuffergLogInput,sizeof(gLogInput),// 任务态 bufgLogOutput,sizeof(gLogOutput),// 输出 bufgLogIsrBuf,sizeof(gLogIsrBuf));// ISR buf// 3. 注册输出回调gLogAsync.print=(int(*)(char*,size_t))uart_send;

初始化函数内部执行:

  1. 设置默认配置(颜色开启、DEBUG 级别、过滤关闭)
  2. 保存所有缓冲区指针和大小
  3. 调用xMessageBufferCreateStatic()创建静态 Message Buffer
  4. 调用xSemaphoreCreateMutex()创建互斥锁

5.2 日志写入流程

log_async_write_core(log, level, tag, file, line, format, ...) │ ├─ 1. 检测上下文:xPortIsInsideInterrupt() │ ├─ 2. 加锁(仅任务态):log_async_lock() │ ISR 路径跳过此步 │ ├─ 3. 参数校验 + 级别过滤 + Tag 过滤 │ 不通过 → 解锁/返回 │ ├─ 4. 格式化日志(分路径): │ │ ┌─ ISR 路径 ─────────────────────────────────────┐ │ │ 使用 isr_buf(独立,无竞争) │ │ │ 格式:[COLOR][LEVEL][TAG][FILE][LINE] message │ │ │ 发送:xMessageBufferSendFromISR() │ │ │ 调度:portYIELD_FROM_ISR() │ │ └─────────────────────────────────────────────────┘ │ │ ┌─ 任务路径 ──────────────────────────────────────┐ │ │ 使用 input_buf(mutex 保护) │ │ │ 格式:[COLOR][LEVEL][TAG][FILE][LINE] message │ │ │ 发送:xMessageBufferSend() │ │ │ 解锁:log_async_unlock() │ │ └─────────────────────────────────────────────────┘

日志格式样例

\033[32m[INF][SENSOR][main.c][00123] temperature: 25.6°C\033[0m\r\n │ │ │ │ │ │ │ │ 颜色 级别 标签 文件名 行号 用户消息 颜色重置 换行

5.3 日志输出流程

输出任务log_async_task()在独立线程中运行:

voidlog_async_task(void*arg){log_async_t*log=(log_async_t*)arg;while(1){// 从 Message Buffer 读取到 output_bufsize_tlen=xMessageBufferReceive(log->buffer,(void*)log->output_buf,log->output_len,pdMS_TO_TICKS(10)// 10ms 超时);// 调用物理输出回调if(len>0&&log->print){log->print((char*)log->output_buf,len);}vTaskDelay(pdMS_TO_TICKS(40));// 40ms 调度间隔}}

关键点

  • 输出任务使用独立的output_buf,与写入侧的input_bufisr_buf完全分离。
  • xMessageBufferReceive()可能一次读取多条日志(如果间隔内有多次写入)。
  • log->print回调由用户注册,将格式化后的日志发送到实际外设(UART、USB CDC、网络等)。

6. 使用指南

6.1 基本使用

步骤 1:分配缓冲区

#include"log_async.h"// 全局缓冲区(静态分配)staticuint8_tgLogStorage[LOG_BUFFER_SIZE];staticchargLogInput[LOG_LINE_MAX_LEN];staticchargLogIsrBuf[LOG_ISR_BUFFER_SIZE];staticchargLogOutput[LOG_LINE_MAX_LEN];

步骤 2:初始化

voidapp_log_init(void){log_async_init(&gLogAsync,gLogStorage,sizeof(gLogStorage),gLogInput,sizeof(gLogInput),gLogOutput,sizeof(gLogOutput),gLogIsrBuf,sizeof(gLogIsrBuf));// 注册物理输出函数gLogAsync.print=(int(*)(char*,size_t))uart_send;}

步骤 3:定义 LOG_TAG 并输出日志

// 在每个 .c 文件顶部定义(可选,默认 "NO_TAG")#defineLOG_TAG"MAIN"voidmain(void){app_log_init();log_i("System started, version %d.%d",1,0);log_d("Debug counter: %d",42);log_w("Battery low: %d%%",15);log_e("Sensor read failed, errno: %d",-5);// 或使用带 Tag 的宏LOG_I("SENSOR","Temperature: %.1f°C",25.6);}

步骤 4:创建输出任务

// 在 FreeRTOS 初始化后创建xTaskCreate(log_async_task,"log_out",256,&gLogAsync,1,NULL);

6.2 配置说明

// 运行时调整日志级别(只输出 >= 该级别的日志)log_async_set_level(&gLogAsync,LOG_LEVEL_INFO);// 只输出 INFO/WARN/ERRORlog_async_set_level(&gLogAsync,LOG_LEVEL_DEBUG);// 输出所有级别// 开关 ANSI 颜色log_async_disable_color(&gLogAsync);// 关闭颜色log_async_enable_color(&gLogAsync);// 开启颜色

6.3 过滤功能

// 启用白名单过滤(只有 Tag 以指定前缀开头的日志才会输出)log_async_enable_filter(&gLogAsync,1);log_async_set_filters(&gLogAsync,"SENSOR");// 只输出 Tag="SENSOR*" 的日志// 关闭过滤(type=2 表示全部通过)log_async_disable_filter(&gLogAsync);

7. 中断上下文支持

本系统通过独立 ISR 缓冲区 + FromISR API实现了安全的中断内日志输出。

设计原理

为什么 ISR 路径不需要锁? 1. Cortex-M 中断嵌套机制:同优先级或更低优先级的中断不会被抢占。 因此单个 isr_buf 不会同时被多个 ISR 使用。 2. isr_buf 与 input_buf 物理独立: 任务态使用 input_buf,ISR 使用 isr_buf,互不干扰。 3. xMessageBufferSendFromISR() 是 FreeRTOS 官方 ISR API: 内部已实现关中断保护,线程安全。

ISR 中调用示例

voidTIM3_IRQHandler(void){if(TIM_GetITStatus(TIM3,TIM_IT_Update)){TIM_ClearITPendingBit(TIM3,TIM_IT_Update);// 在 ISR 中安全输出日志log_i("TIM3 tick (from ISR)");}}

ISR 路径执行流程

ISR 触发 │ ├─ log_async_write_core() 被调用 │ ├─ xPortIsInsideInterrupt() → pdTRUE │ ├─ 跳过 mutex_lock(Mutex 不能在 ISR 中使用) │ ├─ 使用 isr_buf 格式化(独立缓冲区,无竞争) │ ├─ xMessageBufferSendFromISR() 发送 │ 内部关中断,原子写入环形缓冲区 │ ├─ portYIELD_FROM_ISR(xHigherPriorityTaskWoken) │ 如果输出任务优先级更高,触发任务调度 │ └─ 直接 return(无需解锁)

8. 注意事项

8.1 缓冲区大小

场景建议值说明
LOG_BUFFER_SIZE512 ~ 4096Message Buffer 存储区。值越大,能缓存的未输出日志越多
LOG_LINE_MAX_LEN128 ~ 512单条日志最大长度。需容纳完整格式化后的日志行
LOG_ISR_BUFFER_SIZE64 ~ 256ISR 缓冲区。ISR 中日志通常较短,可以较小

如果 Message Buffer 满了:

  • 任务态:xMessageBufferSend()在超时时间内阻塞等待空间。
  • 中断态:xMessageBufferSendFromISR()返回pdFALSE日志被静默丢弃

8.2 性能考虑

  • 格式化开销:日志格式化(vsnprintf)在调用者上下文中执行,不在输出任务中。因此日志内容越长,调用者阻塞时间越长。
  • ISR 中应避免大量日志:虽然 ISR 路径无锁,但vsnprintf仍有 CPU 开销。
  • 输出任务优先级:建议设为较低优先级,避免抢占关键业务逻辑。

8.3 平台依赖

  • xPortIsInsideInterrupt()是 FreeRTOS 特有 API,用于检测当前执行上下文。
  • ANSI 颜色代码(\033[...m)需要终端支持,大多数串口助手都兼容。
  • 如需禁用颜色,在编译选项中定义LOG_DISABLE_COLOR

8.4 Mutex 注意事项

  • 本系统使用xSemaphoreCreateMutex()创建的是非递归互斥锁
  • 同一任务不可在持有锁时再次调用log_async_write_core(),否则会死锁。
  • 若需要递归调用场景,应改用xSemaphoreCreateRecursiveMutex()

9. 完整示例

// ==================== app_log.c ====================#include"log_async.h"#include"usart.h"// ── 缓冲区定义 ──staticuint8_tgLogStorage[LOG_BUFFER_SIZE];staticchargLogInput[LOG_LINE_MAX_LEN];staticchargLogIsrBuf[LOG_ISR_BUFFER_SIZE];staticchargLogOutput[LOG_LINE_MAX_LEN];// ── 物理输出回调 ──staticintuart_output(char*data,size_tlen){HAL_UART_Transmit(&huart1,(uint8_t*)data,len,100);return(int)len;}// ── 初始化 ──voidapp_log_init(void){log_async_init(&gLogAsync,gLogStorage,sizeof(gLogStorage),gLogInput,sizeof(gLogInput),gLogOutput,sizeof(gLogOutput),gLogIsrBuf,sizeof(gLogIsrBuf));gLogAsync.print=uart_output;// 创建输出任务xTaskCreate(log_async_task,"log_out",256,&gLogAsync,1,&gLogAsync.output_task);}// ── 反初始化 ──voidapp_log_deinit(void){log_async_deinit(&gLogAsync);}// ==================== main.c ====================#defineLOG_TAG"MAIN"intmain(void){HAL_Init();SystemClock_Config();MX_USART1_UART_Init();app_log_init();log_i("=== System Boot ===");log_d("FreeRTOS version: %s",tskKERNEL_VERSION_NUMBER);log_w("Watchdog not configured");// ... 业务代码 ...}

预期串口输出

[36m[DBG][MAIN][main.c][0023] FreeRTOS version: V10.4.3[0m [33m[WRN][MAIN][main.c][0024] Watchdog not configured[0m [32m[INF][MAIN][main.c][0021] === System Boot ===[0m

注意:由于是异步输出,日志顺序可能与调用顺序略有差异。


附录:关键 API 速查

函数说明可调用上下文
log_async_init()初始化日志系统任务
log_async_deinit()销毁日志系统任务
log_async_write_core()核心写日志任务 + ISR
log_async_task()输出任务入口作为 FreeRTOS 任务
log_async_set_level()设置输出级别任务
log_async_enable_color()开启颜色任务
log_async_disable_color()关闭颜色任务
log_async_set_filters()设置过滤关键字任务
说明
log_e(fmt, ...)输出 ERROR 级别日志(使用文件级 LOG_TAG)
log_w(fmt, ...)输出 WARN 级别日志
log_i(fmt, ...)输出 INFO 级别日志
log_d(fmt, ...)输出 DEBUG 级别日志
LOG_E(tag, fmt, ...)输出 ERROR 级别日志(指定 Tag)
LOG_W(tag, fmt, ...)输出 WARN 级别日志(指定 Tag)
LOG_I(tag, fmt, ...)输出 INFO 级别日志(指定 Tag)
LOG_D(tag, fmt, ...)输出 DEBUG 级别日志(指定 Tag)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询