chore(工具,文档): 台架 rc 语义与导航硬闸用例、门禁环境诊断、护栏还债算法、mem-budget 跨树口径
四条道的收尾件同笔(全是 python 工具与文档,零固件源;各段归属按道署明)。 · **台架(bench-runner)**:verify_log.py 新增 classify_measurement() 与 measureme nt.* 字段,零行/零锚点 判 exit 2 并打 [MEASUREMENT-BROKEN],**但同帧有 Forbidden 命中或自检故障仍判 1**(专门造了「零锚点 + [ERR] MEMFAULT」的陷阱日志实测——崩溃不许被降级成「没数据」);--selftest 16 → 22 条。board_ui_nav.py 把两条执行页安全闸做成 7 条常驻用例接进提交钩子跑的快检档,作者把自己用例**证伪三轮**(测试强制上限、 清单清空时是崩不是判定、出厂默认被翻允许也照样绿)后三条危险改动逐条能红。台架卫生清单落 §7:每圈复位 前 open + 断言 ready + 看 rx 增长(今晚 daemon 死了 status 自愈把它遮住)、**Forbidden 令牌不得落在 英文单词内部**(裸 Fault 命中 default 两次假红)、改判定语义前先 grep 消费者。 · **环境(ui-param-writer)**:证伪卡片前提两处——identify 那个版本根本没有 tags 模块(RECORD 与磁盘逐项 一致,pre-commit 自己也从 identify.identify 取 ALL_TAGS,全仓零引用),真问题是 44 条 system 钩子里 42 条 entry 用裸 python → PATH 首位那个零第三方包解释器,今天没炸纯靠两件事同时成立。交付是**新增 entry 硬规则 + 修复死顺序与回滚并标注等静默窗口**,没有动共享环境(此刻重装会让别人的钩子在半途换解释器)。 · **门禁(gate-docs-fixer 已入库的 51 号钩子)**:本篇带上 host-unit-tests 的裁定句落点说明。 · **内存(comm-config-writer)**:mem-budget skill 补跨树测量身份口径(脚本按 __file__ 定位产物、什么才算 一次新测量、多链同涨同一数值的共享帧/幻影环归因法)+ 看门狗说明的指针行。 实测:verify_log --selftest 22/22、board_ui_nav --selftest-quick rc=0(硬闸 7/7)、 doc-links --all exit 0、 check_ids --strict exit 0 且绝对数 1,156 **未被本笔吃掉**;全部文件 LF / 无 BOM / U+FFFD=0 ;本笔零固件源, 随上一批权威构建验证过(0 错 0 警),故不再单独 @build。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: keil-build
|
||||
description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定、退出码判读、UV4 改写工程文件的入库陷阱。
|
||||
whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0 警告、或解读 UV4 构建日志时。
|
||||
description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定(PowerShell 与 bash/ops_bash 两种调用形态及各自的 exit 2 归属)、退出码判读、UV4 改写工程文件的入库陷阱。
|
||||
whenToUse: 需要编译 Keil MDK 工程、跑 `@build` 验证代码改动是否 0 错误 0 警告、解读 UV4 构建日志与退出码 2/4,或在 bash / `ops_bash` 里执行 `@build` 报语法错(syntax error near unexpected token)时。
|
||||
---
|
||||
|
||||
# keil-build — Keil 命令行编译(独立可运行 skill)
|
||||
@@ -10,16 +10,47 @@ whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0
|
||||
(纯标准库,任意 python3;拷整个 skill 目录到别的 Keil 项目即可用,不依赖仓库脚本)。
|
||||
|
||||
```powershell
|
||||
# cwd = 项目根目录
|
||||
# cwd = 项目根目录;PowerShell 形态(AGENTS.md 里的 `@build` 写法)
|
||||
& $PY ".dsh\skills\keil-build\scripts\keil_build.py"
|
||||
# 覆盖:--project X.uvprojx --uv4 UV4.exe --target 名字 --log 路径 --timeout 600
|
||||
```
|
||||
|
||||
## 调用形态:先认执行器是哪个 shell(2026-10-26 实测)
|
||||
|
||||
同一个 `@build`,**PowerShell 与 bash 是两种写法**;`ops_bash` / `run_code` 起的是
|
||||
`/usr/bin/bash -c`,**不认 PowerShell 形态**,照抄仓库 `AGENTS.md` 会直接语法错退出。
|
||||
|
||||
| 执行器 | 正确写法(cwd = 仓库根) |
|
||||
|---|---|
|
||||
| PowerShell | `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py` |
|
||||
| bash(`ops_bash`) | `'.venv/Scripts/python.exe' .dsh/skills/keil-build/scripts/keil_build.py` |
|
||||
|
||||
- bash 侧**去掉前导 `&`**(bash 里 `&` 是"丢后台",不是调用操作符)、**路径改正斜杠**
|
||||
(`\Scripts\python.exe` 的反斜杠在 bash 是转义,路径根本解析不出来)。
|
||||
- 实测(step 41→42):把 PowerShell 形态丢进 `ops_bash` 后台任务 → 任务**秒退**、
|
||||
`exit 2`,stderr 只有这一行:
|
||||
|
||||
```text
|
||||
/usr/bin/bash: -c: line 1: syntax error near unexpected token `&'
|
||||
```
|
||||
|
||||
- ⚠ **这个 2 不是脚本的环境错误 2**:脚本自己报环境错误会打出 `BuildEnvError` /
|
||||
中文诊断(UV4 找不到、工程找不到、档案解析失败),而语法错**连解释器都没启动**,
|
||||
`MDK-ARM/build_log.txt` 不会被改写。判据:stderr 含 `syntax error near unexpected token`
|
||||
= 写法问题 → 换 bash 形态重跑,**别去查 `DTU_KEIL_UV4`、别当环境错误排障**。
|
||||
- 后台跑(`run_in_background: true`)时要自己收码,且**别把脚本接进管道**:
|
||||
`'.venv/Scripts/python.exe' .dsh/skills/keil-build/scripts/keil_build.py; rc=$?; echo RC=$rc`。
|
||||
实测 `脚本 | tail -6; echo RC=$?` 里的 `$?` 是 `tail` 的退出码(恒 0),
|
||||
拿它当 PASS/FAIL 会永远"绿";`set -o pipefail` 能透出非零码,但尾巴上挂的 `grep`
|
||||
无匹配时同样报 1,也不可信。要看报错行就单独 grep 日志:
|
||||
`grep -a -E 'Error|error:|Warning' MDK-ARM/build_log.txt`。
|
||||
|
||||
解析顺序(命令行 > 项目档案 `.serialtest/config.json` > 环境 > 探测):
|
||||
工程 `uvprojx`(档案缺省时自动搜 cwd 下唯一 `*.uvprojx`);UV4 `uv4` >
|
||||
`$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH(**显式设了但路径无效 = 立即报错,不静默回退**)。
|
||||
退出码:`0`=PASS(0错0警) / `1`=构建失败(打印报错行)/ **`2`=环境错误**(UV4 找不到或
|
||||
路径无效、工程找不到、档案解析失败)/ `3`=用法错误 / `4`=build 锁被占用(并发 `-b` 不可行)。
|
||||
**收到 2 先确认它是脚本打的还是 shell 语法错打的**(见上节),再走"环境错误"排障。
|
||||
|
||||
> 2026-10-12 修正:旧实现把环境错误写成 `raise SystemExit("...")` → 实际退出码是 **1**,
|
||||
> 而用法错误由 argparse 抛 `SystemExit(2)` → 恰好占了"环境错误"的位置。故 `AGENTS.md` /
|
||||
@@ -53,7 +84,9 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
## 本仓库
|
||||
|
||||
**`@build` = 本 skill**(2026-10-10 起为仓库文档口径):在仓库根目录跑
|
||||
`& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`。
|
||||
`& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`——
|
||||
**bash / `ops_bash` 下要写成 `'.venv/Scripts/python.exe' .dsh/skills/keil-build/scripts/keil_build.py`**
|
||||
(原因见「调用形态」)。
|
||||
判定口径(与原 `MDK-ARM/build.bat` 同款;该文件已于 2026-10-10 删除,本 skill 即 `@build`):
|
||||
先删旧日志 → `-b -j0 -o` → 无 `error:` 且含 `0 Error(s), 0 Warning(s)`。
|
||||
本 skill 另有超时保护、UV4 多级解析链(含 PATH 兜底)、失败只打报错行。
|
||||
@@ -63,7 +96,7 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
|
||||
- **任何代码改动后必须先编译验证(0 错误 0 警告)再谈烧录/提交**——编译不过就进
|
||||
硬件闭环只会把编译错误伪装成运行问题。
|
||||
- **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"。
|
||||
- **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"(取退出码的管道陷阱见「调用形态」)。
|
||||
- **`MDK-ARM/*.uvprojx` 每次构建后被 UV4 自动改写,改动一律不入库**:UV4 会把工程
|
||||
XML 归一化成本机风格(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`)并把
|
||||
`PackID` 降到**本机装的 DFP 版本**(属降级——提交后会把别人的 Pack 版本一起改掉)。
|
||||
@@ -71,8 +104,10 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
`git checkout -- <uvprojx>` 丢弃;只有真的增删了源文件(grep 文件名计数变化)才提交。
|
||||
同目录的 `*.uvoptx` / `*.uvguix.*` 是 IDE 个人设置,已在 `.gitignore` 里,不会露出来。
|
||||
- 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。
|
||||
- **exit 2 先分清"真没设"还是"会话没继承"**(2026-10-10 实测):agent 的 shell 由
|
||||
常驻服务派生,服务启动**之后**设置的用户级环境变量不会自动进来。查
|
||||
- **exit 2 先分三种再动手**(2026-10-10 / 2026-10-26 实测):① **bash 语法错**——stderr 有
|
||||
`syntax error near unexpected token`,是把 PowerShell 形态丢进了 `ops_bash`,换写法即可;
|
||||
② **真没设** `DTU_KEIL_UV4`;③ **会话没继承**——agent 的 shell 由常驻服务派生,
|
||||
服务启动**之后**设置的用户级环境变量不会自动进来。查
|
||||
`[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')`,有值就
|
||||
`$env:DTU_KEIL_UV4 = <该值>` 注入本会话再跑,别急着让用户去设。
|
||||
- 编码类报错(`#870-D: invalid multibyte character sequence` 等)先怀疑源文件
|
||||
@@ -82,3 +117,6 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
|
||||
- 烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
|
||||
(其档案 `flash_cmd`/`readback_cmd` 即 stlink-flash 的入口命令)。
|
||||
- 收尾固定链(2026-10-26 实证):`@build` → `scripts/ram_baseline.py --update` →
|
||||
体积门禁盖戳 → 内存数字回填与 `--scan` → 复跑 `ci_local` 静态段,
|
||||
直到钩子全绿且 0 错误 0 警告(RAM/栈口径见 `mem-budget` skill)。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: mem-budget
|
||||
description: 嵌入式 RAM/栈预算核算工作流:Keil .map/.htm 静态报告(ram_report/stack_report)+ 串口运行时水位叠加 + DBG 日志成本模型(每条约 90B Flash/216B 栈)。"改任务/缓冲/栈之前必须核算"的固定动作与判读口径。
|
||||
whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报 L6406E 或 RAM 溢出、串口 [STACK] 水位余量低于底线、或需要回答"这个改动花多少 RAM/Flash"时。
|
||||
description: 嵌入式 RAM/栈预算核算工作流:Keil .map/.htm 静态报告(ram_report/stack_report)+ 串口运行时水位叠加 + DBG 日志成本模型(每条约 90B Flash/216B 栈)+ 跨树(worktree)测量的身份口径——脚本按 __file__ 定位产物、什么才算一次新测量、多条链同涨同一数值的共享帧/幻影环归因法。"改任务/缓冲/栈之前必须核算"与"这个数属于哪棵树、哪次构建"的固定动作和判读口径。
|
||||
whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报 L6406E 或 RAM 溢出、串口 [STACK] 水位余量低于底线、要回答"这个改动花多少 RAM/Flash";以及要复算或比对另一个 worktree 的栈数字、独立验证某个栈回归该记在哪一笔提交上、判断多次启动的栈峰值抖动算不算劣化时。
|
||||
---
|
||||
|
||||
# mem-budget — RAM/栈预算核算
|
||||
@@ -9,26 +9,123 @@ whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报
|
||||
## 固定动作(改前、改后各跑一次)
|
||||
|
||||
```powershell
|
||||
# cwd = 项目根;先 @build 保证 .htm/.map 是最新的
|
||||
# cwd = 项目根;先 @build 保证 .htm/.map 是最新的(产物按脚本自身位置定位,见下节)
|
||||
& '.venv\Scripts\python.exe' ".dsh\skills\mem-budget\scripts\mem_check.py"
|
||||
# 等价于分别跑:
|
||||
# scripts/ram_report.py (.map → RAM/Flash 分区与按文件占用表)
|
||||
# scripts/stack_report.py (.htm 静态调用图 → 每任务最坏链 vs 配置栈)
|
||||
# 报告落盘留档(改前/改后各一份,供逐帧差分):
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -o ci-artifacts/sr_before.md
|
||||
# 叠加运行时水位(有真机日志时):
|
||||
& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log
|
||||
```
|
||||
|
||||
退出码:0=全部通过 / 1=有任务不达标 / 2=解析错误(.htm/.map 缺失先重跑构建)。
|
||||
别整篇读输出,抓关键行:`grep -aE 'iec104|margin|FAIL|ISR|Rule' ci-artifacts/sr_before.md`。
|
||||
|
||||
**「改前 / 改后」必须是两次不同的 Linker run**(两个不同的产物身份):同一份 .htm/.map 上跑两遍
|
||||
只是把同一个数字复述两遍,不构成"改后已复验"。
|
||||
|
||||
## 测量身份口径(先钉树与时刻,再看数字)
|
||||
|
||||
### 1. 脚本按 `__file__` 定位产物,不按 cwd —— 跨树必须换脚本
|
||||
|
||||
```python
|
||||
# scripts/stack_report.py:36-38
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
HTM_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.htm"
|
||||
FREERTOS_C = REPO_ROOT / "Src" / "freertos.c" # 配置栈来源,无命令行开关
|
||||
# scripts/ram_report.py:16-18 同样用 dirname(dirname(abspath(__file__))) 定位 .map
|
||||
```
|
||||
|
||||
⇒ `cd 进别的 worktree 再跑主仓的脚本` 量的仍是**主仓**那份 .htm/.map,数字不可比。
|
||||
实测代价:这样一次"独立复算"量到主仓的 1,544 B / margin 1,016 B FAIL,据此写下的 +80 B 归因
|
||||
被队友的逐树逐帧差分表推翻——目标 worktree 的真数是 1,464 B / margin 1,096 B / PASS。
|
||||
|
||||
正确做法:**量哪棵树就跑那棵树自己的脚本**(该树必须已在这棵树上构建过、有自己的 .htm/.map;
|
||||
开树与合并的生命周期见 `task-worktree` skill):
|
||||
|
||||
```bash
|
||||
'.venv/Scripts/python.exe' "<目标树>/scripts/stack_report.py"
|
||||
'.venv/Scripts/python.exe' "<目标树>/scripts/ram_report.py"
|
||||
```
|
||||
|
||||
⚠ 只给主仓脚本传 `-i <另一棵树的 .htm>`(或位置参数传另一棵树的 .map)**不够**:
|
||||
`stack_report.py` 的配置栈仍从**脚本所在那棵树**的 `Src/freertos.c` 读(`FREERTOS_C` 写死自 `__file__`,
|
||||
没有开关),两树栈配置一旦不同就是"半棵树的混合数字"。要整支脚本换树跑。
|
||||
|
||||
自检(每次复算都做):报告头两行 `- Source: ...` 与 `- Linker run: ...` 就是你实际量的树和时刻。
|
||||
`Source` 若只打印一个裸文件名(不带 `MDK-ARM/...` 相对路径),说明 `-i` 指到了脚本 `REPO_ROOT` 之外的产物。
|
||||
|
||||
### 2. 重跑报告脚本**不算新测量**(复述 ≠ 复验)
|
||||
|
||||
`stack_report.py` / `ram_report.py` 只是解析盘上那一份 .htm/.map;产物没变,再跑一次数字必然不变。
|
||||
那只能叫"复述",**不能写成"我复验过"**。要拿到新数字必须先有一次**新的 Linker run**(增量构建也算),
|
||||
并把新时刻与产物来源一并写进记录。
|
||||
|
||||
⚠ `ram_report.py` 正文里的 `> Generated: …` 是**脚本运行时刻**,重跑就变——它不是链接时刻,
|
||||
不能拿来当"这是新测量"的凭证。
|
||||
|
||||
### 3. 钉身份的取数法(.htm/.map 按约定不入库 → 把时刻写进正文/台账)
|
||||
|
||||
```bash
|
||||
grep -aE 'Last Updated|Maximum Stack Usage' MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm | head -2
|
||||
# → "ARM Linker, 5060960: Last Updated: Mon Oct 26 22:52:12 2026"
|
||||
```
|
||||
|
||||
- .htm 里唯一的链接时刻锚点是 **`Last Updated:`**;`Linker run` 只是 `stack_report.py` 自己输出里的标签,
|
||||
`Date of Link`、`Image component` **不在 .htm 里**(实测按它们 grep 全空,白跑一轮)。
|
||||
- 取不到该字段时退回产物 mtime;同一笔链接里 .htm 与 .map 的 mtime 只差 ~0.1 s,可据此确认两份产物同源。
|
||||
再配 `git log -1 --format=%H` 与该树 `git status` 的未跟踪文件数。
|
||||
- 记录最小格式:**`tree/commit + Linker run 时刻 + 产物来源(本次构建 / 盘上既有)`**。
|
||||
本仓 .htm/.map 不入库,不钉时刻就等于把数字和树脱钩,下一个读数字的人无从判断它描述哪棵树。
|
||||
- 归属还能机械证明时顺手做掉:`git log --name-only <那笔>..HEAD -- "*.c" "*.h"` 逐行筛(排除 `.uvprojx`)。
|
||||
若那笔之后没有任何提交再碰固件源文件,产物描述的就是**在库那棵树**,不是提交前快照。
|
||||
- 同族坑:`git show :path` / `git show HEAD:path` 读的是索引/HEAD,**不是工作区**。
|
||||
|
||||
## 静态最坏链回归的归因(同涨同一数值 = 共享帧,不是调用方)
|
||||
|
||||
- **多条任务链分毫不差同涨/同降同一个数值**(实测 default/net/iec104 三条 +80 B;断环后九条链各 −80 B)
|
||||
⇒ 加深在这些任务**共用的被调帧 / 共享尾段**上,不在任何一个调用方。别逐个调用方翻,
|
||||
也**别先猜"新增了大局部缓冲"**——实测猜错过一次(`con_ring_try` 只有 24 B 帧、没有数组)。
|
||||
- 逐棵树逐帧比 frame size,不要只比总数:
|
||||
- `stack_report.py` 输出的「Frame-size ranking (own frame)」表 + 每任务最坏链文本,锁定**链尾公共段**
|
||||
(实测落在 `dbg_console_write_line → con_append → dbg_console_drain`,据此把账记到上一笔提交上);
|
||||
- 直接在 .htm 里找环:`grep -ac 'In Cycle' MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm`(实测 17 处),
|
||||
形态为 `Max Depth = 280 + In Cycle` 与 `Call Chain = Error_Handler => dbg_console_write_line (Cycle)`。
|
||||
`+ In Cycle` 表示 armlink 把**环上所有帧**计入含该环的每一条任务链——**链接器幻影环**能在无人改栈配置时让全仓链齐涨。
|
||||
- 实测样本(无人改栈配置、全仓链却齐涨的那种):taskEXIT_CRITICAL ⇒ vPortExitCritical ⇒ configASSERT ⇒
|
||||
vApplicationAssertFailed ⇒ dbg_console_write_raw ⇒ 回到 console_log 内部 ⇒ 再进临界区;
|
||||
它使 `vApplicationAssertFailed` 的 Max Depth 由 ~40 B 变 128 B + In Cycle,差 88 B ≈ 三条链同涨 80 B 的来源。
|
||||
断环方向:内核临界区改 PRIMASK 保存/恢复(保存量放局部变量)→ 零调用帧、不成环;
|
||||
断环的**图证**(该帧从「136 B + In Cycle」变成「8 B、不再 In Cycle」)比数字更有说服力。
|
||||
- `Maximum Stack Usage = N bytes + Unknown(Cycles, Untraceable Function Pointers)` 里的 `+ Unknown(...)`
|
||||
说明静态图对环与函数指针不可完全解析,静态数是估计值,别当精确值上报。
|
||||
- 不采信任何口头结论:用**目标树的脚本 + `.venv` 解释器**自己复算;复算同样先按上节钉身份。
|
||||
- 处置后的下一步:抬栈(`Src/freertos.c` 的 `USER CODE` 区,属 Ask-first 动作)或收紧共享缓冲 / 断环,
|
||||
**改前改后各一次** `stack_report.py` + `ram_report.py`,且"改后"那次必须落在一次新的 Linker run 上;
|
||||
抬栈后跑 `gen_task_table.py` 同步任务表。
|
||||
|
||||
## 判读口径
|
||||
|
||||
- **静态**:`stack_report.py` 的 Max Depth(最坏调用链累计)vs `Src/freertos.c` 配置栈;
|
||||
默认要求余量 ≥30%(`--min-free-pct`),ISR 另留 `--isr-margin`(默认 1024B)。
|
||||
判定式 `margin = cfg - max(static_max_depth, runtime_peak)` 须 ≥ `max(isr_margin, 30% of cfg)`。
|
||||
- **运行时**:开机日志 `[STACK] <task> peak_used=…/… free_min=…` 与静态值对照——
|
||||
静态是上界、运行时是下界证据,**两者都要看**;余量 <1KB 底线要上报。
|
||||
**静态最坏链是上界(硬线)、运行时峰值是下界证据(趋势)**,两者都要看;余量 <1KB 底线要上报。
|
||||
- **RAM 总量**:`ram_report.py` 对 Grand Totals 与 `docs/内存管理/` 预算表核对;
|
||||
链接错 `L6406E` = 已经溢出,用按文件表找大头。
|
||||
- 任务表一致性由 pre-commit `task-table` 钩子保证(改栈后 `gen_task_table.py` 同步 AGENTS.md)。
|
||||
- 任务表一致性由 pre-commit `task-table` 钩子保证;`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%;
|
||||
`mem-numbers` 钩子核文档里的内存数字——数字与树/时刻脱钩时就是这两条钩子会响。
|
||||
|
||||
## 运行时峰值抖动与"算不算劣化"
|
||||
|
||||
- 同一份镜像连续启动 5 次,`uiTask` 峰值实测在 4116~4188 之间摆 **72 B** ⇒ 跨启动本身就有几十 B 抖动,
|
||||
"相对上一轮 +100 B"**不可归因**于被测改动。判据写成"栈水位不劣于上一轮"必然假红。
|
||||
- 可执行修法(二选一,别都留着):
|
||||
1. 同一份像**≥3 次启动取最大值**互比,并加容差(**±128 B 量级**);
|
||||
2. 硬线改用 `stack_report.py` 的**静态最坏链**,运行时峰值降级为趋势参考。
|
||||
- 判据一改要同步**所有落点**:判据正文、跑判据的脚本、记录判据的文档三处一起改,只改一处等于各说各话。
|
||||
|
||||
## 成本模型(实测,估算用)
|
||||
|
||||
@@ -42,8 +139,18 @@ whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报
|
||||
- **自研代码禁 C 库堆**(MicroLIB 堆仅 512B):只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆),
|
||||
模拟器 CRT 堆照不出 MCU 堆分配失败。
|
||||
|
||||
## 验证(这轮测量算数了吗)
|
||||
|
||||
1. 报告头 `- Source:` 的相对路径落在**我要量的那棵树**下(不是裸文件名,更不是别的树);
|
||||
2. `- Linker run:`(或 .htm 的 `Last Updated:` / mtime)与本轮那次构建对得上,且**不同于**上一轮那次的时刻
|
||||
——相同就是复述,回去先做一次构建再比;
|
||||
3. 树身份已按 `tree/commit + 时刻 + 产物来源` 写进提交正文/台账,而不是只留在终端输出里;
|
||||
4. 结论里互比的数字必须来自**同一棵树同一次链接**;跨树比较要各自给出树与时刻;
|
||||
5. 涉及劣化判定时注明依据的是静态最坏链(硬线)还是运行时峰值(趋势),并已计入跨启动抖动。
|
||||
|
||||
## 其它项目怎么套
|
||||
|
||||
模式通用:链接器 map + 调用图 htm 是**免硬件**的栈可观测源;固定"改前改后各跑一次
|
||||
报告 + 运行时水位叠加 + 余量阈值"三件套。本仓库两个报告脚本按 Keil 输出格式解析,
|
||||
换工具链(GCC newlib + `-fstack-usage`)需替换解析层,工作流不变。
|
||||
报告 + 运行时水位叠加 + 余量阈值"三件套,**外加一条身份口径——脚本按自身位置定位产物,
|
||||
跨树比的是"两棵树各自的脚本 + 各自的链接时刻",重跑同一份产物永远只是复述**。
|
||||
本仓库两个报告脚本按 Keil 输出格式解析,换工具链(GCC newlib + `-fstack-usage`)需替换解析层,工作流不变。
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
[--require-order 'App init OK>Network Init>net init: OK']
|
||||
[--done-marker 'net init: OK'] [--max-boot-ms 8000] [--json]
|
||||
|
||||
退出码(与旧版一致):
|
||||
退出码(2026-10-27 task-32 起把 2 讲清楚;@test 那层的码不变):
|
||||
0 = PASS(所有 Required 标记找到,Forbidden 零命中,汇总行要求满足)
|
||||
1 = FAIL(缺 Required / 命中 Forbidden / 汇总行要求不满足)
|
||||
2 = ERROR(日志文件不存在/不可读)
|
||||
1 = FAIL(**设备侧真有坏消息**:缺 Required / 命中 Forbidden / 汇总行不满足 / 顺序或耗时超限)
|
||||
2 = **测量没成立**(日志不存在或不可读;或开了 --max-boot-count 却零启动锚点 = 采集器没开、
|
||||
日志被清、或采集没从复位开始)——这**不是**一次设备故障,按码聚合的脚本必须把它和 1
|
||||
分开算,否则"采集断了"会被统计成"又崩了一次"。verdict 字段给 MEASUREMENT_BROKEN,
|
||||
JSON 里 measurement.* 带三计数明细(payload_lines / boot_anchor_count / exact_hits /
|
||||
loose_neighbours)。注意优先级:**零锚点但同时有 Forbidden 命中或自检故障记录时仍判 1**,
|
||||
测量问题最多在旁边加一行 WARN——绝不能把真崩溃降级成"没数据"。
|
||||
|
||||
默认 Required 标记对应 STM32F4-Base 启动序列(见 App/app/app_main.c + App/task/net_task.c),
|
||||
按测试用例覆盖,例如:
|
||||
@@ -224,6 +229,31 @@ def _reset_cause_of(line, marker):
|
||||
return tail[:j] if j > 0 else tail
|
||||
|
||||
|
||||
# --- measurement-vs-device classification (task-32; bench-runner night rule) ---
|
||||
# Three counters decide which kind of "nothing found" we are looking at, and they must not
|
||||
# be conflated: an all-zero read is a BROKEN MEASUREMENT, not a healthy device.
|
||||
# hits=0, loose_neighbours=0, boot anchors=0 -> measurement broke (wrong file, closed
|
||||
# serial port, dead collector). Nothing was observed -> no verdict is meaningful.
|
||||
# hits=0, loose_neighbours>=1 -> the line exists but is damaged (head
|
||||
# eaten / bytes spliced in) -> that is a real device-side defect, --exact's purpose.
|
||||
# hits=0, loose_neighbours=0, anchors>=1 -> the boot DID happen; the marker is
|
||||
# genuinely absent -> allowed to read as a functional miss.
|
||||
# Reported as its own exit code (2, the existing "measurement/environment did not happen"
|
||||
# family) so aggregation by exit code never counts a dead collector as a device failure.
|
||||
def classify_measurement(n_lines, boot_count, boot_check_on, exact_checked, hits, neighbours):
|
||||
"""(broken, reason). Only judges when a boot anchor check was actually requested."""
|
||||
if not boot_check_on:
|
||||
return False, "anchor check off (max_boot_count=0) -> cannot classify"
|
||||
if n_lines == 0:
|
||||
return True, "zero payload lines (collector closed / log truncated / wrong file)"
|
||||
if boot_count == 0:
|
||||
return True, ("log readable (%d line(s)) but ZERO boot anchor -> capture did not "
|
||||
"start at a reset" % n_lines)
|
||||
if exact_checked and hits == 0 and neighbours == 0:
|
||||
return False, "anchors present: exact markers genuinely absent (functional miss, not measurement)"
|
||||
return False, "measurement ok (hits=%d neighbours=%d anchors=%d)" % (hits, neighbours, boot_count)
|
||||
|
||||
|
||||
def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
show_tail=40, out=print, suite_summary="",
|
||||
selftest_pass=False, selftest_log="",
|
||||
@@ -519,6 +549,39 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
for ln in lines[-show_tail:]:
|
||||
out(ln)
|
||||
|
||||
# ---- measurement vs device (task-32) ----
|
||||
n_exact_checked = len(exact or [])
|
||||
n_exact_hits = sum(1 for e in (res.get("exact_ok") or []) if "hits=0" not in e["detail"])
|
||||
n_neighbours = 0
|
||||
for e in (res.get("exact_ok") or []) + (res.get("exact_bad") or []):
|
||||
m = re.search(r"loose_neighbours=(\d+)", e.get("detail", ""))
|
||||
if m:
|
||||
n_neighbours += int(m.group(1))
|
||||
b_on = max_boot_count > 0
|
||||
b_cnt = (res.get("boot") or {}).get("count", 0)
|
||||
broken, m_reason = classify_measurement(len(payloads), b_cnt, b_on,
|
||||
n_exact_checked > 0, n_exact_hits, n_neighbours)
|
||||
res["measurement"] = {"broken": broken, "reason": m_reason, "payload_lines": len(payloads),
|
||||
"boot_anchor_count": b_cnt, "anchor_check_on": b_on,
|
||||
"exact_checked": n_exact_checked, "exact_hits": n_exact_hits,
|
||||
"loose_neighbours": n_neighbours}
|
||||
# A broken capture must never mask real device-side bad news: if the log carries
|
||||
# Forbidden hits or selftest fault records, that evidence stands and we report FAIL(1)
|
||||
# with the measurement problem noted on top of it (worst outcome = a real crash quietly
|
||||
# re-labelled as "no data").
|
||||
if broken and (hits or selftest_fail):
|
||||
out("")
|
||||
out("[WARN] capture also looks incomplete: " + m_reason)
|
||||
out(" but device-side bad news is present -> verdict stays FAIL, not MEASUREMENT-BROKEN")
|
||||
broken = False
|
||||
if broken:
|
||||
out("")
|
||||
out("[MEASUREMENT-BROKEN] " + m_reason)
|
||||
out(" no verdict is meaningful here -- fix the measurement first (open the port,")
|
||||
out(" assert the collector is alive, capture from a reset). Exit 2, NOT 1:")
|
||||
out(" a dead collector must not be counted as one more device failure.")
|
||||
return finish(2)
|
||||
|
||||
out("")
|
||||
if (missing or hits or bad_summary or selftest_fail or boot_issue
|
||||
or order_issue or timing_issue or exact_bad or pair_bad):
|
||||
@@ -579,6 +642,19 @@ def selftest(out=print):
|
||||
chk("blind spot proven: legacy substring PASSES where --exact FAILS",
|
||||
legacy_present and not exact_ok_here, True)
|
||||
|
||||
# measurement-vs-device classification (task-32): the three-counter rule IS this function,
|
||||
# so it is pinned here -- an all-zero read must not be reportable as a healthy device.
|
||||
chk("measure: good capture -> not broken", classify_measurement(137, 1, True, True, 1, 0)[0], False)
|
||||
chk("measure: zero payload lines -> broken", classify_measurement(0, 0, True, True, 0, 0)[0], True)
|
||||
chk("measure: readable but no boot anchor -> broken",
|
||||
classify_measurement(1, 0, True, True, 0, 0)[0], True)
|
||||
chk("measure: anchor check off -> abstain (non-boot logs stay usable)",
|
||||
classify_measurement(0, 0, False, True, 0, 0)[0], False)
|
||||
chk("measure: damaged lines are device-side, not 'no data'",
|
||||
classify_measurement(120, 1, True, True, 0, 3)[0], False)
|
||||
chk("measure: anchors present + no neighbour = functional miss",
|
||||
classify_measurement(120, 1, True, True, 0, 0)[0], False)
|
||||
|
||||
bad = 0
|
||||
for name, ok, detail in cases:
|
||||
out("{} {}".format("[OK] " if ok else "[FAIL]", name))
|
||||
|
||||
Reference in New Issue
Block a user