499 lines
20 KiB
C
499 lines
20 KiB
C
/*
|
||
* 模块名称: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 内核对象导致的启动失败)
|
||
* v1.2 2026-10-02 王建锋 s_append 落盘序列纳入卷锁 try 语义(vol_lock_wait(0)):
|
||
* 与 ftpTask 的 lftpd 文件操作互斥(并发 FTL 写 →
|
||
* dhara journal 竞争实证);busy 返回 -2 静默顺延
|
||
* (本任务 = 看门狗喂狗点,绝不阻塞等锁)
|
||
* v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场
|
||
* 证据落 /log/wdev.log(掉电不丢)+ flush 诱导
|
||
* (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
|
||
* v1.4 2026-10-04 王建锋 P2-9:syslog_mcu_mounted() 改为幂等 + 卷锁保护;
|
||
* defaultTask 每 2s 复查,建 /log 曾失败时自动重试
|
||
* (原"仅调用一次"→ 失败后 drain 永久 no-op)。
|
||
* P2-10:s_read_raw() 纳入卷锁 try 语义,与写者
|
||
* s_append 的"容量满 unlink 重建"互斥,消除
|
||
* "UI 读记录(FA_READ) 时文件被 unlink"竞态窗口。
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#define DBG_TAG "[SYSLOG]"
|
||
#include "dbg_log.h"
|
||
|
||
#include "sys_log.h"
|
||
#include "ff.h"
|
||
#include "cmsis_os.h"
|
||
#include "sys_clock.h"
|
||
#include "vol_lock.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 目录就绪后置位 */
|
||
static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||
*/
|
||
static int s_append(syslog_id_t id, const char *desc) {
|
||
static FIL f; /* static:FF_FS_TINY=0 → 局部 FIL 吃 528 B 栈;
|
||
* 本函数全程在 vol_lock 区间内,跨任务被卷锁串行化 */
|
||
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;
|
||
}
|
||
/* 卷锁 try:本任务 = 看门狗喂狗点,绝不允许阻塞等锁(阻塞 = 喂狗停摆 =
|
||
* 误复位);busy = FTP 文件操作持有卷,本轮顺延(条目滞留队列/待发
|
||
* 数组),静默跳过、不计故障 */
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return -2;
|
||
}
|
||
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) {
|
||
vol_lock_release();
|
||
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);
|
||
}
|
||
vol_lock_release();
|
||
return rc;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读取第 idx 条原始记录(64B)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* idx - 记录序号(0 起) uint32_t
|
||
* p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t *
|
||
* 返回值:0 - 成功;-1 - 越界/读失败/卷忙 int
|
||
* 函数说明:P2-10(2026-10-04):读者纳入卷锁(try),与写者 s_append 的
|
||
* "容量满 unlink 重建"互斥——原实现读时无锁,写者 unlink 与读者
|
||
* open/read 可交错,读到的可能是半删除态文件。try 语义(timeout=0):
|
||
* 本路径可由 uiTask 调用,uiTask 在 required_mask 内、须每 100ms 喂
|
||
* 心跳,绝不能阻塞等锁;busy = 本次读失败(UI 下轮刷新重读,非故障)。
|
||
*/
|
||
static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
|
||
static FIL f; /* static:省 528 B 栈;open~close 全在卷锁区间内
|
||
* (uiTask 与写者任务由 vol_lock 串行化,见上方 P2-10 说明) */
|
||
FRESULT fr;
|
||
UINT br = 0u;
|
||
int rc;
|
||
|
||
if (p_rec == NULL || idx >= syslog_count(id)) {
|
||
return -1;
|
||
}
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return -1; /* 卷被占用:本次读失败(非故障,下轮重读) */
|
||
}
|
||
fr = f_open(&f, s_paths[id], FA_READ);
|
||
if (fr != FR_OK) {
|
||
vol_lock_release();
|
||
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);
|
||
rc = (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1;
|
||
vol_lock_release();
|
||
return rc;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:日志引擎初始化(内核启动前安全空操作)
|
||
* 入口参数:无
|
||
* 返回值: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 成功后调用;**可重复调用(幂等)**——
|
||
* 产品运行期 defaultTask 每 2s 复查一次(见下 P2-9 说明)
|
||
* 函数说明:P2-9(2026-10-04):原实现"仅调用一次",若此处建 /log 失败则
|
||
* s_fs_ready 恒 0 → syslog_drain 永久 no-op(运行期日志与自检全部
|
||
* 滞留/丢弃),且无任何重试。现改为幂等:s_fs_ready 已置位则立即
|
||
* 返回;否则重试建目录。defaultTask 在 STORAGE_READY 置位后每 2s
|
||
* 调用本函数——卷已就绪但日志后端未就绪时自动补建目录。
|
||
* FS 操作纳入卷锁 try:运行期重试时 ftpTask 可能正操作卷,try 失败
|
||
* = 本轮顺延(下轮再试),绝不阻塞(本任务 = 看门狗喂狗点)。
|
||
*/
|
||
void syslog_mcu_mounted(void) {
|
||
FILINFO finfo;
|
||
uint32_t last = 0u;
|
||
const char *desc = "power on";
|
||
|
||
if (s_fs_ready != 0u) {
|
||
return; /* 幂等:已就绪 */
|
||
}
|
||
/* 卷锁 try:运行期重试时与 ftpTask 卷访问串行;busy = 本轮顺延 */
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return;
|
||
}
|
||
if (f_stat("/log", &finfo) != FR_OK) {
|
||
FRESULT fr = f_mkdir("/log");
|
||
if (fr != FR_OK && fr != FR_EXIST) {
|
||
/* 失败日志去重:持续失败只报第一次(防 2s 周期刷屏),成功后复位 */
|
||
if (s_mount_fail_reported == 0u) {
|
||
s_mount_fail_reported = 1u;
|
||
DBG_ERROR("mkdir /log fail (%d), will retry", (int)fr);
|
||
}
|
||
vol_lock_release();
|
||
return;
|
||
}
|
||
}
|
||
vol_lock_release();
|
||
s_mount_fail_reported = 0u;
|
||
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; /* 队列空 */
|
||
}
|
||
{
|
||
int arc = s_append((syslog_id_t)e.id, e.desc);
|
||
if (arc == -2) {
|
||
break; /* 卷被占用(FTP 文件操作进行中):本轮顺延,静默
|
||
(条目已出队;run.log 尽力而为语义,不计故障) */
|
||
}
|
||
if (arc != 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;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;round 7 纯取证)
|
||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,不阻断判定) int
|
||
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞)
|
||
* 函数说明:
|
||
* 为什么必须落盘:误复位后 RAM 态随复位消失、`[WD] task stall` 行经僵尸串口
|
||
* 可能丢失(2026-10-02 台架 round 3~6 绕圈的判据缺口),故证据必须落到
|
||
* **掉电不丢**的盘上,复位后经 FTP RETR 回读。
|
||
* flush 诱导(关键):写完 wdev.log 后立刻 s_append(SYSLOG_RUN, ...) 写另一个
|
||
* 文件——FTL 单页缓存语义为"写到**不同 LPN** 才把前一脏页刷进 NAND";没有
|
||
* 这一步,本行证据会在约 1.7s 后的 IWDG 复位中回滚丢失(409 条里被回滚的
|
||
* 07:56 记录即同类教训)。s_append 自行取卷锁(非递归锁),故必须在
|
||
* vol_lock_release() **之后**调用,绝不可在持锁状态下调用。
|
||
* 容量护栏:> 8KB 先 unlink 重建,防长跑无界增长(8.3 合法名 wdev.log)。
|
||
*/
|
||
int sys_log_evidence(const char *p_desc) {
|
||
static FIL f; /* static:省 528 B 栈(FF_FS_TINY=0 → FIL=528 B) */
|
||
static char line[192]; /* static:省 192 B 栈;本函数只有 poller(defaultTask)
|
||
* 一个调用者(task_wd_poll 内),且全程在卷锁区间内 */
|
||
FRESULT fr;
|
||
UINT bw = 0u;
|
||
UINT line_len;
|
||
char ts[SYSLOG_TS_LEN + 1u];
|
||
int n;
|
||
int rc = 0;
|
||
|
||
if (p_desc == NULL) {
|
||
return -1;
|
||
}
|
||
/* 卷锁 try:本上下文 = 看门狗喂狗点,绝不阻塞等锁 */
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return -1;
|
||
}
|
||
|
||
/* 容量护栏:超过 8KB 先删除重建(防长跑无界增长) */
|
||
{
|
||
FILINFO finfo;
|
||
if (f_stat("/log/wdev.log", &finfo) == FR_OK && finfo.fsize > 8192u) {
|
||
(void)f_unlink("/log/wdev.log");
|
||
}
|
||
}
|
||
|
||
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';
|
||
n = snprintf(line, sizeof(line), "%s %s\r\n", ts, p_desc);
|
||
if (n <= 0) {
|
||
vol_lock_release();
|
||
return -1;
|
||
}
|
||
line_len = (UINT)strlen(line);
|
||
|
||
fr = f_open(&f, "/log/wdev.log", FA_WRITE | FA_OPEN_APPEND);
|
||
if (fr == FR_OK) {
|
||
fr = f_write(&f, line, line_len, &bw);
|
||
(void)f_close(&f);
|
||
if (fr != FR_OK || bw != line_len) {
|
||
rc = -1;
|
||
}
|
||
} else {
|
||
rc = -1;
|
||
}
|
||
vol_lock_release();
|
||
|
||
/* flush 诱导:写另一个文件(不同 LPN)把 wdev.log 的脏页刷进 NAND。
|
||
* 必须在释放卷锁之后调用(s_append 自行取锁,非递归锁不可重入)。 */
|
||
(void)s_append(SYSLOG_RUN, "wd stall declared");
|
||
|
||
return rc;
|
||
}
|