Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 529c58da39 | |||
| dc5d6a0680 | |||
| ae80bb8e34 | |||
| c1e7e8c42c | |||
| 51ecd36192 | |||
| 2d18193d38 | |||
| 5869c0307b |
@@ -13,7 +13,7 @@
|
||||
"stack-overflow-hook-impl": "03a383dda578d12a",
|
||||
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
|
||||
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
|
||||
"watchdog-module": "5fd7efd90a78fe5a",
|
||||
"watchdog-module": "07808632996d66e8",
|
||||
"net-msgq-adopt": "a0f913e3f34c8e9e",
|
||||
"reset-cause-latch": "983927d587cb9f48",
|
||||
"iwdg-hal-driver": "4b902875a84554e9",
|
||||
@@ -23,11 +23,11 @@
|
||||
"ioc-task-stacks": "58ee94dd2a093177",
|
||||
"adc1-hal-enabled": "1a8ed6cff34f7ba6",
|
||||
"adc1-calibration-call": "8d5f4b40fb218913",
|
||||
"adc1-calibration-impl": "25723dd0449ac701",
|
||||
"adc-tick-tim3-branch": "17d43a565f50c6f2",
|
||||
"adc1-calibration-impl": "d33bc4323e243248",
|
||||
"adc-tick-tim3-branch": "39c3548c37f6c4ae",
|
||||
"ioc-adc-trig-t3trgo": "a9939de260ee46f8",
|
||||
"ioc-adc-dma-continuous": "189126670d85d80a",
|
||||
"adc1-dma-link": "8a57c70611db1a02",
|
||||
"adc1-dma-link": "397fce92dca3b728",
|
||||
"adc1-dma-irq-priority": "beff08e91333229b"
|
||||
},
|
||||
"_note": "CubeMX 存活审计基线;改动用 --write 重建"
|
||||
|
||||
@@ -14,7 +14,7 @@ whenToUse: 要跑板级测试套件(CH395F/STORAGE/GD5F/RTC)回归、要开/
|
||||
实测 GD5F 整轮破坏性跑一次即多记一个 NAND 坏块(容量 95248→95200),
|
||||
计数持久化在 dhara journal checkpoint 头里,普通格式化恢复不回来。
|
||||
- 提交门禁:pre-commit 钩子 `test-suite-switch` 默认拦截测试构建
|
||||
(确需提交用 `ALLOW_TEST_BUILD=1` 显式放行)——**跑完务必改回全注释并 `/keil-build`**。
|
||||
(确需提交用 `ALLOW_TEST_BUILD=1` 显式放行)——**跑完务必改回全注释并 `@build`**。
|
||||
|
||||
## 一键回归(推荐路径,勿手工循环)
|
||||
|
||||
@@ -28,15 +28,15 @@ whenToUse: 要跑板级测试套件(CH395F/STORAGE/GD5F/RTC)回归、要开/
|
||||
- 脚本 try/finally **无条件还原 `test_config.h`**(含 Ctrl-C 路径)——这是它存在的核心
|
||||
理由:跑完忘还原 = 测试固件进库。`--keep-config` 仅调试用。
|
||||
- 退出码 0=全 PASS / 1=有 FAIL / 2=环境错误;4 套件真机一轮数分钟起,**仅回归窗口用,绝不挂 pre-commit**。
|
||||
- 手工循环时的顺序(每轮):改宏(只开一个)→ `/keil-build` 0错0警 → 烧录 →
|
||||
- 手工循环时的顺序(每轮):改宏(只开一个)→ `@build` 0错0警 → 烧录 →
|
||||
`serial-assistant` 采集(open→clear→烧录→wait-idle)→ 判读套件汇总行。
|
||||
|
||||
## 跑后检查清单(必做)
|
||||
|
||||
1. `test_config.h` 恢复全注释(用 run_all_suites.py 则已自动);
|
||||
2. `/keil-build` 确认产品构建 0 错 0 警;
|
||||
2. `@build` 确认产品构建 0 错 0 警;
|
||||
3. 若开过破坏性开关:核对开机日志容量数字(健康满值 96064 pages / 187 MB),
|
||||
掉了 → 按 `docs/测试说明/专项验证/存储维护与容量恢复.md` 走恢复动作;
|
||||
掉了 → 按 `docs/测试说明/存储维护与容量恢复.md` 走恢复动作;
|
||||
4. 提交前 `test-suite-switch` 门禁会再兜一道。
|
||||
|
||||
## 三个维护动作别混用(存储类)
|
||||
@@ -47,8 +47,8 @@ whenToUse: 要跑板级测试套件(CH395F/STORAGE/GD5F/RTC)回归、要开/
|
||||
| B 容量恢复 | A + `STORAGE_RECOVERY_FULL_WIPE`(擦净 2044 块 + 断电重启) | 卷全失,约 4s 擦除 |
|
||||
| C 日常回归 | 只开 `TEST_SUITE_*`,不开破坏性 | 无(只读模式,横幅 `mode: READ-ONLY`) |
|
||||
|
||||
原理与完整步骤见 `docs/测试说明/专项验证/存储维护与容量恢复.md`;各套件用例清单/判读见
|
||||
`docs/测试说明/` 对应 `*_Test_Guide.md`;流程规范 `docs/测试说明/流程与规范/测试开发标准流程.md` §3。
|
||||
原理与完整步骤见 `docs/测试说明/存储维护与容量恢复.md`;各套件用例清单/判读见
|
||||
`docs/测试说明/` 对应 `*_Test_Guide.md`;流程规范 `docs/测试说明/测试开发标准流程.md` §3。
|
||||
|
||||
## 其它项目怎么套
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: board-ui-nav
|
||||
description: 板端 UI 交互的唯一正确入口:先问固件"我在哪"(STATUS 身份查询),再决定按键;到页用闭环 --to 或 bench 直开 --goto。附 #69 实证的四类走错页根因与恢复配方。
|
||||
whenToUse: 需要上真机/台架进某页面看图(如故障录波、回放、定值)、在板端做 UI 取证、或从错误页面/待机态/未知屏恢复时。**写任何注键/取帧代码之前先读本技能**。
|
||||
---
|
||||
|
||||
# 板端 UI 导航(唯一入口 = scripts/board_ui_nav.py)
|
||||
|
||||
一句话纪律:**一切"进页面看图"都走 `scripts/board_ui_nav.py`,禁止另写一次性注键脚本。**
|
||||
2026-11-02 #38 调试里"总是进错误页面浪费大量时间"的直接原因,就是临时手搓
|
||||
`board_walk*.py` 盲按 `d*9`——而闭环导航器 2026-10-25 起就已存在(10-25 盲走事故
|
||||
9 个目标只有 3 个落对,`docs/UI设计/UI_自动化测试流程.md` §7.1)。
|
||||
|
||||
## 为什么不能自己搓(#69 复盘四类根因,全部有实证)
|
||||
|
||||
1. **协议旧版零身份反馈**:KEY 的 ACK 是空帧,"发了 5 个键"≠"到了第 5 层"。(已修:
|
||||
7003 新增 STATUS 命令,固件直读 `s_depth/s_path/s_sel` + 当前内容页 id,见下)
|
||||
2. **N−1 屏坑**:KEY ACK 与 LCDMEM 推流共用同一条 TCP,`sleep(0.30)` 就抓帧拿到的是
|
||||
上一屏(规则①:排空到帧流静默 ≥900 ms;现在 `key()` 代码已真正执行排空+双帧比对)。
|
||||
3. **物理陷阱**:待机态按 `e` 是**重开最近页**不是进菜单;级联列光标记忆上次行不归零;
|
||||
根列最后两行的子列弹框被 160×160 屏裁掉(selftest/runlog 因此"读不到选带")。
|
||||
4. **认屏误判**:菜单子列标签与页标题同字(记录查询子列里的 事故记录/操作记录…),
|
||||
像素分类器会把级联菜单判成 PAGE——**有 STATUS 时以 STATUS 为准**,trace 留痕不一致。
|
||||
|
||||
## 命令(python 一律 .venv;真机动作必须已持 bench 锁并加 --i-hold-bench-lock)
|
||||
|
||||
```powershell
|
||||
# 我在哪?(固件身份 + 像素双口径,不一致会留痕打印)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --where --i-hold-bench-lock
|
||||
|
||||
# 到页:闭环步行(像素态设备/老固件也能用;每键重认屏,绝不猜步数)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --board --to wave_play --grab C:\temp\wx --i-hold-bench-lock
|
||||
|
||||
# 到页:bench 固件直开页(一次往返直达,不注键;见下"直开页的边界")
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --goto wave_play --grab C:\temp\wx --i-hold-bench-lock
|
||||
|
||||
# 提交门禁快检(无台架、无模拟器产物也要绿)
|
||||
.venv\Scripts\python.exe scripts\board_ui_nav.py --selftest-quick
|
||||
```
|
||||
|
||||
`--list` 列出全部合法页 id 与菜单路径;`--trace 路径` 落步迹 jsonl(排错先看它)。
|
||||
退出码:0 成功 / 1 分类或导航失败 / 2 环境错误 / 3 拒动(未持锁确认位等)。
|
||||
|
||||
## 身份回读与直开页(#69 新增,两端共享 ui_net_sink.h 契约)
|
||||
|
||||
- **STATUS(命令 4)**:应答 `[ver][kind][depth][n][id_len][path][sel][page id]`;
|
||||
kind = MENU/PAGE/STANDBY(待机优先判)。老固件不应答 → 主机打印一次后自动退回纯像素。
|
||||
生产固件也支持 STATUS(只读、零画面副作用)。
|
||||
- **GOTO(命令 5)**:**编译期双闸** `DTU_BENCH`(临时 bench 固件)或模拟器宏
|
||||
`UI_REMO_ALLOW_GOTO`;生产固件恒拒 `status=4`。拒绝码:1 无此页 id / 2 执行类页 /
|
||||
3 开页失败 / 5 待机态(先按键唤醒再来)。直开按查看权限 `UI_PERM_VIEW` 进入,
|
||||
**不弹密码**——所以它只用于调试取证,不能当作"过密码"的旁路写路径。
|
||||
- 执行类页面 `clear`/`reset`(第二次 ENTER 即真执行)主机与固件**双侧**默认拒绝,
|
||||
`--allow-exec-pages` 才放行,放行 = 接受后果。
|
||||
|
||||
## 走错页 / 未知屏恢复配方(按序,不跳步)
|
||||
|
||||
1. `--where` 先看 STATUS:`PAGE` → 按 `s` 关页(导航器 `--to` 自动做);
|
||||
`STANDBY` → 按 `e` 唤醒(注意规则②:home 上 `e` 可能重开最近页);
|
||||
`MENU depth>0` → 按 `s` 或 `l` 都收一级(台架复核 2026-11-02:菜单态 `s` 与 `l` 同走
|
||||
`menu_back`,父列选中留在原地;旧说法"`s` 不收列"系把待机态 `s` 无效误读成菜单行为)。
|
||||
2. STATUS 无应答(老固件)才吃退避键序(默认 `esl`,`--backoff-keys sl` 可去 ENTER);
|
||||
ENTER 上限 1 次是硬闸(BACKOFF_MAX_E),报停机就 dump 帧人眼看,不许连按。
|
||||
3. 反复落错页且 trace 里出现"像素与 STATUS 不一致" → 分类器或固件有账要记,
|
||||
把留痕帧 `--dump-fail` 存下交 Lead,**不要**改判据迁就现状。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本脚本不申请锁、不碰串口与烧录;锁的取还见 task-worktree 技能
|
||||
(`lock acquire bench --task <ID>`)。
|
||||
- 台架证据只进 `ci-artifacts/`(`--walk` 默认 `ci-artifacts/uinav/walk/`,与 BOARD_TRUTH
|
||||
读取目录已并一),不进工作树。
|
||||
- UI 源码改动的前后回归是 ui-verify 技能(5 门禁),本技能只管"人到板上去看"。
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: cubemx-regen
|
||||
description: CubeMX 重生成存活工作流(写进 .ioc → 搬进 USER CODE 区/宏改名 → 审计兜底三层):重生成前后各跑什么、哪些定制会被静默覆盖、审计脚本 check_cubemx_survival.py 的 --check/--write 语义。STM32CubeMX 项目通用方法论 + 本仓库清单。
|
||||
description: CubeMX 重生成存活工作流(L1 写进 .ioc → L2 USER CODE 区/宏改名 → L3 审计兜底):重生成前后各跑什么、哪些定制会被静默覆盖、审计脚本 check_cubemx_survival.py 的 --check/--write 语义。STM32CubeMX 项目通用方法论 + 本仓库清单。
|
||||
whenToUse: 要用 CubeMX 重新生成工程(改 .ioc / 外设配置)、改动 Src/Inc 生成区附近代码、或排查"重生成后定制丢失/行为变了但编译不报错"时。
|
||||
---
|
||||
|
||||
@@ -12,12 +12,12 @@ whenToUse: 要用 CubeMX 重新生成工程(改 .ioc / 外设配置)、改
|
||||
|
||||
## 三层优先级(防丢失的唯一正确顺序)
|
||||
|
||||
1. **写进 `.ioc`**(第一层,权限最高):凡 CubeMX 界面能表达的配置(任务栈/优先级/队列/
|
||||
1. **L1 写进 `.ioc`**(权限最高):凡 CubeMX 界面能表达的配置(任务栈/优先级/队列/
|
||||
事件组/堆大小/IWDG 参数/FreeRTOS 钩子开关)必须在 GUI 里设——重生成只会照写。
|
||||
⚠ **别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`
|
||||
(实测 `configCHECK_FOR_STACK_OVERFLOW=2` 在 FreeRTOS → Config parameters →
|
||||
Hook function related definitions 里,自造宏区被抹两次后才找到)。
|
||||
2. **搬进 `USER CODE` 区**(第二层):只信 CubeMX **当前模板真实生成**的区名(优先
|
||||
2. **L2 搬进 `USER CODE` 区**:只信 CubeMX **当前模板真实生成**的区名(优先
|
||||
`USER CODE BEGIN 0` 这类从未变过的标准位)。**不要依赖"自造标记区"**——
|
||||
模板换代时自造区连同内容一起消失(实测中断函数模板换代的教训)。
|
||||
生成区内要**去掉一句调用** → 宏改名:`USER CODE BEGIN 0` 里放空实现 +
|
||||
@@ -26,12 +26,12 @@ whenToUse: 要用 CubeMX 重新生成工程(改 .ioc / 外设配置)、改
|
||||
要**替换生成实体定义**(如 4 个 Fault Handler 本体)→ 我们的定义放
|
||||
`USER CODE BEGIN 0`,**必须写在 `#define X X_Gen` 之前**(写在之后会被自己的宏改名),
|
||||
生成区恢复原样,链接器自动移除未引用的 `*_Gen`。
|
||||
3. **审计兜底**(第三层):清单 + 哈希基线漂移检测(见下)。
|
||||
3. **L3 审计兜底**:清单 + 哈希基线漂移检测(见下)。
|
||||
|
||||
## 重生成前后固定动作
|
||||
|
||||
```powershell
|
||||
# 重生成前:把能在 GUI 表达的都写进 .ioc(第一层检查)
|
||||
# 重生成前:把能在 GUI 表达的都写进 .ioc(L1 检查)
|
||||
# 重生成后【立刻】:
|
||||
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py # 人读报告
|
||||
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --check # 门禁模式(pre-commit 同款)
|
||||
@@ -41,21 +41,21 @@ whenToUse: 要用 CubeMX 重新生成工程(改 .ioc / 外设配置)、改
|
||||
|
||||
- 退出码 0=通过 / 1=有 FAIL 项;已挂 pre-commit `cubemx-survival` 钩子
|
||||
(改 `.ioc`/`Src/`/`Inc/`/`.uvprojx` 触发)。
|
||||
- **审计清单已清空 = 重生成应当零 diff**;仍有 diff 就是被覆盖了,直接跑脚本定位。
|
||||
- **L3 清单已清空 = 重生成应当零 diff**;仍有 diff 就是被覆盖了,直接跑脚本定位。
|
||||
- `uvprojx` 不参与漂移检测(每次构建被 UV4 改写),只保留"源文件仍挂载"断言。
|
||||
|
||||
## 实测陷阱清单
|
||||
|
||||
- CubeMX 对 System Exception(HardFault/MemManage/BusFault/UsageFault)**只生成优先级、
|
||||
从不生成使能代码**;`.ioc` 里 `NVIC.MemoryManagement_IRQn=true` ≠ 已使能(只是向量可见)。
|
||||
`SHCSR` 使能位只能手工写(第二层)+ 审计项兜底。
|
||||
`SHCSR` 使能位只能手工写(L2)+ 审计项兜底。
|
||||
- 启用 FreeRTOS 后模板会生成 `USER CODE BEGIN 4 / 5` 区(默认空桩)——**钩子实现必须
|
||||
整体写进这两个区**,且 helper 函数放更早的区(同名两处定义 = 编译错)。
|
||||
- 生成文件行尾/末尾空行按工具自身风格,**勿用改写型钩子去修剪**(已豁免,勿"顺手格式化")。
|
||||
- 重生成后除审计脚本外,照常走 `/keil-build`(0 错 0 警)+ 必要时 `/serial-boot-test` 闭环。
|
||||
- 重生成后除审计脚本外,照常走 `@build`(0 错 0 警)+ 必要时 `@test` 闭环。
|
||||
|
||||
## 其它 CubeMX 项目怎么套
|
||||
|
||||
这三层方法论与"宏改名两招"完全通用;本仓库的清单/基线在
|
||||
L1/L2/L3 方法论与"宏改名两招"完全通用;本仓库的清单/基线在
|
||||
`scripts/check_cubemx_survival.py`(21 项 + `.cubemx_survival_baseline.json`),
|
||||
新项目照它建自己的清单。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2。
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
---
|
||||
name: draw-diagram
|
||||
description: docs/ 文档技术配图绘制流程(精确布局图用 tools/arch_diagram/ 的纯标准库 SVG 生成器 / 拓扑图用 Mermaid):挑脚本或照抄骨架新建生成器 → 改顶部数据 → 跑生成并把 check_fit/OVERFLOW 溢出预检清零 → Windows 无渲染器时用 headless Chrome/Edge 截图 + read_image 回读目检 → 回改生成器重跑 → 过 generated-fresh 逐字节幂等 → md 相对路径引用 → 脚本+产物一并提交,产物统一放 docs/images/。
|
||||
whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层架构、依赖/就绪屏障、台阶/泳道对照、刻度尺、流程图、时序、状态机),从零新写一个 gen_*_svg.py 生成器,在 Windows 上想"目检"一张 SVG,或用户要求"画个图 / 生成图 / 更新架构图 / 帧格式图"时。
|
||||
description: docs/ 文档技术配图绘制流程(精确布局图用 SVG 生成器 / 拓扑图用 Mermaid):挑脚本 → 改顶部数据 → 重跑生成 → 目检 → md 相对路径引用 → 脚本+产物一并提交,产物统一放 docs/images/。
|
||||
whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层架构、依赖/就绪屏障、流程图、时序、状态机),或用户要求"画个图 / 生成图 / 更新架构图 / 帧格式图"时。
|
||||
---
|
||||
|
||||
# 文档配图绘制(SVG / Mermaid)
|
||||
|
||||
`docs/` 文档技术配图的统一流程;完整选型依据 / 约定 / 检查清单见 `docs/架构设计/图形绘制流程.md`。**产物统一放 `docs/images/`**(专门配图目录,`.svg` 入库;PNG 预览属本地派生物、不入库,`.gitignore` 已含 `docs/images/*.png` 与 `ci-artifacts/`)。
|
||||
|
||||
> 本窗实测(2026-10-29,新建 2 张图):**"浏览器打开 SVG 目检"这一步在 Windows 上不可直接执行**——没有现成 SVG 栅格化工具,只能走 headless Chrome/Edge 截图 + 图片回读;且 `generated-fresh` 是**逐字节**门禁,生成器自己的控制台打印也会成为失败源(见 §五)。
|
||||
`docs/` 文档技术配图的统一流程;完整选型依据 / 约定 / 检查清单见 `docs/架构设计/图形绘制流程.md`。**产物统一放 `docs/images/`**(专门配图目录,`.svg` 入库;PNG 预览属本地派生物、不入库)。
|
||||
|
||||
## 工具选型
|
||||
|
||||
@@ -17,15 +15,15 @@ whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层
|
||||
| 协议帧 / 字节布局(字段宽 ∝ 字节数、偏移标尺) | SVG:`tools/arch_diagram/gen_frame_svg.py` |
|
||||
| 分层架构 / 模块依赖 | SVG:`tools/arch_diagram/gen_arch_svg.py` |
|
||||
| 就绪屏障 / 生产者-消费者 | SVG:`tools/arch_diagram/gen_barrier_svg.py` |
|
||||
| 台阶 / 泳道对照 / 刻度尺等"高度差、并列对照"类 | SVG:**新写一个** `tools/arch_diagram/gen_<主题>_svg.py`(照抄最近生成器的骨架,见 §二) |
|
||||
| 其他精确布局图 | **复制**最接近的生成器改顶部数据(`build()` 与数据解耦,通用) |
|
||||
| 流程 / 时序 / 状态机 / ER(只表达拓扑关系) | Markdown 内嵌 ```mermaid 代码块(`flowchart` / `sequenceDiagram` / `stateDiagram-v2` / `erDiagram`),无需生成图片 |
|
||||
| 流程 / 时序 / 状态机 / ER(只表达拓扑关系) | Markdown 内嵌 ` ```mermaid ` 代码块(`flowchart` / `sequenceDiagram` / `stateDiagram-v2` / `erDiagram`),无需生成图片 |
|
||||
|
||||
> Mermaid 盒子按文本自适应宽、自动加有向边,**只适合"关系"类图**;凡需精确像素 / 比例 / 高度差的布局图一律走 SVG 生成器(实测教训:五级"台阶"用 Mermaid 会被压成同一高度,"还差一级"就看不出来了)。
|
||||
> Mermaid 盒子按文本自适应宽、自动加有向边,**只适合"关系"类图**;凡需精确像素 / 比例的布局图一律走 SVG 生成器。
|
||||
|
||||
## 一、改既有图(最短路线)
|
||||
## SVG 生成流程(仓库根目录执行)
|
||||
|
||||
1. 改**顶部数据 / 常量区**(字段、节点、标题、配色),不改 `build()` 结构。配色沿用既有调色板(主色 / 底色):
|
||||
1. **挑 / 复制脚本**:新类型图复制最接近的生成器,只改顶部数据区。
|
||||
2. **改顶部数据**:字段 / 节点 / 标题 / 配色。沿用既有调色板(主色 / 底色):
|
||||
|
||||
| 语义 | 主色 | 底色 |
|
||||
|---|---|---|
|
||||
@@ -37,175 +35,38 @@ whenToUse: 需要新增/修改 docs/ 文档的技术配图(协议帧、分层
|
||||
| 灰 | `#475569` | `#f1f5f9` |
|
||||
| 玫红 | `#e11d48` | `#fff1f2` |
|
||||
|
||||
2. 重跑(仓库根;脚本内置 `ET.fromstring` 自检,XML 不合法直接抛错):
|
||||
3. **重跑**(脚本内置 `ElementTree` 自检,XML 不合法直接抛错):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' tools/arch_diagram/gen_xxx.py
|
||||
# 期望:OK <字节> bytes, canvas <W>x<H> -> <路径>,且没有 OVERFLOW WARN / overflow warnings 行
|
||||
# 成功输出:OK <字节> bytes, canvas <W>x<H> -> <路径>
|
||||
```
|
||||
|
||||
3. 目检(§三)→ 引用(§四)→ 幂等与门禁(§五)→ 提交(§六)。
|
||||
4. **目检**:浏览器打开 `docs/images/*.svg`——文字不溢出盒外、框 / 线 / 标签对齐、配色符合调色板。
|
||||
5. **md 引用**(相对路径):`docs/` 根的 md 写 ``;`docs/UI设计/` 等子目录 md 写 ``。
|
||||
6. **提交**:生成器脚本(源)+ 生成的 `.svg`(产物)**一并**提交。
|
||||
|
||||
## 二、从零新写生成器(先抄骨架,别裸写)
|
||||
## SVG 约定(手写 / 新增生成器必须遵循)
|
||||
|
||||
1. **先读同目录 1~2 个最近的脚本**(如 `tools/arch_diagram/gen_adc_frontend_svg.py`、`gen_adc_code_scale_svg.py`),抄它们的三条约定:
|
||||
- **文件头 docstring** 写清四件事:产物路径 + 这张图回答什么问题 + **「为什么不用 Mermaid」** + 用法("仓库根目录,任意 python3")与"改内容 vs 调坐标"的边界;
|
||||
- **顶部集中常量块**:画布与几何(`W` / `H` / `PAD` / `RIGHT`)→ `FONT` → 各级字号 → 调色板 → 文案(`TITLE` / `SUB*` / 逐条目 / `FOOTNOTE`);坐标一律由常量推导,别在 `build()` 里塞魔法数;
|
||||
- **纯标准库**:`import html / os / sys / xml.etree.ElementTree as ET`,helpers 只有 `esc` / `text_w` / `txt` / `line` / `rect` / `wrap` / `check_fit` / `emit` / `build`。
|
||||
2. **照抄预检三件套**(`gen_adc_frontend_svg.py` 里还没有;从 `tools/arch_diagram/gen_data_trust_ladder_svg.py` 抄):`OVERFLOW = []` + `check_fit(s, fs, avail, where)` + `wrap(s, fs, avail, max_lines, where)`,标题 / 副标题 / 盒内标题走 `check_fit`,盒内正文与脚注走 `wrap`(`wrap` 按估宽折行、超行数用 `…` 收尾并记一条 OVERFLOW)。`build()` 末尾:`emit("OK ...")` 之后 `if OVERFLOW: emit("!! %d overflow warnings" % len(OVERFLOW))`。估宽口径不变:CJK / 全角 / 圈码 / 箭头 ≈ `1.0em`,其余 ≈ `0.58em`(宁大勿小)。
|
||||
3. **照抄 `emit()`**(幂等门禁的地基,见 §五):
|
||||
- **纯标准库**拼 SVG(只用 `html` / `os`(+ `math`)),不引第三方库(SVG 由浏览器用系统字体渲染)
|
||||
- 所有文本经 `html.escape(s, quote=True)`,防止破坏 XML
|
||||
- 字体栈:`Microsoft YaHei, Segoe UI, PingFang SC, sans-serif`
|
||||
- 文本宽估算(自适应盒宽):CJK / 全角 / 圈码 / 箭头 ≈ `1.0em`,其余 ≈ `0.58em`(**宁大勿小**防溢出)
|
||||
- 落盘:`open(OUT, "w", encoding="utf-8", newline="\n")`(UTF-8 无 BOM + LF)
|
||||
- 写盘后 `xml.etree.ElementTree.fromstring(svg)` 自检
|
||||
- 产物放 `docs/images/`,命名 `<主题>.svg`(可与同名 `.md` 对应)
|
||||
|
||||
```python
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
```
|
||||
## SVG → PNG(可选,本地派生物不入库)
|
||||
|
||||
4. 路径锚在脚本自身位置,别用 cwd(临时目录重跑才不会漂):
|
||||
- 最简单:浏览器打开 `.svg` → 另存为图片
|
||||
- 脚本化:`& '.venv\Scripts\python.exe' -m pip install cairosvg` 后
|
||||
`& '.venv\Scripts\python.exe' -c "import cairosvg; cairosvg.svg2png(url='docs/images/xxx.svg', write_to='x.png', scale=2)"`
|
||||
- `.gitignore` 已含 `docs/images/*.png`
|
||||
|
||||
```python
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "<主题>.svg")
|
||||
```
|
||||
## 提交前检查清单
|
||||
|
||||
5. 跑 `& '.venv\Scripts\python.exe' tools/arch_diagram/gen_<主题>_svg.py`,**把 OVERFLOW 告警打到 0 再进目检**——预检能在渲染前就报出 `foot: 974 > 968` 这类溢出(本窗实测两张图首跑各报一条脚注超宽),比靠眼睛反复量便宜一个数量级。
|
||||
6. 新脚本落库时**同批**把行加进 `tools/arch_diagram/README.md` 的「**产物清单**」——那张表按图逐行,列 =
|
||||
图 / 生成器 / 这张图答什么 / 顶部数据区 / 挂在(`文档:行号`)。**全仓只此一张逐脚本清单**:
|
||||
`docs/架构设计/图形绘制流程.md` 曾另立一张三列表,结果两张各漏各的(2026-10-30 实测 README 漏 5 个、
|
||||
那张漏 9 个),现已换成指针。所以新图只加一行,别在第二篇文档再起一张表。
|
||||
|
||||
## 三b、大图(几十框几十边)加一道碰撞审计,别只靠眼睛
|
||||
|
||||
边数一多,像素回读就查不全(本窗实测:42 框 37 边的端到端链路图,缩略 PNG 看着干净,
|
||||
审计器一眼报出 23 条:边标签压节点、走线穿框、标签出画布)。做法是**解析自己生成的 SVG**
|
||||
而不是解析渲染结果——`ET.parse` 产物,按 `font-size`/`fill` 分诊文字,估宽口径直接复用生成器的
|
||||
`text_w()`(两边同一把尺子才不会假绿),然后判三件事:
|
||||
|
||||
1. **边标签的「文字盒」vs 节点框**(不是白片盒):交集 > 6 px² 就算问题——节点在边之后画,
|
||||
压上去的**文字**会被节点白底吃掉半个字(本窗症状:「串行执行」渲染成「行执行」)。
|
||||
白片本身压到框边角是**无害**的(白片先画、节点后画,只是盖住一点白底,不改字)。
|
||||
除「有没有压上」,还要打印**文字与最近框的最小水平余量**并按 < 3px 报问题:
|
||||
本窗「TRGO 触发」余量 0.7px、「帧 B 下发」1.5px,都没压上却随时会被真实字宽吃笔画,
|
||||
缩短标签后升到 3.5px 以上才算安全。
|
||||
2. **走线段 vs 节点框**(内缩 2px 再判,排除贴边端点):线穿框是硬伤,改边的出入框方位或独占通道。
|
||||
3. **标签互压 / 出画布**:白片两两求交;`box[3] > H-2` 查脚注贴底。
|
||||
4. **零余量复扫(生成器侧,别只跑 SVG 侧那一遍)**:SVG 反推不出哪两个框是本边端点,
|
||||
走线只能内缩着查 → 1~3px 的切边照不到。用生成器自己的 `Box` / `route()` / `label_at()`
|
||||
真值再扫一遍:框不膨胀不内缩、只跳本边两端。跑 SVG 侧与真值侧两遍都归零才算收工。
|
||||
|
||||
脚本骨架与判据见 `reference/svg-collision-audit.py`(可直接拷进 `ci-artifacts/` 当一次性工具用,
|
||||
**别放 `tools/arch_diagram/`**——那里只放生成器,非 `gen_*.py` 的脚本混进去会让人以为在册)。
|
||||
|
||||
配套的两条几何纪律(都是本窗踩出来的):
|
||||
|
||||
- **同轴直连不要插退化折点**:同行两框若按「同轴 → 加一段 gx 走廊」的通用规则生成,会产出
|
||||
`[(195,176),(219,176),(219,176),(243,176)]` 这种点集——线还是直的,但**标签落点算成第一半的中点**,
|
||||
整格偏移半个通道 → 压框。正解:出框轴与入框轴同轴且已对齐时直接返回两点线。
|
||||
- **标签落点用弧长中点,不用「最长段中点」**:多段折线里最长段可能有多条(对称 Z 字),
|
||||
取第一条会把标签推到拐角上;沿总弧长取 50% 再定位,配合 `seg`/`t` 旋钮微调。
|
||||
|
||||
**先验审计器本身,再信它的「0 条」**:本窗把 `hi > y0` 复制粘贴成 `hi > x0`,零余量那一遍
|
||||
凭空报出 3 条「穿框」,差点为此重排三条边的走线。判据脚本的假阳性比漏报更贵——它会让你
|
||||
去改一张本来没问题的图。规矩:写完后挑一条**已知干净**的边(例如两框正对的直连边)单独跑一遍,
|
||||
确认它不报,再跑全量。
|
||||
|
||||
## 三、目检(Windows:本机没有任何现成 SVG 渲染器)
|
||||
|
||||
**先探测,别照抄 cairosvg 命令**:本仓 `.venv` 无 `cairosvg`(`ModuleNotFoundError: No module named 'cairosvg'`),`rsvg-convert` 不存在,只有 Edge / Chrome:
|
||||
|
||||
```bash
|
||||
# ops_bash(git-bash)里探测;哪个存在用哪个
|
||||
ls -1 "/c/Program Files/Google/Chrome/Application/chrome.exe" \
|
||||
"/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" 2>/dev/null
|
||||
command -v rsvg-convert inkscape magick convert 2>/dev/null || echo "no CLI rasterizer"
|
||||
```
|
||||
|
||||
```powershell
|
||||
Test-Path "C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
Test-Path "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||
```
|
||||
|
||||
截图(**`--screenshot=` 必须给绝对 Windows 路径 + 绝对 `file:///` 输入**,并且**固定用
|
||||
`--headless=new`**:本窗用旧 `--headless` 同一套命令时成时不成——一次写出了 168 KB,
|
||||
下一次静默不写文件;也别把 stderr 丢进 `/dev/null`,成功标志就是它自己打印的那行字数):
|
||||
|
||||
```bash
|
||||
mkdir -p ci-artifacts # Chrome 没有建目录的参数,先自己建
|
||||
OUT="D:\\Code\\DTU Code\\<工作树名>\\ci-artifacts\\<主题>.png" # 绝对路径、反斜杠
|
||||
"/c/Program Files/Google/Chrome/Application/chrome.exe" \
|
||||
--headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \
|
||||
--window-size=1060,712 \
|
||||
--screenshot="$OUT" \
|
||||
"file:///D:/Code/DTU Code/<工作树名>/docs/images/<主题>.svg" 2>&1 | tail -2
|
||||
```
|
||||
|
||||
成功标志是 Chrome 自己打印的 `<N> bytes written to file D:\...`。**回读 PNG 才算目检完成**:
|
||||
|
||||
```ts
|
||||
const r = await tools.read_image({ file_path: "D:\\Code\\DTU Code\\<工作树名>\\ci-artifacts\\<主题>.png" });
|
||||
console.log(r.image.width + "x" + r.image.height, "bytes=" + r.image.bytes); // 应等于 --window-size / 画布
|
||||
```
|
||||
|
||||
看什么:文字压框 / 溢出盒外、标签互撞(本窗实测两轮才干净:台阶图首轮"文字压到盒下方 + 标签互撞",第二、三轮才修完)、`wrap` 收尾的省略号 `…`(有 = 文案超长)、虚线与盒边线重叠、配色是否还在调色板里。发现问题**回改生成器常量并重跑预检**,然后**重新截图**(PNG 是派生物,不会自己更新),`--window-size` 跟着 `canvas WxH` 改。
|
||||
|
||||
## 四、md 引用(相对路径)
|
||||
|
||||
`docs/` 根的 md:``;`docs/任务规划/`、`docs/UI设计/` 等子目录 md:``(本窗实例:`docs/任务规划/数据可信.md` 写 ``)。alt 文本写成"这张图的结论是什么",别只重复文件名。
|
||||
|
||||
## 五、generated-fresh:逐字节幂等门禁(新图长期不假红的前提)
|
||||
|
||||
`generated-fresh` 把 `tools/arch_diagram/gen_*.py`(`docs-svg` 项:`per_script: true`、`pattern: gen_*.py`、`mode: exact`)**逐个拷进系统临时目录重跑**,与工作区产物**逐字节**比对:内容不同 = `DIFF`(生成物比生成器旧)、临时目录多出一个工作区没有的文件 = `NEW`、`rc≠0` = 「生成器报错」。由此推出四条硬约束:
|
||||
|
||||
1. **生成器只能写那一个 `.svg`**:绝不在脚本里顺手导出 PNG 预览(预览写 `ci-artifacts/`,不进 `docs/images/`)。
|
||||
2. **stdout 不许崩**:GBK 终端遇上 `²` / `−` / CJK 路径会 `UnicodeEncodeError` → 钩子 `rc≠0`,一片假红。所以打印一律走 `emit()`,脚本里不留裸 `print()`(本窗实测:两份新生成器只改了一份,另一份照旧崩)。
|
||||
3. **产物内容零不确定**:SVG 里不写时间戳 / 绝对路径 / 主机名;浮点坐标一律 `%g` / `%.1f` 定形;字典与 `os.listdir` 结果参与布局时先 `sorted()`。
|
||||
4. **`gen_*.py` 命名即自动在册**(pattern 就是 `gen_*.py`)——故意不叫这个名字等于躲开门禁,门禁最大盲区正是"未登记的生成器",别利用它。
|
||||
|
||||
自查(仓库根):
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python.exe tools/arch_diagram/gen_<主题>_svg.py
|
||||
sha1sum docs/images/<主题>.svg
|
||||
.venv/Scripts/python.exe tools/arch_diagram/gen_<主题>_svg.py # 重跑后 sha1 必须不变
|
||||
.venv/Scripts/python.exe scripts/check_generated_fresh.py # 期望 rc=0,无 DIFF/NEW,[BASELINE] 不增
|
||||
```
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py # 单独跑(不 stash 别人在飞的改动)
|
||||
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py --list # 只看在册声明
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run generated-fresh --files tools/arch_diagram/gen_<主题>_svg.py docs/images/<主题>.svg
|
||||
```
|
||||
|
||||
⚠ **别跑不带 `--files` 也不带 `--all-files` 的裸 `pre_commit run`**(会 stash 掉别人在飞的改动);判读口径 → `docs/门禁说明/generated-fresh.md` §4。
|
||||
|
||||
## 六、提交
|
||||
|
||||
生成器脚本(源)+ `docs/images/<主题>.svg`(产物)+ 引用它的 `.md` + `tools/arch_diagram/README.md`「产物清单」那一行**同一笔**提交;PNG 预览不入库。
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --files <脚本> <svg> <md> <README.md>
|
||||
```
|
||||
|
||||
## 陷阱(本窗实测)
|
||||
|
||||
- **想装渲染器再转 PNG**:`cairosvg` 不在 `.venv`(`ModuleNotFoundError`)、`requirements.txt` 里也没有,`rsvg-convert` / `inkscape` / `magick` 这台机器上都不存在 → 用 §三 的 headless Chrome/Edge(权威命令在 `docs/架构设计/图形绘制流程.md` §四,2026-10-30 已同步成这套,别再往回写)。**没真渲染出 PNG 的情况下不要声称"已目检"。**
|
||||
- **`--screenshot=ci-artifacts/ladder.png`(相对路径)** → `EXIT 2` + `Failed to write file ci-artifacts/ladder.png: 系统找不到指定的路径。 (0x3)`,PNG 压根没落盘;换绝对 Windows 路径后写出 95549 bytes。这条报错**不区分**"相对路径"与"目录不存在"两种成因(实测 `mkdir -p ci-artifacts` 已先跑过、仍报同一句),排查时两条都排。
|
||||
- **只做预检 + 缩略目检就收工(大图)**:`check_fit` 归零、PNG 看着干净,仍可能有二十来条标签压框——节点白底后画会吃掉压上去的标签;大图必须走 §三b 的解析式审计。
|
||||
- **只做预检就收工**:`check_fit` / `OVERFLOW` 归零也不代表干净——本窗实测"门标签压第 5 级盒、蓝底标签挤、多余虚线"三类碰撞只有 PNG 回读看得见;反过来只目检不预检,脚注溢出就得靠眼睛反复量。**两者都要 0**。
|
||||
- **手改 SVG 成品**:等于制造「不是我改的」diff,且 `generated-fresh` 立刻 DIFF;也**不要退化成 ASCII 框图**(AGENTS.md 明令勿手绘)。一切修改都落在生成器常量上再重跑。
|
||||
- **批量改生成器时 `old_string` 不匹配**:一批 `edit` 会在中途抛错,但**前面的编辑已经落盘** → 半成品生成器(本窗实测:`emit` helper 缺失却仍打印 OK,几轮之后才查出)。正解:改前先 `read` 出精确原文,逐条改,末尾统一复验(`grep -n "def emit"`、`grep -c print`、再重跑看 rc 与 OK 行)。
|
||||
- **PNG 尺寸 ≠ 画布**:`--window-size` 忘了跟着 `W`/`H` 改 → 截出来带大片空白或裁掉底部脚注;回读 `r.image.width/height` 与 `canvas WxH` 对一下再判定。
|
||||
- **`docs/images/` 下手写 `.svg` 或放 PNG 预览**:前者没有生成器(NEW / 躲过门禁),后者污染"临时目录多了文件"判定。
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 生成器输出 `OK … bytes, canvas WxH`,**无** `OVERFLOW WARN` / `!! N overflow warnings`;`grep -n "def emit"` 命中、`grep -c print` = 0。
|
||||
- [ ] 绝对路径 PNG 存在、尺寸 = 画布,`read_image` 回读确认:无文字压框 / 溢出、无标签互撞、无 `…` 截断收尾、配色在调色板内。
|
||||
- [ ] 连跑两次产物 sha1 不变;`scripts/check_generated_fresh.py` rc=0(无 DIFF / NEW,`[BASELINE]` 未增加)。
|
||||
- [ ] 大图(≥20 边):跑一遍 §三b 的碰撞审计,报数归零(标签压框 / 走线穿框 / 标签出画布)。
|
||||
- [ ] 引用它的 `.md` 相对路径正确(根目录 `images/`,子目录 `../images/`);`tools/arch_diagram/README.md`「产物清单」已加行,「挂在」列的 `文档:行号` 用 `grep -rn "<图名>.svg" docs/ --include=*.md` 现扫核对(**别抄旧行号**,行号一漂整列就废)。
|
||||
- [ ] 脚本 + 产物 `.svg` + `.md` + README 行一并暂存;`pre-commit run --files …` 全过(无乱码 / 行尾空白 / 编码 OK)。
|
||||
|
||||
> **与规范文档的分工(2026-10-30 已对齐)**:`docs/架构设计/图形绘制流程.md` 是规范归属(AGENTS.md:同一事实只在一处详述),本 skill 是执行侧。该文档 §3.1 第 4 步与 §四 原先写的「浏览器打开 SVG / 给 `.venv` 装 cairosvg 转 PNG」在本机走不通,已同步成 headless Edge/Chrome 那套(命令只在 §四 详述,本 skill §三 留判据指针);**改渲染命令时两处一起改**——做图判据以本 skill 为准,规范条文以该文档为归属。
|
||||
- [ ] 生成器输出 `OK … bytes`(无异常 = XML 合法)
|
||||
- [ ] 浏览器目检:文字不溢出、框 / 线 / 标签对齐、配色符合调色板
|
||||
- [ ] 引用它的 `.md` 里图片**相对路径**正确(能显示)
|
||||
- [ ] 脚本 + 产物 `.svg` **一并**暂存
|
||||
- [ ] `pre-commit run --files <脚本> <svg> <md>` 全过(无乱码 / 行尾空白 / 编码 OK)
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""一次性碰撞审计(拷进 `ci-artifacts/` 用,**不要放进 `tools/arch_diagram/`**):
|
||||
解析自己生成的 SVG,报「边标签压节点 / 走线穿框 / 标签互压 / 文字出画布 / 节点文字出框 /
|
||||
标签余量过窄」,并打印最紧的三条「标签文字 ↔ 最近框」水平余量。
|
||||
|
||||
为什么需要它:`check_fit` 预检只量**框内**文字;几十框几十边的大图,
|
||||
标签压框与走线穿框在缩略 PNG 里根本看不全(实测 42 框 37 边报出 23 条),
|
||||
而像素回读又慢又主观。解析产物是确定性的。
|
||||
|
||||
依赖:待审图对应的生成器模块必须能被 import(脚本靠它复用同一把估宽尺子 `text_w()`
|
||||
与几何常量 `CW` / `CH` / `EDGE_FONT` / `ECOL`,两边同尺子才不会假绿)。
|
||||
生成器侧要求:边标签白片用 `fill-opacity="0.94"`、节点框用 `fill="#ffffff"` 且
|
||||
`width==CW`、`height==CH`(本脚本靠这两条分诊);文字用 `<text font-size fill>`。
|
||||
|
||||
**这一遍照不到的东西**(别把它当唯一判据):
|
||||
- 走线检查必须把框**内缩**几像素,否则合法的「线接在框边上」会全报成穿框——从 SVG
|
||||
反推不出哪两个框是本边的端点。代价:1~3px 的切边也照不到。要零余量就在**生成器侧**
|
||||
再用 `Box` / `route()` / `label_at()` 的真值扫一遍(线段 vs 不膨胀的框,只跳本边两端)。
|
||||
- 白片比通道宽一点、被节点白底盖住边角:无害(白片先画、节点后画),不用改;
|
||||
要盯的是**文字**的余量,上面那三条数就是。
|
||||
- 自写的判据脚本先拿一条**已知干净**的边验它不报,再信它的「0 条」:实测一条
|
||||
`hi > x0` 写成 `hi > y0` 的复制粘贴错,凭空报出 3 条「穿框」,差点为此重排走线。
|
||||
|
||||
用法(仓库根):
|
||||
python ci-artifacts/label_audit.py <生成器模块名> [svg 路径]
|
||||
# 例:python ci-artifacts/label_audit.py gen_arch_endtoend_svg
|
||||
# python ci-artifacts/label_audit.py gen_wave_record_flow_svg docs/images/故障录波数据流.svg
|
||||
退出码:0 = 无问题,1 = 有问题(打印明细)。
|
||||
别的生成器若没有同名常量(多数用 `CHIP_FONT` / 自己的线色变量),改脚本顶部 `EDGE_COLORS` 与 `edge_labels` 那两处筛选即可——估宽必须继续用该生成器自己的 `text_w()`,别换一把尺子。
|
||||
"""
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
sys.path.insert(0, "tools/arch_diagram")
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
mod_name = sys.argv[1]
|
||||
G = __import__(mod_name)
|
||||
svg_path = sys.argv[2] if len(sys.argv) > 2 else G.OUT
|
||||
|
||||
MASK_OP = "0.94" # 边标签白片的 fill-opacity(生成器侧约定)
|
||||
EDGE_COLORS = set(G.ECOL.values()) # 边标签颜色(生成器侧的线色表)
|
||||
|
||||
def box_of_text(el):
|
||||
"""<text> → (x0, y0, x1, y1, 文本, 字号, 颜色)。baseline y 之上约 0.85em、之下 0.22em。"""
|
||||
x, y = float(el.get("x")), float(el.get("y"))
|
||||
fs = float(el.get("font-size"))
|
||||
s = "".join(el.itertext())
|
||||
w = G.text_w(s, fs)
|
||||
a = el.get("text-anchor", "start")
|
||||
x0 = x if a == "start" else (x - w / 2 if a == "middle" else x - w)
|
||||
return (x0, y - fs * 0.85, x0 + w, y + fs * 0.22, s, fs, el.get("fill"))
|
||||
|
||||
def area(a, b):
|
||||
ix = min(a[2], b[2]) - max(a[0], b[0])
|
||||
iy = min(a[3], b[3]) - max(a[1], b[1])
|
||||
return ix * iy if (ix > 0.5 and iy > 0.5) else 0.0
|
||||
|
||||
def segs_of(d):
|
||||
n = [float(v) for v in d.replace("M", " ").replace("L", " ").replace(",", " ").split()]
|
||||
pts = list(zip(n[0::2], n[1::2]))
|
||||
return list(zip(pts, pts[1:]))
|
||||
|
||||
def seg_hits_rect(sg, r, m=2.0):
|
||||
"""轴对齐线段 vs 矩形(内缩 m 排除贴边的合法端点)。斜段一律算可疑。"""
|
||||
(x0, y0), (x1, y1) = sg
|
||||
rx0, ry0, rx1, ry1 = r[0] + m, r[1] + m, r[2] - m, r[3] - m
|
||||
if abs(y0 - y1) < 0.5:
|
||||
return ry0 < y0 < ry1 and min(x0, x1) < rx1 - m and max(x0, x1) > rx0 + m
|
||||
if abs(x0 - x1) < 0.5:
|
||||
return rx0 < x0 < rx1 and min(y0, y1) < ry1 - m and max(y0, y1) > ry0 + m
|
||||
return True
|
||||
|
||||
root = ET.parse(svg_path).getroot()
|
||||
W, H = float(root.get("width")), float(root.get("height"))
|
||||
texts, nodes, masks, paths = [], [], [], []
|
||||
for el in root.iter():
|
||||
tag = el.tag.split("}")[-1] # 去命名空间,否则一个都匹配不上
|
||||
if tag == "text":
|
||||
texts.append(box_of_text(el))
|
||||
elif tag == "rect":
|
||||
w, h = float(el.get("width")), float(el.get("height"))
|
||||
x, y = float(el.get("x")), float(el.get("y"))
|
||||
if el.get("fill") == "#ffffff" and abs(w - G.CW) < 0.6 and abs(h - G.CH) < 0.6:
|
||||
nodes.append((x, y, x + w, y + h, "node"))
|
||||
elif el.get("fill") == "#ffffff" and el.get("fill-opacity") == MASK_OP:
|
||||
masks.append((x, y, x + w, y + h, "mask"))
|
||||
elif tag == "path" and el.get("fill") == "none":
|
||||
paths.append(segs_of(el.get("d")))
|
||||
|
||||
edge_labels = [t for t in texts if t[6] in EDGE_COLORS and t[5] == G.EDGE_FONT]
|
||||
bad = []
|
||||
for lb in edge_labels:
|
||||
b = lb[:4]
|
||||
for n in nodes:
|
||||
a = area(b, n)
|
||||
if a > 6: # 压到框边 6 px² 以上就会被节点吃掉半个字
|
||||
bad.append("标签压节点 %-12s 压 node@(%.0f,%.0f) %.0f px²" % (lb[4][:12], n[0], n[1], a))
|
||||
for ob in edge_labels:
|
||||
if ob is not lb and area(b, ob[:4]) > 6:
|
||||
bad.append("标签互压 %s / %s" % (lb[4][:10], ob[4][:10]))
|
||||
if b[0] < 2 or b[2] > W - 2 or b[3] > H - 2 or b[1] < 2:
|
||||
bad.append("标签出画布 %s box=(%.0f,%.0f,%.0f,%.0f) 画布 %.0fx%.0f"
|
||||
% (lb[4][:12], b[0], b[1], b[2], b[3], W, H))
|
||||
for segs in paths:
|
||||
for sg in segs:
|
||||
for n in nodes:
|
||||
if seg_hits_rect(sg, n):
|
||||
bad.append("走线穿框 (%d,%d)-(%d,%d) 压 node@(%.0f,%.0f)"
|
||||
% (sg[0][0], sg[0][1], sg[1][0], sg[1][1], n[0], n[1]))
|
||||
break
|
||||
# 「节点文字出框」只能按几何判:文字中心落在某个节点框里,却没被该框完整包住。
|
||||
# 别拿 font-size 当筛选——本仓惯例里副题字号 = 边标签字号 = 脚注字号(9.5),按字号筛会全灭。
|
||||
for t in texts:
|
||||
cx, cy = (t[0] + t[2]) / 2, (t[1] + t[3]) / 2
|
||||
host = [n for n in nodes if n[0] <= cx <= n[2] and n[1] <= cy <= n[3]]
|
||||
for n in host:
|
||||
if not (t[0] >= n[0] - 1 and t[2] <= n[2] + 1 and t[1] >= n[1] - 1 and t[3] <= n[3] + 1):
|
||||
bad.append("节点文字出框 %r 超 node@(%.0f,%.0f)" % (t[4][:16], n[0], n[1]))
|
||||
break
|
||||
|
||||
# 余量比「有没有压上」更早暴露问题:本仓约定边标签整段落在框间通道里,
|
||||
# 只要文字离最近框 < 3px,字宽估算稍偏(本脚本与生成器共用同一把尺子,
|
||||
# 但真实字体度量和它有出入)就会被后画的节点吃掉笔画。
|
||||
MIN_CLEAR = 3.0
|
||||
clears = []
|
||||
for lb in edge_labels:
|
||||
b = lb[:4]
|
||||
worst, wid = None, None
|
||||
for n in nodes:
|
||||
if min(b[3], n[3]) - max(b[1], n[1]) <= 0: # 竖直方向不相干
|
||||
continue
|
||||
c = max(n[0] - b[2], b[0] - n[2]) # 正=间距,负=压进去多深
|
||||
if worst is None or c < worst:
|
||||
worst, wid = c, (n[0], n[1])
|
||||
if worst is not None:
|
||||
clears.append((worst, lb[4][:12], wid))
|
||||
if worst < MIN_CLEAR:
|
||||
bad.append("标签余量过窄 %r 距 node@%s 仅 %.1fpx(< %.1f)"
|
||||
% (lb[4][:12], wid, worst, MIN_CLEAR))
|
||||
|
||||
seen = []
|
||||
for b in bad:
|
||||
if b not in seen:
|
||||
seen.append(b)
|
||||
print("%s:边标签 %d / 节点 %d / 文字 %d / 路径 %d"
|
||||
% (svg_path, len(edge_labels), len(nodes), len(texts), len(paths)))
|
||||
for c in sorted(clears)[:3]:
|
||||
print(" 最紧余量 %.1fpx 标签 %r 邻 node@%s" % (c[0], c[1], c[2]))
|
||||
for b in seen:
|
||||
print(" ! " + b)
|
||||
print("合计问题 %d 条" % len(seen))
|
||||
sys.exit(1 if seen else 0)
|
||||
@@ -34,5 +34,5 @@ python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 →
|
||||
- `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 解码,中文注释 → UnicodeDecodeError)
|
||||
- 生僻字(宋体可能无字形)→ **写码前先手动验证字形**,避免"代码写完才发现是空白"
|
||||
- 生成成功后跑 `ui-verify` skill 全回归;黄金参考因字形变化而 FAIL 属预期 → `check_content_pages.py --update` 刷新
|
||||
- 字数变化时同步更新文档中的字符集计数(当前 **224** 汉字/符号 + ASCII 0x20-0x7E,共 **319** 字形;2026-10-29 实测:`ui_cn_chars.txt` 唯一码点 224、`ui_font_cn_12.c` 的 `.bitmap_index` 320 条含 notdef → 319 字形。历史上 119/214 → 179/274 → 185/280 → 218/313 均为当时快照):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4
|
||||
- 字数变化时同步更新文档中的字符集计数(当前 179 汉字/符号 + ASCII 0x20-0x7E,共 274 字形):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4
|
||||
- **新增字低于既有最小码位**(如 Ⅰ U+2160)会下探稀疏 cmap 起点并吞掉单码位补回条目(实测 ▶ 箭头全变方框而旧版 coverage 全绿)——`make_font.py`(2026-10-25 起)按 LVGL 首命中语义插入/守卫/校验,扩字后模拟器像素回归**必跑**
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
name: id-family-semantic-migration
|
||||
description: 编号族整族清退/停发 playbook:把一个编号族(字母+数字如保护功能号 L1-x~L5-x、网络条目号 F-*、登记簿旧号 N-x、台架阶段裸形 T1~T3、用例号 TC-*,或中文前缀「卡」+ 数字这类门禁语料里**没有机器识别面**的族)在全仓代码注释、测试、活文档与门禁配置里改成语义短语(含销项删行与逐号换算表入档),或把「不清退」裁定写成硬口径入档。触发语:清退某族编号、编号改成语义名或语义短语、还有多少号可清、残留多少、批次二/批次三/批次五、「卡 0」「卡 0b」「卡 1~12」、销项删行并去除相关编号、id-check 报悬空引用或不可判定超限、不可判定引用绝对数涨点、「这一族不做了」。
|
||||
whenToUse: 用于**整族**处理:先判这一族有没有机器识别面 → 清点存量 → 逐族分类 → 分批派单清退 → 收口 → 回写 docs/编号规范.md 对应节。单条待办行销项并写 void 别名用 ledger-row-retire-void-id;某个事实数(页数、叶数、钩子数)变了要同步全仓用 stale-doc-count-sweep;单个文件改名或挪目录用 scripts/rename_ref.py;跑 HEAD 版对照组用 task-worktree 的净室 worktree。这四类不由本技能覆盖,但「涨点归因」一节会用到净室 worktree。
|
||||
version: 1.1.0
|
||||
---
|
||||
|
||||
# 编号族清退成语义标识
|
||||
|
||||
**2026-10-29 用户裁定:全族一律停发**(含 `docs/编号规范.md` §4.1 那五个存量族)——本 playbook 的「停发」分支默认已成立,
|
||||
现在唯一还需要决策的是**要不要清退存量**(全仓改语义 + 销项删行 + 逐号换算表入档)。
|
||||
|
||||
**动手前先判这一族有没有「机器识别面」**——它决定计数口径的来源与落地去处,两条路的做法**正好相反**:
|
||||
|
||||
| 族形态 | 计数口径来源 | 改名手段 | `scripts/archive/id_gate/id_aliases.json` |
|
||||
|---|---|---|---|
|
||||
| 字母+数字,且在 `scripts/archive/id_gate/check_ids.py` 语料内(`id_registry.json` 有族、族 pattern 能匹配它) | 门禁自身计数(`ck.refs` / `ck.stats` / 报告行) | `scripts/rename_ids.py`(映射表 + 显式 scope + 默认干跑) | **要登记**:死号 `void`、转短语 `retired-to-phrase` |
|
||||
| 中文前缀(「卡」+ 数字)、纯散文里的号、圈号 ①②③ | **自建带标签正则扫描器**(门禁看不见它,2026-10-28 退役前的 id-check 全库报告与本族无关;两道门禁现都已退役(2026-10-28 / 2026-10-29),要普查形态就手工跑归档脚本 scripts/archive/id_gate/check_ids_new.py --range) | 逐处替换 + 断言命中数(`rename_ids.py` 靠族 pattern 驱动,这里用不上) | **刻意不登记**:登记了机器也看不见,只多一张没人核的表;停发只靠规范 + 评审 |
|
||||
|
||||
怎么判(三条,任一为否即无识别面):① `grep -nE '"name"|"pattern"' scripts/archive/id_gate/id_registry.json`,没有任何族 pattern 能匹配该形态;② `./.venv/Scripts/python.exe scripts/archive/id_gate/check_ids.py --top 0` 的任何校验里找不到它;③ registry 的 `blind_spots` 已明写不判(圈号与正常列举无法机械区分、裸形同字母多义只在属主登记处判)。实测:「卡」+ 数字**没有族**(中文形态只有 `malformed` 里 `陷阱 ?[0-9]` 那条禁令,管的是陷阱编号不是卡号),该族真实站点 288 处 / 56 文件,而门禁对它贡献 0。
|
||||
|
||||
判错的代价是双向的:拿门禁计数当中文字面量族的口径 → 得出「0 处待改」的**假归零**;给无识别面的族登记别名 → 制造「有机器兜底」的错觉,真正生效的两条(§4.1.1 停发行、§9.3 换算表)反而没人写。
|
||||
|
||||
其余铁律照旧:**不要凭 grep 估数、不要凭印象决定改哪几处、不要把「不做」只划掉一行。**
|
||||
|
||||
## 口径:三套计数都要挑明,先问再比
|
||||
|
||||
| 口径 | 怎么数 | 用途 |
|
||||
|---|---|---|
|
||||
| 门禁 refs(去重) | `ck.refs`:字面量 → 文件集合,**只记属主登记处文档之外**的引用 | 门禁趋势、批次验收线 |
|
||||
| 逐行站点 | 按族正则重扫 `ck.iter_files()` 语料,**含属主文档的定义行** | **排工作量、拆单** |
|
||||
| 自建扫描器站点 | 无识别面族:带标签正则全仓 walk(排除 `.git`) | 该族唯一口径,**不能拿 id-check 输出代替** |
|
||||
|
||||
实测例:保护功能号一族「字面量 28 / 站点 184 / 文件 58」(`ck.refs`)与队友报的「219 站点 / 38 文件」并存且都对。队友报任何合并数(指纹、站点数)之前**先问口径**,不要拿自己的数去否他的数。**每个数字标明是本回合实测、动手前基线还是沿用昨日值**;**形似噪声号另立分母**(中文族一节第 4 步),绝不与待清族合并成一个总数。
|
||||
|
||||
## Steps(字母+数字族:门禁看得见的那一路)
|
||||
|
||||
1. 确认范围与定调
|
||||
2. 按族实测三口径(字面量 / 站点 / 文件),不用 grep 估数
|
||||
3. 逐族先分类再动手
|
||||
4. 映射表由属主文档生成
|
||||
5. 按写域拆道并行派单
|
||||
6. 门禁配置注释也是本族写入面
|
||||
7. 每处替换断言命中数
|
||||
8. 同步更新门禁侧配置
|
||||
9. 生成器文本改动必须同笔重跑产物
|
||||
10. 核队友交付
|
||||
11. 分笔提交走真实 pre-commit 钩子
|
||||
12. 收口只认门禁自身计数
|
||||
13. 回写 docs/编号规范.md
|
||||
14. 裁定「不做」也要写成可执行硬口径入档
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [steps-alpha-numeric-family.md](reference/steps-alpha-numeric-family.md)。动手前先读那篇。
|
||||
|
||||
## Steps(中文前缀族:连归档扫描工具都咬不到的那一路)
|
||||
|
||||
1. 先确认连归档扫描工具也咬不到(两道编号门禁 2026-10-28 / 29 已退役,"咬不咬"从此只是工具层面的事),并把这句话写进交付
|
||||
2. 销项删行前先核证据属主
|
||||
3. 自建带标签正则扫描脚本
|
||||
4. 该族与形似噪声号分开计数,各自给分母
|
||||
5. 派单前写一份 spec 文件
|
||||
6. 逐处替换断言命中数
|
||||
7. 落地位置写死(这一族只有这几条兜)
|
||||
8. 替换后三道二次清洗
|
||||
9. 收尾复跑 + 清中间产物
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [steps-chinese-prefix-family.md](reference/steps-chinese-prefix-family.md)。动手前先读那篇。
|
||||
|
||||
## 不可判定引用绝对数涨点的净室归因
|
||||
|
||||
「不可判定引用绝对数」比今日低点涨了 N 处时:
|
||||
|
||||
1. **禁止**用「对提交新增行 grep 字母+数字形态」归因:`git diff -U0` 的加行里绝大多数文本是从旧行搬来的**存量上下文**(注释整段重写、表格加一列、Markdown 重新折行),形态匹配数出来的是「哪段文字最近被搬动」,不是「哪里涨了 N 处」——用它归因必然把老引用算成增量。`diff -U0` 只在**已定位到文件之后**用来绑笔次与行号。
|
||||
2. **正解 A(首选,同一计数器相减)**:在**仓外 detached worktree 跑 HEAD 版**(`task-worktree` skill 的净室测量仪;不 stash、不碰在飞工作区),两边都用同一个 `check_ids.py` 计数器导出 `(rel, literal)` 多重集(`ck.findings` 或 `err_groups(ck)`),做**逐文件集合差**,落差的 N 处直接指向具体文件与字面量。
|
||||
3. **正解 B(逐文件注入)**:一次只把一个文件换成在飞版跑计数器——涨点在哪个文件上,就只在注入它时出现;文件多时按 `git diff --name-only` 折半。
|
||||
4. **当轮跑不完就如实说**:本轮未跑净室归因时,只报「现值需要重新实测」并说明**需要跑净室归因**,**不得**给出候选归因对象,**不得**把上一轮的现值或余量当本回合实测值报出(每个数标明来源与时刻)。
|
||||
5. 与其它交付口径冲突的差值**登记为待查项**(两套计数可能都对),不当场判谁对谁错。
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- 拿门禁计数当中文字面量族的口径
|
||||
- 给无机器识别面的族登记 id_aliases.json
|
||||
- 拿 grep 计数跟人吵口径
|
||||
- heredoc 里嵌 Python 正则 / 中文
|
||||
- 扫描没排除 .git
|
||||
- 凭记忆敲 old 串
|
||||
- 替换后不做同句重复检测
|
||||
- 换算表里复述字面量
|
||||
- | head 之后读 $?
|
||||
- 占位符泄漏
|
||||
- 删行前不核证据属主、不查当初为什么保留
|
||||
- 涨点归因拿 diff 新增行 grep 形态
|
||||
- 全局查找替换
|
||||
- 未分类就动手
|
||||
- 按简称匹配文件清单
|
||||
- 复现不了队友的合并指纹就怀疑丢文件
|
||||
- 替换 token 含
|
||||
- 只改生成器文案不重跑产物
|
||||
- grep 'A\|B' 空返回当文件干净
|
||||
- 一次 run_code 串 5 × wait_agent(118000) ≈ 590s 整段 abort、进度全丢
|
||||
- 游标整型比较静默不生效
|
||||
- git add 目录路径带进别道文件
|
||||
- amend 被 gitlint B1 拦
|
||||
- 改「只动注释」要拿机器证据
|
||||
- 冻结历史与原始证据不改
|
||||
- *.uvprojx 抖动不入库
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [pitfalls.md](reference/pitfalls.md)。动手前先读那篇。
|
||||
|
||||
## Verification
|
||||
|
||||
1. `scripts/archive/id_gate/check_ids.py --strict` 与 plain **均 rc=0**(`[OK] 校验 1~6 无错误`),悬空引用为 0。
|
||||
2. 编号字面量与**不可判定引用绝对数**只降不升、不超 `undecidable_total_ceiling`;可判定比例不低于 `id_registry.json` 基线(86.0%)。
|
||||
3. 本族剩余站点复扫归零:有识别面族从 `ck.refs` 按族正则过滤字面量逐条打印;除冻结历史/原始证据行外无残留。
|
||||
4. `pre-commit run generated-fresh --all-files` PASS + 目检 svg diff 只有文案与框宽自适应(只在动过生成器时适用);`check_doc_links.py --all` 通过。
|
||||
5. `/keil-build`:`BUILD=0`、0 错 0 警、`Program Size` 与改前同数(零行为改动证据,**本回合真跑**);相关主机组(如 `net_socket 457/457`)全绿。
|
||||
6. 真实 pre-commit 钩子全绿(如 `passed=33 failed=0`);提交笔内不含他道文件。
|
||||
7. 规范与台账已回写并复跑核数:自己先前写进规范的清点数字要么按最终实测改对,要么标明为动手前基线;「等你裁定」残留 = 0;「不做」裁定已在 §4.1.1 + §9.1 + 批次结论段落三处落地(原「变更记录」那一处已随 2026-10-28 清流水退场)。
|
||||
8. **无识别面族**:`git grep -nE '卡[ ]*[0-9]' -- .` 无输出(裸跑读 rc);自建扫描器的站点/文件数与提交正文一致;交付里明写「本族不在 check_ids 语料内、门禁判不到」与「刻意不进 `id_aliases.json`」的理由;§4.1.1 停发行 + §9.3 换算表 + 批次结论段落三处齐全,换算表行数 = 该族字面量数(实测 14),且写明旧号永久作废、不得重新取走。
|
||||
9. **二次清洗三道各有实测数**:同句重复短语 0 行、各空格形态先计数后归一(复扫 0)、固定正则残留 0;被代码头注释引用的设计约束章节仍在(`git diff` 里没被动过)。
|
||||
10. **叙述同步**:`AGENTS.md` / `docs/README.md` / `docs/任务规划/README.md` / `docs/任务规划/登记簿.md` 里旧口径(「三项」「卡 N」类)实测归零;`ci-artifacts/` 里本次 spec 与中间产物已删。
|
||||
11. 门禁复跑清单齐:`check_ids.py` plain + `--strict`、`check_doc_links.py --all`、`bom-guard`、`no-mojibake`、`git diff --check`、`/keil-build` 或受影响单测——**每个数字标明是本回合实测**。
|
||||
@@ -1,30 +0,0 @@
|
||||
# Pitfalls(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 id-family-semantic-migration 的 SKILL.md「Pitfalls」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
- **拿门禁计数当中文字面量族的口径** → 得出「0 处待改」的假归零(实测该族真实站点 288 处 / 56 文件);先跑自建扫描器报「站点 / 文件」,并在交付里写明该族不在语料内、门禁判不到。
|
||||
- **给无机器识别面的族登记 `id_aliases.json`** → 中文前缀不进语料,登记了机器也看不见,只制造「有门禁兜底」的错觉;执行面是 §4.1.1 停发行 + §9.3 换算表 + 评审。反过来,有识别面的族不登记别名 → 门禁把旧号引用判成悬空。
|
||||
- **拿 grep 计数跟人吵口径**:先拿实测三口径,再谈拆单;两套计数不一致不是有人算错(见上表)。**形似噪声号并进总数** → 把 132 处里程碑噪声算进 420 处待改,动手就改坏 `docs/芯片手册/` 原文。
|
||||
- **heredoc 里嵌 Python 正则 / 中文** → `invalid escape sequence` 与语法错;脚本落文件再跑。中文正则用 **raw 串 + `\uXXXX`**:非 raw 串的 `"\b"` 会被解成退格符,正则**静默失配不报错**。
|
||||
- **扫描没排除 `.git`** → `.git/COMMIT_MSG_*.txt` 的 GBK 乱码历史被当残留,越清越多;**靠控制台看中文** → 一律写 UTF-8 报告文件再解析,判乱码统计 `EF BF BD` 字节数。
|
||||
- **凭记忆敲 `old` 串** → 中文引号与 ASCII 双引号形态不同,命中 0;回读原文取真身,断言前先看 `git status` 自证文件未变。
|
||||
- **替换后不做同句重复检测** → 留下「运行参数段(「数据可信」运行参数段交付)」这类句子(实测 12 行);**同一短语两种空格形态不归一** → 全仓口径漂移(归一前先 `git grep -c -F` 数各形态)。
|
||||
- **换算表里复述字面量** → 讲规则的文档变成自己的引用站点(规范本身在门禁语料里)。
|
||||
- **`| head` 之后读 `$?`** → 永远是 0,把「还有残留」当「已归零」。
|
||||
- **占位符泄漏**:为躲转义把反引号写成 `«` / `»` 再全文替换,落盘后**必须核占位符残留 = 0**(实测 22 行泄漏到本节之外,含不是自己写的行)。
|
||||
- **删行前不核证据属主、不查当初为什么保留** → 要么删掉唯一记录,要么把已撤销的「✅ 保留行」例外当常态。
|
||||
- **涨点归因拿 diff 新增行 grep 形态** → 把存量上下文当增量;**把上一轮现值当本回合实测**、未跑净室归因就报具体归因对象 = 猜测。
|
||||
- **全局查找替换**:跨族撞号(`S3` 三义)。必走 `rename_ids.py` 映射表 + 显式 scope + 干跑;无识别面族则逐处手写 + 断言命中数。
|
||||
- **未分类就动手**:把变压器位号、Mermaid 节点名、数学记号、gitlint 规则名当编号改坏硬件资料;位号目录进 `excluded_paths`,记号进噪声表。
|
||||
- **按简称匹配文件清单** → 误报 8 个文件丢失;比对前展开真实路径。
|
||||
- **复现不了队友的合并指纹就怀疑丢文件**:先问口径(自定义顺序不可复现),改落自有逐文件 16 位基线并把跨道文件单列。
|
||||
- **替换 token 含 `**` 加粗标记** → 命中数断言 0;取反引号最小形态。
|
||||
- **只改生成器文案不重跑产物** → `generated-fresh` 必红(实证 `0f7b5a87` 同类);「反正就是注释,不影响图」是错的,该文本进产物。
|
||||
- **`grep 'A\|B'` 空返回当文件干净**:交替竖线在多层 bash 引号里被吃掉,是探针坏不是文件干净 → 反证为空的搜索必须换 `grep -F` 复测。
|
||||
- **一次 run_code 串 `5 × wait_agent(118000)` ≈ 590s 整段 abort、进度全丢**;且一次串多条长中文 `send_message` 也撑爆 120s(发出 1 条后 abort,剩余未发)。轮询上限 4×118s,长消息拆开发。
|
||||
- **游标整型比较静默不生效**(字符串形态);抬完用 `--strict` 与 plain 双验。
|
||||
- **`git add` 目录路径带进别道文件**、`git add -A` 卷进在飞改动;撤销自己的改动只按文件点名 `git restore scripts/archive/id_gate/id_registry.json`,不碰同树其它道文件。
|
||||
- **amend 被 gitlint B1 拦**:正文行 >80,别把 ASCII 双引号写成正文首字符。
|
||||
- **改「只动注释」要拿机器证据**:`/keil-build` 0 错 0 警且 `Program Size` 与改前**逐字节同数**(本回合真跑,引用历史提交正文自报的数字不算证据),别口头承诺。
|
||||
- **冻结历史与原始证据不改**:boot 串口日志等采集现场是冻结证据,改了就是篡改;历史「修改记录」行按裁定保持改名后形态(号已全仓退役,留一个查无此号的引用比留改过名的条目更坏)。
|
||||
- **`*.uvprojx` 抖动不入库**;`ci-artifacts/` 里的 spec 与扫描中间产物收尾删掉,别当交付物留下。
|
||||
@@ -1,66 +0,0 @@
|
||||
# Steps(字母+数字族:门禁看得见的那一路)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 id-family-semantic-migration 的 SKILL.md「Steps(字母+数字族:门禁看得见的那一路)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
1. **确认范围与定调**:先按开头的表判识别面(无识别面 → 走下一节);再读 `docs/编号规范.md` §4.1(族总表,穷举)、§4.1.1(已停发栏),存量与批次见 `scripts/archive/id_gate/编号治理历史.md` §9,明确本族属主登记处(`owner_docs_glob`);旧判据「有脚本按字面量匹配它」自 2026-10-29 起作废——全族一律停发。清退 = 全仓改语义;停发 = 在规范表里写死不再新增引用(`id_registry.json` 的游标冻结只是裁定留痕,2026-10-29 起无机器读者)。
|
||||
2. **按族实测三口径(字面量 / 站点 / 文件),不用 grep 估数**。`Checker.refs` 是 **dict(字面量 → 文件集合)不是方法**,`ck.refs(fam)` 这种写法会 `TypeError`;站点数门禁不直接给,用族正则重扫它自己的语料:
|
||||
```python
|
||||
import sys; sys.path.insert(0, "scripts")
|
||||
import check_ids as ci
|
||||
reg = ci.load_json(ci.REGISTRY_FILE, "r"); al = ci.load_json(ci.ALIASES_FILE, "a")
|
||||
ck = ci.Checker(reg, al); ck.run() # run() 填充 refs / stats / findings
|
||||
for name in ("N", "S-LDG", "TC", "FEAT"): # 族名以 id_registry.json 的 "name" 为准
|
||||
rx = next(f for f in ck.families if f["name"] == name)["_re"]
|
||||
lits = {l for l in ck.refs if rx.fullmatch(l)} # 字面量(属主文档之外)
|
||||
files = {r for l in lits for r in ck.refs[l]} # 文件
|
||||
sites = sum(len(rx.findall(line))
|
||||
for rel in ck.iter_files() # 与门禁同一语料
|
||||
for line in ck.read(rel).splitlines()) # 含属主文档定义行
|
||||
print(name, "literals", len(lits), "sites", sites, "files", len(files))
|
||||
```
|
||||
全库扫一遍不便宜(数十秒量级):可丢后台 job,不要和别的长活串在同一个 120s 预算里。把活族清单与每族数字记下来作为**动手前基线**(拆批次与派单依据)。新测试套件发号导致 `id-check` 校验 3 报悬空引用时,**先按号前缀聚合**定位是哪个套件/哪个族(`check_ids.py 2>&1 | grep -a '悬空引用' | ...` 前缀计数),别逐条读。
|
||||
3. **逐族先分类再动手**——不是每个命中的裸形都是编号,四类分治(实测 T1/T2/T3 的 41 处里只有部分是编号):
|
||||
- 真编号 → 语义短语 / 功能名;
|
||||
- 硬件位号(变压器 T1~T3 等)→ 把目录整目录登记进 `check_ids` 的 `excluded_paths`(如 `excluded_paths += tools/netlist_semantics/`),**不逐处改位号名**;
|
||||
- Mermaid 流程图节点名 → 改语义名但**节点数与连线关系不变**,图结构不动;
|
||||
- 数学记号 → 进噪声表;门禁自身的规则名(gitlint `T2 Title has trailing whitespace`)→ 不改。
|
||||
分类前先把剩余每处原文打出来看(`grep -F` 逐文件),否则会把硬件资料改坏。
|
||||
4. **映射表由属主文档生成**:从属主文档取「编号 → 功能名/语义短语」全表(实测 42 个定义 / 28 个在用),改名走 `scripts/rename_ids.py`(映射表驱动 + **显式 scope** + **默认干跑** + 自动往 `scripts/archive/id_gate/id_aliases.json` 写 scoped 旧号→新号条目,状态 `renamed` / `retired-to-phrase` / `void`)。**绝不全局查找替换**:同一裸形字面量可能同时属多个族(实测 `S3` 三义),全局替换等于造新撞号。
|
||||
5. **按写域拆道并行派单**(代码与测试半 / 文档半 / 某功能面各一条道),同笔用 `send_message` 下发**统一改写口径**(措辞、是否动字符串字面量、生成器归属、冻结历史行怎么处理),避免各道自定措辞;口径长就先落成 spec 文件(中文族一节第 5 步的做法同样适用)。看板任务只认 `team_task_update` 的 `task_id` / `expected_revision` / `action=reassign` / `owner` 四个字段。
|
||||
6. **门禁配置注释也是本族写入面**:`.pre-commit-config.yaml` 的注释里会挂着功能号(实测 `7h) ADC 采样有效性校验(语义取数与有效性层)`、`7i) 保护定值模型(P1 / 动作留痕层载体)`、`规划 §3.3 保护判据层`)。配置属 Lead 写域,Lead 自己改(注释不占钩子行为、不影响 51 条钩子口径),但**必须计入批次范围并在提交正文写清是 Lead 改的 config**。
|
||||
7. **每处替换断言命中数**,不做整文件正则批量改写(批量改写会静默漏改):
|
||||
```python
|
||||
def sub(path, pairs):
|
||||
p = pathlib.Path(path); t = p.read_text(encoding='utf-8')
|
||||
for old, new, want in pairs:
|
||||
got = t.count(old); assert got == want, (path, old, got, want)
|
||||
t = t.replace(old, new)
|
||||
p.write_text(t, encoding='utf-8')
|
||||
```
|
||||
**token 取最小形态**:文档里被反引号/加粗包住的号,替换单位取反引号形(```T4```)而**不含 `**` 标记**(第 225 行是 `**裸形 `T4`**`,按「裸形 `T4`」替就命中 0)。讲编号的文档改成**形态描述**(如「字母 T 紧跟数字 4」)能把不可判定绝对数还回余量:实测 4 处 → 982 降到 978,且 lesson 不丢。
|
||||
8. **同步更新归档扫描器的配置**:`check_ids` 的 `excluded_paths` 与噪声表按第 3 步的分类结果改,使剩余命中不再是「假编号」;新测试套件发号的悬空引用要先定**定义位**:
|
||||
- 定义位**只能落一处**——注册表族(`scripts/archive/id_gate/id_registry.json`)或该套件说明文档,**两处并存比没有定义位更坏**;文档真写出来了(含判据与上板清单)就以文档为准、**撤族**;
|
||||
- 要建族就照现有同类族克隆(`pattern` / `digit_width` / `owner_docs_glob` / `scope` / `cursor`),别自造字段;
|
||||
- **抬游标前先确认游标是字符串形态**(`'cursor': 'TC-UIDATA-026'`)——整型比较会静默不生效(实测踩坑),用字符串解析或正则取序号再比。
|
||||
9. **生成器文本改动必须同笔重跑产物**:`tools/arch_diagram/gen_*.py` 里的 docstring/文案会进产物,`generated-fresh` 把生成器拷到临时目录重跑逐字节比对,**只改源不改产物必红**。这类站点整包转给生成器属主,「谁改谁跑生成器」,脚本 + 全部 svg 产物同一笔:
|
||||
```bash
|
||||
./.venv/Scripts/python.exe tools/arch_diagram/gen_net_instability_svg.py 2>&1 | tail -3
|
||||
pre-commit run generated-fresh --all-files # 单钩子复跑必须带 --all-files 或 --files
|
||||
```
|
||||
⚠ **不带这两个参数的裸 `pre-commit run` 会进 staged_files_only 上下文并 stash 别道在飞的改动**(`docs/门禁说明/generated-fresh.md` §2);`pre-commit` 不在 PATH 时改用 `& '.venv\Scripts\python.exe' -m pre_commit run generated-fresh --all-files`。PASS 后再目检 diff:只应文字变长、框宽自适应(526→551、右缘未越界、画布不变)。不把该站点从批次里悄悄丢掉:登记为「待未完成半批」,收口时核对。
|
||||
10. **核队友交付**(按写域并行后必做):
|
||||
- 文件清单**先展开真实路径再比**:按简称匹配会误报丢失(作者列 `phasor.py`/`adc_health.py` 等 8 个,实为 `test/host/<模块>/run_unit_test.py`);用 `git -c core.quotepath=false diff --name-only` 取全路径做集合差。
|
||||
- 合并指纹对不上先问口径:作者按**自定义顺序**算的数 Lead 侧复现不了(`d0a289b7` vs 报的 `a45e7e2b`)。改建 **Lead 自有逐文件 16 位基线**存 `ci-artifacts/lead_lane_b_files.json`,并把**跨道文件单列**(实测 `App/algo/README.md`)——autostash 后按逐文件比对,不假称对上别人的合并数。
|
||||
11. **分笔提交走真实 pre-commit 钩子**:`git add` 用显式清单。坑:`git add -- App/algo` 把文档道的 `App/algo/README.md`(26 处)带上车,正文按 48 只文件写就不准了 → 该笔未 push 时 `--amend` 挪到文档笔并补一句归属说明;`git add -A` 会把别道在飞文件卷进来(覆盖率钩子在他编译不过的半成品上报红)→ 撤出他人域后重跑。提交正文每行 ≤80(gitlint B1),别把 ASCII 双引号写成正文首字符。
|
||||
12. **收口只认门禁自身计数**(**仅限语料内的族**;无识别面族见下一节第 8~9 步),并核**只减不增**:
|
||||
```bash
|
||||
./.venv/Scripts/python.exe scripts/archive/id_gate/check_ids.py 2>&1 | grep -aE '扫描|可判定 |不可判定引用' | head -4
|
||||
./.venv/Scripts/python.exe scripts/archive/id_gate/check_ids.py --strict 2>&1 | tail -1 # [OK] 校验 1~6 无错误
|
||||
```
|
||||
报告「字面量 8787→8299→7828」「不可判定绝对数 1161→984→982→978(上限 1166)」「可判定比例 88.14%(基线 86.0%,容差 0.1pp)」。**绝对数与比例两根线都要报**:把可判定引用改成散文会稀释比值,所以另有不可判定绝对数上限。涨点归因见「净室归因」一节。
|
||||
13. **回写 `docs/编号规范.md`**:§4.1 族表(停发/待清栏)、§4.1.1 已停发栏;存量表与批次结果表在 `scripts/archive/id_gate/编号治理历史.md` §9(只减不增、清点数字按最终实测同步);表格若保留动手前的清点数,**明确标注「不是当前存量,是动手前基线」**,否则就是拿旧数当现状。**用户裁定原文与理由直接写进对应节**(§4.1.1 停发行 + 该族的批次结论段落)——2026-10-28 起不再往变更记录写流水(`docs/文档规范.md` §5.1)。
|
||||
14. **裁定「不做」也要写成可执行硬口径入档**(不是只划掉一行——「不做」最容易被读成「那还可以继续发号」):
|
||||
- §4.1.1「已停发」栏新增该族一行:**游标冻结、现存号只减不增、新用例改用语义化测试函数名 + 文档表格行名**,并写清与 §4.1 存量族栏的关系(那五族 2026-10-29 起也一律停发);
|
||||
- 批次段落写明裁定原文与理由(归档 `scripts/archive/id_gate/编号治理历史.md` §9.1——§9 一族的事实源已在归档篇,规范正文只留口径)(该族是唯一真被脚本按字面量匹配的族:C 断言标签 + 测试函数名 + 文档用例表三处同一号),并给出**将来真要清退的前置条件**:追溯器(`check_test_traceability.py`)先同时认新旧两种形态、并行核对通过,**不许先改文档再改代码**;
|
||||
- 全文核「等你裁定 / 需你裁定」悬空表述残留数归 0;
|
||||
- 无机器识别面的族落地面不同(`docs/编号规范.md` §4.1.1 一行 + 归档 `编号治理历史.md` §9.3 换算表 + 叙述同步三处),见下一节第 7 步。
|
||||
@@ -1,65 +0,0 @@
|
||||
# Steps(中文前缀族:连归档扫描工具都咬不到的那一路)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 id-family-semantic-migration 的 SKILL.md「Steps(中文前缀族:门禁咬不到的那一路)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
1. **先确认连归档扫描工具也咬不到,并把这句话写进交付**(两道编号门禁已退役,机器面整体消失):`scripts/archive/id_gate/check_ids.py` 的族 pattern 是拉丁形态、中文前缀不进语料 → **id-check 的 refs / 可判定比例 / 悬空引用与本族无关**;报告开头写「本族不在 check_ids 语料内,口径 = 自建扫描器」。
|
||||
2. **销项删行前先核证据属主**(这是删行的唯一许可证):
|
||||
- 先取历史口径:`git log --oneline -20 -- <文档>`、`git show <rev>:<文档> | sed -n '/^## 5/,/^## 6/p'` 找回被删的卡表与当初「完成即销项删行 / 标 ✅ 保留行」的理由;
|
||||
- 核对判据与实测记录**已完整沉淀进属主章节**(实测:「已知电压注入验证」六项判据全 PASS、斜率 6544.2 码/V、偏差 −0.14 % 全在 `docs/硬件平台/ADC通道映射说明.md` §8.1,执行侧在 `docs/测试说明/专项验证/ADC前端注入验证流程.md`);
|
||||
- **删的是任务规划侧的行与结论块,不是证据本体**;被代码头注释指称的设计约束章节(§2~§4 那类)**原样保留**,只删已完成记录;
|
||||
- 该行若当初是被「代码头注释按号引用」当例外保住的(实测 `App/task/adc_task.c` 的 v3.3 修订行按卡号当口径出处),必须**先把引用侧改成语义短语**,例外才成立撤销,并在规范里写明撤销理由(上一笔「完成即保留行」的例外就是这么随引用侧改动被撤掉的);
|
||||
- 删完逐行通读被改小节,报**行数变化**,不留孤立残句(实测 J7 dropped line 234 len=639、J13 闭环结论块 6 行删除并注明证据属主)。
|
||||
3. **自建带标签正则扫描脚本**——四条硬规矩,全是实测返工换来的:
|
||||
- 脚本**落文件**(`ci-artifacts/scan_ka_refs.py`)再跑,**不用 bash heredoc**(heredoc 里的 `\s` 被多层引号咬坏:`SyntaxWarning: invalid escape sequence` + 语法错);
|
||||
- 正则**一律 raw 字符串**、汉字写 `\uXXXX`:`KA = r"\u5361"`。非 raw 串里 `"\b"` 会先被 Python 解成退格符(0x08)→ 正则**静默失配且不报错**;`"\s"` 只是警告还能跑,`"\b"` 是真错;
|
||||
- **排除 `.git`**(连同 `.venv`、`ci-artifacts`、`__pycache__`、`node_modules`)——实测扫进 `.git/COMMIT_MSG_*.txt`,一堆 GBK 乱码历史被当成残留,越清越多;
|
||||
- 报告**写 UTF-8 文件**(`ci-artifacts/ka_refs.txt`)再本地解析,别靠控制台输出(Windows 控制台按 GBK 渲染中文 = 乱码);判乱码统计 `EF BF BD` 字节数,别信终端渲染。
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""扫描中文前缀任务卡号与同期形似噪声号;报告写 UTF-8 文件。"""
|
||||
import io, os, re
|
||||
KA = r"\u5361" # 卡
|
||||
PATS = [("CARD_ANY", re.compile(KA + r"\s*[0-9]")),
|
||||
("KA0B", re.compile(KA + r"\s*0b")),
|
||||
("KA12", re.compile(KA + r"\s*12")),
|
||||
("M4", re.compile(r"\bM4\b")),
|
||||
("M5", re.compile(r"\bM5\b")),
|
||||
("M6", re.compile(r"\bM6\b"))]
|
||||
SKIP = {".git", ".venv", "ci-artifacts", "__pycache__", "node_modules"}
|
||||
out = io.open("ci-artifacts/ka_refs.txt", "w", encoding="utf-8")
|
||||
for root, dirs, names in os.walk("."):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP]
|
||||
for n in names:
|
||||
p = os.path.join(root, n)
|
||||
try:
|
||||
txt = io.open(p, encoding="utf-8", errors="ignore").read()
|
||||
except OSError:
|
||||
continue
|
||||
for i, line in enumerate(txt.splitlines(), 1):
|
||||
for tag, rx in PATS:
|
||||
if rx.search(line):
|
||||
out.write("===== %s\n" % p) # 同一行可命中多标签 → 按标签分别计数
|
||||
out.write("%d [%s] %s\n" % (i, tag, line.strip()[:200]))
|
||||
out.close()
|
||||
```
|
||||
末尾打 `TOTAL_SITES / FILES / SCANNED / 各标签计数`;**带标签**才拆得开噪声(同一行可同时命中 `CARD_ANY` 与 `KA12`)。
|
||||
4. **该族与形似噪声号分开计数,各自给分母**:一遍扫描出 `TOTAL_SITES 420 FILES 85 SCANNED 1025`,必须再拆成**卡族 288 处 / 56 文件** 与 **里程碑噪声 132 处 / 44 文件**(`M4/M5/M6` 大片落在 `docs/芯片手册/` 参考手册与料号上下文里)。两套数字各自给分母——**「420 处待改」是错的**:把 132 处噪声算进清退范围就会去改芯片手册原文。噪声侧一律保持原样,是否清退等用户单独裁定,**不要顺手做**。
|
||||
5. **派单前写一份 spec 文件**(`ci-artifacts/card_retire_spec.md`),作为各道唯一口径来源,含五块:① 背景与**用户裁定原文**;② 逐号映射表(旧号形态 → 语义短语 → 属主出处);③ 措辞规则(补短语时括号怎么降级、动不动字符串字面量、生成器归属、悬空引用怎么判);④ 硬约束(BOM 原样补回、行尾由 `.gitattributes` 写死、冻结历史行与原始证据不回改、代码头注释引用的设计约束章节保留、带 BOM 文件名单);⑤ 自检与交付格式(逐文件命中数、删了哪几行、残留数、门禁复跑结果)。`send_message` 只发路径 + 关键裁定,长中文正文不要整段塞进消息(一次串多条长消息会撑爆 120s,发出 1 条后 abort,剩余未发)。
|
||||
6. **逐处替换断言命中数**(同第 7 步的 `rep(tag, old, new, want)`),且 `old` 串**从原文取、不凭记忆敲**:本仓中文引号与 ASCII 双引号混用(实测某文档里的「中文引号」其实是 ASCII `"`),凭印象写 `old` 必 `got=0`;断言抛错后先 `git status --porcelain=v1 -- <文件>` 自证文件未被别道改动,再回读该行真身重跑。删行类改动单独记 tag 并打印删了几行、行号与原行长度。多道并行时同一文件常被别道改过:`edit` 报 `file changed since it was read` → 重新 `read` 再改;报 `ReplaceFileW EIO (Win32 1175)` → 先自证文件字节完好(BOM / CRLF / `EF BF BD` 三项)再重试同一笔,不要顺手改内容。
|
||||
7. **落地位置写死(这一族只有这几条兜)**:
|
||||
- `docs/编号规范.md` **§4.1.1「已停发的族」表加一行**,三列齐全,第三列直写「⚠ **这一族从来没有机器面**:中文前缀不进任何编号语料(族 pattern 全是拉丁形态),而两道编号门禁已于 2026-10-28 / 29 退役 → 只有本表 + 归档换算表 + 评审三条兜;`id_aliases.json` 刻意不登记(没有机器读者,登记等于多一张没人核的表)」;
|
||||
- `scripts/archive/id_gate/编号治理历史.md` **§9.3 逐号换算表**(旧号形态 → 语义短语 → 现行口径出处;本族实测 14 行 = 0、0b、1~12)+ 同篇 §9 批次段落(裁定原文与逐号结论都落在归档正文),**写明旧号永久作废、不得重新取走**;
|
||||
- 表内**把形态写成「卡」+ 数字而不复述字面量**:规范本身是门禁语料,复述字面量会把「讲规则的文档」变成自己的引用站点;
|
||||
- **同步叙述**:`AGENTS.md`「文档与图形」的销项条目、`docs/README.md`(例:`数据可信.md = 三项阻塞任务` → 两项)、`docs/任务规划/README.md`、`docs/任务规划/登记簿.md`——「剩余三项」这类旧口径实测归零;
|
||||
- **与有识别面族的判别写进同一张表**:能被脚本按字面量匹配 → 走 `rename_ids.py` + 别名登记;匹配不到(中文前缀、纯散文号、圈号)→ 走规范换算表,**不登记别名**;
|
||||
- 要机械化怎么办:得先给「中文前缀 + 数字」设计候选形态,属追溯机制改造,按 §7.3 单独开卡,**别夹在清退批里顺手做**。
|
||||
8. **替换后三道二次清洗**(缺一必留疤,全部实测过):
|
||||
```bash
|
||||
git grep -c -F '端子 / 子板变比确认' -- . ; git grep -c -F '端子/子板变比确认' -- . # 先数各形态再归一
|
||||
git grep -nE '卡[ ]*[0-9]' -- . ; echo "rc=$?" # 裸跑读 rc;1 = 归零
|
||||
```
|
||||
- **同句重复短语**:编号位置补上的语义短语与原句已有短语撞车 → 扫「同一行里同一短语出现 ≥2 次」(实测 12 行,如 `.pre-commit-config.yaml:401`「运行参数段(「数据可信」运行参数段交付)」、`App/algo/README.md:110`「遥测快照发布(L2 发布侧,「数据可信」遥测快照发布)」)→ 改法是**把括号里的引用降级**成不含该短语的写法(「道D 交付项」「波次」「该点 DoD 原句」),逐处断言命中 = 1,别删信息;
|
||||
- **空格与引号形态归一**:同一语义短语可能有 `端子 / 子板变比确认` 与 `端子/子板变比确认` 两种形态 → **先 `git grep -c -F` 分别数**(实测带空格 8 处),再整文件归一,归一后复扫到 0(残留常藏在表格单元格里,逐行打印定位);引号形态同理;
|
||||
- **固定正则复扫**:`卡[ ]*[0-9]` 全仓无输出。⚠ 判 rc 时**不要把命令接在 `| head` 之后再读 `$?`**(那拿到的是 `head` 的 0,会把「还有残留」读成「已归零」)。
|
||||
9. **收尾复跑 + 清中间产物**:`check_ids.py`(plain 与 `--strict`)、`check_doc_links.py --all`、bom 与乱码两道(`pre-commit run bom-guard --files <改过的文件>`、`pre-commit run no-mojibake --files <改过的文件>`)、`git diff --check`;**纯注释/纯文档改动不算免编译**——仍跑一次 `/keil-build` 取「0 错误 0 警告」且 `Program Size` 与改前同数(**本回合真跑**,引用历史提交正文自报的四个数不算),受影响的主机组单测照跑。最后**删掉 spec 与全部扫描中间产物**,别往 `ci-artifacts/` 再堆 log/txt。
|
||||
+25
-115
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: keil-build
|
||||
description: "Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用),覆盖 UV4 -b 构建、「0 错误 0 警告」判定、build_log 定位报错、PowerShell 与 bash 两种调用形态及各自的 exit 2 归属、退出码与 build 锁持有者名判读(含插件树的 pw: 前缀)、exit 2 时 UV4.exe 的候选路径探测与单次调用 env 注入闭环、UV4 改写工程文件的入库陷阱、构建副产物「%SystemDrive% 字面量垃圾目录」的取证、清理与入库排除。"
|
||||
whenToUse: "需要编译 Keil MDK 工程、跑 /keil-build 验证代码改动(只改 .c 注释也算改动)是否「0 错误 0 警告」、/keil-build 报 BUILD_ENV_ERROR 与 UV4.exe not found(退出码 2;DSH 会话看不见用户级 DTU_KEIL_UV4)、解读 UV4 构建日志、退出码 2/4 或 build 锁持有者名(含 pw: 前缀),在 bash / ops_bash 里执行 PowerShell 形态的编译命令行报 syntax error near unexpected token,或构建后 git status 冒出 %SystemDrive% 目录时。"
|
||||
description: Keil MDK-ARM 命令行编译闭环(自带独立脚本 keil_build.py,纯标准库,拷到任何 Keil 项目即用):UV4 -b 构建 → "0 错误 0 警告"判定 → build_log 定位报错。封装入口约定、退出码判读、UV4 改写工程文件的入库陷阱。
|
||||
whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0 警告、或解读 UV4 构建日志时。
|
||||
---
|
||||
|
||||
# keil-build — Keil 命令行编译(独立可运行 skill)
|
||||
@@ -10,82 +10,21 @@ whenToUse: "需要编译 Keil MDK 工程、跑 /keil-build 验证代码改动(
|
||||
(纯标准库,任意 python3;拷整个 skill 目录到别的 Keil 项目即可用,不依赖仓库脚本)。
|
||||
|
||||
```powershell
|
||||
# cwd = 项目根目录;PowerShell 形态(AGENTS「构建与验证」的编译入口)
|
||||
# cwd = 项目根目录
|
||||
& $PY ".dsh\skills\keil-build\scripts\keil_build.py"
|
||||
# 覆盖:--project X.uvprojx --uv4 UV4.exe --target 名字 --log 路径 --timeout 600
|
||||
```
|
||||
|
||||
⚠ **改本文件 frontmatter 要么整值加双引号,要么一个 ASCII 冒号都别写**(2026-10-30 这份就因此整份加载失败)。
|
||||
DSH 用 `yaml` 包的 `parse()` 读 frontmatter(`@deepseek-ai/dsh-skill-filesystem` 的 `parseFrontmatter()`),
|
||||
plain scalar 里出现 **ASCII「冒号 + 空格」**(例如把报错原文 `BUILD_ENV_ERROR: UV4.exe not found` 或
|
||||
`pw: 前缀` 直接写进 description)就报 `Nested mappings are not allowed in compact mappings at line 2,
|
||||
column 14` → **技能整份不加载**(`skill()` 读不到、也不进技能目录)。**`js-yaml` 能吃下去** ——
|
||||
拿它自检会假绿,必须用 DSH 自带的那个解析器验。另 — 值首字符不能是 `%`(directive indicator)或 `&`
|
||||
(anchor),双引号里的 `%SystemDrive%` 之类不受影响。落盘后**让 DSH 真加载一次**再收口。
|
||||
解析顺序(命令行 > 项目档案 `.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` 不可行)。
|
||||
|
||||
## 调用形态:先认执行器是哪个 shell(2026-10-26 实测)
|
||||
|
||||
- 同一条编译命令,PowerShell 与 bash 是两种写法:bash 侧要去掉前导 & 并把路径改正斜杠(照抄 AGENTS 的 PowerShell 形态进 ops_bash 会语法错秒退 exit 2)
|
||||
- ⚠ 那个 exit 2 不是脚本的环境错误 2:stderr 含 syntax error near unexpected token = 写法问题,换 bash 形态重跑,别去查 DTU_KEIL_UV4、别当环境错误排障
|
||||
- 后台跑(run_in_background: true)要自己收码,且别把脚本接进管道:实测 $? 取到的是 tail 的码(恒 0);要看报错行就单独 grep MDK-ARM/build_log.txt
|
||||
- 解析顺序:命令行 > 项目档案 .serialtest/config.json > 环境 > 探测;UV4 显式设了但路径无效 = 立即报错,不静默回退
|
||||
- 退出码 0=PASS(0错0警) / 1=构建失败 / 2=环境错误 / 3=用法 / 4=build 锁被占用;2026-10-12 修正过 2 与 3 的归属,该篇末尾记了前后口径
|
||||
- **bash / `ops_bash` 侧「构建到底过没过」的一次自查**(2026-10-26 实测口径):先 `cd` 到仓库根(或该 worktree 根),跑 `.venv/Scripts/python.exe .dsh/skills/keil-build/scripts/keil_build.py; echo BUILD_RC=$?` —— 正斜杠、**无前导 `&`**,退出码归 python、不归 bash;再单独读日志计数 `grep -a -E "Error\(s\)|error:|Warning\(s\)" MDK-ARM/build_log.txt`。报结论要三样齐 —— **`BUILD_RC=0` + `UV4_RC: 0` + `BUILD_OK: 0 errors, 0 warnings`**(脚本自己判「无 `error:` 行 且 尾行含 `0 Error(s), 0 Warning(s)`」);bash 吐的 `syntax error near unexpected token` **既不是构建通过也不是构建失败**,那是写法问题,别拿它当构建结论报。
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [executor-shell-forms.md](reference/executor-shell-forms.md)。动手前先读那篇。
|
||||
|
||||
## exit 2 实战闭环:`UV4.exe not found` 的探测 → 注入 → 复核(2026-10-30 实测)
|
||||
|
||||
**先定性,别动源码**:脚本报
|
||||
`BUILD_ENV_ERROR: UV4.exe not found (--uv4 / config 'uv4' / $DSH_KEIL_UV4 / $DTU_KEIL_UV4 / PATH)`
|
||||
且**退出码 2** = **环境错,不是构建失败**——本次 **UV4 根本没启动、`MDK-ARM/build_log.txt` 也没被改写**,
|
||||
日志里看到的是上一次的。此时去改 `.c`/`.h` 或怀疑代码,就是把环境问题当编译问题排。
|
||||
(会话为什么看不见:DSH 会话的 shell 由常驻服务派生,**服务启动之后**设的用户级变量不会自动进来;
|
||||
`run_code` 起的 Node 进程 `process.env` 干脆是空的——AGENTS.md「会话不继承用户级变量」同记此坑。
|
||||
所以**变量"已设但本会话看不见"是常态**,不是用户没设。)
|
||||
|
||||
1. **探测候选安装路径**(逐个 `fs.existsSync` / `test -f`,命中即取绝对路径)——
|
||||
`C:/Keil_v5/UV4/UV4.exe`、`C:/Keil/UV4/UV4.exe`、`D:/Keil_v5/UV4/UV4.exe`、`D:/Keil/UV4/UV4.exe`、
|
||||
`C:/Program Files/Keil_v5/UV4/UV4.exe`、`D:/Program Files/Keil_v5/UV4/UV4.exe`
|
||||
(2026-10-30 实测只命中 `C:/Keil_v5/UV4/UV4.exe`,其余五个都不存在)。
|
||||
```bash
|
||||
for p in 'C:/Keil_v5/UV4/UV4.exe' 'C:/Keil/UV4/UV4.exe' 'D:/Keil_v5/UV4/UV4.exe' \
|
||||
'D:/Keil/UV4/UV4.exe' 'C:/Program Files/Keil_v5/UV4/UV4.exe' 'D:/Program Files/Keil_v5/UV4/UV4.exe'; do
|
||||
[ -f "$p" ] && echo "HIT $p"; done
|
||||
```
|
||||
2. **读用户级来源确认"值本来就对"**(同一轮里顺手做掉,别只靠猜路径):
|
||||
bash / Node 侧 `reg query HKCU\Environment`(整表打出来再找 `DTU_KEIL_UV4` / `DSH_KEIL_UV4`),
|
||||
PowerShell 侧 `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')`。
|
||||
有值 → 直接用它;没值/值指向旧目录 → 用第 1 步的命中路径。
|
||||
⚠ **不替用户改全局**:不要 `setx`、不要 `SetEnvironmentVariable(...,'User')`——那是用户自己在真终端里
|
||||
做的事(`docs/环境与部署/本地工具路径环境变量方案.md`);本闭环只解决"这一次构建怎么跑起来",
|
||||
报告里也必须这么写。
|
||||
3. **只给这一次调用注入**再重跑(两种等价形态,二选一):
|
||||
```js
|
||||
// run_code:options.env 是**整体替换**,所以要连 SystemRoot / Path / USERPROFILE 一起给
|
||||
const { execSync } = await import('node:child_process');
|
||||
execSync('".venv/Scripts/python.exe" .dsh/skills/keil-build/scripts/keil_build.py', {
|
||||
cwd: MAIN, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
||||
env: { SystemRoot: 'C:\\Windows', Path: 'C:\\Windows;C:\\Windows\\System32',
|
||||
USERPROFILE: 'C:\\Users\\<你>', DTU_KEIL_UV4: 'C:\\Keil_v5\\UV4\\UV4.exe' } });
|
||||
```
|
||||
- bash / `ops_bash` 侧的等价写法 = 显式参数 `--uv4 'C:\Keil_v5\UV4\UV4.exe'`(值含空格要整体加引号)。
|
||||
- `run_code` 的 `timeoutMs` 给足:注入后这次构建实测 **10 s** 跑完,但首次冷构建更长(我给的 500000)。
|
||||
- **别顺手加 `--no-lock`**:注入重跑时锁链照旧(`LOCK: 已获取 build 锁(task=build-cli)` → `已释放锁`)。
|
||||
4. **复核口径**(三样齐了才算真跑过,缺一样就是没构建):脚本回显
|
||||
`UV4: <绝对路径> (<来源>)`(来源随注入方式变:`$DTU_KEIL_UV4` 或 `--uv4`)→
|
||||
`UV4_RC: 0` → `BUILD_OK: 0 errors, 0 warnings`,且**外层退出码 0**(自己包 execSync 就显式把码打出来,
|
||||
别接管道,见「调用形态」)。报告里写清"本次注入 + 来源",**不得声称已永久设置系统/用户环境变量**。
|
||||
|
||||
- ⚠ 注入的若是**注册表里那个陈旧值**,会撞同族的另一种 2:
|
||||
`BUILD_ENV_ERROR: $DTU_KEIL_UV4 is set but not a file: … (fix the value; no silent fallback)`
|
||||
—— 值在但指向旧盘符/旧目录名(本仓踩过 `D:\Code\DTU 程序\…` 搬迁)。脚本**刻意不静默回退**,
|
||||
回到第 1 步用探测命中的路径覆盖它,别去猜目录、更别改脚本让它"猜 C 盘"(设计上明确禁止从旧路径猜)。
|
||||
- ⚠ 本闭环只回答"UV4 在哪儿",**不回答"调用写法对不对"**:从 bash 侧照抄 PowerShell 的 `&` 也是 exit 2,
|
||||
但 stderr 是 `syntax error near unexpected token`(见「调用形态」)。**先看 stderr 再决定排障方向**,
|
||||
两种 2 的处置完全不同。
|
||||
- **改 `.c` 注释(哪怕只改一行中文注释)也必须跑完这道构建验证**:2026-10-30 的实测就是
|
||||
「改注释 → `/keil-build` → 吃 rc=2 → 探测+注入 → 10 s 通过」。注释类坑(BOM 被剥 → `#870-D`、
|
||||
注释里写 `**` 路径 → 9-D)只在**重编译那个 TU 时**才暴露,"以前 0 警告"不代表安全。
|
||||
> 2026-10-12 修正:旧实现把环境错误写成 `raise SystemExit("...")` → 实际退出码是 **1**,
|
||||
> 而用法错误由 argparse 抛 `SystemExit(2)` → 恰好占了"环境错误"的位置。故 `AGENTS.md` /
|
||||
> `new-machine-setup` 里写的"报 exit 2 = 环境问题"当时**不成立**。现按文档口径改正:
|
||||
> 环境错误 → 2(`BuildEnvError`),用法错误 → 3(覆盖 argparse 的 `error()`)。
|
||||
|
||||
## build 锁:脚本自持自释(2026-10-12)
|
||||
|
||||
@@ -96,7 +35,7 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
|
||||
| 开关 | 作用 |
|
||||
|---|---|
|
||||
| `--lock-task <ID>` | 持有者名。默认 `$DTU_TASK_ID` → 从 cwd 反推:`_wt/<ID>-<slug>/` → `<ID>`(`@task` 那批);`.dsh-worktrees/worktree/<name>/` → `pw:<name>`(DSH worktree **插件**建的树,`plugin_tree_owner()`)→ 都没有才落 `build-cli` |
|
||||
| `--lock-task <ID>` | 持有者名。默认 `$DTU_TASK_ID` → 从 cwd 的 `_wt/<ID>-<slug>/` 反推 `<ID>` → `build-cli` |
|
||||
| `--lock-wait <N>` | 锁被占时等待 N 秒(默认 0 = 立即失败,退出码 4) |
|
||||
| `--no-lock` | 完全不加锁(危险:并发 `-b` 互相干扰) |
|
||||
| `DTU_LOCK_HELD=build` | 环境变量:**调用方已持锁**时设它,本脚本跳过自持自释(`task-worktree done` 的门禁即如此用) |
|
||||
@@ -107,68 +46,39 @@ Agent 手工 `lock acquire build` 后忘了 `release`,锁要挂到 TTL 才被
|
||||
- 锁工具 = 兄弟 skill `task-worktree/scripts/task_worktree.py`;**找不到则降级为不加锁
|
||||
并告警**(保持本 skill 可独立搬运)。
|
||||
- ⚠ 从 `_wt/<ID>-<slug>/` 反推持有者名,意味着"手工 `lock acquire build --task <ID>`
|
||||
后再跑 `/keil-build`"仍**幂等**(同名即视为自己持有),不会自我死锁。
|
||||
- **退出码 4 的持有者名带 `pw:`(如 `pw:refactor-logging`)= 插件在 `.dsh-worktrees/worktree/<name>/` 里跑的构建占着 UV4,不是某个 `@task` 任务 ID**:
|
||||
插件树**没有台账**,别拿它去 `lock release <ID>`(释放不掉别人的锁)、更别去杀 UV4 进程。正解 =
|
||||
等那次构建自己 `finally` 释放(自持自释;崩溃则按自报 TTL 兜底),或本次加 `--lock-wait <N>` 排队;
|
||||
想知道是哪棵树的谁:`"$PY" "$TW" plugin list`(缺联接再 `plugin adopt <name>`)→ 细节归
|
||||
`task-worktree` skill F 节(插件桥)。
|
||||
后再跑 `@build`"仍**幂等**(同名即视为自己持有),不会自我死锁。
|
||||
- **只 build 锁自动;`bench` 锁仍须 Agent 手工持**(它是**会话作用域**,跨
|
||||
烧录→`/serial-boot-test`→探针多条命令,逐命令自动释放会让别的任务插进来扰动设备)。
|
||||
|
||||
## 构建副产物:`%SystemDrive%` 字面量垃圾目录(2026-10-26 两次复发 → 2026-10-28 定性并加守卫)
|
||||
|
||||
- 症状:/keil-build(或 check_font_charset.py 这类 fontTools 工具)跑完,仓库根冒出未跟踪的 %SystemDrive%/ProgramData/…/Caches/(几个 .db,约 1 MB)= 本机缓存被按字面量原地复刻
|
||||
- 根因(2026-10-28 实测):run_code 的 Node 进程 process.env 是空的 → known-folder 可展开串展不开变成相对路径 → 按当前目录建在仓库根;同族还会冒出 %USERPROFILE%/AppData/Local/Temp
|
||||
- 清理有安全阀:⚠ 绝不用 cmd / PowerShell 直接删(rd /s /q %SystemDrive% 会被展开成 C:);只用 bash 引号形态,先 find '%SystemDrive%' -type l 预检再 rm -rf --。它是工具副产物,直接删、别 stash、更别提交
|
||||
- 两层防线均已落地可核验:.gitignore:167 挡入库 + 脚本 harden_windows_env() 回填环境变量、check_percent_literal_junk() 只告警不自动删;守卫是兜底不是许可,/keil-build 仍要走 ops_bash / PowerShell
|
||||
- **取证在前,处置在后**(2026-10-28 实测,别一上来 `rm -rf`):`ls -laR -- "%SystemDrive%"` + `du -sh "%SystemDrive%"` 枚举内容与体积,再 `find "%SystemDrive%" -printf "%T@ %p\n" | sort -rn | head -3` 看**最新 mtime** —— 最近几分钟还在被重建 = 环境仍残缺(脚本会打 `WARN: ENV_SPARSE:`),先断源头(换 `ops_bash`/PowerShell 跑,或按「UV4.exe not found」一节把 `env` 传全)再删,否则删完立刻长回来;顺带确认仓库里只有这一个该名字的东西(`find . -maxdepth 1 -name "*SystemDrive*"`)
|
||||
- **要留证据就搬走、别就地删** —— ⚠ **shell 的 `mv` 搬不动它**:`mv "%SystemDrive%/..."` 报 `cannot stat`(含 `%` 的目录名被当变量展开 / 吃掉,路径根本对不上)。改用 Python 遍历搬到**仓库外**的临时目录 —— `os.walk` + `shutil.move`(或 `glob.glob("%SystemDrive%/*")` 逐个 `shutil.move` 到 `os.environ["TEMP"]` 下);确认是纯工具副产物(几个 `.db` 缓存)才 `rm -rf --`
|
||||
- **入库排除要落成条目并核验**:补 `.gitignore` 前先 `grep -n "SystemDrive" .gitignore` 定位精确原文行再 Edit(凭记忆写 `old_string` 必不匹配,2026-10-28 踩过)。本仓条目 = `.gitignore:167` 的 `%SystemDrive%/`,上面那段注释就是复发证据(新增条目也照此**写明第几次复发、谁造的**)。收尾核验 —— `git status --porcelain` 里该目录零命中(且别的未跟踪产物一个没少)、`git check-ignore -v` 落在那一行、`git add -A --dry-run` 零命中;**绝不提交**(把本机缓存塞进历史)
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [systemdrive-junk-dir.md](reference/systemdrive-junk-dir.md)。动手前先读那篇。
|
||||
烧录→`@test`→探针多条命令,逐命令自动释放会让别的任务插进来扰动设备)。
|
||||
|
||||
## 本仓库
|
||||
|
||||
**本 skill 的调用名 = `/keil-build`**(**技能调用名不是 shell 命令**——原样敲进终端是 command not found / syntax error,可执行形态永远是下面这条命令行):在仓库根目录跑
|
||||
`& '.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` = 本 skill**(2026-10-10 起为仓库文档口径):在仓库根目录跑
|
||||
`& '.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 兜底)、失败只打报错行。
|
||||
环境变量 `DTU_KEIL_UV4`(用户级,指向 UV4.exe)——注意**它设了不等于本会话能看见**,仍报退出码 2 时走「exit 2 实战闭环」那一节,别先去找用户。
|
||||
环境变量 `DTU_KEIL_UV4`(用户级,指向 UV4.exe)。
|
||||
|
||||
## 铁律与陷阱(实测教训)
|
||||
|
||||
- **任何代码改动后必须先编译验证(0 错误 0 警告)再谈烧录/提交**——编译不过就进
|
||||
硬件闭环只会把编译错误伪装成运行问题。**只改 `.c` 里一行中文注释也算改动**(2026-10-30 实测:
|
||||
改注释 → `/keil-build` → 环境错 → 探测注入 → 10 s 通过):BOM/嵌套注释这类坑只在重编译那个 TU 时才暴露。
|
||||
- **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"(取退出码的管道陷阱见「调用形态」)。
|
||||
硬件闭环只会把编译错误伪装成运行问题。
|
||||
- **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"。
|
||||
- **`MDK-ARM/*.uvprojx` 每次构建后被 UV4 自动改写,改动一律不入库**:UV4 会把工程
|
||||
XML 归一化成本机风格(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`)并把
|
||||
`PackID` 降到**本机装的 DFP 版本**(属降级——提交后会把别人的 Pack 版本一起改掉)。
|
||||
判断:`git diff` 里只见 XML 格式/`PackID` 差异而**无源文件挂载增删** →
|
||||
`git checkout -- <uvprojx>` 丢弃;只有真的增删了源文件(grep 文件名计数变化)才提交。
|
||||
同目录的 `*.uvoptx` / `*.uvguix.*` 是 IDE 个人设置,已在 `.gitignore` 里,不会露出来。
|
||||
- **构建后仓库根冒出 `%SystemDrive%/`** = UV4 / 字体工具在残缺环境(缺 `SystemDrive`、`ProgramData`)
|
||||
里按**未展开的 known-folder 字面量**建出来的**相对路径**目录,不是任何人在飞的改动:
|
||||
`rm -rf -- '%SystemDrive%'`(bash 引号形态)删掉即可,**严禁 `rd /s /q %SystemDrive%`——cmd 会把它
|
||||
展开成 `C:`**;`.gitignore` 已兜住 + 脚本已加 `ENV_SPARSE`/`JUNK_DIR` 守卫 → 见「构建副产物」一节。
|
||||
- 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。
|
||||
- **exit 2 先分三种再动手**(2026-10-10 / 2026-10-26 / 2026-10-30 实测):① **bash 语法错**——stderr 有
|
||||
`syntax error near unexpected token`,是把 PowerShell 形态丢进了 `ops_bash`,换写法即可;
|
||||
② **真没设** `DTU_KEIL_UV4`;③ **已设但本会话看不见**(agent 的 shell 由常驻服务派生,服务启动**之后**
|
||||
设的用户级变量不会进来;`run_code` 的 `process.env` 直接是空的)。
|
||||
②③ 一律走 **「exit 2 实战闭环」** 那一节:探测候选路径 + 读 `HKCU\Environment`,
|
||||
再**只往那一次调用**的 env / `--uv4` 里注入命中路径重跑——别急着让用户去设,也别替他改全局。
|
||||
- **exit 2 先分清"真没设"还是"会话没继承"**(2026-10-10 实测):agent 的 shell 由
|
||||
常驻服务派生,服务启动**之后**设置的用户级环境变量不会自动进来。查
|
||||
`[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')`,有值就
|
||||
`$env:DTU_KEIL_UV4 = <该值>` 注入本会话再跑,别急着让用户去设。
|
||||
- 编码类报错(`#870-D: invalid multibyte character sequence` 等)先怀疑源文件
|
||||
BOM/编码被改写,而不是代码本身(见仓库 AGENTS.md 编码铁律)。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
- **build 锁的对面是谁**(`BUILD_LOCK_BUSY`、退出码 4、`pw:<name>` 插件树、`@task plugin list` / `plugin adopt`、`_wt/` 台账与合并回收)归 `task-worktree` skill(F 节 = DSH worktree 插件桥);换机/新克隆时 `DTU_*` 工具路径变量的设立与「会话不继承」坑 = `new-machine-setup` skill。
|
||||
- 烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
|
||||
(其档案 `flash_cmd`/`readback_cmd` 即 stlink-flash 的入口命令)。
|
||||
- 收尾固定链(2026-10-26 实证):`/keil-build` → `scripts/ram_baseline.py --update` →
|
||||
体积门禁盖戳 → 内存数字回填与 `--scan` → 复跑 `ci_local` 静态段,
|
||||
直到钩子全绿且 0 错误 0 警告(RAM/栈口径见 `mem-budget` skill)。
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# 调用形态:先认执行器是哪个 shell(2026-10-26 实测)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 keil-build 的 SKILL.md「调用形态:先认执行器是哪个 shell(2026-10-26 实测)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
同一条编译命令,**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` /
|
||||
> `new-machine-setup` 里写的"报 exit 2 = 环境问题"当时**不成立**。现按文档口径改正:
|
||||
> 环境错误 → 2(`BuildEnvError`),用法错误 → 3(覆盖 argparse 的 `error()`)。
|
||||
@@ -1,44 +0,0 @@
|
||||
# 构建副产物:`%SystemDrive%` 字面量垃圾目录(2026-10-26 两次复发 → 2026-10-28 定性并加守卫)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 keil-build 的 SKILL.md「构建副产物:`%SystemDrive%` 字面量垃圾目录(2026-10-26 两次复发 → 2026-10-28 定性并加守卫)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
**症状**:`/keil-build`(或 `scripts/check_font_charset.py` 这类 fontTools 字体工具)跑完,仓库根
|
||||
冒出未跟踪目录 `%SystemDrive%/ProgramData/Microsoft/Windows/Caches/`,里面是几个 `.db`
|
||||
(`cversions.2.db` + `{6AF0698E-…}` 图标缓存 + `{DDF571F2-…}` 字体缓存,合计约 1 MB)——
|
||||
就是本机 `C:\ProgramData\Microsoft\Windows\Caches` 那份缓存被**按字面量原地复刻**了一份。
|
||||
|
||||
**根因(2026-10-28 实测)**:Windows 的 known-folder 路径在注册表里是**可展开串**;展开不了,
|
||||
它就变成一个**相对路径**,于是按「当前目录」(= 仓库根)建出来。三条现场证据:
|
||||
|
||||
```bash
|
||||
reg.exe query HKCU\Environment # 本机 TMP/TEMP = REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Temp
|
||||
env | wc -l # ops_bash 里 = 74(完整环境)
|
||||
node -e "console.log(Object.keys(process.env).length)" # run_code 里 = 0(空环境!)
|
||||
```
|
||||
|
||||
`run_code` 的 Node 进程 `process.env` 是**空的**,从它里面 `spawn` 出去的 UV4 / 字体脚本就拿不到
|
||||
`SystemDrive`/`ProgramData`/`ALLUSERSPROFILE`;实测 `os.path.expandvars("%SystemDrive%\ProgramData")`
|
||||
原样返回、且 `os.path.isabs()` = **False** → 目录就长在工作区里(同族还会冒出
|
||||
`%USERPROFILE%/AppData/Local/Temp`)。**正解不是「让用户去设环境变量」**,是别在残缺环境里跑构建(见第 2 条)。
|
||||
|
||||
**清理(有安全阀)**:
|
||||
|
||||
- ⚠ **绝不用 cmd / PowerShell 直接删**:`rd /s /q %SystemDrive%` 会被 cmd **展开成 `C:`**——
|
||||
等于对整块系统盘下令递归强删且不带确认。
|
||||
- ✅ 只用 bash 引号形态(`%` 在 bash 里没有特殊含义):先 `find '%SystemDrive%' -type l` 确认不是
|
||||
reparse point / 符号链接,再 `rm -rf -- '%SystemDrive%'`;或 Node
|
||||
`fs.rmSync("%SystemDrive%", { recursive: true, force: true })`。
|
||||
- 它是**工具副产物**,不是谁在飞的改动——直接删,别 stash、更别提交(提交它 = 把本机缓存塞进历史)。
|
||||
|
||||
**让它不再污染工作区(两层,均已落地、可核验)**:
|
||||
|
||||
1. **进不了版本库**:`.gitignore:167` 的 `%SystemDrive%/`(2026-10-26 入库,注释里就写着复发证据)。
|
||||
核验三步(2026-10-28 复跑全绿):造一份同名目录 + `.db` → `git status --porcelain` 零命中 →
|
||||
`git check-ignore -v <路径>` 落在 `.gitignore:167:%SystemDrive%/` → `git add -A --dry-run` 零暂存。
|
||||
2. **根本别再长出来**:本 skill 脚本 2026-10-28 起在拉起 UV4 前跑 `harden_windows_env()`——回填
|
||||
`SystemDrive`/`SystemRoot`/`windir`/`ComSpec`/`ProgramData`/`ALLUSERSPROFILE`/`ProgramFiles` 与
|
||||
`USERPROFILE`/`APPDATA`/`LOCALAPPDATA`/`TMP`/`TEMP`,就地展开环境里未展开的 `%VAR%` 字面量,
|
||||
并把 `TMP`/`TEMP` 指到真实目录;命中即打 `WARN: ENV_SPARSE:`。构建后再跑
|
||||
`check_percent_literal_junk()`,看到 `%SystemDrive%`/`%USERPROFILE%` 一类字面量目录就打
|
||||
`WARN: JUNK_DIR:`(**只告警不自动删**——删目录不该由构建脚本代劳)。守卫是兜底不是许可:
|
||||
**`/keil-build` 仍要走 `ops_bash` / PowerShell(继承完整环境)**,非在 Node 里 spawn 就把 `env` 传全。
|
||||
@@ -14,7 +14,7 @@
|
||||
(环境变量**显式设了但路径无效 = 立即报错,不静默回退**)
|
||||
日志 --log > 档案 "build_log" > <工程目录>\\build_log.txt
|
||||
|
||||
判定口径(实测教训沉淀;原仓库 MDK-ARM/build.bat 已删除,本脚本即仓库编译入口):
|
||||
判定口径(实测教训沉淀;原仓库 MDK-ARM/build.bat 已删除,本脚本即 @build):
|
||||
1. 先删旧日志——UV4 -o 只在真跑了构建才写日志,残留旧日志会被当成本次结果
|
||||
2. UV4 单实例:IDE 开着时 -b 可能被转发给 IDE(退出码丢失)→ 只 WARN 不杀进程
|
||||
3. PASS = 日志存在 + 无 "error:"(大小写不敏感)+ 含 "0 Error(s), 0 Warning(s)"
|
||||
@@ -64,117 +64,6 @@ class BuildEnvError(Exception):
|
||||
"""环境/前置错误(UV4 或工程找不到、档案解析失败)→ 退出码 2。"""
|
||||
|
||||
|
||||
def _expand_deep(v):
|
||||
"""展开未展开的 REG_EXPAND_SZ 字面量(百分号形态的变量引用),最多四轮,防自指死循环。"""
|
||||
for _ in range(4):
|
||||
nv = os.path.expandvars(v)
|
||||
if nv == v:
|
||||
break
|
||||
v = nv
|
||||
return v
|
||||
|
||||
|
||||
def harden_windows_env():
|
||||
"""拉起 UV4 前补齐/展开子进程环境——堵住仓库根的「%SystemDrive%/」字面量垃圾目录。
|
||||
|
||||
Windows 的 known-folder 路径在注册表里是**可展开串**:ProgramData 的真实取值来自
|
||||
「%SystemDrive%/ProgramData/…」,本机 HKCU Environment 里的 TMP/TEMP 实测就是未展开的
|
||||
「%USERPROFILE%/AppData/Local/Temp」。父进程环境残缺时(典型:Agent 在 Node
|
||||
child_process / run_code 里 spawn,子进程继承到一个**空环境**,SystemDrive、ProgramData、
|
||||
USERPROFILE 一个都没有)这些串展不开、留在原地就成了**相对路径**,于是 UV4 / fontTools 类
|
||||
字体工具按它在**当前目录**(= 仓库根)建出
|
||||
「%SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db」(图标+字体缓存库,约 1 MB),
|
||||
污染 git status(2026-10-26 同一会话两次复发;仓库 .gitignore 已入库兜住它,但正解是
|
||||
**根本别让它长出来**)。故此处回填 + 展开,并打可定位的 WARN:ENV_SPARSE。
|
||||
"""
|
||||
if os.name != "nt":
|
||||
return
|
||||
filled, expanded = [], []
|
||||
|
||||
for key, val in list(os.environ.items()):
|
||||
if isinstance(val, str) and "%" in val:
|
||||
new = _expand_deep(val)
|
||||
if new != val and "%" not in new:
|
||||
os.environ[key] = new
|
||||
expanded.append(key)
|
||||
|
||||
root = os.environ.get("SystemRoot") or os.environ.get("windir") or ""
|
||||
root = _expand_deep(root)
|
||||
if "%" in root or (root and not os.path.isdir(root)):
|
||||
root = os.path.join(os.path.abspath(os.sep), "Windows") # 兜底:当前盘根下的 Windows 目录
|
||||
drive = os.path.splitdrive(root)[0] or os.path.abspath(os.sep)[:2]
|
||||
drive = drive.rstrip(os.sep) or "C:"
|
||||
sysroot = drive + os.sep
|
||||
user = _expand_deep(os.environ.get("USERPROFILE") or "")
|
||||
if "%" in user:
|
||||
user = ""
|
||||
defaults = {
|
||||
"SystemDrive": drive,
|
||||
"SystemRoot": root,
|
||||
"windir": root,
|
||||
"ComSpec": os.path.join(root, "System32", "cmd.exe"),
|
||||
"ProgramData": os.path.join(sysroot, "ProgramData"),
|
||||
"ALLUSERSPROFILE": os.path.join(sysroot, "ProgramData"),
|
||||
"ProgramFiles": os.path.join(sysroot, "Program Files"),
|
||||
}
|
||||
if not user:
|
||||
guess = _expand_deep(os.path.expanduser("~"))
|
||||
if not guess.startswith("~") and "%" not in guess:
|
||||
user = guess
|
||||
if user:
|
||||
defaults["USERPROFILE"] = user
|
||||
defaults["APPDATA"] = os.path.join(user, "AppData", "Roaming")
|
||||
defaults["LOCALAPPDATA"] = os.path.join(user, "AppData", "Local")
|
||||
defaults["TMP"] = os.path.join(user, "AppData", "Local", "Temp")
|
||||
defaults["TEMP"] = defaults["TMP"]
|
||||
for key, val in defaults.items():
|
||||
if not os.environ.get(key):
|
||||
os.environ[key] = val
|
||||
filled.append(key)
|
||||
|
||||
# TMP/TEMP 必须落在真实目录:留未展开值 = 工具按字面量在当前目录建「%USERPROFILE%/…」垃圾树
|
||||
td = os.environ.get("TEMP") or os.environ.get("TMP") or ""
|
||||
if "%" in td or (td and not os.path.isdir(td)):
|
||||
cand = []
|
||||
lad = os.environ.get("LOCALAPPDATA") or ""
|
||||
if lad and "%" not in lad:
|
||||
cand.append(os.path.join(lad, "Temp"))
|
||||
cand.append(os.path.join(sysroot, "Temp"))
|
||||
picked = next((c for c in cand if os.path.isdir(c)), None)
|
||||
if not picked:
|
||||
for c in cand:
|
||||
try:
|
||||
os.makedirs(c, exist_ok=True)
|
||||
picked = c
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if picked:
|
||||
os.environ["TEMP"] = picked
|
||||
os.environ["TMP"] = picked
|
||||
filled.append("TEMP/TMP=" + picked)
|
||||
|
||||
if filled or expanded:
|
||||
print("WARN: ENV_SPARSE: 子进程环境残缺/含未展开字面量 —— 已回填[{}] 已展开[{}]".format(
|
||||
", ".join(sorted(filled)) or "-", ", ".join(sorted(expanded)) or "-"))
|
||||
print(" 正解:用继承**完整环境**的 shell 跑编译(ops_bash / PowerShell),"
|
||||
"别从 Node spawn(空 process.env)直接起 UV4 或字体门禁脚本")
|
||||
|
||||
|
||||
def check_percent_literal_junk():
|
||||
"""构建后自检:环境残缺时 Windows 会在**当前目录**按字面量建 %SystemDrive%/ 之类的垃圾树。
|
||||
|
||||
只告警、不自动删——删除口径见本 skill「构建副产物」一节:必须用 bash 引号形态,
|
||||
绝不能用 cmd(cmd 会把 %SystemDrive% 展开成 C:,等于对整块系统盘下 rd /s /q)。
|
||||
"""
|
||||
for name in ("%SystemDrive%", "%USERPROFILE%", "%LOCALAPPDATA%", "%APPDATA%",
|
||||
"%ProgramData%", "%TEMP%", "%TMP%"):
|
||||
if os.path.isdir(name):
|
||||
print("WARN: JUNK_DIR: 工作区出现字面量目录 {}(未展开的 known-folder 路径;"
|
||||
".gitignore 已兜住,不得入库)".format(name))
|
||||
print(" 清理(只用 bash 引号形态,禁用 cmd 以免展开成 C:): rm -rf -- '{}'".format(name))
|
||||
|
||||
|
||||
class _ArgumentParser(argparse.ArgumentParser):
|
||||
"""用法错误 → 退出码 3。
|
||||
|
||||
@@ -273,25 +162,8 @@ def find_lock_tool():
|
||||
return cand if cand.is_file() else None
|
||||
|
||||
|
||||
def plugin_tree_owner(cwd: Path):
|
||||
"""DSH worktree 插件的树(仓库内 .dsh-worktrees/worktree/<name>)→ 持有者名 pw:<name>。
|
||||
|
||||
2026-10-29 与 task-worktree skill 的 `plugin` 子命令对齐:插件建的树不在 _wt/ 约定里,
|
||||
以前在这里跑构建,持有者名一律落成 "build-cli"——`lock status` 看不出是谁在占 UV4,
|
||||
多个插件树 / 主树并发时就只剩猜。名字里的多段斜杠压平成 -,与
|
||||
task_worktree.py 的 plugin_lock_owner() 完全一致(同一个可 grep 的标识)。
|
||||
"""
|
||||
parts = cwd.parts
|
||||
if ".dsh-worktrees" not in parts:
|
||||
return None
|
||||
tail = parts[parts.index(".dsh-worktrees") + 1:]
|
||||
if tail and tail[0] == "worktree":
|
||||
tail = tail[1:]
|
||||
return "pw:" + "-".join(tail) if tail else "pw:?"
|
||||
|
||||
|
||||
def default_lock_task():
|
||||
"""持有者名:$DTU_TASK_ID > cwd 反推 `_wt/<ID>-<slug>/` 的 `<ID>` > 插件树 `pw:<name>` > "build-cli"。"""
|
||||
"""持有者名:$DTU_TASK_ID > cwd 反推 `_wt/<ID>-<slug>/` 的 `<ID>` > "build-cli"。"""
|
||||
v = os.environ.get("DTU_TASK_ID")
|
||||
if v:
|
||||
return v
|
||||
@@ -302,7 +174,7 @@ def default_lock_task():
|
||||
for p in (cwd, *cwd.parents):
|
||||
if p.parent.name == "_wt":
|
||||
return p.name.split("-", 1)[0] or p.name
|
||||
return plugin_tree_owner(cwd) or "build-cli"
|
||||
return "build-cli"
|
||||
|
||||
|
||||
def build_lock_ttl_hours(timeout_s):
|
||||
@@ -370,9 +242,6 @@ def run_build(args):
|
||||
print("PROJ: {} target={}".format(proj, target or "(default)"))
|
||||
print("LOG: {}".format(log))
|
||||
|
||||
# 环境残缺守卫:先补齐/展开,否则 UV4 会在仓库根按字面量建 %SystemDrive%/ 垃圾树
|
||||
harden_windows_env()
|
||||
|
||||
# UV4 单实例陷阱:IDE 开着时 -b 可能被转发(退出码丢失、GUI 弹出)——只 WARN
|
||||
try:
|
||||
tl = subprocess.run(["tasklist", "/fi", "imagename eq UV4.exe", "/nh"],
|
||||
@@ -399,7 +268,6 @@ def run_build(args):
|
||||
print("BUILD_FAIL: UV4 timed out after {}s (hung or forwarded to IDE?)".format(args.timeout))
|
||||
return 1
|
||||
print("UV4_RC: {}".format(rc))
|
||||
check_percent_literal_junk()
|
||||
|
||||
if not log.is_file():
|
||||
print("BUILD_FAIL: no build log produced - UV4 did not run or hung")
|
||||
|
||||
@@ -33,20 +33,20 @@ PC 上编译运行与 MCU **同一份** UI 源码(CMake + MSVC + SDL2,x64,
|
||||
|
||||
## UI 改动回归 → 用 `ui-verify` skill(勿在此零散手跑)
|
||||
|
||||
5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 66 张黄金参考 + 往返池)→ `/keil-build` → `coding-standard`。
|
||||
5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 11 页黄金参考 + 往返池)→ `@build` → `coding-standard`。
|
||||
RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/check_remo_sink.py`。
|
||||
黄金参考 `tests/ref/*.bin` **随仓库入库**(`.gitignore` 在全仓 *.bin 兜底之后又显式反选,2026-10-22 起;缺基准 = 新克隆跑 `ui-golden` 必 FAIL):不入库的只是派生目检图 `ref/*.png`,换机后先 `check_content_pages.py --update` 生成,再 `regen_ref_png.py` 出目检 PNG。
|
||||
黄金参考 `tests/ref/` 是**本地生成物不入库**:换机后先 `check_content_pages.py --update` 生成,再 `regen_ref_png.py` 出目检 PNG。
|
||||
|
||||
## 两条铁律
|
||||
|
||||
- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);四类判定与端专属白名单 → `docs/门禁说明/lv-conf-sync.md`;顶层 exclude 的教训 → `docs/代码规范/pre-commit使用说明.md` §5.5
|
||||
- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/专项验证/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过
|
||||
- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过
|
||||
|
||||
## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」)
|
||||
|
||||
| 现象 | 根因 |
|
||||
|---|---|
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) |
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,须有 `include/SDL2/` 副本。⚠ **这份 SDK 不入库**(`.gitignore` 排除 `simulator/SDL2/`,实测 `git ls-files` 在该路径命中 0 个文件)——"下载才有"不等于"仓库已内置";CI/runner 的干净检出里没有它,2026-10-27 run 45 就在这里红。CI 侧用机器级 `DTU_SDL2_DIR` 指一份 SDK,见 `docs/CI/远程CI完整指南.md` §4.5 |
|
||||
| 找不到 `lv_conf.h` | `LV_CONF_DIR`/`SDL2_INCLUDE_DIR` 必须在 `add_subdirectory(lvgl)` **之前**定义 |
|
||||
| 窗口全黑 | mono 主题未生效 / 缺 `lv_refr_now(NULL)` / 重复喂 tick(SDL 驱动内部已 `lv_tick_set_cb`) |
|
||||
| 画面正常但按键全无效 | 主循环**不得**调 `SDL_PollEvent`——LVGL SDL 驱动内部 5ms 定时器统一分发,先轮询会抢光事件 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: mem-budget
|
||||
description: 嵌入式 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 数字钩子红了、提交前台账怎么回填"的固定动作和判读口径。
|
||||
whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报 L6406E 或 RAM 溢出、串口 [STACK] 水位余量低于底线、要回答"这个改动花多少 RAM/Flash";以及要复算或比对另一个 worktree 的栈数字、独立验证某个栈回归该记在哪一笔提交上、判断多次启动的栈峰值抖动算不算劣化时;还有 /keil-build 之后要把内存数字回填进台账/文档、纠结该跑 ram_baseline.py --write 还是 --update、提交时 mem-budget / mem-numbers 两条钩子报"与 .map 不一致"时。
|
||||
description: 嵌入式 RAM/栈预算核算工作流:Keil .map/.htm 静态报告(ram_report/stack_report)+ 串口运行时水位叠加 + DBG 日志成本模型(每条约 90B Flash/216B 栈)。"改任务/缓冲/栈之前必须核算"的固定动作与判读口径。
|
||||
whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报 L6406E 或 RAM 溢出、串口 [STACK] 水位余量低于底线、或需要回答"这个改动花多少 RAM/Flash"时。
|
||||
---
|
||||
|
||||
# mem-budget — RAM/栈预算核算
|
||||
@@ -9,120 +9,26 @@ whenToUse: 新增任务/加大缓冲/改栈/接入日志之前之后、链接报
|
||||
## 固定动作(改前、改后各跑一次)
|
||||
|
||||
```powershell
|
||||
# cwd = 项目根;先 /keil-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 —— 跨树必须换脚本(在主树跑脚本量任务树 = 拿旧 .map 当真值)
|
||||
2. 重跑报告脚本不算新测量(复述 ≠ 复验):只有新的 Linker run 才算一次新测量
|
||||
3. 钉身份的取数法:.htm/.map 按约定不入库 → 把时刻写进正文/台账;记录最小格式 = tree/commit + Linker run 时刻 + 产物来源(本次构建 / 盘上既有)
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [measurement-identity.md](reference/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](reference/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 底线要上报。
|
||||
静态是上界、运行时是下界证据,**两者都要看**;余量 <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。
|
||||
|
||||
### 提交前固定顺序(构建 → 回填 → 真钩子提交)
|
||||
|
||||
```powershell
|
||||
# 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 <path> 只换读、不换写
|
||||
- --scan 的散句告警 --write 不替你改
|
||||
- --update 的盖戳排在索引后面
|
||||
- 改写型钩子当场改文件是流程不是故障
|
||||
- mem-budget 触发面很宽
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [backfill-pitfalls.md](reference/backfill-pitfalls.md)。动手前先读那篇。
|
||||
|
||||
## 运行时峰值抖动与"算不算劣化"
|
||||
|
||||
- 同一份镜像连续启动 5 次,`uiTask` 峰值实测在 4116~4188 之间摆 **72 B** ⇒ 跨启动本身就有几十 B 抖动,
|
||||
"相对上一轮 +100 B"**不可归因**于被测改动。判据写成"栈水位不劣于上一轮"必然假红。
|
||||
- 可执行修法(二选一,别都留着):
|
||||
1. 同一份像**≥3 次启动取最大值**互比,并加容差(**±128 B 量级**);
|
||||
2. 硬线改用 `stack_report.py` 的**静态最坏链**,运行时峰值降级为趋势参考。
|
||||
- 判据一改要同步**所有落点**:判据正文、跑判据的脚本、记录判据的文档三处一起改,只改一处等于各说各话。
|
||||
- 任务表一致性由 pre-commit `task-table` 钩子保证(改栈后 `gen_task_table.py` 同步 AGENTS.md)。
|
||||
|
||||
## 成本模型(实测,估算用)
|
||||
|
||||
@@ -136,21 +42,8 @@ git add docs/内存管理/RAM预算台账.md docs/内存管理/RAM使用分析
|
||||
- **自研代码禁 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`)需替换解析层,工作流不变。
|
||||
报告 + 运行时水位叠加 + 余量阈值"三件套。本仓库两个报告脚本按 Keil 输出格式解析,
|
||||
换工具链(GCC newlib + `-fstack-usage`)需替换解析层,工作流不变。
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 回填链上的坑(实测)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 mem-budget 的 SKILL.md「回填链上的坑(实测)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
- **`--write` 说「已是最新」而钩子还红** ≠ 钩子坏了,是**落点找错了**:tsv / 台账受管区 / 文档快照区
|
||||
三处各归一个旗标。按上表对号,别把同一个旗标重跑三遍;手改台账数字更不行——下次 `--write` 直接覆盖你,
|
||||
钩子仍按 .map 判漂移。
|
||||
- **tsv 落后只 WARN、不拦**:`--gate` 对「tsv 末行落后于当前 .map」只打印 `[WARN] …请跑 --update`
|
||||
(`ram_baseline.py:232`),**拦下来的是受管区漂移**。所以"钩子绿"绝不等于"趋势行也补了",反之亦然,各查各的。
|
||||
- **`--gate` 无 .map → SKIP,而脚本自己写明「SKIP 不等于 PASS」**(`ram_baseline.py:177`):
|
||||
新开的 task-worktree 从没在这棵树上构建过时必然走到这里;同一状态下 `--write` 直接
|
||||
`[FAIL] 找不到 .map 事实源,先 /keil-build`(`ram_baseline.py:250`)——不是代码缺陷,是这棵树还没测过体积。
|
||||
无需 Keil 的强制兜底是 `scripts/check_size_gate.py --local`(= `scripts/ci_local.py` 的体积门禁步)与 CI 的 `--ci`。
|
||||
- **`--map <path>` 只换读、不换写**:`LEDGER`(`ram_baseline.py:45`)与 `TARGETS`(`check_mem_numbers.py:53-54`)
|
||||
都由 `REPO_ROOT = Path(__file__).resolve().parent.parent` 算出。拿主仓脚本配 `--map <另一棵树>/…map` 去 `--write`,
|
||||
等于用别的树的数字**改写主仓的台账**——比"半棵树混合数字"更糟,因为它落盘了。回填永远用本树自己的脚本。
|
||||
- **`--scan` 的散句告警 `--write` 不替你改**:正文里手抄的旧值("现 RW_IRAM1 62.6%" 这类散句)只告警,
|
||||
脚本明确写着「`--write` 不会自动改写正文」(`check_mem_numbers.py:418`)→ 人工按受管区改那一行
|
||||
(Edit 精确替换;含中文的文档绝不整文件重写)。
|
||||
- **`--update` 的盖戳排在索引后面**:它顺带调 `check_size_gate.cmd_stamp()`(`ram_baseline.py:345`),
|
||||
而凭证指纹按 **git 索引内容**生成,有未 staged 的脏改动时**拒盖**,且这里只 `[WARN]`、不改返回值
|
||||
→ 现象是 tsv 多了一行而凭证仍是旧的,要等 `ci_local` 的体积门禁步 / CI 体积门禁那一侧才报出来。
|
||||
要么先 `git add` 再 `--update`,要么补跑 `python scripts/check_size_gate.py --stamp`。
|
||||
- **改写型钩子当场改文件是流程不是故障**:`trailing-whitespace` / `end-of-file-fixer` 报
|
||||
`files were modified by this hook`(新建的 .c 最常触发)时,先读 `ci-artifacts/cui.log` 的真实报错确认是哪几条红,
|
||||
`git add` 被改写的文件后重提。两条改写红 + 两条内存红混在同一次里时,别顺手把内存那条也当"钩子误报"绕过去。
|
||||
- **`mem-budget` 触发面很宽**:`files:` 覆盖 `App/` `Drivers/BSP/` `Src/` `Inc/` `Lib/lv_conf.h`
|
||||
`test/board/` `scripts/ram_baseline.` `docs/内存管理/` `.pre-commit-config.yaml`,且 pre-commit 4.x 已无
|
||||
`always: true` —— 纯 UI / 纯文档提交照样被它拦:盘上 .map 与台账不同源就红,跟这笔提交碰没碰内存文件无关。
|
||||
@@ -1,58 +0,0 @@
|
||||
# 测量身份口径(先钉树与时刻,再看数字)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 mem-budget 的 SKILL.md「测量身份口径(先钉树与时刻,再看数字)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
### 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,**不是工作区**。
|
||||
@@ -1,24 +0,0 @@
|
||||
# 静态最坏链回归的归因(同涨同一数值 = 共享帧,不是调用方)(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 mem-budget 的 SKILL.md「静态最坏链回归的归因(同涨同一数值 = 共享帧,不是调用方)」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
- **多条任务链分毫不差同涨/同降同一个数值**(实测 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` 同步任务表。
|
||||
@@ -36,5 +36,5 @@ cmake --build simulator/build --config Debug # 0 警告
|
||||
- **禁止**把源文件复制进 `simulator/`——两端共用 `Drivers/BSP/` 原件(UI_界面制作流程.md C7)
|
||||
- 只注册 `.c`(含 `.s`);`.h` 靠 include 路径找到,不加工程
|
||||
- 漏注册的典型症状:一端编译失败(undefined reference)或一端行为悄悄停留在旧逻辑
|
||||
- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `/serial-boot-test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)
|
||||
- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)
|
||||
- 改过 `CMakeLists.txt` 后需重新 `cmake -B build` 配置
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: remote-ci-ops
|
||||
description: 远程 CI(Gitea Actions + 本机 Windows 自建 runner)运维闭环:runner 保活与抢锁清理、"没有 run / 任务消失"的四因定位、红因三分法(job 没跑起来 / runner 缺环境 / 真门禁失败)、无令牌时用 Gitea 的 MySQL 直查 runner 在线与历次 run/job 时长与红绿史(含 status 枚举定死办法)、用 gitea-runner.exe generate-config 枚举 runner 真实配置键而不猜键名、本地对照日志落 ci-artifacts/、门禁数字口径、gitlint 在 CI 的两种空转(含 runner 无 git 身份致 exit 254 假失败)、PS 5.1 原生 stderr 与编码陷阱、改 workflow / 门禁脚本的红线与改后验收清单。背景与全部实测细节在 docs/CI/远程CI完整指南.md,本 skill 只给操作顺序与判据。
|
||||
whenToUse: CI 没跑、run 被取消或零日志、页面报的失败时长/红绿史跟本地或日志对不上、"这 job 到底跑没跑起来"、门禁假绿/假红、runner 显示 online 却每 10 秒重启、自建 runner 有哪些可用配置项(cache 之类,不确定键名)、要改 .gitea/workflows/ci.yml 或 scripts/ci_*.ps1|py、或没有 API 令牌却要判断 CI 状态时。
|
||||
---
|
||||
|
||||
# remote-ci-ops — 远程 CI 运维闭环
|
||||
|
||||
**权威细节**:`docs/CI/远程CI完整指南.md`(部署、全部实测坑、章节导航),唯一入口 `docs/CI/README.md`。
|
||||
本 skill 只回答两件事:**出问题时按什么顺序查**、**动手前后各验什么**。同一事实不在两处详述。
|
||||
|
||||
> **TODO(待同步回指南)**:`docs/CI/远程CI完整指南.md` §6.1 目前只写 `action_runner.last_online` 的在线核验与 `mysql.exe` CLI;
|
||||
> 本轮把同一通道扩到 `action_run` / `action_run_job` / `action_task` 的历史与时长,且实际驱动是
|
||||
> **pymysql**(不是 mysql.exe)。下面 §4.3 / §7 是本轮实测口径,落定后应并回`docs/CI/远程CI完整指南.md` §6.1 与 §2.3。
|
||||
|
||||
**本机事实**(变了就改指南,别改这里):runner `win-stm32-ci`(`gitea-runner` v4.0.1,labels `[windows]`,与 `runs-on: windows` 匹配);目录 `D:\Docker\act_runner\`;Gitea 1.25.4 监听 `127.0.0.1:3001`,数据库 MySQL `127.0.0.1:3306`/gitea(凭据在 `D:\Docker\Gitea\custom\conf\app.ini` 的 `[database]`,**口令不进脚本、不进报告**);本仓库 `repository.id = 10`(`wandering1/stm32f4-base`,另一仓 stm32f103 = 13);**CI 只跑这一台机器**,与其他机器无关。
|
||||
|
||||
## 0. 先分层,别一上来读 workflow
|
||||
|
||||
**分层之前先做 §1**:一次红很可能压根不是"门禁内容"问题,直接读 workflow 会白绕一圈。
|
||||
|
||||
| 层 | 就位判据 | 不在位的症状 |
|
||||
|---|---|---|
|
||||
| ① 服务端 Actions 开关 | `docs/CI/远程CI完整指南.md` §1.1 | push 后 Actions 页**完全空白** |
|
||||
| ② runner 在线且工具链对 SYSTEM 可见 | `docs/CI/远程CI完整指南.md` §4.1 + 下面 §4 | run 建了但一直排队;或 L0 死在"环境自检"、一个钩子都没跑 |
|
||||
| ③ workflow 与门禁 | `scripts/ci_local.py` 本地全绿 | 具体 job 红/绿与本地不一致 |
|
||||
|
||||
**本地永远先跑这个**(runner 掉线也不影响它,是 L0 的唯一保障;日志落法见 §5):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going # L0 + L0b + L0.5
|
||||
```
|
||||
|
||||
## 1. 第一步:这个 job 到底跑没跑起来(红因三分法)
|
||||
|
||||
**只看一条判据**:runner 侧那份 job 日志(`D:\Docker\act_runner\log\job\*-task-NN.log`)里
|
||||
**有没有出现任何一条钩子名(`cppcheck.......Passed`)或 `pre-commit hooks -- passed=A failed=B skipped=C` 汇总行**。
|
||||
|
||||
| 类 | 判据(日志里的证据行) | 结论与动作 |
|
||||
|---|---|---|
|
||||
| **A. job 没跑起来** | checkout 步骤报拉不到 `actions/checkout` / 连 `github.com` 超时,**通篇零钩子输出** | 门禁内容无罪。内网无公网时的三种处置在 `docs/CI/远程CI完整指南.md` §5.3(推荐零依赖纯 git checkout)。**先修它再谈别的**,否则后面所有"红"都不可读 |
|
||||
| **B. runner 机器缺环境** | `::warning::runner 未安装 MSVC`、`no Python with a SYSTEM-visible pre-commit`、`runner missing cppcheck`、`test-coverage` 因缺 **mscoverage** 组件而 FAIL(按用户裁定缺件即 FAIL,不 skip)、`ui-golden` 因缺 **SDL2 SDK** 而 FAIL(该 SDK 不入库,见指南 §4.5) | 也是"非代码问题",但**别改成 skip 蒙过去**:按`docs/CI/远程CI完整指南.md` §4.1/§4.2 补机器级工具链或 `DTU_*`,改完重启 runner |
|
||||
| **C. 真门禁失败** | 钩子清单完整且 `failed>0` 或某条 `Failed`,**且 §5 的本地同命令复现同样的红** | 这才轮到读 workflow / 门禁脚本,按失败钩子逐条定位 |
|
||||
|
||||
**顺序不能反**:A/B 两类在本地都是"全绿",用"本地绿 + CI 红"直接推"环境问题"也推不出是 A 还是 B——只有钩子输出能把它们分开。本轮实测:页面报的红看着像门禁失败,实际是 A(checkout 联网超时),而 A 修好之后 L0 还会再红两次——一条 C、两条 B。
|
||||
|
||||
## 2. 症状 → 一条判据 → 动作
|
||||
|
||||
| 症状 | 判据(跑这一条就够) | 结论 / 动作 |
|
||||
|---|---|---|
|
||||
| Actions 页空白、无 run | 看改了啥 + 怎么推的 | 四因:①服务端开关未开 ②workflow 没推到远程 ③**纯文档**(`paths-ignore: docs/**`、`**/*.md`)④**新建分支的首次 push**——`before` 是全零 SHA,变更文件集算不出来被 `paths-ignore` 滤空。④ 的实测:建分支那次 push 无 run,紧接着一次普通 push 立刻出 task;绕法 `workflow_dispatch` 或再推一个非文档提交 |
|
||||
| **页面报的时长/红数与本地或日志对不上** | §4.3 查 `action_run`/`action_run_job` 的 created/started/stopped | **页面时长不可照抄**。本轮实测页面写 `Failing after 6m49s`,库里 41 次 run **最长 ≈4m33s**,run 41 总时长 95 s(L0 45 s / L0.5 43 s)——**没有任何一条记录对得上 6m49s**(多半掺了排队/重试/页面自身口径)。以库里的 `total=stopped-created`、`run=stopped-started`、`queue=started-created` 三个数为准 |
|
||||
| run 全 **cancelled**、零日志 | `& '.\scripts\ci_runner_bootstrap.ps1' -Status` | runner 掉线;且 `ci.yml` 的 `concurrency.cancel-in-progress: true` 会把排队 run 被下次 push 取消 → **症状是"任务消失"而不是"任务失败"**(历史上 16–21 连续 12 个 job 零日志即此) |
|
||||
| `ui-golden` 红、日志是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除它,`git ls-files` 在该 SDK 目录下命中 0 个文件);后者 = 上一轮配置在硬校验处中断,**CMake 缓存文件已写下、工程文件却没生成**,于是下一轮跳过配置 | 设机器级 `DTU_SDL2_DIR`(指南 §4.5);模拟器构建脚本已能自愈,也可手删那份构建目录。**别把构建产物路径当文档引用写**——它不入库,CI 的干净检出里不存在,`doc-links` 会「本地过、CI 挂」(指南 §4.1.2 同族) |
|
||||
| runner 显示 **online** 却每 10 秒重启、日志暴涨(实测 ~20 MB/天) | daemon 日志出现 `another gitea-runner process is already using ".runner"` | **双看门狗抢锁**:`Unregister-ScheduledTask` **不会**停掉旧实例起的进程 → 孤儿看门狗养着旧 daemon → 它持 `.runner.lock` → 新任务里的 daemon 起不来,而**服务端仍显示 online**(旧 daemon 在服务)。修:管理员 `-StartNow`(它**先杀看门狗、再杀 daemon**,顺序反了会被 10 秒内复活) |
|
||||
| L0 失败但**本地同一条命令全绿**,日志 `exit code: 254` + `Missing git configuration: please set user.name` | 看是不是 254 | **不是提交信息问题**:`actions/checkout` 把 HOME 指向每次任务的临时目录,SYSTEM 无全局 git 身份 → gitlint 在读规则**之前**就配置错误退出。已由 `scripts/ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份修掉;**254 = 门禁没跑起来(exit 2),1 = 信息真违规(exit 1)**,别把两者混成一句 |
|
||||
| 钩子条数对不上 | `grep -c 'id:' .pre-commit-config.yaml` | **报告数 = 定义数 − 1**(gitlint 属 `commit-msg` stage,`--all-files` 跑不到且表现为全绿 = 静默空转)。增删钩子后重算这个差值 |
|
||||
| `test-coverage` 恒 `[ENV] exit 2`(缺 `Microsoft.CodeCoverage.Console.exe`),或换 VS 世代后大面积假红 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py --list` 看它探到几个根;再看机器级 `DTU_MSVC_CL` 指的是哪套 cl | 该 exe **只随 VS 分发**(NuGet 实测拿不到);装在非默认盘还要显式指 `DTU_MSVC_CL`。安装入口 + 组件 ID + 三条管理员命令在 `docs/CI/远程CI完整指南.md` §4.4 |
|
||||
| `mem-numbers` 报"文档与 .map 不一致",但**没改过代码也没改过那篇文档** | `.map` 的 mtime 是否早于最近一次切分支 | **跨分支假象**:`.map` 是 gitignored 的 Keil 产物,切分支后它仍属于另一分支的代码。重新 `@build` 即一致,不要去改文档数字 |
|
||||
| 明明设了 `DTU_KEIL_UV4` 却报 `UV4.exe not found` | `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')` | 会话没继承用户级变量。**回填时别按"进程里已有的变量名"枚举**——Machine 轮会把只在 User 定义的变量置空(PS 赋空值 = 删除),User 轮就再也看不到它。正确:名字取 Machine/User **并集**,且只在取到值时写入 |
|
||||
| 日志里出现 `gitea-runner.exe : …` + 4 行 `CategoryInfo`/`FullyQualifiedErrorId` | 实测 2 行真日志写成 8 行 | PS 5.1 的 `2>&1` 把每条原生 stderr 包成 ErrorRecord。修:`2>&1 \| ForEach-Object { "$_" }`(等价于原始行,且 `$LASTEXITCODE` 不受影响)。详见`docs/CI/远程CI完整指南.md` §7.3 |
|
||||
| 日志文件半 UTF-8 半乱码 | 文件头是否 `FF FE` | `>` / `*>>` 在 PS 5.1 = `Out-File` = **默认 UTF-16LE**;与 `Add-Content -Encoding ascii` 混用会一个文件两种编码 |
|
||||
|
||||
## 3. runner 保活与注册(唯一入口脚本)
|
||||
|
||||
```powershell
|
||||
# 免管理员预演,先看它会做什么
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -DryRun
|
||||
# 正式:写机器级 DTU_* + 装看门狗任务 + 立即起(需管理员)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
|
||||
# 体检(非管理员会明说哪些项读不到,而不是谎报)
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -Status
|
||||
```
|
||||
|
||||
要点(细节与理由见 `docs/CI/远程CI完整指南.md` §2.4):
|
||||
|
||||
- **不能只起 daemon**:v4 无服务支持,且**首次 Declare 失败就退出** → 一次 Gitea 重启就永久静默掉线。
|
||||
- **不能用 Windows 服务**(服务化方案已被实测推翻,见 `docs/CI/远程CI完整指南.md` §3.0);任务计划程序的"失败后重启"**永不触发**,因为 daemon 的退出是**正常退出**。
|
||||
- 任务对象:`-AtStartup` + `Delay=PT1M`、`ExecutionTimeLimit` 为零、`RestartCount 999`、`MultipleInstances IgnoreNew`、`$settings.StartWhenAvailable=$true`——⚠ `StartWhenAvailable` 是**任务设置**属性,挂到 trigger 上会抛异常(曾因此注册崩溃)。
|
||||
- `0x41301` = `SCHED_S_TASK_RUNNING`(不是失败);`-AtStartup` 的任务"下次运行时间"为空是正常的。
|
||||
- 改完 bootstrap 脚本里的启动器模板,**磁盘上的看门狗要下次 `-StartNow` 或开机才刷新**——不必为此专门重启 runner。
|
||||
|
||||
## 4. 没有 API 令牌时怎么判断状态
|
||||
|
||||
匿名调 `/api/v1/repos/.../actions/runs` 是 **401**。三条不依赖令牌的路,按"在线 → 落没落地 → 历史与时长"排。
|
||||
|
||||
### 4.1 在线核验(查 `action_runner`)
|
||||
|
||||
`action_runner.last_online` 与当前 Unix 秒相差 **< 30 秒**即在正常心跳;`agent_labels` 应为 `["windows"]`(不含 windows 就是标签配错)。口径见 `docs/CI/远程CI完整指南.md` §6.1。
|
||||
|
||||
### 4.2 看 runner 侧(最直接,能证明"job 真落到本机了")
|
||||
|
||||
```powershell
|
||||
$d = 'D:\Docker\act_runner\log'
|
||||
# 日志被 daemon 占着,必须共享读打开(ReadAllBytes 会 IOException)
|
||||
$fs = [System.IO.File]::Open("$d\runner-daemon.log",'Open','Read','ReadWrite')
|
||||
# 关键行:declare successfully / writing the log of task N to … / Running job with …
|
||||
Get-ChildItem "$d\job" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 5
|
||||
```
|
||||
|
||||
判读:`declare successfully` 后长时间零输出 = **在线且空闲**;`log\job\*.log` 出现新文件 = 有 task 落地;每个 job 结束时 `[runner] task N finished: success|failure`。**这份 `finished:` 行也是 §4.3 里校验 status 枚举的裁判**(§1 的三分法同样从这些文件起手)。
|
||||
|
||||
⚠ 非管理员**读不到 SYSTEM 进程的 `CommandLine`**(返回 `$null` → 过滤器静默匹配 0 个)→ 绝不能据此说"没有看门狗"。`Process.StartTime` 对 SYSTEM 也不可读,但 CIM 的 `CreationDate` 可读 → 用**进程存活时长**对比 10 秒重启间隔来判断重启循环,**不要 grep 不轮转的日志尾部**(旧失败一直在尾部,会对健康 runner 报假警)。
|
||||
⚠ 查询进程自己的命令行含关键字会**自匹配**:把模式拆开写,如 `'run_runner_watch' + 'dog\.ps1'`。
|
||||
|
||||
### 4.3 查 run / job 历史与时长(推翻页面上的说法)
|
||||
|
||||
**驱动就认 pymysql**:本机 `C:/ProgramData/Anaconda3/python.exe` 实测 `pymysql=True`、`MySQLdb=False`、`mysql.connector` **缺失**(import 直接 `ModuleNotFoundError`)→ `docs/CI/远程CI完整指南.md` §6.1 的 `mysql.exe` CLI 路线本机走不通,改 pymysql 连 `127.0.0.1:3306`/gitea。
|
||||
|
||||
**口令只从 app.ini 运行时读**:`[database]` 段里 `HOST` 是 `host:port` 一体要拆开,`NAME`/`USER`/`PASSWD` 直接用;**绝不要把口令写进脚本字面量、报告、截图或提交**(本轮就是这么漏的一次)。同段还有 `PATH = ...gitea.db`,但 `DB_TYPE = mysql` → **别去读那个 sqlite 文件**。`token`/`PASSWD` 一类行在打印任何配置文件时统一 redact。
|
||||
|
||||
```python
|
||||
# 写成文件跑(ci-artifacts/_q.py),别内联,见 §6
|
||||
import re, pymysql
|
||||
ini = open(r'D:/Docker/Gitea/custom/conf/app.ini', encoding='utf-8').read()
|
||||
sec = ini.split('[database]')[1].split('\n[', 1)[0]
|
||||
kv = dict(re.findall(r'(?m)^(\w+)\s*=\s*(.*)$', sec))
|
||||
host, port = kv['HOST'].split(':')
|
||||
db = pymysql.connect(host=host, port=int(port), user=kv['USER'], password=kv['PASSWD'],
|
||||
database=kv['NAME'], charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor)
|
||||
cur = db.cursor()
|
||||
cur.execute("SHOW TABLES LIKE 'action%'") # → action_run / action_run_job / action_task /
|
||||
# action_run_index / action_runner / action_task_step …
|
||||
cur.execute("SHOW COLUMNS FROM action_run_job") # 🔴 列名以这里为准,别凭记忆 SELECT
|
||||
```
|
||||
|
||||
**取数(本仓库务必带 `repo_id=10`,否则混进 stm32f103 的 run)**:
|
||||
|
||||
- `action_run`:`id, repo_id, status, title, ref, sha, event, created, started, stopped`,`ORDER BY id DESC`;
|
||||
- `action_run_job`:按 run 关联到 `id, status, attempt, task_id, created, started, stopped`(本轮 run 41 → job 85/86)。
|
||||
**`task_id` 就是 job 日志文件名的 `task-NN`** → `action_run_job.task_id = 66` ↔ `log/job/20261025-133227-task-66.log`,这是 DB 与 runner 日志互校的唯一把手;
|
||||
- 时长三个数:`total = stopped-created`、`run = stopped-started`、`queue = started-created`(本轮实测 queue 普遍 1 s,所以页面 6m49s 不可能是排队)。时间戳是 **Unix 秒**,`datetime.datetime.fromtimestamp(x)` 再打印;
|
||||
- **连续红次数按 job 数、不按 run**:把同一 job 历次 `status` 从新到旧排,数连续非"成功"的长度(本轮据此得出 L0 自上线 **13 次全红、从未绿过**,run 28~41)。
|
||||
|
||||
**status 枚举:先定死再下结论**(本轮最大的误判源)。
|
||||
|
||||
| 码 | 含义 | 依据 |
|
||||
|---|---|---|
|
||||
| 0 | Unknown / 未开始 | 本轮映射试错后才定 |
|
||||
| 1 | Success | 同上 |
|
||||
| 2 | **Failure** | run 41 = 2 → Failure,与页面红、与 runner 日志 `task finished: failure` 三方一致 |
|
||||
| ≥3 | **本轮未核实** | 试错时猜过 `3=Failure`/`4=Cancelled`/`5=Skipped`,**都没有交叉验证** → 用到之前先拿一条已知结局的 run 反推(TODO:从 Gitea 源码定死,别再靠猜) |
|
||||
| 5 / 6 | **非终态**(排队中 / 运行中) | 观测于 run 45:推送后先看到 job=5(未开始)与 6(进行中),数分钟后转 1 / 2;**别把它当成库里坏了或卡死** |
|
||||
|
||||
- **写查询时同时打印原始数字和映射名**;`action_run` 与 `action_run_job` **默认按同一套码处理是危险假设**,跨到别的表要重验。
|
||||
- 判据:同一个 run 换一套映射后红绿翻转(本轮 step 53 判 `Success`、step 56 判 `Failure`)→ 说明你还没定死枚举,**这时任何"红绿史"都不许出口**。
|
||||
- 中文标题在 bash 终端回显成乱码(本轮 `'docs(log): ???'`)只是**显示**问题,库里数据没坏 → **认 run 用 `id`/`sha`/`ref`,不要用标题文本匹配**;要看中文就把结果写文件再按 UTF-8 读。
|
||||
|
||||
## 5. 本地对照:日志一律落 `ci-artifacts/`
|
||||
|
||||
判"本地绿 CI 红"必须有同一份可引用的本地日志。落点在仓库内 gitignored 的 `ci-artifacts/`(`.gitignore`:159),**不要写仓库外绝对路径**:
|
||||
|
||||
```bash
|
||||
mkdir -p ci-artifacts && .venv/Scripts/python.exe scripts/ci_local.py --keep-going \
|
||||
> ci-artifacts/local_ci_$(date +%Y%m%d).log 2>&1; echo EXIT=$? >> ci-artifacts/local_ci_$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
**本轮实测的替换**:先写到 `D:/Code/_sync_backup_20261026/ci_local_*.log`(仓库外)在后台 bash job 里直接 `No such file or directory` → 改落 `ci-artifacts/` 才跑通。跑门禁这类分钟级任务用后台 job 起,再 `job_output` 收,别占着前台。Keil 构建对照同理落 `ci-artifacts/local_build_*.log`。
|
||||
|
||||
## 6. 脚本层:写查询与批量探测的规矩(本轮踩点集中在这里)
|
||||
|
||||
- **一段 Python 查询写成 JS 模板字符串会炸**(本轮两次:`Expected unicode escape`、`Expected ';', '}' or <eof>`)。正确做法:按行存字符串数组 → `join('\n')` → 落盘 `ci-artifacts/_qN.py` → 用 `C:/ProgramData/Anaconda3/python.exe` 跑 → **收尾统一 `fs.rmSync` 删掉**(临时查询脚本不入库,也别留在仓库里)。
|
||||
- **`run_code` 里不能用 `import x from '…'`**(`'import', and 'export' cannot be used outside of module code`)→ `const fs = await import('node:fs')`。
|
||||
- **批量探测拆小批**:一次调用塞 4 条 Windows 探测(worktree / 目录 / 服务枚举 / app.ini)撞过 120 s deadline,只拿到部分输出。一次 1~2 条,慢源(`Get-ChildItem -Recurse`、`Get-CimInstance Win32_Service` 全量、`generate-config`)单独跑并显式 `timeoutMs`。
|
||||
- **别对全部 runner job 日志一把梭 grep**:日志混合编码(UTF-16LE / CP936),逐文件返回一堆 `[ERR …]`。要么单文件 `head`/`sed -n 'A,Bp'`/`grep`(本轮有效路径),要么解析时逐文件 try/catch 容错。
|
||||
- **`pwsh`/`read`/`subagent` 这类工具只能在 `run_code` 程序里 `await tools.xxx()` 调**,直接当工具名调会 `unknown tool`;每次调用**都要带 `description`**(漏了会 `missing required property "description"`)。
|
||||
|
||||
## 7. runner 有哪些配置项:先 `generate-config`,别猜键名
|
||||
|
||||
**判断"这个键存不存在"的唯一凭据是 runner 自己 dump 出来的默认配置文本**,全程只读——不写 `config.yaml`、不删 `.runner`(那是注册凭据,删了要重新 register)、不重启/注销 runner(只有改 `runner.labels` 才必须重启,`docs/CI/远程CI完整指南.md` §2.3.2)。
|
||||
|
||||
```powershell
|
||||
& 'D:\Docker\act_runner\gitea-runner.exe' generate-config 2>&1 | Out-String -Width 160
|
||||
```
|
||||
|
||||
- 从输出里 **grep 目标键名**(本轮实测清单含 `cache` / `capacity` / `container` / `default_image` / `action_shallow_clone` / `access_key_file` / `addr` / `allocate_pty` / `bind_workdir` / `bucket` / `docker_host` / `docker_timeout` / `enabled` / `endpoint` / `env_file` / `envs` / `external_secret` / `external_secret_file` …,以及 `#set_act_env: true`)。
|
||||
- **dump 里绝大多数键是带 `#` 的注释行** → "在清单里" ≠ "本机已启用";是否被显式覆盖要跟 `config.yaml` 逐行比:**注释=默认值生效,非注释=本机写死了**。
|
||||
- **别拿段名当 grep 锚点**:本轮找 `[actions]` 段和 `DEFAULT_ACTIONS_URL` 时段名锚点落空、上下文打出来的是 `log_report_*` 那几行 → 直接按**键名** grep。另外服务端的 `DEFAULT_ACTIONS_URL` 是 app.ini 的键(`docs/CI/远程CI完整指南.md` §1.2.1),不要当成 runner 的键去 `config set`。
|
||||
- 老教训仍然成立:v4 的 `log` 段只有 `level` 和 `job`,**没有 `file`**,`config set log.file …` 会 `unknown config key`(`docs/CI/远程CI完整指南.md` §2.3.2)→ 改键前以 `generate-config` 输出为准;写完用 `gitea-runner.exe config get <键>` 自证。
|
||||
|
||||
## 8. 门禁口径:讲规则,不讲数字
|
||||
|
||||
数字随分支和提交漂移(实测同一句"18 个钩子"在另一分支是 26,master 后来涨到 33;单测从 3 套件/288 断言涨到 10 套件/647)。**只有规则稳定**:
|
||||
|
||||
- L0:`pre-commit hooks -- passed=A failed=B skipped=C`,其中 **A+B+C = 定义数 − 1**;`skipped` 本机唯一常见项是 `mem-numbers`(无 `.map` 即跳过,并以 `::warning::` 明示"CI 不守体积门禁",见`docs/CI/远程CI完整指南.md` §0.0)。
|
||||
- L0b:`gitlint: N commit message(s) checked, 0 failed`。
|
||||
- L0.5:`<N> runner(s), A/A assertions passed, fail=0`,**N = `test/host/` 下 `run_unit_test.py` 的个数**。
|
||||
- **判定只看 `failed=0`、`fail=0`、A==A**;抄历史数字当基准必然误判。
|
||||
|
||||
## 9. 改 workflow / 门禁脚本的红线
|
||||
|
||||
`.gitea/workflows/ci.yml`:
|
||||
|
||||
- `shell: powershell`,**不是 `pwsh`**(Windows 自带只有 5.1,`pwsh` 会 `Cannot find: pwsh in PATH`)。
|
||||
- **`run:` 块内必须纯 ASCII**:runner 把每块写成无 BOM 临时 `.ps1` 交给 5.1 → 按 CP936 解码 → 中文导致"字符串缺少终止符"(报错行号正好在含中文那行,极易误判为逻辑错)。中文只写在 YAML 的 `name:`/注释里。
|
||||
- 写 `GITHUB_ENV` / `GITHUB_PATH` / `GITHUB_OUTPUT` 必须显式 `-Encoding ascii`(`Out-File -Append` 默认 UTF-16LE)。
|
||||
- 零依赖存档:`Tee-Object` 到 `ci-artifacts/*.log` + `$GITHUB_STEP_SUMMARY` + `::notice::`;**不用 `actions/upload-artifact`**(内网可能拉不到外层 action,`docs/CI/远程CI完整指南.md` §5.3)。
|
||||
- 工具链探测收在 `scripts/ci_find_python.ps1`(可离线自测 + 机器级安装兜底);`DTU_*` 最高优先级,且**显式设置就必须有效、不静默回退**。
|
||||
|
||||
`scripts/ci_*.ps1`:纯 ASCII(含中文的 `.ps1` 才需要 BOM,见`docs/CI/远程CI完整指南.md` §7.1);`-Encoding utf8NoBOM` 是 **PS 7 才有的**,5.1 要写文件用 `[System.IO.File]::WriteAllText($p, $s, (New-Object System.Text.UTF8Encoding($false)))`。
|
||||
|
||||
gitlint 专属:消息文件必须写在**仓库内**(跨盘符被 pre-commit 拒 `path is on mount 'C:', start on mount 'D:'`)且 **UTF-8 无 BOM**(BOM 占标题 1 字符,扭曲 100 上限)。
|
||||
|
||||
## 10. 改完必须验(顺序别省)
|
||||
|
||||
```powershell
|
||||
# .ps1:纯 ASCII + 语法
|
||||
$b=[System.IO.File]::ReadAllBytes($p); @($b|?{$_ -gt 127}).Count # 必须 0
|
||||
$e=$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $p),[ref]$null,[ref]$e)|Out-Null; @($e).Count # 必须 0
|
||||
|
||||
# run: 块:必须按 runner 的语义测——点源(10-27 用 run 44 换来的两条教训)
|
||||
# runner 执行方式: powershell -command . '<块体>' ← 只有这个是真语义
|
||||
$tmp = '.gitea/_blk_probe.ps1' # 用 python 从 ci.yml 抽出某步 run 正文写到这里,别另抄一份
|
||||
& powershell.exe -NoProfile -Command "`$ErrorActionPreference='Stop'; . (Resolve-Path $tmp)"
|
||||
# 教训一:块被点源进会话,$ErrorActionPreference 继承为 Stop 时,原生程序往 stderr
|
||||
# 写一行就变成**终止性** NativeCommandError,`2>` 重定向挡不住(`2>` 加 `$null` 也不行)。
|
||||
# 所以块首显式 `$ErrorActionPreference='Continue'`,原生调用一律摊平成字符串。
|
||||
# 教训二:`exit 1` 的退出码语义随调用方式变——`-File` 直跑=1;`-File` 包一层点源=**0 且继续执行**;
|
||||
# `-Command` 点源=1。用 `-File` 测会把失败全测成 rc=0(我第一版就栽在这,四条失败路径全绿)。
|
||||
|
||||
# .yml:可解析 + 全部 run: 块纯 ASCII + 无 BOM + LF
|
||||
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open(r'.gitea/workflows/ci.yml',encoding='utf-8'));print(list(d))"
|
||||
|
||||
# 文档:链接与章节引用
|
||||
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
|
||||
|
||||
# 全量门禁(等价 CI 的两个 job)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going
|
||||
```
|
||||
|
||||
提交信息:标题 ≤100、**正文每行 ≤80**、UTF-8 无 BOM、`git commit -F <文件>`;**先程序化数一遍长度再提交**(中文标题最容易超,`-m` 还会被终端代码页转码)。
|
||||
|
||||
推送后到 runner 侧确认(§4.2):两个 job 各自的 `SUMMARY` 行 + `finished: success`。**"推送成功"不等于"CI 跑过"**。
|
||||
|
||||
**汇报口径**:说"红了几次 / 跑了多久"必须写明**来源**(库里的 `action_run_job` 时长,还是 runner 日志,还是页面)与**所用的 status 映射**;三者不一致时以库 + 日志为准并说清页面为什么不可信。只读排查就全程只读——不改文件、不 commit/push、不动 runner 注册状态。
|
||||
|
||||
## 11. 分支与发布(走通的做法)
|
||||
|
||||
- 预演冲突面,不动工作区:`git merge-tree --write-tree --name-only <a> <b>`(exit 1 = 有冲突)。
|
||||
- 数字口径类冲突的正确解法:**改成规则 + 分分支快照**,而不是选一边留数字——否则同一篇文档在另一分支上必错,下次合并再撞一次。
|
||||
- 变更记录是**历史,永不改写**,只在最上方按日期加行。
|
||||
- 想验证 CI 又不想动主干:复制一条分支推上去(`git branch <copy> <src>` → `git push -u origin <copy>`),跑完再删。注意**新建分支的首次 push 不触发 run**(§2 症状表第 1 行)。
|
||||
- 主干落后很多时先同步再排障:备份脏文件 → 只 stash 被跟踪的改动 → `checkout` → `merge --ff-only` → `stash pop`,同步完再跑 §1(本轮本地落后 248 个提交,先同步才看得出真实红因)。
|
||||
- 删分支前先算独有提交:`git rev-list --count origin/master..<branch>`,**> 0 就别删**(删了只能靠 SHA 找回);本地一律 `git branch -d`(未合并它会自己拒绝)。
|
||||
- `git commit` / `git push` 都是"先问"动作(AGENTS.md 边界节),不是任何任务的隐含步骤。
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""serial_tool — Python 串口助手(常驻 daemon + 薄 CLI + 日志文件即接口)。
|
||||
|
||||
设计文档:docs/测试说明/测试工具/串口助手设计说明.md
|
||||
设计文档:docs/测试说明/串口助手设计说明.md
|
||||
|
||||
架构:
|
||||
CLI(短命进程,硬超时) --HTTP JSON--> daemon(detached 单实例,127.0.0.1)
|
||||
@@ -19,7 +19,7 @@
|
||||
status 全状态(单条 JSON)
|
||||
log --tail N --grep P 读日志(先过滤后截尾;-i 忽略大小写 / --regex 正则)
|
||||
clear 清空日志(原子截断 + 代际递增)
|
||||
wait-idle 等 RX 静默(/serial-boot-test 用;--settle-ms / --max-ms / --first-timeout-ms)
|
||||
wait-idle 等 RX 静默(@test 用;--settle-ms / --max-ms / --first-timeout-ms)
|
||||
start / stop / restart daemon 生命周期(其余命令会自动拉起 daemon)
|
||||
selftest 无头自检(行拆分/解码回退/截头算法,无需硬件)
|
||||
|
||||
@@ -615,10 +615,10 @@ class Daemon:
|
||||
def wait_idle(self, settle_s, max_s, first_timeout_s=None):
|
||||
"""等 RX 静默,返回 reason ∈ {idle, no_data, timeout}。
|
||||
|
||||
2026-10-09 修三处(旧实现会让 /serial-boot-test 在"板子全程没输出"时误判收尾):
|
||||
2026-10-09 修三处(旧实现会让 @test 在"板子全程没输出"时误判收尾):
|
||||
1. no_data 判据:旧实现比的是"进入函数时 daemon 累计 rx_bytes 是否为 0"。
|
||||
daemon 常驻(本机实测 rx_bytes=1.2MB)→ 永远 >0 → 板子一个字节都不发
|
||||
也返回 idle:true,/serial-boot-test 的 no_data 拦截形同虚设。现按**本次增量**判。
|
||||
也返回 idle:true,@test 的 no_data 拦截形同虚设。现按**本次增量**判。
|
||||
2. 首字节门:旧实现从进入即开始计静默时间,板子第一行比 settle_ms 来得
|
||||
晚就提前收尾("开机日志缺尾巴"的另一半成因)。现首字节到达前不启动
|
||||
settle 计时,改由 first_timeout_s 兜底(默认 = max_s)。
|
||||
@@ -1123,26 +1123,26 @@ def run_selftest():
|
||||
def check(name, cond, detail=""):
|
||||
results.append((name, bool(cond), detail))
|
||||
|
||||
# 行拆分:CRLF / 多行 / 残行冲刷
|
||||
# T1 行拆分:CRLF / 多行 / 残行冲刷
|
||||
sp = LineSplitter()
|
||||
got = [l for _, l in sp.feed(b"abc\r\n", 1000.0)]
|
||||
check("行拆分:CRLF 行", got == [b"abc"], repr(got))
|
||||
check("T1a CRLF 行", got == [b"abc"], repr(got))
|
||||
got = [l for _, l in sp.feed(b"de\nf\n", 1001.0)]
|
||||
check("行拆分:多行", got == [b"de", b"f"], repr(got))
|
||||
check("行拆分:残行不入文件", sp.feed(b"ghi", 1002.0) == [] and sp.buf == b"ghi")
|
||||
check("T1b 多行", got == [b"de", b"f"], repr(got))
|
||||
check("T1c 残行不入文件", sp.feed(b"ghi", 1002.0) == [] and sp.buf == b"ghi")
|
||||
got = [l for _, l in sp.flush_partial()]
|
||||
check("行拆分:残行冲刷", got == [b"ghi"], repr(got))
|
||||
check("T1d 残行冲刷", got == [b"ghi"], repr(got))
|
||||
got = sp.feed(b"\n\n", 1003.0)
|
||||
check("行拆分:空行忽略", all(not raw for _, raw in got) and sp.buf == b"", repr(got))
|
||||
check("T1e 空行忽略", all(not raw for _, raw in got) and sp.buf == b"", repr(got))
|
||||
|
||||
# 解码回退链
|
||||
# T2 解码回退链
|
||||
check("T2a UTF-8", decode_line("中文ok".encode("utf-8")) == "中文ok")
|
||||
check("T2b GBK", decode_line("中文".encode("gbk")) == "中文")
|
||||
check("T2c HEX 兜底", decode_line(b"\xff\xfe\x01") == "HEX:fffe01")
|
||||
|
||||
# 时间戳格式
|
||||
# T3 时间戳格式
|
||||
s = line_at(time.time(), "RX", "x")
|
||||
check("时间戳行格式", s.startswith("[RX] ") and "T" in s.split(" ")[1], s)
|
||||
check("T3 行格式", s.startswith("[RX] ") and "T" in s.split(" ")[1], s)
|
||||
|
||||
# T4 截头算法(保留尾部 + 截后继续追加)
|
||||
pat = b"0123456789abcdef"
|
||||
@@ -1298,8 +1298,8 @@ def run_selftest():
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# 端口建议:换 USB 口后 COM 号变了,唯一候选直接给(多候选不猜)
|
||||
check("端口建议:唯一候选才给建议",
|
||||
# T10 端口建议:换 USB 口后 COM 号变了,唯一候选直接给(多候选不猜)
|
||||
check("T10 port_hint 唯一候选才给建议",
|
||||
port_hint("COM7", ["COM6"]) == "COM6"
|
||||
and port_hint("COM7", ["COM6", "COM9"]) is None
|
||||
and port_hint("COM6", ["COM6"]) is None)
|
||||
@@ -1353,7 +1353,7 @@ def build_parser():
|
||||
|
||||
sub.add_parser("clear", parents=[common], help="清空日志(代际 +1)")
|
||||
|
||||
p = sub.add_parser("wait-idle", parents=[common], help="等 RX 静默(/serial-boot-test 用)")
|
||||
p = sub.add_parser("wait-idle", parents=[common], help="等 RX 静默(@test 用)")
|
||||
p.add_argument("--settle-ms", type=int, default=8000,
|
||||
help="静默判据毫秒(默认 8000:boot 有 3~4s 自然静默间隙)")
|
||||
p.add_argument("--max-ms", type=int, default=30000, help="最长等待毫秒(默认 30000)")
|
||||
|
||||
@@ -14,11 +14,9 @@ whenToUse: 需要向目标板烧录或复位后,用串口日志自动判定运
|
||||
| 脚本 | 作用 |
|
||||
|---|---|
|
||||
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | 一键闭环:open+clear → 烧录 → wait-idle → 提取 boot log → 烧录回读校验 → 标记校验。串口助手按 `$DSH_SERIAL_TOOL` > 同目录 > `../serial-assistant/scripts/serial_tool.py` 解析 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对任意日志文件做标记校验(Required/Forbidden/锚点计数/顺序/耗时),可单独用。`--exact` 做**整行 `^…$` 锚定**(子串口径照不出"行还在但行首被吃掉/行内被插字节"那一类损坏)、`--pair` 做跨复位配对、`--allow-residual/--head-char` 服务崩溃注入腿、`--selftest` 跑内置判别用例(**证明这套判据不是空跑**)。四个行级开关(`--exact` / `--pair` / `--allow-residual` / `--head-char`)同时接进了 `run_boot_test.py`,/serial-boot-test 闭环直接可用 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对任意日志文件做标记校验(Required/Forbidden/锚点计数/顺序/耗时),可单独用。`--exact` 做**整行 `^…$` 锚定**(子串口径照不出"行还在但行首被吃掉/行内被插字节"那一类损坏)、`--pair` 做跨复位配对、`--allow-residual/--head-char` 服务崩溃注入腿、`--selftest` 跑内置判别用例(**证明这套判据不是空跑**)。四个行级开关(`--exact` / `--pair` / `--allow-residual` / `--head-char`)同时接进了 `run_boot_test.py`,@test 闭环直接可用 |
|
||||
|
||||
> **`/serial-boot-test` 是 DSH 会话的技能调用名、不是 shell 命令**(同 `/keil-build`,见 AGENTS「构建与验证」总口径):可执行形态 = 上表 `run_boot_test.py` 命令行(仓库根跑);项目配置与判据见本仓 `docs/测试说明/流程与规范/自动化测试流程.md`。
|
||||
|
||||
> **判据取向(重要,会复发)**:`--exact` 默认**只锚文本、不钉长度**——字节长度属于某一份构建,钉死它会在无辜改动上假红(`@@LEN=<n>` 是显式逃生口,不是默认)。子串 `--required` 不能用于"可能丢行首/被插字节"的行。来龙去脉与固定动作见本仓 `docs/测试说明/专项验证/启动日志原子性复验流程.md`。
|
||||
> **判据取向(重要,会复发)**:`--exact` 默认**只锚文本、不钉长度**——字节长度属于某一份构建,钉死它会在无辜改动上假红(`@@LEN=<n>` 是显式逃生口,不是默认)。子串 `--required` 不能用于"可能丢行首/被插字节"的行。来龙去脉与固定动作见本仓 `docs/测试说明/启动日志原子性复验流程.md`。
|
||||
|
||||
交互式串口操作(open/send/log/wait-idle/status 等)不属于本 skill——
|
||||
**用 `serial-assistant` skill**。
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
5. verify_log.py 标记校验(含启动锚点计数 config "boot_marker" + --max-boot-count;
|
||||
--expect-reset-cause 原因白名单;--require-order 标记顺序;
|
||||
--done-marker + --max-boot-ms 开机耗时)
|
||||
6. (仅当 --require-selftest-pass)自检台账联合判定:默认**只判本次启动新增**
|
||||
6. (仅当 --require-selftest-pass)自检台账联合判定(S7):默认**只判本次启动新增**
|
||||
——烧录前先经 FTP 取 /log/selftest.log 基线,启动后再取一次,只把"基线之后新增"的
|
||||
记录交给判定(该文件是 512 条环形累计历史,"非空即 FAIL"判一次启动永远不可能过)。
|
||||
设备侧无此文件 = 台账干净 = PASS;**抓取失败 / 没有数据源 / 基线被环覆盖 → FAIL**,
|
||||
@@ -140,8 +140,8 @@ def _split_cmd(cmd_str):
|
||||
直接交给 subprocess、**不经 `cmd /c`**:CreateProcess 能直接跑 .bat,且 Python
|
||||
会给含空格的路径正确加引号。旧写法 `['cmd','/c', 串]` 在本仓库(目录名
|
||||
`DTU Code` 含空格)会被 cmd 的 /c 引号规则玩坏——实测报
|
||||
「'D:\Code\DTU' is not recognized」,回读校验被假判成"芯片内容不一致"。
|
||||
路径本身含空格时把该段用引号包住(「"D:\some dir\flash_tool.bat" flash」)。
|
||||
`'D:\Code\DTU' is not recognized`,回读校验被假判成"芯片内容不一致"。
|
||||
路径本身含空格时把该段用引号包住(`"D:\some dir\flash_tool.bat" flash`)。
|
||||
posix=False 是必须的:posix=True 会吃掉 Windows 路径里的反斜杠。
|
||||
"""
|
||||
return [p.strip('"') for p in shlex.split(cmd_str, posix=False)]
|
||||
@@ -226,7 +226,7 @@ def _ftp_quit(ftp):
|
||||
def _prune_out_dir(out_dir, keep):
|
||||
"""只保留最近 N 份产物(`boot_log_*.txt` / `selftest_log_*.log`)。
|
||||
|
||||
这些文件按 `%Y%m%d_%H%M%S` 命名、每次 /serial-boot-test 各留一份,旧实现从不清理——
|
||||
这些文件按 `%Y%m%d_%H%M%S` 命名、每次 @test 各留一份,旧实现从不清理——
|
||||
本机实测 out 目录已堆到 31 份 boot_log + 7 份 selftest_log(2026-10-10)。
|
||||
文件名含时间戳 → 按名字排序即按时间排序。
|
||||
"""
|
||||
@@ -294,7 +294,7 @@ def _fetch_selftest_log(device_ip, out_dir, stamp, ftp_user="admin", ftp_pass="a
|
||||
2026-10-02 台架修复:原实现只 `except OSError`,而 ftplib 的 error_temp /
|
||||
error_perm 继承自 Exception 而非 OSError —— 台账"仅故障才写",干净设备上
|
||||
/log/selftest.log 根本不存在,RETR 返回 450 直接抛出未捕获异常,令整个
|
||||
`run_boot_test.py --require-selftest-pass` 在最后一步崩掉(而它本该是 PASS 路径)。
|
||||
`@test --require-selftest-pass` 在最后一步崩掉(而它本该是 PASS 路径)。
|
||||
现把"设备侧无此文件"单列为正常路径(INFO),其余 FTP 错误保留 WARN。
|
||||
"""
|
||||
local = Path(out_dir) / "selftest_log_{}.log".format(stamp)
|
||||
@@ -313,7 +313,7 @@ def _fetch_selftest_log(device_ip, out_dir, stamp, ftp_user="admin", ftp_pass="a
|
||||
try:
|
||||
ftp.retrbinary("RETR /log/selftest.log", buf.extend)
|
||||
except ftplib.all_errors as e:
|
||||
# 450/550 = 设备侧无此文件:干净台账 → 判定 PASS(正常路径,非故障)
|
||||
# 450/550 = 设备侧无此文件:干净台账,S7 判 PASS(正常路径,非故障)
|
||||
print("SELFTEST_FETCH: {} -> (ledger absent: {}) 台账干净"
|
||||
.format(device_ip, str(e).strip()))
|
||||
_ftp_quit(ftp)
|
||||
@@ -357,7 +357,7 @@ def main(argv=None):
|
||||
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
|
||||
"with N>0, ';' separated. Empty = off")
|
||||
ap.add_argument("--require-selftest-pass", action="store_true", default=None,
|
||||
help="jointly judge /log/selftest.log: missing/empty = PASS, "
|
||||
help="jointly judge /log/selftest.log (S7): missing/empty = PASS, "
|
||||
"any 64B record = FAIL")
|
||||
ap.add_argument("--selftest-log", default=None,
|
||||
help="local copy of /log/selftest.log (skips device-side fetch when given)")
|
||||
@@ -526,7 +526,7 @@ def main(argv=None):
|
||||
print("Port: {} @ {} settle={}ms max={}ms".format(
|
||||
port, args.baud_rate, args.settle_ms, args.max_wait_ms))
|
||||
|
||||
# 0.6 自检台账基线(只看本次新增):**必须排在串口采集武装之前**。
|
||||
# 0.6 自检台账基线(S7「只看本次新增」):**必须排在串口采集武装之前**。
|
||||
# /log/selftest.log 是 512 条环形累计历史,用"文件非空即 FAIL"去判一次启动,在任何
|
||||
# 做过故障注入或长跑的设备上永远不可能 PASS(本仓台架实测:抓到的 29 条记录时间戳
|
||||
# 全是 2026-10-10 ~ 10-14 留下的,与本次启动无关)。这里记下基线的最后一条记录,
|
||||
@@ -698,7 +698,7 @@ def main(argv=None):
|
||||
print("RUN_TEST_ERROR: flash readback mismatch (chip content != built image)")
|
||||
return 2
|
||||
|
||||
# 5.5 自检记录判定:默认只判「本次启动新增」,累计口径要显式指定。
|
||||
# 5.5 自检记录判定(S7):默认只判「本次启动新增」,累计口径要显式指定。
|
||||
# 三种抓取结果必须分开对待(旧实现把后两种都当"无记录 → PASS",就是假绿):
|
||||
# absent(设备侧无此文件) → 台账干净 → 无新增 → PASS
|
||||
# error(连不上/登录被拒/故障)→ 未知 → FAIL(要求判定却没数据,不能当通过)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify_log.py - 对抓取的 MCU 调试日志做标记校验(纯标准库,2026-09-20 替代 verify_log.ps1)。
|
||||
|
||||
用于 /serial-boot-test 闭环:烧录后从串口助手日志提取的开机日志交给本脚本 grep 标记,
|
||||
用于 @test 闭环:烧录后从串口助手日志提取的开机日志交给本脚本 grep 标记,
|
||||
输出结构化判定(格式与旧 PowerShell 版逐字一致,下游文档/习惯不受影响)。
|
||||
|
||||
用法:
|
||||
@@ -16,15 +16,10 @@
|
||||
[--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 讲清楚;/serial-boot-test 那层的码不变):
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(所有 Required 标记找到,Forbidden 零命中,汇总行要求满足)
|
||||
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——绝不能把真崩溃降级成"没数据"。
|
||||
1 = FAIL(缺 Required / 命中 Forbidden / 汇总行要求不满足)
|
||||
2 = ERROR(日志文件不存在/不可读)
|
||||
|
||||
默认 Required 标记对应 STM32F4-Base 启动序列(见 App/app/app_main.c + App/task/net_task.c),
|
||||
按测试用例覆盖,例如:
|
||||
@@ -46,7 +41,7 @@
|
||||
命中即判 PASS,**套件是否跑到汇总行、断言数是否为 0 完全不可见**(套件中途
|
||||
挂死/复位、或 Phase 宏全关时会被误判为通过)。`--require-suite-summary`
|
||||
要求日志中出现 `=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。
|
||||
- 自检记录联合判定(2026-10 实机窗口;判据 2026-10-22 改为"只看本次新增"):
|
||||
- 自检记录联合判定(S7,2026-10 实机窗口;判据 2026-10-22 改为"只看本次新增"):
|
||||
`--require-selftest-pass` 对 /log/selftest.log 的本地副本(定长 64B 记录,格式见
|
||||
App/log/sys_log.h)判定。自检仅故障写盘(v1 不记通过项):**文件缺失 / 0 字节 = 无故障
|
||||
→ PASS**;存在记录 = FAIL(退出码 1),逐条打印时间戳 + 描述。
|
||||
@@ -54,7 +49,7 @@
|
||||
run_boot_test.py 在烧录前取基线、启动后只把"基线之后新增"的记录写成临时副本交给本
|
||||
函数(`--selftest-scope this-run`,默认)。**抓取失败/无数据源/无法证明无新增一律由
|
||||
调用方判 FAIL**——旧实现把这些都当"无记录 → PASS",是假绿(详见
|
||||
docs/测试说明/流程与规范/自动化测试流程.md 的自检结果判定节)。
|
||||
docs/测试说明/自动化测试流程.md 的自检结果判定节)。
|
||||
- 启动锚点计数(2026-10-10):`--max-boot-count N`(run_boot_test.py 默认传 1)统计
|
||||
`reset cause:` 锚点出现次数——**0 次** = 采集没从复位开始(标记全中也可能只是上一轮
|
||||
的残留日志);**>N 次** = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
@@ -229,31 +224,6 @@ 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="",
|
||||
@@ -549,39 +519,6 @@ 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):
|
||||
@@ -642,19 +579,6 @@ 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))
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: stale-doc-count-sweep
|
||||
description: 全仓陈旧计数同步 playbook——某个事实数(黄金参考页数/张数、菜单树叶数与已绑叶数、字库字数与字形数、提交钩子数等)变了之后:每个数先剔伪命中再双源互证定真值 → 全形态全命中面扫描(含 .pre-commit 钩子显示名与 .dsh/skills 正文)→ 逐条分类(当前态改 / 历史行冻结(只有保留变更记录的 5 篇真台账与 scripts/archive/ 归档) / 同数不同义不动 / 带日期戳的测量只加「当时」/ 数字对但口径描述错另起一笔)→ 每处先读现文再字面替换并断言命中数 → 全文通读核对各小节 → 提交后再按全部旧形态复扫归零 → 跑门禁分笔提交。
|
||||
whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「同步一下 / 全仓改一遍 / 这个页数(张数、叶数、字形数、钩子数)不对」、怀疑某个数是伪命中或口径错了(含 notdef 吗、含 ASCII 吗、是张数还是页数)、发现文档数字与实测不符、或同一份文档两节数字/顺序对不上要先核口径时。
|
||||
---
|
||||
|
||||
# 陈旧计数全仓同步
|
||||
|
||||
适用:**一个事实数变了**(黄金参考 27→40→47→53→60→66、已绑叶数 22→28→30、主机单测套数 28→33→41→44、无专属钩子套数 18→19→22、字库 185/280→218/313→224/319、菜单节点行数 42→43、钩子 51→52→51→52…),仓库里多处引用它的当前态表述要跟着变,而历史记录、同数异义、带日期戳的测量**不能**跟着变。
|
||||
|
||||
本仓已跑过四轮,照序列复跑、别临场重新发明分类:`b24b2b5f`(27→40,13 文件)、`31ca4a1f`(40→47,11 文件)、UI 补齐批(`94b0d790` 落地 → `cbb4fdb8` 产品需求同步 → `11d60057` 架构总览补漏 → `6daa01ae` 字体**口径**更正)、钩子一增一减(`1668842d`,52→51)、**第五轮 2026-10-29 录波落地**(套数 41→44、无专属钩子 19→22、黄金 60→66、菜单 42→43 节点 / 33→34 叶、已绑 28→30、字库 218/313→224/319;改动面含 `.pre-commit-config.yaml` 显示名与 `docs/门禁说明/README.md` 成对、三个 skill 正文)。
|
||||
|
||||
## 各计数的权威口径速查(本仓实测值,动手前先对表)
|
||||
|
||||
| 计数 | 真值来源(**至少两处互证**) | 已知伪命中 / 换算 |
|
||||
|---|---|---|
|
||||
| 内容页黄金参考**张数** | `simulator/tests/ref/` 下 `shot_*.bin` 实数(2026-10-29 实测 **66**)= `simulator/tests/check_content_pages.py` 的 `PAGES` 条目数(实测 **66**,其中 2 个 `_p2` 分页 → 页面 64) | 分页页要**单独再拍一张**(id 加 `_p2`,如 `runreport_p2`)→ **张数 = 页面个数 + 分页多出的张数**;两个数分开报,不许互相冒充(53 张时代的一个 12 行分页页会让 55 页变 56 张这类差 1) |
|
||||
| 已绑内容页**叶数** | `Drivers/BSP/UI/ui_page_registry.h` 的 `#define UI_PAGE_MAP_COUNT 30u`(由 `tools/menu_gen/gen_menu.py` 生成;2026-10-29 录波道绑两叶后 28→30) | `grep -c '#page=' ui_menu_tree.txt` 得 **31**:第 5 行是菜单文件**头部注释里的示例行** → 剔掉注释/模板行后的 30 才是真值 |
|
||||
| 提交**钩子数** | YAML 解析后的条目数(实测 **51**):自查命令见 `docs/门禁说明/README.md` §2 首条;与 AGENTS.md「共 N 个钩子」、该目录 §2 标题**三数同源** | `grep -c` 配置里的声明行 = 52(多算一条**被注释掉的** clang-format 候选);在配置**注释里复述那个 grep 字面量**会让文本计数再虚高 1 |
|
||||
| 字库**唯一字数** | `ui_cn_chars.txt` 唯一码点数(2026-10-29 实测 **224**,其中 ASCII 区 **0** 个) | `make_font.py` 的 `-r 0x20-0x7E` 是 ASCII 子集参数,**不算进字数** |
|
||||
| **字形数** | `Drivers/BSP/UI/ui_font_cn_12.c` 里 `.bitmap_index` 出现次数 **320 条含 notdef → 319 字形**;换算式 = 224 汉字/符号 + ASCII 95 = 319 | 直接报 314 就是把 notdef 当字形;文档必须写出「不含 ASCII」「含 notdef 共 314 条」这类口径句 |
|
||||
| 变更记录**保留篇数** | 变体标题感知的普查(`grep -rn "^## " docs scripts` 再看哪些标题是变更记录 / 修订记录);当前态清单以 `docs/文档规范.md` §5.1 那行为准 | 只 grep 裸 `^## 变更记录` 会**漏算带编号与中文序号的标题**(实测 5 篇真台账里 2 篇是 `## 6.` / `## 九、`,另有 `## 修订记录` 变体)——2026-10-29 就差点把 5 改成 3 |
|
||||
| 菜单**节点数 / 叶子数** | `ui_menu_tree.txt` 头部规则:缩进即层级(每级 2 空格)、`#` 开头为注释、空行忽略;叶子 = **不存在更深缩进后继行**的行 | 43 是节点行数、34 才是叶子(2026-10-29 现值;42 / 33 是此前时点);`未绑 = 待做 + 不做` 要与分类表逐项闭合 |
|
||||
|
||||
## Steps
|
||||
|
||||
1. 实测真值:先剔伪命中,再双源互证
|
||||
2. 全形态 + 全命中面扫描
|
||||
3. 逐条分类,五类各走各的路
|
||||
4. 每处先读现文,再逐条字面替换并断言命中数
|
||||
5. 一笔只装一类事
|
||||
6. 被改文件全文通读,核对其余小节
|
||||
7. 提交后再复扫一遍
|
||||
8. 跑门禁、分笔提交
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [steps.md](reference/steps.md)。动手前先读那篇。
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- 把伪命中计进真值
|
||||
- 字形数没剔 notdef
|
||||
- 数字对但口径描述错
|
||||
- 权威脚本乱码就臆读或退回抄文档
|
||||
- 钩子数用 grep 声明行
|
||||
- 张数与页数互相冒充
|
||||
- 只扫 docs/
|
||||
- 提交完就收工
|
||||
- 去已删掉变更记录的文档里「补一行流水」
|
||||
- 历史行被当陈旧值改掉
|
||||
- 照抄用户给的数
|
||||
- 把节点行当叶子数
|
||||
- 整文件正则批处理
|
||||
- 反引号截断
|
||||
- 只改记得的那节
|
||||
- 误报跟着改
|
||||
- 带日期戳的测量臆改新数
|
||||
- 用截断的 grep 输出判定
|
||||
- 新写文本撞编号门禁
|
||||
> 本节全文(逐字搬移、未改写,含命令与实测数字)→ [pitfalls.md](reference/pitfalls.md)。动手前先读那篇。
|
||||
|
||||
## Verification
|
||||
|
||||
提交前逐条打勾;回报时**逐处列文件 + 命中数断言**,不要笼统说「已全部更新」。
|
||||
|
||||
- [ ] 每个数都写了**两个独立来源**且数值一致(`ref/shot_*.bin` 实数 = `PAGES` 条目数;`UI_PAGE_MAP_COUNT` = 剔伪命中后的 `#page=` 计数),并在结论里贴出该数字与伪命中取舍理由
|
||||
- [ ] 字形数说明**是否剔除 notdef**并给出条目数→字形数的换算(条目数 − 1 = 字形数,2026-10-29 现值 320 − 1 = 319);字数说明**是否含 ASCII**,且该口径句与实测一致
|
||||
- [ ] 分类表逐条列出:改了哪些当前态、哪些历史行保留、哪些误报不动(附理由)、哪些只加「当时」、哪些属**口径错**并已另起一笔
|
||||
- [ ] 替换脚本打印了每处 `old -> new xN` 且每处 `assert` 通过(无半落地;报错时已 `git diff --stat` 确认零写入)
|
||||
- [ ] 命中面覆盖 `.pre-commit-config.yaml` 钩子 `name:`、AGENTS.md、`docs/**`、`scripts/**`、`.dsh/skills/*/SKILL.md`,逐条在改动清单里出现
|
||||
- [ ] 被改文件已全文通读,同文档各小节与配套 README/清单的顺序、计数、完成状态一致;口径闭合(如 `未绑 = 待做 + 不做`)
|
||||
- [ ] 变更记录:**保留的 5 篇真台账**(清单 → `docs/文档规范.md` §5.1)历史行原文未动;其余文档本轮**零新增流水行**(当前态改完即止);钩子一增一减这类过程细节写进提交正文,不写进文档
|
||||
- [ ] **提交后**又按全部旧形态复扫一次,当前态残留 = **0**,每条剩余命中都能归类(历史行 / 误报 / 已加「当时」);发现的漏网当前态已单独补一笔
|
||||
- [ ] `check_doc_links.py --all --sections-strict` 打印「文档路径检查通过(N 个文档)」;`ci_local.py` / `pre-commit` 全绿(RC=0、`failed=0`);BOM 与原文件一致、无 `EF BF BD`
|
||||
- [ ] 分笔提交完成,回报 commit 号 + 每笔改了哪些文件 + 没动的那几类及理由
|
||||
@@ -1,24 +0,0 @@
|
||||
# Pitfalls(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 stale-doc-count-sweep 的 SKILL.md「Pitfalls」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
- **把伪命中计进真值**:`grep '#page='` 的首条是菜单文件头部注释里的示例行,29 里含它;真值是注册表 `UI_PAGE_MAP_COUNT` 的 28。任何「数出现次数」式计数,先打印全部命中逐条看,剔注释行/模板行/示例行。
|
||||
- **字形数没剔 notdef**:`.bitmap_index` 314 条里第 1 条是 notdef,字形数是 **313**;照抄 314 就把占位符当成了字形。
|
||||
- **数字对但口径描述错**:218 这个数两处都对,错在「含 ASCII/符号」这句口径(实测 218 个码点全为非 ASCII)。它不进任何数字 diff,只能逐句读口径句;且必须**另起一笔**,混进数字同步那笔会让 review 者以为只是顺手改措辞。
|
||||
- **权威脚本乱码就臆读或退回抄文档**:Windows 代码页下 `font-charset` 的整行输出退化成 U+FFFD 替换字符时,改数源文件符号出现次数,别猜、别抄旧文档。
|
||||
- **钩子数用 `grep` 声明行**:多算一条注释掉的候选钩子(51 变 52);反过来在配置注释里复述那个 grep 字面量会让文本计数再虚高 1。口径只认 YAML 解析后的条目数,且要与 AGENTS.md、`docs/门禁说明/README.md` §2 标题、`ci_local.py` 实跑 passed+failed+skipped 三数同源。
|
||||
- **张数与页数互相冒充**:一个 12 行分页页第 2 页要单独拍一张,于是「+2 页」可能等于「+3 张」。两个数分别报、分别换算,别互相顶替。
|
||||
- **数篇数只 grep 裸标题**:`^## 变更记录` 漏掉 `## 6. 变更记录`、`## 九、变更记录`、`## 修订记录` 三种写法 → 篇数直接少数一半,拿这个错数去"同步"全仓就是把对的改成错的(2026-10-29 实测:5 篇真台账只数出 3 篇,改到第 10 处才发现)。普查一律 `grep -rn "^## " docs scripts` 再看标题,或直接读 `docs/文档规范.md` §5.1 那行的例外清单。
|
||||
- **只扫 docs/**:漏 `.pre-commit-config.yaml` 钩子 `name:` 显示名里的页数、AGENTS.md 钩子清单、`.dsh/skills/*/SKILL.md` 自带的计数与脚本路径(skill 正文也是命中面:font-regen 的 179/274 本窗最后才改)。
|
||||
- **提交完就收工**:第 7 步的提交后复扫才是把漏网当前态(架构总览 §3)揪出来的那一步;漏了它,仓库里就留下一行「当前态写旧数」直到别人再扫。
|
||||
- **去已删掉变更记录的文档里「补一行流水」**:2026-10-29 起全仓只剩 **5 篇**真台账保留变更记录表(清单 → `docs/文档规范.md` §5.1),外加 `scripts/archive/` 归档;其余文档**没有这一节**。计数同步**只改当前态**、过程交给 `git log`,别再到别的文档里找或加变更记录行。真要往那 5 篇的台账里加行,历史坑照旧:它们的表按日期**升序** → 新行**追加为末行**,插表顶会把首行「初版」挤成第二行。
|
||||
- **历史行被当陈旧值改掉**:「47 → 53 页」是当天事实,改了就是伪造历史;正确动作是**只改当前态那几处**、历史行一个字不动(保留变更记录的 5 篇真台账要写台账也只追加、不回改)。**历史行被写坏**时按无反引号兜底写法整行重写,别用同样写法再改一遍。
|
||||
- **照抄用户给的数**:用户说「从 40 扩到 47」也要自己数 `ref/*.bin`;数出来不一定是 47。
|
||||
- **把节点行当叶子数**:42 是 `ui_menu_tree.txt` 的节点行数、33 才是叶子;不读文件头规则就数错(实测把错数写进两份文档,下一轮才自查出来)。
|
||||
- **整文件正则批处理**:第一次尝试的正则没编译过、整批零落地。改法 = 逐条字面替换 + 每处 `assert count == expect`。
|
||||
- **反引号截断**:脚本里含 `ui-golden`、`#page=` 这类反引号时,经 TS 模板字符串/内嵌 heredoc 传递会被截断,表现为「整批零落地」或「历史行写坏」。落成脚本文件再执行,或用 `chr(96)` / `String.fromCharCode(96)` 拼。
|
||||
- **只改记得的那节**:上一笔只改 §2 与开篇、漏改 §1,用户当场问出「§1 说 16 叶、§2 说剩 3 项,自相矛盾」。改完必须全文通读同一文档所有小节。
|
||||
- **误报跟着改**:「< 40KB」是绘制缓冲红线,与页数无关。
|
||||
- **带日期戳的测量臆改新数**:「2026-10-26 实跑 …覆盖 40 页」改成 47 就是把历史测量冒充现值;只加「当时」。
|
||||
- **用截断的 grep 输出判定**:`cut -c` 截掉的后半句常含日期戳/口径,据此分类必错。
|
||||
- **新写文本别再造编号**:两道编号门禁都已退役(全库八项校验 2026-10-28、只查新增行 2026-10-29,脚本与白名单在 `scripts/archive/id_gate/`),**撞号不再有机器报警**,只剩规范与评审——所以这批文本的形态自查要自己做(`docs/编号规范.md` §1 三句;要机器帮忙就手工跑 `scripts/archive/id_gate/check_ids_new.py --worktree`)。
|
||||
@@ -1,88 +0,0 @@
|
||||
# Steps(自 SKILL.md 外置)
|
||||
|
||||
> 本文由 stale-doc-count-sweep 的 SKILL.md「Steps」一节逐字外置而来(渐进展开:主文件只留索引与指针)。步骤号沿用原文,未重编。
|
||||
|
||||
1. **实测真值:先剔伪命中,再双源互证**(不抄用户口述、不抄另一处文档)。逐数走上面那张表;两个来源对不上时**先找分歧根因**(把全部命中逐条打印出来看),不要取第一个、不要取平均,并在结论里写明分歧与取舍(实测:菜单 `#page=` 计数 29 对 注册表 28,根因是头部注释示例行)。
|
||||
|
||||
```bash
|
||||
cd "<repo>"
|
||||
ls simulator/tests/ref/shot_*.bin | wc -l # 张数(判据 = 入库实数)
|
||||
awk '/^PAGES = \[/,/^\]/' simulator/tests/check_content_pages.py | grep -cE '^ *\("' # 页数(第二个来源,两者必须一致)
|
||||
grep -n '#page=' ui_menu_tree.txt | head -3 # 先看首条是不是注释示例行,再定叶数
|
||||
grep -rn 'UI_PAGE_MAP_COUNT' Drivers/BSP/UI/ui_page_registry.h # 已绑叶数(生成物)
|
||||
grep -c '\.bitmap_index' Drivers/BSP/UI/ui_font_cn_12.c # 314 = 含 notdef;字形 = 该数 - 1
|
||||
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open('.pre-commit-config.yaml',encoding='utf-8'));print(sum(len(r['hooks']) for r in d['repos']))" # 钩子数口径
|
||||
```
|
||||
|
||||
⚠ **权威脚本输出乱码时**(`check_font_charset.py` 在 GBK 代码页下整行打印成 U+FFFD 替换字符串 + `: PASS`,数字全读不出来),**改为直接数源文件里符号出现次数**(`.bitmap_index` / `.range_start` / 码点唯一化)——不许臆读乱码,也不许因此退回去抄文档里的旧数。
|
||||
|
||||
2. **全形态 + 全命中面扫描**。一个数一次列出它全部历史形态,别只扫当前旧值:本轮 = `122/217`、`179/274`、`185/280`、`47 → 53`、`53 → 60`、`PAGES = 53`、`UI_PAGE_MAP_COUNT = 22`、`已绑 22 叶`、`未绑 11`、`内容页 53 页`。
|
||||
|
||||
```bash
|
||||
grep -rn --include=*.md --include=*.py --include=*.yaml --include=*.txt --include=*.c --include=*.h \
|
||||
-E '122 唯一字|217 字形|179 汉字|274 字形|185 唯一字|280 字形|47 → 53|53 → 60|内容页 53|PAGES = 53|UI_PAGE_MAP_COUNT = 22|已绑 22|未绑 11' . \
|
||||
| grep -v -E '^\./(ci-artifacts|\.mnemon|\.dsh/runs)/'
|
||||
```
|
||||
|
||||
**命中面比「文档」宽**,至少包含:`docs/**`、`AGENTS.md`、`.pre-commit-config.yaml`(**钩子 `name:` 显示名内嵌页数**:`name: UI 像素回归(菜单 8 状态 + 内容页 60 页 黄金参考)` 就是命中点)、`scripts/*.py`、`simulator/**/README.md`、以及 **`.dsh/skills/*/SKILL.md`**(本窗 `font-regen` 正文自带 179/274 计数,直到 step 150 才发现;编号门禁退役那轮 `id-family-semantic-migration`、`task-worktree` 两个 skill 正文与 `scripts/README.md` 同样是引用面)。
|
||||
|
||||
先列出 `文件:行:全文`,**判定前必须看整行**——`cut -c1-120` 会截掉后半句(那里常写着日期戳或口径)。
|
||||
|
||||
3. **逐条分类,五类各走各的路**(本 playbook 的核心,别用一条规则套全部命中):
|
||||
|
||||
| 类别 | 判定 | 动作 |
|
||||
|---|---|---|
|
||||
| 当前态陈述 | 描述「现在是多少」 | 改成实测真值 |
|
||||
| 变更记录历史行 | 形如「47 → 53 页」的当日事实 | **原样冻结不回改**——但这条**只适用于保留变更记录的 5 篇真台账**(台账 / 被别处当唯一证据出处的文档,清单 → `docs/文档规范.md` §5.1)与 `scripts/archive/` 归档;其余文档没有变更记录节,**不再追加任何流水行**,数字同步只改**当前态** |
|
||||
| 同数不同义 | `最坏路径 < 40KB` 这类内存/绘制缓冲红线 | **不动**,并在结论里写明「为什么不动」 |
|
||||
| 带日期戳的测量 | 「2026-10-26 实跑 …覆盖当时 40 页中的」 | **不臆改新数**,只补「当时」两字锁口径;新数留待下次实跑回填 |
|
||||
| 数字对但**口径描述错** | 数没错、句子把口径讲错了(实测:写「218 字(含 ASCII/符号,共 313 字形)」,而 `ui_cn_chars.txt` 的 218 个码点**全为非 ASCII**) | **另起一笔只改口径的提交**(`6daa01ae`),不与数字同步混笔;这类命中在任何数字 diff 里都看不见,只能把口径句逐句读一遍 |
|
||||
|
||||
拿不准就回到第 2 步把整行打印出来再判;判错历史行 = 把当天事实改成假账。
|
||||
|
||||
4. **每处先读现文,再逐条字面替换并断言命中数**(弃用整文件正则批处理)。本窗两度返工都是没先读现文:step 45 断言 `HIT 0` 抛错(目标串与现文不一致),step 44 是脚本语法错。脚本写成临时文件再跑(内嵌 heredoc/模板字符串里的反引号会截断整脚本),按**字节**替换以保 BOM 与行尾不动:
|
||||
|
||||
```python
|
||||
import pathlib, sys
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
JOBS = [ # (文件, old, new, 期望命中数)
|
||||
('AGENTS.md', '内容页 53 页黄金参考', '内容页 60 页黄金参考', 1),
|
||||
('.pre-commit-config.yaml', '内容页 53 页 黄金参考', '内容页 60 页 黄金参考', 1),
|
||||
('docs/架构设计/项目架构总览.md', '122 字/217 字形', '218 字/313 字形', 1),
|
||||
('docs/UI设计/UI_自动化测试流程.md', '文本断言 **310 条**、覆盖 40 页中的', '文本断言 **310 条**、覆盖当时 40 页中的', 1),
|
||||
]
|
||||
for path, old, new, expect in JOBS:
|
||||
p = pathlib.Path(path); raw = p.read_bytes()
|
||||
o, n = old.encode('utf-8'), new.encode('utf-8')
|
||||
got = raw.count(o)
|
||||
assert got == expect, f'{path}: 命中 {got},期望 {expect}(未写入)'
|
||||
assert o != n, f'{path}: old 与 new 相同'
|
||||
p.write_bytes(raw.replace(o, n))
|
||||
print(f'OK {path}: {old} -> {new} x{got}')
|
||||
```
|
||||
|
||||
每行打印 `old -> new xN`,收口时这串输出就是「改了哪几处」的账。脚本报错/没编译过,**先 `git diff --stat` 确认零写入**再换写法(不要在半落地状态上叠改)。
|
||||
|
||||
- **反引号**:TS 侧先用可见占位符(如 `~TB~`)写文本,落盘前再还原成真反引号 —— `const BT = String.fromCharCode(96); const q = s => s.split(TOKEN).join(BT)`(本窗实测:直接把带反引号的串塞进模板字符串/heredoc,整批零落地或把变更记录行写坏)。用 `tools.edit` 批量改时同样先 `await tools.read` 现文再传 `old_string`;批量里若某条 `old_string` 与 `new_string` 相同会报 `must differ` —— **只有该条作废、其余已落盘**,别整批重跑。
|
||||
- **历史行被写坏**(反引号及紧随其后的字符经工具传递被吞 → 变更记录行只剩半行、尾部出现 `??` 这类残迹):立刻按**无反引号兜底写法整行重写**——裸词 `accident / oper / alarm / start / yk`、页面绑定写成不带反引号的 `#page=`——而不是拿同样带反引号的串再改一遍(实测连返工三次:step 81→82→83→85)。
|
||||
|
||||
5. **一笔只装一类事**。数字同步一笔(`cbb4fdb8`:11 处 + 变更记录一行);口径更正另起一笔(`6daa01ae`);提交后复扫逮到的漏网当前态再补独立一笔(`11d60057`)。钩子一增一减写成一个当日行的两个阶段(51 → 52 加 `id-check-new`、随即 52 → 51 摘 `id-check`),别写两行互相打脸。提交说明如实写(「先认一笔自己的错:上一笔的未绑 21 叶是探针错」),别把口径修正包装成「补充说明」。
|
||||
|
||||
6. **被改文件全文通读,核对其余小节**——不是只看 diff。同一文档内互相引用的小节必须一致(本窗产品需求:§2.4 立项未排、§5.14 FR-SOE-3、§8.2 绑定现状、§8.3 已绑分页表、§9.1 差距表、§9.2 顺序表六处同时引用这四个数;`docs/任务规划/README.md` §1 当前执行表 vs §2 全项目顺序表)。若发现上一笔自己写进文档的数**口径就错了**,明确认错并改正,同时把**可复核的重算方法**写进文档(「按缩进判叶子:有更深缩进后继者即非叶子」),防止再错。
|
||||
|
||||
7. **提交后再复扫一遍**(收尾硬步骤,不许省,也不许只在提交前扫)。按第 2 步的**全部**旧形态整仓再扫一次,逐条分类为 当前态 / 历史行 / 同数不同义:本窗 `cbb4fdb8` 提交后复扫得 14 条命中,12 条以上都在变更记录历史行(`47 → 53`、`53 → 60`、`122/217` 冻结),**唯一残留的当前态命中**是 `docs/架构设计/项目架构总览.md` §3「界面层」那行 → 单独补一笔 `11d60057`。说不出归属的就是漏改。
|
||||
|
||||
```bash
|
||||
grep -rn -E '22 叶|未绑 11|11 叶|53 页|122 唯一字|217 字形|185 唯一字|280 字形|内容页 53|PAGES = 53|UI_PAGE_MAP_COUNT = 22' \
|
||||
. | grep -v -E '^\./(ci-artifacts|\.mnemon|\.dsh/runs)/'
|
||||
```
|
||||
|
||||
8. **跑门禁、分笔提交**(`git commit` 属 Ask first,须用户明确指示后再做):
|
||||
|
||||
```bash
|
||||
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --sections-strict # 期望「文档路径检查通过(N 个文档)」
|
||||
& '\.venv\Scripts\python.exe' scripts/archive/id_gate/check_ids_new.py --range A..B # 编号形态普查(两道门禁 2026-10-28 / 2026-10-29 都已退役,不再是提交钩子)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py # 或 pre-commit run --files <被改文件>
|
||||
```
|
||||
|
||||
PowerShell 形态把 `./.venv/Scripts/python.exe` 换成 `& '.venv\Scripts\python.exe'`。提交信息一律 `git commit -F <UTF-8 无 BOM 文件>`,别用 `-m`(中文长标题易超 100 字符、终端代码页会转码);标题 ≤100、**正文每行 ≤80**(实测把正文压到 66/78 字符才过 gitlint)。
|
||||
@@ -47,7 +47,7 @@ whenToUse: 需要向 STM32 目标烧录 .hex/.bin、只复位重启目标、校
|
||||
被拒,本 skill 会打印 `RESET_NOTE` 并**自动降级**为这条系统复位(仍按降级结果给 rc)。
|
||||
- 探针在位先 `list`;`Programming Complete` 但行为像旧固件 → 先怀疑缺 `-Rst -Run`,
|
||||
再怀疑 hex 没重编(对比 hex 时间戳)。
|
||||
- 完整复位语义/故障排查(本仓库):`docs/测试说明/测试工具/ST-LINK_CLI_复位使用说明.md`。
|
||||
- 完整复位语义/故障排查(本仓库):`docs/测试说明/ST-LINK_CLI_复位使用说明.md`。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
|
||||
+237
-124
@@ -1,104 +1,166 @@
|
||||
---
|
||||
name: task-worktree
|
||||
description: >-
|
||||
并行开发的空间管理(git worktree 生命周期;`@task` 只是文档别名不是可执行命令,DSH 会话显式调用 `/task-worktree`):开任务树与分支、联接共享 `.venv` 与模拟器 SDK、build/bench 独占锁、并发上限与腾位(A)、放不掉的锁(B)、`done` 是合并点不是查看器与补绿链(C)、主树被别的会话弄脏时的手工合并(C2)、门禁指标异常的净室归因(D)、回收非登记树与悬挂台账(E)、与 DSH worktree 插件共存及联接分级(F)、树内跑门禁与消息禁词(G)、树内自查与合并后补绿命令化(`precheck` / `postmerge`)。规格 = docs/环境与部署/并行任务工作树方案.md;深规程全文在 reference/。
|
||||
whenToUse: >-
|
||||
要为新任务开隔离空间、同时推进多个任务、把任务合回主干或回收工作树;或撞到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿」或「主工作树不干净」而那份脏改动是别的会话/插件留的、两个任务抢 COM6 或 UV4、并集合并残留;或在任务树里跑门禁写输出报 ENOENT、`--all` 的问题全在 `simulator/SDL2` 联接下;或门禁指标异常增长查不出哪个文件造成;或要回收 `list` 台账看不到的树、清 branch/path 都已不存在的悬挂台账;或要确认插件树里 `.venv` 与 `simulator/SDL2` 哪个必须补;或要改并行上限这个数字并同步全仓口径。
|
||||
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计。规格见 docs/环境与部署/并行任务工作树方案.md。
|
||||
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
|
||||
---
|
||||
# task-worktree — 多任务并行开发空间(`@task` 仅为文档别名;DSH 显式调用 `/task-worktree`;可执行形态见「固定命令」)
|
||||
|
||||
- **执行体** = `.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库;拷整个 skill 目录即可换项目用)。
|
||||
- **深规程全文在 reference/ 下:做 C2 / D / E / F / G 任何一条之前先把对应那篇读完**——本文只留判得出来「要不要读」的判据。
|
||||
- 为什么这么拆:本文件受 dsh-mnemon 写侧上限 **65536 字节**(`skill-size` 钩子 49152 告警;实测 72,601 字节的技能照常可读可提交,只是自进化每次「生成草案」被拒、永久卡在「生成中」)→ **新增深规程进 reference/,本文只加判据**。瘦身前 frontmatter 原文 → [frontmatter-history.md](reference/frontmatter-history.md)。
|
||||
# task-worktree — 多任务并行开发空间(短命令 `@task`)
|
||||
|
||||
## 症状 → 去哪一节
|
||||
**执行体 = 本 skill 自带脚本** `.dsh/skills/task-worktree/scripts/task_worktree.py`
|
||||
(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。
|
||||
|
||||
| 现场 / 目标 | 去 |
|
||||
|---|---|
|
||||
| 开新树 / 看全景 / 追平主干 / 放弃任务 | 固定命令、典型流程 |
|
||||
| `new` 报「已有 8 个活跃任务(上限 8)」 | **A** |
|
||||
| `lock release` 报「任务 X 不存在(缺台账)」,锁照旧挂着 | **B** |
|
||||
| `done` 做了什么、红了怎么补绿、能不能反悔、怎么算「真完成」 | **C** |
|
||||
| `done` 报「主工作树不干净」,而那份脏是别的会话/插件留的 | **C2** → [merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) |
|
||||
| 门禁指标异常增长要逐文件归因(主树全程只读) | **D** → [clean-room-measure.md](reference/clean-room-measure.md) |
|
||||
| `list` 台账里没有的树要回收 / detached / 有在飞内容 / 悬挂台账 | **E** → [reclaim-unregistered.md](reference/reclaim-unregistered.md) |
|
||||
| 插件树缺联接、锁归属 `pw:<name>`、「有插件为何还自持生命周期」 | **F** → [dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) |
|
||||
| 树内写门禁输出 ENOENT、`--all` 假红、写提交/合并信息、字节体检 | **G** → [gates-in-tree.md](reference/gates-in-tree.md) |
|
||||
| 收工前树内自查、或合并后体积凭证/台账红了想一条命令补绿 | `precheck` / `postmerge`(固定命令表 + C 节第 5~6 步) |
|
||||
| 退出码读到 0 其实是 2、`tail` 之后误判「有冲突」 | 读输出 |
|
||||
| 谁不许跑 `done`/`abort`、清场前必做什么、任务期哪些文件不许改 | 铁律 1–9 |
|
||||
| 收工前最后一遍自查(各节该留什么证据)/全部实测坑 | [verification.md](reference/verification.md) / [pitfalls.md](reference/pitfalls.md) |
|
||||
```powershell
|
||||
# cwd = 仓库根(或任意子目录)
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
|
||||
```
|
||||
|
||||
## 固定命令
|
||||
bash(`ops_bash`)里固定两个变量,下文用 `$TW` 代指脚本:
|
||||
|
||||
```bash
|
||||
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
|
||||
"$PY" "$TW" <子命令> [参数] # PowerShell:& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令>(cwd = 仓库根或任意子目录)
|
||||
"$PY" "$TW" list
|
||||
```
|
||||
|
||||
退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。
|
||||
**⚠ 这几个码只有「不接管道」时才读得到**——原因与对策见「读输出」一节。
|
||||
|
||||
## 固定命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---|---|
|
||||
| `new <ID> <slug>` | 建分支 `task/<ID>-<slug>` + 工作树 + 共享联接 + 台账,打印新空间路径 |
|
||||
| `list` | 分支 / 落后领先 / 脏文件数 / 台架状态 + 两把锁归属。**只遍历台账** → 非登记树看不见(E 节) |
|
||||
| `status <ID>` | 未提交清单、相对基线漂移、台架记录、已建联接 |
|
||||
| `sync <ID>` | 在任务树里 `rebase` 追平主干,把冲突留在任务分支上 |
|
||||
| `lock acquire|release|status <build\|bench>` | 独占资源锁(→ 铁律 3、B 节) |
|
||||
| `plugin list|adopt|unadopt [插件树名]` | 插件桥:看清单 / 补共享联接 / 摘联接(**不删树**)→ F 节 |
|
||||
| `new <ID> <slug>` | 建分支 `task/<ID>-<slug>` + 工作树 + 共享联接 + 台账;打印新空间路径 |
|
||||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||||
| `status <ID>` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
|
||||
| `sync <ID>` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 |
|
||||
| `lock acquire|release|status <build|bench>` | 独占资源锁 |
|
||||
| `done <ID>` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
|
||||
| `abort <ID>` | 工作树 + 分支一起删,**永久丢弃** |
|
||||
| `precheck` | 树内自查(**只读**):BOM 漂移 / CR / U+FFFD + docs 逐档门禁 + `--msgfile --ban` 消息检查;顺手补 `ci-artifacts/`(G 节机械项) |
|
||||
| `postmerge` | 主树合并后补绿:体积凭证**自动诊断**(新鲜跳过;红→先短链 `--update`,只读复检真漂了才升全链)+ `check_doc_links --all` + 列待提交文件,**不代提交** |
|
||||
| `abort <ID>` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) |
|
||||
|
||||
- **退出码** `0` 成功 / `1` 操作失败 / `2` 前置或环境错误 / `3` 用法——**只有不接管道才读得到**(→ 读输出)。
|
||||
- `new`:`--base`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)· `--lean`(跳 LFS smudge,只省磁盘不省时间)· `--no-bootstrap`(不建联接)· `--force`(**只跳过并发上限检查**,不解决谁让位 → A)
|
||||
- `done`:`--dry-run` · `--skip-build` · `--skip-gates` · `--force`。⚠ `--dry-run` **只保护预演段、不保护清场段**(实测:对已合入的分支它照样摘联接、删分支、清台账;且它的前置也要抢 `build` 锁)→ 用完复验 `git worktree list`,细节 → 陷阱清单。
|
||||
- `lock`:`--task`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(默认 `build`=1h / `bench`=6h,超期视为陈旧、下次 acquire 自动接管)· `--note` · `--bench-pass` · `--force`。⚠ `--note` 只在 **acquire** 写进 payload,**release 时被忽略** → 验收证据写主干提交说明或文档。
|
||||
- 环境变量:`DTU_WT_ROOT`(默认仓库同级 `_wt/`)· `DTU_TASK_BASE`(默认基线)· `DTU_INTEGRATION_WORKTREE`(集成点,用于隔离演练)。
|
||||
`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)·
|
||||
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)·
|
||||
`--force`(**仅跳过并发上限 3 的检查**,不解决「谁该让位」,见 A 节)。
|
||||
|
||||
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`(清场时强制移除工作树)。
|
||||
|
||||
`lock` 开关:`--task <ID>`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(自报窗口)· `--note <文本>` ·
|
||||
`--bench-pass`(release 时给台账记 PASS)· `--force`。
|
||||
⚠ `--note` 只在 **acquire** 时写进锁 payload,**release 时被忽略**——别指望它留下验收证据(证据写进主干提交说明或文档)。
|
||||
|
||||
**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)·
|
||||
`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)·
|
||||
`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树——用于隔离演练/测试)。
|
||||
|
||||
## 典型流程
|
||||
|
||||
```bash
|
||||
"$PY" "$TW" new N21 dhara-isfree --needs-bench # 把新会话工作目录切到打印出来的 _wt/N21-dhara-isfree/
|
||||
"$PY" "$TW" lock acquire bench --task N21 # 烧录 / /serial-boot-test / 板级套件
|
||||
"$PY" "$TW" lock release bench --task N21 --bench-pass
|
||||
"$PY" "$TW" done N21 # 回主工作树(铁律 1);先 --dry-run 读全量
|
||||
```powershell
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" new N21 dhara-isfree --needs-bench
|
||||
# → 把新会话的工作目录切到打印出来的 _wt/N21-dhara-isfree/,在那里干活
|
||||
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock acquire bench --task N21
|
||||
# 烧录 / @test / 板级套件……
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock release bench --task N21 --bench-pass
|
||||
|
||||
# 回到主工作树执行回收
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
|
||||
```
|
||||
|
||||
## 读输出:管道与截断(两起误判的共同根因)
|
||||
|
||||
1. **`| tail -N` 之后的退出码是 `tail` 的,恒 0** → 脚本的 1/2 全被吞(实测三条「rc=0」真码都是 2)。判据先看文本(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**不接管道**或取 `${PIPESTATUS[0]}`。
|
||||
2. **`done` 的输出别 `tail -2`**:管道下 python stdout 块缓冲、git 子进程 stderr 不缓冲,两股流错位 → 只剩尾部那句「--dry-run:到此为止…」加带 `conflict` 字样的 stderr 片段,据此误判「有冲突」。dry-run 全文 5~8 行,**读全量**。
|
||||
1. **`... 2>&1 | tail -N` 的退出码是 `tail` 的,恒为 0** → 脚本的 `1` / `2` 全被吞。实测三条印着
|
||||
「rc=0」的输出其实都是 exit 2:`new` 撞活跃上限、`lock release` 缺台账两次。
|
||||
所以**判据先看文本**(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**直接执行不接管道**或补一句
|
||||
`echo RC=$?`;坚持接管道就在 bash 里取 `${PIPESTATUS[0]}`。
|
||||
2. **`done` / `done --dry-run` 的输出不要用 `tail -2` 截**:接管道时 python 的 stdout 是块缓冲、git
|
||||
子进程的 stderr 不缓冲,两股流错位混排,`tail -2` 只剩「--dry-run:到此为止…」加上 stderr 里带
|
||||
`conflict` 字样的片段 → 据此误判「有冲突」。实测:`tail -2` 误读成 conflict,放宽到 `tail -10` 才看到
|
||||
`[ok] 无冲突,预演结果树 <sha>`。dry-run 全文只有 5~8 行,直接读全量。
|
||||
|
||||
## 分步操作(节号 A–G/C2、各节步骤号与铁律编号被 reference/ 与 docs 引用,**不许重排**)
|
||||
## 分步操作(三条运行时机制)
|
||||
|
||||
### A. `new` 撞并发上限 → 腾位
|
||||
### A. `new` 被拒「已有 3 个活跃任务(上限 3)」——怎么腾位
|
||||
|
||||
1. 认原文 `前置/环境错误:已有 8 个活跃任务(上限 8);先 done/abort,或加 --force`(exit 2)。「活跃」= `_wt/.state/<ID>.json` 在且台账 `path` 目录还在 → **手工删目录不占名额、但留悬挂台账**(E 第 6 步)。
|
||||
2. `$TW list` 定谁让位:**已完成的先 `done`**(合并回收一步不省)。
|
||||
3. 无人认领、零提交、未 claim 的空闲树 → `abort <ID>`;有未提交或有提交时脚本会拒,确认可弃才 `--force`。
|
||||
4. 真要第 9 棵才 `--force`:并行度上去,**冲突面与台架排队一起上去**(每棵磁盘代价见铁律 6)。
|
||||
5. **别用手工 `git worktree remove` 给登记任务腾位**:台账不删(同 ID 报「任务已存在」)、又绕过「先摘联接」(铁律 5),有让 git 递归进联接删掉主树 `.venv` 的风险。要回收的是**非登记树**(`list` 里没有它)→ **E 节**。
|
||||
6. 复验:`list` 条目数 == `ls _wt` 目录数,再 `new`。
|
||||
1. 读原文:`前置/环境错误:已有 3 个活跃任务(上限 3);先 done/abort,或加 --force`(exit 2)。
|
||||
上限是脚本常量 `MAX_CONCURRENT = 3`;**「活跃」的判据 = `_wt/.state/<ID>.json` 台账存在,且台账里
|
||||
`path` 指向的目录还在**(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步)。
|
||||
2. `$TW list` 看全景(分支 / 落后领先 / 脏文件数)决定谁让位:**已完成的先 `done`**(合并回收,一步不省)。
|
||||
3. 没人认领、零提交、尚未 claim 的空闲树 → `abort <ID>` 腾位(实测就是 abort 掉排在后面的空闲树换出名额)。
|
||||
`abort` = 工作树 + 分支一起删、**永久丢弃**;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加 `--force`。
|
||||
4. 确实要同时开第 4 棵才 `new <ID> <slug> --force`:磁盘代价约 **83 MB/棵**(`--lean` 约 45 MB),
|
||||
并行度上去了、**冲突面与台架排队也一起上去**——不是免费的。
|
||||
5. **别用手工 `git worktree remove` 腾位**:台账不会被删(下次 `new` 同 ID 会报「任务 <ID> 已存在」),
|
||||
还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树 `.venv` 删掉的风险。
|
||||
6. 腾位后复验:`$TW list` 的条目数与 `ls _wt` 的目录数一致,再 `new`。
|
||||
|
||||
### B. 非登记持有者的锁放不掉
|
||||
### B. 自己占的锁没有台账:`lock release` 报「任务 X 不存在(缺台账)」
|
||||
|
||||
症状:`lock release bench --task acceptwin --bench-pass`(带 `--force` 同)报 `前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)`,**锁照旧挂着**。根因:`release` 带 `--bench-pass` 就先 `load_state(task)`,而 `acquire` 侧不查台账 → 「释放」被「记账」挡死;锁没放,`done` 的 `release_stale_locks()` 对未到 TTL 的活锁判失败 → **一个没登记的名字挡住所有任务收口**。
|
||||
**症状**(lead 在主工作树跑台架验收窗,用临时名占锁:`lock acquire bench --task acceptwin`):
|
||||
|
||||
**2026-10-29 已修**:无台账降级为 `[warn] …跳过 PASS 记录,仅释放锁`,**不再需要手写占位台账**。
|
||||
1. `$TW lock status` 认现场(持有者 / pid / 已持有 / TTL)——别对同一句报错反复重试。
|
||||
2. `$TW lock release <build|bench> --task <NAME> [--bench-pass]` 直接放;PASS 记不上会 warn,锁照放。
|
||||
3. **PASS 后面还要用**(`done` 只认台账 `bench_pass`)→ 从一开始就用已 `new` 的 ID 占锁,或把结论写进主干提交/文档。插件树 `pw:<name>` 天然无台账 → F 节。
|
||||
4. 最后手段:锁 = `_wt/.state/locks/<name>.lock` 的单行 JSON(`name/task/pid/host/epoch/at/note/ttl`),`release` 就是 unlink 它——手工删 = 不记 PASS 的强制释放,前面各步都不可用才做。**早期为绕坑手写的占位台账现在有害**(既占上限名额又被 `list` 当任务)→ E 第 6 步备份后删。
|
||||
```
|
||||
"$PY" "$TW" lock release bench --task acceptwin --bench-pass
|
||||
前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)
|
||||
"$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在
|
||||
```
|
||||
|
||||
### C. `done <ID>`:它是合并点,不是查看器
|
||||
**根因**(读脚本确认):`release` 分支里**只要带 `--bench-pass` 就先 `load_state(task)`** 去给台账写 PASS,
|
||||
台账不在就抛「前置/环境错误」,**根本走不到释放那一步**;`--force` 只覆盖「持有者与 `--task` 不符」和
|
||||
acquire 的陈旧接管,管不到台账缺失。而 `acquire` 侧**不查台账**——这个不对称就是坑本身;报错文案里的
|
||||
「任务不存在」也别误读成「我没占过锁」。
|
||||
|
||||
1. **合并前确认属主提交完**:`$TW status <ID>` 看未提交与领先数,跨会话再问属主一句。实测教训:属主还剩「2/2」没提交就 `done` → 只含 1/2 的分支进了主干,只能另开一棵树补后半(自己的时序错,不是分支问题)。
|
||||
2. 先 `--dry-run` 且**读全量**。判据两行:`[ok] 前置检查通过(领先 <base> 共 N 个提交)` + `[ok] 无冲突,预演结果树 <sha>`;**N 必须等于属主应交的提交数**,对不上回第 1 步。
|
||||
1. 先 `$TW lock status` 确认持有者 / pid / 持有时长 / TTL(如 `bench acceptwin 持有(pid=2784,
|
||||
已 0.1 / TTL 6.0 小时)`)——**别反复重试 release**。
|
||||
2. **不需要同步记 PASS** → 直接 `$TW lock release bench --task <NAME>`(不带 `--bench-pass`)就能释放。
|
||||
3. **需要 `--bench-pass` 而这个名字不是登记任务** → 手写**最小占位台账** `_wt/.state/<NAME>.json`
|
||||
(键与 `new` 写出的完全一致,共 11 个):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "acceptwin",
|
||||
"slug": "lead-window",
|
||||
"branch": "(none: lead 验收窗占位台账)",
|
||||
"path": "D:\\Code\\DTU Code\\STM32F4-Base",
|
||||
"base": "v2-on-ui",
|
||||
"created_at": "2026-10-26 17:00:00",
|
||||
"junctions": [],
|
||||
"needs_bench": true,
|
||||
"bench_pass": false,
|
||||
"note": "lead 验收窗(非工作树任务,为 lock 台账占位)",
|
||||
"lean": true
|
||||
}
|
||||
```
|
||||
|
||||
用写文件的工具落 UTF-8(无 BOM),别用 `echo` / `Set-Content`(中文乱码铁律)。
|
||||
4. `$TW lock release bench --task <NAME> --bench-pass` → `[ok] 已为任务 … 记录台架 PASS` +
|
||||
`已释放锁 bench(原持有者 …)`。
|
||||
5. **立刻删掉占位文件**:它活着期间会被 `list` 当任务列出,并**占掉 3 个活跃名额之一**(`path` 指向的
|
||||
目录存在就算活跃);而且**绝不对这个 ID 跑 `done` / `abort`**——清场会拿台账里的 `path` / `branch`
|
||||
去 `git worktree remove` 和删分支。
|
||||
6. 复验 `$TW lock status`:`build 空闲(TTL 1.0h)` / `bench 空闲(TTL 6.0h)`。
|
||||
7. 占位台账一删,那条 PASS 也随之消失。**这个 PASS 后面还要用的话,从一开始就用已登记的任务 ID 占锁**
|
||||
(lead 的验收窗借某个已 `new` 出来的 ID 最省事),或把验收结论写进主干提交说明/文档。
|
||||
8. **为什么必须马上释放**:`done` 的前置会跑 `release_stale_locks()`——**未到 TTL 的活锁直接判失败**:
|
||||
`独占资源 bench 仍被 acceptwin 占用(已 0.1 / TTL 6.0 小时),先 release`。lead 挂着锁 = 挡住所有任务
|
||||
的收口;只有超过 TTL 的锁才会被自动接管。
|
||||
9. 最后手段(脚本本身都走不通时):锁就是 `_wt/.state/locks/<name>.lock` 里的单行 JSON
|
||||
(`name/task/pid/host/epoch/at/note/ttl`),`release` 的实现就是 unlink 它——手工删这个文件等价于
|
||||
「不记 PASS 的强制释放」,只在前面几步都不可用时才用。
|
||||
|
||||
### C. `done <ID>` 的正确用法(它是合并点,不是查看器)
|
||||
|
||||
1. **合并前确认任务属主已把全部改动提交完**。`$TW status <ID>` 看未提交清单与领先提交数;跨会话任务
|
||||
再问一句属主(`send_message`)。实测教训:属主还剩「2/2」没提交就 `done`,把只含 1/2 的分支合进了主干,
|
||||
只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。
|
||||
2. **先 `--dry-run`,并读全量输出**(别 `tail -2`,见「读输出」)。判据两行:
|
||||
`[ok] 前置检查通过(领先 <base> 共 N 个提交)` + `[ok] 无冲突,预演结果树 <sha>`;
|
||||
**N 必须等于属主应交的提交数**,对不上回到第 1 步。dry-run 零改动。
|
||||
3. 正式 `$TW done <ID>`——**必须在主工作树**(铁律 1)。
|
||||
4. 见 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 <sha>` 说明**合并已落主干**;先定位哪条钩子红(`[SUMMARY] static: pre-commit hooks -- passed=N failed=M`),最常态是 `.map` 滞后 → 第 5 步。
|
||||
- ✅ 走提示 a):主工作树修正后追加提交。
|
||||
- ❌ **绝不用 b) 的 `reset --hard <sha>`**——那是丢弃已入库的合并提交,且 `reset --hard` 是项目级 Never(连未提交工作一起丢)。提示 b 是列出的可能性,不是建议。
|
||||
5. 补绿固定链(顺序即因果,全在主工作树,逐条不接管道看退出码):
|
||||
4. 输出 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 <sha>` 已经说明
|
||||
**合并落进主干了**。
|
||||
- ✅ 走提示里的 **a) 在主工作树修正后追加提交**;
|
||||
- ❌ **绝不用 b) 的 `git -C "<主树>" reset --hard <sha>`**——那是丢弃已入库的合并提交,而
|
||||
`reset --hard` 本身是项目级 Never(会连未提交工作一起丢)。提示 b 只是列出的可能性,不是建议。
|
||||
- 先定位是**哪条钩子**红:门禁输出里的 `[SUMMARY] L0: pre-commit hooks -- passed=N failed=M`,
|
||||
不同根因处置不同(见陷阱「`done` 的门禁 = `pre-commit run --all-files`」)。最常态是 `.map` 滞后 → 第 5 步。
|
||||
5. 补绿固定链(2026-10-25 实测;顺序即因果,全在**主工作树**跑,逐条不接管道看退出码):
|
||||
|
||||
```bash
|
||||
"$PY" scripts/check_mem_numbers.py --write # 台账受管区 ↔ 新 .map 对齐
|
||||
@@ -106,68 +168,119 @@ PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktr
|
||||
"$PY" scripts/ram_baseline.py --update # 趋势基线追加一行
|
||||
"$PY" scripts/check_test_coverage.py --update-baseline # 覆盖率基线(新单测 / 新套件时必刷)
|
||||
"$PY" scripts/check_doc_links.py --all # 文档链接与章节引用
|
||||
git add <上面刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交
|
||||
git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md 边界)
|
||||
git add <上面几条刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交
|
||||
git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md 边界),按用户指示再做
|
||||
```
|
||||
|
||||
为什么必然要补 → 陷阱「`done` 的门禁跑在 `/keil-build` 之前」。合并解过冲突时,**合并提交期 pre-commit 只查冲突文件**(→ G 节第 3 条)→ 落主干后必须另跑全量。
|
||||
**注释/文档-only 但碰了 .c/.h 的短链**:先比 `Program Size` 与合并前是否逐项相同——相同就只 `ram_baseline.py --update` 重写体积凭证(`scripts/size_attestation.json` 跟着补提交笔走),**别 `--write` 动受管区**(等于给在飞分支造冲突面);数字变了才走上面全链。**已命令化**:`"$PY" "$TW" postmerge`——新鲜直接跳过;红则先短链,再用只读 `check_mem_numbers` 复检裁定是否升全链;末尾跑 `--all` 并列出待提交文件(不代提交)。上面的手工全链仍是权威兜底。
|
||||
6. **重跑 `done` 前主树必须干净**,否则 `失败:主工作树不干净(…)——合并会把未提交改动卷进来` → 第 5 步产物先提交。分支已并时重跑打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场`——**该路径在门禁之前就返回,不会替你复绿**:上一次门禁没跑到的尾巴(主树 `ci_local.py` + `check_doc_links.py --all`;体积/文档那半可 `postmerge` 一把跑)必须自己补。主树脏且那份脏不是你的 → **C2**。
|
||||
7. 清场后再敲一次 `done <ID>` 得 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——**不是失败**:清场最后一步 unlink 了台账。**判完成以 `git log` 为准**。
|
||||
|
||||
### C2. `done` 被「主工作树不干净」挡住,而那份脏是**别的并发会话**留的
|
||||
|
||||
- **不许**对它做 stash / checkout / reset / 提交 / 删除(项目级 Never,你不知它写没写完)。`done` 要求主树干净 → 合并与回收一起被挡,**那是保护不是工具坏了**。
|
||||
- 正解六步,编号与 [reference/merge-dirty-main-tree.md](reference/merge-dirty-main-tree.md) 第 1~6 步一一对应(每步的确切命令与判据在那篇):**取证**(mtime + diff 定性有无交集,顺带 `worktree list` 看有无别人中途新建的树)→ **预演** `git merge-tree --write-tree <base> <task>`(打出结果树 sha 且 RC=0)→ **主树手工合并** `git merge --no-ff --no-edit -F ci-artifacts/merge-msg-<slug>.txt <task>` → **对账**(合并后 blob id 与 stat 同合并前存值逐项一致)→ **备份台账** `cp <ID>.json <ID>.json.state-bak` 后让 `done` 自己摘联接拆树 → **复验** `worktree list` + `lock status`。
|
||||
- **blob id 变了 = stash pop 半冲突**:立刻停止收工,把两份 blob id 与 diff 交给该文件属主,绝不 `checkout` 抹平。
|
||||
- **报告要点名**:绕开 `done` 的理由 + 补回它的证据(预演 sha 与 RC、blob 对账、主树全库 `check_doc_links.py --all`、两把锁空闲)+ 那份外部脏文件与中途冒出来的第二棵树(谁的、你没动它的证据)。合并信息也过 G 的禁词自检;「这份脏通常是谁」见陷阱清单。
|
||||
|
||||
### D. 门禁指标异常增长 → 净室逐文件归因(主树全程只读)
|
||||
|
||||
三条判据:仓外 detached 树当净室、**一次只注入一份在飞文件**、与 HEAD 基线逐文件差分;主树前后 `git status --porcelain` 逐行一致才算「全程只读」。全文 → [reference/clean-room-measure.md](reference/clean-room-measure.md)。
|
||||
|
||||
### E. 回收**非登记**工作树(`list` 为空 / detached / 树里有在飞或未跟踪内容)
|
||||
|
||||
三条不可跳过:① 逐文件内容**三桶归类**(与主线工作区/HEAD 比对;「很久没动」不是判据)② 动手前跑**只读 reparse 预扫**,拿到 `REPARSE: none` 才许 `git worktree remove`(铁律 5)③ 唯一未合并残留先备份再 `--force`,拆完复验 + 清悬挂台账。**绝不跑 `done`/`abort`**(铁律 9)。全文(第 1~6 步 + 两份探针脚本)→ [reference/reclaim-unregistered.md](reference/reclaim-unregistered.md)。
|
||||
|
||||
### F. 与 DSH worktree 插件(`/worktree`、`worktree_create`)共存
|
||||
|
||||
| 联接 | 级别 | 依据 | 缺了会怎样 | 逃生门 |
|
||||
|---|---|---|---|---|
|
||||
| `simulator/SDL2` | **硬需求** | `simulator/CMakeLists.txt:12` 的 `set(SDL2_DIR …)` **非 `CACHE`**,include / `lib/x64` / `SDL2.dll` 全从它派生 | `build_sim.bat` 当场失败 | **无** |
|
||||
| `.venv` | 软需求 | `scripts/ci_local.py` 有 `DTU_PRECOMMIT_PYTHON` + 五级解释器候选 | **静默**落机器级解释器(缺 fontTools → `generated-fresh` 假红) | 环境变量 |
|
||||
|
||||
- `plugin adopt` 两条都补只是因为便宜,不代表两条都硬。插件树占锁记 `pw:<name>`、**回收仍归 /worktree**(本 skill 不删插件树,插件也不删 `_wt/`)。「有插件为何还自持生命周期」=先读插件包(`npm root -g`)再 grep 三个概念名报命中数,插件侧零命中就是理由——取证姿势与答案边界 → [reference/dsh-plugin-bridge.md](reference/dsh-plugin-bridge.md) F-2。
|
||||
|
||||
### G. 任务树内跑门禁:落点、`--all` 假红、消息禁词
|
||||
|
||||
- **新树没有 `ci-artifacts/`**(被 ignore → 不检出):重定向/写文件直接 `ENOENT` 且**整段程序零执行** → 落盘前先单独一条 `mkdir -p ci-artifacts`;树内输出一律树内相对路径(`cd` 进树后 `../_wt/x.txt` 落到 `_wt/_wt/`)。一次性产物**不进仓库根**。
|
||||
- **文档门禁树内按单文件跑;全库 `--all` 留到合并后在主树跑**——树内 `simulator/SDL2` 是同一份仓库的联接,`--all` 把它一起扫成假红(实测 RC=1 而问题全在联接下、要改的文档一条没有)。非要树内跑:① 主树同命令拿基线 ② 过滤联接下的问题行 ③ 剩下的逐行比。**只做一步就是误判。**
|
||||
- **合并提交期 pre-commit 只查冲突文件**(日志首行 `[INFO] Checking merge-conflict files only.`,其余全 `(no files to check) Skipped`)→ 合并落主干后**必须另跑全量** `scripts/ci_local.py`。
|
||||
- **跨层改动拆不成「按层分笔」**:钩子看到的世界 = HEAD + 本笔 staged(未暂存被 stash;未跟踪文件不被 stash 但会拖红同一棵树上别人的提交)→ 登记文件 / 生成物 / 互锁引用的文档分到他笔,本笔必被整仓门禁拦(`doc-links`/`registration-check`/`generated-fresh`/`mem-numbers`)。判据:**一笔提交必须自洽**。
|
||||
- **跑门禁前字节体检**:`lines / bytes / BOM / CR / U+FFFD / 三连续空行 / 行尾空格 / 末行单个 LF` + Markdown 表格每行列数一致性(单元格内竖线写成反斜杠竖线,否则被当分隔符断列)。坏行**未必是本次改动**,先定归属。`doc-links`/`no-mojibake` 只覆盖编码那半。机械项已命令化:`"$TW" precheck`(BOM 漂移 / CR / U+FFFD + docs 逐档门禁,只读);列数一致性与归属判定仍人做。
|
||||
- **提交/合并信息一律 `-F` 读文件**(UTF-8 无 BOM、标题 ≤100、正文每行 ≤80,放 `ci-artifacts/`);清退类任务加**禁词自检**:`git log -1 --pretty=%B | grep -c '<关键词>'` 必须是 **0**。未推送前发现消息写错 → `--amend -F` 就地修,别追加「更正」提交(等于把禁词写回历史)。BOM/CRLF/行长/禁词一次查:`"$TW" precheck --msgfile <文件> --ban <词1> --ban <词2>…`。
|
||||
- 全文(第 1~7 条,含 stash 机制与四道整仓门禁现场)→ [reference/gates-in-tree.md](reference/gates-in-tree.md)。
|
||||
为什么必然要补,见陷阱「`done` 的门禁跑在 `@build` 之前」。
|
||||
6. **重跑 `done` 之前主工作树必须干净**——否则前置检查直接
|
||||
`失败:主工作树不干净(…)——合并会把未提交改动卷进来`,所以第 5 步的产物必须**先提交**。重跑时若分支
|
||||
已并入,会打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场` → 摘联接 → `完成(分支此前已合入)`。
|
||||
7. 清场完成后再敲一次 `done <ID>` 会得到 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——
|
||||
**这不是失败**:清场的最后一步 unlink 了台账。判完成以 `git log` 为准。
|
||||
|
||||
## 铁律
|
||||
|
||||
1. **`done` 只能在主工作树执行**——主干分支检出在主树,git 不允许同一分支两处检出。
|
||||
2. **`done` 不 push**:它结束于本地合入 + 门禁全绿;推送需用户明确许可。
|
||||
3. **`build` 与 `bench` 都必须串行**(UV4 不能两个实例同时 `-b`;COM6、ST-LINK、设备 IP `192.168.1.100` 各一份)。`build` 锁由 **/keil-build 自持自释**(`keil_build.py`)→ 调用方**不要**手工 acquire/release;`bench` 锁**必须手工** acquire → 烧录 / `/serial-boot-test` → release `--bench-pass`。非登记持有者怎么放 → B 节;插件树 → F 节。
|
||||
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 任务表块、`MDK-ARM/*.uvprojx`、`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务树里改**,留到 `done` 统一处理;用户显式豁免时(在任务树写、由 done 合回)要在交付报告里点名这条例外。
|
||||
5. **清场必须先摘联接**:脚本先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再 `git worktree remove`。手工拆树前必跑 **E 第 3 步**预扫拿到 `REPARSE: none`——否则 git 可能递归进联接把主树 `.venv` 一起删掉。
|
||||
6. **活跃任务硬上限 8 = 脚本常量 `MAX_CONCURRENT`(`task_worktree.py:30`)——唯一事实源**:报错文案与 `new` 的插件树提示都插值它,AGENTS.md / 方案文档 / frontmatter / A 节的数字全是引用同步。改这个数字 → 全形态扫描走 `stale-doc-count-sweep` skill + 隔离小仓验证上限真生效;每棵磁盘代价按实测口径重述、别照抄旧数(细节 → 验证清单末条)。满了 exit 2 → A;悬挂台账 → E 第 6 步。
|
||||
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**(否则污染 `git status`、`--all-files` 与全库搜索)。唯一例外 = 插件写死的仓内 `.dsh-worktrees/`(改不了,已用预置 .gitignore + 三个全库扫描排除兜住 → F 节)。
|
||||
8. **`done` 是合并点**:属主没提交完就别跑;合并提交进了主干就只用「主树补提交」收尾,**不用 `reset --hard`** 反悔 → C 节。
|
||||
9. **非登记 / detached 的树绝不跑 `done` / `abort`**:两者都以台账为前置(无台账 exit 2),带残留台账时 `abort --force` 会绕过内容保全把别人在飞的改动连树一起删 → E 节。
|
||||
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
||||
2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
|
||||
3. **`build` 与 `bench` 都必须串行**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
||||
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
|
||||
- `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release;
|
||||
- `bench` 锁**必须手工**:`lock acquire bench --task <ID>` → 烧录/`@test`/探针 →
|
||||
`lock release bench --task <ID> --bench-pass`。非登记任务(lead 的验收窗)占了锁怎么放 → B 节。
|
||||
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
|
||||
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
|
||||
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
|
||||
`git worktree remove`。**手工清场时千万别直接 `git worktree remove`**——git 可能递归进联接,
|
||||
把主工作树的 `.venv` 一起删掉。
|
||||
6. **活跃任务硬上限 3**(脚本常量 `MAX_CONCURRENT = 3`,每棵约 83 MB 检出)。满了 `new` 直接 exit 2,
|
||||
腾位顺序与 `--force` 的代价 → A 节。
|
||||
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。
|
||||
8. **`done` 是合并点**:属主没提交完就别 `done`;合并提交一旦进了主干,只用「主工作树补提交」收尾,
|
||||
**不用 `reset --hard` 反悔** → C 节。
|
||||
|
||||
## 陷阱与验证(收工前各扫一遍)
|
||||
## 陷阱(实测)
|
||||
|
||||
- [reference/pitfalls.md](reference/pitfalls.md):全部实测坑逐条(含 `--dry-run` 不保护清场段、门禁跑在 `/keil-build` 之前、未跟踪文件在 `git log` 与合并里都不留痕、并集合并残留、新树从未构建 → 体积类门禁必然红、注释-only 也刷红体积凭证、「已合入」快路径在门禁前返回不替你复绿、`--force` 只覆盖四类检查、联接不是 symlink(`core.symlinks=false`)、锁不绑进程存活、文件工具先读后改按绝对路径记账、中文门禁回显乱码但 rc 可信)。收工前扫一眼。
|
||||
- [reference/verification.md](reference/verification.md):每节要留的证据(预演 sha + `MT_RC=0`、blob id 对账、两把锁空闲、禁词计数 0、`REPARSE: none`、改上限数字后的全形态扫描)。
|
||||
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
|
||||
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
|
||||
--force --bench-pass` 报的是同一句话,锁照旧没放)。
|
||||
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
|
||||
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
|
||||
不需要管理员权限),且联接必须建在版本控制之外。
|
||||
实测(2026-10-12):用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)`
|
||||
会被系统拒绝(Win32 4392),`mklink /J` 正常——所以脚本走后者,参数以列表交给
|
||||
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
|
||||
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
|
||||
但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。
|
||||
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
|
||||
- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
|
||||
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
|
||||
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||||
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
|
||||
误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。
|
||||
**处置(2026-10-25 补全)**:按 C 节第 5 步的完整链补——`check_mem_numbers.py --write` →
|
||||
`ram_baseline.py --write` → `ram_baseline.py --update` → `check_test_coverage.py --update-baseline` →
|
||||
`check_doc_links.py --all` → 提交 → 重跑 `done`(它会检测到「已合入」直接清场)。
|
||||
此时主干已含合并提交,**绝不能用提示里的 `reset --hard` 回退**。
|
||||
- **`done` 的门禁 = `ci_local.py` = `pre-commit run --all-files`(50 条钩子全跑)**(2026-10-26):红了先读
|
||||
`[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K`,**按钩子 id 定根因**,别一律当「`.map` 滞后」
|
||||
去跑补基线链。典型例外:`generated-fresh`(生成器幂等性,触发面 `tools/**`、`docs/images/*.svg`、UI 生成文件)——
|
||||
合进来的分支改了生成器却没重跑产物时,补基线链救不了,处置是在**主工作树重跑那个生成器**、脚本与产物同一笔提交,
|
||||
再重跑 `done`。另注意 `pre-commit run --all-files` 只跑 pre-commit stage,个别只在 commit-msg / 手工阶段跑的守卫不在此列
|
||||
(口径见 `scripts/ci_local.py` 头部说明)。
|
||||
- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的
|
||||
构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py`
|
||||
的 L0c 都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。
|
||||
同理 `done` 的重生成步骤会 `[skip] 未找到 .map,跳过内存快照同步(需先 @build)`。
|
||||
**处置**:新空间第一次提交前先 `@build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
|
||||
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
|
||||
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
|
||||
`@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
|
||||
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
|
||||
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
|
||||
- **`ram_baseline.py` 的 help 串里有未转义的 `%`**(2026-10-25 实测:某些直调姿势会渲染 argparse help 并崩)——
|
||||
查用法读脚本头部 docstring,别靠 `--help`;修脚本本身属 `mem-budget` skill 的范围。
|
||||
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
|
||||
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
|
||||
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
|
||||
- **并集合并残留**:双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,
|
||||
可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 +
|
||||
连续重复行;**权威判据是 base / 两个 tip 三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
|
||||
别用 `tail -N` 目测。
|
||||
- **`.workbuddy-ai/` 刻意不共享**:记忆不随任务分叉,也不给联接递归删除的机会。
|
||||
任务工作树自己的记忆由 `done` / `abort` 在清场前归并回主工作树。
|
||||
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
|
||||
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
|
||||
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
|
||||
所以判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
|
||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||
- **TTL 按锁分设**(2026-10-12):`build` = **1h**、`bench` = **6h**(`LOCK_TTL_HOURS`)。
|
||||
口径是「兜底窗口应与操作真实时长同量级」——编译是秒~分钟级,6h 的兜底太钝;
|
||||
bench 是会话级(flash→`@test`→探针),才配得上小时级。持有者还可用
|
||||
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
|
||||
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
|
||||
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
|
||||
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
|
||||
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
||||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
|
||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
||||
|
||||
## 验证
|
||||
|
||||
- `$TW list`:条目数 = `ls _wt` 的目录数且 ≤ 3;刚回收的任务与 B 节的占位条目都不再出现。
|
||||
- `$TW lock status`:`build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)`——没有本会话留下的持有者。
|
||||
- `git log --oneline -3`:能看到 `merge: task/<ID>-<slug> 合入 <base>` 与其后的补基线提交;
|
||||
`git status --porcelain` 为空(补链改动已提交,才谈得上重跑 `done`)。
|
||||
- `ls _wt/.state/`:该 ID 的台账已无(清场最后一步 unlink);重复 `done` 报「任务不存在」不必再追。
|
||||
- 补绿链的每一条都自己 `echo RC=$?` 看到 0,最后以重跑 `done` 打出的**清场日志**(`已摘除联接` /
|
||||
`已删除分支` / `完成(分支此前已合入)`)为成功判据,而不是 `tail` 出来的最后一行。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
- 构建 = `keil-build`· 烧录/回读 = `stlink-flash` · 串口闭环 = `serial-boot-test`(真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准——实测 `MDK-ARM/Objects/STM32F407-Demo.hex` 不存在,拷错 = 烧旧镜像)
|
||||
- 新 `.c` 双端注册 = `new-source-file` · RAM/.map 台账 = `mem-budget` · UI 回归 = `ui-verify` · 计数同步 = `stale-doc-count-sweep` · 卡死的技能台账行 = `mnemon-skill-ledger-stuck-repair`
|
||||
- DSH worktree 插件(`/worktree` / `worktree_create`,仓内 `.dsh-worktrees/`)与本 skill 的分工、桥接命令、锁归属 → F 节 · 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`
|
||||
- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
|
||||
- 烧录产物的真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准(实测
|
||||
`MDK-ARM/Objects/STM32F407-Demo.hex` 这个路径并不存在),拷错等于烧了旧镜像 → `serial-boot-test` skill
|
||||
- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册)
|
||||
- `.map` 口径 / RAM 台账与趋势 → `mem-budget` skill;UI 回归 → `ui-verify` skill
|
||||
- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
### D. 门禁指标异常增长 → 逐文件归因(把净室工作树当**测量仪**,主树全程只读)
|
||||
|
||||
**场景**(2026-10-27 实测):`id-check` 突然 FAIL「不可判定 1,178 > 上限 1,166」——比上一轮量到的 1,162 又涨了 **+16**,
|
||||
而主工作树里同时压着 4 条在飞任务道的未提交改动。**不许 stash / checkout / reset**(那是搬动甚至丢弃别人未提交的工作,
|
||||
项目级 Never;`git status` 一乱,整条道的现场就没了),**不许改门禁脚本本身**(它就是那把尺子,改了数字就不可信),
|
||||
**也别在脏主树里跑门禁**(那测的是 4 条道 + 未跟踪文件的混合体,永远分不出谁)。正解:**在仓库之外另起一棵
|
||||
detached HEAD 的干净工作树当净室,一次只往里注入一份在飞文件**,与基线差分即得每个文件的独立贡献。两步:
|
||||
**净室建树 → 单文件注入测量**。
|
||||
|
||||
1. **建净室 + 先落基线数**(路径必须**在仓库目录之外**;铁律 7 对临时树同样成立——放仓内就污染 `git status` /
|
||||
`pre-commit --all-files` / 全库搜索):
|
||||
|
||||
```bash
|
||||
MAIN="D:/Code/DTU Code/STM32F4-Base" # 主工作树绝对路径(含空格 → 变量整体加引号)
|
||||
WT="$MAIN/../_wt/und-attr" # 仓外同级 _wt/ 下;一次测量固定用同一个名字
|
||||
rm -rf "$WT" 2>/dev/null
|
||||
git -C "$MAIN" worktree add --detach "$WT" HEAD >/dev/null 2>&1 || { echo WT_FAIL; exit 9; }
|
||||
git -C "$WT" status --porcelain # 应为空
|
||||
"$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 口径:id-check 钩子已于 2026-10-28 退役(脚本移入 scripts/archive/id_gate/),按该脚本自身默认参数跑;基线与注入用**同一条命令**
|
||||
```
|
||||
|
||||
- `--detach`(**不要 `-b`**):净室没有分支、不写 `_wt/.state/<ID>.json` 台账 → **不占 `MAX_CONCURRENT = 8`
|
||||
名额、不出现在 `$TW list`**,也没人会被误指去对它跑 `done`(建与拆都不走 `$TW new` / `$TW done`)。
|
||||
- 能这么量的前提:该门禁**只读仓库内文本**。依赖本树构建产物的门禁(`mem-numbers` / `mem-budget` 要 `.map`)
|
||||
在净室里必然报「找不到 .map 事实源」(见陷阱「新建工作树从未构建过」)——**那类指标不能这么归因**。
|
||||
- `git status --porcelain` 非空 = 有别人的未跟踪文件被带进净室 → 先 `git clean -xdn` **干扫**(别去 `n`)
|
||||
看清会删什么,只挪走多出来的那些再测;语料被污染出来的数字与 CI 不可比。
|
||||
2. **一次只注入一份在飞文件 → 跑同一个扫描器 → 与基线差分**(得到的是每个文件的**独立**贡献,不是合计):
|
||||
|
||||
```bash
|
||||
for f in "App/log/sys_log_mcu.c" "test/host/sys_log_mcu/sys_log_mcu_test.c" "docs/子系统设计/运行与自检记录设计说明.md"; do
|
||||
mkdir -p "$WT/$(dirname "$f")"
|
||||
cp "$MAIN/$f" "$WT/$f" # 拷进净室;主树那份只读、一字节不动
|
||||
echo "== $f =="; "$PY" "$WT/scripts/archive/id_gate/check_ids.py" 2>&1 | tail -25 # ← 与第 1 步同一条命令
|
||||
git -C "$WT" checkout -- "$f" # 只还原净室里自己刚拷进来的那份
|
||||
done
|
||||
```
|
||||
|
||||
实测:`run_par.c` / `board_ui_nav.py` / `check_nested_comment.py` / keil `SKILL.md` **各 +0**,
|
||||
而 log 道那三个文件 **+6 / +8 / +2**,合计**正好 +16**。**Σ(逐文件增量) ≠ 观察到的涨幅 → 还有没被注入的
|
||||
在飞文件,或存在文件间交互项**,不许就此交卷。
|
||||
3. **定性增量是哪一类字面量**(只有「谁」没有「为什么」= 给不出可执行的改法):同一计数器在 **HEAD 版**与
|
||||
**在飞版**各数一遍,**差值才是新增行带来的**(存量的不算这条道的账);再对新增行直接 grep。本次 16 处全是
|
||||
**裸形 「字母 T 累进数字 4」**(`T` 在 `scripts/archive/id_gate/id_registry.json` 的 `bare_check_letters` 里,规范形态是连字符形),
|
||||
全部落进 `skip_bare_unmanaged` —— 到这一步结论才配发给属主。
|
||||
4. **闭合 + 复验主树一个字节没被污染**:
|
||||
|
||||
```bash
|
||||
git -C "$MAIN" worktree remove --force "$WT" # 净室里只有自己的拷贝、没有原件,--force 安全
|
||||
git -C "$MAIN" worktree list # 不该再有 und-attr
|
||||
git -C "$MAIN" status --porcelain | sort # 与测量前**逐行一致**(测量前先存一份基线清单)
|
||||
```
|
||||
|
||||
⚠ **上面第一行的前提是有条件的**:树里混进了别人的在飞 / 未跟踪内容时「只有自己的拷贝」就不成立了,
|
||||
`--force` 直接拆等于替别人把唯一版本删掉 → 先走 **E 节**(全景 → 逐文件分桶 → 联接预扫 → 唯一残留先备份),
|
||||
再回来执行本步;复验按 **E 第 5 步**做(拆前/拆后主树 `status --porcelain -uall` 行数必须相同)。
|
||||
**同一棵净室树的第二种用法**:按指定 commit **只读**构建出像 / 烧录验收——不搬动任何在飞改动,也不为台架
|
||||
提前抢发一笔提交;构建与烧录都在这棵树里做(不在主树),仍受 `bench` / `build` 锁串行(铁律 3)。
|
||||
@@ -1,87 +0,0 @@
|
||||
### F. 与 DSH worktree 插件(`/worktree`、模型工具 `worktree_create`)共存
|
||||
|
||||
本仓现在有**两套**隔离空间。别混用,也别互相回收:
|
||||
|
||||
| | 本 skill(文档别名 `@task`,DSH 显式调用 `/task-worktree`) | DSH worktree 插件(`dsh-task-worktree`) |
|
||||
|---|---|---|
|
||||
| 树在哪 | 仓库**外** `../_wt/<ID>-<slug>/` | 仓库**内** `.dsh-worktrees/worktree/<name>/` |
|
||||
| 谁建 | `new <ID> <slug>`(人来敲命令) | 会话选「Worktree模式」/ 模型调 `worktree_create`(零命令) |
|
||||
| 台账 | `_wt/.state/<ID>.json`(本 skill) | `.dsh-worktrees/manifest.json`(插件私有格式,别去改) |
|
||||
| 分支名 | `task/<ID>-<slug>` | `worktree/<name>` |
|
||||
| 共享联接 | `new` 自动建 | **建树时没有** → 要 `plugin adopt` 补 |
|
||||
| 回收 | `done`(预演 / 合并 / 门禁 / 清场)或 `abort` | 用户敲 `/worktree finish\|bring-back\|remove\|prune`(模型碰不到收尾命令) |
|
||||
|
||||
**为什么允许它把树建在仓内**(铁律 7 的唯一例外):路径写死在插件里(`WORKTREE_NAMESPACE = worktree`
|
||||
+ `<repo>/.dsh-worktrees/`),而暴露给模型的工具只有 `worktree_create` / `worktree_list` /
|
||||
`worktree_status` 三个,配置面改不动。兜法三件(均已落地):
|
||||
|
||||
1. `.gitignore` **预置** `.dsh-worktrees/` —— 插件首次 `create` 会往 `.gitignore` 追加一模一样的字面量,
|
||||
`ensureIgnored()` 见到同名规则即跳过;不预置就等于让插件在会话中途改这份受控文件(diff 里冒出
|
||||
一行"不是我加的")。
|
||||
2. 全库扫描的排除面登记了它:`scripts/check_doc_links.py` 的 `SKIP_PARTS`、
|
||||
`tools/refgraph/gen_refgraph.py` 的 `EXCLUDE_DIRNAMES`、`scripts/check_layer_deps.py` 的 `os.walk` 兜底。
|
||||
一份仓库副本被全库扫描吞进去 = 文档链接假红 + 配图生成物逐字节漂移砸在某位同事头上。
|
||||
3. 联接与锁归属由本节命令接上(下面三步)。
|
||||
|
||||
**这棵树是不是插件的**:`git worktree list` 里路径含 `.dsh-worktrees/` 就是。`$TW list` 现在会在台账段
|
||||
下面另起一段列出它们,联接列**点名缺哪一样**(`已联` / `缺 .venv` / `缺 SDL2` / `齐(实体SDL2)`)。**插件树不占
|
||||
`MAX_CONCURRENT = 8` 名额**——回收不归本 skill,
|
||||
硬计入只会让别人那棵关不掉的树把 `new` 挡死(`new` 会打印一行「另有 N 个插件工作树不计入上限」提醒并发代价)。
|
||||
|
||||
三步接上(`<name>` 可带 `worktree/` 前缀,也可多段如 `refactor/logging`):
|
||||
|
||||
```bash
|
||||
"$PY" "$TW" plugin list # 一览:分支 / 脏 / 缺哪个联接(缺项另起一行说后果)
|
||||
"$PY" "$TW" plugin adopt N21-dhara-isfree # 只补缺失的 .venv 与 simulator/SDL2 联接
|
||||
"$PY" "$TW" plugin unadopt N21-dhara-isfree # 只摘联接(os.rmdir),绝不动目标内容与树里文件
|
||||
```
|
||||
|
||||
- **两条联接坏了的样子完全不同**:缺 `.venv` → `ci_local.py` 的四级解释器回退**静默**落到机器级
|
||||
python(无 fontTools = `generated-fresh` 假红的老根因,AGENTS.md 记着);缺 `simulator/SDL2` →
|
||||
`build_sim.bat` 当场报错(`SDL2_DIR` 是裸 `set`,没有 env 逃生口)。所以列表只说「已联」不够——
|
||||
它按缺的那一样分别给后果,`adopt` 没补齐也只报「还不能算就绪」,不许说「现在可以跑」。
|
||||
- **adopt 只碰联接**:已联的跳过;`simulator/SDL2` 若已是实体目录只告警不动;`.dsh-worktrees/` 之外的路径
|
||||
一律 exit 2(实测 `plugin adopt ".."` 被拒)——这道闸防的是把主树或 `_wt` 目录当插件树遮蔽掉真实内容。
|
||||
- **锁归属现在认得出来**:`/keil-build` 在插件树里自持自释时,持有者名从 cwd 反推成 `pw:<name>`(多段名压平成
|
||||
`-`),与 `plugin adopt` 打印的那个名字逐字相同(`keil_build.py: plugin_tree_owner()` ↔
|
||||
`task_worktree.py: plugin_lock_owner()`)→ `lock status` 不再一律落成 `build-cli`。
|
||||
- 上台架仍须手工占锁、用同一个名:`lock acquire bench --task pw:<name> --note <说明>` →
|
||||
`lock release bench --task pw:<name> --bench-pass`。插件树没有台账 → 按 B 节只 warn、锁照常放。
|
||||
- **命名建议**:插件 `<name>` 沿用本 skill 的 `<ID>-<slug>` 形态(如 `N21-dhara-isfree`),两套视图同名好对账;
|
||||
但分支前缀必然不同(`worktree/` vs `task/`),别把两者说成同一个分支。
|
||||
- **收尾不在这**:`done` / `abort` 拿插件树名下手只会 exit 2(这是保护,别绕过去手工 `git worktree remove`;
|
||||
要拆先 `plugin unadopt` 摘联接,再由**用户** `/worktree remove`)。`/worktree bring-back` 要求主工作树干净,
|
||||
本仓常有在飞改动 → 提交或 stash 由用户定。反过来插件也不会碰 `_wt/`(路径根不同,谁也删不掉谁)。
|
||||
|
||||
### F-1. 两条共享联接**不是同级需求**(按仓库实配定级;别答成「两个都必须」)
|
||||
|
||||
- **`simulator/SDL2` = 硬需求**:`simulator/CMakeLists.txt:12` 是
|
||||
`set(SDL2_DIR "\${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10")`——普通 `set()`、**非 `CACHE`**,所以既没有环境变量入口、
|
||||
命令行 `-DSDL2_DIR=` 也会被这个普通变量盖掉;`SDL2_INCLUDE_DIR`(:34)、`SDL2_INCLUDE_PARENT`(:70)、`SDL2_LIB_X64`(:71)、
|
||||
链接目录(:180)、`SDL2.dll` 拷贝(:207)**全部从它派生**。缺它 `build_sim.bat` 当场配不出来(脚本里的 `SDL2_DIR` 同样是
|
||||
裸 `set`,也没有 env 逃生口)。
|
||||
- **`.venv` = 软需求**(有显式逃生门,但绕开有代价):`scripts/ci_local.py` 的解释器解析顺序(:108-:138)——
|
||||
① `DTU_PRECOMMIT_PYTHON`(显式配置,指错**立即报错**、不静默降级)→ ② `sys.executable` → ③ 仓库 `.venv` →
|
||||
④ `shutil.which("python")` → ⑤ `_global_python_candidates()` 的常见机器级位置。树内没有 `.venv` 时会**静默**落到机器级
|
||||
解释器 → 无 fontTools → `generated-fresh` 假红(AGENTS.md 记着的老根因);可以用 `DTU_PRECOMMIT_PYTHON` 顶掉,
|
||||
但要每台机器自己配——这就是它「软」的意思:有路可走,走得不爽。
|
||||
|
||||
结论口径:**要补就两条都补(`plugin adopt` 一次干完,成本近乎零),但「都必须」是错的**——SDL2 没有任何替代路径,
|
||||
`.venv` 有 `DTU_PRECOMMIT_PYTHON` 这条门。现役命令形式仍是 `plugin list` / `plugin adopt <name>`;**不改代码、
|
||||
不改 `CMakeLists.txt`**(真要给 SDL2 加 CACHE 逃生门属于改模拟器构建口径,另案 → `lvgl-simulator` skill)。
|
||||
|
||||
### F-2. 「DSH 都有 worktree 插件了,为什么还自持 Python 生命周期」——取证姿势与答案边界
|
||||
|
||||
这题不许凭印象答,口径 = **实读插件源码**:在 DSH 安装目录定位插件包(`npm root -g` → `<global>/node_modules/@deepseek-ai/dsh/`,
|
||||
或读 profile 的 `dsh.profile.bundles` 找包名),**记下版本号与文件路径**,再对三个概念名逐个 grep 并**报出命中数**:
|
||||
|
||||
1. 独占锁 / 资源串行(`lock`、`acquire`、`release`、TTL)——插件侧**零命中**:它的模型是「一棵树一个会话」,没有跨会话排队概念。
|
||||
2. 共享目录联接(`.venv`、`simulator/SDL2`、`mklink /J`、reparse point)——零命中:插件建树就是纯 `git worktree add`,
|
||||
所以插件树天生缺这两条联接(定级见 F-1)。
|
||||
3. 联接状态显示(三态:已联 / 缺 .venv / 缺 SDL2 / 齐(实体SDL2))——零命中。
|
||||
|
||||
**重叠面要说清**:两侧底层是**同一条** `git worktree add`,本 skill 没有重造「建树」;重叠只在**开**与**收**两个动作,
|
||||
而插件的收(`/worktree finish|bring-back|remove|prune`)是**人类命令**、模型碰不到,也没有「合并前预演 → 并集残留审计 →
|
||||
补基线 → 门禁 → 先摘联接」这条编排。本仓为什么必须自持:硬件各只有一份(一块 ST-LINK、一套 Keil 授权、一个 COM6、
|
||||
一个 `192.168.1.100`)→ 要 `build` / `bench` 锁串行;SDL2 是模拟器硬依赖且无逃生门 → 要共享联接;台架 PASS 要留给
|
||||
`done` 记账 → 要台账。**给不出版本号与 grep 命中数,就不要下结论。**
|
||||
@@ -1,9 +0,0 @@
|
||||
# 原 frontmatter(2026-10-29 因超 65536 字节写侧上限而瘦身,原文逐字留档)
|
||||
|
||||
本技能正文受 dsh-mnemon 插件写侧上限 65536 字节约束(只在写侧校验,加载侧不查),超限时自进化每次「生成草案」都会被拒、行永久卡在「生成中」。以下是瘦身前的 description / whenToUse 原文,触发词语义更全,写路由时可参考;如需恢复请连正文一起改回并先确认总体积 < 65536。
|
||||
|
||||
---
|
||||
name: task-worktree
|
||||
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 8 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、**主工作树被别的并发会话弄脏时的手工合并路径(取证 → `merge-tree` 预演 → `merge --no-ff -F` → 外部改动 blob id 前后对账 → C2 节)**、门禁未全绿的补基线收尾、并集合并残留审计、**任务树内门禁与审计输出的落点(`<tree>/ci-artifacts/` 先 `mkdir -p`;文档链接按单文件在树内跑、全库 `--all` 基线合并后在主树跑;`simulator/SDL2` 联接假红 → G 节)**、**非登记工作树(detached HEAD / 无台账 / 树内有在飞或未跟踪内容)的安全回收**(逐文件内容分桶 + 联接/reparse 预扫 + 唯一残留先备份再 --force + 悬挂台账清理复验),以及**把仓外 detached worktree 当净室测量仪**——门禁指标异常增长时一次只注入一份在飞文件、与基线差分做逐文件归因;**以及与 DSH worktree 插件(`/worktree`、模型工具 `worktree_create`,树在仓内 .dsh-worktrees/)的桥接**——`plugin adopt` 补共享联接、锁归属 `pw:<name>`、回收仍归 /worktree。规格见 docs/环境与部署/并行任务工作树方案.md。
|
||||
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 8 个活跃任务(上限 8)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、或 `done` 报「主工作树不干净」而那份未提交改动是**别的会话/插件**留下的(不能提交也不能丢弃 → C2 节手工合并)、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时;或**要在任务树里跑文档/审计门禁**——`--all` 报一堆问题却全在 `simulator/SDL2` 联接下、或往 `ci-artifacts/` 写输出报 ENOENT / No such file or directory(→ G 节);或**门禁指标异常增长(如 id-check 的不可判定数从 1,162 涨到 1,178、+16)却查不出是哪个文件 / 哪条任务道造成的**、主工作树压着多条在飞改动而不许 stash / 提交 / 改门禁脚本、要按 HEAD 基线只读出像验收时;或**要回收 `@task list` 里根本看不到的工作树**(`git worktree list` 有它、台账没有 / detached HEAD / 树里疑似留着别的会话的在飞或未跟踪内容)、或**要清台账里 branch / path 都已不存在的悬挂条目**(`@task list` 输出发怪、`new` 报「任务已存在」或白占 8 个名额之一);或**这棵隔离空间是 DSH worktree 插件开的**(会话选了「Worktree模式」/ `/worktree create`,树在仓库内 `.dsh-worktrees/`)——进去发现 `.venv` 不在、`@build` 与门禁全部起跑失败,或想看清谁在占 UV4 / 台架 → F 节。
|
||||
---
|
||||
@@ -1,61 +0,0 @@
|
||||
### G. 任务树内跑门禁与审计:输出落点(ci-artifacts)与 SDL2 联接假红
|
||||
|
||||
三条硬规矩,全是 2026-10-29 实测付过失败成本的(两起 ENOENT + 一次假红误判):
|
||||
|
||||
1. **门禁/审计输出只落 `<tree>/ci-artifacts/`,而新树里它不存在 → 落盘前先 `mkdir -p`**。`ci-artifacts/` 被
|
||||
`.gitignore` 忽略(不污染脏行数,E 第 4 步的备份同理),但也因此**不会被检出**——`$TW new` 出来的树里没有这个
|
||||
目录。直接 `fs.writeFileSync("<tree>/ci-artifacts/head-md.md", …)` 当场 `ENOENT: no such file or directory`,
|
||||
**整段程序零执行**(实测:那一轮的门禁体检与快照读取全没跑成,白重跑一次)。用 Node 写之前先
|
||||
`fs.mkdirSync(dir, { recursive: true })`;用 bash 重定向就先单独敲一条 `mkdir -p ci-artifacts` 再落盘
|
||||
——重定向在命令执行**之前**解析,目录没建出来时是同一条命令直接失败,后面串着的 `grep` 跟着一起报
|
||||
`No such file or directory`,看着像「门禁没跑出结果」,其实是写错了地方。
|
||||
2. **`cd` 进任务树之后,`../_wt/x.txt` 不是你以为的地方**:`cd` 之后 `..` = `_wt`,`../_wt/x.txt` 落到
|
||||
`_wt/_wt/x.txt`(实测就是这么报的)。**树内命令的输出一律用树内相对路径**(`ci-artifacts/xxx.txt`);
|
||||
要绝对路径就写完整绝对路径,跨树取文件用 `$MAIN/…`、`$WTROOT/…` 这种变量打头的全路径,
|
||||
不靠 `cd` 之后的相对层级往上跳。
|
||||
3. **任务树内文档门禁按单文件跑;全库 `--all` 基线留到合并后在主树跑**。任务树的 `simulator/SDL2` 是指向
|
||||
主树同一份的**目录联接**(`new` 的第 2/3 步建的),`--all` 会把这份「仓库里的第二份仓库副本」一起扫:
|
||||
实测任务树里 `check_doc_links.py --all` 打 `检查 222 个文档,35 处问题`、RC=1,而 **35 处全落在
|
||||
`simulator/SDL2` 联接下**、要改的文档一条都没有 → 判红纯属联接带来的假红。单文件检查不受影响
|
||||
(扫描根就是那一个文件,联接目录不在面内)。主树本身没有 SDL2 时不产生这类假红(联接如实镜像主树,
|
||||
F 节那棵树连 SDL2 都没得联),但**别据此认为 `--all` 在树内可信**。
|
||||
|
||||
```bash
|
||||
F="docs/<子目录>/<本次改动的文件>.md" # 本次改动的那一个文件
|
||||
"$PY" scripts/check_doc_links.py "$F" > ci-artifacts/dl-<slug>.txt 2>&1; echo RC=$?
|
||||
cat ci-artifacts/dl-<slug>.txt
|
||||
```
|
||||
|
||||
逐文件跑的实测读数:需求文档 161 处章节引用 / 98% 机器可判定 / rc=0;方案文档 16 处 / 88% / rc=0。
|
||||
确实要在树内跑 `--all` 时(例如怀疑主干基线本身),判红顺序**固定三步**,缺一步都不许下结论:
|
||||
① 在**主树**跑同一条命令拿基线;② 过滤掉 `simulator/SDL2` 联接下的问题行;③ 剩下的与主树基线逐行比。
|
||||
只跑 `--all` 见 RC=1 就回「文档门禁挂了」= 把基线噪声算成本任务的账;只跑「过滤后为空」就回「全绿」
|
||||
= 会漏掉真红。全库 `--all` 的正解位置是**合并后的主工作树**:实测合入后在主树跑 `check_doc_links.py --all`
|
||||
= 2,597 处章节引用 / 194 个文档 / rc=0 —— 这才证明「就地删叙述」没打断任何引用。
|
||||
另:**不因 `--all` 的既有假红跳过对本次改动文件的单文件复检**(假红是基线的事,你的文件仍要逐个过)。
|
||||
|
||||
4. **文档改动在树内提交前的固定体检**(2026-10-29 实测):`lines / bytes / BOM / CR / U+FFFD / 三连续空行 /
|
||||
行尾空格 / 末行单个 LF` + 一个自写的 **Markdown 表格每行列数一致性**检查(数格子前先把转义管道 `\|` 还原成占位符,
|
||||
否则会把 `\|` 当成列边界数错)。为什么要自己数:`doc-links` 只管链接与章节、`no-mojibake` 只管编码面,
|
||||
**表格断列与删段留下的三连续空行没有任何钩子会报**。
|
||||
实测读数:需求文档 `lines=2665 bytes=196137 BOM=false CR=0 U+FFFD=0 tripleBlank=0 trailingWS=0 endLF=true`,
|
||||
表格体检另报两处坏表格(`L988-992 counts=5/7`、`L2472-2486 counts=2/3`)——**与本次编辑无关的历史行**。
|
||||
所以体检要**区分「本次改动 bad=0」与「整份文件 bad=0」**:先定位归属再决定动不动手,顺手改别人的区域等于把
|
||||
没打算提交的内容卷进这笔改动(报告里写「本次改动引入 0 处」,并点名那两处是存量)。
|
||||
|
||||
5. **提交 / 合并信息一律 `-F` 读文件;清退类任务还要做禁词自检**(2026-10-29 两遍实测):消息文件写在
|
||||
`ci-artifacts/`(被忽略 → 不污染脏行数),UTF-8 无 BOM、CR=0、标题 ≤100 字符、正文每行 ≤80(实测标题 31 / 32 字符、
|
||||
正文最长行 50 / 55)——`-m` 会把中文按终端代码页转码搞坏,长中文标题还容易撞 gitlint。硬自检:
|
||||
|
||||
```bash
|
||||
git log -1 --pretty=%B | grep -c '485' # 被清退主题的关键词:必须是 0
|
||||
git log -1 --pretty=%B | grep -ci '\bDE\b' # 同主题的次要关键词:同样 0
|
||||
```
|
||||
|
||||
关键词不许出现在**标题、正文、合并信息**任何一处(实测标题改写成「删除与本方案无关的其它串口通道对照内容」这种
|
||||
语义短语替掉了它)——否则「别让我看到 X」这条要求在 `git log` 里当场破产。合并信息是另一份文件:
|
||||
`ci-artifacts/merge-msg-<slug>.txt` 落盘后**同样 grep 一遍**再 `git merge --no-ff --no-edit -F`(C2 第 3 步)。
|
||||
自审发现消息里的行数 / 统计算错而**尚未推送** → `git commit --amend -F <改好的文件>` 就地修(实测就这么修掉一次
|
||||
off-by-one);**别追加一笔「更正」提交**——那等于把禁词和被更正的错数一起写回历史。
|
||||
6. **合并提交期 pre-commit 只查冲突文件(2026-10-29 实测)**:合并那一次提交的钩子日志首行是 `[INFO] Checking merge-conflict files only.`,其余钩子全打 `(no files to check) Skipped` —— 于是「合并提交通过了 30 项钩子」这句话**只覆盖冲突文件**,别拿它当全量绿。正解:合并落主干后立刻在主工作树跑全量 `scripts/ci_local.py`(静态 + 提交信息 + 主机侧单测 + manual 段),实测全量这一步才会抓到「表格断列」「章节引用悬空」这类跨文件问题。合并解冲突时若上游把 Markdown 表格重排过,注意**单元格里的竖线必须写成反斜杠竖线**(先例 `docs/子系统设计/运行与自检记录设计说明.md` 的 `FA_WRITE\|FA_SEEKEND`),否则 `FA_WRITE|FA_OPEN_APPEND` 会被当列分隔符打断,而 `doc-links` 不查表格列数——要自己数(第 4 条)。
|
||||
7. **跨层改动拆不成「按层分笔」提交(2026-10-29 实测,四笔改一笔)**:钩子看到的世界 = HEAD + 本笔 staged——pre-commit 会 stash 已跟踪文件的未暂存改动,而**未跟踪文件不在 `git diff-index` 里、不会被藏**(`git add -N` 也救不了,pre-commit 会把它撤下索引、文件仍在盘上)。后果:把双端登记(`MDK-ARM/*.uvprojx` / `simulator/CMakeLists.txt`)、生成器产物、互锁引用的文档分到别的笔里,本笔必被 `doc-links`(引用了还没入册的小节)、`registration-check`(源文件登记在另一笔)、`generated-fresh`(`#page=` 找不到页面描述符、字库清单与源不一致)、`mem-numbers`(受管区还是旧 .map 的数)四道**整仓**门禁一起拦。判据:**一笔提交必须自洽**——互锁的改动(代码 + 登记 + 生成物 + 文档)走同一笔;机制与三条处置结论(半成品要么先落地要么先挪出工作树)→ `docs/代码规范/pre-commit使用说明.md` §2「stash 覆盖面」。
|
||||
@@ -1,86 +0,0 @@
|
||||
### C2. `done` 被「主工作树不干净」挡住,而那份脏改动是**别的并发会话**留下的
|
||||
|
||||
**场景**(2026-10-29 同一套流程在同一天跑了两遍,两遍都撞上它):任务树里的文档改完、准备合回 `v2-on-ui`,
|
||||
主工作树却压着一个自己从没碰过的未提交改动(两次都是 `M .dsh/skills/serial-boot-test/scripts/run_boot_test.py`,
|
||||
另一个并发会话/插件留下的)。它既不能提交(不是我的改动,也不知道写没写完),也不能
|
||||
stash / checkout / reset / 删除(项目级 Never:丢弃别人的工作)。`$TW done <ID>` 的前置正好卡在这——
|
||||
它要求主工作树干净,否则未提交改动会被卷进合并(见 C 第 6 步),于是**合并与回收一起被挡**。
|
||||
|
||||
**正解 = 取证 → `merge-tree` 预演 → 主树手工 `--no-ff` 合并 → 自证外部文件没被动过 → 照常收工**,六步:
|
||||
|
||||
1. **先取证再动手**(别一看见脏就下「谁弄坏的」结论)。记下它的 mtime 与 diff 内容,判断与本合并有无交集,
|
||||
并把**合并前**的 blob id 与 `diff --stat` 存进 `ci-artifacts/`(第 4 步拿它对账):
|
||||
|
||||
```bash
|
||||
MAIN="D:/Code/DTU Code/STM32F4-Base"
|
||||
EXT=".dsh/skills/serial-boot-test/scripts/run_boot_test.py" # 外部脏文件的仓库相对路径
|
||||
mkdir -p ci-artifacts
|
||||
git -C "$MAIN" status --porcelain # 全量脏清单(本会话只认自己那几条)
|
||||
stat -c "%y %s %n" "$MAIN/$EXT" # mtime + 字节数:定性是谁在什么时候改的
|
||||
git -C "$MAIN" diff -- "$EXT" | cat # 内容性质;与本次合并有无同文件同区域
|
||||
git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-before.txt # ← 存下这个 blob id
|
||||
git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-before.txt # ← 以及这份统计(几加几减)
|
||||
```
|
||||
|
||||
实测:mtime 落在另一会话的活跃时段内,diff 是「docstring 引号改写」2+/2−,与要合的文档毫无交集 →
|
||||
定性为**与本合并无交集、不丢弃**。顺手 `git -C "$MAIN" worktree list` 看一眼有没有**别人中途新建的任务树**
|
||||
(两次里都撞见第二棵树 `data-trust-restructure`,正是这份外部改动的来路)——这是并发会话的硬证据。
|
||||
2. **`merge-tree` 预演:先证明能干净合并,再碰主干**。`$TW done --dry-run` 在这条路上同样被前置挡住,
|
||||
预演只能自己敲(git 2.38+ 的新语法,打印的是**结果树 sha**):
|
||||
|
||||
```bash
|
||||
git -C "$MAIN" merge-tree --write-tree v2-on-ui task/<ID>-<slug>; echo MT_RC=$?
|
||||
```
|
||||
|
||||
判据 = **打印出一个 tree sha 且 RC=0**(实测打出 `4220906b…` + `MT_RC=0` 才敢往下走)。⚠ 本机 git 太旧没有
|
||||
这个子命令时退回 `$TW done <ID> --dry-run`——它的前置也是「主树干净」,可用环境变量 `DTU_INTEGRATION_WORKTREE`
|
||||
把集成点指到一棵干净的临时树去演练(见本文开头的环境变量);**别为了跑 dry-run 去动主树那份脏改动**。
|
||||
预演报冲突 → 回到冲突本身处理,不许直接 `--no-ff` 硬合。
|
||||
3. **主工作树里手工 `--no-ff` 合并**(合并信息走 `-F` 文件,放在被忽略的 `ci-artifacts/`;编码口径同 C 第 5 步:
|
||||
UTF-8 无 BOM、正文每行 ≤80 字符;标题格式 `merge: task/<ID>-<slug> 合入 <base>`):
|
||||
|
||||
```bash
|
||||
git -C "$MAIN" merge --no-ff --no-edit -F ci-artifacts/merge-msg-<slug>.txt task/<ID>-<slug>
|
||||
```
|
||||
|
||||
pre-commit 会**自己**把那份未提交改动 stash 起来、跑完钩子再放回(输出里能看到 `[INFO] Stashing unstaged files to …`)
|
||||
——**这条不是让你去手工 stash**,正好相反:钩子的 stash 往返让它自己走完,你只需在第 4 步验证它把东西原样还回来了。
|
||||
**为什么可以绕开 `done`**(报告里要写清这句):`done` 的前置要求主工作树干净,而那份外部改动既不能提交也不能
|
||||
丢弃,所以合并只剩「手工预演 + 手工合并」一条合法路径;被跳过的门禁与审计由第 2 步(预演 sha + RC=0)、
|
||||
第 4 步(blob id 对账)、G 节(合并后在主树跑全库基线)、第 6 步(`lock status`)逐项补回。
|
||||
**这不是绕过保护**,是把保护要求的证据逐条自己交出来。
|
||||
4. **自证那份外部改动原样还在**(本节唯一的硬证据,两次实测都靠它结案):合并后**再跑一次**同一对命令,
|
||||
与第 1 步存的两个值**逐项相同**:
|
||||
|
||||
```bash
|
||||
git -C "$MAIN" hash-object "$MAIN/$EXT" | tee ci-artifacts/ext-blob-after.txt
|
||||
git -C "$MAIN" diff --stat -- "$EXT" | tee ci-artifacts/ext-stat-after.txt
|
||||
diff ci-artifacts/ext-blob-before.txt ci-artifacts/ext-blob-after.txt # 空输出才算过
|
||||
git -C "$MAIN" status --porcelain # 该文件仍是 M:没被顺手提交,也没被清成干净
|
||||
```
|
||||
|
||||
实测:合并前后 blob id 同为 `369380ca8eb024b593d872455b6a020797abcd8d`、`diff --stat` 同为 2+/2−
|
||||
→ 证明 pre-commit 的 stash 往返没动别人的工作;合进主干的那份文档另按 G 节复核(零残留 + 全库基线)。
|
||||
blob id 变了 = stash pop 出了半冲突(现场会留下 stash 与冲突标记)→ **立刻停止收工**,把两份 blob id 与两份
|
||||
diff 原样贴进报告交给该文件属主;**绝不用 `git checkout -- $EXT` 抹平**(那是销毁别人的工作,项目级 Never)。
|
||||
5. **收工仍走技能自身的代码路径,别自己手写 rm**:先把台账**备份成 `*.state-bak`**(后缀与 E 第 6 步统一),
|
||||
再让脚本自己摘联接:
|
||||
|
||||
```bash
|
||||
cp "$WTROOT/.state/<ID>.json" "$MAIN/ci-artifacts/<ID>.json.state-bak" # 先备份再动
|
||||
"$PY" "$TW" done <ID> # 打「已合入 <base>,跳过合并,直接清场」→ 先摘 .venv 与 simulator/SDL2 联接 → 删工作树 → 删分支 → unlink 台账
|
||||
```
|
||||
|
||||
实测这条路径能走完(日志:`state backup -> ci-artifacts\screenlink-doc-history.json.state-bak` +
|
||||
`[ok] 已摘除联接 .venv` + `[ok] 已摘除联接 simulator/SDL2`)。**若它仍拿「主工作树不干净」挡在合并前**(没走到
|
||||
清场分支),改走 **E 节**的手工回收路径:台账仍按本步先 `cp` 成 `.state-bak`;第 1~2 步的前置换成
|
||||
「`status --porcelain -uall` 为空 + 唯一提交为空 + `merge-base --is-ancestor` 判 MERGED」三条(本次实测三条齐),
|
||||
第 3 步的联接预扫 + `rmdir` 摘联接**一步不能省**;再 `git worktree remove`(三条前置齐了就不必 `--force`)、
|
||||
删分支、`git worktree prune`。清完再敲一次 `done` 会得到「任务 <ID> 不存在(缺台账 …json)」(exit 2)——
|
||||
**这不是失败**(台账已被 unlink;判完成以 `git log` 为准,见 C 第 7 步)。
|
||||
6. **复验收工**:`git -C "$MAIN" worktree list` 只剩主树 + **别人的树**(那棵不是你的,保留并在报告里点名,
|
||||
别顺手回收 → E 节);`"$PY" "$TW" lock status` 打 `build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)` 才收工。
|
||||
|
||||
**并发会话证据必须写进报告**:主树里那份外部脏文件、中途冒出来的第二棵任务树,都要点名——是谁的、
|
||||
你怎么定性的、对这次合并的影响(本例:与本合并无交集 / 不丢弃 / 不进本次提交),以及你**没动它**的证据
|
||||
(blob id 前后一致)。「不能丢弃别人的工作」类问题九成是并发会话造成的,不写清就等于没取证。
|
||||
@@ -1,134 +0,0 @@
|
||||
## 陷阱(实测)
|
||||
|
||||
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
|
||||
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
|
||||
--force --bench-pass` 报的是同一句话,锁照旧没放)。同理 `abort --force` 也只覆盖「树里有东西」这一条拒绝,
|
||||
**不会**替你做 E 节的内容保全——它只会删得更快。
|
||||
- **未跟踪文件在 `git log`、提交门禁与 `done` 的合并里都不留痕**(2026-10-29 回收现场):钩子按 HEAD tree 的
|
||||
diff-index 取文件,未跟踪文件从没进过任何钩子的视野;`git log` 也照不出「唯一未合并」——它压根没被提交过。
|
||||
所以「这棵树没有遗留改动」这种结论**只能**由 E 第 2 步的逐文件内容比对给出,`log --oneline -1` + 脏计数都不够。
|
||||
- **`git status --porcelain` 不带 `-uall` 会折叠未跟踪目录**(E 第 1 步):「全景」少一项、分桶少一个文件,
|
||||
最后拆掉的是别人唯一的草稿。回收路径上**一律 `-uall`**。
|
||||
- **`$TW list` 空 ≠ 没有工作树**:`list` 只遍历台账(A 第 1 步),用户说的「那 3 棵 done 掉」很可能在 `$TW list` 里
|
||||
一个都不存在(实测就是)→ 别照 `$TW list` 的空结果回「没有任务」,先 `git worktree list` 拿全集再逐棵定性(E 节)。
|
||||
2026-10-29 起 `list` 还会另起一段列出插件树(`.dsh-worktrees/`,F 节)——**"看见"不等于"归 `done` 管"**。
|
||||
- **「这棵树很久没动」不是回收判据**:实测 `bench-*` 两棵既没台账也没脏文件、mtime 很旧,但真正决定去留的是
|
||||
`git merge-base --is-ancestor <c> HEAD` = MERGED(提交已进主线)+ 未跟踪层为空;而另一棵 mtime 更新的树反而
|
||||
全是已在主线的副本。**判据是内容与祖先关系,不是时间戳。**
|
||||
- **「我记得没建过联接」不是安全判据**:判据只有扫出来的 `REPARSE: none`(E 第 3 步实测 `bench-*` 各挂一个 `.venv`
|
||||
联接,正是「不是自己建的」那类)。
|
||||
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
|
||||
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
|
||||
不需要管理员权限),且联接必须建在版本控制之外。
|
||||
实测(2026-10-12):用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)`
|
||||
会被系统拒绝(Win32 4392),`mklink /J` 正常——所以脚本走后者,参数以列表交给
|
||||
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
|
||||
但**读属性**用 ctypes 没问题:`GetFileAttributesW` + `FILE_ATTRIBUTE_REPARSE_POINT` 就是 E 第 3 步的预扫手段。
|
||||
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
|
||||
但该任务里跑不了 `.venv\Scripts\python.exe`,`/keil-build` / `ci_local.py` 会找不到解释器。
|
||||
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
|
||||
- **⚠ 任务树里 `check_doc_links.py --all` 的红多半是 `simulator/SDL2` 目录联接的假红**:全库扫描会吞下那份
|
||||
「仓库里的第二份仓库副本」(实测任务树 222 个文档 / 35 处问题全在联接下、RC=1,而主树同命令基线全绿)。
|
||||
判红必须先过滤 `simulator/SDL2` 下的问题行、再与主树**同一条命令**的基线比对;正解是树内按单文件跑、
|
||||
`--all` 基线合并后在主树跑 → **G 节**。
|
||||
- **新建的任务树里没有 `ci-artifacts/`**(被 `.gitignore` 忽略 → 不被检出):`> ci-artifacts/x.txt` 与
|
||||
`fs.writeFileSync("<tree>/ci-artifacts/x", …)` 都会 `ENOENT`;`cd` 进树后再用 `../_wt/x.txt` 落到 `_wt/_wt/` →
|
||||
`No such file or directory`(两起实测,第二次直接废掉一整轮体检)。落盘前先 `mkdir -p ci-artifacts`,
|
||||
树内输出一律用树内相对路径 → **G 节**。
|
||||
- **`done` 报「主工作树不干净」而那份脏改动不是你的**(别的并发会话 / 插件留下的,实测两次都是
|
||||
`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):**不许用 stash / 提交 / 删除去清场**(项目级 Never,
|
||||
你也无从判断它写没写完)→ 走 **C2**:取证(mtime + diff + `hash-object` 存进 ci-artifacts)→ `merge-tree` 预演
|
||||
→ 主树 `merge --no-ff -F` → 合并前后 blob id 与 `diff --stat` 对账。**「它看着像废弃的」不是丢弃判据**。
|
||||
- **⚠ `done` 的门禁跑在 `/keil-build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
|
||||
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `/keil-build`」,而
|
||||
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||||
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
|
||||
误报 `[FAIL] 台账受管区与 .map 不一致`。`/keil-build` 跑完后 `.map` 才是新的。
|
||||
**处置(2026-10-25 补全)**:按 C 节第 5 步的完整链补——`check_mem_numbers.py --write` →
|
||||
`ram_baseline.py --write` → `ram_baseline.py --update` → `check_test_coverage.py --update-baseline` →
|
||||
`check_doc_links.py --all` → 提交 → 重跑 `done`(它会检测到「已合入」直接清场)。
|
||||
此时主干已含合并提交,**绝不能用提示里的 `reset --hard` 回退**。
|
||||
- **`done` 的门禁 = `ci_local.py` = `pre-commit run --all-files`(50 条钩子全跑)**(2026-10-26):红了先读
|
||||
`[SUMMARY] static: pre-commit hooks -- passed=N failed=M skipped=K`,**按钩子 id 定根因**,别一律当「`.map` 滞后」
|
||||
去跑补基线链。典型例外:`generated-fresh`(生成器幂等性,触发面 `tools/**`、`docs/images/*.svg`、UI 生成文件)——
|
||||
合进来的分支改了生成器却没重跑产物时,补基线链救不了,处置是在**主工作树重跑那个生成器**、脚本与产物同一笔提交,
|
||||
再重跑 `done`。另注意 `pre-commit run --all-files` 只跑 pre-commit stage,个别只在 commit-msg / 手工阶段跑的守卫不在此列
|
||||
(口径见 `scripts/ci_local.py` 头部说明)。
|
||||
- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的
|
||||
构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py`
|
||||
的体积门禁步都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。
|
||||
同理 `done` 的重生成步骤会 `[skip] 未找到 .map,跳过内存快照同步(需先 /keil-build)`。
|
||||
**处置**:新空间第一次提交前先 `/keil-build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
|
||||
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
|
||||
- **⚠ 只改注释里的文档路径,也会把「体积同码凭证」刷红**(2026-10-30 实测,`done testdoc`):
|
||||
`scripts/check_size_gate.py` 的判据是**索引 blob 指纹**(实测 1466 个文件),**不是体积数字**——
|
||||
那笔全库路径同步顺带改了 20 个 `.c/.h` 的注释,`Program Size` 逐项没变
|
||||
(Code=246160 / RO-data=29816 / RW-data=2036 / ZI-data=149700),凭证照旧红:
|
||||
`本次代码(指纹 172c6c78b8fb…,1466 文件)与体积测量(指纹 1ed4267c77f6…,2026-10-30T01:15:01)不同码`。
|
||||
**处置比上一条那条补绿链短**:合并后 `done` 自己那次 `/keil-build` 已 0 错 0 警,此时只需
|
||||
`ram_baseline.py --update` 重写凭证(改的是 `scripts/size_attestation.json`;同日同数字时
|
||||
`ram_baseline.tsv` 连趋势行都不会新增),**别顺手 `--write` 去动内存受管快照**——数字没变就改受管区,
|
||||
等于给别的在飞分支造冲突面(台账归属口径见 `mem-budget` skill)。
|
||||
**判据顺序**:先比 `Program Size` 与合并前是否逐项相同 → 相同就只补凭证;不同才走 C 节第 5 步全链。
|
||||
连带的第二个坑:`done` 对「分支已合入」的路径**在门禁之前就 return**(`cmd_done` 里
|
||||
`already and ahead == 0` 那条分支直接清场),所以补完凭证重跑 `done` **不会替你复绿**——
|
||||
落主干后的全量证据得自己跑:`scripts/ci_local.py`(实测 47 runner / 3896 断言全绿)
|
||||
+ 主树 `check_doc_links.py --all`(实测 215 个文档 / 2993 处章节引用 rc=0)。
|
||||
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
|
||||
`/keil-build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
|
||||
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
|
||||
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
|
||||
- **`ram_baseline.py` 的 help 串里有未转义的 `%`**(2026-10-25 实测:某些直调姿势会渲染 argparse help 并崩)——
|
||||
查用法读脚本头部 docstring,别靠 `--help`;修脚本本身属 `mem-budget` skill 的范围。
|
||||
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
|
||||
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
|
||||
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
|
||||
- **并集合并残留**:双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,
|
||||
可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 +
|
||||
连续重复行;**权威判据是 base / 两个 tip 三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
|
||||
别用 `tail -N` 目测。
|
||||
- **⚠ 别用 heredoc 打补丁改门禁脚本造探针**(2026-10-27 实测三连败,白烧一整轮):heredoc 里的 python
|
||||
把门禁脚本改坏——`patched 3` 之后 `PROBE_RC=1`、`SyntaxError: unterminated string literal (detected at
|
||||
line 882)`,而它**正是被测对象**,改完的数不可信;另一处 `replace('\', '/')`(想把 Windows 反斜杠换成正斜杠)里的反斜杠被多层引号吞掉
|
||||
→ `SyntaxError (detected at line 16)`。**正解**:探针落成**独立 `.py` 文件**(写文件的工具,UTF-8 无 BOM)
|
||||
再 `"$PY" probe.py`;非用 heredoc 不可就用单引号定界 `<<'PY'`(防变量展开),且**路径一律 `as_posix()`、
|
||||
一个反斜杠都不写**——反斜杠字面量要穿「JS 模板串 → bash → shell 引号 → python」四层,必被吞。
|
||||
E 节那两段扫描脚本同理:**落成 `ci-artifacts/*.py` 再跑**(该目录已被忽略,不会污染第 5 步复验用的脏行数)。
|
||||
- **取门禁数字:import 门禁模块,别猜输出、别打补丁**(2026-10-27;grep 源码 / 手抄正则 / 解析 stdout 三种姿势
|
||||
都拿不到可复核的数,换成这句立刻拿到):先读门禁脚本的 `main()`(直读末尾那几十行)看它怎么装配检查器,
|
||||
把**那几行照抄**进探针——`sys.path.insert(0, 'scripts')` → `import check_ids as ci` →
|
||||
`ci.load_json(ci.REGISTRY_FILE, 'reg')` + `ci.load_json(ci.ALIASES_FILE, 'alias')` →
|
||||
`ck = ci.Checker(reg, al, verbose=False)` → `ck.run()` → `print(ck.stats)`。别猜键名:先
|
||||
`print(sorted(ck.stats))` 列出全量计数项,再按精确键取数(顺带 `git diff` 确认门禁脚本自己**没被改过**)。
|
||||
- **别把「不可判定」当成一个数**(2026-10-27):`id-check` 有两种口径——检查器自己算的**硬不可判定**实测只有
|
||||
**2** 处,而门禁红线(`scripts/archive/id_gate/id_registry.json` 的 `undecidable_total_ceiling`)量的是**派生值**
|
||||
`refs_total - decidable`(实测 8,744 - 7,582 = **1,162**,拆开 = `skip_bare_unmanaged` 834 +
|
||||
`noise_allowed` 326 + 硬不可判定 2,**三数精确闭合**)。所以**带属主登记处的族(TC-XXX 这类)加多少引用都算
|
||||
「可判定」、一点不吃这条余量**,真正吃余量的是**裸形字母+数字**字面量;反过来「把可判定引用改写成散文」才是
|
||||
**稀释比值**的动作,那才是绝对数红线要防的。口径混用会当场给整条道下错指令(本次就下错过、量完立刻撤回)。
|
||||
- **队友的自查结论要复核,小心它测的是错变量**(2026-10-27):log 道做过受控实验、结论是「我们没推高这个数」——
|
||||
他们改的是 TC 号、盯的是 `skip_bare_unmanaged`,那个组合当然不动;而 +16 走的是另一条路(新增裸形 「字母 T 累进数字 4」)。
|
||||
定责用 D 节:逐文件注入 + HEAD/在飞两版同一计数器相减 + 对新增行 grep;闭合后 +16 = 16 处裸形 「字母 T 累进数字 4」,
|
||||
**推翻**了自查结论。口头自查与 `--note` 都不是证据,**可复核的差分数字才是**。
|
||||
- **`.workbuddy-ai/` 刻意不共享**:记忆不随任务分叉,也不给联接递归删除的机会。
|
||||
任务工作树自己的记忆由 `done` / `abort` 在清场前归并回主工作树。⚠ **手工回收(E 节)没有这一步归并**:
|
||||
拆之前顺手 `ls <tree>/.workbuddy-ai/` 看一眼,有内容就一并备份进 `ci-artifacts/wt-leftover-backup/`。
|
||||
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
|
||||
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
|
||||
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
|
||||
所以判「陈旧」只看**时效**(该锁的 TTL)加显式 `--force`;pid 仅作排障记录。
|
||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||
- **TTL 按锁分设**(2026-10-12):`build` = **1h**、`bench` = **6h**(`LOCK_TTL_HOURS`)。
|
||||
口径是「兜底窗口应与操作真实时长同量级」——编译是秒~分钟级,6h 的兜底太钝;
|
||||
bench 是会话级(flash→`/serial-boot-test`→探针),才配得上小时级。持有者还可用
|
||||
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
|
||||
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
|
||||
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
|
||||
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
|
||||
- **`build` 锁不必手工持**(2026-10-12 起):`/keil-build`(`keil_build.py`)**自持自释**——
|
||||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
|
||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`/serial-boot-test`→探针,见 `keil-build` SKILL.md)。
|
||||
- **⚠ `done --dry-run` 只保护预演段、不保护清场段**(2026-10-29 实测):`done <ID> --dry-run --skip-build --skip-gates` 在「分支已合入主干」这条路上**照样执行清场**——摘 `.venv` / `simulator/SDL2` 联接、删分支、清台账,然后打「完成(分支此前已合入)」。想真·零改动就只跑不带 `--skip-gates` 的 `--dry-run` 并停在预演输出;用完一律 `git worktree list` 复验(判完成以 `git log` 为准,不是以有没有再敲一次 `done` 为准)。另:`--dry-run` 的前置检查同样要抢 `build` 锁——被别的任务占着时它会直接报「独占资源 build 仍被 X 占用,先 release」,此时加 `--skip-build` 或先 release,**别为了预演去抢别人的锁**。
|
||||
- **⚠ 文件工具「先读后改」按解析后的绝对路径记账,切树后要重新读**(2026-10-30 实测,屏板链路任务树):开新树后先用**相对路径**读过一批文件(读历史记到当时解析到的主树文件上),再进任务树直接 `edit` → 6 条并行编辑全被 `file has not been read — read the file, then retry` 拒。判据:**树内改动一律用树内绝对路径,先 read 再改**——读历史不跨路径复用,工具不做「同名相对路径」宽容。
|
||||
- **⚠ 中文门禁输出回显乱码 ≠ 路径坏**(2026-10-30 实测):对含中文文件名的文档跑 `check_doc_links.py`,终端回显整段乱码,但 **rc 与判定计数全部正确**。只认 `[SUMMARY]` 行与退出码,别误诊成「文档丢失」;脚本化批量调用走 python subprocess + 显式 `encoding`,别经命令行中转中文名。
|
||||
@@ -1,203 +0,0 @@
|
||||
### E. 回收**非登记**工作树(`$TW list` 为空 / detached HEAD / 树里有在飞或未跟踪内容)
|
||||
|
||||
**这类树的判定特征**(命中任意一条就适用本节):`git worktree list` 里有它、但 `_wt/.state/<ID>.json` 台账不存在;
|
||||
HEAD 是 detached(`git rev-parse --abbrev-ref HEAD` 打 `HEAD`);或树里的改动不属于本会话(别的会话留下的未提交 /
|
||||
未跟踪文件)。典型来源:D 节的 `--detach` 净室、`new` 之后被手工删目录只剩台账的残骸、别人手工 `git worktree add` 建的分析树
|
||||
(2026-10-28 实测一次回收 3 棵:`verify-task16` / `verify-batch` / `und-attr`,各 89 MB,全部无台账 + detached)。
|
||||
|
||||
**为什么 `done` / `abort` 在这里整体不适用**(铁律 9):两条子命令的前置都是 `load_state()`,无台账 →
|
||||
`前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)当场停住、什么也不会发生;而**假如有**一份残留台账,
|
||||
`abort --force` 会拿台账的 `path` 直接 `git worktree remove` —— **绕过下面第 1~4 步的全部内容保全**,把别的会话的唯一版本一次删光;
|
||||
`done` 则会去找那条(多半已不存在的)`task/` 分支做合并。另:本路径没有「合并」这一步,所以 `done` 的并集合并残留审计
|
||||
在这里不发生,也没有三方对比可依据 —— **唯一性只由第 2 步的内容分桶给出**。回收只能手工走本节第 1~5 步 + 第 6 步清台账。
|
||||
|
||||
1. **建全景(全程只读)**:先看 `git worktree list` 的全集,再**逐棵**记三件事(脏行数含未跟踪 / 树顶提交 / 有无分支)
|
||||
加一条台账判定,同时存下主树的拆前基线清单:
|
||||
|
||||
```bash
|
||||
MAIN="D:/Code/DTU Code/STM32F4-Base"; WTROOT="$MAIN/../_wt"
|
||||
git -c core.quotePath=false worktree list # 全集:登记树 + 非登记树都在这份里
|
||||
MAIN_SHA=$(git -C "$MAIN" rev-parse --short HEAD) # 下面所有比对都以这一版为主线尺子
|
||||
mkdir -p ci-artifacts
|
||||
git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-before.txt # 拆前主树基线(第 5 步逐行比)
|
||||
wc -l < ci-artifacts/wt-main-before.txt # 记下行数(实测 0)
|
||||
for t in "$WTROOT"/*/; do
|
||||
echo "--- $t"
|
||||
git -c core.quotePath=false -C "$t" status --porcelain -uall | wc -l # 脏 + 未跟踪行数
|
||||
git -c core.quotePath=false -C "$t" log --oneline -1 # 树顶提交
|
||||
git -C "$t" rev-parse --abbrev-ref HEAD # 打 HEAD = detached,压根没分支可 done
|
||||
ls "$WTROOT/.state/$(basename "$t").json" 2>/dev/null || echo "NO-LEDGER" # 台账位置随 DTU_WT_ROOT
|
||||
done
|
||||
```
|
||||
|
||||
- **`-uall` 不是可选**:默认 `status --porcelain` 会把未跟踪目录折叠成一行 `dir/`,`-uall` 才展开到文件级;少了它
|
||||
第 2 步的分桶就漏项(实测 `und-attr` 唯一那份就是未跟踪 `?? scripts/check_nested_comment.py`)。
|
||||
- 路径含空格(本项目 `DTU Code`)→ 变量整体加引号;`core.quotePath=false` 让中文路径原样输出,便于逐行核对。
|
||||
- 判定「是不是登记树」看两样:**台账在不在 + HEAD 是否 detached**。两者都指「非登记」→ 从这里开始**禁止**
|
||||
`done` / `abort`(见本节开头),继续第 2 步。
|
||||
2. **逐文件与主线 HEAD 比内容并分三桶**:`IN-MAINLINE`(内容等价,拆 = 零损失)/ `UNIQUE-UNMERGED`(主线没有这份
|
||||
内容,拆 = 丢)/ `OLDER-DRAFT`(同路径主线已有更新版本)。落成独立只读探针跑(别用 heredoc 现写现改,
|
||||
见陷阱「别用 heredoc 打补丁造探针」):
|
||||
|
||||
```python
|
||||
# ci-artifacts/wt_bucket_scan.py — 只读:逐文件与主线工作区/HEAD 比对,三桶归类
|
||||
import os, subprocess, tempfile
|
||||
MAIN = r"D:\Code\DTU Code\STM32F4-Base"
|
||||
WTROOT = os.path.join(os.path.dirname(MAIN), "_wt")
|
||||
TREES = ["verify-task16", "verify-batch", "und-attr"] # ← 第 1 步全景里挑出的非登记树
|
||||
SKIP_TOP = {".venv", "MDK-ARM", ".git", "ci-artifacts"} # 联接 / 构建产物 / 忽略区,按项目调
|
||||
def git(*a):
|
||||
return subprocess.run(("git", "-c", "core.quotePath=false", "-C", MAIN) + a,
|
||||
capture_output=True, text=True, errors="replace")
|
||||
def bucket(rel, tree):
|
||||
p = os.path.join(tree, rel)
|
||||
if not os.path.exists(p):
|
||||
return "OTHER", "工作树里已不存在(多为已 staged 删除)"
|
||||
tf = tempfile.NamedTemporaryFile(delete=False)
|
||||
tf.write(open(p, "rb").read()); tf.close()
|
||||
# 1) 与主线工作区(= 当前 HEAD 的检出内容)逐字节比
|
||||
if git("diff", "--no-index", "--quiet", tf.name, rel).returncode == 0:
|
||||
os.unlink(tf.name); return "IN-MAINLINE", "与主线 HEAD 逐字节相同"
|
||||
# 2) 与主线历史各版比:内容等价于某个旧 commit → 只是主线又往前走了
|
||||
hist = [x.strip() for x in git("log", "--format=%H", "HEAD", "--", rel).stdout.splitlines()]
|
||||
for sha in hist[1:]:
|
||||
old = git("show", f"{sha}:{rel}")
|
||||
if old.returncode == 0 and old.stdout == open(p, encoding="utf-8", errors="replace").read():
|
||||
os.unlink(tf.name); return "IN-MAINLINE", f"内容等价于主线历史版 {sha[:8]}"
|
||||
# 3) 主线从没这条路径 → 唯一;否则同路径主线有更新版 → 更旧草稿(再用 blob diff 定性方向)
|
||||
os.unlink(tf.name)
|
||||
if not hist:
|
||||
return "UNIQUE-UNMERGED", "主线 HEAD 与历史都没有此路径"
|
||||
return "OLDER-DRAFT", "同路径主线有更新版本(用 diff --stat <HEAD> -- <f> 定性)"
|
||||
for name in TREES:
|
||||
tree = os.path.join(WTROOT, name)
|
||||
lines = subprocess.run(("git", "-c", "core.quotePath=false", "-C", tree,
|
||||
"status", "--porcelain", "-uall"),
|
||||
capture_output=True, text=True, errors="replace").stdout.splitlines()
|
||||
tally = {}
|
||||
for line in lines:
|
||||
rel = line[3:].split(" -> ")[-1].strip('"') # 兼容 ?? / M / A / R 的箭头形态
|
||||
if rel.split("/")[0] in SKIP_TOP:
|
||||
continue
|
||||
b, why = bucket(rel, tree)
|
||||
tally.setdefault(b, []).append((rel, why))
|
||||
g = lambda k: len(tally.get(k, []))
|
||||
print(f"== {name}: IN-MAINLINE={g('IN-MAINLINE')} UNIQUE-UNMERGED={g('UNIQUE-UNMERGED')} "
|
||||
f"OLDER-DRAFT={g('OLDER-DRAFT')} OTHER={g('OTHER')}")
|
||||
for b in ("UNIQUE-UNMERGED", "OLDER-DRAFT", "OTHER"):
|
||||
for rel, why in tally.get(b, []):
|
||||
print(f" {b}: {rel} # {why}")
|
||||
```
|
||||
|
||||
报告里必须**逐桶给计数 + 判定证据**(实测:`verify-task16` IN-MAINLINE=6 / UNIQUE=0;`verify-batch` IN-MAINLINE=35 /
|
||||
UNIQUE=1;`und-attr` IN-MAINLINE=0 / UNIQUE=1)。两个非空桶再各钉一条命令把证据坐实:
|
||||
|
||||
```bash
|
||||
# 「唯一未合并」是真更新还是更旧草稿?blob 级 diff + 逐行看方向
|
||||
git -c core.quotePath=false -C "$MAIN" diff --stat "$MAIN_SHA" -- simulator/tests/README.md
|
||||
diff <(git -C "$MAIN" show "$MAIN_SHA":simulator/tests/README.md) "$WTROOT/verify-batch/simulator/tests/README.md"
|
||||
# 「树顶提交要不要保」只看一件事:是否已并入 HEAD。别拿「目录很久没动」当判据
|
||||
for c in 77779d49 1d4b6a17; do
|
||||
git -C "$MAIN" merge-base --is-ancestor "$c" HEAD && echo "$c MERGED" || echo "$c NOT-merged"
|
||||
done
|
||||
```
|
||||
|
||||
实测:那份 `simulator/tests/README.md` 树内写「27 页 × 3200 B」、主线 HEAD 已是「60 张 × 3200 B」→ **更旧草稿**(被取代的
|
||||
早先版本),不是未合并成果;`bench-21-77779d49` / `bench-23-1d4b6a17` 两棵 `merge-base --is-ancestor` 判 **MERGED** 且脏数 = 0
|
||||
→ **保留不动**(不属本节,等属主自己收,报告里点名说清为什么留)。
|
||||
**回收判据不是「看着旧」**:唯一内容如果没被提交过,`git log` 里根本看不见它;反过来「提交已 MERGED」也不等于可以拆——
|
||||
还得看未跟踪那一层。`UNIQUE-UNMERGED` 非空又确实要留 → 它进第 4 步的备份,**不许靠 `--force` 顺手带走**。
|
||||
3. **拆前扫联接 / reparse point(物理安全性 = 铁律 5 的可执行判据)**。存成 `ci-artifacts/wt_junction_scan.py` 再跑:
|
||||
|
||||
```python
|
||||
# 只读预扫:一个路径是不是 reparse point(Windows 目录联接在这里现形);发现即停止深入,绝不 walk 进联接
|
||||
import os, ctypes
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
||||
GA = ctypes.windll.kernel32.GetFileAttributesW
|
||||
def is_reparse(p):
|
||||
a = GA(p)
|
||||
return a != -1 and bool(a & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
for name in ["verify-task16", "verify-batch", "und-attr", "bench-21-77779d49", "bench-23-1d4b6a17"]:
|
||||
top = os.path.join(r"D:\Code\DTU Code\_wt", name)
|
||||
hits, entries = [], 0
|
||||
for root, ds, fs in os.walk(top):
|
||||
bad = [d for d in ds if is_reparse(os.path.join(root, d))]
|
||||
if bad:
|
||||
hits += [os.path.join(root, d) + "/" for d in bad]
|
||||
ds[:] = [] # 有联接就不再深入:防转进主树 .venv 把别人整棵树数一遍
|
||||
hits += [os.path.join(root, f) for f in fs if is_reparse(os.path.join(root, f))]
|
||||
entries += len(ds) + len(fs)
|
||||
print(f"== {name} scanned entries={entries}")
|
||||
print(" REPARSE: none → 可直接 git worktree remove --force"
|
||||
if not hits else " REPARSE:\n " + "
|
||||
".join(hits))
|
||||
```
|
||||
|
||||
实测输出 `== verify-task16 ... REPARSE: none`(每棵的体积 / 条目数 + 零联接就是证据),而 `bench-*` 两棵各挂一个 `.venv`
|
||||
联接 → 判 **KEEP**(本节不拆它们;将来真要拆,先 `rmdir` 摘掉联接再 remove)。
|
||||
**判据是「扫出来一个都没有」,不是「我记得没建过联接」**:`new --no-bootstrap` 的树、别的手工 `mklink /J` 的树都可能带着指向主树的联接。
|
||||
摘联接用 `rmdir`(只删联接本身;**不是** `rd /s`,更不是 `rm -rf`——那会顺着联接删目标)。
|
||||
非 Windows 环境改用 `os.path.islink` / `find <tree> -type l`(本项目是 Windows + Keil 工具链,按上面这条走)。
|
||||
**到这里才算「拆前唯一性自检 + 物理安全预扫」齐**:`--force` 的安全前提 = 第 2 步 `UNIQUE-UNMERGED` 为 0
|
||||
(或剩下的已全部进第 4 步备份)**且**这里 `REPARSE: none`。
|
||||
4. **唯一内容先备份,再 `--force` 拆**。`UNIQUE-UNMERGED` 与需要留档的 `OLDER-DRAFT` 一律先备份进主树
|
||||
`ci-artifacts/wt-leftover-backup/`(`ci-artifacts/` 已被 `.gitignore` 忽略 → **备份不会改变主树脏行数**;仓库根不放一次性产物),
|
||||
**文件名 + 字节数必须打进输出**当证据:
|
||||
|
||||
```bash
|
||||
BK="$MAIN/ci-artifacts/wt-leftover-backup"; mkdir -p "$BK"
|
||||
# 命名约定:<树名>__<原路径里的 / 换成 _>,反向可定位;台账备份统一后缀 .state-bak
|
||||
cp "$WTROOT/verify-batch/simulator/tests/README.md" "$BK/verify-batch__simulator_tests_README.md"
|
||||
cp "$WTROOT/und-attr/scripts/check_nested_comment.py" "$BK/und-attr__scripts_check_nested_comment.py"
|
||||
ls -l "$BK" # 实测:verify-batch__simulator_tests_README.md 1769 bytes / und-attr__scripts_check_nested_comment.py 14827 bytes
|
||||
for t in verify-task16 verify-batch und-attr; do
|
||||
git -C "$MAIN" worktree remove --force "$WTROOT/$t"; echo "$t remove_rc=$?" # 不接管道才读得到码(见「读输出」)
|
||||
done
|
||||
git -C "$MAIN" worktree prune # 顺带清 .git/worktrees 里的残籍
|
||||
```
|
||||
|
||||
自检:**备份条目数 = 第 2 步里判「要留档」的条目数**,且每个都带字节数。这一步就是本节相对 D 第 4 步的增量——
|
||||
D 的前提是「净室里只有自己的拷贝」,本节的树里混着别人在飞的东西,`--force` 是一次不可逆删除,**先备份才谈得上安全**。
|
||||
5. **复验:主树一个字节没动 + 树确实没了**:
|
||||
|
||||
```bash
|
||||
git -C "$MAIN" status --porcelain -uall > ci-artifacts/wt-main-after.txt
|
||||
wc -l < ci-artifacts/wt-main-before.txt; wc -l < ci-artifacts/wt-main-after.txt # 两个数必须相同(实测 0 → 0)
|
||||
diff ci-artifacts/wt-main-before.txt ci-artifacts/wt-main-after.txt # 空输出 = 逐行一致
|
||||
git -c core.quotePath=false -C "$MAIN" worktree list # 已拆路径逐条消失;主树与判「保留」的树原样在列
|
||||
```
|
||||
|
||||
行数不同 = 备份或摘除碰到了主线(多半是备份写进了未被忽略的目录,或漏摘联接)→ 立刻定位,别报「回收完成」。
|
||||
第 2 步判「保留不动」的树还在 `worktree list` 里是**预期结果**,在报告里点名解释为什么留(附 `MERGED` 判据)。
|
||||
6. **顺手清悬挂台账**(branch / path 都已不存在的 `_wt/.state/<ID>.json`;它会让 `$TW list` 打出一行怪条目、并让下次
|
||||
`new` 同 ID 报「任务 <ID> 已存在」)。先给证据、后动手:
|
||||
|
||||
```bash
|
||||
for j in "$WTROOT"/.state/*.json; do # 台账位置随 DTU_WT_ROOT
|
||||
id=$(basename "$j" .json)
|
||||
br=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('branch',''))" "$j")
|
||||
pp=$("$PY" -c "import json,sys;print(json.load(open(sys.argv[1],encoding='utf-8')).get('path',''))" "$j")
|
||||
git -C "$MAIN" rev-parse --verify "$br" >/dev/null 2>&1 && b=branch-OK || b=branch-MISSING
|
||||
[ -d "$pp" ] && p=path-OK || p=path-MISSING
|
||||
echo "$id $b $p <- $j"
|
||||
done
|
||||
```
|
||||
|
||||
实测这条判据打出的正是 `v5tool2 branch-MISSING path-MISSING`:`git rev-parse task/v5tool2-v11` 报
|
||||
`fatal: ambiguous argument 'task/v5tool2-v11': unknown revision ...`,台账 `path` 指的目录已无,`git worktree list` 里也没有这棵树。
|
||||
**三条齐(rev-parse fatal + path 目录不存在 + worktree list 无此树)才叫悬挂**;缺任何一条都回到第 1~2 步查是不是真有东西要保。
|
||||
然后**备份 → 删 → 复跑两条 list 复验**:
|
||||
|
||||
```bash
|
||||
cp "$WTROOT/.state/v5tool2.json" "$BK/v5tool2.json.state-bak" && rm "$WTROOT/.state/v5tool2.json"
|
||||
"$PY" "$TW" list # 该条目消失(全空时打「(没有任务空间。用 new <ID> <slug> 建一个)」),echo RC=$? 应为 0
|
||||
"$PY" "$TW" lock status # build 空闲(TTL 1.0h)/ bench 空闲(TTL 6.0h)—— 证明清台账没牵连两把锁
|
||||
ls "$WTROOT"/.state/locks/ 2>/dev/null # 若该 ID 名下还有 .lock,先按 B 节释放,再删台账
|
||||
```
|
||||
|
||||
- **必须 `cp` 先备份再 `rm`**:台账里存着 base / junctions / needs_bench / bench_pass,是真出纠纷时唯一的现场记录。
|
||||
- 悬挂条目按定义 `path` 已不存在 → 通常**不占** `MAX_CONCURRENT = 8` 名额(A 第 1 步的「活跃」判据要求 path 目录还在),
|
||||
但它把 `$TW list` 搅成怪输出、并永久挡住同 ID 复用;**path 还在的占位/残留台账才占名额**(早期为绕 B 节那个坑手写的占位台账就是这种)。两种都要清。
|
||||
- **仍不对这个 ID 跑 `done` / `abort`**:清场会拿台账里的死 `path` / 死 `branch` 去做 `git worktree remove` 和删分支
|
||||
(这正是 A 第 5 步说的「手工删目录留下的尾巴」,正解就是本步)。历史遗留的占位台账同理——活着时也别
|
||||
`done` / `abort`,用完直接删文件(来历与现状见 B 末条)。
|
||||
@@ -1,44 +0,0 @@
|
||||
## 验证
|
||||
|
||||
- `$TW list`:台账段条目数 = `ls _wt` 的目录数且 ≤ 8(`MAX_CONCURRENT`);刚回收的任务与历史占位条目都不再出现
|
||||
(插件树另起一段,不参与这条计数)。
|
||||
- `$TW lock status`:`build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)`——没有本会话留下的持有者。
|
||||
- `git log --oneline -3`:能看到 `merge: task/<ID>-<slug> 合入 <base>` 与其后的补基线提交;
|
||||
`git status --porcelain` 为空(补链改动已提交,才谈得上重跑 `done`)。
|
||||
- `ls _wt/.state/`:该 ID 的台账已无(清场最后一步 unlink);重复 `done` 报「任务不存在」不必再追。
|
||||
- **E 节回收**:`git worktree list` 只剩主树 + 判「保留不动」的树(已拆路径逐条消失);**拆前/拆后主树
|
||||
`git status --porcelain -uall` 行数相同且 `diff` 为空**(实测 0 → 0);`ci-artifacts/wt-leftover-backup/` 里每个留档件都打印过
|
||||
文件名与字节数、条目数 = 分桶判「要留档」的条目数;每棵被拆的树都留有 `REPARSE: none` 预扫记录;全景输出里
|
||||
`UNIQUE-UNMERGED` 归零(或全部已备份);保留树在报告里点名给出判据(`merge-base --is-ancestor` = MERGED)。
|
||||
- **E 第 6 步清悬挂台账**:该 json 已从 `_wt/.state/` 消失(`.state-bak` 在备份目录里)、`$TW list` 不再打出它、
|
||||
`$TW lock status` 两把锁仍空闲;全程未对该 ID 跑 `done` / `abort`。
|
||||
- **F 节桥接**:`plugin list` 的「联接」列按实际三态点名(`已联` / `缺 .venv` / `缺 SDL2` /
|
||||
`齐(实体SDL2)`——旧实现只取样 `.venv`,半联树会说谎);**adopt/unadopt 前后主树 `.venv` 与树内文件都
|
||||
还在**;主线缺 SDL2 时 `adopt` 打 `[skip]` + 「还不能算就绪」而不打「现在可以跑」;`plugin adopt ".."`
|
||||
必须 exit 2;打印的 `pw:<name>` 与 `keil_build.py` 从 cwd 反推的名字逐字相同;`done <插件树名>` 只 exit 2。
|
||||
- **C2 主树被别的会话弄脏时的手工合并**:合并前打印过 `git merge-tree --write-tree` 的结果树 sha 且 `MT_RC=0`;
|
||||
合并用 `git merge --no-ff --no-edit -F ci-artifacts/merge-msg-<slug>.txt`(UTF-8 无 BOM)并写明绕开 `done` 的理由;
|
||||
外部文件合并前后的 `git hash-object` **逐字符相同**、`git diff --stat` 同一组数字、`status` 里仍是 `M`
|
||||
(没被提交也没被清掉;实测同为 `369380ca…`、2+/2−);台账先 `cp` 成 `<ID>.json.state-bak` 再由 `$TW done`
|
||||
(或 E 节手工路径)摘联接拆树;收工前 `git worktree list` 只剩主树 + 别人的在飞树(点名保留)、
|
||||
`$TW lock status` 两把锁均空闲(`build 空闲(TTL 1.0h)` / `bench 空闲(TTL 6.0h)`)。
|
||||
- **G 节树内门禁**:`<tree>/ci-artifacts/` 先 `mkdir -p` 再写、事后 `ls` 得到真实文件(`cat` 读得到内容);
|
||||
树内 `check_doc_links.py` 按**单文件**跑并打印「N 处 / 机器可判定百分比 / rc」(实测 161 处/98%/rc=0、16 处/88%/rc=0);
|
||||
全库 `--all` 只在合并后的**主树**跑(实测 2,597 处 / 194 个文档 / rc=0);若树内跑过 `--all` 且判红,报告里
|
||||
必须给得出「过滤 `simulator/SDL2` 后与主树同命令基线对比」这一步。
|
||||
- D 节归因:测量前后 `git -C "$MAIN" status --porcelain` **逐行一致**(主树全程只读);
|
||||
`git worktree list` 里净室路径已消失;逐文件增量之和 **等于**观察到的涨幅(实测 +6+8+2 = +16),且每处增量
|
||||
都指到具体字面量类别(裸形 「字母 T 累进数字 4」)与属主;取数走 `import check_ids` 读 `ck.stats`,且门禁脚本自身 `git diff` 为空。
|
||||
- 补绿链的每一条都自己 `echo RC=$?` 看到 0,最后以重跑 `done` 打出的**清场日志**(`已摘除联接` /
|
||||
`已删除分支` / `完成(分支此前已合入)`)为成功判据,而不是 `tail` 出来的最后一行。
|
||||
- **树内提交 / 合并信息(G 节)**:`git log -1 --pretty=%B | grep -c '<被清退主题关键词>'` 打出 **0**(标题 + 正文 +
|
||||
合并信息三处都查过);消息文件在 `ci-artifacts/`、UTF-8 无 BOM、CR=0、标题 ≤100、正文最长行 ≤80(实测 31/50、32/55);
|
||||
自审发现消息统计算错是在**未推送**状态下 `--amend -F` 修掉的,历史里没有「更正」提交。
|
||||
- **文档改动体检(G 节)**:提交前打印过 `lines/bytes/BOM/CR/U+FFFD/三连续空行/行尾空格/末行 LF` 与表格列数一致性,
|
||||
并区分「本次改动引入 = 0」与「文件里既有的坏行」(实测两处历史坏表格与本次无关,只在报告里点名)。
|
||||
- **F 节分级与自持理由**:判 `simulator/SDL2` 为硬需求要给出 `simulator/CMakeLists.txt:12`(非 CACHE)+ 派生路径行号
|
||||
(:34/:70/:71/:180/:207);判 `.venv` 为软需求要给出 `DTU_PRECOMMIT_PYTHON` 与 `ci_local.py` 五级解释器候选顺序;
|
||||
答「为什么不用插件」时报出插件包版本、文件与三个概念的 grep 命中数,并说明重叠只在 `git worktree add` 的开/收两个动作。
|
||||
- **改并行上限数字**:唯一事实源 = `task_worktree.py` 的 `MAX_CONCURRENT`(报错文案与 `new` 提示都 `.format()` 插值它,
|
||||
实测 `:764-766` / `:768-771`),其余全是引用同步;隔离小仓验证到「8 棵成功、第 9 棵不带 `--force` 被拒」,
|
||||
且没在真仓 `../_wt` 及其 `.state/` 留下验证树。
|
||||
@@ -27,12 +27,12 @@ for _stream in (sys.stdout, sys.stderr):
|
||||
EXIT_OK, EXIT_FAIL, EXIT_ENV, EXIT_USAGE = 0, 1, 2, 3
|
||||
|
||||
BRANCH_PREFIX = "task/"
|
||||
MAX_CONCURRENT = 8
|
||||
MAX_CONCURRENT = 3
|
||||
LOCK_NAMES = ("build", "bench")
|
||||
# 按锁分设陈旧窗口(小时)——兜底窗口应与「操作真实时长」同量级:
|
||||
# build:编译是秒~分钟级(keil_build.py 自持自释,持有窗口 ≈ UV4 运行时长,
|
||||
# 上限 = --timeout,默认 600s);崩溃泄漏时应在数十分钟内即可接管 → 1h。
|
||||
# bench:会话级(flash → /serial-boot-test → 探针,天然几十分钟~数小时)→ 保留 6h。
|
||||
# bench:会话级(flash → @test → 探针,天然几十分钟~数小时)→ 保留 6h。
|
||||
# 另:锁 payload 里持有者可用 `ttl` 字段自报窗口(见 lock_acquire(ttl=...)),
|
||||
# 优先于这里的默认值——这样 build 锁的窗口能自动跟随其 --timeout。
|
||||
LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}
|
||||
@@ -82,15 +82,6 @@ BASE_BRANCH = os.environ.get("DTU_TASK_BASE", "v2-on-ui")
|
||||
STATE_DIR = WT_ROOT / ".state"
|
||||
LOCKS_DIR = STATE_DIR / "locks"
|
||||
|
||||
# DSH 的 worktree 插件(包名 dsh-task-worktree;模型工具 worktree_create、人类命令
|
||||
# /worktree)把任务树建在**仓库内**这个命名空间下,与本 skill 的仓外 _wt/ 刻意分开。
|
||||
# .gitignore 里预置同名规则,正是为了让插件的 ensureIgnored() 见到就跳过、
|
||||
# 不在会话中途往这份受控文件里追加一行。
|
||||
PLUGIN_ROOT = REPO_ROOT / ".dsh-worktrees"
|
||||
PLUGIN_NAMESPACE = "worktree" # 插件侧 WORKTREE_NAMESPACE:name 即分支尾段
|
||||
PLUGIN_SUBDIR = PLUGIN_ROOT / PLUGIN_NAMESPACE
|
||||
PLUGIN_LOCK_PREFIX = "pw" # 插件树在锁 payload 里的持有者前缀
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 基础工具
|
||||
|
||||
@@ -399,232 +390,33 @@ def bootstrap(wt: Path):
|
||||
return made
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- DSH 插件工作树
|
||||
#
|
||||
# 插件建的树(.dsh-worktrees/worktree/<name>)缺三样东西,下面的命令逐条补。
|
||||
# 回收**不在这里**:插件树由「/worktree remove|prune」收,done/abort 也不该拿去处理它。
|
||||
# 1. 共享联接——没有 .venv 与 simulator 的 SDL2 SDK,插件树里「.venv\Scripts\python.exe」
|
||||
# 直接找不到解释器:/keil-build、ci_local.py、所有 scripts/check_*.py 全部起跑失败;
|
||||
# 2. 可见性——插件树不在台账里,「list」看不见它,8 个活跃名额也只数登记任务;
|
||||
# 3. 独占资源归属——build 锁由 keil_build.py 自持自释(按 cwd 反推持有者名,
|
||||
# 插件树里算成 pw:<name>),bench 锁仍要人占,占锁名得和「lock status」对得上。
|
||||
|
||||
|
||||
def plugin_lock_owner(name: str) -> str:
|
||||
"""插件树在锁 payload 里的持有者名:pw:<name>(多段名压平成 -)。
|
||||
|
||||
必须与 keil_build.py 的 default_lock_task() 对同一种树算出的名字一致——这样构建
|
||||
自持自释留下的归属,和手工「lock acquire bench --task pw:x」才是同一个可 grep 的标识。
|
||||
"""
|
||||
# 与 plugin_dir_for 同样容忍「worktree/」前缀,否则同一棵树会出现两个持有者名
|
||||
# (pw:refactor-logging vs pw:worktree-refactor-logging),lock status 就散了。
|
||||
segs = [s for s in name.replace(os.sep, "/").strip("/").split("/") if s]
|
||||
if segs and segs[0] == PLUGIN_NAMESPACE:
|
||||
segs = segs[1:]
|
||||
return "{}:{}".format(PLUGIN_LOCK_PREFIX, "-".join(segs) or "?")
|
||||
|
||||
|
||||
def plugin_dir_for(name: str) -> Path:
|
||||
"""插件 worktree 名 → 树目录;**只允许落在 .dsh-worktrees/worktree/ 里面**。
|
||||
|
||||
adopt/unadopt 会在解析出的目录上建/摘目录联接;不加这道闸,等于允许把主工作树或
|
||||
任意 _wt 目录当插件树动手(联接会遮蔽真实内容,事后很难看出来)。
|
||||
"""
|
||||
raw = (name or "").strip().replace(os.sep, "/").strip("/")
|
||||
if not raw:
|
||||
raise EnvError("缺插件 worktree 名(先跑「plugin list」看现有树)")
|
||||
if raw == PLUGIN_NAMESPACE or raw.startswith(PLUGIN_NAMESPACE + "/"):
|
||||
raw = raw[len(PLUGIN_NAMESPACE):].strip("/")
|
||||
p = (PLUGIN_SUBDIR / raw).resolve()
|
||||
base = PLUGIN_SUBDIR.resolve()
|
||||
if p == base or base not in p.parents:
|
||||
raise EnvError("拒绝 {}:只接受 {} 里面的树".format(p, PLUGIN_SUBDIR))
|
||||
return p
|
||||
|
||||
|
||||
def junction_states(wt: Path):
|
||||
"""每个共享联接的三态:link(目录联接)/ real(实体内容)/ absent(真缺)。
|
||||
|
||||
这是「联没联上」的**唯一**判定处——列表显示与 adopt 收尾都读它。早先列表只取样
|
||||
JUNCTION_SPECS 的第一项,于是「有 .venv、缺 simulator/SDL2」的半联树被报成已联,
|
||||
而 adopt 可以只成功一条(主线缺 SDL2 走 skip、联接失败走 warn)→ 说谎的正是这一列。
|
||||
"""
|
||||
st = {}
|
||||
for rel, _desc in JUNCTION_SPECS:
|
||||
link = wt / rel
|
||||
if is_reparse(link):
|
||||
st[rel] = "link"
|
||||
elif os.path.lexists(str(link)):
|
||||
st[rel] = "real"
|
||||
else:
|
||||
st[rel] = "absent"
|
||||
return st
|
||||
|
||||
|
||||
def junction_label(st) -> str:
|
||||
"""把三态压成一列短标签:缺什么都点名,实体内容也算「齐」但不算共享。"""
|
||||
tag = {rel: rel.rsplit("/", 1)[-1] for rel in st}
|
||||
absent = [tag[r] for r, s in st.items() if s == "absent"]
|
||||
real = [tag[r] for r, s in st.items() if s == "real"]
|
||||
if absent:
|
||||
return "缺 " + "+".join(absent)
|
||||
return "齐(实体" + "+".join(real) + ")" if real else "已联"
|
||||
|
||||
|
||||
def plugin_trees():
|
||||
""".dsh-worktrees/worktree/ 下的工作树(事实源 = git worktree list,不读插件 manifest)。
|
||||
|
||||
manifest.json 是插件私有格式(MANIFEST_VERSION 1,字段随版本变),而 git 的注册表
|
||||
同时覆盖「插件正常建的树」和「manifest 丢了 / 树被手工删」的残留——桥接命令要看的
|
||||
是真实存在的东西。
|
||||
"""
|
||||
try:
|
||||
base = PLUGIN_SUBDIR.resolve()
|
||||
except OSError:
|
||||
return []
|
||||
trees = []
|
||||
for ent in worktree_entries():
|
||||
p = ent["path"]
|
||||
try:
|
||||
rp = p.resolve()
|
||||
except OSError:
|
||||
continue
|
||||
if rp == base or base not in rp.parents:
|
||||
continue
|
||||
dirty = (len(git_out("status", "--porcelain", cwd=p, check=False).splitlines())
|
||||
if p.is_dir() else -1)
|
||||
trees.append({
|
||||
"name": rp.relative_to(base).as_posix(),
|
||||
"path": str(p),
|
||||
"branch": ent.get("branch") or "(detached)",
|
||||
"dirty": dirty,
|
||||
"jn": junction_states(p),
|
||||
})
|
||||
return trees
|
||||
|
||||
|
||||
def print_plugin_trees() -> int:
|
||||
"""打印插件树一览(无树则静默),返回数量——「list」与「new」共用同一事实。"""
|
||||
trees = plugin_trees()
|
||||
if not trees:
|
||||
return 0
|
||||
print()
|
||||
print("DSH 插件工作树({} 下;**不在本 skill 台账** → 不占 {} 个名额、done/abort "
|
||||
"管不到,回收走 /worktree remove|prune):".format(PLUGIN_SUBDIR, MAX_CONCURRENT))
|
||||
print("{:<26} {:<34} {:>4} {:>16}".format("名称", "分支", "脏", "联接"))
|
||||
for t in trees:
|
||||
print("{:<26} {:<34} {:>4} {:>16}".format(
|
||||
t["name"], t["branch"], "-" if t["dirty"] < 0 else str(t["dirty"]),
|
||||
junction_label(t["jn"])))
|
||||
def miss(rel):
|
||||
return any(t["jn"].get(rel) == "absent" for t in trees)
|
||||
if miss(".venv"):
|
||||
print(" 缺 .venv:进去跑 /keil-build / ci_local.py 会**静默**落到机器级解释器(无 "
|
||||
"fontTools,generated-fresh 假红的老根因)")
|
||||
if miss("simulator/SDL2"):
|
||||
print(" 缺 simulator/SDL2:模拟器 build_sim.bat 直接报错——CMakeLists.txt 的 "
|
||||
"SDL2_DIR 是裸 set,没有环境变量逃生口")
|
||||
if miss(".venv") or miss("simulator/SDL2"):
|
||||
print(" 补法:「plugin adopt <name>」——只建缺失的联接,不碰树里任何文件")
|
||||
return len(trees)
|
||||
|
||||
|
||||
def adopt_junctions(wt: Path):
|
||||
"""只补**缺失**的共享联接(已联的跳过、实体目录绝不碰),返回 (已有, 本次新建)。"""
|
||||
already, made = [], []
|
||||
st = junction_states(wt)
|
||||
for rel, desc in JUNCTION_SPECS:
|
||||
link = wt / rel
|
||||
if st[rel] == "link":
|
||||
already.append(rel)
|
||||
continue
|
||||
if st[rel] == "real":
|
||||
print("[warn] {} 已是实体目录/文件(不是联接)——不动它".format(rel))
|
||||
already.append(rel)
|
||||
continue
|
||||
target = REPO_ROOT / rel
|
||||
if not target.exists():
|
||||
print("[skip] {}({})主线里不存在,跳过".format(rel, desc))
|
||||
continue
|
||||
try:
|
||||
make_junction(link, target)
|
||||
except (Fail, EnvError) as err:
|
||||
print("[WARN] {} 联接失败:{}".format(rel, err))
|
||||
continue
|
||||
made.append(rel)
|
||||
print("[ok] 联接 {} -> {}".format(rel, target))
|
||||
return already, made
|
||||
|
||||
|
||||
def cmd_plugin(a) -> int:
|
||||
if a.action == "list":
|
||||
if print_plugin_trees() == 0:
|
||||
print("({} 下没有插件工作树)".format(PLUGIN_SUBDIR))
|
||||
print(" 建法:新会话输入框选「Worktree模式」/ 让模型调 worktree_create / "
|
||||
"/worktree create <name>")
|
||||
print(" 建完回来「plugin adopt <name>」补共享联接,才能在里面跑 /keil-build 与门禁。")
|
||||
return EXIT_OK
|
||||
|
||||
wt = plugin_dir_for(a.name)
|
||||
if not wt.is_dir():
|
||||
raise EnvError("目录不存在:{}(先用插件建树:/worktree create 或 worktree_create)".format(wt))
|
||||
|
||||
if a.action == "adopt":
|
||||
if git("rev-parse", "--git-dir", cwd=wt, check=False).returncode != 0:
|
||||
raise EnvError("{} 不是 git 工作树(别拿普通目录当插件树)".format(wt))
|
||||
print("为插件树补共享联接:{}".format(wt))
|
||||
already, made = adopt_junctions(wt)
|
||||
owner = plugin_lock_owner(a.name)
|
||||
# 复算一次:adopt 里 skip/warn/失败都不算成功,「已有 N + 新建 M」两两相加可能对不上
|
||||
# 总数——就绪判定只能按**现在的实际状态**说,否则又是把没联上的说成联上了
|
||||
still = [r for r, s in junction_states(wt).items() if s == "absent"]
|
||||
print()
|
||||
print("[ok] 联接:已有 {} 个、本次新建 {} 个".format(len(already), len(made)))
|
||||
if still:
|
||||
print("[warn] 仍缺 {}:主线里该路径不存在或联接失败——先补主线再复跑 adopt"
|
||||
"(这棵树还不能算就绪)".format("、".join(still)))
|
||||
else:
|
||||
print("该目录现在可以跑 /keil-build / ci_local.py / /serial-boot-test")
|
||||
print("build 锁由 keil_build.py 自持自释,持有者名就是 {}".format(owner))
|
||||
print("上台架前:$TW lock acquire bench --task {} --note <说明>".format(owner))
|
||||
print("下台架后:$TW lock release bench --task {}".format(owner))
|
||||
print("回收仍归插件:/worktree remove {}(本 skill 不删插件树)".format(a.name))
|
||||
return EXIT_OK
|
||||
|
||||
removed = [rel for rel, _spec in JUNCTION_SPECS if remove_junction(wt / rel)]
|
||||
print("[ok] 摘掉联接:{}(只摘联接本身,绝不动目标内容与树里文件)".format(
|
||||
"、".join(removed) if removed else "无(该树没有联接着)"))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 子进程脚本
|
||||
|
||||
|
||||
|
||||
def regen(cwd: Path) -> None:
|
||||
run_script("scripts/gen_task_table.py", ["--write"], cwd, "重生成 FreeRTOS 任务表")
|
||||
if list((Path(cwd) / "MDK-ARM").glob("**/*.map")):
|
||||
run_script("scripts/check_mem_numbers.py", ["--write"], cwd, "同步内存/Flash 快照")
|
||||
else:
|
||||
print("[skip] 未找到 .map,跳过内存快照同步(需先 /keil-build)")
|
||||
print("[skip] 未找到 .map,跳过内存快照同步(需先 @build)")
|
||||
|
||||
|
||||
def run_gates(cwd: Path, skip_build: bool) -> bool:
|
||||
ok = run_script("scripts/ci_local.py", [], cwd, "静态门禁 + 主机侧纯逻辑单测 本地等价 CI")
|
||||
ok = run_script("scripts/ci_local.py", [], cwd, "L0 + L0.5 本地等价 CI")
|
||||
if skip_build:
|
||||
print("[skip] 按 --skip-build 跳过 /keil-build")
|
||||
print("[skip] 按 --skip-build 跳过 @build")
|
||||
return ok
|
||||
got, info = lock_acquire("build", "__done__", wait=900)
|
||||
if not got:
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 /keil-build".format(info.get("task")))
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task")))
|
||||
return False
|
||||
# 告知 /keil-build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||
# 告知 @build「调用方已持锁」:新版 keil_build.py 自持自释(见其 docstring),
|
||||
# 读到本变量即跳过,避免自我死锁;**旧版根本不看此变量、也不会自锁**,
|
||||
# 故此处对「工作树里还是旧脚本」的在途任务同样安全(向后兼容)。
|
||||
os.environ["DTU_LOCK_HELD"] = "build"
|
||||
try:
|
||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||
"/keil-build(0 错 0 警)"):
|
||||
"@build(0 错 0 警)"):
|
||||
ok = False
|
||||
finally:
|
||||
os.environ.pop("DTU_LOCK_HELD", None)
|
||||
@@ -765,10 +557,6 @@ def cmd_new(a) -> int:
|
||||
raise EnvError("已有 {} 个活跃任务(上限 {});先 done/abort,或加 --force".format(
|
||||
len(active), MAX_CONCURRENT))
|
||||
|
||||
n_plugin = len(plugin_trees())
|
||||
if n_plugin:
|
||||
print("[i] 另有 {} 个 DSH 插件工作树(.dsh-worktrees/,不计入上限 {});"
|
||||
"磁盘与 UV4 / 台架仍是共享资源——「plugin list」看清单".format(n_plugin, MAX_CONCURRENT))
|
||||
WT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
print("[1/3] 建工作树(分支 {},基线 {}{})…".format(
|
||||
branch, a.base, ",跳过 LFS smudge" if a.lean else ""))
|
||||
@@ -789,7 +577,7 @@ def cmd_new(a) -> int:
|
||||
print()
|
||||
print("下一步:把新会话的工作目录切到")
|
||||
print(" " + str(wt))
|
||||
print("在该目录里干活、跑 /keil-build 与 ci_local.py(并行安全);")
|
||||
print("在该目录里干活、跑 @build 与 ci_local.py(并行安全);")
|
||||
print("要动台架先 `lock acquire bench --task {}`,完事 `lock release bench --task {} --bench-pass`。".format(
|
||||
a.task_id, a.task_id))
|
||||
print("回收用 `done {}`(**必须在主工作树执行**)。".format(a.task_id))
|
||||
@@ -800,7 +588,6 @@ def cmd_list(a) -> int:
|
||||
states = all_states()
|
||||
if not states:
|
||||
print("(没有任务空间。用 `new <ID> <slug>` 建一个)")
|
||||
print_plugin_trees()
|
||||
return EXIT_OK
|
||||
print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format(
|
||||
"任务", "分支", "落后", "领先", "脏", "台架"))
|
||||
@@ -825,7 +612,6 @@ def cmd_list(a) -> int:
|
||||
else:
|
||||
print("锁 {:<6} {} 持有(已 {:.1f} 小时,于 {})".format(
|
||||
n, info.get("task"), lock_age_hours(info), info.get("at")))
|
||||
print_plugin_trees()
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
@@ -906,19 +692,10 @@ def cmd_lock(a) -> int:
|
||||
if a.bench_pass:
|
||||
if not a.task:
|
||||
raise EnvError("--bench-pass 需要 --task <ID>")
|
||||
# 非登记持有者(lead 的验收窗、插件树 pw:*)没有台账。以前这里先 load_state
|
||||
# 就抛「任务 X 不存在(缺台账)」,**根本走不到释放**:锁还挂着,而 done 的前置
|
||||
# 会因活锁判失败——一个名字没登记,就挡住所有任务的收口。现在降级为显式告警 +
|
||||
# 照常释放:PASS 没进台账是事实,但不该拿「锁放不掉」当代价。
|
||||
if state_file(a.task).is_file():
|
||||
st = load_state(a.task)
|
||||
st["bench_pass"] = True
|
||||
save_state(st)
|
||||
print("[ok] 已为任务 {} 记录台架 PASS".format(a.task))
|
||||
else:
|
||||
print("[warn] 任务 {} 无台账({})→ **跳过 PASS 记录,仅释放锁**;要把 PASS 留在"
|
||||
"台账里就得用已「new」出来的任务 ID 占锁,或把验收结论写进主干提交说明/文档"
|
||||
.format(a.task, state_file(a.task)))
|
||||
st = load_state(a.task)
|
||||
st["bench_pass"] = True
|
||||
save_state(st)
|
||||
print("[ok] 已为任务 {} 记录台架 PASS".format(a.task))
|
||||
ok, msg = lock_release(a.name, a.task, force=a.force)
|
||||
print(msg)
|
||||
return EXIT_OK if ok else EXIT_FAIL
|
||||
@@ -970,15 +747,13 @@ def cmd_done(a) -> int:
|
||||
raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base))
|
||||
if already and ahead == 0:
|
||||
print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base))
|
||||
print("[i] 该路径在门禁之前就返回——不替你复绿;上一次门禁没跑到的尾巴,"
|
||||
"清场后去主树跑 `postmerge`(或手工 ci_local + check_doc_links --all)")
|
||||
print("=== 清场 ===")
|
||||
teardown_worktree(st, force=a.force, base=base)
|
||||
print("\n完成(分支此前已合入)。")
|
||||
return EXIT_OK
|
||||
|
||||
if st.get("needs_bench") and not st.get("bench_pass"):
|
||||
raise Fail("任务标记 --needs-bench 但无台架 PASS 记录;跑完 /serial-boot-test 后执行 "
|
||||
raise Fail("任务标记 --needs-bench 但无台架 PASS 记录;跑完 @test 后执行 "
|
||||
"`lock release bench --task {} --bench-pass`".format(st["id"]))
|
||||
print("[ok] 前置检查通过(领先 {} 共 {} 个提交)".format(base, ahead))
|
||||
|
||||
@@ -1022,7 +797,6 @@ def cmd_done(a) -> int:
|
||||
print("!! 门禁未全绿:工作树与分支**保留**,未清场。")
|
||||
print(" 主干已含合并提交 {}".format(base_after[:12]))
|
||||
print(" 可选:a) 在主工作树修正后追加提交(推荐)")
|
||||
print(" ——体积凭证/台账红就用 `postmerge` 自动诊断回填,别手抄补链")
|
||||
print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
|
||||
return EXIT_FAIL
|
||||
|
||||
@@ -1034,130 +808,6 @@ def cmd_done(a) -> int:
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
# ------------------------------------------------- 树内预检 / 合并后补绿(机械化)
|
||||
|
||||
|
||||
HYGIENE_BINARY = (".png", ".jpg", ".gif", ".ico", ".ttf", ".bin", ".hex", ".elf",
|
||||
".axf", ".o", ".crf", ".lst", ".zip", ".gz", ".pdf", ".cur")
|
||||
CR_EXEMPT = (".bat", ".cmd") # .gitattributes 强制 CRLF
|
||||
|
||||
|
||||
def _git_bytes(*args, cwd=None):
|
||||
r = subprocess.run(["git"] + [str(x) for x in args], cwd=str(cwd or REPO_ROOT),
|
||||
capture_output=True, stdin=subprocess.DEVNULL)
|
||||
return r.stdout if r.returncode == 0 else None
|
||||
|
||||
|
||||
def _changed_paths(cwd: Path):
|
||||
q = ("-c", "core.quotePath=false") # 中文文件名不许被八进制转义出去
|
||||
tracked = git(*q, "diff", "--name-only", "HEAD", cwd=cwd, check=False).stdout
|
||||
others = git(*q, "ls-files", "--others", "--exclude-standard", cwd=cwd, check=False).stdout
|
||||
return ([p.strip() for p in tracked.splitlines() if p.strip()],
|
||||
[p.strip() for p in others.splitlines() if p.strip()])
|
||||
|
||||
|
||||
def cmd_precheck(a) -> int:
|
||||
"""树内自查(只读):字节体检 + docs 逐档门禁 + 可选提交信息检查。绝不代改。"""
|
||||
cwd = Path.cwd()
|
||||
(cwd / "ci-artifacts").mkdir(exist_ok=True) # 新树没有 ci-artifacts(G 节:ENOENT 坑)
|
||||
tracked, untracked = _changed_paths(cwd)
|
||||
print("[i] {}:改动 {} 个(tracked {} + untracked {})".format(
|
||||
cwd, len(tracked) + len(untracked), len(tracked), len(untracked)))
|
||||
bom3, cr, fffd = bytes.fromhex("efbbbf"), b"\r", bytes.fromhex("efbfbd")
|
||||
problems, docs = 0, []
|
||||
for p in tracked + untracked:
|
||||
f = cwd / p
|
||||
if not f.is_file() or p.lower().endswith(HYGIENE_BINARY):
|
||||
continue
|
||||
b = f.read_bytes()
|
||||
bad = []
|
||||
if cr in b and not p.lower().endswith(CR_EXEMPT):
|
||||
bad.append("CR")
|
||||
n = b.count(fffd)
|
||||
if n:
|
||||
bad.append("U+FFFD x{}".format(n))
|
||||
if p in tracked:
|
||||
hb = _git_bytes("show", "HEAD:" + p, cwd=cwd)
|
||||
if hb is not None and hb.startswith(bom3) and not b.startswith(bom3):
|
||||
bad.append("BOM 漂移(HEAD 有 BOM、工作区没了——bom-guard 会拦)")
|
||||
if p.startswith("docs/") and p.endswith(".md"):
|
||||
docs.append(p)
|
||||
if bad:
|
||||
problems += 1
|
||||
print("!! {}:{}".format(p, " / ".join(bad)))
|
||||
for d in docs:
|
||||
if not run_script("scripts/check_doc_links.py", [d], cwd, "文档门禁 " + d, tail=6):
|
||||
problems += 1
|
||||
if a.msgfile:
|
||||
m = Path(a.msgfile)
|
||||
if not m.is_file():
|
||||
raise EnvError("消息文件不存在:{}".format(m))
|
||||
mb = m.read_bytes()
|
||||
if mb.startswith(bom3):
|
||||
problems += 1
|
||||
print("!! 消息带 BOM(commit -F 要 UTF-8 无 BOM)")
|
||||
if cr in mb:
|
||||
problems += 1
|
||||
print("!! 消息含 CRLF")
|
||||
ml = mb.decode("utf-8", "replace").splitlines()
|
||||
for i, ln in enumerate(ml):
|
||||
lim = 100 if i == 0 else 80
|
||||
if len(ln) > lim:
|
||||
problems += 1
|
||||
print("!! 消息第 {} 行长 {} > {}".format(i + 1, len(ln), lim))
|
||||
break
|
||||
for w in a.ban:
|
||||
c = mb.decode("utf-8", "replace").lower().count(w.lower())
|
||||
print("[i] 禁词 {!r}:{} 次".format(w, c))
|
||||
if c:
|
||||
problems += 1
|
||||
print("完成:问题 {} 个".format(problems))
|
||||
return EXIT_OK if problems == 0 else EXIT_FAIL
|
||||
|
||||
|
||||
def cmd_postmerge(a) -> int:
|
||||
"""主树合并后补绿:体积凭证自动诊断(短链/全链)+ 文档全量。列文件,不提交。
|
||||
|
||||
裁定外包给现有门禁:凭证红 → 先短链(--update 重写)→ 用只读的
|
||||
check_mem_numbers 复检裁定数字是否真变——红了才升级全链(--write 族)。
|
||||
"""
|
||||
main = main_worktree()
|
||||
if Path.cwd().resolve() != main.resolve():
|
||||
raise EnvError("postmerge 必须在主工作树跑:{}".format(main))
|
||||
if not list((main / "MDK-ARM").glob("**/*.map")):
|
||||
raise EnvError("主树没有 .map——先 /keil-build 0/0 再回来(C.5 的前提)")
|
||||
gate = "scripts/check_size_gate.py"
|
||||
if not a.force and run_script(gate, ["--local"], main, "体积凭证检查(同码)", tail=8):
|
||||
print("[ok] 凭证同码新鲜,无需补链")
|
||||
else:
|
||||
run_script("scripts/ram_baseline.py", ["--update"], main, "短链:重写体积凭证", tail=6)
|
||||
mem_ok = (a.skip_mem or run_script("scripts/check_mem_numbers.py", [], main,
|
||||
"受管区一致性复检(只读)", tail=8))
|
||||
if not mem_ok:
|
||||
print("[i] 快照与 .map 真漂了 → 升级全链(数字变了才动受管区)")
|
||||
run_script("scripts/check_mem_numbers.py", ["--write"], main, "同步内存快照")
|
||||
run_script("scripts/ram_baseline.py", ["--write"], main, "同步 RAM 预算台账")
|
||||
if a.coverage:
|
||||
run_script("scripts/check_test_coverage.py", ["--update-baseline"], main,
|
||||
"覆盖率基线刷新", tail=6)
|
||||
if not run_script(gate, ["--local"], main, "凭证复检(期望绿)", tail=8):
|
||||
print("!! 凭证复检仍红——贴输出查 check_size_gate 原因,别硬提交")
|
||||
return EXIT_FAIL
|
||||
ok = True
|
||||
if not a.skip_docs:
|
||||
ok = run_script("scripts/check_doc_links.py", ["--all"], main, "文档全库检查", tail=8)
|
||||
dirty = git("-c", "core.quotePath=false", "diff", "--name-only", "HEAD",
|
||||
cwd=main, check=False).stdout.splitlines()
|
||||
dirty = [x.strip() for x in dirty if x.strip()]
|
||||
if dirty:
|
||||
print()
|
||||
print("[i] 回填产物(提交属 Ask first,本命令不代提交):")
|
||||
for p in dirty:
|
||||
print(" - " + p)
|
||||
print(" 建议:git add <上面列的文件> && git commit -F <正文文件>")
|
||||
return EXIT_OK if ok else EXIT_FAIL
|
||||
|
||||
|
||||
def cmd_abort(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
@@ -1224,17 +874,11 @@ def build_parser():
|
||||
help="release bench 时顺带为该任务记录台架 PASS")
|
||||
p.set_defaults(func=cmd_lock)
|
||||
|
||||
p = sub.add_parser("plugin", help="DSH worktree 插件的树(.dsh-worktrees/):list / adopt / unadopt")
|
||||
p.add_argument("action", choices=["list", "adopt", "unadopt"])
|
||||
p.add_argument("name", nargs="?", default="",
|
||||
help="插件 worktree 名(可带 worktree/ 前缀或多段斜杠)")
|
||||
p.set_defaults(func=cmd_plugin)
|
||||
|
||||
p = sub.add_parser("done", help="回收:预演 → 合入主干 → 审计 → 重生成 → 门禁 → 清场")
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--base", default=None)
|
||||
p.add_argument("--dry-run", action="store_true", help="只预演,不做任何改动")
|
||||
p.add_argument("--skip-build", action="store_true", help="跳过 /keil-build")
|
||||
p.add_argument("--skip-build", action="store_true", help="跳过 @build")
|
||||
p.add_argument("--skip-gates", action="store_true", help="跳过全部门禁")
|
||||
p.add_argument("--force", action="store_true", help="清场时强制移除工作树")
|
||||
p.set_defaults(func=cmd_done)
|
||||
@@ -1243,18 +887,6 @@ def build_parser():
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.set_defaults(func=cmd_abort)
|
||||
|
||||
p = sub.add_parser("precheck", help="树内自查(只读):字节体检 + docs 逐档门禁 + 可选消息检查")
|
||||
p.add_argument("--msgfile", default=None, help="附带检查提交信息文件(BOM/CRLF/行长/禁词)")
|
||||
p.add_argument("--ban", action="append", default=[], help="禁词,可重复")
|
||||
p.set_defaults(func=cmd_precheck)
|
||||
|
||||
p = sub.add_parser("postmerge", help="主树合并后补绿:体积凭证自动诊断(短/全链)+ 文档全量;不代提交")
|
||||
p.add_argument("--force", action="store_true", help="凭证新鲜也强制走一遍 --update")
|
||||
p.add_argument("--coverage", action="store_true", help="附带覆盖率基线刷新")
|
||||
p.add_argument("--skip-docs", action="store_true", help="跳过 check_doc_links --all")
|
||||
p.add_argument("--skip-mem", action="store_true", help="只挪凭证,跳过受管区复检/全链")
|
||||
p.set_defaults(func=cmd_postmerge)
|
||||
return ap
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|---|---|---|
|
||||
| 内容页 FAIL,且**有意**改了该页视觉 | 预期变化 | 先肉眼确认新效果正确(窗口模式或 `i1_to_png.py` 出图对照 mockup),再 `.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷参考,`git diff` 检查 ref/ 变化后随代码提交 |
|
||||
| 内容页 FAIL,但**未**有意改视觉 | bug(缺字/几何漂移/样式回归) | 修代码;**禁止**刷参考掩盖 |
|
||||
| 池水位门禁失败但像素一致 | 结构/样式变更(常量样式转换、增删框体等) | 确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新脚本常量 + 把新值与它的实测来源写进 `docs/UI设计/UI_自动化测试流程.md` 正文常量表(2026-10-28 起不再记流水,见 `docs/文档规范.md` §5.1) |
|
||||
| 池水位门禁失败但像素一致 | 结构/样式变更(常量样式转换、增删框体等) | 确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新脚本常量 + 在流程文档变更记录登记 |
|
||||
| 菜单回归失败 | 结构断言(白带占比/边框/往返一致) | 按脚本打印的未过断言清单逐项修代码,无参考文件可刷 |
|
||||
|
||||
## 池水位门禁常量(在脚本头部)
|
||||
@@ -47,7 +47,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|
||||
## 关联注意
|
||||
|
||||
- `.venv` 缺失/换机:`& <本机任一 python ≥3.10> -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`(`requirements.txt` 已含 `pre-commit`,重建后静态门禁直接可跑)
|
||||
- `.venv` 缺失/换机:`& <本机任一 python ≥3.10> -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`(`requirements.txt` 已含 `pre-commit`,重建后 L0 门禁直接可跑)
|
||||
- 新增 UI `.c` 文件先走 `new-source-file` skill 双端注册,否则一端静默缺文件
|
||||
- 改 `ui_menu_tree.txt` 或界面里的中文字符串 → **同一笔**重跑生成器(`tools/menu_gen/gen_menu.py`;字体走 **font-regen** 四步),否则提交门禁 `generated-fresh` 直接拦:它把生成器拷进系统临时目录重跑、与工作区逐字节比对(实证 `0f7b5a87` 改了模板没重跑)→ `docs/门禁说明/generated-fresh.md`
|
||||
- 字体重生成后全页大面积像素差异属预期:确认字形正确后 `--update` 刷参考
|
||||
|
||||
+287
-98
@@ -1,24 +1,22 @@
|
||||
# Gitea Actions CI —— STM32F4-Base 持续集成
|
||||
#
|
||||
# 覆盖范围(**只做不依赖硬件的层**,见 docs/测试说明/流程与规范/测试开发标准流程.md §1/§9):
|
||||
# 静态门禁 static = pre-commit 全部钩子(等价本地 `pre-commit run --all-files`)
|
||||
# + 手动阶段三根(覆盖率总闸 / 全量 cppcheck / test-cppcheck,`--hook-stage manual`;
|
||||
# 它们在 .pre-commit-config.yaml 里带 stages: [manual],提交期不跑,由本 job 强制)
|
||||
# 主机侧纯逻辑单测 host-unit = test/host/**(MSVC 离线编译,零硬件)
|
||||
# 覆盖范围(**只做不依赖硬件的层**,见 docs/测试说明/测试开发标准流程.md §1/§9):
|
||||
# L0 静态门禁 = pre-commit 全部钩子(等价本地 `pre-commit run --all-files`)
|
||||
# L0.5 主机侧纯逻辑单测 = test/host/**(MSVC 离线编译,零硬件)
|
||||
#
|
||||
# ⛔ 板端测试套件(test/board/,需真机 + ST-LINK)/ 主机侧回归(/serial-boot-test 闭环)/ 人工目检
|
||||
# ⛔ L1 板端套件(test/board/,需真机 + ST-LINK)/ L2 @test 闭环 / L3 人工目检
|
||||
# **不上 CI** —— 需要开发板与烧录器,只能在本地跑。
|
||||
#
|
||||
# 明确不做的两件事(都是刻意为之):
|
||||
# 1. `/keil-build`(Keil UV4)—— 需本机 Keil 授权,runner 上不具备;且静态门禁的 cppcheck +
|
||||
# 1. `@build`(Keil UV4)—— 需本机 Keil 授权,runner 上不具备;且 L0 的 cppcheck +
|
||||
# 代码规范已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(AGENTS.md)。
|
||||
# 2. `mem-numbers` / `mem-budget` 两根钩子 —— 事实源都是 `MDK-ARM/**/*.map`(Keil
|
||||
# 构建产物、已 gitignore),无 `/keil-build` 即无 `.map`。故本 workflow 在**检测不到
|
||||
# 构建产物、已 gitignore),无 `@build` 即无 `.map`。故本 workflow 在**检测不到
|
||||
# .map 时自动跳过它们**(见下方 "决定跳过哪些钩子" 步骤),有 .map 的 runner 照常跑。
|
||||
#
|
||||
# 🔴 体积门禁:盲区已关闭为**强制路径**(2026-10-25 起;原先只做到"声明")
|
||||
# 跳过钩子只解释了"runner 没有 Keil",没解决**"没跑 = 通过"**:钩子在无 .map 时
|
||||
# 自适应 SKIP 并返回 0,于是 CI 全绿仍可能被读成"体积也守住了"。现由静态门禁作业**最后
|
||||
# 自适应 SKIP 并返回 0,于是 CI 全绿仍可能被读成"体积也守住了"。现由 L0 作业**最后
|
||||
# 一步** `python scripts/check_size_gate.py --ci` 在**不需要 Keil** 的前提下补两条判据:
|
||||
# · 同码性:`scripts/size_attestation.json` 的源码指纹必须等于本次检出树的指纹,
|
||||
# 而该凭证只能由"新鲜 .map + 当时未超线"的真实链接测量写出(见 --stamp 三道前置)
|
||||
@@ -61,7 +59,7 @@
|
||||
# · 仓库 `.venv` 已 gitignore,runner 的全新工作区里**根本不存在**。
|
||||
# 唯一可用的机器级解释器(`C:\ProgramData\anaconda3\python.exe`)不再被探测,而文档
|
||||
# 要求的机器级 `DTU_PRECOMMIT_PYTHON` 从未设置(Machine 作用域一个 DTU_* 都没有)
|
||||
# → run 14/15 的静态门禁 **全部死在"环境自检",一个钩子都没跑**。cppcheck 同理:
|
||||
# → run 14/15 的 L0 **全部死在"环境自检",一个钩子都没跑**。cppcheck 同理:
|
||||
# 旧写法"不在 PATH 就把 C:\Program Files\Cppcheck 加进 PATH"的兜底也被删了。
|
||||
# **故探测逻辑搬进 `scripts/ci_find_python.ps1`**(两个 job 共用、可离线跑、失败时
|
||||
# 打印全部候选),并恢复常见机器级安装位置兜底;`DTU_*` 仍是最高优先级、
|
||||
@@ -88,7 +86,7 @@
|
||||
# - 该 Gitea 实例已启用 Actions:`[actions] ENABLED = true`(site admin 配置)
|
||||
# - 已注册一个 **Windows** runner(labels 含 `windows`),且装有:
|
||||
# Python 3.x、cppcheck、pre-commit、MSVC(VS2019/2022 的 C++ 工具链 + Windows SDK)
|
||||
# - 缺 MSVC 时主机侧单测步骤会**降级为提示并跳过**(不硬失败),但会打印醒目 WARNING
|
||||
# - 缺 MSVC 时 L0.5 步骤会**降级为提示并跳过**(不硬失败),但会打印醒目 WARNING
|
||||
#
|
||||
# 🔧 runner 机器级环境变量(可选项覆盖;runner 以 SYSTEM 服务运行,用户级变量不可见):
|
||||
# DTU_PRECOMMIT_PYTHON / DTU_CPPCHECK / DTU_MSVC_CL —— 未设置时按 PATH / 标准 VS 布局自动发现。
|
||||
@@ -121,17 +119,134 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
# ------------------------------------------------------------------
|
||||
# 静态门禁(pre-commit 钩子全集)
|
||||
# L0:静态门禁(pre-commit 钩子全集)
|
||||
# ------------------------------------------------------------------
|
||||
static-gates:
|
||||
name: 静态门禁 (pre-commit)
|
||||
l0-gates:
|
||||
name: L0 静态门禁 (pre-commit)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 环境自检(Python / cppcheck / pre-commit)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -143,7 +258,7 @@ jobs:
|
||||
# * both jobs share ONE implementation,
|
||||
# * it can be run (and its failure list inspected) offline,
|
||||
# * the well-known machine-wide install locations stay probed.
|
||||
# The previous inline 2-candidate probe is what killed every static-gate run
|
||||
# The previous inline 2-candidate probe is what killed every L0 run
|
||||
# since 33a4aa4 -- see the file header.
|
||||
#
|
||||
# The script prints diagnostics with Write-Host and emits ONLY the
|
||||
@@ -165,7 +280,7 @@ jobs:
|
||||
# The directory fallback is NOT cosmetic: the runner service runs as
|
||||
# SYSTEM, whose PATH has no Cppcheck entry (a USER-scope PATH entry is
|
||||
# invisible to it), and no machine-level DTU_CPPCHECK is set on a
|
||||
# stock runner. Dropping this fallback in 33a4aa4 is what made the static-gate job die
|
||||
# stock runner. Dropping this fallback in 33a4aa4 is what made L0 die
|
||||
# on cppcheck as well, right after the Python probe had been fixed.
|
||||
$found = $null
|
||||
$envCpp = $env:DTU_CPPCHECK
|
||||
@@ -268,14 +383,14 @@ jobs:
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
} else {
|
||||
Write-Host "::warning::no .map found (no Keil build) -- SKIPPING mem-numbers and mem-budget, so absolute Flash/RAM numbers are NOT recomputed here."
|
||||
Write-Host "::warning::that gap is no longer silent: the final step of this job runs scripts/check_size_gate.py --ci (forced path: attestation parity + 92% budget re-check). Locally: scripts/ci_local.py size step."
|
||||
Write-Host "::warning::that gap is no longer silent: the final step of this job runs scripts/check_size_gate.py --ci (forced path: attestation parity + 92% budget re-check). Locally: scripts/ci_local.py step L0c."
|
||||
"value=mem-numbers,mem-budget" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"sizegate=off" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"> :warning: Volume gates (mem-numbers, mem-budget): **NOT RUN** -- no `.map` (no Keil build); compensated by the size-gate step below." |
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
}
|
||||
|
||||
- name: 运行静态门禁(--all-files)
|
||||
- name: 运行 L0 门禁(--all-files)
|
||||
shell: powershell
|
||||
env:
|
||||
SKIP: ${{ steps.skip.outputs.value }}
|
||||
@@ -289,7 +404,7 @@ jobs:
|
||||
# would ABORT the pipeline. Relax the preference around the pipeline and
|
||||
# restore it afterwards (same pattern as the env-check step above).
|
||||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||||
$log = 'ci-artifacts\static-pre-commit.log'
|
||||
$log = 'ci-artifacts\l0-pre-commit.log'
|
||||
if ($env:SKIP) { Write-Host "SKIP=$env:SKIP" }
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
@@ -303,58 +418,17 @@ jobs:
|
||||
$passed = @($lines | Select-String -Pattern 'Passed$').Count
|
||||
$failed = @($lines | Select-String -Pattern 'Failed$').Count
|
||||
$skipd = @($lines | Select-String -Pattern 'Skipped$').Count
|
||||
$summary = "static: pre-commit hooks -- passed=$passed failed=$failed skipped=$skipd (log: $log)"
|
||||
$summary = "L0: pre-commit hooks -- passed=$passed failed=$failed skipped=$skipd (log: $log)"
|
||||
Write-Host "::notice::$summary"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### 静态门禁 (static gates)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0 static gates" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||||
|
||||
if ($rc -ne 0) {
|
||||
Write-Host "::error::static gates failed (see output above and $log)"
|
||||
Write-Host "::error::L0 static gates failed (see output above and $log)"
|
||||
exit $rc
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 手动阶段门禁(2026-10-28 用户裁定):主机单测覆盖率总闸 + 全量 cppcheck。
|
||||
# 这三根钩子在 .pre-commit-config.yaml 里带 `stages: [manual]`,**不在 git commit 时跑**
|
||||
# (实测合计 ~165 s = 一次带代码提交耗时的三分之二);提交期只跑快的那些,全量扫描由本 job 强制。
|
||||
# 逐根调用而不是一条命令:`pre_commit run <a> <b>` 不支持多 id(实测 argparse 报
|
||||
# "unrecognized arguments")。
|
||||
- name: 手动阶段门禁(覆盖率总闸 + 全量 cppcheck)
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
# Pure ASCII only (see file header). Same $ErrorActionPreference dance as the
|
||||
# static-gate step above: the runner injects 'Stop', and '2>&1 |' turns a
|
||||
# native command's stderr into an ErrorRecord that would abort the pipeline.
|
||||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||||
$log = 'ci-artifacts/manual-stage.log'
|
||||
if (Test-Path $log) { Remove-Item $log -Force }
|
||||
$rc = 0
|
||||
$hooks = @('cppcheck', 'test-cppcheck', 'test-coverage')
|
||||
foreach ($h in $hooks) {
|
||||
Write-Host "== manual hook: $h =="
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $env:PYTHON -s -m pre_commit run --all-files --hook-stage manual $h 2>&1 |
|
||||
Tee-Object -FilePath $log -Append
|
||||
$one = $LASTEXITCODE
|
||||
$ErrorActionPreference = $saved
|
||||
Write-Host "manual hook $h rc=$one"
|
||||
if ($one -ne 0) { $rc = $one }
|
||||
}
|
||||
$lines = Get-Content $log -ErrorAction SilentlyContinue
|
||||
$passed = @($lines | Select-String -Pattern 'Passed$').Count
|
||||
$failed = @($lines | Select-String -Pattern 'Failed$').Count
|
||||
$summary = "manual-stage: coverage + cppcheck -- passed=$passed failed=$failed (log: $log)"
|
||||
Write-Host "::notice::$summary"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### manual stage gates (coverage + cppcheck)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||||
if ($rc -ne 0) {
|
||||
Write-Host "::error::manual-stage gates failed (see output above and $log)"
|
||||
exit $rc
|
||||
}
|
||||
|
||||
- name: 归档静态门禁日志(供下载/排障)
|
||||
- name: 归档 L0 日志(供下载/排障)
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -362,22 +436,17 @@ jobs:
|
||||
# actions/upload-artifact (inner networks may not fetch outer actions);
|
||||
# instead we keep the log on the runner AND surface its path + tail in
|
||||
# the step summary, so the UI is self-sufficient for triage.
|
||||
if (Test-Path 'ci-artifacts\static-pre-commit.log') {
|
||||
$size = (Get-Item 'ci-artifacts\static-pre-commit.log').Length
|
||||
Write-Host "archived: ci-artifacts/static-pre-commit.log ($size bytes)"
|
||||
if (Test-Path 'ci-artifacts\l0-pre-commit.log') {
|
||||
$size = (Get-Item 'ci-artifacts\l0-pre-commit.log').Length
|
||||
Write-Host "archived: ci-artifacts/l0-pre-commit.log ($size bytes)"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/static-pre-commit.log`` ($size bytes on runner)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/l0-pre-commit.log`` ($size bytes on runner)" -Encoding utf8
|
||||
}
|
||||
if (Test-Path 'ci-artifacts\manual-stage.log') {
|
||||
$msize = (Get-Item 'ci-artifacts\manual-stage.log').Length
|
||||
Write-Host "archived: ci-artifacts/manual-stage.log ($msize bytes)"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/manual-stage.log`` ($msize bytes on runner)" -Encoding utf8
|
||||
}
|
||||
Write-Host "::notice::static-gate artifacts kept on runner at: ci-artifacts\ (static-pre-commit.log, manual-stage.log)"
|
||||
Write-Host "::notice::L0 artifacts kept on runner at: ci-artifacts\l0-pre-commit.log"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 强制体积门禁(2026-10-25):不需要 Keil,也不需要 .map
|
||||
# 放在静态门禁作业**最后**且 if: always()——静态门禁失败时这条结论同样要看得到,
|
||||
# 放在 L0 作业**最后**且 if: always()——L0 失败时这条结论同样要看得到,
|
||||
# 而它自己失败会让整个作业变红(这正是"强制"的含义)。
|
||||
- name: 体积门禁(强制路径:同码凭证 + 92% 红线,无需 Keil)
|
||||
if: always()
|
||||
@@ -441,14 +510,134 @@ jobs:
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 主机侧纯逻辑单测(零硬件;缺 MSVC 则跳过)
|
||||
# L0.5:主机侧纯逻辑单测(零硬件;缺 MSVC 则跳过)
|
||||
# ------------------------------------------------------------------
|
||||
host-unit-tests:
|
||||
name: 主机侧单测 (MSVC)
|
||||
l05-host-unit-tests:
|
||||
name: L0.5 主机侧单测 (MSVC)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 探测 MSVC(VS BuildTools / Community 等)
|
||||
id: msvc
|
||||
shell: powershell
|
||||
@@ -502,7 +691,7 @@ jobs:
|
||||
if ($found) {
|
||||
"value=yes" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
} else {
|
||||
Write-Host "::warning::runner has no MSVC (VS C++ toolchain) -- skipping host-side unit tests. Install VS BuildTools and re-run."
|
||||
Write-Host "::warning::runner has no MSVC (VS C++ toolchain) -- skipping L0.5. Install VS BuildTools and re-run."
|
||||
"value=no" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
}
|
||||
|
||||
@@ -511,9 +700,9 @@ jobs:
|
||||
if: steps.msvc.outputs.value == 'yes'
|
||||
shell: powershell
|
||||
run: |
|
||||
# Resolve a working Python with the SAME shared probe as the static-gate job (no
|
||||
# Resolve a working Python with the SAME shared probe as L0 (no
|
||||
# -NeedPreCommit: these harnesses are stdlib-only). The old inline
|
||||
# probe here had the same 2-candidate blind spot as the static-gate job -- it only
|
||||
# probe here had the same 2-candidate blind spot as L0 -- it only
|
||||
# survived by luck, because the stdlib tests also run on the msys64
|
||||
# python that happens to be on SYSTEM's PATH.
|
||||
$py = & './scripts/ci_find_python.ps1' -WriteEnv
|
||||
@@ -539,12 +728,12 @@ jobs:
|
||||
$sumPass = 0
|
||||
$sumTotal = 0
|
||||
# Relax preference around each `2>&1 | Tee-Object` pipeline (native stderr
|
||||
# becomes ErrorRecord -> 'Stop' would abort). See the static-gate steps for details.
|
||||
# becomes ErrorRecord -> 'Stop' would abort). See L0 step for details.
|
||||
$savedPref = $ErrorActionPreference
|
||||
foreach ($r in $runners) {
|
||||
$runnerCount++
|
||||
$name = $r.Directory.Name
|
||||
$log = "ci-artifacts\host-unit-$name.log"
|
||||
$log = "ci-artifacts\l05-$name.log"
|
||||
Write-Host "================================"
|
||||
Write-Host "run: $($r.FullName)"
|
||||
Write-Host "================================"
|
||||
@@ -570,19 +759,19 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
$agg = "host-unit: $runnerCount runner(s), $sumPass/$sumTotal assertions passed, fail=$fail"
|
||||
$agg = "L0.5: $runnerCount runner(s), $sumPass/$sumTotal assertions passed, fail=$fail"
|
||||
Write-Host "::notice::$agg"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### 主机侧纯逻辑单测 (host-unit)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0.5 host-side unit tests" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $agg -Encoding utf8
|
||||
"count=$runnerCount" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"agg=$agg" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
exit $fail
|
||||
|
||||
- name: 归档主机侧单测日志(供下载/排障)
|
||||
- name: 归档 L0.5 日志(供下载/排障)
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
# Result visibility: same zero-dependency archival as the static-gate job.
|
||||
# Result visibility: same zero-dependency archival as L0.
|
||||
if (Test-Path 'ci-artifacts') {
|
||||
$logs = Get-ChildItem -Path 'ci-artifacts' -Filter '*.log' -ErrorAction SilentlyContinue
|
||||
foreach ($f in $logs) {
|
||||
@@ -594,15 +783,15 @@ jobs:
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived logs on runner: $names" -Encoding utf8
|
||||
}
|
||||
} else {
|
||||
Write-Host "no ci-artifacts directory (host-side unit tests skipped or failed early)"
|
||||
Write-Host "no ci-artifacts directory (L0.5 skipped or failed early)"
|
||||
}
|
||||
|
||||
- name: 主机侧单测跳过说明
|
||||
- name: L0.5 跳过说明
|
||||
if: steps.msvc.outputs.value == 'no'
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "host-side unit tests skipped: runner has no MSVC."
|
||||
Write-Host "L0.5 skipped: runner has no MSVC."
|
||||
Write-Host "Local equivalent: & '.venv\Scripts\python.exe' scripts/ci_local.py"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### 主机侧纯逻辑单测 (host-unit)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### L0.5 host-side unit tests" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value ":warning: **NOT RUN** -- runner has no MSVC (VS C++ toolchain)." -Encoding utf8
|
||||
exit 0
|
||||
|
||||
+1
-19
@@ -139,10 +139,7 @@ docs/images/*.png
|
||||
.dsh/skills/serial-assistant/scripts/state/
|
||||
|
||||
# agent 会话草稿(2026-10-14 定):提交信息草稿 / 门禁输出 / 台架日志 / 一次性改名
|
||||
# 脚本 / 故障注入 patch —— 一律不入库,**而且不落仓库根**(写进 ci-artifacts/ 或系统临时
|
||||
# 目录:2026-10-28 清理仓库根时那里堆着四个一次性计数改名脚本 + 一份提交信息草稿,全是
|
||||
# 未跟踪的 ?? —— 一次 git add -A 就进历史;仓库根只留工程配置与生成器输入两份 .txt)。
|
||||
# 判据三条,命中任一就不值得留:
|
||||
# 脚本 / 故障注入 patch —— 一律不入库。判据三条,命中任一就不值得留:
|
||||
# ① 能重跑的(门禁输出、日志);② 内容已在 git 历史里的(已合并实现的过程 diff);
|
||||
# ③ 离开已丢弃的任务分支就跑不通的(依赖 NET_FAULT_INJECT 的注入产物)——
|
||||
# 这种提交进去是"看着有证据、实际不可复现"的假证据,**方法应沉淀成文档文字**
|
||||
@@ -166,18 +163,3 @@ docs/images/*.png
|
||||
# 副产物,不兜住就会被某次 git add -A 带进历史。正解仍是继承完整环境跑这类工具。
|
||||
%SystemDrive%/
|
||||
ci-artifacts/
|
||||
|
||||
# 提交信息草稿的兜底反选(上一条草稿块的「不入库」靠人自觉把文件写进 ci-artifacts/,
|
||||
# 但 `git commit -F` 那份文件随手落在仓库根就会以 ?? 出现,被某次 add -A 带走):
|
||||
# 正文已经进了 commit message,落盘副本零信息量 → 根目录锚定挡掉这个命名。
|
||||
/commitmsg*.txt
|
||||
|
||||
# DSH worktree 插件(dsh-task-worktree)把任务树建在**仓库内**这个命名空间下,与
|
||||
# @task skill 的仓外 _wt/ 两套并存。这一行必须**先由我们自己写好**:插件首次 create
|
||||
# 会往 .gitignore 追加分毫不相同的字面量(ensureIgnored 见同名规则即跳过),但那等于
|
||||
# 在会话中途改这份受控文件——门禁 diff 里冒出一行"不是我加的"最坑人。
|
||||
# 该目录同时登记进了 check_doc_links / gen_refgraph / check_layer_deps 的排除面
|
||||
# (整份仓库副本不能进全库扫描);细节 → .dsh/skills/task-worktree/SKILL.md
|
||||
.dsh-worktrees/
|
||||
|
||||
.obsidian/
|
||||
|
||||
+55
-127
@@ -76,11 +76,6 @@ repos:
|
||||
hooks:
|
||||
- id: cppcheck
|
||||
name: cppcheck
|
||||
# ⚠ stages: [manual](2026-10-28 用户裁定):全库扫描实测 ~20 s,不压在提交期;
|
||||
# 由**远端 CI** 强制(.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),
|
||||
# 本地的等价入口 = scripts/ci_local.py(manual 段)或
|
||||
# python -m pre_commit run --all-files --hook-stage manual cppcheck
|
||||
stages: [manual]
|
||||
entry: cppcheck
|
||||
# 注意:YAML 流序列中逗号是元素分隔符,含逗号/空格的参数必须加引号
|
||||
args: ['--enable=warning,performance,portability,style', '--error-exitcode=1',
|
||||
@@ -120,9 +115,6 @@ repos:
|
||||
# 依据:docs/审查记录/测试与架构改进建议.md §3.4;实机测试实施指南.md §8.1
|
||||
- id: test-cppcheck
|
||||
name: cppcheck (test/, warning only)
|
||||
# ⚠ stages: [manual](2026-10-28,同 cppcheck):实测 ~38 s;由远端 CI 与
|
||||
# scripts/ci_local.py 的 manual 段跑,提交期不跑。
|
||||
stages: [manual]
|
||||
entry: cppcheck
|
||||
args: ['--enable=warning', '--error-exitcode=1',
|
||||
'--inline-suppr', '--std=c99', '--quiet',
|
||||
@@ -187,29 +179,6 @@ repos:
|
||||
MDK-ARM/RTE/|
|
||||
docs/芯片手册/
|
||||
)
|
||||
# mermaid 图:围栏内确定性语法预检(第 52 条,2026-10-30 加,口径 51 → 52,提交期实跑 48 → 49)。
|
||||
# 为什么要有:屏板链路设计说明.md §1 的 flowchart 里 `-.|"文字"|`(`-.` 直接跟管道,不是任何合法
|
||||
# 箭头 token)让 mermaid 渲染 `Syntax error in text`——该图 2026-10-29 落库、期间 doc-links 等
|
||||
# 全绿:文档门禁查链接与章节,**从不查围栏内图表语法**,这是它们的共同盲区。
|
||||
# v1 只查「错了必渲染失败」三条(不重造解析器):R1 缺/错图类型头;R2 `-.` 后直接跟 `|`
|
||||
# (带文字点线合法形态只有 `-.->|t|` 与 `-. 文字 .->`,selftest 证明两种合法形态不误伤);
|
||||
# R3 flowchart 的 subgraph 与独占行 `end` 计数配对、sequence 的块起点与 `end` 计数配对。
|
||||
# 存量基线:接线当日 --all 实测 29 个自有文件 60 块围栏、0 命中 ⇒ **空基线零语料**(还清类
|
||||
# 先例:id-check-new);docs/芯片手册/ 是导入转录、围栏不受控,与 doc-links 同排除面不进语料。
|
||||
# 自证:--selftest 五条(正例含三种合法点线 / 本坑形态必响 / 缺 end 必响 / 假头必响且
|
||||
# sequence alt-else-end 不误伤 / 基线吞得下且清空后同命中算新发)。
|
||||
- id: mermaid-syntax
|
||||
name: "mermaid 围栏语法预检(图类型头 / 非法点线管道 / 块配对)"
|
||||
entry: python scripts/check_mermaid.py
|
||||
language: system
|
||||
files: \.md$
|
||||
exclude: |
|
||||
(?x)^(
|
||||
\.opencode/node_modules/|
|
||||
Lib/lvgl/|
|
||||
MDK-ARM/RTE/|
|
||||
docs/芯片手册/
|
||||
)
|
||||
# 数值:README/AGENTS 任务表须与 Src/freertos.c 一致
|
||||
- id: task-table
|
||||
name: FreeRTOS 任务表一致性
|
||||
@@ -264,7 +233,7 @@ repos:
|
||||
# 累计 / 100ms 节流 / tick 回绕)是纯逻辑,板端套件无法穷举边界;本钩子
|
||||
# 用 MSVC 离线编译真实源文件(#include 纳入)并用可观测桩驱动,秒级、零
|
||||
# 硬件、不占 MCU RAM。仅在被测文件或其平台层 / 测试变动时触发。
|
||||
# 详见 docs/测试说明/单元测试/看门狗单元测试说明.md
|
||||
# 详见 docs/测试说明/看门狗单元测试说明.md
|
||||
- id: watchdog-unit-test
|
||||
name: 看门狗判定状态机单测(主机侧)
|
||||
entry: python test/host/watchdog/run_unit_test.py
|
||||
@@ -302,7 +271,7 @@ repos:
|
||||
files: ^(App/cfg/cfg_file\.(c|h)|App/cfg/cfg_file_store\.h|App/log/sys_log\.h|test/host/cfg_file/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7d) 相量层(全周傅氏相量)主机侧单元测试(2026-10-10 加)
|
||||
# 7d) 相量层(L2)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:App/algo/ 为**纯算法**层(无寄存器、无 RTOS、无外设依赖),是本项目
|
||||
# 首个可完全离线验证的算法层。板端**无信号源**,无法注入已知幅值/相位/频率的
|
||||
# 标准波形,故"读数 = 设定值"这一核心判据在板上不可验;本钩子用 MSVC 离线编译
|
||||
@@ -316,7 +285,7 @@ repos:
|
||||
files: ^(App/algo/(phasor|sample_ring)\.(c|h)|test/host/phasor/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7e) ADC 输入层(语义取数与有效性)主机侧单元测试(2026-10-10 加)
|
||||
# 7e) ADC 输入层(L1)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:adc_input.c 的核心是"通道号 <-> Rank 顺序 <-> 语义"三者映射的正确性;
|
||||
# 板端无信号源,无法注入已知电压验证"读到的是哪只脚"(映射说明 §8)。本钩子用
|
||||
# MSVC 离线编译真实源文件(#include 纳入)+ HAL/TPAFE 可注入桩:给每个 Rank
|
||||
@@ -375,7 +344,7 @@ repos:
|
||||
files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h) ADC 采样有效性校验主机侧单元测试(2026-10-11 加)
|
||||
# 7h) ADC 采样有效性校验(L1-6)主机侧单元测试(2026-10-11 加)
|
||||
# 背景:adc_health.c 是"静默/丢帧/序号倒退/错误计数增量"的判定层——这些
|
||||
# 故障在健康台架上**不可复现**(复现 = 拔芯片/改驱动),板端闭环只能证明
|
||||
# "健康时不误报",证不了"故障时必报";100ms 门限、tick 回绕、事件边沿
|
||||
@@ -389,7 +358,7 @@ repos:
|
||||
files: ^(App/algo/adc_health\.(c|h)|App/task/adc_task\.c|test/host/adc_health/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h2) 通道标定层(标定折算,「数据可信」标定折算与落盘整定)主机侧单元测试(2026-10-25 加,道A)
|
||||
# 7h2) 通道标定层(L1.5,「数据可信」卡 2/卡 4)主机侧单元测试(2026-10-25 加,道A)
|
||||
# 背景:标定折算是"码值→工程量"的唯一插入点,Q16 系数/零点两段落盘 +
|
||||
# 合法带哨兵回退 + 两点标定公式——板端无标准源、坏文件注入路径在真机
|
||||
# 上难以复现,只有离线端能逐系数钉死。钩子用 MSVC 编译真实源文件
|
||||
@@ -402,7 +371,7 @@ repos:
|
||||
language: system
|
||||
files: ^(App/algo/adc_cal\.(c|h)|App/algo/adc_health\.(c|h)|App/cfg/cfg_file\.(c|h)|App/task/adc_task\.c|test/host/adc_cal/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
# 7h3) 量纲回归(「数据可信」"不该动作时不动作"用例,道A)主机侧单元测试(2026-10-25 加)
|
||||
# 7h3) 量纲回归(「数据可信」卡 7,道A)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:台架曾长期缺"不该动作时不动作"的判据,码值/工程量错配曾以
|
||||
# 恒假动作的形态混过验收。本钩子用真实链(adc_cal→ring→phasor→
|
||||
# prot_elm)+ 显式换算的合成波把七类元件的量纲钉死:折算点被旁路
|
||||
@@ -414,11 +383,11 @@ repos:
|
||||
files: ^(App/algo/(adc_cal|prot_elm|prot_set|phasor|sample_ring|freq)\.(c|h)|App/task/(adc_task|protect_task)\.c|test/host/prot_dim/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h4) 遥测快照层(全周傅氏相量发布侧,「数据可信」遥测快照发布,道D)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:遥测快照发布项的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗
|
||||
# 7h4) 遥测快照层(L2 发布侧,「数据可信」卡 8,道D)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:卡 8 的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗
|
||||
# 200 ms 量级,台架人工比对观测不到)、seq 只增、§4.5 布局与字段偏移
|
||||
# (104 与 UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总
|
||||
# (削顶位图要等两点标定才真实出现)。主机侧以 #include 原样纳入
|
||||
# (卡 9/UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总
|
||||
# (削顶位图要等卡 12 标定才真实出现)。主机侧以 #include 原样纳入
|
||||
# meas/phasor/sample_ring 生产源,合成 9 通道帧序列一次性钉死;
|
||||
# 读数与注入源比对类判据仍归台架(实测采样类)。files 覆盖被测源、
|
||||
# 同链核(phasor/sample_ring)、编排方 adc_task 与 harness 本身。
|
||||
@@ -429,7 +398,7 @@ repos:
|
||||
files: ^(App/algo/meas\.(c|h)|App/algo/(phasor|sample_ring)\.(c|h)|App/task/adc_task\.c|test/host/meas/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h5) 运行参数段(「数据可信」道D 交付项)主机侧单元测试(2026-10-25 加)
|
||||
# 7h5) 运行参数段(「数据可信」卡 10,道D)主机侧单元测试(2026-10-25 加)
|
||||
# 死区把守突发上报的量、104 整定段是现场唯一远程写入口:注册契约、
|
||||
# 分档量程守卫、两段式、系数直通的合法带与生效值回显必须离线钉死。
|
||||
- id: run-par-unit-test
|
||||
@@ -440,7 +409,7 @@ repos:
|
||||
pass_filenames: false
|
||||
|
||||
|
||||
# 7i) 保护定值模型(定值校验和载体)主机侧单元测试(2026-10-11 加)
|
||||
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
|
||||
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
|
||||
# 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在
|
||||
# 任何元件接线之前钉死——板端无法穷举越界定值注入,主机侧毫秒级全跑。
|
||||
@@ -451,7 +420,7 @@ repos:
|
||||
files: ^(App/algo/prot_set\.(c|h)|test/host/prot_set/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7j) 保护元件框架(保护判据 P1 子集)主机侧单元测试(2026-10-11 加)
|
||||
# 7j) 保护元件框架(L3 P1 子集)主机侧单元测试(2026-10-11 加)
|
||||
# 判据核(滞回/定时限/欠量方向/闭锁联动/时基回绕)是纯逻辑,
|
||||
# 板端复现需要标准源 + 故障注入,主机侧用合成测量序列即可穷举
|
||||
# 1.2×/0.9× 验收口径(规划 §8)。
|
||||
@@ -462,10 +431,10 @@ repos:
|
||||
files: ^(App/algo/prot_elm\.(c|h)|App/algo/prot_set\.(c|h)|App/algo/phasor\.(c|h)|App/algo/freq\.(c|h)|test/host/prot_elm/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7j2) 频率测量层(测频与遥测快照,B8)主机侧单元测试(2026-10-12 加)
|
||||
# 7j2) 频率测量层(L2.5,B8)主机侧单元测试(2026-10-12 加)
|
||||
# 自适应窗测频(K_eff 对齐负序泄漏纹波周期)的 J1~J6 判据
|
||||
# (扫频/噪声/收敛/阶跃/浮空 valid=0/钳制恢复)是纯合成信号
|
||||
# 可穷举的精度契约;板端无标准源照不出来(规划 §3.3 过频 / 低频)。
|
||||
# 可穷举的精度契约;板端无标准源照不出来(规划 §3.3 L3-5)。
|
||||
- id: freq-unit-test
|
||||
name: 频率测量单测(主机侧)
|
||||
entry: python test/host/freq/run_unit_test.py
|
||||
@@ -498,7 +467,7 @@ repos:
|
||||
|
||||
# 7m) IEC 60870-5-104 被控站协议栈主机侧单元测试(2026-10-12 加,B4)
|
||||
# APCI 重组/STARTDT/总召分片/COV/k-w-t1~t3/遥控定值两段式/C_CS/错误
|
||||
# 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为主机侧纯逻辑单测全离线
|
||||
# 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为 L0.5 全离线
|
||||
# 穷举设计(分层约束);板端复现这些分支要真主站+真等待。
|
||||
# 触发面含数据源依赖(prot_set/prot_elm/phasor/sample_ring)——测试
|
||||
# 直接 #include 这些生产源,签名变了要立刻在这里暴露。
|
||||
@@ -506,7 +475,7 @@ repos:
|
||||
name: IEC104 协议栈单测(主机侧)
|
||||
entry: python test/host/iec104/run_unit_test.py
|
||||
language: system
|
||||
# 运行参数段落地起本 TU 链入真实 cfg_file/adc_cal/run_par(板端真源链),触发面同步扩
|
||||
# 卡 10 起本 TU 链入真实 cfg_file/adc_cal/run_par(板端真源链),触发面同步扩
|
||||
files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq|meas|adc_cal|run_par)\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
@@ -514,7 +483,7 @@ repos:
|
||||
# 64 B 定长记录(类型/seq/CP56Time2a/epoch/校验和)是磁盘上的
|
||||
# 字节级契约——编解码错了要 FTP 下载+解码才看得见;纯 codec
|
||||
# 零平台依赖(soe_store 胶水 SIM_EXEMPT),主机侧穷举往返/
|
||||
# 截断/坏校验/IV 位/seq 连续性(规划 §3.4 出口与记录)。
|
||||
# 截断/坏校验/IV 位/seq 连续性(规划 §3.4 L4)。
|
||||
- id: soe-unit-test
|
||||
name: SOE 动作记录编解码单测(主机侧)
|
||||
entry: python test/host/soe/run_unit_test.py
|
||||
@@ -672,7 +641,7 @@ repos:
|
||||
# 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看,
|
||||
# 连续两次失败才当真(不许直接 --no-verify 绕过)。
|
||||
- id: ui-golden
|
||||
name: UI 像素回归(菜单 8 状态 + 内容页 66 张 黄金参考)
|
||||
name: UI 像素回归(菜单 8 状态 + 内容页 40 页 黄金参考)
|
||||
entry: python scripts/check_ui_golden.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$)
|
||||
@@ -682,7 +651,7 @@ repos:
|
||||
# 背景:大窗实测——靠预置键序盲走到深页时,9 帧里只有 3 帧真是它声称的态;
|
||||
# KEY 的 ACK 是空应答、不携带状态,所以**画面是唯一事实源**。判据脚本
|
||||
# scripts/board_ui_nav.py 据此改成闭环:每按一键重新认屏再决定下一个键。
|
||||
# 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(接线当时 18 张、现 66 张黄金参考 +
|
||||
# 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(18 张黄金参考 +
|
||||
# 真机留档帧,目录不在版本控制里、缺就整段跳过),秒级、无构建产物依赖。
|
||||
# ⚠ 不挂完整 --selftest:第三段闭环需要 lvgl_sim.exe(构建产物,新工作树里没有)
|
||||
# —— 正是同一天 mem-numbers 缺 .map 那课的形状;脚本对缺 exe 报 FAIL 并给出
|
||||
@@ -698,17 +667,32 @@ repos:
|
||||
files: ^(scripts/board_ui_nav\.py$|simulator/tests/ref/.*\.bin$)
|
||||
pass_filenames: false
|
||||
|
||||
# 13) 编号门禁的历史:2026-10-20 起曾有一版「全库语料审计」(规范 §8 的 8 项校验 +
|
||||
# 99 族 registry + 别名表)。它 2026-10-28 退役——维护成本高过收益(近 80 笔提交
|
||||
# 61 笔被编号体系牵连),脚本与语料整体移入 `scripts/archive/id_gate/`
|
||||
# (保留可复现性,**不再进提交门禁**)。退役路线与判据替换见规范 §8.5。
|
||||
|
||||
# 14) 编号门禁(2026-10-28 上线只查新增行那一道;2026-10-29 用户裁定**整体退役**,钩子已摘)
|
||||
# 退役后编号形态没有机器面:判据回到 docs/编号规范.md §1 三句自问 + §4.1 族总表 + 评审。
|
||||
# 脚本移入 scripts/archive/id_gate/check_ids_new.py(手工可跑,--self-test 保留),留档说明
|
||||
# → docs/门禁说明/id-check-new.md;手工普查与恢复机器面 → docs/编号规范.md §8.5。
|
||||
# ⚠ 计数口径:本条摘除使 YAML 解析条目 52 → **51**,须与 AGENTS.md 的「共 N 个钩子」、
|
||||
# docs/门禁说明/README.md §2 标题三数同源同批改(自查命令见该条;别在注释里写数钩子的字面量)。
|
||||
# 13) 编号门禁(2026-10-20 加,docs/编号规范.md §8 的 8 项校验)
|
||||
# 背景:§1.2 用仓内证据列了 10 类编号事故(同字母多义 / 跨文档同号不同义 /
|
||||
# 销项删行后残留悬空引用 / 自造前缀 / 会话临时号入册…),而 36 个钩子里
|
||||
# **只有 TC 一族**有机械检查(check_test_traceability.py,且未挂钩子、正则还漏 [示例]
|
||||
# `TC-AI-01A` [示例] 这类大写后缀)——E/N/S/B/C/D/A/U/O/gen/Trap 全部裸奔。本钩子把
|
||||
# 规范 §8 的 8 项机械化:配置两份 `scripts/id_registry.json`(前缀 → 属主登记处
|
||||
# /形态/冻结/游标,逐项来自规范 §4 总表)+ `scripts/id_aliases.json`(旧号→新号,
|
||||
# 照搬 section_aliases.json 的成功范式)。
|
||||
# 判据要点:「定义」= 字面量出现在该族**属主登记处文档**里;于是 §5.5 的
|
||||
# 「销项 = 删行 + 清全库引用」只做一半,校验 3 立刻报(销项号被重新取走那类撞号正是这条)。
|
||||
# 盲区显式登记在 registry 的 `blind_spots` / `unmanaged_bare`:裸形同字母多义
|
||||
# (§1.2:P1 五义 / L1 四义 / UI 状态机号四义)与元器件位号同形(§4.4),机械全库判定
|
||||
# 必然一边倒误报 → 裸形族只在属主文档内判越游标、文档外降告警;收口属 §9 阶段
|
||||
# 1-② 的迁移批次。**不是「检查通过」,是记在案上的盲区。**
|
||||
# ⚠ 当前 warn-only(§8 上线节奏:先清 backlog → 清零后 entry 加 --strict 拦提交,
|
||||
# 与 doc-links 章节校验同一条路径)。校验 8 的「可判定比例」跌破
|
||||
# id_registry.json 里的基线时**任何模式都失败**——那根线就是防回退的本体。
|
||||
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有能改编号的触发面
|
||||
# (任意 md/c/h/py/yaml/json/txt + 本配置文件);扫描范围恒为全库
|
||||
# (悬空/孤儿/重复定义是跨文件判定,pass_filenames: false)。
|
||||
- id: id-check
|
||||
name: 编号门禁(docs/编号规范.md §8 八项校验)
|
||||
entry: python scripts/check_ids.py --strict
|
||||
language: system
|
||||
files: \.(md|c|h|py|ya?ml|json|txt)$|^\.pre-commit-config\.yaml$
|
||||
pass_filenames: false
|
||||
|
||||
# 14) 分层方向门禁(2026-10-22 加,结构债的"防扩大"那一半)
|
||||
# 背景:AGENTS.md 与 项目架构总览.md 早就写着"六层单向依赖",但**从来没有机械检查**
|
||||
@@ -749,29 +733,24 @@ repos:
|
||||
# 为什么现在才挂:接线前本文件只有 16 个 *-unit-test 钩子,而仓库已有 28 套主机单测——
|
||||
# P1 新增的 12 套不在任何提交钩子里。原先"再加一份全量插桩等于同一件事做两遍"
|
||||
# 这条理由到现在已经**反了**(现状是有一遍没做),所以必须挂;缺口此前如实登记在
|
||||
# docs/测试说明/单元测试/主机单测覆盖率口径.md §8。
|
||||
# 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子当时跑**全部 28 套**(现值 47 套,台账见
|
||||
# docs/门禁说明/host-unit-tests.md §6 第 4 条),并且守 §5 的
|
||||
# docs/测试说明/主机单测覆盖率口径.md §8。
|
||||
# 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子跑**全部 28 套**,并且守 §5 的
|
||||
# 回退台账(某套被删、或某文件的已覆盖行变少 → [REGRESS] 直接红)。这是 per-module
|
||||
# 钩子做不到的——删掉一套测试,它那根钩子只会因为"没有文件要检查"而静默跳过。
|
||||
# ⚠ FAIL 语义(用户裁定 2026-10-24):找不到 mscoverage 时脚本退出码 2 → 本钩子**失败**,
|
||||
# 不做 skip。理由:skip 等于造一根会静默失效的门禁(与 pre-commit 4.x 移除 always: true
|
||||
# 是同一类坑);宁可在没装组件的机器上被挡一次、看清 [ENV] 那行提示,也不要一根假绿的闸。
|
||||
# 本机与 CI runner 同一台;实测两份 VS 安装都自带该 exe(口径 §7 第 8 条已按实测更正)。
|
||||
# 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/单元测试/主机单测覆盖率口径.md,本处只留接线事实。
|
||||
# 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/主机单测覆盖率口径.md,本处只留接线事实。
|
||||
# 触发面**含门禁自身**(.pre-commit-config.yaml):不带自身就等于谁改坏这根闸的 files:
|
||||
# 模式,它就再也不跑且无人察觉(同 lv-conf-sync / bsp-err 的既有做法)。
|
||||
# ⚠ stages: [manual](2026-10-28 用户裁定):本钩子跑光全部 47 套单测(41 套时实测 ~105 s,套件数
|
||||
# 涨了耗时就涨,47 套这轮未重测耗时;
|
||||
# 占一次带代码提交耗时的三分之二)→ 不压在提交期,由**远端 CI** 强制
|
||||
# (.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),本地等价入口 =
|
||||
# scripts/ci_local.py(manual 段);手工复跑:
|
||||
# python -m pre_commit run --all-files --hook-stage manual test-coverage
|
||||
# 另:此前它没写 stages: → 默认属于所有阶段,同一次提交输出里出现两次(commit-msg
|
||||
# 阶段那遍几乎必然 Skipped,只看结尾会把实跑读成"没跑")。固定 manual 阶段后不再有这一坑。
|
||||
# ⚠ 读输出的坑(2026-10-24 连踩两次才定位):本钩子没写 stages: → 默认属于**所有阶段**,
|
||||
# 于是同一次提交的输出里它出现两次:pre-commit 阶段那一遍才可能实跑;commit-msg 阶段
|
||||
# 那一遍的"文件"是提交信息文件,几乎必然 (no files to check)Skipped。**只看输出结尾会
|
||||
# 把"实跑通过"读成"根本没跑"**——判它跑没跑要认**第一次**出现的那一行。
|
||||
# 实跑证据:只改本文件的提交(e327636 / c4ca292)里 test-coverage 与 bsp-err 都是 Passed。
|
||||
- id: test-coverage
|
||||
name: 主机单测覆盖率(增量拦截:已覆盖行只增不减)
|
||||
stages: [manual]
|
||||
entry: python scripts/check_test_coverage.py
|
||||
language: system
|
||||
files: ^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$)
|
||||
@@ -798,7 +777,7 @@ repos:
|
||||
# 交实测解到 C:/Python314(无 fontTools)→ 字体链报 No module named fontTools = 假红。所以脚本
|
||||
# 自己按「显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH」解析并**逐个探针校验**(要真能
|
||||
# import fontTools 才算数),整轮统一用同一个解释器;挑不出来打 [ENV]+候选表+修法、退出码 2 不
|
||||
# SKIP。不把 entry 写死成 .venv 路径:那是 ci_local 2026-10-11「候选收窄 → runner 上静态门禁全灭」
|
||||
# SKIP。不把 entry 写死成 .venv 路径:那是 ci_local 2026-10-11「候选收窄 → runner 上 L0 全灭」
|
||||
# 的同一条坑(详解 docs/门禁说明/generated-fresh.md §4)。
|
||||
# ⚠ 字体链外部工具(node + 全局 lv_font_conv + 系统 simsun.ttc / seguisym.ttf)缺失时该项 SKIP 并
|
||||
# 打印 [SKIP],--strict-env 才升 FAIL——与 test-coverage「缺 mscoverage 必 FAIL」刻意不同:
|
||||
@@ -812,54 +791,3 @@ repos:
|
||||
language: system
|
||||
files: ^(tools/.+|Drivers/BSP/UI/.+|docs/images/.*\.svg|App/task/ui_task\.(c|h)|ui_menu_tree\.txt|ui_cn_chars\.txt|MDK-ARM/STM32F407-Demo\.uvprojx|simulator/CMakeLists\.txt|scripts/check_generated_fresh\.py|scripts/check_font_charset\.py|\.pre-commit-config\.yaml)$
|
||||
pass_filenames: false
|
||||
# ── 块注释嵌套(第 51 条,2026-10-27 加,task-29)─────────────────────────────
|
||||
# 查什么:C/C++ 块注释体内再出现 /* —— ARMCC 诊断 9-D「nested comment is not allowed」。
|
||||
# 本仓的诱因是「在注释里写作用域 / 通配路径」这种常态写法:Drivers/BSP/UI/** 里那个
|
||||
# /* 就是第二次注释的起点(C 的块注释不嵌套)。
|
||||
# 为什么要闸:它**只在重编译那个 TU 时才暴露**,历史「0 警告」不代表安全;而且按包含者
|
||||
# 数量刷屏——一份被广泛 include 的头里一处嵌套 = 每个包含者一行警告(实测一例扩散到 16
|
||||
# 条,把整批真警告挤到看不见)。有了这根,提交时就报,不用等 /keil-build。
|
||||
# 接线当日实证(冻结引用,别去翻构建日志——它每次 /keil-build 就被覆盖、也不入库):当日 02:22
|
||||
# 那次构建日志报过 ..\App\algo\run_par.c(267): warning: #9-D: nested comment is not
|
||||
# allowed ——那是别人**未提交**的在飞状态。原文抄在 docs/门禁说明/nested-comment.md §2.1;
|
||||
# 已提交历史回放 281 份旧字节 0 命中 ⇒ 这类问题一直靠人工在编译后清,从没被机器拦过。
|
||||
# 存量基线(只减不增,除这 2 处外**零豁免**):全仓 --all 实扫 403 个 C/H,新发 0,存量
|
||||
# 2 处 = test/host/common/ch395f_fake.h:7 与 fatfs_fake.h:9(注释里写 test/host/**,属
|
||||
# test/ 写域,等那侧改成语义短语;还清后删 BASELINE 行,--prune 打台账)。
|
||||
# 自证:--selftest 四项(正例照得出 / 字符串与行注释不误报 / 基线吞得下 / 只减不增有牙),
|
||||
# 另有 10 项形态电池(含「注释里写 UI/*」「块注释内行首再开 /*」等真形态)。
|
||||
# ⚠ 计数口径:本条使 YAML 解析条目 50 → **51**;配置里那条被注释掉的 clang-format 候选仍
|
||||
# 在,所以**按文本 grep 声明行会得 52**(多算那条候选)——那不是口径。本行刻意不复现那个字面量,
|
||||
# 否则注释自己也会被 grep 数进去(2026-10-27 实测踩过:写过一次,计数立刻虚高 1)。
|
||||
# 口径与自查命令见 docs/门禁说明/README.md §2 首条。
|
||||
- id: nested-comment
|
||||
name: "块注释体内不得再出现 /*(ARMCC 9-D 嵌套注释)"
|
||||
entry: python scripts/check_nested_comment.py
|
||||
language: system
|
||||
types: [c]
|
||||
|
||||
# ─── SKILL.md 体积与 reference/ 指针门禁(第 52 条,2026-10-29 加)─────────────────────
|
||||
# 为什么要有:dsh-mnemon 插件对技能正文有 65536 字节的**写侧**硬上限(validateSkillContent),
|
||||
# 但**加载侧不查**——超限的 SKILL.md 照常可读、照常进技能目录,只有一件事会坏:自进化每次
|
||||
# 「生成草案」都在落盘那一步被拒,那一行永久卡在「生成中」、界面只剩转圈、连「忽略」都不出现
|
||||
# (Draft/Dismiss 只在 pending 渲染)。实测 2026-10-29 一天内两个技能各卡一次(第二个 72,601
|
||||
# 字节),每次白烧一个 30 分钟的草案 Agent。**在加这根之前,仓库里没有任何一处会告诉你某个
|
||||
# 技能已经废了**——文件能读、能改、能提交,全绿。
|
||||
# 判据:> 65536 FAIL(与插件同数、同口径:utf-8 字节数不是字符数);>= 49152(75%)WARN 不拦
|
||||
# (越线通常只隔一次草案采纳:实测 49821 → 62806);正文里 (reference/xxx.md) 指向的文件不在
|
||||
# → FAIL;带 UTF-8 BOM → FAIL(插件按 utf-8 解析,BOM 会让 frontmatter 读歪)。
|
||||
# 全树扫不只看变更文件:钩子拿到 filenames 只当触发用,判定始终扫整个 .dsh/skills/——因为改
|
||||
# reference/*.md 时 SKILL.md 没动,但它可能正是被那次外置撑过线/掏空的那一份。
|
||||
# 为什么 entry 敢用裸 python:AGENTS.md「钩子 entry 的裸 python 不等于 .venv」那条教训针对的是
|
||||
# 第三方依赖(fontTools);本脚本纯标准库,任意 python3 均可(实测 .venv 3.12 与 PATH 上 3.14
|
||||
# 都跑通),所以不需要脚本内再自解析解释器。
|
||||
# ⚠ 盲区:只管仓库内 .dsh/skills/;用户级 ~/.dsh/skills/ 不在仓库里,本钩子照不到(手工
|
||||
# `node "$HOME/.dsh/scripts/mnemon-skill-ledger-doctor.mjs" --root .` 或本脚本 --root 都只报,
|
||||
# 要拦得另配 shell 钩子)。只认 markdown 链接形态的指针,纯文本里写的路径不算。
|
||||
# 自证:--selftest 五形态(干净 0 / 75% 线 0 带 WARN / 超上限 1 / 悬空指针+BOM 1 / 根不存在 2)。
|
||||
# 详解与限制 → docs/门禁说明/skill-size.md。
|
||||
- id: skill-size
|
||||
name: "SKILL.md 不超插件写侧上限 65536 且 reference/ 指针不悬空"
|
||||
entry: python scripts/check_skill_size.py
|
||||
language: system
|
||||
files: ^\.dsh/skills/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"portName": "COM3",
|
||||
"portName": "COM6",
|
||||
"baudRate": 115200,
|
||||
|
||||
"flash_cmd": ".venv\\Scripts\\python.exe .dsh\\skills\\stlink-flash\\scripts\\stlink_flash.py flash",
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ FreeRTOS/CMSIS-RTOS API**(见 `AGENTS.md`「平台约束」)。
|
||||
| `app/` | **应用入口与全局**:`app_main.c/h`(初始化入口)、`app_cfg.h`(裁剪决策占位说明,当前无宏)、`sys_events.h`(就绪事件) | [`app/README.md`](app/README.md) |
|
||||
| `cfg/` | **配置引擎**:`/config.ini` 解析 + 原子持久化(旧二进制档已废弃,2026-10-10) | [`cfg/README.md`](cfg/README.md) |
|
||||
| `sync/` | **时间同步任务**:`time_sync.c/h` | [`sync/README.md`](sync/README.md) |
|
||||
| `algo/` | **采集与算法层**:语义取数 + 有效性判定 + 采样环/相量/测频 + 定值模型 + 保护元件框架 | [`algo/README.md`](algo/README.md)(索引);设计 → `docs/算法/采集与算法层设计说明.md` |
|
||||
| `algo/` | **采集与算法层(L1/L2/L3)**:语义取数 + 有效性判定 + 采样环/相量/测频 + 定值模型 + 保护元件框架 | [`algo/README.md`](algo/README.md)(索引);设计 → `docs/算法/采集与算法层设计说明.md` |
|
||||
| `log/` | **日志子系统**:syslog(NAND)+ 控制台单写者日志 | [`log/README.md`](log/README.md) |
|
||||
| `task/` | **FreeRTOS 任务实现**(6 个任务) | [`task/README.md`](task/README.md) |
|
||||
| `util/` | **通用工具**:CRC / 环形缓冲 / 墙钟时间 | [`util/README.md`](util/README.md) |
|
||||
|
||||
+39
-129
@@ -1,4 +1,4 @@
|
||||
# App/algo/ — 采集与算法层(语义取数与有效性 / 全周傅氏相量 / 保护判据)
|
||||
# App/algo/ — 采集与算法层(L1 / L2 / L3)
|
||||
|
||||
> 本目录的**索引**:文件清单、依赖、逐文件的"改代码必须知道"、构建与测试入口、新增模块动作。
|
||||
> **详细设计——机制、契约、判据、为什么这样设计 → `docs/算法/采集与算法层设计说明.md`**(本文不复制其内容)。
|
||||
@@ -9,25 +9,21 @@
|
||||
|
||||
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|
||||
|---|---|---|---|---|
|
||||
| `adc_input.h` / `.c` | 语义取数与有效性 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | TPAFE 快照消费 / STM32 ADC1 启动与扫描 / 通道语义映射 |
|
||||
| `adc_health.h` / `.c` | 语义取数与有效性 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);「采样通道自检」残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | 「采样有效性校验」/「采样通道自检」(残余) |
|
||||
| `adc_cal.h` / `.c` | 标定折算 | 通道标定折算(「数据可信」标定折算与落盘整定):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | 「通道标定(一次系数 + 零漂)」(M4 半边) |
|
||||
| `meas.h` / `.c` | 全周傅氏相量发布 | 遥测快照(「数据可信」遥测快照发布,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(104 改读快照与频率点绑定、UI 是读者) |
|
||||
| `sample_ring.h` / `.c` | 全周傅氏相量输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | 全周傅氏相量输入(为「每 1 ms 滑动窗刷新」供数) |
|
||||
| `phasor.h` / `.c` | 全周傅氏相量 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | 全周傅氏系数表 / 基波相量实例 / 相量有效值输出 / 每 1 ms 滑动窗刷新 / 双区指针原子交换 |
|
||||
| `freq.h` / `.c` | 全周傅氏相量的派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | 「过频 / 低频」测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
|
||||
| `prot_set.h` / `.c` | 保护判据输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | 「定值校验和」载体 |
|
||||
| `prot_elm.h` / `.c` | 保护判据 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | 「三段式相间过流」/「过负荷」/「过压 / 欠压」/「过频 / 低频」/「零序过压」(软件侧) |
|
||||
| `wave_ring.h` / `.c` | 故障录波数据面 | 录波环与冻结状态机:int16 × 320 点 × 8 路按通道分列,触发即冻结写指针、后触发计满 160 点封段、落盘回执才解冻、冷却期只加溢出计数 | `wave_ring_push()` `wave_ring_tick()` `wave_ring_request()` `wave_ring_sealed()` `wave_ring_read_ch()` | 「故障录波」数据面(环 + 触发冻结语义) |
|
||||
| `wave_store.h` / `.c` | 故障录波段格式 | 段文件字节层的编解码与分次搬运游标:段头 + 通道表 + 两处 CRC-16/MODBUS + 段名轮转与反解 + 整段自检;不碰文件系统 | `wave_store_crc()` `wave_store_head_build()` `wave_store_head_parse()` `wave_store_seg_check()` `wave_store_pack_next()` `wave_store_seg_path()` | 「故障录波」段格式(布局 + 自检) |
|
||||
| `adc_input.h` / `.c` | L1 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);L5-2 残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | L1-6 / L5-2(残余) |
|
||||
| `adc_cal.h` / `.c` | L1.5 | 通道标定折算(「数据可信」卡 2/卡 4):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | L1-5(M4 半边) |
|
||||
| `meas.h` / `.c` | L2 发布 | 遥测快照(「数据可信」卡 8,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(卡 9/11 与 UI 是读者) |
|
||||
| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) |
|
||||
| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
|
||||
| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
|
||||
| `prot_set.h` / `.c` | L3 输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | L5-1 载体 |
|
||||
| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
|
||||
|
||||
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `adc_cal`(另依赖 `cfg_file.h` 接口,
|
||||
纯头文件级)← `sample_ring` ← `phasor` ← `freq`;`meas` ← `sample_ring` + `phasor`
|
||||
(仅 `phasor_calc` 纯核;频率/品质由调用方注入,**不链** `freq`/`adc_cal`);
|
||||
`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。
|
||||
`wave_store` **独立**(只用 `stdint.h` 与 `stddef.h`,编码与解码共用同一个字段校验函数);`wave_ring` ← `wave_store`(只取段头类型与返回码;**不链** `adc_cal`——封存瞬间的系数由
|
||||
adcTask 现取现填后交棒,本层不复制第二个事实源)。两份录波文件同样零平台依赖、不碰文件系统。
|
||||
`adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 语义取数与有效性实现(「采样通道自检」贴轨门限由调用方
|
||||
`adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 L1 实现(L5-2 贴轨门限由调用方
|
||||
从 `adc_cal_rail_ths()` 取,本层仍零算法依赖)。
|
||||
|
||||
**除 `adc_input.c` 外全部零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头)
|
||||
@@ -36,62 +32,57 @@ adcTask 现取现填后交棒,本层不复制第二个事实源)。两份录
|
||||
|
||||
## 2. 一拍的数据流(一眼版)
|
||||
|
||||
`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(标定折算折算,现口径 =
|
||||
ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」,码值域)→ `sample_ring_push_frame()`
|
||||
→ `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 语义取数与有效性健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
|
||||
`prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,即「数据可信」遥测快照发布)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
|
||||
`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(L1.5 折算,现口径 =
|
||||
ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `sample_ring_push_frame()`
|
||||
→ `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
|
||||
`prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,卡 8)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
|
||||
完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。
|
||||
|
||||
录波支路(与上面这条相量主线**并行**、同拍只多走两步,互不阻塞):`adc_input_raw()` 取 8 路 →
|
||||
`wave_ring_push()` → `wave_ring_tick()`(触发即冻结写指针,后触发计满 160 点封段)→ defaultTask
|
||||
只见 `wave_ring_sealed()` 与已封存区读口 → 游标分次搬运落盘 → `wave_ring_write_done()` 才解冻。
|
||||
→ docs/子系统设计/故障录波设计说明.md §4.2 与 §5.1。
|
||||
|
||||
## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档)
|
||||
|
||||
> **改本目录前先记五条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做
|
||||
> 相位 / 功率 / 矢量运算(双转换器无共同采样触发);③ 帧值已是 标定折算工程量,但 **未标定(`adc_cal_flags()` 无
|
||||
> 相位 / 功率 / 矢量运算(双转换器无共同采样触发);③ 帧值已是 L1.5 工程量,但 **未标定(`adc_cal_flags()` 无
|
||||
> VALID)时单位只是 ADC 引脚电压**,不是端子二次值——下游消费精度判据见「数据可信」§4.2 三态
|
||||
> 表;④ 工程量 → 帧的唯一折算点是 `adc_cal_apply()`,禁止旁路再乘系数;⑤ 本层不建任务、
|
||||
> 不等事件、不写日志(`adc_cal` 的 `[ERR]` 哨兵留痕是唯一例外,且每通道每开机一次),
|
||||
> `*_init()` 才可在内核前调。
|
||||
> 全文 → `docs/算法/采集与算法层设计说明.md` §10。
|
||||
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — 语义取数与有效性语义取数
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 语义取数
|
||||
- 语义映射表 `k_adc_map[]` 的**内容**唯一权威是 `docs/硬件平台/ADC通道映射说明.md`;V0.2 改版只改表内容,API 不动。
|
||||
- `adc_input_poll()` **仅任务上下文**(TPAFE 快照是临界区);STM32 侧固定滞后 1 拍。
|
||||
- 四类 STM32 计数(`adc1_seq` / `adc1_gap` / `adc1_err` / `adc1_hw`)成因不同,**勿混读**。
|
||||
- 本目录**唯一**允许出现 HAL 调用的文件。→ `docs/算法/采集与算法层设计说明.md` §3。
|
||||
|
||||
### 3.2 `sample_ring.c` / `.h` — 全周傅氏相量输入采样环
|
||||
### 3.2 `sample_ring.c` / `.h` — L2 输入采样环
|
||||
- 整帧写入后才推进写指针 → 取到的窗永远落在整帧边界。
|
||||
- 本拍取数失败 → **整拍跳过入环**(`adc_task.c` 的 `skip`),**绝不补 0 点**。
|
||||
- 不做跨通道运算、不做丢帧判定(只出 `frames()` 计数)。→ `docs/算法/采集与算法层设计说明.md` §5。
|
||||
|
||||
### 3.3 `phasor.c` / `.h` — 全周傅氏相量计算
|
||||
### 3.3 `phasor.c` / `.h` — L2 相量计算
|
||||
- 系数表预乘 √2/N → `fAmSQ` **直接是有效值**(口径 `docs/算法/傅氏算法详解.md` §2.3)。
|
||||
- 实例表 `k_inst[]` 数据驱动:**新增实例只在表尾加一行**。
|
||||
- 任一实例不足一周波 → **本拍整体不产出**(不切使用区);每拍 20 点**全部重算**(不做增量递推)。
|
||||
- → `docs/算法/采集与算法层设计说明.md` §6。
|
||||
|
||||
### 3.4 `adc_health.c` / `.h` — 语义取数与有效性判定
|
||||
### 3.4 `adc_health.c` / `.h` — L1 有效性判定
|
||||
- 三类位**严格分开**:电平(可自愈)/ 锁存(必留痕)/ 边沿事件(仅当拍);**静默 ≠ 丢帧**。
|
||||
- **gap 不告警**(消费者超拍是已知现象);告警编排在 `adc_task.c`,不在本层。
|
||||
- 首拍 `check()` 只建基线。→ `docs/算法/采集与算法层设计说明.md` §4。
|
||||
|
||||
### 3.5 `prot_set.c` / `.h` — 保护定值模型(「定值校验和」载体)
|
||||
### 3.5 `prot_set.c` / `.h` — 保护定值模型(L5-1 载体)
|
||||
- 装载 **all-or-nothing**:任一项越界整表不装载,越界项由 `prot_set_bad_mask()` 指认。
|
||||
- 批量 `prot_set_set()` 后**必须** `prot_set_seal()`;单项原子写用 `prot_set_set_sealed()`。
|
||||
- 定值项 / 实例**只许尾部追加**(枚举序 = 数组序 = 校验和覆盖序)。
|
||||
- blob **换代(返回 2)≠ 损坏(返回负)**:前者打 INFO、不记自检。→ `docs/算法/采集与算法层设计说明.md` §8、§12.3。
|
||||
|
||||
### 3.6 `prot_elm.c` / `.h` — 保护判据元件框架
|
||||
### 3.6 `prot_elm.c` / `.h` — L3 保护元件框架
|
||||
- 数据缺失两种处理**刻意不同**:相量类任一未就绪 → 整拍跳过;频率类无有效读数 → 按方向填"必不启动"值,**不整拍跳过**。
|
||||
- `prot_set_locked()` 或投退 = 0 → 元件整体复位不判据;跨上下文复归走 `prot_elm_request_reset()`。
|
||||
- 零序过流(IN 走 STM32)**故意不在实例表**;OF / UF 无独立 104 遥信点,只经 YX15 总聚合可见。
|
||||
- `prot_elm_init()` 须在 `phasor_init()` 之后。→ `docs/算法/采集与算法层设计说明.md` §9。
|
||||
|
||||
### 3.7 `freq.c` / `.h` — 频率测量(「过频 / 低频」的测量输入)
|
||||
### 3.7 `freq.c` / `.h` — 频率测量(L3-5 的测量输入)
|
||||
- `freq_run()` **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调;保护口径 =
|
||||
`freq_get() == 0 && freq_valid() == 1`。
|
||||
- 精度靠**自适应窗长**(窗长 = 泄漏纹波周期的整数倍);定长平均压不到 0.02 Hz,
|
||||
@@ -99,104 +90,48 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」
|
||||
- 源切换即废相位基准重建(UA / IA 无共同相位基准,即双转换器无共同采样触发)。
|
||||
- 判据 **J1~J6** 与实测口径 → `docs/算法/采集与算法层设计说明.md` §7.2(与 `test/host/freq/freq_test.c` 文件头同源)。
|
||||
|
||||
### 3.8 `adc_cal.c` / `.h` — 标定折算通道标定折算(「数据可信」标定折算与落盘整定)
|
||||
### 3.8 `adc_cal.c` / `.h` — L1.5 通道标定折算(「数据可信」卡 2 / 卡 4)
|
||||
- 折算模型:工程量 = (code + b) × k;**k 落盘用 int32 Q16(µV/码 × 65536)**、b 用
|
||||
int32 码值(负零点靠 `CFG_VAL_INT`)——单位与定点格式的选型依据见
|
||||
`docs/任务规划/数据可信.md` §4.1,勿改回 V/码 直存(Q16 只剩 20 个刻度)。
|
||||
- **ROM 默认 = 量程口径冻结定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码,
|
||||
- **ROM 默认 = 卡 1 定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码,
|
||||
`docs/硬件平台/ADC通道映射说明.md` §7.1);表下标 = 语义枚举(UA0…IN7),
|
||||
V0.2 改版与本目录 `adc_input` 映射表同步改。
|
||||
- 三态可用性 `adc_cal_flags()`:VALID(已按变比标定,二次值成立——端子变比确认与两点标定前恒 0)
|
||||
- 三态可用性 `adc_cal_flags()`:VALID(已按变比标定,二次值成立——卡 0b/12 前恒 0)
|
||||
/ CLIPPED(运行期按来源派生,V0.2 后自动清零)。**消费者判「能不能信」只许查它**,
|
||||
`adc_cal_apply()` 不返回可用性(「数据可信」§8 风险表)。
|
||||
- 合法带哨兵:k ∉ ROM×[0.5,2.0] 或 |b| > 8192 → 折算回退 ROM + 每通道每开机一条
|
||||
`[ERR]` + VALID 被压制(**即便文件位图说已标定**);这层兜底同时挡坏文件与坏标定。
|
||||
- `adc_cal_set()` / `adc_cal_two_point()`:整定与两点公式的算术核已齐备(单测还原合成
|
||||
k/b);**台架两点标定待实测**,本层不含注入。
|
||||
k/b);**台架两点标定(卡 12)待实测**,本层不含注入。
|
||||
- cfg 两段 `[adc_cal]` / `[adc_cal_ofs]` **不挂回调**(RAM 副本即生效值,引擎 load/poll
|
||||
原地改写,逐点现读);注册在 `app_main_init()` 内核前完成,必须先于 `cfg_file_load`。
|
||||
|
||||
### 3.9 `meas.c` / `.h` — 遥测快照发布(全周傅氏相量发布侧,「数据可信」波次)
|
||||
### 3.9 `meas.c` / `.h` — 遥测快照发布(L2 发布侧,「数据可信」卡 8)
|
||||
- **唯一发布者 = adcTask**:`phasor_run()`/`freq_run()` 同拍、200 ms **tick 经过**分频
|
||||
调 `meas_tick()`(非拍计数——idle/skip 拍拉长名义节拍)。docs/任务规划/数据可信.md §8
|
||||
风险表"快照双写"行 = 评审判据:全仓 `meas_tick` 调用点数须为 1;iec104Task /
|
||||
uiTask 只读。
|
||||
- 值口径(第二轮「遥测快照发布」的 A-lite 裁决):**基波有效值瞬时透传**,与 phasor 使用区
|
||||
- 值口径(批 2 卡 8 A-lite 裁决):**基波有效值瞬时透传**,与 phasor 使用区
|
||||
**同拍同核逐位相等**(单测钉死);**不做** 4 点平均 / 谐波方和根——按
|
||||
docs/任务规划/数据可信.md §4.5"上送节拍非平均窗,再平均引入滞后";谐波 k-bin 轨
|
||||
(docs/算法/继电保护功能复现规划.md 「遥测谐波轨(可选)」)扩展只许结构尾部追加,
|
||||
(docs/算法/继电保护功能复现规划.md L2-8)扩展只许结构尾部追加,
|
||||
docs/任务规划/数据可信.md §4.5 字段序 = 协议(布局由单测按偏移钉死,72 B × 双区)。
|
||||
- 并发 = phasor 双区 + 单字节索引翻转(「双区指针原子交换」同型,无锁);seq 只增供消费者判换帧。
|
||||
- 并发 = phasor 双区 + 单字节索引翻转(L2-7 同型,无锁);seq 只增供消费者判换帧。
|
||||
- 品质注入:削顶位图唯一源 = `adc_cal_flags()`(口径 docs/任务规划/数据可信.md §4.2
|
||||
三态表;由 adc_task 编排注入,本层不链 `adc_cal`);TEMP = 最新一拍原始值透传 +
|
||||
`NT`(「数据可信」决策 5);`MEAS_QDS_*` 低字节与 104 QDS 线上字节同值
|
||||
(本层**不** include 协议层头——分层方向)。
|
||||
- 104 死区迁移 / 频率点绑定 = 104 改读快照那一波(道D 后续分支);读数与注入源比对 = **待台架实测**。
|
||||
|
||||
### 3.10 `wave_ring.c` / `wave_ring.h` — 录波环与冻结状态机(「故障录波」数据面)
|
||||
- **形态三条钉死**(动它之前读 docs/子系统设计/故障录波设计说明.md §6.2 与 §7):只存 `int16`
|
||||
原始码值(不做折算、不许 `float`)、**按通道分列**(不按帧交错——回放与搬运都是整列取)、
|
||||
`ch_count` 固定 8(TEMP 本波不录:未折算的非线性量进录波只多占字节不增信息)。环本体 5120 B,
|
||||
本层全部可写静态 5298 B,落 SRAM1(scatter 的 `RW_IRAM1` 收 `.ANY (+RW +ZI)`,代码里**没有**
|
||||
任何 section 属性;CCM 只放 FreeRTOS 堆,那 5 KB 既塞不进也不该塞)。
|
||||
- **冻结 ≠ 停采集**:触发冻住的是覆盖窗口(写指针停走),丢的只是录波样本,`drop_cnt` 的单位是
|
||||
「点」(一拍 8 路 = 8 点);`sample_ring` / 相量 / 保护在同拍照常跑,本层不碰它们。
|
||||
- **点序**:触发那一拍的样本落在段内点 159(预触发窗最后一点),点 160 起是触发后第一拍,两半
|
||||
各 160 点、不重叠;回放页横轴与 PC 脚本都按这条对齐(设计说明未钉死 ±1,本层按不重叠实现)。
|
||||
- **封存握手(顺序不许换,单测逐拍钉死)**:`wave_ring_tick()` 计满那一拍返回
|
||||
`WAVE_RING_EVT_SEALED` → adcTask 才去取标定事实并 `wave_ring_latch_meta()` → `wave_ring_sealed()`
|
||||
要求「状态 SEALED **且**已 latch」才返 1(两个标志并成一个,落盘道不可能拿半份快照落盘)→
|
||||
defaultTask 只读锁副本 → `wave_ring_write_done(0 / 非 0)` 是唯一解冻入口:成功才推进轮转号与段
|
||||
计数,失败只计 err 且**保持同号**(下一段重写同一个文件,盘上不留半截)。
|
||||
- **两条不许把环焊死的兜底**:SEALED 滞留超过 `WAVE_SEAL_TIMEOUT_MS`(2000 拍,待台架核)自弃本段
|
||||
并计 err;冷却窗 `WAVE_RING_COOL_MS`(500 拍,待台架核)**只在回执之后**计时——「落完解冻」用的是
|
||||
状态不是时间,回执前任何新触发只加 `ovf_cnt` 不重录。
|
||||
- **上下文归属直接长在函数命名上**(跨任务安全不靠约定):`wave_ring_init` / `push` / `tick` /
|
||||
`latch_meta` 只能在 adcTask(⚠ 严禁挂到 TIM3 回调:那是 ISR 且比调度器早约 100 ms,本层没有
|
||||
消费者就绪门);`wave_ring_write_done` 只能在 defaultTask;`request` / `state` / `sealed` /
|
||||
`sealed_meta` / `read_ch` / `read_cb` / `get_stat` / `col` 任意上下文可调。**本层不提供任何读活
|
||||
缓冲的口子**(docs/子系统设计/故障录波设计说明.md §5.2 第一条陷阱:`sample_ring_window()` 无临界区,本波不复用)。
|
||||
- `seg_idx` 只是本次上电的本地计数;盘上的下一个写号归落盘道自持(开机扫目录用
|
||||
`wave_store_idx_from_name()` 重建、写成功后 `wave_store_next_idx()` 推进)。
|
||||
|
||||
### 3.11 `wave_store.c` / `wave_store.h` — 段文件字节层(「故障录波」段格式)
|
||||
- **只做字节**:文件落点、路径拼法、标定系数、数据本体全部由调用方注入(meta 由 adcTask 填、
|
||||
数据经 `wave_reader_fn` 回调取、路径由落盘道拼)→ 两份文件零平台依赖,主机单测免桩 `#include`
|
||||
原样纳入;`wave_store_meta_init()` 只填格式侧静态事实(并把通道表 `sem` 钉成下标、按归属填
|
||||
`conv` / `bits`),`avail` / `k_q16` / `b_codes` 一律留 0 等封存瞬间现取现填——不复制第二个事实源。
|
||||
- **尺寸等式**(单测钉死,落盘道拿它比对写回字节数):整段 = 32 + 12×通道数 + 2×点数×通道数 + 2;
|
||||
当前口径(8 路 320 点)= 头 128 + 数据 5120 + 尾 CRC 2 = **5250 字节**(这条等式是给落盘道比对写回
|
||||
字节数用的):FatFs 该移植在簇分配失败时会返回 FR_OK + 短写且无错误码,判成功必须校验字节数
|
||||
(故障录波设计说明「四条已知陷阱」第二条)。
|
||||
- **CRC-16/MODBUS 自带无表实现**(反射多项式 0xA001、初值 0xFFFF、异或输出 0,自检锚点字符串
|
||||
"123456789" = 0x4B37)。设计说明 §6.2 原写「与屏板链路同一张表」经全仓实测**不成立**(这张表不
|
||||
存在:crc.c 与 crc.h 已于 2026-10-02 删除,屏板侧 `remo_crc()` 是单字节异或)——文档等文档道回改,
|
||||
代码不擅自改口径,也不再新造第三张表。
|
||||
- **编解码同一把尺**:`wave_store_head_build()` 与 `wave_store_head_parse()` 走同一个字段校验函数,
|
||||
判据与 PC 侧读段脚本的拒绝清单逐条对齐(零长度、每周波点数不足 2、每通道点数不够一周波、
|
||||
预触发点数超段长、通道表项语义号不等于下标、版本号与魔数第三字节不一致)——防的就是「板端
|
||||
写得出、PC 读不了」。反过来**不设**任何品质侧不变式:`avail` 同时带有效与削顶是合法事实
|
||||
(lead 裁定 2026-10-29),编码器如实透传、解码器不许据此判废。
|
||||
- **游标分次搬运**(256 B 缓冲够用,整段绝不驻留 RAM;数据 CRC 走增量中间值):
|
||||
`wave_store_pack_next()` 头那一步要求缓冲容量不小于整头 128,否则返 `WAVE_ST_E_PARAM` 且**不推进
|
||||
游标**;没有数据源却进数据区 → `WAVE_ST_E_STATE` 且**不产空字节**(产空字节等于写出「长度对、
|
||||
内容全 0」的段 = docs/子系统设计/故障录波设计说明.md §8.1 明令禁止的伪造证据)。
|
||||
- 段名严格 8.3(该移植 `FF_USE_LFN=0`):`wave_store_seg_path()` 当前布局恒返回 16 个字符(不含结束
|
||||
符),**调用方缓冲至少 17 字节**(`WAVE_ST_PATH_MAX`,原写 16 会让结束符写到缓冲外一字节);
|
||||
100 个号轮转,`wave_store_next_idx()` 回绕,`wave_store_idx_from_name()` 只认 basename 上的段名。
|
||||
- `wave_store_head_build()` / `wave_ring_read_ch()` 这类函数**成功时返回实际字节数 / 点数**(不是
|
||||
`WAVE_ST_OK`),负值才是错误码——写断言前先读头文件的「返 回 值」行(本层单测两次踩在这上面)。
|
||||
- 104 死区迁移 / 频率点绑定 = 卡 9/卡 11(道D 后续分支);读数与注入源比对 = **待台架实测**。
|
||||
|
||||
## 4. 构建与测试
|
||||
|
||||
### 4.1 挂载与模拟器边界
|
||||
- 十一个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
|
||||
- 模拟器侧分两类(Lead 裁定 2026-10-29):`wave_ring.c` 与 `wave_store.c` **真实参与**
|
||||
`simulator/CMakeLists.txt`——回放页要在模拟器里出真波形,两份又零平台依赖,与「双端共享核心、不造
|
||||
第二真相」同一条口径,故**不进** `SIM_EXEMPT`;其余九个采集链文件(UI 模拟器不跑采集链路)仍登记在
|
||||
- 九个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
|
||||
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(登记簿条目「rs485_task 白名单缺口」)。
|
||||
|
||||
### 4.2 主机侧纯逻辑单测——生产源文件以 `#include` 原样纳入
|
||||
### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入
|
||||
|
||||
| 模块 | 命令 | pre-commit 钩子 |
|
||||
|---|---|---|
|
||||
@@ -208,48 +143,26 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」
|
||||
| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` |
|
||||
| 通道标定(含真实 cfg_file 引擎集成) | `& '.venv\Scripts\python.exe' test/host/adc_cal/run_unit_test.py` | `adc-cal-unit-test` |
|
||||
| 遥测快照(布局钉死/双区无撕裂/品质汇总) | `& '.venv\Scripts\python.exe' test/host/meas/run_unit_test.py` | `meas-unit-test` |
|
||||
| 录波环(几何与点序 / 冻结与丢样 / 封存握手 / 读窗夹取 / 自弃与冷却) | `& '.venv\Scripts\python.exe' test/host/wave_ring/run_unit_test.py` | 无专属钩子:靠 `test-coverage` 总闸真跑 |
|
||||
| 录波段格式(尺寸等式 / CRC 锚点 / 编解码往返与拒绝清单 / 搬运游标 / 段名轮转) | `& '.venv\Scripts\python.exe' test/host/wave_store/run_unit_test.py` | 同上 |
|
||||
| 录波触发与全链路(三条来路 / 同拍多请求 / 已封存区不变 / 字节逐列对环) | `& '.venv\Scripts\python.exe' test/host/wave_trig/run_unit_test.py` | 同上 |
|
||||
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各
|
||||
`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。
|
||||
|
||||
录波三套**刻意不新增专属钩子**(Lead 裁定 2026-10-27:钩子口径冻结,新增套数由 `test-coverage`
|
||||
总闸全量真跑并守基线回退,失败照样拦,只是归因落在总闸输出上)。两套录波单测之间刻意留了
|
||||
一份**分工**:环那套管语义、段格式那套管字节、`wave_trig` 管「接起来之后的语义」(含把封存区
|
||||
逐列读出来与打包游标产出的字节逐字节对齐)。两套合成段落在临时目录并把绝对路径打进日志(日志里
|
||||
同时标明是哪一份形态),要跟 PC 侧读段脚本互验时把当次产物拷进 `ci-artifacts/wave_interop/` 再报
|
||||
路径——**别拿旧副本当本次产物**(踩过:拷过一次之后没刷,别人读到的是上一轮的字节)。
|
||||
|
||||
### 4.3 板端
|
||||
`/keil-build` 0 错误 0 警告 → `/serial-boot-test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、
|
||||
`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、
|
||||
`cal: ua_mv=… vl=0x00 cl=0xE6 rail=…`、`ring` / `skip` / `adc1_*` / `tgap`);
|
||||
健康判据 = **零 `[ERR]`**(健康告警与标定哨兵全以 `[ERR]` 开头,误报会被闭环直接抓住;
|
||||
`vl` 在未标定默认构建必须为 0x00,未标定即闭锁落地后 `cal:` 行就是台架期望的直接证据)。
|
||||
`vl` 在未标定默认构建必须为 0x00,卡 5 闭锁落地后 `cal:` 行就是台架期望的直接证据)。
|
||||
→ `docs/算法/采集与算法层设计说明.md` §12。
|
||||
|
||||
## 5. 新增一个算法模块的固定动作
|
||||
|
||||
1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
|
||||
需要 `NULL` 的 `.c` 自己 `#include` 一个 `stddef.h`:ARMCC 的 `stdint.h` 不提供 `NULL`,而主机侧
|
||||
MSVC / gcc 会传递性把它拉进来,**主机单测照不出这个分叉**(2026-10-29 台基线构建实测报 22 条
|
||||
identifier "NULL" 未定义,根因即此;与 `sample_ring.c` / `freq.c` 同口径,别塞进公共头)。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(新建子目录还要补 `<IncludePath>`)。
|
||||
模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。
|
||||
3. 加 `test/host/<模块>/` 主机单测(照 `test/host/meas` 或 `test/host/soe_store` 抄目录形态与
|
||||
`run_unit_test.py`)。**要不要补 `.pre-commit-config.yaml` 专属钩子按 Lead 裁定 2026-10-27:默认不补**,
|
||||
由 `test-coverage` 总闸全量真跑;确有秒级归因需求再单独申请。用例标题写「它在验证什么」这类语义
|
||||
短语,**不要发字母紧跟数字的新编号**(`docs/编号规范.md` §4.1「已停发」栏:停发不等于可以新发)。
|
||||
3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。
|
||||
5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。
|
||||
6. **单测产出的互验字节(要被别人的脚本读的那些):跑测之后立刻刷新副本并记下 sha256**。三套录波单测把合成段落
|
||||
在临时目录、并把**绝对路径 + 是哪一份形态**打进日志,所以「跑完顺手拷一次、以后再没刷」必然留下旧副本——
|
||||
本仓这类坑不止一次(旧副本被当本次证据引用过:一次是别人拿上一轮的字节下了「字节级已闭合」的结论)。
|
||||
判据:引用方只认**当次运行日志里那条路径 + 当次 sha256**;产物有第二种形态(例如本波真值形态与「已标定后
|
||||
仍削顶」的构造形态)就在文件名或日志里把形态写明,别让读者靠猜;拷进 `ci-artifacts/wave_interop/` 之类的
|
||||
共享目录时,同一笔操作里就把新 sha 一起报给对岸,别隔一轮。
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
@@ -258,8 +171,5 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」
|
||||
- 分层与功能项清单:`docs/算法/继电保护功能复现规划.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1 / DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- **故障录波**(数据面 / 段格式 / 现场回放契约):docs/子系统设计/故障录波设计说明.md §6.2 是段文件
|
||||
字节布局的属主,同一篇的 docs/子系统设计/故障录波设计说明.md §6.3 是字段口径裁定——两节合起来是板端
|
||||
编码器与 PC 读段脚本两份独立实现的共同依据;板级判据与执行顺序 → docs/子系统设计/故障录波设计说明.md §10
|
||||
- 相量层 / ADC 输入层单测细则:`docs/测试说明/单元测试/相量层单元测试说明.md`、`docs/测试说明/单元测试/ADC输入层单元测试说明.md`
|
||||
- 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`
|
||||
- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md`
|
||||
|
||||
+12
-74
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* 模块名称:通道标定与量纲折算(标定折算)——实现
|
||||
* 模块名称:通道标定与量纲折算(L1.5)——实现
|
||||
* 模块功能:见 adc_cal.h。折算模型(工程量 = (code + b) × k × 1e-6,k 为 Q16
|
||||
* µV/码)与两段 cfg 布局([adc_cal]:k_* + valid 位图;[adc_cal_ofs]:
|
||||
* b_*)照 docs/任务规划/数据可信.md §4.1 / §4.4 落地;系数口径
|
||||
* (ROM 默认 = 量程口径冻结定死的引脚侧 LSB)见 docs/硬件平台/ADC通道映射说明.md
|
||||
* (ROM 默认 = 卡 1 定死的引脚侧 LSB)见 docs/硬件平台/ADC通道映射说明.md
|
||||
* §7.1。
|
||||
* 并发契约:cfg RAM 副本属主 = 配置引擎(defaultTask 的 load/poll 原地
|
||||
* 改写),本层逐点折算现读——int32 对齐字在 Cortex-M4 上读写原子,
|
||||
@@ -15,10 +15,10 @@
|
||||
* VALID,每通道每次开机首见沿打一条 [ERR](adc_cal_init 重置留痕)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道A 标定折算与落盘整定)
|
||||
* 创建日期:2026-10-25(道A 卡 2/卡 4)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点两段落盘 + 折算 + 三态 flags
|
||||
* v1.1 2026-10-25 DTU 项目组 运行参数段(道D):新增 adc_cal_b_codes() 生效零点
|
||||
* v1.1 2026-10-25 DTU 项目组 卡 10(道D):新增 adc_cal_b_codes() 生效零点
|
||||
* 读取口;set() 授权写者扩至 iec104Task 直通路径
|
||||
* (run_par 0x8040~/0x8048~,契约同 §4.4)
|
||||
* + 合法带哨兵回退 + 两点标定公式(台架注入待跑)
|
||||
@@ -27,9 +27,6 @@
|
||||
* DTU_BENCH 旁路同时收成全仓唯一一处;此前该判据在
|
||||
* protect_task.c 与 ui_data_feed.c 各写一份,两处一旦
|
||||
* 漂移,屏上「未标定」与判据实际停走就不是同一件事
|
||||
* v1.3 2026-10-29 DTU 项目组 故障录波 task-2:新增 adc_cal_k_q16() int32 原值直读口
|
||||
* (段头 k 字段 LSB 精确,float 反推在 5.28e7 掉尾数,
|
||||
* 口径见 docs/子系统设计/故障录波设计说明.md §6.3)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -64,7 +61,7 @@ typedef struct {
|
||||
uint8_t valid;
|
||||
} adc_cal_kcfg_t;
|
||||
|
||||
/* [adc_cal_ofs] 段 RAM 副本:b 按语义下标 0..7(int32 有符号,有符号零点类型
|
||||
/* [adc_cal_ofs] 段 RAM 副本:b 按语义下标 0..7(int32 有符号,卡 3 的
|
||||
* CFG_VAL_INT 是负零点偏移能干净落盘的前提) */
|
||||
typedef struct {
|
||||
int32_t b[8];
|
||||
@@ -73,12 +70,12 @@ typedef struct {
|
||||
static adc_cal_kcfg_t s_kcfg; /* 生效系数副本(引擎原地更新) */
|
||||
static adc_cal_bcfg_t s_bcfg; /* 生效零点副本 */
|
||||
|
||||
/* 工厂默认(ROM 理论系数,量程口径冻结定死,「数据可信」§4.4):**下标 = 语义枚举**
|
||||
/* 工厂默认(ROM 理论系数,卡 1 定死,「数据可信」§4.4):**下标 = 语义枚举**
|
||||
* (UA0 / UB1 / UC2 / UN3 / IA4 / IB5 / IC6 / IN7)——UA/UN/IA 走 TPAFE
|
||||
* (16 位 ±5 V 档),UB/UC/IB/IC/IN 走 STM32 ADC1;逐路对过
|
||||
* docs/硬件平台/ADC通道映射说明.md §3(IN=ADC11/STM32,勿再按分组顺序误写)。
|
||||
* ROM 表按**当前 V0.1 来源排布**显式书写(不查 desc 运行期派生),因为它是
|
||||
* 量程口径冻结定稿的存档事实;V0.2 改版(消除双转换器无共同采样触发)时本表与 adc_input 映射表同步改,
|
||||
* 卡 1 定稿的存档事实;V0.2 改版(消除双转换器无共同采样触发)时本表与 adc_input 映射表同步改,
|
||||
* 上层语义与 API 不动。CLIPPED 位则**运行期按来源派生**(见 adc_cal_flags) */
|
||||
static const adc_cal_kcfg_t s_kcfg_rom = {
|
||||
/* idx0 UA / idx1 UB / idx2 UC / idx3 UN */
|
||||
@@ -95,15 +92,6 @@ static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } };
|
||||
* adc_cal_init 清零 = 新一轮观测窗) */
|
||||
static uint8_t s_err_latched;
|
||||
|
||||
/*
|
||||
* 观测计数(三态语义唯一落点在 adc_cal.h,这里只写"怎么自增"):与上面那个留痕位
|
||||
* 不同,这三个数一个 [ERR] 都不打、也不参与任何判定,因此 **adc_cal_init() 刻意不清**
|
||||
* ——清了水位与计数就失配,会凭空造出一段"假未落盘"差额。
|
||||
*/
|
||||
static volatile uint32_t s_st_set_ok; /* 成功改写 RAM 副本的次数 */
|
||||
static volatile uint32_t s_st_set_reject; /* 被拒次数(非法通道 / 越出合法带) */
|
||||
static volatile uint32_t s_st_persist_water; /* 落盘水位:盖写,绝不自增 */
|
||||
|
||||
/* 键表:offset 用 offsetof 取真值(k[0]@0 / k[1]@4 … valid@32) */
|
||||
static const cfg_key_def_t s_k_keys_tbl[9] = {
|
||||
{ "k_ua", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[0]), 4u },
|
||||
@@ -339,31 +327,12 @@ float adc_cal_k_uv(adc_semantic_t sem) {
|
||||
return ((float)k) / 65536.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效系数的 int32 Q16 原值(adc_cal_k_uv 孪生,见 .h 声明)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:k[Q16];TEMP/越界返 0 int32_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:与 adc_cal_k_uv() 共用 s_eff_kb 生效视图(越带回退 ROM 口径一致),
|
||||
* 差别只在不做浮点还原——录波段头按 LSB 精确记账必须走本口(§6.3)
|
||||
*/
|
||||
int32_t adc_cal_k_q16(adc_semantic_t sem) {
|
||||
int32_t k;
|
||||
int32_t b;
|
||||
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return 0;
|
||||
}
|
||||
(void)s_eff_kb((unsigned)sem, &k, &b);
|
||||
return k;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 孪生入口
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:b[码值](越带时 s_eff_kb 已回 0);TEMP/越界返 0 float
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:运行参数段的 104 b 覆盖回显 + k 单侧改写 RMW 读侧(App/algo/run_par.c)
|
||||
* 函数说明:卡 10 的 104 b 覆盖回显 + k 单侧改写 RMW 读侧(App/algo/run_par.c)
|
||||
*/
|
||||
float adc_cal_b_codes(adc_semantic_t sem) {
|
||||
int32_t k;
|
||||
@@ -377,60 +346,30 @@ float adc_cal_b_codes(adc_semantic_t sem) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」标定落盘与整定入口)
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
|
||||
* 入口参数:sem - 语义通道;k_q16 - 系数(µV/码×65536);b_codes - 零点(码值)
|
||||
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
|
||||
* 限定条件:整定路径上下文(defaultTask 整定/标定链;iec104Task 经 run_par
|
||||
* 直通为运行参数段授权写者,单字原子契约同 §4.4);落盘由属主经 cfg_file_save
|
||||
* 直通为卡 10 授权写者,单字原子契约同 §4.4);落盘由属主经 cfg_file_save
|
||||
* 函数说明:见 adc_cal.h 同名声明
|
||||
*/
|
||||
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) {
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
s_st_set_reject++; /* 观测:连通道号都非法,RAM 未动 */
|
||||
return -1;
|
||||
}
|
||||
if (s_kband_ok((unsigned)sem, k_q16, b_codes) == 0) {
|
||||
DBG_ERROR("set ch=%u rejected: k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld",
|
||||
(unsigned)sem, (long)k_q16, (long)b_codes, (long)ADC_CAL_B_MAX);
|
||||
s_st_set_reject++; /* 观测:越出合法带,RAM 未动([ERR] 由上一行打) */
|
||||
return -1;
|
||||
}
|
||||
s_kcfg.k[sem] = k_q16;
|
||||
s_bcfg.b[sem] = b_codes;
|
||||
s_kcfg.valid |= (uint8_t)(1u << (unsigned)sem);
|
||||
s_st_set_ok++; /* 观测:RAM 副本确实被改写了(与落盘无关) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读标定层观测计数(不取锁、不取时刻、任意上下文可读)
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:无;adc_cal_init() 之前可读(全 0 = 本层还没被改写过)
|
||||
* 函数说明:见 adc_cal.h 同名声明(三态语义与那张成对读表都在那里,本处只取数)
|
||||
*/
|
||||
void adc_cal_get_stats(adc_cal_stats_t *p_out) {
|
||||
if (p_out == NULL) {
|
||||
return; /* 观测件不得反过来制造故障 */
|
||||
}
|
||||
p_out->set_ok = s_st_set_ok;
|
||||
p_out->set_reject = s_st_set_reject;
|
||||
p_out->persisted_set_ok = s_st_persist_water;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘成功回执——把水位盖写为当前的 set_ok(不是自增)
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:仅"整档保存已成功"那一条路径(当前唯一调用点 run_par_store_save())
|
||||
* 函数说明:盖写而非自增的理由与回归用例见 adc_cal.h 同名声明。水位直接取 set_ok 的
|
||||
* 当前值,因此它**在结构上不可能越过** set_ok——差额是 uint32 相减,一旦越过
|
||||
* 就绕回成一个巨大的假未落盘数,这里不留那种可能
|
||||
*/
|
||||
void adc_cal_note_persisted(void) {
|
||||
s_st_persist_water = s_st_set_ok; /* 水位只会追平,不会越过 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:两点标定公式(标定项的算术核,纯函数无状态)
|
||||
* 函数功能:两点标定公式(卡 12 的算术核,纯函数无状态)
|
||||
* 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、p_k_q16/p_b 见 adc_cal.h 同名声明
|
||||
* 返 回 值:0 成功 / -1 参数或斜率非法 / -2 零点越 ±B_MAX int
|
||||
* 限定条件:无(int64 中间量,主机可测;台架注入待跑)
|
||||
@@ -464,8 +403,7 @@ int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:通道码值域贴轨带门限(采样通道自检 判定的门限来源,按 FS 百分比)
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 函数功能:通道码值域贴轨带门限(L5-2 判定的门限来源,按 FS 百分比)
|
||||
* 入口参数:sem - 语义通道;p_lo_th/p_hi_th - 输出下/上轨带界 int32_t *
|
||||
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界/参数空(不做贴轨监视) int
|
||||
* 限定条件:无
|
||||
|
||||
+21
-102
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:通道标定与量纲折算(标定折算,「数据可信」标定折算与落盘整定)
|
||||
* 模块名称:通道标定与量纲折算(L1.5,「数据可信」卡 2/卡 4)
|
||||
* 模块功能:把 adc_input 的原始码值折算成工程量——模型与系数口径的唯一落点:
|
||||
* 工程量 = (code + b) × k_f,k_f = k[µV/码] × 1e-6
|
||||
* k 以 int32 **Q16**(µV/码 × 65536)落盘、b 以 int32 码值落盘,
|
||||
@@ -8,33 +8,29 @@
|
||||
* 即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒浮点乘,可忽略)。
|
||||
* 三态可用性:ADC_CAL_F_VALID(已按变比标定 → 工程量为二次值)与
|
||||
* ADC_CAL_F_CLIPPED(V0.1 硬件削顶)由 adc_cal_flags() 单点查询,
|
||||
* 104 / UI / 保护闭锁(未标定即闭锁)共用(docs/任务规划/数据可信.md §4.2)。
|
||||
* 104 / UI / 保护闭锁(卡 5)共用(docs/任务规划/数据可信.md §4.2)。
|
||||
* **未标定闭锁的判定本身也归本层**(`adc_cal_prot_blocked()`):它讲的是
|
||||
* 「这几路标定齐了没」,属标定层事实,不是任务编排逻辑——保护任务与 UI
|
||||
* 组装器都只是它的消费者(道F task-16 把此前的两份实现收成一份)。
|
||||
* ⚠ 输出单位口径 = **ADC 引脚电压 V**(eng=0):ROM 默认系数按量程口径冻结
|
||||
* ⚠ 输出单位口径 = **ADC 引脚电压 V**(eng=0):ROM 默认系数按卡 1
|
||||
* 定死的引脚侧 LSB(TPAFE 152.59 µV/码、STM32 805.66 µV/码,见
|
||||
* docs/硬件平台/ADC通道映射说明.md §7.1);端子/子板变比确认与两点
|
||||
* 标定完成前不得把输出当二次值消费。TEMP 不参与线性标定
|
||||
* docs/硬件平台/ADC通道映射说明.md §7.1);端子变比(卡 0b)与两点
|
||||
* 标定(卡 12)完成前不得把输出当二次值消费。TEMP 不参与线性标定
|
||||
* (决策 5),adc_cal_apply() 原样透传码值。
|
||||
* 本层**纯算法 + 纯数据**:不碰寄存器、不等事件;adc_cal_init() 内核前
|
||||
* 可调(注册进引擎必须先于 cfg_file_load,FTP 鉴权确立的契约);apply/flags
|
||||
* 任意上下文可读(cfg RAM 副本 4 字节对齐字读写在 Cortex-M4 上原子,
|
||||
* 与 defaultTask 的 load/poll 复位存在 µs 级混窗——设计接受,见 .c 头)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F);主机侧纯逻辑单测 test/host/adc_cal/
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F);主机侧 L0.5 单测 test/host/adc_cal/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道A 标定折算与落盘整定)
|
||||
* 创建日期:2026-10-25(道A 卡 2/卡 4)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点 + 折算/flags/集
|
||||
* v1.1 2026-10-25 DTU 项目组 运行参数段:+adc_cal_b_codes()(run_par 回显/RMW 用)
|
||||
* v1.1 2026-10-25 DTU 项目组 卡 10:+adc_cal_b_codes()(run_par 回显/RMW 用)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-16:+adc_cal_prot_blocked()——「UA/UN/IA
|
||||
* 三路 VALID 是否齐」这份判据停走条件从
|
||||
* protect_task.c 与 ui_data_feed.c 的两份重复实现收成本层
|
||||
* 单点(DTU_BENCH 旁路同时收成一处)
|
||||
* v1.3 2026-10-29 DTU 项目组 故障录波 task-2:新增 adc_cal_k_q16()——k 的 int32
|
||||
* Q16 原值直读口(adc_cal_k_uv 的孪生)。录波段头 k 字段
|
||||
* 是 LSB 精确记账,float 在 5.28e7 码字上掉尾数,
|
||||
* 禁止经浮点反推(docs/子系统设计/故障录波设计说明.md §6.3)
|
||||
*/
|
||||
|
||||
#ifndef ADC_CAL_H
|
||||
@@ -49,11 +45,11 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* 通道可用性标志(adc_cal_flags() 返回值位) */
|
||||
#define ADC_CAL_F_VALID (0x01u) /* k 已按变比标定 → 工程量为二次值(两点标定后置位) */
|
||||
#define ADC_CAL_F_VALID (0x01u) /* k 已按变比标定 → 工程量为二次值(卡 12 后置位) */
|
||||
#define ADC_CAL_F_CLIPPED (0x02u) /* 硬件削顶(V0.1 STM32 侧单极性无偏置)→ 交流量不可信 */
|
||||
|
||||
/*
|
||||
* ROM 默认系数(int32 Q16 = µV/码 × 65536,量程口径冻结定死的引脚侧口径):
|
||||
* ROM 默认系数(int32 Q16 = µV/码 × 65536,卡 1 定死的引脚侧口径):
|
||||
* TPAFE ±5 V 档:10 V 跨度 / 65536 码 = 152.587890625 µV/码 → ×65536 = 10,000,000(整)
|
||||
* STM32 0~3.3 V:3.3 V / 4096 码 = 805.6640625 µV/码 → ×65536 = 52,800,000(整)
|
||||
* 两个默认值在 Q16 下**无舍入**(系数定点舍入项从误差预算里抹掉,
|
||||
@@ -103,7 +99,7 @@ float adc_cal_apply(adc_semantic_t sem, int32_t code);
|
||||
uint32_t adc_cal_flags(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:**未标定闭锁判定**——保护判据所需的三路标定是否齐(「数据可信」未标定即闭锁
|
||||
* 函数功能:**未标定闭锁判定**——保护判据所需的三路标定是否齐(「数据可信」卡 5
|
||||
* / §4.3;道F task-16 把它从两处重复实现收敛成这一份真源)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 闭锁(UA/UN/IA 中至少一路 `ADC_CAL_F_VALID` 未置位,判据链应停走);
|
||||
@@ -115,8 +111,7 @@ uint32_t adc_cal_flags(adc_semantic_t sem);
|
||||
* 的测量通道;UB/UC/IB/IC/IN 即使将来标定也另受 `CLIPPED` 品质约束
|
||||
* (M5 快照层处理),不掺进本闭锁源。
|
||||
* ② ⚠ **不得当「保护在跑」的总括用**:判据停走共四路来源,本函数只覆盖
|
||||
* 第二路——(a) 自检失败闭锁 采样故障闭锁(`adc_health_flags()`);(b) 本函数
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 第二路——(a) L5-4 采样故障闭锁(`adc_health_flags()`);(b) 本函数
|
||||
* (未标定);(c) 定值校验和失配闭锁(`prot_set_locked()`);(d) 全局软压板
|
||||
* 退出(`PS_GLOBAL_EN` = 0)。要判「这拍到底评估了没有」必须四路一起看
|
||||
* (编排顺序见 `App/task/protect_task.c`)。
|
||||
@@ -139,7 +134,7 @@ uint8_t adc_cal_prot_blocked(void);
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:k[µV/码](回退后口径);TEMP / 越界返 0 float
|
||||
* 限定条件:无
|
||||
* 函数说明:采样通道自检 门限换算、日志与整定界面回显用;勿据其反推二次值(看 flags)
|
||||
* 函数说明:L5-2 门限换算、日志与整定界面回显用;勿据其反推二次值(看 flags)
|
||||
*/
|
||||
float adc_cal_k_uv(adc_semantic_t sem);
|
||||
|
||||
@@ -148,42 +143,27 @@ float adc_cal_k_uv(adc_semantic_t sem);
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:b[码值](越带时为 0,与折算同视图);TEMP/越界返 0 float
|
||||
* 限定条件:无
|
||||
* 函数说明:104 运行参数段(「数据可信」这一项)b 覆盖回显与 k 单侧改写的 RMW
|
||||
* 函数说明:104 运行参数段(「数据可信」卡 10)b 覆盖回显与 k 单侧改写的 RMW
|
||||
* 读侧用(App/algo/run_par.c);勿据其反推二次值(看 flags)。
|
||||
*/
|
||||
float adc_cal_b_codes(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效系数的 **int32 Q16 原值**(µV/码 × 65536)——
|
||||
* adc_cal_k_uv() 的孪生入口,给「必须 LSB 精确」的消费者用
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:k[Q16 原值](与折算同一生效视图:越带回退 ROM、b 不参与);TEMP/越界返 0 int32_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:故障录波段头的 k 字段是 int32 LSB 精确记账(docs/子系统设计/故障录波设计说明.md
|
||||
* §6.3 裁定)——float 还原后在 5.28e7 码字上掉尾数,**禁止**拿
|
||||
* adc_cal_k_uv() 反乘 65536 回算落盘,故开本原值口。
|
||||
*/
|
||||
int32_t adc_cal_k_q16(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」标定落盘与整定入口)
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
|
||||
* 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t
|
||||
* k_q16 - 系数,µV/码 × 65536 内 int32_t
|
||||
* b_codes - 零点偏移,码值 内 int32_t
|
||||
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
|
||||
* 限定条件:**整定路径上下文**(defaultTask 标定流程/整定链;运行参数段落地起 iec104Task
|
||||
* 限定条件:**整定路径上下文**(defaultTask 标定流程/整定链;卡 10 起 iec104Task
|
||||
* 经 App/algo/run_par.c 的 0x8040~/0x8048~ 直通写亦为授权写者——写的仍是
|
||||
* 引擎对齐 int32 单字,µs 级混窗契约同「数据可信」§4.4)。勿在 adcTask 调用
|
||||
* 函数说明:**只写 RAM 副本,不置脏、不落盘、不返回"稍后会存"的承诺**——落盘由属主经
|
||||
* cfg_file_save 完成,而"要不要落盘"这件事只有一个真源:run_par 的脏标志。
|
||||
* 因此授权写者是 run_par 的语义 setter(写值 + 置脏成对发生,lead 裁②);
|
||||
* 绕过它直调本函数 = 值只活在 RAM 里,重启即丢且**没有错误返回**,
|
||||
* 该形态现可被本层观测计数抓到(差分口径见下方 adc_cal_stats_t 那张表)
|
||||
* 函数说明:只写 RAM 副本;落盘由属主经 cfg_file_save 完成(卡 10 前整定链
|
||||
* 未接,台架两点标定(卡 12)是首个消费者)
|
||||
*/
|
||||
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes);
|
||||
|
||||
/*
|
||||
* 函数功能:两点标定公式(「数据可信」标定项的算术核,纯函数无状态)
|
||||
* 函数功能:两点标定公式(「数据可信」卡 12 的算术核,纯函数无状态)
|
||||
* 入口参数:c_lo/c_hi - 两点码值(c_hi > c_lo) int32_t
|
||||
* v_lo_uv/v_hi_uv - 两点实测电压(µV,v_hi > v_lo,端子/引脚域均可) int32_t
|
||||
* p_k_q16 / p_b - 输出:系数(Q16)与零点偏移(码值) int32_t *
|
||||
@@ -197,78 +177,17 @@ int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_
|
||||
int32_t *p_k_q16, int32_t *p_b);
|
||||
|
||||
/*
|
||||
* 函数功能:通道码值域「贴轨带」门限(采样通道自检 越界检出的门限来源,按 FS 百分比)
|
||||
* 函数功能:通道码值域「贴轨带」门限(L5-2 越界检出的门限来源,按 FS 百分比)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* p_lo_th - 下轨带上界(code ≤ 此值判触底) int32_t *
|
||||
* p_hi_th - 上轨带下界(code ≥ 此值判触顶) int32_t *
|
||||
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界(不做贴轨监视) int
|
||||
* 限定条件:无
|
||||
* 函数说明:TPAFE 双极性 ±98% FS(±32112);STM32 上轨 98% FS(4013)、
|
||||
* 下轨 2% FS(81);判定状态机在 adc_health_rail_watch()(采样通道自检)
|
||||
* 下轨 2% FS(81);判定状态机在 adc_health_rail_watch()(L5-2)
|
||||
*/
|
||||
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th);
|
||||
|
||||
/*
|
||||
* 观测计数:把「改了没落盘」从意图式陈述变成可诊断事实(道G task-43 第②项;
|
||||
* 先例 App/fs/vol_lock.h 的同族设计——只计数、不取锁、不在调用点打日志、不打 [ERR])
|
||||
*
|
||||
* 三态语义(**定义只在本头文件这一处**,消费者只描述取数与断言,不重定义):
|
||||
* set_ok RAM 副本被本层 API 成功改写的累计次数
|
||||
* set_reject 被本层拒掉的整定请求累计次数(参数非法 / 越出合法带)
|
||||
* persisted_set_ok **水位**而不是计数:最近一次随整档保存落盘成功时的
|
||||
* set_ok 快照(由 adc_cal_note_persisted() 盖写,永不越过 set_ok)
|
||||
*
|
||||
* 未落盘差额 = set_ok − persisted_set_ok,即"自上次成功落盘之后被改写的次数"。
|
||||
* 单看差额不够定性,必须与 run_par_dirty_peek() 成对读(两列一起才只有一种解释):
|
||||
*
|
||||
* 差额 run_par_dirty_peek() 说的是什么事
|
||||
* ------ ---------------------- ----------------------------------------------
|
||||
* 0 0 盘与 RAM 齐(或压根没改过)——健康
|
||||
* >0 1 改了、还没到落盘时机——正常在途
|
||||
* >0 0 **改了且永远不会落盘**:写者绕过 run_par 的语义
|
||||
* setter 直调 adc_cal_set()(运行参数段起的事)
|
||||
*
|
||||
* 消费者告诫(与 vol_lock_get_stats() 同一套,别重犯):
|
||||
* ① 本组接口不取锁、不取时刻,任意上下文可读;三个数各自是 uint32 单字读写,
|
||||
* Cortex-M4 上原子,但三个数之间**没有共同时刻**——别把它们拼成"一个瞬间的快照"
|
||||
* 下结论,定性只按上面那张两列表成对读。
|
||||
* ② 需要"被观测到的时刻"请在消费者侧自盖并冠 obs_ 前缀命名(先例:喂狗点单写者 latch),
|
||||
* 本层不存时刻——存了就得连带管溢出与单位。
|
||||
* ③ 这些计数**不参与任何判定**:能不能信只看 adc_cal_flags() /
|
||||
* adc_cal_prot_blocked(),要不要落盘只看 run_par 的脏标志。别拿差额去改控制流,
|
||||
* 那等于把观测升级成第二真源(标定写入口已由 run_par 语义 setter 收口,不再加口)。
|
||||
* ④ 写清盲区:本层刻意**不挂** cfg_file 回调,因此"盘 → RAM 的覆盖"(cfg_file_load
|
||||
* 与外部变更重载)不产生任何计数;未落盘的改值被一次重载无声吞掉,正是这三个
|
||||
* 数照不到的那一条。要照它得先给本层挂回调 = 行为改动,另行立项处置。
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t set_ok; /* 成功改写 RAM 副本次数(见上方唯一落点) */
|
||||
uint32_t set_reject; /* 被拒次数(见上方唯一落点) */
|
||||
uint32_t persisted_set_ok; /* 落盘水位,不是计数(见上方唯一落点) */
|
||||
} adc_cal_stats_t;
|
||||
|
||||
/*
|
||||
* 函数功能:读标定层观测计数(只读,不改任何状态)
|
||||
* 入口参数:p_out - 输出:三个观测计数 adc_cal_stats_t *
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:任意上下文可读(含 ISR 与内核启动前);adc_cal_init() 之前可读,得全 0
|
||||
* 函数说明:p_out 为 NULL 时静默返回——观测件不得反过来制造故障
|
||||
*/
|
||||
void adc_cal_get_stats(adc_cal_stats_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘成功回执——把水位 persisted_set_ok 盖写为当前的 set_ok(**不是自增**)
|
||||
* 入口参数:无 void
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:只允许配置引擎属主侧"整档保存已成功"那一条路径调用;当前唯一调用点 =
|
||||
* App/algo/run_par.c 的 run_par_store_save() 中 cfg_file_save() 返 0 之后
|
||||
* 函数说明:为什么盖写不自增——一次 save 覆盖整档,可能对应多次整定改写(逐通道
|
||||
* 标定 3 路 = 3 次 adc_cal_set + 1 次 save);自增会留下 2 次"假未落盘"。
|
||||
* 水位语义让差额只反映"上次落盘之后的改写",回归锁见
|
||||
* test/host/adc_cal/adc_cal_test.c 的 TC-ADCCAL-021
|
||||
*/
|
||||
void adc_cal_note_persisted(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* 模块名称:ADC 采样有效性校验(语义取数与预处理)
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
|
||||
* 模块功能:见 adc_health.h 文件头。本文件为**纯逻辑实现**:输入计数快照、
|
||||
* 输出故障位/事件位,零平台依赖(仅 stdint.h/string.h 标准 C)→
|
||||
* 主机侧免桩单测。
|
||||
@@ -8,18 +7,18 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:采样有效性校验
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验
|
||||
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale():无模块状态的帧年龄
|
||||
* 纯判据(参照系由调用方自存),供 protectTask 的
|
||||
* 兜底新鲜度门使用,见 adc_health.h 同名声明
|
||||
* v1.2 2026-10-25 DTU 项目组 新增 采样通道自检 残余:码值越界(贴轨)观察器
|
||||
* v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余:码值越界(贴轨)观察器
|
||||
* (rail_watch/rail_sticky/rail_init),判据与释放
|
||||
* 沿理由见 adc_health.h 采样通道自检 段注释
|
||||
* 沿理由见 adc_health.h L5-2 段注释
|
||||
*/
|
||||
|
||||
#include "adc_health.h"
|
||||
|
||||
#include <string.h> /* memset(采样通道自检 贴轨观察器数组复位) */
|
||||
#include <string.h> /* memset(L5-2 贴轨观察器数组复位) */
|
||||
|
||||
/* ===================== 内部状态 ===================== */
|
||||
|
||||
@@ -43,7 +42,7 @@ static uint32_t s_flags; /* 电平位(stall 可自愈;backward 锁存在
|
||||
static uint32_t s_sticky; /* 锁存位(含电平位历史并集) */
|
||||
static uint32_t s_tpafe_gaps;
|
||||
|
||||
/* 采样通道自检 贴轨观察器(每通道独立小状态机,单写者 = adcTask) */
|
||||
/* L5-2 贴轨观察器(每通道独立小状态机,单写者 = adcTask) */
|
||||
typedef struct {
|
||||
uint16_t run_hi; /* 连续贴上轨计数(饱和 0xFFFF) */
|
||||
uint16_t run_lo; /* 连续贴下轨计数 */
|
||||
@@ -258,7 +257,7 @@ uint32_t adc_health_tpafe_gaps(void) {
|
||||
return s_tpafe_gaps;
|
||||
}
|
||||
|
||||
/* ===================== 采样通道自检 贴轨观察器实现 ===================== */
|
||||
/* ===================== L5-2 贴轨观察器实现 ===================== */
|
||||
|
||||
/*
|
||||
* 函数功能:贴轨观察器复位(幂等;adc_health_init() 已内含)
|
||||
|
||||
+9
-10
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* 模块名称:ADC 采样有效性校验(语义取数与预处理)
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
|
||||
* 模块功能:对两片转换器的"节拍是否还在喂数据"做**判定层**(观测计数由
|
||||
* `adc_input` / `tpafe5160` 驱动提供,本模块只做判定与告警位):
|
||||
* ① 静默(stall):帧序号连续 >= ADC_HEALTH_STALL_MS 无前进;
|
||||
@@ -9,19 +8,19 @@
|
||||
* ④ 错误计数增量:FRSTDATA 跳变 / HAL ADC·DMA 错误 / STM32 停摆计数。
|
||||
* ⚠ 本层**纯逻辑**:不碰寄存器、不取数、不打日志——输入是调用方
|
||||
* 采集的计数快照,输出是故障位;日志与自检上报由消费者
|
||||
* (`App/task/adc_task.c`)按位执行 → 主机侧可纯逻辑单测。
|
||||
* (`App/task/adc_task.c`)按位执行 → 主机侧可单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:采样有效性校验(静默/丢帧/单调性/错误增量)
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验(静默/丢帧/单调性/错误增量)
|
||||
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale()(无模块状态的帧年龄
|
||||
* 纯判据,参照系由调用方自存)与 ADC_HF_STALE 位:
|
||||
* 判定层的 stall/backward 位只在 adcTask 每拍跑
|
||||
* check() 时才可能置起,节拍源停摆会把本任务卡在
|
||||
* osThreadFlagsWait 上 → 那些位永远是 0,而相量冻在
|
||||
* 最后一帧仍被 protectTask 用于判据。该纯判据让保护侧
|
||||
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新" * v1.2 2026-10-25 DTU 项目组 新增 采样通道自检 残余「码值越界(贴轨)检出」:
|
||||
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新" * v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余「码值越界(贴轨)检出」:
|
||||
* adc_health_rail_watch()/rail_sticky()/rail_init()
|
||||
* ——连续 N 拍贴满量程带且该通道见过释放沿才锁存
|
||||
* (空载恒 0 / 开机恒 0 是合法工况,不误报);
|
||||
@@ -45,7 +44,7 @@ extern "C" {
|
||||
#define ADC_HEALTH_STALL_MS 100U
|
||||
|
||||
/* 电平故障位(adc_health_flags(),当前活跃;stall 可自愈清除,backward 锁存)
|
||||
* ——供后续「自检失败闭锁」直接消费 */
|
||||
* ——供后续 L5-4「自检失败闭锁」直接消费 */
|
||||
#define ADC_HF_TPAFE_STALL (1uL << 0) /* TPAFE 帧序号静默 */
|
||||
#define ADC_HF_STM32_STALL (1uL << 1) /* STM32 DMA 完成计数静默 */
|
||||
#define ADC_HF_SEQ_BACKWARD (1uL << 2) /* 帧序号倒退(锁存,不自愈) */
|
||||
@@ -143,7 +142,7 @@ uint8_t adc_health_age_stale(adc_health_age_t *a_p_age, uint32_t a_cur_seq,
|
||||
* 入口参数:无
|
||||
* 返 回 值:ADC_HF_TPAFE_STALL / ADC_HF_STM32_STALL / ADC_HF_SEQ_BACKWARD 组合
|
||||
* 限定条件:无
|
||||
* 函数说明:供自检失败闭锁判定与 UI;与 check() 出参 flags 同源。
|
||||
* 函数说明:供 L5-4 闭锁判定与 UI;与 check() 出参 flags 同源。
|
||||
*/
|
||||
uint32_t adc_health_flags(void);
|
||||
|
||||
@@ -166,7 +165,7 @@ uint32_t adc_health_sticky(void);
|
||||
*/
|
||||
uint32_t adc_health_tpafe_gaps(void);
|
||||
|
||||
/* ===================== 采样通道自检 残余:码值越界(贴轨)检出,2026-10-25 ===================== */
|
||||
/* ===================== L5-2 残余:码值越界(贴轨)检出,2026-10-25 ===================== */
|
||||
|
||||
/* 贴轨锁存的连续拍数:20 拍 = 1 kHz 节拍下一个 50 Hz 周波。削顶/触轨波形的
|
||||
* 贴带段持续 ≥ 半周波;噪声毛刺不可能连续 20 拍钉在同侧轨带内(短板评测 §6.2
|
||||
@@ -186,7 +185,7 @@ uint32_t adc_health_tpafe_gaps(void);
|
||||
void adc_health_rail_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:单通道单拍贴轨观察(采样通道自检 码值越界检出,纯算术)
|
||||
* 函数功能:单通道单拍贴轨观察(L5-2 码值越界检出,纯算术)
|
||||
* 入口参数:ch - 通道号(0..ADC_RAIL_CH_NUM-1,越界忽略) uint8_t
|
||||
* code - 本拍原始码值(**码值域**,与标定无关) int32_t
|
||||
* lo_th - 下轨带上界(code ≤ 此值算贴底) int32_t
|
||||
@@ -206,7 +205,7 @@ uint32_t adc_health_rail_watch(uint8_t ch, int32_t code, int32_t lo_th, int32_t
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit ch = 该通道曾检出贴轨(1 kHz × 9 路 → 低 9 位有效) uint32_t
|
||||
* 限定条件:无
|
||||
* 函数说明:供心跳日志 / 后续品质位映射(遥测快照发布与 104 消费的 qds 消费者)只读。
|
||||
* 函数说明:供心跳日志 / 后续品质位映射(卡 8/9 的 qds 消费者)只读。
|
||||
*/
|
||||
uint32_t adc_health_rail_sticky(void);
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/*
|
||||
* 模块名称:ADC 输入层(语义取数与预处理)
|
||||
* 模块功能:语义通道映射(通道语义映射)+ STM32 ADC1 采样消费(STM32 ADC1 启动与扫描,TIM3 TRGO 触发 +
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 模块名称:ADC 输入层(L1 采集与预处理)
|
||||
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 采样消费(L1-3,TIM3 TRGO 触发 +
|
||||
* DMA 双缓冲)+ TPAFE 快照消费。
|
||||
* 消费侧经 adc_input_raw() 按语义取数,不写死下标。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:STM32 ADC1 启动与扫描(STM32 ADC1 扫描)+ 通道语义映射(语义映射)
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧由"软件触发 + 逐通道轮询"改为
|
||||
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
|
||||
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
|
||||
@@ -38,7 +37,7 @@
|
||||
/* 连续"无新帧"达到该拍数才计一次停摆故障(单拍是相位伪影,见 stm32_take 注释) */
|
||||
#define ADC_INPUT_STALL_RUN_MAX 2U
|
||||
|
||||
/* ===== 语义映射表(通道语义映射)=====
|
||||
/* ===== 语义映射表(L1-4)=====
|
||||
* 权威口径:docs/硬件平台/ADC通道映射说明.md(2026-10-09 硬件确认的语义分配)。
|
||||
* index 含义:TPAFE 源 = 驱动 buf 下标 0..7;STM32 源 = ADC1 通道号 0..15。
|
||||
* ⚠ 消费侧一律经本表按语义取数,**不得写死下标**(V0.1 有 13/17 路被置换,
|
||||
@@ -84,7 +83,7 @@ static int s_stm32_rc; /* STM32 最近一次扫描状态码 */
|
||||
static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
|
||||
|
||||
/*
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(STM32 ADC1 启动与扫描,DMA 双缓冲)
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(L1-3,DMA 双缓冲)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错)
|
||||
* 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA
|
||||
@@ -155,8 +154,7 @@ static int stm32_take(void) {
|
||||
* buf[0..7];② STM32 源的通道号必须出现在 k_stm32_rank_ch[] 中,
|
||||
* 否则该通道永远不会被扫描(读到的恒为陈旧值)。这是"通道宏写错 →
|
||||
* 读到另一只脚/读不到"这一警告的代码级兜底(警告口径与逐通道验证方法:
|
||||
* docs/硬件平台/ADC通道映射说明.md §5、§8。台架注入验证只在台架做一次,
|
||||
* 本自检每次开机都跑,两者不互相替代)。
|
||||
* docs/硬件平台/ADC通道映射说明.md §5、§8;台架无信号源时注入验证做不了)。
|
||||
*/
|
||||
static int map_check(void) {
|
||||
uint8_t s;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/*
|
||||
* 模块名称:ADC 输入层(语义取数与预处理)
|
||||
* 模块名称:ADC 输入层(L1 采集与预处理)
|
||||
* 模块功能:把两片转换器的原始码值按**语义**(UA/UB/UC/UN/IA/IB/IC/IN/温度)统一
|
||||
* 暴露,使上层(UI / 自检 / 保护算法)**按语义取数,不写死下标**:
|
||||
* ① 语义通道映射表(通道语义映射)——权威口径 docs/硬件平台/ADC通道映射说明.md;
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* ② STM32 ADC1 9 通道采样(STM32 ADC1 启动与扫描)——由 **TIM3 TRGO 硬件触发 + DMA 双缓冲
|
||||
* ① 语义通道映射表(L1-4)——权威口径 docs/硬件平台/ADC通道映射说明.md;
|
||||
* ② STM32 ADC1 9 通道采样(L1-3)——由 **TIM3 TRGO 硬件触发 + DMA 双缓冲
|
||||
* 搬运**,本层只消费、不触发、不等待。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷:双转换器无共同采样触发):UA/UN/IA 走
|
||||
* TPAFE5160(16 位 / ±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC1
|
||||
@@ -14,7 +13,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:STM32 ADC1 启动与扫描(STM32 ADC1 扫描)+ 通道语义映射(语义映射)
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧改 TIM3 TRGO 硬件触发 + DMA 双缓冲消费:
|
||||
* 消除逐通道轮询的 2 ms 忙等;节拍源 TIM6 -> TIM3
|
||||
* (换源判据:F4 上 ADC1 收不到 TIM6/TIM7 的 TRGO,见
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:频率测量层(全周傅氏相量的派生量)——实现
|
||||
* 模块名称:频率测量层(L2 派生量)——实现
|
||||
* 模块功能:见 freq.h。相邻两拍基波相量共轭乘积取角(= 每拍相位增量,免
|
||||
* 显式解缠绕)+ **自适应窗长滑动平均** + 瞬时值钳制 + 幅值门。
|
||||
* 输入 = phasor 使用区基波相量(UA 优先,UA 无幅值用 IA)。
|
||||
|
||||
+8
-9
@@ -1,14 +1,13 @@
|
||||
/*
|
||||
* 模块名称:频率测量层(全周傅氏相量的派生量,过频 / 低频的测量输入)
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 模块功能:由 全周傅氏相量使用区的基波相量导出系统频率——相邻两拍基波相量取
|
||||
* 模块名称:频率测量层(L2 派生量,L3-5 过频/低频的测量输入)
|
||||
* 模块功能:由 L2 相量使用区的基波相量导出系统频率——相邻两拍基波相量取
|
||||
* **共轭乘积** P = Z(m)·conj(Z(m-1))(其角 = 每拍相位增量,纯音 =
|
||||
* +2πf/Fs,免显式解缠绕),再对**自适应窗长**的相位增量做滑动平均:
|
||||
* K_eff = round(8·Fs/(2·f_hat)) ≈ 泄漏纹波周期 fs/(2f) 的整数倍
|
||||
* -> 非整周波 DFT 负序泄漏造成的 ~10 ms 周期相位纹波相消
|
||||
* (定长窗平均实测扫频误差 0.2~0.32 Hz,自适应窗进入 0.01 Hz 量级,
|
||||
* 推导与实测见 freq.c 文件头与 test/host/freq 输出)。
|
||||
* 覆盖规划 §3.3 过频 / 低频 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。
|
||||
* 覆盖规划 §3.3 L3-5 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。
|
||||
*
|
||||
* 原理与判据(详见 docs/算法/采集与算法层设计说明.md §7 与 test/host/freq/):
|
||||
* - 采样率 Fs = 1000 Hz(TIM3 节拍,与 phasor 同源);45~55 Hz 段每拍
|
||||
@@ -22,7 +21,7 @@
|
||||
* - 信号源优先级:UA 基波 → IA 基波(UA 无幅值时用 IA,台架只注电流亦可测频)。
|
||||
*
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(输入 = phasor 使用区)
|
||||
* → 主机侧可免桩纯逻辑单测。
|
||||
* → 主机侧可免桩单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-12(B8)
|
||||
@@ -50,14 +49,14 @@
|
||||
#define FREQ_INST_MIN_HZ 35.0f
|
||||
#define FREQ_INST_MAX_HZ 65.0f
|
||||
|
||||
/* 幅值门限(有效值,**二次值 V 口径**,「数据可信」下游常数按工程量重定的结果,依据其 §2.3 表第 1 行):
|
||||
/* 幅值门限(有效值,**二次值 V 口径**,「数据可信」卡 6 重定,依据其 §2.3 表第 1 行):
|
||||
* 低于该值判"无有效波形"。取值 50 V = 0.5×Un(相电压标称 100 V,与
|
||||
* App/algo/prot_set.c 头部量纲基准同源):正常带载 UA 在 0.9~1.1×Un,
|
||||
* 50 V 之下只可能是断保险/PT 空载/浮空噪声(浮空实测 ~1.7 V 引脚域、
|
||||
* 二次域更小),不会吃掉正常纹波(纹波谷值远大于 0.5×Un)。
|
||||
* ⚠ 过渡期行为(eng=0,两点标定完成前):帧值还是引脚电压(≤5 V < 50 V),
|
||||
* 本门**恒失效** → freq_valid()=0 → OF/UF 按"必不启动"填充复位——与未标定即闭锁
|
||||
* 同向(判据链本就停走),不构成额外风险;标定后自动恢复本意。
|
||||
* ⚠ 过渡期行为(eng=0,卡 12 标定前):帧值还是引脚电压(≤5 V < 50 V),
|
||||
* 本门**恒失效** → freq_valid()=0 → OF/UF 按"必不启动"填充复位——与卡 5
|
||||
* 未标定闭锁同向(判据链本就停走),不构成额外风险;标定后自动恢复本意。
|
||||
* 数值沿用原 50.0f 属巧合(原为"远小于千级码值"的拟定值),不是漏改。 */
|
||||
#define FREQ_AMP_MIN 50.0f
|
||||
|
||||
|
||||
+4
-5
@@ -1,17 +1,16 @@
|
||||
/*
|
||||
* 模块名称:遥测快照发布层(全周傅氏相量派生·发布侧,「数据可信」§4.5)
|
||||
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
|
||||
* 模块功能:实现见 meas.h——200 ms 基波有效值 + 品质快照,双区无锁读。
|
||||
* 本文件纯算法:输入 = sample_ring 取窗 + 调用方注入(频率/削顶位图),
|
||||
* 无 RTOS / 无寄存器 / 无日志(「数据可信」层内禁令:本目录不写日志)。
|
||||
* 双区不变量(docs/任务规划/数据可信.md §4.5,双区指针原子交换 同型,先例 = App/algo/
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 双区不变量(docs/任务规划/数据可信.md §4.5,L2-7 同型,先例 = App/algo/
|
||||
* phasor.c 的双区注释):写侧只碰**非使用区**,全部字段填完后一次
|
||||
* volatile 单字节 store 翻转使用区;读者先取索引再整体拷贝 → 拷贝期
|
||||
* 间写者不可能改读者所在区(两帧间隔 200 ms 远大于 72 B 拷贝)。
|
||||
* 单核顺序执行,无需内存屏障;volatile 阻编译器把字段写入挪到翻转后。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 遥测快照发布)
|
||||
* 创建日期:2026-10-25(道D 卡 8)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
|
||||
*/
|
||||
@@ -23,7 +22,7 @@
|
||||
#include "phasor.h" /* phasor_calc():基波 DFT 纯函数核(与使用区同核) */
|
||||
#include "sample_ring.h" /* sample_ring_window():以本拍为终止的一周波窗 */
|
||||
|
||||
/* ===== 双区(计算区 / 使用区),口径同 App/algo/phasor.c 的 双区指针原子交换===== */
|
||||
/* ===== 双区(计算区 / 使用区),口径同 App/algo/phasor.c 的 L2-7 ===== */
|
||||
static meas_snap_t s_ms_snap[2];
|
||||
static volatile uint8_t s_ms_use;
|
||||
static uint8_t s_ms_ready; /* 使用区是否已产出过有效结果 */
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
/*
|
||||
* 模块名称:遥测快照发布层(全周傅氏相量派生·发布侧,「数据可信」§4.5)
|
||||
* 模块功能:每 **200 ms** 从 全周傅氏相量层取一次基波有效值 + 品质,产出一份全仓唯一的
|
||||
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
|
||||
* 模块功能:每 **200 ms** 从 L2 层取一次基波有效值 + 品质,产出一份全仓唯一的
|
||||
* 遥测快照(meas_snap_t,布局权威 = docs/任务规划/数据可信.md §4.5),
|
||||
* 让 104 与 UI **共用同一份上游**——"什么时候算新值、品质怎么判"只此
|
||||
* 一处决定(M5 遥测链的地基;104 侧消费见「数据可信」104 改读快照,UI 侧 ui_data_feed 属
|
||||
* 一处决定(M5 遥测链的地基;104 改读快照属卡 9,UI 侧 ui_data_feed 属
|
||||
* 「UI 扩展」波次,两者都只是本快照的读者)。
|
||||
* 值口径(批 2「遥测快照发布」的 A-lite 裁决,2026-10-25):**基波有效值瞬时透传**
|
||||
* (同拍 phasor 使用区口径,系数已在 标定折算折算进帧值——现单位 = ADC
|
||||
* 值口径(批 2 卡 8 的 A-lite 裁决,2026-10-25):**基波有效值瞬时透传**
|
||||
* (同拍 phasor 使用区口径,系数已在 L1.5 折算进帧值——现单位 = ADC
|
||||
* 引脚电压 V,二次值语义等 adc_cal_flags() 出 VALID,见
|
||||
* docs/算法/采集与算法层设计说明.md §10 红线③);**不做** 4 点平均 /
|
||||
* 谐波方和根——docs/任务规划/数据可信.md §4.5 明示"200 ms 是上送节拍
|
||||
* 不是平均窗,再平均引入滞后";谐波 k-bin 轨(docs/算法/继电保护功能
|
||||
* 复现规划.md 遥测谐波轨(可选))将来扩展时只许在本结构**尾部追加**或留在发布侧
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 复现规划.md L2-8)将来扩展时只许在本结构**尾部追加**或留在发布侧
|
||||
* 内部状态,既有字段序不得变更。
|
||||
* TEMP 通道(「数据可信」决策 5:不参与线性标定):yc = 最新一拍原始
|
||||
* 帧值透传,qds 恒置 NT——值在场但非交流量语义,UI 按「数据可信」§4.2
|
||||
* 三态表显示占位符,104 无对应点。
|
||||
* 并发(docs/任务规划/数据可信.md §4.5 原文口径):沿用 phasor 的
|
||||
* **双区 + 单字节活跃索引翻转**(双区指针原子交换 同型,不是新机制)——写侧填满
|
||||
* **双区 + 单字节活跃索引翻转**(L2-7 同型,不是新机制)——写侧填满
|
||||
* 计算区后一次 store 翻转,读者先取索引再整体拷贝,全程无锁;
|
||||
* seq 只增,消费者据此判"是否换帧"。
|
||||
* 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(窗自 sample_ring、
|
||||
* 频率/品质由调用方注入)→ 主机侧免桩纯逻辑单测 test/host/meas/。
|
||||
* 频率/品质由调用方注入)→ 主机侧免桩单测 test/host/meas/(L0.5)。
|
||||
* 发布纪律:唯一发布者 = adcTask(在 phasor_run()/freq_run() 之后 200 ms 分频
|
||||
* 调 meas_tick())。评审判据 = 全仓 meas_tick 调用点数 == 1(「数据可信」
|
||||
* §8 风险表"快照双写"行);其余任务/模块**只读**。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 遥测快照发布)
|
||||
* 创建日期:2026-10-25(道D 卡 8)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
|
||||
*/
|
||||
@@ -51,19 +50,19 @@ extern "C" {
|
||||
/* 每点品质位(uint16)。**低字节与线上 104 QDS 字节同值**(OV/NT/IV,数值
|
||||
* 口径同 App/protocol/iec104/iec104_asdu.h 的 IEC104_QDS_*)——但本层**不得**
|
||||
* include 协议层头(分层方向:算法层在协议层之下,反向依赖禁止),故在此自
|
||||
* 定义同值常量;104 侧消费(104 改读快照)直接截低字节上送,一致性由本目录单测与
|
||||
* 定义同值常量;卡 9 的 104 侧消费直接截低字节上送,一致性由本目录单测与
|
||||
* test/host/iec104/ 既有断言双向钉住。高字节为 meas 域扩展位(线上不直接
|
||||
* 出现,由消费者映射): */
|
||||
#define MEAS_QDS_OV (0x0001u) /* 溢出(留位,本项不产出——判据待运行参数化) */
|
||||
#define MEAS_QDS_OV (0x0001u) /* 溢出(留位,卡 8 不产出——判据待运行参数化) */
|
||||
#define MEAS_QDS_NT (0x0020u) /* 非当前值:未就绪 / 语义未处理(TEMP) */
|
||||
#define MEAS_QDS_IV (0x0080u) /* 无效:该路取窗失败,无有效读数 */
|
||||
#define MEAS_QDS_CLIPPED (0x0100u) /* 硬件削顶(adc_cal_flags 位图直通,
|
||||
* 「数据可信」§4.2 三态表;AC 量不可信,
|
||||
* 消费者应映射为品质无效,如 104 改读快照侧的 IV) */
|
||||
* 消费者应映射为品质无效,如卡 9 的 104 IV) */
|
||||
|
||||
/* 遥测快照(docs/任务规划/数据可信.md §4.5 权威布局;实测 72 B,2×72 ≈ 144 B
|
||||
* 双区 = 本项 DoD 预算口径,由 test/host/meas/ 的布局钉死用例锁字段偏移——
|
||||
* 改字段 = 破协议,须同步 104 改读快照 / UI) */
|
||||
* 双区 = 卡 8 DoD 预算口径,由 test/host/meas/ 的布局钉死用例锁字段偏移——
|
||||
* 改字段 = 破协议,须同步卡 9 / UI) */
|
||||
typedef struct {
|
||||
uint32_t seq; /* 只增;消费者据此判断是否换帧 */
|
||||
uint32_t tick_ms; /* 发布拍时基(osKernelGetTickCount 原值) */
|
||||
|
||||
+15
-15
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(全周傅氏相量计算)
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏算法(单频 DFT)——系数表预乘 √2/N,fAmSQ 直接即有效值。
|
||||
* 覆盖 全周傅氏系数表 / 基波相量实例 / 相量有效值输出 / 每 1 ms 滑动窗刷新 / 双区指针原子交换。
|
||||
* 覆盖 L2-1 系数表 / L2-2 实例 / L2-3 有效值 / L2-6 滑动窗 / L2-7 双区交换。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:全周傅氏系数表 等
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7
|
||||
*/
|
||||
|
||||
#include "phasor.h"
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "sample_ring.h"
|
||||
|
||||
/* ===== 全周傅氏系数表 =====
|
||||
/* ===== L2-1 全周傅氏系数表 =====
|
||||
* g_fFFTSin20[k] = sin(2πk/N)·(√2/N)
|
||||
* g_fFFTCos20[k] = cos(2πk/N)·(√2/N) N = PHASOR_N = 20
|
||||
* 权威口径:docs/算法/傅氏算法详解.md §2.3("把 √2 藏进系数表")。
|
||||
@@ -41,23 +41,23 @@ static const float k_fft_cos[PHASOR_N] = {
|
||||
/* √2:幅值 = √2 × 有效值(与系数表的 √2 折算互逆) */
|
||||
#define PHASOR_SQRT2 1.4142135623730951f
|
||||
|
||||
/* ===== 基波相量实例表 =====
|
||||
/* ===== L2-2 相量实例表 =====
|
||||
* 参考项目为 **4 个实例**(C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波,
|
||||
* `APP/CLS100_Dsp/InstCalCell.c`)。本项目按 V0.1 **有效通道**(`UA`/`UN`/`IA`,
|
||||
* 见 docs/算法/继电保护功能复现规划.md §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U"
|
||||
* 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(二次谐波/五次谐波提取入口)。
|
||||
* 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(L1-9)。
|
||||
* 新增实例只需在表尾加一行,其余代码自动适配。 */
|
||||
static const phasor_inst_t k_inst[] = {
|
||||
{ "C01-IA-F1", ADC_SEM_IA, 1u }, /* A 相电流基波(三段式相间过流 / 过负荷) */
|
||||
{ "C02-UA-F1", ADC_SEM_UA, 1u }, /* A 相电压基波(过压 / 欠压、过频 / 低频) */
|
||||
{ "C03-UN-F1", ADC_SEM_UN, 1u }, /* 零序电压基波(零序过压) */
|
||||
{ "C04-IA-F2", ADC_SEM_IA, 2u }, /* 二次谐波(涌流制动) */
|
||||
{ "C05-IA-F5", ADC_SEM_IA, 5u }, /* 五次谐波(二次谐波/五次谐波提取入口(选线用)) */
|
||||
{ "C01-IA-F1", ADC_SEM_IA, 1u }, /* A 相电流基波(L3-1 过流 / L3-3 过负荷) */
|
||||
{ "C02-UA-F1", ADC_SEM_UA, 1u }, /* A 相电压基波(L3-4 过压/欠压、L3-5 频率) */
|
||||
{ "C03-UN-F1", ADC_SEM_UN, 1u }, /* 零序电压基波(L3-6 零序过压) */
|
||||
{ "C04-IA-F2", ADC_SEM_IA, 2u }, /* 二次谐波(L3-7 涌流制动) */
|
||||
{ "C05-IA-F5", ADC_SEM_IA, 5u }, /* 五次谐波(L1-9 选线) */
|
||||
};
|
||||
|
||||
#define PHASOR_INST_N (sizeof(k_inst) / sizeof(k_inst[0]))
|
||||
|
||||
/* ===== 双区指针原子交换(计算区 / 使用区)=====
|
||||
/* ===== L2-7 双区(计算区 / 使用区)=====
|
||||
* 本拍结果先写入"计算区",全部实例写完后**一次性**切换 s_use_zone
|
||||
* (单字节 store,Cortex-M 上原子),消费侧读"使用区" → 无锁、无撕裂。
|
||||
* s_use_zone 声明 volatile:阻止编译器把结果写入挪到切换之后
|
||||
@@ -83,7 +83,7 @@ static void phasor_zero(phasor_t *p) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(全周傅氏系数表 + 相量有效值输出)
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
@@ -134,7 +134,7 @@ int phasor_init(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
@@ -159,7 +159,7 @@ const phasor_inst_t *phasor_inst(uint8_t idx) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例 + 双区原子切换(每 1 ms 滑动窗刷新 / 双区指针原子交换)
|
||||
* 函数功能:重算全部实例 + 双区原子切换(L2-6 / L2-7)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先 sample_ring_push_frame() 压入本拍数据
|
||||
|
||||
+10
-11
@@ -1,19 +1,18 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(全周傅氏相量计算)
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏(全周波傅里叶)算法——对一周波 20 点采样做单频 DFT,
|
||||
* 产出基波/谐波相量。系数表**预乘 √2/N**(口径见
|
||||
* `docs/算法/傅氏算法详解.md` §2.3),故 `fAmSQ = √(fRe²+fIm²)`
|
||||
* **直接就是有效值**,无需再除 √2。
|
||||
* 覆盖规划 §3.2:全周傅氏系数表 / 基波相量实例 / 相量有效值输出 /
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 每 1 ms 滑动窗刷新 / 双区指针原子交换。
|
||||
* 覆盖规划 §3.2:L2-1 系数表 / L2-2 实例 / L2-3 有效值输出 /
|
||||
* L2-6 每 1 ms 滑动窗 / L2-7 双区指针原子交换。
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰任务调度、不自己取数(输入由
|
||||
* `sample_ring` 提供)→ 主机侧可纯逻辑单测。
|
||||
* `sample_ring` 提供)→ 主机侧可单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:全周傅氏系数表 等(基波 + 谐波相量)
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7(基波 + 谐波相量)
|
||||
*/
|
||||
|
||||
#ifndef PHASOR_H
|
||||
@@ -36,7 +35,7 @@ extern "C" {
|
||||
* fIm - 虚部 = b1/√2(有效值相量虚部)
|
||||
* fAm - 幅值 = √2·√(fRe²+fIm²)(正弦峰值 A1)
|
||||
* fAmSQ - 有效值 = √(fRe²+fIm²)(系数已折算 √2/N,故直接即有效值)
|
||||
* 相位输出按需由 `atan2(fIm, fRe)` 导出,不常驻本结构。 */
|
||||
* 相位(L2-4)按需由 `atan2(fIm, fRe)` 导出,不常驻本结构。 */
|
||||
typedef struct {
|
||||
float fRe;
|
||||
float fIm;
|
||||
@@ -44,7 +43,7 @@ typedef struct {
|
||||
float fAmSQ;
|
||||
} phasor_t;
|
||||
|
||||
/* 基波相量实例:**数据驱动**——新增实例只需在实例表尾加一行 */
|
||||
/* 相量实例(L2-2):**数据驱动**——新增实例只需在实例表尾加一行 */
|
||||
typedef struct {
|
||||
const char *name; /* 实例名(日志 / UI 用) */
|
||||
adc_semantic_t sem; /* 语义通道 */
|
||||
@@ -61,7 +60,7 @@ typedef struct {
|
||||
int phasor_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
@@ -79,7 +78,7 @@ uint8_t phasor_inst_count(void);
|
||||
const phasor_inst_t *phasor_inst(uint8_t idx);
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例并从计算区原子切换到使用区(每 1 ms 滑动窗刷新 + 双区指针原子交换)
|
||||
* 函数功能:重算全部实例并从计算区原子切换到使用区(L2-6 滑动窗 + L2-7 双区交换)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先经 `sample_ring_push_frame()` 压入本拍数据
|
||||
@@ -100,7 +99,7 @@ void phasor_run(void);
|
||||
int phasor_get(uint8_t idx, phasor_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(全周傅氏系数表 + 相量有效值输出)
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新;长度须 = PHASOR_N)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
|
||||
+6
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:保护元件框架(保护判据,P1 子集)——实现
|
||||
* 模块名称:保护元件框架(L3,P1 子集)——实现
|
||||
* 模块功能:见 prot_elm.h。判据核 prot_elm_eval() 为纯逻辑(prot_set RAM +
|
||||
* 测量数组 → 状态机),prot_elm_run() 是 phasor→测量的薄适配层。
|
||||
*
|
||||
@@ -53,7 +53,7 @@ static const pe_cfg_t k_pe[PE_COUNT] = { /* ← 表头注释见上,行内列
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PROT_ELM_NO_TRIP, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PROT_ELM_NO_TRIP, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PROT_ELM_NO_TRIP, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 过频/低频(过频 / 低频,B8):测量量 = freq 层测得频率(Hz),返回系数独立
|
||||
/* 过频/低频(L3-5,B8):测量量 = freq 层测得频率(Hz),返回系数独立
|
||||
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PROT_ELM_NO_TRIP, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PROT_ELM_NO_TRIP, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
@@ -136,8 +136,7 @@ void prot_elm_request_reset(void) {
|
||||
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
|
||||
* meas - PE_COUNT 个测量值(工程量 A/V);NULL 视为全 0
|
||||
* 返 回 值:无
|
||||
* 函数说明:投退=0 或定值闭锁(定值校验和)→ 状态整体复位、不判据;
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 函数说明:投退=0 或定值闭锁(L5-1)→ 状态整体复位、不判据;
|
||||
* 滞回 = 已启动用返回门槛(定值×系数)、未启动用动作门槛;
|
||||
* 延时 dly=0 → 启动同拍即动作;返回即时(P4 接出口时再补返回延时)。
|
||||
*/
|
||||
@@ -204,8 +203,8 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
|
||||
* 函数功能:适配层——按实例表 src 组装测量数组后调判据核
|
||||
* 入口参数:now_ms - 单调时基(tick ms)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅保护消费者任务上下文;测量已是 标定折算工程量(未标定域由
|
||||
* protectTask 未标定即闭锁挡在门外,本函数不感知标定态)
|
||||
* 限定条件:仅保护消费者任务上下文;测量已是 L1.5 工程量(未标定域由
|
||||
* protectTask 卡 5 闭锁挡在门外,本函数不感知标定态)
|
||||
* 函数说明:src=PHASOR 行任一相量未就绪(未满一周波)→ **整拍跳过**,
|
||||
* 不引入陈旧/半新测量(B5 口径)。src=FREQ 行:freq 有有效读数
|
||||
* (started 且幅值门 OK)→ 填测得频率;否则按方向填"必不启动"值
|
||||
@@ -260,7 +259,7 @@ void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out) {
|
||||
/*
|
||||
* 函数功能:聚合动作位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 元件 i asserted(供 出口与记录 / 心跳日志)
|
||||
* 返 回 值:bit i = 元件 i asserted(供 L4 出口 / 心跳日志)
|
||||
*/
|
||||
uint32_t prot_elm_active_mask(void) {
|
||||
uint32_t mask = 0u;
|
||||
|
||||
+13
-14
@@ -1,16 +1,15 @@
|
||||
/*
|
||||
* 模块名称:保护元件框架(保护判据,P1 子集)
|
||||
* 模块名称:保护元件框架(L3,P1 子集)
|
||||
* 模块功能:通用"越限 + 返回系数滞回 + 定时限"元件核 + 实例表(数据驱动,
|
||||
* 同 phasor 实例表模式)。判据为**纯逻辑**(吃测量值 + 定值),
|
||||
* 主机侧可离线单测;出口/SOE(出口与记录)与任务编排为后续。
|
||||
* 主机侧可离线单测;出口/SOE(L4)与任务编排为后续。
|
||||
*
|
||||
* V0.1 有效实例(双转换器无共同采样触发 → 仅 TPAFE 三路):
|
||||
* OC1/OC2/OC3 三段式相间过流(A 相 IA)
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* OL 过负荷(A 相 IA)
|
||||
* OV/UV 过压/欠压(UA,过压 / 欠压)
|
||||
* 3U0 零序过压(UN 外部零序电压通道)
|
||||
* OF/UF 过频/低频(测量量 = freq 层测得频率,过频 / 低频,B8 落地)
|
||||
* OC1/OC2/OC3 三段式相间过流(A 相 IA,L3-1)
|
||||
* OL 过负荷(A 相 IA,L3-3)
|
||||
* OV/UV 过压/欠压(UA,L3-4)
|
||||
* 3U0 零序过压(UN 外部零序电压通道,L3-6)
|
||||
* OF/UF 过频/低频(测量量 = freq 层测得频率,L3-5,B8 落地)
|
||||
*
|
||||
* 测量数组口径:相量类实例 = 基波有效值(A/V);OF/UF = 测得频率(Hz)。
|
||||
* run() 按实例表 src 字段取数(0=phasor rms,1=freq)。freq 无有效读数
|
||||
@@ -23,9 +22,9 @@
|
||||
* - 返回**即时**(参考 GRly_TimeRelay 带 10 ms 返回延时——V0.1 无出口硬件,
|
||||
* 抖动只影响日志,P4 接出口时再补返回延时);
|
||||
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
|
||||
* - 测量输入 = adc_task 经 adc_cal_apply() 折算后的工程量(标定折算,标定折算已落地):
|
||||
* - 测量输入 = adc_task 经 adc_cal_apply() 折算后的工程量(L1.5,卡 2 已落地):
|
||||
* 未标定(VALID=0)时单位只是 ADC 引脚电压——该域下判据必然错配,由
|
||||
* protectTask 的「数据可信」未标定即闭锁停走整条判据链,元件核本身不掺和
|
||||
* protectTask 的未标定闭锁(卡 5)停走整条判据链,元件核本身不掺和
|
||||
* 标定语义(保持纯逻辑);标定后单位 = 二次值,定值口径才名副其实。
|
||||
*
|
||||
* 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务
|
||||
@@ -49,8 +48,8 @@ typedef enum {
|
||||
PE_OV, /* 过压(UA) */
|
||||
PE_UV, /* 欠压(UA,欠量方向) */
|
||||
PE_3U0, /* 零序过压(UN) */
|
||||
PE_OF, /* 过频(测量量 = freq 层测得频率,B8,过频 / 低频) */
|
||||
PE_UF, /* 低频(测量量 = freq 层测得频率,欠量方向,B8,过频 / 低频) */
|
||||
PE_OF, /* 过频(测量量 = freq 层测得频率,B8,L3-5) */
|
||||
PE_UF, /* 低频(测量量 = freq 层测得频率,欠量方向,B8,L3-5) */
|
||||
PE_COUNT
|
||||
} prot_elm_id_t;
|
||||
|
||||
@@ -82,7 +81,7 @@ void prot_elm_request_reset(void);
|
||||
* meas - PE_COUNT 个测量值(工程量:A / V),NULL 视为全 0
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅保护消费者任务上下文;调用方保证定值已装载
|
||||
* 函数说明:越限方向由实例表决定;投退=0 或定值闭锁(定值校验和)→ 状态复位、
|
||||
* 函数说明:越限方向由实例表决定;投退=0 或定值闭锁(L5-1)→ 状态复位、
|
||||
* 不判据。延时判定用 (uint32_t)(int32_t)(now - t0) >= dly,回绕安全。
|
||||
*/
|
||||
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]);
|
||||
@@ -98,7 +97,7 @@ void prot_elm_run(uint32_t now_ms);
|
||||
/* 状态快照拷贝(NULL 安全;越界全零) */
|
||||
void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out);
|
||||
|
||||
/* 动作位图(bit i = 元件 i asserted),供 出口与记录 / 心跳日志 */
|
||||
/* 动作位图(bit i = 元件 i asserted),供 L4 出口 / 心跳日志 */
|
||||
uint32_t prot_elm_active_mask(void);
|
||||
|
||||
/* 启动位图(bit i = 元件 i picked),供「保护启动」记录边沿(task-10,2026-10-26):
|
||||
|
||||
+7
-8
@@ -1,17 +1,16 @@
|
||||
/*
|
||||
* 模块名称:保护定值模型(P1,定值校验和 载体)——实现
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 模块名称:保护定值模型(P1,L5-1 载体)——实现
|
||||
* 模块功能:见 prot_set.h。纯逻辑,零平台依赖(仅 stdint.h + 自身头)。
|
||||
*
|
||||
* ⚠ 默认值/范围为拟定值(参考 EEPROM 出厂默认表不在本仓库,规划 §10.3 待补);
|
||||
* 通讯地址逐项对照参考 Comm/Pub/Const_CommSetPara.c g_tCommSetParaTab。
|
||||
*
|
||||
* 量纲口径(「数据可信」下游常数按工程量重定一项复核,2026-10-25):A/V 项 = **二次值**工程量,
|
||||
* 量纲口径(「数据可信」卡 6 复核,2026-10-25):A/V 项 = **二次值**工程量,
|
||||
* 标称基准 100 V / 1 A(UA/UB/UC 相电压、3U0、OV/UV 同域——OV 120 = 1.2×Un、
|
||||
* UV 20 = 0.2×Un、3U0 15 = 0.15×Un;OC1/2/3 = 10/5/2 ×Ir、OL = 1.0×Ir,
|
||||
* 三段递升与过负荷低于速断均自洽)。ADC 帧仅在 adc_cal VALID 置位后才处
|
||||
* 于此域(未标定 = 引脚电压域,由未标定即闭锁停走判据,见 protect_task.c);
|
||||
* min/max 范围覆盖 0~4×标称,无需随这一项改动数值——换算依据与三处下游
|
||||
* 于此域(未标定 = 引脚电压域,由卡 5 闭锁停走判据,见 protect_task.c);
|
||||
* min/max 范围覆盖 0~4×标称,无需随本卡改动数值——换算依据与三处下游
|
||||
* 常数同源,见 docs/任务规划/数据可信.md §2.3。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
@@ -56,7 +55,7 @@ const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = {
|
||||
{ 0x8825, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10000.0) },
|
||||
{ 0x8826, 0u, PROT_SET_Q10(5.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10.0) },
|
||||
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 全局软压板投退(YK12,B7) */
|
||||
/* --- 过频 / 低频(B8):地址全 0 = 本地项(参考逐项地址不在
|
||||
/* --- 过频 / 低频(L3-5,B8):地址全 0 = 本地项(参考逐项地址不在
|
||||
* 本仓库,规划 §5.1 仅示"8820~8882 含低压/低频"类目);
|
||||
* 默认值/范围**拟定**(DTU 馈线典型量级),待对照参考出厂表修订 --- */
|
||||
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OF 投退(本地) */
|
||||
@@ -76,7 +75,7 @@ static volatile uint8_t s_act; /* 活跃区下标(0/1);交换 =
|
||||
static uint32_t s_sum; /* 封存的正码 */
|
||||
static uint32_t s_sumn; /* 封存的反码 */
|
||||
static uint64_t s_bad_mask; /* load_raw/parse 越界位图(B8:40 项 → 64 位) */
|
||||
static uint8_t s_locked; /* 定值非法 → 闭锁(定值校验和) */
|
||||
static uint8_t s_locked; /* 定值非法 → 闭锁(L5-1) */
|
||||
static uint8_t s_dirty; /* 未落盘定值改动(B7 持久化脏标志) */
|
||||
|
||||
/*
|
||||
@@ -294,7 +293,7 @@ void prot_set_seal(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:定值自检(定值校验和)——重算 sum32 并与封存值比对
|
||||
* 函数功能:定值自检(L5-1)——重算 sum32 并与封存值比对
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 通过 / -1 失配(已置闭锁)
|
||||
* 函数说明:失配 → s_locked 置位并保持,直到下一次 seal;
|
||||
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:保护定值模型(P1,定值校验和 载体)
|
||||
* 模块名称:保护定值模型(P1,L5-1 载体)
|
||||
* 模块功能:保护定值的 RAM 模型——编号 / 通讯地址 / 范围 / 默认值 / 校验和自检,
|
||||
* 以及 B7 定值持久化的**纯逻辑层**(blob 序列化/解析 + 原子装载)。
|
||||
* 文件读写胶水在 App/cfg/prot_set_store.c(MCU 端)。
|
||||
@@ -8,7 +8,7 @@
|
||||
* - 定点存储:Q10(×1024),与参考系统定点系数 2^10 一致(§5.3);
|
||||
* - 通讯地址:与参考 g_tCommSetParaTab(Comm/Pub/Const_CommSetPara.c)逐项对齐,
|
||||
* 基址 0x8820(=34848);告警类沿用运行参数段 0x8027/0x8029;
|
||||
* - 校验和(定值校验和):sum32 正码 + 反码成对封存(参考 tagPRS 的
|
||||
* - 校验和(L5-1):sum32 正码 + 反码成对封存(参考 tagPRS 的
|
||||
* dwTagSum/dwTagSumN 模式),selfcheck 失配 → 置闭锁;
|
||||
* - ⚠ 默认值/范围为**拟定值**:参考 EEPROM 出厂默认表未在本仓库,
|
||||
* 待对照后逐项修订(规划 §10.3 待补项);范围先按 DTU 馈线典型量级。
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
/* 定值项编号(枚举序 = RAM 数组序 = 校验和覆盖序,只增不删不改序) */
|
||||
typedef enum {
|
||||
/* --- 三段式相间过流(A 相):投退/跳闸投退/电流/延时 --- */
|
||||
/* --- 三段式相间过流(A 相,L3-1):投退/跳闸投退/电流/延时 --- */
|
||||
PS_OC1_EN = 0, /* 0x8856 Ⅰ段投退 */
|
||||
PS_OC1_TRIP, /* 0x8857 Ⅰ段跳闸投退(0 告警 / 1 跳闸) */
|
||||
PS_OC1_CUR, /* 0x8858 Ⅰ段电流定值(A,二次值) */
|
||||
@@ -49,19 +49,19 @@ typedef enum {
|
||||
PS_OC3_TRIP, /* 0x885F */
|
||||
PS_OC3_CUR, /* 0x8860 */
|
||||
PS_OC3_DLY, /* 0x8861 */
|
||||
/* --- 过负荷(A 相) --- */
|
||||
/* --- 过负荷(A 相,L3-3) --- */
|
||||
PS_OL_EN, /* 0x8871 */
|
||||
PS_OL_TRIP, /* 0x8872 */
|
||||
PS_OL_CUR, /* 0x8873 */
|
||||
PS_OL_DLY, /* 0x8874 */
|
||||
/* --- 过压 / 欠压(UA;沿用参考告警段 0x8029/0x8027) --- */
|
||||
/* --- 过压 / 欠压(UA,L3-4;沿用参考告警段 0x8029/0x8027) --- */
|
||||
PS_OV_EN, /* 本地投退(参考告警类无独立投退,V0.1 补) */
|
||||
PS_OV_VOL, /* 0x8029 过电压门限(V,二次值) */
|
||||
PS_OV_DLY, /* 0x802A 过电压延时(ms) */
|
||||
PS_UV_EN, /* 本地投退 */
|
||||
PS_UV_VOL, /* 0x8027 低电压门限(V,二次值) */
|
||||
PS_UV_DLY, /* 0x8028 低电压延时(ms) */
|
||||
/* --- 零序过压(UN) --- */
|
||||
/* --- 零序过压(UN,L3-6) --- */
|
||||
PS_3U0_EN, /* 0x888E */
|
||||
PS_3U0_VOL, /* 0x888F 3U0 定值(V,二次值) */
|
||||
PS_3U0_DLY, /* 0x8890 延时(ms) */
|
||||
@@ -74,7 +74,7 @@ typedef enum {
|
||||
PS_CT_SEC, /* 0x8826 CT 二次额定(A) */
|
||||
/* --- 全局软压板(B7):104 YK12 两段式投退;0 = 保护退出服务 --- */
|
||||
PS_GLOBAL_EN, /* 本地项(无通讯地址,经 YK12 写、C_RD 不可读) */
|
||||
/* --- 过频 / 低频(B8):投退/动作频率/延时 + 各自返回系数 ---
|
||||
/* --- 过频 / 低频(L3-5,B8):投退/动作频率/延时 + 各自返回系数 ---
|
||||
* ⚠ 通讯地址:参考 g_tCommSetParaTab 的保护定值段(0x8820~0x8882)
|
||||
* 含"低压/低频"类目(规划 §5.1),但 OF/UF 的**逐项地址不在本仓库**
|
||||
* (参考 Comm 源文件未随仓库提供)→ 按"找不到对应即本地项"规则全部
|
||||
@@ -130,7 +130,7 @@ int32_t prot_set_get(prot_set_id_t id); /* Q10 原值 */
|
||||
float prot_set_get_f(prot_set_id_t id); /* 工程量浮点 */
|
||||
uint8_t prot_set_get_ctl(prot_set_id_t id); /* 投退项:!=0 视为投入 */
|
||||
|
||||
/* 校验和(定值校验和):sum32 覆盖 RAM 值区;seal 封存正/反码对 */
|
||||
/* 校验和(L5-1):sum32 覆盖 RAM 值区;seal 封存正/反码对 */
|
||||
uint32_t prot_set_checksum(void);
|
||||
void prot_set_seal(void);
|
||||
/* 自检:重算并与封存值比对,失配 → 置闭锁并返回 -1(已闭锁也返回 -1) */
|
||||
|
||||
+28
-111
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* 模块名称:运行参数段(全周傅氏相量/保护判据整定层,「数据可信」这一项)——实现
|
||||
* 模块功能:见 run_par.h。工厂默认 = 「数据可信」下游常数按工程量重定后的额定 0.5 % 档(电压 500
|
||||
* 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)——实现
|
||||
* 模块功能:见 run_par.h。工厂默认 = 「数据可信」卡 6 额定 0.5 % 档(电压 500
|
||||
* mV / 电流 5 mA / 频率 500 mHz,毫单位 int32 定点落盘);[run_par]
|
||||
* 段 9 键(db_ua…db_freq,均 ≤15 字符)≤ CFG_FILE_MAX_KEYS 不拆段
|
||||
* (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs],
|
||||
* 系数唯一真源在那两段,lead 裁②a)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 运行参数段)
|
||||
* 创建日期:2026-10-25(道D 卡 10)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal
|
||||
* + B7 式脏标志落盘请求
|
||||
@@ -52,7 +52,7 @@ typedef struct {
|
||||
} run_par_db_t;
|
||||
|
||||
static const run_par_db_t s_db_rom = {
|
||||
{ 500L, 500L, 500L, 500L, /* UA/UB/UC/UN:0.5 V(工程量重定后的额定档) */
|
||||
{ 500L, 500L, 500L, 500L, /* UA/UB/UC/UN:0.5 V(卡 6 额定档) */
|
||||
5L, 5L, 5L, 5L, /* IA/IB/IC/IN:0.005 A */
|
||||
500L } /* 频率:0.5 Hz(绝对档,非额定百分比) */
|
||||
};
|
||||
@@ -77,21 +77,10 @@ static uint8_t s_dirty;
|
||||
|
||||
/* 内部函数实现区 */
|
||||
|
||||
/*
|
||||
* 函数功能:毫单位定点 → 工程量浮点(**本模块唯一换算点**)
|
||||
* 入口参数:milli - 毫单位 内 int32_t
|
||||
* 返 回 值:工程量 内 float
|
||||
* 限定条件:仅本文件内使用(消费侧一律走 run_par_deadband()/run_par_db_range(),
|
||||
* 不许各自再写一次 /1000——那才是"第二真源"最常见的样子)
|
||||
*/
|
||||
static float s_milli_to_eng(int32_t milli) {
|
||||
return (float)milli / 1000.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:死区点量程上限(毫单位)
|
||||
* 入口参数:idx - 0..8 uint8_t
|
||||
* 返 回 值:上限;idx 越界返 0 内 int32_t
|
||||
* 返 回 值:上限;idx 越界返 0 长(一切值皆拒) 内 int32_t
|
||||
*/
|
||||
static int32_t s_db_cap_milli(uint8_t idx) {
|
||||
if (idx < 4u) {
|
||||
@@ -167,29 +156,7 @@ float run_par_deadband(uint8_t idx) {
|
||||
if (idx >= RUN_PAR_DB_N) {
|
||||
return 0.0f;
|
||||
}
|
||||
return s_milli_to_eng(s_db.db[idx]); /* 毫→工程:唯一换算点 = s_milli_to_eng */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口**
|
||||
* 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(工程量 V/A/Hz) float *
|
||||
* 返回值:0 - 已写出;-1 - idx 越界或指针为空(调用方不得回退成自造默认域) int
|
||||
* 限定条件:任意上下文(纯常量读,无锁、无 I/O)
|
||||
* 函数说明:为什么公开这个口:UI 整定页要给数字编辑器设 min/max,而这些上限的
|
||||
* 真源是本模块的 RUN_PAR_DB_MAX_*(电压 50V / 电流 1A / 频率 5Hz,
|
||||
* "额定 × 二档裕量")。页面里抄一份就是第二真源——判据「不新增第二份
|
||||
* 真源」最典型的复发形态是"顺手把常量写进界面代码"。因此本函数的
|
||||
* 消费者是装配层 App/app/ui_data_feed.c 的注入表实现,不是页面本身。
|
||||
* ⚠ 给的是**粗守域**(挡误写),不是工况域;真提交仍由 s_db_check
|
||||
* 同源守卫裁(104 选择段与 UI 编辑器共用同一批常量,不会漂移)。
|
||||
*/
|
||||
int run_par_db_range(uint8_t idx, float *lo, float *hi) {
|
||||
if ((idx >= RUN_PAR_DB_N) || (lo == NULL) || (hi == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
*lo = 0.0f;
|
||||
*hi = s_milli_to_eng(s_db_cap_milli(idx));
|
||||
return 0;
|
||||
return ((float)s_db.db[idx]) / 1000.0f; /* 毫→工程唯一换算点 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -231,74 +198,19 @@ int run_par_104_read(uint32_t addr, float *out) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写某路系数 k(µV/码)**并在同一函数内置脏**——整定的语义写入口
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;k_uv - 系数(µV/码,>0) float
|
||||
* 返回值:0 - RAM 副本已写 + 已置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:整定路径上下文(iec104Task 的 104 执行段;UI 经 ui_data_yt_ops_t 由
|
||||
* 装配层 App/app/ui_data_feed.c 转进来)。勿在 adcTask 调用
|
||||
* 函数说明:**为什么"写值 + 置脏"必须长在一个函数里**(lead 裁②,2026-10-27):
|
||||
* 置脏是持久化承诺,写值是 RAM 事实,两者拆开就会出现"屏上已改、盘上
|
||||
* 永不落、且没有任何错误返回"这种无人报错的状态——本仓今天已实测到它的
|
||||
* 既有形态(绕过本模块直调 adc_cal_set() 的写者不置脏,值永远不落盘,
|
||||
* 另立 task-43 处置)。所以这里不提供单独的 mark_dirty 公开口:它一旦
|
||||
* 存在就一定有人只调它(同"零消费者的状态查询口"那条教训)。
|
||||
* 零点 b 走 RMW 取**当前生效值**(经合法带哨兵,天然满足再写校验)。
|
||||
* 粗守(0 < k ≤ RUN_PAR_K_MAX_UV)只为挡误写,真带在 adc_cal 执行段裁:
|
||||
* 两层各说各的,页面上"合法域内仍被拒(ERR)"就是执行段拒的,不伪装。
|
||||
*/
|
||||
int run_par_cal_k_set(adc_semantic_t sem, float k_uv) {
|
||||
int32_t k_q16;
|
||||
int32_t b_codes;
|
||||
|
||||
/* 通道号不在这里判:adc_cal_set 自带合法性与合法带裁决(越界一律 -1),
|
||||
* 本层再判一次就是第二真源。粗守只挡误写:0 < k ≤ RUN_PAR_K_MAX_UV */
|
||||
if (!isfinite(k_uv) || !(k_uv > 0.0f) || (k_uv > RUN_PAR_K_MAX_UV)) {
|
||||
return -1;
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(k_uv * 65536.0f);
|
||||
b_codes = (int32_t)lrintf(adc_cal_b_codes(sem));
|
||||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||||
return -1; /* 执行段拒:副本未动,脏位也不置 = 不承诺任何持久化 */
|
||||
}
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写某路零点偏移 b(码值)并置脏(与 k 同款语义,粗守 ±1e6 码)
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;b_codes - 零点(码值) int32_t
|
||||
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:同 run_par_cal_k_set()
|
||||
* 函数说明:k 侧 RMW 用当前生效值;±8192 真带由 adc_cal 执行段裁(粗守目的是
|
||||
* 挡误写不是限工况)。本函数的消费者 = 104 的 0x8048~ 分支(改造后),
|
||||
* 不是"为将来预留"的口
|
||||
*/
|
||||
int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes) {
|
||||
int32_t k_q16;
|
||||
|
||||
if (((float)b_codes < -1.0e6f) || ((float)b_codes > 1.0e6f)) {
|
||||
return -1;
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(adc_cal_k_uv(sem) * 65536.0f);
|
||||
if (adc_cal_set(sem, k_q16, b_codes) != 0) {
|
||||
return -1;
|
||||
}
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址写(两段式;k/b 经本模块语义 setter——**run_par 不存
|
||||
* 函数功能:104 运行参数地址写(两段式;k/b 直通 adc_cal_set——**run_par 不存
|
||||
* 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a)
|
||||
* 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int
|
||||
* 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带
|
||||
* 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此
|
||||
* 明说防"选择过了怎么还 PN"的现场困惑)。k/b 两支自 2026-10-27 起一律
|
||||
* 走 run_par_cal_k_set() / run_par_cal_b_set():写值与置脏在同一个函数
|
||||
* 里发生,本函数不再自己碰 s_dirty(lead 裁②:消除两份置脏逻辑)。
|
||||
* 明说防"选择过了怎么还 PN"的现场困惑);k/b 单侧写 RMW 用**当前
|
||||
* 生效值**做另一侧(生效值经合法带哨兵,天然满足再写校验)。
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */
|
||||
int32_t k_q16;
|
||||
int32_t b_codes;
|
||||
int rc;
|
||||
|
||||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||||
int32_t milli;
|
||||
@@ -322,20 +234,29 @@ int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||||
if (select != 0u) {
|
||||
return 0; /* 粗守过了 ≠ 执行必过:合法带裁决在执行段(见函数头) */
|
||||
}
|
||||
/* 写值 + 置脏收进语义 setter(粗守 0<k≤上限 也在里面,与本支
|
||||
* 原先那条逐字同值守卫一致,行为不变)*/
|
||||
return run_par_cal_k_set((adc_semantic_t)slot, v);
|
||||
k_q16 = (int32_t)lrintf(v * 65536.0f);
|
||||
b_codes = (int32_t)lrintf(adc_cal_b_codes((adc_semantic_t)slot));
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||||
slot = (uint8_t)(addr - RUN_PAR_IOA_B0);
|
||||
if (!isfinite(v)) {
|
||||
return -1; /* NaN/Inf 不得进 lrintf(该转换是 UB) */
|
||||
if (!isfinite(v) || (v < -1000000.0f) || (v > 1000000.0f)) {
|
||||
return -1; /* 粗守 ±1e6 码;±8192 真带由 adc_cal 执行段裁 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0;
|
||||
}
|
||||
/* 同款语义 setter:粗守 ±1e6 码在 setter 内,±8192 真带由 adc_cal 执行段裁 */
|
||||
return run_par_cal_b_set((adc_semantic_t)slot, (int32_t)lrintf(v));
|
||||
b_codes = (int32_t)lrintf(v);
|
||||
k_q16 = (int32_t)lrintf(adc_cal_k_uv((adc_semantic_t)slot) * 65536.0f);
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
@@ -354,9 +275,6 @@ uint8_t run_par_dirty_peek(void) {
|
||||
* 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动
|
||||
* 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的
|
||||
* [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。
|
||||
* 成功路径上给标定层盖一次落盘水位(adc_cal_note_persisted())——它是
|
||||
* **观测回执**,不参与本函数任何判定;失败路径不盖,于是"save 一直失败"
|
||||
* 在标定层侧表现为差额长期不归零(道G task-43 第②项)。
|
||||
*/
|
||||
int run_par_store_save(void) {
|
||||
if (s_dirty == 0u) {
|
||||
@@ -367,6 +285,5 @@ int run_par_store_save(void) {
|
||||
return -1;
|
||||
}
|
||||
s_dirty = 0u;
|
||||
adc_cal_note_persisted(); /* 观测回执:水位追平,差额归零(不是判定输入) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
+9
-78
@@ -1,17 +1,17 @@
|
||||
/*
|
||||
* 模块名称:运行参数段(全周傅氏相量/保护判据整定层,「数据可信」这一项)
|
||||
* 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)
|
||||
* 模块功能:遥测死区(9 点)与通道标定系数覆盖(k/b 各 8 路)的**单一真源**:
|
||||
* - 死区 = 本模块 int32 毫单位 RAM 副本(注册进 cfg 引擎 [run_par] 段,
|
||||
* 工厂默认 = 「数据可信」下游常数按工程量重定后的额定 0.5 % 档),104 消费侧
|
||||
* 工厂默认 = 「数据可信」卡 6 额定 0.5 % 档),104 消费侧
|
||||
* (App/protocol/iec104/iec104_pts.c)每拍实时读取——无投递、无缓存
|
||||
* 副本(104 改读快照曾在 pts 侧留运行时副本 s_yc_db,运行参数段落地后收编于此,
|
||||
* 副本(卡 9 曾在 pts 侧留运行时副本 s_yc_db,卡 10 落地后收编于此,
|
||||
* 双源必打架);
|
||||
* - 系数覆盖 = **直通 adc_cal_set()**([adc_cal]/[adc_cal_ofs] 是系数
|
||||
* 唯一真源,标定落盘与整定入口已定;本模块只加 104 地址映射 + 落盘请求,不另存
|
||||
* 唯一真源,卡 4 已定;本模块只加 104 地址映射 + 落盘请求,不另存
|
||||
* 一份系数——同 D1 裁"两处判断必打架"逻辑)。
|
||||
* 104 地址映射(docs/算法/继电保护功能复现规划.md §5.1 运行参数类
|
||||
* 0x8020H~0x8058H;逐项表原文"待补"(参考项目 CommPara_ConfigTable
|
||||
* 不在仓),本表即运行参数段的落实,送检点表若另给权威地址只改常量段:
|
||||
* 不在仓),本表即卡 10 的落实,送检点表若另给权威地址只改常量段:
|
||||
* 0x8030~0x8038 = 死区 UA/UB/UC/UN/IA/IB/IC/IN/FREQ(工程量 V/A/Hz);
|
||||
* 0x8040~0x8047 = k 覆盖(µV/码);0x8048~0x804F = b 覆盖(码值,可负);
|
||||
* 其余留白(未启用类别),**0x8027~0x802A 属 prot_set 告警段**,本表
|
||||
@@ -22,18 +22,6 @@
|
||||
* 保存(含 [run_par] 与刚被 adc_cal_set 改写的 [adc_cal]/[adc_cal_ofs]
|
||||
* 副本)→ 成功清脏;失败留脏 = 下轮天然重试。本模块自身零文件 IO
|
||||
* (属主纪律)。
|
||||
* ⚠ 三份职责别混(道G task-43 第③项,此前只有意图式陈述):
|
||||
* - **谁写 RAM**:系数/零点的唯一真源是 adc_cal 的引擎副本,而本模块那两支
|
||||
* 语义 setter(run_par_cal_k_set / run_par_cal_b_set,task-7 落地)是**唯一
|
||||
* "写值 + 置脏"成对发生**的入口;任何消费者绕过它们直调 adc_cal_set(),
|
||||
* 值只进 RAM、脏标志不动 ⇒ 屏上"当前生效"是新的、盘上仍是旧的、重启即丢,
|
||||
* 且该路径**没有错误返回**。这条形态现在可观测:adc_cal 侧
|
||||
* set_ok 与落盘水位的差额 > 0,而 run_par_dirty_peek() 恒为 0。
|
||||
* - **谁落盘 / 什么时机**:只有 defaultTask 的 run_par_store_save()(由
|
||||
* iec104Task peek 后置的 SYS_EVT_RUN_PAR_SAVE 驱动);本模块零文件 IO。
|
||||
* - **cfg_file_poll() 不是保存路径**:它的方向是**盘 → RAM 的重载**(外部
|
||||
* 改档 → 沉降去抖 → 重载),拿它"兜住"未落盘的改值会把 FTP 侧的旧档倒灌
|
||||
* 回 RAM,比不兜更坏。设计依据见 docs/算法/采集与算法层设计说明.md §10.1。
|
||||
* 并发契约:引擎副本 int32 对齐字读写在 Cortex-M4 上原子;poll 重载 µs 级窗口
|
||||
* 为「数据可信」§4.4 明载设计(adc_cal 同款)。写路径 = 104 执行段
|
||||
* (iec104Task)与引擎读路径互不加锁,勿改成"读侧缓存"(缓存 = 新真源)。
|
||||
@@ -41,22 +29,10 @@
|
||||
* 接口级)→ 主机侧可单测(test/host/run_par,链真 cfg 引擎 + 真 adc_cal)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 运行参数段)
|
||||
* 创建日期:2026-10-25(道D 卡 10)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 地址映射 + 直通
|
||||
* adc_cal + B7 式脏标志落盘请求
|
||||
* v1.1 2026-10-27 DTU 项目组 道F task-7(遥测整定两页)配套:新增语义
|
||||
* setter run_par_cal_k_set / run_par_cal_b_set
|
||||
* (写值 + 置脏成对;104 的 k/b 两支同批改调它,
|
||||
* 消除两份置脏逻辑)+ 合法域唯一出口
|
||||
* run_par_db_range() + 系数粗守上限
|
||||
* RUN_PAR_K_MAX_UV;毫→工程换算收进模块内单点
|
||||
* s_milli_to_eng()(顺带把 task-43 第①项所需的
|
||||
* "写值必置脏"这一半满足掉)
|
||||
* v1.2 2026-10-27 DTU 项目组 道G task-43 第②③项:run_par_store_save() 成功路径
|
||||
* 给标定层盖一次落盘水位(纯观测回执,不参与判定);
|
||||
* 本头把"谁写 RAM / 谁落盘 / 什么时机 / poll 不是保存
|
||||
* 路径"从意图式陈述改成逐条契约
|
||||
*/
|
||||
|
||||
#ifndef RUN_PAR_H
|
||||
@@ -64,7 +40,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_cal.h" /* adc_semantic_t:公开语义 setter 的形参类型(同层 algo→algo,方向不逆)*/
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -73,11 +48,6 @@ extern "C" {
|
||||
#define RUN_PAR_DB_N 9u
|
||||
#define RUN_PAR_CH_N 8u
|
||||
|
||||
/* 系数 k 的**粗守**上限(µV/码):只为挡误写(如把 mV 当 µV 填),不是工况域;
|
||||
* 真合法带在 adc_cal 执行段裁(µV/码 ×[0.5,2.0]),两层各说各的、互不掩盖。
|
||||
* 公开它是因为 UI 整定页要给编辑器设上限——上限在这里,就不许页面再抄一份。*/
|
||||
#define RUN_PAR_K_MAX_UV 1.0e9f
|
||||
|
||||
/*
|
||||
* 函数功能:运行参数初始化——副本置工厂默认 + 注册 [run_par] 段进 cfg 引擎
|
||||
* 入口参数:无
|
||||
@@ -94,9 +64,7 @@ void run_par_init(void);
|
||||
* 入口参数:idx - 0..7 = 语义通道序(UA..IN),8 = 频率 uint8_t
|
||||
* 返 回 值:死区 V / A / Hz;idx 越界返 0.0(调用方按"无死区"保守处理) float
|
||||
* 限定条件:任意上下文(int32 单字读原子;104 COV 每拍调用)
|
||||
* 函数说明:毫单位 → 浮点的换算在本模块内也只有一处(run_par.c 的
|
||||
* s_milli_to_eng(),run_par_deadband 与 run_par_db_range 共用);
|
||||
* 消费侧一律取本模块给的工程量,不要再各自除 1000。
|
||||
* 函数说明:毫单位 → 浮点的换算点(除以 1000 唯一在此,勿在消费侧另写)。
|
||||
*/
|
||||
float run_par_deadband(uint8_t idx);
|
||||
|
||||
@@ -104,11 +72,8 @@ float run_par_deadband(uint8_t idx);
|
||||
* 函数功能:写第 idx 路死区(工程量,含范围守卫 + 脏置位)
|
||||
* 入口参数:idx - 同上 uint8_t;db - 死区工程量(V/A/Hz,≥0) float
|
||||
* 返 回 值:0 成功;-1 越界/越程/NaN(副本不动) int
|
||||
* 限定条件:写者两门——**iec104Task**(104 C_SE 执行段)与 **uiTask**(遥测死区页
|
||||
* 经 ui_data 通道五注入转达,道F task-7 起;ui_data.h 通道五表注:合法域
|
||||
* 与"写值+置脏"必须原子发生在执行侧)。两门都只写 RAM 副本 + 置脏,
|
||||
* 落盘统一收敛在 defaultTask(勿在 adcTask 调用;此句为 2026-10-27 措辞
|
||||
* 收口——原写"UI 若整定需另行走属主路径"是 task-2 期遗留,task-7 已接线)
|
||||
* 限定条件:**iec104Task 上下文**(104 执行段 / 卡 11 后的整定路径;UI 若整定
|
||||
* 需另行走配置引擎属主路径,勿跨任务直写)
|
||||
* 函数说明:合法域按点型分档——电压 0~50 V、电流 0~1 A、频率 0~5 Hz(上限 =
|
||||
* 额定 × 二档裕量,守卫目的是挡误写不是限工况;见 .c 常量段)。
|
||||
* 成功即置脏(落盘请求,见文件头持久化契约)。
|
||||
@@ -140,40 +105,6 @@ int run_par_104_read(uint32_t addr, float *out);
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
/*
|
||||
* 函数功能:写某路系数 k(µV/码)**并在同一函数内置脏**——整定的语义写入口
|
||||
* 入口参数:sem - 电气语义通道(adc_semantic_t 枚举序) adc_semantic_t;
|
||||
* k_uv - 系数(µV/码,须 >0 且 <= RUN_PAR_K_MAX_UV) float
|
||||
* 返回值:0 - RAM 副本已写且已置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:整定路径上下文(iec104Task 的 C_SE 执行段;UI 侧由装配层
|
||||
* App/app/ui_data_feed.c 经 ui_data_yt_ops_t 转进来)。勿在 adcTask 调用
|
||||
* 函数说明:**写值与置脏成对,不许拆开**(lead 裁②):拆开就等于"屏上已改、
|
||||
* 盘上永不落、且没有任何错误返回"。因此本模块**不提供**公开的
|
||||
* mark_dirty——单独调它等于空承诺;零点 b 由本函数 RMW 取当前生效值。
|
||||
*/
|
||||
int run_par_cal_k_set(adc_semantic_t sem, float k_uv);
|
||||
|
||||
/*
|
||||
* 函数功能:写某路零点偏移 b(码值)并置脏(与 k 同款语义,粗守 ±1e6 码)
|
||||
* 入口参数:sem - 电气语义通道 adc_semantic_t;b_codes - 零点(码值) int32_t
|
||||
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段拒(副本未动、不置脏) int
|
||||
* 限定条件:同 run_par_cal_k_set()
|
||||
* 函数说明:k 侧 RMW 取当前生效值;±8192 真带由 adc_cal 执行段裁。消费者 =
|
||||
* 104 的 0x8048~ 分支(改造后走这里),不是为将来预留的口
|
||||
*/
|
||||
int run_par_cal_b_set(adc_semantic_t sem, int32_t b_codes);
|
||||
|
||||
/*
|
||||
* 函数功能:取第 idx 路死区的合法域(工程量)——**合法域的唯一出口**
|
||||
* 入口参数:idx - 0..8 uint8_t;lo/hi - 输出下限/上限(V/A/Hz) float *
|
||||
* 返回值:0 - 已写出;-1 - idx 越界或指针空(调用方不得回退成自造默认域) int
|
||||
* 限定条件:任意上下文(纯常量读)
|
||||
* 函数说明:存在的唯一理由:UI 整定页要给编辑器设 min/max,而这些上限的真源是
|
||||
* 本模块的 RUN_PAR_DB_MAX_*;页面抄一份就是第二真源。给的是**粗守域**,
|
||||
* 真提交仍由与 104 选择段同源的守卫裁,两条路不会漂移到两套数字。
|
||||
*/
|
||||
int run_par_db_range(uint8_t idx, float *lo, float *hi);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏标志探查(非消费)
|
||||
* 入口参数:无
|
||||
|
||||
+10
-24
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(全周傅氏相量计算的输入)
|
||||
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 全周傅氏相量计算按窗取数。
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 L2 相量计算按窗取数。
|
||||
* 适用平台:STM32F407ZGTx(纯算法,无寄存器/无 RTOS 依赖)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:全周傅氏相量输入环缓冲(20 点/周波 × 2 周波)
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#include "sample_ring.h"
|
||||
@@ -48,7 +48,7 @@ void sample_ring_init(void) {
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错
|
||||
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 语义取数与有效性负责)
|
||||
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧写入完毕后**统一**推进写指针——单帧内不存在"半新半旧",
|
||||
* 消费侧取到的窗永远落在整帧边界上(无帧内撕裂)。
|
||||
*/
|
||||
@@ -73,41 +73,28 @@ int sample_ring_push_frame(const float *values, uint8_t n) {
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道(0 .. ADC_SEM_COUNT-1)
|
||||
* n - 取点数(1 .. SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(调用方保证长度 >= n,本层不校验容量)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:仅 adcTask 上下文(写者与读者同任务串行,故不加锁、不用 volatile);
|
||||
* 须在同拍 sample_ring_push_frame() 之后调用。
|
||||
* 函数说明:以最新一点为终点、向前回溯 n 点;out[0] 最老、out[n-1] 最新,
|
||||
* 与 phasor_run() 的求和方向一致。
|
||||
* 为什么需要这一层、环内布局与窗首下标的推导 →
|
||||
* docs/算法/采集与算法层设计说明.md §5(本文件只留落地要点)。
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。最老点下标 = head - n(模 DEPTH)。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
|
||||
uint16_t start;
|
||||
uint16_t i;
|
||||
|
||||
/* 通道号先转 unsigned 再比:枚举底层可为有符号,负值会骗过 ch >= COUNT 的判据。 */
|
||||
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
/* n = 0 是空窗;n > DEPTH 一个环装不下连续 n 点(回绕会把同一点重复读出)。 */
|
||||
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
|
||||
return -1;
|
||||
}
|
||||
/* -2 = 窗还没攒够,调用方跳过本拍即可(首周波启动期必然出现),不是故障。 */
|
||||
if (s_filled < (uint16_t)n) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* 窗首 = s_head - n(s_head 是"下一待写"位置,故窗尾 = s_head - 1)。
|
||||
* 必须先加 DEPTH 再减:直接减在 s_head < n 时下溢或得负数,而
|
||||
* (x mod 65536) mod 40 不等于 x mod 40、C99 的负数取模也不是同余解。
|
||||
* 加 DEPTH 不改同余类,并把中间值锁在 [0, 2·DEPTH - 2]。详见设计说明 §5.3。 */
|
||||
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
|
||||
|
||||
/* 逐点取模拷贝:窗可能跨环尾,环在内存里首尾不相连,不能用一次 memcpy。 */
|
||||
for (i = 0u; i < (uint16_t)n; i++) {
|
||||
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
|
||||
}
|
||||
@@ -130,8 +117,7 @@ uint16_t sample_ring_filled(void) {
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数
|
||||
* 限定条件:无
|
||||
* 函数说明:供心跳日志 / 丢帧观测(采样有效性校验 判据之一)。
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 函数说明:供心跳日志 / 丢帧观测(L1-6 判据之一)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void) {
|
||||
return s_frames;
|
||||
|
||||
+7
-11
@@ -1,16 +1,16 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(全周傅氏相量计算的输入)
|
||||
* 模块功能:按**语义通道**保存最近若干周波的采样值,供 全周傅氏相量计算按窗取数。
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按**语义通道**保存最近若干周波的采样值,供 L2 相量计算按窗取数。
|
||||
* V0.1 上 8 路电气量分属两片转换器(双转换器无共同采样触发),本层只做"按时序
|
||||
* 存/取",**不做任何跨通道运算**——同刻性由上游(语义取数与有效性)保证,本层不假设。
|
||||
* 存/取",**不做任何跨通道运算**——同刻性由上游(L1)保证,本层不假设。
|
||||
* 容量取 **2 个周波(40 点)**,与参考系统 `CN_BUFSAM_CYCLE = 2`
|
||||
* (规划 §10.1)一致,为后续加长数据窗(1.5~2 周波,抗衰减直流,
|
||||
* 见 `傅氏算法详解.md` §9 Q4)预留空间。
|
||||
* 适用平台:STM32F407ZGTx(**纯算法**:不碰寄存器、不碰 RTOS → 可主机侧纯逻辑单测)
|
||||
* 适用平台:STM32F407ZGTx(**纯算法**:不碰寄存器、不碰 RTOS → 可主机侧单测 L0.5)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:全周傅氏相量输入环缓冲(20 点/周波 × 2 周波)
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#ifndef SAMPLE_RING_H
|
||||
@@ -45,7 +45,7 @@ void sample_ring_init(void);
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(浮点工程量,长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT,否则拒收)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错(空指针或长度不符)
|
||||
* 限定条件:调用方保证同一帧内各通道**同刻**(同族量同刻性由上游 语义取数与有效性负责)
|
||||
* 限定条件:调用方保证同一帧内各通道**同刻**(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧一次性写入,写完统一推进写指针——单帧内不可能出现"半新半旧",
|
||||
* 即天然无帧内撕裂。消费侧读到的窗永远是**整帧**边界。
|
||||
*/
|
||||
@@ -59,9 +59,6 @@ int sample_ring_push_frame(const float *values, uint8_t n);
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。相量层以本函数取"以当前时刻为终止的一周波"。
|
||||
* ⚠ 仅 adcTask 上下文调用(写者与读者同任务串行,本层不加锁);
|
||||
* 为什么需要这一层、环内布局与窗首下标的推导 →
|
||||
* `docs/算法/采集与算法层设计说明.md` §5。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out);
|
||||
|
||||
@@ -79,8 +76,7 @@ uint16_t sample_ring_filled(void);
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数(32 位,1 kHz 下约 49.7 天回绕)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 语义取数与有效性的 `tpafe5160_frame_seq()` 对照可判丢帧(采样有效性校验
|
||||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||||
* 函数说明:与 L1 的 `tpafe5160_frame_seq()` 对照可判丢帧(L1-6 采样有效性校验
|
||||
* 的判据之一,本层只提供计数,不做判定)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码)
|
||||
* 模块功能:sb_led_map_eval()——状态快照进、32 位 LED 掩码出。位定义与理由全在
|
||||
* sb_led_map.h(映射表唯一出处),本文件只做折算。
|
||||
* 适用平台:平台无关(纯标准 C)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-29
|
||||
* 修改记录:
|
||||
* 2026-10-29 王建锋 创建初始版本(批 3 的 3-1)
|
||||
*/
|
||||
|
||||
#include <stddef.h> /* NULL:纯逻辑不经 main.h,NULL 要自己带来 */
|
||||
|
||||
#include "sb_led_map.h"
|
||||
|
||||
/* NULL 输入归一用的全零快照(static const:不占 RAM,语义就是"什么都没发生") */
|
||||
static const sb_led_in_t s_in_zero = { 0UL, 0UL, 0UL, 0U, { 0U, 0U, 0U } };
|
||||
|
||||
/*
|
||||
* 函数功能:把状态快照折算成屏板 LED 32 位掩码(契约见 sb_led_map.h)
|
||||
* 入口参数:p_in - 状态快照,允许 NULL(= 全零输入,只跑心跳)
|
||||
* now_ms - 毫秒时基(只用于运行灯相位)
|
||||
* 返回值:32 位 LED 掩码(bit i = 灯 ID i;本函数不触碰 bit0 与 ID 6~19)
|
||||
* 限定条件:任意上下文,纯函数
|
||||
* 函数说明:心跳相位取 (now_ms / SB_LED_BLINK_HALF_MS) 的奇偶——用**除法取奇偶**
|
||||
* 而不是"与上次比差值再翻转",因为后者要把相位存成静态量,纯函数就没了
|
||||
* (单测要能不开机就验完整个闪烁序列)。回绕只截短一个半周期,无害。
|
||||
*/
|
||||
uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms)
|
||||
{
|
||||
const sb_led_in_t *q = (p_in != NULL) ? p_in : &s_in_zero;
|
||||
uint32_t mask = 0UL;
|
||||
|
||||
/* ID 1 运行心跳:偶数个 500 ms 段 = 亮 */
|
||||
if (((now_ms / SB_LED_BLINK_HALF_MS) & 1UL) == 0UL)
|
||||
{
|
||||
mask |= SB_LED_BIT_RUN;
|
||||
}
|
||||
|
||||
if (q->prot_active != 0UL)
|
||||
{
|
||||
mask |= SB_LED_BIT_ALARM;
|
||||
}
|
||||
if (q->iec104_run != 0U)
|
||||
{
|
||||
mask |= SB_LED_BIT_COMM;
|
||||
}
|
||||
if (q->adc_flags != 0UL)
|
||||
{
|
||||
mask |= SB_LED_BIT_ADC_FAULT;
|
||||
}
|
||||
if (q->prot_pick != 0UL)
|
||||
{
|
||||
mask |= SB_LED_BIT_PICKUP;
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码)
|
||||
* 模块功能:把装置的业务状态(保护动作 / 保护启动 / 采样故障 / 104 主站在线 /
|
||||
* 运行心跳)折算成屏板帧 B `info[0..3]` 的 32 位 LED 掩码。
|
||||
* ⚠ 本层**纯逻辑**:不碰 HAL、不读全局、不打日志——输入是调用方
|
||||
* (uiTask)采好的状态快照,输出是一个 uint32_t → 主机侧可纯逻辑单测
|
||||
* (test/host/sb_led_map/)。与 Drivers/BSP/ScreenLink/sb_proto 同一分层理由。
|
||||
* 适用平台:平台无关(纯标准 C)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-29
|
||||
* 修改记录:
|
||||
* 2026-10-29 王建锋 创建初始版本(映射表口径见 docs/子系统设计/屏板链路设计说明.md §6)
|
||||
*
|
||||
* 这张表为什么必须"自定":屏板源码(DTU-LCD LED_Printf)只规定了"位 → 灯号"的
|
||||
* 物理口径(bit1~bit19 → ID 1~19,ID 0 电源灯硬件常亮、不看位,bit20~31 未使用),
|
||||
* **"哪路状态占哪一位"在参考主机源码里不存在**(参考的 LCD_LED_Update 实现在
|
||||
* 未开源的屏板 BSP 库里)。出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。
|
||||
*/
|
||||
#ifndef SB_LED_MAP_H
|
||||
#define SB_LED_MAP_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ============================================================================
|
||||
* 一、位定义(屏板物理口径:bit i = 灯 ID i;bit0 被屏板忽略,别指望用它)
|
||||
* ==========================================================================*/
|
||||
|
||||
/* ID 1 = 运行灯:装置活着的心跳,500 ms 半周期闪烁(与主板 LED1~3 心跳节奏同源,
|
||||
* docs/产品需求/产品需求设计文档.md §5.8)。它**不取决于任何输入状态**——只要这段
|
||||
* 代码还在被调度,灯就在闪;灯不闪 = uiTask 停了,这正是操作员要看的信号。 */
|
||||
#define SB_LED_BIT_RUN (1UL << 1)
|
||||
|
||||
/* ID 2 = 告警灯:任一保护元件处于**动作**态(prot_elm_active_mask != 0)。
|
||||
* ⚠ 产品构建处于「未标定闭锁」(App/task/protect_task.c 的 VALID 门禁),
|
||||
* 标定前恒灭——与屏上"保护动作 N"同一已知口径,不是本灯的缺陷。 */
|
||||
#define SB_LED_BIT_ALARM (1UL << 2)
|
||||
|
||||
/* ID 3 = 通信灯:IEC 60870-5-104 链路处于 RUN(STARTDT 已确认 = 主站真的在线在
|
||||
* 轮询)。选 RUN 而不选"TCP 已建连":连上不发 STARTDT 的扫描器不该点亮它。 */
|
||||
#define SB_LED_BIT_COMM (1UL << 3)
|
||||
|
||||
/* ID 4 = 采样故障灯:ADC 采样有效性**电平**故障位非零(adc_health_flags,
|
||||
* 静默/倒退等,当前活跃、可自愈熄灭)。选电平不选 sticky:这盏灯要能熄灭,
|
||||
* "曾经故障过"的账在自检记录里,不该用一盏长明灯喊给操作员。 */
|
||||
#define SB_LED_BIT_ADC_FAULT (1UL << 4)
|
||||
|
||||
/* ID 5 = 保护启动灯:任一保护元件处于**启动**(picked)态(prot_elm_pick_mask)。
|
||||
* picked 早于 asserted,"越限启动了但没到动作点"这段过程只在这盏灯上可见
|
||||
* (与 104「保护启动」记录同一动机,App/algo/prot_elm.h 的 pick_mask 注释)。
|
||||
* 同样受未标定闭锁口径约束。 */
|
||||
#define SB_LED_BIT_PICKUP (1UL << 5)
|
||||
|
||||
/* 已赋义位的并集(判据用:掩码里不得出现本表之外的置位)。
|
||||
* ID 6~19 保留未用,恒 0;将来扩展从 ID 6 顺序取,跳号留白不回收语义。 */
|
||||
#define SB_LED_BITS_DEFINED (SB_LED_BIT_RUN | SB_LED_BIT_ALARM | SB_LED_BIT_COMM | \
|
||||
SB_LED_BIT_ADC_FAULT | SB_LED_BIT_PICKUP)
|
||||
|
||||
/* 运行灯闪烁半周期(ms):0 ~ SB_LED_BLINK_HALF_MS-1 亮,下一个 500 ms 灭,交替。
|
||||
* 取 500 是为与主板 LED 心跳(defaultTask 500 ms 翻转)同拍,两套灯闪烁同相。 */
|
||||
#define SB_LED_BLINK_HALF_MS (500U)
|
||||
|
||||
/* ============================================================================
|
||||
* 二、状态输入快照(调用方每拍现采,本模块不缓存、不加锁)
|
||||
* ==========================================================================*/
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t prot_active; /* prot_elm_active_mask():bit i = 元件 i 动作 */
|
||||
uint32_t prot_pick; /* prot_elm_pick_mask():bit i = 元件 i 启动 */
|
||||
uint32_t adc_flags; /* adc_health_flags():ADC_HF_* 电平故障位 */
|
||||
uint8_t iec104_run; /* 1 = IEC 104 链路 RUN(iec104_serv_link_run()) */
|
||||
uint8_t pad[3]; /* 显式补齐:结构体大小固定 16 B,主机/目标端一致 */
|
||||
} sb_led_in_t;
|
||||
|
||||
/* ============================================================================
|
||||
* 三、求值入口
|
||||
* ==========================================================================*/
|
||||
|
||||
/*
|
||||
* 函数功能:把状态快照折算成屏板 LED 32 位掩码(纯函数,映射表唯一出处见本头 §一)
|
||||
* 入口参数:p_in - 状态快照;**允许 NULL**:等价于全零输入(只跑运行灯心跳——
|
||||
* 心跳证明的是"这段代码活着",与别的状态无关,不该被空输入连坐)
|
||||
* now_ms - 当前毫秒时基(运行灯相位唯一依赖;osKernelGetTickCount 同域)
|
||||
* 返回值:32 位 LED 掩码(bit i = 灯 ID i;bit0 与 ID 6~19 恒 0,直接进帧 B info[0..3])
|
||||
* 限定条件:任意上下文(纯计算:无分支副作用、不读写任何静态量、时基回绕安全——
|
||||
* 回绕瞬间至多把一个半周期截短或延长 1 拍,500 ms 的心跳灯看不出来)
|
||||
* 函数说明:闪烁相位 = (now_ms / 500) 的奇偶,偶 = 亮。上电 first 500 ms 为亮,
|
||||
* "刚上电灯就在闪"是最直观的自检信号。各状态位相互独立,任一非零即置位,
|
||||
* 无优先级、无互斥(告警与通信可以同时亮,那是两件同时在真发生的事)。
|
||||
*/
|
||||
uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SB_LED_MAP_H */
|
||||
@@ -1,535 +0,0 @@
|
||||
/*
|
||||
* 模块名称:录波环(故障录波 · 数据面纯逻辑层:滚动环 + 触发冻结状态机 + 已封存区读口)
|
||||
* 模块功能:见 App/algo/wave_ring.h 的文件头注释块——所有权矩阵、封存握手、跨任务安全、
|
||||
* 形态钉死与预算全部写在那里,本文件不重抄一遍,只在实现处补「为什么这么写」。
|
||||
* 本文件是纯逻辑:只 include App/algo/wave_ring.h(它带 wave_store.h),
|
||||
* 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不建任何 RTOS 对象、不打日志。
|
||||
* 临界区也不需要:写者只有 adcTask 一个,读者只碰 SEALED 期间冻结的已封存区,
|
||||
* §5.2 第一条陷阱(无临界区的活缓冲窗口)在本层根本不成立,因为没有那种入口。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_ring/
|
||||
* 与 test/host/wave_trig/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-29 DTU 项目组 创建
|
||||
* v1.1 2026-10-29 DTU 项目组 触发挂号改单字节载荷编码(lead 评审:三段写在
|
||||
* adcTask 抢占下会把段头的触发源 / 元件号录成上一段的旧值,溯源字段作废)
|
||||
*/
|
||||
|
||||
#include "wave_ring.h"
|
||||
|
||||
/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 ——
|
||||
* 这个差别主机单测照不出来(2026-10-29 台基线实跑 /keil-build 才暴露:本层
|
||||
* wave_ring.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c
|
||||
* 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */
|
||||
#include <stddef.h>
|
||||
|
||||
/* 触发挂号的载荷编码(v1.1):挂号量必须**一次单字节存写**才原子,所以把载荷本身编进
|
||||
* 那一个字节 —— 0 = 无请求,非 0 = (触发源 << 位移) | 元件号。触发源取值 1 到 3 占高 3 位、
|
||||
* 元件号取 prot_elm 的 picked 位号(uint32 位图 → 0 到 31)占低 5 位,合起来 8 位够用。
|
||||
* 本层双端编译(板端 ARMCC 与模拟器),不能引 FreeRTOS / CMSIS 的临界区内在函数,
|
||||
* 所以「把三个字节压成一个字节」是唯一既原子又不依赖平台的写法。 */
|
||||
#define WAVE_RG_REQ_SHIFT (5u) /* 载荷里触发源的低位置 */
|
||||
#define WAVE_RG_REQ_ELM_MSK (0x1Fu) /* 载荷里元件号的位掩码(低 5 位) */
|
||||
#define WAVE_RG_REQ_ELM_MAX (32u) /* 元件号合法上界(不含),picked 位图只有 32 位 */
|
||||
|
||||
/* ===== 一、静态对象(全部文件作用域 static,禁堆分配;落位与字节数见头注第七节) ===== */
|
||||
|
||||
/* 数据区:按通道分列,8 列 × 320 点 × 2 字节 = 5120 字节(§7 预算的大头)。
|
||||
* 不许改成 float、不许按帧交错(§12:形态错一次 RAM 翻倍 + 跨步取数)。
|
||||
* 不放在 CCM:DMA 看不见它,且 .sct 里 RW_CCM 只收 heap_4.o,普通 .bss 天然落 SRAM1 */
|
||||
static int16_t s_ring[WAVE_RING_CH][WAVE_RING_PTS];
|
||||
|
||||
/* 已封存段的段头:latch 之前只有环侧字段有效,latch 之后才是完整段头。
|
||||
* 这是 defaultTask 与 uiTask 能看到的唯一「段头视图」——内部指针一律不外露 */
|
||||
static wave_seg_meta_t s_sealed;
|
||||
|
||||
/* 观测计数(§9 周期行的 seg= / ovf= / drop= / err= 全从这里取,落盘道不数第二份账) */
|
||||
static uint32_t s_seg_total; /* 成功 ACK 的段数 */
|
||||
static uint32_t s_ovf_cnt; /* 被忽略的触发数 */
|
||||
static uint32_t s_drop_cnt; /* SEALED 窗口丢弃的点数(单位 = 点,一次丢一拍 = 加 8) */
|
||||
static uint32_t s_err_cnt; /* 落盘失败 + 滞留超时自弃 */
|
||||
static uint32_t s_last_t0_ms; /* 最近一段段首毫秒 */
|
||||
static uint32_t s_last_seq; /* 最近一段段首帧序号 */
|
||||
|
||||
/* 状态机与索引 */
|
||||
static uint16_t s_head; /* 下一个写入点(滚动位置) */
|
||||
static uint16_t s_base; /* 已封存 / 正在录的段首在环内的下标(段内点 0) */
|
||||
static uint16_t s_post_left; /* 后触发还差多少点封段 */
|
||||
static uint16_t s_cool_left; /* 冷却剩余拍数 */
|
||||
static uint16_t s_seal_hold; /* SEALED 已滞留拍数 */
|
||||
static uint8_t s_state; /* wave_ring_state_t */
|
||||
/* 触发请求挂号:0 = 无请求;非 0 = (触发源 << WAVE_RG_REQ_SHIFT) | 元件号。
|
||||
* 刻意只用这一个字节承载「是谁要录波」:旧实现分三个字节写(先写挂号位、后写载荷),
|
||||
* adcTask 抢在两条指令之间就会把上一段的旧触发源 / 旧元件号抄进本段 —— 而这两个字段是
|
||||
* 这段记录唯一的溯源信息(现场靠它与 /log/start.log 对表),错一次事后无从发现。
|
||||
* 单字节存写在 Cortex-M4 上原子,写序不再有意义,所以不需要任何顺序假设或屏障。 */
|
||||
static uint8_t s_req;
|
||||
static uint8_t s_trig_src; /* 本段实际用的触发源(钉基址时从请求抄过来) */
|
||||
static uint8_t s_trig_elm; /* 本段实际用的元件序号 */
|
||||
static uint8_t s_meta_ready; /* 1 = 段头已 latch */
|
||||
static uint8_t s_seg_idx; /* 本次上电的轮转下标(跨上电接号由落盘道自持,见头注) */
|
||||
|
||||
/* 语义号 → 列号(本波恒等,但把这条恒等式收在一处:二期带 TEMP 时只改这张表,
|
||||
* 写死下标的调用点会静默错位,走本函数的不会) */
|
||||
static const int8_t s_col_of_sem[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, -1
|
||||
};
|
||||
|
||||
/* ===== 二、内部小工具 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:环内下标 +n 后取模
|
||||
* 入口参数:a_idx 当前下标;a_step 步长(小于环长)
|
||||
* 返 回 值:取模后的下标 uint16_t
|
||||
* 限定条件:仅本文件;a_idx 与 a_step 均在环长量级,无溢出风险
|
||||
* 函数说明:环长 320 是 2 的幂吗?不是(320 = 64×5),所以老老实实取模,
|
||||
* 不用位与——写错一次就是「看起来能跑、偶尔串段」的那种 bug。
|
||||
*/
|
||||
static uint16_t wave_rg_wrap(uint16_t a_idx, uint16_t a_step) {
|
||||
uint16_t v = (uint16_t)(a_idx + a_step);
|
||||
|
||||
if (v >= (uint16_t)WAVE_RING_PTS) {
|
||||
v = (uint16_t)(v - (uint16_t)WAVE_RING_PTS);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把已封存段的「环侧字段」初始化(封存瞬间调,标定字段一律清零待 latch)
|
||||
* 入口参数:a_now_ms 段首毫秒;a_frame_seq 段首帧序号
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:仅本文件;只在钉基址那一拍调一次
|
||||
* 函数说明:先把 ch[] 全部清成「sem = 下标、其余 0」,是为了让**没有 latch** 的段
|
||||
* 不可能带着上一段的系数被写进文件:sealed() 在未 latch 时本就是 0,
|
||||
* 但清零让「万一有人绕过握手直接读 s_sealed」也读不出旧真值。
|
||||
*/
|
||||
static void wave_rg_seal_reset(uint32_t a_now_ms, uint32_t a_frame_seq) {
|
||||
uint8_t i;
|
||||
|
||||
s_sealed.fs_hz = (uint16_t)WAVE_ST_FS_DEF;
|
||||
s_sealed.pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
|
||||
s_sealed.ch_count = (uint8_t)WAVE_RING_CH;
|
||||
s_sealed.pts_per_ch = (uint16_t)WAVE_RING_PTS;
|
||||
s_sealed.pre_pts = (uint16_t)WAVE_RING_PRE;
|
||||
s_sealed.trig_src = s_trig_src;
|
||||
s_sealed.trig_elm = s_trig_elm;
|
||||
s_sealed.t0_ms = a_now_ms;
|
||||
s_sealed.wall = 0u;
|
||||
s_sealed.eng = (uint8_t)WAVE_ENG_PIN;
|
||||
s_sealed.flags_seq = 0u;
|
||||
s_sealed.clip_mask = 0u;
|
||||
s_sealed.frame_seq = a_frame_seq;
|
||||
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
|
||||
s_sealed.ch[i].sem = i;
|
||||
s_sealed.ch[i].conv = 0u;
|
||||
s_sealed.ch[i].bits = 0u;
|
||||
s_sealed.ch[i].avail = 0u;
|
||||
s_sealed.ch[i].k_q16 = 0;
|
||||
s_sealed.ch[i].b_codes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 三、初始化与压环 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:复位环与全部状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:只能在 adcTask 调,且在开始压环之前(见头文件同函数注释)
|
||||
* 函数说明:数据区显式清零而不是依赖 .bss 清零:本函数也可能在运行中被重新调
|
||||
* (显式重配 / 单测复用),留旧数据就等于留下「上一段的尾巴混进下一段」的口子。
|
||||
*/
|
||||
void wave_ring_init(void) {
|
||||
uint8_t c;
|
||||
uint16_t i;
|
||||
|
||||
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
|
||||
for (i = 0u; i < (uint16_t)WAVE_RING_PTS; i++) {
|
||||
s_ring[c][i] = 0;
|
||||
}
|
||||
}
|
||||
s_head = 0u;
|
||||
s_base = 0u;
|
||||
s_post_left = 0u;
|
||||
s_cool_left = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
s_req = 0u;
|
||||
s_trig_src = (uint8_t)WAVE_TRIG_NONE;
|
||||
s_trig_elm = 0u;
|
||||
s_meta_ready = 0u;
|
||||
s_seg_idx = 0u;
|
||||
s_seg_total = 0u;
|
||||
s_ovf_cnt = 0u;
|
||||
s_drop_cnt = 0u;
|
||||
s_err_cnt = 0u;
|
||||
s_last_t0_ms = 0u;
|
||||
s_last_seq = 0u;
|
||||
wave_rg_seal_reset(0u, 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍的 8 路原始码值
|
||||
* 入口参数:p_codes 语义序数组(前 8 项);a_n 必须等于 WAVE_RING_CH
|
||||
* 返 回 值:WAVE_ST_OK 已写入;WAVE_ST_DONE 本拍被丢弃(SEALED 窗口);WAVE_ST_E_PARAM 参数错 int32_t
|
||||
* 限定条件:只能在 adcTask 调,每拍一次
|
||||
* 函数说明:写的是「列优先」的 8 个分散地址(每列同一行)——这就是 §6.2 分列形态的代价,
|
||||
* 换来的是落盘与回放都能按列连续搬,不用跨步长取数。
|
||||
*/
|
||||
int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n) {
|
||||
uint8_t c;
|
||||
|
||||
if ((p_codes == NULL) || (a_n != (uint8_t)WAVE_RING_CH)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (s_state == (uint8_t)WAVE_RING_ST_SEALED) {
|
||||
/* 冻结窗口丢样本(§4.2):计数单位是点,一次丢一整拍 = WAVE_RING_CH 点 */
|
||||
s_drop_cnt += (uint32_t)a_n;
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
|
||||
s_ring[c][s_head] = p_codes[c];
|
||||
}
|
||||
s_head = wave_rg_wrap(s_head, 1u);
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 四、触发、推进、交棒、ACK ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:投递一次录波请求
|
||||
* 入口参数:a_src 触发源(1 手动 / 2 保护 / 3 bench);a_elm 元件序号(0 到 31)
|
||||
* 返 回 值:WAVE_ST_OK 受理;WAVE_ST_DONE 被忽略(ovf 已加一);WAVE_ST_E_PARAM 触发源
|
||||
* 非法或元件号越界 int32_t
|
||||
* 限定条件:任意上下文(**只写一个字节**,理由见 s_req 声明处)
|
||||
* 函数说明:这里判「非 IDLE 就拒」而不是「非 IDLE 就排队」——本波不做多段排队(已定裁定),
|
||||
* 而且排队会把「最近一次故障」降级成「上上次故障」。
|
||||
* s_req 已挂号时也拒:同一拍内多个元件同时 picked 只该录一段。
|
||||
* 两条入参校验排在状态判断之前:非法调用是调用方的错,不该记进 ovf 账 ——
|
||||
* ovf 的语义是「真来了第二次触发」。
|
||||
* 元件号上界写死在这里:今天没有调用方会越界(picked 是 uint32 位图),这条是给
|
||||
* 二期加元件的人留的牙 —— 载荷只有 5 位,越界必须当场拒,不许静默截断。
|
||||
*/
|
||||
int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm) {
|
||||
if ((a_src < (uint8_t)WAVE_TRIG_MANUAL) || (a_src > (uint8_t)WAVE_TRIG_BENCH)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_elm >= (uint8_t)WAVE_RG_REQ_ELM_MAX) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((s_state != (uint8_t)WAVE_RING_ST_IDLE) || (s_req != 0u)) {
|
||||
s_ovf_cnt++;
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
/* 一次存写:触发源与元件号同字节落地。别的上下文要么看到「没请求」、要么看到
|
||||
* 「完整的这次请求」,不存在读到半份载荷的中间态 */
|
||||
s_req = (uint8_t)(((uint32_t)a_src << (uint32_t)WAVE_RG_REQ_SHIFT) | (uint32_t)a_elm);
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:推进一拍状态机
|
||||
* 入口参数:a_now_ms 当前相对上电毫秒;a_frame_seq 当前帧序号
|
||||
* 返 回 值:wave_ring_evt_t(本拍刚封段返回 WAVE_RING_EVT_SEALED) int32_t
|
||||
* 限定条件:只能在 adcTask 调,每拍在 wave_ring_push 之后一次
|
||||
* 函数说明:五个分支各管一件事,互不重叠:
|
||||
* IDLE —— 消费挂号、钉段首基址、转 FREEZE
|
||||
* FREEZE —— 后触发计点,计满转 SEALED
|
||||
* SEALED —— 只计滞留,超时自弃(绝不让环永久焊死)
|
||||
* COOL —— 递减冷却,到 0 回 IDLE
|
||||
* 段首基址 = 触发那一拍写入点的「往前 160 点」,于是段内点 0 是最老的预触发点、
|
||||
* 点 160 正好是触发后第一点;后触发的 160 点落在环的另一半(本来就即将被覆盖的
|
||||
* 最老那 160 槽),所以「冻结」在物理上是**停止滚动覆盖**,不是停止写入。
|
||||
*/
|
||||
int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq) {
|
||||
int32_t evt = (int32_t)WAVE_RING_EVT_NONE;
|
||||
|
||||
switch (s_state) {
|
||||
case (uint8_t)WAVE_RING_ST_IDLE:
|
||||
s_cool_left = 0u;
|
||||
s_seal_hold = 0u;
|
||||
{
|
||||
/* 只读一次快照再解码:分两次读就会把两次请求的字段拼成一段的身份 */
|
||||
uint8_t req = s_req;
|
||||
|
||||
if (req != 0u) {
|
||||
/* 钉基址:本拍的样本已写进 (head-1),段首 = head - 预触发点数 */
|
||||
s_trig_src = (uint8_t)((uint32_t)req >> (uint32_t)WAVE_RG_REQ_SHIFT);
|
||||
s_trig_elm = (uint8_t)((uint32_t)req & (uint32_t)WAVE_RG_REQ_ELM_MSK);
|
||||
s_req = 0u;
|
||||
s_base = wave_rg_wrap(s_head, (uint16_t)((uint32_t)WAVE_RING_PTS - (uint32_t)WAVE_RING_PRE));
|
||||
s_post_left = (uint16_t)WAVE_RING_POST;
|
||||
s_meta_ready = 0u;
|
||||
s_last_t0_ms = a_now_ms;
|
||||
s_last_seq = a_frame_seq;
|
||||
wave_rg_seal_reset(a_now_ms, a_frame_seq);
|
||||
s_state = (uint8_t)WAVE_RING_ST_FREEZE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_FREEZE:
|
||||
if (s_post_left > 0u) {
|
||||
s_post_left--;
|
||||
}
|
||||
if (s_post_left == 0u) {
|
||||
/* 封段:此刻起 s_head 正好回到 s_base,再写就覆盖段内点 0 —— 所以 push 开始丢 */
|
||||
s_state = (uint8_t)WAVE_RING_ST_SEALED;
|
||||
s_seal_hold = 0u;
|
||||
evt = (int32_t)WAVE_RING_EVT_SEALED;
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_SEALED:
|
||||
/* 兜底:defaultTask 挂住 / 卷没挂载 / 落盘道被更高优先级的事挡住,都不能让环
|
||||
* 永久冻结——下一次真故障无波可录是比丢一段更贵的代价(lead 裁定) */
|
||||
s_seal_hold++;
|
||||
if (s_seal_hold >= (uint16_t)WAVE_SEAL_TIMEOUT_MS) {
|
||||
s_err_cnt++;
|
||||
s_meta_ready = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
|
||||
s_state = (uint8_t)WAVE_RING_ST_COOL;
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_COOL:
|
||||
/* 冷却只在 ACK(或自弃)之后计时:它是二次抑制窗,不是冻结窗 */
|
||||
if (s_cool_left > 0u) {
|
||||
s_cool_left--;
|
||||
}
|
||||
if (s_cool_left == 0u) {
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
/* 状态字节被写坏(跑飞 / 越界写)时回到 IDLE 而不是卡死:录波宁可多录也不能停 */
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
return evt;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:封存交棒——把封存瞬间事实锁进段头
|
||||
* 入口参数:p_cal 封存瞬间事实(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK 已锁存;WAVE_ST_E_STATE 不在待交棒态;WAVE_ST_E_PARAM 参数错 int32_t
|
||||
* 限定条件:只能在 adcTask 调,只在 EVT_SEALED 那一拍调一次
|
||||
* 函数说明:只 copy 封存瞬间的事实,不动环侧字段(点数 / 采样率 / 预触发这些
|
||||
* 在封段那刻已按 §6.2 定死)。重复调用直接拒,不覆盖已有锁存:
|
||||
* 半路换系数会让落盘道写进去的头与它已经在搬的数据不同源。
|
||||
*/
|
||||
int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal) {
|
||||
uint8_t i;
|
||||
|
||||
if (p_cal == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (p_cal->eng > (uint8_t)WAVE_ENG_PRI) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((s_state != (uint8_t)WAVE_RING_ST_SEALED) || (s_meta_ready != 0u)) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
s_sealed.eng = p_cal->eng;
|
||||
s_sealed.flags_seq = p_cal->flags_seq;
|
||||
s_sealed.clip_mask = p_cal->clip_mask;
|
||||
s_sealed.wall = p_cal->wall;
|
||||
for (i = 0u; i < s_sealed.ch_count; i++) {
|
||||
s_sealed.ch[i].conv = p_cal->ch[i].conv;
|
||||
s_sealed.ch[i].bits = p_cal->ch[i].bits;
|
||||
s_sealed.ch[i].avail = p_cal->ch[i].avail;
|
||||
s_sealed.ch[i].k_q16 = p_cal->ch[i].k_q16;
|
||||
s_sealed.ch[i].b_codes = p_cal->ch[i].b_codes;
|
||||
}
|
||||
s_meta_ready = 1u;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘 ACK
|
||||
* 入口参数:a_result 0 = 成功;非 0 = 失败(短写 / 打开失败 / CRC 自弃)
|
||||
* 返 回 值:WAVE_ST_OK 已解冻并起冷却;WAVE_ST_E_STATE 当前不在 SEALED int32_t
|
||||
* 限定条件:只能由 defaultTask 调,一段一次
|
||||
* 函数说明:无论成败都解冻 + 起冷却:错误只计数不冻结(环冻着的代价是下一次故障无波可录)。
|
||||
* 成功才推进轮转号与段计数(失败段留同号,下次重写覆盖,不留半截文件)。
|
||||
*/
|
||||
int32_t wave_ring_write_done(int32_t a_result) {
|
||||
if (s_state != (uint8_t)WAVE_RING_ST_SEALED) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (s_meta_ready == 0u) {
|
||||
/* 没 latch 就没有「段」可 ACK:这时候段头从未就绪,落盘道根本拿不到
|
||||
* wave_ring_sealed_meta(),绝不可能真写出文件。若在这里放过 ACK,
|
||||
* 就会白推进轮转号(号 +1 而盘上没那个文件 = 凭空吃掉一个号)并把
|
||||
* seg_total 报成一个没发生过的数(串口周期行的 seg=NN 从此对不上账)。
|
||||
* 拒绝而不是记账,把「没交棒就 ACK」变成调用方看得见的错误。
|
||||
* 本段的出口只有一个:滞留到 WAVE_SEAL_TIMEOUT_MS 自弃(那一路照实计 err)。 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (a_result == 0) {
|
||||
s_seg_idx = wave_store_next_idx(s_seg_idx);
|
||||
s_seg_total++;
|
||||
} else {
|
||||
s_err_cnt++;
|
||||
}
|
||||
s_meta_ready = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
|
||||
s_state = (uint8_t)WAVE_RING_ST_COOL;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 五、状态查询与已封存区读口 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:查当前状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:wave_ring_state_t 取值 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_state(void) {
|
||||
return s_state;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查「有已封存段可读」——跨任务唯一握手量
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = SEALED 且段头已 latch;0 = 不可用 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_sealed(void) {
|
||||
if ((s_state == (uint8_t)WAVE_RING_ST_SEALED) && (s_meta_ready != 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取已封存段的段头快照
|
||||
* 入口参数:p_out 输出(整份拷出)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文(defaultTask / uiTask 只读)
|
||||
*/
|
||||
int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out) {
|
||||
uint16_t i;
|
||||
const uint8_t *p_src;
|
||||
uint8_t *p_dst;
|
||||
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (wave_ring_sealed() == 0u) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
/* 逐字节拷出而不是 *p_out = s_sealed:结构体赋值在 ARMCC 上会调 __aeabi_memcpy,
|
||||
* 这里没必要为一个赋值多拉一层依赖,量也就 136 字节 */
|
||||
p_src = (const uint8_t *)&s_sealed;
|
||||
p_dst = (uint8_t *)p_out;
|
||||
for (i = 0u; i < (uint16_t)sizeof(wave_seg_meta_t); i++) {
|
||||
p_dst[i] = p_src[i];
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通道读已封存区的连续点窗
|
||||
* 入口参数:a_ch 列号;a_from 段内起点;a_cnt 期望点数(超出段尾自动夹);p_dst 输出
|
||||
* 返 回 值:实际点数;WAVE_ST_E_PARAM / WAVE_ST_E_RANGE / WAVE_ST_E_STATE int32_t
|
||||
* 限定条件:任意上下文,只读已封存区(本层没有任何读活缓冲的入口)
|
||||
* 函数说明:按 s_base 取模展开,读者看到的是 320 点的线性数组。
|
||||
*/
|
||||
int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst) {
|
||||
uint16_t left;
|
||||
uint16_t n;
|
||||
uint16_t idx;
|
||||
uint16_t i;
|
||||
|
||||
if ((p_dst == NULL) || (a_cnt == 0u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((a_ch >= (uint8_t)WAVE_RING_CH) || (a_from >= (uint16_t)WAVE_RING_PTS)) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
if (wave_ring_sealed() == 0u) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
left = (uint16_t)((uint16_t)WAVE_RING_PTS - a_from);
|
||||
n = (a_cnt < left) ? a_cnt : left;
|
||||
idx = wave_rg_wrap(s_base, a_from);
|
||||
for (i = 0u; i < n; i++) {
|
||||
p_dst[i] = s_ring[a_ch][idx];
|
||||
idx = wave_rg_wrap(idx, 1u);
|
||||
}
|
||||
return (int32_t)n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读口的 wave_reader_fn 形态适配(落盘道直接把它交给打包游标)
|
||||
* 入口参数:p_user 未用;其余同 wave_ring_read_ch(点数与起点是 uint32 形态)
|
||||
* 返 回 值:实际点数;负 = WAVE_ST_E_* int32_t
|
||||
* 限定条件:任意上下文,只读已封存区
|
||||
* 函数说明:32 位入参先夹到 16 位形态再转调,越界一律报错不夹成 0 ——
|
||||
* 夹成 0 会让落盘道以为「这一列读完了」而写出一个短段。
|
||||
*/
|
||||
int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst) {
|
||||
(void)p_user;
|
||||
|
||||
if (a_cnt > (uint32_t)WAVE_RING_PTS) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_from > (uint32_t)WAVE_RING_PTS) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
return wave_ring_read_ch(a_ch, (uint16_t)a_from, (uint16_t)a_cnt, p_dst);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取观测计数快照
|
||||
* 入口参数:p_out 输出(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
int32_t wave_ring_get_stat(wave_ring_stat_t *p_out) {
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
p_out->state = s_state;
|
||||
p_out->trig_src = s_trig_src;
|
||||
p_out->trig_elm = s_trig_elm;
|
||||
p_out->meta_ready = s_meta_ready;
|
||||
p_out->seg_idx = s_seg_idx;
|
||||
/* 对外字段是 8 位:本波后触发最多 160 点,不会溢出;真溢出说明环长被改过,
|
||||
* 饱和在 255 让它在日志里显眼,而不是绕回 0 假装快封段了 */
|
||||
p_out->post_left = (s_post_left > 255u) ? 255u : (uint8_t)s_post_left;
|
||||
p_out->rsv1 = 0u;
|
||||
p_out->rsv2 = 0u;
|
||||
p_out->cool_left = s_cool_left;
|
||||
p_out->seal_hold = s_seal_hold;
|
||||
p_out->seg_total = s_seg_total;
|
||||
p_out->ovf_cnt = s_ovf_cnt;
|
||||
p_out->drop_cnt = s_drop_cnt;
|
||||
p_out->err_cnt = s_err_cnt;
|
||||
p_out->last_t0_ms = s_last_t0_ms;
|
||||
p_out->last_seq = s_last_seq;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:语义通道号 → 本环列号
|
||||
* 入口参数:a_sem 语义号
|
||||
* 返 回 值:列号;WAVE_ST_E_RANGE = 该语义不录 int32_t
|
||||
* 限定条件:任意上下文(纯计算)
|
||||
*/
|
||||
int32_t wave_ring_col(uint8_t a_sem) {
|
||||
if (a_sem >= (uint8_t)WAVE_ST_CH_MAX) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
if (s_col_of_sem[a_sem] < 0) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
return (int32_t)s_col_of_sem[a_sem];
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
/*
|
||||
* 模块名称:录波环形缓冲层(故障录波 · 8 路 × 320 点原始码值环 + 触发冻结状态机)
|
||||
* 模块功能:按拍把 8 路电气量的原始码值压进环形缓冲,在「预触发 + 后触发」窗口计满后
|
||||
* 把这一段冻结成只读的已封存区,等落盘道取走并明确 ACK 之后才解冻。口径属主 =
|
||||
* docs/子系统设计/故障录波设计说明.md §4.2(段长与冻结语义)、§5.1(任务所有权)、
|
||||
* §5.2(三条陷阱)、§6.2 与 §6.3(段格式;本层的已封存区就是数据区的字节形态)、
|
||||
* §7(预算与落位)、§12(风险表)。本文件是它的机器可执行形态;冲突以设计文档
|
||||
* 为准并回报 lead,不在代码里单方面「修正」。
|
||||
* 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不建任务、不打日志、不碰文件系统,
|
||||
* 也不链 App/algo/adc_cal —— 折算档位、有效性位图、削顶位图、epoch 秒与每路
|
||||
* k_q16 / b_codes 由 adcTask 在「刚封存那一拍」作为参数交棒(见 wave_ring_latch_meta
|
||||
* 与下面的「封存握手顺序」),本层只负责把它们随段头一起锁存。因此本层可被
|
||||
* 主机侧单测 test/host/wave_ring/ 与 test/host/wave_trig/ 原样纳入断言、零桩。
|
||||
* 与 sample_ring 的关系(为什么另起一个环):App/algo/sample_ring.c 是相量与保护的
|
||||
* 上游、float 形态、深度 256,且它的 sample_ring_window() 没有临界区
|
||||
* (§5.2 第一条陷阱)。录波要的是 int16 原始码值、320 点、跨任务只读,复用那个环
|
||||
* 要么改它的形态与深度(动相量链),要么在跨任务读上继续裸奔 —— 两条都比另起
|
||||
* 一个环贵,故本波解耦(§2.1 第 3 条结构约束)。
|
||||
*
|
||||
* 形态钉死(§7 预算 + §12 风险表,动这里之前先读):
|
||||
* - 元素类型 int16,不许 float:float 让环从 5120 字节变 10240 字节,直接顶穿 §7 的
|
||||
* 约 5.9 KB 静态增量目标;
|
||||
* - 按通道分列(一路连续 320 点),不按帧交错:回放页只画一路、PC 重算全周傅氏也逐路
|
||||
* 独立,交错布局两种用法都要跨步长取数(§6.2 末段「为什么按通道分列」);
|
||||
* - 通道数固定 8,TEMP 不录:温度是片内 ADC 的 12 位单极性原始值、非线性且未参与线性
|
||||
* 标定(docs/任务规划/数据可信.md §7 已定案),进录波只多占 640 字节,并让回放页多一
|
||||
* 格会被误读成电气量的通道;带 TEMP 属二期,且必须连 flags_seq 与 clip_mask 一起升
|
||||
* 版本(§6.2 那两行的括注);
|
||||
* - 落位 SRAM1:本层全部是普通文件级静态,链接脚本 MDK-ARM/STM32F407-Demo/
|
||||
* STM32F407-Demo.sct 把 RW_IRAM1 的 .ANY (+RW +ZI) 收在 0x20000000,CCM 只收
|
||||
* heap_4.o 与 .noinit → 不加任何段属性就落在 SRAM1。CCM 只剩 7904 字节没有余量,
|
||||
* 而本环不是 DMA 可见对象,也没有非进 CCM 不可的理由。
|
||||
*
|
||||
* 封存握手顺序(唯一合法次序,写死在这里,接线道与后续改动都必须照此):
|
||||
* 1) adcTask 每拍 wave_ring_push() 然后 wave_ring_tick();
|
||||
* 2) tick 计满后触发点返回 WAVE_RING_EVT_SEALED —— 此刻状态进 WAVE_RING_ST_SEALED 但
|
||||
* **尚未就绪**,wave_ring_sealed() 仍返 0;
|
||||
* 3) adcTask 在**同一拍**现取封存瞬间事实(adc_cal_k_q16 / adc_cal_flags / sys_clock /
|
||||
* 削顶位图)并调 wave_ring_latch_meta() 一次 —— 只在第 2 步那一拍取一次,
|
||||
* 既不每拍读 adc_cal(1 kHz 下 8 路白读),也不低频刷新(低频刷新等于快照可能
|
||||
* 来自标定之前的旧值,正是 §12「标定落地后老文件被解释错」的入口);
|
||||
* 4) 此后 wave_ring_sealed() 才返 1,defaultTask 才准开文件落盘;meta 未就绪时
|
||||
* defaultTask 必须把本段当「不可用」处理并计 err,**不许拿半份快照落盘**;
|
||||
* 5) defaultTask 搬完(成功或失败都算搬完)调 wave_ring_write_done(result) 明确 ACK;
|
||||
* ACK 之前冻结一直有效、新触发一律只加 ovf;ACK 之后才起冷却抑制窗。
|
||||
* 为什么必须用状态而不是用时间来挡(lead 裁定 2026-10-29):若解冻靠计时,会出现
|
||||
* 「冷却走完、adcTask 重新推进写指针,而 defaultTask 还在读那份已封存区」→
|
||||
* 半新半旧的段落进文件且没有任何错误码 —— §5.2 第一条陷阱的翻版。
|
||||
* 兜底:WAVE_RING_ST_SEALED 滞留超过 WAVE_SEAL_TIMEOUT_MS(写者没来取:卷未挂载、
|
||||
* 落盘链卡死都算)→ 自弃本段、err 计数加一、走冷却解冻。**绝不许让录波把环永久焊死**,
|
||||
* 那会让下一次真故障无波可录。
|
||||
*
|
||||
* 跨任务安全(§5.1 与 §5.2 第一条;本层的唯一并发契约):
|
||||
* 写者只有 adcTask,读者是 defaultTask 与 uiTask,因此只暴露「已封存」标志与已封存区
|
||||
* 读口,不给任何跨任务读活缓冲的路径:
|
||||
* 只能在 adcTask 调:wave_ring_init、wave_ring_push、wave_ring_tick、
|
||||
* wave_ring_latch_meta;
|
||||
* 只能由文件系统唯一写者 defaultTask 调:wave_ring_write_done;
|
||||
* 任意上下文可调:wave_ring_request(只投一个字节,protectTask / uiTask /
|
||||
* bench 都走它)、wave_ring_state、wave_ring_sealed、
|
||||
* wave_ring_sealed_meta、wave_ring_read_ch、wave_ring_read_cb、
|
||||
* wave_ring_get_stat、wave_ring_col。
|
||||
* 为什么这样就够、不需要临界区:单字节状态量与请求量在 Cortex-M4 上单字读写天然原子;
|
||||
* sealed 为 1 期间已封存区对 adcTask 是只读不写(写指针被冻在窗口之外),读者看到的数据
|
||||
* 不会在脚下变;请求走「单字投递 + 下一拍由 adcTask 自己消费」,与 App/algo/prot_elm.c
|
||||
* 的 prot_elm_request_reset() 同一模式。
|
||||
* 「单字」是硬要求不是措辞(lead 评审 2026-10-29):投递方是 protectTask(Normal)与
|
||||
* uiTask(BelowNormal),消费者是 adcTask(AboveNormal,会**抢占**它们)。旧实现先写挂号位
|
||||
* 再写触发源与元件号(三个字节、两条指令的窗口),adcTask 抢在中间就会把上一段的旧触发源
|
||||
* 与旧元件号抄进本段 —— 波形没错,错的恰好是这段记录唯一的溯源字段(现场靠它与
|
||||
* /log/start.log 对表),一次故障只录一段、错过事后无从发现。现在载荷本身编进那一个字节
|
||||
* (0 = 无请求,非 0 = 触发源占高位 | 元件号占低 5 位),一次存写要么看不见、要么看见完整
|
||||
* 一份,不需要顺序假设,也不需要任何平台相关的屏障或临界区(本层双端编译,禁止引
|
||||
* FreeRTOS / CMSIS 内在函数)。
|
||||
*
|
||||
* 观测计数的已知容差(**不修、只入档**,lead 裁定 2026-10-29):ovf 与 err 两个计数是跨上下文
|
||||
* 的读-改-自增(ovf 由任意上下文的拒绝路径加、err 由 adcTask 的超时自弃与 defaultTask 的落盘
|
||||
* 失败分别加),本层不设临界区,所以在并发拒绝 / 并发失败时理论上可能少计一次。定性成
|
||||
* **观测计数的已知容差**:它们只用于趋势与判读,不作为任何判据的唯一依据 —— 要坐实「冷却期
|
||||
* 内确实来了第二次触发」,同时看 armed 位与 /log/start.log 的时刻,别只盯 ovf 数字。
|
||||
*
|
||||
* 触发与冻结语义(§4.2,严格按此实现,别「顺手优化」):
|
||||
* IDLE →(触发)FREEZE:段首基址当拍钉死(预触发区从此不再被覆盖,后触发区继续写入),
|
||||
* 后触发计满 160 点 → SEALED:整段只读,新样本一律丢弃并计 drop,等 meta latch 与落盘
|
||||
* ACK → (ACK)COOL:恢复正常滚动写入,但本窗口内新触发只加 ovf 不重录 → IDLE。
|
||||
* 冻结期间丢的是录波样本:sample_ring、相量、保护照常跑,本层不碰它们、也无权影响它们。
|
||||
*
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_ring/、
|
||||
* test/host/wave_trig/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-29 DTU 项目组 创建:8 路 × 320 点 int16 分列环、请求/冻结/封段/ACK/
|
||||
* 冷却状态机(含 SEALED 滞留超时自弃)、段头锁存与
|
||||
* 已封存区按通道读口
|
||||
*/
|
||||
|
||||
#ifndef WAVE_RING_H
|
||||
#define WAVE_RING_H
|
||||
|
||||
/* 本头对外只暴露 stdint 的类型;文件里出现的 NULL 全在注释内(「不可为 NULL」这类
|
||||
* 约束说明),所以这里刻意不 include stddef.h —— 需要 NULL 的是包含本头的 .c,
|
||||
* 它们自己 include(台基线实测:ARMCC 的 stdint.h 不带 NULL)。 */
|
||||
#include <stdint.h>
|
||||
#include "wave_store.h" /* 同层配对头:wave_seg_meta_t / wave_ch_desc_t / wave_reader_fn /
|
||||
WAVE_ST_* 返回码(本层复用同一套 int 返回码通道,不另发明) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ===== 一、环几何(数值属主 = docs/子系统设计/故障录波设计说明.md §4.2 与 §7) ===== */
|
||||
|
||||
/* 每通道点数 = 预触发 8 周波 + 后触发 8 周波 = 160 + 160 = 320 点(1 kHz 下 320 ms) */
|
||||
#define WAVE_RING_PTS (320u)
|
||||
/* 预触发点数:触发点之前保留 8 周波;回放画触发光标的位点就是这个数(也是段内触发点下标) */
|
||||
#define WAVE_RING_PRE (160u)
|
||||
/* 后触发点数:触发点之后采满 8 周波才封段 */
|
||||
#define WAVE_RING_POST (160u)
|
||||
/* 通道数 = 8(TEMP 不录,理由见本文件头注);必须与 WAVE_ST_CH_NUM 相等,单测里钉住 */
|
||||
#define WAVE_RING_CH (8u)
|
||||
/* 冷却抑制窗:ACK 之后这么多拍内新触发只加 ovf 不重录(§4.2「只记第一次」)。
|
||||
* 500 拍 = 500 ms @1 kHz。注意:「覆盖一轮落盘窗口」这件事由 ACK 保证,本数值只是
|
||||
* ACK 之后的二次抑制(防同一次故障的抖动重复触发),待台架核。 */
|
||||
#define WAVE_RING_COOL_MS (500u)
|
||||
/* 已封存滞留上限:SEALED 状态超过这么多拍仍没人来取(卷未挂载、落盘链卡死都算)
|
||||
* → 自弃本段 + err 计数加一 + 走冷却解冻。2000 拍 = 2 s @1 kHz,待台架核。
|
||||
* 判据是 tick 的推进次数(也就是 adcTask 的存活),不是墙上时间:adcTask 自己挂了
|
||||
* 就轮到看门狗处理,本层不越权。 */
|
||||
#define WAVE_SEAL_TIMEOUT_MS (2000u)
|
||||
|
||||
/* 环数据区字节数:int16 × 320 × 8 = 5120(§7 预算里最大的一项;动它等于动预算) */
|
||||
#define WAVE_RING_BYTES (WAVE_RING_PTS * WAVE_RING_CH * (uint32_t)sizeof(int16_t))
|
||||
|
||||
/* ===== 二、类型 ===== */
|
||||
|
||||
/* wave_ring_tick 的事件:返回值语义是「本拍发生了什么」,不是错误 */
|
||||
typedef enum {
|
||||
WAVE_RING_EVT_NONE = 0, /* 正常推进,无事件 */
|
||||
WAVE_RING_EVT_SEALED = 1 /* 本拍刚计满后触发:**adcTask 必须在本拍调
|
||||
* wave_ring_latch_meta() 交棒封存瞬间事实**;
|
||||
* 不调则本段永不就绪,最终由 WAVE_SEAL_TIMEOUT_MS
|
||||
* 自弃(err 加一),可从串口周期行看到 */
|
||||
} wave_ring_evt_t;
|
||||
|
||||
/* 环状态。迁移只发生在 wave_ring_tick 与 wave_ring_write_done 里(单写者) */
|
||||
typedef enum {
|
||||
WAVE_RING_ST_IDLE = 0, /* 滚动:新样本覆盖最旧点,接受触发 */
|
||||
WAVE_RING_ST_FREEZE = 1, /* 已触发:段首基址已钉死(预触发区不再被覆盖),
|
||||
后触发区继续写入,直到计满 WAVE_RING_POST 点 */
|
||||
WAVE_RING_ST_SEALED = 2, /* 已封存:整段只读,新样本一律丢弃并计 drop;
|
||||
等 meta latch、等 defaultTask 落盘并 ACK */
|
||||
WAVE_RING_ST_COOL = 3 /* 冷却:已 ACK 解冻、恢复正常滚动,但新触发只加 ovf */
|
||||
} wave_ring_state_t;
|
||||
|
||||
/* 封存瞬间必须由 adcTask 交棒的事实(本层不链 App/algo/adc_cal,全靠这份入参)。
|
||||
* 段长 / 采样率 / 每周波点数 / 预触发点数 / 触发源与序号 / t0_ms / frame_seq 由本层自己填
|
||||
* (环就是这些数的物理实现,让调用方填只会多一个「忘了填」的失败面)。 */
|
||||
typedef struct {
|
||||
uint8_t eng; /* 折算档位(wave_eng_t:0 引脚域 / 1 二次值 / 2 一次值) */
|
||||
uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */
|
||||
uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */
|
||||
uint32_t wall; /* 对时后的 epoch 秒(未对时 0;未对时就写 0,不许拿上电毫秒冒充) */
|
||||
wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表前 8 项的 avail / k_q16 / b_codes:
|
||||
必须取 adc_cal_k_q16() 的 int32 原值,
|
||||
不许经 adc_cal_k_uv() 的 float 回算(§6.3:
|
||||
float 在 5.28e7 量级掉尾数);
|
||||
conv 与 bits 也在这里给(本层无从知道两片
|
||||
转换器的归属);sem 由本层按「表内语义号 == 下标」
|
||||
自己钉,传进来的 sem 被忽略 */
|
||||
} wave_seal_cal_t;
|
||||
|
||||
/* 观测快照(给 §9 那条周期行 wave: 与主机单测;字段序 ≈ 日志序,别随手换) */
|
||||
typedef struct {
|
||||
uint8_t state; /* wave_ring_state_t */
|
||||
uint8_t trig_src; /* 当前/最近一段的触发源(wave_trig_src_t;未触发为 0) */
|
||||
uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与 bench 为 0) */
|
||||
uint8_t meta_ready; /* 1 = 段头已 latch 可落盘;SEALED 期间为 0 就是「本段不可用」 */
|
||||
uint8_t seg_idx; /* 下一个要写的段号 0..99(轮转号由本层持有,defaultTask 照它取名) */
|
||||
uint8_t post_left; /* 后触发还差多少点封段(FREEZE 期间有意义;>255 时饱和在 255) */
|
||||
uint8_t rsv1; /* 对齐补齐 */
|
||||
uint8_t rsv2; /* 对齐补齐 */
|
||||
uint16_t cool_left; /* 冷却剩余拍数 */
|
||||
uint16_t seal_hold; /* 本段已在 SEALED 滞留多少拍(与 WAVE_SEAL_TIMEOUT_MS 对照) */
|
||||
uint32_t seg_total; /* 累计成功 ACK 的段数(给日志 seg=NN 与对账) */
|
||||
uint32_t ovf_cnt; /* 被丢弃的触发数:ACK 前重复触发 + 冷却期内触发(§9 ovf=) */
|
||||
uint32_t drop_cnt; /* 冻结窗口丢弃的录波点数,单位 = 点不是拍(§9 drop=) */
|
||||
uint32_t err_cnt; /* 落盘失败(write_done 非 0,含短写与 CRC 自弃)
|
||||
+ SEALED 滞留超时自弃 的次数(§9 err=) */
|
||||
uint32_t last_t0_ms; /* 最近一段段首相对上电毫秒 */
|
||||
uint32_t last_seq; /* 最近一段段首帧序号(与串口周期行的 frames 对账) */
|
||||
} wave_ring_stat_t;
|
||||
|
||||
/* ===== 三、生命周期(写者侧) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:复位环与全部状态(清 5120 字节数据区 + 索引 + 状态机 + 观测计数 + 轮转号)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:**只能在 adcTask 调**,且只能在开始压环之前调(启动期或显式重配)。
|
||||
* 内核启动前调用也安全:本函数不调任何 RTOS API、不建对象,纯数组清零
|
||||
* (平台约束「osKernelStart 之前禁止创建类 API」对本层不适用)
|
||||
* 函数说明:观测计数(seg_total / ovf_cnt / drop_cnt / err_cnt)与轮转号一并归零:
|
||||
* 重启录波就是重新起账,不留旧账,免得串口周期行的累计数与段文件对不上。
|
||||
* 轮转号归零意味着**本层的 seg_idx 只是本次上电的本地计数**(供日志与首轮参考),
|
||||
* 不是盘上真值:跨上电的权威下标必须由落盘道自持——启动扫目录用
|
||||
* wave_store_idx_from_name 重建、每次写成功后用 wave_store_next_idx 推进。
|
||||
* 若照本层归零后的 0 直接取名,重启后第一笔覆盖的就是上次上电的**最新**段,
|
||||
* 与轮转「新证据优先」的初衷正相反(§6.2 轮转语义)。
|
||||
*/
|
||||
void wave_ring_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍的 8 路原始码值(数据面的唯一写入路径)
|
||||
* 入口参数:p_codes 8 路码值数组,顺序 = adc_semantic_t 的前 8 项
|
||||
* (UA UB UC UN IA IB IC IN,即语义号升序,与段文件通道表列序
|
||||
* 同一口径,§6.3)
|
||||
* a_n 数组长度,必须等于 WAVE_RING_CH
|
||||
* 返 回 值:WAVE_ST_OK = 已写入;WAVE_ST_DONE = 本拍样本被丢弃(SEALED 窗口,drop 已加一);
|
||||
* WAVE_ST_E_PARAM = 空指针或长度不对 int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,每拍一次(1 kHz);不得从其它任务或 ISR 调。
|
||||
* 尤其禁止图省事挂在 Src/freertos.c 的 TIM3 回调里(adc_tick_irq_notify()
|
||||
* 旁边):那是 ISR 上下文,且 ISR 比调度器早约 100 ms 启动
|
||||
* (docs/子系统设计/看门狗设计说明.md §10.1),本层没有「消费者就绪」门,
|
||||
* 挂上去就是拿 ISR 改状态机。
|
||||
* 函数说明:IDLE、FREEZE、COOL 三态都写入;SEALED 一律丢弃并计数 —— 这就是 §4.2
|
||||
* 「冻结期间丢的是录波样本」,不是丢采样链路:sample_ring、相量、保护照常被
|
||||
* adcTask 喂,本层无权也不该影响它们。
|
||||
* 码值是转换器原始输出:TPAFE 为 16 位补码双极性、片内 ADC 为 0..4095 原样
|
||||
* 正数,**不做中点偏置、不做折算**(§6.3「样本编码」行:加偏置等于伪造双极性)。
|
||||
*/
|
||||
int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n);
|
||||
|
||||
/* ===== 四、触发请求、每拍推进、封存交棒、落盘 ACK ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:投递一次录波请求(单字投递,不碰环、不碰索引)
|
||||
* 入口参数:a_src 触发源(wave_trig_src_t 的 1 / 2 / 3;0 与其它值按参数错拒绝)
|
||||
* a_elm 触发元件序号(prot_elm 的 picked 位号,**必须小于 32**;
|
||||
* 手动与 bench 定时传 0。越界当场拒,不许静默截断 —— 载荷只有 5 位)
|
||||
* 返 回 值:WAVE_ST_OK = 已受理(下一拍由 adcTask 自己钉段首基址);
|
||||
* WAVE_ST_DONE = 被忽略(处于 FREEZE 或 SEALED 或 COOL,或请求还没被消费;
|
||||
* ovf 计数已加一,§9 的 ovf=);
|
||||
* WAVE_ST_E_PARAM = 触发源非法或元件号越界(参数错**不记 ovf 账**) int32_t
|
||||
* 限定条件:**任意上下文可调**(protectTask 的 picked 上升沿、uiTask 的手动确认、
|
||||
* bench 定周期、adcTask 自己),它只写**一个字节**(载荷编码见文件头
|
||||
* 「跨任务安全」段),与 App/algo/prot_elm.c 的 prot_elm_request_reset()
|
||||
* 同一模式(§5.1)
|
||||
* 函数说明:本函数不产生「立刻冻结」的效果 —— 段首基址由 adcTask 在下一拍
|
||||
* wave_ring_tick 里钉,避免跨上下文改环索引。最坏延迟一拍 = 1 ms,
|
||||
* 相对 160 点的预触发窗可以忽略。
|
||||
* **ACK 前一律只加 ovf**(lead 裁定):SEALED 期间再来触发不重录、不排队,
|
||||
* 本波不做多段排队。
|
||||
* **返回 WAVE_ST_E_PARAM 的两条路径不触碰任何计数**(ovf / err / drop 一个都不加,
|
||||
* 也不留挂号):那是调用方传错参数,与「第二次真触发」是两回事。台架不得拿 ovf
|
||||
* 当「谁误用了接口」的证据——误用只在返回值里体现(lead 裁定 2026-10-29)。
|
||||
*/
|
||||
int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm);
|
||||
|
||||
/*
|
||||
* 函数功能:推进一拍状态机(消费请求钉基址、计后触发点数、到点进 SEALED、
|
||||
* 冷却递减、SEALED 滞留超时自弃)
|
||||
* 入口参数:a_now_ms 当前相对上电毫秒(填段头 t0_ms;用 adcTask 已有的时基即可)
|
||||
* a_frame_seq 当前帧序号(填段头 frame_seq,与串口周期行的 frames 对账)
|
||||
* 返 回 值:wave_ring_evt_t(WAVE_RING_EVT_NONE / WAVE_RING_EVT_SEALED) int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,且每拍在 wave_ring_push 之后调一次
|
||||
* (先写样本再算点数,后触发才正好 160 个点)
|
||||
* 函数说明:FREEZE 期间写入行为与滚动无异,只是段首基址已钉死、预触发区不再被覆盖
|
||||
* —— 这就是 §4.2「触发即冻结写指针」的准确含义:冻住的是覆盖窗口,
|
||||
* 不是停止采集。计满 WAVE_RING_POST 点的本拍返回 WAVE_RING_EVT_SEALED。
|
||||
* SEALED 期间本函数只做「滞留计时 + 超时自弃」,不动数据、不动基址。
|
||||
*/
|
||||
int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq);
|
||||
|
||||
/*
|
||||
* 函数功能:封存交棒 —— 把「环已知字段 + adcTask 现取的封存瞬间事实」拼成完整段头并锁存
|
||||
* 入口参数:p_cal 封存瞬间事实(折算档位 / 有效性位图 / 削顶位图 / epoch 秒 / 通道表的
|
||||
* conv、bits、avail、k_q16、b_codes;不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK = 已锁存,wave_ring_sealed() 从此为 1;
|
||||
* WAVE_ST_E_STATE = 不在「刚封段待交棒」态(含已 latch 过第二次、非 SEALED);
|
||||
* WAVE_ST_E_PARAM = 空指针或折算档位非法 int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,且只在 wave_ring_tick 返回 WAVE_RING_EVT_SEALED 的
|
||||
* 那一拍调一次(重复调返回 WAVE_ST_E_STATE 且不覆盖已有锁存值 —— 半路覆盖
|
||||
* 会让落盘道读到的头与数据不同源)
|
||||
* 函数说明:为什么系数要调用方传、而不让本层自己读 adc_cal —— 本层要能被主机单测零桩
|
||||
* 纳入;更根本的是 §12 的要求:标定一旦落地,老文件必须还能按**当时**的系数
|
||||
* 解释,所以 eng / k / b / clip 属于段头、不属于当前固件状态。
|
||||
* 取值只能在第 3 步那一拍做一次(不每拍读、不低频刷),理由见本文件头注。
|
||||
*/
|
||||
int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘 ACK —— defaultTask 明确告知本段已搬完(成功或失败),据此解冻并起冷却
|
||||
* 入口参数:a_result 0 = 落盘成功(打开、写入字节数相符、目录可用);
|
||||
* 非 0 = 失败(含 f_write 短写、打开/建目录失败、CRC 自弃)——
|
||||
* 失败时段已丢,但环必须解冻,错误只计数不冻结
|
||||
* 返 回 值:WAVE_ST_OK = 已 ACK 并转入冷却;WAVE_ST_E_STATE = 当前不在 SEALED
|
||||
* (重复 ACK 或没有段可 ACK —— 不静默改状态,防落盘道把上一段的 ACK 打到新段),
|
||||
* 或段头还没 latch(第 3 步没做):这时候落盘道拿不到 sealed_meta,根本没可能
|
||||
* 写出文件,放过 ACK 等于白吃一个轮转号并把 seg=NN 报成没发生过的数,
|
||||
* 故一律拒绝。这种段唯一的出口是滞留到 WAVE_SEAL_TIMEOUT_MS 自弃并计 err int32_t
|
||||
* 限定条件:**只能由文件系统唯一写者 defaultTask 调**,一段一次,调之前先看
|
||||
* wave_ring_sealed()(它就是「SEALED 且已 latch」的合并判据)
|
||||
* 函数说明:失败不推进轮转号(下一段仍用同号重写,同号文件按 FA_CREATE_ALWAYS 截断,
|
||||
* 不留半截文件骗过 PC 脚本);成功才推进 seg_idx 与 seg_total。
|
||||
* 本函数不删文件、不碰 FatFs:那些都在落盘道(§5.1 所有权)。
|
||||
*/
|
||||
int32_t wave_ring_write_done(int32_t a_result);
|
||||
|
||||
/* ===== 五、状态查询与已封存区读口(跨任务安全的唯一入口) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:查当前状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:wave_ring_state_t 的取值 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_state(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查「有已封存段可读」标志 —— 跨任务的唯一握手量(§5.2 第一条陷阱)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = 状态为 SEALED **且段头已 latch**,此刻起 wave_ring_sealed_meta 与
|
||||
* wave_ring_read_ch 可用;0 = 不可用 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
* 函数说明:为什么把「已 latch」并进来而不是单开一个查询:省得 defaultTask 自己
|
||||
* 拼两个标志、拼错就是拿半份快照落盘(第 4 步明写的禁忌)。
|
||||
* 为 1 期间已封存区对 adcTask 只读不写,读者可以慢慢分次取、不需要临界区;
|
||||
* 为 0 时所有读口一律返 WAVE_ST_E_STATE 而**不是返回旧数据** ——
|
||||
* 把上一段的字节当这一段,等于伪造证据。
|
||||
*/
|
||||
uint8_t wave_ring_sealed(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取已封存段的段头快照(落盘道编头、回放页画标题都用它)
|
||||
* 入口参数:p_out 输出(整份拷出,约 136 字节;不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文(defaultTask / uiTask 只读)。整份拷贝、不暴露内部指针,
|
||||
* 免得调用方攥着指针跨到下一段去看
|
||||
* 函数说明:这里的 fs_hz / pts_per_cycle / pre_pts 是**数据**,回放页与 PC 脚本必须
|
||||
* 从这里读,不许当编译期常量(§2.1 第 1 条)。
|
||||
*/
|
||||
int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:按通道读已封存区的连续点窗(回放页画一路、落盘道搬一路都走它)
|
||||
* 入口参数:a_ch 列号 = 通道表下标 = 语义号(0 到 WAVE_RING_CH-1)
|
||||
* a_from 段内起始点下标(0 = 段首 = 最早的一点。点序口径:触发那一拍的
|
||||
* 样本落在点 WAVE_RING_PRE-1(预触发窗的最后一点),段内点
|
||||
* WAVE_RING_PRE 起是触发之后的第一拍——预触发 8 周波与后触发
|
||||
* 8 周波各 160 点,故障时刻正好是两半的分界(§4.2)
|
||||
* a_cnt 点数(1 起;超出段尾时函数内部夹到剩余点数,不报错)
|
||||
* p_dst 输出 int16 数组(不可为 NULL;画窗 40 点或搬运 128 点按调用方给的容量)
|
||||
* 返 回 值:不小于 0 = 实际读出的点数(到段尾会少于 a_cnt);
|
||||
* WAVE_ST_E_PARAM 空指针或 a_cnt 为 0;WAVE_ST_E_RANGE 列号或起始点越界;
|
||||
* WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文,但**只读已封存区**:本层不提供任何读活缓冲的接口,
|
||||
* IDLE / FREEZE / COOL 下一律 WAVE_ST_E_STATE(§5.2 第一条:不给跨任务读活环的路径)
|
||||
* 函数说明:内部按段首基址取模展开,对读者来说已封存区就是一段 320 点的线性数组。
|
||||
* 回放页画 2 周波 = 40 点(§8.1)一次调用即可;字节形态 = 段文件数据区的
|
||||
* 字节形态(int16 小端),所以落盘道能直接把它交给 wave_store 的打包游标。
|
||||
*/
|
||||
int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/*
|
||||
* 函数功能:同上,形态适配 App/algo/wave_store.h 的 wave_reader_fn(落盘道直接传它)
|
||||
* 入口参数:p_user 上下文(本层是单例,忽略,传 NULL 即可)
|
||||
* a_ch 列号
|
||||
* a_from 段内起始点下标
|
||||
* a_cnt 点数
|
||||
* p_dst 输出 int16 数组
|
||||
* 返 回 值:实际读出的点数(不小于 0);负 = WAVE_ST_E_*(同上三条) int32_t
|
||||
* 限定条件:任意上下文;只读已封存区(同上)
|
||||
* 函数说明:存在的唯一理由是让 wave_store_pack_start 的 p_reader 直接指过来,
|
||||
* 省掉接线索里一个只为转发的包装函数。
|
||||
*/
|
||||
int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/*
|
||||
* 函数功能:取观测计数快照(给 §9 的周期行与主机单测)
|
||||
* 入口参数:p_out 输出(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t
|
||||
* 限定条件:任意上下文(只读;一次拷约 44 字节,个别字段可能与 adcTask 的推进错开一拍,
|
||||
* 这是观测量的固有精度,不构成判据错误)
|
||||
* 函数说明:err_cnt 的两条来源都在本层收口(write_done 非 0、SEALED 滞留超时自弃),
|
||||
* 所以 §9 日志行的 err= 直接取它即可 —— 落盘道不需要自己数第二份账。
|
||||
*/
|
||||
int32_t wave_ring_get_stat(wave_ring_stat_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:把「语义通道号」换算成本环的列号(列序 = 语义号升序,§6.3)
|
||||
* 入口参数:a_sem 语义号(adc_semantic_t 值)
|
||||
* 返 回 值:0 到 WAVE_RING_CH-1 = 列号;WAVE_ST_E_RANGE = 该语义不录(TEMP 与越界值) int32_t
|
||||
* 限定条件:任意上下文(纯计算)
|
||||
* 函数说明:本波只录前 8 个语义通道,所以「列号 == 语义号」这条恒等式成立;
|
||||
* 但调用方必须走这个函数而不是自己写死下标 —— 二期带 TEMP 时列号与语义号
|
||||
* 就会分叉,写死下标的地方会静默错位。
|
||||
*/
|
||||
int32_t wave_ring_col(uint8_t a_sem);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WAVE_RING_H */
|
||||
@@ -1,981 +0,0 @@
|
||||
/*
|
||||
* 模块名称:录波段格式层实现(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转)
|
||||
* 模块功能:见 App/algo/wave_store.h 的文件头注释块——口径属主、职责边界、跨任务安全、
|
||||
* 二期不返工的约束全部写在那里,本文件不重复一遍。
|
||||
* 本文件是纯逻辑:只 include App/algo/wave_store.h 与 C 标准库的 stdint/stddef,
|
||||
* 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、不打日志。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-29 DTU 项目组 创建
|
||||
*/
|
||||
|
||||
#include "wave_store.h"
|
||||
|
||||
/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 ——
|
||||
* 这个差别主机单测照不出来(2026-10-29 台基线实跑 /keil-build 才暴露:本层
|
||||
* wave_store.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c
|
||||
* 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */
|
||||
#include <stddef.h>
|
||||
|
||||
/* 打包游标一次最多从数据源取多少个点(128 字节栈上临时区,落盘道在 defaultTask 上跑,
|
||||
* 不在 1 kHz 采集链上;取批是为了把「每点一次回调」压成「每 64 点一次回调」) */
|
||||
#define WAVE_ST_BATCH (64u)
|
||||
/* 数据区点下标非法时的内部哨兵(对外一律走 wave_store_data_off 的 0xFFFFFFFFu) */
|
||||
#define WAVE_ST_BAD_OFF (0xFFFFFFFFu)
|
||||
|
||||
/* ===== 内部:小端读写(显式移位拼装,不依赖宿主字节序,板端与 PC 侧同一判据) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:写 16 位小端
|
||||
* 入口参数:p 目标位置(至少 2 字节);v 值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 2 字节
|
||||
*/
|
||||
static void wave_st_put16(uint8_t *p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写 32 位小端
|
||||
* 入口参数:p 目标位置(至少 4 字节);v 值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 4 字节
|
||||
*/
|
||||
static void wave_st_put32(uint8_t *p, uint32_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFFu);
|
||||
p[2] = (uint8_t)((v >> 16) & 0xFFu);
|
||||
p[3] = (uint8_t)((v >> 24) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 16 位小端
|
||||
* 入口参数:p 源位置(至少 2 字节)
|
||||
* 返 回 值:值 uint16_t
|
||||
* 限定条件:调用方保证 p 可读 2 字节
|
||||
*/
|
||||
static uint16_t wave_st_get16(const uint8_t *p) {
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 32 位小端
|
||||
* 入口参数:p 源位置(至少 4 字节)
|
||||
* 返 回 值:值 uint32_t
|
||||
* 限定条件:调用方保证 p 可读 4 字节
|
||||
*/
|
||||
static uint32_t wave_st_get32(const uint8_t *p) {
|
||||
return ((uint32_t)p[0]) | (((uint32_t)p[1]) << 8) |
|
||||
(((uint32_t)p[2]) << 16) | (((uint32_t)p[3]) << 24);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把 int16 写成小端两字节
|
||||
* 入口参数:p 目标位置;v 码值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 2 字节
|
||||
* 函数说明:先转 uint16 再移位——负数右移是实现定义行为,转成无符号后行为确定。
|
||||
*/
|
||||
static void wave_st_put_code(uint8_t *p, int16_t v) {
|
||||
wave_st_put16(p, (uint16_t)v);
|
||||
}
|
||||
|
||||
/* ===== 内部:段头字段一致性自检(编码前与解码后各跑一次,同一把尺子) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:校验段头元数据的**几何与表序**是否自相矛盾
|
||||
* 入口参数:p_meta 段头元数据(非空)
|
||||
* 返 回 值:WAVE_ST_OK = 合法;WAVE_ST_E_SIZE 长度类不成立(零通道 / 零点数 / 预触发超段长);
|
||||
* WAVE_ST_E_FORMAT 档位 / 触发源 / 表序非法 int
|
||||
* 限定条件:p_meta 由调用方保证非空
|
||||
* 函数说明:为什么编码侧也要跑:写一个「看起来像样的坏段文件」比拒写糟得多——落盘道会
|
||||
* 把 CRC 与长度都算自洽的垃圾写进 NAND,等到回放页或 PC 脚本才发现,那时
|
||||
* 现场已经没了(§8.1「画成一条直线等于伪造证据」是同一条忌讳的另一半)。
|
||||
*/
|
||||
static int wave_st_meta_check(const wave_seg_meta_t *p_meta) {
|
||||
uint8_t i;
|
||||
|
||||
/* 零通道 / 零点数:这一段连长度都不成立(§6.2 末行「长度为 0 判废」) */
|
||||
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->pts_per_ch == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 预触发点数不能大于一段的点数;后触发 = pts_per_ch - pre_pts,允许为 0(手动触发
|
||||
* 撞上落盘窗口等极端情形),但不允许负数——那是把段长算错 */
|
||||
if (p_meta->pre_pts > p_meta->pts_per_ch) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 采样率与每周波点数是数据,但为 0 的数据没法解释(回放与 PC 都要拿它做除数) */
|
||||
if ((p_meta->fs_hz == 0u) || (p_meta->pts_per_cycle == 0u)) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 与 PC 侧 reader 的拒绝清单对齐(谁都不能产出一段「自己写得出、对面读不了」的文件):
|
||||
* 每周波不足 2 点连一个周期都描不出来;段长不足一个整周期则基波算法无从取窗。 */
|
||||
if (p_meta->pts_per_cycle < 2u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->pts_per_ch < (uint32_t)p_meta->pts_per_cycle) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->eng > (uint8_t)WAVE_ENG_PRI) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
if (p_meta->trig_src > (uint8_t)WAVE_TRIG_BENCH) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* 表序:第 i 项的语义号必须等于下标(§6.3「通道表序与列序」行)——
|
||||
* 数据区第 i 列归第 i 项解释,错一位就是拿 IB 的波形配 IC 的系数 */
|
||||
for (i = 0u; i < p_meta->ch_count; i++) {
|
||||
if (p_meta->ch[i].sem != i) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
if (p_meta->ch[i].conv > (uint8_t)1u) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* 位宽留个合理域:本波只有 16(TPAFE)与 12(片内 ADC),但格式层不写死这两个值,
|
||||
* 只挡明显不成立的(0 位、超过 32 位)——二期换转换器不必改这里 */
|
||||
if ((p_meta->ch[i].bits == 0u) || (p_meta->ch[i].bits > 32u)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== CRC-16/MODBUS ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 增量计算
|
||||
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
|
||||
* p_data 数据(NULL 视为空操作)
|
||||
* a_len 字节数
|
||||
* 返 回 值:新的中间值 uint16_t
|
||||
* 限定条件:任意上下文;纯计算
|
||||
* 函数说明:反射形态 = 右移、异或 0xA001、初值 0xFFFF、异或输出 0;
|
||||
* 写进文件时低字节在前(§6.3)。无表实现的理由见 wave_store.h 头注。
|
||||
*/
|
||||
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len) {
|
||||
uint16_t crc = a_crc;
|
||||
uint32_t i;
|
||||
|
||||
if (p_data == NULL) {
|
||||
return crc;
|
||||
}
|
||||
for (i = 0u; i < a_len; i++) {
|
||||
uint8_t b = p_data[i]; /* 身兼两职:本路字节 + 下面内层 8 次的计数器 */
|
||||
|
||||
crc = (uint16_t)(crc ^ (uint16_t)b);
|
||||
for (b = 0u; b < 8u; b++) {
|
||||
if ((crc & 0x0001u) != 0u) {
|
||||
crc = (uint16_t)((crc >> 1) ^ WAVE_ST_CRC_POLY);
|
||||
} else {
|
||||
crc = (uint16_t)(crc >> 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 异或输出恒 0:写成宏引用,换异或输出时只改一处 */
|
||||
return (uint16_t)(crc ^ WAVE_ST_CRC_XOROUT);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 一次算完
|
||||
* 入口参数:p_data 数据;a_len 字节数
|
||||
* 返 回 值:CRC;空输入返回初值定义 uint16_t
|
||||
* 限定条件:任意上下文。零长度的判废由调用方做(见 wave_store.h 同函数注释)
|
||||
*/
|
||||
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len) {
|
||||
return wave_store_crc_update(WAVE_ST_CRC_INIT, p_data, a_len);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CRC 实现自检(对规范锚点,不是自算自验)
|
||||
* 入口参数:无
|
||||
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符 int
|
||||
* 限定条件:任意上下文;开机或首次落盘前一次,不在每拍路径上
|
||||
* 函数说明:锚点串「123456789」的 CRC-16/MODBUS = 0x4B37(§6.3)。字面量必须纯 ASCII,
|
||||
* 所以这里用显式字节数组而不是把中文塞进字符串。
|
||||
*/
|
||||
int wave_store_crc_selftest(void) {
|
||||
static const uint8_t s_anchor[9] = {
|
||||
(uint8_t)0x31u, (uint8_t)0x32u, (uint8_t)0x33u,
|
||||
(uint8_t)0x34u, (uint8_t)0x35u, (uint8_t)0x36u,
|
||||
(uint8_t)0x37u, (uint8_t)0x38u, (uint8_t)0x39u
|
||||
};
|
||||
uint16_t got;
|
||||
|
||||
got = wave_store_crc(s_anchor, (uint32_t)sizeof(s_anchor));
|
||||
if (got != WAVE_ST_CRC_CHECK) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 段头编解码 ===== */
|
||||
|
||||
/* 语义通道的转换器归属与位宽(0 = TPAFE5160,1 = 片内 ADC1;口径属主是
|
||||
* docs/硬件平台/ADC通道映射说明.md,本表是其抄本,改硬件接线时两处一起改)。
|
||||
* 第 9 项 TEMP 本波不录入段文件(ch_count 恒 8),仍按实际归属填,
|
||||
* 免得将来放开 TEMP 时这里是个 0。 */
|
||||
static const uint8_t s_sem_conv[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
(uint8_t)0u, (uint8_t)1u, (uint8_t)1u,
|
||||
(uint8_t)0u, (uint8_t)0u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u
|
||||
};
|
||||
static const uint8_t s_sem_bits[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
16u, 12u, 12u,
|
||||
16u, 16u, 12u, 12u, 12u, 12u
|
||||
};
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据填成本波默认值(只填格式侧静态事实)
|
||||
* 入口参数:p_meta 输出
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:填三样格式侧的静态事实:语义号 = 下标(§6.3 表序口径,也是 PC 侧判据)、
|
||||
* conv / bits 按两片转换器的归属(下面那张表的口径属主是
|
||||
* docs/硬件平台/ADC通道映射说明.md 的抄本,本层刻意不带 adc_input.h,
|
||||
* 这样格式层在主机侧不需要任何假件就能编译;单测里 0xE6 的算术交叉验证
|
||||
* 会同时钉住这套顺序,抄错就会红)。
|
||||
* 不填 avail / k_q16 / b_codes:那些是封存瞬间的事实,必须由调用方现取现填
|
||||
* (adc_cal_k_q16 / adc_cal_flags / sys_clock),本层不链它们;
|
||||
* 调用方忘填 k_q16 与 avail 不会让编码失败(0 是合法整数),
|
||||
* 但会让 PC 侧把波形解释成一条 0 线——所以单测里有一条专门检查
|
||||
* 「填了系数的往返」与「没填系数时段的形状仍然正确、只是幅值归零」。
|
||||
*/
|
||||
int wave_store_meta_init(wave_seg_meta_t *p_meta) {
|
||||
uint8_t i;
|
||||
|
||||
if (p_meta == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
p_meta->fs_hz = (uint16_t)WAVE_ST_FS_DEF;
|
||||
p_meta->pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
|
||||
p_meta->ch_count = (uint8_t)WAVE_ST_CH_NUM;
|
||||
p_meta->pts_per_ch = (uint16_t)WAVE_ST_PTS_DEF;
|
||||
p_meta->pre_pts = (uint16_t)WAVE_ST_PRE_DEF;
|
||||
p_meta->trig_src = (uint8_t)WAVE_TRIG_NONE;
|
||||
p_meta->trig_elm = 0u;
|
||||
p_meta->t0_ms = 0u;
|
||||
p_meta->wall = 0u;
|
||||
p_meta->eng = (uint8_t)WAVE_ENG_PIN;
|
||||
p_meta->flags_seq = 0u;
|
||||
p_meta->clip_mask = 0u;
|
||||
p_meta->frame_seq = 0u;
|
||||
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
|
||||
p_meta->ch[i].sem = i;
|
||||
p_meta->ch[i].conv = s_sem_conv[i];
|
||||
p_meta->ch[i].bits = s_sem_bits[i];
|
||||
p_meta->ch[i].avail = 0u;
|
||||
p_meta->ch[i].k_q16 = 0;
|
||||
p_meta->ch[i].b_codes = 0;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:内部——把固定头 + 通道表写进缓冲(不含头 CRC)
|
||||
* 入口参数:p_meta 段头;p_out 缓冲(容量须 >= 头 CRC 覆盖区)
|
||||
* 返 回 值:写入字节数(= 头 CRC 覆盖区长度) uint32_t
|
||||
* 限定条件:p_meta 已过 wave_st_meta_check、p_out 容量已核过
|
||||
*/
|
||||
static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) {
|
||||
uint8_t i;
|
||||
const uint32_t tbl_n = (uint32_t)p_meta->ch_count * WAVE_ST_CH_BYTES;
|
||||
|
||||
p_out[WAVE_ST_OFF_MAGIC] = (uint8_t)WAVE_ST_MAGIC0;
|
||||
p_out[WAVE_ST_OFF_MAGIC + 1u] = (uint8_t)WAVE_ST_MAGIC1;
|
||||
/* 魔数第三字节 = 版本号:与 ver 字段同源同值(§6.2 第 0 行 + §6.3 判废口径) */
|
||||
p_out[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER;
|
||||
p_out[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_FS_HZ], p_meta->fs_hz);
|
||||
p_out[WAVE_ST_OFF_PTS_CYC] = p_meta->pts_per_cycle;
|
||||
p_out[WAVE_ST_OFF_CH_CNT] = p_meta->ch_count;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_PTS_CH], p_meta->pts_per_ch);
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_PRE_PTS], p_meta->pre_pts);
|
||||
p_out[WAVE_ST_OFF_TRIG_SRC] = p_meta->trig_src;
|
||||
p_out[WAVE_ST_OFF_TRIG_ELM] = p_meta->trig_elm;
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_T0_MS], p_meta->t0_ms);
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_WALL], p_meta->wall);
|
||||
p_out[WAVE_ST_OFF_ENG] = p_meta->eng;
|
||||
p_out[WAVE_ST_OFF_FLAGS] = p_meta->flags_seq;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_CLIP], p_meta->clip_mask);
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_SEQ], p_meta->frame_seq);
|
||||
|
||||
for (i = 0u; i < p_meta->ch_count; i++) {
|
||||
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)i * WAVE_ST_CH_BYTES);
|
||||
|
||||
p_out[pos + WAVE_ST_CH_OFF_SEM] = p_meta->ch[i].sem;
|
||||
p_out[pos + WAVE_ST_CH_OFF_CONV] = p_meta->ch[i].conv;
|
||||
p_out[pos + WAVE_ST_CH_OFF_BITS] = p_meta->ch[i].bits;
|
||||
p_out[pos + WAVE_ST_CH_OFF_AVAIL] = p_meta->ch[i].avail;
|
||||
/* int32 系数按小端原样落 4 字节:右移到 24 位前把符号扩展掉,
|
||||
* 保证板端 ARMCC 与主机 CRT 写出同一串字节 */
|
||||
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_KQ16], (uint32_t)p_meta->ch[i].k_q16);
|
||||
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_BCODE], (uint32_t)p_meta->ch[i].b_codes);
|
||||
}
|
||||
return (uint32_t)WAVE_ST_OFF_TBL + tbl_n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:编码段头(固定头 + 通道表 + 头 CRC)
|
||||
* 入口参数:p_meta 段头元数据;p_out 输出缓冲;a_cap 容量
|
||||
* 返 回 值:>= 0 写入字节数;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文;无静态缓冲、可重入
|
||||
* 函数说明:头 CRC 覆盖 [0, 30+12×ch_count)、自身紧随其后、低字节在前(§6.3)。
|
||||
*/
|
||||
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap) {
|
||||
uint32_t crc_pos;
|
||||
uint16_t crc;
|
||||
int rc;
|
||||
|
||||
if ((p_meta == NULL) || (p_out == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_st_meta_check(p_meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
crc_pos = WAVE_ST_HEAD_CRC_POS(p_meta->ch_count);
|
||||
if (a_cap < (crc_pos + 2u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
(void)wave_st_head_raw(p_meta, p_out);
|
||||
crc = wave_store_crc(p_out, crc_pos);
|
||||
/* 低字节在前(§6.3 首行):put16 本来就是小端,这里不额外翻转 */
|
||||
wave_st_put16(&p_out[crc_pos], crc);
|
||||
return (int)(crc_pos + 2u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解码 + 全套判废(魔数 / 版本 / 缓冲长度 / 几何 / 表序 / 头 CRC)
|
||||
* 入口参数:p_buf 头字节流;a_len 可读字节数;p_meta 输出
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:顺序刻意是「先验魔数与版本,再算整头长度,再验 CRC,最后验语义」——
|
||||
* 前两步决定我们**有没有资格**按这份布局去数偏移;反过来先按 ch_count 算长度,
|
||||
* 一个被写坏的 ch_count 就能让我们读到缓冲外面去。
|
||||
*/
|
||||
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta) {
|
||||
uint8_t ch_count;
|
||||
uint32_t crc_pos;
|
||||
uint32_t head_sz;
|
||||
uint16_t want;
|
||||
uint16_t got;
|
||||
int rc;
|
||||
wave_seg_meta_t tmp;
|
||||
|
||||
if ((p_buf == NULL) || (p_meta == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
/* 固定头是 30 + 1 字节通道数才能算出整头长度,至少得读到偏移 7 */
|
||||
if (a_len < ((uint32_t)WAVE_ST_OFF_CH_CNT + 1u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((p_buf[WAVE_ST_OFF_MAGIC] != (uint8_t)WAVE_ST_MAGIC0) ||
|
||||
(p_buf[WAVE_ST_OFF_MAGIC + 1u] != (uint8_t)WAVE_ST_MAGIC1)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* ver 必须等于魔数第三字节、且等于本层认识的版本(§6.2 末行) */
|
||||
if ((p_buf[WAVE_ST_OFF_VER] != p_buf[WAVE_ST_OFF_MAGIC + 2u]) ||
|
||||
(p_buf[WAVE_ST_OFF_VER] != (uint8_t)WAVE_ST_VER)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
ch_count = p_buf[WAVE_ST_OFF_CH_CNT];
|
||||
head_sz = wave_store_head_bytes(ch_count);
|
||||
if (head_sz == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_len < head_sz) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
crc_pos = WAVE_ST_HEAD_CRC_POS(ch_count);
|
||||
want = wave_st_get16(&p_buf[crc_pos]);
|
||||
got = wave_store_crc(p_buf, crc_pos);
|
||||
if (want != got) {
|
||||
return WAVE_ST_E_CRC;
|
||||
}
|
||||
|
||||
/* 头 CRC 过了才开始解释字段:下面填的是一份可交回调用方的完整元数据 */
|
||||
tmp.fs_hz = wave_st_get16(&p_buf[WAVE_ST_OFF_FS_HZ]);
|
||||
tmp.pts_per_cycle = p_buf[WAVE_ST_OFF_PTS_CYC];
|
||||
tmp.ch_count = ch_count;
|
||||
tmp.pts_per_ch = wave_st_get16(&p_buf[WAVE_ST_OFF_PTS_CH]);
|
||||
tmp.pre_pts = wave_st_get16(&p_buf[WAVE_ST_OFF_PRE_PTS]);
|
||||
tmp.trig_src = p_buf[WAVE_ST_OFF_TRIG_SRC];
|
||||
tmp.trig_elm = p_buf[WAVE_ST_OFF_TRIG_ELM];
|
||||
tmp.t0_ms = wave_st_get32(&p_buf[WAVE_ST_OFF_T0_MS]);
|
||||
tmp.wall = wave_st_get32(&p_buf[WAVE_ST_OFF_WALL]);
|
||||
tmp.eng = p_buf[WAVE_ST_OFF_ENG];
|
||||
tmp.flags_seq = p_buf[WAVE_ST_OFF_FLAGS];
|
||||
tmp.clip_mask = wave_st_get16(&p_buf[WAVE_ST_OFF_CLIP]);
|
||||
tmp.frame_seq = wave_st_get32(&p_buf[WAVE_ST_OFF_SEQ]);
|
||||
for (ch_count = 0u; ch_count < (uint8_t)WAVE_ST_CH_MAX; ch_count++) {
|
||||
tmp.ch[ch_count].sem = 0u;
|
||||
tmp.ch[ch_count].conv = 0u;
|
||||
tmp.ch[ch_count].bits = 0u;
|
||||
tmp.ch[ch_count].avail = 0u;
|
||||
tmp.ch[ch_count].k_q16 = 0;
|
||||
tmp.ch[ch_count].b_codes = 0;
|
||||
}
|
||||
for (ch_count = 0u; ch_count < p_buf[WAVE_ST_OFF_CH_CNT]; ch_count++) {
|
||||
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)ch_count * WAVE_ST_CH_BYTES);
|
||||
tmp.ch[ch_count].sem = p_buf[pos + WAVE_ST_CH_OFF_SEM];
|
||||
tmp.ch[ch_count].conv = p_buf[pos + WAVE_ST_CH_OFF_CONV];
|
||||
tmp.ch[ch_count].bits = p_buf[pos + WAVE_ST_CH_OFF_BITS];
|
||||
tmp.ch[ch_count].avail = p_buf[pos + WAVE_ST_CH_OFF_AVAIL];
|
||||
tmp.ch[ch_count].k_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_KQ16]);
|
||||
tmp.ch[ch_count].b_codes = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_BCODE]);
|
||||
}
|
||||
rc = wave_st_meta_check(&tmp);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
*p_meta = tmp;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 长度 / 偏移换算 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整头字节数
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* 返 回 值:字节数;通道数非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_head_bytes(uint8_t a_ch_count) {
|
||||
if ((a_ch_count == 0u) || (a_ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return 0u;
|
||||
}
|
||||
return WAVE_ST_HEAD_BYTES(a_ch_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按头部字段算整段应有字节数
|
||||
* 入口参数:p_meta 段头
|
||||
* 返 回 值:期望字节数;字段非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:等式 = 32 + 12×ch_count + pts_per_ch×ch_count×2 + 2(§6.3;
|
||||
* 8 路 320 点 = 5250)。落盘道拿它比对 f_write 写回字节数与 f_size。
|
||||
*/
|
||||
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta) {
|
||||
if (p_meta == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return 0u;
|
||||
}
|
||||
if (p_meta->pts_per_ch == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return WAVE_ST_SEG_TOTAL(p_meta->ch_count, p_meta->pts_per_ch);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:数据区某点的文件绝对偏移
|
||||
* 入口参数:a_ch_count 通道数;a_pts_ch 每通道点数;a_ch 列号;a_pt 段内点下标
|
||||
* 返 回 值:偏移;非法返回 0xFFFFFFFFu 哨兵 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:按通道分列 → 偏移 = 整头长 + (列 × 点数 + 点下标) × 2。单通道整列是连续的,
|
||||
* 这正是 §6.2「只读那一路」能一次连续读的原因。
|
||||
*/
|
||||
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
|
||||
uint8_t a_ch, uint32_t a_pt) {
|
||||
uint32_t head_sz = wave_store_head_bytes(a_ch_count);
|
||||
|
||||
if ((head_sz == 0u) || (a_pts_ch == 0u) || (a_ch >= a_ch_count)) {
|
||||
return WAVE_ST_BAD_OFF;
|
||||
}
|
||||
if (a_pt >= (uint32_t)a_pts_ch) {
|
||||
return WAVE_ST_BAD_OFF;
|
||||
}
|
||||
return head_sz + (((uint32_t)a_ch * (uint32_t)a_pts_ch) + a_pt) * 2u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从某点起当前列还能连续读多少字节
|
||||
* 入口参数:a_pts_ch 每通道点数;a_pt 起始点下标;a_want 想读点数
|
||||
* 返 回 值:可连续读字节数(参数非法返回 0) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want) {
|
||||
uint32_t left;
|
||||
uint32_t n;
|
||||
|
||||
if ((a_pts_ch == 0u) || (a_want == 0u)) {
|
||||
return 0u;
|
||||
}
|
||||
if (a_pt >= (uint32_t)a_pts_ch) {
|
||||
return 0u;
|
||||
}
|
||||
left = (uint32_t)a_pts_ch - a_pt;
|
||||
n = (a_want < left) ? a_want : left;
|
||||
return n * 2u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:小端字节 → int16 点序列
|
||||
* 入口参数:p_raw 原始字节;a_len 字节数;p_out 输出;a_max_pts 容量;p_pts 实际点数(可空)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:奇数字节按参数错处理而不是「丢掉尾巴」——半截样本被静默吞掉,
|
||||
* 回放页画出来的波形会比真的少最后一个点,而这种误差正好最难查。
|
||||
*/
|
||||
int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len,
|
||||
int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts) {
|
||||
uint32_t n;
|
||||
uint32_t i;
|
||||
|
||||
if ((p_raw == NULL) || (p_out == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (((a_len & 0x00000001u) != 0u) || (a_len > (a_max_pts * 2u))) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
n = a_len / 2u;
|
||||
for (i = 0u; i < n; i++) {
|
||||
/* 先攒成 uint16 再转 int16:补码回绕在 C 里是实现定义但两大编译器一致,
|
||||
* 而这里需要的正是「按补码位型解释」——TPAFE 的 16 位补码就是这么存的 */
|
||||
p_out[i] = (int16_t)wave_st_get16(&p_raw[i * 2u]);
|
||||
}
|
||||
if (p_pts != NULL) {
|
||||
*p_pts = n;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:码值 → 定点毫单位(mV)
|
||||
* 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出 mV
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;用 int64 中间量)
|
||||
* 函数说明:模型 eng = (code + b) × k × 1e-6(§6.3,与 adc_cal_apply() 同式),
|
||||
* k = k_q16 / 65536 µV/码 → mV = (code + b) × k_q16 / 65536000,四舍五入。
|
||||
* 中间量最大约 (32767 + 8192) × 5.28e7 ≈ 2.2e12,int64 宽裕。
|
||||
*/
|
||||
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
|
||||
int32_t *p_milli) {
|
||||
int64_t num;
|
||||
int64_t den = 65536000;
|
||||
int64_t v;
|
||||
|
||||
if ((p_desc == NULL) || (p_milli == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
num = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16;
|
||||
if (num >= 0) {
|
||||
v = (num + (den / 2)) / den;
|
||||
} else {
|
||||
v = -(((-num) + (den / 2)) / den);
|
||||
}
|
||||
*p_milli = (int32_t)v;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 整段校验 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整段镜像自检(头 + 数据区 + 数据 CRC)
|
||||
* 入口参数:p_img 镜像;a_len 长度;p_meta 输出段头(可空)
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
|
||||
* 限定条件:任意上下文;整段驻留时用(本波 5250 字节)
|
||||
* 函数说明:三条判废一条都不能省:长度 0 或不等、头 CRC、数据 CRC。
|
||||
* 期望总长用**头里的字段**算,不用编译期常量——文件是自己说它多长,
|
||||
* 与镜像实际长度不一致就是坏段(§6.2 末行)。
|
||||
*/
|
||||
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta) {
|
||||
wave_seg_meta_t meta;
|
||||
uint32_t want_total;
|
||||
uint32_t data_off;
|
||||
uint32_t data_len;
|
||||
uint16_t want;
|
||||
uint16_t got;
|
||||
int rc;
|
||||
|
||||
if (p_img == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_len == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
rc = wave_store_head_parse(p_img, a_len, &meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
want_total = wave_store_seg_size(&meta);
|
||||
if (want_total == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_len != want_total) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
data_off = wave_store_head_bytes(meta.ch_count); /* 数据区起点 = 整头字节数 */
|
||||
data_len = a_len - 2u - data_off;
|
||||
if (data_len == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
want = wave_st_get16(&p_img[a_len - 2u]);
|
||||
got = wave_store_crc(&p_img[data_off], data_len);
|
||||
if (want != got) {
|
||||
return WAVE_ST_E_CRC;
|
||||
}
|
||||
if (p_meta != NULL) {
|
||||
*p_meta = meta;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 打包游标 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化打包游标
|
||||
* 入口参数:p_ctx 游标;p_meta 段头;p_reader 数据源(可空 = 只产头);p_user 上下文
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文(纯计算);一段一个游标
|
||||
* 函数说明:段头整份拷进游标——搬运窗口几十毫秒,期间调用方完全可以去改它自己那份
|
||||
* (比如 adcTask 又跑了几拍),本段的字节不能受影响。
|
||||
*/
|
||||
int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta,
|
||||
wave_reader_fn p_reader, void *p_user) {
|
||||
int rc;
|
||||
|
||||
if ((p_ctx == NULL) || (p_meta == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_st_meta_check(p_meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
|
||||
return rc;
|
||||
}
|
||||
p_ctx->total = wave_store_seg_size(p_meta);
|
||||
if (p_ctx->total == 0u) {
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_HEAD;
|
||||
p_ctx->ch = 0u;
|
||||
p_ctx->head_done = 0u;
|
||||
p_ctx->ch_done = 0u;
|
||||
p_ctx->tail_done = 0u;
|
||||
p_ctx->rsv = 0u;
|
||||
p_ctx->crc_data = WAVE_ST_CRC_INIT;
|
||||
p_ctx->sent = 0u;
|
||||
p_ctx->p_reader = p_reader;
|
||||
p_ctx->p_user = p_user;
|
||||
p_ctx->meta = *p_meta;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取整段字节流的下一片
|
||||
* 入口参数:p_ctx 游标;p_out 缓冲;a_cap 容量;p_len 实际产出字节数
|
||||
* 返 回 值:WAVE_ST_OK 还有后续;WAVE_ST_DONE 整段产完;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文,同一游标不得并发使用
|
||||
* 函数说明:三段推进——头(一次给完,要求容量 >= 整头)、数据区(按列连续、整样本切,
|
||||
* 绝不切半个 int16,也不跨列,便于回调按列读)、尾 CRC 两字节。
|
||||
* 数据 CRC 边产边算(增量中间值),所以全程不需要整段驻留 RAM。
|
||||
*/
|
||||
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
|
||||
uint32_t a_cap, uint32_t *p_len) {
|
||||
uint32_t n = 0u;
|
||||
|
||||
if ((p_ctx == NULL) || (p_out == NULL) || (p_len == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
*p_len = 0u;
|
||||
if ((p_ctx->phase == (uint8_t)WAVE_PACK_IDLE) || (a_cap == 0u)) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) {
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
|
||||
/* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 128 字节整头) --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_HEAD) {
|
||||
uint32_t head_sz;
|
||||
int rc;
|
||||
|
||||
head_sz = wave_store_head_bytes(p_ctx->meta.ch_count);
|
||||
if (head_sz == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_cap < head_sz) {
|
||||
/* 不拆着头给:半截头意味着游标要存 128 字节中间态,纯浪费 */
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_store_head_build(&p_ctx->meta, p_out, a_cap);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
n = (uint32_t)rc;
|
||||
p_ctx->head_done = (uint16_t)n;
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_DATA;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* --- 数据区:按列推进,一次最多一批 --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_DATA) {
|
||||
int16_t batch[WAVE_ST_BATCH];
|
||||
|
||||
if (p_ctx->p_reader == NULL) {
|
||||
/* 没有数据源却进了数据区:宁可报错也不产空字节。
|
||||
* 产空字节会写出「长度对、内容是 0」的段文件,回放页会画出一条直线——
|
||||
* 那正是 §8.1 明令禁止的伪造证据形态。 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
while ((n < a_cap) && (p_ctx->ch < p_ctx->meta.ch_count)) {
|
||||
uint32_t room_pts = (a_cap - n) / 2u;
|
||||
uint32_t col_left = (uint32_t)p_ctx->meta.pts_per_ch - (uint32_t)p_ctx->ch_done;
|
||||
uint32_t take;
|
||||
int32_t got;
|
||||
uint32_t i;
|
||||
|
||||
if (col_left == 0u) {
|
||||
p_ctx->ch++;
|
||||
p_ctx->ch_done = 0u;
|
||||
continue;
|
||||
}
|
||||
if (room_pts == 0u) {
|
||||
break;
|
||||
}
|
||||
take = (room_pts < col_left) ? room_pts : col_left;
|
||||
if (take > (uint32_t)WAVE_ST_BATCH) {
|
||||
take = (uint32_t)WAVE_ST_BATCH;
|
||||
}
|
||||
got = p_ctx->p_reader(p_ctx->p_user, p_ctx->ch, (uint32_t)p_ctx->ch_done,
|
||||
take, batch);
|
||||
if (got < 0) {
|
||||
return (int)got;
|
||||
}
|
||||
if ((uint32_t)got != take) {
|
||||
/* 数据源给不满就是数据源的问题(越界 / 未封存),不拿半批凑长度 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
uint32_t base_n = n;
|
||||
for (i = 0u; i < take; i++) {
|
||||
wave_st_put_code(&p_out[n], batch[i]);
|
||||
n += 2u;
|
||||
}
|
||||
p_ctx->crc_data = wave_store_crc_update(p_ctx->crc_data, &p_out[base_n], take * 2u);
|
||||
p_ctx->ch_done = (uint16_t)((uint32_t)p_ctx->ch_done + take);
|
||||
}
|
||||
if (p_ctx->ch >= p_ctx->meta.ch_count) {
|
||||
/* 最后一列的最后一点产出后,「列走完了」要到下一次进循环才发现,
|
||||
* 所以这一拍可能一个字节都没产。这时候不许当参数错报出去:整段就差
|
||||
* 最后两片 CRC,报 E_PARAM 会让落盘道把一次真故障判成落盘失败(白丢)。
|
||||
* 让它落到下面的尾段分支,在同一次调用里把尾产完。 */
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_TAIL;
|
||||
}
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
if (n > 0u) {
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
if (p_ctx->phase != (uint8_t)WAVE_PACK_TAIL) {
|
||||
return WAVE_ST_E_PARAM; /* 容量连一个整样本都装不下 */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- 尾:数据区 CRC,低字节在前 --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_TAIL) {
|
||||
if (p_ctx->tail_done == 0u) {
|
||||
p_out[n] = (uint8_t)(p_ctx->crc_data & 0xFFu);
|
||||
n++;
|
||||
p_ctx->tail_done = 1u;
|
||||
}
|
||||
if ((p_ctx->tail_done == 1u) && (n < a_cap)) {
|
||||
p_out[n] = (uint8_t)((p_ctx->crc_data >> 8) & 0xFFu);
|
||||
n++;
|
||||
p_ctx->tail_done = 2u;
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_DONE;
|
||||
}
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
return (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) ? WAVE_ST_DONE : WAVE_ST_OK;
|
||||
}
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:游标已产出字节数
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:字节数;空指针返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx) {
|
||||
return (p_ctx == NULL) ? 0u : p_ctx->sent;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:游标对应整段的期望总长
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:字节数;空指针返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx) {
|
||||
return (p_ctx == NULL) ? 0u : p_ctx->total;
|
||||
}
|
||||
|
||||
/* ===== 段文件名轮转(只算名字,不碰文件系统) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:内部——把两位十进制写进缓冲
|
||||
* 入口参数:p 目标(至少 2 字节);a_idx 0..99
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:a_idx 已由调用方限制在 0..99
|
||||
*/
|
||||
static void wave_st_put_idx2(char *p, uint8_t a_idx) {
|
||||
p[0] = (char)('0' + (a_idx / 10u));
|
||||
p[1] = (char)('0' + (a_idx % 10u));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按段号生成段文件路径
|
||||
* 入口参数:p_out 缓冲;a_cap 容量;a_idx 段号 0..99
|
||||
* 返 回 值:>= 0 写入字符数(不含结束符);WAVE_ST_E_PARAM / WAVE_ST_E_RANGE int
|
||||
* 限定条件:任意上下文;纯字符拼装(不用格式化函数),板端与 PC 侧逐字节一致
|
||||
* 函数说明:形态 = 目录 / 前缀 + 两位十进制 + 扩展名,严格 8.3(§5.2 第三条)。
|
||||
*/
|
||||
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx) {
|
||||
static const char s_dir[] = WAVE_ST_DIR;
|
||||
static const char s_pfx[] = WAVE_ST_PFX;
|
||||
static const char s_ext[] = WAVE_ST_EXT;
|
||||
uint32_t pos = 0u;
|
||||
uint32_t i;
|
||||
char buf[WAVE_ST_PATH_MAX];
|
||||
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_dir) - 1u); i++) {
|
||||
buf[pos++] = s_dir[i];
|
||||
}
|
||||
buf[pos++] = '/';
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
|
||||
buf[pos++] = s_pfx[i];
|
||||
}
|
||||
wave_st_put_idx2(&buf[pos], a_idx);
|
||||
pos += 2u;
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_ext) - 1u); i++) {
|
||||
buf[pos++] = s_ext[i];
|
||||
}
|
||||
if (pos >= (uint32_t)sizeof(buf)) {
|
||||
/* 宏与拼装长度漂移时的兜底:宁可给不出名字,也绝不把结束符写到别人缓冲外 */
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
/* 容量必须容得下结束符:写成 a_cap < pos 会放过「刚好差一个字节」的调用方,
|
||||
* 于是把 p_out[pos] 写到别人缓冲外面(越界写在板子上是静默的) */
|
||||
if (a_cap <= pos) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
for (i = 0u; i <= pos; i++) {
|
||||
p_out[i] = buf[i];
|
||||
}
|
||||
return (int)pos;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:下一个轮转段号
|
||||
* 入口参数:a_idx 当前段号(越界按 0 处理)
|
||||
* 返 回 值:下一个段号 uint8_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint8_t wave_store_next_idx(uint8_t a_idx) {
|
||||
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
|
||||
return 0u;
|
||||
}
|
||||
if (a_idx >= ((uint8_t)WAVE_ST_SEG_MAX - 1u)) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint8_t)(a_idx + 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:内部——大小写无关地比对一个字符
|
||||
* 入口参数:a 待测字符(视作 ASCII);b 期望字符(取自段名宏)
|
||||
* 返 回 值:1 相同;0 不同 uint8_t
|
||||
* 限定条件:两侧都是 ASCII;非字母字符原样比
|
||||
* 函数说明:**两侧都**归一化成小写再比。只归一化一侧是个真踩过一次的坑——
|
||||
* 段名宏 WAVE_ST_PFX / WAVE_ST_EXT 是大写形态(FatFs 8.3 短名就是大写),
|
||||
* 只把待测串转小写等于「小写 vs 大写」永不相等,反解会拒绝掉全部合法段名
|
||||
* (单测 用例:首轮 100% 拒绝就是这个)。盘上大小写与宏写法
|
||||
* 谁都不该去猜,故两侧各归一一次。
|
||||
*/
|
||||
static uint8_t wave_st_ci_eq(char a, char b) {
|
||||
char x = a;
|
||||
char y = b;
|
||||
|
||||
if ((x >= 'A') && (x <= 'Z')) {
|
||||
x = (char)(x + ('a' - 'A'));
|
||||
}
|
||||
if ((y >= 'A') && (y <= 'Z')) {
|
||||
y = (char)(y + ('a' - 'A'));
|
||||
}
|
||||
return (x == y) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从文件名或路径反解段号
|
||||
* 入口参数:p_path 路径或纯文件名;p_idx 输出
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:只认 basename 上的「wave + 两位数字 + .bin」(大小写都收)。
|
||||
* 不匹配就返回错误,**绝不**把陌生名字当录波段——扫目录重建段账时误认一个
|
||||
* 别的文件,等于把别人的文件当波形解释(名字/参数类错误码严禁升级成卷操作,
|
||||
* 存储架构说明 §10.1 同一条教训)。
|
||||
*/
|
||||
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx) {
|
||||
static const char s_pfx[] = WAVE_ST_PFX;
|
||||
static const char s_ext[] = WAVE_ST_EXT;
|
||||
const char *base = p_path;
|
||||
const char *q = p_path;
|
||||
uint32_t i;
|
||||
uint32_t len = 0u;
|
||||
uint8_t hi;
|
||||
uint8_t lo;
|
||||
|
||||
if ((p_path == NULL) || (p_idx == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
/* 找 basename:支持反斜杠形态(PC 侧脚本拼出来的路径可能被原样传进来) */
|
||||
while (*q != '\0') {
|
||||
if ((*q == '/') || (*q == '\\')) {
|
||||
base = q + 1;
|
||||
}
|
||||
q++;
|
||||
}
|
||||
q = base;
|
||||
while ((*q != '\0') && (len < (uint32_t)WAVE_ST_PATH_MAX)) {
|
||||
q++;
|
||||
len++;
|
||||
}
|
||||
if ((*q != '\0') || (len != ((uint32_t)sizeof(s_pfx) - 1u) + 2u +
|
||||
((uint32_t)sizeof(s_ext) - 1u))) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
|
||||
if (wave_st_ci_eq(base[i], s_pfx[i]) == 0u) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
}
|
||||
hi = (uint8_t)base[i];
|
||||
lo = (uint8_t)base[i + 1u];
|
||||
if ((hi < '0') || (hi > '9') || (lo < '0') || (lo > '9')) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
for (i = (i + 2u); i < len; i++) {
|
||||
if (wave_st_ci_eq(base[i], s_ext[i - ((uint32_t)sizeof(s_pfx) - 1u) - 2u]) == 0u) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
}
|
||||
*p_idx = (uint8_t)(((hi - '0') * 10u) + (lo - '0'));
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
/*
|
||||
* 模块名称:录波段格式层(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转)
|
||||
* 模块功能:把「一段录波」在内存字节缓冲与结构化元数据之间逐字节翻译,并给落盘道
|
||||
* 提供分次搬运的打包游标。口径唯一属主 = docs/子系统设计/故障录波设计说明.md §6.2
|
||||
* (字节布局)与 §6.3(九问裁定:板端编码器与 PC 解码器 test/host/wave_read.py
|
||||
* 两份独立实现的共同依据)。本文件是它的机器可执行形态,不在此重新定义任何口径;
|
||||
* 两处冲突以设计文档为准并回报 lead,不在代码里单方面「修正」。
|
||||
* 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、
|
||||
* 不建任务、不打日志(文件 I/O 与任务钩子全在接线层 App/task/ 里做),
|
||||
* 因此可被主机侧单测 test/host/wave_store/ 原样纳入做逐字节断言。
|
||||
* 职责边界(为什么这样切):
|
||||
* 1) 固定头 + 通道表编解码、头 CRC 与数据区 CRC 自检 —— 格式知识只此一处;
|
||||
* 2) 打包游标(分次搬运):文件系统唯一写者只要「每次给我不超过 256 字节的下一片」,
|
||||
* 整段 5250 字节不驻留 RAM(设计说明 §7:搬运缓冲 256 B × 2,不做整段驻留);
|
||||
* 3) 段文件名轮转与反解:只算名字,不打开文件(FatFs FF_USE_LFN=0 只能严格
|
||||
* 8.3 短名,见 docs/架构设计/STM32F4-Base存储架构说明.md §10.1);
|
||||
* 4) 数据区偏移换算 + 小端解码:回放页只画一路(设计说明 §8.1),按通道分列
|
||||
* 让「只读那一路」成为一次连续读。
|
||||
* 不链 App/algo/adc_cal(零依赖 = 主机免桩可测):通道表里的 k_q16 / b_codes /
|
||||
* 可用性由调用方在封存瞬间取 int32 原值填进 meta 后交给本层编码。系数必须走
|
||||
* adc_cal_k_q16()(接线道补的 int32 直读入口),不得经 adc_cal_k_uv() 的 float
|
||||
* 通道回算 —— float 在 5.28e7 量级掉尾数(设计说明 §6.3「k / b 宽度与角色」行)。
|
||||
* CRC 一张表的原始说法不成立,lead 已在 §6.3 记录实测依据:全仓无可复用的
|
||||
* CRC-16 表(App/util/README.md 记 crc.c 与 crc.h 已于 2026-10-02 删除;屏板链路
|
||||
* Drivers/BSP/UI/ui_net_sink.c 的 remo_crc() 是单字节异或、不是 CRC-16)。
|
||||
* 故本层自带无表位运算 CRC-16/MODBUS,并用规范锚点自校验。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-29 DTU 项目组 创建:段头/通道表编解码、CRC-16/MODBUS 无表实现、
|
||||
* 打包游标、段名轮转与反解、数据区偏移换算与小端解码
|
||||
*
|
||||
* 二期不返工的结构约束(设计说明 §2.1,改本层前先读):
|
||||
* 1) 采样率与每周波点数是写进文件头的数据,不是编译期假设 —— 回放端与 PC 脚本一律
|
||||
* 从头里读,本层算长度也照 meta 里的值算,不在代码里写死 1000 / 20;
|
||||
* 2) 版本号并进魔数第三字节,换格式即变;本层解到不认识的版本一律判废;
|
||||
* 3) 「带 TEMP 就要连 flags_seq 与 clip_mask 一起升版本」同源(§6.2 那两行的括注),
|
||||
* 所以本层的通道数上限留到 9,但本波实际只用 8。
|
||||
*/
|
||||
|
||||
#ifndef WAVE_STORE_H
|
||||
#define WAVE_STORE_H
|
||||
|
||||
/* 同 App/algo/wave_ring.h:本头的 NULL 只出现在注释里(字段与入参的约束说明),
|
||||
* 代码只用 stdint 的类型,故不 include stddef.h;用 NULL 的 .c 自己带。 */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ===== 一、段文件布局常量(逐字段照 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3) ===== */
|
||||
|
||||
/* 魔数前两字节固定 'W' 与 'R',第三字节 = 版本号(设计说明 §6.2 偏移 0 行) */
|
||||
#define WAVE_ST_MAGIC0 (0x57u) /* 'W' */
|
||||
#define WAVE_ST_MAGIC1 (0x52u) /* 'R' */
|
||||
/* 版本:本波恒 1。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行) */
|
||||
#define WAVE_ST_VER (1u)
|
||||
|
||||
/* 固定头字段偏移(字节,小端) */
|
||||
#define WAVE_ST_OFF_MAGIC (0u) /* 3 字节:'W' 'R' ver */
|
||||
#define WAVE_ST_OFF_VER (3u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_FS_HZ (4u) /* 2 字节 本波 1000 */
|
||||
#define WAVE_ST_OFF_PTS_CYC (6u) /* 1 字节 本波 20 */
|
||||
#define WAVE_ST_OFF_CH_CNT (7u) /* 1 字节 本波 8 */
|
||||
#define WAVE_ST_OFF_PTS_CH (8u) /* 2 字节 本波 320 */
|
||||
#define WAVE_ST_OFF_PRE_PTS (10u) /* 2 字节 本波 160 */
|
||||
#define WAVE_ST_OFF_TRIG_SRC (12u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_TRIG_ELM (13u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_T0_MS (14u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_WALL (18u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_ENG (22u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_FLAGS (23u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_CLIP (24u) /* 2 字节 */
|
||||
#define WAVE_ST_OFF_SEQ (26u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_TBL (30u) /* 通道表起点 */
|
||||
|
||||
/* 通道表一路 12 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4
|
||||
* 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) */
|
||||
#define WAVE_ST_CH_OFF_SEM (0u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_CONV (1u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_BITS (2u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_AVAIL (3u) /* 1 字节 = adc_cal_flags(sem) 低 8 位原值,不自造位义 */
|
||||
#define WAVE_ST_CH_OFF_KQ16 (4u) /* 4 字节 int32 */
|
||||
#define WAVE_ST_CH_OFF_BCODE (8u) /* 4 字节 int32 */
|
||||
#define WAVE_ST_CH_BYTES (12u)
|
||||
|
||||
/* 本波通道数固定 8(TEMP 不录:理由见 §6.3 末段与 docs/任务规划/数据可信.md §7) */
|
||||
#define WAVE_ST_CH_NUM (8u)
|
||||
/* 通道数上限:给二期「带 TEMP 且同时升版本」留位;真正的口径是 meta 里的 ch_count 字段 */
|
||||
#define WAVE_ST_CH_MAX (9u)
|
||||
/* 每通道点数默认(= 预触发 + 后触发 = 160 + 160;长度是文件里的数据,宏只作默认值) */
|
||||
#define WAVE_ST_PTS_DEF (320u)
|
||||
#define WAVE_ST_PRE_DEF (160u)
|
||||
#define WAVE_ST_FS_DEF (1000u)
|
||||
#define WAVE_ST_PPC_DEF (20u)
|
||||
|
||||
/* 尺寸一律由字段算出,不写死字面量(改通道数/点数时自动跟着走):
|
||||
* 头 CRC 覆盖区 = 30 + 12 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 12 × ch 起
|
||||
* ch_count = 8 时:覆盖区 126、整头 128、数据区 5120、整段 5250(§6.3「CRC 覆盖范围」行) */
|
||||
#define WAVE_ST_HEAD_CRC_POS(ch) ((uint32_t)(WAVE_ST_OFF_TBL) + \
|
||||
((uint32_t)(ch) * WAVE_ST_CH_BYTES))
|
||||
#define WAVE_ST_HEAD_BYTES(ch) ((uint32_t)(WAVE_ST_HEAD_CRC_POS(ch)) + 2u)
|
||||
#define WAVE_ST_DATA_BYTES(ch, pts) ((uint32_t)(pts) * (uint32_t)(ch) * 2u)
|
||||
#define WAVE_ST_SEG_TOTAL(ch, pts) (WAVE_ST_HEAD_BYTES(ch) + \
|
||||
WAVE_ST_DATA_BYTES(ch, pts) + 2u)
|
||||
/* 本波实际值(只给注释与单测引用,代码里一律用上面的参数化形态) */
|
||||
#define WAVE_ST_HEAD_SZ_CUR (128u)
|
||||
#define WAVE_ST_SEG_SZ_CUR (5250u)
|
||||
|
||||
/* 段目录与轮转(设计说明 §5.2 第三条:只能严格 8.3 短名;目录与 /log 平级、FTP 根下可见)
|
||||
* 名字形态 = 目录 + 前缀 + 两位十进制 + 扩展名,号 0..99 共 100 个轮转覆盖 */
|
||||
#define WAVE_ST_DIR "/wave"
|
||||
#define WAVE_ST_PFX "WAVE"
|
||||
#define WAVE_ST_EXT ".BIN"
|
||||
#define WAVE_ST_SEG_MAX (100u)
|
||||
#define WAVE_ST_PATH_MAX (17u) /* /wave/WAVE00.BIN = 16 字符 + 结束符 = 17 字节 */
|
||||
|
||||
/* CRC-16/MODBUS:多项式 0x8005 反射形态 = 0xA001、初值 0xFFFF、异或输出 0、低字节在前 */
|
||||
#define WAVE_ST_CRC_POLY (0xA001u)
|
||||
#define WAVE_ST_CRC_INIT (0xFFFFu)
|
||||
#define WAVE_ST_CRC_XOROUT (0x0000u)
|
||||
/* 自检锚点:串 123456789 的 CRC(§6.3 首行钉的常量) */
|
||||
#define WAVE_ST_CRC_CHECK (0x4B37u)
|
||||
|
||||
/* ===== 二、返回码(int 通道;调用方必须接住或显式 (void),不得裸丢) ===== */
|
||||
|
||||
#define WAVE_ST_OK (0) /* 成功 */
|
||||
#define WAVE_ST_DONE (1) /* 打包游标:整段已产完(不是错误) */
|
||||
#define WAVE_ST_E_PARAM (-1) /* 空指针 / 容量不足 / 字段非法 */
|
||||
#define WAVE_ST_E_CRC (-2) /* CRC 不符(头或数据区):整段判废 */
|
||||
#define WAVE_ST_E_FORMAT (-3) /* 魔数不符 / 版本不认识 / 表内语义号不等于下标 */
|
||||
#define WAVE_ST_E_SIZE (-4) /* 长度为 0 或长度与头部字段矛盾:整段判废 */
|
||||
#define WAVE_ST_E_RANGE (-5) /* 列号 / 点下标 / 段号越界 */
|
||||
#define WAVE_ST_E_STATE (-6) /* 状态错:未封存就读、未 start 就步进、缺数据源 */
|
||||
#define WAVE_ST_E_NAME (-7) /* 文件名不是本层命名形态(扫目录重建段账时用) */
|
||||
|
||||
/* ===== 三、类型 ===== */
|
||||
|
||||
/* 触发源(§6.2 偏移 12 的取值;0 表示「无触发」,只出现在段外的状态查询里) */
|
||||
typedef enum {
|
||||
WAVE_TRIG_NONE = 0, /* 无 */
|
||||
WAVE_TRIG_MANUAL = 1, /* 手动(UI「手动录波」确认,task-2 接线) */
|
||||
WAVE_TRIG_PROTECT = 2, /* 保护启动(App/algo/prot_elm.c 的 picked 上升沿) */
|
||||
WAVE_TRIG_BENCH = 3 /* bench 定周期自抓(只在 DTU_BENCH 构建里有这条来路) */
|
||||
} wave_trig_src_t;
|
||||
|
||||
/* 折算档位(§6.2 偏移 22;单位一律以头里的 eng 为准,PC 不许按当前固件口径猜) */
|
||||
typedef enum {
|
||||
WAVE_ENG_PIN = 0, /* 引脚域(未标定;V0.1 产品构建停在这档) */
|
||||
WAVE_ENG_SEC = 1, /* 二次值 */
|
||||
WAVE_ENG_PRI = 2 /* 一次值 */
|
||||
} wave_eng_t;
|
||||
|
||||
/* 通道表一路的内存形态(字段序 = §6.2 通道表字节序) */
|
||||
typedef struct {
|
||||
uint8_t sem; /* 语义号(adc_semantic_t 值;必须等于表内下标,§6.3) */
|
||||
uint8_t conv; /* 来源转换器(adc_source_t 值:0 = TPAFE,1 = STM32) */
|
||||
uint8_t bits; /* 转换器输出位宽(TPAFE 16 / STM32 12) */
|
||||
uint8_t avail; /* 可用性 = adc_cal_flags(sem) 低 8 位原值(不自造位义) */
|
||||
int32_t k_q16; /* 封存瞬间生效系数:µV/码 × 65536,就是 LSB */
|
||||
int32_t b_codes; /* 封存瞬间生效零点偏移:码值 */
|
||||
} wave_ch_desc_t;
|
||||
|
||||
/* 段头元数据 = 固定头 + 通道表的内存形态。
|
||||
* 这是「待编码的段头」,不是「已写盘的证据」:打包游标只读它、不改它。 */
|
||||
typedef struct {
|
||||
uint16_t fs_hz; /* 采样率 Hz(从文件读出来的数据,不是编译期常量) */
|
||||
uint8_t pts_per_cycle; /* 每周波点数 */
|
||||
uint8_t ch_count; /* 通道数(本波 8) */
|
||||
uint16_t pts_per_ch; /* 每通道点数(本波 320) */
|
||||
uint16_t pre_pts; /* 预触发点数(本波 160;回放画触发光标用) */
|
||||
uint8_t trig_src; /* 触发源(wave_trig_src_t) */
|
||||
uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与定时为 0) */
|
||||
uint32_t t0_ms; /* 段首相对上电毫秒 */
|
||||
uint32_t wall; /* 对时后的 epoch 秒(未对时 0) */
|
||||
uint8_t eng; /* 折算档位(wave_eng_t) */
|
||||
uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */
|
||||
uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */
|
||||
uint32_t frame_seq; /* 段首帧序号(与串口周期行的 frames 对账) */
|
||||
wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表(前 ch_count 项有效) */
|
||||
} wave_seg_meta_t;
|
||||
|
||||
/* 打包游标阶段(对外只用于「已搬完」判定与观测日志;推进全在 wave_store_pack_next 内部) */
|
||||
typedef enum {
|
||||
WAVE_PACK_IDLE = 0, /* 未初始化 */
|
||||
WAVE_PACK_HEAD = 1, /* 正在产头(固定头 + 通道表 + 头 CRC) */
|
||||
WAVE_PACK_DATA = 2, /* 正在产数据区(按通道分列,一列一列产) */
|
||||
WAVE_PACK_TAIL = 3, /* 只剩数据区 CRC 两字节 */
|
||||
WAVE_PACK_DONE = 4 /* 整段已产完 */
|
||||
} wave_pack_phase_t;
|
||||
|
||||
/* 数据源回调:把「第 a_ch 列、段内第 a_from 点起的 a_cnt 个点」取进 p_dst。
|
||||
* 返 回 值:实际拷贝的点数(等于 a_cnt 表示全给到);负 = 错误码 int32_t
|
||||
* 只允许读已封存区:本层不提供、也不允许任何跨任务读活缓冲的路径
|
||||
* (设计说明 §5.2 第一条陷阱:App/algo/sample_ring.c 的 sample_ring_window() 无临界区,
|
||||
* 本波不复用它,跨任务只传「已封存」标志)。
|
||||
* 生产实现 = wave_ring_read_cb()(签名一致,可直接传给 wave_store_pack_start)。 */
|
||||
typedef int32_t (*wave_reader_fn)(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/* 打包游标:段文件字节流的「拉模式」状态机。
|
||||
* 为什么拉模式 + 小缓冲:defaultTask 是文件系统唯一写者,落盘窗口几十毫秒量级,
|
||||
* 一次心跳最多一段;整段驻留 RAM 既超预算也没必要(设计说明 §7)。
|
||||
* 游标不校验写回字节数 —— 那是调用方对着 f_write 做的事(§5.2 第二条陷阱:本移植
|
||||
* f_write 簇分配失败会返 FR_OK + 短写、没有错误码,必须靠比对写回字节数判失败)。 */
|
||||
typedef struct {
|
||||
uint8_t phase; /* wave_pack_phase_t */
|
||||
uint8_t ch; /* 数据区当前列 */
|
||||
uint16_t head_done; /* 头区已产出字节数(含头 CRC 两字节) */
|
||||
uint16_t ch_done; /* 当前列已产出点数 */
|
||||
uint8_t tail_done; /* 尾部数据 CRC 已产出字节数(0..2) */
|
||||
uint8_t rsv; /* 补齐用,保持结构体偶数字节 */
|
||||
uint16_t crc_data; /* 数据区运行 CRC(增量中间值,避免整段驻留) */
|
||||
uint32_t total; /* 整段期望字节数 */
|
||||
uint32_t sent; /* 已产出字节数(= 下一片的文件偏移) */
|
||||
wave_seg_meta_t meta; /* 段头快照(start 时拷入;搬运期间调用方改自己那份不影响) */
|
||||
wave_reader_fn p_reader; /* 数据源;NULL = 只产头 */
|
||||
void *p_user; /* 数据源上下文 */
|
||||
} wave_pack_ctx_t;
|
||||
|
||||
/* ===== 四、CRC-16/MODBUS(无表位运算) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 增量计算(输入输出均低位在前)
|
||||
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
|
||||
* p_data 数据指针(NULL 且 a_len = 0 视为空操作,原样返回 a_crc)
|
||||
* a_len 字节数
|
||||
* 返 回 值:新的中间值(写文件时低字节在前,见 §6.3 首行) uint16_t
|
||||
* 限定条件:任意上下文(含 adcTask 之外的 defaultTask / uiTask);纯计算、无静态状态、
|
||||
* 不碰外设、不取临界区
|
||||
* 函数说明:为什么自带实现 —— 全仓没有可复用的 CRC-16 表(依据见本文件头注)。
|
||||
* 无表 = 零新增表 RAM 与表 Flash;数据区 5120 字节每字节 8 次移位,在 168 MHz
|
||||
* 上量级是几十微秒,且只在 defaultTask 的落盘链上出现,不在 1 kHz 采集链上。
|
||||
*/
|
||||
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len);
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 一次算完(从初值起步)
|
||||
* 入口参数:p_data 数据指针
|
||||
* a_len 字节数
|
||||
* 返 回 值:CRC uint16_t
|
||||
* 限定条件:任意上下文。p_data 为 NULL 或 a_len = 0 时返回 WAVE_ST_CRC_INIT —— 这是
|
||||
* 空输入的定义值,不是「算好的段 CRC」:零长度一律判废,调用方必须先判长度
|
||||
* (设计说明 §6.2 末行:长度为 0 → 整段作废)
|
||||
*/
|
||||
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len);
|
||||
|
||||
/*
|
||||
* 函数功能:CRC 自检 —— 用规范锚点验算实现是否仍是 CRC-16/MODBUS
|
||||
* 入口参数:无
|
||||
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符(实现或编译配置被改坏) int
|
||||
* 限定条件:任意上下文;开机或首次落盘前调一次即可,不在每拍路径上
|
||||
* 函数说明:锚点 = 串 123456789 的 CRC = 0x4B37(§6.3)。这条是机器可执行的口径锚,
|
||||
* 防的是「自算自验永远相符」:只有对已知常数才咬得住(换机、换编译器、
|
||||
* 反射方向写反都在这里暴露)。落盘前失败就自弃本段并计入 err(§9 观测行)。
|
||||
*/
|
||||
int wave_store_crc_selftest(void);
|
||||
|
||||
/* ===== 五、段头编解码 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据填成本波默认值(只填格式侧的静态事实)
|
||||
* 入口参数:p_meta 输出(调用方持有的结构;本层不返回静态地址)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文;不碰文件系统
|
||||
* 函数说明:填 fs_hz = 1000、pts_per_cycle = 20、ch_count = 8、pts_per_ch = 320、
|
||||
* pre_pts = 160,以及通道表的 sem = 下标 + conv / bits 按两片转换器
|
||||
* (UA / UN / IA 走 TPAFE 16 位;UB / UC / IB / IC / IN 走片内 ADC 12 位 ——
|
||||
* 通道归属口径属主是 docs/硬件平台/ADC通道映射说明.md,本函数只是它的抄本,
|
||||
* 单测里对 0xE6 的算术交叉验证会同时钉住这套顺序)。
|
||||
* 不填 avail / k_q16 / b_codes / trig_* / t0_ms / wall / eng / flags_seq /
|
||||
* clip_mask / frame_seq:那些是封存瞬间的事实,必须由调用方现取现填
|
||||
* (adc_cal_k_q16 / adc_cal_flags / sys_clock / wave_ring),本层不链它们。
|
||||
*/
|
||||
int wave_store_meta_init(wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据编码成段文件头字节流(固定头 + 通道表 + 头 CRC,全小端)
|
||||
* 入口参数:p_meta 段头元数据(只读)
|
||||
* p_out 输出缓冲
|
||||
* a_cap 缓冲容量(须不小于按 ch_count 算出的整头字节数)
|
||||
* 返 回 值:不小于 0 = 实际写入字节数;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文;无静态缓冲、可重入;不碰文件系统
|
||||
* 函数说明:编码前先自检字段一致性 —— ch_count 为 0 或越界、pts_per_ch = 0、
|
||||
* pre_pts 大于 pts_per_ch、表内语义号不等于下标(§6.3「通道表序与列序」那条
|
||||
* PC 侧判据在编码侧同样成立)一律拒绝:不把坏头写成看起来像样的文件。
|
||||
*/
|
||||
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap);
|
||||
|
||||
/*
|
||||
* 函数功能:把段文件头字节流解回元数据,并做全套判废检查(魔数 / 版本 / 长度 / 表序 / 头 CRC)
|
||||
* 入口参数:p_buf 头字节流
|
||||
* a_len 可读字节数
|
||||
* p_meta 输出元数据(判废时内容不保证有效,调用方不得消费)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针或缓冲不足;WAVE_ST_E_FORMAT 魔数 / 版本 /
|
||||
* 表序不符;WAVE_ST_E_SIZE 零长度或长度与字段矛盾;WAVE_ST_E_CRC 头 CRC 不符 int
|
||||
* 限定条件:任意上下文(uiTask 与 defaultTask 都可调)。UI 与 PC 侧必须用同一套判废语义:
|
||||
* 解不过就只能显示「该段不可读」,绝不允许退化画一条直线
|
||||
* (设计说明 §8.1 空态行:画直线等于伪造证据)
|
||||
* 函数说明:a_len 不足「按头里 ch_count 推出来的整头长度」时按缓冲不足返回,不猜没读到
|
||||
* 的字节;ver 不等于魔数第三字节、或不等于 WAVE_ST_VER 一律判废。
|
||||
*/
|
||||
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta);
|
||||
|
||||
/* ===== 六、整段校验与长度 / 偏移换算(读侧) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整段镜像自检 —— 头 + 数据区 + 数据 CRC 三道全过才算「这一段可读」
|
||||
* 入口参数:p_img 整段文件镜像(调用方已读进内存)
|
||||
* a_len 镜像长度
|
||||
* p_meta 输出段头(可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_SIZE 长度为 0 或与期望总长不符;
|
||||
* WAVE_ST_E_CRC 任一 CRC 不符;WAVE_ST_E_FORMAT 头本身判废 int
|
||||
* 限定条件:任意上下文;只在整段驻留(本波 5250 字节)时用。回放页不要走这条 ——
|
||||
* 用 wave_store_head_parse + wave_store_data_off + wave_store_crc_update 分段查,
|
||||
* 才符合 §7 的 256 字节读窗预算
|
||||
* 函数说明:这是「段文件 CRC 不符时脚本必须拒绝而不是画出东西」那条判据在板侧的同一份
|
||||
* 实现,也是本层与 PC 解码器逐字节同解的对照物(两套主机单测互验)。
|
||||
*/
|
||||
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:按头部字段算整段文件应有的字节数(写侧记账与读侧长度判废共用)
|
||||
* 入口参数:p_meta 段头元数据
|
||||
* 返 回 值:期望字节数(8 路 320 点 = 5250);字段非法(含零长度)返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:返回 0 就是「这一段连长度都不成立」,据此判废。不要拿 0 去开缓冲,
|
||||
* 也不要拿 0 去比对 f_write 的写回字节数(那会把失败读成成功)。
|
||||
*/
|
||||
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:整头字节数(固定头 + 通道表 + 头 CRC 两字节)
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* 返 回 值:字节数;通道数为 0 或越界返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_head_bytes(uint8_t a_ch_count);
|
||||
|
||||
/*
|
||||
* 函数功能:数据区某一点在段文件里的绝对偏移(按通道分列 → 单通道是一整段连续区)
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* a_pts_ch 每通道点数
|
||||
* a_ch 列号(= 通道表下标 = 语义号,0 到 a_ch_count-1)
|
||||
* a_pt 段内点下标(0 到 a_pts_ch-1)
|
||||
* 返 回 值:文件偏移;参数非法返回 0xFFFFFFFFu 哨兵(这个值不是任何合法偏移) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:回放页据此定位「只读那一路」,配合 wave_store_data_bytes_avail 一次连续读;
|
||||
* PC 侧同一算法,两份实现互验。
|
||||
*/
|
||||
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
|
||||
uint8_t a_ch, uint32_t a_pt);
|
||||
|
||||
/*
|
||||
* 函数功能:从某点起当前列还能连续读多少字节(不跨列、不越段尾)
|
||||
* 入口参数:a_pts_ch 每通道点数
|
||||
* a_pt 起始点下标
|
||||
* a_want 想读的点数
|
||||
* 返 回 值:可连续读字节数 = 2 × 实际可给点数;参数非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want);
|
||||
|
||||
/*
|
||||
* 函数功能:把段文件读出的小端字节解码成 int16 点序列(按时间序,旧到新)
|
||||
* 入口参数:p_raw 原始字节(来自文件,2 × 点数)
|
||||
* a_len 字节数(须为偶数,且不大于 a_max_pts × 2)
|
||||
* p_out 输出 int16 数组
|
||||
* a_max_pts 输出容量(点数)
|
||||
* p_pts 输出实际点数(可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:显式按小端逐字节拼装,不依赖宿主字节序,所以板端与 PC 侧同一判据。
|
||||
* 片内 ADC 那几路的值本就是 0..4095 的原样正数,不做中点偏置
|
||||
* (§6.3「样本编码」行:加偏置等于伪造双极性)。
|
||||
*/
|
||||
int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len,
|
||||
int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts);
|
||||
|
||||
/*
|
||||
* 函数功能:把一路的 int16 码值折成定点毫单位(回放页峰值标注用;纯整数,不用 float)
|
||||
* 入口参数:p_desc 该路通道表项(k_q16 / b_codes 来自段文件头,不来自当前固件)
|
||||
* a_code 原始码值
|
||||
* p_milli 输出四舍五入后的毫单位(k_q16 是 µV/码 → 输出单位 mV)(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文(只在回放页与单测里用,不在 1 kHz 采集链上;内部用 int64 中间量,
|
||||
* 落在这条路径上时可接受)
|
||||
* 函数说明:算式 eng = (code + b) × k × 1e-6,与 App/algo/adc_cal.c 的 adc_cal_apply()
|
||||
* 同一模型(§6.3)。输出单位是 mV;单位含义由段头 eng 字段决定(eng = 0 是
|
||||
* 引脚域,别读成二次值)。必须用段文件里的 k / b,不许拿当前固件的系数解释
|
||||
* 老文件(§12 风险表「标定落地后老文件被解释错」)。
|
||||
*/
|
||||
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
|
||||
int32_t *p_milli);
|
||||
|
||||
/* ===== 七、打包游标(分次搬运,给文件系统唯一写者) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化打包游标(段头拷进游标,随后逐次拉整段字节流)
|
||||
* 入口参数:p_ctx 游标(调用方持有,栈上或静态都行;本层不记它的地址)
|
||||
* p_meta 段头元数据(内部拷贝;搬运期间调用方改自己那份不影响本段)
|
||||
* p_reader 数据源回调;NULL = 只产头(回放页只校验头 / 主机单测只测头)
|
||||
* p_user 数据源上下文(生产传 NULL,wave_ring 是单例不需要)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_FORMAT 段头字段非法;
|
||||
* WAVE_ST_E_SIZE 算不出合法长度 int
|
||||
* 限定条件:任意上下文(纯计算,无临界区);一段一个游标,搬完再 start 下一段
|
||||
* 函数说明:p_reader 为 NULL 而 ch_count 大于 0 时,游标仍能产头,但进数据区那一步
|
||||
* 返回 WAVE_ST_E_STATE —— 不静默跳过数据区,否则调用方会写出一个
|
||||
* 「长度看着对、内容是空的」段文件(那正是 §8.1 要防的伪造证据形态)。
|
||||
*/
|
||||
int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta,
|
||||
wave_reader_fn p_reader, void *p_user);
|
||||
|
||||
/*
|
||||
* 函数功能:取整段字节流的下一片(分次搬运的唯一推进口)
|
||||
* 入口参数:p_ctx 已 start 的游标
|
||||
* p_out 输出缓冲
|
||||
* a_cap 本片容量(建议 256 字节,见 §7;数据区不切半点,奇数容量会少产 1 字节)
|
||||
* p_len 输出本片实际字节数(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK 产出本片且还有后续;WAVE_ST_DONE 整段已产完(*p_len 可为 0);
|
||||
* 负 = WAVE_ST_E_*(未 start / 容量为 0 / 数据源回调返错 / 游标状态错) int
|
||||
* 限定条件:任意上下文,但同一游标不得并发使用(文件系统唯一写者单线程推进)
|
||||
* 函数说明:头区字节流与 wave_store_head_build 完全一致(内部同一份写字段函数);
|
||||
* **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 128),否则返回
|
||||
* WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 128 字节
|
||||
* 中间态;§7 的 256 字节搬运缓冲天然满足。
|
||||
* 数据区 CRC 走增量中间值,整段最后两片是数据 CRC 的低字节、高字节。
|
||||
* 调用方每写完一片都必须比对 f_write 写回字节数,短写按失败处理(§5.2)。
|
||||
*/
|
||||
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
|
||||
uint32_t a_cap, uint32_t *p_len);
|
||||
|
||||
/*
|
||||
* 函数功能:读游标已产出字节数(= 当前文件偏移;供记账、观测与「写没写全」判定)
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:已产出字节数;p_ctx 为 NULL 返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:读游标对应整段文件的期望总长(写完与 f_size / 累计写回字节数比对)
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:期望总字节数;p_ctx 为 NULL 返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx);
|
||||
|
||||
/* ===== 八、段文件名轮转(只算名字,不碰文件系统) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:按段号生成段文件路径(严格 8.3 短名:目录 + 前缀 + 两位十进制 + 扩展名)
|
||||
* 入口参数:p_out 输出缓冲(容量须不小于 WAVE_ST_PATH_MAX)
|
||||
* a_cap 容量
|
||||
* a_idx 段号 0 到 WAVE_ST_SEG_MAX-1
|
||||
* 返 回 值:不小于 0 = 写入的字符数(不含结束符;当前布局恒为 16,故缓冲至少要 17 字节);
|
||||
* WAVE_ST_E_PARAM 空指针或容量不足;
|
||||
* WAVE_ST_E_RANGE 段号越界 int
|
||||
* 限定条件:任意上下文;纯字符拼装(不用 sprintf,见 CODE-RULE-06 系),逐字节可预期
|
||||
* 函数说明:号 0..99 轮转覆盖,共 100 个名字,写满回绕盖最老的(§5.2 第三条)。
|
||||
* 本函数不检查文件是否存在、不建目录:那些都是接线道的职责。
|
||||
*/
|
||||
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx);
|
||||
|
||||
/*
|
||||
* 函数功能:下一个轮转段号(到 99 后回 0)
|
||||
* 入口参数:a_idx 当前段号(越界输入按 0 处理,不猜)
|
||||
* 返 回 值:下一个段号 uint8_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint8_t wave_store_next_idx(uint8_t a_idx);
|
||||
|
||||
/*
|
||||
* 函数功能:从文件名或路径反解段号(掉电后靠扫目录重建段账用)
|
||||
* 入口参数:p_path 路径或纯文件名(只看最后一段,大小写都收)
|
||||
* p_idx 输出段号(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:只认「前缀 + 两位数字 + 扩展名」,其余(目录名、长名、以点开头)一律判不匹配
|
||||
* —— 扫目录重建账时不许把陌生名字当录波段;名字/参数类错误码严禁升级成卷操作
|
||||
* (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 的同一条教训)。
|
||||
*/
|
||||
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WAVE_STORE_H */
|
||||
+33
-71
@@ -17,22 +17,19 @@
|
||||
#include "stm32f4xx_it.h" /* fault_handler_enable() */
|
||||
#include "usart.h" /* huart5 */
|
||||
#include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */
|
||||
#include "adc.h" /* adc1_disable():挂 DMA 前把 ADC 关掉,让 Start_DMA 自己使能 */
|
||||
#include "rs485.h"
|
||||
#include "sb_link.h" /* 屏板链路传输层:sb_link_init()(USART1 收中断,批 1 的 1-3) */
|
||||
#include "sd2506.h"
|
||||
#include "boot_stats.h" /* 开机账本:上电次数(RTC 用户 SRAM)+ 本次复位原因快照(道G task-12) */
|
||||
#include "sys_clock.h"
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* 语义取数与有效性层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "adc_cal.h" /* 标定折算层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 标定折算与落盘整定) */
|
||||
#include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 这一项) */
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "adc_cal.h" /* L1.5 标定层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 卡2/4) */
|
||||
#include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 卡10) */
|
||||
#include "iec104_comm.h" /* [comm] 段:iec104_comm_init()(ca/端口注册,道G task-8) */
|
||||
#include "phasor.h" /* 全周傅氏相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "freq.h" /* 全周傅氏相量的派生量:freq_init()(相位解缠绕测频,B8) */
|
||||
#include "prot_set.h" /* 保护判据定值模型:prot_set_init()(B5,内核前封存) */
|
||||
#include "prot_elm.h" /* 保护判据元件框架:prot_elm_init()(B5,解析相量索引) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */
|
||||
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
|
||||
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():第 8 轮复位不丢"进段打点"上报 */
|
||||
#include "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
|
||||
@@ -79,11 +76,23 @@ volatile uint32_t g_rcc_csr_latched;
|
||||
* 兜底,两者同时置位时报告看门狗更有价值)。纯 ASCII,防编码坑。
|
||||
*/
|
||||
static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
|
||||
/* 优先级链**不在这里再写一遍**:改由 boot_stats 出一份(IWDG > WWDG > 软件 >
|
||||
* POR > PIN > BOR)。原先这条 if-else 链与页面上要显示的编码各写一份,两份早晚
|
||||
* 会漂——漂了的症状是串口说 "IWDG"、屏幕写 "software",两条却都"看着有据"。
|
||||
* 顺序本身的理由见本函数上方原注释与 App/util/boot_stats.h 的函数头。 */
|
||||
const char *cause = boot_stats_cause_str(boot_stats_cause_from_csr(csr));
|
||||
const char *cause;
|
||||
|
||||
if ((csr & RCC_CSR_IWDGRSTF) != 0u) {
|
||||
cause = "IWDG (independent watchdog)";
|
||||
} else if ((csr & RCC_CSR_WWDGRSTF) != 0u) {
|
||||
cause = "WWDG (window watchdog)";
|
||||
} else if ((csr & RCC_CSR_SFTRSTF) != 0u) {
|
||||
cause = "software (NVIC_SystemReset)";
|
||||
} else if ((csr & RCC_CSR_PORRSTF) != 0u) {
|
||||
cause = "power-on (POR)";
|
||||
} else if ((csr & RCC_CSR_PINRSTF) != 0u) {
|
||||
cause = "external pin (NRST)";
|
||||
} else if ((csr & RCC_CSR_BORRSTF) != 0u) {
|
||||
cause = "brown-out (BOR)";
|
||||
} else {
|
||||
cause = "unknown";
|
||||
}
|
||||
|
||||
/* 简单有界拼接(不引 snprintf,避免依赖与栈开销) */
|
||||
if (buf == NULL || len == 0u) {
|
||||
@@ -109,7 +118,7 @@ static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
|
||||
* 1) 时间子系统(SD2506 RTC → 墙钟基准)
|
||||
* 2) RS-485
|
||||
* 3) GD5F NAND
|
||||
* 4) TPAFE5160 ADC → ADC 输入层(语义取数与有效性,语义映射 + STM32 ADC1)
|
||||
* 4) TPAFE5160 ADC → ADC 输入层(L1,语义映射 + STM32 ADC1)
|
||||
* 5) SD2506 诊断信息
|
||||
* 6) 启动 TIM3 1 kHz 采样节拍(ISR 内脉冲 CONVST;同一更新事件经 TRGO 硬件触发 ADC1 + DMA)
|
||||
*/
|
||||
@@ -130,7 +139,7 @@ int app_main_init(void) {
|
||||
|
||||
/* 固件身份戳(2026-10-10):__DATE__/__TIME__ 由编译器展开,与前面的字面量做
|
||||
* C 字符串拼接 → .rodata 里是一段连续 ASCII,可直接在 .hex 里原样搜到。
|
||||
* 用途:/serial-boot-test 把"芯片里正在跑的镜像"和"刚编出来的镜像"对上号。此前开机标记
|
||||
* 用途:@test 把"芯片里正在跑的镜像"和"刚编出来的镜像"对上号。此前开机标记
|
||||
* 全是固定串(App init OK / Network Init / ...),旧镜像也照样全中,于是
|
||||
* "反复重下载、芯片一直是旧镜像"这类事故在自动闭环里完全不可见
|
||||
* (AGENTS.md 烧录铁律记的正是它)。代价:1 条 DBG ≈ 90 B Flash + 30 B 常量。
|
||||
@@ -178,44 +187,15 @@ int app_main_init(void) {
|
||||
bsp_err_t rtc_ret = sd2506_init();
|
||||
DBG_INFO("SD2506 init: %s (ret=%d)",
|
||||
(rtc_ret == BSP_OK) ? "OK" : "FAILED", rtc_ret);
|
||||
/* 1b) 开机账本(道G task-12):紧接 sd2506_init 之后,是**唯一**的调用点。
|
||||
* 为什么放这儿而不是 defaultTask 的存储 bring-up:
|
||||
* ① 这一步只做 1 次 I2C 读 + 至多 1 次 I2C 写,落在内核启动前——
|
||||
* 单线程、非 ISR、调度器还没跑(本函数不调任何创建类内核 API,
|
||||
* AGENTS「内核前禁调创建类 API」那条与此无关);
|
||||
* ② 保持介质是 RTC 用户 SRAM,不是文件系统:不必等 STORAGE_READY,
|
||||
* 也就不必在喂狗点上碰卷锁(这条链上根本没有卷操作可言);
|
||||
* ③ sd2506_init 失败时这里自然走"读不到"那一支:状态标成不可信、
|
||||
* 且不回写,不会把可能还完好的记录覆盖掉。
|
||||
* 失败一律不打日志(可观测性事件不进 [ERR]),状态留在 boot_stats_get()。 */
|
||||
boot_stats_init();
|
||||
}
|
||||
if (sys_clock_init() != 0) {
|
||||
syslog_post(SYSLOG_SELFTEST, "clock chip fault");
|
||||
}
|
||||
|
||||
/* 2) RS-485 初始化(UART4 + 方向脚 = ST_DIR4 宏:V0.1 板实测 PA15,V0.0 曾是 PD0,
|
||||
* 轮换记录 = docs/硬件平台/V0.1_网表详细分析报告.md 的 V0.0→V0.1 引脚迁移表),
|
||||
* 初始化即自动开启接收 */
|
||||
/* 2) RS-485 初始化(UART4 + PD0 方向控制),初始化即自动开启接收 */
|
||||
rs485_init(&g_rs485, &huart4, ST_DIR4_GPIO_Port, ST_DIR4_Pin);
|
||||
DBG_INFO("RS485 init: OK (UART4, DE=PA15)");
|
||||
|
||||
/* 2b) 屏板链路初始化(USART1,700 kbaud 8N1,IDLE 中断接收)。
|
||||
* 放在 NAND 全片扫描之前:这一步只是挂接收(µs 级),挂上之后屏板的周期召唤
|
||||
* 就已经能被收下来并计数,台架不必等启动跑完才能看到链路状态。
|
||||
* 波特率不在本调用里设——在 Src/usart.c 的 USER CODE 区(CubeMX 重生成只保护那里),
|
||||
* 本调用只做一次交叉核对,对不上会报 BSP_ERR_STATE 并打 ERR 行。 */
|
||||
{
|
||||
bsp_err_t sb_ret = sb_link_init(&huart1);
|
||||
/* BRR 直接读出来打:1-2 的判据是"实测位宽误差 < 1 %",而 BRR 是位宽的唯一决定量——
|
||||
* 84 MHz / (16 x 700 kHz) = 7.5 → 期望 0x0078(整数 7、小数 0.5x16 = 8),误差 0 %。
|
||||
* 有这一行就不必为了核波特率去架示波器:BRR 对不上就说明 USER CODE 覆盖没生效。 */
|
||||
DBG_INFO("ScreenLink init: %s (ret=%d, USART1 %lu baud, BRR=0x%04lX)",
|
||||
(sb_ret == BSP_OK) ? "OK" : "FAILED",
|
||||
sb_ret, (unsigned long)SB_LINK_BAUD,
|
||||
(unsigned long)huart1.Instance->BRR);
|
||||
}
|
||||
|
||||
/* 3) GD5F 初始化测试(BBT 全片扫描,启动段最耗时的一步) */
|
||||
int ret = gd5f2gq5ue_init();
|
||||
DBG_INFO("NAND init: %s",
|
||||
@@ -240,25 +220,7 @@ int app_main_init(void) {
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC fault");
|
||||
}
|
||||
|
||||
/* 4b-pre) 挂 DMA 之前**先把 ADC 关掉**,让使能动作由 HAL_ADC_Start_DMA 内部完成
|
||||
* (它自己 __HAL_ADC_ENABLE + 等 Tstab ~20 µs,然后紧接着开 CR2.DMA 并启流)。
|
||||
*
|
||||
* 为什么必须这样做:MX_ADC1_Init() 在 main() 里就调用了,而挂 DMA 在这里,
|
||||
* 中间隔着 GD5F 全片 BBT 扫描(数秒)。台架实测(同一次开机内用无 DMA 单通道
|
||||
* 探针取真值对照,9/9 完美置换):**ADC 使能后闲置数秒才挂 DMA,DMA 乒乓缓冲
|
||||
* 会整体 +2 格旋转** —— 槽 i 装的是 rank ((i+1) mod 9)+1,9 路通道按 rank 序
|
||||
* 整体错开 2 位。表象是"接地 AIN2、UB 却读到别的通道的值",极易被误判成
|
||||
* 映射表写错或夹子位置不对。
|
||||
* 把使能与挂 DMA 压到同一时刻后旋转消失(台架 4/4 次,同开机对照差值 ≤4 码)。
|
||||
* 证据与复现方法见 docs/子系统设计/ADC采样节拍设计说明.md §4.5。
|
||||
*
|
||||
* ⚠ 顺序不可调换:本调用必须严格早于下面的 adc_input_init()。
|
||||
* 若 ADC 在本步之后仍处于使能态,旋转会重新出现。 */
|
||||
if (adc1_disable() != 0) {
|
||||
DBG_ERROR("ADC1 disable before DMA arm: TIMEOUT");
|
||||
}
|
||||
|
||||
/* 4b) ADC 输入层(语义取数与有效性)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
|
||||
/* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
|
||||
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)
|
||||
* + 挂载一次 STM32 ADC1 的 DMA(失败返回 -2)。
|
||||
* ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
|
||||
@@ -278,21 +240,21 @@ int app_main_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 4c) 相量层(全周傅氏相量)初始化:清双区结果与有效标志 + 复位采样环缓冲。
|
||||
/* 4c) 相量层(L2)初始化:清双区结果与有效标志 + 复位采样环缓冲。
|
||||
* 纯算法层(无外设 / 无 RTOS 依赖,见 docs/算法/继电保护功能复现规划.md
|
||||
* §3.2),须在启动采样节拍(步骤 6)之前。幂等、当前无失败路径
|
||||
* (返回值保留供后续自检扩展)。 */
|
||||
(void)phasor_init();
|
||||
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
|
||||
|
||||
/* 4c-2) 测频层(全周傅氏相量的派生量,B8)初始化:清平均窗/相位基准/有效标志。
|
||||
/* 4c-2) 测频层(L2 派生量,B8)初始化:清平均窗/相位基准/有效标志。
|
||||
* 纯算法(输入 = phasor 使用区),消费在 adcTask(freq_run 每拍)
|
||||
* 与 protectTask(OF/UF 判据经 prot_elm_run 取数)。 */
|
||||
(void)freq_init();
|
||||
DBG_INFO("freq layer init: OK");
|
||||
|
||||
/* 4c-3) 通道标定层(标定折算,「数据可信」标定折算与落盘整定)初始化:RAM 系数副本置
|
||||
* ROM 默认(量程口径冻结定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1)
|
||||
/* 4c-3) 通道标定层(L1.5,「数据可信」卡 2/卡 4)初始化:RAM 系数副本置
|
||||
* ROM 默认(卡 1 定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1)
|
||||
* + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] 两段。注册必须先于
|
||||
* defaultTask 的 cfg_file_load(「先注册后 load」契约)→ 只能落内核前;
|
||||
* 折算消费在 adcTask 每拍(adc_cal_apply),标定文件值随 load/poll 原地
|
||||
@@ -301,7 +263,7 @@ int app_main_init(void) {
|
||||
DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld)",
|
||||
(long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32);
|
||||
|
||||
/* 4c2) 运行参数段初始化(「数据可信」这一项):死区 9 点工厂默认 + 注册
|
||||
/* 4c2) 运行参数段初始化(「数据可信」卡 10):死区 9 点工厂默认 + 注册
|
||||
* [run_par] 段。同 adc_cal 的「先注册后 load」契约 → 必须内核前;
|
||||
* 104 消费(COV 实时读 / 0x8020 段读写映射)在 iec104Task 启动后进行。 */
|
||||
run_par_init();
|
||||
|
||||
@@ -22,12 +22,11 @@ extern "C" {
|
||||
* 由 MX_FREERTOS_Init 在 freertos.c 的 generated 区域自动声明并 osEventFlagsNew 创建) */
|
||||
extern osEventFlagsId_t sys_evtHandle;
|
||||
|
||||
/* 各子系统"就绪"事件位(32 位上限内的 bit 0~30)。**bit0 / bit1 在用,bit2 / bit3 是预留位**
|
||||
* ——逐位的生产者与消费者对照表 = docs/架构设计/项目启动顺序管理.md §3.2 */
|
||||
/* 各子系统"就绪"事件位(32 位上限内的 bit 0~30) */
|
||||
#define SYS_EVT_NET_READY (1u << 0) /* netTask:net_init + PHY 稳定完成 */
|
||||
#define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */
|
||||
#define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成后置位;暂无消费者(预留) */
|
||||
#define SYS_EVT_TIMESYNC_READY (1u << 3) /* 预留:timeSyncTask 在跑但不置本位,生产者消费者都没有 */
|
||||
#define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成 */
|
||||
#define SYS_EVT_TIMESYNC_READY (1u << 3) /* timeSyncTask:首次时间同步完成 */
|
||||
#define SYS_EVT_NET_RECONFIG (1u << 4) /* UI:请求立即应用新网口配置(netTask 消费后重建网络) */
|
||||
#define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */
|
||||
#define SYS_EVT_CONSOLE_TX (1u << 6) /* 控制台单写者:生产者入环形缓冲后置位,defaultTask 消费并发送
|
||||
@@ -35,7 +34,7 @@ extern osEventFlagsId_t sys_evtHandle;
|
||||
#define SYS_EVT_PROT_SET_SAVE (1u << 7) /* 保护定值落盘请求(B7):iec104Task 检测到定值脏置位,
|
||||
* defaultTask(配置引擎属主)消费并写 /prot_set.bin
|
||||
* (消费型命令位;写盘失败脏标志保留 → 下轮再通知重试) */
|
||||
#define SYS_EVT_RUN_PAR_SAVE (1u << 8) /* 运行参数段落盘请求(「数据可信」这一项):iec104Task
|
||||
#define SYS_EVT_RUN_PAR_SAVE (1u << 8) /* 运行参数段落盘请求(「数据可信」卡 10):iec104Task
|
||||
* 检测到 run_par 脏(死区 9 键 / 系数覆盖联动 [adc_cal])
|
||||
* 置位,defaultTask 消费并 cfg_file_save 整档外科保存
|
||||
* (消费型命令位;失败脏保留 → 下轮重试,B7 同款) */
|
||||
|
||||
+7
-246
@@ -24,14 +24,6 @@
|
||||
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
|
||||
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
|
||||
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
|
||||
* v1.3 2026-10-27 DTU 项目组 道F task-7:遥测整定注入表(ui_data_yt_ops_t)
|
||||
* v1.4 2026-10-27 DTU 项目组 道G task-9:通讯参数注入表(ui_data_comm_ops_t,
|
||||
* 通讯设置页);port_104 快照从 IEC104_SERV_PORT 宏
|
||||
* 切 iec104_comm_port() 真源(宏同笔删除,交接口径见
|
||||
* IEC104上行设计说明.md §8)
|
||||
* 装配层实现 + 首轮懒绑(时序与通道四绑定等价,
|
||||
* 见 feed_yt_bind_once 函数头);行数等值由编译期
|
||||
* 守卫钉,量纲分档与合法域分档同源同序
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-16:删除本地镜像 feed_cal_block(),
|
||||
* 两个调用点(填帧 / 门控比较)改调
|
||||
* adc_cal_prot_blocked()——同一份三路判定此前在
|
||||
@@ -45,8 +37,6 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */
|
||||
#include "boot_stats.h" /* 运行报告:上电次数 + 本次复位原因快照(道G task-12 数据面) */
|
||||
#include "iec104_serv.h" /* 运行报告:四个 104 只读观测计数(本次开机以来,跨重连不清零) */
|
||||
|
||||
#include "ui_data_feed.h"
|
||||
|
||||
@@ -65,14 +55,13 @@
|
||||
|
||||
/* 同层(App)数据源 */
|
||||
#include "adc_input.h" /* ADC_SEM_COUNT / adc_input_raw(TEMP 码值) */
|
||||
#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性;adc_cal_prot_blocked:未标定闭锁单点;adc_cal_k_uv:系数当前生效值(与 104 回显同视图) */
|
||||
#include "run_par.h" /* 道F task-7 遥测整定注入表:死区/合法域/系数 setter/脏位(真源全在本模块与 adc_cal,feed 只转达) */
|
||||
#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性;adc_cal_prot_blocked:未标定闭锁单点 */
|
||||
#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */
|
||||
#include "freq.h" /* 测频读数与新鲜度 */
|
||||
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
|
||||
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
|
||||
#include "sys_clock.h" /* 墙钟字符串 */
|
||||
#include "iec104_comm.h" /* 道G task-9:[comm] 真源 getter/setter/脏位;快照端口切 getter,IEC104_SERV_PORT 字面宏同笔删除 */
|
||||
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
|
||||
#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
|
||||
|
||||
/* 私有宏定义区 */
|
||||
@@ -146,8 +135,8 @@ static uint8_t s_map_done;
|
||||
/* 发布节拍与参数变化检测状态
|
||||
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
|
||||
* s_par_sum 定值改动(校验和)
|
||||
* s_par_locked 定值闭锁(定值校验和自检失配,校验和不动)
|
||||
* s_par_cal_block 未标定闭锁(两点标定完成,校验和也不动)
|
||||
* s_par_locked 定值闭锁(L5-1 自检失配,校验和不动)
|
||||
* s_par_cal_block 未标定闭锁(卡 12 标定完成,校验和也不动)
|
||||
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
|
||||
static uint32_t s_last_rt_ms;
|
||||
static uint32_t s_par_sum;
|
||||
@@ -470,7 +459,7 @@ static int feed_param_check(uint8_t a_id, int32_t a_q10) {
|
||||
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
|
||||
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
|
||||
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
|
||||
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被定值校验和自检判成
|
||||
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被 L5-1 自检判成
|
||||
* "定值被非法改写"→ 整台保护闭锁
|
||||
*/
|
||||
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
|
||||
@@ -487,206 +476,6 @@ static const ui_data_param_ops_t s_param_ops = {
|
||||
feed_param_apply
|
||||
};
|
||||
|
||||
/*
|
||||
* ======================= 道F task-7:遥测整定注入表(ui_data_yt_ops_t)装配
|
||||
*
|
||||
* 两页唯一的执行侧转达处。四条装配纪律:
|
||||
* ① 数值与合法域**都**现给执行侧(run_par_deadband / run_par_db_range /
|
||||
* adc_cal_k_uv / RUN_PAR_K_MAX_UV)——本文件不出现任何范围数字字面量;
|
||||
* 系数真带(µV/码执行段裁)没有公开访问器,也**不在此补一份**(task-43 收口),
|
||||
* 真带拒的值经 set 原样返 -1,页面反显 + 保存行 ERR,两层守卫互不掩盖。
|
||||
* ② 写走语义 setter(写值与置脏同一函数内发生,run_par_cal_k_set 文件头契约);
|
||||
* 本层不存在"只置脏不写值"或"只写值不置脏"的组合,UI 永远够不到裸脏位。
|
||||
* ③ 跨任务写口径:真机默认档 = iec104Task 的 C_SE;本文件的调用在 uiTask,
|
||||
* 这**正是** Lead 裁定(task-7 派发②)批准的"装配层转达"整定路径——与 104
|
||||
* 走同一批 setter、同一 int32 单字原子契约,不新开工;run_par.h 里死区旧
|
||||
* 接口"iec104Task 上下文"的限定条件语写于该裁定之前,语义已被裁定覆盖
|
||||
* (系数两支的限定条件已明写装配层入口,措辞同步归算法层下一笔)。
|
||||
* ④ 落盘不靠本表:写成功置脏 → defaultTask 每轮无条件 run_par_store_save()
|
||||
* (查实:唯一 cfg_file_save 调用者,首行"无脏立即返回";104 的 peek→事件位
|
||||
* 只是加速器不是必经链)——所以"OK = 盘上真落了"在真机与模拟器(pump 清脏)
|
||||
* 两端口径一致,黄金参考拍的 WAIT/OK 不是表演。
|
||||
*/
|
||||
|
||||
/* 跨层行数等值守卫:BSP 侧契约值 ↔ 执行侧真值。本文件是全仓第一处两份头
|
||||
* 同时可见的编译单元——BSP 物理上无法自证(看不到 App 头),守卫落这里 */
|
||||
typedef char feed_yt_db_n_check_t[(UI_DATA_YT_DB_N == (uint8_t)RUN_PAR_DB_N) ? 1 : -1];
|
||||
typedef char feed_yt_ch_n_check_t[(UI_DATA_YT_CH_N == (uint8_t)RUN_PAR_CH_N) ? 1 : -1];
|
||||
|
||||
/*
|
||||
* 函数功能:死区点数(执行侧真值直通)
|
||||
* 入口参数:无
|
||||
* 返回值:RUN_PAR_DB_N uint8_t
|
||||
* 限定条件:uiTask(注入表回调)
|
||||
* 函数说明:行数从这一头出、BSP 契约常量在文件头编译期守卫对表——两边不等
|
||||
* 编不过,等值不靠注释维持
|
||||
*/
|
||||
static uint8_t feed_yt_db_n(void) {
|
||||
return (uint8_t)RUN_PAR_DB_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 行死区现状(值 + 粗守域现给;量纲按点型分档)
|
||||
* 入口参数:idx - 0..8(0..7 电气语义通道序,8 频率) uint8_t
|
||||
* a_out - 行现状 ui_data_yt_row_t *
|
||||
* 返回值:0 - 已写出;-1 - 越界 / 指针空(run_par_db_range 自判,本层不重复判) int
|
||||
* 函数说明:unit 的分档(前 4 电压、中 4 电流、末 1 频率)**必须**与
|
||||
* run_par_db_range 的档位分档同序——两处真源都没导出量纲,唯一防线是
|
||||
* run_par.h 地址映射表(0x8030~0x8038 = UA..IN/FREQ)与本表同序这条
|
||||
* 写死的对表纪律 + 黄金像素(档位漂了屏上会呈现"0~50"配 "A" 的肉眼矛盾);
|
||||
* decimals 取 3 = 死区在引擎里的存储分辨率(毫单位 int32,换算单点
|
||||
* s_milli_to_eng 在 run_par.c,本层不重除)
|
||||
*/
|
||||
static int feed_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
|
||||
float lo;
|
||||
float hi;
|
||||
|
||||
if (run_par_db_range(a_idx, &lo, &hi) != 0) {
|
||||
return -1;
|
||||
}
|
||||
a_out->val = run_par_deadband(a_idx);
|
||||
a_out->lo = lo;
|
||||
a_out->hi = hi;
|
||||
a_out->decimals = 3u;
|
||||
a_out->unit = (a_idx < 4u) ? UI_DATA_YT_UNIT_V
|
||||
: ((a_idx < 8u) ? UI_DATA_YT_UNIT_A : UI_DATA_YT_UNIT_HZ);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 函数功能:写第 idx 行死区(直通语义 setter;返回值原样上抛,页面据此记账) int */
|
||||
static int feed_yt_db_set(uint8_t a_idx, float a_eng) {
|
||||
return run_par_deadband_set(a_idx, a_eng);
|
||||
}
|
||||
|
||||
static uint8_t feed_yt_cal_n(void) {
|
||||
return (uint8_t)RUN_PAR_CH_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 路系数现状(k = adc_cal **当前生效值**,µV/码)
|
||||
* 返回值:0 - 已写出;-1 - idx 越界或指针空 int
|
||||
* 函数说明:显示值与 104 C_RD 回显同视图(run_par_104_read 用的就是 adc_cal_k_uv:
|
||||
* 文件坏值被真带回退 ROM 时,屏与主站看到同一份 ROM——"页面值按比例
|
||||
* 变化"判据因此量的就是执行段真正吃的数);
|
||||
* lo/hi 只有粗守一层(0..RUN_PAR_K_MAX_UV,上限来自 run_par 唯一出口),
|
||||
* 真带在执行段裁——两层守卫的注释纪律见 ui_w_ytview 头注;
|
||||
* decimals 取 0 是**显示域容量约束**不是精度偏好:粗守上限 1e9 在
|
||||
* ui_w_setlist 的 int32 显示域里只够零位小数(带 2 位则上限装不下),
|
||||
* 屏上输入按整 µV 量化、写口仍收 float;要小数位得收紧粗守或改组件
|
||||
* 值通道,都不在本批,也不许在此抄一份更小的假上限
|
||||
*/
|
||||
static int feed_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
|
||||
if ((a_out == NULL) || (a_idx >= (uint8_t)RUN_PAR_CH_N)) {
|
||||
return -1;
|
||||
}
|
||||
a_out->val = adc_cal_k_uv((adc_semantic_t)a_idx);
|
||||
a_out->lo = 0.0f; /* 下限 0 由写口拒 k<=0 兜住,这里只给输入框边界 */
|
||||
a_out->hi = RUN_PAR_K_MAX_UV;
|
||||
a_out->decimals = 0u;
|
||||
a_out->unit = UI_DATA_YT_UNIT_UV;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 函数功能:写第 idx 路系数 k(越界防御后再进语义 setter——setter 形参是枚举,
|
||||
* 编译器帮不上越界忙;返回码直通) int */
|
||||
static int feed_yt_cal_k_set(uint8_t a_idx, float a_uv) {
|
||||
if (a_idx >= (uint8_t)RUN_PAR_CH_N) {
|
||||
return -1;
|
||||
}
|
||||
return run_par_cal_k_set((adc_semantic_t)a_idx, a_uv);
|
||||
}
|
||||
|
||||
/* 函数功能:未标定闭锁徽标源(task-16 单点访问器,与判据停走同一个判定) uint8_t */
|
||||
static uint8_t feed_yt_cal_blocked(void) {
|
||||
return (adc_cal_prot_blocked() != 0) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/* 函数功能:整档落盘脏位(保存行三态判据;与 104 留痕同一个 peek,不另发明) uint8_t */
|
||||
static uint8_t feed_yt_pending(void) {
|
||||
return run_par_dirty_peek();
|
||||
}
|
||||
|
||||
/* 遥测整定注入表(八件齐全;ui_data_yt_bind 拒残表,缺件就是装配错) */
|
||||
static const ui_data_yt_ops_t s_yt_ops = {
|
||||
feed_yt_db_n,
|
||||
feed_yt_db_row,
|
||||
feed_yt_db_set,
|
||||
feed_yt_cal_n,
|
||||
feed_yt_cal_row,
|
||||
feed_yt_cal_k_set,
|
||||
feed_yt_cal_blocked,
|
||||
feed_yt_pending
|
||||
};
|
||||
|
||||
/* ======================= 道G task-9:通讯参数注入表(ui_data_comm_ops_t)===
|
||||
*
|
||||
* 页面形状(两行的名/序)归 ui_page_comm.c;本层只转达值与合法带——
|
||||
* lo/hi 现取 iec104_comm.h 带宏(唯一出口),值现取 getter(带内自愈),
|
||||
* 装配层**不写第二份数值**(通道五同一条纪律)。
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 行通讯参数现状(值 + 合法带)
|
||||
* 入口参数:idx - UI_DATA_COMM_ROW_CA/_PORT uint8_t
|
||||
* a_out - 行现状 ui_data_comm_row_t *
|
||||
* 返回值:0 - 已整份写出;-1 - idx 越界(不猜、不顶替) int
|
||||
* 限定条件:uiTask 页面绘制链(经 ui_data 通道六转发)
|
||||
*/
|
||||
static int feed_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) {
|
||||
if (a_idx == UI_DATA_COMM_ROW_CA) {
|
||||
a_out->val = iec104_comm_ca();
|
||||
a_out->lo = IEC104_COMM_CA_MIN;
|
||||
a_out->hi = IEC104_COMM_CA_MAX;
|
||||
return 0;
|
||||
}
|
||||
if (a_idx == UI_DATA_COMM_ROW_PORT) {
|
||||
a_out->val = iec104_comm_port();
|
||||
a_out->lo = IEC104_COMM_PORT_MIN;
|
||||
a_out->hi = IEC104_COMM_PORT_MAX;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写第 idx 行通讯参数(守卫在 setter:带外拒、副本不动、不置脏)
|
||||
* 返回值:0 - 已写并置脏;-1 - idx 越界或带外拒 int
|
||||
* 限定条件:uiTask 按键链(编辑提交)
|
||||
* 函数说明:a_val > 65535 不在此先裁——截断成 u16 会把 65536 洗成 0 再被
|
||||
* setter 合法拒掉,等于替屏幕编了个假值;越界一律原样送 setter 拒
|
||||
*/
|
||||
static int feed_comm_set(uint8_t a_idx, uint32_t a_val) {
|
||||
if (a_val > 65535uL) {
|
||||
return -1;
|
||||
}
|
||||
if (a_idx == UI_DATA_COMM_ROW_CA) {
|
||||
return iec104_comm_ca_set((uint16_t)a_val);
|
||||
}
|
||||
if (a_idx == UI_DATA_COMM_ROW_PORT) {
|
||||
return iec104_comm_port_set((uint16_t)a_val);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 函数功能:[comm] 段落盘脏位(与页面 WAIT/OK 同一枚 peek,不另发明) uint8_t */
|
||||
static uint8_t feed_comm_pending(void) {
|
||||
return iec104_comm_dirty_peek();
|
||||
}
|
||||
|
||||
/* 通讯参数注入表(三件齐全;ui_data_comm_bind 拒残表) */
|
||||
static const ui_data_comm_ops_t s_comm_ops = {
|
||||
feed_comm_row,
|
||||
feed_comm_set,
|
||||
feed_comm_pending
|
||||
};
|
||||
|
||||
/* 首轮懒绑一次:ui_data_feed_tick() 的首个调用点在 App/task/ui_task.c 里先于
|
||||
* ui_menu_init()(首帧发布那次),与通道四 ui_data_maint_bind 的绑定时序等价;
|
||||
* 不在 ui_task.c 直接绑是为了本批写域收在派发单列出的文件里。残表是真故障,
|
||||
* 必须能在启动日志里看见,不能静默(通道四同口径) */
|
||||
static uint8_t s_yt_bound;
|
||||
static uint8_t s_comm_bound; /* 道G task-9 通讯参数表懒绑门(同款先置位防刷日志) */
|
||||
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁)
|
||||
@@ -734,12 +523,10 @@ static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
|
||||
*/
|
||||
static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
|
||||
const ftp_cfg_t *p_ftp = ftp_cfg_get();
|
||||
boot_stats_t boot;
|
||||
iec104_serv_stats_t st104;
|
||||
|
||||
p_snap->valid = 1u;
|
||||
p_snap->uptime_s = tick_ms / 1000u;
|
||||
p_snap->port_104 = iec104_comm_port(); /* 真源 = [comm] RAM 副本(getter 带内恒成立,u16 读原子) */
|
||||
p_snap->port_104 = (uint16_t)IEC104_SERV_PORT;
|
||||
p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT;
|
||||
feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL);
|
||||
feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER);
|
||||
@@ -751,19 +538,6 @@ static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
|
||||
} else {
|
||||
p_snap->ftp_user[0] = '\0';
|
||||
}
|
||||
/* 运行报告四数(2026-10-28 UI 补齐):纯搬运,页面不得自己去 include 这两个头。
|
||||
* uptime_s 仍由本函数用 tick 差算——boot_stats 不提供时长,别在那里再算一份。 */
|
||||
boot_stats_get(&boot);
|
||||
iec104_serv_get_stats(&st104);
|
||||
p_snap->boot_count = boot.boot_count;
|
||||
p_snap->boot_seq_state = boot.seq_state;
|
||||
p_snap->boot_write_failed = boot.write_failed;
|
||||
p_snap->rst_cause = boot.rst_cause;
|
||||
p_snap->boot_ready = boot.ready;
|
||||
p_snap->iec104_conn_up = st104.conn_up;
|
||||
p_snap->iec104_apdu_tx = st104.apdu_tx;
|
||||
p_snap->iec104_tx_fail = st104.tx_fail;
|
||||
p_snap->iec104_rx_bytes = st104.rx_bytes;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -784,19 +558,6 @@ void ui_data_feed_tick(void) {
|
||||
uint32_t sum;
|
||||
uint8_t due_rt;
|
||||
|
||||
if (s_yt_bound == 0u) {
|
||||
s_yt_bound = 1u; /* 先置位再绑:失败也不每轮重试刷日志(90 B/条) */
|
||||
if (ui_data_yt_bind(&s_yt_ops) != 0) {
|
||||
DBG_ERROR("UI yt bind: FAIL");
|
||||
}
|
||||
}
|
||||
if (s_comm_bound == 0u) {
|
||||
s_comm_bound = 1u; /* 道G task-9:同款一轮一绑;两表时序等价(先于 ui_menu_init) */
|
||||
if (ui_data_comm_bind(&s_comm_ops) != 0) {
|
||||
DBG_ERROR("UI comm bind: FAIL");
|
||||
}
|
||||
}
|
||||
|
||||
now = osKernelGetTickCount();
|
||||
|
||||
if (s_map_done == 0u) {
|
||||
@@ -834,7 +595,7 @@ void ui_data_feed_tick(void) {
|
||||
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
|
||||
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
|
||||
* 而整定页保存行要显示 LOCK 并禁编辑;
|
||||
* · cal_block(未标定闭锁):两点标定写完 [adc_cal] 的 valid 就翻转,
|
||||
* · cal_block(未标定闭锁):卡 12 两点标定写完 [adc_cal] 的 valid 就翻转,
|
||||
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
|
||||
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
|
||||
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| `cfg_file.c/h` | **配置引擎核心**:解析 / 查询 / 保存 / 2s 周期 `cfg_file_poll` |
|
||||
| `cfg_file_store.h` | **存储后端抽象接口**(MCU=FreeRTOS 堆 / 模拟器=宿主堆,共享核心零分配符号) |
|
||||
| `cfg_file_store_mcu.c` | **MCU 端后端实现**:`/config.ini`(临时名由目标路径同目录推导 + `f_rename` 原子写) |
|
||||
| `prot_set_store.c/h` | **保护定值持久化胶水**:`/prot_set.bin` blob 读写(blob 格式/校验/原子装载在 `App/algo/prot_set.c` 纯逻辑层);装载 = storage bring-up(屏障前),落盘 = defaultTask 消费 `SYS_EVT_PROT_SET_SAVE` |
|
||||
| `prot_set_store.c/h` | **保护定值持久化胶水(B7)**:`/prot_set.bin` blob 读写(blob 格式/校验/原子装载在 `App/algo/prot_set.c` 纯逻辑层);装载 = storage bring-up(屏障前),落盘 = defaultTask 消费 `SYS_EVT_PROT_SET_SAVE` |
|
||||
|
||||
## 设计要点
|
||||
|
||||
|
||||
+4
-4
@@ -25,7 +25,7 @@
|
||||
* s_gen_document 两处 `off += snprintf(...)` 改检查式(截断即
|
||||
* 返回 -1,不再跳过缓冲末尾);注册拒超长键名(≤ 15 字符),
|
||||
* 使每行 112 字节余量成为可编译期验证的前提
|
||||
* v1.5 2026-10-25 DTU 项目组 「数据可信」有符号零点类型:新增值类型 CFG_VAL_INT(有符号十进制,
|
||||
* v1.5 2026-10-25 DTU 项目组 数据可信卡 3:新增值类型 CFG_VAL_INT(有符号十进制,
|
||||
* len 1/2/4 小端补码)——s_decode_int() 用 strtol + ERANGE +
|
||||
* 宽度上下界校验,s_encode() 带负号输出;负零点偏移从此可
|
||||
* 干净落盘(动因见 docs/任务规划/数据可信.md §4.4 与 §7 决策 3)
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
#include <errno.h> /* ERANGE:strtol 饱和判别(INT 键解码需要,「数据可信」有符号零点类型) */
|
||||
#include <errno.h> /* ERANGE:strtol 饱和判别(INT 键解码需要,数据可信卡 3) */
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
@@ -358,7 +358,7 @@ static int s_decode_uint(uint8_t *p_dst, size_t len, const char *p_val) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析有符号十进制值(小端补码写入,「数据可信」有符号零点类型)
|
||||
* 函数功能:解析有符号十进制值(小端补码写入,数据可信卡 3)
|
||||
* 入口参数:p_dst - 目标字段 uint8_t *
|
||||
* len - 字段字节数(1/2/4) size_t
|
||||
* p_val - 值文本(可带 +/- 号) const char *
|
||||
@@ -828,7 +828,7 @@ static int s_surgical(char *p_in, size_t in_len, char *p_out, size_t out_cap, si
|
||||
char v[80];
|
||||
/* s_encode 成功返回文本长度(≥1),失败返回 -1:
|
||||
* 故判成功必须 `< 0` 取反,不能写 `== 0`(恒假,
|
||||
* 使补写路径沦为死码,见 2026-10-02 主机侧纯逻辑单测修复) */
|
||||
* 使补写路径沦为死码,见 2026-10-02 L0.5 修复) */
|
||||
if (s_encode(p_key, (const uint8_t *)s_sections[idx].p_data + p_key->offset,
|
||||
v, sizeof(v)) >= 0) {
|
||||
if (s_emit_kv(p_out, out_cap, &off, p_key->name, v) != 0) {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* 超长;超长处置补自检留痕
|
||||
* v1.3 2026-10-15 王建锋 新增公开常量
|
||||
* CFG_FILE_MAX_NAME_LEN(段名/键名上限 15 字符)
|
||||
* v1.4 2026-10-25 DTU 项目组 「数据可信」有符号零点类型:新增 CFG_VAL_INT 有符号十进制值类型
|
||||
* v1.4 2026-10-25 DTU 项目组 数据可信卡 3:新增 CFG_VAL_INT 有符号十进制值类型
|
||||
* (len = 1/2/4,小端补码)——标定零点偏移等负值需有符号
|
||||
* 落盘,动因与取舍见 docs/任务规划/数据可信.md §4.4
|
||||
*/
|
||||
|
||||
@@ -52,7 +52,7 @@ static uint32_t s_ld32(const uint8_t *p) {
|
||||
* -1 损坏被拒
|
||||
* 函数说明:读失败与"文件不存在"同归 1——启动期卷锁无竞争(本任务独占
|
||||
* bring-up),try-lock 失败即真 I/O 故障,此时保持出厂默认并让
|
||||
* 调用方按"无档"处理是安全侧(定值仍封存自洽,定值校验和不误锁)。
|
||||
* 调用方按"无档"处理是安全侧(定值仍封存自洽,L5-1 不误锁)。
|
||||
* **换代与损坏分开**(B8):magic 完好但 ver/cnt 与当前固件不符 =
|
||||
* 定值表升级(如 B8 32→40 项),是**预期升级路径**不是损坏 →
|
||||
* 归 2 并打 INFO("version change"措辞),调用方不得按 CORRUPT
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* ⚠ 边界(勿当成掉电保护):本板无掉电检测硬件,"掉电前强制 flush"
|
||||
* 不可行;"单次写事务进行中掉电"的丢失边界也不因本模块改变。
|
||||
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2);仅 MCU 端,
|
||||
* 主机侧纯逻辑单测 test/host/vol_idle_sync/
|
||||
* 主机侧 L0.5 单测 test/host/vol_idle_sync/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道F 批 2:写回缓存空闲 flush 收口)
|
||||
* 修改记录:
|
||||
|
||||
+9
-10
@@ -6,8 +6,7 @@
|
||||
* 创建日期:2026-10-02
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-02 王建锋 创建
|
||||
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 新增 4 个观测计数(持锁 / try 超时 /
|
||||
* 分片退让 / 零互斥 四态,
|
||||
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 新增 4 个观测计数(T-a/T2/T3/T4,
|
||||
* 共 16 B 静态)与 vol_lock_get_stats() 只读口 —— 不取锁、
|
||||
* 零外部调用、零局部变量,13 处调用点一字未动;
|
||||
* ② 修 :38「返回值」注释与代码相反的错(fail-open 原被
|
||||
@@ -35,10 +34,10 @@ static osMutexId_t s_lock; /* 卷串行化互斥锁(vol_lock_init 创建)
|
||||
* 并发:多个任务可同时自增同一个计数,竞态下少计不会多计(判据是"是否 > 0 / 是否还
|
||||
* 在增长",不是精确值),所以不为此上临界区 —— 上临界区反倒把观测做成了故障本身。
|
||||
* volatile:跨任务读写且无锁保护,禁止把读侧重复读优化掉。 */
|
||||
static volatile uint32_t s_st_acquired; /* 持锁态:真取到互斥(四态定义见 vol_lock.h) */
|
||||
static volatile uint32_t s_st_try_timeout; /* try 超时态:timeout=0 没取到,卷被别人占住 */
|
||||
static volatile uint32_t s_st_blk_timeout; /* 分片退让态:分片等待超时,ftpTask 正常退让 */
|
||||
static volatile uint32_t s_st_failopen; /* 零互斥态:锁不存在却返 1,谎报成功那一态 */
|
||||
static volatile uint32_t s_st_acquired; /* T-a 真取到互斥 */
|
||||
static volatile uint32_t s_st_try_timeout; /* T2 try(timeout=0)失败:卷被别人占住 */
|
||||
static volatile uint32_t s_st_blk_timeout; /* T3 分片等待超时:ftpTask 正常退让 */
|
||||
static volatile uint32_t s_st_failopen; /* T4 锁不存在却返 1:谎报成功那一态 */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
@@ -74,18 +73,18 @@ int vol_lock_wait(uint32_t timeout_ms) {
|
||||
if (s_lock == NULL) {
|
||||
(void)vol_lock_init();
|
||||
if (s_lock == NULL) {
|
||||
s_st_failopen++; /* 零互斥态:返 1 但零互斥 —— 谎报成功那一态 */
|
||||
s_st_failopen++; /* T4:返 1 但零互斥 —— 谎报成功那一态 */
|
||||
return 1; /* 锁创建失败:fail-open(不阻塞,保持启动段可用) */
|
||||
}
|
||||
}
|
||||
if (osMutexWait(s_lock, timeout_ms) == osOK) {
|
||||
s_st_acquired++; /* 持锁态:真持锁 */
|
||||
s_st_acquired++; /* T-a:真持锁 */
|
||||
return 1;
|
||||
}
|
||||
if (timeout_ms == 0u) {
|
||||
s_st_try_timeout++; /* try 超时态:压根没等(喂狗点/绘制路径只有这一态会发生) */
|
||||
s_st_try_timeout++; /* T2:压根没等(喂狗点/绘制路径只有这一态会发生) */
|
||||
} else {
|
||||
s_st_blk_timeout++; /* 分片退让态:等了一会儿没等到,与 try 超时态语义不同 */
|
||||
s_st_blk_timeout++; /* T3:等了一会儿没等到(分片退让),语义与 T2 不同 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+7
-8
@@ -72,14 +72,13 @@ int vol_lock_wait(uint32_t timeout_ms);
|
||||
|
||||
/*
|
||||
* 卷锁观测计数(道G task-17「卷锁 5 态可观测性」)。四个字段就是四态,语义**不得**合并:
|
||||
* `acquired` 持锁态 = 真取到互斥的次数
|
||||
* `try_timeout` try 超时态 = timeout=0 的 try 失败次数(卷被别的任务占住;
|
||||
* defaultTask/uiTask 的喂狗与绘制路径只有这一态会发生)
|
||||
* `blk_timeout` 分片退让态 = timeout>0 的分片等待超时次数(ftpTask 100 ms 分片正常
|
||||
* 退让;与 try 超时态语义不同:那是"等了一会儿没等到",
|
||||
* 这是"压根没等")
|
||||
* `failopen` 零互斥态 = 锁不存在却返 1 的次数(**谎报成功**那一态,见 wait 函数头)
|
||||
* 「未调用」这一态不占计数:三个计数全 0 就说明这条路径没跑过——区分"没跑过"与
|
||||
* T-a `acquired` = 真取到互斥的次数
|
||||
* T2 `try_timeout` = timeout=0 的 try 失败次数(卷被别的任务占住;defaultTask/uiTask
|
||||
* 的喂狗与绘制路径只有这一态会发生)
|
||||
* T3 `blk_timeout` = timeout>0 的分片等待超时次数(ftpTask 100 ms 分片正常退让,
|
||||
* 语义与 T2 不同:那是"等了一会儿没等到",这是"压根没等")
|
||||
* T4 `failopen` = 锁不存在却返 1 的次数(**谎报成功**那一态,见 wait 的函数头)
|
||||
* T1「从未调用过」不占计数:三个计数全 0 就说明这条路径没跑过——区分"没跑过"与
|
||||
* "跑了但一直忙"正是本卡的目的(此前两者在日志上完全一样)。
|
||||
* ⚠ 本层**刻意不记时刻**:记时刻就得读 tick,等于给 defaultTask 的取锁链添一个
|
||||
* callee 与几字节帧,而这条链的余量我们整天都在省着花(诊断不值那几个字节)。
|
||||
|
||||
+10
-10
@@ -10,8 +10,8 @@
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录,七类各一个 `/log` 文件(run / selftest / accid / alarm / start / oper / yk)|
|
||||
| `sys_log_mcu.c` | `sys_log` 的 MCU 端后端实现(NAND + FatFs) |
|
||||
| `console_log.c/h` | **控制台单写者日志**:多任务日志经队列汇入唯一写者,避免串口输出交错 |
|
||||
| `soe_rec.c/h` | **SOE 记录纯编解码**:64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
|
||||
| `soe_store.c/h` | **SOE 落盘胶水**:`/log/soe.log`,protectTask 投递(零文件系统)→ defaultTask 排空落盘 |
|
||||
| `soe_rec.c/h` | **SOE 记录纯编解码**(B9):64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
|
||||
| `soe_store.c/h` | **SOE 落盘胶水**(B9):`/log/soe.log`,protectTask 投递(零文件系统)→ defaultTask 排空落盘 |
|
||||
|
||||
## 关键机制
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
`syslog_request_clear()` 调用内就删完了,因此 `WAIT` 态在模拟器上永不出现
|
||||
(同 `syslog_post` 在两端"有队列 / 直写无队列"的差异)
|
||||
|
||||
### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做,2026-10-28 五页已落地)
|
||||
### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做)
|
||||
|
||||
**决策一:记录类型就是 `syslog_id_t` 本身,一类记录 = `/log` 下一个严格 8.3 短名的文件。**
|
||||
64B 定长记录体里**没有类型字段位**:`[0..18]` 时标 / `[19]` 空格 / `[20..29]` epoch /
|
||||
@@ -59,11 +59,11 @@
|
||||
|
||||
| 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 |
|
||||
|---|---|---|---|---|
|
||||
| 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:188` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
|
||||
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `191` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
|
||||
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `183` | `picked` 上升沿(越限电平,早于/等于动作) |
|
||||
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:582/610`、`App/task/default_task.c:266`、`App/log/sys_log_mcu.c:682`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
|
||||
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:581/584/590/594/604/606/617` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
|
||||
| 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:181` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
|
||||
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `184` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
|
||||
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `176` | `picked` 上升沿(越限电平,早于/等于动作) |
|
||||
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:574/602`、`App/task/default_task.c:242`、`App/log/sys_log_mcu.c:646`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
|
||||
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:573/576/582/586/596/598/609` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
|
||||
|
||||
- **"哪些元件算跳闸"只有一个真源**:定值表 `g_prot_set_meta` 里的 `PS_*_TRIP`(枚举注释
|
||||
即"0 告警 / 1 跳闸")+ 实例表 `k_pe[]` 的 `trip_ctl` 列。分类入口
|
||||
@@ -78,7 +78,7 @@
|
||||
任务用(**protectTask、iec104Task 必须用它**),丢弃只累加 `syslog_post_dropped()`、
|
||||
由唯一写者 defaultTask 在 `syslog_drain()` 里按差值补一条留痕——同 `soe_store_post`
|
||||
当年那条 `872 → 1192 B FAIL` 的实证纪律。收/丢判据两个口完全一致。
|
||||
- **读侧契约(给 task-11;2026-10-28 五页已按此接上)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len,
|
||||
- **读侧契约(给 task-11)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len,
|
||||
desc, desc_len)` 与运行/自检记录**同一枚 reader**,"最近 20 条"= 从 `count-20` 起读;
|
||||
`count()==0` 就是空态,页面显示"无记录"。**不要**在共享代码里调 MCU 专属的
|
||||
`syslog_clear_apply()`(模拟器不实现 → 链接期报错是故意的);清除一律走
|
||||
@@ -90,7 +90,7 @@
|
||||
- 多任务的生产者 → **单写者消费者**,消除任务间日志行交错
|
||||
- 设计详见 `docs/子系统设计/控制台单写者日志设计说明.md`
|
||||
|
||||
### soe(SOE 事件顺序记录,SOE 记录)
|
||||
### soe(SOE 事件顺序记录,B9)
|
||||
|
||||
- **记录 = 64 B 定长二进制**(`soe_rec.h` 布局),落 `/log/soe.log`;
|
||||
与 run.log 分文件——run.log 是文本记录、有 UI/主机读取方,二进制 SOE
|
||||
|
||||
+26
-49
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(RS232#1 / UART5)实现
|
||||
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1)实现
|
||||
* 模块功能:控制台单写者日志的环形缓冲 + 唯一写者发送实现(API 见 console_log.h);
|
||||
* 兼载 MicroLIB printf 底层 fputc 重定向
|
||||
* 适用平台:STM32F407ZGTx(仅 MCU 端)
|
||||
@@ -62,13 +62,6 @@
|
||||
* 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记
|
||||
* 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才
|
||||
* 销账",通道不可用期间不再把丢弃证据一起冲掉。
|
||||
* 2026-10-28 王建锋 **根因归因更正(纯注释,无行为变更)**:原注释把 ORE 锁存
|
||||
* 写成"PA10 悬空 / 未接线",用户确认 PA10 **一直是接线的**,该归因不成立。
|
||||
* 真正的充分条件是"**RX 使能了却从不读 DR**"——线上任意一个字节或一个
|
||||
* 噪声沿使 RXNE 置位后无人读,下一个事件即锁存 ORE(ORE 只能靠"先读 SR
|
||||
* 再读 DR"清除 ⇒ 永不自愈);引脚悬空只影响发生率,不是必要条件。
|
||||
* 缓解措施与结论不变,仅改注释;机制详见
|
||||
* docs/子系统设计/控制台单写者日志设计说明.md §12。
|
||||
*/
|
||||
|
||||
#include "console_log.h"
|
||||
@@ -99,18 +92,6 @@
|
||||
* 与当场直发逐字节相同。ISR / 调度器未启动两种上下文放行(不许等)。
|
||||
* 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。
|
||||
*/
|
||||
|
||||
/* ===== 控制台端口(单点收口)=====
|
||||
* 换口只改这一段:句柄 / IRQn / 初始化函数。时钟宏随句柄走——UART5 在 APB1、
|
||||
* USART1 在 APB2,别照抄;串口模式与波特率在 Src/usart.c 的 MX_*_Init 里,本模块不管。
|
||||
* 控制台在 RS232#1/UART5(USART1 专用给屏板链路——700 kbaud 只有它 MCU 引脚直出)。
|
||||
* 端口排布、台架端子与判据见 docs/子系统设计/屏板链路设计说明.md 的 §2 与 §3.2。 */
|
||||
#define CON_UART huart5
|
||||
#define CON_UART_IRQN UART5_IRQn
|
||||
#define CON_UART_CLK_DISABLE() __HAL_RCC_UART5_CLK_DISABLE()
|
||||
#define CON_UART_CLK_ENABLE() __HAL_RCC_UART5_CLK_ENABLE()
|
||||
#define CON_UART_INIT() MX_UART5_Init()
|
||||
|
||||
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
|
||||
#define DBG_CON_CHUNK_MAX 256U /* 写者单次 HAL_UART_Transmit 字节上限 */
|
||||
#define DBG_CON_WAIT_MS 200U /* 缓冲满时生产者最长等待(ms),仍满则丢弃并计数 */
|
||||
@@ -152,7 +133,7 @@ static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保
|
||||
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
|
||||
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
|
||||
static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */
|
||||
/* 行级单一所有者(道G task-23,O1 第二实例):控制台串口数据寄存器同一时刻只允许
|
||||
/* 行级单一所有者(道G task-23,O1 第二实例):USART1 数据寄存器同一时刻只允许
|
||||
* 一个上下文在逐字节轮询写入。NULL = 空闲。
|
||||
* ⚠ 这不是"锁",是**所有权凭证**:拿不到就当场让路,绝不排队等待(见
|
||||
* con_tx_try_claim 函数头:裸发一行 @115200 约 7.8 ms,把它包进临界区等于给
|
||||
@@ -187,7 +168,7 @@ static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护
|
||||
* 窗口可能被更高优先级上下文再次进入,共享量会被它改写、退出时就恢复错了。
|
||||
* ⚠ 只允许包住"几条指令"的量级,**绝不允许**包住串口发送本身:一行 90 B @115200
|
||||
* 约 7.8 ms,把它关进中断等于给启动窗口塞 7.8 ms 中断延迟与最坏喂狗抖动
|
||||
* (红线与机器判据见 con_tx_bounded 函数头与主机单测 TC-CONLOG-015b 与 TC-CONLOG-177)。
|
||||
* (红线与机器判据见 con_tx_bounded 函数头与主机单测 TC-CONLOG-015b/177)。
|
||||
*/
|
||||
#define CON_CRIT_ENTER(pm_var) do { (pm_var) = __get_PRIMASK(); __disable_irq(); } while (0)
|
||||
#define CON_CRIT_EXIT(pm_var) do { __set_PRIMASK(pm_var); } while (0)
|
||||
@@ -240,7 +221,7 @@ static void con_dwt_ensure(void) {
|
||||
* 入口参数:u - USART 寄存器基址(调用方已判非 NULL) USART_TypeDef *
|
||||
* p_buf - 待发数据;len - 字节数
|
||||
* 返回值:HAL_OK - 已发出;HAL_TIMEOUT - 计数上界内未完成(本块放弃)
|
||||
* 限定条件:控制台串口已初始化
|
||||
* 限定条件:USART1 已初始化
|
||||
* 函数说明:刻意不用 HAL_UART_Transmit——理由见 CON_TX_GUARD_MAX 处注释
|
||||
* (tick 依赖不可信 + 其调用链污染所有任务的最坏栈链)
|
||||
*/
|
||||
@@ -270,12 +251,12 @@ static HAL_StatusTypeDef con_tx_counted(USART_TypeDef *u, const uint8_t *p_buf,
|
||||
* 函数功能:有界裸发送(不依赖 uwTick / HAL 超时)
|
||||
* 入口参数:p_buf - 待发数据;len - 字节数(调用方已限 ≤ DBG_CON_CHUNK_MAX)
|
||||
* 返回值:HAL_OK - 已全部写入发送寄存器;HAL_TIMEOUT - 有界上界内未完成
|
||||
* 限定条件:控制台串口已初始化
|
||||
* 限定条件:USART1 已初始化
|
||||
* 函数说明:寄存器级轮询 TXE/TC,超时判据用 DWT CYCCNT(CON_TX_TIMEOUT_CYC);
|
||||
* DWT 未使能时走 con_tx_counted(指令计数上界,2026-10-10 取代
|
||||
* HAL_UART_Transmit 回退)。本函数只做 TX,
|
||||
* 不触碰 RX/错误标志——僵尸通道自愈由调用方按返回值处理。
|
||||
* ⚠ 控制台串口未初始化(CON_UART.Instance == NULL,如 HAL_Init 早期或
|
||||
* ⚠ USART1 未初始化(huart1.Instance == NULL,如 HAL_Init 早期或
|
||||
* 断言/故障上报路径)时立即返回 HAL_ERROR:否则会解引用 NULL 去读写
|
||||
* 地址 0(flash 别名区)→ 在故障处理里再触发故障(2026-10-02 台架实测)。
|
||||
*/
|
||||
@@ -285,14 +266,14 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
uint16_t i;
|
||||
|
||||
con_dwt_ensure();
|
||||
if (CON_UART.Instance == NULL) {
|
||||
return HAL_ERROR; /* 控制台串口未初始化:无可用通道,安全让位(不是超时,不记超时账) */
|
||||
if (huart1.Instance == NULL) {
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */
|
||||
}
|
||||
if (s_con_dwt_up == 0U) {
|
||||
return con_tx_counted(CON_UART.Instance, p_buf, len);
|
||||
return con_tx_counted(huart1.Instance, p_buf, len);
|
||||
}
|
||||
|
||||
u = CON_UART.Instance;
|
||||
u = huart1.Instance;
|
||||
t0 = DWT->CYCCNT;
|
||||
for (i = 0U; i < len; i++) {
|
||||
while ((u->SR & USART_SR_TXE) == 0U) {
|
||||
@@ -311,7 +292,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:尝试取得"控制台串口整行发送所有权"(**非阻塞**,道G task-23)
|
||||
* 函数功能:尝试取得"USART1 整行发送所有权"(**非阻塞**,道G task-23)
|
||||
* 返回值:CON_TX_HELD 已持有(发完必须 con_tx_release)/ CON_TX_BYPASS 放行且不记账
|
||||
* / CON_TX_NO 别人正持有整行,调用方须让路
|
||||
* 限定条件:任意上下文(内部只有一次几条指令的临界区,绝不睡眠、绝不长期关中断)
|
||||
@@ -427,7 +408,7 @@ void dbg_console_tx_end(void) {
|
||||
* · 拿到所有权 -> 整行一次发完,中途不会有别人的字节插进来;
|
||||
* · 拿不到 -> **本行不丢**,改投既有环形缓冲延后发。竞争失败不该变成丢行,更不该
|
||||
* 变成 [ERR]:台架那个未注册窗口每 3 次启动命中 2 次,按"丢弃 + [ERR]"处理只会
|
||||
* 把静默缺陷换成常红的门禁([ERR] 是 /serial-boot-test 的 Forbidden 标记)。
|
||||
* 把静默缺陷换成常红的门禁([ERR] 是 @test 的 Forbidden 标记)。
|
||||
* 入环的是**同一批字节原文**,本模块与 drain 都不补前缀、不重打时间戳(DBG_* 的
|
||||
* 前缀在调用点就已 snprintf 进调用方缓冲)-> 延后发出去的行与它当场直发时
|
||||
* 逐字节相同(判据⑦),取证时不会出现"证据说谎";
|
||||
@@ -485,24 +466,20 @@ uint8_t dbg_console_writer_ready(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台串口僵尸通道自愈(清错误锁存 + 整外设重初始化)
|
||||
* 函数说明:**RX 使能了却从不读 DR** → 线上任意一个字节或一个噪声沿使 RXNE 置位后无人读,
|
||||
* 下一个事件即锁存 ORE(常伴 NE/FE/PE);ORE 只能靠"先读 SR 再读 DR"清除 ⇒ 永不自愈。
|
||||
* 锁存后 HAL 轮询 TX 恒快速失败(控制台静默、系统不复位);发送器卡死状态则使 TXE
|
||||
* 等待耗满超时(冻结唯一写者 → 看门狗误复位,2026-10-02 台架实证)。外设软复位清锁存与卡死状态后
|
||||
* 重新初始化,故障时间被单块超时限定。
|
||||
* ⚠ 2026-10-28 更正:原注释把根因写成"PA10 未接线 / 悬空",用户确认 PA10 一直是接线的;
|
||||
* 充分条件是"RX 使能却不消费 DR",与引脚是否悬空无关
|
||||
* (详见 docs/子系统设计/控制台单写者日志设计说明.md §12)。
|
||||
* 函数功能:USART1 僵尸通道自愈(清错误锁存 + 整外设重初始化)
|
||||
* 函数说明:RX 悬空噪声(PA10 未接线)锁存的 ORE/NE/FE/PE 使 HAL 轮询 TX 恒快速失败
|
||||
* (控制台静默、系统不复位);发送器卡死状态则使 TXE 等待耗满超时(冻结唯一
|
||||
* 写者 → 看门狗误复位,2026-10-02 台架实证)。外设软复位清锁存与卡死状态后
|
||||
* 重新初始化,故障时间被单块超时限定
|
||||
*/
|
||||
static void con_uart_reinit(void) {
|
||||
__NVIC_ClearPendingIRQ(CON_UART_IRQN);
|
||||
CON_UART_CLK_DISABLE();
|
||||
CON_UART_CLK_ENABLE();
|
||||
CON_UART.gState = HAL_UART_STATE_RESET;
|
||||
CON_UART.RxState = HAL_UART_STATE_RESET;
|
||||
CON_UART.ErrorCode = HAL_UART_ERROR_NONE;
|
||||
(void)CON_UART_INIT();
|
||||
__NVIC_ClearPendingIRQ(USART1_IRQn);
|
||||
__HAL_RCC_USART1_CLK_DISABLE();
|
||||
__HAL_RCC_USART1_CLK_ENABLE();
|
||||
huart1.gState = HAL_UART_STATE_RESET;
|
||||
huart1.RxState = HAL_UART_STATE_RESET;
|
||||
huart1.ErrorCode = HAL_UART_ERROR_NONE;
|
||||
(void)MX_USART1_UART_Init();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -524,7 +501,7 @@ void dbg_console_drain(void) {
|
||||
return; /* 重入保护:写者自记录不触发嵌套 drain */
|
||||
}
|
||||
|
||||
/* 整行所有权(道G task-23):本函数按块直写 CON_UART->DR,若此刻别的应用上下文
|
||||
/* 整行所有权(道G task-23):本函数按块直写 USART1->DR,若此刻别的应用上下文
|
||||
* 还在未注册期那条裸发里发到自己一半(attach 就发生在 defaultTask 里,与别的
|
||||
* 任务裸发**可以**前后脚撞上),两边就会逐字节交错——那正是本卡要修的形态。
|
||||
* 拿不到就**本轮让位**:tail 不推进 => 环里一条不少,主循环 ≤50 ms 后再试,
|
||||
@@ -572,7 +549,7 @@ void dbg_console_drain(void) {
|
||||
|
||||
if (st != HAL_OK) {
|
||||
if (st == HAL_ERROR) {
|
||||
/* 2026-10-24 修复:控制台串口 **未初始化**(CON_UART.Instance == NULL,
|
||||
/* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL,
|
||||
* 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事:
|
||||
* 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配
|
||||
* RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(RS232#1 / UART5)
|
||||
* 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1 调试串口)
|
||||
* 模块功能:调试控制台单写者环形缓冲的公共 API:所有任务/上下文(含 printf)只把
|
||||
* 已格式化整行文本入环形缓冲,写者任务(defaultTask)为唯一
|
||||
* HAL_UART_Transmit 发送点(行级原子 + 指针可控)
|
||||
@@ -50,7 +50,7 @@ extern "C" {
|
||||
* (道G task-23)。诊断;[ERR] CONSOLE dropped 行报的就是它。
|
||||
* ⚠ 抢不到所有权但成功让路入环的那条行**不入本账**(它没丢,
|
||||
* 只是晚一点发)——这本账必须保持"红得有意义",因为
|
||||
* [ERR] 是 /serial-boot-test 的 Forbidden 标记)
|
||||
* [ERR] 是 @test 的 Forbidden 标记)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变)
|
||||
* dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。
|
||||
* 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次
|
||||
|
||||
+4
-4
@@ -2,9 +2,9 @@
|
||||
* 模块名称:SOE Record — 事件顺序记录编解码(纯逻辑,双端可测)
|
||||
* 模块功能:定长 64B SOE(Sequence-of-Events)记录的构建与解析:
|
||||
* CP56Time2a 时标 + 记录类型 + 元件号 + 单调序号 + 值 + 校验和。
|
||||
* **零 I/O、零 OS 依赖**(仅 stdint/string)→ 主机侧纯逻辑离线单测
|
||||
* **零 I/O、零 OS 依赖**(仅 stdint/string)→ 主机侧 L0.5 离线单测
|
||||
* (分层约束:协议/记录与硬件 I/O 分离)。
|
||||
* 记录口径(docs/算法/继电保护功能复现规划.md §3.4「SOE 记录」「动作记录」,B9):
|
||||
* 记录口径(docs/算法/继电保护功能复现规划.md §3.4 L4-4/L4-5,B9):
|
||||
* 落盘文件 = /log/soe.log(8.3 合规;FF_USE_LFN=0 约束),定长 64B 一条
|
||||
* (照 run.log 先例:count = filesize/64 O(1)、容量满删除重建)。
|
||||
* 独立于 run.log 的理由:run.log 是文本记录(时间戳+epoch+UTF-8 描述,
|
||||
@@ -61,7 +61,7 @@ extern "C" {
|
||||
* 4 软压板投入 value = 1
|
||||
* 5 采样闭锁 value = adc_health 闭锁位图(触发时的 hf & mask)
|
||||
* 6 采样恢复 value = 0
|
||||
* 7 标定闭锁 value = UA/UN/IA VALID 未齐的通道位图(未标定即闭锁,2026-10-25)
|
||||
* 7 标定闭锁 value = UA/UN/IA VALID 未齐的通道位图(卡 5,2026-10-25)
|
||||
* 8 标定复归 value = 0
|
||||
*/
|
||||
typedef enum {
|
||||
@@ -71,7 +71,7 @@ typedef enum {
|
||||
SOE_T_PLATE_IN,
|
||||
SOE_T_SAMPLE_BLOCK,
|
||||
SOE_T_SAMPLE_RESUME,
|
||||
/* 未标定闭锁(「数据可信」未标定即闭锁一项,2026-10-25 道A 追加):UA/UN/IA 的 VALID
|
||||
/* 未标定闭锁(「数据可信」卡 5,2026-10-25 道A 追加):UA/UN/IA 的 VALID
|
||||
* 未全置位 → 判据停走;与采样闭锁分型,边沿各留一条记录 */
|
||||
SOE_T_CAL_BLOCK,
|
||||
SOE_T_CAL_RESUME,
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
|
||||
(void)f_close(&f);
|
||||
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
|
||||
/* 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
|
||||
/* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
|
||||
* 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是
|
||||
* 重试失败 → 留痕计数,绝不升级成卷操作。 */
|
||||
(void)f_unlink(SOE_PATH);
|
||||
|
||||
+1
-2
@@ -44,8 +44,7 @@ extern "C" {
|
||||
* 同理二进制 SOE 通道的 soe_type_t 本轮**不动**:改它必须同步 UI 侧的类型名表
|
||||
* (类型名表在 UI 侧 <Drivers/BSP/UI/ui_w_soe.c>,不在本道写域),而且 SOE
|
||||
* 页与这五页是两套读者。
|
||||
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做——五个页面已于
|
||||
* 2026-10-28 落地:ui_page_accident / oper / alarm / start / yk,各页只读一个文件)。
|
||||
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做)。
|
||||
*/
|
||||
typedef enum {
|
||||
SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */
|
||||
|
||||
+3
-55
@@ -7,15 +7,6 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.8 2026-10-27 DTU 项目组 道G task-40:syslog_drain() 尾部按**边沿**补投一条
|
||||
* 卷锁 fail-open(零互斥那一态)自检记录。锁层只自增计数不产日志
|
||||
* (唯一能写的通道自己也要这把**非递归**卷锁),留痕必须由
|
||||
* 唯一写者做;一个 episode(= 一次上电)只投一条,不打
|
||||
* DBG 错级、不打 [ERR]([ERR] 是"设备出故障"的唯一信号
|
||||
* 通道,塞进可观测性事件就变成第二个信号源——与 task-23
|
||||
* "日志让路不计 [ERR]"同一根判据)。
|
||||
* 成本:.bss +48 B(3 枚计数/标志 + 静态快照 + 静态行缓冲),
|
||||
* **零栈增量**(缓冲全静态;前后两表见 task-40 交付)。
|
||||
* v1.7 2026-10-26 王建锋 「记录 5 页」的数据面(task-10):syslog_id_t 尾部追加
|
||||
* SYSLOG_ACCIDENT / ALARM / START / OPER / YK 五枚,本文件
|
||||
* s_paths[]/s_caps[] 同步到七项(**必须与枚举同序**,越界
|
||||
@@ -171,33 +162,6 @@ static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃
|
||||
static volatile uint32_t s_quiet_drop; /* quiet 通道累计丢弃条数 */
|
||||
static uint32_t s_quiet_reported; /* drain 侧已上报水位(唯一写者私有) */
|
||||
|
||||
/* 卷锁 fail-open(零互斥那一态)的边沿留痕(道G task-40,2026-10-27)
|
||||
* ------------------------------------------------------------------
|
||||
* 锁层只**自增计数**、绝不产日志:唯一能把事写下来的通道(/log/wdev.log 等)自己也要
|
||||
* 取这把卷锁,而卷锁**非递归**——被卷忙挡住时既写不出东西,也不许在失败分支里再去取
|
||||
* 同一把锁(App/fs/vol_lock.h「可观测性」段 + task-17 查出的事实)。所以留痕由唯一写者
|
||||
* (defaultTask)在这里按**边沿**补投,投的是自检记录。
|
||||
* ⚠ episode = 一次上电,只投一条。fail-open 的成因是"互斥体从未创建成功"(vol_lock.c
|
||||
* 的那一支),一旦成立,后面每一次取锁都零互斥;若"计数每前进一次报一条"
|
||||
* 就等于每轮 drain 一条记录 + 一次 NAND 写——诊断自己变成负载,正是上面 s_quiet_drop
|
||||
* 那条纪律要避免的事(同款先例另有 App/fs/vol_idle_sync.c 的 s_vol_sync_fail_latch)。
|
||||
* ⚠ **不打 DBG 错级、不打 [ERR]**:[ERR] 在本仓是"设备出故障"的唯一信号通道(/serial-boot-test 的
|
||||
* Forbidden 标记也依赖它,四个 Fault 的崩溃日志都以 [ERR] 开头)。把一个可观测性事件
|
||||
* 塞进 [ERR],它就同时不再是故障、也不再是禁令,两边一起失去意义——与 task-23 里
|
||||
* "日志让路不计 [ERR]"是同一根判据。机器判据 = TC-SYSLOG-137 里的 g_sys_err_count == 0。
|
||||
* 时刻:本模块在这里**不读 tick**(读一次 tick 就给 defaultTask 的 drain 最坏链多挂一个
|
||||
* callee,而这条链的余量我们一直在省着花)。记录自带的 ts 字段就是"**被观测到的时刻**"
|
||||
* ——不是发生时刻,粒度 = 一轮 drain;这正是 vol_lock.h 第②条告诫要求消费方自己说清的
|
||||
* 那件事,所以这里不另造 obs_first_ms/obs_last_ms 字段(那只会造出第二个"看起来像发生
|
||||
* 时刻"的东西)。要"何时开始忙"的精确窗口,请读 vol_lock_get_stats() 的现值。
|
||||
* 成本:3 枚计数/标志 + 一份静态快照 + 一行静态文本 ≈ 48 B .bss,**零栈增量**
|
||||
* (缓冲全静态,照 App/util/task_prof.c 的 s_prof_line 先例;前后 stack_report 两表
|
||||
* 见 task-40 交付)。 */
|
||||
static volatile uint32_t s_vl_failopen_seen; /* drain 侧上次读到的 fail-open 计数 */
|
||||
static uint8_t s_vl_reported; /* 本 boot 已留痕(1 = 本 episode 不再投) */
|
||||
static vol_lock_stats_t s_vl_now; /* 静态快照,不走调用方栈 */
|
||||
static char s_vl_note[32]; /* 记录文本(落盘按 SYSLOG_DESC_MAX=28 截断) */
|
||||
|
||||
/* 日志读写放大收口(2026-10-15,读侧):8 条滑动读窗口 + 写者代数。
|
||||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||||
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
|
||||
@@ -279,7 +243,7 @@ static volatile int8_t s_clear_st[SYSLOG_COUNT]; /* SYSLOG_CLEAR_NONE / OK /
|
||||
*/
|
||||
/*
|
||||
* 函数功能:判定一个 f_open 返回码是否属于"把自己的日志文件删掉重建一次"能修好
|
||||
* 的那一类,即文件级自愈的触发集(D 项整改,逐码依据见上方资料块)
|
||||
* 的那一类,即 L1 文件级自愈的触发集(D 项整改,逐码依据见上方资料块)
|
||||
* 入口参数:fr - f_open 的返回码 FRESULT
|
||||
* 返回值:1 - 属触发集,可 unlink 后重开一次;0 - 不属,一律立即失败、不 unlink、
|
||||
* 不计卷损坏 int
|
||||
@@ -362,8 +326,8 @@ static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建
|
||||
* 同一机制);错误去重:持续故障只报第一次,恢复前静默(消除"每次
|
||||
/* L1 文件级自愈:陈旧/损坏条目 → 丢弃重建、重试一次(与上方容量满重建
|
||||
* 同一机制);L2 错误去重:持续故障只报第一次,恢复前静默(消除"每次
|
||||
* 开机同一错误刷屏")。打开模式必须带 FA_WRITE 位:本 FatFs 移植的
|
||||
* FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write
|
||||
* 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */
|
||||
@@ -879,22 +843,6 @@ void syslog_drain(void) {
|
||||
s_quiet_reported = s_quiet_drop;
|
||||
DBG_ERROR("quiet drop total=%lu", (unsigned long)s_quiet_drop);
|
||||
}
|
||||
/* 卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(约束与理由全在上面那组静态量旁边)。
|
||||
* vol_lock_get_stats() 不取锁、不关中断、不阻塞:卷被长期占住时正是最需要读它的
|
||||
* 时刻,观测口若再去抢同一把锁,就把观测做成了故障的一部分。
|
||||
* 必须排在 s_clear_service() 之前吗?不必——两者无先后关系(清除的是记录文件,
|
||||
* 本条是**新增**一条自检记录);但必须排在下面 soe_store_drain() 之前,因为本函数
|
||||
* 这条支路不持卷锁,而 soe_store_drain() 自己 try 锁(沿用本函数既有纪律)。 */
|
||||
vol_lock_get_stats(&s_vl_now);
|
||||
if (s_vl_now.failopen != s_vl_failopen_seen) {
|
||||
s_vl_failopen_seen = s_vl_now.failopen; /* 水位每轮都追:别让下一轮误判"新增" */
|
||||
if (s_vl_reported == 0U) {
|
||||
s_vl_reported = 1U; /* 一个 episode 一条(= 一次上电) */
|
||||
(void)snprintf(s_vl_note, sizeof(s_vl_note), "vollock failopen=%lu",
|
||||
(unsigned long)s_vl_failopen_seen);
|
||||
syslog_post(SYSLOG_SELFTEST, s_vl_note);
|
||||
}
|
||||
}
|
||||
/* 「清除记录」的落地半边——必须在上面的条目排空**之后**(顺序是判据的一部分:
|
||||
* 先删后写会把本轮刚排空的旧记录又落回去,count 不归零 = 没实际效果的按钮),
|
||||
* 也在 soe_store_drain() 之前:本函数自带卷锁 try,绝不在此持锁状态下调用。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
| 子目录 | 协议 | 说明 |
|
||||
|---|---|---|
|
||||
| `iec104/` | IEC 60870-5-104 被控站(TCP 工厂默认 2404 上行) | **纯逻辑**三件套(asdu/pts/link,零 I/O,数据源 vtable 注入)→ 全栈主机侧纯逻辑单测;板端胶水与点表决策 → `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
| `iec104/` | IEC 60870-5-104 被控站(TCP 2404 上行) | **纯逻辑**三件套(asdu/pts/link,零 I/O,数据源 vtable 注入)→ 全栈 L0.5 主机单测;板端胶水与点表决策 → `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
|
||||
## 何时使用本目录
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:104 规约应用服务数据单元(ASDU)的类型/传送原因常量、组帧与解析。
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧可离线纯逻辑单测
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧 L0.5 可离线单测
|
||||
* (分层约束:协议解析与硬件 I/O 分离)。
|
||||
* 口径来源:docs/算法/继电保护功能复现规划.md §1.5(B0 产品定义)——
|
||||
* 公共地址 2 字节、传送原因 2 字节、信息体地址 IOA **3 字节**(101 为 2 字节);
|
||||
|
||||
@@ -40,13 +40,6 @@ static const iec104_comm_t s_comm_rom = {
|
||||
IEC104_COMM_DEF_CA, IEC104_COMM_DEF_PORT
|
||||
};
|
||||
|
||||
/* 落盘脏位(B7 形态,run_par/prot_set 同款):setter 成功置 1,
|
||||
* iec104_comm_store_save() 写盘成功清 0。**不挂回调、不加事件位**(task-8
|
||||
* D5 裁定的持久化面延续):defaultTask 每轮无条件调 store_save,无脏空转。
|
||||
* 单字节标志:uiTask 写、defaultTask 读清——置 1 幂等、清零窗口最坏
|
||||
* 多跑一轮 save,语义无损(run_par s_dirty 同一条竞态分析)。 */
|
||||
static volatile uint8_t s_comm_dirty;
|
||||
|
||||
/* 键表(s_comm 布局内偏移,同 net_cfg/ftp_cfg 写法) */
|
||||
static const cfg_key_def_t s_comm_keys[] = {
|
||||
{ "ca", CFG_VAL_UINT, offsetof(iec104_comm_t, ca), 2u },
|
||||
@@ -64,7 +57,6 @@ int iec104_comm_init(void) {
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
(void)memcpy(&s_comm, &s_comm_rom, sizeof(s_comm));
|
||||
s_comm_dirty = 0u;
|
||||
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = IEC104_COMM_SEC;
|
||||
@@ -85,7 +77,11 @@ int iec104_comm_init(void) {
|
||||
uint16_t iec104_comm_ca(void) {
|
||||
uint16_t v = s_comm.ca;
|
||||
|
||||
if ((v < IEC104_COMM_CA_MIN) || (v > IEC104_COMM_CA_MAX)) {
|
||||
/* 只判下界。上界 IEC104_COMM_CA_MAX 就是 uint16_t 的最大值,写成
|
||||
* 「v > 65535」对 uint16_t 恒假(cppcheck compareValueOutOfTypeRangeError),
|
||||
* 留着只会让下一个读代码的人以为上界真被守过。合法带口径不变,
|
||||
* 仍照 iec104_comm.h 的常量注释(ca 1..65535 / port 1024..65535)。 */
|
||||
if (v < IEC104_COMM_CA_MIN) {
|
||||
return IEC104_COMM_DEF_CA;
|
||||
}
|
||||
return v;
|
||||
@@ -99,7 +95,7 @@ uint16_t iec104_comm_ca(void) {
|
||||
uint16_t iec104_comm_port(void) {
|
||||
uint16_t v = s_comm.port;
|
||||
|
||||
if ((v < IEC104_COMM_PORT_MIN) || (v > IEC104_COMM_PORT_MAX)) {
|
||||
if (v < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return IEC104_COMM_DEF_PORT;
|
||||
}
|
||||
return v;
|
||||
@@ -108,63 +104,27 @@ uint16_t iec104_comm_port(void) {
|
||||
/*
|
||||
* 函数功能:写 ca 进 RAM 副本(守卫 = getter 同带;非法不提交)
|
||||
* 入口参数:ca - 公共地址 uint16_t
|
||||
* 返回值:0 提交并置落盘脏;-1 带外(副本不动、不置脏) int
|
||||
* 限定条件:UI 保存交接路(通讯设置页经 ui_data 通道六转进来)与宿主测试;
|
||||
* 落盘 = defaultTask 每轮无条件 iec104_comm_store_save()(无事件位)
|
||||
* 返回值:0 提交;-1 带外(副本不动) int
|
||||
* 限定条件:UI 保存交接路(task-9);落盘走 SYS_EVT_CFG_SAVE 整档外科 save
|
||||
*/
|
||||
int iec104_comm_ca_set(uint16_t ca) {
|
||||
if ((ca < IEC104_COMM_CA_MIN) || (ca > IEC104_COMM_CA_MAX)) {
|
||||
if (ca < IEC104_COMM_CA_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.ca = ca;
|
||||
s_comm_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写 port 进 RAM 副本(守卫 = getter 同带;非法不提交)
|
||||
* 入口参数:port - 监听端口 uint16_t
|
||||
* 返回值:0 提交并置落盘脏;-1 带外(副本不动、不置脏) int
|
||||
* 限定条件:同 iec104_comm_ca_set(生效时刻 = 下一次重 bind 监听)
|
||||
* 返回值:0 提交;-1 带外(副本不动) int
|
||||
* 限定条件:同 iec104_comm_ca_set
|
||||
*/
|
||||
int iec104_comm_port_set(uint16_t port) {
|
||||
if ((port < IEC104_COMM_PORT_MIN) || (port > IEC104_COMM_PORT_MAX)) {
|
||||
if (port < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.port = port;
|
||||
s_comm_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏探查(非消费)——页面保存行 WAIT/OK 的唯一判定源
|
||||
* 返回值:1 有未落盘改动;0 干净 uint8_t
|
||||
* 限定条件:无(任意上下文纯读)
|
||||
* 函数说明:脏位是全段级(ca+port 一次 save 一起落):归零只证明
|
||||
* 「这一档落盘了」,不证明「我这一项单独落盘」;104 侧无自动
|
||||
* 写者(ca/port 不经 C_SE 整定),比 run_par 少一条「他人再改」
|
||||
* 干扰,页面仍按「结果锁存」画(run_par 页同口径,别做两套)
|
||||
*/
|
||||
uint8_t iec104_comm_dirty_peek(void) {
|
||||
return s_comm_dirty;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:执行落盘(整档外科 save 成功后清脏;失败脏保持 = 下轮重试)
|
||||
* 返回值:0 成功/无脏;-1 写盘失败 int
|
||||
* 限定条件:**defaultTask(配置引擎属主)上下文**——run_par_store_save()
|
||||
* 同一调用点旁挂,每轮无条件
|
||||
* 函数说明:失败不打日志是刻意的(App/protocol 零日志惯例,本文件无 DBG):
|
||||
* cfg_file_save 自身已有 DBG_ERROR 留痕 + 脏位保留 = 屏上 ERR 的
|
||||
* 判定源,再打一条就是重复锚点(AGENTS 日志成本口径)
|
||||
*/
|
||||
int iec104_comm_store_save(void) {
|
||||
if (s_comm_dirty == 0u) {
|
||||
return 0;
|
||||
}
|
||||
if (cfg_file_save() != 0) {
|
||||
return -1;
|
||||
}
|
||||
s_comm_dirty = 0u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
* 判定;初版曾寄身 iec104_pts.c 尾段,2026-10-26 lead 纠偏迁出)。
|
||||
* 修改记录:
|
||||
* 2026-10-26 DTU 项目组 创建(道G task-8);同日实现位置改配对拆分。
|
||||
* 2026-10-27 DTU 项目组 道G task-9:脏位 + iec104_comm_dirty_peek() /
|
||||
* store_save()(run_par B7 同款,通讯设置页与
|
||||
* defaultTask 轮询的消费面)。
|
||||
* 线程约定:注册/复位 = app_main_init 内核前(run_par_init / adc_cal_init 同款
|
||||
* "先注册后 load"契约);load/save/poll = defaultTask(配置引擎属主,
|
||||
* netTask/uiTask 不得直接触碰引擎);getter = 任意消费上下文(u16
|
||||
* 半字对齐读天然原子,重载瞬间最坏 = 单字段新旧值之一);setter =
|
||||
* UI 保存交接路(task-9,net_cfg_set_* 先例)。
|
||||
* 生效语义:**改后重启生效**(通讯设置页验收口径,docs/任务规划/UI补齐.md §2)——
|
||||
* 生效语义:**改后重启生效**(docs/任务规划/UI补齐.md §2 第 5 行验收口径)——
|
||||
* iec104Task 启动时取一次 getter 填 link cfg;端口另在下次重 bind
|
||||
* (拆链/网络代次重建)自然生效,不必整机重启。不挂段回调、不新增
|
||||
* 事件位、不走 SYS_EVT_NET_RECONFIG(网络代次链语义不同;且 CH395F
|
||||
@@ -37,10 +34,9 @@ extern "C" {
|
||||
/* 段名(cfg_file 引擎注册用;≤ CFG_FILE_MAX_NAME_LEN) */
|
||||
#define IEC104_COMM_SEC "comm"
|
||||
|
||||
/* 工厂默认:CA = 1(点表口径,V0.1 沿用);端口 = 2404(DL/T 634.5104 标准)。
|
||||
* 原同名宏 IEC104_SERV_PORT(App/task/iec104_serv.h)已随道G task-9 快照切
|
||||
* getter 删除——本段 getter 自 task-8 起就是运行期唯一真源,本头文件是这两个
|
||||
* 常量的唯一出口 */
|
||||
/* 工厂默认:CA = 1(点表口径,V0.1 沿用);端口 = 2404(DL/T 634.5104 标准,
|
||||
* 与 App/task/iec104_serv.h 的 IEC104_SERV_PORT 同值——该宏现仅作
|
||||
* ui_data_feed 快照兼容出口保留,运行期真源 = 本段 getter) */
|
||||
#define IEC104_COMM_DEF_CA 1u
|
||||
#define IEC104_COMM_DEF_PORT 2404u
|
||||
|
||||
@@ -83,9 +79,8 @@ uint16_t iec104_comm_port(void);
|
||||
* 函数功能:写 ca 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
|
||||
* 入口参数:ca - 公共地址 1..65535 uint16_t
|
||||
* 返回值:0 - 提交;-1 - 带外(副本不动) int
|
||||
* 限定条件:调用方 = 通讯设置页(ui_data 通道六注入转 uiTask 上下文);
|
||||
* 成功置落盘脏——写盘 = defaultTask 每轮无条件
|
||||
* iec104_comm_store_save()(run_par B7 同款,无事件位、不写盘于本模块)
|
||||
* 限定条件:调用方 = UI 保存交接路(task-9);落盘经 SYS_EVT_CFG_SAVE
|
||||
* 整档外科 save(net_cfg_save_request 同款交接,本模块不写盘)
|
||||
*/
|
||||
int iec104_comm_ca_set(uint16_t ca);
|
||||
|
||||
@@ -93,28 +88,10 @@ int iec104_comm_ca_set(uint16_t ca);
|
||||
* 函数功能:写 port 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
|
||||
* 入口参数:port - 监听端口 1024..65535 uint16_t
|
||||
* 返回值:0 - 提交;-1 - 带外(副本不动) int
|
||||
* 限定条件:同 iec104_comm_ca_set(生效时刻 = 下一次重 bind 监听,改后不必重启)
|
||||
* 限定条件:同 iec104_comm_ca_set
|
||||
*/
|
||||
int iec104_comm_port_set(uint16_t port);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏探查(非消费)——页面保存行 ENTER/WAIT/OK 判定源
|
||||
* 返回值:1 - 有已改未落盘;0 - 干净 uint8_t
|
||||
* 限定条件:无(任意上下文纯读)
|
||||
* 函数说明:脏位段级(ca+port 同档共落);页面按「结果锁存」画,与 run_par
|
||||
* 页同一条纪律(ui_w_ytview 文件头)
|
||||
*/
|
||||
uint8_t iec104_comm_dirty_peek(void);
|
||||
|
||||
/*
|
||||
* 函数功能:执行落盘(整档外科 save 成功清脏;失败脏保持下轮重试)
|
||||
* 返回值:0 - 成功/无脏;-1 - 写盘失败(屏上 ERR 的判定源) int
|
||||
* 限定条件:**defaultTask(配置引擎属主)上下文**;由 default_task.c 的
|
||||
* run_par_store_save() 同点位每轮无条件调用(本模块不挂回调、
|
||||
* 不设事件位——task-8 D5 口径的持久化面)
|
||||
*/
|
||||
int iec104_comm_store_save(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -161,18 +161,23 @@ static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
|
||||
*/
|
||||
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
|
||||
uint16_t obj_len, uint8_t cot, uint8_t pn) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint16_t n = obj_len;
|
||||
|
||||
if (n > BODY_MAX) {
|
||||
n = 0u;
|
||||
n = 0u; /* 超长回显退化为空信息体 */
|
||||
}
|
||||
if (n > 0u) {
|
||||
uint8_t body[BODY_MAX]; /* 作用域收进真要回显的分支,不把未初始化缓冲传下去 */
|
||||
|
||||
(void)memcpy(body, obj, n);
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
} else {
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, NULL, 0u, 0u);
|
||||
}
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
@@ -280,12 +285,13 @@ static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[8]; /* 最大 = SIQ + CP56(7)(M_SP_TB_1) */
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t blen;
|
||||
uint8_t type;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
uint8_t blen;
|
||||
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
* → clock_sync 回调)、试验(C_TS)与进程复位(C_RP,清快照)。
|
||||
* **零 I/O**:收 = on_rx 喂 TCP 流片段,发 = send_apdu 回调,
|
||||
* 时基 = 调用方注入 now_ms(tick ms,回绕安全:差值法)
|
||||
* → 全栈(asdu+pts+link)主机侧纯逻辑单测可离线跑(分层约束)。
|
||||
* → 全栈(asdu+pts+link)主机侧 L0.5 可离线跑(分层约束)。
|
||||
* 参数(DL/T 634.5104 标准默认,§1.5 未另定):k=12 w=8 t1=15s t2=10s t3=20s。
|
||||
* 端口:TCP **2404** = 工厂默认(§1.5 B0 定义 / IEC 60870-5-104 标准端口;
|
||||
* 本 DTU = 被控站/服务器,主站连入)。真源与可改性见本文件下方 [comm] 段说明。
|
||||
* 端口:TCP **2404**(§1.5 B0 定义;本 DTU = 被控站/服务器,主站连入)。
|
||||
* 线程约定:单实例单任务上下文(板端 = 104 服务在 netTask 消息循环内驱动,
|
||||
* 见 iec104_serv 胶水层);非线程安全,禁止多任务共享一实例。
|
||||
* 失败语义:send_apdu 内**不得阻塞**;发送失败由胶水层断链(主站会重连),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(实现)
|
||||
* 模块功能:实现 iec104_pts.h——YC/YX/YK/定值点表(ROM)、板端默认源
|
||||
* (**测频与遥测快照**(104 改读快照)/ prot_elm / prot_set)、COV 比对(遥信变位 +
|
||||
* (**L2 遥测快照**(卡 9)/ prot_elm / prot_set)、COV 比对(遥信变位 +
|
||||
* 遥测死区 2 点确认 + 品质变化)。点表口径见 iec104_pts.h 文件头与
|
||||
* 设计文档 §3。
|
||||
* 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
* 修改记录:
|
||||
* 2026-10-25 DTU 项目组 下游常数按工程量重定(「数据可信」):YC 死区由拟定码值域重定为
|
||||
* 2026-10-25 DTU 项目组 卡 6(「数据可信」):YC 死区由拟定码值域重定为
|
||||
* 二次值工程量域(额定 0.5 % 规则),判据依据见
|
||||
* k_yc 表头注释;数值随 104 改读快照迁运行参数,本表将退化为
|
||||
* k_yc 表头注释;数值随卡 9 迁运行参数,本表将退化为
|
||||
* ROM 默认载体。
|
||||
* 2026-10-25 DTU 项目组 104 改读快照与频率点绑定(「数据可信」,道D):YC 板端源改读
|
||||
* 测频与遥测快照(App/algo/meas.h,meas_get 无锁双区);
|
||||
* 2026-10-25 DTU 项目组 卡 9/卡 11(「数据可信」,道D):YC 板端源改读
|
||||
* L2 遥测快照(App/algo/meas.h,meas_get 无锁双区);
|
||||
* 削顶五路由快照品质 CLIPPED 驱动线上 IV(不再绑死 -1,
|
||||
* V0.2 消除双转换器无共同采样触发后零改动自动启用);0x401d 绑频率源(频率点绑定);
|
||||
* V0.2 消除双转换器无共同采样触发后零改动自动启用);0x401d 绑频率源(卡 11);
|
||||
* 死区改运行时副本 + set/get 入口;COV 遥测腿 = 死区
|
||||
* 连续 2 点确认(板端按快照 seq 去重)+ 品质翻转即报。
|
||||
* 单测 = test/host/iec104 TC-IEC104-025~028。
|
||||
* 运行参数段 2026-10-25 DTU 项目组 死区真源收编 App/algo/run_par.c(本层 s_yc_db
|
||||
* 卡 10 2026-10-25 DTU 项目组 死区真源收编 App/algo/run_par.c(本层 s_yc_db
|
||||
* 副本删除,set/get 转转发壳,壳保留清确认计数语义);
|
||||
* 定值读写路由 = run_par 表先、prot_set 后(0x8020~
|
||||
* 0x8058 运行参数段 C_RD/C_SE,系数覆盖直通 adc_cal)。
|
||||
@@ -36,36 +36,36 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */
|
||||
#include "meas.h" /* 板端源:遥测 = 测频与遥测快照(104 改读快照;品质域位同源) */
|
||||
#include "run_par.h" /* 运行参数段:死区真源 + 104 读写路由 */
|
||||
#include "meas.h" /* 板端源:遥测 = L2 遥测快照(卡 9;品质域位同源) */
|
||||
#include "run_par.h" /* 卡 10:死区真源 + 运行参数段 104 读写路由 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "sys_log.h" /* task-10:遥控记录(SYSLOG_YK)/ 操作记录(SYSLOG_OPER) */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区列 = ROM 默认值) ------------ */
|
||||
/* 源类型(「数据可信」频率点绑定:YC 表加 src 类型区分相量/频率) */
|
||||
#define PTS_SRC_MEAS 0u /* 全周傅氏相量快照槽 sem(电气量,yc/qds 直读) */
|
||||
/* 源类型(「数据可信」卡 11:YC 表加 src 类型区分相量/频率) */
|
||||
#define PTS_SRC_MEAS 0u /* L2 快照槽 sem(电气量,yc/qds 直读) */
|
||||
#define PTS_SRC_FREQ 1u /* 快照频率(freq_valid 门 → IV) */
|
||||
|
||||
typedef struct {
|
||||
uint32_t ioa;
|
||||
int8_t sem; /* 快照 yc 槽 = 语义通道(-1 = 非测量量点,当前仅频率) */
|
||||
float db_default; /* 死区工厂默认(生效真源 = run_par [run_par] 段 = 运行参数段真源) */
|
||||
float db_default; /* 死区工厂默认(生效真源 = run_par [run_par] 段,卡 10) */
|
||||
uint8_t src; /* PTS_SRC_* 源类型 */
|
||||
} pts_yc_def_t;
|
||||
|
||||
/* 死区规则 = 额定二次值的 0.5 %(「数据可信」§2.3 表第 2 行建议规则,随下游常数按工程量重定采纳;
|
||||
/* 死区规则 = 额定二次值的 0.5 %(「数据可信」§2.3 表第 2 行建议规则,卡 6 采纳;
|
||||
* 额定基准 100 V / 1 A 与 App/algo/prot_set.c 头部同源,勿在此另立口径):
|
||||
* 电压类 0.5 % × 100 V = 0.5 V 电流类 0.5 % × 1 A = 0.005 A
|
||||
* 频率点不适用"额定百分比"(额定附近 0.5 Hz 与旧值同量级,维持 0.5 并随
|
||||
* 运行参数段可调)。自 104 改读快照起本表**只作工厂默认载体**:生效死区 =
|
||||
* App/algo/run_par.c 的 [run_par] 引擎副本(运行参数段真源收编,本层不再存副本);削顶五路不再 sem=-1 绑死,而是绑快照槽、由品质 CLIPPED
|
||||
* 卡 10 运行参数段可调)。自卡 9 起本表**只作工厂默认载体**:生效死区 =
|
||||
* App/algo/run_par.c 的 [run_par] 引擎副本(卡 10 真源收编,本层不再存副本);削顶五路不再 sem=-1 绑死,而是绑快照槽、由品质 CLIPPED
|
||||
* 驱动线上 IV——V0.2 削顶消失后点位零改动自动启用(docs/任务规划/
|
||||
* 数据可信.md §10 的「双转换器无共同采样触发」行)。
|
||||
* ⚠ 过渡期(eng=0 引脚电压域,两点标定完成前):上送值本身非二次值,死区
|
||||
* ⚠ 过渡期(eng=0 引脚电压域,卡 12 标定前):上送值本身非二次值,死区
|
||||
* 数字按本域偏保守(迟报不漏报);标定落地后复核(「数据可信」§2.3 表
|
||||
* 第 2 行)。判定 = 死区连续 2 点确认(104 改读快照),见 iec104_pts.h COV 说明。 */
|
||||
* 第 2 行)。判定 = 死区连续 2 点确认(卡 9),见 iec104_pts.h COV 说明。 */
|
||||
static const pts_yc_def_t k_yc[] = {
|
||||
{ 0x4011u, (int8_t)ADC_SEM_UA, 0.5f, PTS_SRC_MEAS }, /* 1Ua:0.5 % × 100 V */
|
||||
{ 0x4012u, (int8_t)ADC_SEM_UB, 0.5f, PTS_SRC_MEAS }, /* 1Ub:削顶 → 品质驱动 IV */
|
||||
@@ -75,12 +75,12 @@ static const pts_yc_def_t k_yc[] = {
|
||||
{ 0x4016u, (int8_t)ADC_SEM_IB, 0.005f, PTS_SRC_MEAS }, /* 1Ib:削顶 → 品质驱动 IV */
|
||||
{ 0x4017u, (int8_t)ADC_SEM_IC, 0.005f, PTS_SRC_MEAS }, /* 1Ic:同上 */
|
||||
{ 0x4018u, (int8_t)ADC_SEM_IN, 0.005f, PTS_SRC_MEAS }, /* 1I0:同上 */
|
||||
{ 0x401Du, -1, 0.5f, PTS_SRC_FREQ }, /* 1f:频率点绑定——绑频率源,
|
||||
{ 0x401Du, -1, 0.5f, PTS_SRC_FREQ }, /* 1f:卡 11 绑频率源,
|
||||
* freq_valid=0 → IV */
|
||||
};
|
||||
#define YC_N ((uint8_t)(sizeof(k_yc) / sizeof(k_yc[0])))
|
||||
|
||||
/* ===== 遥测 COV 运行时状态(104 改读快照;仅 iec104Task 上下文访问,无并发) =====
|
||||
/* ===== 遥测 COV 运行时状态(卡 9;仅 iec104Task 上下文访问,无并发) =====
|
||||
* 判据链:新测量点 → |v − 上次上送值| > 死区?→ 连续 2 点确认 → 置变化位。
|
||||
* 基准 = **上次上送值**(非上次读值——否则慢漂移永不出死区,参考系统同款);
|
||||
* 品质(线上 qds)翻转 = 事件:不参与 2 点计数、当拍直报。 */
|
||||
@@ -124,7 +124,7 @@ static uint8_t s_yx_snap_ok;
|
||||
/*
|
||||
* 函数功能:初始化点表模块(死区装 ROM 默认、清 COV/快照状态、恢复默认源)
|
||||
* 限定条件:内核启动前或 104 服务启动前,调一次
|
||||
* 函数说明:104 改读快照起不再扫 phasor 实例(数据源 = 快照,槽位 = 语义通道,编译期
|
||||
* 函数说明:卡 9 起不再扫 phasor 实例(数据源 = 快照,槽位 = 语义通道,编译期
|
||||
* 直接可定);重复调用 = 全状态复位(单测 reset 依赖此语义)。
|
||||
*/
|
||||
void iec104_pts_init(void) {
|
||||
@@ -172,12 +172,12 @@ uint32_t iec104_pts_yc_ioa(uint8_t idx) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:品质域位映射(uint16 meas 域 → uint8 线上 QDS,104 改读快照)
|
||||
* 函数功能:品质域位映射(uint16 meas 域 → uint8 线上 QDS,卡 9)
|
||||
* 入口参数:q16 - 源侧品质(低字节与线上同值;高字节为 MEAS_QDS_CLIPPED 域内位)
|
||||
* 返 回 值:线上 QDS 字节 uint8_t
|
||||
* 函数说明:CLIPPED(硬件削顶,104 无对应位)映射为 IV——「削顶通道 104 恒
|
||||
* IV」口径(docs/任务规划/数据可信.md §8 风险表「削顶通道被当成可测」
|
||||
* 行)改由快照品质驱动,不再靠点表绑死 -1;OV/NT/IV 透传,其余高位丢弃。
|
||||
* IV」口径(docs/任务规划/数据可信.md §8 风险表末行)改由快照品质
|
||||
* 驱动,不再靠点表绑死 -1;OV/NT/IV 透传,其余高位丢弃。
|
||||
*/
|
||||
static uint8_t s_q_map(uint16_t q16) {
|
||||
uint8_t w = (uint8_t)(q16 & (uint16_t)(IEC104_QDS_OV | IEC104_QDS_NT
|
||||
@@ -195,7 +195,7 @@ static uint8_t s_q_map(uint16_t q16) {
|
||||
* 返 回 值:0 有效;-1 IV(含 CLIPPED 映射);-2 NT(未就绪) int
|
||||
* 函数说明:ex 源在场 → 品质由源给(板端 = 快照槽位透传);否则退 legacy
|
||||
* yc_read,品质从返回值推导(-1→IV / -2→NT / 0→无位)——旧注入源
|
||||
* 行为与 104 改读快照前**完全一致**(单测假源零改动)。判定顺序 IV > NT:
|
||||
* 行为与卡 9 前**完全一致**(单测假源零改动)。判定顺序 IV > NT:
|
||||
* 开机首帧 qds=IV|NT 按无效处理(NT 只留给纯暂态)。任何无效 →
|
||||
* 信息体清零(线上口径:主站只信品质)。
|
||||
*/
|
||||
@@ -245,14 +245,14 @@ int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写死区(104 改读快照的 API 壳,转发 run_par 真源 + 清本层确认计数)
|
||||
* 函数功能:写死区(卡 9 API 壳,转发 run_par 真源 + 清本层确认计数)
|
||||
* 入口参数:idx - YC 表下标 db - 工程量绝对死区(≥0,分档上限见 run_par)
|
||||
* 返 回 值:0 成功;-1 越界/负值/NaN/越程(run_par 副本不动) int
|
||||
* 限定条件:**iec104Task 上下文**(run_par 副本写 = 引擎对齐 int32 单字,
|
||||
* 104 执行段之外调用需自行避免与 104 写并发)
|
||||
* 函数说明:成功时清零该点待确认计数——跨死区档位不保留半截确认序列;
|
||||
* 不隐式重建基线(上送基准延续,新死区即刻生效)。落盘由 run_par
|
||||
* 脏标志 + SYS_EVT_RUN_PAR_SAVE 走 defaultTask(运行参数段持久化链)。
|
||||
* 脏标志 + SYS_EVT_RUN_PAR_SAVE 走 defaultTask(卡 10 持久化链)。
|
||||
*/
|
||||
int iec104_pts_yc_set_deadband(uint8_t idx, float db) {
|
||||
if (idx >= YC_N) {
|
||||
@@ -348,7 +348,7 @@ void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
s_yx_snap_ok = 1u;
|
||||
}
|
||||
|
||||
/* YC(104 改读快照):测点推进 = 板端源按快照换帧(100 ms poll vs 200 ms 快照,
|
||||
/* YC(卡 9):测点推进 = 板端源按快照换帧(100 ms poll vs 200 ms 快照,
|
||||
* 不去重则同一帧计两次 → "连续 2 点确认"退化成"连续 2 次轮询",单帧
|
||||
* 噪声即可被确认,抗噪语义失效);注入源 = 每次调用一测点。
|
||||
* 判定 = 死区连续 2 点确认 + 品质翻转直报(口径全文 → iec104_pts.h)。 */
|
||||
@@ -383,7 +383,7 @@ void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
continue;
|
||||
}
|
||||
if (q != s_yc_repq[i]) {
|
||||
/* 品质翻转 = 事件直报(含 0x401d 的 IV 进出,频率点绑定) */
|
||||
/* 品质翻转 = 事件直报(含 0x401d 的 IV 进出,卡 11) */
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
s_yc_repq[i] = q;
|
||||
s_yc_cnf[i] = 0u;
|
||||
@@ -458,15 +458,15 @@ int iec104_pts_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
/* ============================ 板端默认源 ================================= */
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥测 = 测频与遥测快照直读(「数据可信」104 改读快照与频率点绑定)
|
||||
* 函数功能:板端默认源 - 遥测 = L2 遥测快照直读(「数据可信」卡 9/卡 11)
|
||||
* 入口参数:idx - YC 表下标 out/qds - 值 + uint16 域品质(MEAS_QDS_*)
|
||||
* 返 回 值:0 / -1 越界或频率未新鲜(IV)/ -2 快照从未发布(NT) int
|
||||
* 限定条件:iec104Task 上下文(meas_get 为双区无锁读,App/algo/meas.h)
|
||||
* 函数说明:**不做任何本地判断**——电气路值/品质整体透传快照槽(削顶路快照
|
||||
* 已置 CLIPPED,s_q_map 映上线 IV;开机首帧 IV|NT 由 s_yc_point 按
|
||||
* IV 处理),"新不新、能不能信"全在发布侧一处决定(遥测快照发布的设计意图,
|
||||
* IV 处理),"新不新、能不能信"全在发布侧一处决定(卡 8 设计意图,
|
||||
* 两处判断必打架)。频率路(PTS_SRC_FREQ):snap.freq_valid==0 →
|
||||
* IV(频率点绑定 DoD 原句);有效时值 = snap.freq_hz,与 freq_get() 同源。
|
||||
* IV(卡 11 DoD 原句);有效时值 = snap.freq_hz,与 freq_get() 同源。
|
||||
*/
|
||||
static int s_src_yc_read_ex(uint8_t idx, float *out, uint16_t *qds) {
|
||||
meas_snap_t snap;
|
||||
@@ -633,7 +633,7 @@ static int s_src_set_read(uint32_t addr, float *out) {
|
||||
return -1;
|
||||
}
|
||||
if (run_par_104_read(addr, out) == 0) {
|
||||
return 0; /* 运行参数段命中 */
|
||||
return 0; /* 运行参数段命中(卡 10) */
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if ((g_prot_set_meta[i].comm_addr != 0u)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* (asdu+pts+link)可离线跑(分层约束:协议解析与硬件 I/O 分离)。
|
||||
* 点表口径(docs/算法/继电保护功能复现规划.md §1.5,验收 = 省电科院点表):
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;数据源 =
|
||||
* 测频与遥测快照(App/algo/meas.h,「数据可信」104 改读快照与频率点绑定):电气 8 路绑快照槽,
|
||||
* L2 遥测快照(App/algo/meas.h,「数据可信」卡 9/11):电气 8 路绑快照槽,
|
||||
* 有效性由快照品质驱动——V0.1 削顶路(docs/算法/继电保护功能复现规划.md
|
||||
* §1.6)快照置 CLIPPED → 本层映射为线上 IV(V0.2 后自动启用,绑定零
|
||||
* 改动,见 docs/任务规划/数据可信.md §10「双转换器无共同采样触发」行);0x401d 绑频率源,
|
||||
@@ -25,11 +25,11 @@
|
||||
* 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
* 修改记录:
|
||||
* 2026-10-25 DTU 项目组 104 改读快照与频率点绑定(「数据可信」,道D):YC 数据源 = 全周傅氏相量遥测
|
||||
* 2026-10-25 DTU 项目组 卡 9/卡 11(「数据可信」,道D):YC 数据源 = L2 遥测
|
||||
* 快照(削顶路由品质驱动 IV);死区改运行时副本 +
|
||||
* set/get 入口;COV 遥测腿 = 死区 2 点确认(seq 去重)
|
||||
* + 品质变化即报;0x401d 绑频率源(freq_valid → IV)。
|
||||
* 2026-10-25 DTU 项目组 运行参数段:死区真源迁 run_par([run_par] cfg 段 +
|
||||
* 2026-10-25 DTU 项目组 卡 10:死区真源迁 run_par([run_par] cfg 段 +
|
||||
* 0x8020~0x8058 读写映射;本层 set/get 为转发壳)。
|
||||
*/
|
||||
#ifndef IEC104_PTS_H
|
||||
@@ -52,7 +52,7 @@ typedef struct {
|
||||
int (*set_read)(uint32_t addr, float *out);
|
||||
/* 定值写:select=1 预置校验(范围)/ 0 执行写入。返回 0 / -1 越界或未知 */
|
||||
int (*set_write)(uint32_t addr, float v, uint8_t select);
|
||||
/* 遥测读(带品质,104 改读快照时追加于表尾——旧源 NULL = 走 yc_read 退路,品质由
|
||||
/* 遥测读(带品质,卡 9 追加于表尾——旧源 NULL = 走 yc_read 退路,品质由
|
||||
* 返回值推导:-1 → IV、-2 → NT、0 → 无位,行为与旧源完全一致)。
|
||||
* qds = uint16 域位(App/algo/meas.h MEAS_QDS_*:低字节 = 线上 QDS 同值,
|
||||
* 高字节 CLIPPED 由本层映射线上 IV);返回 0 / -1 IV / -2 NT */
|
||||
@@ -83,11 +83,11 @@ uint32_t iec104_pts_yc_ioa(uint8_t idx);
|
||||
/* 读 YC:v 有效值(无效时为 0);qds 输出线上品质位(IV/NT/OV,CLIPPED 已映射
|
||||
* 为 IV)。返回 0/无效-1(NT 亦无效,品质位区分) */
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds);
|
||||
/* 死区写(104 改读快照 API;运行参数段起 = 转发 App/algo/run_par.c 真源 + 清本层确认计数):
|
||||
/* 死区写(卡 9 API;卡 10 起 = 转发 App/algo/run_par.c 真源 + 清本层确认计数):
|
||||
* db = 工程量绝对值 ≥0(电压 50 V / 电流 1 A / 频率 5 Hz 分档上限);越界/负值/
|
||||
* NaN → -1 且真源不动;成功时清零该点待确认计数(跨档位不残留半截确认——此壳
|
||||
* 行为有单测钉住)。仅 104 服务任务上下文调用。落盘 = run_par 脏 → 事件 →
|
||||
* defaultTask(运行参数段持久化链,docs/子系统设计/IEC104上行设计说明.md §3.4)。 */
|
||||
* defaultTask(卡 10 持久化链,docs/子系统设计/IEC104上行设计说明.md §3.4)。 */
|
||||
int iec104_pts_yc_set_deadband(uint8_t idx, float db);
|
||||
/* 读当前生效死区(越界 idx 返 0) */
|
||||
float iec104_pts_yc_deadband(uint8_t idx);
|
||||
@@ -104,12 +104,12 @@ void iec104_pts_yx_read(uint8_t idx, uint8_t *out);
|
||||
* 函数功能:读全表并与上次比对(遥信变位 + 遥测死区 2 点确认 + 品质变化),
|
||||
* 输出变化集
|
||||
* 入口参数:cov - 输出(可为 NULL = 只刷快照不取变化)
|
||||
* 函数说明:遥测判据(「数据可信」104 改读快照,对标参考系统 CalfYcMea 死区 2 点确认,
|
||||
* 函数说明:遥测判据(「数据可信」卡 9,对标参考系统 CalfYcMea 死区 2 点确认,
|
||||
* 短板评测口径):**连续 2 个新测量点** |v - 上次上送值| > 死区才置变化位
|
||||
* (单点尖峰不确认——噪声回摆即撤销候选);品质位(线上 qds)相对上次
|
||||
* 上送值翻转 → 当拍直接置位(不等 2 点,有效性变化必须即时可见,
|
||||
* 含 0x401d 的 IV 进出)。死区 = 运行时副本(ROM 默认 = 点表列,
|
||||
* iec104_pts_yc_set_deadband() 覆盖——运行参数段的接线点)。
|
||||
* iec104_pts_yc_set_deadband() 覆盖——卡 10 运行参数段的接线点)。
|
||||
* 测量点定义:板端默认源 = **快照换帧**(meas seq 前进;100 ms poll 对
|
||||
* 200 ms 快照,同一帧重复比对会计数两次 → seq 去重是确认语义成立的
|
||||
* 前提);注入源(单测假源)= 每次调用一测点。首次读到有效值只建基线
|
||||
|
||||
+4
-20
@@ -10,11 +10,6 @@
|
||||
* v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask,
|
||||
* 所有 socket 操作经 net_socket 线程安全 API 走消息队列,
|
||||
* netTask 仅负责 net_poll/net_process_messages,多任务并发安全
|
||||
* v1.3 2026-10-27 DTU 项目组 道G task-50:对时**成功应用**后投一条 quiet
|
||||
* 操作记录("clock set by 8888")——task-49 盘点盘出
|
||||
* "104 对时留痕、本端口不留"的同类不对称,Lead 批准补齐;
|
||||
* 被拒不落痕;epoch 时标语义未裁定,本改动不碰、不补
|
||||
* 任何时区补偿。
|
||||
* v1.2 2026-10-02 王建锋 第 8b 轮消抖:等待片长 200ms→50ms,并把各处长
|
||||
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
|
||||
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
|
||||
@@ -26,7 +21,6 @@
|
||||
#include "net_socket.h"
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h"
|
||||
|
||||
/* 深调用链模块(timeSyncTask 栈仅 2048 B,本任务链 = 建监听 → net_bind → CH395F):
|
||||
@@ -39,11 +33,11 @@
|
||||
#define TIME_SYNC_PORT 8888
|
||||
|
||||
/*
|
||||
* 头阻塞修复 + 挂起请求(pending)化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
|
||||
* 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
|
||||
* 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间
|
||||
* netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 /
|
||||
* FTP)整体停摆(历史审计遗留问题)。
|
||||
* 2026-09-22 先落地分片有界读(5ms 片),同日 pending 化后改为"挂起请求":
|
||||
* 2026-09-22 先落地分片有界读(5ms 片),同日 L3 pending 化后改为"挂起请求":
|
||||
* 每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),调用方
|
||||
* 单次超时 = 剩余总预算(总等待 ≤ TIME_SYNC_TOTAL_BUDGET_MS,语义与旧 30s
|
||||
* 一致);等待期间调用方在自己上下文睡任务通知,旧分片协议(片长/片间让出)退役。
|
||||
@@ -142,7 +136,7 @@ static void time_sync_handle_conn(int conn_sock) {
|
||||
TickType_t t0 = xTaskGetTickCount();
|
||||
|
||||
while (got < (int)sizeof(buf)) {
|
||||
/* 单次预算 = 剩余总预算(pending:等待期 netTask 零占用,调用方
|
||||
/* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方
|
||||
* 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役)
|
||||
* ⚠ 看门狗约束:本任务在 required_mask 内,而单次 net_recv_to 可睡满
|
||||
* 30s 预算 —— 等待期必须周期性喂心跳,否则"对端连上但不发时间包"这种
|
||||
@@ -182,17 +176,7 @@ static void time_sync_handle_conn(int conn_sock) {
|
||||
((uint32_t)buf[5] << 16) |
|
||||
((uint32_t)buf[6] << 8) |
|
||||
((uint32_t)buf[7]);
|
||||
if (sys_clock_set_unix(epoch) == 0) {
|
||||
/* 操作留痕(道G task-50):对时改的是装置时钟——与 104 的
|
||||
* "clock set by 104"(App/task/iec104_serv.c 对时处)同类同口径,
|
||||
* 补齐 task-49 盘出的不对称。**被拒不投**:失败不得记成"时钟被改过";
|
||||
* 连续两次成功各投一条——每次对时是独立事件,无合并语义。
|
||||
* quiet 口:本任务栈 2048 B(见本文件 DBG_LINE_BUF_MAX 段),
|
||||
* syslog_post 的两个满态留痕分支各内联约 216 B 行缓冲,不选它。
|
||||
* ⚠ 记录里的时标语义(本端口写钟面、104 写 UTC 的 8 小时差)
|
||||
* 未裁定——留痕按现有通道原样落,不做任何时区补偿。 */
|
||||
syslog_post_quiet(SYSLOG_OPER, "clock set by 8888");
|
||||
}
|
||||
sys_clock_set_unix(epoch);
|
||||
DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch);
|
||||
}
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user