现状口径:/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 全过;字节卫生零漂移。
13 KiB
name, description, whenToUse
| name | description | whenToUse |
|---|---|---|
| mem-budget | 嵌入式 RAM/栈预算核算工作流:Keil .map/.htm 静态报告(ram_report/stack_report)+ 串口运行时水位叠加 + 权威构建后的台账回填(ram_baseline.py --update 只往 ram_baseline.tsv 追加趋势行、--write 才同步 RAM预算台账 受管区、check_mem_numbers.py --write 同步文档快照区)+ DBG 日志成本模型(每条约 90B Flash/216B 栈)+ 跨树(worktree)测量的身份口径——脚本按 __file__ 定位产物、什么才算一次新测量、多条链同涨同一数值的共享帧/幻影环归因法。"改任务/缓冲/栈之前必须核算"、"这个数属于哪棵树、哪次构建"与"内存/Flash 数字钩子红了、提交前台账怎么回填"的固定动作和判读口径。 | 新增任务/加大缓冲/改栈/接入日志之前之后、链接报 L6406E 或 RAM 溢出、串口 [STACK] 水位余量低于底线、要回答"这个改动花多少 RAM/Flash";以及要复算或比对另一个 worktree 的栈数字、独立验证某个栈回归该记在哪一笔提交上、判断多次启动的栈峰值抖动算不算劣化时;还有 /keil-build 之后要把内存数字回填进台账/文档、纠结该跑 ram_baseline.py --write 还是 --update、提交时 mem-budget / mem-numbers 两条钩子报"与 .map 不一致"时。 |
mem-budget — RAM/栈预算核算
固定动作(改前、改后各跑一次)
# cwd = 项目根;先 /keil-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 上跑两遍 只是把同一个数字复述两遍,不构成"改后已复验"。
测量身份口径(先钉树与时刻,再看数字)
- 脚本按 file 定位产物,不按 cwd —— 跨树必须换脚本(在主树跑脚本量任务树 = 拿旧 .map 当真值)
- 重跑报告脚本不算新测量(复述 ≠ 复验):只有新的 Linker run 才算一次新测量
- 钉身份的取数法:.htm/.map 按约定不入库 → 把时刻写进正文/台账;记录最小格式 = tree/commit + Linker run 时刻 + 产物来源(本次构建 / 盘上既有)
本节全文(逐字搬移、未改写,含命令与实测数字)→ measurement-identity.md。动手前先读那篇。
静态最坏链回归的归因(同涨同一数值 = 共享帧,不是调用方)
- 多条任务链分毫不差同涨/同降同一个数值 ⇒ 加深在这些任务共用的被调帧 / 共享尾段上,不在任何一个调用方(别先猜「新增了大局部缓冲」,实测猜错过一次)
- 逐棵树逐帧比 frame size、不只比总数:stack_report.py 的 own-frame 表锁链尾公共段;grep -ac 'In Cycle' 在 .htm 里找环(实测 17 处)—— 链接器幻影环能在无人改栈配置时让全仓链齐涨
- Maximum Stack Usage 里的 + Unknown(Cycles, Untraceable Function Pointers) 说明静态数是估计值,别当精确值上报;不采信口头结论,用目标树的脚本 + .venv 解释器自己复算
- 处置后的下一步:抬栈(Src/freertos.c 的 USER CODE 区,属 Ask-first)或收紧共享缓冲 / 断环;改前改后各跑一次 stack_report.py + ram_report.py,且「改后」那次必须落在一次新的 Linker run 上
本节全文(逐字搬移、未改写,含命令与实测数字)→ shared-frame-attribution.md。动手前先读那篇。
判读口径
- 静态:
stack_report.py的 Max Depth(最坏调用链累计)vsSrc/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 底线要上报。 - RAM 总量:
ram_report.py对 Grand Totals 与docs/内存管理/预算表核对; 链接错L6406E= 已经溢出,用按文件表找大头。 - 任务表一致性由 pre-commit
task-table钩子保证;mem-budget钩子拦 RW_IRAM1/RW_CCM >92%, 并核台账受管区是否与 .map 同源;mem-numbers钩子核文档里的内存数字—— 数字与树/时刻脱钩、或构建后只跑了--update没回填受管区时,响的就是这两条(回填见下一节)。
权威构建后的台账回填(三个旗标,别混)
.map 是内存/Flash 数字的唯一事实源;台账与文档里的数字由脚本从它生成——不手抄、不手改
(docs/内存管理/RAM预算台账.md 首段就写着"数字不手抄")。三份落点 = 三个旗标,一条命令只干其中一件:
ram_baseline.py 的 --gate/--write/--update/--trend 是 add_mutually_exclusive_group()
(scripts/ram_baseline.py:415-419),想干两件就得调两次。
| 落点 | 旗标 | 实际改了什么 |
|---|---|---|
scripts/ram_baseline.tsv 趋势基线(顺带刷 scripts/size_attestation.json 同码凭证) |
ram_baseline.py --update |
只追加/覆盖当日那一行趋势(Code/RO/RW/ZI + 三区水位 + Top-10 RW+ZI)。不碰台账正文 |
docs/内存管理/RAM预算台账.md 的 <!-- MEM_BUDGET:BEGIN/END --> 受管区 |
ram_baseline.py --write |
从 .map 重渲染区域水位 + 子系统配额对照(打印「已同步台账受管区。」或「台账受管区已是最新。」) |
docs/内存管理/RAM使用分析与优化方案.md 的 <!-- MEM_SNAPSHOT:BEGIN/END --> 快照区 |
check_mem_numbers.py --write |
从同一份 .map 重渲染内存/Flash 快照表(打印「已更新内存快照:…」) |
AGENTS.md 与台账 §4 那句「每次 /keil-build 后跑 scripts/ram_baseline.py --update」喂的是趋势行,不等于回填受管区。
实测代价:照那句跑完 --update 就提交 → mem-budget、mem-numbers 两条钩子当场红。
mem-budget 的 entry 是 python scripts/ram_baseline.py --gate(.pre-commit-config.yaml:571)——它在 92% 阈值之外
还核台账受管区与 .map 的一致性,漂移即 [FAIL] 台账受管区与 .map 不一致(区域水位或子系统配额表漂移)
并打印 修复:python scripts/ram_baseline.py --write(scripts/ram_baseline.py:221-224);
mem-numbers 的 entry 是 python scripts/check_mem_numbers.py --scan(同文件 :193),不一致时打印
修复:运行 python scripts/check_mem_numbers.py --write(scripts/check_mem_numbers.py:416)。
改用两个 --write 后重提,同一笔提交触发的 26 条钩子全 Passed。
提交前固定顺序(构建 → 回填 → 真钩子提交)
# cwd = 项目根,一律 .venv 解释器
# 0) 权威构建先行(0 错 0 警):回填读的是**盘上那份 .map**,构建没落定就 --write = 把旧数字写进台账,钩子照样红
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py
# 1) 趋势行(纪律项,不是回填):覆盖 tsv 当日行 + 顺带刷体积同码凭证
& '.venv\Scripts\python.exe' scripts/ram_baseline.py --update
# 2) 回填台账受管区(mem-budget 钩子核的就是这一段)
& '.venv\Scripts\python.exe' scripts/ram_baseline.py --write
# 3) 回填文档快照区(mem-numbers 钩子核的就是这一张表)
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --write
# 4) 逐字自跑提交时会跑的这两条 entry(比事后翻 pre-commit 输出快)
& '.venv\Scripts\python.exe' scripts/ram_baseline.py --gate # 0 = 阈值 + 受管区都过
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan # 0 = 快照区与 .map 一致
# 5) 把刚落盘的落点一并 add,然后**真钩子**提交(绝不 --no-verify、绝不手改数字)
git add docs/内存管理/RAM预算台账.md docs/内存管理/RAM使用分析与优化方案.md scripts/ram_baseline.tsv scripts/size_attestation.json
退出码:ram_baseline.py 0=通过或跳过 / 1=阈值超限或受管区漂移 / 2=.map 解析失败;
check_mem_numbers.py 0=一致 / 1=不一致、缺受管锚点或找不到事实源。
回填链上的坑(实测)
- --write 说「已是最新」而钩子还红
- tsv 落后只 WARN、不拦
- --gate 无 .map → SKIP,而脚本自己写明「SKIP 不等于 PASS」
- --map 只换读、不换写
- --scan 的散句告警 --write 不替你改
- --update 的盖戳排在索引后面
- 改写型钩子当场改文件是流程不是故障
- mem-budget 触发面很宽
本节全文(逐字搬移、未改写,含命令与实测数字)→ backfill-pitfalls.md。动手前先读那篇。
运行时峰值抖动与"算不算劣化"
- 同一份镜像连续启动 5 次,
uiTask峰值实测在 4116~4188 之间摆 72 B ⇒ 跨启动本身就有几十 B 抖动, "相对上一轮 +100 B"不可归因于被测改动。判据写成"栈水位不劣于上一轮"必然假红。 - 可执行修法(二选一,别都留着):
- 同一份像**≥3 次启动取最大值互比,并加容差(±128 B 量级**);
- 硬线改用
stack_report.py的静态最坏链,运行时峰值降级为趋势参考。
- 判据一改要同步所有落点:判据正文、跑判据的脚本、记录判据的文档三处一起改,只改一处等于各说各话。
成本模型(实测,估算用)
DBG_*日志不是免费的:每条调用点约 90 B Flash(字符串常量另计)+ 深链上 每帧一份行缓冲进最坏栈(实测一条链 9 帧 ≈ 1.5 KB 纯日志缓冲)。 已覆盖步骤勿加重复锚点,只在原先静默返回的黑洞处补。- 深调用链模块收紧行缓冲:
#define DBG_LINE_BUF_MAX 128U写在#include "dbg_log.h"之前;取值必须 ≥ 该模块最长日志行(时间戳 20 + 标签 + 正文),否则截断。 - 加大局部缓冲的替代方案是静态化(实测 lftpd 9 处大局部缓冲静态化:BSS +4.6 KB, 任务峰值 3480B→1184B)——栈紧张时先找大局部变量。
- 自研代码禁 C 库堆(MicroLIB 堆仅 512B):只用
pvPortMalloc/vPortFree(FreeRTOS 堆), 模拟器 CRT 堆照不出 MCU 堆分配失败。
验证(这轮测量算数了吗)
- 报告头
- Source:的相对路径落在我要量的那棵树下(不是裸文件名,更不是别的树); - Linker run:(或 .htm 的Last Updated:/ mtime)与本轮那次构建对得上,且不同于上一轮那次的时刻 ——相同就是复述,回去先做一次构建再比;- 树身份已按
tree/commit + 时刻 + 产物来源写进提交正文/台账,而不是只留在终端输出里; - 结论里互比的数字必须来自同一棵树同一次链接;跨树比较要各自给出树与时刻;
- 涉及劣化判定时注明依据的是静态最坏链(硬线)还是运行时峰值(趋势),并已计入跨启动抖动。
- 要入库的数字:本轮已按「
/keil-build→ram_baseline.py --update→ram_baseline.py --write→check_mem_numbers.py --write」回填,并逐字自跑ram_baseline.py --gate与check_mem_numbers.py --scan各返 0; 台账 / 文档 / tsv 的改动与代码同一笔入库,未手改数字、未用--no-verify。
其它项目怎么套
模式通用:链接器 map + 调用图 htm 是免硬件的栈可观测源;固定"改前改后各跑一次
报告 + 运行时水位叠加 + 余量阈值"三件套,外加一条身份口径——脚本按自身位置定位产物,
跨树比的是"两棵树各自的脚本 + 各自的链接时刻",重跑同一份产物永远只是复述。
本仓库两个报告脚本按 Keil 输出格式解析,换工具链(GCC newlib + -fstack-usage)需替换解析层,工作流不变。