在嵌入式系统开发中,有效的运行日志记录对于监控设备状态、追踪异常和定位问题至关重要。一个设计良好的日志系统能够将关键信息和错误详情持久化存储,为后期调试和分析提供可靠的数据支撑。
本文介绍一种适用于嵌入式设备的简易系统日志记录框架。该框架将日志视为一个文件系统进行管理,支持将日志存储在外部Flash(也可适配MCU内部Flash或EEPROM),并详细阐述了其分层存储结构、环形管理机制及C语言实现。
系统架构与设计思路
整个日志系统在存储介质上划分为三个核心区域:目录区、参数区和日志区。
- 目录区:按日期对日志进行归类,记录每天的日志存储起始地址、索引ID和大小,提供了整个日志文件的全局视图。
- 参数区:保存日志系统的运行时状态,包括当前写位置、目录项数量、环形写入状态标志等关键参数。
- 日志区:主要的日志数据存储区,采用环形写入策略,以延长Flash使用寿命。
通过AT指令与系统交互,可以实现以下功能:
- 查询日志目录概况:
AT+CATALOG?
- 查询指定日期日志:
AT+CATALOG=<LOG_ID> (LOG_ID为0时查询全部)
- 清除所有日志:
AT+RMLOG

系统日志目录查询结果示意

查询指定ID的日志内容示意
Flash存储空间划分
首先需要根据具体设备的Flash容量进行合理的空间划分。以下代码示例定义了区域枚举和地址映射表,实现了环形存储的基础布局。
#define FLASH_SECTOR_SIZE ((uint32_t)0x001000)
#define FLASH_BLOCK_32K_SIZE ((uint32_t)0x008000)
#define FLASH_BLOCK_64K_SIZE ((uint32_t)0x010000)
#define SECTOR_MASK (FLASH_SECTOR_SIZE - 1)
#define SECTOR_BASE(addr) (addr & (~SECTOR_MASK))
#define SECTOR_OFFSET(addr) (addr & SECTOR_MASK)
#define BLOCK_32K_BASE(addr) (addr & (~(FLASH_BLOCK_32K_SIZE)))
#define BLOCK_64K_BASE(addr) (addr & (~(FLASH_BLOCK_64K_SIZE)))
typedef enum {
FLASH_BLOCK_4K = 0,
FLASH_BLOCK_32K = 1,
FLASH_BLOCK_64K = 2
} flash_block_t;
typedef enum {
FLASH_CATALOG_ZONE = 0,
FLASH_SYSLOG_PARA_ZONE,
FLASH_SYSLOG_ZONE,
FLASH_ZONEX,
} flash_zone_e;
typedef struct {
flash_zone_e zone;
uint32_t start_address;
uint32_t end_address;
} flash_table_t;
static const flash_table_t flash_table[] = {
{ .zone = FLASH_CATALOG_ZONE, .start_address = 0x03200000, .end_address = 0x032FFFFF},
{ .zone = FLASH_SYSLOG_PARA_ZONE, .start_address = 0x03300000, .end_address = 0x033FFFFF},
{ .zone = FLASH_SYSLOG_ZONE, .start_address = 0x03400000, .end_address = 0x03FFFFFF},
};
基于上述分区表,可以实现统一的Flash操作接口(擦除、写、读),这些接口内部会校验地址是否在合法区域内。具体的Flash底层驱动(如 bsp_spi_flash_erase, bsp_spi_flash_buffer_write)需要开发者根据硬件平台自行实现,这是嵌入式开发中常见的硬件抽象层工作。
flash_table_t *get_flash_table(flash_zone_e zone) {
int i = 0;
for (i = 0; i < flash_zone_count; i++) {
if (zone == flash_table[i].zone)
return (flash_table_t *)&flash_table[i];
}
return NULL;
}
int flash_erase(flash_zone_e zone, uint32_t address, flash_block_t block_type) {
flash_table_t *flash_table_tmp = get_flash_table(zone);
if (flash_table_tmp == NULL) return -1;
if (address < flash_table_tmp->start_address || address > flash_table_tmp->end_address)
return -1;
return bsp_spi_flash_erase(address, block_type);
}
// flash_write 和 flash_read 函数类似,省略...
关键数据结构定义
1. 时间与目录结构
日志需要时间戳,因此需依赖RTC接口。
typedef struct {
uint16_t Year;
uint8_t Month;
uint8_t Day;
uint8_t Hour;
uint8_t Minute;
uint8_t Second;
} time_t;
int bsp_rtc_get_time(time_t *date);
2. 参数与目录结构
参数区数据必须具备完整性和可靠性,因此引入包含CRC校验的包装结构。
#define SYSTEM_LOG_MAGIC_PARAM 0x87654321
typedef struct {
uint32_t magic;
uint16_t crc;
uint16_t len;
} single_sav_t;
typedef struct {
uint32_t write_pos;
uint32_t catalog_num;
uint8_t log_cyclic_status;
uint8_t catalog_cyclic_status;
time_t log_latest_time;
} system_log_t;
typedef struct {
uint32_t log_id;
uint32_t log_addr;
uint32_t log_offset;
time_t log_time;
} system_catalog_t;
typedef struct {
single_sav_t crc_val;
system_log_t system_log;
system_catalog_t system_catalog;
} sys_log_param_t;
// 全局变量
sys_log_param_t SysLogParam;
核心功能实现
1. 参数保存与加载
每次写日志操作后,都需要保存当前系统参数。参数区本身也采用环形存储,当空间不足时从头开始覆盖写入。
void save_system_log_param(void) {
// ... 计算CRC,处理环形地址,分段写入Flash ...
}
设备启动时,需要从参数区加载最新的有效参数。搜索逻辑是从参数区末尾向前扫描,找到第一个魔数、长度和CRC都匹配的数据块。
int load_system_log_param(void) {
// ... 从Flash参数区末端向前扫描...
// 若找到合法参数,则加载;否则,初始化默认参数。
if (找到合法参数) {
return 0;
} else {
load_system_log_default_param(); // 初始化默认值
return 1;
}
}
2. 目录管理
目录区记录按日期划分的日志索引。当检测到日期变更(例如新的一天),会将当前累计的日志信息作为一个新的目录项写入目录区。
int system_catalog_write(system_catalog_t *catalog, uint32_t id) {
// ... 计算写入地址,处理扇区擦除与写入 ...
}
3. 日志写入
这是最核心的函数,负责将缓冲区数据写入日志区,并处理日期变更、目录更新、环形写入和扇区擦除等逻辑。
int system_log_write(uint8_t *wbuf, int wlen) {
uint32_t start_addr;
// 1. 计算写入地址,处理日志区环形覆盖
// 2. 检查日期是否变化,若变化则写入一个新目录项
// 3. 若写入地址是扇区起始,则先擦除该扇区
// 4. 分段写入数据,处理跨扇区情况
// 5. 调用 save_system_log_param() 保存最新参数
return 0;
}
4. 日志读取与打印
提供按日志ID读取或读取全部日志的功能,并通过调试串口输出。
int system_log_task(int argc) {
// ... 根据ID或全部打印的标志,计算起始地址和长度...
while (剩余长度 > 0) {
system_log_read(sector_buf, start_addr, 本次读取长度);
bsp_debug_send(sector_buf, 本次读取长度); // 输出到串口
// 更新地址和剩余长度
}
return 0;
}
与系统调试对接
为了无缝集成,可以将日志框架与现有的调试打印系统结合。定义不同的日志等级,并约定某些等级(如LOG_RECORD_LEVEL和LOG_ERROR_LEVEL)会自动触发日志存储操作。
#define LOG_ERROR_LEVEL 0x01
#define LOG_RECORD_LEVEL 0x10
#define log_error(fmt, args...) log_format(LOG_ERROR_LEVEL, fmt, ##args)
#define log_record(fmt, args...) log_format(LOG_RECORD_LEVEL, fmt, ##args)
int log_format(uint8_t level, const char *fmt, ...) {
va_list args;
char buf[PRINT_MAX_SIZE];
time_t time = {0};
// 1. 根据等级判断是否输出到串口
// 2. 格式化字符串
if ((GET_LOG_LEVEL() >= level) || (level == LOG_PRINT_LEVEL)) {
// 输出到调试串口
bsp_debug_send((uint8_t*)output_buf, len);
}
// 3. 如果是需要存储的等级,则添加时间戳并写入日志
if ((level == LOG_ERROR_LEVEL) || (level == LOG_RECORD_LEVEL)) {
bsp_rtc_get_time(&time);
// 在buf头部添加时间戳 "[YYYY-MM-DD HH:MM:SS]"
system_log_write((uint8_t *)buf, total_len); // 调用核心写入函数
}
return ret;
}
这种算法与业务逻辑的结合,使得关键调试信息能被自动持久化。
总结
本文详细介绍了一种用于嵌入式设备的轻量级系统日志记录方案。该方案通过目录区、参数区、日志区的分层设计,实现了日志的结构化存储和高效管理;利用环形存储策略优化了Flash使用寿命;并通过CRC校验和参数恢复机制保证了数据的可靠性。
整个框架代码量适中,逻辑清晰,开发者可以根据具体设备的存储资源(Flash大小、擦除块尺寸)灵活调整分区参数。将其与系统调试信息输出结合,能显著提升嵌入式系统在测试、生产和运维阶段的故障诊断与问题定位效率。