/* * 模块名称:System Log (MCU 后端) — 运行/自检记录落盘 * 模块功能:FatFS /log 目录日志文件(run.log/selftest.log)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列, * defaultTask(唯一写者)排空落盘;容量满删除重建;上电/重启分类 * 适用平台:STM32F407ZGTx * 作者:王建锋 * 创建日期:2026-09-14 * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4.4) * v1.1 2026-09-21 王建锋 消息队列改为内核启动后由 syslog_drain 惰性创建 * (修复 osKernelStart 前创建 FreeRTOS 内核对象导致的启动失败) */ /* 头文件包含区 */ #include #include #define DBG_TAG "[SYSLOG]" #include "dbg_log.h" #include "sys_log.h" #include "ff.h" #include "cmsis_os.h" #include "sys_clock.h" /* 静态变量定义区 */ static const char *const s_paths[SYSLOG_COUNT] = { "/log/run.log", "/log/selftest.log" }; static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX }; #define SYSLOG_QUEUE_MAX 8u /* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */ typedef struct { uint8_t id; const char *desc; } syslog_entry_t; static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */ static volatile uint8_t s_preboot_cnt; /* 数组条目数 */ static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */ static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" }; static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */ /* 函数定义区 */ /* * 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建) * 入口参数:id - 日志 ID syslog_id_t * desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char * * 返回值:0 - 成功;-1 - 写失败 int * 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载 */ static int s_append(syslog_id_t id, const char *desc) { FIL f; FRESULT fr; UINT bw = 0u; uint8_t rec[SYSLOG_REC_SIZE]; char ts[SYSLOG_TS_LEN + 1u]; uint32_t epoch; size_t dl; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { return -1; } if (syslog_count(id) >= s_caps[id]) { fr = f_unlink(s_paths[id]); DBG_INFO("cap full, recreate %s", s_paths[id]); if (fr != FR_OK && fr != FR_NO_FILE) { return -1; } } memset(rec, 0u, sizeof(rec)); if (sys_clock_get_str(ts, sizeof(ts)) == NULL) { memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN); } ts[SYSLOG_TS_LEN] = '\0'; epoch = sys_clock_get(); if (epoch == UINT32_MAX) { epoch = 0u; } memcpy(rec, ts, SYSLOG_TS_LEN); rec[SYSLOG_TS_LEN] = ' '; (void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch); rec[SYSLOG_DESC_OFFS - 1u] = ' '; dl = strlen(desc); if (dl > SYSLOG_DESC_MAX) { dl = SYSLOG_DESC_MAX; } memcpy(rec + SYSLOG_DESC_OFFS, desc, dl); /* L1 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建 * 同一机制);L2 错误去重:持续故障只报第一次,恢复前静默(消除"每次 * 开机同一错误刷屏")。打开模式必须带 FA_WRITE 位:本 FatFs 移植的 * FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write * 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */ static uint8_t s_fault_reported[SYSLOG_COUNT]; int rc = 0; fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT || fr == FR_INT_ERR || fr == FR_DISK_ERR || fr == FR_NO_FILE || fr == FR_NO_PATH || fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) { (void)f_unlink(s_paths[id]); fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); } if (fr == FR_OK) { fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw); (void)f_close(&f); if (fr != FR_OK || bw != SYSLOG_REC_SIZE) { (void)f_unlink(s_paths[id]); bw = 0u; fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); if (fr == FR_OK) { fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw); (void)f_close(&f); } if (fr != FR_OK || bw != SYSLOG_REC_SIZE) { rc = -1; } } } else { rc = -1; } if (rc == 0) { s_fault_reported[id] = 0u; } else if (s_fault_reported[id] == 0u) { s_fault_reported[id] = 1u; DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr); } return rc; } /* * 函数功能:读取第 idx 条原始记录(64B) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t * p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t * * 返回值:0 - 成功;-1 - 越界/读失败 int */ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { FIL f; FRESULT fr; UINT br = 0u; if (p_rec == NULL || idx >= syslog_count(id)) { return -1; } fr = f_open(&f, s_paths[id], FA_READ); if (fr != FR_OK) { return -1; } fr = f_lseek(&f, (FSIZE_t)idx * SYSLOG_REC_SIZE); if (fr == FR_OK) { fr = f_read(&f, p_rec, SYSLOG_REC_SIZE, &br); } (void)f_close(&f); return (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1; } /* * 函数功能:日志引擎初始化(内核启动前安全空操作) * 入口参数:无 * 返回值:0 - 成功 int * 限定条件:app_main_init() 起始处调用(osKernelStart 之前); * 不得调用任何 FreeRTOS API(内核对象须在内核启动后才可创建) * 函数说明:启动前投递走 s_preboot 静态数组(syslog_post 按内核状态自动分流); * FreeRTOS 消息队列由 syslog_drain 在运行期(defaultTask 上下文)惰性创建。 * 历史缺陷:此处曾 osMessageQueueNew 创建队列,早于 osKernelInitialize * 提前拉起内核,其后任务创建在未完成初始化的内核状态上执行,导致启动失败 */ int syslog_mcu_init(void) { return 0; } /* * 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录) * 入口参数:无 * 返回值:无 * 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次 */ void syslog_mcu_mounted(void) { FILINFO finfo; uint32_t last = 0u; const char *desc = "power on"; if (f_stat("/log", &finfo) != FR_OK) { FRESULT fr = f_mkdir("/log"); if (fr != FR_OK && fr != FR_EXIST) { DBG_ERROR("mkdir /log fail (%d)", (int)fr); return; } } s_fs_ready = 1u; /* 上电/重启分类:末条 epoch 距 now < SYSLOG_REBOOT_WINDOW_S → 重启,否则上电 */ if (syslog_last_epoch(SYSLOG_RUN, &last) == 0 && sys_clock_get() != UINT32_MAX) { if ((uint32_t)(sys_clock_get() - last) < SYSLOG_REBOOT_WINDOW_S) { desc = "reboot"; } } (void)s_append(SYSLOG_RUN, desc); DBG_INFO("runlog boot entry: %s", desc); } /* * 函数功能:排空待发日志并落盘(启动前数组 + 运行期队列,FIFO);首次调用时惰性创建运行期队列 * 入口参数:无 * 返回值:无 * 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试) */ void syslog_drain(void) { syslog_entry_t e; /* 运行期惰性创建消息队列:本函数仅由 defaultTask(调度器运行中)调用, 保证内核对象创建发生在 osKernelStart 之后 */ if (s_queue == NULL) { s_queue = osMessageQueueNew(SYSLOG_QUEUE_MAX, sizeof(syslog_entry_t), &s_queue_attr); if (s_queue == NULL) { DBG_ERROR("queue create fail"); } } if (!s_fs_ready || s_queue == NULL) { return; } while (s_preboot_cnt > 0u) { e = s_preboot[0]; if (s_append((syslog_id_t)e.id, e.desc) != 0) { break; /* 落盘失败:保留剩余条目,下轮重试 */ } memmove(&s_preboot[0], &s_preboot[1], sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - 1u)); s_preboot_cnt--; } for (;;) { if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) { break; /* 队列空 */ } if (s_append((syslog_id_t)e.id, e.desc) != 0) { DBG_ERROR("drop entry id=%d", (int)e.id); break; /* 落盘失败:停止排空,避免连续丢弃 */ } } } /* * 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃) * 入口参数:id - 日志 ID syslog_id_t * desc - 描述串(UTF-8,静态存储期,超长截断) const char * * 返回值:无 * 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串 */ void syslog_post(syslog_id_t id, const char *desc) { syslog_entry_t e; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { return; } e.id = (uint8_t)id; e.desc = desc; if (s_queue != NULL && osKernelGetState() == osKernelRunning) { if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) { DBG_ERROR("queue full, drop: %s", desc); } } else { uint8_t n = s_preboot_cnt; if (n < SYSLOG_QUEUE_MAX) { s_preboot[n] = e; s_preboot_cnt = n + 1u; } else { DBG_ERROR("preboot full, drop: %s", desc); } } } /* * 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1)) * 入口参数:id - 日志 ID syslog_id_t * 返回值:条数;文件缺失/未挂载返回 0 uint32_t */ uint32_t syslog_count(syslog_id_t id) { FILINFO finfo; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { return 0u; } if (f_stat(s_paths[id], &finfo) != FR_OK) { return 0u; } return (uint32_t)finfo.fsize / SYSLOG_REC_SIZE; } /* * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * * ts_len - p_ts 容量 size_t * p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char * * desc_len - p_desc 容量 size_t * 返回值:0 - 成功;-1 - 越界/读失败 int */ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) { uint8_t rec[SYSLOG_REC_SIZE]; size_t dl = 0u; if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) { return -1; } if (s_read_raw(id, idx, rec) != 0) { return -1; } memcpy(p_ts, rec, SYSLOG_TS_LEN); p_ts[SYSLOG_TS_LEN] = '\0'; while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') { dl++; } if (dl >= desc_len) { dl = desc_len - 1u; } memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl); p_desc[dl] = '\0'; return 0; } /* * 函数功能:读取末条记录的 epoch 秒(上电/重启分类用) * 入口参数:id - 日志 ID syslog_id_t * p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t * * 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int */ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) { uint8_t rec[SYSLOG_REC_SIZE]; uint32_t v = 0u; uint32_t i; if (syslog_count(id) == 0u) { if (p_epoch != NULL) { *p_epoch = 0u; } return -1; } if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) { if (p_epoch != NULL) { *p_epoch = 0u; } return -1; } for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) { char c = (char)rec[SYSLOG_EPOCH_OFFS + i]; if (c >= '0' && c <= '9') { v = v * 10u + (uint32_t)(c - '0'); } } if (p_epoch != NULL) { *p_epoch = v; } return 0; }