# 启动日志原子性复验流程(台架) > **这篇是什么**:验证"控制台整行独占 / 崩溃遗言原子性"这类**行级完整性**判据的固定动作与口径。 > 它补的是 `../../子系统设计/控制台单写者日志设计说明.md` 的**验收侧**:那边讲怎么修,这边讲怎么证明修好了、 > 以及**为什么"标记全中"不等于"日志没坏"**。 > 来源:2026-10-26 台架实测(缺陷立项 task-23、收口复验 task-30、工具与流程吸收 task-36)。 ## 1. 为什么子串标记照不出交错 `.dsh/skills/serial-boot-test/scripts/verify_log.py` 的 `--required` 是**大小写敏感子串**匹配。 交错缺陷的真实产物是这样的两行(原始捕获,未改写): ```text [PROT] uncalibrated -> protection [blocked (UA/UN/IA VALI2D not all s0et) 26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL... ``` 第二行**仍然包含**标记串 `Initializing NAND FTL` —— 于是旧口径把这份启动判成 PASS。 实测对照(同一份文件、同一台机器): | 口径 | 命令要点 | 对缺陷捕获的判定 | |---|---|---| | 旧(子串) | `--required 'Initializing NAND FTL;uncalibrated -> protection'` | **PASS(假绿)** | | 新(整行) | `--exact '\[...\]\[NAND_FTL\] Initializing NAND FTL\.\.\.'` | **FAIL**,并把缺行首的邻居原样打出来 | **结论**:任何"这一行可能丢行首 / 可能被插字节"的标记,必须用 `--exact` 整行锚定, 不得只用 `--required`。 ## 2. 校验器口径(`verify_log.py`) | 开关 | 语义 | 为什么这样默认 | |---|---|---| | `--exact 'REGEX'` | 剥掉采集器前缀(`[RX] ` / `[+Nms]`)后**整行** `^REGEX$` 必须命中;同时报告"含该式最长字面串但未整行命中"的邻居行 = 交错特征 | 单点插字节即失配;邻居不得静默 | | `…@@LEN=` 后缀 | **显式**钉该行字节长度 | 长度属于**某一份像**的属性;默认不钉,否则无辜改动就假红(实测同一份像 uiTask 峰值自己摆 72 B) | | `--allow-residual N` | 允许行首前 N 个杂散字节(复位打断移位寄存器的已知残留),**记录但不判失败** | 崩溃注入腿允许 ≤1 残留;写成失败就会把已知现象当回归 | | `--head-char C` | 命中行(去残留后)必须以 C 开头,如 `[` | 崩溃日志的行首形态本身就是判据 | | `--pair FIRST THEN` | 恰好一条 FIRST,且其后**只有一条** THEN(跨复位配对) | 崩溃遗言必须与下一次启动的 `reset cause:` 成对;出现两条 = 有未记录/重复复位 | | `--include-comments` | 不跳过 `#` 行 | 默认**跳过**:归档证据文件带 ASCII 溯源头,头里提到 `[ERR]` 不该触发 Forbidden(工具级假红防线) | | `--selftest` | 跑内置 **22 条**判别用例后退出,无需日志与硬件(含下表三计数分类器) | "不可能失败的 PASS 就是空跑"——用例里含交错、缺行首、残留 1/2、配对错序,以及**"旧子串口径在同一条缺陷行上判 PASS"** 这一条盲区证明 | 用法示例(一次仅复位启动的原子性判定)。四个行级开关**也已接进 `run_boot_test.py`**, 所以 /serial-boot-test 闭环里直接加 `--exact ... --pair ... --allow-residual 1 --head-char '['` 即可,无需单独跑本脚本: ```powershell & '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\verify_log.py ` --log-path ` --required 'App init OK;net init: OK' ` --exact '\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[PROT\] uncalibrated -> protection blocked \(UA/UN/IA VALID not all set\)' ` --exact '\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[NAND_FTL\] Initializing NAND FTL\.\.\.' ` --max-boot-count 1 --show-tail-lines 0 ``` **退出码与三计数(task-32 定稿)——判据必须自己说清「没启动」与「没采到」**: | 码 | 含义 | 触发条件 | 聚合口径 | |---|---|---|---| | 0 | PASS | 标记齐、Forbidden 零、锚点合法 | 一次好启动 | | 1 | **设备侧真有坏消息** | 缺 Required / 命中 Forbidden / 顺序或耗时超限 / 整行锚定失配 | 记一次设备故障 | | 2 | **测量没成立** | 日志读不到;或开了 `--max-boot-count` 却**零启动锚点**(采集器没开、日志被清、采集没从复位开始) | **不得**计入设备故障 | 三计数读法:`hits=0 + loose_neighbours=0 + 锚点=0` = 采集断了,先修测量,此时任何通过都无意义; `hits=0 + neighbours≥1` = 行被啃(真缺陷,正是 `--exact` 要抓的); `锚点≥1 + hits=0 + neighbours=0` 才允许当"功能缺失"看。 优先级铁律:**零锚点但同时有 Forbidden 命中或自检故障记录 → 仍判 1**,测量问题只在一边加 WARN ——绝不能把一次真崩溃降级成「没数据」(实测已按此口径验过:造一份"零锚点但含 `[ERR]` MEMFAULT"的日志, 工具仍判 FAIL 并另加一行 WARN,没有把崩溃改判成测量问题)。 `--selftest` 里钉了 6 条分类器用例;把分类器改成常量返回"不坏",自检立刻红(真跑过)。 ## 3. 固定动作(顺序即判据,别调) 1. **环境**:继承完整环境,只补 `DTU_KEIL_UV4` / `DTU_STLINK_CLI`,并把 `TEMP`/`TMP` 指到真实目录。 ⚠ 根因记账:本机注册表 **User 级 `TMP` 是未展开的字面量 `%USERPROFILE%\AppData\Local\Temp`** —— 残缺环境里 UV4 会在仓库根建出字面量目录 `%SystemDrive%/`,而 `task_worktree.py` 会因找不到 `git` 让 `keil_build.py` 报**假的"build 锁被占用"(rc=4)**。看到 rc=4 先 `lock status` 再判断,别默认是并发。 2. **出像**:像必须来自**已提交 commit + 干净工作树**。主树脏(别人在飞)时走仓外只读 detached worktree: `git worktree add --detach <_wt>/bench-- ` + `mklink /J \.venv
\.venv`; 建树后 `git -C status --short` 必须为空,全程只读、不 commit、不跑生成器。 3. **/keil-build**:0 错误 0 警告;随后**从 .hex 二进制里抠身份戳**(不是从 build_log 里读),与之后线上读到的 `fw build:` 逐字对上——这是"烧进去的就是这份像"的唯一硬证据。 4. **烧录启动**:`/serial-boot-test` 已保证 flash(`-Rst -Run`) → 采日志 → 回读(`-CmpFile`) 的次序(`-CmpFile` 会复位目标, 必须排最后)。 5. **N 次仅复位对照**(N≥3):每次 `clear`(记下 gen,证明窗口不重叠)→ `stlink_flash.py reset`(`-c SWD -Rst -Run`, **零重烧**)→ `wait-idle` → 取日志逐块存档。"保存的值重启后还在"用重烧去验是**无意义**的。 6. **逐块校验**:`--exact` 锚定受害行 + `--max-boot-count 1` + Forbidden 默认含 `[ERR]`; 崩溃注入腿才加 `--pair\ --allow-residual 1\ --head-char '['`。 7. **非空跑对照**:每换一版校验器,至少跑一次 `--selftest`,并把**缺陷发生时的原始捕获**再判一遍——必须 FAIL。 8. **收尾**:最后一次硬件动作是 `cmp`(排在全批日志之后);台账与证据按 `../测试数据/boot_log_data/README.md` 的口径归档。 ## 4. 三条容易踩的纪律 1. **故意复位的腿不得并入"连续仅复位不重烧"的计数**。崩溃注入腿(MemManage/栈溢出钩子)本身就要复位设备, 并进常规序列会被下一个人数锚点数判成异常。它必须单独跑、单独进台账、标题写明"本腿含故意复位"。 **同一条口径覆盖"前面带烧录的圈"**:烧录启动之后紧跟的那一次启动不属于"连续仅复位不重烧", 报数写成两句并列——「烧录启动 ×1(不进序列)+ 仅复位 ×N」;若那一圈连字节都没采到,则**两条独立理由**都排除它(没数据谈不上计数)。 2. **崩溃腿的判据是原子性,不是 `[ERR]` 为零**。内核故障钩子的输出**本身就以 `[ERR]` 开头** (`[ERR] MEMFAULT …` / `[ERR] STACK OVERFLOW task=…`),在同一条路径上要求 `[ERR]=0` 等于要求它别工作; 而**产品构建的正常启动根本不走 IPSR 路径**(多段裸发只在 `Src/freertos.c` 的两个内核钩子里), 所以"跑 5 次正常启动、[ERR] 零命中"**证不到 IPSR**——那是定义上的必然,是空跑。 IPSR 主张交给主机侧套件(假 USART + IPSR + 临界区深度水位);要硬件证据就得由代码道**提交触发器**, 判据写"崩溃遗言一整行、恰好一次、`dropped=0`",并按 `../测试数据/boot_log_data/README.md` §2「故障注入日志」 运行时放宽 `/serial-boot-test`(`--forbidden` 去掉 `[ERR]`、`--max-boot-count 2`)——**不改仓库配置**。 ## 5. 栈判据(task-32 定稿:三档分权,不再是"待定") **一句话裁定:静态最坏链是唯一拦提交项(`scripts/stack_report.py` 每任务 verdict);运行时 水位只出告警(`scripts/stack_gate.py`,超容差 = WARN);采集健康门不过则整轮作废(VOID, 既不红也不绿、也不得写进台账)。** | 档 | 判据 | 等级与权限 | 由谁跑 | |---|---|---|---| | 硬线 | `margin = cfg − max(静态最坏链, 运行时峰值) ≥ max(1024 B ISR, cfg×30%)` | **FAIL = 拦提交** | `scripts/stack_report.py`(看它退出码。**Lead 裁定 2026-10-27:提交时守这条的一直是 stack_report 本身;`stack_gate.py` 定为台架侧门禁、不接 pre-commit**——提交时既无设备也无当轮日志,挂上去只会「永远 SKIP」或「拿上一轮日志判这一轮的码」,后者是假凭证同族) | | 趋势 | 同镜像 ≥3 圈取最大值 vs 基线轮 ≥3 圈最大值,`Δ > tol(task)` | **WARN = 记录 + 归因,永不单独拦提交** | `scripts/stack_gate.py` | | 绝对底线 | 任一任务 `free_min < 25% × cfg` | **FAIL**(运行时唯一有 FAIL 权限的一条,不需要跨轮比较) | `scripts/stack_gate.py` | | 采集健康门 | 锚点 `reset cause:` 恰 1 次 / 身份戳存在且**同轮唯一** / 每块 ≥20 行 / 每块都有 `[STACK]` 行 / ≥3 圈(基线轮同样要过门,否则本轮只跳趋势比较) | **VOID,exit 2** | `scripts/stack_gate.py` | **容差是推导值,不是拍脑袋**:`tol(task) = 向上取整到 16 B 格的 max(128 B, 2 × 同镜像实测最大摆动)`。 实测来源:同一份像 4 圈仅复位里 uiTask 摆 72 B、adcTask 摆 80 B(task-30),2026-10-27 那份像 4 圈零摆动 (task-42)→ 最大摆动 80 B ⇒ **全任务统一 160 B**,登记在 `scripts/stack_tolerance.tsv`(含来源列)。 **谁能改这个数**:只有台架在「改栈配置 / 抬栈 / 新增 `DBG_*` 调用点」之后重测同镜像摆动才允许回填; **禁止为了让某一轮变绿而调大容差**——被这把尺考核的人不得当场决定尺子的松紧(与 §7 里 "Forbidden 令牌不得落在英文单词内部"同源:判据的松紧与写法都要由测量侧一次性定死)。 **身份戳与归因**:跨轮比较本来就是两份镜像,正常;但 Δ 只有在**附上两次构建之间的源码 diff** 时才可归因到某笔改动。台账行必须写**两次的 `fw build` 时间戳(含 `suite=`/`dstr=`)**, SHA 只当坐标(2026-10-26 那天 SHA 被 amend 挪了三轮)。同镜像比较里出现"零摆动"不代表判据失效, 只代表这次没噪声。 **为什么必须留 VOID 这一档**(设计输入=当天两次自纠):常驻 daemon 死掉后 `status` 会自动拉起 一个新 daemon 并"正常"应答,`rx_lines=0` 也照样返回成功 JSON;而 Forbidden 令牌 `Fault` 会命中 `de`+`fault` 制造假红。所以判据**不许依赖采集健康的自我感觉**:门不过 → exit 2 与 exit 1 分开, 按码聚合的人不会把"端口没开"记成"又崩了一次栈"。 **判据自己必须能红**:`& '.venv\Scripts\python.exe' scripts/stack_gate.py --selftest`(14 条,含 "72 B 实测摆动不算红"、"超容差只 WARN"、"单圈基线拒比"、"死采集器 VOID"、"同轮混两份身份戳 VOID"、 "绝对底线 FAIL"、"峰值取最坏一圈而非最后一圈")。**改完判据不跑这条,就等于没改。** ## 6. 锁与脚手架 - bench 锁是**会话级**,必须手工 acquire/release,且**用同一个名字**释放。 - 非登记任务名(如临时验收窗)走 `lock release bench --task `(**不带** `--bench-pass`)即可释放; `--bench-pass` 会先读 `_wt/.state/.json` 台账,没有就报"任务不存在",`--force` 也救不了。 为记一个 PASS 去**造假台账**会白占并发工作树名额——PASS 写进任务卡描述即可。 - ⚠ **脚手架 `*.json` 一律不得放 `_wt/.state/`**:那是 task-worktree 的**台账目录**,脚本把每个 `*.json` 当台账读,实测 `task_worktree.py list` 直接崩在 `KeyError: 'id'`。放 `_wt/scratch/`。 ## 7. 台架卫生清单(每一条都是实测踩过的,不是设想) 1. **每个台架窗口开头、以及每一圈复位之前**:`serial_tool.py open` + `status` 断言 `port.ready=true`,并**观察 `rx_lines` 在几秒内增长**。 实测:常驻 daemon 会死(今晚 pid 18020 跑了 156589 s 后没了),而 `status` 会**自动拉起一个新 daemon**,于是它一直"正常"地报 `ready=false / rx_lines=0` —— 端口状态**不会**跨小时存活,`/serial-boot-test` 开过也不算数。 2. **`serial_tool.py log / status / clear` 返回 JSON,不是纯文本**。按 `[RX]` 行首过滤纯文本会得到**恒 0 行**且看起来"跑成功了"。 3. **Forbidden 令牌不得落在英文单词内部**:裸 `Fault` 命中 `de`+`fault` → 打到 `[FTP] using factory default password` 与 `[STACK] default peak_used`(实测假红两次)。四个 Fault 的崩溃日志都以 `[ERR]` 开头,列 `[ERR]` 就够,要加就加 `MEMFAULT/BUSFAULT/USAGEFAULT/HardFault`。 4. **工具输出别混流**:非零退出时 stdout(结果)与 stderr(诊断)拼接后再 `JSON.parse` 必崩;解析只吃 stdout。 5. **给同事/lead 发消息时,正文里的引号用全角「」**:在 JS/TS 双引号字符串里放 ASCII `"` 会直接语法错,消息发不出去(本人一晚三次)。 6. **判据落点顺序**:先 grep 谁在消费这个码/字段(今晚的唯一消费者 `run_boot_test.py` 只对 `rc==0` 分支),再决定动不动语义。 ## 8. 关联 - 修法与设计口径:`../../子系统设计/控制台单写者日志设计说明.md`(O1 两实例与字节账、§4.4.1 崩溃路径验证设计) - 闭环与标记口径:`../流程与规范/自动化测试流程.md` - 证据归档与命名:`../测试数据/boot_log_data/README.md` - 复位语义与回读次序:`../测试工具/ST-LINK_CLI_复位使用说明.md` - 烧录 / 采集 / 校验脚本:`../../../.dsh/skills/serial-boot-test/SKILL.md`、`../../../.dsh/skills/stlink-flash/SKILL.md`、 `../../../.dsh/skills/serial-assistant/SKILL.md`