在嵌入式系统开发中,日志系统是调试和问题定位的重要工具。一个设计良好的日志模块能有效提升开发效率和问题追踪能力。本文将详细介绍一个基于 C语言 和 FreeRTOS 的简易嵌入式日志系统的核心设计思路与实现。

1. 简易嵌入式日志系统
1.1 日志系统测试
1.1.1 同步 vs 异步输出
性能是日志系统设计的关键考量之一,尤其是在资源受限的嵌入式环境中。同步输出会阻塞调用者,而异步输出则通过缓冲机制提升响应速度。以下测试代码对比了两种模式的性能差异:
static void log_compare_task(void *param)
{
(void)param;
const int lines_per_burst = 50;
const uint32_t gap_ms = 6000;
const uint32_t max_flush_wait_ms = 8000;
while (1)
{
// ---------- SYNC: 直接输出(包含 I/O 时间) ----------
TickType_t t0 = xTaskGetTickCount();
for (int i = 0; i < lines_per_burst; i++)
{
log_write(&g_logger_sync, LOG_LEVEL_INFO, __FILE__, __LINE__,
"SYNC #%d: payload=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", i);
}
TickType_t t1 = xTaskGetTickCount();
const uint32_t sync_ms = (uint32_t)((t1 - t0) * portTICK_PERIOD_MS);
// ---------- ASYNC: 先入队,再等待后台 flush 刷空 ----------
while (log_buffer_available(&g_logger) > 0)
{
vTaskDelay(pdMS_TO_TICKS(1));
}
TickType_t enq0 = xTaskGetTickCount();
for (int i = 0; i < lines_per_burst; i++)
{
log_write(&g_logger, LOG_LEVEL_INFO, __FILE__, __LINE__,
"ASYNC #%d: payload=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", i);
}
TickType_t enq1 = xTaskGetTickCount();
const uint32_t enq_ms = (uint32_t)((enq1 - enq0) * portTICK_PERIOD_MS);
TickType_t flush0 = xTaskGetTickCount();
const TickType_t timeout_ticks = pdMS_TO_TICKS(max_flush_wait_ms);
while (log_buffer_available(&g_logger) > 0)
{
if ((xTaskGetTickCount() - flush0) >= timeout_ticks)
{
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
TickType_t flush1 = xTaskGetTickCount();
const uint32_t flush_ms = (uint32_t)((flush1 - flush0) * portTICK_PERIOD_MS);
log_write(&g_logger_sync, LOG_LEVEL_WARN, __FILE__, __LINE__,
"PERF %d lines: SYNC=%lu ms | ASYNC enqueue=%lu ms, flush=%lu ms",
lines_per_burst,
(unsigned long)sync_ms,
(unsigned long)enq_ms,
(unsigned long)flush_ms);
vTaskDelay(pdMS_TO_TICKS(gap_ms));
}
}
测试输出结果展示了密集日志下的性能对比:
性能对比 :
| 模式 |
50条日志耗时 |
说明 |
| 同步模式 |
~472ms |
每条日志都立即输出到串口 |
| 异步模式 |
~17ms |
写入内存缓冲区+刷新 |
异步模式的核心在于把整行字符串写入环形缓冲区;当突发产生速度大于后台消费速度时,缓冲满了就覆盖最旧字节。优化方向通常包括:调大 LOG_BUFFER_SIZE、提高输出硬件速率或优化后台刷新策略。
1.1.2 不同日志级别
一个实用的日志系统需要支持分级输出,便于在开发和生产环境中控制信息量。
void test_basic_levels(void)
{
LOG_ERROR("This is an ERROR message");
LOG_WARN("This is a WARN message");
LOG_INFO("This is an INFO message");
LOG_DEBUG("This is a DEBUG message");
LOG_VERBOSE("This is a VERBOSE message");
}
1.1.3 格式化输出
支持类似 printf 的格式化输出,方便记录各种变量和状态。
void test_formatted_output(void)
{
int temp = 25;
float voltage = 3.3f;
const char *status = "running";
LOG_INFO("Temperature: %d°C", temp);
LOG_INFO("Voltage: %.2fV", voltage);
LOG_INFO("System status: %s", status);
LOG_DEBUG("Hex data: 0x%02X 0x%04X", 0xAB, 0x1234);
}
1.1.4 级别过滤
可以动态设置日志级别,过滤掉低于该级别的日志,这在发布版本中节省资源非常有用。
log_config_t config;
log_get_freertos_config(&config);
config.level = LOG_LEVEL_WARN;
log_init(&g_logger, &config);
void test_level_filter(void)
{
LOG_ERROR("ERROR");
LOG_WARN("WARN");
LOG_INFO("INFO"); // 被过滤
LOG_DEBUG("DEBUG"); // 被过滤
}
级别过滤的性能优势:被过滤的日志在格式化之前就被拒绝,避免了不必要的字符串处理开销。
1.2 本文最小实现设计思路
本设计遵循“最小可用”原则,核心思路如下:
- 只做最小闭环:能“产生日志 → 缓存 → 输出”即可。
- 静态资源优先:只使用静态/编译期分配的缓冲区与控制结构,避免
malloc/free 带来的复杂性和碎片问题。
- 基于FreeRTOS,不依赖复杂特性:先不做过度抽象,优先用临界区/轻量锁保证一致性;需要异步时再引入一个后台任务。
- 写日志尽量短、可失败:记录路径以“尽快返回”为目标;缓冲满时允许丢弃或覆盖,策略可配置但实现保持简单。
- 异步为可选项:默认直接调用平台输出;当输出可能阻塞时再启用环形缓冲 + 刷新任务。
- 接口最小化:只抽象 2 个平台钩子(输出函数、时间戳函数),其余参数提供合理默认值。
1.3 核心功能需求
根据嵌入式系统的特点,本日志系统需具备以下核心功能:
- 日志级别:分5级(ERROR/WARN/INFO/DEBUG/VERBOSE),低级别日志自动过滤。
- 格式化输出:支持
printf 风格格式化。
- 时间戳:每条日志附加时间戳,方便分析时序。
- 文件名和行号:快速定位问题代码位置。
- 同步/异步模式:同步模式实时输出但可能阻塞;异步模式先写缓冲区,非阻塞。
- 环形缓冲区:异步模式的核心,使用固定大小的环形缓冲区,写满后覆盖旧数据。
- 后台刷新任务:自动创建 FreeRTOS 任务,定期刷新日志缓冲区。
- 平台适配:通过函数指针抽象接口,仅需实现输出和时间戳两个函数。

同步模式:直接输出,实时性强但可能阻塞。

异步模式:写入缓冲区后立即返回,由后台任务输出。
1.4 日志配置项
系统行为通过一系列编译时常量进行配置,提供了高度的灵活性。
// 环形缓冲区大小,根据RAM大小调整
#ifndef LOG_BUFFER_SIZE
#define LOG_BUFFER_SIZE 512
#endif
// 单条日志最大长度
#ifndef LOG_MAX_LINE_SIZE
#define LOG_MAX_LINE_SIZE 256
#endif
// 刷新任务配置
#ifndef LOG_FLUSH_INTERVAL_MS
#define LOG_FLUSH_INTERVAL_MS 50 // 刷新间隔
#endif
#ifndef LOG_FLUSH_TASK_STACK_SIZE
#define LOG_FLUSH_TASK_STACK_SIZE 512 // 刷新任务栈大小
#endif
#ifndef LOG_FLUSH_TASK_PRIORITY
#define LOG_FLUSH_TASK_PRIORITY 1 // 刷新任务优先级
#endif
// 功能开关
#ifndef LOG_ENABLE_TIMESTAMP
#define LOG_ENABLE_TIMESTAMP 1 // 启用时间戳
#endif
#ifndef LOG_ENABLE_FILE_LINE
#define LOG_ENABLE_FILE_LINE 1 // 启用文件名和行号
#endif
#ifndef LOG_ENABLE_COLOR
#define LOG_ENABLE_COLOR 1 // 启用颜色(终端)
#endif
#ifndef LOG_ENABLE_THREAD_SAFE
#define LOG_ENABLE_THREAD_SAFE 0 // 线程安全(需要提供锁函数)
#endif
#ifndef LOG_ENABLE_ASYNC
#define LOG_ENABLE_ASYNC 1 // 异步模式
#endif
#ifndef LOG_ENABLE_FLUSH_TASK
#define LOG_ENABLE_FLUSH_TASK 1 // 启用自动刷新任务
#endif
1.5 数据结构设计
1.5.1 日志级别定义
typedef enum
{
LOG_LEVEL_NONE = 0, // 关闭日志
LOG_LEVEL_ERROR, // 错误
LOG_LEVEL_WARN, // 警告
LOG_LEVEL_INFO, // 信息
LOG_LEVEL_DEBUG, // 调试
LOG_LEVEL_VERBOSE, // 详细
} log_level_t;
5个级别一般足够覆盖大多数场景:
- ERROR:致命错误,如硬件故障、通信失败。
- WARN:非致命问题,系统仍可运行,如温度过高、缓冲区将满。
- INFO:关键运行节点信息,如系统启动、连接成功。
- DEBUG:调试信息,如函数调用、状态机转换。
- VERBOSE:最详细信息,如数据包原始内容、寄存器值(通常只在深度调试时开启)。
1.5.2 配置结构
typedef struct
{
log_level_t level; // 日志级别
log_backend_t backend; // 后端类型
log_output_fn output_fn; // 输出函数
log_timestamp_fn timestamp_fn; // 时间戳函数
log_lock_fn lock_fn; // 加锁函数
log_unlock_fn unlock_fn; // 解锁函数
bool enable_color; // 是否启用颜色
bool enable_async; // 是否启用异步
#if LOG_ENABLE_FLUSH_TASK
// 平台相关的任务操作(用于启动后台刷新任务)
log_task_create_fn task_create_fn; // 创建任务函数
log_task_delete_fn task_delete_fn; // 删除任务函数
log_delay_ms_fn delay_ms_fn; // 延时函数
#endif
} log_config_t;
初始化时需要填充此结构体。其中 output_fn 和 timestamp_fn 是必须实现的回调函数,其他如锁、任务函数等可根据需求选择性提供。
1.5.3 日志对象
typedef struct
{
log_config_t config; // 配置
log_buffer_t buffer; // 缓冲区
bool initialized; // 初始化标志
#if LOG_ENABLE_FLUSH_TASK
void *flush_task_handle; // 刷新任务句柄(平台相关)
bool flush_task_running; // 刷新任务运行状态
#endif
} logger_t;
1.5.4 环形缓冲区
异步模式的核心是环形缓冲区(Ring Buffer),它是一种高效的内存复用数据结构。

typedef struct
{
char buffer[LOG_BUFFER_SIZE]; // 环形缓冲区
uint16_t write_pos; // 写位置
uint16_t read_pos; // 读位置
uint16_t count; // 当前数据量
} log_buffer_t;
// 环形缓冲区写入
static size_t ring_buffer_write(log_buffer_t *buf, const char *data, size_t len)
{
if (!buf || !data || len == 0)
return 0;
size_t written = 0;
for (size_t i = 0; i < len; i++)
{
// 缓冲区满,覆盖最旧的数据
if (buf->count >= LOG_BUFFER_SIZE)
{
buf->read_pos = (buf->read_pos + 1) % LOG_BUFFER_SIZE;
buf->count--;
}
buf->buffer[buf->write_pos] = data[i];
buf->write_pos = (buf->write_pos + 1) % LOG_BUFFER_SIZE;
buf->count++;
written++;
}
return written;
}
// 环形缓冲区读取
static size_t ring_buffer_read(log_buffer_t *buf, char *data, size_t len)
{
if (!buf || !data || len == 0)
return 0;
size_t read = 0;
while (read < len && buf->count > 0)
{
data[read++] = buf->buffer[buf->read_pos];
buf->read_pos = (buf->read_pos + 1) % LOG_BUFFER_SIZE;
buf->count--;
}
return read;
}
为什么选择环形缓冲区?
- 固定大小,静态分配:编译时确定大小,无内存碎片之忧。
- 覆盖策略简单有效:缓冲区满时自动覆盖最旧数据。虽然可能丢失历史日志,但避免了因日志堆积导致的内存耗尽或程序阻塞,这在许多实时嵌入式场景中是可接受的权衡。
- 操作高效:读写操作都是 O(1) 时间复杂度,仅移动读写指针,无需大规模数据搬移。
1.6 API接口设计
系统提供一组简洁的API。
// 初始化日志系统
bool log_init(logger_t *logger, const log_config_t *config);
// 反初始化日志系统
void log_deinit(logger_t *logger);
// 设置日志级别
void log_set_level(logger_t *logger, log_level_t level);
// 获取日志级别
log_level_t log_get_level(const logger_t *logger);
// 日志输出核心函数
void log_write(logger_t *logger, log_level_t level,
const char *file, int line,
const char *fmt, ...);
// 刷新缓冲区(强制输出)
void log_flush(logger_t *logger);
// 从缓冲区读取数据
size_t log_read_buffer(logger_t *logger, char *buf, size_t size);
// 获取缓冲区可用数据量
size_t log_buffer_available(const logger_t *logger);
// 获取日志级别字符串
const char* log_level_str(log_level_t level);
// 获取日志级别颜色
const char* log_level_color(log_level_t level);
#if LOG_ENABLE_FLUSH_TASK
// 启动后台刷新任务
bool log_start_flush_task(logger_t *logger);
// 停止后台刷新任务
void log_stop_flush_task(logger_t *logger);
#endif
1.6.1 宏定义
为了方便使用,提供了带文件名和行号的宏。
#if LOG_ENABLE_FILE_LINE
#define LOG_ERROR(fmt, ...) log_write(&g_logger, LOG_LEVEL_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) log_write(&g_logger, LOG_LEVEL_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) log_write(&g_logger, LOG_LEVEL_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) log_write(&g_logger, LOG_LEVEL_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_VERBOSE(fmt, ...) log_write(&g_logger, LOG_LEVEL_VERBOSE, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#else
#define LOG_ERROR(fmt, ...) log_write(&g_logger, LOG_LEVEL_ERROR, NULL, 0, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) log_write(&g_logger, LOG_LEVEL_WARN, NULL, 0, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) log_write(&g_logger, LOG_LEVEL_INFO, NULL, 0, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) log_write(&g_logger, LOG_LEVEL_DEBUG, NULL, 0, fmt, ##__VA_ARGS__)
#define LOG_VERBOSE(fmt, ...) log_write(&g_logger, LOG_LEVEL_VERBOSE, NULL, 0, fmt, ##__VA_ARGS__)
#endif
1.6.2 日志写入核心函数
这是整个系统的中枢,负责格式化、过滤和路由日志。
void log_write(logger_t *logger, log_level_t level,
const char *file, int line,
const char *fmt, ...)
{
if (!logger || !logger->initialized)
return;
// 级别过滤
if (level > logger->config.level)
return;
// 加锁(多任务环境)
if (LOG_ENABLE_THREAD_SAFE && logger->config.lock_fn)
logger->config.lock_fn();
char log_line[LOG_MAX_LINE_SIZE] = {0};
int offset = 0;
// 时间戳
if (LOG_ENABLE_TIMESTAMP && logger->config.timestamp_fn)
{
uint32_t ts = logger->config.timestamp_fn();
offset += snprintf(log_line + offset, LOG_MAX_LINE_SIZE - offset,
"[%u.%03u] ", ts / 1000, ts % 1000);
}
// 日志颜色、级别
if (logger->config.enable_color)
{
offset += snprintf(log_line + offset, LOG_MAX_LINE_SIZE - offset,
"%s[%s]%s ",
log_level_color(level),
log_level_str(level),
LOG_COLOR_RESET);
}
else
{
offset += snprintf(log_line + offset, LOG_MAX_LINE_SIZE - offset,
"[%s] ", log_level_str(level));
}
// 文件名和行号
if (LOG_ENABLE_FILE_LINE && file)
{
offset += snprintf(log_line + offset, LOG_MAX_LINE_SIZE - offset,
"[%s:%d] ", get_filename(file), line);
}
// 用户消息
va_list args;
va_start(args, fmt);
offset += vsnprintf(log_line + offset, LOG_MAX_LINE_SIZE - offset, fmt, args);
va_end(args);
// 换行符
if (offset < LOG_MAX_LINE_SIZE - 3)
{
log_line[offset++] = '\r';
log_line[offset++] = '\n';
log_line[offset] = '\0';
}
// 输出逻辑
if (logger->config.enable_async)
{
// 异步模式:写入缓冲区
ring_buffer_write(&logger->buffer, log_line, offset);
}
else
{
// 同步模式:直接输出
if (logger->config.output_fn)
logger->config.output_fn(log_line, offset);
}
// 解锁
if (LOG_ENABLE_THREAD_SAFE && logger->config.unlock_fn)
logger->config.unlock_fn();
}
1.6.4 后台刷新任务
异步模式下,需要一个独立的任务来定期清空缓冲区。
static void log_flush_task_entry(void *param)
{
logger_t *logger = (logger_t *)param;
while (logger->flush_task_running)
{
// 如果有数据就刷新
if (log_buffer_available(logger) > 0)
{
log_flush(logger);
}
if (logger->config.delay_ms_fn)
{
logger->config.delay_ms_fn(LOG_FLUSH_INTERVAL_MS);
}
}
if (logger->config.task_delete_fn)
{
logger->config.task_delete_fn(NULL);
}
}
bool log_start_flush_task(logger_t *logger)
{
if (!logger || !logger->initialized)
return false;
// 检查平台回调函数是否提供
if (!logger->config.task_create_fn || !logger->config.delay_ms_fn)
{
return false;
}
// 检查是否已经启动
if (logger->flush_task_running)
return true;
logger->flush_task_running = true;
// 使用平台提供的任务创建函数
logger->flush_task_handle = logger->config.task_create_fn(
log_flush_task_entry,
logger,
LOG_FLUSH_TASK_STACK_SIZE,
LOG_FLUSH_TASK_PRIORITY
);
if (logger->flush_task_handle == NULL)
{
logger->flush_task_running = false;
return false;
}
return true;
}
1.7 FreeRTOS 平台适配
本日志系统设计时考虑了 FreeRTOS 环境,但通过函数指针保持了可移植性。
1.7.1 输出函数
通常实现为串口发送,支持阻塞或 DMA 方式。
typedef void(*log_output_fn)(const char *data, size_t len);
void log_output_uart_freertos(const char *data, size_t len)
{
if (data == NULL || len == 0)
return;
if (uart1_tx_done == NULL)
{
// 未初始化时退化为阻塞发送
HAL_UART_Transmit(&huart1, (uint8_t*)data, (uint16_t)len, 0xFFFF);
return;
}
while (len > 0)
{
size_t chunk = (len > sizeof(uart1_tx_buf)) ? sizeof(uart1_tx_buf) : len;
// 等待上一次 DMA 完成
xSemaphoreTake(uart1_tx_done, portMAX_DELAY);
// 拷贝到静态缓冲,保证 DMA 期间数据稳定
memcpy(uart1_tx_buf, data, chunk);
// 启动 DMA
if (HAL_UART_Transmit_DMA(&huart1, uart1_tx_buf, (uint16_t)chunk) != HAL_OK)
{
xSemaphoreGive(uart1_tx_done);
break;
}
data += chunk;
len -= chunk;
// 发送完成由 HAL_UART_TxCpltCallback() 释放 uart1_tx_done
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1 && uart1_tx_done != NULL)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
(void)xSemaphoreGiveFromISR(uart1_tx_done, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
1.7.2 时间戳函数
typedef uint32_t(*log_timestamp_fn)(void);
uint32_t log_timestamp_rtos(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
1.7.3 线程安全(互斥锁)
typedef void(*log_lock_fn)(void);
typedef void(*log_unlock_fn)(void);
static SemaphoreHandle_t log_mutex = NULL;
void log_lock_freertos(void)
{
if (log_mutex != NULL)
{
xSemaphoreTake(log_mutex, portMAX_DELAY);
}
}
void log_unlock_freertos(void)
{
if (log_mutex != NULL)
{
xSemaphoreGive(log_mutex);
}
}
1.7.4 日志任务管理适配
void* log_task_create_freertos(void (*task_func)(void*), void *param,
uint32_t stack_size, uint32_t priority)
{
TaskHandle_t task_handle = NULL;
BaseType_t ret = xTaskCreate(
task_func,
"log_flush",
stack_size / sizeof(StackType_t),
param,
priority,
&task_handle
);
return (ret == pdPASS) ? task_handle : NULL;
}
void log_task_delete_freertos(void *task_handle)
{
vTaskDelete((TaskHandle_t)task_handle);
}
void log_delay_ms_freertos(uint32_t ms)
{
vTaskDelay(pdMS_TO_TICKS(ms));
}
1.7.5 获取 FreeRTOS 平台的默认配置
void log_get_freertos_config(log_config_t *config)
{
if (config == NULL)
return;
// 填充默认配置
config->level = LOG_LEVEL_INFO;
config->backend = LOG_BACKEND_UART;
config->output_fn = log_output_uart_freertos;
config->timestamp_fn = log_timestamp_freertos;
config->lock_fn = log_lock_freertos;
config->unlock_fn = log_unlock_freertos;
config->enable_color = false;
config->enable_async = true;
#if LOG_ENABLE_FLUSH_TASK
config->task_create_fn = log_task_create_freertos;
config->task_delete_fn = log_task_delete_freertos;
config->delay_ms_fn = log_delay_ms_freertos;
#endif
}
2. 局限性
这是一个面向学习和轻量级应用的最小实现,存在一些局限性:
2.1 缓冲区容量限制
- 固定 512B 环形缓冲,写满后会覆盖旧数据,高频时容易丢关键日志。
- 常见改进做法:
- 内存充足时直接加大缓冲区。
- 使用双缓冲或多缓冲降低数据覆盖概率。
- 将覆盖策略做成可配置:可选择丢弃新日志保护历史,或提供溢出回调进行告警和计数。
2.2 时间戳精度
- 时间戳精度受平台系统 tick 影响,密集日志可能出现“同一时间戳”。
- 需要更高精度时:可接入硬件计数器或高精度定时器(如 Cortex-M 的 DWT 周期计数器)。
2.3 Flash存储支持
当前设计不支持 Flash 持久化,掉电后日志会丢失。若需此功能,需扩展后端支持。
3. 总结
本文介绍的日志系统设计偏向“最小可用”,旨在阐明嵌入式日志系统的核心机制,适合学习和小型项目使用。在实际的复杂或高频场景中,可能需要更成熟的日志库。
若需用于更复杂的场景,可以考虑以下几个扩展方向:
- 平台抽象:进一步抽象延时、锁、任务接口,以适配裸机、RT-Thread、嵌入式 Linux 等多种环境。
- 存储扩展:支持 Flash 环形持久化、文件系统落盘、远程集中存储等。
- 传输方式:增加 TCP/UDP、MQTT 等网络传输后端。
- 高级功能:实现运行时动态调整日志级别、基于模块或标签的过滤、日志统计与分析等。
希望这份关于简易嵌入式日志系统的设计思路能为你带来启发。如果你对嵌入式开发中的其他系统设计感兴趣,欢迎在 云栈社区 交流讨论。