Files
STM32F4-Base/docs/子系统设计/运行与自检记录设计说明.md
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

346 lines
31 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.
# 运行/自检记录与待机显示设计说明
> **版本**:v1.3(§4.4 新增"卷锁 fail-open 边沿补投自检记录"一条,道G task-40;v1.2 为 §4.3 随七类记录更新,v1.1 的原始设计口径不变)
> **日期**:2026-09-14(v1.2 更新 2026-10-26)
> **状态**:步骤 1–9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 @test 待上电回归(§8.2)
> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI设计/内容页架构.md`(如存在)· `docs/UI设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg)
## 1. 背景与需求
| # | 需求 | 说明 |
|---|------|------|
| R1 | 密码错误提示 | 用户密钥页输错密码时提示"密码输入错误",并允许重新输入(现状:仅清空重输、无提示) |
| R2 | 待机显示页 | 长时间无操作时默认显示的界面(参考 `mockups/待机显示页面.jpg`:6 路电流值 + 底部时钟);**只有按确认键才进入菜单** |
| R3 | 运行记录页 | 记录装置上电时间、重启时间(参考 `mockups/运行记录页.jpg`:标题 + 逐条"时间戳 + 事件" + 页脚分页) |
| R4 | 自检记录页 | 记录开机自检结果,有问题才记录(参考 `mockups/自检记录页.jpg`) |
| R5 | FTP 可读存储 | 记录以**纯文本文件**落盘,FTP(根目录 = FatFS 卷根)可直接下载读取 |
| R6 | 先调研后设计 | 网络调研存储方式选型 → 写入本设计文档 → 按文档实施 |
## 2. 现状分析
### 2.1 UI 框架(`Drivers/BSP/UI/`,MCU/模拟器共用)
- **内容页 = 描述符驱动**:`content_page_def_t`(标题/几何/行数据源/4 钩子),框架 `ui_content_page.c` 统一建屏、切屏、焦点、按键分发与绘制;页面模块零 LVGL 对象。
- **分页已内建**:`selectable=1` + `rows_per_page`(↑↓移选、窗口跟随)+ 页脚"第 X 页,共 Y 页"自动计算;动态行经 `refresh(row, name, value)` 逐行取数(栈缓冲 **32B/列**),行数经 `count_cb()`。
- **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。
- **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。
- **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。
- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。
### 2.2 存储与时间
| 项 | 现状 |
|---|------|
| 卷 | GD5F2GQ5UE SPI NAND **256MB**(2048 块 × 64 页 × 2KB),dhara FTL(磨损均衡/坏块管理),FatFS FAT32,`FF_USE_LFN=0` → **文件名必须 8.3** |
| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`(8.3);旧档 `/cfg/netcfg.bin` 已废弃——现固件不读不写也不删(仅可能残留在旧设备上) |
| 并发 | `FF_FS_REENTRANT=1` + 系统锁(`ffsystem.c` osMutex):单次 `f_*` 调用受保护,**多调用序列(open→seek→write→close)需单写者纪律** |
| 墙钟 | `sys_clock_get_str(buf,20)` → `"YYYY-MM-DD HH:MM:SS"`;`sys_clock_get()` → Unix 秒(失败 = `UINT32_MAX`);SD2506 RTC + timeSyncTask TCP 校时 |
| 挂载 | defaultTask:`f_mount(forced=1)`,空卷自动 `f_mkfs` FAT32,成功后置 `SYS_EVT_STORAGE_READY` |
### 2.3 现有自检检查点(v1 埋点位置)
| 检查点 | 位置 | 失败表现 |
|---|------|---------|
| RTC 读取/墙钟建立 | `app_main_init()`:`sd2506_init()` + `sys_clock_init()`(FreeRTOS 启动前) | 时间戳无效,后续日志无意义时间 |
| NAND 初始化 | `app_main_init()`:`gd5f2gq5ue_init()` 返回非 `GD5F_OK` | 存储不可用 |
| ADC 初始化 | `app_main_init()`:`tpafe5160_init()` 超时 | 采样不可用 |
| FatFS 挂载/建卷 | defaultTask:`f_mount`/`f_mkfs` 失败(置位 STORAGE_READY 前) | FTP/配置/记录全部不可写 |
| 网络链路 | net_task `net_stack_bring_up()`:PHY 等待 ≤3s 后仍 DISCONNECT | 组网可用但无链路 |
### 2.4 RAM/LVGL 池预算(依据 `docs/内存管理/RAM使用分析与优化方案.md`)
- FreeRTOS 堆真实余量 ≈ **11~12KB**;LVGL 池 16KB 静态,菜单常驻占用 ≈5KB。
- 本设计新增 RAM:**全部 < 1KB**(§7),不新增任务、不动任务栈。
## 3. 网络调研摘要(R6)
| 主题 | 结论 | 来源 |
|---|------|------|
| 日志行格式与时间戳 | syslog(RFC 5424):一行一条记录;TIMESTAMP 采用 **ISO 8601/RFC 3339**(`YYYY-MM-DDTHH:MM:SS[.frac][Z]`,机器可读、字典序可排序);MSG 用 UTF-8。本设计取空格变体 `YYYY-MM-DD HH:MM:SS`(与既有 `sys_clock_get_str` 输出一致),另附 epoch 秒字段供程序化解析 | RFC 5424 §6.2.3/§6.4(datatracker.ietf.org/doc/html/rfc5424) |
| 写入模型 | Linux printk:消息先进 **RAM 环形缓冲**,由**专用线程**异步刷持久存储;热路径禁止同步 I/O。本设计对应"RAM 待发队列 + defaultTask 单写者落盘" | kernel.org/doc/html/latest/core-api/printk-basics.html |
| FatFs 追加语义 | `FA_OPEN_APPEND`(= FA_WRITE\|FA_SEEKEND):打开即定位 EOF,文件不存在则创建;配合 `f_lseek(fp, FILE_SIZE)` 可取文件大小。本设计每条记录一次 `f_open→f_write(64B)→f_close` | 工程内 `Lib/FatFs/ff.h:399`、`ff.c:3871`(与官方文档一致) |
| NAND 写入代价 | dhara FTL 已做磨损均衡;FAT32 下每次小追加约消耗 1 个簇(4KB)。本场景**写频率极低**(每上电 1~N 条、故障才写),簇开销与磨损可接受;以文件容量上限约束增长 | `docs/架构设计/STM32F4-Base存储架构说明.md` §6.3 + GD5F 驱动参数 |
**选型结论**:纯文本、**定长 64B 记录、一行一条**(ISO 时间戳 + epoch + UTF-8 描述);追加写 + 容量上限截断;RAM 队列 + 单写者任务落盘。
不选变长行(需扫描计数/解析,掉电半行难界定);不选二进制格式(违背 R5 FTP 直读)。
## 4. 总体设计
### 4.1 模块与文件清单
| 文件 | 端 | 说明 |
|---|---|------|
| `App/log/sys_log.h` | 共享 | 公共 API + 记录格式常量 + 行拆分内联函数(无 OS 依赖) |
| `App/log/sys_log_mcu.c` | MCU | FatFS 后端:待发队列(启动前静态数组 + FreeRTOS 消息队列)、defaultTask 落盘、容量截断、上电/重启分类 |
| `sim/sim_syslog.c` | 模拟器 | 宿主文件后端(CWD 下与七类一一对应的 `sim_*.log` 七份),直接追加,`time()` 时间戳;**没有队列与丢弃**,`syslog_post_quiet()` 在本端等价于 `syslog_post()`(两端差异登记在 `docs/测试说明/模拟器差距清单.md` §2.2) |
| `Drivers/BSP/UI/ui_standby.h/.c` | 共享 | 空闲检测 + 待机屏(屏幕覆盖层:保存/还原前屏与焦点) |
| `Drivers/BSP/UI/ui_page_runlog.h/.c` | 共享 | 运行记录内容页描述符(动态行,数据源 = sys_log API) |
| `Drivers/BSP/UI/ui_page_selftest.h/.c` | 共享 | 自检记录内容页描述符(同上) |
| `App/app/app_main.c` | MCU | 埋点:RTC/NAND/ADC 失败 → `syslog_post(SYSLOG_SELFTEST, …)` |
| `Src/freertos.c`(defaultTask USER CODE) | MCU | 挂载失败埋点 + **上电/重启分类记录** + 队列排空落盘循环 |
| `App/task/net_task.c` | MCU | 埋点:链路超时 → `syslog_post(SYSLOG_SELFTEST, "通讯异常")` |
| `Drivers/BSP/UI/ui_page_password.c` | 共享 | R1:错误提示 + 重输 |
| `Drivers/BSP/UI/ui_content_page.h/.c`、`ui_menu.c` | 共享 | 新增 2 个内容页 ID/描述符表项/node 39·40 映射;键事件挂钩空闲通知 |
| `ui_cn_chars.txt` + `tools/font_gen/` | — | 补 10 字重生成字体(§6) |
| `MDK-ARM/*.uvprojx`、`simulator/CMakeLists.txt` | — | 新文件双端注册(new-source-file skill) |
| `simulator/main.c` | 模拟器 | `--logtest` / `--standby` / `--idle <s>` 无头自检与测试钩子 |
### 4.2 日志记录格式(定长 64B,一行一条)
```
偏移 长度 内容
[0..18] 19 "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体;墙钟无效时 = "1970-01-01 08:00:00")
[19] 1 ' '
[20..29] 10 epoch 秒 ASCII("%010u",机器解析/上电重启分类用;墙钟无效 = "0000000000")
[30] 1 ' '
[31..58] 28 描述 UTF-8(≤28B = 9 个汉字),不足补 '\0'
[59..63] 5 '\0' 填充
```
示例行:`2026-09-14 16:48:34 1789465714 装置上电记录`(后随 NUL 填充至 64B)。
**定长收益**:条数 = `filesize / 64`(O(1),免扫描);第 k 条偏移 = `k*64`(随机读,UI 翻页零解析成本);掉电半行被 `floor(size/64)` 自然排除。
**epoch 10 位约束**:2026 年 Unix 秒已达 10 位(2001-09-23 起),字段按 `%010u` 定宽;uint32 上限 4294967295(2106 年)内不溢出。
**描述 ≤28B 约束**:受框架动态行栈缓冲 `CP_ROW_BUF_LEN=32`(含 NUL)限制,不改框架。
### 4.3 文件与容量
**记录类型 = 日志 ID 本身,一类记录 = `/log` 下一个文件**(记录体内没有类型字段位,
理由与逐类判据、写入点清单见 `App/log/README.md`「五类记录与『记录类型 = 日志 ID』」——
本节只登记"有哪些文件、各多大"这一层,判据表**不在这里抄第二份**)。
| 文件 | 记录类型(= `syslog_id_t`) | 上限 | 满时策略 |
|---|------|------|---------|
| `/log/run.log` | `SYSLOG_RUN` 上电/重启与运行事件 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记;来源含**卷锁 fail-open 边沿留痕**,见 §4.4 最后一条) | **512 条(32KB)** | 同上 |
| `/log/accid.log` | `SYSLOG_ACCIDENT` 事故(跳闸型动作) | **512 条(32KB)** | 同上 |
| `/log/alarm.log` | `SYSLOG_ALARM` 保护告警 | **1024 条(64KB)** | 同上 |
| `/log/start.log` | `SYSLOG_START` 保护启动(越限电平) | **1024 条(64KB)** | 同上 |
| `/log/oper.log` | `SYSLOG_OPER` 操作(改动了装置状态的运维动作) | **512 条(32KB)** | 同上 |
| `/log/yk.log` | `SYSLOG_YK` 遥控(104 命令的选择/执行/拒绝) | **256 条(16KB)** | 同上 |
- 表内上限是**逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇,到顶才
删除重建;七类全满的名义峰值 = 4864 条 × 64 B = **304 KB**(NAND 2 GB,且这些都只在
事件边沿写,不构成容量或磨损瓶颈)。
- 告警/启动给到 1024 条:一次系统故障会连片触发多个元件,别让最能说明故障过程的两类
记录最先被截掉。
- `id` 只准**尾部追加**:枚举序 = 两端 `s_paths[]`/`s_caps[]` 的下标,中间插值会让
某类记录安静地写进别的文件(不报错、屏上照看照翻),机械防线是
`TC-SYSLOG-130`/`131`/`131b`(逐 id 验"哪类落哪个文件、各用各的上限")。
- 另有两类**不属于本表**的文件各有各的属主与通道:`/log/wdev.log`(停喂取证,
`sys_log_evidence()`,§6)、`/log/soe.log`(二进制 SOE,64 B 定长但**另一套
布局与读者**,见本节末尾与 `App/log/README.md` soe 小节)。
- 目录 `/log/` 首次落盘前 `f_mkdir`(已存在则忽略);文件名均**严格 8.3**
(`FF_USE_LFN=0`:主名 ≤8、扩展 ≤3、不以点开头),逐条机器核
`TC-SYSLOG-081`/`130c`。
- FTP 视角:卷根下 `LOG` 目录内上述记录文件均为**同一格式**的 64 B 定长文本记录,
可直接下载后按固定偏移解析(R5)。
- 记录**只增不改不删**;删除只有两条既有途径——满时重建,以及 UI「清除记录」
(`syslog_request_clear(id)` 请求 / defaultTask `syslog_drain()` 尾部落地,
语义与判据见 `App/log/README.md`,O3 已由此关闭)。
### 4.4 写路径(单写者,调研结论落地)
**待发条目存副本而非指针(B 方案整改,2026-10-24,台架实证驱动)**:`syslog_entry_t` 原为
`{ uint8_t id; const char *desc; }`——只存调用方指针,隐含「desc 必须活到落盘」的契约,
但该契约既没写进 `sys_log.h`、也没有任何机械检查。一次可控台架实验把它照了出来:临时在
testTask 挂投递钩子,复用**同一块缓冲**连投 12 条带序号记录,并与一次 8 MB FTP STOR(占住
卷锁)重叠——落盘的 12 条变成「1~5 + 第 12 条七份」,即所有滞留条目别名到该缓冲的最后一次
写入。危险不止于内容错:卷忙整批回投让条目最长滞留到卷空闲,生产者若投栈缓冲,函数返回后
就是悬垂读。现改为条目内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1u]`,`syslog_post` 投递时按
`SYSLOG_DESC_MAX` 截断拷贝并补终止符,调用方缓冲区可立即复用;`sys_log.h` 的参数说明同步
改写。RAM 代价(`scripts/ram_report.py` 同口径前后各跑一次):`ZI 131,500 → 131,852 B`
(+352 B,`RW_IRAM1` 57.4% → 57.7%)、`Code +60 B`、`RW` 不变;`RW_CCM` 静态不变,队列
存储多要的 192 B 取自 FreeRTOS 堆池,烧录后串口水位行 `[STACK] heap free=15624
min_ever=8464` 说明余量充足。回归由 **TC-SYSLOG-108**(投出去之后改写调用方缓冲,落盘必须
仍是投递时的原文)与 **TC-SYSLOG-108b**(超长在条目内钳位)钉住。**为什么主机侧此前照不出
来**:用例传的都是字符串字面量、静态存储期无限,正好绕开生命周期维度——要照出这类问题必须
专门构造可变缓冲。
```
任意上下文 syslog_post(id, desc)
├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数)
└─ 调度器已运行 → osMessageQueueSend(8 槽,满则丢弃+计数;日志不得阻塞业务)
defaultTask(唯一写者,挂载成功后):
1) f_mkdir("/log")(幂等)
2) 排空启动前数组 + 队列 → 按文件合并落盘(一批最多 8 条:一次
f_open FA_OPEN_APPEND → f_write(n×64B) → f_close,见下方「日志读写放大收口」说明)
3) 写"上电/重启"分类记录(§5.3,基于 run.log 末条 epoch)
4) 主循环:osMessageQueueGet(阻塞, 10s 超时) → 落盘;超时空转继续 LED 闪烁
```
- **日志读写放大收口(2026-10-15,写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写:
- **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;`f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会 `ftl_flush_cache()` 把 FTL 的单页写回缓存刷进 NAND(`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `CTRL_SYNC` 分支)。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**:"写完即关"的路径关文件即落盘,残余只到"单次写事务进行中掉电/复位"为止(`../任务规划/登记簿.md` 条目「写回缓存空闲 flush」:无空闲/掉电前周期 flush,2026-10-22 用户裁定暂不做)。
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**。
⚠ **2026-10-24 更正本条**:旧实现里 -2 只在**按条慢路径**被正确回投,**批量路径根本没有 -2 分支**(`done` 保持 `n` → 回投循环空转)→ 卷忙时一批(≤8 条)既不落盘也不回投、静默丢失,而本节长期写着"条目不丢"替它背书。现已补 `rc == -2` 分支整批回投(不计故障、不打日志、保序),由 `test/host/sys_log_mcu/` 的 TC-SYSLOG-035 组逐条钉住。**真机必验**:FTP 传输期间持续投日志,传输结束后条数 == 投递条数、顺序不乱。
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
- **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。
- **卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(2026-10-27 道G task-40)**:锁层只自增计数、
绝不产日志——唯一能落盘的通道(`/log/wdev.log` 等)自己也要取这把**非递归**卷锁,被卷忙
挡住时既写不出、也不许在失败分支里再取同一把锁(机理与四态语义的唯一出处是
`App/fs/vol_lock.h` 的「可观测性」段,本文不复述)。因此留痕由唯一写者在 `syslog_drain()`
尾部按**边沿**补投:判据是"计数比上一轮读到的大了",不是"计数大于零"(后者是电平,每轮一条
= 每轮一次 NAND 写,诊断自己变成负载);**一个 episode(= 一次上电)只投一条**,重新上电
复位 latch 后故障复发仍会再投(否则第二次开机起永久静默)。文本形如
`vollock failopen=3`(累计值就是证据,不带态号),落 `selftest.log`。
⚠ 三条约束(与 task-23「日志让路不计 `[ERR]`」同一根判据):① 不在失败调用点就地打日志;
② 不打 DBG 错级、不打 `[ERR]`——`[ERR]` 是"设备出故障"的唯一信号通道(`@test` 的 Forbidden
也依赖它),塞进可观测性事件就同时不再是故障、不再是禁令;③ 记录自带的时间戳是
**被观测到的时刻**(粒度 = 一轮 `syslog_drain()`),不是发生时刻,所以本模块刻意不再另造
`obs_first_ms`/`obs_last_ms` 字段去"看起来更精确"。主机判据:`TC-SYSLOG-137` 至
`TC-SYSLOG-137d`(首投一条并带累计值 / 同 boot 不再投 / 计数不变不投 / 重新上电可再投),
其中 `g_sys_err_count == 0` 就是"不打错级"的机器证据。
- **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义,FatFS 系统锁保护单次调用。
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
### 4.5 读路径(UI / 模拟器)
```c
uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */
int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len,
char *desc, size_t desc_len); /* 越界 / 读失败 / 缓冲容量不足 均返回 -1 */
```
- MCU:**8 条滑动读窗口**(日志读写放大收口,读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。
- 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。
- **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。
- **容量契约(2026-10-24 写死在 `App/log/sys_log.h`)**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,**不足一律 -1,既不静默截断也不写调用方一个字节**(描述在写入侧已截到 `SYSLOG_DESC_MAX`,按最小容量给就一定装得下,读侧不需要第二道截断)。旧实现里那句 `if (dl >= desc_len) dl = desc_len - 1` 由上面的守卫保证**永不可达**,已删除;它的存在还会误导调用方"给 28 字节缓冲能读到 27 字符"——实际是整条读不到。UI 侧现用 32 B 列缓冲,走允许路径、行为不变。钉住:TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098。
- 模拟器:宿主文件同语义(UI 页双端零差异)。
- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。
## 5. 功能设计
### 5.1 R1 密码错误提示(`ui_page_password.c`)
- ENTER 比对失败 → `s_err=1`:3 框清零、光标回首框,**数字框下方居中显示"密码输入错误"**(y≈104)。
- 错误态下按 ↑↓←→:**先清除提示**再正常执行该键(即"让用户重新输入");再按 ENTER = 重新比对。
- 比对成功路径不变(回调授权/关页)。布局常量与绘制沿用现有 culling 模式,零 LVGL 对象。
### 5.2 R2 待机显示页(`ui_standby.c/h`)
**进入条件**:UI 初始化完成后,**60s(默认,可配)内无任何按键** → 切待机屏。
- 空闲检测 = `lv_timer`(1s 周期,MCU/模拟器均经 `lv_timer_handler` 驱动,无 OS 依赖);
- 活动通知挂钩:`menu_key_cb`、`cp_key_cb` 每收到一个键调 `ui_standby_notify_activity()`(重置计时)。
**待机屏内容**(参考 mockup,全 ASCII 无需新字体字):
```
Ib = 0.00A ← 6 行电流值(v1 占位 0.00A,与 AC/DC 页现状一致;
Ic = 0.00A 待交流量标定管线落地后接 TPAFE5160 实时值)
Io = 0.00A
Ia2 = 0.00A
Ib2 = 0.00A
Ic2 = 0.00A
────────────────────── ← 分隔线(y≈132)
YYYY-MM-DD HH:MM:SS ← 底部实时时钟(每秒重绘,lv_timer 驱动 invalidate)
```
**键语义(用户定案)**:待机屏上**只有 ENTER(确认键)进入菜单**;其余按键忽略(同时重置空闲计时)。
- 实现为**屏幕覆盖层**:进入时保存当前活动屏 + 组内焦点对象,加载待机屏并聚焦其容器;ENTER 时还原前屏与焦点。
- 内容页打开期间超时 → 同样切待机(页面对象保留在底层,ENTER 返回后原样恢复,编辑态不丢)。
### 5.3 R3 运行记录(上电/重启分类)
defaultTask 挂载成功、排空待发队列**之后**写一条:
```
读 run.log 末条记录的 epoch 字段 E:
墙钟有效 且 (now - E) < 600s → "装置重启记录"(近期刚运行过 = 软复位/看门狗)
其余(无文件/首条/间隔≥10min/墙钟无效)→ "装置上电记录"
```
- 阈值 `SYSLOG_REBOOT_WINDOW_S = 600`:断电 <10min 会被误判为重启——无掉电检测硬件时该歧义不可消除,取工程折中并在此声明。
- 串口标记(供 @test):`runlog boot entry: 装置上电记录 / 装置重启记录`(`sys_log_mcu.c` `DBG_INFO`,与落盘描述串同源)。
### 5.4 R4 自检记录(v1 项表)
| 检查点 | 触发时机 | 描述串(落盘 + UI 显示同源) |
|---|------|------|
| RTC/墙钟建立失败 | app_main_init | `时钟芯片异常` |
| NAND 初始化失败 | app_main_init | `NAND异常` |
| ADC 初始化超时 | app_main_init | `ADC异常` |
| FatFS 挂载/建卷失败 | defaultTask(仅串口,存储不可写) | (无法落盘,串口 `[SELFTEST] fs: FAIL`) |
| 网络链路 3s 未建立 | net_task bring_up 后 | `通讯异常` |
- **只记故障**;全部通过不写记录(与 mockup"有问题进行记录"一致)。
- v1 不做"已复归/动作/标志字"(mockup 中为保护继电器专有语义)——列入开放问题 O2。
- UI 页空态:`count_cb` 返回 0 时以单条伪行 `无记录` 呈现(页脚"共 1 页",避免"共 0 页")。
## 6. UI 布局与字体
### 6.1 新增字符集(11 字)→ 重生成 `ui_font_cn_12.c`
| 用途 | 新字 |
|---|------|
| 密码错误提示"**密码输入错误**" | 输、入、错、误 |
| 运行记录"装置**上电**记录 / 装置**重**启记录" | 上、电、重 |
| 自检故障名"时**钟芯片异常**"等(异/常为公共后缀) | 芯、片、异、常 |
流程按 `font-regen` skill:改 `ui_cn_chars.txt` → extract_cn → make_font → diff。注意:**extract_cn 只扫 UI 源码**,故障描述串在非 UI 源码(sys_log_mcu.c/app_main.c/net_task.c)中,须手动补入字符集;其余文案(运行记录/自检记录/无记录/第 X 页共 Y 页)均已在字符集内;待机页全 ASCII。
### 6.2 页面几何(160×160,沿用框架常量体系)
| 页 | 参数 |
|---|------|
| 运行记录 / 自检记录 | `selectable=1, rows_per_page=8, top_y=24, pitch=14, band_h=13`(同 AC/DC 约定;**偏离 mockup**:实机照片无选中带,但框架 selectable 页选中行恒黑字、band_h=0 时黑字黑底不可见,故保留标准白带),`left_x=7`;**每条记录 = 2 行**:偶数行 = 时间戳(`YYYY-MM-DD HH:MM:SS`,19 ASCII ≈114px 可容)、奇数行 = 描述(≤9 汉字);↑↓按行移动(8 行/页 = 4 条/页),页脚自动分页 |
| 待机屏 | 6 数据行 y=28 起 pitch=18(label x≈24 / `=` x≈72 / 值 x≈96);分隔线 y≈132;时钟 y≈140 居中 |
| 密码页错误提示 | "密码输入错误" 居中,y≈104(数字框行 y=78..92 之下) |
## 7. 内存预算
| 项 | 大小 | 位置 |
|---|------|------|
| sys_log MCU:启动前待发数组 8×12B + 队列 8×12B + 状态 | ≈300B(静态)+ ≈150B(FreeRTOS 堆,消息队列) | .bss / heap_4 |
| ui_standby:待机屏 + 容器 LVGL 对象(常驻) | ≈300B | LVGL 池(16KB,当前占用 ≈5KB) |
| 记录页模块 | 0(描述符驱动,零 LVGL 对象;仅栈缓冲) | — |
| 字体 +10 字形 | ≈250B Flash | .rodata |
**结论:不新增任务、不动任务栈、FreeRTOS 堆增量 <200B,LVGL 池余量充足。**
## 8. 测试计划
### 8.1 模拟器(无头自动化)
| 用例 | 手段 |
|---|------|
| 日志引擎闭环 `--logtest` | post N 条 → count/get 回读断言 + 宿主文件字节级校验(64B 定长、ISO 时间戳、NUL 填充),0=PASS(仿 `--cfgtest`) |
| 待机屏渲染 `--standby` | 无头注键后直接调测试钩子强制进入待机 → 截图比对(布局/时钟行);窗口模式 `--idle <s>` 可人工验证真实超时路径 |
| 记录页像素回归 | `--keys` 导航至 node 39/40 → 截图 + 文本断言(时间戳行/描述行/页脚分页),纳入 ui-verify 门禁 |
| 密码错误提示 | `--keys` 输错密码序列 → 截图断言"密码输入错误"可见;再按方向键 → 提示消失 |
### 8.2 真机(@test 串口标记)
Required:`App init OK`、`Network Init`、`net init: OK`、**`runlog boot entry:`**(装置上电记录 / 装置重启记录)、**`UI standby: ready (idle=60s)`**;
Forbidden:`FAIL`/`ERROR`/`HardFault`。
人工目检:待机屏(静置 60s)、记录页翻页、FTP 下载 `/log/*.log` 文本可读性。
## 9. 风险与开放问题
| # | 项 | 策略 |
|---|----|------|
| O1 | 断电 <10min 的"上电/重启"歧义 | §5.3 阈值折中,文档声明;如需精确可后续加掉电标志(PVD) |
| O2 | 故障"已复归"/持续监测(mockup 语义) | v2:周期自检 + 活动故障状态机 + 恢复记录 |
| O3 | ~~"清除记录"菜单项~~ **已实施(2026-10-26 道F task-4)** | 数据面 = `syslog_request_clear(id)` + `syslog_clear_state(id)`,落地在 defaultTask(唯一写者)的 `syslog_drain()` 尾部;UI 侧页面归记录/维护页批次。细口径见 `App/log/README.md` |
| O4 | 待机页电流值接实时数据 | 待交流量标定管线(AC 页占位数据替换)落地后接入 `tpafe5160_snapshot()`(原子快照;旧 `tpafe5160_get_buf()` 已于 2026-10-10 删除,见 `AGENTS.md`) |
| — | FAT32 小追加簇开销 | §3 调研结论:写频率极低可接受;容量上限约束增长 |
| — | UI 读与落盘并发 | 只读既有内容 + EOF 追加 + FatFS 系统锁,无互斥需求(§4.4) |
## 10. 实施步骤(按序执行,每步双端构建 0/0)
> ✅ **步骤 1–9 已于 2026-09-14 全部完成**:Keil `@build` 0 错 0 警 + 模拟器 0 警告;步骤 8 = `--logtest` PASS(期间修复 ts 19 字符 / epoch 10 位两处格式 bug)+ ui-verify 五门禁全绿(池基线 5440/5520B、pwc 参考更新)+ 记录页/待机屏/密码错误提示截图目检通过;步骤 9 = 文档同步。
| # | 步骤 | 产物 |
|---|------|------|
| 步骤 1 | 字体补字 + 重生成 | `ui_cn_chars.txt`、`ui_font_cn_12.c` |
| 步骤 2 | 日志引擎:`sys_log.h`(共享 API/格式)+ `sys_log_mcu.c` + `sim/sim_syslog.c` | 双端可 post/count/get |
| 步骤 3 | 自检埋点 + defaultTask 落盘循环 + 上电/重启分类 | app_main/freertos/net_task 接线 |
| 步骤 4 | 密码错误提示 | ui_page_password.c |
| 步骤 5 | 待机屏 `ui_standby`(空闲检测 + 覆盖层)+ 键挂钩 | ui_menu/ui_content_page 各一行通知 |
| 步骤 6 | 记录页 ×2 + 内容页框架接线(枚举/描述符表/node 映射) | ui_page_runlog/selftest、ui_content_page.h/.c、ui_menu.c |
| 步骤 7 | 双端注册新文件(uvprojx + CMakeLists)+ 构建 0/0 | new-source-file skill |
| 步骤 8 | 模拟器无头回归(--logtest/--standby/截图断言)+ 真机 @test | 测试通过记录 |
| 步骤 9 | 文档同步(项目架构总览、AGENTS.md、本文档状态) | docs |