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

31 KiB
Raw Blame History

运行/自检记录与待机显示设计说明

版本: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 / 模拟器)

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