1. 嵌入式系统日志记录方案设计在嵌入式系统开发中日志记录是调试和问题定位的重要工具。一个完善的日志系统可以帮助开发人员快速定位问题了解系统运行状态。本文将详细介绍一种基于外部Flash的嵌入式系统日志记录方案。这个方案的核心思想是将日志系统划分为三个主要区域目录区、参数区和日志区。目录区用于记录日志的存储位置和索引信息参数区保存系统运行时的关键参数日志区则是实际存储日志数据的地方。这种分区设计既保证了数据的组织性又提高了存储效率。在实际项目中我曾遇到因日志系统设计不当导致的关键数据丢失问题。后来发现是因为没有合理规划存储区域导致日志覆盖了重要参数。这个教训让我深刻认识到日志系统设计的重要性。2. 存储区域划分与实现2.1 Flash存储空间规划首先需要合理划分Flash存储空间。在我们的方案中使用以下宏定义来管理Flash的基本操作单元#define FLASH_SECTOR_SIZE ((uint32_t)0x001000) // 4KB扇区 #define FLASH_BLOCK_32K_SIZE ((uint32_t)0x008000) // 32KB块 #define FLASH_BLOCK_64K_SIZE ((uint32_t)0x010000) // 64KB块 #define SECTOR_MASK (FLASH_SECTOR_SIZE - 1)然后定义Flash的存储区域枚举typedef enum { FLASH_CATALOG_ZONE 0, // 目录区 FLASH_SYSLOG_PARA_ZONE, // 参数区 FLASH_SYSLOG_ZONE, // 日志区 FLASH_ZONEX, } flash_zone_e;具体的地址划分如下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 }, };2.2 底层Flash操作接口实现基本的Flash操作接口是日志系统的基础。我们需要提供擦除、写入和读取三个基本操作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); } int flash_write(flash_zone_e zone, uint32_t address, const uint8_t *data, uint32_t length) { 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 length) flash_table_tmp-end_address)) return -1; return bsp_spi_flash_buffer_write(address, (uint8_t*)data, length); } int flash_read(flash_zone_e zone, uint32_t address, uint8_t *buffer, uint32_t length) { 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 length) flash_table_tmp-end_address)) return -1; bsp_spi_flash_buffer_read(buffer, address, length); return 0; }3. 日志系统核心数据结构3.1 时间戳结构为了准确记录日志时间我们需要定义时间戳结构typedef struct { uint16_t Year; /* 年份:YYYY */ uint8_t Month; /* 月份:MM */ uint8_t Day; /* 日:DD */ uint8_t Hour; /* 小时:HH */ uint8_t Minute; /* 分钟:MM */ uint8_t Second; /* 秒:SS */ } time_t;3.2 参数区数据结构参数区需要保存系统运行时的关键参数我们定义以下结构体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;4. 日志系统关键实现4.1 参数保存与加载参数区的数据需要定期保存防止意外丢失void save_system_log_param(void) { uint32_t i 0; uint32_t addr 0; uint32_t remainbyte 0; uint32_t start_addr; int len sizeof(sys_log_param_t); uint8_t *pdata (uint8_t*)SysLogParam; flash_table_t *flash_tmp get_flash_table(FLASH_SYSLOG_PARA_ZONE); /* 校验参数 */ gp_sys_log-crc_val.magic SYSTEM_LOG_MAGIC_PARAM; gp_sys_log-crc_val.len sizeof(sys_log_param_t) - sizeof(single_sav_t); gp_sys_log-crc_val.crc CRC16(pdata[sizeof(single_sav_t)], gp_sys_log-crc_val.len); start_addr gp_sys_ram-system_log_param_addr; /* 剩余内存不够写则重新从起始地址开始写 */ if((start_addr len) flash_tmp-end_address) { start_addr flash_tmp-start_address; } gp_sys_ram-system_log_param_addr start_addr len; /* 首地址存储擦除整个系统日志参数存储区 */ if(flash_tmp-start_address start_addr) { addr flash_tmp-start_address; do { if((addr FLASH_BLOCK_64K_SIZE) flash_tmp-end_address) { flash_erase(FLASH_SYSLOG_PARA_ZONE, BLOCK_64K_BASE(i), FLASH_BLOCK_64K); addr FLASH_BLOCK_64K_SIZE; } else if((addr FLASH_BLOCK_32K_SIZE) flash_tmp-end_address) { flash_erase(FLASH_SYSLOG_PARA_ZONE, BLOCK_32K_BASE(i), FLASH_BLOCK_32K); addr FLASH_BLOCK_32K_SIZE; } else if((addr FLASH_SECTOR_SIZE) flash_tmp-end_address) { flash_erase(FLASH_SYSLOG_PARA_ZONE, SECTOR_BASE(i), FLASH_BLOCK_4K); addr FLASH_SECTOR_SIZE; } else { break; } } while(addr flash_tmp-end_address); } remainbyte FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE); if(remainbyte len) { remainbyte len; } while(1) { flash_write(FLASH_SYSLOG_PARA_ZONE, start_addr, pdata, remainbyte); if(remainbyte len) { break; } else { pdata remainbyte; start_addr remainbyte; len - remainbyte; remainbyte (len FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : len; } } }4.2 日志写入实现日志写入是系统的核心功能需要考虑环形存储和日期变更处理int system_log_write(uint8_t *wbuf, int wlen) { uint32_t start_addr; uint8_t *pdata wbuf; uint32_t remainbyte; int system_catalog_max_id; flash_table_t *flash_tmp get_flash_table(FLASH_SYSLOG_ZONE); /* 计算目录区的最大存储目录项个数 */ system_catalog_max_id ((flash_tmp-end_address - flash_tmp-start_address) / sizeof(system_catalog_t)); start_addr flash_tmp-start_address gp_sys_log-system_log.write_pos; /* 存储数据地址大于规划内存地址范围处理 */ if((start_addr wlen) flash_tmp-end_address) { start_addr flash_tmp-start_address; /* 写位置偏移量重置 */ gp_sys_log-system_log.write_pos 0; /* LOG回环存储标志置位 */ gp_sys_log-system_log.log_cyclic_status 0x01; } /* 写位置偏移 */ gp_sys_log-system_log.write_pos wlen; if((gp_sys_log-system_log.log_latest_time.Year ! gp_sys_log-system_catalog.log_time.Year) || (gp_sys_log-system_log.log_latest_time.Month ! gp_sys_log-system_catalog.log_time.Month) || (gp_sys_log-system_log.log_latest_time.Day ! gp_sys_log-system_catalog.log_time.Day)) { /* 日期改变记录目录信息 */ system_catalog_write(gp_sys_log-system_catalog, gp_sys_log-system_catalog.log_id); /* 记录存储日期 */ gp_sys_log-system_catalog.log_time gp_sys_log-system_log.log_latest_time; if((gp_sys_log-system_catalog.log_id 1) system_catalog_max_id) { gp_sys_log-system_log.catalog_num system_catalog_max_id; /* 目录循环写目录数应为最大 */ gp_sys_log-system_log.catalog_cyclic_status 1; } else { if(0 gp_sys_log-system_log.catalog_cyclic_status) { /* 获取目录数 */ gp_sys_log-system_log.catalog_num gp_sys_log-system_catalog.log_id 1; } } /* 存储最新目录项信息 */ gp_sys_log-system_catalog.log_id (gp_sys_log-system_catalog.log_id 1) % system_catalog_max_id; gp_sys_log-system_catalog.log_addr start_addr; gp_sys_log-system_catalog.log_offset wlen; } else { gp_sys_log-system_catalog.log_offset wlen; } /* 写位置为存储起始地址并且不为扇区首地址 */ if((flash_tmp-start_address start_addr) (SECTOR_OFFSET(flash_tmp-start_address))) { flash_read(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), sector_buf, FLASH_SECTOR_SIZE); flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K); /* 将扇区头部至起始地址区间的数据回写 */ flash_write(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), sector_buf[0], SECTOR_OFFSET(start_addr)); } /* 写位置为扇区首地址则擦除一个扇区的存储区 */ if(0 SECTOR_OFFSET(start_addr)) { flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K); } /* 本扇区剩余空间大小 */ remainbyte FLASH_SECTOR_SIZE - (start_addr % FLASH_SECTOR_SIZE); /* 写入数据长度小于本扇区剩余长度直接写入 */ if(remainbyte wlen) { remainbyte wlen; } while(1) { flash_write(FLASH_SYSLOG_ZONE, start_addr, pdata, remainbyte); if(remainbyte wlen) { break; } else { pdata remainbyte; start_addr remainbyte; wlen - remainbyte; remainbyte (wlen FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : wlen; /* 扇区首地址则擦除整个扇区该扇区数据不保存 */ if(0 SECTOR_OFFSET(start_addr)) { flash_erase(FLASH_SYSLOG_ZONE, SECTOR_BASE(start_addr), FLASH_BLOCK_4K); } } } /* 环形存储参数 */ save_system_log_param(); return 0; }5. 日志查询与管理5.1 日志目录查询提供查询日志目录的功能方便用户了解存储的日志情况int system_catalog_all_print(void) { int i 0; system_catalog_t catalog; printf(System Log Command Information:\r\n); printf(Query Specifies Log : ATCATALOGLOG_IDCRLF\r\n); printf(Query All Log : ATCATALOG0CRLF\r\n\r\n); printf(Query All System Catalog:\r\n); printf(LOG_ID LOG_DATE LOG_ADDR LOG_OFFSET \r\n); for(i 0; i gp_sys_log-system_log.catalog_num; i) { /* 当前最新目录信息 */ if(i (gp_sys_log-system_catalog.log_id - 1)) { catalog gp_sys_log-system_catalog; } else { system_catalog_read(catalog, i 1); } printf(%d %04d-%02d-%02d 0x%08X %d \r\n, catalog.log_id, catalog.log_time.Year, catalog.log_time.Month, catalog.log_time.Day, catalog.log_addr, catalog.log_offset); memset((char*)catalog, 0, sizeof(system_catalog_t)); } return 0; }5.2 指定日志查询支持按日志ID查询特定日期的日志int system_log_task(int argc) { int rlen 0; uint32_t offset, start_addr, end_addr; system_catalog_t catalog; flash_table_t *flash_tmp get_flash_table(FLASH_SYSLOG_ZONE); if(0 gp_sys_ram-system_log_print_enable) return 1; gp_sys_ram-system_log_print_enable 0x00; if(gp_sys_ram-system_log_print_id ALL_LOG_PRINT) { /* log回环写标志,打印整个LOG存储区 */ if(0x01 gp_sys_log-system_log.log_cyclic_status) { start_addr flash_tmp-start_address; end_addr flash_tmp-end_address; offset end_addr - start_addr; } else { start_addr flash_tmp-start_address; end_addr start_addr gp_sys_log-system_log.write_pos; offset gp_sys_log-system_log.write_pos; } } else { /* 读取指定ID日志 */ if(gp_sys_ram-system_log_print_id gp_sys_log-system_catalog.log_id) { catalog gp_sys_log-system_catalog; } else { system_catalog_read(catalog, gp_sys_ram-system_log_print_id); } start_addr catalog.log_addr; offset catalog.log_offset; } if(0 offset) return 1; while(1) { rlen (offset 512) ? 512 : offset; system_log_read(sector_buf, start_addr, rlen); HAL_Delay(80); /* 目录信息通过调式串口打印 */ bsp_debug_send(sector_buf, rlen); start_addr rlen; offset - rlen; if(0 offset) break; } return 0; }6. 调试等级与日志记录6.1 调试等级定义定义不同的调试等级方便控制日志输出#define LOG_CLOSE_LEVEL 0x00 /* 关闭调试信息 */ #define LOG_ERROR_LEVEL 0x01 /* 错误调试信息 */ #define LOG_WARN_LEVEL 0x02 /* 警告调试信息 */ #define LOG_INFO_LEVEL 0x03 /* 关键调试信息 */ #define LOG_DEBUG_LEVEL 0x04 /* debug调试信息 */ #define LOG_RECORD_LEVEL 0x10 /* 保存日志并输出信息 */ #define LOG_PRINT_LEVEL 0xff #define SET_LOG_LEVEL(LEVEL) (gp_sys_param-system_print_level LEVEL) #define GET_LOG_LEVEL() (gp_sys_param-system_print_level) #define log_debug(fmt, args...) log_format(LOG_DEBUG_LEVEL, fmt, ##args) #define log_info(fmt, args...) log_format(LOG_INFO_LEVEL, fmt, ##args) #define log_warn(fmt, args...) log_format(LOG_WARN_LEVEL, fmt, ##args) #define log_error(fmt, args...) log_format(LOG_ERROR_LEVEL, fmt, ##args) #define log_record(fmt, args...) log_format(LOG_RECORD_LEVEL, fmt, ##args) #define printf(fmt, args...) log_format(LOG_PRINT_LEVEL, fmt, ##args)6.2 日志格式化输出实现统一的日志格式化输出函数int log_format(uint8_t level, const char *fmt, ...) { #define TIME_PREFIX_SIZE (21) #define PRINT_MAX_SIZE (1024 TIME_PREFIX_SIZE) va_list args; int num 0, i 0, fmt_index 0; int fmt_str_len 0, ret -1; int file_str_len 0, line_str_len 0; char line_buf[20] {0}; static char buf[PRINT_MAX_SIZE]; static QueueHandle_t sem NULL; time_t time {0}; /* 针对os系统 */ if(NULL sem) { sem xSemaphoreCreateCounting(1, 1); } xSemaphoreTake(sem, portMAX_DELAY); ret -1; fmt_str_len 0; if(level ! LOG_PRINT_LEVEL) { if((GET_LOG_LEVEL() level) (level ! LOG_RECORD_LEVEL) (level ! LOG_ERROR_LEVEL)) goto exit_end; for(i 0; i SYSTEM_PRINT_FMT_LIST_MAX; i) { if(level system_print_fmt_list[i].level) { fmt_index i; break; } } if(i SYSTEM_PRINT_FMT_LIST_MAX) { goto exit_end; } fmt_str_len strlen(system_print_fmt_list[fmt_index].fmt_str); strncpy((char*)buf[TIME_PREFIX_SIZE], system_print_fmt_list[fmt_index].fmt_str, fmt_str_len); } va_start(args, fmt); num vsnprintf((char*)buf[fmt_str_len TIME_PREFIX_SIZE], PRINT_MAX_SIZE - fmt_str_len - TIME_PREFIX_SIZE - 2, fmt, args); va_end(args); if(num 0) { goto exit_end; } if(level ! LOG_PRINT_LEVEL) { num fmt_str_len; buf[num TIME_PREFIX_SIZE] \r; buf[num TIME_PREFIX_SIZE 1] \n; num 2; } if((GET_LOG_LEVEL() level) (level LOG_ERROR_LEVEL)) { //do nothing } else { ret bsp_debug_send((uint8_t*)buf[TIME_PREFIX_SIZE], num); } if((LOG_ERROR_LEVEL level) || (LOG_RECORD_LEVEL level)) { bsp_rtc_get_time(time); sprintf(buf[0], [%04d-%02d-%02d %02d:%02d:%02d, time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second); buf[TIME_PREFIX_SIZE - 1] ]; gp_sys_log-system_log.log_latest_time time; system_log_write((uint8_t*)buf, num TIME_PREFIX_SIZE); } exit_end: xSemaphoreGive(sem); return ret; }7. 实际应用中的注意事项Flash寿命管理Flash有擦写次数限制通常10万次左右需要合理设计环形存储策略避免频繁擦写同一区域。电源稳定性在写入Flash时如果突然断电可能导致数据损坏。建议在关键操作前检查电源状态或增加备用电源。日志级别选择生产环境中应适当提高日志级别避免产生过多日志影响性能开发调试时可降低级别获取更多信息。时间同步确保RTC时间准确错误的时序记录会给问题排查带来困难。建议定期同步时间或使用NTP协议。存储空间监控定期检查剩余存储空间避免日志写满导致新日志无法记录。可以设置自动清理最旧日志的机制。性能考量频繁的Flash操作会影响系统性能可以设置日志缓冲机制批量写入减少操作次数。安全性敏感信息不应记录在日志中或者应该进行加密处理。同时考虑日志的完整性校验防止篡改。