Files
STM32F4-Base/docs/子系统设计/故障录波设计说明.md
T
wandering1 4ceb5b41e6 docs(任务规划): 故障录波卡片登记簿销项——判据并入设计说明后删除
用户指令(2026-10-30):确认录波任务完成度,把除实际操作之外的所有
步骤做完,然后删除规划文件,避免已完成的工作被误读为未做。

审计结论:代码面确已全部收口——wave_read.py --selftest 92/92、
三套主机单测 113/170/110 复跑全 rc=0、bench 自抓与 PC 读段脚本在仓、
数据可信/复刻明细/短板评测/UI 两处回写均已做过。**唯一未跑的是
板级台架那一轮(实际操作)**,判据不得丢,故先迁后删:

- 设计说明 §9 承接落盘道失败留痕名全清单(13+1 条,打印点现值
  default_task.c:816/:831 实测核对)
- 设计说明 §10 承接原卡 2/卡 3 的板级段(触发来路一行、回放页
  一行)与本波执行顺序图(mermaid,原规划 §3 逐字迁)
- §4/§11 头部定位、§11 尾注、§14 关系行改属主口径
- 全库 32 处引用改挂设计说明 §10/§9(18 文件):任务规划 README
  独立行(顺带订正单测数 100→113 为实测)、登记簿、数据可信 ×2、
  复刻明细、产品需求 ×2、ADC 注入 ×3、短板评测 ×2、页面补齐规划、
  App/algo README、配图生成器注释、录波三套单测注释 ×6
- git rm docs 该规划文件;人类索引任务规划 6→5 个文件(沿革注照
  屏板链路先例走裸句,不留反引号死引用)

验证:残留两形态(任务规划/故障录波、裸文件名)全树扫描 0;
mermaid 预检新发 0;doc-links 11 个被改文件逐文件 PASS;生成器
注释改动重跑产物逐字节不变(svg 60a0dd39e4);三套单测与
wave_read selftest 复跑全绿;18 文件 BOM/CR/U+FFFD 全零。

本笔不含固件编译单元(test/host 仅注释行与文档),构建留待合并
轮统一跑;板级轮启动判据与顺序见设计说明 §10。
2026-10-30 18:20:37 +08:00

409 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 故障录波设计说明(1 kHz · 8 路原始码值 · 现场单通道回放)
> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(`App/algo/wave_ring.c`,已落地)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。
> **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**板级判据与本波执行顺序的属主即本文 §9 与 §10**——原任务规划下的《故障录波》卡片登记簿已于 2026-10-30 随销项并入本文后删除(沿革见 §14);排期顺位在 `docs/任务规划/README.md` §2.0「独立」行。
> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 和 104 浮点 YC 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在本文 §10 板级各行。三条已裁定口径见 §1.2。
> **相关文档**:
> `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它)
> `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属)
> `docs/任务规划/数据可信.md` §4.6(动态波形验证判据;录波落地后它的读数出口升级,见 §11)
> `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1(FatFs 两条实证陷阱与 8.3 短名限制)
> `docs/内存管理/内存内容分配策略.md` §6 与 R6 规则(新增静态缓冲大于 4 KB 的前置动作)
> `docs/UI设计/内容页架构.md`、`docs/UI设计/RemoDispBus协议与接收设计说明.md`(回放页与远端看图的能力边界)
## 目录
- `§1` 三条已裁定口径:1 kHz、8 路全录、一屏一路
- `§2` 与产品需求 80 点/周波口径的偏离,与二期不返工的四条结构约束
- `§3` 录什么:通道归属、削顶类标记、为什么存原始码值
- `§4` 触发模型与段长冻结语义
- `§5` 四个所有者与三条并发陷阱
- `§6` 段文件字节布局
- `§7` 预算(R6 是硬前置)
- `§8` 现场回放显示契约
- `§9` 观测:只加一条周期行
- `§10` 测试与门禁,含 bench 自抓红线
- `§11` 与数据可信任务卡的三处口径变更
- `§12` 风险 `§13` 范围边界 `§14` 与其它文档的关系
---
## 1. 这一波要解决什么
### 1.1 两个动机,一个数据面
| 动机 | 谁提的 | 要求 |
|---|---|---|
| **现场能大概看一眼波形** | 产品(用户裁定 2026-10-29) | 装置上按几下就能看「这一路现在是不是正弦、削没削顶」,不必插串口、不必带笔记本 |
| **台架能把样本拿出来对表** | `docs/任务规划/数据可信.md` §4.6 | 把板上采到的原始样本导出,在 PC 上独立重算全周傅氏,与板上派生读数逐个对——这才是「验证我们后面的各种计算」唯一硬的做法 |
两条共用同一个数据面:**录波环 + 段文件 + 取回通路**。区别只在「谁来触发、给谁看」。分开做会变成两套导出机制,所以并成一件。
### 1.2 三条已裁定口径(本文其余章节都以此为前提)
| # | 口径 | 裁定 | 代价与换来什么 |
|---|---|---|---|
| 1 | **采样率 = 1 kHz(20 点/周波)** | 复用现有 TIM3 节拍,不改采集 | 与产品需求的「80 点/周波(4 kHz)」口径存在**已登记偏离**(§2);换来的是采集主线零改动——不动 `Src/tim.c`、不动 CubeMX、不动相量与测频 |
| 2 | **8 路电气量全录** | 五路削顶也照录 | 数据不筛选,现场看见的是真实形状;代价是文件必须自带「这一路可信吗」的标记(§3.1 与 §6.2),否则削顶会被读成「系统里真有零序」 |
| 3 | **一屏只画一路,支持 8 路切换** | 160×160 单色屏画不出多路 | 不需要图例(本来也没有颜色);8 路切换用方向键在同一页内换通道,**不新增菜单叶子**(§8.2) |
---
## 2. 与产品需求口径的偏离(显式登记,不许静默降级)
`docs/产品需求/参考产品复刻明细.md` §7 的「录波采样率」行写着:录波 = 20 × 4 = **80 点/周波(50 Hz 下 4 kHz)**,理由也写在那一行——「保护计算用 20 点/周波,录波要 4 倍过采样才能看清波形」。本波只做 20 点/周波,是**明知故犯的偏离**,把理由与代价写在明处:
| 项 | 内容 |
|---|---|
| 为什么先做 1 kHz | 抬到 4 kHz 的第一块骨头不在录波,而在采集主线:一拍时序账要重算(现在 TPAFE 转换约 23 µs、EXTI 读 8 通道约 0.5 µs、ADC1 第 9 点约 67 µs、读窗与下次 CONVST 相隔约 977 µs,见 `docs/子系统设计/ADC采样节拍设计说明.md` 的时序账),半区语义与「恒定小偏移」都要重推;算法层还有两处按 1 kHz 写死的假设(`App/algo/phasor.h` 的 `PHASOR_N 20U`、`App/algo/freq.h` 的 `FREQ_FS_HZ 1000.0f`)会被一起牵动 |
| 芯片支持吗 | **支持**:TPAFE5160 是 16 bit / 350 kSPS 全通道同时采样(`docs/芯片手册/TPAFE5160.md` 首行)→ 4 kHz 是纯固件与算法的工程账,不是硬件上限 |
| 1 kHz 能看到什么 | 基波形状、削顶、直流偏置、明显畸变、频率(数过零)。**看不到**:2/5 次谐波的形状细节(每周期只剩 10 / 4 个点)、暂态前沿 |
| 偏离登记 | 落码时在产品需求那一行补一句指向本文;「看清波形」这一目标在 1 kHz 下**只算部分达成**,不许拿 1 kHz 的图当满足 80 点口径的证据 |
### 2.1 二期不返工的结构约束(本波就写进代码注释)
将来抬到 4 kHz 时**只允许加、不允许改**的四条规则,本波先立着:
1. **录波环按「全速原始序列」建模**:「每周波点数」(现 20)与「采样率」(现 1000)是**写进文件头的数据**,不是编译期假设——回放端与 PC 脚本一律从头里读;
2. **算法层永远看 1 kHz 视图**:抬频时在 ADC 侧加 4:1 抽取喂 `sample_ring` 与 `freq`,`PHASOR_N` 与 `FREQ_FS_HZ` 不许被改;
3. **录波环与 `sample_ring` 解耦**(各有独立缓冲与指针,见 §5.1),二期全速只扩录波环,不牵动相量输入;
4. 文件格式的版本号字段本波置 1,二期换格式时递增,PC 侧按版本分派。
---
## 3. 数据源:录什么、从哪一帧录
### 3.1 语义通道顺序与转换器归属(不可混为一谈的两类)
语义枚举与归属见 `App/algo/adc_input.h`(`ADC_SEM_UA` 到 `ADC_SEM_IN`,再加 `ADC_SEM_TEMP` 与 `ADC_SEM_COUNT`):
| 通道 | 转换器 | 位宽与量程 | 在录波里的含义 |
|---|---|---|---|
| UA / UN / IA | TPAFE5160 | 16 bit,±5 V **双极性** | **可信类**:能录出真实的双向工频波形 |
| UB / UC / IB / IC / IN | STM32 ADC1 | 12 bit,0 到 3.3 V **单极性、无中点偏置** | **形状受限类**:交流必然把负半周钉在下轨。录它是为了如实呈现 V0.1 现状,不是为了当工频量用 |
| TEMP | STM32 ADC1 | 12 bit | 可选附带单点(非线性未折算,`docs/任务规划/数据可信.md` §7 已定案) |
两条硬件事实决定回放能做到什么,**必须进文件头,也必须进页面**:
- **跨转换器无共同采样触发**(V0.1 硬件缺陷):8 路分属两片转换器,没有同一时刻性 → **禁止把 U 与 I 叠在同一张图上比相位**。文件头按通道写「来源转换器 + 采样时刻偏移」;回放只画单通道,这不是妥协,是这条硬件事实逼出来的形态;
- **STM32 侧另有 1 拍乒乓滞后**(`App/algo/adc_input.h` 的取数口径注释)→ 同一转换器内也不是严格同刻。
### 3.2 存原始码值,不存折算工程量(本波最重要的一条数据形态决策)
| 理由 | 说明 |
|---|---|
| 现在根本没有工程量 | 两点标定未做,`adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位 → `adc_cal_prot_blocked()` 返 1,帧值仍是引脚域电压(`docs/任务规划/数据可信.md` §4.3)。「录波必须先有工程量」这条旧前提被本条推翻 |
| 绕开测频幅值门 | `App/algo/freq.h` 的 `FREQ_AMP_MIN 50.0f` 以二次值 V 为口径,`eng=0` 期间帧值远小于它恒不过门 → `freq_valid()=0`。**录波取的是这道门之前的原始帧**,所以不受影响,动态验证才有样本可对 |
| 最省且可追溯 | int16 码值 2 B/点;标定完成后不必重录,PC 端拿头里的 `k` / `b` 就能把老文件重算成工程量 |
| 诚实 | 文件里装的是「板子看见的东西」,不是「我们希望它看见的东西」 |
**文件头因此必须带**:每路的来源转换器、位宽、LSV 用 µV 计的定点值(TPAFE 152.59 µV、STM32 805.66 µV)、封存瞬间的 `eng` 档位、折算 `k` / `b` 快照、有效性位图与削顶位图。缺任何一项,老文件在新固件上就会被解释错。
---
## 4. 触发模型
### 4.1 四种触发,本波做前三种
| 触发 | 来源 | 本波 |
|---|---|---|
| **手动** | UI「手动录波」操作页(叶子在菜单装置维护组,页面模块 `Drivers/BSP/UI/ui_page_wave_manual.c`,2026-10-29 已绑页;确认键在 `App/task/ui_task.c`:582 行调 `wave_ring_request()`,走手动来路、元件号填 0) | 做(已落地) |
| **保护启动** | 落点在 `App/task/protect_task.c` 的 `s_log_record_edge()`(函数体自 158 行起,picked 上升沿分支 170 到 177 行,实际调用在 176 行:`wave_ring_request(WAVE_TRIG_PROTECT, 最低位上升元件)`),与 SYSLOG_START 同一条边沿、同一事件点,避免两处各自判定。**刻意不落在 `App/algo/prot_elm.c`**:判据核被三套主机单测源码级纳入编译,往它加录波依赖会一次全破——这条就是「判据核零改动」的前提。多元件同拍启动只录一段(环不重入,后续请求由环计 ovf,不算失败) | 做(复用事件点,不新增判据) |
| 定周期自抓 | `DTU_BENCH` 构建下每 N 秒强制一段,供信号发生器对表 | 做,**只在 bench 构建**,产品构建无此路径 |
| **突变量启动** | 需要增量化 `Δi` 类算法与定值 | 不做,归二期(`docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行) |
### 4.2 段长与冻结语义(RAM 就靠这一条省下来)
- 一段 = **预触发 8 周波 + 后触发 8 周波 = 320 点/路**(1 kHz 下 320 ms);
- **段内点序**(两份实现共同遵守,PC 与回放页都按这条):触发那一拍的样本落在**段内点 159**(预触发 160 点的最后一点),后触发第一拍是**点 160**,两半不重叠,故障时刻就是这条分界。因此段内点 0 相对工频周期边界有任意偏移(触发瞬间在周期内任意落点),**段字节里没有绝对时间参考**——能核对的是相对相位与极性,绝对相位只能以段内点 0 为参考报出(§10 板级判据据此不比绝对相位);
- **触发即冻结环**:停止推进录波写指针,defaultTask 直接从已冻结的环落盘,落完解冻;
- **冻结期间丢的是录波样本,不是采集**——`sample_ring`、相量、保护判据照常跑(这就是 §2.1 第 3 条要求解耦的原因),冻结不拖慢保护动作,也不让 104 断数;
- **解冻的唯一入口是落盘方回执**(`wave_ring_write_done(result)`,只在 defaultTask 上下文调):写成功才推进轮转段号,写失败只计 err 且**下一段重写同一个号**(不留半截文件);冷却期从回执之后才开始计时,回执前新触发一律只加 ovf。「落完解冻」这条原本按时间窗口的假设,实现里改由回执保证,冷却数值只是二次抑制。
- 兜底(绝不允许录波把环焊死):封存态滞留超过 `WAVE_SEAL_TIMEOUT_MS`(当前 2000 拍 = 2 s,**待台架核**)就自弃本段、计 err 并解冻——卡在一次落盘上,代价是下一次真故障无波可录。
- 代价:一次触发的落盘窗口(约几十 ms)内再来触发只记第一次(冷却期),**不做多段排队**——那是二期「6 段 / RAM 16 个」的形态。
```mermaid
flowchart TD
IDLE[空闲: 录波环照常滚动写入] --> ARM{触发源有效?}
ARM -- 无 --> IDLE
ARM -- 手动 / 保护启动 / bench 定时 --> FRZ[冻结环: 停止推进写指针]
FRZ --> POST[后触发计满 8 周波]
POST --> PACK[封段: 写文件头与通道表]
PACK --> W[defaultTask 落盘到 /wave 8.3 短名]
W --> OK{写成功且字节数相符?}
OK -- 否 --> ERR[记 wave: 失败日志, 丢弃本段]
OK -- 是 --> MARK[段计数与最新段号入快照]
ERR --> REL
MARK --> REL[解冻: 恢复滚动写入]
REL --> COOL[冷却期: 期间新触发只计数不重录]
COOL -. 计满 .-> IDLE
COOL -. 期间又来触发 .-> SKIP[丢弃: 溢出计数加一]
SKIP --> COOL
```
图内不写步骤号;冻结 → 后触发 → 封段 → 落盘 的四段动作在 §4.2 与 §5 正文里。
## 5. 所有权与并发边界(这块最容易做错)
### 5.1 谁写谁读
| 角色 | 职责 | 不许做 |
|---|---|---|
| **adcTask**(生产者) | 每拍把 8 路原始码值(可带 TEMP)压进录波环;查触发标志;触发时冻结 | 不碰 FatFs、不碰网络 |
| **protectTask** | 在 `picked` 上升沿置一个**请求标志**(原子置位,与 `prot_elm_request_reset()` 同一模式) | 不写环、不落盘 |
| **defaultTask**(文件系统唯一写者) | 排空已封存的段 → `f_write` → 校验字节数 → 更新段账 | 不读活缓冲,只读**已封存**区 |
| **uiTask**(回放页) | 只读窗口取段数据 → 自绘(与 `App/log/soe_store.c` 读侧同构) | 不写文件 |
| **netTask / lftpd** | 把 /wave 暴露给 FTP 取回 | 不在 netTask 里读录波环 |
### 5.2 四条已知陷阱,本波必须绕开
| 陷阱 | 事实出处 | 本波处置 |
|---|---|---|
| **取窗竞态** | `App/algo/sample_ring.c` 的 `sample_ring_window()` 没有临界区(读 `s_head` 后逐点拷),现在只有 adcTask 同任务调用才安全 | 录波**不复用它**:adcTask 在自己的节拍里取「本拍帧」压专用环;跨任务只传递「已封存」标志,杜绝跨任务读活缓冲 |
| **FatFs 追加写** | 该移植 `FA_OPEN_APPEND` 不含 `FA_WRITE`,且 `f_write` 簇分配失败时返回 `FR_OK` 加短写、无错误码(`docs/架构设计/STM32F4-Base存储架构说明.md` §10.1) | 打开一律 `FA_WRITE\|FA_OPEN_APPEND`;**判成功必须比对写回字节数**,短写按失败处理(丢段 + 日志),绝不当成功记账 |
| **只能 8.3 短名** | 出处同一张存储架构说明的短名限制:`FF_USE_LFN=0`,不得以点开头、不得长名 | 文件名 = WAVE 加两位十进制再加 .BIN,共 100 个名字轮转覆盖;目录 /wave(与 /log 平级,FTP 根下可见) |
| **落盘途中环被解冻、甚至已换段**(实现期复核新增第四条,2026-10-29) | adcTask 优先级高于 defaultTask:搬段窗口里 adcTask 可能正好走到 SEALED 滞留 2 s 自弃,甚至已经封出下一段。**数据区 CRC 是对「本道实际读到的字节」算的**,半新半旧的段照样自洽——长度等式照过、两个 CRC 照对,文件骗得过任何读者,也骗过我们自己 | 写完关档后**复检环侧身份**:`wave_ring_sealed()` 仍为 1,且 `wave_ring_get_stat()` 的 `last_t0_ms` 与 `last_seq` 跟本道开始搬时那份段头一致;不符即走既有失败路——unlink 本道开过的文件 + `write_done(-1)`(此时多半回 `E_STATE`,环侧 `err` 已由自弃计过,不重复补账)+ 段号不推进(下一段同号重写覆盖)+ 单条 `[ERR] … seg drop: ring abandoned` 留痕。落点 `App/task/default_task.c`:786 到 796 行。**当前只有代码与单测面,板级未测**;可观察项见 §10 板级行 |
![录波数据的四个所有者与跨任务边界:adcTask 只写环,defaultTask 独占落盘,uiTask 与 PC 只读](../images/故障录波数据流.svg)
图由 `tools/arch_diagram/gen_wave_record_flow_svg.py` 生成(改图改脚本重跑,勿手改 SVG)。
取窗竞态的原因:
![image.png](https://lsky.bitnasdaq.vip/5rJRrZ.png)
---
## 6. 段文件格式
### 6.1 本波为什么不用 COMTRADE
| 判据 | 结论 |
|---|---|
| 谁要用 | 本波消费者只有两个:装置自己的回放页、仓内一个 PC 脚本——**没有任何第三方要求 COMTRADE** |
| 代价 | COMTRADE 要「配置 + 数据」两份文件(扩展名 CFG 与 DAT)、ASCII 通道表、时标与采样率段;FatFs 只有 8.3 短名,两套文件名要各占一个号,段数一多就不够分;多段与时标在 Flash 余量里换不来可读性收益 |
| 归属 | 送检或第三方交接时才需要 → 二期(`docs/问题记录/采集与数据处理短板评测.md` 的记录链缺口行已登记 COMTRADE) |
### 6.2 段文件布局(小端;文件名 WAVE 加两位十进制)
| 偏移 | 字段 | 宽度 | 说明 |
|---|---|---|---|
| 0 | magic | 3 | 前两字节固定 WR,第三字节是版本号(本波写 1);版本号并进 magic,换格式即变(§2.1 第 4 条) |
| 3 | ver | 1 | 恒 1 |
| 4 | fs_hz | 2 | 采样率,本波恒 1000 |
| 6 | pts_per_cycle | 1 | 每周波点数,本波 20 |
| 7 | ch_count | 1 | 8(带 TEMP 则 9) |
| 8 | pts_per_ch | 2 | 320 |
| 10 | pre_pts | 2 | 160(预触发点数,回放画触发光标用) |
| 12 | trig_src | 1 | 1 是手动,2 是保护启动,3 是 bench 定时 |
| 13 | trig_elm | 1 | 触发元件序号(`prot_elm` 实例下标;手动与定时为 0) |
| 14 | t0_ms | 4 | 段首相对上电时刻;配合墙钟还原绝对时刻 |
| 18 | wall | 4 | 对时后的 epoch 秒(未对时为 0) |
| 22 | eng | 1 | 封存瞬间的折算档位:0 引脚域、1 二次值、2 一次值 |
| 23 | flags_seq | 1 | 每路 VALID 压成的位图:bit s = 语义通道 s 的 `ADC_CAL_F_VALID`(电气 8 路恰好占满 1 字节;TEMP 不参与标定,若要带 TEMP 连本行与下一行一起升版本) |
| 24 | clip_mask | 2 | 削顶归属位图:bit s = 语义通道 s,与 `App/algo/meas.h` 同名参数同口径(封存瞬间 `ADC_CAL_F_CLIPPED` 快照,V0.1 恒 0xE6 = UB/UC/IB/IC/IN,见 `docs/任务规划/数据可信.md` §2.7)。⚠ **这个位图的本体是「链路可信性标记(按来源派生)」,不是「本段实测饱和标记」**:置位条件一次样本都不看(`App/algo/adc_cal.c` 291 到 296 行,该路 ROM 系数属片内 ADC 侧即置位);「这一段有没有真钉轨」走另一条链——`App/algo/adc_health.c` 的 `adc_health_rail_watch()`(280 行起)与 `adc_health_rail_sticky()`,门限按满量程百分比由 `adc_cal_rail_ths()` 给、须连续 `ADC_RAIL_N_RUN` 拍。V0.2(8 路全接 TPAFE)后本位自动恒 0,届时「与 clip_mask 对表」这条判据退化为只查钉轨实测(§10) |
| 26 | frame_seq | 4 | 段首帧序号,可与串口周期行的 frames 对账 |
| 30 | 通道表 | 每路 12 | 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1(= `adc_cal_flags(sem)` 低 8 位原值,不自造位义)+ `k_q16` 4(int32,µV/码 × 65536)+ `b_codes` 4(int32,码值)。**没有独立 LSB 字段**,LSB 就是 `k_q16`,见 §6.3 |
| — | CRC16(头) | 2 | CRC-16/MODBUS,**低字节在前**(与全文件小端统一);覆盖 `[0, 30+12×ch_count)`,本字段自身紧随其后 |
| — | 数据区 | pts_per_ch × ch_count × 2 | **按通道分列**:一路连续 320 点,单通道回放只读那一路 |
| — | CRC16(数据区) | 2 | 同上字节序;覆盖整个数据区、自身在文件尾(8 路 320 点整段 5250 B);长度为 0、任一 CRC 不符、或 `ver` 与魔数第三字节不等 → 整段作废,页面必须显示「该段不可读」 |
**段名轮转的呈现语义**(回放页与 PC 侧取「哪一段最新」都按这条):段表按**数值升序**呈现,开屏落点取**数值最大的段号**。回绕(99 之后回到 00)之后「数值最大」不再等于「时间最新」——要真时间序必须逐段读段头 `frame_seq` / `t0_ms`,代价是 100 次开文件,**本波不做**。所以页顶把段号本身显示出来(§8.1 顶部行),让读者自己看见这是第几号,不去伪造一个「最新」的假象。
### 6.3 字段口径裁定(两份独立实现的共同依据)
本波段格式有**两份独立实现**:板端编码器 `App/algo/wave_store.c` 与 PC 解码器 `test/host/wave_read.py`,两份必须逐字节同解,所以动工前把 §6.2 的含糊处一次钉死(PC 道九问的逐条答复):
| 问题 | 裁定 |
|---|---|
| CRC 字节序 | **低字节在前**,与全文件小端一致。原稿「与屏板链路同一张 CRC-16/MODBUS 表」不成立:全仓**没有**可复用的 CRC-16 表——`App/util/README.md` 记 `crc.c/h` 已于 2026-10-02 删除,屏板链路 `Drivers/BSP/UI/ui_net_sink.c` 的 `remo_crc()` 是单字节异或、不是 CRC-16。本波在 `wave_store.c` 内自带**无表位运算** CRC-16/MODBUS(0x8005 反射、初值 0xFFFF、异或输出 0),单测锚点:串 123456789 的 CRC = 0x4B37 |
| CRC 覆盖范围 | 头 CRC 覆盖 `[0, 30+12×ch_count)`、自身在 `30+12×ch_count`;数据区自 `32+12×ch_count` 起;数据 CRC 覆盖整个数据区、自身在文件尾。8 路 320 点整段 5250 B |
| LSB 从哪来 | **不设独立 LSB 字段**:通道表的 `k_q16` 就是 LSB(µV/码 × 65536),与 `App/algo/adc_cal.h` 的 `ADC_CAL_K_Q16_TPAFE 10000000` / `ADC_CAL_K_Q16_STM32 52800000` 同口径,Q16 下无舍入(选型依据 `docs/任务规划/数据可信.md` §4.1)。PC 侧 `µV = k_q16 / 65536`,不许再乘一遍 |
| k / b 宽度与角色 | `k_q16` int32、`b_codes` int32 = 封存瞬间该路生效的两个标定量。`adc_cal_apply()` 的算式是 `eng = (code + b) × k × 1e-6`,文件里存的正是等号右边两个数加原始 code,PC 照此重算。**必须取 int32 原值**,不得经 `adc_cal_k_uv()` 的 float 通道回算(float 在 5.28e7 量级掉尾数)→ 接线道在 `App/algo/adc_cal.[ch]` 补一个 int32 直读入口 `adc_cal_k_q16()`(与 `adc_cal_k_uv()` 孪生);eng=0 时两者是 ROM 默认值,PC 折出来的单位是**引脚域 µV** 而非二次值——单位一律以头里的 `eng` 为准,PC 不许按当前固件口径猜 |
| `flags_seq` 位义 | bit s = 语义通道 s 的 `ADC_CAL_F_VALID`;本波 `ch_count` 固定 8 |
| 可用性字段真值表 | 两行,实现期按 `App/algo/adc_cal.c` 280 到 298 行核出(VALID 需文件位图与 k 合法带**同时**成立;CLIPPED 纯由来源派生——该路 ROM 系数属片内 ADC 侧即置位,与样本内容无关):① **未标定**(产品构建,两点标定未做)逐路 availability = UA / UN / IA 取 `0x00`,UB / UC / IB / IC / IN 取 `0x02`,`flags_seq` = `0x00`,`clip_mask` = 0xE6;② **已标定**(电气 8 路系数全部写入且落合法带)逐路 = `0x01`(TPAFE 三路)/ `0x03`(片内 ADC 五路,VALID 与 CLIPPED 同时为真),`flags_seq` = `0xFF`,`clip_mask` 仍是 0xE6。**VALID 与 CLIPPED 是两件独立事实**:写侧如实透传 `adc_cal_flags()` 逐路原值(`App/task/adc_task.c` 548 行),读侧不得据任何品质组合判废,PC 与回放页只给软告警(见下面「判废面与软告警面」一行) |
| eng 派生式 | `eng` 不是独立事实,是逐路有效位图的派生量:**电气 8 路全 VALID → 二次值(1),缺任意一路 → 引脚域(0)**。真机两处必须同口径,行号一并登记:判据本体 `App/app/ui_data_feed.c` 226 到 235 行的 `feed_eng()`(屏显侧),段头字段填写 `App/task/adc_task.c` 532 到 553 行(其中 552 到 553 行是段头侧的同一派生)。两处分布在**不同任务里现算**,理论上会撞上配置重载的毫秒级沉降窗而不一致——这是允许态,见下一行 |
| 判废面与软告警面 | **完整性判废面自始至终只有这些**:精确尺寸等式、`magic` 与 `ver`(含 `ver` 须等于魔数第三字节)、头 CRC 与数据 CRC、表序 `sem` 等于下标、`ch_count` 上限、`pts_per_cycle` / `pts_per_ch` / `pre_pts` 下限、长度为零、文件短读。**品质组合与单位标签永远不进这道门**。本轮新增的只是**软告警面**:`avail` 同时带 VALID 与 CLIPPED 时 PC 在 NOTE 列打「同时为真,两点标定完成后属正常,非故障」;`eng` 与 `flags_seq` 违反上一条派生式时打 WARN。两者都**不改退出码、不影响出图、不拒渲染**。理由:把「单位标签不一致」当数据损坏,等于让一次改标定文件的沉降窗销毁故障证据 |
| 通道表序与列序 | 第 i 个通道表项 ↔ 数据区第 i 列,按语义号**升序**:UA=0 UB=1 UC=2 UN=3 IA=4 IB=5 IC=6 IN=7(`App/algo/adc_input.h` 的 `adc_semantic_t`)。PC 必须校验表内语义号等于下标,不等即整段作废。交叉验证:`clip_mask` 恒 0xE6 = 2+4+32+64+128,恰好是 UB/UC/IB/IC/IN 五个语义号位,这份算术就是这个顺序的证据 |
| 样本编码 | int16 小端;TPAFE 侧 16 位补码双极性;STM32 侧 0..4095 原样存正数,**不加中点偏置**——V0.1 单极性无偏置是硬件现状,加偏置等于伪造双极性(`docs/任务规划/数据可信.md` §2.7) |
| 三个时间字段 | `t0_ms`(上电毫秒)、`wall`(epoch 秒,未对时 0)、`frame_seq`(段首帧序号)均 u32 无符号小端 |
**本波 TEMP 不录**(`ch_count` = 8):温度非线性未折算,进录波只多占一路字节、不增加可解释信息(`docs/任务规划/数据可信.md` §7 的「温度不参与线性标定」已定案)。
**为什么按通道分列而不是按帧交错**:回放页只画一路 → 只需连续读那一路;PC 重算全周傅氏也是逐路独立。交错布局两种用法都要跨步长取数,白付代价。
**实现期纠正记录(三条,只作追溯,不构成规格)**
1. `flags_seq` 的 V0.1 真值一度被写成 `0x27`:那是把「按来源派生的 CLIPPED」当成了 VALID。按 `App/algo/adc_cal.c` 280 到 298 行,未标定态就是 `0x00`(见上面真值表第一行)。凡引用这个数字的地方一律以真值表为准。
2. 一版互验夹具写成 `eng` = 0 而 `flags_seq` = `0xFF`——违反上面的 eng 派生式,是一串**真机写不出来**的字节。已改为按派生式自洽,并在 `test/host/wave_store` 那套单测里加了夹具自洽断言防复发。教训一句话:**互验夹具的资格来自「真机能写出这串字节」**,一份不可能状态的字节只能验证两个读者对幻觉的一致反应,正好掩盖真分歧。
3. 顺带钉一条因果,免得后文写反:PC 侧早期一次 `shape` 子命令在 `--strict` 下 rc=1,与品质位**无关**,那是「实测触轨路数 vs 段头 `clip_mask`」对表不符(合成夹具的波形本来就没真触轨)。那条对表是 `docs/任务规划/数据可信.md` §4.6 与本文 §10 板级判据的本职,不是解码器的判废扩展。
---
## 7. 预算(先算后写,R6 是硬前置)
| 项 | 算式 | 结果 |
|---|---|---|
| 录波环 | int16 × 320 点 × 8 路 | **5,120 B** |
| 环索引与状态 | 头尾指针、封存标志、段计数、冷却计数 | 约 32 B |
| 落盘搬运缓冲 | 256 B × 2(分次写,不做整段驻留) | 512 B |
| 回放页读窗 | 256 B(与 `App/log/soe_store.c` 读窗同量级) | 256 B |
| **合计静态增量** | — | **约 5.9 KB** |
- **大于 4 KB ⇒ 触发 R6**:`docs/内存管理/内存内容分配策略.md` 的 R6 要求「任何新增静态缓冲 >4 KB 必须先核 §6 台账余量,改后同步 §6 数字、.sct 头注释与 AGENTS 栈表」;加大缓冲本身属 **Ask-first**;
- **落位 = SRAM1,不进 CCM**:CCM 受管区已 87.9 %、只剩 7,904 B,本波开工时 SRAM1 为 62.2 %(数字出自 `docs/内存管理/RAM预算台账.md` §1 受管区表;录波落地后的终值见本节末「实测回填」);录波环不是 DMA 可见对象,没有「CCM 不可达」的约束,但 CCM 也**没有余量**给它;
- **UI 子系统配额早已超**(同一张表的子系统行)→ 回放页的描述符与名表仍要在台账记账,别拿「反正已超」当免检;
- 前后各跑一次 `scripts/ram_report.py` 与 `scripts/stack_report.py`(defaultTask 多了落盘链、uiTask 多了读窗与自绘);`mem-budget` 钩子是总量红线,`ram_baseline.py --update` 回填趋势;
- **Flash**:段格式编解码 + 状态机 + 回放页 + 一条日志行,粗估 3 到 5 KB(Flash 基线在 25 % 量级,不是瓶颈);
- **NAND**:一段约 5.2 KB 数据加头。轮转 100 段约 0.5 MB,对 256 MB 单分区是零头——**瓶颈不是容量,是写次数**(§10 红线)。
**实测回填(2026-10-29 权威构建,0 错误 0 警告;逐对象台账见 `docs/内存管理/内存内容分配策略.md` §6)**
| 口径 | 上面的预估 | 实测 |
|---|---|---|
| SRAM1 静态增量 | 约 5.9 KB | 数据面 **+6,760 B**(81,536 → 88,296);UI 道另 +1,640 B;累计 **+8,400 B** → 终值 **89,936 B / 68.6 %** |
| CCM | 0(不得进 CCM) | **0**:`RW_CCM` 三档恒 57,376 B / 87.9 %,既没进 CCM 也没挤动它 |
| Flash | 3 到 5 KB | `ER_IROM1` 264,580 → 270,248(数据面 +5,668)→ 272,300(UI 道 +2,052)→ 272,360(落盘判废复检 +60)→ **272,336** B(cppcheck 返工纯搬声明,−24),累计 **+7,756 B**、占 26.0 %;`Total ROM` 274,308 B、`Code` 243,552 B |
| 任务栈 | 未估 | 九任务全 PASS、**未抬栈**:adcTask 静态最坏链 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B(余 2,080 B / 40 %)、uiTask 1,336 B(余 15,048 B / 91 %) |
- **差额构成(比预估多 860 B,可对账)**:原估漏记 `FIL` 528 B(`FF_FS_TINY=0`,落盘道刻意不上栈)、`DIR`/`FILINFO`、已封存段头副本 136 B、打包上下文(内部又含一份 136 B 段头);反向少记一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。**预估与实测都留在本节**:只写实测,下一次估算就看不见原估漏在哪。
- **回放页读窗不在数据面那 6,760 B 内**:它计入 UI 道的 +1,640 B;两张表不能相加当总量,逐对象拆分见 `docs/内存管理/内存内容分配策略.md` §6。
## 8. 现场回放显示契约
### 8.1 一屏画什么
单通道,8 路切换(上下键切通道,左右键切段),页面元素:
| 元素 | 内容 | 为什么这么定 |
|---|---|---|
| 顶部 | 通道名 + 段号 + 触发源缩写(M 手动 / P 保护 / T 定时) | 一眼知道「哪一路、哪一段、怎么来的」 |
| 波形区 | 约 120×90 px,画 **2 个周波(40 点)**,**阶梯连线**(每点占 3 px 宽) | 1 bpp 下平滑折线的平缓段会被阈值吃掉(`Drivers/BSP/UI/ui_deco.h` 的实测口径:斜线边缘易被吃、须加粗);正弦的过零段正是最平缓的部分。阶梯线每段非水平即垂直,恰好绕开这条,而且更贴近「这是离散采样」的事实 |
| 参考线 | 0 线(虚线)+ 峰值标注(定点 mV) | 没有 0 线就看不出直流偏置与削顶 |
| 底行 | 单位徽标:引脚域 / 二次值(取头里 `eng`) | 与 UI 交流量页第 7 行同一诚实性口径(`docs/UI设计/页面扩充详细方案.md` §3.1.1 的落地注) |
| 徽标 | 该路属「形状受限」类时右上打「削」 | 让钉下轨不被读成系统故障 |
| 空态 | 「无录波段」/「该段不可读」 | CRC 不符或零长度必须显式说;**画成一条直线等于伪造证据** |
### 8.2 控件与菜单:两条省事且省 RAM 的裁定
- **不开 `LV_USE_CHART`**:它在 `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 两份里都是 0,开它要过 `lv-conf-sync` 同步门禁、吃 Flash 与 RAM,而 UI 子系统配额已超;内容页框架本身是「一次性自绘、无子对象」,绘制链尾就是页面的 `draw_cb`(`docs/UI设计/内容页架构.md` 的「什么是内容页」一节),自绘线条有先例(`ui_deco`);
- **显示页要新增一条菜单叶子**(「录波回放」,放记录查询组),触发入口仍用装置维护组那条既有的「手动录波」叶子。新增叶子会改菜单树叶数与页面绑定口径:落地时必须同步 `docs/UI设计/页面补齐规划.md` 与 `docs/任务规划/UI补齐.md` 的叶数闭合,重跑 `tools/menu_gen/gen_menu.py`(`generated-fresh` 逐字节),新增汉字走中文字库链(`font-charset`)。菜单树与字库都是整文件生成物 → **UI 道单写者,同刻只允许一条道写**(`docs/任务规划/README.md` §1 的执行纪律)。
### 8.3 触发入口与回放页的分工
| 页 | 干什么 | 归哪张卡 |
|---|---|---|
| 「手动录波」操作页 | 确认即触发一段,形态沿用已规划好的操作页确认模式(`docs/UI设计/页面补齐规划.md` §二 第 26 叶) | 录波 P2 |
| 「录波回放」页 | 列段 → 选通道 → 画单通道 | 录波 P3 |
### 8.4 现场之外,PC 侧必须同时具备
- **取回**:FTP 拉 /wave/WAVE 段文件(lftpd 与 config.ini 的 ftp 段鉴权现成,无需新代码);
- **PC 脚本**:仓内新增一个纯标准库脚本(与 `test/host/iec104_master.py` 同族风格)——读段、画图、重算全周傅氏,输出「板上报数 vs PC 重算」对照表。**这才是 `docs/任务规划/数据可信.md` §4.6 那套判据想要的东西**;
- **远端看图不是波形通路**:RemoDispBus 一帧 3204 B、最坏约 4 s(`Drivers/BSP/UI/ui_net_sink.h` 的阻塞口径),只适合拍一张回放页的屏做留证。
---
## 9. 观测(日志不是免费的)
新增**一条**周期行,不铺开:
wave: seg=NN armed=1 src=P pts=320 err=N ovf=N drop=N
- `err` 是落盘失败或 CRC 自弃次数(**字节数不符就算失败**,见 §5.2);`ovf` 是冷却期丢弃的触发数;`drop` 是冻结窗口丢掉的录波点数;
- **实际落地形态(逐字)**:打印点在 `App/task/adc_task.c`:782 行,格式串 `"wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu"`——**字段名与顺序就是上面的模板**(台架与 PC 脚本按行名 grep,动它就断脚本);`src` 取 M(手动)/ P(保护)/ T(定时自抓)/ `-`(本次上电还没触发过);`armed` 只在 FREEZE 与 SEALED 为 1(COOL 期只挡新触发不录新段,画进 armed 会被台架误读);`seg` 是累计**成功回执**的段数、不是轮转号;`err` = 落盘失败 + SEALED 滞留超时自弃(含 §5.2 第四条的 ring abandoned 判废),由环内计数收口,落盘道不数第二份账。`err` 与 `ovf` 的**已知容差**(lead 裁定 2026-10-29,不修只入档):这两个计数是跨上下文的读-改-自增(`ovf` 由任意上下文的拒绝路径加、`err` 由 adcTask 的滞留自弃与 defaultTask 的落盘失败分别加),本层双端编译、不许引 FreeRTOS / CMSIS 的临界区内在函数,所以并发拒绝或并发失败时**理论上可能少计一次**。定性为**观测计数的已知容差**:只用于趋势与判读,**不作为任何判据的唯一依据**——要坐实「冷却期内确实来了第二次触发」,同时看 `armed` 与那一刻 `/log/start.log` 的时刻。契约原文见 `App/algo/wave_ring.h` 文件头「观测计数的已知容差」段;
- **落盘道失败留痕名清单(2026-10-30 并入原卡片登记簿;台架判读按名索引,不新增第二种形态)**:单板上一律一行 `[WAVE] seg drop: <原因> (idx=… written=…)`,统一打印点 `App/task/default_task.c`:816——`crc selftest FAIL` / `mkdir /wave` / `opendir /wave` / `readdir /wave` / `seg_path` / `pack_start` / `f_open` / `pack_next` / `f_write short` / `pack stall` / `f_close` / `len mismatch` / **`ring abandoned`**(最后一条即 §5.2 第四条陷阱的处置留痕,代码与主机单测面已有、板级未测,观察项见 §10);另有一条非失败的慢段留痕 `[WAVE][WARN] seg held <毫秒> ms (idx=…)`(`App/task/default_task.c`:831)——搬得慢不是错,真失败才走 `seg drop`。
- 只在原先静默的黑洞处补锚点,别重复已有步骤——每条 DBG 日志约 90 B Flash 加 216 B 栈(`AGENTS.md` 的日志与工具链节);
- 该行已同步进 `docs/测试说明/专项验证/ADC前端注入验证流程.md` 的读数出口表(2026-10-29;同一张表还登记了段文件出口与 PC 侧读段工具)。
---
## 10. 测试与门禁
| 层 | 内容 |
|---|---|
| 主机侧纯逻辑单测(新增,进 `test/host/`) | ① 环:预触发与后触发点数、冻结与解冻、冷却丢弃计数;② 段格式编解码往返,含 CRC 不符、零长度、字节数不符三条失败路径;③ 触发状态机三条来路(手动 / 保护 / bench)。**环与格式必须是纯逻辑**(不碰寄存器、不碰 RTOS)才能这样测——这是 `App/algo/` 既有分层纪律 |
| PC 侧工具的定性 | `test/host/wave_read.py` 是 **bench 工具**(`test/host/` 下的 loose .py,先例 `test/host/ftp_capture.py`),**不是**主机单测套件 → 不建 `run_unit_test.py`、不加 pre-commit 专属钩子(专属钩子默认不补,按 Lead 裁定 2026-10-27);它自带 `--selftest` 就是它的自带回归,跑法与四个子命令见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §4 |
| 跨工具链分叉(实证,写进来是为了下一个模块别重踩) | 两份录波 .c 只 include 自身头(链上只有 stdint.h)时,ARMCC 报 22 条 identifier "NULL" is undefined;而主机 MSVC / gcc 会传递性把 stddef.h 拉进来,**主机单测完全照不出这个分叉** → 新增模块需要用 NULL 就自己 include stddef.h(与 `App/algo/sample_ring.c` 同口径,别塞公共头) |
| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性与注入一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表。**判据只量四样:周波数、幅值、极性、钉轨路数——不比绝对相位**(理由见 §4.2「段内点序」:触发瞬间在工频周期内任意落点,段字节里没有绝对时间参考;真要比相位必须让发生器给同步触发,本波不做)。**钉轨对表的合格判据 = 头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释**——因为 `clip_mask` 是按来源派生的链路可信性标记(§6.2 偏移 24 行),不是本段实测值:只加直流或小幅注入时那五路本就不该钉轨,硬取等式会假红。**同段另起一条强观察项**:V0.1 下这五路若在做**交流注入**时反而没钉轨,先怀疑接线或该路根本没被驱动——这是告警面,不是失败面(等式仍作为默认尺子留在 PC 侧 `shape --strict` 里,因为它有这个信号价值;要放行差异用那条判据的解释权,不靠改尺子)。**§5.2 第四条的板级观察项(代码与单测面已有、板级未测)**:bench 连续自抓期间人为把卷占住 2 s 以上(例如同一段跑 FTP 大文件读),看串口 `wave:` 行的 `err` 是否加一、是否只出现**一条** `[ERR] … seg drop: ring abandoned` 留痕(不是每拍刷屏)、且段号不推进(下一段仍写同号文件) |
| 板级·触发来路(2026-10-30 并入原卡「手动触发与保护启动接线」板级段) | 真机面板按一次「手动录波」出一段;造一次过流,段号与该时刻 `/log/start.log` 事件对齐;冷却期内二次触发只加 `ovf` 不重录(**同时看 `armed` 与那一刻 start.log 的时刻,不要只盯 `ovf` 数字**——容差见 §9);冻结窗口内 104 上送与保护判据不受影响(这条是「丢录波样本不丢采集」的验收) |
| 板级·回放页(2026-10-30 并入原卡「现场单通道回放页」板级段) | 真机上放一段真波(不是模拟器合成节拍);远端看图能拍到该页做留证(只作留证,不是波形通路,§8.4);**空态与 CRC 失败态绝不允许画成一条直线**——用坏段在真机复现一次 |
| UI | `ui-verify` 五步;回放页入 `ui-golden` 黄金参考(**参考图一旦入库只能改代码对齐,禁止刷参考掩盖**);新叶走 `gen_menu.py` 重生成 + 字库链 |
| 构建 | `/keil-build` 要求 0 错误 0 警告;新 .c 双端注册走 `new-source-file`(`MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt` 两处显式列表) |
| 台账 | R6 三处同步 + `scripts/ram_baseline.py --update` + `mem-numbers` 与 `mem-budget` 钩子 |
| 板级(读路径耗时,2026-10-29 补) | 进回放页后**连续切段 + 连续切道**,读 `task_prof` 里 uiTask 的单轮工作耗时峰值与 800 ms 窗的余量;**不要为此新加埋点**(现成读数够用)。判读:峰值贴着窗走就是读路径该再加节流(先想缓存与分次,别去动心跳窗) |
| 图与生成物 | 本设计文档的配图生成器受 `generated-fresh` 逐字节幂等约束;脚本与产物同一笔提交 |
**bench 定时自抓的红线**:定周期自抓在 1 kHz / 8 路下每 2 s 一段,每 5 分钟就是一轮 NAND 写。**长时间挂着它白烧写次数**,而 dhara 的坏块记账是持久代价、普通恢复清不掉(`AGENTS.md` 测试构建节,`docs/测试说明/专项验证/存储维护与容量恢复.md`)→ 定时模式必须**默认关闭 + 有时长上限 + 启动时打一次告警**,用完立刻回产品构建。
**本波执行顺序(2026-10-30 并入原卡片登记簿 §3;BENCH 与 COOL 两个判定就是「板级那一轮」未跑的回路,DONE = 台账回填 + 文档现状化)**:
```mermaid
flowchart TD
DSG[设计定稿: 本文 + §10 判据] --> RING[专用录波环与段格式编解码]
RING --> PC[PC 侧读段脚本与对表]
PC --> BENCH{bench 台架对表通过?}
BENCH -- 否: 回头改环或格式 --> RING
BENCH -- 是 --> TRIG[两路真实触发来路: 手动 + 保护启动]
TRIG --> COOL{冻结窗口内采集与上送无恙?}
COOL -- 否 --> TRIG
COOL -- 是 --> PAGE[单通道回放页与叶子上线]
PAGE --> DONE[录波本波收口: 台账回填 + 文档现状化]
PAGE -. 二期另立项 .-> NEXT[4 kHz 全速 / 突变量启动 / COMTRADE / 104 文件传输];
```
图内不写步骤号,节点名与 §10 各行语义逐字可对。并行度规则见 `docs/环境与部署/并行任务工作树方案.md` §7.3;**UI 道是单写者**——菜单树与中文字库都是整文件生成物,动叶子的那条道不许与别的改 UI 的道并行。
---
## 11. 与 `docs/任务规划/数据可信.md` 的口径变更(三处)
| 旧口径 | 新口径 | 原因 |
|---|---|---|
| 「录波通道必须先有工程量,排在本波之后」 | 录波录**原始码值**,不等工程量;解释靠头里的系数快照 | §3.2:现在根本没有工程量,等下去等于永远不动 |
| 动态波形验证的读数出口 = 派生标量(`meas:` / 104 / `phasor(hb)`) | 派生标量仍是**判据**,但样本改从录波段导出 → 判据升级为「PC 独立重算 vs 板上读数」对表 | 录波段就是现成的样本出口,不必再造一条串口通路 |
| 动态波形验证「手边可做、排在最前」 | 排在录波 P1 之后(用户裁定 2026-10-29:录波先行) | 先有样本通路,验证才不留遗憾 |
(2026-10-30 更新:原卡片登记簿已销项并入本文,§10 尾有本波执行顺序图;排期顺位在 `docs/任务规划/README.md` §2.0「独立」行。)
---
## 12. 风险
| 风险 | 后果 | 处置 |
|---|---|---|
| 削顶五路被读成「系统有零序 / 有谐波」 | 现场误判 | 头里 `clip_mask` + 页面「削」徽标 + 空态不许画直线(§8.1) |
| 有人把 U 与 I 两段叠图比相位 | 得出**没有物理意义**的相位差 | 回放不做多路叠加;头里写明来源转换器;与 `docs/任务规划/数据可信.md` §9 的 V0.1 禁止行同源 |
| 落盘把 defaultTask 拖住 | 日志与配置引擎变慢(defaultTask 是两者唯一写者) | 分次写(256 B 双缓冲)、一次心跳最多一段、`f_write` 前后有耗时观测;不做整段驻留再写 |
| **回放读路径把 uiTask 拖住**(2026-10-29 lead 评审补,此前风险表只有写侧没有读侧) | 一次渲染的实码 I/O(`App/task/ui_task.c` 376 到 398 行):段表刷新 1 次 `f_opendir` + 至多 100 次 `f_readdir`;未缓存段的段头装载 = 1 次 `f_open` + 1 次 128 B 头读 + **20 次 256 B 数据区读并边读边算数据 CRC** + 尾 2 B 一次 = **最坏 22 次小读**;已验过则只剩窗装载 = 1 次 `f_open` + 1 次 `f_lseek` + 1 次 80 B 窗读 + 40 点解码与峰值扫描(**一屏只碰一路通道一个窗,不是 8 路 320 点**)。uiTask 在 IWDG `required_mask` 内、窗 800 ms(口径同 `docs/子系统设计/看门狗设计说明.md` §11.3 的解耦心跳)→ **现象先是 LVGL 停摆与远程推流缺席,不是复位**(心跳与画屏解耦) | 卷锁 try 非阻塞(拿不到就显示「存储忙」,不等)+ 256 B 分次读 + 已验段结果缓存(同段同道不重复验)+ 只在首次进页 / 切段 / 切道时读。**单轮耗时上界未实测**:不新加埋点,用现成的 `task_prof` 读数(板级动作见 §10) |
| 触发风暴 + 轮转覆盖 | 现场唯一一段有效波形被后到的抖动冲掉 | 冷却期 + `ovf` 计数;「最新段号」进快照;分段优先级属二期 |
| 定时自抓忘关 | 白烧 NAND 写次数(持久代价) | 默认关 + 上限 + 告警(§10 红线) |
| 标定落地后老文件被解释错 | 同一个系数字段值变了含义漂移 | 头里存**封存瞬间**的 `eng` 与系数快照;PC 脚本按头解释,绝不按当前固件口径解释 |
| 环写成 float 或按帧交错 | RAM 翻倍 / 单通道回放跨步取数 | §6.2 与 §7 把形态钉死为 int16 分列 |
| 拿 1 kHz 的图宣称「看清波形」 | 掩盖与产品 80 点口径的偏离 | §2 偏离登记 + §2.1 结构约束写进代码注释 |
---
## 13. 范围边界(本波不做)
| 不做 | 归属 |
|---|---|
| 4 kHz 全速、每周波 80 点 | 二期;触发条件 = 采集节拍改造与算法层抽取结构一并评审(§2.1) |
| 突变量启动 | `docs/算法/继电保护功能复现规划.md` 的「故障启动(波形启动)」行 |
| COMTRADE 导出、104 文件传输 | 送检与第三方交接时才需要(产品需求「文件传输(录波)」行至今标未开始) |
| 参考机形态(录波 65 通道;3 类型 / 6 段 / RAM 16 个) | 二期对标;本波只有 8 路单段 |
| 电度与暂态量录波 | 不属本波 |
| 60 Hz 工况自适应 | `pts_per_cycle` 已留字段,但不做实现 |
---
## 14. 与其它文档的关系
| 文档 / 工作 | 关系 |
|---|---|
| 原任务规划《故障录波》卡片登记簿(2026-10-30 销项删除) | 卡片判据与顺序图已并入本文 §9 与 §10;排期顺位归 `docs/任务规划/README.md` §2.0「独立」行——判据不再另立文件 |
| `docs/任务规划/数据可信.md` | 上游:静态与动态信度、削顶归属由它定义;本文 §11 记录因录波而变的三处口径 |
| `docs/子系统设计/ADC采样节拍设计说明.md` | 数据源与节拍属主;本波**不改**它,二期改节拍时必须回头改它 |
| `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 | FatFs 陷阱与 8.3 短名的属主;本文 §5.2 只是消费者 |
| `docs/UI设计/页面补齐规划.md`、`docs/任务规划/UI补齐.md` | 「手动录波」叶子旧状态是「不做(无录波模块)」;P2 与 P3 落地时两处的叶数闭合与重开条件要同步 |
| `docs/问题记录/采集与数据处理短板评测.md` | 记录链缺口(波形数据面 / COMTRADE / 104 文件服务)的登记处;P1 落地后该行改指本文 |
| `docs/产品需求/参考产品复刻明细.md` | §7「录波采样率」行是本文 §2 偏离的对照基准 |
| `docs/硬件平台/ADC通道映射说明.md` | 通道归属与注入点清单的属主;回放页「削」徽标的判据出处 |