找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1163

积分

0

好友

163

主题
发表于 15 小时前 | 查看: 2| 回复: 0

在嵌入式系统开发中,有效的运行日志记录对于监控设备状态、追踪异常和定位问题至关重要。一个设计良好的日志系统能够将关键信息和错误详情持久化存储,为后期调试和分析提供可靠的数据支撑。

本文介绍一种适用于嵌入式设备的简易系统日志记录框架。该框架将日志视为一个文件系统进行管理,支持将日志存储在外部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_LEVELLOG_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大小、擦除块尺寸)灵活调整分区参数。将其与系统调试信息输出结合,能显著提升嵌入式系统在测试、生产和运维阶段的故障诊断与问题定位效率。




上一篇:小红书本地生活业务探索:从会员卡折戟看内容社区的商业化困境与战略调整
下一篇:Nginx惊群效应深度解析:高并发场景下的原因与四种解决方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 16:02 , Processed in 0.116425 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表