现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
15 KiB
启动日志原子性复验流程(台架)
这篇是什么:验证"控制台整行独占 / 崩溃遗言原子性"这类行级完整性判据的固定动作与口径。 它补的是
../../子系统设计/控制台单写者日志设计说明.md的验收侧:那边讲怎么修,这边讲怎么证明修好了、 以及为什么"标记全中"不等于"日志没坏"。 来源:2026-10-26 台架实测(缺陷立项 task-23、收口复验 task-30、工具与流程吸收 task-36)。
1. 为什么子串标记照不出交错
.dsh/skills/serial-boot-test/scripts/verify_log.py 的 --required 是大小写敏感子串匹配。
交错缺陷的真实产物是这样的两行(原始捕获,未改写):
[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] <ts> / [+Nms])后整行 ^REGEX$ 必须命中;同时报告"含该式最长字面串但未整行命中"的邻居行 = 交错特征 |
单点插字节即失配;邻居不得静默 |
…@@LEN=<n> 后缀 |
显式钉该行字节长度 | 长度属于某一份像的属性;默认不钉,否则无辜改动就假红(实测同一份像 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 '[' 即可,无需单独跑本脚本:
& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\verify_log.py `
--log-path <boot_log.txt> `
--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. 固定动作(顺序即判据,别调)
- 环境:继承完整环境,只补
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再判断,别默认是并发。 - 出像:像必须来自已提交 commit + 干净工作树。主树脏(别人在飞)时走仓外只读 detached worktree:
git worktree add --detach <_wt>/bench-<ID>-<sha> <sha>+mklink /J <wt>\.venv <main>\.venv; 建树后git -C <wt> status --short必须为空,全程只读、不 commit、不跑生成器。 - /keil-build:0 错误 0 警告;随后从 .hex 二进制里抠身份戳(不是从 build_log 里读),与之后线上读到的
fw build:逐字对上——这是"烧进去的就是这份像"的唯一硬证据。 - 烧录启动:
/serial-boot-test已保证 flash(-Rst -Run) → 采日志 → 回读(-CmpFile) 的次序(-CmpFile会复位目标, 必须排最后)。 - N 次仅复位对照(N≥3):每次
clear(记下 gen,证明窗口不重叠)→stlink_flash.py reset(-c SWD -Rst -Run, 零重烧)→wait-idle→ 取日志逐块存档。"保存的值重启后还在"用重烧去验是无意义的。 - 逐块校验:
--exact锚定受害行 +--max-boot-count 1+ Forbidden 默认含[ERR]; 崩溃注入腿才加--pair\ --allow-residual 1\ --head-char '['。 - 非空跑对照:每换一版校验器,至少跑一次
--selftest,并把缺陷发生时的原始捕获再判一遍——必须 FAIL。 - 收尾:最后一次硬件动作是
cmp(排在全批日志之后);台账与证据按../测试数据/boot_log_data/README.md的口径归档。
4. 三条容易踩的纪律
- 故意复位的腿不得并入"连续仅复位不重烧"的计数。崩溃注入腿(MemManage/栈溢出钩子)本身就要复位设备, 并进常规序列会被下一个人数锚点数判成异常。它必须单独跑、单独进台账、标题写明"本腿含故意复位"。 同一条口径覆盖"前面带烧录的圈":烧录启动之后紧跟的那一次启动不属于"连续仅复位不重烧", 报数写成两句并列——「烧录启动 ×1(不进序列)+ 仅复位 ×N」;若那一圈连字节都没采到,则两条独立理由都排除它(没数据谈不上计数)。
- 崩溃腿的判据是原子性,不是
[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 <NAME>(不带--bench-pass)即可释放;--bench-pass会先读_wt/.state/<NAME>.json台账,没有就报"任务不存在",--force也救不了。 为记一个 PASS 去造假台账会白占并发工作树名额——PASS 写进任务卡描述即可。 - ⚠ 脚手架
*.json一律不得放_wt/.state/:那是 task-worktree 的台账目录,脚本把每个*.json当台账读,实测task_worktree.py list直接崩在KeyError: 'id'。放_wt/scratch/。
7. 台架卫生清单(每一条都是实测踩过的,不是设想)
- 每个台架窗口开头、以及每一圈复位之前:
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开过也不算数。 serial_tool.py log / status / clear返回 JSON,不是纯文本。按[RX]行首过滤纯文本会得到恒 0 行且看起来"跑成功了"。- Forbidden 令牌不得落在英文单词内部:裸
Fault命中de+fault→ 打到[FTP] using factory default password与[STACK] default peak_used(实测假红两次)。四个 Fault 的崩溃日志都以[ERR]开头,列[ERR]就够,要加就加MEMFAULT/BUSFAULT/USAGEFAULT/HardFault。 - 工具输出别混流:非零退出时 stdout(结果)与 stderr(诊断)拼接后再
JSON.parse必崩;解析只吃 stdout。 - 给同事/lead 发消息时,正文里的引号用全角「」:在 JS/TS 双引号字符串里放 ASCII
"会直接语法错,消息发不出去(本人一晚三次)。 - 判据落点顺序:先 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