基于 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;初始化函数内部执行:
- 设置默认配置(颜色开启、DEBUG 级别、过滤关闭)
- 保存所有缓冲区指针和大小
- 调用
xMessageBufferCreateStatic()创建静态 Message Buffer - 调用
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_buf和isr_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_SIZE | 512 ~ 4096 | Message Buffer 存储区。值越大,能缓存的未输出日志越多 |
LOG_LINE_MAX_LEN | 128 ~ 512 | 单条日志最大长度。需容纳完整格式化后的日志行 |
LOG_ISR_BUFFER_SIZE | 64 ~ 256 | ISR 缓冲区。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) |