refactor(编号): 16 个测试模块码单词化 + 门禁识别面连带放宽

批 2(用户授权改名 + 「新名要一眼看懂」裁定)
- 16 个有歧义模块码单词化,共 1,146 处随 scripts/rename_ids.py 改写:
  AI→ADCIN、AH→ADCHEALTH、PS→PROTSET、PE→PROTELEM、PH→PHASOR、
  TS→TIMESYNC、VL→VOLLOCK、NP→NANDPORT、CFG→CONFIG、CLK→SYSCLK、
  WD→WATCHDOG、STO→STORAGE、SH→SHA256、FA→FTPAUTH、FR→FREQ、104→IEC104
- 不动 NET/GD5F/RTC/SOE/UI/ADC/LOG/RS485/TOOL(已是单词、芯片名或业界通用词,
  改了纯属 churn)
- 别名按全局登记:这批字面量已完全限定(TC-<码>-<nnn>),不存在裸形多义
- 映射表存档 scripts/rename_maps/batch2.json 作审计轨迹;§6 模块码表同步

连带修门禁(关键教训)
- 模块码涨到 9 位(ADCHEALTH/WATCHDOG)后,候选正则与族正则的 {0,4} 上界
  会让改名后的号**整批从门禁眼里消失**:实测编号字面量从 8271 掉到 7315、
  悬空假降 490 处。已放宽到 {0,9} 并写进 §6 注记
- 结论:改名批次必须连带复核识别面,只改数据不改判据 = 静默放行;
  这次是靠「可判定比例跌破基线即失败」那条线拦下来的

现状与遗留
- 校验 1 未登记前缀 0 处;可判定比例 82.71%(基线 82.6%,只涨不跌成立)
- 遗留:7 个模块的「单元测试说明.md」仍缺(PROTSET 116、NANDPORT 109、
  PROTELEM 65、FREQ 48、FTPAUTH 45、SHA256 25、SOE 14 处悬空)——改名不解决
  缺文档,处置方式待议
- 遗留:T-2/T-3/T-4/T-6 历史任务号悬空(别名表未收全,属本工具批次漏项)
- 未改任何 C/H 可编译语义(仅注释与宏名内的编号),故不需 @build
This commit is contained in:
2026-10-21 09:53:28 +08:00
parent 301c3e5183
commit bda7ab2b50
45 changed files with 3706 additions and 1295 deletions
+1 -1
View File
@@ -455,7 +455,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| **八项校验**([示例] 本行引用的是各种非法形态示例,不是真实编号) | ①未登记前缀 ②形态不合规(裸形 `S3` / 空格形 `Trap 07` / 中文形 `陷阱 1` / 补零错 / 档位词带后缀 / 大写后缀 / 缺模块码)③悬空引用 ④重复定义 ⑤冻结前缀越游标 ⑥文档内编号未紧邻文档名 ⑦孤儿条目(告警)⑧可判定比例(只许涨不许跌) |
| **配置两份** | `scripts/id_registry.json`:前缀族 → 属主登记处 / 形态正则 / 是否冻结 / 游标,**逐项来自 `docs/编号规范.md` §4 总表**,游标取「规范记载值」与「全库实测最大号」的较大者,偏差写进 `cursor_note`(实测偏差见规范 §8.2);`scripts/id_aliases.json`:旧号 → 新号 / 作废标记(照搬 `section_aliases.json` 的成功范式) |
| **判定口径** | 「定义」= 字面量出现在该族**属主登记处文档**里(不依赖表格行首等易漂移形态)→ §5.5「销项 = 删行 + 清全库引用」只做一半,校验 ③ 立刻报(`N-31`/`N-32` 正是这条)。「重复定义」= 字面量出现在**定义位**(表格首格 / 标题行 / 列表项行首),正文提到不算,否则任何交叉引用都会误判成事故 |
| **顺带收编** | `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-AI-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径 |
| **顺带收编** | `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-ADCIN-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径 |
| **首轮实测(2026-10-20)** | 8271 处编号字面量 / 可判定 **82.3%**(6808/8271);错误级:②416 ③626 ⑥269,告警级:③239 ⑦88。错误集中在 `TC-PS-*`/`TC-PE-*`/`TC-AH-*`/`TC-SOE-*`(**主机侧单测有码无文档**,正是 §7.2 列的"缺文档 = 追溯断链")与报告自有编号被裸引(`D-x`/`R-x`/`F-x`/`P0-x`,属 §9 阶段 1-③)。逐项数字与口径见 `docs/编号规范.md` §8.1 |
| **有牙实测(牙齿)**([示例] 下列字面量是注入用的假编号) | 注入打在临时档(`git add -N` 进索引,跑完 `git rm --cached` + 删除,`git status` 干净):`Z-99`→①、裸形 `S3`→②、`TC-NET-9999`→③、第二篇属主文档重复定义 `DP-01`→④、`E-70`→⑤(`E-xx` 冻结于 `E-63`)、裸引 `D-7`→⑥;六项分别被对应校验点名,`--strict` 退出码 1 |
| **显式盲区(不是"检查通过")** | ① 裸形同字母多义(§1.2:`P1` 五义 / `L1` 四义 / `U1` 四义 / `D7` 三义)与元器件位号同形(§4.4)→ 裸形族**只在属主登记处文档内**判越游标,文档外降告警;② 圈号编号(①②③…)与正常列举无法机械区分,不入判定;③ `docs/芯片手册/`、`docs/硬件平台/` 整体排除(位号 `B1`/`C1`/`L1` 与编号同形,是撞形根源)。前两块收口属 §9 阶段 1-②/1-③ 迁移批次,落地后再扩判定范围——盲区登记在 `id_registry.json` 的 `blind_spots` / `unmanaged_bare` |
@@ -127,7 +127,7 @@ LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o`
1. `@build`(0 错误 0 警告)
2. 产品构建 `@test`(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`):标记校验 PASS,且**零 `[ERR]`**(默认 Forbidden 已含 `[ERR]`)
3. 存储套件(`test_config.h` → `TEST_SUITE_STORAGE`):只读模式实测 **3/3 PASSED**(破坏性关,卷内容不变);开启 `ENABLE_DESTRUCTIVE_TESTS` 后为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STO-01/02/03/05/06)
3. 存储套件(`test_config.h` → `TEST_SUITE_STORAGE`):只读模式实测 **3/3 PASSED**(破坏性关,卷内容不变);开启 `ENABLE_DESTRUCTIVE_TESTS` 后为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STORAGE-01/02/03/05/06)
4. GD5F 套件(`TEST_SUITE_GD5F`):只读模式实测 **16/16 PASSED**(含只读的守卫负路径 TC-GD5F-704);开启 `ENABLE_DESTRUCTIVE_TESTS` 后 **68/68 PASSED**,并确认日志出现 2 条守卫负路径 `[ERR]`(TC-GD5F-704/705)——破坏性模式运行须放宽 Forbidden(`--forbidden "FAIL;HardFault"`)
5. 恢复跑(`ENABLE_STORAGE_RECOVERY_TESTS`):期望 **3/3 PASSED**
6. **破坏性用例默认关闭**(见 `test_config.h` 的 `ENABLE_DESTRUCTIVE_TESTS`):写/擦 NAND、格式化、坏块注入与 FatFS 写类用例只在首次上电/产线首启或明确需要时开启——2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200,且该计数持久化在 dhara journal checkpoint 头里,`nand_ftl_format` 清不掉);日常回归请保持只读模式。容量满值 = **96064 pages(187 MB)**,被扣掉的容量可用 `ENABLE_STORAGE_RECOVERY_TESTS` + `STORAGE_RECOVERY_FULL_WIPE`(全片擦除 + 重建 BBT)恢复(实测 95200 → 96064 ✓)
@@ -318,7 +318,7 @@ t=1000 µs 下一个 TIM6 更新事件
只统计"从未成功过"、抓不到它。
**硬化排序**:① ✅ **已实施(2026-10-10)**——`stm32_scan()` 两条失败支路(启动失败 / 轮询
超时)都清 `s_stm32_valid` → 本拍 STM32 侧判为无有效数据,走既有 `skip` 整帧跳过路径,
下一拍成功自动恢复;主机侧新增 **TC-AI-070..078** 钉死并做反向验证(改回旧行为 3 条立即
下一拍成功自动恢复;主机侧新增 **TC-ADCIN-070..078** 钉死并做反向验证(改回旧行为 3 条立即
FAIL)。② ✅ **已实施(2026-10-10)= DMA**:见 **§11**——TIM3 更新事件经 TRGO 硬件触发
ADC1 一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)`(乒乓双缓冲)由 DMA 自动搬运,
任务侧 `HAL_ADC_PollForConversion` **全部删除**,实测单拍 `adcp` **122 µs → 8 µs**;
@@ -588,7 +588,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配**
→ 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。
- **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §10.4;单测 TC-AI-070..078 钉死)。
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §10.4;单测 TC-ADCIN-070..078 钉死)。
### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线
@@ -582,7 +582,7 @@ if (nand_ftl_format() == 0) {
10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。
11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。
12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。
13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
13. **BBT 恢复模式** — 运行期坏块会随 TC-STORAGE-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
14. **lftpd 路径口径(归档更正 2026-10-13:原误记于 `docs/问题记录/CH395F_Trap_Records.md`——非 CH395F 议题,移入本节)** — FTP STOR/CWD 曾频发 `FR_INVALID_NAME`(6):真约束即上条第 10 项的严格 8.3。原记录"FatFS 不接受 `/` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录),**已删**。现行口径:`lftpd_io.c` 的 `to_fatfs_path()` 剥 `/` 前缀(行为正确,保留)+ 本仓路径用 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。
### 10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19)
@@ -114,34 +114,34 @@ test/host/adc_input/adc_input_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-AI-001** | 前置:注入 `HAL_ADC_Start` 失败 → poll 后失败计数 = 1 | 统计可见性(前置构造) |
| **TC-AI-002** | 前置:注入 TPAFE 有效帧 → poll 后 `UA` 可取数且值正确 | 取数通路(前置构造) |
| **TC-AI-003** | `adc_input_init()` 返回 0 | 映射表自检通过 |
| **TC-AI-004** | init 后 `adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN` | 状态码复位 |
| **TC-AI-005** | init 后 `adc_input_stm32_errs() == 0` | 失败计数归零 |
| **TC-AI-006** | init 后 TPAFE / STM32 **两侧** `adc_input_raw()` 均返回 -2 | 有效标志被清(不会取到上一轮陈旧值) |
| **TC-ADCIN-001** | 前置:注入 `HAL_ADC_Start` 失败 → poll 后失败计数 = 1 | 统计可见性(前置构造) |
| **TC-ADCIN-002** | 前置:注入 TPAFE 有效帧 → poll 后 `UA` 可取数且值正确 | 取数通路(前置构造) |
| **TC-ADCIN-003** | `adc_input_init()` 返回 0 | 映射表自检通过 |
| **TC-ADCIN-004** | init 后 `adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN` | 状态码复位 |
| **TC-ADCIN-005** | init 后 `adc_input_stm32_errs() == 0` | 失败计数归零 |
| **TC-ADCIN-006** | init 后 TPAFE / STM32 **两侧** `adc_input_raw()` 均返回 -2 | 有效标志被清(不会取到上一轮陈旧值) |
### 2.2 语义映射表(`adc_input_desc`)
| 用例 | 断言(语义 → 来源 / 下标) | 权威口径 |
|---|---|---|
| **TC-AI-010** | `UA` → TPAFE `buf[6]` | `docs/硬件平台/ADC通道映射说明.md` |
| **TC-AI-011** | `UB` → STM32 `CH14` | 同上 |
| **TC-AI-012** | `UC` → STM32 `CH3` | 同上 |
| **TC-AI-013** | `UN` → TPAFE `buf[5]` | 同上 |
| **TC-AI-014** | `IA` → TPAFE `buf[2]` | 同上 |
| **TC-AI-015** | `IB` → STM32 `CH6` | 同上 |
| **TC-AI-016** | `IC` → STM32 `CH1` | 同上 |
| **TC-AI-017** | `IN` → STM32 `CH2` | 同上 |
| **TC-AI-018** | `TEMP` → STM32 `CH5`(V_ZL2) | 同上 |
| **TC-AI-019** | `adc_input_desc(ADC_SEM_COUNT)` → `NULL` | 越界防御 |
| **TC-AI-01A** | `adc_input_desc((adc_semantic_t)-1)` → `NULL` | 无符号比较兜底 |
| **TC-ADCIN-010** | `UA` → TPAFE `buf[6]` | `docs/硬件平台/ADC通道映射说明.md` |
| **TC-ADCIN-011** | `UB` → STM32 `CH14` | 同上 |
| **TC-ADCIN-012** | `UC` → STM32 `CH3` | 同上 |
| **TC-ADCIN-013** | `UN` → TPAFE `buf[5]` | 同上 |
| **TC-ADCIN-014** | `IA` → TPAFE `buf[2]` | 同上 |
| **TC-ADCIN-015** | `IB` → STM32 `CH6` | 同上 |
| **TC-ADCIN-016** | `IC` → STM32 `CH1` | 同上 |
| **TC-ADCIN-017** | `IN` → STM32 `CH2` | 同上 |
| **TC-ADCIN-018** | `TEMP` → STM32 `CH5`(V_ZL2) | 同上 |
| **TC-ADCIN-019** | `adc_input_desc(ADC_SEM_COUNT)` → `NULL` | 越界防御 |
| **TC-ADCIN-01A** | `adc_input_desc((adc_semantic_t)-1)` → `NULL` | 无符号比较兜底 |
### 2.3 映射表自检(`map_check`,静态)
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-AI-020** | `map_check() == 0` | 真实映射表自洽:TPAFE 下标 < 8;每个 STM32 映射通道都出现在扫描 Rank 内 |
| **TC-ADCIN-020** | `map_check() == 0` | 真实映射表自洽:TPAFE 下标 < 8;每个 STM32 映射通道都出现在扫描 Rank 内 |
> 反向路径(表不自洽 → -1)**结构上不可达**,见 §4.1。
@@ -149,27 +149,27 @@ test/host/adc_input/adc_input_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-AI-021** | 有新帧 → 返回 0 | 正常路径 |
| **TC-AI-022** | `adc_input_stm32_rc() == RC_OK` | 状态码 |
| **TC-AI-023** | 成功不计入失败计数 | 统计语义 |
| **TC-AI-024** | `HAL_ADC_Start_DMA` 只在 `init()` 挂**一次** | 外设生命周期(每拍不重启) |
| **TC-AI-025** | DMA 完成计数 `seq` = 1 | 计数只由 HAL 回调推进 |
| **TC-AI-026** | ⭐ **Rank → 通道号映射**:`UB` = CH14 / rank9 = 108 | **本层核心判据**(板端无法验) |
| **TC-AI-027** | `UC` = CH3 / rank4 = 103 | 同上 |
| **TC-AI-028** | `IB` = CH6 / rank7 = 106 | 同上 |
| **TC-AI-029** | `IC` = CH1 / rank2 = 101 | 同上 |
| **TC-AI-02A** | `IN` = CH2 / rank3 = 102 | 同上 |
| **TC-AI-02B** | `TEMP` = CH5 / rank6 = 105 | 同上 |
| **TC-AI-02C** | 取帧后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 |
| **TC-AI-080..085** | ⭐ **乒乓双缓冲**:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,`stm32_take()` 仍必须读到**刚完成那一半**(108 / 208) | **撕裂帧防线**(读错半区 = 半新半旧混合帧) |
| **TC-AI-086..089** | 连投两帧不消费 → 取到最新帧(408)且 `gaps()` = 1、`errs()` = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停) |
| **TC-AI-090..09A** | `HAL_ADC_ErrorCallback(&hadc1)` ×2 → `hw_errs()` = 2;用**别的句柄**调三个回调 → `seq` / `hw_errs` 均不动 | 三个 HAL 弱回调的 `hadc == &hadc1` **句柄守卫** |
| **TC-AI-030..034** | `HAL_ADC_Start_DMA` 返回 `HAL_ERROR` → `init()` 返回 **-2**、状态码 `RC_START`、计数 +1、STM32 侧无有效数据 | 挂载失败(一次性,非每拍) |
| **TC-AI-035..039** | 无任何 DMA 完成 → `RC_STALL`、**单拍不计故障**、`seq` 不前进、STM32 侧无有效数据 | 无新帧路径 + 相位伪影不误报 |
| **TC-AI-03A..03E** | 连续两拍无新帧 → `errs()` = 1;第三拍 → 2;恢复拍成功且 `gaps()` 仍 0 | 停摆判据 = **连续 ≥2 拍**;跨次单调累计 |
| **TC-AI-070..078** | ⭐ **停摆拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入无新帧拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) |
| **TC-ADCIN-021** | 有新帧 → 返回 0 | 正常路径 |
| **TC-ADCIN-022** | `adc_input_stm32_rc() == RC_OK` | 状态码 |
| **TC-ADCIN-023** | 成功不计入失败计数 | 统计语义 |
| **TC-ADCIN-024** | `HAL_ADC_Start_DMA` 只在 `init()` 挂**一次** | 外设生命周期(每拍不重启) |
| **TC-ADCIN-025** | DMA 完成计数 `seq` = 1 | 计数只由 HAL 回调推进 |
| **TC-ADCIN-026** | ⭐ **Rank → 通道号映射**:`UB` = CH14 / rank9 = 108 | **本层核心判据**(板端无法验) |
| **TC-ADCIN-027** | `UC` = CH3 / rank4 = 103 | 同上 |
| **TC-ADCIN-028** | `IB` = CH6 / rank7 = 106 | 同上 |
| **TC-ADCIN-029** | `IC` = CH1 / rank2 = 101 | 同上 |
| **TC-ADCIN-02A** | `IN` = CH2 / rank3 = 102 | 同上 |
| **TC-ADCIN-02B** | `TEMP` = CH5 / rank6 = 105 | 同上 |
| **TC-ADCIN-02C** | 取帧后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 |
| **TC-ADCIN-080..085** | ⭐ **乒乓双缓冲**:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,`stm32_take()` 仍必须读到**刚完成那一半**(108 / 208) | **撕裂帧防线**(读错半区 = 半新半旧混合帧) |
| **TC-ADCIN-086..089** | 连投两帧不消费 → 取到最新帧(408)且 `gaps()` = 1、`errs()` = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停) |
| **TC-ADCIN-090..09A** | `HAL_ADC_ErrorCallback(&hadc1)` ×2 → `hw_errs()` = 2;用**别的句柄**调三个回调 → `seq` / `hw_errs` 均不动 | 三个 HAL 弱回调的 `hadc == &hadc1` **句柄守卫** |
| **TC-ADCIN-030..034** | `HAL_ADC_Start_DMA` 返回 `HAL_ERROR` → `init()` 返回 **-2**、状态码 `RC_START`、计数 +1、STM32 侧无有效数据 | 挂载失败(一次性,非每拍) |
| **TC-ADCIN-035..039** | 无任何 DMA 完成 → `RC_STALL`、**单拍不计故障**、`seq` 不前进、STM32 侧无有效数据 | 无新帧路径 + 相位伪影不误报 |
| **TC-ADCIN-03A..03E** | 连续两拍无新帧 → `errs()` = 1;第三拍 → 2;恢复拍成功且 `gaps()` 仍 0 | 停摆判据 = **连续 ≥2 拍**;跨次单调累计 |
| **TC-ADCIN-070..078** | ⭐ **停摆拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入无新帧拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) |
> ⚠ **为什么 TC-AI-034 照不出这条缺陷**:它是"`adc_input_init()` 后**立刻**失败",此时
> ⚠ **为什么 TC-ADCIN-034 照不出这条缺陷**:它是"`adc_input_init()` 后**立刻**失败",此时
> `s_stm32_valid` 本来就是 0,断言 -2 恒成立。真正的缺陷路径是**成功过之后再失败**:
> 本层只有一份 `s_stm32_raw[]`,而有效标志此前**只在成功时置 1、失败时从不清零** →
> 消费侧照样取数、照样压入采样环(`adc_input_poll()` 返回值只由 TPAFE 决定,
@@ -177,7 +177,7 @@ test/host/adc_input/adc_input_test.c
> 修复 = 失败支路清 `s_stm32_valid`(`App/algo/adc_input.c`)。
> **这组用例在轮询版与 DMA 版都保留**:触发条件从"轮询超时"变成"本拍无新帧",
> **同一条缺陷、同一个防线**。
> **反向验证已做(v2.0)**:把停摆支路改回 `valid=1` → TC-AI-039 / 073 / 074 / 074B 四条立即
> **反向验证已做(v2.0)**:把停摆支路改回 `valid=1` → TC-ADCIN-039 / 073 / 074 / 074B 四条立即
> FAIL(89/93 时点);把半区奇偶取反 → 11 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL;
> gap 阈值改 `>2` → 1 条 FAIL → 证明这些用例真的咬得住。
@@ -185,45 +185,45 @@ test/host/adc_input/adc_input_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-AI-040** | `out == NULL` → -1 | 参数防御 |
| **TC-AI-041** | `ch == ADC_SEM_COUNT` → -1 | 越界防御 |
| **TC-AI-042** | `ch == (adc_semantic_t)-1` → -1 | 负数兜底 |
| **TC-AI-043** | 参数错时**不写** `*out` | 无副作用 |
| **TC-AI-044** | TPAFE 无有效帧 → -2 | 有效标志语义 |
| **TC-AI-045** | STM32 无有效扫描 → -2 | 有效标志语义 |
| **TC-AI-049** | `UA` → `buf[6]` 的值(注入 3006) | 按**下标**取数(非按枚举序) |
| **TC-AI-04A** | `UN` → `buf[5]` 的值(注入 2005) | 同上 |
| **TC-AI-04B** | `IA` → `buf[2]` 的值(注入 **-1234**) | 16 位补码负值原样保留(不截断 / 不取绝对值) |
| **TC-ADCIN-040** | `out == NULL` → -1 | 参数防御 |
| **TC-ADCIN-041** | `ch == ADC_SEM_COUNT` → -1 | 越界防御 |
| **TC-ADCIN-042** | `ch == (adc_semantic_t)-1` → -1 | 负数兜底 |
| **TC-ADCIN-043** | 参数错时**不写** `*out` | 无副作用 |
| **TC-ADCIN-044** | TPAFE 无有效帧 → -2 | 有效标志语义 |
| **TC-ADCIN-045** | STM32 无有效扫描 → -2 | 有效标志语义 |
| **TC-ADCIN-049** | `UA` → `buf[6]` 的值(注入 3006) | 按**下标**取数(非按枚举序) |
| **TC-ADCIN-04A** | `UN` → `buf[5]` 的值(注入 2005) | 同上 |
| **TC-ADCIN-04B** | `IA` → `buf[2]` 的值(注入 **-1234**) | 16 位补码负值原样保留(不截断 / 不取绝对值) |
### 2.6 采集一拍(`adc_input_poll`)
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-AI-046** | 快照返回 1 → `poll()` 返回 1 | 返回码透传 |
| **TC-AI-047** | 快照被请求 **8** 通道 | 与 `ADC_INPUT_TPAFE_CH_NUM` 一致 |
| **TC-AI-048** | 快照目的缓冲非空 | 不传 NULL 给驱动 |
| **TC-AI-050** | 快照返回 0(无新帧)→ `poll()` 返回 0 | 无新帧语义 |
| **TC-AI-051** | 无新帧时 TPAFE 侧无效 | 不置有效标志 |
| **TC-AI-052** | 同拍 STM32 扫描**照常执行**(`rc == RC_OK`) | 两侧独立 |
| **TC-AI-053** | 快照返回 -1(参数错)→ `poll()` 返回 -1 | 错误透传 |
| **TC-AI-054** | 参数错时 TPAFE 侧无效 | 不置有效标志 |
| **TC-AI-055** | 已有有效帧后再"无新帧" → **保持**上次有效值 | 有效标志只置不清 |
| **TC-AI-056** | STM32 失败**不影响** `poll()` 返回值(仍为 1) | 两侧解耦 |
| **TC-AI-057** | STM32 失败不影响 TPAFE 取数 | 同上 |
| **TC-AI-058** | STM32 失败另由 `adc_input_stm32_rc()` 暴露 | 故障通道分离 |
| **TC-ADCIN-046** | 快照返回 1 → `poll()` 返回 1 | 返回码透传 |
| **TC-ADCIN-047** | 快照被请求 **8** 通道 | 与 `ADC_INPUT_TPAFE_CH_NUM` 一致 |
| **TC-ADCIN-048** | 快照目的缓冲非空 | 不传 NULL 给驱动 |
| **TC-ADCIN-050** | 快照返回 0(无新帧)→ `poll()` 返回 0 | 无新帧语义 |
| **TC-ADCIN-051** | 无新帧时 TPAFE 侧无效 | 不置有效标志 |
| **TC-ADCIN-052** | 同拍 STM32 扫描**照常执行**(`rc == RC_OK`) | 两侧独立 |
| **TC-ADCIN-053** | 快照返回 -1(参数错)→ `poll()` 返回 -1 | 错误透传 |
| **TC-ADCIN-054** | 参数错时 TPAFE 侧无效 | 不置有效标志 |
| **TC-ADCIN-055** | 已有有效帧后再"无新帧" → **保持**上次有效值 | 有效标志只置不清 |
| **TC-ADCIN-056** | STM32 失败**不影响** `poll()` 返回值(仍为 1) | 两侧解耦 |
| **TC-ADCIN-057** | STM32 失败不影响 TPAFE 取数 | 同上 |
| **TC-ADCIN-058** | STM32 失败另由 `adc_input_stm32_rc()` 暴露 | 故障通道分离 |
### 2.7 状态码与失败计数观测
| 用例 | 断言 |
|---|---|
| **TC-AI-060** | init 后未取帧 → `RC_NOT_RUN(-3)` |
| **TC-AI-061** | init 后各计数 = 0 |
| **TC-AI-062** | 一次成功后 `rc == OK` 且 `errs` 不变 |
| **TC-AI-063** | 连续两拍无新帧 → `errs == 1`(单拍伪影不计) |
| **TC-AI-064** | `rc` = **最近一次**(`RC_STALL`) |
| **TC-AI-065** | 恢复成功后 `rc == OK` |
| **TC-AI-066** | `errs` **单调不回退**(不因成功回落) |
| **TC-AI-067** | `seq` 只由 DMA 完成回调推进(消费动作不改它) |
| **TC-ADCIN-060** | init 后未取帧 → `RC_NOT_RUN(-3)` |
| **TC-ADCIN-061** | init 后各计数 = 0 |
| **TC-ADCIN-062** | 一次成功后 `rc == OK` 且 `errs` 不变 |
| **TC-ADCIN-063** | 连续两拍无新帧 → `errs == 1`(单拍伪影不计) |
| **TC-ADCIN-064** | `rc` = **最近一次**(`RC_STALL`) |
| **TC-ADCIN-065** | 恢复成功后 `rc == OK` |
| **TC-ADCIN-066** | `errs` **单调不回退**(不因成功回落) |
| **TC-ADCIN-067** | `seq` 只由 DMA 完成回调推进(消费动作不改它) |
**当前结果**:**101/101 PASSED**(`failed=0`)。
+22 -22
View File
@@ -526,7 +526,7 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
> - 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。
> - 容量随 dhara 计到的坏块数递减,且该计数**持久化在 journal checkpoint 头里**
> (`journal.c` `hdr_set_bb_current/bb_last`);`dhara_journal_clear()` 只做
> `bb_last = bb_current`(**不归零**)——所以 `nand_ftl_format()`(TC-STO-04 普通恢复)
> `bb_last = bb_current`(**不归零**)——所以 `nand_ftl_format()`(TC-STORAGE-04 普通恢复)
> **清不掉被扣掉的容量**,断电重扫也一样。
> - 恢复满容量:启用 `ENABLE_STORAGE_RECOVERY_TESTS` **并叠加** `STORAGE_RECOVERY_FULL_WIPE`
> 跑一次 STORAGE 套件(擦净 dhara 可见区 2044 块 + 重建 BBT),随后断电重启——
@@ -643,7 +643,7 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
> 本套件独立于 GD5F 驱动套件,由 `test_config.h` 的 `TEST_SUITE_STORAGE` 选择,
> 入口 `storage_test_run()`(`test/board/storage_test_task.c`),统一在 `ch395fTestTask` 中调度。
> 当前 `storage_test_run()` 已覆盖 mkfs/mount/文件写读/性能(与阶段 9 FatFs 同类路径),
> 并新增下列三项**集成测试**(TC-STO-01~03),覆盖 FTL journal 掉电恢复、大文件/随机访问、坏块注入下的存储可用性。
> 并新增下列三项**集成测试**(TC-STORAGE-01~03),覆盖 FTL journal 掉电恢复、大文件/随机访问、坏块注入下的存储可用性。
> 三项均已于 STORAGE 套件实测通过(`=== Storage Tests: 11/11 PASSED (failed=0) ===`)。
>
> **已知陷阱(已修复)**:FTL 页缓存用 `s_cached_lpn = 0` 表示"缓存未加载",但 LPN 0 是合法页(FAT 引导扇区所在页)。
@@ -651,11 +651,11 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
> 引导扇区签名变成 `0000`,FatFs 报 `FR_NO_FILESYSTEM`。正常启动因 `f_mkfs` 兜底分支掩盖了此问题(第二次挂载才真正读盘)。
> 修复:将"缓存空"哨兵改为非法值 `(dhara_sector_t)-1`(0xFFFFFFFF,远超实际容量),涉及 `disk_initialize` / `nand_ftl_deinit` / `nand_ftl_format`。
### TC-STO-01: FTL 掉电恢复(journal 持久化)
### TC-STORAGE-01: FTL 掉电恢复(journal 持久化)
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-01 |
| **ID** | TC-STORAGE-01 |
| **优先级** | P1 |
| **类型** | 集成/可靠性测试 |
| **标题** | 写入文件并同步后,可经 `dhara_map_resume` 恢复映射,文件不丢 |
@@ -666,11 +666,11 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
| **依赖** | `nand_ftl_deinit()`(清 `s_initialized`/`s_cached_lpn`,使下次 init 真正重新 resume) |
| **实测** | PASS(STORAGE 套件:resume 后 `pl.dat` 内容逐字节一致;修复 `s_cached_lpn` 哨兵 bug 后通过) |
### TC-STO-02: 大文件 / 多扇区 / 随机 seek
### TC-STORAGE-02: 大文件 / 多扇区 / 随机 seek
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-02 |
| **ID** | TC-STORAGE-02 |
| **优先级** | P1 |
| **类型** | 功能/压力测试 |
| **标题** | 256KB 文件顺序写读 + 随机偏移 seek 读一致性 |
@@ -680,25 +680,25 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
| **覆盖原则** | FTL 大范围 LBA 映射 / 垃圾回收 / FatFs `f_lseek` + `f_read` |
| **实测** | PASS(STORAGE 套件:256KB 顺序 + 随机 seek 读回一致) |
### TC-STO-03: 坏块注入下存储写入
### TC-STORAGE-03: 坏块注入下存储写入
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-03 |
| **ID** | TC-STORAGE-03 |
| **优先级** | P1 |
| **类型** | 负向/健壮性测试 |
| **标题** | 运行时注入坏块后,FatFS 仍可正常写读且坏块未被占用 |
| **前置条件** | `gd5f2gq5ue_init()` 成功;FTL 已复位 |
| **测试步骤** | 1. `nand_ftl_format()` 清空旧 map → 选若干出厂好块 `gd5f2gq5ue_mark_block_bad()` 注入(更新并持久化 BBT)<br>2. `nand_ftl_deinit()` + `nand_ftl_init()` 使 dhara 经 `is_block_bad` 看到新坏块<br>3. `f_mkfs` 重建文件系统 → 正常文件写读 `bbt.dat` 并校验<br>4. 校验注入坏块仍 `gd5f2gq5ue_is_block_bad()==1` |
| **通过标准** | 文件内容完整;注入坏块未被 dhara 分配使用(仍报告坏),存储可用 |
| **注意事项** | 本例会**永久**将若干好块标记为坏并持久化(测试设备专用);如需恢复,运行 TC-STO-04(启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 STORAGE 套件一次)即可重建 BBT + 重新格式化 |
| **注意事项** | 本例会**永久**将若干好块标记为坏并持久化(测试设备专用);如需恢复,运行 TC-STORAGE-04(启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 STORAGE 套件一次)即可重建 BBT + 重新格式化 |
| **实测** | PASS(STORAGE 套件:注入 3 坏块后文件完整且坏块仍报告坏) |
### TC-STO-04: 重建 BBT + 重新格式化(恢复)
### TC-STORAGE-04: 重建 BBT + 重新格式化(恢复)
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-04 |
| **ID** | TC-STORAGE-04 |
| **优先级** | P1 |
| **类型** | 恢复/清理测试 |
| **标题** | 重建 BBT(重新扫描出厂坏块并持久化)并清空 dhara map,恢复设备干净态 |
@@ -753,22 +753,22 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
| TC-GD5F-1002 | size=0 | P2 | PASS |
| TC-GD5F-1003 | 未擦除写入 | P0 | PASS |
| TC-GD5F-1004 | 写保护解除 | P1 | PASS |
| TC-STO-01 | FTL 掉电恢复(journal) | P1 | PASS |
| TC-STO-02 | 大文件/随机 seek | P1 | PASS |
| TC-STO-03 | 坏块注入下存储写入 | P1 | PASS |
| TC-STO-04 | BBT 重建 + 重新格式化(恢复) | P1 | 受 `ENABLE_STORAGE_RECOVERY_TESTS` 控制(默认关) |
| TC-STORAGE-01 | FTL 掉电恢复(journal) | P1 | PASS |
| TC-STORAGE-02 | 大文件/随机 seek | P1 | PASS |
| TC-STORAGE-03 | 坏块注入下存储写入 | P1 | PASS |
| TC-STORAGE-04 | BBT 重建 + 重新格式化(恢复) | P1 | 受 `ENABLE_STORAGE_RECOVERY_TESTS` 控制(默认关) |
> 当前实测(2026-09-21,`ENABLE_DESTRUCTIVE_TESTS` 关闭 / 打开两种配置):
> 只读模式 `=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`(PHASE1/2/7 的 TC-704/10,无任何写操作,
> 卷内容与容量均不变);破坏性模式 `=== GD5F2GQ5UE Tests: 68/68 PASSED (failed=0) ===`(全部阶段)。
> `TEST_SUITE_STORAGE` 只读模式实测 `=== Storage Tests: 3/3 PASSED (failed=0) ===`(mount + TC-STO-05,
> 卷内容与容量不变);破坏性模式为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STO-01/02/03/05/06)。
> `TEST_SUITE_STORAGE` 只读模式实测 `=== Storage Tests: 3/3 PASSED (failed=0) ===`(mount + TC-STORAGE-05,
> 卷内容与容量不变);破坏性模式为全量集成用例(2026-09-20 实测 **20/20 PASSED**,含 TC-STORAGE-01/02/03/05/06)。
> 各套件在两种模式下均 0 错误 0 警告通过 MDK 编译。
>
> **破坏性用例清单**(仅在 `ENABLE_DESTRUCTIVE_TESTS` 打开时编译运行):
> GD5F:TC-GD5F-206/207、301~305、401~404、501~502、601~605、701~703、705、801~802、901、1003~1004
> (TC-GD5F-704 为**只读**,两次运行都跑);存储:TC-STO-01、02、03、06 与内建文件系统写读/
> 性能压测(TC-STO-05 为只读,始终运行);RTC:TC-RTC-002(写时间,跑完复原)、014(写 SRAM)、015(写报警)。
> (TC-GD5F-704 为**只读**,两次运行都跑);存储:TC-STORAGE-01、02、03、06 与内建文件系统写读/
> 性能压测(TC-STORAGE-05 为只读,始终运行);RTC:TC-RTC-002(写时间,跑完复原)、014(写 SRAM)、015(写报警)。
---
@@ -798,9 +798,9 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
| `gd5f_check_ecc` | TC-GD5F-501/502 |
| `nand_ftl_init` | TC-GD5F-801 |
| `nand_ftl_format` | TC-GD5F-802 |
| `nand_ftl_deinit` | TC-STO-01 / TC-STO-03 |
| `gd5f2gq5ue_bbt_rebuild` | TC-STO-04 |
| `storage_test_run`(存储套件) | TC-STO-01/02/03/04 |
| `nand_ftl_deinit` | TC-STORAGE-01 / TC-STORAGE-03 |
| `gd5f2gq5ue_bbt_rebuild` | TC-STORAGE-04 |
| `storage_test_run`(存储套件) | TC-STORAGE-01/02/03/04 |
---
+16 -16
View File
@@ -63,7 +63,7 @@ test/host/vol_lock/vol_lock_test.c
| **未创建 + 创建失败** | **`1`** | **否(零阻塞)** | **照常进入临界区**(无保护),事后 `release` 判空静默 |
即:**返回 1 = "你可以往下走"**,其代价是"可能没有真正持锁"。这保证了喂狗点在任何情况下
都不会被锁阻塞。`TC-VL-008`(返回 1)与 `TC-VL-008b`(`osMutexWait` 调用次数 == 0)
都不会被锁阻塞。`TC-VOLLOCK-008`(返回 1)与 `TC-VOLLOCK-008b`(`osMutexWait` 调用次数 == 0)
两条断言共同钉死该语义——只断言返回 1 是不够的,必须同时证明**没有进入任何等待路径**。
> `vol_lock_init()` 的返回值与 `wait()` 的语义**无关**:`init` 失败返回 `-1`,但
@@ -112,32 +112,32 @@ test/host/vol_lock/vol_lock_test.c
| 用例 | 断言 |
|---|---|
| **TC-VL-001** | 首次调用返回 `0`;`osMutexNew` 恰好调用 1 次;模块静态态已持有句柄 |
| **TC-VL-002** | 重复调用仍返回 `0`(幂等);**不再**创建第二个互斥量(`osMutexNew` 仍为 1 次) |
| **TC-VL-003** | `osMutexNew` 返回 `NULL` 时 `init` 返回 `-1`;**不留下半初始化句柄**(`s_lock == NULL`) |
| **TC-VL-004** | 失败后重试可成功(返回 `0`);重试确实**再次**调用了 `osMutexNew` |
| **TC-VOLLOCK-001** | 首次调用返回 `0`;`osMutexNew` 恰好调用 1 次;模块静态态已持有句柄 |
| **TC-VOLLOCK-002** | 重复调用仍返回 `0`(幂等);**不再**创建第二个互斥量(`osMutexNew` 仍为 1 次) |
| **TC-VOLLOCK-003** | `osMutexNew` 返回 `NULL` 时 `init` 返回 `-1`;**不留下半初始化句柄**(`s_lock == NULL`) |
| **TC-VOLLOCK-004** | 失败后重试可成功(返回 `0`);重试确实**再次**调用了 `osMutexNew` |
### 2.2 `vol_lock_wait` — 获取与 fail-open ⭐
| 用例 | 断言 |
|---|---|
| **TC-VL-005** | 获取成功返回 `1`;`osMutexWait` 调用 1 次;**`timeout` 原样透传**(100 → 100) |
| **TC-VL-014** | `wait(0)` 的 try 语义:返回 `1` 且透传值为 `0`(defaultTask 路径) |
| **TC-VL-006** | `osMutexWait` 返回非 `osOK` → `wait` 返回 `0`;超时路径确实尝试过等待 |
| **TC-VL-007** | 锁未创建时 `wait` **自动 `init`** 并正常获取(创建 1 次 + 等待 1 次) |
| **TC-VL-008** ⭐ | **fail-open**:锁未创建且创建失败 → 返回 **`1`**;**`osMutexWait` 调用次数 == 0**(零阻塞);`s_lock` 仍为 `NULL` |
| **TC-VL-009** | fail-open 之后 `release` 无害:`osMutexRelease` **未被调用**(判空静默返回) |
| **TC-VL-010** | 已创建时多次 `wait` **不重复创建**(`osMutexNew` 恒 1 次);每次 `wait` 各等待一次 |
| **TC-VOLLOCK-005** | 获取成功返回 `1`;`osMutexWait` 调用 1 次;**`timeout` 原样透传**(100 → 100) |
| **TC-VOLLOCK-014** | `wait(0)` 的 try 语义:返回 `1` 且透传值为 `0`(defaultTask 路径) |
| **TC-VOLLOCK-006** | `osMutexWait` 返回非 `osOK` → `wait` 返回 `0`;超时路径确实尝试过等待 |
| **TC-VOLLOCK-007** | 锁未创建时 `wait` **自动 `init`** 并正常获取(创建 1 次 + 等待 1 次) |
| **TC-VOLLOCK-008** ⭐ | **fail-open**:锁未创建且创建失败 → 返回 **`1`**;**`osMutexWait` 调用次数 == 0**(零阻塞);`s_lock` 仍为 `NULL` |
| **TC-VOLLOCK-009** | fail-open 之后 `release` 无害:`osMutexRelease` **未被调用**(判空静默返回) |
| **TC-VOLLOCK-010** | 已创建时多次 `wait` **不重复创建**(`osMutexNew` 恒 1 次);每次 `wait` 各等待一次 |
> **TC-VL-008 是本套件存在的主要理由**:真机上需堆耗尽才可能触发,板端无法构造。
> **TC-VOLLOCK-008 是本套件存在的主要理由**:真机上需堆耗尽才可能触发,板端无法构造。
### 2.3 `vol_lock_release` — 释放与判空
| 用例 | 断言 |
|---|---|
| **TC-VL-011** | 已持锁时 `release` 调用 `osMutexRelease` **一次** |
| **TC-VL-012** | 锁未创建时 `release` **静默返回**(不调用、不崩溃) |
| **TC-VL-013** | 完整周期 `init → wait → release → wait → release`:全程只创建 **1** 个互斥量、释放 **2** 次、第二次 `wait` 的 `timeout` 透传正确(100) |
| **TC-VOLLOCK-011** | 已持锁时 `release` 调用 `osMutexRelease` **一次** |
| **TC-VOLLOCK-012** | 锁未创建时 `release` **静默返回**(不调用、不崩溃) |
| **TC-VOLLOCK-013** | 完整周期 `init → wait → release → wait → release`:全程只创建 **1** 个互斥量、释放 **2** 次、第二次 `wait` 的 `timeout` 透传正确(100) |
**当前结果**:33/33 PASSED(`failed=0`)。
@@ -36,12 +36,12 @@
5. 确认日志:
```
=== Storage Test: TC-STO-04 maintenance (recovery / full wipe) ===
[PASS] TC-STO-04 bbt rebuild
=== Storage Test: TC-STORAGE-04 maintenance (recovery / full wipe) ===
[PASS] TC-STORAGE-04 bbt rebuild
wipe progress: ... 2044/2044
wiped 2044 blocks (N skipped as bad, 2044 visible)
[PASS] TC-STO-04 full wipe (2044 blocks erased)
[PASS] TC-STO-04 persisted BBT version=N
[PASS] TC-STORAGE-04 full wipe (2044 blocks erased)
[PASS] TC-STORAGE-04 persisted BBT version=N
=== Storage Recovery: 3/3 PASSED (failed=0) ===
```
@@ -329,7 +329,7 @@ cppcheck --enable=warning --error-exitcode=1 test/ 2>&1 | tee test_cppcheck_base
**结论(✅ 已闭环,2026-10-03)**:`scripts/check_test_traceability.py` 已落地并跑通,
**默认仅告警(exit 0)、不阻断提交**。当前输出 **30 处差异**(26 处"文档有、代码无" +
4 处"代码有、文档无":`TC-GD5F-1100/1101/1102`、`TC-STO-06`)。差异清单已收敛为
4 处"代码有、文档无":`TC-GD5F-1100/1101/1102`、`TC-STORAGE-06`)。差异清单已收敛为
可追踪项,后续按需补登记;**暂不升门禁**(与 §8.2 目标一致)。
### 8.3 待核实项:`TEST_SUITE_SELECTED` 未含 `TEST_SUITE_RTC` · ✅ 已核实(非缺陷)
@@ -127,52 +127,52 @@ test/host/time_sync/time_sync_test.c
> 命名 `TC-TS-NNN`(TS = Time Sync)。断言数(80)多于用例数(27),因为每个用例通常
> 包含 2~9 条子断言(后缀 `b` / `c` …)。桩状态在每用例前由 `fakes_reset()` 复位。
### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(TC-TS-001..006)
### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(TC-TIMESYNC-001..006)
| 用例 | 断言 |
|---|---|
| **TC-TS-001** | `delay_fed(0)` 既不睡眠也不喂狗(零开销快路径) |
| **TC-TS-002** | 恰好一片(50ms):1 次 `vTaskDelay(50)` + 1 次心跳 |
| **TC-TS-003** | 非整片(120ms)切成 **50 / 50 / 20** 三片,每片喂一次心跳 |
| **TC-TS-004** | 长延时(2000ms)= **40 片 × 50ms**,每片喂心跳(监听退避路径) |
| **TC-TS-005** | 不足一片(49ms)单片处理,**不**被拆成 0 |
| **TC-TS-006** | 总预算 30s = **600 片**,片长上限**恒** 50ms |
| **TC-TIMESYNC-001** | `delay_fed(0)` 既不睡眠也不喂狗(零开销快路径) |
| **TC-TIMESYNC-002** | 恰好一片(50ms):1 次 `vTaskDelay(50)` + 1 次心跳 |
| **TC-TIMESYNC-003** | 非整片(120ms)切成 **50 / 50 / 20** 三片,每片喂一次心跳 |
| **TC-TIMESYNC-004** | 长延时(2000ms)= **40 片 × 50ms**,每片喂心跳(监听退避路径) |
| **TC-TIMESYNC-005** | 不足一片(49ms)单片处理,**不**被拆成 0 |
| **TC-TIMESYNC-006** | 总预算 30s = **600 片**,片长上限**恒** 50ms |
### 2.2 `time_sync_open_listener` — 监听建立与退避(TC-TS-010..015)
### 2.2 `time_sync_open_listener` — 监听建立与退避(TC-TIMESYNC-010..015)
| 用例 | 断言 |
|---|---|
| **TC-TS-010** | 首次成功:`socket` / `bind` / `listen` 各 1 次;端口经 `net_htons(8888)`;`sin_family` = `NET_AF_INET`;成功路径**无退避延时** |
| **TC-TS-011** | `bind` 失败 → **关闭该 socket** + 退避 2000ms(40 片)+ 留痕 `DBG_ERROR`,随后重试成功 |
| **TC-TS-012** | `listen` 失败 → 关闭该 socket + 退避 2000ms,随后重试成功 |
| **TC-TS-013** | `socket` 连续失败 → **5 次后返回 -1**;5 条 `DBG_ERROR`;5 × 500ms 退避(50 片) |
| **TC-TS-014** | 第 3 次重试成功并返回**新**描述符;恰好尝试 3 次;两次失败退避 500ms × 2 |
| **TC-TS-015** | 心跳总次数 = 每轮重试(5)+ 每片延时(50);**心跳位恒为 `TASK_WD_TIMESYNC`** |
| **TC-TIMESYNC-010** | 首次成功:`socket` / `bind` / `listen` 各 1 次;端口经 `net_htons(8888)`;`sin_family` = `NET_AF_INET`;成功路径**无退避延时** |
| **TC-TIMESYNC-011** | `bind` 失败 → **关闭该 socket** + 退避 2000ms(40 片)+ 留痕 `DBG_ERROR`,随后重试成功 |
| **TC-TIMESYNC-012** | `listen` 失败 → 关闭该 socket + 退避 2000ms,随后重试成功 |
| **TC-TIMESYNC-013** | `socket` 连续失败 → **5 次后返回 -1**;5 条 `DBG_ERROR`;5 × 500ms 退避(50 片) |
| **TC-TIMESYNC-014** | 第 3 次重试成功并返回**新**描述符;恰好尝试 3 次;两次失败退避 500ms × 2 |
| **TC-TIMESYNC-015** | 心跳总次数 = 每轮重试(5)+ 每片延时(50);**心跳位恒为 `TASK_WD_TIMESYNC`** |
### 2.3 `time_sync_handle_conn` — 时间包重组状态机(TC-TS-020..029)
### 2.3 `time_sync_handle_conn` — 时间包重组状态机(TC-TIMESYNC-020..029)
| 用例 | 断言 |
|---|---|
| **TC-TS-020** | 8 字节一次收齐 → **大端**解析 epoch + 更新墙钟 1 次 + 关连接;首次请求 16 字节;单次等待片长 = 50ms;留痕 `wall clock updated` |
| **TC-TS-021** | 分段送达(4+4)→ 累积后解析正确;读了两次;第二次**只请求剩余 12 字节**;每片 50ms |
| **TC-TS-022** | 一次到达 16 字节 → **只取前 8 字节**;一次读够即停 |
| **TC-TS-023** | 前缀非 `"TIME"` → **不更新墙钟**,但仍关闭连接 |
| **TC-TS-024** | 对端关闭(`n == 0`)→ 立即退出、不解析;关闭连接 |
| **TC-TS-025** | 真错误(`n == -1`)→ 立即退出、不解析 |
| **TC-TS-026** | 只收到 4 字节后 EOF → **不足 8 字节不解析**;期间继续等待后续字节 |
| **TC-TS-027** | 30s 预算耗尽 → 不再读(只读 1 次)+ 留痕 `no TIME packet in 30s, drop conn` + 关连接 |
| **TC-TS-028** | 剩余预算不足一片 → **末片按剩余(20ms)切分** |
| **TC-TS-029** | **32 位 tick 回绕** → 预算计算仍正确(无符号减法) |
| **TC-TIMESYNC-020** | 8 字节一次收齐 → **大端**解析 epoch + 更新墙钟 1 次 + 关连接;首次请求 16 字节;单次等待片长 = 50ms;留痕 `wall clock updated` |
| **TC-TIMESYNC-021** | 分段送达(4+4)→ 累积后解析正确;读了两次;第二次**只请求剩余 12 字节**;每片 50ms |
| **TC-TIMESYNC-022** | 一次到达 16 字节 → **只取前 8 字节**;一次读够即停 |
| **TC-TIMESYNC-023** | 前缀非 `"TIME"` → **不更新墙钟**,但仍关闭连接 |
| **TC-TIMESYNC-024** | 对端关闭(`n == 0`)→ 立即退出、不解析;关闭连接 |
| **TC-TIMESYNC-025** | 真错误(`n == -1`)→ 立即退出、不解析 |
| **TC-TIMESYNC-026** | 只收到 4 字节后 EOF → **不足 8 字节不解析**;期间继续等待后续字节 |
| **TC-TIMESYNC-027** | 30s 预算耗尽 → 不再读(只读 1 次)+ 留痕 `no TIME packet in 30s, drop conn` + 关连接 |
| **TC-TIMESYNC-028** | 剩余预算不足一片 → **末片按剩余(20ms)切分** |
| **TC-TIMESYNC-029** | **32 位 tick 回绕** → 预算计算仍正确(无符号减法) |
### 2.4 `time_sync_task` — 任务主循环(TC-TS-040..044)
### 2.4 `time_sync_task` — 任务主循环(TC-TIMESYNC-040..044)
| 用例 | 断言 |
|---|---|
| **TC-TS-040** | `sys_evtHandle == NULL` → **跳过就绪屏障**;`accept` 轮询 3 次(2 次无连接 + 1 次成功);收到时间包并更新墙钟;延时片数 = 4+4+1(200+200+50ms)且**全部按 50ms 分片**;心跳次数 = 主循环 1 + 退避 8 + 收连接 1 + 收尾 1 + 次轮 1 = **13**;所有心跳均打 `TASK_WD_TIMESYNC` 位 |
| **TC-TS-041** | 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪);屏障等待按 **100ms 分片**;等待 `SYS_EVT_NET_READY` 位;`osFlagsWaitAny \| osFlagsNoClear`(**就绪位不被消费**);未就绪的两次各喂一次心跳;仍先 `task_wd_bind` 登记心跳路由 |
| **TC-TS-042** | 屏障等待 4 次均未就绪 → **循环未挂死**(跳出并继续);每次未就绪后各喂一次心跳(2026-10-02 台架根因的**回归防线**) |
| **TC-TS-043** | 网络代次变化 → **关闭旧监听**并重监听;留痕 `network re-init, relisten`(另含 2 条 listen 成功);每次循环只轮询一次 `accept` 即放弃 |
| **TC-TS-044** | 监听建立失败 → 内部重试 5 次后放弃;6 条 `DBG_ERROR`(5 条 `net_socket FAIL` + 1 条 `retry later`);退避 5×500ms + 2000ms = **90 片**;心跳次数 = 2 + 5 + 50 + 40 = **97**;所有心跳均打 `TIMESYNC` 位 |
| **TC-TIMESYNC-040** | `sys_evtHandle == NULL` → **跳过就绪屏障**;`accept` 轮询 3 次(2 次无连接 + 1 次成功);收到时间包并更新墙钟;延时片数 = 4+4+1(200+200+50ms)且**全部按 50ms 分片**;心跳次数 = 主循环 1 + 退避 8 + 收连接 1 + 收尾 1 + 次轮 1 = **13**;所有心跳均打 `TASK_WD_TIMESYNC` 位 |
| **TC-TIMESYNC-041** | 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪);屏障等待按 **100ms 分片**;等待 `SYS_EVT_NET_READY` 位;`osFlagsWaitAny \| osFlagsNoClear`(**就绪位不被消费**);未就绪的两次各喂一次心跳;仍先 `task_wd_bind` 登记心跳路由 |
| **TC-TIMESYNC-042** | 屏障等待 4 次均未就绪 → **循环未挂死**(跳出并继续);每次未就绪后各喂一次心跳(2026-10-02 台架根因的**回归防线**) |
| **TC-TIMESYNC-043** | 网络代次变化 → **关闭旧监听**并重监听;留痕 `network re-init, relisten`(另含 2 条 listen 成功);每次循环只轮询一次 `accept` 即放弃 |
| **TC-TIMESYNC-044** | 监听建立失败 → 内部重试 5 次后放弃;6 条 `DBG_ERROR`(5 条 `net_socket FAIL` + 1 条 `retry later`);退避 5×500ms + 2000ms = **90 片**;心跳次数 = 2 + 5 + 50 + 40 = **97**;所有心跳均打 `TIMESYNC` 位 |
**当前结果**:80/80 PASSED(`failed=0`)。
@@ -200,7 +200,7 @@ test/host/time_sync/time_sync_test.c
**取舍**:这比"把循环体抽成可测函数"更贴近真实——**测的就是任务主体本身**,
不需要为了可测性改动生产代码结构。代价是**必须精确设定跳出点**:跳出点设错
(多一次或少一次桩调用)会导致断言数不符,`TC-TS-040` 的跳出点曾因此修正。
(多一次或少一次桩调用)会导致断言数不符,`TC-TIMESYNC-040` 的跳出点曾因此修正。
### 4.2 ⚠ C 的"暂定定义"会静默合并同名静态量
@@ -66,7 +66,7 @@ G0~G3 命令详见 §6 / §7;ui-verify 完整命令见 `UI_自动化测试流
| 对象 | 格式 | 现用示例 |
|---|---|---|
| 阶段 | `TM-{MOD}-{NN}` | TM-PHY-01 |
| 测试用例 | `TC-{MOD}-{NNN}` | `TC-NET-601`(CH395F)、`TC-GD5F-205`、`TC-STO-03`、`TC-RTC-002` |
| 测试用例 | `TC-{MOD}-{NNN}` | `TC-NET-601`(CH395F)、`TC-GD5F-205`、`TC-STORAGE-03`、`TC-RTC-002` |
| 设计原则 | `DP-{NN}` | DP-01(单连接 socket) |
模块缩写 `{MOD}` 在新模块测试文档立文档时确定(2~4 个大写字母);现有:`NET`=CH395F 网络、`GD5F`、`STO`=存储、`RTC`、`ADC`、`RS485`。
@@ -101,7 +101,7 @@ flowchart TD
| 套件 | 宏 | 文件 | 阶段组织 | 备注 |
|---|---|---|---|---|
| CH395F | `TEST_SUITE_CH395F` | `ch395f_test.h/.c` + `net_test_task.c` | 硬件层 1~5 / NET 层 6~11 | 两层互斥(`#error`);阶段 2/3/4/5 与 6/7/8/10/11 需 PC 端配合 |
| STORAGE | `TEST_SUITE_STORAGE` | `storage_test.h` + `storage_test_task.c` | FTL + FatFS 集成(TC-STO-01~04) | 恢复测试(重建 BBT)默认关闭 |
| STORAGE | `TEST_SUITE_STORAGE` | `storage_test.h` + `storage_test_task.c` | FTL + FatFS 集成(TC-STORAGE-01~04) | 恢复测试(重建 BBT)默认关闭 |
| GD5F | `TEST_SUITE_GD5F` | `gd5f_test.h` + `gd5f_test_task.c` | 10 阶段(选中时自动全开) | NAND 驱动全覆盖 |
| RTC | `TEST_SUITE_RTC` | `sd2506_test.h` + `sd2506_test_task.c` | 阶段 1 基本 / 阶段 2 API(默认全开) | SD2506 RTC |
@@ -598,7 +598,7 @@ CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。
| `test_common.h` | **板端测试公共断言内核**(`test_stats_t` + `TEST_CHECK_IMPL`/`TEST_REPORT_IMPL`) |
| `ch395f_test.h` / `ch395f_test_task.c` | CH395F 套件:阶段开关、断言宏、Phase 1~5 实现 |
| `net_test_task.h` / `net_test_task.c` | CH395F 套件 NET 层(6~11):TCP 监听循环 / Client 主动连接测试 |
| `storage_test.h` / `storage_test_task.c` | STORAGE 套件:FTL + FatFS 集成(TC-STO-01~04)+ 恢复测试 |
| `storage_test.h` / `storage_test_task.c` | STORAGE 套件:FTL + FatFS 集成(TC-STORAGE-01~04)+ 恢复测试 |
| `gd5f_test.h` / `gd5f_test_task.c` | GD5F 套件:NAND 驱动 10 阶段 |
| `sd2506_test.h` / `sd2506_test_task.c` | RTC 套件:SD2506 RTC 2 阶段组 |
| `run_boot_test.py` / `verify_log.py` + `serial_tool` | 自动化闭环三件套(纯 Python 标准库 / 仓库内置) |
@@ -157,7 +157,7 @@ TC-{MOD}-{NNN}
```
- **分阶段套件**:序号**百位放阶段号**,编号即自文档化——`TC-NET-101` = CH395F Phase 1 第 1 项、`TC-NET-601` = Phase 6 第 1 项、`TC-NET-1101` = Phase 11 第 1 项;
- **无阶段划分套件**:全局顺序编号(如 `TC-STO-03`、`TC-RTC-001`);
- **无阶段划分套件**:全局顺序编号(如 `TC-STORAGE-03`、`TC-RTC-001`);
- 编号不复用:废弃用例保留编号并注明"已废弃"。
### 6.4 设计原则 ID
@@ -50,7 +50,7 @@ test/host/phasor/phasor_test.c
- **`fAmSQ = √(fRe² + fIm²)` 直接就是有效值(RMS)**,无需再除 √2;
- `fAm = √2 · fAmSQ` 是正弦峰值。
判据是归一化恒等式 **`Σsin² = Σcos² = 1/N`**(`TC-PH-001/002` 逐项校验,防转录错)。
判据是归一化恒等式 **`Σsin² = Σcos² = 1/N`**(`TC-PHASOR-001/002` 逐项校验,防转录错)。
术语权威见 `docs/算法/继电保护功能复现规划.md` §10.2:
`{fRe, fIm, fAm, fAmSQ}` = 实部 / 虚部 / **幅值** / **有效值**。
@@ -100,62 +100,62 @@ test/host/phasor/phasor_test.c
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-001** | 逐项:`sin[k] == sin(2πk/N)·(√2/N)`、`cos[k] == cos(2πk/N)·(√2/N)`(k = 0..19) | L2-1 防转录错 |
| **TC-PH-002** | 归一化 `Σsin² == Σcos² == 1/N`;精确零点 `sin@0,10`、`cos@5,15`;`sin[5] == √2/N` | L2-1 判据基础 |
| **TC-PHASOR-001** | 逐项:`sin[k] == sin(2πk/N)·(√2/N)`、`cos[k] == cos(2πk/N)·(√2/N)`(k = 0..19) | L2-1 防转录错 |
| **TC-PHASOR-002** | 归一化 `Σsin² == Σcos² == 1/N`;精确零点 `sin@0,10`、`cos@5,15`;`sin[5] == √2/N` | L2-1 判据基础 |
### 2.2 L2-3 / L2-4 基波幅值 / 有效值 / 相位
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-003** | 纯基波:`fAmSQ == A/√2`(**有效值**)、`fAm == √2·fAmSQ`(幅值),误差 ≤ 1% | L2-3 |
| **TC-PH-004** | `atan2(fIm, fRe)` == 注入相角(6 组幅值 / 相位) | L2-4 |
| **TC-PHASOR-003** | 纯基波:`fAmSQ == A/√2`(**有效值**)、`fAm == √2·fAmSQ`(幅值),误差 ≤ 1% | L2-3 |
| **TC-PHASOR-004** | `atan2(fIm, fRe)` == 注入相角(6 组幅值 / 相位) | L2-4 |
### 2.3 直流与谐波抑制 / 任意次谐波提取
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-005** | 直流 + 2 / 3 / 5 次谐波被**完全滤除**(基波读数不变,误差 ≤ 1%,5 例) | L2-1 |
| **TC-PH-006** | 任意次谐波提取(**索引步进 = 谐波次数**):k = 1 / 2 / 5 从混合信号中提取;纯基波下 2 / 5 次分量 ≈ 0 | L2-1 / L1-9 入口 |
| **TC-PHASOR-005** | 直流 + 2 / 3 / 5 次谐波被**完全滤除**(基波读数不变,误差 ≤ 1%,5 例) | L2-1 |
| **TC-PHASOR-006** | 任意次谐波提取(**索引步进 = 谐波次数**):k = 1 / 2 / 5 从混合信号中提取;纯基波下 2 / 5 次分量 ≈ 0 | L2-1 / L1-9 入口 |
### 2.4 频率敏感度
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-007** | 幅值误差:50 Hz ≤ 0.1%、±1 Hz ≤ 1.5%、±2 Hz ≤ 3%(**最坏相位**) | L2-3 / L1-7 依据 |
| **TC-PHASOR-007** | 幅值误差:50 Hz ≤ 0.1%、±1 Hz ≤ 1.5%、±2 Hz ≤ 3%(**最坏相位**) | L2-3 / L1-7 依据 |
### 2.5 L2 输入采样环(`sample_ring.c`)
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-010** | `init` 清填充与帧计数;未攒满取窗 → -2;入环后取窗**按时序**(老 → 新);通道之间**互相独立**;填充量饱和于 `DEPTH` 但帧计数**不**饱和;回绕后取窗 == 最后 N 个值(有序);参数守卫:`n=0` / `n>DEPTH` / 越界通道 / `NULL out` / `NULL frame` / 错帧长 均拒绝 | L2 输入 |
| **TC-PHASOR-010** | `init` 清填充与帧计数;未攒满取窗 → -2;入环后取窗**按时序**(老 → 新);通道之间**互相独立**;填充量饱和于 `DEPTH` 但帧计数**不**饱和;回绕后取窗 == 最后 N 个值(有序);参数守卫:`n=0` / `n>DEPTH` / 越界通道 / `NULL out` / `NULL frame` / 错帧长 均拒绝 | L2 输入 |
### 2.6 L2-2 实例层
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-020** | 实例数 == 5;实例表可取 `name`;越界实例 → `NULL`;未攒满一周波 `phasor_get()` → -2;越界 index / `NULL out` → -1;攒满 20 帧后 `get()` == 0 且实例 0 的 RMS == `A/√2`;谐波实例(`C04-IA-F2` / `C05-IA-F5`)在纯基波下 ≈ 0;**逐实例复核"实例窗 == 独立 `phasor_calc()` 结果"** | L2-2 |
| **TC-PHASOR-020** | 实例数 == 5;实例表可取 `name`;越界实例 → `NULL`;未攒满一周波 `phasor_get()` → -2;越界 index / `NULL out` → -1;攒满 20 帧后 `get()` == 0 且实例 0 的 RMS == `A/√2`;谐波实例(`C04-IA-F2` / `C05-IA-F5`)在纯基波下 ≈ 0;**逐实例复核"实例窗 == 独立 `phasor_calc()` 结果"** | L2-2 |
### 2.7 L2-6 滑动窗
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-021** | 连续滑动 20 拍,基波读数**恒定**(误差 ≤ 1%) | L2-6 |
| **TC-PHASOR-021** | 连续滑动 20 拍,基波读数**恒定**(误差 ≤ 1%) | L2-6 |
### 2.8 L2-7 双区指针原子交换
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-022** | 使用区逐次产出**交替** 0 → 1 → 0 → 1;**连测 60 拍**:每拍 `get()` == 当前环窗的独立重算(幅值每 20 拍在 100 / 10 之间切换 → 读到陈旧区会立刻不相等);扫描后结果仍可读 | L2-7 |
| **TC-PHASOR-022** | 使用区逐次产出**交替** 0 → 1 → 0 → 1;**连测 60 拍**:每拍 `get()` == 当前环窗的独立重算(幅值每 20 拍在 100 / 10 之间切换 → 读到陈旧区会立刻不相等);扫描后结果仍可读 | L2-7 |
### 2.9 `phasor_zero` / `phasor_init` 清零与幂等
| 用例 | 断言 | 设计点 |
|---|---|---|
| **TC-PH-030** | 直接白盒调 `phasor_zero()` → 四字段(含 `fAm` / `fAmSQ`)全清零 | 静态辅助 |
| **TC-PH-031** | 先把双区写脏 → `phasor_init()` 后**双区全零** | init 清零 |
| **TC-PH-032** | `phasor_init()` 后使用区复位为 0 | init 复位 |
| **TC-PH-033** | `phasor_init()` 后就绪标志清(`phasor_get()` → -2) | init 复位 |
| **TC-PH-034** | 重复 `phasor_init()` 幂等 | 可重复调用 |
| **TC-PHASOR-030** | 直接白盒调 `phasor_zero()` → 四字段(含 `fAm` / `fAmSQ`)全清零 | 静态辅助 |
| **TC-PHASOR-031** | 先把双区写脏 → `phasor_init()` 后**双区全零** | init 清零 |
| **TC-PHASOR-032** | `phasor_init()` 后使用区复位为 0 | init 复位 |
| **TC-PHASOR-033** | `phasor_init()` 后就绪标志清(`phasor_get()` → -2) | init 复位 |
| **TC-PHASOR-034** | 重复 `phasor_init()` 幂等 | 可重复调用 |
**当前结果**:57/57 PASSED(`failed=0`)。
@@ -178,7 +178,7 @@ test/host/phasor/phasor_test.c
每来一个新点,`phasor_run()` 对 20 点**整体重算**(滑动窗)。迭代法(加减首尾项)会累积
浮点误差且难以保证与"整体重算"逐位一致;暴力法的代价是每拍 `N × 实例数` 次乘加 + 取模,
在 1 ms 节拍下仍有余量。本单测的 `TC-PH-022`(每拍 `get()` == 独立重算)正是这条取舍的
在 1 ms 节拍下仍有余量。本单测的 `TC-PHASOR-022`(每拍 `get()` == 独立重算)正是这条取舍的
**回归防线**——一旦有人改成迭代法,逐位一致性断言会立刻失败。
### 4.2 主机侧合成波形**不代表真实硬件**
@@ -103,21 +103,21 @@ test/host/watchdog/watchdog_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-WD-001** | 未启动时 `poll` 不喂狗、`is_armed()==0` | 启动前不判定(`s_started` 门卫) |
| **TC-WD-016** | 未启动时即使全员置位也不喂、不更新 `last_mask` | 同上(位图无关) |
| **TC-WD-014** | 重复 `task_wd_start()` 只启动 IWDG 一次 | 启动幂等(IWDG 不可逆,重复启动无意义) |
| **TC-WD-015** | `start` 内部先喂一次(`feed_count==1`) | 首个抽样窗口从启动时刻起算,避免启动即超时 |
| **TC-WD-002** | 50 ms 内 `poll` 不喂;累计 100 ms 喂 1 次 | 100 ms 抽样节流 |
| **TC-WD-003** | 全员置位 → 喂狗 1 次;`last_mask==0x37` | 全员到位判定 + 掩码取值 |
| **TC-WD-004** | 不重新置位则第二轮不喂 | **取走清零**语义(旧位不残留) |
| **TC-WD-005** | 缺 UI(bit4)→ 不喂;`last_mask` 精确记录缺失位 | 必需掩码比较 + 缺失定位 |
| **TC-WD-006** | 连续 1..7 次缺席不报;**第 8 次**打 `[WD] task stall`;缺席期间不喂 | `WD_MISS_TOLERANCE=8`(800 ms)停喂触发点 |
| **TC-WD-007** | 停喂后**每 100 ms 复报**一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) |
| **TC-WD-008** | 恢复全员到位 → 立即喂狗 + `streak` 归零 | 停顿可恢复(非锁死状态机) |
| **TC-WD-009** | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口**边界**(易错点) |
| **TC-WD-010** | `tick` 跨 2^32 回绕后 100 ms 仍正确判 | 无符号差比较(回绕安全) |
| **TC-WD-011** | `is_armed`/`last_mask`/`miss_count` 语义 | 观测接口正确性 |
| **TC-WD-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图 | 防位图越界(`task_wd_beat` 的守卫) |
| **TC-WATCHDOG-001** | 未启动时 `poll` 不喂狗、`is_armed()==0` | 启动前不判定(`s_started` 门卫) |
| **TC-WATCHDOG-016** | 未启动时即使全员置位也不喂、不更新 `last_mask` | 同上(位图无关) |
| **TC-WATCHDOG-014** | 重复 `task_wd_start()` 只启动 IWDG 一次 | 启动幂等(IWDG 不可逆,重复启动无意义) |
| **TC-WATCHDOG-015** | `start` 内部先喂一次(`feed_count==1`) | 首个抽样窗口从启动时刻起算,避免启动即超时 |
| **TC-WATCHDOG-002** | 50 ms 内 `poll` 不喂;累计 100 ms 喂 1 次 | 100 ms 抽样节流 |
| **TC-WATCHDOG-003** | 全员置位 → 喂狗 1 次;`last_mask==0x37` | 全员到位判定 + 掩码取值 |
| **TC-WATCHDOG-004** | 不重新置位则第二轮不喂 | **取走清零**语义(旧位不残留) |
| **TC-WATCHDOG-005** | 缺 UI(bit4)→ 不喂;`last_mask` 精确记录缺失位 | 必需掩码比较 + 缺失定位 |
| **TC-WATCHDOG-006** | 连续 1..7 次缺席不报;**第 8 次**打 `[WD] task stall`;缺席期间不喂 | `WD_MISS_TOLERANCE=8`(800 ms)停喂触发点 |
| **TC-WATCHDOG-007** | 停喂后**每 100 ms 复报**一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) |
| **TC-WATCHDOG-008** | 恢复全员到位 → 立即喂狗 + `streak` 归零 | 停顿可恢复(非锁死状态机) |
| **TC-WATCHDOG-009** | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口**边界**(易错点) |
| **TC-WATCHDOG-010** | `tick` 跨 2^32 回绕后 100 ms 仍正确判 | 无符号差比较(回绕安全) |
| **TC-WATCHDOG-011** | `is_armed`/`last_mask`/`miss_count` 语义 | 观测接口正确性 |
| **TC-WATCHDOG-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图 | 防位图越界(`task_wd_beat` 的守卫) |
**当前结果**:39/39 PASSED(`failed=0`)。
@@ -139,7 +139,7 @@ test/host/watchdog/watchdog_test.c
## 4. 已知行为与可优化点
### 4.1 stall 日志在停喂后复报(TC-WD-007,现状)
### 4.1 stall 日志在停喂后复报(TC-WATCHDOG-007,现状)
实现(`task_watchdog.c`):
@@ -153,7 +153,7 @@ if (s_miss_streak >= WD_MISS_TOLERANCE) { DBG_ERROR("[WD] task stall: ..."); }
**评估**:这是**刻意的"最后遗言"**——复位前日志窗口内信息不丢。代价是日志冗余。
若改为"每停顿周期仅报一次",需加一个 `s_stall_reported` 标志(在 `streak` 归零时清除)。
**当前保留现状**(单测按现状断言,行为变更须同步改 TC-WD-007 与本文)。
**当前保留现状**(单测按现状断言,行为变更须同步改 TC-WATCHDOG-007 与本文)。
### 4.2 测试隔离依赖 `task_wd_test_reset()`
@@ -105,27 +105,27 @@ test/host/sys_clock/sys_clock_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-CLK-000** | 未初始化时 `get()==UINT32_MAX`、`get_time()==-1`、`get_str()` 输出占位串 | "未初始化"哨兵语义(**必须最先跑**) |
| **TC-CLK-001** | RTC 合法 → `init()==0`,基准 = 换算 Unix 秒;不写回 RTC | 正常初始化路径 |
| **TC-CLK-002** | 2024-02-29 / 2000-02-29(世纪闰)换算正确;2/29→3/1 间隔恰 86400 s | 闰年规则(含世纪闰) |
| **TC-CLK-003** | 1/31、2/28、4/30 精确值;平年 2/28→3/1 间隔 86400 s | 月末边界 |
| **TC-CLK-004** | 2000-01-01 换算;2038-01-19 03:14:07 → `0x7FFFFFFF`;`set(0)` → 1970-01-01 | 世纪边界 / uint32 上限 |
| **TC-CLK-005** | RTC 读失败 → `init()==-1`,降级为编译期默认时间,打 `DBG_ERROR` | RTC 读故障降级 |
| **TC-CLK-006** | 年/月/日越界 → 自愈写回默认时间,`init()==0` | 越界自愈(启用默认值时) |
| **TC-CLK-020** | 越界 + 写回失败 → 降级默认时间,`init()==-1` | 组合故障降级 |
| **TC-CLK-007** | 越界 → 写回 RTC 一次,阴影 RTC 被修正,`init()==0` | 自愈写回 + 复验 |
| **TC-CLK-008** | 999ms 不变 / 1000ms +1s / 再 +2500ms 共 +3s | 正常推进 + 亚秒截断 |
| **TC-CLK-009** | +29 天线性推进(未达 30 天阈值,不触发再基准化) | 阈值下不触发 |
| **TC-CLK-010** | 30 天 + 1500ms → 值无损(+30 天 1 秒);再 +500ms 补足 1 秒 | **惰性再基准化** + 余数结转 |
| **TC-CLK-011** | 基准近 2^32,推进跨回绕 0x2000ms → +8s | tick 回绕(无符号差)安全 |
| **TC-CLK-012** | 10 天步进 ×9(累计 90 天,含 3 轮再基准)零漂移 | 长期运行无漂移 |
| **TC-CLK-013** | `get_str` 格式化 + 零点补零;未初始化占位串 | 字符串接口 |
| **TC-CLK-014** | 2026-10-02 08:09:10 → 年月日时分秒 + `week==5`(周五) | 日历字段 + 星期 |
| **TC-CLK-015** | `get_time(NULL) == -1` | NULL 参数防御 |
| **TC-CLK-016** | `set` 更新基准 + 写回 RTC;写失败仍更新基准 + 打错误 | set 两条路径 |
| **TC-CLK-017** | `set(..., NULL)` 不碰 RTC | 可选 RTC 写回 |
| **TC-CLK-018** | `set_unix` 写回 RTC,日历字段 + `week` 正确 | Unix 时间戳校时 |
| **TC-CLK-019** | 16 个已知日期(跨 1~12 月 / 世纪闰 / 闰日 / 范围首尾)星期对照 | **Zeller 全域正确性** |
| **TC-SYSCLK-000** | 未初始化时 `get()==UINT32_MAX`、`get_time()==-1`、`get_str()` 输出占位串 | "未初始化"哨兵语义(**必须最先跑**) |
| **TC-SYSCLK-001** | RTC 合法 → `init()==0`,基准 = 换算 Unix 秒;不写回 RTC | 正常初始化路径 |
| **TC-SYSCLK-002** | 2024-02-29 / 2000-02-29(世纪闰)换算正确;2/29→3/1 间隔恰 86400 s | 闰年规则(含世纪闰) |
| **TC-SYSCLK-003** | 1/31、2/28、4/30 精确值;平年 2/28→3/1 间隔 86400 s | 月末边界 |
| **TC-SYSCLK-004** | 2000-01-01 换算;2038-01-19 03:14:07 → `0x7FFFFFFF`;`set(0)` → 1970-01-01 | 世纪边界 / uint32 上限 |
| **TC-SYSCLK-005** | RTC 读失败 → `init()==-1`,降级为编译期默认时间,打 `DBG_ERROR` | RTC 读故障降级 |
| **TC-SYSCLK-006** | 年/月/日越界 → 自愈写回默认时间,`init()==0` | 越界自愈(启用默认值时) |
| **TC-SYSCLK-020** | 越界 + 写回失败 → 降级默认时间,`init()==-1` | 组合故障降级 |
| **TC-SYSCLK-007** | 越界 → 写回 RTC 一次,阴影 RTC 被修正,`init()==0` | 自愈写回 + 复验 |
| **TC-SYSCLK-008** | 999ms 不变 / 1000ms +1s / 再 +2500ms 共 +3s | 正常推进 + 亚秒截断 |
| **TC-SYSCLK-009** | +29 天线性推进(未达 30 天阈值,不触发再基准化) | 阈值下不触发 |
| **TC-SYSCLK-010** | 30 天 + 1500ms → 值无损(+30 天 1 秒);再 +500ms 补足 1 秒 | **惰性再基准化** + 余数结转 |
| **TC-SYSCLK-011** | 基准近 2^32,推进跨回绕 0x2000ms → +8s | tick 回绕(无符号差)安全 |
| **TC-SYSCLK-012** | 10 天步进 ×9(累计 90 天,含 3 轮再基准)零漂移 | 长期运行无漂移 |
| **TC-SYSCLK-013** | `get_str` 格式化 + 零点补零;未初始化占位串 | 字符串接口 |
| **TC-SYSCLK-014** | 2026-10-02 08:09:10 → 年月日时分秒 + `week==5`(周五) | 日历字段 + 星期 |
| **TC-SYSCLK-015** | `get_time(NULL) == -1` | NULL 参数防御 |
| **TC-SYSCLK-016** | `set` 更新基准 + 写回 RTC;写失败仍更新基准 + 打错误 | set 两条路径 |
| **TC-SYSCLK-017** | `set(..., NULL)` 不碰 RTC | 可选 RTC 写回 |
| **TC-SYSCLK-018** | `set_unix` 写回 RTC,日历字段 + `week` 正确 | Unix 时间戳校时 |
| **TC-SYSCLK-019** | 16 个已知日期(跨 1~12 月 / 世纪闰 / 闰日 / 范围首尾)星期对照 | **Zeller 全域正确性** |
**当前结果**:100/100 PASSED(`failed=0`)。
@@ -145,7 +145,7 @@ test/host/sys_clock/sys_clock_test.c
>
> ⚠ **惰性再基准化的"正确用法"**:差值窗口必须恒 < 30 天。一次 `tick_advance`
> 推进超过 ~49.7 天会让 `uint32_t diff` 溢出混叠——那是**调用方违约**(不是模块
> 缺陷),测试应按 <30 天步长周期性调用 `get()`。见 TC-CLK-012 的注释。
> 缺陷),测试应按 <30 天步长周期性调用 `get()`。见 TC-SYSCLK-012 的注释。
---
@@ -166,7 +166,7 @@ test/host/sys_clock/sys_clock_test.c
| 目标状态 | 手段 |
|---|---|
| **未初始化**(`UINT32_MAX`) | 只能靠 **进程首用例**(TC-CLK-000,在任何 `init`/`set` 之前)观测 |
| **未初始化**(`UINT32_MAX`) | 只能靠 **进程首用例**(TC-SYSCLK-000,在任何 `init`/`set` 之前)观测 |
| **确定的已初始化状态** | `sys_clock_set(unix_secs, NULL)` —— 更新基准且**不碰 RTC**,是理想的隔离原语 |
| **RTC 阴影态** | `rtc_set(...)` + `clear_obs()` 直接重置阴影寄存器与观测计数 |
@@ -176,8 +176,8 @@ test/host/sys_clock/sys_clock_test.c
### 5.1 现象
首次运行 TC-CLK-014/019 时,`2026-10-02`(真实为周五)返回 `week == 4`;
TC-CLK-019 的 16 个已知日期中,**仅 `2038-01-19` 正确**,其余 9 个(3~12 月)
首次运行 TC-SYSCLK-014/019 时,`2026-10-02`(真实为周五)返回 `week == 4`;
TC-SYSCLK-019 的 16 个已知日期中,**仅 `2038-01-19` 正确**,其余 9 个(3~12 月)
全错。
### 5.2 根因
@@ -238,7 +238,7 @@ static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) { /* m: 1-based
### 5.5 校验数据来源
TC-CLK-019 的期望星期由 Python `datetime.date(...).weekday()` 独立生成
TC-SYSCLK-019 的期望星期由 Python `datetime.date(...).weekday()` 独立生成
(`(weekday()+1) % 7` → 0=周日),**与被测实现算法正交**,避免"用错的算法
验证错的实现"。
@@ -112,51 +112,51 @@ test/host/cfg_file/cfg_file_test.c
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-CFG-001** | UINT 1/2/4 字节往返(0 / 最大值);小端序(`0x1234` → `34 12`) | 小端编解码 |
| **TC-CFG-002** | UINT 非法:空 / 非数字 / 带后缀 / 超范围 / 负数 / len=3 | 严格校验 |
| **TC-CFG-003** | IP4:`0.0.0.0` / `255.255.255.255` / 常规;段数不足/超、>255、非数字、尾点、空 | 点分四段 |
| **TC-CFG-004** | MAC:`:` / `-` / `.` 三种分隔符、大小写;过短/过长/非十六进制/坏分隔符 | 分隔符 = **字节边界**替代 |
| **TC-CFG-005** | STR:正常 + NUL;恰好 `len-1` 放下;`len` 字符拒绝;空串 | 长度边界 |
| **TC-CONFIG-001** | UINT 1/2/4 字节往返(0 / 最大值);小端序(`0x1234` → `34 12`) | 小端编解码 |
| **TC-CONFIG-002** | UINT 非法:空 / 非数字 / 带后缀 / 超范围 / 负数 / len=3 | 严格校验 |
| **TC-CONFIG-003** | IP4:`0.0.0.0` / `255.255.255.255` / 常规;段数不足/超、>255、非数字、尾点、空 | 点分四段 |
| **TC-CONFIG-004** | MAC:`:` / `-` / `.` 三种分隔符、大小写;过短/过长/非十六进制/坏分隔符 | 分隔符 = **字节边界**替代 |
| **TC-CONFIG-005** | STR:正常 + NUL;恰好 `len-1` 放下;`len` 字符拒绝;空串 | 长度边界 |
### 2.2 INI 解析
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-CFG-010** | `;` 与 `#` 注释、空行忽略 | 注释语义 |
| **TC-CFG-011** | LF-only 行结束符解析正常 | CRLF/LF 兼容 |
| **TC-CFG-012** | 段名/键名大小写无关;`=` 两侧与值两侧空白修剪 | 宽松匹配 |
| **TC-CFG-013** | 键值行 `=` 前后空白、值尾空白被修剪 | 值修剪 |
| **TC-CFG-014** | 未知键 / 未知段 / 缺失键 → 保留工厂默认,不崩溃 | 前向兼容 |
| **TC-CFG-014b** | 非法值 → 保留默认 + `DBG_ERROR` 留痕 | 无效键留痕 |
| **TC-CFG-015** | 段头之前的键值行被忽略 | 段作用域 |
| **TC-CONFIG-010** | `;` 与 `#` 注释、空行忽略 | 注释语义 |
| **TC-CONFIG-011** | LF-only 行结束符解析正常 | CRLF/LF 兼容 |
| **TC-CONFIG-012** | 段名/键名大小写无关;`=` 两侧与值两侧空白修剪 | 宽松匹配 |
| **TC-CONFIG-013** | 键值行 `=` 前后空白、值尾空白被修剪 | 值修剪 |
| **TC-CONFIG-014** | 未知键 / 未知段 / 缺失键 → 保留工厂默认,不崩溃 | 前向兼容 |
| **TC-CONFIG-014b** | 非法值 → 保留默认 + `DBG_ERROR` 留痕 | 无效键留痕 |
| **TC-CONFIG-015** | 段头之前的键值行被忽略 | 段作用域 |
### 2.3 段注册校验
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-CFG-020** | 正常注册;重复段名拒绝;NULL 描述符 / NULL p_data 拒绝 | 参数防御 |
| **TC-CFG-021** | UINT len=3、MAC len≠6、STR len>79、key len=0 拒绝 | 键定义校验 |
| **TC-CFG-022** | 段名 ≥ `CFG_NAME_MAX`(16) 拒绝 | 段名长度 |
| **TC-CFG-023** | 挂回调段 `data_size > CFG_SNAP_MAX`(64) 拒绝;同段不挂回调则接受 | 回调段快照约束 |
| **TC-CONFIG-020** | 正常注册;重复段名拒绝;NULL 描述符 / NULL p_data 拒绝 | 参数防御 |
| **TC-CONFIG-021** | UINT len=3、MAC len≠6、STR len>79、key len=0 拒绝 | 键定义校验 |
| **TC-CONFIG-022** | 段名 ≥ `CFG_NAME_MAX`(16) 拒绝 | 段名长度 |
| **TC-CONFIG-023** | 挂回调段 `data_size > CFG_SNAP_MAX`(64) 拒绝;同段不挂回调则接受 | 回调段快照约束 |
### 2.4 load / save / poll 契约
| 用例 | 断言 | 验证的设计点 |
|---|---|---|
| **TC-CFG-030** | 文件缺失 → 生成出厂模板写盘 + 返回 0;模板含段头与键 | 首启建文件 |
| **TC-CFG-031** | 文件存在 → 有效键进 RAM 副本 | 正常加载 |
| **TC-CFG-032** | 超长文件 → 工厂默认 + 重建模板 + 自检留痕 + 返回 0 | 损坏自愈 |
| **TC-CFG-033** | 读取失败 → 返回 -1,RAM 保持工厂默认 | 读故障 |
| **TC-CFG-034** | 未注册任何段 → 返回 -1 | 前置校验 |
| **TC-CFG-040** | 文件缺失 → 重建完整文档 | 整文档生成 |
| **TC-CFG-041** | 外科手术:保留注释/未知段/未知键,**受管键原位改写** | ⭐ 核心特性 |
| **TC-CFG-042** | 缺失受管段/键 → 文末补写 | 补写路径 |
| **TC-CFG-043** | 输出超长 → 拒绝保存(-1) + 原文件不动 + 自检留痕 | 溢出保护 |
| **TC-CFG-050** | 首轮 poll 建立基线,不重载 | 基线建立 |
| **TC-CFG-051/052** | 沉降去抖(连续 2 次稳定才重载);与基线一致不重载 | 去抖语义 |
| **TC-CFG-053** | 变更后重载 + 挂回调段差异触发回调 | 变更响应 |
| **TC-CFG-055** | `(size, mtime)` 令牌不变 → 不重载(令牌 = 变更信号) | 令牌契约 |
| **TC-CFG-054** | poll 检出超长 → 工厂默认 + 重建模板 + 留痕;**不触发回调** | 在线修复通道保留 |
| **TC-CONFIG-030** | 文件缺失 → 生成出厂模板写盘 + 返回 0;模板含段头与键 | 首启建文件 |
| **TC-CONFIG-031** | 文件存在 → 有效键进 RAM 副本 | 正常加载 |
| **TC-CONFIG-032** | 超长文件 → 工厂默认 + 重建模板 + 自检留痕 + 返回 0 | 损坏自愈 |
| **TC-CONFIG-033** | 读取失败 → 返回 -1,RAM 保持工厂默认 | 读故障 |
| **TC-CONFIG-034** | 未注册任何段 → 返回 -1 | 前置校验 |
| **TC-CONFIG-040** | 文件缺失 → 重建完整文档 | 整文档生成 |
| **TC-CONFIG-041** | 外科手术:保留注释/未知段/未知键,**受管键原位改写** | ⭐ 核心特性 |
| **TC-CONFIG-042** | 缺失受管段/键 → 文末补写 | 补写路径 |
| **TC-CONFIG-043** | 输出超长 → 拒绝保存(-1) + 原文件不动 + 自检留痕 | 溢出保护 |
| **TC-CONFIG-050** | 首轮 poll 建立基线,不重载 | 基线建立 |
| **TC-CONFIG-051/052** | 沉降去抖(连续 2 次稳定才重载);与基线一致不重载 | 去抖语义 |
| **TC-CONFIG-053** | 变更后重载 + 挂回调段差异触发回调 | 变更响应 |
| **TC-CONFIG-055** | `(size, mtime)` 令牌不变 → 不重载(令牌 = 变更信号) | 令牌契约 |
| **TC-CONFIG-054** | poll 检出超长 → 工厂默认 + 重建模板 + 留痕;**不触发回调** | 在线修复通道保留 |
**当前结果**:139/139 PASSED(`failed=0`)。
@@ -213,7 +213,7 @@ test/host/cfg_file/cfg_file_test.c
### 5.1 现象
`TC-CFG-041`(外科手术保存)首次运行即失败:对**已存在**的 `/config.ini` 调用
`TC-CONFIG-041`(外科手术保存)首次运行即失败:对**已存在**的 `/config.ini` 调用
`cfg_file_save()`,即使 RAM 副本的值已改(如 `u8: 1 → 200`),落盘文件仍是
**原值** `u8 = 1`。
@@ -264,7 +264,7 @@ if (s_encode(p_key, ..., v, sizeof(v)) >= 0) {
}
```
修复后 `TC-CFG-041` / `TC-CFG-042` 转 PASS,`@build` 0 errors 0 warnings。
修复后 `TC-CONFIG-041` / `TC-CONFIG-042` 转 PASS,`@build` 0 errors 0 warnings。
### 5.5 建议后续动作
+21 -18
View File
@@ -35,10 +35,10 @@
| **连字符/补零偷偷承载语义** | `R1`=风险、`R-2`=内存优化项;`D7`=验证窗口、`D-7`=文档门禁、`D1~D7`=设计决策;`N-2`(登记簿)与 `N-02`(报告)同号不同义 |
| **跨文档同号不同义** | `Trap 02` 在 CH395F 是"缓冲区重叠"、在 GD5F 是"测试缓冲越界";`B10` 在规划是"送检窗口"、在陷阱审计是"孤儿监听" |
| **语义进编号 → 事实一变编号就撒谎** | `P1-x` 把严重度写进号里,而审计中大量条目事后降级(`E-59` P0→P1、dhara `is_free` P1→P2):改号则引用全断,不改则号实不符 |
| **孤儿编号(代码有、文档无)** | `I-1`/`I-2`、`Task #32` 会话号、`P1-01~P1-09`、`TC-GD5F-1100/1101/1102`、`TC-STO-06`、`TC-AH-*`、`TC-PE-*`(前两项项目已在 `docs/测试说明/实机测试实施指南.md` 自行承认) |
| **孤儿编号(代码有、文档无)** | `I-1`/`I-2`、`Task #32` 会话号、`P1-01~P1-09`、`TC-GD5F-1100/1101/1102`、`TC-STORAGE-06`、`TC-AH-*`、`TC-PE-*`(前两项项目已在 `docs/测试说明/实机测试实施指南.md` 自行承认) |
| **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31/N-32` 销项删行后仍被引用(`N-32` = 监听生命周期模块化,被 `docs/网络/` 三篇与代码注释多处引用) |
| **同一事物多种字面** | `Trap 07` / `TRAP-07` / `陷阱 1`;`gen 181` / `gen-288` / `gen68`;`TC-NET-704` / `TC704` / `TC-203` |
| **规范自身就不自洽** | `docs/测试说明/测试文档模板规范.md` §6.3 同一行并列 `TC-STO-03`(2 位)与 `TC-RTC-001`(3 位);`docs/测试说明/测试文档模板规范.md` §6.2 已废止 `PHY` 模块码,而阶段 ID 至今仍是 `TM-PHY-01` |
| **规范自身就不自洽** | `docs/测试说明/测试文档模板规范.md` §6.3 同一行并列 `TC-STORAGE-03`(2 位)与 `TC-RTC-001`(3 位);`docs/测试说明/测试文档模板规范.md` §6.2 已废止 `PHY` 模块码,而阶段 ID 至今仍是 `TM-PHY-01` |
| **机器校验只覆盖一个族** | (2026-10-15 现状,2026-10-20 已由 §8.1 收口)当时 31 个 pre-commit 钩子中只有 `TC` 有 `scripts/check_test_traceability.py`,且**未挂钩子、warn-only、正则 `TC-[A-Za-z0-9]+-\d+` 匹配不到大写后缀形态**;`E/N/S/B/C/D/A/U/O/gen/Trap` 全部裸奔 |
> **可借鉴的先例**:`§N.M` 章节引用当年 1,578 处也全靠自觉,靠「紧邻写法 + 别名表 + 可判定比例 + 钩子」把可判定比例做到 58% 并转为拦提交([`docs/文档规范.md`](文档规范.md) §3.5)。编号走同一条路径即可收口。
@@ -291,22 +291,24 @@
| 码 | 模块(源文件/目录) | 码 | 模块 |
|---|---|---|---|
| `NET` | `Drivers/BSP/CH395F` + `Drivers/BSP/NET` | `AI` | `App/algo/adc_input` |
| `GD5F` | `Drivers/BSP/GD5F2GQ5UE` | `AH` | `App/algo/adc_health` |
| `STO` | 存储栈(FTL + FatFs + `App/fs`) | `PH` | `App/algo/phasor` + `sample_ring` |
| `RTC` | `Drivers/BSP/SD2506` | `PS` | `App/algo/prot_set` |
| `ADC` | 采集节拍与通道映射(`Src/adc.c`、TIM3) | `PE` | `App/algo/prot_elm` |
| `RS485` | `Drivers/BSP/RS485` | `VL` | `App/fs/vol_lock` |
| `UI` | `Drivers/BSP/UI` | `TS` | `App/sync/time_sync` |
| `LOG` | `App/log` | `CLK` | `App/util/sys_clock` |
| `CFG` | `App/cfg` + `Drivers/BSP/NETCFG` | `WD` | `App/util/task_watchdog` |
| `SH` | `App/util/sha256` | `FA` | `Drivers/BSP/NET/lftpd` 鉴权 |
| `I104` | `App/protocol/iec104` | `TOOL` | PC 端工具(串口助手等) |
| `NET` | `Drivers/BSP/CH395F` + `Drivers/BSP/NET` | `ADCIN` | `App/algo/adc_input` |
| `GD5F` | `Drivers/BSP/GD5F2GQ5UE` | `ADCHEALTH` | `App/algo/adc_health` |
| `STORAGE` | 存储栈(FTL + FatFs + `App/fs`) | `PHASOR` | `App/algo/phasor` + `sample_ring` |
| `RTC` | `Drivers/BSP/SD2506` | `PROTSET` | `App/algo/prot_set` |
| `ADC` | 采集节拍与通道映射(`Src/adc.c`、TIM3) | `PROTELEM` | `App/algo/prot_elm` |
| `RS485` | `Drivers/BSP/RS485` | `VOLLOCK` | `App/fs/vol_lock` |
| `UI` | `Drivers/BSP/UI` | `TIMESYNC` | `App/sync/time_sync` |
| `LOG` | `App/log` | `SYSCLK` | `App/util/sys_clock` |
| `CONFIG` | `App/cfg` + `Drivers/BSP/NETCFG` | `WATCHDOG` | `App/util/task_watchdog` |
| `SHA256` | `App/util/sha256` | `FTPAUTH` | `Drivers/BSP/NET/lftpd` 鉴权 |
| `IEC104` | `App/protocol/iec104` | `TOOL` | PC 端工具(串口助手等) |
> 废止码:`PHY`(→ `NET`)、`NAND`(→ `GD5F`)。历史文档不回改,新文档按本表。
---
> ⚠ **2026-10-20 批 2**:上表 16 个有歧义的码已单词化(`AI`→`ADCIN`、`AH`→`ADCHEALTH`、`PS`→`PROTSET`、`PE`→`PROTELEM`、`PH`→`PHASOR`、`TS`→`TIMESYNC`、`VL`→`VOLLOCK`、`NP`→`NANDPORT`、`CFG`→`CONFIG`、`CLK`→`SYSCLK`、`WD`→`WATCHDOG`、`STO`→`STORAGE`、`SH`→`SHA256`、`FA`→`FTPAUTH`、`FR`→`FREQ`、`104`→`IEC104`),全库 1,146 处随 `scripts/rename_ids.py` 改写,旧号入 `scripts/id_aliases.json`。`NET`/`GD5F`/`RTC`/`SOE`/`UI`/`ADC`/`LOG`/`RS485`/`TOOL` 不动(已是单词、芯片名或业界通用词)。**连带修门禁**:模块码变长到 9 位(`ADCHEALTH`),候选正则与族正则的 `{0,4}` 上界必须放宽到 `{0,9}`,否则改名后的号会整批从门禁眼里消失(实测漏掉 950 处——改名类批次必须连带复核识别面,不能只改数据不改判据)。
## 7. 迁移映射(legacy → 新号)
### 7.1 映射规则
@@ -331,8 +333,8 @@
| 审计模式 `P1~P9` | `PAT-01`~`PAT-09` | 正文冻结,新文档用新号 |
| 保护 `R1~R8`、看门狗 `R1~R7` | 各文档 `RSK-01`… | 文档内编号 + 引用带文档名 |
| 保护阶段 `P0~P5`、方案 `P0~P3` | 各文档 `ST-0`… | 与档位词 `P0` 解耦 |
| `TC-STO-01~06` | `TC-STO-001~006` | 补零 |
| `TC-AI-01A`/`02A`/`02B`/`02C`、`TC-VL-008b` | `TC-AI-001a`… / `TC-VL-008b` | 后缀统一小写字母,基号补零 |
| `TC-STORAGE-01~06` | `TC-STORAGE-001~006` | 补零 |
| `TC-ADCIN-01A`/`02A`/`02B`/`02C`、`TC-VOLLOCK-008b` | `TC-ADCIN-001a`… / `TC-VOLLOCK-008b` | 后缀统一小写字母,基号补零 |
| `TM-PHY-01~11` | `TM-NET-01~11` | 模块码 |
| `DP-01~04` | `DP-NET-01~04` | 加模块码 |
@@ -343,7 +345,7 @@
| `I-1`/`I-2`(`ui_net_sink.c`) | 就地展开成事实描述(原属主《网络Socket监听生命周期设计说明》已并入 `docs/网络/网络自愈架构设计.md` §9.3,不再补编号定义行) |
| `Task #32` 会话号及其小节引用 | 会话号,改写为可解析引用(结论已沉淀进 `docs/子系统设计/看门狗设计说明.md` §11) |
| `P1-01~P1-09`(`ch395f_test_task.c`) | 改指 `TC-NET-101`…,或补文档条目 |
| `TC-GD5F-1100/1101/1102`、`TC-STO-06` | 补进 `docs/测试说明/GD5F2GQ5UE_Test_Guide.md` |
| `TC-GD5F-1100/1101/1102`、`TC-STORAGE-06` | 补进 `docs/测试说明/GD5F2GQ5UE_Test_Guide.md` |
| `TC-AH-*`、`TC-PE-*`、iec104 `TC-014/015/024` | 补单测说明文档(缺文档 = 追溯断链) |
| `Trap 19` | 悬空引用,删除或改指实际条目 |
@@ -400,7 +402,7 @@ git grep -n '<旧字面量>' # 零残留
| 编号字面量出现 | 8271 处 |
| **可判定比例基线** | **82.6%**(6840 / 8271 = 82.70%,批 1 改名后重打,存进 `id_registry.json` 时**下取整到 0.1pp**——用 `round()` 会让 82.34 进位成 82.3 再反过来把自己判死,实测踩过;比较容差 0.1pp) |
| 校验 1 未登记前缀 | 0 处 |
| 校验 2 形态不合规 | 396 处(**批 1 已从 417 降到 396**:裸形 `S`/`T` 类 71 → 50。剩余大头:真无模块码 `TC-nnn` 181 处 / 5 文件、数字模块码 `TC-104-nnn` 23 处(§6 表里模块码是 `I104`,非数字)、`DP-0n` 22 处、`Task #nn` 33 处、`Trap 空格形`+`陷阱 中文形` 43 处、`gen/round` 36 处、大写后缀 31 处) |
| 校验 2 形态不合规 | 396 处(**批 1 已从 417 降到 396**:裸形 `S`/`T` 类 71 → 50。剩余大头:真无模块码 `TC-nnn` 181 处 / 5 文件、数字模块码 `TC-104-nnn` 23 处(§6 表里模块码是 `I104`,非数字)、`DP-01`~`DP-04` 22 处、`Task #nn` 33 处、`Trap 空格形`+`陷阱 中文形` 43 处、`gen/round` 36 处、大写后缀 31 处) |
| 校验 3 悬空引用 | 错误 626 / 告警 223(scoped 别名收编旧号后 −16)(错误集中在 `TC-PS-*`/`TC-PE-*`/`TC-AH-*`/`TC-FR-*`/`TC-SOE-*`——**主机侧单测有码无文档**,正是 §7.2 列的"缺文档 = 追溯断链") |
| 校验 4 重复定义 | 0 处(`§4.3` 已声明的同号不同义族对 443 个字面量走 `collides_with` 白名单,不计事故) |
| 校验 5 冻结越游标 | 0 处 |
@@ -418,7 +420,7 @@ git grep -n '<旧字面量>' # 零残留
|---|---|---|
| §4.1 `E\d+`「已封闭 `E0`~`E15`」 | `docs/网络/网络自愈架构设计.md` §5.2 实验表已到 **E19** | 游标按实测 E19 收录;封闭线待修订 |
| §4.1 `FR-[A-Z0-9]+-\d{2}` | 现存 `FR-STO-1` 等全是**未补零**的 1 位序号 | 正则放宽到 `\d{1,2}`,补零归 §9 阶段 1 |
| §4.1 `TC-[A-Z]{1,4}-\d{3,4}` | 模块码 `GD5F`/`I104` **含数字**,按原文正则会漏掉 GD5F 全族(实测 285 处) | 放宽为 `[A-Z][A-Z0-9]{0,4}`;序号暂收 2 位(`TC-STO-06` 属 §7.2 孤儿) |
| §4.1 `TC-[A-Z]{1,4}-\d{3,4}` | 模块码 `GD5F`/`I104` **含数字**,按原文正则会漏掉 GD5F 全族(实测 285 处) | 放宽为 `[A-Z][A-Z0-9]{0,4}`;序号暂收 2 位(`TC-STORAGE-06` 属 §7.2 孤儿) |
| §4.1/§4.2 总表无 `F-<MOD>-<nn>` | `F-NET-2~17`/`F-DRV-*`/`F-MS-*` 定义在 `docs/网络/网络自愈架构设计.md` 且被大量代码注释引用 | 先按实测收编成族(§5.1 第 1 步:表里没有的类别先改本规范) |
| §4.1 `N-\d{2}` | 报告用 `N-03a`/`N-03c` 子项后缀;§5.5 扩位示例 `N-100` | 放宽到 `\d{1,3}[a-z]?` |
@@ -430,6 +432,7 @@ git grep -n '<旧字面量>' # 零残留
| 批 | 日期 | 族 | 旧 → 新 | 处数 / 文件 | 别名 |
|---|---|---|---|---|---|
| **批 1** | 2026-10-20 | 代码规范条款 | `S1`~`S19` → `CODE-RULE-01`~`19` | 78 处 / 2 文件 | 18 条(scoped:`scripts/check_coding_standard.py`、`docs/代码规范/pre-commit使用说明.md`) |
| **批 2** | 2026-10-20 | 测试用例模块码 | 16 个码 → 单词化(见 §6) | 1,146 处 / 约 30 文件 | 全局别名(字面量已完全限定,无歧义) |
| **批 1** | 2026-10-20 | UI 像素回归状态 | `S0`~`S7` → `MENU-STATE-00`~`07` | 61 处 / 3 文件 | 8 条(scoped:`simulator/tests/check_menu_rows.py`、`docs/UI设计/UI_自动化测试流程.md`、`simulator/README.md`) |
⚠ **别名必须带 scope**:同一裸形字面量可同时是多族的旧号(实测 `S3` 三义)。全局别名会把另一族的真悬空洗成「已解析」——等于用工具复制 §1.2 的 `N-31`/`N-32` 撞号事故。