Files
STM32F4-Base/App/log/README.md
T
wandering1 f5902bcc21 docs(design): 运行/自检记录与 ADC 节拍两篇改为现状设计并去掉档名"说明"
两篇规划期文档改成对当前系统的描述,档名统一去掉"设计说明"的"说明"。

运行与自检记录:
- 旧档重写为现状版(10 节:概览 / 记录布局 / 写入路径 / 读取与呈现 /
  事件分类 / 控制台日志 / 双端差异 / 内存 / 异常恢复 / 自测要点),
  删掉需求表、调研摘要、模块清单动作列、实施步骤与开放问题。
- 按代码纠正:七类记录齐备(原文只写了两类)、自检描述串换成实际
  落盘的 28 条、待机阈值以 ui_tokens.h 为准、记录页已走 ui_w_records
  共享组件、容量满只删该文件而非整卷清空。
- 待机屏移出本篇:它不属于日志,归 UI 侧;内容页架构补"待机屏
  (覆盖层)"一节,产品需求 FR-LOG-3 拆成单纯的上电/重启分类,
  待机显示归口已有的 FR-UI-3。

ADC 采样节拍:
- §4.5 从 45 行压到 24 行,只留规则 / 违反的代价 / 为什么 / 兜底与
  档案指针;九路置换实测表、A/B 对照、探针计数等取证归
  ADC_Trap_Records.md 的 TRAP-ADC-01(那里已有完整档案)。
- adc1_gap 字段去掉"2026-10-21 起、此前固件为次数"的修订记录式括号。
- app_main.c 的注释改为指向 TRAP-ADC-01 取证、§4.5 设计口径。

引用同步:两个旧档名全仓替换(含代码注释、角色档、归档 id 表),
旧名残留 0;对 ADC 篇的 § 引用逐条复核落在真实章节上。

验证:48 个提交期钩子全通过,check_doc_links --sections-strict 通过。
2026-11-15 21:30:07 +08:00

119 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# App/log/ — 日志子系统
> 两类日志:**持久化运行日志**(`sys_log`,落 NAND)与**控制台日志**(`console_log`,走串口)。
> 两者都遵循**单写者**模型,避免多任务交错输出。
## 文件一览
| 文件 | 作用 |
|---|---|
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录,七类各一个 `/log` 文件(run / selftest / accid / alarm / start / oper / yk)|
| `sys_log_mcu.c` | `sys_log` 的 MCU 端后端实现(NAND + FatFs) |
| `console_log.c/h` | **控制台单写者日志**:多任务日志经队列汇入唯一写者,避免串口输出交错 |
| `soe_rec.c/h` | **SOE 记录纯编解码**:64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
| `soe_store.c/h` | **SOE 落盘胶水**:`/log/soe.log`,protectTask 投递(零文件系统)→ defaultTask 排空落盘 |
## 关键机制
### syslog(运行/自检记录)
- **唯一写者 = defaultTask**;其它任务只投递,不直接写盘
- **单记录定长 64 B**,循环写;待机页可显示记录
- **内核启动前的日志**走**无锁静态缓冲** `s_preboot[8]` 暂存(因为内核启动前不能建队列),
内核启动后由 `syslog_drain` 惰性创建真队列并排空
- ⚠️ 写盘必须校验 `f_write` 返回的**字节数**(该 FatFs 移植在簇分配失败时返回 `FR_OK` + 短写,
不报错误码);追加写必须 `FA_WRITE|FA_OPEN_APPEND`(`FA_OPEN_APPEND` 单独用**不含写位**)
- **清除记录 = 请求 / 落地分离**:非写者上下文(uiTask 等)只能 `syslog_request_clear(id)`
置一个标志(零盘触点、零卷锁),真正的 `f_unlink` 在 **defaultTask** 的 `syslog_drain()`
**尾部**执行——顺序也是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被
写回去、`syslog_count()` 不归零。结果读 `syslog_clear_state(id)`
(`NONE/OK/WAIT/FAIL`),事实复核读 `syslog_count()`。
落地函数 `syslog_clear_apply(id)` 是 MCU 后端专属(模拟器不实现 → 共享代码误调在
链接期就报错):卷锁一律 try,卷占用返 -1 且**不动原文件**;幂等只认 `FR_NO_FILE`,
`FR_NO_PATH` 如实返 -1 且**不做任何卷级动作**(不补目录、不重格式化)
- 删除的作用域 = 上表七个记录文件(`syslog_request_clear(id)` 逐 id 独立请求);取证文件
`wdev.log` 与 `soe.log` 各有各的属主与通道,不在本 API 作用域,想清它们要另开 API。
清除成功会往**操作记录**里落一条 `clear <文件>`——但清 `oper.log` 自己时**刻意不投**,
否则"我清除了记录"这条会把刚删空的文件又写成 1 条,直接砸掉「删除后 `syslog_count()`
归零」这条判据(`TC-SYSLOG-133b` 钉住)
- ⚠ 宿主后端(`simulator/sim/sim_syslog.c`)与 MCU 端**判据相同、时序不同**:模拟器单线程,
七类对应工作目录下七份宿主文件(`sim_accid.log` 等),`syslog_post_quiet()` 在本端
等价于 `syslog_post()`(宿主进程没有那条栈硬线,也没有队列与丢弃);
`syslog_request_clear()` 调用内就删完了,因此 `WAIT` 态在模拟器上永不出现
(同 `syslog_post` 在两端"有队列 / 直写无队列"的差异)
### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做,2026-10-28 五页已落地)
**决策一:记录类型就是 `syslog_id_t` 本身,一类记录 = `/log` 下一个严格 8.3 短名的文件。**
64B 定长记录体里**没有类型字段位**:`[0..18]` 时标 / `[19]` 空格 / `[20..29]` epoch /
`[30]` 空格 / `[31..58]` 描述 / 尾部补零,这个布局被 UI 记录页与主机解码工具按**固定偏移**
读了了很久(run.log / selftest.log 是既有文件,既有读者)。塞类型字节要么挪走 epoch
(上电/重启分类就断),要么挪走描述(屏上少字),且两端读者同批改——所以
"这条记录是什么类型"由**它在哪个文件里**回答。`TC-SYSLOG-136` 把这条钉成机器断言:
同一条内容在七类之间逐字节相同,记录体不编码类型。
**决策二:不动 `soe_type_t`(二进制 SOE 通道保持现有 8 型)。** 加 SOE 类型必须同步 UI 侧
`ui_w_soe.c` 的类型名表(那是另一位队友的写域),而 SOE 页与这五页本就是两套读者
(二进制解码器 vs 文本列表)。**别把新类型顺手加进 SOE**:那会同时造出
"同一个事件两个真源"。
| 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 |
|---|---|---|---|---|
| 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:188` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `191` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `183` | `picked` 上升沿(越限电平,早于/等于动作) |
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:582/610`、`App/task/default_task.c:266`、`App/log/sys_log_mcu.c:682`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:581/584/590/594/604/606/617` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
- **"哪些元件算跳闸"只有一个真源**:定值表 `g_prot_set_meta` 里的 `PS_*_TRIP`(枚举注释
即"0 告警 / 1 跳闸")+ 实例表 `k_pe[]` 的 `trip_ctl` 列。分类入口
`prot_elm_record_kind()`(`App/algo/prot_elm.c`)里**没有第二份元件名单**,
没有该投退项的五项(OV/UV/3U0/OF/UF)填 `PROT_ELM_NO_TRIP=0xFF` → 恒告警;
`TC-PROTELEM-015` 逐元钉住"逐项独立 / 改定值即改类别 / 无此项者不受别人定值影响"。
- **描述只用 ROM 常量或 `prot_elm_name()` 直传**:所有写入点零 `snprintf`、零拼接。
最长一条 20 字节 < `SYSLOG_DESC_MAX`(28);`TC-IEC104-030` 逐条量长度(超 28 会被
投递侧静默截断,屏上少几个字,没人会去数)。
- ⚠️ **两个投递口,按调用者的栈预算选**:`syslog_post()` 的两个"满态丢弃留痕"分支
内联着日志行缓冲(每处约 216 B 栈),因此 `syslog_post_quiet()` 给静态最坏链吃紧的
任务用(**protectTask、iec104Task 必须用它**),丢弃只累加 `syslog_post_dropped()`、
由唯一写者 defaultTask 在 `syslog_drain()` 里按差值补一条留痕——同 `soe_store_post`
当年那条 `872 → 1192 B FAIL` 的实证纪律。收/丢判据两个口完全一致。
- **读侧契约(给 task-11;2026-10-28 五页已按此接上)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len,
desc, desc_len)` 与运行/自检记录**同一枚 reader**,"最近 20 条"= 从 `count-20` 起读;
`count()==0` 就是空态,页面显示"无记录"。**不要**在共享代码里调 MCU 专属的
`syslog_clear_apply()`(模拟器不实现 → 链接期报错是故意的);清除一律走
`syslog_request_clear()` + `syslog_clear_state()`,并把 `WAIT` 当正常态看(模拟器端
永不出现 `WAIT`,见下条)。
### console_log(控制台单写者)
- 多任务的生产者 → **单写者消费者**,消除任务间日志行交错
- 设计详见 `docs/子系统设计/控制台单写者日志设计说明.md`
### soe(SOE 事件顺序记录,SOE 记录)
- **记录 = 64 B 定长二进制**(`soe_rec.h` 布局),落 `/log/soe.log`;
与 run.log 分文件——run.log 是文本记录、有 UI/主机读取方,二进制 SOE
混入会破坏读取器;分文件保持"定长 64B + 条数=fsize/64 + 满删重建 +
FTP 下载 + 主机解码"同一套先例
- **生产者 = protectTask**(元件动作/返回、软压板投退、采样闭锁/恢复六类
边沿):`soe_store_post()` 纯 RAM(取时标 + 组记录 + 队列/待发环),
**零 NAND I/O**;⚠️ **post 内禁 `DBG_*`**——会把 protectTask 静态最坏链
垫高一帧(实证 872→1192 B FAIL),丢弃留痕由 drain(defaultTask)差值上报
- **唯一写者 = defaultTask**:挂 `syslog_drain()` 尾部(继承其卷就绪门),
50 ms 节拍 → SOE 落盘延迟 ≤50 ms;写盘纪律全套照 syslog(卷锁 try /
`FA_WRITE|FA_OPEN_APPEND` / 校验字节数 / 满删重建 / 失败留痕去重),
**绝不把名字/参数类错误码归入卷损坏触发集**
- **时标口径**:sys_clock 秒域时基 → CP56 ms 域 = 秒×1000(亚秒恒 0,
不造假);墙钟未建 → IV 位如实置位;seq 仅上电生命周期内单调
(容量重建不重置,仅复位重置——主机解码按 boot 段分段断言)
- 主机侧:`test/host/soe/decode_soe.py` 解码 + 断言(校验和/seq 单调/
CP56↔epoch UTC 交叉);⚠️ 交叉校验用 `calendar.timegm`,不是本地时区的
`dt.timestamp()`(+08 机差 28800 s,首跑实证坑)
## 相关文档
- `docs/子系统设计/控制台单写者日志设计说明.md`
- `docs/子系统设计/运行与自检记录设计.md`
- FatFs 陷阱:`AGENTS.md`「平台约束」