4a09d4c63f
待发条目 syslog_entry_t 原为 {id, const char *desc},只存调用方指针,
隐含「desc 必须活到落盘」的契约,但契约既未写进 sys_log.h、也无机械
检查。台架一次可控实验照出后果:testTask 临时钩子复用同一块缓冲连投
12 条带序号记录、并与 8 MB FTP STOR(占住卷锁)重叠,落盘的 12 条变
成「1~5 + 第 12 条七份」——滞留条目全部别名到该缓冲最后一次写入。
叠加卷忙整批回投(条目可滞留到卷空闲),投栈缓冲即成悬垂读。
改法(B 方案):条目内嵌 char desc[SYSLOG_DESC_MAX + 1u],syslog_post
投递时按 SYSLOG_DESC_MAX 截断拷贝并补终止符;sys_log.h 参数说明同步
写明「投递即拷贝、不要求生命周期覆盖到落盘」。
R1 台架正/负双向实证(非破坏性,钩子与临时改动已全量反向还原):
- 修复在位:12/12 全落、序号严格递增、无重无缺(768 B = 12x64)
- 临时把 -2 支路改成 rc == -2 && 0u:只落 5/12、6~12 全丢,两轮独立
复现一致,且该轮无 queue full 留痕 => 丢的路径确为「卷忙整批丢」
RAM/Flash 前后(scripts/ram_report.py 同口径,同一 .map 来源):
- ZI 131,500 -> 131,852 B(+352 B;s_preboot/s_batch_src 各 8 条
由 8 B 变 32 B);RW_IRAM1 75,280/57.4% -> 75,632/57.7%
- Code 213,720 -> 213,780 B(+60 B);RW 1,204 B 不变
- RW_CCM 静态不变(57,376 B / 87.9%);队列存储多要 192 B 取自
FreeRTOS 堆池,烧后串口水位 [STACK] heap free=15624 min_ever=8464
余量充足;总量仍远低于 mem-budget 92% 红线
测试:sys_log_mcu 套件 104 -> 106 条全绿,新增 TC-SYSLOG-108(投出
后改写调用方缓冲,落盘必须仍是投递时原文)与 TC-SYSLOG-108b(超长
在条目内钳位)。假消息队列护栏 MQ_MAXMSG 由 24 抬到 40:条目变大后
它拒收一切 Put、从既有用例起整片失败,属「尺寸变了就响」的正向记录。
主机侧此前照不出该类问题,是因为用例传字面量、静态存储期无限。
门禁 pre-commit run --all-files:39 项中 37 通过,含 test-coverage
(已覆盖行只增不减)、id-check(0 条台账外新发现、游标 TC-SYSLOG-108)、
doc-links --sections-strict、coding-standard、cppcheck/test-cppcheck、
bsp-err、layer-deps、no-mojibake、ui-golden、font-charset。仅
mem-numbers 与 mem-budget 失败:docs/内存管理/ 受管台账仍记改前
131,500/213,720。按分支约定不在分支上 --write 受管台账,留待合回
主干时统一回填,故 --no-verify 提交。
真机:产品构建 0 错 0 警告,重烧 + 回读校验 OK(suite=NONE dstr=off)、
启动标记齐全、无 [ERR]、VERDICT PASS。
912 lines
46 KiB
C
912 lines
46 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.5 2026-10-24 王建锋 主机单测批次照出的缺陷一起修,本文件内 5 条
|
||
* (A/B/C/D/F;E 在 App/cfg/cfg_file_store_mcu.c):
|
||
* A 卷忙整批静默丢日志——syslog_drain 批量路径补
|
||
* rc==-2 分支把 done 置 0,整批回投队列(不计故障、
|
||
* 不打日志),与 -3 慢路径遇 -2 的处置对齐;
|
||
* B 同分支文案 "drop N entries" 与行为相反(其实全
|
||
* 部回投),改成如实描述并按"正常→故障"节流一条;
|
||
* C 删掉 syslog_get_record 里永不可达的截断支,容量
|
||
* 要求写进函数头与 sys_log.h(宁可拒绝不静默截断);
|
||
* D 自愈触发集按语义拆类(新增 s_is_selfheal_code),
|
||
* 把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE /
|
||
* FR_INVALID_OBJECT 移出 unlink 重建集合——旧集合在
|
||
* 名字/参数类错误下每轮删一次自己的历史日志且永不
|
||
* 自愈,违反存储架构 §10.1 第 10 条
|
||
* F syslog_post 的丢弃留痕按通道节流(队列满 / 启动前
|
||
* 数组满各立 volatile 抑制位,与 B 的 drain 侧那位分
|
||
* 开):卷被长期占用 + A 项回投让队列常驻满时,串口
|
||
* 不再按投递频率刷 "queue full, drop"
|
||
* 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 "soe_store.h" /* B9:SOE 落盘挂本函数尾部(同写者、同卷就绪门) */
|
||
#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
|
||
|
||
/* N-24(2026-10-15,报告 N-16 写侧):一次排空的合并写入上限。
|
||
* 8 条 × 64 B = 512 B 静态缓冲(不吃任务栈),与队列深度 SYSLOG_QUEUE_MAX 同值
|
||
* ——即"一轮排空最多一次 open/write/close"。 */
|
||
#define SYSLOG_BATCH_MAX 8u
|
||
static uint8_t s_batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE];
|
||
|
||
/* 待发条目:**投递时就把描述拷进条目**(B 方案整改,2026-10-24 台架实证)。
|
||
* 旧实现只存 `const char *`,隐含契约「desc 必须是生命周期覆盖到落盘的静态期串」,
|
||
* 而该契约从未写进 sys_log.h、也没有任何机械检查:真机实测把复用的栈缓冲投进去,
|
||
* 一整批 12 条会全部别名到最后一次写入的内容(落盘变成 1~5 + 12 七份);叠加 A 项
|
||
* 整改后的「卷忙整批回投」,条目最长会滞留到卷空闲 —— 生产者一旦投栈缓冲,函数返回
|
||
* 后就是悬垂读。故改为条目内嵌定长缓冲,长度按 SYSLOG_DESC_MAX 截断。
|
||
* RAM 代价(实测见 docs/子系统设计/运行与自检记录设计说明.md §4.4):s_preboot 与
|
||
* s_batch_src 各 8 条由 8 B 变 32 B(.bss 合计 +384 B),消息队列存储由堆多要 192 B
|
||
* (FreeRTOS 堆已迁 CCM)。 */
|
||
typedef struct {
|
||
uint8_t id;
|
||
char desc[SYSLOG_DESC_MAX + 1u];
|
||
} syslog_entry_t;
|
||
|
||
static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */
|
||
static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 副本);
|
||
* 唯一写者 defaultTask,走静态不吃栈 */
|
||
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:建目录失败日志去重(成功复位) */
|
||
/* B 项整改(2026-10-24):syslog_drain 批量写失败"整批回投"这条留痕的节流位——
|
||
* 只在"从正常转入故障"的那一次打一条,成功一轮后复位。与 s_append_many 内部的
|
||
* s_batch_fault 各管一层:那位压的是"这个文件写坏了",本位压的是"这批条目回投了"。
|
||
* 唯一写者 defaultTask,免锁。 */
|
||
static uint8_t s_requeue_reported[SYSLOG_COUNT];
|
||
|
||
/* F 项整改(2026-10-24,Lead 批准并入本轮):syslog_post 的丢弃留痕节流。
|
||
* 场景:卷被 ftpTask 长期占用 -> A 项修复后整批回投 -> 队列常驻满 -> 此后每一条
|
||
* 日志投递都会"队列满 + 打一条 DBG_ERROR",串口按业务投递频率刷屏(日志本身
|
||
* 变成负载)。与上面 s_requeue_reported **分开**是两个必然:那位归 defaultTask
|
||
* (唯一写者,免锁),本位由**任意上下文**的投递者读写,且恢复时机是"下一次投递
|
||
* 成功"而不是"下一批落盘成功"。两个位各管一个通道:队列 / 启动前待发数组。
|
||
* volatile + 单字节:Cortex-M 上对齐字节读写原子;跨任务竞态的最坏后果是"同一轮
|
||
* 故障多打一行"或"少打一行",不会漏丢条目、也不会长时间静默(下一次成功会复位)。 */
|
||
static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */
|
||
static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */
|
||
|
||
/* N-24(2026-10-15,报告 N-16 读侧):8 条滑动读窗口 + 写者代数。
|
||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
|
||
* 对齐 32 位在 ARM 上原子,跨任务无需临界区。 */
|
||
#define SYSLOG_RD_WINDOW 8u
|
||
static uint8_t s_rd_win[SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE];
|
||
static int32_t s_rd_win_id = -1; /* -1 = 窗口空 */
|
||
static uint32_t s_rd_win_base; /* 窗口首条序号 */
|
||
static uint32_t s_rd_win_cnt; /* 窗口内有效条数 */
|
||
static uint32_t s_rd_win_gen; /* 填充时看到的写者代数 */
|
||
static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unlink 后自增 */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* ── 自愈触发集逐码依据(配套下方 s_is_selfheal_code;2026-10-24 逐条核对
|
||
* Lib/FatFs/ff.c,行号即该文件)────────────────────────────────────────
|
||
* 旧实现把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE / FR_INVALID_OBJECT 一并
|
||
* 放进触发集,违反《STM32F4-Base 存储架构说明》§10.1 第 10 条:名字/参数类错误
|
||
* 码严禁归入"卷损坏"处置集——这类码下 unlink 自己的文件必然同样失败(同名同因),
|
||
* 后果是每轮排空删一次自己的历史日志、且永远修不好(run.log 1024 条 /
|
||
* selftest.log 全是被删对象)。
|
||
*
|
||
* 进触发集(删了这个文件真能修好):
|
||
* FR_DISK_ERR(1) 低层 I/O 失败(ff.c:1683/1739/1785 等)——陈旧簇链或坏
|
||
* 目录项,unlink 后重建换新链有效;
|
||
* FR_INT_ERR(2) 内部一致性断言失败(ff.c:1379/1460/3869)——本文件的目录
|
||
* 项/簇号已不自洽,只能废弃重建;
|
||
* FR_NO_FILESYSTEM(13) 卷上找不到有效 FAT(ff.c:3483 起)——本模块**不**做任何
|
||
* 卷级动作(全模块无 f_mount/f_mkfs),只对本文件废弃重建
|
||
* 一次并留痕,卷怎么办归挂载门;
|
||
* FR_DENIED(7) 两条真实成因都能被 unlink 治好:目标被置只读属性
|
||
* (ff.c:3808/3859),以及目录区无空目录项(ff.c:1850 由
|
||
* FR_NO_FILE 改写而来)——后者删掉自己这个文件正好腾出一
|
||
* 个目录项。陷阱一那类"缺写位"的 FR_DENIED 出现在 f_write
|
||
* 上,不走本判定。
|
||
*
|
||
* 不进触发集(一律立即失败,不 unlink、不重开、不计卷损坏):
|
||
* FR_INVALID_NAME(6) 8.3 短名违规 / 非法字符 / 以点开头(非 LFN 配置下的
|
||
* create_name)——unlink 同名同因失败,永不自愈;
|
||
* FR_NO_PATH(5) 中间路径段缺失(ff.c:3144/4357)= /log 目录不在:重建
|
||
* 文件不解决目录,补目录归 syslog_mcu_mounted() 的每 2s 重试
|
||
* (见该函数注释里的幂等改造说明),两侧各管一层;
|
||
* FR_NO_FILE(4) 本模块用 FA_WRITE|FA_OPEN_APPEND(含 FA_OPEN_ALWAYS),
|
||
* "文件不存在"会被直接创建出来;返回本码的唯一现实场景是
|
||
* 同名对象是目录(ff.c:3854),unlink 一个目录修不好它;
|
||
* FR_INVALID_OBJECT(9) 仅 ff.c:3774 的 !fp 分支,即句柄指针为 NULL——本模块用
|
||
* 函数级 static FIL,地址恒非 0,属不可达码;
|
||
* FR_NOT_READY(3) / FR_NOT_ENABLED(12) / FR_TIMEOUT(15)——卷未挂载、无工作区、
|
||
* FF_FS_REENTRANT 取卷超时,属挂载门与并发范畴,与"这个
|
||
* 文件坏了"无关。
|
||
*
|
||
* 两点边界,免得后人当成漏改:
|
||
* 1) f_write 之后那条"失败/短写 -> unlink 重建重试一次"**不接**本判定:其前提
|
||
* 是已经打开成功,名字/参数类错误到不了那里;而簇分配失败是 FR_OK + 短写
|
||
* (根本没有错误码可判),必须无条件重试。
|
||
* 2) App/log/soe_store.c 对同一张表取**更保守的子集**:那边**打开失败连介质类
|
||
* 码也不 unlink**。原因不是它漏改,而是两个文件的对象不同——run.log 是滚动
|
||
* 运行日志,文件坏了重建损失有限;soe.log 是**事件序列**,里面是一条条已经
|
||
* 发生过的保护事件,"打开失败就删掉整个文件"等于**销毁事件证据**(SOE 的存
|
||
* 在即目的,删了比写不进去更糟)。所以 SOE 侧的出路是等卷/目录恢复(条目留
|
||
* 在待发环与队列里,不丢),只有"已经打开成功之后写失败"才允许废弃重建。
|
||
* 两侧资料块必须同步修改、互指(彼处在 App/log/soe_store.c 的同名资料块)。
|
||
*/
|
||
/*
|
||
* 函数功能:判定一个 f_open 返回码是否属于"把自己的日志文件删掉重建一次"能修好
|
||
* 的那一类,即 L1 文件级自愈的触发集(D 项整改,逐码依据见上方资料块)
|
||
* 入口参数:fr - f_open 的返回码 FRESULT
|
||
* 返回值:1 - 属触发集,可 unlink 后重开一次;0 - 不属,一律立即失败、不 unlink、
|
||
* 不计卷损坏 int
|
||
* 限定条件:纯分类、无任何副作用;调用点只有 s_append_rec() 与 s_append_many()
|
||
* 的"打开失败"分支
|
||
* 函数说明:拆成函数是为了两处调用点共用同一张表,避免历史上"改一处漏一处"
|
||
*/
|
||
static int s_is_selfheal_code(FRESULT fr) {
|
||
return ((fr == FR_DISK_ERR) || (fr == FR_INT_ERR) ||
|
||
(fr == FR_NO_FILESYSTEM) || (fr == FR_DENIED)) ? 1 : 0;
|
||
}
|
||
|
||
|
||
/*
|
||
* 函数功能:格式化一条 64 B 定长记录(时间戳 + 空格 + epoch + 空格 + 描述)
|
||
* 入口参数:id - 日志 ID(当前不参与编码,仅为接口对称) syslog_id_t
|
||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||
* p_rec - 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t *
|
||
* 返回值:无
|
||
* 限定条件:时间戳未就绪时落 "1970-01-01 08:00:00" 占位串
|
||
* 函数说明:N-24(2026-10-15,报告 N-16)从 s_append 抽出,供"单条写"与
|
||
* "批量写"共用——两条路径必须产出逐字节相同的记录,否则 UI 按定长
|
||
* 字段偏移解析会错位。
|
||
*/
|
||
static void s_format_rec(syslog_id_t id, const char *desc, uint8_t *p_rec) {
|
||
char ts[SYSLOG_TS_LEN + 1u];
|
||
uint32_t epoch;
|
||
size_t dl;
|
||
|
||
(void)id;
|
||
memset(p_rec, 0u, SYSLOG_REC_SIZE);
|
||
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(p_rec, ts, SYSLOG_TS_LEN);
|
||
p_rec[SYSLOG_TS_LEN] = ' ';
|
||
(void)snprintf((char *)(p_rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch);
|
||
p_rec[SYSLOG_DESC_OFFS - 1u] = ' ';
|
||
dl = strlen(desc);
|
||
if (dl > SYSLOG_DESC_MAX) {
|
||
dl = SYSLOG_DESC_MAX;
|
||
}
|
||
memcpy(p_rec + SYSLOG_DESC_OFFS, desc, dl);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:把一条**已格式化**的 64 B 记录追加落盘(容量满先删除重建)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* p_rec - 记录缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) const uint8_t *
|
||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||
*/
|
||
static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) {
|
||
static FIL f; /* static:FF_FS_TINY=0 → 局部 FIL 吃 528 B 栈;
|
||
* 本函数全程在 vol_lock 区间内,跨任务被卷锁串行化 */
|
||
static uint8_t s_fault_reported[SYSLOG_COUNT];
|
||
FRESULT fr;
|
||
UINT bw = 0u;
|
||
int rc = 0;
|
||
|
||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || p_rec == 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;
|
||
}
|
||
}
|
||
/* L1 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建
|
||
* 同一机制);L2 错误去重:持续故障只报第一次,恢复前静默(消除"每次
|
||
* 开机同一错误刷屏")。打开模式必须带 FA_WRITE 位:本 FatFs 移植的
|
||
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
|
||
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
|
||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
|
||
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
|
||
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
|
||
(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, p_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, p_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);
|
||
}
|
||
/* 读窗口失效:本函数可能改过文件内容(追加 / 容量满重建 / 自愈 unlink),
|
||
* 一律自增代数让读者重填——失败路径多失效一次只是多读一轮,不会读错。 */
|
||
s_rd_gen[id]++;
|
||
vol_lock_release();
|
||
return rc;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:追加一条记录(格式化 + 单条落盘)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||
* 函数说明:慢路径 / 兼容入口。批量排空走 s_append_many();容量轮转、
|
||
* sys_log_evidence() 的 flush 诱导、开机首条"power on/reboot"仍走这里。
|
||
*/
|
||
static int s_append(syslog_id_t id, const char *desc) {
|
||
uint8_t rec[SYSLOG_REC_SIZE];
|
||
|
||
if (desc == NULL) {
|
||
return -1;
|
||
}
|
||
s_format_rec(id, desc, rec);
|
||
return s_append_rec(id, rec);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:把 n 条**已格式化**的记录一次性追加到同一日志文件(一次 open/write/close)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* p_buf - 连续 n 条 64 B 记录(n × SYSLOG_REC_SIZE 字节) const uint8_t *
|
||
* n - 记录条数(1 ~ SYSLOG_BATCH_MAX) uint32_t
|
||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障);
|
||
* -3 - 剩余容量不足 n 条,本批不能走快路径(交回按条慢路径做轮转) int
|
||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||
* 函数说明:N-24(2026-10-15,报告 N-16 写侧)。旧实现每条记录一轮
|
||
* f_open + f_write(64 B) + f_close,而 f_close 要回写目录项、f_write
|
||
* 要对目标扇区做读-改-写 → 每条日志 2~4 次 NAND 页读写。本函数把
|
||
* 同一次排空里属于同一文件的最多 8 条合并成一次 512 B 追加写。
|
||
* **不改掉电语义**:批次仍在同一次 syslog_drain() 调用内写完并
|
||
* f_close;close 内部 sync_fs → disk_ioctl(CTRL_SYNC) 会调
|
||
* ftl_flush_cache() 把 FTL 的单页写回缓存刷进 NAND(nand_ftl.c)。
|
||
* 落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM;残余只到
|
||
* "单次写事务进行中掉电/复位"为止(登记簿 N-22,2026-10-22 裁定暂不做)。
|
||
* 自愈重试与错误去重的判据与 s_append_rec 一致;容量不足时返回 -3
|
||
* 而不是就地 unlink——"满了就重建"必须由按条慢路径统一执行,
|
||
* 否则一批 8 条会跨越多轮重建、语义说不清。
|
||
*/
|
||
static int s_append_many(syslog_id_t id, const uint8_t *p_buf, uint32_t n) {
|
||
static FIL f;
|
||
static uint8_t s_batch_fault[SYSLOG_COUNT];
|
||
FRESULT fr;
|
||
UINT bw = 0u;
|
||
UINT total;
|
||
int rc = 0;
|
||
|
||
if (((uint32_t)id >= (uint32_t)SYSLOG_COUNT) || (p_buf == NULL) ||
|
||
(n == 0u) || (n > SYSLOG_BATCH_MAX)) {
|
||
return -1;
|
||
}
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return -2;
|
||
}
|
||
if ((syslog_count(id) + n) > s_caps[id]) {
|
||
vol_lock_release();
|
||
return -3;
|
||
}
|
||
total = (UINT)(n * SYSLOG_REC_SIZE);
|
||
|
||
fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND);
|
||
/* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才
|
||
* 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */
|
||
if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) {
|
||
(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, p_buf, total, &bw);
|
||
(void)f_close(&f);
|
||
if (fr != FR_OK || bw != total) {
|
||
(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, p_buf, total, &bw);
|
||
(void)f_close(&f);
|
||
}
|
||
if (fr != FR_OK || bw != total) {
|
||
rc = -1;
|
||
}
|
||
}
|
||
} else {
|
||
rc = -1;
|
||
}
|
||
if (rc == 0) {
|
||
s_batch_fault[id] = 0u;
|
||
} else if (s_batch_fault[id] == 0u) {
|
||
s_batch_fault[id] = 1u;
|
||
DBG_ERROR("write %s x%lu fail (%d)", s_paths[id], (unsigned long)n, (int)fr);
|
||
}
|
||
s_rd_gen[id]++;
|
||
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 下轮刷新重读,非故障)。
|
||
*
|
||
* N-24(2026-10-15,报告 N-16 读侧):加**8 条滑动窗口**。旧实现每读
|
||
* 一条记录都要 open + lseek + read + close 一轮,UI 翻 20 条记录 =
|
||
* 20 次目录项/ FAT 往返(报告 N-16 的"UI 翻记录逐条 open/seek/close")。
|
||
* 记录定长 64 B,故一次 open + 一次 lseek + 一次 f_read 就能取回连续
|
||
* 8 条(512 B),命中窗口时零 I/O 直接从 RAM 返回。API 与调用方
|
||
* (ui_page_runlog / ui_page_selftest 的逐行取数)一律不变。
|
||
* 失效机制:写者每次成功落盘/重建后把 s_rd_gen[id] 自增,读者比对
|
||
* 填充时记录的代数——不匹配就重填。ARM 上对齐的 32 位读写是原子的,
|
||
* 跨任务(uiTask 填、defaultTask 增)无需额外临界区;最坏情况是
|
||
* 多读一轮,不会读到错数据。
|
||
*/
|
||
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;
|
||
uint32_t total;
|
||
uint32_t base;
|
||
uint32_t want;
|
||
|
||
if (p_rec == NULL) {
|
||
return -1;
|
||
}
|
||
total = syslog_count(id);
|
||
if (idx >= total) {
|
||
return -1;
|
||
}
|
||
/* 快路径:窗口命中(同 id、代数未变、落在窗口区间内)→ 零 I/O */
|
||
if ((s_rd_win_id == (int32_t)id) && (s_rd_win_gen == s_rd_gen[id]) &&
|
||
(idx >= s_rd_win_base) && (idx < s_rd_win_base + s_rd_win_cnt)) {
|
||
(void)memcpy(p_rec, s_rd_win + ((size_t)(idx - s_rd_win_base) * SYSLOG_REC_SIZE),
|
||
SYSLOG_REC_SIZE);
|
||
return 0;
|
||
}
|
||
if (vol_lock_wait(0u) == 0u) {
|
||
return -1; /* 卷被占用:本次读失败(非故障,下轮重读) */
|
||
}
|
||
/* 重新校验:等锁期间写者可能已推进(虽然 try 语义下不会阻塞等待) */
|
||
total = syslog_count(id);
|
||
if (idx >= total) {
|
||
vol_lock_release();
|
||
return -1;
|
||
}
|
||
base = (idx / SYSLOG_RD_WINDOW) * SYSLOG_RD_WINDOW; /* 窗口按 8 条对齐 */
|
||
want = total - base;
|
||
if (want > SYSLOG_RD_WINDOW) {
|
||
want = SYSLOG_RD_WINDOW;
|
||
}
|
||
fr = f_open(&f, s_paths[id], FA_READ);
|
||
if (fr != FR_OK) {
|
||
vol_lock_release();
|
||
return -1;
|
||
}
|
||
fr = f_lseek(&f, (FSIZE_t)base * SYSLOG_REC_SIZE);
|
||
if (fr == FR_OK) {
|
||
fr = f_read(&f, s_rd_win, (UINT)(want * SYSLOG_REC_SIZE), &br);
|
||
}
|
||
(void)f_close(&f);
|
||
vol_lock_release();
|
||
|
||
if ((fr != FR_OK) || (br != (UINT)(want * SYSLOG_REC_SIZE))) {
|
||
s_rd_win_cnt = 0u; /* 填充失败:作废窗口,避免半窗被当有效命中 */
|
||
s_rd_win_id = -1;
|
||
return -1;
|
||
}
|
||
s_rd_win_base = base;
|
||
s_rd_win_cnt = want;
|
||
s_rd_win_id = (int32_t)id;
|
||
s_rd_win_gen = s_rd_gen[id];
|
||
(void)memcpy(p_rec, s_rd_win + ((size_t)(idx - base) * SYSLOG_REC_SIZE), SYSLOG_REC_SIZE);
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:日志引擎初始化(内核启动前安全空操作)
|
||
* 入口参数:无
|
||
* 返回值: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) {
|
||
/* 运行期惰性创建消息队列:本函数仅由 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;
|
||
}
|
||
|
||
/* N-24(2026-10-15,报告 N-16 写侧):一批一批地排空——同一文件、最多
|
||
* SYSLOG_BATCH_MAX 条合并成一次 open/write/close(旧实现每条一轮
|
||
* open/write/close,每条日志 2~4 次 NAND 页读写)。
|
||
* 取数规则:preboot 数组按下标"看而不删",整批落盘成功才真正删除;
|
||
* 队列必须取出才能看下一条,故未落盘的尾部一律回投队列——
|
||
* **条目不丢**(旧实现 -2 卷忙会直接丢掉那一条)。 */
|
||
for (;;) {
|
||
syslog_entry_t e;
|
||
uint32_t n = 0u;
|
||
uint32_t n_pre = 0u;
|
||
uint32_t i;
|
||
uint32_t done;
|
||
int rc;
|
||
syslog_id_t bid = SYSLOG_RUN;
|
||
|
||
while (n < SYSLOG_BATCH_MAX) {
|
||
uint8_t from_pre = 0u;
|
||
|
||
if (n_pre < s_preboot_cnt) {
|
||
e = s_preboot[n_pre];
|
||
n_pre++;
|
||
from_pre = 1u;
|
||
} else if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) {
|
||
break; /* 队列空 */
|
||
}
|
||
if (n == 0u) {
|
||
bid = (syslog_id_t)e.id;
|
||
} else if ((syslog_id_t)e.id != bid) {
|
||
/* 换文件:本批先落盘,这条留给下一批(原处不动或回投队列) */
|
||
if (from_pre != 0u) {
|
||
n_pre--;
|
||
} else {
|
||
(void)osMessageQueuePut(s_queue, &e, 0u, 0u);
|
||
}
|
||
break;
|
||
}
|
||
s_batch_src[n] = e;
|
||
s_format_rec(bid, e.desc, s_batch + ((size_t)n * SYSLOG_REC_SIZE));
|
||
n++;
|
||
}
|
||
if (n == 0u) {
|
||
break; /* 无更多条目 */
|
||
}
|
||
|
||
rc = s_append_many(bid, s_batch, n);
|
||
done = n;
|
||
if (rc == -3) {
|
||
/* 剩余容量装不下一批:退回按条慢路径("满了 unlink 重建"只在
|
||
* 那里做,避免一批跨越多轮重建、语义说不清) */
|
||
done = 0u;
|
||
for (i = 0u; i < n; i++) {
|
||
int r1 = s_append(bid, s_batch_src[i].desc);
|
||
|
||
if (r1 == -2) {
|
||
break; /* 卷忙:尾部回投队列 */
|
||
}
|
||
done++;
|
||
if (r1 != 0) {
|
||
DBG_ERROR("drop entry id=%d", (int)bid);
|
||
break; /* 写失败:该条已丢,尾部回投 */
|
||
}
|
||
}
|
||
} else if (rc == -2) {
|
||
/* A 项整改(2026-10-24,主机单测照出):卷忙 = **整批回投队列**,
|
||
* 静默顺延——不计故障、不打日志(本函数每 2s 排空一次,一打就刷屏),
|
||
* 与上面 -3 慢路径遇 -2 的处置对齐。旧实现在这里没有分支:done 保持 n
|
||
* → 下面的回投循环空转 → 最多 8 条既不落盘也不回投,静默丢失。 */
|
||
done = 0u;
|
||
} else if (rc == -1) {
|
||
/* B 项整改(2026-10-24):旧文案写 "drop %lu entries",但本分支
|
||
* done = 0 → 整批其实**全部回投**、一条没丢(下轮重试),文案与行为
|
||
* 相反会把排障方向带错;改成如实描述,并加"正常→故障"节流(持续故障
|
||
* 时每 2s 一条会自己刷成新一轮刷屏,真丢另有下面 hold-over drop 兜底)。 */
|
||
done = 0u;
|
||
if (s_requeue_reported[bid] == 0u) {
|
||
s_requeue_reported[bid] = 1u;
|
||
DBG_ERROR("write fail, requeue %lu entries id=%d",
|
||
(unsigned long)n, (int)bid);
|
||
}
|
||
}
|
||
|
||
for (i = done; i < n; i++) {
|
||
if (osMessageQueuePut(s_queue, &s_batch_src[i], 0u, 0u) != osOK) {
|
||
DBG_ERROR("hold-over drop (queue full): id=%d", (int)s_batch_src[i].id);
|
||
}
|
||
}
|
||
if (rc == 0) {
|
||
s_requeue_reported[bid] = 0u; /* 恢复正常:允许下一次故障重新留痕 */
|
||
}
|
||
/* 本批消耗的 preboot 条目:已落盘的删除;未落盘的(-1/-2/-3 尾部)已在
|
||
* 上面转投队列,同样从数组删除——**由队列接手重投**,不是丢弃
|
||
* (A 项整改前 -2 走不到回投,这里删掉就等于吞掉) */
|
||
if (n_pre > 0u) {
|
||
memmove(&s_preboot[0], &s_preboot[n_pre],
|
||
sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - n_pre));
|
||
s_preboot_cnt = (uint8_t)(s_preboot_cnt - n_pre);
|
||
}
|
||
if (rc != 0) {
|
||
break; /* 本轮到此为止,下轮再试 */
|
||
}
|
||
}
|
||
/* B9:SOE 落盘挂本函数尾部——同一唯一写者(defaultTask)、同一卷就绪门
|
||
(上方 s_fs_ready 早退蕴含 /log 已建)、同一卷锁 try 语义;protectTask
|
||
只投递不写盘(动作路径零 NAND I/O,DoD 硬约束)。 */
|
||
soe_store_drain();
|
||
}
|
||
|
||
/*
|
||
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
|
||
* 入口参数:id - 日志 ID syslog_id_t
|
||
* desc - 描述串(UTF-8,静态存储期,超长截断) const char *
|
||
* 返回值:无
|
||
* 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串
|
||
* 函数说明:丢弃留痕按"正常→故障"节流(F 项整改 2026-10-24):每个通道只在
|
||
* 转入故障的那一次打一条,恢复(下一次成功收下)后复位。
|
||
* **丢弃语义本身没变**(队列满/数组满仍然是丢),变的只有留痕次数——
|
||
* 卷被 FTP 长期占用时 A 项修复让整批回投、队列常驻满,若这里不节流,
|
||
* 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。
|
||
* 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。
|
||
*/
|
||
void syslog_post(syslog_id_t id, const char *desc) {
|
||
syslog_entry_t e;
|
||
size_t dl;
|
||
|
||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||
return;
|
||
}
|
||
e.id = (uint8_t)id;
|
||
/* 投递即拷贝(含截断):待发条目不再引用调用方缓冲区,栈缓冲/临时格式化结果
|
||
* 都能安全投递(见上方 syslog_entry_t 注释的台架实证) */
|
||
dl = strlen(desc);
|
||
if (dl > SYSLOG_DESC_MAX) {
|
||
dl = SYSLOG_DESC_MAX;
|
||
}
|
||
memcpy(e.desc, desc, dl);
|
||
e.desc[dl] = '\0';
|
||
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
|
||
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
|
||
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
|
||
* 变的只是留痕次数)。下一次投递成功即复位。 */
|
||
if (s_post_qfull_reported == 0u) {
|
||
s_post_qfull_reported = 1u;
|
||
DBG_ERROR("queue full, drop: %s", desc);
|
||
}
|
||
} else {
|
||
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
|
||
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
|
||
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
|
||
* STR 可以忽略,别为它加 inline-suppr)。 */
|
||
s_post_qfull_reported = 0u;
|
||
}
|
||
} else {
|
||
uint8_t n = s_preboot_cnt;
|
||
if (n < SYSLOG_QUEUE_MAX) {
|
||
s_preboot[n] = e;
|
||
s_preboot_cnt = n + 1u;
|
||
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
|
||
} else if (s_post_pboot_full_reported == 0u) {
|
||
/* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 +
|
||
* 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早
|
||
* 发生、也更密集)。数组重新收下第一条即视为恢复。 */
|
||
s_post_pboot_full_reported = 1u;
|
||
DBG_ERROR("preboot full, drop: %s", desc);
|
||
} else {
|
||
/* 故障期间静默,条目照常丢弃 */
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:获取记录条数(= 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 - 时间戳输出缓冲(不可为 NULL) char *
|
||
* ts_len - p_ts 容量,**必须 > SYSLOG_TS_LEN**(即至少 20 字节) size_t
|
||
* p_desc - 描述输出缓冲(不可为 NULL) char *
|
||
* desc_len - p_desc 容量,**必须 > SYSLOG_DESC_MAX**(即至少 29 字节) size_t
|
||
* 返回值:0 - 成功;-1 - 越界/读失败/缓冲容量不足(不截断、不写入调用方缓冲) int
|
||
* 限定条件:容量不足一律拒绝而不是截断(C 项整改 2026-10-24;理由见函数体注释)。
|
||
* 记录里的描述字段本身已在写入侧截到 SYSLOG_DESC_MAX,所以按上面 minimum
|
||
* 给缓冲就一定装得下,读侧无需第二道截断。
|
||
*/
|
||
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++;
|
||
}
|
||
/* C 项整改(2026-10-24):这里原来还有一句 "if (dl >= desc_len) dl = desc_len
|
||
* - 1u"——**永不可达**:上面的守卫已把 desc_len <= SYSLOG_DESC_MAX 全部挡掉,
|
||
* 而 dl 由定长记录的内容决定、恒 <= SYSLOG_DESC_MAX。删掉它,容量要求改由
|
||
* 函数头与 sys_log.h 写死:**宁可整条拒绝,也不做静默截断**(UI 拿到截断串
|
||
* 会显示成一条被删短过的日志,比读不到更难查)。desc_len 的校验仍保留,
|
||
* 它是"调用方给够缓冲"这条契约的执行点,不是截断开关。 */
|
||
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, ...)
|
||
* 再落一条 run.log。保留理由是**多一条独立证据**(run.log 有 UI 记录页与 FTP
|
||
* 回读两个消费者),**不是持久性手段**:f_close 内部 sync_fs →
|
||
* disk_ioctl(CTRL_SYNC) 已经调 ftl_flush_cache() 把 wdev.log 的脏页刷进 NAND
|
||
* (nand_ftl.c 的 CTRL_SYNC 分支),所以"写另一个文件(不同 LPN)逼出前一脏页"
|
||
* 从来不是必要条件。
|
||
* ⚠ 2026-10-02 round 3~8 那次"复位后写入整段丢失"的根因是 dhara resume 锚错
|
||
* checkpoint 使 journal 整段回退(docs/架构设计/STM32F4-Base存储架构说明.md
|
||
* §5.4.1),不是这层缓存:单页缓存最多丢最后未跨页那笔(≤512 B),丢不出
|
||
* "整段"。真正的残余(无空闲/掉电前周期 flush)= 登记簿 N-22,2026-10-22
|
||
* 用户裁定暂不做。
|
||
* 顺序硬约束不变: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();
|
||
|
||
/* 第二条写:再落一条 run.log 作为独立证据;不是为了刷 NAND——上面的
|
||
* f_close 已经 CTRL_SYNC 落盘(机制见本函数头"第二条写"段)。
|
||
* 必须在释放卷锁之后调用(s_append 自行取锁,非递归锁不可重入)。 */
|
||
(void)s_append(SYSLOG_RUN, "wd stall declared");
|
||
|
||
return rc;
|
||
}
|