Files
STM32F4-Base/.dsh/skills/mem-budget/SKILL.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/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 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

13 KiB
Raw Blame History

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 上跑两遍 只是把同一个数字复述两遍,不构成"改后已复验"。

测量身份口径(先钉树与时刻,再看数字)

  1. 脚本按 file 定位产物,不按 cwd —— 跨树必须换脚本(在主树跑脚本量任务树 = 拿旧 .map 当真值)
  2. 重跑报告脚本不算新测量(复述 ≠ 复验):只有新的 Linker run 才算一次新测量
  3. 钉身份的取数法:.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(最坏调用链累计)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 底线要上报。
  • 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"不可归因于被测改动。判据写成"栈水位不劣于上一轮"必然假红。
  • 可执行修法(二选一,别都留着):
    1. 同一份像**≥3 次启动取最大值互比,并加容差(±128 B 量级**);
    2. 硬线改用 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 堆分配失败。

验证(这轮测量算数了吗)

  1. 报告头 - Source: 的相对路径落在我要量的那棵树下(不是裸文件名,更不是别的树);
  2. - Linker run:(或 .htm 的 Last Updated: / mtime)与本轮那次构建对得上,且不同于上一轮那次的时刻 ——相同就是复述,回去先做一次构建再比;
  3. 树身份已按 tree/commit + 时刻 + 产物来源 写进提交正文/台账,而不是只留在终端输出里;
  4. 结论里互比的数字必须来自同一棵树同一次链接;跨树比较要各自给出树与时刻;
  5. 涉及劣化判定时注明依据的是静态最坏链(硬线)还是运行时峰值(趋势),并已计入跨启动抖动。
  6. 要入库的数字:本轮已按「/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)需替换解析层,工作流不变。