From d64bbdc57fbe5354690c44560449a06da6d7af5e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:23:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(wave):=20=E6=95=85=E9=9A=9C=E5=BD=95?= =?UTF-8?q?=E6=B3=A2=E6=95=B0=E6=8D=AE=E9=9D=A2=E3=80=81=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=E4=B8=8E=E5=8D=95=E9=80=9A=E9=81=93=E5=9B=9E=E6=94=BE=E9=A1=B5?= =?UTF-8?q?=EF=BC=88=E8=BD=AF=E4=BB=B6=E9=97=AD=E7=8E=AF=E4=BA=A4=E4=BB=98?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据面(App/algo,纯逻辑,双端编译) - wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask 唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃 兜底——下一次真故障无波可录比丢一段更贵 - wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带 锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度 - 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式 - 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样 纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉 - test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条 自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合 - 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定) 接线(任务层) - adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图), 段头按最低有效位精确记账,不经浮点回算 - adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实; 台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行 - defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到 关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms 补一条告警并记历史峰值(喂狗余量可核) - Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留 超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的, 半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进 - Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到 上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数; 段文件字节布局零改动,三份合成段哈希逐字节不变 - protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体 零改动 人机(160×160 单色屏) - 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器 得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30 - 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与 垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、 单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据 - ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、 手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口 - 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有 60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形 - 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变 - 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏 22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、 仪器用已在跑的单轮工作耗时探针,不新加埋点 台账与文档 - 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936 字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名; 散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建 重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置) - 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据); §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径 风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」 - 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项 - 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划 索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动) - 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件 形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记 「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场 - 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行 227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退 为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与 体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账 同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道 编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串 生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过 --- App/algo/README.md | 96 +- App/algo/adc_cal.c | 22 + App/algo/adc_cal.h | 16 + App/algo/wave_ring.c | 535 +++++++ App/algo/wave_ring.h | 406 ++++++ App/algo/wave_store.c | 981 +++++++++++++ App/algo/wave_store.h | 491 +++++++ App/task/adc_task.c | 180 +++ App/task/default_task.c | 269 ++++ App/task/protect_task.c | 23 + App/task/ui_task.c | 473 +++++++ Drivers/BSP/UI/ui_data.c | 84 ++ Drivers/BSP/UI/ui_data.h | 105 ++ Drivers/BSP/UI/ui_font_cn_12.c | 481 ++++--- Drivers/BSP/UI/ui_menu_tab.c | 1 + Drivers/BSP/UI/ui_menu_tab.h | 2 +- Drivers/BSP/UI/ui_page_registry.c | 4 + Drivers/BSP/UI/ui_page_registry.h | 2 +- Drivers/BSP/UI/ui_page_wave_manual.c | 126 ++ Drivers/BSP/UI/ui_page_wave_manual.h | 30 + Drivers/BSP/UI/ui_page_wave_play.c | 435 ++++++ Drivers/BSP/UI/ui_page_wave_play.h | 29 + MDK-ARM/STM32F407-Demo.uvprojx | 20 + MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct | 9 +- docs/README.md | 2 +- docs/UI设计/LVGL模拟器部署与使用说明.md | 4 +- docs/UI设计/UI_中文字体生成与配置.md | 6 +- docs/UI设计/UI_数据供给设计说明.md | 2 +- docs/UI设计/UI_界面制作流程.md | 2 +- docs/UI设计/UI_自动化测试流程.md | 8 +- docs/UI设计/UI_设计架构.md | 2 +- docs/UI设计/mockups/01_主界面_说明.md | 2 +- docs/UI设计/内容页架构.md | 2 +- docs/UI设计/菜单页面设计.md | 4 +- docs/UI设计/页面扩充详细方案.md | 4 +- docs/UI设计/页面补齐规划.md | 9 +- docs/images/故障录波数据流.svg | 2 +- docs/产品需求/产品需求设计文档.md | 40 +- docs/任务规划/README.md | 15 +- docs/任务规划/UI补齐.md | 35 +- docs/任务规划/故障录波.md | 10 +- docs/任务规划/数据可信.md | 7 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 15 +- docs/内存管理/内存内容分配策略.md | 24 +- docs/子系统设计/故障录波设计说明.md | 75 +- docs/子系统设计/运行与自检记录设计说明.md | 2 +- docs/架构设计/项目架构总览.md | 2 +- docs/测试说明/ADC前端注入验证流程.md | 4 +- docs/测试说明/主机单测覆盖率口径.md | 2 +- docs/测试说明/自动化测试流程.md | 2 +- docs/门禁说明/README.md | 2 +- docs/门禁说明/coding-standard.md | 10 + docs/门禁说明/nested-comment.md | 9 + docs/门禁说明/ui-golden.md | 6 +- docs/问题记录/采集与数据处理短板评测.md | 16 +- scripts/check_nested_comment.py | 4 +- scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 26 +- scripts/test_coverage_baseline.json | 211 ++- simulator/CMakeLists.txt | 9 + simulator/README.md | 4 +- simulator/main.c | 394 +++++- simulator/tests/README.md | 2 +- simulator/tests/check_content_pages.py | 74 +- simulator/tests/ref/shot_wave_manual.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_manual_done.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_manual_ro.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play_bad.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_wave_play_empty.bin | Bin 0 -> 3200 bytes test/host/wave_read.py | 1256 +++++++++++++++++ test/host/wave_ring/run_unit_test.py | 50 + test/host/wave_ring/wave_ring_test.c | 538 +++++++ test/host/wave_store/run_unit_test.py | 51 + test/host/wave_store/wave_store_test.c | 772 ++++++++++ test/host/wave_trig/run_unit_test.py | 43 + test/host/wave_trig/wave_trig_test.c | 677 +++++++++ .../arch_diagram/gen_wave_record_flow_svg.py | 23 +- ui_cn_chars.txt | 2 +- ui_menu_tree.txt | 3 +- 81 files changed, 8868 insertions(+), 424 deletions(-) create mode 100644 App/algo/wave_ring.c create mode 100644 App/algo/wave_ring.h create mode 100644 App/algo/wave_store.c create mode 100644 App/algo/wave_store.h create mode 100644 Drivers/BSP/UI/ui_page_wave_manual.c create mode 100644 Drivers/BSP/UI/ui_page_wave_manual.h create mode 100644 Drivers/BSP/UI/ui_page_wave_play.c create mode 100644 Drivers/BSP/UI/ui_page_wave_play.h create mode 100644 simulator/tests/ref/shot_wave_manual.bin create mode 100644 simulator/tests/ref/shot_wave_manual_done.bin create mode 100644 simulator/tests/ref/shot_wave_manual_ro.bin create mode 100644 simulator/tests/ref/shot_wave_play.bin create mode 100644 simulator/tests/ref/shot_wave_play_bad.bin create mode 100644 simulator/tests/ref/shot_wave_play_empty.bin create mode 100644 test/host/wave_read.py create mode 100644 test/host/wave_ring/run_unit_test.py create mode 100644 test/host/wave_ring/wave_ring_test.c create mode 100644 test/host/wave_store/run_unit_test.py create mode 100644 test/host/wave_store/wave_store_test.c create mode 100644 test/host/wave_trig/run_unit_test.py create mode 100644 test/host/wave_trig/wave_trig_test.c diff --git a/App/algo/README.md b/App/algo/README.md index 7ac0243d..27659538 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -18,11 +18,15 @@ | `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`(纯枚举 + 声明)← `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_cal_rail_ths()` 取,本层仍零算法依赖)。 @@ -38,6 +42,11 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 `prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,即「数据可信」遥测快照发布)。节拍 = 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()`,不写死下标;② **禁止跨转换器**做 @@ -124,11 +133,67 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 (本层**不** 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`),负值才是错误码——写断言前先读头文件的「返 回 值」行(本层单测两次踩在这上面)。 + ## 4. 构建与测试 ### 4.1 挂载与模拟器边界 -- 九个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 -- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在 +- 十一个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `` 内。 +- 模拟器侧分两类(Lead 裁定 2026-10-29):`wave_ring.c` 与 `wave_store.c` **真实参与** + `simulator/CMakeLists.txt`——回放页要在模拟器里出真波形,两份又零平台依赖,与「双端共享核心、不造 + 第二真相」同一条口径,故**不进** `SIM_EXEMPT`;其余九个采集链文件(UI 模拟器不跑采集链路)仍登记在 `scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(登记簿条目「rs485_task 白名单缺口」)。 ### 4.2 主机侧纯逻辑单测——生产源文件以 `#include` 原样纳入 @@ -143,10 +208,20 @@ 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 板端 `@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、 `cal: ua_mv=… vl=0x00 cl=0xE6 rail=…`、`ring` / `skip` / `adc1_*` / `tgap`); @@ -157,12 +232,24 @@ ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(「采样通道自检」 ## 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`(新建子目录还要补 ``)。 模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加 `scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。 -3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。 +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「已停发」栏:停发不等于可以新发)。 4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。 5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。 +6. **单测产出的互验字节(要被别人的脚本读的那些):跑测之后立刻刷新副本并记下 sha256**。三套录波单测把合成段落 + 在临时目录、并把**绝对路径 + 是哪一份形态**打进日志,所以「跑完顺手拷一次、以后再没刷」必然留下旧副本—— + 本仓这类坑不止一次(旧副本被当本次证据引用过:一次是别人拿上一轮的字节下了「字节级已闭合」的结论)。 + 判据:引用方只认**当次运行日志里那条路径 + 当次 sha256**;产物有第二种形态(例如本波真值形态与「已标定后 + 仍削顶」的构造形态)就在文件名或日志里把形态写明,别让读者靠猜;拷进 `ci-artifacts/wave_interop/` 之类的 + 共享目录时,同一笔操作里就把新 sha 一起报给对岸,别隔一轮。 ## 6. 相关文档 @@ -171,5 +258,8 @@ 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 §2 - 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md` - RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index c13d24fa..fdb4e436 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -27,6 +27,9 @@ * 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) */ /* 头文件包含区 */ @@ -336,6 +339,25 @@ 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 diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 62f63981..c5caeb52 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -31,6 +31,10 @@ * 三路 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 @@ -149,6 +153,18 @@ float adc_cal_k_uv(adc_semantic_t sem); */ 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(「数据可信」标定落盘与整定入口) * 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t diff --git a/App/algo/wave_ring.c b/App/algo/wave_ring.c new file mode 100644 index 00000000..2d0d91a2 --- /dev/null +++ b/App/algo/wave_ring.c @@ -0,0 +1,535 @@ +/* + * 模块名称:录波环(故障录波 · 数据面纯逻辑层:滚动环 + 触发冻结状态机 + 已封存区读口) + * 模块功能:见 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 台基线实跑 @build 才暴露:本层 + * wave_ring.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c + * 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */ +#include + +/* 触发挂号的载荷编码(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]; +} diff --git a/App/algo/wave_ring.h b/App/algo/wave_ring.h new file mode 100644 index 00000000..d70ff782 --- /dev/null +++ b/App/algo/wave_ring.h @@ -0,0 +1,406 @@ +/* + * 模块名称:录波环形缓冲层(故障录波 · 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 +#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 */ diff --git a/App/algo/wave_store.c b/App/algo/wave_store.c new file mode 100644 index 00000000..76a9d7a2 --- /dev/null +++ b/App/algo/wave_store.c @@ -0,0 +1,981 @@ +/* + * 模块名称:录波段格式层实现(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) + * 模块功能:见 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 台基线实跑 @build 才暴露:本层 + * wave_store.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c + * 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */ +#include + +/* 打包游标一次最多从数据源取多少个点(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; +} diff --git a/App/algo/wave_store.h b/App/algo/wave_store.h new file mode 100644 index 00000000..4c0ec498 --- /dev/null +++ b/App/algo/wave_store.h @@ -0,0 +1,491 @@ +/* + * 模块名称:录波段格式层(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) + * 模块功能:把「一段录波」在内存字节缓冲与结构化元数据之间逐字节翻译,并给落盘道 + * 提供分次搬运的打包游标。口径唯一属主 = 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 + +#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 */ diff --git a/App/task/adc_task.c b/App/task/adc_task.c index c01a2f31..10c2f87c 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -106,6 +106,15 @@ * 不并进 cal: 行是因为 9 个码值会把该行推到 168 字符、 * 撞 DBG_CON_LINE_MAX(192) 的静默截断边界(见该行注释)。 * 无新增跨任务状态、无新增外设访问,栈 +36 B(局部码值数组) + * v3.4 2026-10-29 DTU 项目组 故障录波数据面接线(task-2):新增 2c——每拍把 8 路 + * 语义原码压进 wave_ring 并 tick 推进;EVT_SEALED 那一拍现取 + * 折算/有效性/墙钟事实组 wave_seal_cal_t 并 latch_meta 恰好 + * 一次(握手次序照 App/algo/wave_ring.h 头注唯一合法次序); + * 心跳末尾加 §9 唯一周期行 wave:(字段逐字,取数 + * wave_ring_get_stat);新增 2d——bench 定周期自抓,只在 + * DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0 的双层编译门内存在, + * 默认关闭 + 5 分钟时长上限 + 启动一次性告警(§10 红线); + * adc_task_init 内接 wave_ring_init(init 禁日志约束不破) */ #include "adc_task.h" @@ -123,6 +132,8 @@ #include "console_log.h" #include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */ #include "task_prof.h" /* 常开耗时探针:单拍耗时 */ +#include "wave_ring.h" /* 故障录波环(task-2 数据面接线):push/tick/latch_meta/get_stat */ +#include "sys_clock.h" /* 封存交棒的 wall 字段:sys_clock_get() epoch 秒(UINT32_MAX=未对时,写 0) */ #define DBG_TAG "[ADC_TASK]" #include "dbg_log.h" @@ -132,6 +143,17 @@ * 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */ #define ADC_TASK_REPORT_MS 10000U +/* bench 定周期自抓(§10 红线「默认关闭 + 有时长上限 + 启动打一次告警」的落点): + * WAVE_BENCH_AUTOCAP 默认 0——产品档连外层 DTU_BENCH 都没有,双层编译门保证 + * 产品镜像里 2d 段整段不存在;台架确实要定周期刷段时才本地改 1 重编译, + * 用完立刻回 0(每 2 s 一段 = 每 5 分钟一轮 NAND 写,长挂白烧写次数,dhara + * 坏块记账还是持久代价)。宏形态照 App/algo/adc_cal.c 的 adc_cal_prot_blocked(): + * 联合条件放 #if,未定义宏恒为假,不把 cppcheck 引到旁路上。 + * 周期/上限取自 §10 的算式:2 s/段、300 s 自动停。 */ +#define WAVE_BENCH_AUTOCAP 0 +#define WAVE_BENCH_PERIOD_MS 2000u +#define WAVE_BENCH_MAX_MS 300000u + /* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。 * 原始码值缓冲由 语义取数与有效性层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */ static uint32_t s_frames_consumed; @@ -157,6 +179,20 @@ static uint8_t s_rail_syslogged; * 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */ static uint32_t s_meas_ms; +/* 故障录波环接线状态(task-2;状态机与计数全在 App/algo/wave_ring.c,这里只搬运): + * s_wave_codes - 本拍 8 路语义原码(文件级 static,不上单拍的栈——本任务静态 + * 最坏链余量按 1024 B ISR 底线核) + * s_wave_cal - EVT_SEALED 拍的交棒结构(同理不放局部:整块 ~160 B) */ +static int16_t s_wave_codes[WAVE_RING_CH]; +static wave_seal_cal_t s_wave_cal; +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) +/* bench 自抓编排:on = 已过写者就绪门开始跑;off = 到时自动停(init 重置) */ +static uint8_t s_wave_bench_on; +static uint8_t s_wave_bench_off; +static uint32_t s_wave_bench_t0; +static uint32_t s_wave_bench_next; +#endif + /* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。 * ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就 @@ -288,6 +324,13 @@ int adc_task_init(void) { s_meas_ms = 0u; adc_health_init(); /* 内含 采样通道自检 贴轨观察器复位(rail_init) */ (void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,init 无日志) */ + wave_ring_init(); /* 录波环纯清 RAM(无内核依赖;本函数在开闸前跑,init 禁日志不破) */ +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) + s_wave_bench_on = 0u; + s_wave_bench_off = 0u; + s_wave_bench_t0 = 0u; + s_wave_bench_next = 0u; +#endif return 0; } @@ -454,6 +497,103 @@ void adc_task_func(void *arg) { adc_health_report(&h_st); } + /* 2c) 故障录波环泵送(task-2 数据面接线;状态机与计数全在 App/algo/wave_ring.c, + * 本段只做搬运)。每拍取 8 路语义**原码**压环 + tick 推进;触发那拍的样本 + * 落在段内点 WAVE_RING_PRE-1(点序 lead 钉死)。只存原码不存折算工程量—— + * §6.3 裁定:样本域 = 原始码,折算系数进段头,回放/PC 侧统一换算。 + * 某路取不到(开机头几拍)保持上值(与 adc: 心跳行同一兜底口径)。 + * tick 返 EVT_SEALED 的那拍必须**同拍** latch meta(头注唯一合法次序), + * 漏了 = 2 s 后环自弃计 err,defaultTask 根本不会见到这一段。 */ + { + uint8_t w; + int32_t wev; + + for (w = 0u; w < WAVE_RING_CH; w++) { + int32_t wc = 0; + + if (adc_input_raw((adc_semantic_t)w, &wc) == 0) { + s_wave_codes[w] = (int16_t)wc; + } + } + (void)wave_ring_push(s_wave_codes, WAVE_RING_CH); + wev = wave_ring_tick(now, s_frames_consumed); + + if (wev == WAVE_RING_EVT_SEALED) { + uint8_t c; + + /* 封存瞬间事实(交棒结构 s_wave_cal 为文件级 static,不上栈): + * eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b。 + * eng 口径 = adc_cal.h 注③ 的 8 路量纲标签(判据源 + * App/app/ui_data_feed.c feed_eng(),与 prot_blocked 的三路口径**不是** + * 一件事):8 路电气量全 VALID 才是二次值;V0.1 无 CT 变比,一次值不可达。 + * wall 未对时写 0(sys_clock_get 失败 = UINT32_MAX,不许拿上电毫秒冒充)。 + * k 取 adc_cal_k_q16() int32 原值;b 的 float 回乘在本域无损 + * (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k)。 */ + s_wave_cal.flags_seq = 0u; + s_wave_cal.clip_mask = 0u; + for (c = 0u; c < WAVE_RING_CH; c++) { + const uint32_t f = adc_cal_flags((adc_semantic_t)c); + const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c); + + if ((f & ADC_CAL_F_VALID) != 0u) { + s_wave_cal.flags_seq |= (uint8_t)(1u << c); + } + if ((f & ADC_CAL_F_CLIPPED) != 0u) { + s_wave_cal.clip_mask |= (uint16_t)(1u << c); + } + s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source + : (uint8_t)ADC_SRC_STM32; + s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE) + ? 16u : 12u; + s_wave_cal.ch[c].avail = (uint8_t)f; + s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c); + s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c); + } + s_wave_cal.eng = (s_wave_cal.flags_seq == 0xFFu) + ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; + { + uint32_t wall = sys_clock_get(); + + s_wave_cal.wall = (wall == UINT32_MAX) ? 0u : wall; + } + /* 只在 EVT_SEALED 拍调一次;返错不补救——环 2 s 超时自弃兜底, + * 这里补第二行日志就违反 §9「不在已留痕处重复锚点」 */ + (void)wave_ring_latch_meta(&s_wave_cal); + } + } + + /* 2d) bench 定周期自抓(§10 红线落点:默认关闭 + 时长上限 + 启动一次性告警)。 + * 编译门 = DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0,产品镜像无本段。 + * 节拍:每 WAVE_BENCH_PERIOD_MS 投一次 bench 请求(环侧冷却/去重自管, + * 被丢的计入 ovf——这是 bench 的预期观测面,不是故障)。 + * 计时起点等写者就绪门(v2.1 理由:就绪前直发会与 defaultTask 存储 + * 日志字节级交错);到时自动停 + 一次性 STOP 行,之后不再投任何请求。 + * ⚠ 这两条 WARN/STOP 是一次性生命周期日志,不是周期行,§9 一条线约束不破 */ +#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0) + if (s_wave_bench_off == 0u) { + if (s_wave_bench_on == 0u) { + if (dbg_console_writer_ready() != 0u) { + s_wave_bench_on = 1u; + s_wave_bench_t0 = now; + s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; + /* [WARN] 进正文(本仓无 WARN 级;用 ERR 级会带 [ERR] 误触 @test 禁用标记) */ + DBG_INFO("[WARN] wave bench autocap ON: 1 seg / %u ms, auto-stop %u s (BENCH BUILD ONLY)", + (unsigned int)WAVE_BENCH_PERIOD_MS, + (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); + } + } else if ((uint32_t)(now - s_wave_bench_t0) >= WAVE_BENCH_MAX_MS) { + s_wave_bench_off = 1u; + DBG_INFO("wave bench autocap STOP after %u s", + (unsigned int)(WAVE_BENCH_MAX_MS / 1000u)); + } else if ((uint32_t)(now - s_wave_bench_next) >= WAVE_BENCH_PERIOD_MS) { + s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS; + (void)wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 0u); + } else { + /* 本拍未到周期点:无事可做(比较用无符号差值,回绕安全) */ + } + } +#endif + /* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走 * 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错 * (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。 @@ -610,6 +750,46 @@ void adc_task_func(void *arg) { (long)(ms.freq_hz * 100.0f), (unsigned)ms.freq_valid); } + + /* 录波环周期行(docs/子系统设计/故障录波设计说明.md §9 逐字模板, + * 字段名与顺序**不许动**——PC/台架脚本按行名 grep)。全链路只加这一条 + * 周期行(每条 DBG ≈90 B Flash + 216 B 栈,§9 明令不加第二条): + * seg = 累计成功 ACK 的段数(wave_ring_stat.seg_total,不是轮转号) + * armed= 1 表示此刻有段在飞(FREEZE/SEALED;IDLE/COOL 为 0—— + * COOL 只挡新触发不录新段,画进 armed 会让台架误读) + * src = 触发源 M 手动 / P 保护 / T bench 定时,- = 本 boot 未触发过 + * err = 落盘失败 + SEALED 超时自弃(环内 err_cnt 收口,落盘道不数 + * 第二份账);ovf = ACK 前重复触发与冷却期丢弃数; + * drop = 冻结窗口丢弃的录波点数(单位=点) + * 判读:bench 自抓一轮后 seg_total 应增长;err 涨而 seg 不涨 = + * 落盘道在丢段,查 defaultTask 侧 [WAVE] 失败行 */ + { + wave_ring_stat_t wst; + + (void)wave_ring_get_stat(&wst); + { + const char *sc; + + if (wst.trig_src == (uint8_t)WAVE_TRIG_MANUAL) { + sc = "M"; + } else if (wst.trig_src == (uint8_t)WAVE_TRIG_PROTECT) { + sc = "P"; + } else if (wst.trig_src == (uint8_t)WAVE_TRIG_BENCH) { + sc = "T"; + } else { + sc = "-"; + } + DBG_INFO("wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu", + (unsigned long)wst.seg_total, + (unsigned)((wst.state == (uint8_t)WAVE_RING_ST_FREEZE) || + (wst.state == (uint8_t)WAVE_RING_ST_SEALED)) ? 1u : 0u, + sc, + (unsigned int)WAVE_RING_PTS, + (unsigned long)wst.err_cnt, + (unsigned long)wst.ovf_cnt, + (unsigned long)wst.drop_cnt); + } + } } report_at = now + ADC_TASK_REPORT_MS; } diff --git a/App/task/default_task.c b/App/task/default_task.c index 9e9498dd..7d04413e 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -26,6 +26,18 @@ * v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空 * 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底, * 模块 = App/fs/vol_idle_sync.[ch],道F 批 2) + * v1.4 2026-10-29 DTU 项目组 故障录波落盘道(task-2):vol_idle_sync_poll 之后 + * 新增 wave_store_drain()——一次心跳整段搬完(lead 裁③: + * 一次 try 锁内 pack_next 256 B 分片 + 完整 open...close), + * 持锁耗时 >50 ms 补一条 WARN、峰值 ms 记本层静态; + * 轮转号自持(启动扫 /wave 重建,成功 ACK 才推进,lead 裁(b)); + * 失败 = 先 unlink 半成品再 write_done(-1)(lead 裁①); + * 卷未就绪 / 环未封存静默跳过(§5.1 所有权 + §9 一条线约束) + * v1.5 2026-10-29 DTU 项目组 Lead 复核补第四道并发陷阱(本道与 adcTask 的抢占窗): + * 搬段中途 adcTask 可能正好走到 SEALED 滞留自弃(2 s)甚至 + * 已封出下一段——CRC 是对"读到的字节"算的,半新半旧的段照样 + * 自洽,只能靠写完关档后复检环侧身份(握手量 + 段首 t0/帧序) + * 判废:判废即 unlink、不 ACK、号不推进,失败按既有单条留痕 */ #include "default_task.h" @@ -53,6 +65,8 @@ #include "task_prof.h" /* 各任务最坏耗时上报 */ #include "vol_lock.h" #include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */ +#include "wave_ring.h" /* 录波环消费侧:sealed/sealed_meta/read_cb/write_done(task-2) */ +#include "wave_store.h" /* 段打包游标 + 段号轮转纯函数(落盘道本体,task-2) */ #include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */ /* Private defines -----------------------------------------------------------*/ @@ -72,6 +86,14 @@ * (自检已上报、卷保留、不重格式化),等下次上电重新 resume */ #define STORAGE_BRINGUP_RETRIES 3u +/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5250 B(约 21 次 256 B + * f_write),设计 §4.2 预估落盘窗「几十 ms」;越过本线说明本段在占用 defaultTask + * 喂狗链前的最慢一段,按事件补一条 WARN(不是周期行),并把历史峰值留在本层 + * 静态里供收工报告核喂狗余量(vol_lock.h 铁律「等锁即毁喂狗链」的前例在案) */ +#define WAVE_DRAIN_HELD_WARN_MS 50u +/* 搬运缓冲(§7 的 256 B;wave_store_pack_next 头阶段要求 cap >= 128,lead 转告(d)) */ +#define WAVE_DRAIN_CHUNK_B 256u + /* Private typedef -------------------------------------------------------------*/ typedef struct { const char *p_name; @@ -97,6 +119,7 @@ static void stack_probe_poll(void); #if !defined(TEST_SUITE_STORAGE) static void default_task_net_cfg_save_notify(void); static FRESULT storage_bring_up(FATFS *fs); +static void wave_store_drain(void); #endif /* @@ -301,6 +324,10 @@ void default_task_func(void *argument) { * 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close * 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */ vol_idle_sync_poll(); + /* 录波段落盘(task-2):卷空闲 + 环已封存才搬,就绪门/try 锁全在函数内; + * 一次心跳最多一段(lead 裁③)。放在 idle sync 之后:本拍若刚写过录波段, + * 下一拍空闲 flush 会兜住残留脏页,语义与 syslog 排空同位 */ + wave_store_drain(); #endif /* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */ task_wd_seg(TASK_WD_SEG_STACK); @@ -565,6 +592,248 @@ static FRESULT storage_bring_up(FATFS *fs) { } #endif /* !TEST_SUITE_STORAGE */ +/* + * 函数功能:故障录波落盘道——把已封存的 wave_ring 段整段搬进 /wave(task-2) + * 入口参数:无 void + * 返回值:无 void + * 限定条件:仅 defaultTask 主循环调用(vol_idle_sync_poll 之后);内部绝不阻塞—— + * try 锁 + 有界段长(5250 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms), + * 绝不把喂狗点做成等待态(vol_lock.h 使用约定:defaultTask 一律 try) + * 函数说明:所有权按 docs/子系统设计/故障录波设计说明.md §5.1——环的生产者是 + * adcTask,本道是唯一落盘消费者;跨任务握手只认 wave_ring_sealed()。 + * 本道承担五条口径: + * ① 打开一律 FA_WRITE|FA_OPEN_APPEND:本移植 FA_OPEN_APPEND 不含 FA_WRITE + * (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 陷阱一),少写位 + * f_write 恒 FR_DENIED; + * ② 每次 f_write 比对写回字节数:本移植簇分配失败返 FR_OK+短写无错误码 + * (同处陷阱二),短写一律判失败; + * ③ 轮转号由本道自持(lead 转告(b)):ring.seg_idx 只是本 boot 本地计数, + * 盘上权威下标 = 首次持锁扫 /wave 用 wave_store_idx_from_name() 重建 + * (取已存在最大号 +1,空目录 = 0,陌生名字不入账),写成功才 + * wave_store_next_idx() 推进;失败不推进、重试同号; + * ④ 成败都必须 wave_ring_write_done() ACK——ACK 前环一直冻结;err 账 + * 由 ring 的 err_cnt 单点收口(§9),本道不数第二份(ACK 前环已 + * 超时自弃的那次例外:skip 掉 ACK,不去补第二笔账); + * ⑤ 锁跨越一次完整 open...close(vol_lock.h 铁律),持锁耗时超 + * WAVE_DRAIN_HELD_WARN_MS 按事件补一条 WARN(§9 唯一周期行不在本道), + * 历史峰值 s_wheld_ms_max 供核 defaultTask 喂狗余量(lead 裁③)。 + * 失败处置(lead 裁①):开过文件才 unlink 半成品(未开成的失败不动盘, + * 绝不拿未初始化的路径误删旧段);unlink 失败照旧 ACK(-1)——最坏留 + * 一个坏段文件,PC 侧被 CRC 判废,不会画成证据。 + * 静默跳过(不刷屏):卷未就绪 / 环未封存 / try 锁没到(下轮再来, + * 环侧 2 s 超时自弃是兜底账,本道不为此打日志)。 + * (上面五条口径就是本函数功能「把已封存段整段搬进 /wave」的逐条展开, + * 细则出处 docs/子系统设计/故障录波设计说明.md §5.1 与 §10) + */ +static void wave_store_drain(void) { + static FIL s_wfil; /* static:FF_FS_TINY=0 时 FIL ~528 B, + * 绝不上栈(FS_VOL_PROBE 同一理由) */ + static uint8_t s_wbuf[WAVE_DRAIN_CHUNK_B]; + static wave_pack_ctx_t s_wpack; + static wave_seg_meta_t s_wmeta; + static char s_wpath[WAVE_ST_PATH_MAX]; + static DIR s_wdir; + static FILINFO s_wfno; + static uint8_t s_wnext_idx; /* 本道自持的下一个段号 0..99 */ + static uint8_t s_wscan_done; /* 1 = 本 boot 已扫过 /wave */ + static uint8_t s_wdir_ready; /* 1 = /wave 已建或已存在 */ + static uint8_t s_wcrc_state; /* 0 未验 1 过 2 败(败=永久自弃,不烧垃圾段) */ + static uint32_t s_wheld_ms_max; /* 持锁搬段历史峰值 ms(收工报告核余量用) */ + uint32_t t0_ms; + uint32_t held_ms; + uint32_t written = 0u; + uint32_t len = 0u; + uint32_t iter; + uint8_t opened = 0u; + uint8_t skip_ack = 0u; /* 1 = 环已自行了断本段,本道不 ACK 不补账 */ + wave_ring_stat_t wst2; /* 关档后复检环侧身份用(v1.5) */ + const char *why = NULL; + FRESULT fr; + UINT bw; + int rc; + + /* 就绪门(同 vol_idle_sync_poll 的四级门风格) */ + if (sys_evtHandle == NULL) { + return; + } + if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) { + return; + } + /* 封存门:只搬已封存段(唯一跨任务握手量,§5.1) */ + if (wave_ring_sealed() == 0u) { + return; + } + /* 空闲门:try 锁;卷被 syslog/配置/FTP 占住 → 本轮放弃,不排队不打日志 */ + if (vol_lock_wait(0u) == 0u) { + return; + } + t0_ms = (uint32_t)xTaskGetTickCount(); + + /* CRC 锚点自检每 boot 一次(落盘前失败 = 整族失败,永久自弃免得白烧写次数) */ + if (s_wcrc_state == 0u) { + s_wcrc_state = (wave_store_crc_selftest() == WAVE_ST_OK) ? 1u : 2u; + } + if (s_wcrc_state == 2u) { + why = "crc selftest FAIL"; + } + if ((why == NULL) && (s_wdir_ready == 0u)) { + fr = f_mkdir(WAVE_ST_DIR); + if ((fr == FR_OK) || (fr == FR_EXIST)) { + s_wdir_ready = 1u; + } else { + why = "mkdir /wave"; + } + } + if ((why == NULL) && (s_wscan_done == 0u)) { + FRESULT rr; + + fr = f_opendir(&s_wdir, WAVE_ST_DIR); + if (fr == FR_OK) { + uint8_t found = 0u; + uint8_t mx = 0u; + + for (;;) { + uint8_t ix; + + rr = f_readdir(&s_wdir, &s_wfno); + if ((rr != FR_OK) || (s_wfno.fname[0] == 0)) { + break; + } + if (wave_store_idx_from_name(s_wfno.fname, &ix) == WAVE_ST_OK) { + if ((found == 0u) || (ix > mx)) { + mx = ix; + } + found = 1u; + } + } + (void)f_closedir(&s_wdir); + if (rr == FR_OK) { + /* max+1 即下一写号;回绕号复用由 PC 侧 t0/frame_seq 对账兜底, + * 目录里没有的事实不去猜更多(陌生名字一律不入账) */ + s_wnext_idx = (found != 0u) ? wave_store_next_idx(mx) : 0u; + s_wscan_done = 1u; + } else { + why = "readdir /wave"; + } + } else { + why = "opendir /wave"; + } + } + if ((why == NULL) && (s_wscan_done != 0u)) { + if (wave_ring_sealed_meta(&s_wmeta) != WAVE_ST_OK) { + /* sealed 检查与此读之间环 2 s 超时自弃(err 已由环计过): + * 本道不 ACK、不补账、不打日志,放锁走人 */ + skip_ack = 1u; + } + } + if ((why == NULL) && (skip_ack == 0u)) { + /* lead 转告(e):E_FORMAT/E_SIZE = 坏段头,一律当落盘失败丢段计 err, + * 不当参数错吞掉 */ + rc = wave_store_pack_start(&s_wpack, &s_wmeta, wave_ring_read_cb, NULL); + if (rc != WAVE_ST_OK) { + why = "pack_start"; + } + } + if ((why == NULL) && (skip_ack == 0u)) { + if (wave_store_seg_path(s_wpath, (uint32_t)sizeof(s_wpath), + s_wnext_idx) <= 0) { + why = "seg_path"; + } else { + fr = f_open(&s_wfil, s_wpath, FA_WRITE | FA_OPEN_APPEND); + if (fr != FR_OK) { + why = "f_open"; + } else { + opened = 1u; + } + } + } + iter = 0u; + while ((why == NULL) && (opened != 0u)) { + rc = wave_store_pack_next(&s_wpack, s_wbuf, + (uint32_t)sizeof(s_wbuf), &len); + if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) { + why = "pack_next"; + break; + } + if (len != 0u) { + bw = 0u; + fr = f_write(&s_wfil, s_wbuf, (UINT)len, &bw); + if ((fr != FR_OK) || (bw != (UINT)len)) { + why = "f_write short"; /* ②:FR_OK+短写 = 本移植独有失败形态 */ + break; + } + written += len; + } + if (rc == WAVE_ST_DONE) { + break; + } + if (++iter > 32u) { /* 5250/256 = 21 片;游标卡死时的有界保险 */ + why = "pack stall"; + break; + } + } + if (opened != 0u) { + fr = f_close(&s_wfil); + if ((why == NULL) && (skip_ack == 0u) && (fr != FR_OK)) { + why = "f_close"; + } + } + if ((why == NULL) && (skip_ack == 0u) && (opened != 0u)) { + if (written != wave_store_pack_total(&s_wpack)) { + why = "len mismatch"; /* 长度等式(§6.3):半截文件骗不过这条 */ + } + } + /* v1.5(lead 复核):adcTask 优先级高于本道,搬段中途它可能正好把滞留 2 s 的本段 + * 自弃(甚至已经封出下一段)。数据区的 CRC 是对"本道读到的字节"算的,半新半旧的 + * 段一样自洽 → 关档后必须复检:握手量没了、或段首身份不是本段,一律判废。 + * 判废走既有失败路:unlink 本道开过的文件 + write_done(-1)(此时多半回 E_STATE, + * 环侧的 err 已由自弃计过,这里不重复补账)+ 段号不推进(同号重写覆盖)。 */ + if ((why == NULL) && (skip_ack == 0u)) { + if ((wave_ring_sealed() == 0u) || (wave_ring_get_stat(&wst2) != WAVE_ST_OK) + || (wst2.last_t0_ms != s_wmeta.t0_ms) || (wst2.last_seq != s_wmeta.frame_seq)) { + why = "ring abandoned"; + } + } + if (skip_ack == 0u) { + /* 失败留痕边沿锁存(搬进本块,lead 裁方案 A): + * ① 它是 static,初值只在编译期发生一次、不是每次进块重置; + * ② 它是"上一拍是否处于失败态"的边沿锁存,所以必须跨调用存活; + * ③ 单独放这里而不在函数入口那 12 连排台账里,因为 variableScope + * 检查要求把局部量收进真正覆盖全部使用点的最小块(本仓刻意开了 style)。 */ + static uint8_t s_wfail_latch; /* 一个 episode 只留一条痕 */ + + if (why == NULL) { + (void)wave_ring_write_done(0); + s_wnext_idx = wave_store_next_idx(s_wnext_idx); /* 成功才推进(转告(b)) */ + s_wfail_latch = 0u; + } else { + if (opened != 0u) { + (void)f_unlink(s_wpath); /* 只清本道开过的文件;unlink 失败照旧 ACK */ + } + (void)wave_ring_write_done(-1); + if (s_wfail_latch == 0u) { + s_wfail_latch = 1u; /* 持续失败不刷屏;成功一次重新武装 */ + DBG_ERROR("[WAVE] seg drop: %s (idx=%u written=%lu)", why, + (unsigned)s_wnext_idx, (unsigned long)written); + } + } + } + + /* 持锁耗时台账(lead 裁③i/ii):超线按事件补一条 WARN(原先这里静默占锁, + * 就是「等锁即毁喂狗链」前例的同族黑洞);峰值进静态供收工报告 */ + held_ms = (uint32_t)xTaskGetTickCount() - t0_ms; + if (held_ms > s_wheld_ms_max) { + s_wheld_ms_max = held_ms; + } + if ((skip_ack == 0u) && (held_ms > (uint32_t)WAVE_DRAIN_HELD_WARN_MS)) { + /* 本仓 dbg_log 无 WARN 级(宏只有 ERROR/INFO/DEBUG,ERR 级自动带 [ERR] = @test + * 禁用标记):[WARN] 按 console_log.c 先例进正文,慢不是错(真失败另有 seg drop) */ + DBG_INFO("[WAVE][WARN] seg held %lu ms (idx=%u)", + (unsigned long)held_ms, (unsigned)s_wnext_idx); + } + vol_lock_release(); /* 成败/跳过都放锁:锁泄漏比丢一段录波重一百倍 */ +} + /* * 函数功能:"保存网络配置"请求通知(注册给 net_cfg 的保存交接; * 2026-09-25 自 Src/freertos.c 迁出,逻辑零改动) diff --git a/App/task/protect_task.c b/App/task/protect_task.c index d34e85d7..591f8974 100644 --- a/App/task/protect_task.c +++ b/App/task/protect_task.c @@ -45,6 +45,12 @@ 行为不变:判定式、调用位置(采样恢复分支内、巡检/压板 之前)、边沿日志与 SOE 分型全部照旧;s_prev_cal_lock 仍只作边沿日志用,**不是**判据缓存 + * v1.4 2026-10-29 DTU 项目组 故障录波触发接线(task-2,lead 裁②): + * s_log_record_edge() 的 picked 上升沿分支补一次 + * wave_ring_request()——与 SYSLOG_START 同一事件点 + * (设计 §4.1「同源,避免两处各自判定」),判据本体 + * (prot_elm_run/判定逻辑)零改动;一拍只请求一次, + * elm = 本拍最低位上升元件 */ #include "protect_task.h" @@ -62,6 +68,7 @@ #include "prot_set.h" #include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */ #include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */ +#include "wave_ring.h" /* task-2:picked 上升沿 → 录波请求(与 SYSLOG_START 同源同点) */ #include "task_watchdog.h" #include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */ @@ -151,8 +158,24 @@ static void s_log_active_edge(uint32_t active) { static void s_log_record_edge(uint32_t active, uint32_t pick) { uint32_t p_chg = pick ^ s_prev_pick; uint32_t a_chg = active ^ s_prev_active; + const uint32_t p_rise = p_chg & pick; /* picked 0->1 的元件位集合 */ uint32_t i; + /* task-2 补(第 3 类边沿的用途):picked 上升沿同点请求录波。一拍只调一次—— + * 多元件同拍启动录同一段波就够(环本就不重入,后续请求由 ring 计 ovf,不当 + * 失败);元件号取最低位上升元件(位号最小 = 表中靠前,速断段优先于定时限段)。 + * 返回值刻意忽略:本函数在 protectTask 的最坏栈链上(B9 写进 DoD 的硬线), + * 不为此加日志;err/ovf 两本账全在串口 wave: 周期行与环 stat 里 + * (docs/子系统设计/故障录波设计说明.md §9)。 */ + if (p_rise != 0u) { + uint32_t e = 0u; + + while (((p_rise >> e) & 1u) == 0u) { + e++; /* p_rise != 0 → 必在 32 位内命中,无越界 */ + } + (void)wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, (uint8_t)e); + } + for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) { const uint32_t bit = 1u << i; diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 4ac1ca29..d0fa91fc 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -57,6 +57,16 @@ #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" +/* 故障录波回放道(UI 道):录波注入表执行侧需要的四份头。 + * wave_ring.h 给触发投递(request 任意上下文可调、非阻塞); + * wave_store.h 给段格式解析与定点 mV 换算(唯一口径,页面不复述算式); + * vol_lock.h + ff.h:/wave 只读遍历按 soe_store.c 读者同款纪律—— + * try 锁非阻塞(卷忙本轮按「无段」处理,uiTask 绘制节拍绝不等待)、 + * 锁跨完整文件操作序列、小窗口分次读、整段 5250B 绝不驻留 RAM */ +#include "ff.h" +#include "vol_lock.h" +#include "wave_ring.h" +#include "wave_store.h" #include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */ /* @@ -355,6 +365,463 @@ static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) { static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get }; +/* ═══════════════ 故障录波注入表执行侧(UI 道,2026-10-29)═══════════════ + * 纪律逐条对齐 soe_store.c 读者(vol_lock try / 锁跨完整文件序列 / 小窗口): + * - 卷忙(vol_lock_wait(0) 返 0)一律按「本轮无段」处理——uiTask 是 + * LVGL + 推流节拍的所有者,等锁 = 停摆,宁可空一帧也不阻塞(lead 裁定); + * - 段完整性判定只信 wave_store 自己的判据(尺寸等式 / magic+ver / 双 CRC / + * sem==index),本胶水不读 avail/flags 做二次裁断(§6.3 与 2026-10-29 + * 「VALID+CLIPPED 非矛盾」裁定同源:可用性不是完整性); + * - 整段 5250B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。 + * uiTask 阻塞上界(IWDG 800ms 窗、uiTask 在 required_mask 内;写法对齐上方 + * 维护通道那段上界,同文件先例): + * - 第零项是「本块不等待」:vol_lock_wait(0) 拿不到锁即按「本轮无段」 + * 返回——不等锁 = 零阻塞,它本身就是上界的一项,不是只保心跳判读; + * - 一次渲染实际发生的 I/O(进回放页首绘、或按键后重绘那帧)三件叠加: + * ① 表重扫 = 1 次 f_opendir + 至多 WAVE_ST_SEG_MAX(100)次 f_readdir, + * 收尾是 100 元插入排序(纯内存); + * ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 128B 读 + + * head_parse 内的头 CRC + 数据区 20 次 256B 流式读 + 尾 2B 一次 + * = 最坏 22 次小读;5120B 逐字节 CRC 与 sem 表校验是纯计算, + * 168MHz 下微秒到毫秒量级;缓存命中零卷操作; + * ③ 窗口装载 = 1 次 f_open + 1 次 f_lseek + 1 次 80B 读(40 点一窗), + * 解码 40 点 + 逐点 code_to_milli 峰值扫描 40 次(int64 乘除, + * 微秒级)——不是全段 8×320 扫描,回放页一屏只碰一个通道一窗; + * - 纯内存不碰卷的路径(评审据此看最坏的落点):manual_req 只投一个 + * 请求字节;已缓存段的上下切道、左右切段的段号表环游;空态与坏态 + * 直接返回。所以最坏只落在「第一次进回放页 / 第一次切到未缓存段」 + * 这类一次性动作上,同段内连续按键不重复付 ②。 + * 没有实测数,上面是与落盘道同一把尺的**推算**:落盘道的告警线是 + * 50 ms / 21 次写,读路径无簇分配、不写 FAT 元数据,只会更便宜,故按 + * 同量级不高于它估——一次渲染最坏一窗 ≈ 段表 + 一段全验 + 一窗,仍远 + * 小于 800ms。实测归台架,仪器就用本文件已经在跑的 task_prof 单轮 + * 工作耗时探针,不必新加埋点。 + * 段序口径(V0.1 已知限制,终报点名):目录重建后按段号数值排序环游, + * 盘上写满 100 段翻转后「数值序 = 时间序」不再严格成立——每屏顶行都带 + * 段号自证身份,查事故先看号再看序;翻转窗口排序升级留给台架结论。 + */ +static uint8_t s_wave_idx[WAVE_ST_SEG_MAX]; /* 盘上现存段号升序表 */ +static uint8_t s_wave_cnt; /* 表内条数 */ +static uint8_t s_wave_scanned; /* 表是否有效(manual_req 后作废) */ +static uint8_t s_wave_cached_idx; /* 段头缓存属于哪一段 */ +static uint8_t s_wave_cache_ok; /* 该段已验完整(头+数据 CRC) */ +static wave_seg_meta_t s_wave_meta; /* 已验段的头(172B,唯一驻留) */ +static uint8_t s_wave_buf[256u]; /* 读窗:数据区流式 256B/次(lead 裁定 + * 2026-10-29 第 1 条·第一条:128B 抬 256B, + * f_read 次数对半砍,RAM 只多 128B; + * 不选 100 位「已验」位图——位图的作废纪律 + * 一旦漏做,「CRC 不符必须拒收」这条台架 + * 判据对旧段号会静默失效,多 128B 没这个面)*/ + +/* + * 函数功能:重扫 /wave 目录,把现存段号收集成升序表 + * 入口参数:无 + * 返回值:0 - 扫描完成(条数见 s_wave_cnt);-1 - 卷忙本轮跳过 int + * 限定条件:uiTask;一次 f_opendir + 有界 f_readdir,全程锁内 + * 函数说明:目录不存在(从未录过波)与目录空都归「0 段」,不是错误; + * 名字不合形态的文件(E_NAME)静默跳过——那是别的道写进来的 + * 东西,回放页不替它背书也不报警 + */ +static int ui_task_wave_scan(void) { + static DIR dir; /* static:同 FIL 理由,压栈深 */ + FILINFO finfo; + uint8_t idx; + uint8_t i; + + if (vol_lock_wait(0u) == 0u) { + return -1; + } + s_wave_cnt = 0u; + if (f_opendir(&dir, WAVE_ST_DIR) == FR_OK) { + for (;;) { + if (f_readdir(&dir, &finfo) != FR_OK || finfo.fname[0] == '\0') { + break; + } + if (finfo.fattrib & AM_DIR) { + continue; + } + if (wave_store_idx_from_name(finfo.fname, &idx) == 0) { + if (s_wave_cnt < WAVE_ST_SEG_MAX) { + s_wave_idx[s_wave_cnt++] = idx; + } + } + } + f_closedir(&dir); + } + vol_lock_release(); + /* 插入排序:条数上限 100,不引 qsort 的 stdlib 依赖 */ + for (i = 1u; i < s_wave_cnt; i++) { + uint8_t j; + uint8_t tmp; + + tmp = s_wave_idx[i]; + j = i; + while ((j > 0u) && (s_wave_idx[j - 1u] > tmp)) { + s_wave_idx[j] = s_wave_idx[j - 1u]; + j--; + } + s_wave_idx[j] = tmp; + } + s_wave_scanned = 1u; + return 0; +} + +/* + * 函数功能:确保段表可用(必要时重扫) + * 入口参数:无 + * 返回值:0 - 表可用;-1 - 卷忙 int + * 限定条件:uiTask 绘制/按键链 + */ +static int ui_task_wave_table(void) { + if (s_wave_scanned == 0u) { + return ui_task_wave_scan(); + } + return 0; +} + +/* + * 函数功能:表内查段号下标 + * 入口参数:a_idx - 段号;a_pos - 出参下标 + * 返回值:1 - 命中;0 - 不在表内 uint8_t + * 限定条件:表已可用(先过 ui_task_wave_table) + */ +static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) { + uint8_t i; + + for (i = 0u; i < s_wave_cnt; i++) { + if (s_wave_idx[i] == a_idx) { + *a_pos = i; + return 1u; + } + } + return 0u; +} + +/* + * 函数功能:读段并做完整性判定(头 CRC 由 head_parse 判,数据 CRC 流式判), + * 成功则把段头留在 s_wave_meta 供 seg_win 免重读 + * 入口参数:a_idx - 段号 + * 返回值:UI_WAVE_OK / UI_WAVE_NONE(卷忙/文件没了)/ UI_WAVE_BAD(判废) int + * 限定条件:uiTask;锁跨 open→close 完整序列;头 128B 一次、数据区 256B + * 分窗流式(共约 1 + 20 次 f_read),整段不驻留 + * 耗时账(lead 裁定 2026-10-29 第 1 条·第三条并存):未缓存段的完整性 + * 判定最坏约 21 次小读 + 5120B 逐位 CRC,全部在卷锁内、uiTask 上; + * IWDG 800ms 窗内余量几何**实测毫秒归台架**(终报点名待测项)。 + * 缓存只留一段:左右切段 A→B→A 会把 B 的校验再来一遍——接受该 + * 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图 + * 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切 + * 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的 + * 写者轮转掉)按 NONE 处理,页面下轮自然重扫 + */ +static int ui_task_wave_load_head(uint8_t a_idx) { + static FIL f; /* static:FIL = 528B,理由同 soe_store_read */ + char path[WAVE_ST_PATH_MAX]; /* 容量 = WAVE_ST_PATH_MAX(17 字节口径, + * 自己声明路径缓冲不得写死 16) */ + FRESULT fr; + UINT br = 0u; + int rc; + + if ((s_wave_cache_ok != 0u) && (s_wave_cached_idx == a_idx)) { + return UI_WAVE_OK; + } + if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) { + return UI_WAVE_NONE; + } + if (vol_lock_wait(0u) == 0u) { + return UI_WAVE_NONE; /* 卷忙 = 本轮按无段处理(不阻塞 uiTask) */ + } + rc = UI_WAVE_NONE; + if (f_open(&f, path, FA_READ) == FR_OK) { + uint32_t size; + uint32_t total; + uint32_t n; + + size = (uint32_t)f_size(&f); + total = 0u; + rc = UI_WAVE_BAD; + /* 第一步:头区(首窗 128B = 当前形态整头);head_parse 自带判废 */ + n = (size < (uint32_t)WAVE_ST_HEAD_SZ_CUR) ? size + : (uint32_t)WAVE_ST_HEAD_SZ_CUR; + fr = f_read(&f, s_wave_buf, n, &br); + if ((fr == FR_OK) && (br == n)) { + if (wave_store_head_parse(s_wave_buf, n, &s_wave_meta) == 0) { + total = wave_store_seg_size(&s_wave_meta); + } + } + /* 第二步:尺寸等式(零长/长度与头部字段矛盾 = 整段判废,§6.3) */ + if ((total != 0u) && (size == total)) { + uint32_t off; + uint16_t crc; + uint8_t tail[2]; + + /* 第三步:数据区流式 CRC(128B 窗;CRC 覆盖不含尾两字节自身) */ + crc = WAVE_ST_CRC_INIT; + off = wave_store_head_bytes(s_wave_meta.ch_count); + rc = UI_WAVE_BAD; + while ((off + 2u) < size) { + n = (size - 2u - off < (uint32_t)sizeof(s_wave_buf)) + ? (size - 2u - off) : (uint32_t)sizeof(s_wave_buf); + fr = f_read(&f, s_wave_buf, n, &br); + if ((fr != FR_OK) || (br != n)) { + rc = UI_WAVE_BAD; + break; + } + crc = wave_store_crc_update(crc, s_wave_buf, n); + off += n; + } + /* 第四步:读回存储值比对(低字节在前) */ + if ((rc == UI_WAVE_BAD) && (f_lseek(&f, size - 2u) == FR_OK) && + (f_read(&f, tail, 2u, &br) == FR_OK) && (br == 2u)) { + if (crc == (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8))) { + rc = UI_WAVE_OK; + } + } + } else if (total == 0u) { + rc = UI_WAVE_BAD; /* seg_size 返 0 = 头字段自相矛盾,判废 */ + } else { + rc = UI_WAVE_BAD; /* 长度与头部字段不符,判废 */ + } + (void)f_close(&f); + } + vol_lock_release(); + if (rc == UI_WAVE_OK) { + s_wave_cached_idx = a_idx; + s_wave_cache_ok = 1u; + } else { + s_wave_cache_ok = 0u; + } + return rc; +} + +/* + * 函数功能:录波触发页唯一写出口——投一次手动触发请求 + * 入口参数:无 + * 返回值:0 - 已受理;非零 - 拒收(未 IDLE/冷却中/参数),页面显示 ERR int + * 限定条件:uiTask(request 本身任意上下文可调,这里只是转发不等待) + * 函数说明:受理 = ring 进冻结窗,成段最快也在约一秒后由默认任务落盘; + * 顺手作废段表缓存,页面下次取数会看到新段 + */ +static int ui_task_wave_manual_req(void) { + int r = wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u); + + if (r == 0) { + s_wave_scanned = 0u; + s_wave_cache_ok = 0u; /* lead 裁定 2026-10-29 第 3 条:段表与段头缓存 + * 一起作废——同名文件会被轮转重写,旧头字段 + * (pts/pre/k/b/逐路 avail)不作废就会顶着新段 + * 的号用旧尺子。单槽缓存本身 = 已知限制: + * 只挡「本段验过不再重验」,多段来回切会重验 */ + } + return r; +} + +/* + * 函数功能:取盘上「数值最大段号」(回放页开屏落点) + * 入口参数:a_idx - 出参段号 + * 返回值:UI_WAVE_OK / NONE(无段或卷忙本轮)/ BAD 本件不产(表只含现存文件) + * 函数说明:口径 = 数值最大,**不是时间最新**(lead 裁定 2026-10-29): + * 回绕后数值序不等于时间序,页顶显示的段号即自证;真做时间序要 + * 逐段开文件读 frame_seq/t0,一百次开档不在回放页的账上,本波不做 + */ +static int ui_task_wave_seg_first(uint8_t *a_idx) { + if (ui_task_wave_table() != 0) { + return UI_WAVE_NONE; /* 卷忙 = 本轮无段(同一条不阻塞纪律) */ + } + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_idx = s_wave_idx[s_wave_cnt - 1u]; + return UI_WAVE_OK; +} + +/* + * 函数功能:环游取邻段(a_dir:-1 更早 / +1 更新;段号数值序环回) + * 入口参数:a_cur - 当前段号;a_out - 出参邻段段号 + * 返回值:UI_WAVE_OK / NONE(表不可用或已无邻段可去) int + * 限定条件:uiTask;当前段已被轮转覆盖时先重扫一次再定位 + */ +static int ui_task_wave_seg_around(uint8_t a_cur, int8_t a_dir, uint8_t *a_out) { + uint8_t pos = 0u; + + if (ui_task_wave_table() != 0) { + return UI_WAVE_NONE; + } + if (ui_task_wave_find(a_cur, &pos) == 0u) { + if ((ui_task_wave_scan() != 0) || (s_wave_cnt == 0u) || + (ui_task_wave_find(a_cur, &pos) == 0u)) { + /* 当前段没了(翻转覆盖):退回首段,画面由调用侧重载 */ + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_out = s_wave_idx[0u]; + return UI_WAVE_OK; + } + } + if (s_wave_cnt == 1u) { + *a_out = s_wave_idx[0u]; /* 单段:原地(用户看到的仍是这段) */ + return UI_WAVE_OK; + } + if (a_dir > 0) { + *a_out = s_wave_idx[(uint8_t)((pos + 1u) % s_wave_cnt)]; + } else { + *a_out = s_wave_idx[(uint8_t)((pos + s_wave_cnt - 1u) % s_wave_cnt)]; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:切到段号表里的前一段(LR 左键的落点,数值升序表 → 「前」= 号更小) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写) + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int + * 限定条件:仅 uiTask 上下文(段号表是本任务私有缓存) + * 函数说明:只是 ui_task_wave_seg_around 的方向包装,存在的理由是 ops 表要两个 + * 独立函数指针,不写第二份查找逻辑。 + */ +static int ui_task_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { + return ui_task_wave_seg_around(a_cur, -1, a_out); +} + +/* + * 函数功能:切到段号表里的后一段(LR 右键;数值升序表 → 「后」= 号更大) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写) + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int + * 限定条件:仅 uiTask 上下文(同上) + * 函数说明:与 seg_prev 共用 ui_task_wave_seg_around,切段即触发一次懒加载; + * 未缓存段的读盘 + 流式 CRC 也发生在这条调用链上(单轮上界见本 + * 文件「故障录波注入表执行侧」块首注释的 uiTask 阻塞上界段)。 + */ +static int ui_task_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { + return ui_task_wave_seg_around(a_cur, 1, a_out); +} + +/* + * 函数功能:读段头(含双 CRC 完整性判定——判定发生在 ui_task_wave_load_head) + * 入口参数:a_idx - 段号;a_eng/a_trig/a_clip - 段头三字段原值 + * 返回值:UI_WAVE_OK / NONE / BAD int + * 限定条件:uiTask;命中缓存零卷操作 + */ +static int ui_task_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, + uint8_t *a_trig, uint8_t *a_clip) { + int rc = ui_task_wave_load_head(a_idx); + + if (rc != UI_WAVE_OK) { + return rc; + } + *a_eng = s_wave_meta.eng; + *a_trig = s_wave_meta.trig_src; + *a_clip = (uint8_t)(s_wave_meta.clip_mask & 0xFFu); + return UI_WAVE_OK; +} + +/* + * 函数功能:读固定显示窗(触发点前后各一周波)的码值与峰值 + * 入口参数:a_idx - 段号;a_ch - 通道号;a_codes - UI_WAVE_WIN_PTS 个码值 + * (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的 mV + * 返回值:UI_WAVE_OK / NONE(段头未验过/卷忙)/ BAD(数据区读不齐) int + * 限定条件:uiTask;先段头后窗口(未过完整性判定的段不供数据,页面显示 + * 「该段不可读」而不是半截波形——判废优先于展示) + * 函数说明:窗口起点用段头 pre_pts 现算(§8.2「数据不是编译常量」: + * 起点 = pre_pts - 窗长一半,与光标居中是同一个 20 的一半, + * 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的 mV + * 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径, + * 这里不重抄公式(§6.3;PC 道同一条款) + */ +static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch, + int16_t *a_codes, int32_t *a_peak_milli) { + static FIL f; + char path[WAVE_ST_PATH_MAX]; + UINT br = 0u; + uint32_t off; + uint32_t want; + uint32_t start; + uint32_t got_pts = 0u; + int16_t raw[UI_WAVE_WIN_PTS]; + int32_t milli; + int rc; + + if (ui_task_wave_load_head(a_idx) != UI_WAVE_OK) { + return UI_WAVE_BAD; /* 段头坏了/读不到:窗口无从谈起 */ + } + if (a_ch >= s_wave_meta.ch_count) { + return UI_WAVE_BAD; + } + if ((s_wave_meta.pts_per_ch < UI_WAVE_WIN_PTS) || + (s_wave_meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) { + return UI_WAVE_BAD; + } + start = (uint32_t)s_wave_meta.pre_pts - (UI_WAVE_WIN_PTS / 2u); + off = wave_store_data_off(s_wave_meta.ch_count, s_wave_meta.pts_per_ch, + a_ch, start); + if (off == 0xFFFFFFFFu) { + return UI_WAVE_BAD; + } + want = wave_store_data_bytes_avail(s_wave_meta.pts_per_ch, (uint16_t)start, + UI_WAVE_WIN_PTS); + if (want != UI_WAVE_WIN_PTS * 2u) { + return UI_WAVE_BAD; /* 窗口贴不出完整 40 点:显式不可读 */ + } + if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) { + return UI_WAVE_NONE; + } + if (vol_lock_wait(0u) == 0u) { + return UI_WAVE_NONE; + } + rc = UI_WAVE_BAD; + if (f_open(&f, path, FA_READ) == FR_OK) { + if ((f_lseek(&f, off) == FR_OK) && + (f_read(&f, s_wave_buf, want, &br) == FR_OK) && (br == want)) { + if ((wave_store_le_decode(s_wave_buf, want, raw, UI_WAVE_WIN_PTS, + &got_pts) == 0) && + (got_pts == UI_WAVE_WIN_PTS)) { + rc = UI_WAVE_OK; + } + } + (void)f_close(&f); + } + vol_lock_release(); + if (rc == UI_WAVE_OK) { + uint8_t i; + int32_t peak = 0; + + for (i = 0u; i < UI_WAVE_WIN_PTS; i++) { + int32_t v; + + v = (int32_t)raw[i] + s_wave_meta.ch[a_ch].b_codes; + if (v > 32767L) { + v = 32767L; + } else if (v < (-32768L)) { + v = -32768L; + } + a_codes[i] = (int16_t)v; + /* 显示与度量同一把尺(「谁被显示谁被度量」,§6.3): + * a_codes 画的是 raw+b,而 wave_store_code_to_milli 的公式是 + * (a_code + b_codes) × k_q16 / 65536000 取整(App/algo/ + * wave_store.c:567 实测口径)——传 raw 进去,b 已由唯一口径 + * 自己加过,PK 数与屏上波形天然同尺,这里不再加第二遍 + * (加两遍才是错);clamp 只护 int16 画布,b 的 V0.1 恒 0 */ + if (wave_store_code_to_milli(&s_wave_meta.ch[a_ch], raw[i], + &milli) == 0) { + if ((milli < 0 ? -milli : milli) > + (peak < 0 ? -peak : peak)) { + peak = milli; + } + } + } + *a_peak_milli = peak; + } + return rc; +} + +/* 录波通道函数表(六件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */ +static const ui_data_wave_ops_t s_wave_ops = { + ui_task_wave_manual_req, + ui_task_wave_seg_first, + ui_task_wave_seg_prev, + ui_task_wave_seg_next, + ui_task_wave_seg_head, + ui_task_wave_seg_win, +}; + /* * 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink) * 入口参数:无 @@ -431,6 +898,12 @@ void ui_task_func(void *argument) { if (ui_data_maint_bind(&s_maint_ops) != 0) { DBG_ERROR("UI maint bind: FAIL"); } + /* 录波通道注入(故障录波道 UI 侧,2026-10-29):手动录波触发页与回放页 + * 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里六件按「无录波段」 + * 空态渲染(不画线、不崩);残表 = 装配错,启动日志必须可见 */ + if (ui_data_wave_bind(&s_wave_ops) != 0) { + DBG_ERROR("UI wave bind: FAIL"); + } /* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据" * 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */ ui_data_init(); diff --git a/Drivers/BSP/UI/ui_data.c b/Drivers/BSP/UI/ui_data.c index 0763602e..e3572c22 100644 --- a/Drivers/BSP/UI/ui_data.c +++ b/Drivers/BSP/UI/ui_data.c @@ -1087,3 +1087,87 @@ uint8_t ui_data_comm_pending(void) { } return s_comm->pending(); } + +/* ── 通道六:故障录波(wave)注入与包装 ───────────────────────────────────── + * 包装层零判据:未注入统一按「无录波段」态返(页面显式空态文案,绝不 + * 画平线——设计 §8.1 空态行);六件的参数校验只到指针级,语义判定 + * (卷忙/段损坏)全在执行侧,与通道四/五同一条分层纪律。 + */ +static const ui_data_wave_ops_t *s_wave = NULL; + +/* + * 函数功能:注入录波执行侧函数表(启动期一次) + * 入口参数:ops - 六件齐;NULL = 撤销注入 + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 comm bind) int + * 限定条件:uiTask 启动期,先于 ui_menu_init() + */ +int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops) { + if (a_ops == NULL) { + s_wave = NULL; + return 0; + } + if ((a_ops->manual_req == NULL) || (a_ops->seg_first == NULL) || + (a_ops->seg_prev == NULL) || (a_ops->seg_next == NULL) || + (a_ops->seg_head == NULL) || (a_ops->seg_win == NULL)) { + return -1; + } + s_wave = a_ops; + return 0; +} + +/* 函数功能:投手动录波请求(未注入 = 拒收,页面显示 ERR) bool */ +int ui_data_wave_manual_req(void) { + if (s_wave == NULL) { + return -1; + } + return s_wave->manual_req(); +} + +/* 函数功能:录波通道三态查询的统一未注入兜底(返 NONE 显空态,不崩) int */ +static int wp_wave_none(void) { + return UI_WAVE_NONE; +} + +/* 函数功能:取数值最大段号(回绕后不保证时间最新——段号自证,见 .h 契约) int */ +int ui_data_wave_seg_first(uint8_t *a_idx) { + if ((s_wave == NULL) || (a_idx == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_first(a_idx); +} + +/* 函数功能:取更早一段段号 int */ +int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { + if ((s_wave == NULL) || (a_out == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_prev(a_cur, a_out); +} + +/* 函数功能:取更新一段段号 int */ +int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { + if ((s_wave == NULL) || (a_out == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_next(a_cur, a_out); +} + +/* 函数功能:读段头(执行侧含 CRC 完整性判定) int */ +int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip) { + if ((s_wave == NULL) || (a_eng == NULL) || (a_trig == NULL) || + (a_clip == NULL)) { + return wp_wave_none(); + } + return s_wave->seg_head(a_idx, a_eng, a_trig, a_clip); +} + +/* 函数功能:读显示窗口码值与峰值 mV int */ +int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli) { + if ((s_wave == NULL) || (a_codes == NULL) || (a_peak_milli == NULL) || + (a_ch >= UI_WAVE_CH_CNT)) { + return wp_wave_none(); + } + return s_wave->seg_win(a_idx, a_ch, a_codes, a_peak_milli); +} diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h index ed4a12ce..cb02c5f1 100644 --- a/Drivers/BSP/UI/ui_data.h +++ b/Drivers/BSP/UI/ui_data.h @@ -924,6 +924,111 @@ int ui_data_comm_set(uint8_t a_idx, uint32_t a_val); */ uint8_t ui_data_comm_pending(void); +/* ── 通道六:故障录波(wave)──────────────────────────────────────────────── + * 页面零 App include、零存储触点:录波执行侧(触发投递 / 段索引 / 段头字段 / + * 窗口码值与峰值 mV)全部经本注入表进来,真实现见 App/task/ui_task.c 的 + * s_wave_ops;PC 模拟器绑同一形状的假件(simulator/main.c)。取数口径的 + * 唯一真源是 docs/子系统设计/故障录波设计说明.md §6.3(定点 mV 换算走 + * wave_store_code_to_milli,eng 只做徽标、绝不进算式)。 + */ + +/* 录波通道数(与段头 ch_count 恒等:电气八路,温度不入波) */ +#define UI_WAVE_CH_CNT 8u +/* 回放显示窗口点数(两周波;口径推导与设计 §4.2 的触发点位置钉在一起, + * 窗起点与虚线光标由执行侧与页面各按同一定义实现,页面侧写死 */ +#define UI_WAVE_WIN_PTS 40u + +/* 通道查询返回态:段存在且可用 */ +#define UI_WAVE_OK 0 +/* 无段(目录扫空 / 卷忙跳过 / 执行侧未注入,页面显示「无录波段」) */ +#define UI_WAVE_NONE 1 +/* 有段但不可读(长度不符 / 版本不符 / 任一段 CRC 不符,页面显示 + * 「该段不可读」——两种态都必须显式说,画直线等于伪造证据(设计 §8.1) */ +#define UI_WAVE_BAD 2 + +/* + * 录波执行侧注入表(六件齐全;ui_data_wave_bind 拒绝残表) + * 返回值约定:除 manual_req 外,各件返回 UI_WAVE_OK / NONE / BAD 三态; + * seg_win 的出参仅在返 UI_WAVE_OK 时可读(通道五同一条出参契约)。 + */ +typedef struct { + /* 投一次手动录波请求。返回值:0 - 已受理;非零 - 拒收(含未落段通道 + * 不可用),页面据此走确认链的 ERR 支 */ + int (*manual_req)(void); + /* 取盘上数值最大的段号(回放页开屏落点;左右键从此起环游)。 + * 回绕后本序不保证时间最新,页顶段号即自证;时间序要靠段头 + * frame_seq/t0,代价是 100 次开文件,本波不做(lead 裁定 2026-10-29) */ + int (*seg_first)(uint8_t *a_idx); + /* 取比 a_cur 更早一段的段号(到最早端后环回数值最大端) */ + int (*seg_prev)(uint8_t a_cur, uint8_t *a_out); + /* 取比 a_cur 更新一段的段号(到最大端后环回数值最小端) */ + int (*seg_next)(uint8_t a_cur, uint8_t *a_out); + /* 读段头并做完整性判定(头 CRC 与数据 CRC 都在执行侧验完)。 + * 出参:eng - 单位口径(UI_DATA_ENG_* 同值域);trig - 触发源 + * (段头 trig_src 原值:手动/保护/定时);clip - 形状受限通道位图 + * (段头 clip 字节,位号 = 通道号) */ + int (*seg_head)(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip); + /* 读当前显示窗口的码值(已并入 b_codes 偏移)与窗口峰值。 + * 出参:a_codes - UI_WAVE_WIN_PTS 个码值;a_peak_milli - 窗口内 + * 绝对值最大的那一点折算的 mV(定点,执行侧经 code_to_milli 算好, + * 页面不重复换算式——§6.3 的单一口径条款;b_codes 由 code_to_milli + * 内部加,与 a_codes 里已并的偏移同一把尺:谁被显示谁被度量) */ + int (*seg_win)(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli); +} ui_data_wave_ops_t; + +/* + * 函数功能:注入录波执行侧函数表(启动期一次;形状对齐 ui_data_comm_bind) + * 入口参数:ops - 函数表(六件都不得为 NULL);NULL = 撤销注入(宿主测试用) + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动) int + * 限定条件:uiTask 启动期、先于 ui_menu_init() + */ +int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops); + +/* + * 函数功能:投一次手动录波请求(录波触发页唯一写出口) + * 返回值:0 - 已受理;-1 - 未注入或拒收(未注入与拒收同码,同通道五) int + */ +int ui_data_wave_manual_req(void); + +/* + * 函数功能:取数值最大段号(排序口径见 ui_data.c 同名胶水注释:回绕后本序 + * 不保证时间最新,页顶段号即自证;真时间序要逐段读 frame_seq/t0, + * 代价 100 次开文件,本波不做) + * 入口参数:a_idx - 出参,段号(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD(未注入按 NONE 渲染空态,不崩) int + */ +int ui_data_wave_seg_first(uint8_t *a_idx); + +/* + * 函数功能:取 a_cur 的更早一段段号(环形回绕) + * 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int + */ +int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out); + +/* + * 函数功能:取 a_cur 的更新一段段号(环形回绕,方向与 prev 相反) + * 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读) + * 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int + */ +int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out); + +/* + * 函数功能:读段头(含执行侧 CRC 完整性判定)——出参仅返 OK 时可读 + * 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int + */ +int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip); + +/* + * 函数功能:读显示窗口码值与峰值 mV——出参仅返 OK 时可读 + * 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int + */ +int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli); + #ifdef __cplusplus } #endif diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index b758ee60..ac0a6a4e 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -329,6 +329,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x2, 0x0, 0x40, 0x8, 0x1, 0x0, 0x20, 0x7f, 0xf0, + /* U+4E0D "不" */ + 0xff, 0xe0, 0x40, 0x8, 0x2, 0x0, 0xd0, 0x29, + 0x9, 0x16, 0x21, 0x4, 0x0, 0x80, 0x10, 0x0, + /* U+4E3B "主" */ 0x8, 0x0, 0x80, 0x0, 0x3f, 0xe0, 0x40, 0x8, 0x1, 0x3, 0xfe, 0x4, 0x0, 0x80, 0x10, 0x7f, @@ -423,6 +427,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x5f, 0x4a, 0xa9, 0x55, 0x2a, 0x85, 0x70, 0x88, 0x70, + /* U+524A "削" */ + 0x10, 0x32, 0x45, 0x52, 0x88, 0x5f, 0xeb, 0x5, + 0x7f, 0xac, 0x15, 0xfe, 0xb0, 0x46, 0x8, 0xc3, + 0x30, + /* U+524D "前" */ 0x20, 0x82, 0x23, 0xff, 0x80, 0x7, 0xc4, 0x8a, 0x9f, 0x52, 0x2a, 0x7d, 0x48, 0xa9, 0x11, 0x26, @@ -535,6 +544,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x51, 0xc, 0x3, 0x4, 0xf, 0xf8, 0x10, 0x7f, 0xf0, + /* U+57DF "域" */ + 0x40, 0xa8, 0x11, 0x3f, 0xa0, 0x4e, 0xe8, 0x95, + 0x52, 0xaa, 0x76, 0x40, 0xcc, 0x57, 0x35, 0x81, + 0x10, + /* U+590D "复" */ 0x3f, 0xe8, 0x2, 0xff, 0x10, 0x23, 0xfc, 0x40, 0x8f, 0xf0, 0x80, 0x3f, 0xca, 0x10, 0x3c, 0x38, @@ -735,6 +749,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xa4, 0x95, 0x94, 0xd1, 0x12, 0x50, 0x51, 0xc, 0x10, + /* U+653E "放" */ + 0x41, 0x4, 0x20, 0x4, 0x7c, 0xf4, 0x24, 0x8a, + 0x9e, 0x52, 0x4a, 0x49, 0x49, 0x11, 0x25, 0x4d, + 0x10, + /* U+6545 "故" */ 0x22, 0x4, 0x40, 0x88, 0x7d, 0xf2, 0x44, 0x44, 0xbe, 0x94, 0x52, 0x8a, 0x91, 0x23, 0xea, 0x46, @@ -1134,6 +1153,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x53, 0xea, 0x45, 0x48, 0xad, 0xf5, 0x20, 0x80, 0x60, + /* U+8BE5 "该" */ + 0x42, 0x4, 0x20, 0x3f, 0x81, 0xe, 0x44, 0x5f, + 0x8, 0x49, 0x12, 0x2c, 0x86, 0x30, 0x89, 0x6, + 0x10, + /* U+8BEF "误" */ 0x8f, 0x89, 0x10, 0x22, 0x7, 0xcc, 0x0, 0xbf, 0x90, 0x83, 0xff, 0x42, 0xc, 0xa1, 0x22, 0x8, @@ -1144,6 +1168,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xd2, 0x12, 0x7e, 0x48, 0x49, 0xf9, 0xa1, 0x24, 0x60, + /* U+8BFB "读" */ + 0x82, 0x9, 0xf8, 0x8, 0xf, 0xfc, 0x2, 0x8a, + 0x14, 0xc2, 0x48, 0x5f, 0xec, 0x51, 0x11, 0xc, + 0x10, + /* U+8D1F "负" */ 0x20, 0x7, 0xe1, 0x4, 0x41, 0x7, 0xfc, 0x80, 0x91, 0x12, 0x22, 0x44, 0x41, 0x60, 0xc3, 0x60, @@ -1462,218 +1491,224 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 511, .adv_w = 192, .box_w = 11, .box_h = 1, .ofs_x = 0, .ofs_y = 3}, {.bitmap_index = 513, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 529, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 546, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 563, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 580, .adv_w = 192, .box_w = 11, .box_h = 9, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 593, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 610, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 627, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 644, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 661, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 678, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 695, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 712, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 729, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 746, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 763, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 780, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 797, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 814, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 831, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 848, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 865, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 882, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 899, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 916, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 930, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 946, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 962, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 546, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 562, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 579, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 596, .adv_w = 192, .box_w = 11, .box_h = 9, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 609, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 677, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 694, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 711, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 728, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 745, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 762, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 779, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 796, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 813, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 830, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 847, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 864, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 881, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 898, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 915, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 932, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 949, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 963, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 979, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1012, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1028, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, - {.bitmap_index = 1040, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1056, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1073, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1090, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1107, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1124, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1141, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1158, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1172, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1189, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1206, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1223, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1240, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1257, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1274, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1308, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1028, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1045, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1061, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, + {.bitmap_index = 1073, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1157, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1174, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1191, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1290, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1409, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1423, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1440, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1457, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1474, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1491, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1508, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1525, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1358, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1459, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1473, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1490, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1507, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1524, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1541, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1558, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1575, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1589, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1606, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1623, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1637, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1654, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1671, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1688, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1705, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1722, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1739, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1756, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1773, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1807, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1858, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1875, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1892, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1909, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1926, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1943, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1960, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1977, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1994, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2011, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2027, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2040, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2057, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2073, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2090, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2104, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2121, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2138, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2155, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2172, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2189, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2206, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2223, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2240, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2257, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2274, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2308, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2325, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2357, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2476, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2493, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2510, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2527, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2544, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2561, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2578, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2595, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2612, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2677, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2692, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2709, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2726, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2743, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1575, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1591, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1608, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1625, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1639, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1656, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1673, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1687, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1704, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1721, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1738, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1755, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1772, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1789, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1806, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1823, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1840, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1857, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1874, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1891, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1908, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1925, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1942, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1959, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1976, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1993, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2010, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2027, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2044, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2061, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2078, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2094, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 2107, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2124, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2157, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2171, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2188, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2205, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2222, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2239, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2256, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2273, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2290, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2307, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2324, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2358, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2375, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2424, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2441, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2458, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2475, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2492, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2509, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2526, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2543, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2560, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2577, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2594, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2611, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2628, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2645, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2662, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2679, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2693, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2710, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2727, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2744, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 2759, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2776, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2792, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2809, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2776, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2793, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2810, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2826, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2860, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2877, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2894, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2911, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2928, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2945, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2962, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2979, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2996, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3013, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3030, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3047, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3064, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3081, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3095, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3112, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3129, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3146, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3163, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3180, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3197, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3214, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3231, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3248, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3265, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3282, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3299, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3316, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3333, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3350, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3367, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3384, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3401, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3418, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3435, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3452, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3469, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3486, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3503, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3520, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3537, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3554, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3588, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3605, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3622, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3639, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2843, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2859, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2876, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2893, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2910, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2927, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2944, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2961, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2978, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2995, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3012, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3029, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3046, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3063, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3080, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3097, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3114, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3131, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3148, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3162, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3179, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3196, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3213, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3230, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3247, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3264, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3281, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3298, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3315, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3332, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3349, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3366, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3383, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3400, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3417, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3434, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3451, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3468, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3485, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3502, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3519, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3536, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3553, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3570, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3740, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3774, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3791, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3806, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3821, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3836, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3853, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3870, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3887, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3904, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3921, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3740, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3756, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3773, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3807, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3858, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3875, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3892, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3907, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3922, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 3937, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3954, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3971, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3988, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 4005, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4022, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 4039, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4040, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 4042, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 4022, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4038, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4055, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4072, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 4140, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4141, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 4143, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ }; /*--------------------- @@ -1681,34 +1716,34 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_1[] = { - 0x0, 0x1, 0x2, 0x2ca0, 0x2ca9, 0x2caa, 0x2cdb, 0x2d2b, - 0x2d2c, 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, 0x2e81, - 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x3039, 0x309a, 0x30a6, - 0x30d6, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, 0x323f, 0x3262, - 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, 0x3297, 0x32a8, 0x32cc, - 0x32cf, 0x32ea, 0x3308, 0x357e, 0x3580, 0x3594, 0x35d0, 0x35e0, - 0x362b, 0x37ad, 0x37b6, 0x37d1, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, - 0x3a3e, 0x3a66, 0x3c92, 0x3cc7, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, - 0x3d96, 0x3da0, 0x3da2, 0x3daf, 0x3db5, 0x3dda, 0x3df2, 0x3df3, - 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, 0x40d7, 0x40eb, 0x4135, - 0x4136, 0x4144, 0x4145, 0x420e, 0x4229, 0x4245, 0x4247, 0x4249, - 0x436d, 0x43d6, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480, 0x4485, - 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, 0x45da, - 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0, - 0x49c1, 0x4a03, 0x4a1b, 0x4a55, 0x4ae2, 0x4b00, 0x4b82, 0x4be1, - 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, - 0x5177, 0x518c, 0x53c8, 0x53d5, 0x53ec, 0x5571, 0x5594, 0x55ab, - 0x5685, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, 0x58ab, 0x5937, - 0x596b, 0x5979, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, - 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, 0x5e0e, 0x5fba, 0x608a, 0x6122, - 0x614f, 0x61a3, 0x6217, 0x66ec, 0x6765, 0x6a06, 0x6a41, 0x6a44, - 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, 0x6bc5, - 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, - 0x6e74, 0x6ea0, 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, - 0x7067, 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, - 0x7488, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, - 0x7602, 0x7715, 0x7731, 0x7828, 0x7836, 0x792c, 0x7d78, 0xddac, - 0xddba + 0x0, 0x1, 0x2, 0x2ca0, 0x2ca9, 0x2caa, 0x2cad, 0x2cdb, + 0x2d2b, 0x2d2c, 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, + 0x2e81, 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x3039, 0x309a, + 0x30a6, 0x30d6, 0x30ea, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, + 0x323f, 0x3262, 0x326c, 0x3271, 0x3276, 0x3283, 0x328f, 0x3297, + 0x32a8, 0x32cc, 0x32cf, 0x32ea, 0x3308, 0x357e, 0x3580, 0x3594, + 0x35d0, 0x35e0, 0x362b, 0x367f, 0x37ad, 0x37b6, 0x37d1, 0x39f0, + 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, 0x3c92, 0x3cc7, 0x3cd8, + 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da0, 0x3da2, 0x3daf, 0x3db5, + 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, + 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4229, + 0x4245, 0x4247, 0x4249, 0x436d, 0x43d6, 0x43de, 0x43e5, 0x43e8, + 0x4410, 0x4414, 0x4480, 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, + 0x45bf, 0x45ca, 0x45cc, 0x45da, 0x461f, 0x4685, 0x46a7, 0x46c1, + 0x46d7, 0x4760, 0x48c1, 0x49c0, 0x49c1, 0x4a03, 0x4a1b, 0x4a55, + 0x4ae2, 0x4b00, 0x4b82, 0x4be1, 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, + 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x5177, 0x518c, 0x53c8, 0x53d5, + 0x53ec, 0x5571, 0x5594, 0x55ab, 0x5685, 0x56a1, 0x570c, 0x570e, + 0x57da, 0x5872, 0x58ab, 0x5937, 0x596b, 0x5979, 0x598f, 0x59cc, + 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d7f, 0x5d8d, 0x5d94, 0x5df1, + 0x5e0e, 0x5fba, 0x608a, 0x6122, 0x614f, 0x61a3, 0x6217, 0x66ec, + 0x6765, 0x6a06, 0x6a41, 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, + 0x6a85, 0x6a8f, 0x6a97, 0x6a9b, 0x6bbf, 0x6bc5, 0x6c2a, 0x6c93, + 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, 0x6e74, 0x6ea0, + 0x6ea1, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067, 0x706d, + 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x7488, 0x748d, + 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715, + 0x7731, 0x7828, 0x7836, 0x792c, 0x7d78, 0xddac, 0xddba }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1719,12 +1754,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 313, + .range_start = 9654, .range_length = 1, .glyph_id_start = 319, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { .range_start = 8544, .range_length = 56763, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 217, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 223, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Drivers/BSP/UI/ui_menu_tab.c b/Drivers/BSP/UI/ui_menu_tab.c index 8c5ae3fb..f84ef76f 100644 --- a/Drivers/BSP/UI/ui_menu_tab.c +++ b/Drivers/BSP/UI/ui_menu_tab.c @@ -54,6 +54,7 @@ const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = { { 32, 0u, "自检记录" }, /* 39 二级菜单 */ { 32, 0u, "运行记录" }, /* 40 二级菜单 */ { 32, 0u, "运行报告" }, /* 41 二级菜单 */ + { 32, 0u, "录波回放" }, /* 42 二级菜单 */ }; /* diff --git a/Drivers/BSP/UI/ui_menu_tab.h b/Drivers/BSP/UI/ui_menu_tab.h index c17e7700..24053433 100644 --- a/Drivers/BSP/UI/ui_menu_tab.h +++ b/Drivers/BSP/UI/ui_menu_tab.h @@ -25,7 +25,7 @@ typedef struct { const char *name; /* 显示文本(字面量,存 Flash) */ } ui_menu_node_t; -#define UI_MENU_TAB_COUNT 42 +#define UI_MENU_TAB_COUNT 43 /* 护栏:8 位索引上限 255 项(哨兵 0xFF 占用 255);超界则编译失败(C99 写法,兼容 ARMCC v5) */ typedef char menu_tab_count_check[(UI_MENU_TAB_COUNT <= 255) ? 1 : -1]; diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 34e54d3e..5ee11d0b 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -32,6 +32,8 @@ #include "ui_page_view_plate.h" #include "ui_page_view_set.h" #include "ui_page_view_word.h" +#include "ui_page_wave_manual.h" +#include "ui_page_wave_play.h" #include "ui_page_yk.h" #include "ui_page_yx.h" @@ -52,6 +54,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 19u, &ui_page_calib_desc }, /* node 19 遥测系数 */ { 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */ { 25u, &ui_page_reset_desc }, /* node 25 强制复归 */ + { 26u, &ui_page_wave_manual_desc }, /* node 26 手动录波 */ { 27u, &ui_page_clear_desc }, /* node 27 清除记录 */ { 29u, &ui_page_comm_desc }, /* node 29 通讯设置 */ { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ @@ -64,6 +67,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */ { 40u, &ui_page_runlog_desc }, /* node 40 运行记录 */ { 41u, &ui_page_runreport_desc }, /* node 41 运行报告 */ + { 42u, &ui_page_wave_play_desc }, /* node 42 录波回放 */ }; /* diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index 4e1b1dab..c7091e81 100644 --- a/Drivers/BSP/UI/ui_page_registry.h +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -25,7 +25,7 @@ typedef struct { const content_page_def_t *desc; /* 页面描述符(Flash const) */ } ui_page_entry_t; -#define UI_PAGE_MAP_COUNT 28u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 30u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/Drivers/BSP/UI/ui_page_wave_manual.c b/Drivers/BSP/UI/ui_page_wave_manual.c new file mode 100644 index 00000000..91741a56 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_manual.c @@ -0,0 +1,126 @@ +/* + * 模块名称:UI Page wave_manual (手动录波内容页) + * 模块功能:装置维护 → 手动录波 叶子(node 26,#P 受密码保护)的内容页—— + * 复用 ui_page_action 共享确认链(一次确认 → 二次确认 → 执行回报): + * 执行只做一件事——经录波注入表投一次「手动触发」请求,受理即回报 + * 「已请求录波」;采点、冻结、落盘全在 ring/store/落盘道,本页不 + * 等待也不查询结果(录波从触发到成段最快也要一秒后,页面上做同步 + * 等待等于把 uiTask 绘制定律「零 I/O」当场破掉); + * 只读进入(查看密码)整页拒执行并显式 LOCK——与强制复归同一条 + * 「触发是操作不是查看」的权限判据(docs/任务规划/UI补齐.md §2 + * 维护组的既有口径,本页不自造第二种); + * 本文件零 App include、零存储触点(分层方向由注入表守住)。 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;触发入口形态 = + * docs/子系统设计/故障录波设计说明.md §8.3; + * 冷却/忽略的屏上反馈:拒收即 ERR,ring 侧 + * 「冷却中丢弃并计数」不在页面上细分原因—— + * 用户要的原因区分在回放页看段号) + */ + +/* 头文件包含区 */ +#include "ui_page_wave_manual.h" + +#include "ui_content_page.h" +#include "ui_data.h" +#include "ui_page_action.h" + +/* 私有宏定义区 */ +/* (无——文案词全部是 Flash 字面量,见下面静态配置) */ + +/* 私有函数声明区(静态初始化器要引用下面函数的地址,故先声明) */ + +static bool wv_ok(void); + +/* 静态变量定义区 */ + +/* + * 本页操作页实例(配置全为 Flash 字面量与函数地址;运行态两字节由组件写)。 + * 字段顺序 = ui_page_action_t 声明序(ARMCC v5 = C99 要求指定顺序一致)。 + * 观测行留空(label 与 cb 成对给 NULL,组件允许):录波段数要扫卷才能知道, + * 而组件 watch_cb 的契约是「绘制路径零 I/O 直调」——不为一个观测数破坏 + * 绘制定律,段数的事交给回放页(它本来就要读段)。 + */ +static ui_page_action_t s_act = { + "确认手动录波?", /* prompt:问句用字全在既有子集(确认/手动录波 均来自 + 菜单与强制复归页现词,'?' 为 ASCII) */ + "已请求录波", /* done_text:回报「请求已受理」,不谎称「已存成段」—— + 受理到落盘之间还隔着冻结窗与卷写,页面无权代言 */ + wv_ok, /* ok_cb */ + 1u, /* need_modify:只读进入一律拒执行(触发是操作) */ + NULL, /* watch_label(观测行不给,理由见上方块注释) */ + NULL, /* watch_cb */ + 0u, /* stage(运行态初值) */ + 0u /* err (运行态初值) */ +}; + +/* 函数定义区 */ + +/* + * 函数功能:执行投递——向录波环形缓冲投一次手动触发请求 + * 入口参数:无 + * 返回值:true = 已受理(组件转成功回报);false = 拒收(组件显示 ERR) bool + * 限定条件:由 ui_page_action_key 在第二次 ENTER 时调用(uiTask 上下文) + * 函数说明:ring 的 request 本身是「任意上下文可调的一次性请求」,非阻塞、 + * 不等待采点;注入未绑(装配缺件)同样返非零 → 屏上显 ERR, + * 比「按了像成功其实什么都没发生」诚实(强制复归页同款纪律)。 + * 不补操作记录:录波不是五类审计记录之一(触发证据在段文件头里, + * 落盘道已带 trig_src/t0/frame_seq),页面上再投一条 = 两处两个真源。 + */ +static bool wv_ok(void) { + return (ui_data_wave_manual_req() == 0) ? true : false; +} + +/* + * 函数功能:开页复位确认链(框架 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内调用 + * 函数说明:复位由组件做(stage/err 是它的运行态),本页不重复清一遍 + */ +static void pg_init(void) { + ui_page_action_init(&s_act); +} + +/* + * 函数功能:自绘(框架 draw_cb;四行居中全由组件画) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链末尾调用 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_page_action_draw(&s_act, a_layer, a_clip); +} + +/* + * 函数功能:按键转发(框架 key_cb) + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true = 本页已消费;false = 交回框架 bool + * 限定条件:框架按键链内 + * 函数说明:确认链的判定全在组件里(含只读拒执行),本页只如实透传返回值 + */ +static bool pg_key(lv_key_t a_key) { + return (ui_page_action_key(&s_act, a_key) != 0u); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=wave_manual 绑定引用) + * 操作页形态(强制复归页同款):无浅 V、无页脚、无行列表; + * edit_capable=1 是「能不能改」的那半,另半(本次进入授予的权限)由 + * ui_content_page_is_editable() 给;key_intercept=0:LEFT 不拦截,关页 = 取消 + */ +const content_page_def_t ui_page_wave_manual_desc = { + .id = "wave_manual", + .title = "手动录波", + .hide_deco = 1u, + .hide_footer = 1u, + .selectable = 0u, + .key_intercept = 0u, + .edit_capable = 1u, + .init_cb = pg_init, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_wave_manual.h b/Drivers/BSP/UI/ui_page_wave_manual.h new file mode 100644 index 00000000..5f11b153 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_manual.h @@ -0,0 +1,30 @@ +/* + * 模块名称:UI Page wave_manual (手动录波内容页) + * 模块功能:装置维护 → 手动录波 叶子(node 26,#P 受密码保护)的内容页声明—— + * 向框架 ui_content_page 提供页面描述符;确认链全部走 ui_page_action + * 共享实现,本模块零 App include、零存储触点(触发经录波注入表) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;形态与分工 = + * docs/子系统设计/故障录波设计说明.md §8.3 + * 「手动录波操作页」一行) + */ +#ifndef UI_PAGE_WAVE_MANUAL_H +#define UI_PAGE_WAVE_MANUAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_wave_manual.c;生成物按菜单树 #page=wave_manual 引用本符号) */ +extern const content_page_def_t ui_page_wave_manual_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_WAVE_MANUAL_H */ diff --git a/Drivers/BSP/UI/ui_page_wave_play.c b/Drivers/BSP/UI/ui_page_wave_play.c new file mode 100644 index 00000000..2f7258b6 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_play.c @@ -0,0 +1,435 @@ +/* + * 模块名称:UI Page wave_play (录波回放内容页) + * 模块功能:记录查询 → 录波回放 叶子的内容页——单通道阶梯波形回放(设计 + * docs/子系统设计/故障录波设计说明.md §8.1 元素表逐条落地): + * 顶行 = 通道名 + 段号 + 触发源字母(M 手动 / P 保护 / T 定时); + * 波形区 = 两周波 40 点、每点占 3 px 的**阶梯线**(1 bpp 下平滑 + * 折线的过零平缓段会被阈值吃掉,ui_deco.h 实测口径;阶梯线每段 + * 非水平即垂直,天然绕开,也更贴近「离散采样」的事实); + * 参考线 = 0 线(虚线)+ 窗口峰值标注(定点 mV,ASCII); + * 底行 = 单位徽标(只读段头 eng,eng 绝不参与算式——§6.3 钉死, + * PC 道同一口径);形状受限通道右上打「削」徽标; + * 空态显式文案「无录波段」/「该段不可读」——画成一条直线等于 + * 伪造证据(§8.1 空态行,此判据优先于「页面上总得画点东西」)。 + * 键位:上下切通道、左右切段(key_intercept=1 才能把 LEFT 从 + * 「关页」手里要来);ESC 关页。 + * 取数纪律:本文件零 App include——段号/段头/窗口码值/峰值 mV 全部经 + * ui_data.h 录波注入表进来(波形算式与定点换算在执行侧,页面 + * 连 b_codes 都不碰,收到的码值已并偏移);首帧绘制才懒取数 + * (开页链零 I/O,绘制定律不破)。 + * 显示窗口与光标(与落盘道/PC 道的同一裁定,写在这里免得两头漂): + * 窗口 = 触发点前一周波 + 后一周波共 40 点,窗口起点 = 段内点号 + * 「预窗点数 减 二十」(即执行侧 seg_win 的固定窗);虚线竖光标 + * 落在窗内第 二十 与第 廿一 点之间(0 基第 19/20 点边界,即 + * 波形区正中 x)——依据 = 设计 §4.2 触发点位置 + App/algo/ + * wave_ring.h 的点序注释(触发 tick 采样 = 预窗最后一点), + * 页面不复述任何换算式。 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧) + */ + +/* 头文件包含区 */ +#include "ui_page_wave_play.h" + +#include + +#include "ui_content_page.h" +#include "ui_data.h" +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +/* 绘制区几何(屏宽 160:波形区左右各留 20 px 边;高 90 px) */ +#define WP_WX0 20 /* 波形区左边界 x */ +#define WP_PX_PER_PT 3 /* 每点占宽(§8.1 阶梯线口径) */ +#define WP_WTOP 34 /* 波形区顶 y(信息行下方) */ +#define WP_WBOT 124 /* 波形区底 y(0 线最多贴到这里) */ +#define WP_WSPANS 90 /* 纵向可用像素 = WP_WBOT - WP_WTOP */ +#define WP_CUR_X (WP_WX0 + (UI_WAVE_WIN_PTS / 2u) * WP_PX_PER_PT) + /* 触发虚线光标 x(窗口正中,理由见文件头) */ +#define WP_INFO_Y 20 /* 顶行(通道/段号/触发 + 削徽标)条带 y */ +#define WP_HINT_Y 128 /* 倒数第二行(峰值/键位提示)条带 y */ +#define WP_ENG_Y 143 /* 底行(单位徽标)条带 y(页脚已隐藏) */ +#define WP_EMPTY_Y 60 /* 空态文案条带 y(近屏中部) */ + +/* 页面数据态:未取数前不画任何波形元素 */ +#define WP_ST_IDLE 0u /* 未取数/无段(空态文案) */ +#define WP_ST_OK 1u /* 段完整,可画波形 */ +#define WP_ST_BAD 2u /* 段不可读(显式文案,绝不画线) */ + +/* 私有函数声明区 */ + +static void wp_load_seg(void); +static void wp_ensure(void); +static void wp_draw_text(lv_layer_t *a_layer, const char *a_text, + int32_t a_x, int32_t a_y, uint8_t a_align, + const lv_area_t *a_clip); +static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span); + +/* 私有变量定义区 */ + +/* + * 通道名(ASCII 段名 = 语义取数层的通道语义名,页面自带表避免为了 + * 六个字母再开一条注入边;与段头通道表的语义顺序一致:三相电压/零序/ + * 三相电流在前,形状受限路按段头 conv 位区分——名字本身两端一致) + */ +static const char *const s_ch_names[UI_WAVE_CH_CNT] = { + "UA", "UB", "UC", "UN", "IA", "IB", "IC", "IN" +}; + +/* 触发源字母(值域 = 段头 trig_src;未识别给 '?' 不给猜) */ +static const char s_trig_letters[] = { '?', 'M', 'P', 'T' }; +#define WP_TRIG_MAX ((uint8_t)(sizeof(s_trig_letters) - 1u)) + +static uint8_t s_state; /* WP_ST_* 三态 */ +static uint8_t s_idx; /* 当前段号(盘上编号,0..99) */ +static uint8_t s_ch; /* 当前通道(上下键切) */ +static uint8_t s_eng; /* 段头单位口径(只读徽标) */ +static uint8_t s_trig; /* 段头触发源 */ +static uint8_t s_clip; /* 段头形状受限位图(位号=通道) */ +static int16_t s_codes[UI_WAVE_WIN_PTS]; /* 当前窗码值(执行侧已并偏移) */ +static int32_t s_peak_milli; /* 窗口峰值 mV(执行侧算好) */ + +/* 函数定义区 */ + +/* + * 函数功能:取当前段的头与窗口(三态落到 s_state,坏态一律显式文案) + * 入口参数:无(读 s_idx / s_ch) + * 返回值:无 + * 限定条件:仅在 uiTask 绘制/按键链内调用(经注入表触到执行侧) + * 函数说明:头都读不成时连顶行都不画——通道名以外的字段全来自段头, + * 缺一半字段还硬凑顶行等于把「读不到」伪装成「读到了」 + */ +static void wp_load_seg(void) { + int rc = ui_data_wave_seg_head(s_idx, &s_eng, &s_trig, &s_clip); + + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + return; + } + rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli); + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + return; + } + s_state = WP_ST_OK; +} + +/* + * 函数功能:首帧懒取数(开页链零 I/O 的落地方式:真正的卷读取推迟到 + * 第一次绘制——模拟器截图与真机重绘走同一条链,行为一致) + * 入口参数:无 + * 返回值:无 + * 限定条件:draw_cb 开头调用 + * 函数说明:取不到段就停在空态;此后再按左右键仍会重试(手动录波从 + * 触发到成段有一秒级延迟,页开着等段出现是回放页的正常用法, + * 不做「试过就没有了」的缓存) + */ +static void wp_ensure(void) { + uint8_t idx = 0u; + int rc; + + if (s_state != WP_ST_IDLE) { + return; + } + rc = ui_data_wave_seg_first(&idx); + if (rc == UI_WAVE_OK) { + s_idx = idx; + wp_load_seg(); + } +} + +/* + * 函数功能:单行文本绘制(对齐可选;ui_page_action 私有拷贝同法,此处 + * 多一个左/右对齐是因为顶行要把「削」徽标钉到右上角) + * 入口参数:a_layer - 绘制层;a_text - Flash 字面量;a_x - 锚点 x(对齐 + * 含义:左=起点 中=屏宽轴 右=终点);a_y - 条带顶; + * a_align - 0 左 / 1 中 / 2 右;a_clip - 重绘区 + * 返回值:无 + * 限定条件:a_text 非空;条带与 clip 不相交即返回(culling) + * 函数说明:lv_text_get_width 必须传 strlen(传 0 不测量、传 -1 死循环, + * 框架同款警示);本 LVGL 版条带矩形给 lv_draw_label 做底色无关 + * 文本定位用,字模颜色固定 UI_COL_FG + */ +static void wp_draw_text(lv_layer_t *a_layer, const char *a_text, + int32_t a_x, int32_t a_y, uint8_t a_align, + const lv_area_t *a_clip) { + lv_area_t band; + lv_draw_label_dsc_t ld; + int32_t w = (int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), + LV_FONT_DEFAULT, 0); + + band.x1 = 0; + band.x2 = UI_SCREEN_W - 1; + band.y1 = a_y; + band.y2 = a_y + UI_TEXT_BAND_H - 1; + if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) || + (band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) { + return; + } + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = a_text; + ld.text_static = 1u; + ld.color = UI_COL_FG; + ld.ofs_x = (a_align == 1u) ? (a_x - (w / 2)) + : ((a_align == 2u) ? (a_x - w) : a_x); + ld.ofs_y = 0; + lv_draw_label(a_layer, &ld, &band); +} + +/* + * 函数功能:顶行触发字母(越界值给 '?' 不猜) + * 入口参数:a_trig - 段头 trig_src 原值 + * 返回值:字母 ASCII char + * 限定条件:仅绘制路径 + */ +static char wp_trig_letter(uint8_t a_trig) { + return (a_trig <= WP_TRIG_MAX) ? s_trig_letters[a_trig] : '?'; +} + +/* + * 函数功能:码值域到屏幕 y 的线性映射(0 基线必在域内:域取 + * 「min(0, 窗最小) .. max(0, 窗最大)」,削顶钉下轨的通道也能 + * 一眼看出贴的是 0 线还是别的什么) + * 入口参数:a_v - 码值;a_dmin - 域底;a_span - 域高(域宽,恒 >0) + * 返回值:波形区内 y 像素(WP_WBOT = 域底,WP_WTOP = 域顶) int32_t + * 限定条件:绘制路径纯算术,零除由调用侧钳 span 保证 + * 函数说明:整数乘除即可——域高不超过 65534,先乘后除无溢出风险 + */ +static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span) { + return WP_WBOT - (((a_v - a_dmin) * WP_WSPANS) / a_span); +} + +/* + * 函数功能:波形区自绘——阶梯线 + 0 线(虚线)+ 触发点虚线竖光标 + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:仅 s_state == WP_ST_OK 时被调用;s_codes 已备 + * 函数说明:阶梯 = 每点先画 3 px 水平段、再在点边界补一段垂直连线, + * 全程只有水平与垂直两种线段(1 bpp 抗阈值裁定的直接落地); + * 0 线与光标用 dash(ui_deco 先例只画过实线,dash 字段是 LVGL + * 线段 dsc 自带的,单色层同样按位落墨);整区一次 cull,区内 + * 不再逐线判交——波形区就 120x90,逐线判交省不了多少,判错 + * 反而留下半截线 + */ +static void wp_draw_wave(lv_layer_t *a_layer, const lv_area_t *a_clip) { + lv_area_t band; + lv_draw_line_dsc_t ld; + int32_t vmin = s_codes[0]; + int32_t vmax = s_codes[0]; + int32_t dmin; + int32_t dmax; + int32_t span; + int32_t i; + int32_t y; + int32_t y_prev = 0; + + band.x1 = 0; + band.x2 = UI_SCREEN_W - 1; + band.y1 = WP_WTOP; + band.y2 = WP_WBOT; + if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) || + (band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) { + return; + } + for (i = 1; i < (int32_t)UI_WAVE_WIN_PTS; i++) { + int32_t v = s_codes[i]; + if (v < vmin) { + vmin = v; + } + if (v > vmax) { + vmax = v; + } + } + dmin = (vmin < 0) ? vmin : 0; + dmax = (vmax > 0) ? vmax : 0; + span = dmax - dmin; + if (span < 1) { + span = 1; /* 全零窗:0 线与波形线重合贴在区底,诚实且无除零 */ + } + + lv_draw_line_dsc_init(&ld); + ld.base.layer = a_layer; + ld.color = UI_COL_FG; + ld.width = 1; + + /* 0 线(虚线):恒在域内,永远画得出来 */ + ld.dash_width = 2; + ld.dash_gap = 2; + y = wp_map_y(0, dmin, span); + ld.p1.x = WP_WX0 - 2; + ld.p1.y = y; + ld.p2.x = WP_WX0 + ((int32_t)UI_WAVE_WIN_PTS * WP_PX_PER_PT) + 1; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + + /* 触发点竖光标(虚线):第 二十/廿一 点之间的点边界(见文件头裁定) */ + ld.p1.x = WP_CUR_X; + ld.p1.y = WP_WTOP; + ld.p2.x = WP_CUR_X; + ld.p2.y = WP_WBOT; + lv_draw_line(a_layer, &ld); + + /* 阶梯线(实线) */ + ld.dash_width = 0; + ld.dash_gap = 0; + for (i = 0; i < (int32_t)UI_WAVE_WIN_PTS; i++) { + int32_t x; + + x = WP_WX0 + (i * WP_PX_PER_PT); + y = wp_map_y((int32_t)s_codes[i], dmin, span); + ld.p1.x = x; + ld.p1.y = y; + ld.p2.x = x + WP_PX_PER_PT - 1; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + if ((i > 0) && (y != y_prev)) { + ld.p1.x = x; + ld.p1.y = y_prev; + ld.p2.x = x; + ld.p2.y = y; + lv_draw_line(a_layer, &ld); + } + y_prev = y; + } +} + +/* + * 函数功能:单位徽标取词(只读 eng 选词,eng 不进任何算式——§6.3 口径; + * 未识别值按最保守的「引脚域」渲染,不猜高口径) + * 入口参数:a_eng - 段头 eng 原值 + * 返回值:徽标文案(Flash 字面量) const char * + * 限定条件:仅绘制路径 + */ +static const char *wp_eng_word(uint8_t a_eng) { + if (a_eng == (uint8_t)UI_DATA_ENG_SEC) { + return "二次值"; + } + if (a_eng == (uint8_t)UI_DATA_ENG_PRI) { + return "一次值"; + } + return "引脚域"; +} + +/* + * 函数功能:开页复位(框架 init_cb)——通道回到 UA、数据回到未取态 + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内;只清状态零 I/O(懒取数在首帧绘制) + */ +static void pg_init(void) { + s_state = WP_ST_IDLE; + s_ch = 0u; +} + +/* + * 函数功能:自绘(框架 draw_cb) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链末尾调用 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + char buf[UI_BUF_STRLEN]; + + wp_ensure(); + if (s_state == WP_ST_BAD) { + wp_draw_text(a_layer, "该段不可读", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u, + a_clip); + return; + } + if (s_state != WP_ST_OK) { + wp_draw_text(a_layer, "无录波段", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u, + a_clip); + return; + } + + /* 顶行:左 = 通道 + 段号 + 触发字母;右 = 形状受限「削」徽标 */ + (void)lv_snprintf(buf, sizeof(buf), "%s SEG%u %c", s_ch_names[s_ch], + (unsigned int)s_idx, wp_trig_letter(s_trig)); + wp_draw_text(a_layer, buf, UI_LIST_LEFT_X, WP_INFO_Y, 0u, a_clip); + if ((s_clip & (uint8_t)(1u << s_ch)) != 0u) { + wp_draw_text(a_layer, "削", UI_SCREEN_W - 1, WP_INFO_Y, 2u, a_clip); + } + + wp_draw_wave(a_layer, a_clip); + + /* 倒数第二行:左 = 键位提示(ASCII);右 = 窗口峰值 mV(定点,ASCII) */ + wp_draw_text(a_layer, "UD=CH LR=SEG", UI_LIST_LEFT_X, WP_HINT_Y, 0u, + a_clip); + (void)lv_snprintf(buf, sizeof(buf), "PK%dmV", (int)s_peak_milli); + wp_draw_text(a_layer, buf, UI_SCREEN_W - 1, WP_HINT_Y, 2u, a_clip); + + /* 底行:单位徽标(eng) */ + wp_draw_text(a_layer, wp_eng_word(s_eng), UI_SCREEN_W / 2, WP_ENG_Y, 1u, + a_clip); +} + +/* + * 函数功能:按键处理(框架 key_cb)——上下切通道、左右切段 + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true = 本页已消费;false = 交回框架 bool + * 限定条件:框架按键链内(ESC 不到这里;LEFT 因 key_intercept=1 先到这里) + * 函数说明:环游语义 = 上下在八路间绕回、左右在现存段间绕回;切段失败 + * (卷忙/无邻段)保持当前画面不动——「按了没动」比「画面被 + * 忙态清成空」诚实,忙不忙在回放页本来就不是用户要的答案 + */ +static bool pg_key(lv_key_t a_key) { + uint8_t next; + int rc; + + switch (a_key) { + case LV_KEY_UP: + case LV_KEY_DOWN: + next = (a_key == LV_KEY_UP) + ? (uint8_t)((s_ch + UI_WAVE_CH_CNT - 1u) % UI_WAVE_CH_CNT) + : (uint8_t)((s_ch + 1u) % UI_WAVE_CH_CNT); + s_ch = next; + if (s_state == WP_ST_OK) { + rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli); + if (rc != UI_WAVE_OK) { + s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE; + } + } + return true; + case LV_KEY_LEFT: + case LV_KEY_RIGHT: + if (s_state == WP_ST_IDLE) { + wp_ensure(); /* 空态下按左右 = 再探一次盘(新段可能已落) */ + return true; + } + rc = (a_key == LV_KEY_RIGHT) ? ui_data_wave_seg_next(s_idx, &next) + : ui_data_wave_seg_prev(s_idx, &next); + if (rc == UI_WAVE_OK) { + s_idx = next; + s_state = WP_ST_IDLE; /* 先回空态再载,坏段不会留着旧画面 */ + wp_load_seg(); + } + return true; + default: + return false; + } +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=wave_play 绑定引用) + * 查看页形态:无浅 V、无页脚(段号在顶行,页脚「共 N 页」与段号两套计数 + * 反而打架)、不可选(单元素自绘页)、可编辑位 0(本页零写动作); + * key_intercept=1 是本页存在的理由级配置:LEFT 要拿来切上一段,不能让 + * 框架默认的「LEFT 关页」吃掉 + */ +const content_page_def_t ui_page_wave_play_desc = { + .id = "wave_play", + .title = "录波回放", + .hide_deco = 1u, + .hide_footer = 1u, + .selectable = 0u, + .key_intercept = 1u, + .edit_capable = 0u, + .init_cb = pg_init, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_wave_play.h b/Drivers/BSP/UI/ui_page_wave_play.h new file mode 100644 index 00000000..9454eabb --- /dev/null +++ b/Drivers/BSP/UI/ui_page_wave_play.h @@ -0,0 +1,29 @@ +/* + * 模块名称:UI Page wave_play (录波回放内容页) + * 模块功能:记录查询 → 录波回放 叶子的内容页声明——向框架 ui_content_page + * 提供页面描述符;单通道阶梯波形回放,数据全走录波注入表 + * (本模块零 App include、零存储触点、零波形算式) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:DTU 项目组 + * 创建日期:2026-10-29 + * 修改记录: + * v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧;元素表 = + * docs/子系统设计/故障录波设计说明.md §8.1) + */ +#ifndef UI_PAGE_WAVE_PLAY_H +#define UI_PAGE_WAVE_PLAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +/* 页面描述符(定义在 ui_page_wave_play.c;生成物按菜单树 #page=wave_play 引用本符号) */ +extern const content_page_def_t ui_page_wave_play_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_WAVE_PLAY_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e591b41c..eb3663bb 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1052,6 +1052,16 @@ 1 ..\App\algo\freq.c + + wave_ring.c + 1 + ..\App\algo\wave_ring.c + + + wave_store.c + 1 + ..\App\algo\wave_store.c + ui_data_feed.c 1 @@ -3676,6 +3686,16 @@ 1 ..\Drivers\BSP\UI\ui_page_yk.c + + ui_page_wave_manual.c + 1 + ..\Drivers\BSP\UI\ui_page_wave_manual.c + + + ui_page_wave_play.c + 1 + ..\Drivers\BSP\UI\ui_page_wave_play.c + diff --git a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct index b51cffcf..557aadcd 100644 --- a/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct +++ b/MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct @@ -10,6 +10,13 @@ ; *** 两条前提缺任何一条 = 静默数据损坏(2026-09-20 首轮 *** ; *** 扩容实验实证)。CCM 存在性本机 ST-LINK 实测可读写; *** ; *** 克隆片可能缺 SRAM3,换料必须重测! *** +; *** 录波支路(2026-10-29 R6 台账同步):wave_ring.o 5,300B *** +; *** (内含 int16×320×8 主环 5,120B)与落盘道那批 static 全 *** +; *** 部按 .ANY 落 RW_IRAM1——环不是 DMA 可见对象,但按 R1 *** +; *** CCM 只放 FreeRTOS 堆,且 CCM 只剩 7,904B(87.9%)本身 *** +; *** 无位可给;实测三档 RW_CCM 恒 57,376B、RW_IRAM1 从 *** +; *** 81,536B 升到 89,936B。凡「把大环挪进 CCM 腾 SRAM」的 *** +; *** 念头,先过策略文档 §5 裁决与 §7 验证阶梯再动手。 *** ; ************************************************************* LR_IROM1 0x08000000 0x00100000 { ; load region size_region @@ -23,7 +30,7 @@ LR_IROM1 0x08000000 0x00100000 { ; load region size_region .ANY (+RW +ZI) } RW_CCM 0x10000000 0x0000FF00 { ; CCM 64KB 减 256B 尾: FreeRTOS 堆 only (no DMA!) - heap_4.o (+RW +ZI) ; ucHeap 48KB: 任务栈/队列/定时器 + heap_4.o (+RW +ZI) ; ucHeap 56KB(2026-10-12 B5 由 48KB 扩容): 任务栈/队列/定时器 } UNINIT_CCM 0x1000FF00 UNINIT 0x00000100 { ; round 8 取证: 复位不丢"进段打点"槽 *(.noinit) ; IWDG/引脚复位不清 SRAM → 复位后仍可读 diff --git a/docs/README.md b/docs/README.md index cc5e9faf..2f1038a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -91,7 +91,7 @@ docs/ | [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 | | [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(诊断沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) | | [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 | -| [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与三条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 | +| [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与四条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 | | [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + 主机侧纯逻辑单测全栈单测 | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / 保护判据挂起 / 重试对账的调用关系与设计意图 | diff --git a/docs/UI设计/LVGL模拟器部署与使用说明.md b/docs/UI设计/LVGL模拟器部署与使用说明.md index 114819dd..a9dbc1e1 100644 --- a/docs/UI设计/LVGL模拟器部署与使用说明.md +++ b/docs/UI设计/LVGL模拟器部署与使用说明.md @@ -186,7 +186,7 @@ MCU 端 `Drivers/BSP/dbg_log.h` 会引用 `sys_clock.h`(RTC 时间戳),PC - `LV_USE_THEME_MONO` 是否 = 1 且在 `sim_port_init` 中调了 `lv_theme_mono_init`(1bpp 无主题 = 黑底黑字) - 主循环是否调用 `lv_refr_now(NULL)` 强制首帧输出(PARTIAL 模式下保证) - 是否重复设置 tick:SDL 驱动内部 `lv_tick_set_cb(SDL_GetTicks)`,**不要**再 `SDL_AddTimer` + `lv_tick_inc` -2. **中文字体缺字**:`ui_font_cn_12` 是子集字体(当前 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测)。**缺字才会显示为空白**——新增汉字必须重生成字体(见下文 6.3)。早期只有 83 字、当时菜单需要 186 字,那是 2026-09-03 的历史快照,不是现状。 +2. **中文字体缺字**:`ui_font_cn_12` 是子集字体(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测)。**缺字才会显示为空白**——新增汉字必须重生成字体(见下文 6.3)。早期只有 83 字、当时菜单需要 186 字,那是 2026-09-03 的历史快照,不是现状。 3. **`LV_SDL_RENDER_MODE` 必须为 `PARTIAL`**:1bpp + SDL 若非 PARTIAL,`lv_sdl_window.c` 会 `#error` 编译失败。 ### 6.3 字体生成(如需补全中文字符) @@ -234,7 +234,7 @@ A: `LV_CONF_DIR` 变量未在 `add_subdirectory(lvgl)` 前定义,检查 `simul A: 按 6.2 黑屏排查顺序检查:mono 主题 → `lv_refr_now` → tick 不重复。 ### Q4: 菜单某些中文显示空白? -A: 子集字体未含该字(当前字符集 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测),见 6.3 重新生成字体。 +A: 子集字体未含该字(当前字符集 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测),见 6.3 重新生成字体。 ### Q5: 修改了 UI 代码但模拟器没变化? A: 确认修改的是 `Drivers/BSP/UI/ui_menu.c` 而非 `simulator/` 内的副本(本工程无副本);然后重新 `cmake --build`。 diff --git a/docs/UI设计/UI_中文字体生成与配置.md b/docs/UI设计/UI_中文字体生成与配置.md index 2cca483b..5a168c7b 100644 --- a/docs/UI设计/UI_中文字体生成与配置.md +++ b/docs/UI设计/UI_中文字体生成与配置.md @@ -55,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78 -> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt ``` -产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 218 字,含 ▶/,/: 等符号;2026-10-28 UI 补齐批「记录 5 页 + 运行报告」+33 字,185→218——本行旧数 183 是未回填的历史值,改前实测 185;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。 +产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 224 字,含 ▶/,/: 等符号;2026-10-29 录波 UI 道 +6 字(218→224);2026-10-28 UI 补齐批「记录 5 页 + 运行报告」+33 字,185→218——本行旧数 183 是未回填的历史值,改前实测 185;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。 --- @@ -152,8 +152,8 @@ OK 67325 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c 1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。 2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。 3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。 -4. **字体体积**:12px + 1bpp + 218 个汉字/符号(**不含 ASCII**——ASCII 0x20~0x7E 由 make_font 的 -r 参数一并打进子集, - 故字形数 = 218 + 95 = **313**,`glyph_dsc` 数组实含 notdef 共 314 条;2026-10-28 实测)源文件 ≈ 65.7KB(2026-10-28 实测 67,325 B;改前 185 字/280 字形/58.2KB,旧文本 183 字/278 字形系未回填);ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 +4. **字体体积**:12px + 1bpp + 224 个汉字/符号(**不含 ASCII**——ASCII 0x20~0x7E 由 make_font 的 -r 参数一并打进子集, + 故字形数 = 224 + 95 = **319**,`glyph_dsc` 数组实含 notdef 共 320 条;2026-10-29 录波 UI 道实测)源文件 ≈ 67.1KB(2026-10-29 实测 68,729 B;改前 185 字/280 字形/58.2KB,旧文本 183 字/278 字形系未回填);ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。 --- diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index a160a97d..771dec4b 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -694,7 +694,7 @@ typedef struct { | 4 | 未标定时页面明确标识(不把码值当伏特) | 目检 + 文本断言(`eng=0` 时带标记) | | 5 | 首帧前显示 `--` 而非 `0.000` | 模拟器:不 publish 直接开页,断言文本 | | 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**半收口(2026-10-26)**:模拟器侧「改 → 保存 → OK → 回首屏新值已在帧里」已由像素 + 文本断言覆盖(黄金用例 `sparam_save` / `sparam_landed`,越界侧 `sparam_oob`);**重启保持与主站 C_RD 一致只能上板**——模拟器没有 NAND,`apply()` 到 `prot_set_set_sealed()` 就是终点,这条差距已登记在 `docs/测试说明/模拟器差距清单.md`;另注意 40 项里有 13 项 `comm_addr=0`(纯本地项),主站侧根本读不到,不能拿 C_RD 当它们的判据 | -| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26,当时下限为 27 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 60 张**,双次一致需按同口径重跑回填;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | +| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异。**已验(2026-10-26,当时下限为 27 页)**:27 页连跑两次逐字节一致(含整定 9 态)——**集合现已扩至 66 张**,**双次一致已验(2026-10-29:同一批 66 张连跑三次逐字节一致,池水位 5,448 B;菜单 8 状态连跑三次亦一致,5,368 B)**;轮询定时器不影响——无头模式不调 `lv_timer_handler()`(§9.2 注 6) | | 9 | 「写不进去」与「改了不动作」两条事实互不冒充 | 反向文本断言(`check_content_pages.py` 的 `ABSENT`:查看页与只读态不得出现「未标定」徽标与「保存」行)+ 台架:未制造该态——要它必须改坏 /prot_set.bin(破坏产品配置存储,用户裁定 2026-10-26 不做),故 LOCK 态目前只有代码级证据与三重反证(ENTER 被正常消费、遥信 YX31=OFF、开机 CORRUPT=0),**没有像素证据** | | 8 | 主机单测覆盖数据模块 | `test/host/ui_data` 全绿并挂 pre-commit 钩子 | diff --git a/docs/UI设计/UI_界面制作流程.md b/docs/UI设计/UI_界面制作流程.md index 7de2b718..77745727 100644 --- a/docs/UI设计/UI_界面制作流程.md +++ b/docs/UI设计/UI_界面制作流程.md @@ -48,7 +48,7 @@ flowchart TD |---|---|---|---| | C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` | | C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` | -| C3 | 字符集 | 子集字体(当前 218 汉字/符号 + ASCII 0x20-0x7E,共 313 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | +| C3 | 字符集 | 子集字体(当前 224 汉字/符号 + ASCII 0x20-0x7E,共 319 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | | C4 | LVGL 内存池 | 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩;实测峰值 6,568 B = 池的 40%);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` | | C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 | | C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 | diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index da8b2c4c..c2c11f63 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -26,7 +26,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | 步 | 脚本/命令 | 门禁内容 | 预期输出 | |---|---|---|---| | 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(MENU-STATE-03==MENU-STATE-01、MENU-STATE-06==MENU-STATE-01、MENU-STATE-07==MENU-STATE-02)+ MENU-STATE-00 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` | -| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,正向 425 条 + 反向 36 条,独立于 `ref/`)→ ② 60 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 60 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | +| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,正向 444 条 + 反向 45 条,独立于 `ref/`)→ ② 66 张截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 66 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | | 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK | | 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS | @@ -52,7 +52,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **5448 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B | > 水位历史:4784 →(2026-09-14 待机模块常驻 +656)5440 / 5520 →(2026-10-11 裁剪 LVGL 控件类 −72)5368 / 5448。 -> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 60 张), +> 后一次漂移已核实为**有意且更优**:像素逐字节未变(**裁剪当时黄金参考只有 11 页**,在裁剪**前**生成、裁剪后全部一致;该集合现已扩至 66 张), > 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。 **漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见 @@ -61,7 +61,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ## 四、黄金参考(入库)与目检 PNG(本地产物) -`simulator/tests/ref/*.bin`(像素黄金参考,60 张 × 3200 B)**随仓库入库**;派生的 `.png` +`simulator/tests/ref/*.bin`(像素黄金参考,66 张 × 3200 B)**随仓库入库**;派生的 `.png` 目检图仍是本地产物(`.gitignore`)。入库的理由:比对是**逐字节**的,基线不入库时任何新克隆 或新工作树跑 `ui-golden` 都必然 FAIL(缺基准),这条门禁就只在"有老副本的机器"上真实存在。 可复现性实测(2026-10-22):两份各自独立构建的 `lvgl_sim`(不同构建目录、相隔 11 天的代码) @@ -148,7 +148,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 ### 7.3 用法与门禁档位 ```powershell -# 认屏快检:60 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 +# 认屏快检:66 张黄金参考(+ 若有台架留档帧),秒级,不需要构建产物 & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest-quick # 完整自检:再加全绑定页闭环导航(需模拟器可执行文件,约 30 s) & '.venv\Scripts\python.exe' scripts/board_ui_nav.py --selftest diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index 6347b417..71cf28e7 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -38,7 +38,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | LVGL 内存池 | 16 KB(`LV_MEM_SIZE`,2026-10-15 由 40 KB 收缩;实测峰值 6,568 B = 池的 40%) | `Lib/lv_conf.h` | | 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` | | 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` | -| 默认字体 | `ui_font_cn_12`(12px 1bpp,218 汉字/符号 + ASCII 0x20–0x7E,共 313 字形) | `Lib/lv_conf.h` | +| 默认字体 | `ui_font_cn_12`(12px 1bpp,224 汉字/符号 + ASCII 0x20–0x7E,共 319 字形) | `Lib/lv_conf.h` | | uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` | ### 1.4 硬约束(改 UI 必须遵守) diff --git a/docs/UI设计/mockups/01_主界面_说明.md b/docs/UI设计/mockups/01_主界面_说明.md index 37737e70..4535d1f2 100644 --- a/docs/UI设计/mockups/01_主界面_说明.md +++ b/docs/UI设计/mockups/01_主界面_说明.md @@ -32,7 +32,7 @@ 说明:选中行(第 1 行)无 ▶ 箭头;其余行 ▶ 紧跟在 4 字文本后(非右对齐)。行内文字左对齐、左留白 8px。 -## 三、字符集核对(对照当时(2026-09-03)的 ui_font_cn_12:78 汉字 + ASCII 0x20~0x7E;本 mockup 的核对结论只对该时点成立,现值 = 218 汉字/符号 + ASCII,共 313 字形,见 docs/UI设计/UI_中文字体生成与配置.md §7) +## 三、字符集核对(对照当时(2026-09-03)的 ui_font_cn_12:78 汉字 + ASCII 0x20~0x7E;本 mockup 的核对结论只对该时点成立,现值 = 224 汉字/符号 + ASCII,共 319 字形,见 docs/UI设计/UI_中文字体生成与配置.md §7) - **菜单 7 项**共 22 个唯一汉字:命中 21(装 置 信 息 实 时 数 据 参 定 值 遥 设 维 护 通 讯 记 录 查 询),**新增 1(三)**。 - **状态行**"当前模式 : 无模式":**新增 5(当 前 模 式 无)** + **全角冒号 :(U+FF1A,新增)**;空格为 ASCII(已在字库)。 diff --git a/docs/UI设计/内容页架构.md b/docs/UI设计/内容页架构.md index d96398ca..d2cc0449 100644 --- a/docs/UI设计/内容页架构.md +++ b/docs/UI设计/内容页架构.md @@ -453,7 +453,7 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash ## 八、验证与回归 - **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): - 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 60 张黄金参考逐字节比对 + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 66 张黄金参考逐字节比对 (网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 - **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 5368 B`、`POOL_AFTER_ROUNDTRIP = 5448 B` - **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → diff --git a/docs/UI设计/菜单页面设计.md b/docs/UI设计/菜单页面设计.md index 5c7c014e..a3a65be6 100644 --- a/docs/UI设计/菜单页面设计.md +++ b/docs/UI设计/菜单页面设计.md @@ -399,7 +399,7 @@ for (uint8_t i = 0; i < cnt; i++) { python tools/menu_gen/gen_menu.py ``` -> 产物 `ui_menu_tab.c/.h` 含 42 节点(9 菜单 + 33 叶子,最深 三级菜单);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。 +> 产物 `ui_menu_tab.c/.h` 含 43 节点(9 菜单 + 34 叶子,最深 三级菜单;2026-10-29 录波道加两叶后由 42 升到 43);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。 > **编码陷阱(已踩)**:产物含中文**字符串字面量**,必须 **UTF-8 带 BOM**(同 `ui_menu.c`)。 > 无 BOM 时 cl.exe(代码页 936/GBK)会按 GBK 误读 UTF-8 字节 → 字符串解析错乱(C2001/C4819)。`gen_menu.py` 已用 `utf-8-sig` 输出。 @@ -792,7 +792,7 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 1. 本 LVGL v9.3 的 `lv_text_get_width(txt, length, font, letter_space)` **length=0 时返回 0**(不是"整串");必须传 `strlen`——否则框宽计算全 0(根框退化 8 px)。 2. 无头模式**不得**调 `lv_timer_handler()` / `lv_refr_now()`:I1 + `LV_DISPLAY_RENDER_MODE_FULL` 全屏刷新会进渲染死循环(截图因此走 `lv_snapshot`);`--keys` 注键是同步处理,无需驱动帧。 3. 选中行**保留 ▶**(随反色变黑):参考图 02/04 原图即"选中行箭头仍在、仅随反色变黑",勿实现成"选中隐箭头"(§4.4)。 -4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 218 唯一字,2026-10-28 实测)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 +4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 224 唯一字,2026-10-29 实测)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 5. **参考图保真度**:参考图为手工 mockup,4 个样本(子框顶位置)不满足单一公式;实现采用自洽规则(顶对齐/底对齐上移),实测与参考图偏差:顶位置 2/4/6/1 px(三遥/装置维护/通讯参数/记录查询),框宽 2/8/0/2 px(4字+箭头/4字/3字/2字+箭头)。两端极例(三遥、记录查询)精确吻合;统一规则优于逐屏特调,偏差范围内不影响识读与操作。 6. 装饰"浅 V"两腿线宽必须 3(水平线 2):1bpp 下细斜线边缘被阈值吃掉,腿不粗于水平线即消失;且依赖 `LV_DRAW_SW_COMPLEX=1`(见 `UI_设计架构.md` §1.4 约束 7)。 diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md index 6fda7b70..6070dea5 100644 --- a/docs/UI设计/页面扩充详细方案.md +++ b/docs/UI设计/页面扩充详细方案.md @@ -21,7 +21,7 @@ 已绑定 6 页:`装置信息`(node 1)、`交流量`(3)、`直流量`(4)、`网口设置`(30)、`自检记录`(39)、`运行记录`(40)。 -> ⚠ **本表 = 2026-10-25 立项时快照,按历史冻结不回改。** 2026-10-28 复扫现值:**已绑 28 叶 / 未绑 5 叶**(判据两处互证:`ui_menu_tree.txt` 剔掉文件头注释里的示例行后 `#page=` 实数 = 28,与生成物 `ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` 一致);未绑 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信 / 手动录波 / SNTP 设置。现状口径见 `docs/任务规划/UI补齐.md` §1 与 `docs/产品需求/产品需求设计文档.md` §8.2 / §8.3。 +> ⚠ **本表 = 2026-10-25 立项时快照,按历史冻结不回改。** 2026-10-28 复扫现值:**已绑 28 叶 / 未绑 5 叶**;**2026-10-29 再复扫:已绑 30 叶 / 未绑 4 叶**(手动录波与录波回放两叶已绑)(判据两处互证:`ui_menu_tree.txt` 剔掉文件头注释里的示例行后 `#page=` 实数 = 30,与生成物 `ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` 一致);未绑 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信 / 手动录波 / SNTP 设置。现状口径见 `docs/任务规划/UI补齐.md` §1 与 `docs/产品需求/产品需求设计文档.md` §8.2 / §8.3。 ### 1.2 27 个空叶子清单(按菜单顺序) @@ -626,7 +626,7 @@ SNTP 客户端 ──────→ 批4 SNTP设置 | 4 | 无源点显示 `--` | 遥信量无效点、直流量直流通道 | | 5 | 定值整定闭环 | 改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致。**2026-10-26 半收口**:模拟器侧「改 → 保存 → `OK` → 回首屏新值已在帧里」与「越界被拒、值不落盘」已由像素 + 文本断言覆盖(`sparam_save` / `sparam_landed` / `sparam_oob`);**重启保持与 C_RD 一致只能上板**(模拟器无 NAND)。另注意 40 项里 **13 项 `comm_addr = 0`**(投退、返回系数、过频/低频段、全局软压板)主站不可寻址,不能拿 C_RD 当它们的判据 | | 6 | 分层门禁无新增 | `check_layer_deps.py` 退出码 0,基线 6 → 3。**现状:3**(记录 reader 已削 3 条;写路径走注入式 ops 函数表,未新增 include,见 `docs/UI设计/UI_数据供给设计说明.md` §6.2) | -| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致——**该下限此后已扩至 60 张**(新增 clock / reset / clear 系列 13 张,其后 47→53→60),60 张上的双次逐字节一致需按同口径重跑一次再回填本行 | +| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异。**已验(2026-10-26)**:27 页连跑两次逐字节一致——**该下限此后已扩至 66 张**(新增 clock / reset / clear 系列 13 张,其后 47→53→60→66,2026-10-29 录波 UI 道 +6 张),66 张上的双次逐字节一致**已验(2026-10-29:任务树重建模拟器后同一批 66 张连跑三次逐字节一致,页面池水位 5,448 B;菜单侧 `check_menu_rows.py` 8 状态连跑三次亦逐字节一致,5,368 B;三次之间未用更新模式写过任何参考图,故这是同口径的传递性证据)** | | 8 | 每个新页有 golden 参考 | `simulator/tests/ref/` 新增对应 `.bin`。**整定 4 页已有 9 张**(含保存/越界/落定/回绕各态);参考图入库,改它 = 改判据,须逐页过目 | --- diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md index 4cbbaffc..af741912 100644 --- a/docs/UI设计/页面补齐规划.md +++ b/docs/UI设计/页面补齐规划.md @@ -27,7 +27,7 @@ ### 2.1 规模 > ⚠ **本表是 2026-10-23 立项当天快照**(节点 42 / 有内容页叶子 6 / 空叶子 27 / 黄金参考 11 份均为当日实数,按"完成即冻结"不回改)。 -> 页面扩充落地后现值已变——黄金参考现为 60 份(`simulator/tests/ref/` 入库的 `shot_*.bin` 实数,2026-10-28 记录 5 页与运行报告页落地 +7 张),现状口径见 +> 页面扩充落地后现值已变——黄金参考现为 66 份(`simulator/tests/ref/` 入库的 `shot_*.bin` 实数,2026-10-28 记录 5 页与运行报告页落地 +7 张;2026-10-29 录波 UI 道 +6 张),现状口径见 > `docs/UI设计/页面扩充详细方案.md` 与 `docs/UI设计/UI_数据供给设计说明.md`。 | 项 | 数 | @@ -77,7 +77,7 @@ | 22 | 双点遥信 | 可编辑列表 | 开入量 | ❌ 同上 | | 24 | 时钟设置 | 表单(6 字段) | `sys_clock_get_time/set` | ✅ | | 25 | 强制复归 | 操作页(确认) | `prot_elm_request_reset()` | ✅ | -| 26 | 手动录波 | 操作页(确认) | 录波模块(docs/任务规划/故障录波.md 已立项) | 🔶 录波立项未落地 | +| 26 | 手动录波 | 操作页(确认) | `wave_ring_request` 手动触发经 `ui_data_wave_ops_t` 注入 | ✅ 已落地(`ui_page_wave_manual.c`,2026-10-29 录波 UI 道;配套新增「录波回放」叶 node 43 = `ui_page_wave_play.c`,不在本表 27 叶之列) | | 27 | 清除记录 | 操作页(选类型 + 确认) | `sys_log`(**缺删除 API**) | 🔶 需补 API | | 29 | 通讯设置 | 可编辑列表(4~6 项) | 104 公共地址/端口/主站(当前固定) | 🔶 需 `comm_cfg` | | 31 | SNTP设置 | 可编辑列表(3 项) | SNTP 客户端 | ❌ 未实现(产品口径亦未定) | @@ -91,7 +91,8 @@ **统计(2026-10-25 复核)**:本表 27 叶中 **6 叶已落地**(遥信量 / 查看 4 页 / SOE),剩 21 叶 = 数据现成 **8** 项(整定 4 + 时钟 + 复归 + 死区 + 系数)· 需新数据层 **2** 项(清除记录 / 通讯设置)· 数据源不存在 **11** 项(遥信 3 + 手动录波 + SNTP + 记录 5 + 运行报告)。**分批与 DoD 见 `docs/任务规划/UI补齐.md`**——本表只讲逐页形态与数据源,不重复排期。 -**2026-10-28 复核**:本表 27 叶中 **22 叶已落地**(本批 +6:记录 5 页与运行报告,见 docs/任务规划/UI补齐.md §2 与 docs/任务规划/登记簿.md §7);未落地的 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信(无开入硬件)+ 手动录波(无录波模块)+ SNTP设置(产品口径未定),取舍与重开条件见 docs/任务规划/UI补齐.md §5。口径闭合:本表 22 叶 + 立项前就已绑定、不在本表内的 6 叶(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录)= 现值已绑 28 叶(= UI_PAGE_MAP_COUNT);本表未落地 5 叶 = 菜单树未绑 5 叶(33 − 28)。复核方法:按缩进判叶子(有更深缩进后继者即非叶子),再按行内 #page= 数已绑。**2026-10-29 补充**:「手动录波」已随故障录波独立立项(docs/任务规划/故障录波.md 与 docs/子系统设计/故障录波设计说明.md),上面那行数字仍是 2026-10-28 当日复核值,未随立项改动 +**2026-10-28 复核**:本表 27 叶中 **22 叶已落地**(本批 +6:记录 5 页与运行报告,见 docs/任务规划/UI补齐.md §2 与 docs/任务规划/登记簿.md §7);未落地的 5 叶 = 遥信类型 / 遥信防抖 / 双点遥信(无开入硬件)+ 手动录波(无录波模块)+ SNTP设置(产品口径未定),取舍与重开条件见 docs/任务规划/UI补齐.md §5。口径闭合:本表 22 叶 + 立项前就已绑定、不在本表内的 6 叶(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录)= 现值已绑 28 叶(= UI_PAGE_MAP_COUNT);本表未落地 5 叶 = 菜单树未绑 5 叶(33 − 28)。复核方法:按缩进判叶子(有更深缩进后继者即非叶子),再按行内 #page= 数已绑。**2026-10-29 补充**:「手动录波」已随故障录波独立立项(docs/任务规划/故障录波.md 与 docs/子系统设计/故障录波设计说明.md),上面那行数字仍是 2026-10-28 当日复核值,未随立项改动。 +**2026-10-29 二补(录波 UI 道落地)**:本表「手动录波」转已落地(23 叶);菜单树另新增「录波回放」叶(node 43,不在本表立项快照内)——现值 43 节点 / 34 叶 / 已绑 30 / 未绑 4(= 遥信 3 + SNTP 1,UI_PAGE_MAP_COUNT = 30),口径复核方法与上一段相同 --- @@ -211,7 +212,7 @@ ui_page_ac / ui_page_yx / ui_page_setlist ... 12. **三遥设置 5 页**(18~22)——等运行参数段与开入量。 13. **事故/操作/告警/启动/遥控 5 页**(34~38)——等记录写入方(每类记录 = 一个写入点 + 一个 `syslog_id_t`)。 -14. **手动录波**(26)——等录波模块。 +14. **手动录波**(26):操作页(`ui_page_action` 确认链,受理后回「已请求录波」)——已落地(2026-10-29);同批新增「录波回放」单页(上下键切通道、左右键切段、阶梯线 3px/点、0 线虚线、峰值 mV 经 `wave_store_code_to_milli` 唯一口径、削徽标、空态/坏段显式文案,规格见 docs/子系统设计/故障录波设计说明.md §8.1)。 15. **运行报告**(41)——等计数器(上电次数/运行时长/复位原因/104 统计)。 ### 批 4(可选):厂家设置 diff --git a/docs/images/故障录波数据流.svg b/docs/images/故障录波数据流.svg index c90a33b2..f77cdf9c 100644 --- a/docs/images/故障录波数据流.svg +++ b/docs/images/故障录波数据流.svg @@ -1 +1 @@ -录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。触发来路:只置标志UI 手动录波操作页prot_elm 的 picked 上升沿bench 构建定周期自抓谁都只置一个原子标志adcTask:唯一写环的人每拍压 8 路 int16 原始码值专用录波环 320 点 × 8 路查标志 → 冻结 → 计满封段= 5,120 B,落 SRAM1defaultTask:文件唯一写者排空已封存区 → f_write写回字节数不符即算失败分次写 256 B,不整段驻留更新段账与最新段号/wave 段文件文件名 WAVE 加两位十进制头存 eng、系数与 clip_mask数据区按通道分列存放与 /log 平级,FTP 根下可见只置标志已封存标志字节数不符即失败采集主线照常推进(本波一行不改它)sample_ring → phasor → freq → meas → 104 / UI,全链同源冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常三个读者:一律只读已封存的段uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表陷阱一 · 取窗竞态sample_ring_window() 读 head 后逐点拷,没有临界区现在只有 adcTask 同任务调用才安全处置:录波不复用它,另立专用环;跨任务只传已封存标志陷阱二 · FatFs 追加写该移植 FA_OPEN_APPEND 不含 FA_WRITE簇分配失败还返 FR_OK 加短写,无错误码处置:打开带 FA_WRITE,判成功必须比对写回字节数陷阱三 · 只有 8.3 短名FF_USE_LFN=0:长名与点开头一律拒处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码四条「不许」——上面那张图上没有箭头经过的地方不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。 +录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界写侧只有一条路:adcTask 压环 → defaultTask 落盘 → /wave 段文件;读者一律只读已封存的段。「已封存」是唯一的跨任务接口:没有任何人读别人的活缓冲——这是本图唯一要记住的一件事。触发来路:只置标志UI 手动录波操作页prot_elm 的 picked 上升沿bench 构建定周期自抓谁都只置一个原子标志adcTask:唯一写环的人每拍压 8 路 int16 原始码值专用录波环 320 点 × 8 路查标志 → 冻结 → 计满封段= 5,120 B,落 SRAM1defaultTask:文件唯一写者排空已封存区 → f_write写回字节数不符即算失败分次写 256 B,不整段驻留更新段账与最新段号/wave 段文件文件名 WAVE 加两位十进制头存 eng、系数与 clip_mask数据区按通道分列存放与 /log 平级,FTP 根下可见只置标志已封存标志字节数不符即失败采集主线照常推进(本波一行不改它)sample_ring → phasor → freq → meas → 104 / UI,全链同源冻结窗口丢的是录波样本,不是采集:相量、测频、判据、上送一律照常三个读者:一律只读已封存的段uiTask 回放页 —— 只读窗口取一段,自己画(不写文件)FTP(netTask / lftpd)—— 只搬文件,不碰采集上下文PC 脚本 —— 重算全周傅氏,与板上 meas: 与 104 浮点对表陷阱一 · 取窗竞态sample_ring_window() 读 head 后逐点拷,没有临界区现在只有 adcTask 同任务调用才安全处置:录波不复用它,另立专用环;跨任务只传已封存标志陷阱二 · FatFs 追加写该移植 FA_OPEN_APPEND 不含 FA_WRITE簇分配失败还返 FR_OK 加短写,无错误码处置:打开带 FA_WRITE,判成功必须比对写回字节数陷阱三 · 只有 8.3 短名FF_USE_LFN=0:长名与点开头一律拒处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码陷阱四 · 落盘途中环被解冻或换段adcTask 优先级高于 defaultTask,搬段窗口里环可能自弃、甚至封出下一段数据区 CRC 是对本道实际读到的字节算的 → 半新半旧照样自洽处置:关档后复检环侧身份(sealed + last_t0_ms + last_seq),不符即判废四条「不许」——上面那张图上没有箭头经过的地方不许跨任务读活的录波环或 sample_ring——没有临界区,读出来是两帧混合数据不许 protectTask 写环或落盘——它只置一个请求标志,同 prot_elm_request_reset 的模式不许 netTask 与 lftpd 读环——文件服务只搬已封存的段,不碰采集上下文不许把 U 与 I 两段叠在同一张图上比相位——跨转换器无共同采样触发,V0.1 的硬件事实字段级布局见 docs/子系统设计/故障录波设计说明.md §6.2;预算与 R6 前置见 §7;触发源与段长冻结语义见 §4。本图由 tools/arch_diagram/gen_wave_record_flow_svg.py 生成,改图请改脚本重跑,勿手改 SVG。 diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 657df359..2b1090c3 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -201,7 +201,7 @@ flowchart LR | 7 | 开入 KI | `APP/EquipCfg/Const_IO.c` `g_tInputModelTab` | **138 点** | 0(无硬件,X3) | ⬜ | | 8 | 开出 KO | 同上 `g_tOutputModelTab` | **31 点** | 0(无硬件,X3) | ⬜ | | 9 | 动作事件 | `APP/EquipCfg/Const_Rec.c` `g_tActRecTab` | **33 条**(421~453) | 7 类记录(`App/log/sys_log.h`) | 🔶 | -| 10 | 录波测量通道 | 同上 `g_tMeasureModelTab` | **65 通道** | 0 | ⬜ | +| 10 | 录波测量通道 | 同上 `g_tMeasureModelTab` | **65 通道** | 8(本波口径,2026-10-29 代码面已交付;65 通道仍为 0,参考机口径未改)| 🔶 | | 11 | 软压板 | `APP/EquipCfg/Const_Soft.c` `g_tSoftTab` | **9 面**(表容量 32) | 1 面(`PS_GLOBAL_EN`) | 🔶 | | 12 | 保护元件 | `RelayUnit/*.c` | **45 个模块** | 9 个实例 | 🔶 | | 13 | 记录队列 | `RecUnit/RecFile.c` + `GlobalDef/Const_Sys.h` | **13 类**(128/256 条) | 8 通道(7 文本 + SOE) | 🔶 | @@ -329,7 +329,7 @@ tTimer_NoUI 无压无流判据(CN_ |---|---|---| | **外部资源**(标准源、端子变比) | 定值标定、保护判据解卡 | 两点标定(§9.2 第 4 步) | | **硬件缺失**(X3 / X4) | 开入 138、开出 31、电度 8、同期、三相通道 | V0.2 硬件 | -| **立项未排** | 多规约、SNTP、定值铺开 | 按 §9.2 顺序;**录波已立项**(2026-10-29):卡片 docs/任务规划/故障录波.md、设计 docs/子系统设计/故障录波设计说明.md,1 kHz 起步与本文 80 点/周波口径的偏离在该设计文档 §2 显式登记 | +| **立项未排** | 多规约、SNTP、定值铺开 | 按 §9.2 顺序。**录波最小闭环已交付代码面**(2026-10-29,卡片 docs/任务规划/故障录波.md、设计 docs/子系统设计/故障录波设计说明.md;1 kHz 起步与本文 80 点/周波口径的偏离在该设计文档 §2 显式登记);**4 kHz 全速、多段、COMTRADE、104 文件传输仍未排**,属二期范围 | | **算力 / 形态**(X1 / X2) | 双核分工、VxWorks | 不解除。**屏板链路已从范围外项移入范围内待做**(规格 §5.16,执行规划 docs/任务规划/屏板链路.md):主机侧链路与参考 1:1 同构,不属"替代方案" | > **结论**:本产品与参考产品的差距**全部是"进度"而非"范围"**——没有一项是"参考有而我们不做"。 @@ -890,7 +890,7 @@ sequenceDiagram 上电次数 + 本次复位原因快照)与 iec104_serv 的四个只读观测计数,经 info 帧下发 (口径 docs/UI设计/UI_数据供给设计说明.md §4.4)。 - 判据:五类记录各有写入点、可翻最近 20 条、空记录明确显示"无记录"、共用 reader——**软件面已满足** - (空态与四个范围标签由 60 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 + (空态与四个范围标签由 66 张像素黄金钉住;模拟器侧这些文件没有写入方,只拍到空态)。 真记录翻页、掉电后保持、清除记录后归零、boot_count 自增与四态判定**待台架复核** (docs/任务规划/登记簿.md §7)。 @@ -942,7 +942,7 @@ sequenceDiagram | 遥控选择 / 撤销 / 执行 | ≤5 s | 同轮应答(ms 级) | | 读定值 / 改定值预置 / 执行 | ≤10 s / ≤5 s | 同轮应答(ms 级) | | 读文件目录 / 文件 | ≤30 s | ⬜ 未实现(文件传输未立项) | -| 录波文件存储 | ≤15 s | ⬜ 未实现(**已立项**,见 docs/任务规划/故障录波.md:本波按 1 kHz / 8 路 / 单段做,≤15 s 与 80 点口径属二期) | +| 录波文件存储 | ≤15 s | ⬜ 未验收(代码面 2026-10-29 已交付:/wave 段文件 + FTP 取回;本波按 1 kHz / 8 路 / 单段做;≤15 s 时限未测,80 点/周波属二期;卡片见 docs/任务规划/故障录波.md) | **FR-IEC104-8 单连接与自愈** - 需求:单连接语义(CH395F 单 socket);断链自动重听;网络重建(改 IP)后服务自愈;协议故障主动拆链。 @@ -1806,10 +1806,11 @@ pre-commit run --all-files ![级联框总览](../images/级联框总览.svg) -**规模**:菜单节点 **9** 个(含根)+ 叶子节点 **33** 个 = **42** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 -**绑定现状**(2026-10-28 实测):**已绑内容页 28 叶 / 未绑 5 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 28, -与已绑叶数一致);未绑 5 叶 = 无开入硬件 3 + 手动录波 1 + SNTP 1(取舍与重开条件见 -docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 页 + 运行报告)已于 2026-10-28 落地绑页。 +**规模**:菜单节点 **9** 个(含根)+ 叶子节点 **34** 个 = **43** 节点;最大深度 **4 级(主界面 / 一级 / 二级 / 三级菜单)**。 +**绑定现状**(2026-10-29 复扫):**已绑内容页 30 叶 / 未绑 4 叶**(页面注册表 `UI_PAGE_MAP_COUNT` = 30, +与已绑叶数一致);未绑 4 叶 = 无开入硬件 3 + SNTP 1(取舍与重开条件见 +docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 页 + 运行报告)已于 2026-10-28 落地绑页, +「手动录波」与「录波回放」两叶于 2026-10-29 随录波道绑页(判据见 docs/子系统设计/故障录波设计说明.md §4.1 与 §8.1)。 **根页(主界面)7 个一级条目**:装置信息 / 实时数据 / 参数定值 / 三遥设置 / 装置维护 / 通讯参数 / 记录查询。 @@ -1843,7 +1844,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ### 8.3 内容页 -**已绑 28 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): +**已绑 30 页**(按分组;逐页形态见 [`docs/UI设计/页面补齐规划.md`](../UI设计/页面补齐规划.md)): | 分组 | 页面 | 形态 | |---|---|---| @@ -1852,21 +1853,22 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 参数定值 | 查看 4 页(参数 / 定值 / 控制字 / 软压板) | 只读列表 | | 参数定值 | 整定 4 页(参数 / 定值 / 控制字 / 软压板) | 可编辑(写值 + 置脏 + 保存反馈) | | 三遥设置 | 遥测死区 / 遥测系数 | 可编辑(运行参数段,`0x8030~0x804F`) | -| 装置维护 | 时钟设置 / 强制复归 / 清除记录 | 操作页(执行类必须出真反馈,不得"点了没效果") | +| 装置维护 | 时钟设置 / 强制复归 / 清除记录 / 手动录波 | 操作页(执行类必须出真反馈,不得"点了没效果";手动录波触发后回读同一份计数,判据见 docs/子系统设计/故障录波设计说明.md §8.2) | | 通讯参数 | 通讯设置 / 网口设置 | 可编辑(`[comm]` / `[network]` 段,保存后按各自生效时机) | | 记录查询 | SOE 记录 / 自检记录 / 运行记录 | 记录列表(共用 reader) | | 记录查询 | 事故记录 / 操作记录 / 保护告警 / 保护启动 / 遥控记录 | 记录列表(每记录 2 行、空态显「无记录」;写入点与判据见 App/log/README.md §五类记录) | | 记录查询 | 运行报告 | 只读列表 13 行分 2 页;四个数逐个挂范围标签(自电池周期 / 本次开机),跨开机累计按裁定不做 | +| 记录查询 | 录波回放 | 单通道波形页(阶梯线自绘、8 路切换、单位徽标、削顶标记、空态与不可读态显式文案);显示契约见 docs/子系统设计/故障录波设计说明.md §8.1 | -**未绑 5 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零): -遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」);手动录波 = 等录波子系统 -(§9.1.2「录波子系统」);SNTP 设置 = 等 SNTP 对时(§9.1.2「SNTP 对时」)。 +**未绑 4 叶**(2026-10-28:记录 5 页与运行报告页已落地绑页,原「当前执行 6 项」清零;2026-10-29: +录波道落地,「手动录波」叶子随之一并绑页): +遥信类型 / 遥信防抖 / 双点遥信 = 等开入硬件(§9.1.1「出口与开入」);SNTP 设置 = 等 SNTP 对时(§9.1.2「SNTP 对时」)。 - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),`ESC` 返回菜单屏。 - 菜单导航状态**不压层**,返回即原样还原;同一时刻至多一个内容页打开。 - 节点→页映射在逻辑层 `menu_content_page_for(node)`;未绑叶子进入空白终点框(不伪造数据)。 - 数据供给走**注入通道**(rt / par / info / 遥测死区与系数第四表),页面不直接触算法层; - 像素黄金用例当前 **60 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地,+7 张)。 + 像素黄金用例当前 **66 张**(菜单 8 状态另计;2026-10-28 记录 5 页与运行报告 2 页落地 +7 张;2026-10-29 录波 UI 道 6 张:手动录波 3 态 + 录波回放 3 态)。 ### 8.4 密码与权限 @@ -1879,7 +1881,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 ### 8.5 中文字体 -- 字体 `ui_font_cn_12.c`:宋体 12px 1bpp **子集**(当前 **218 唯一字 / 313 字形**,2026-10-28 实测; +- 字体 `ui_font_cn_12.c`:宋体 12px 1bpp **子集**(当前 **224 唯一字 / 319 字形**,2026-10-29 实测; 122/217、179/274、185/280 均为历史快照。字形数 = 汉字/符号数 + ASCII 0x20~0x7E 共 95 个)。 - 生成流程:`tools/font_gen/extract_cn.py`(扫 UI 源码 → `ui_cn_chars.txt`)→ `tools/font_gen/make_font.py`。 - **约束**:任何 UI 文案新增汉字后必须走此流程,否则显示为空白方块。 @@ -1922,7 +1924,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | **映射铺开** | 3 | 107 项 `0x8820~0x8891` | 40 项局部 | +67 项 | | **元件铺开** | 12 | 45 个模块 | 8 个模块 / 9 个实例 | +37 个模块(过流类 6 / 电压类 5 / 重合闸 5 / 方向功率 4 / 涌流故障 5 / 自动控制 5 / 记录 5 / 录波采样 2) | | **遥测铺开** | 4、5 | 31 点 + 模型 35 行 | 9 点 | +22 点 + 模型表 | -| **录波子系统** | 10、15 | 3 类型 / 6 段 / 65 通道 / RAM 16 个 / COMTRADE 64·64·16 | 0 | 整子系统 | +| **录波子系统** | 10、15 | 3 类型 / 6 段 / 65 通道 / RAM 16 个 / COMTRADE 64·64·16 | **最小闭环代码面已交付**(2026-10-29):1 kHz / 8 路原始码值 / 单段 320 点 / 轮转段文件 / 手动与保护启动 / 单通道回放页;**板级判据一轮未跑** | 仍差 4 kHz 全速与 80 点/周波、多段与 65 通道、COMTRADE、104 文件传输;口径偏离登记见 docs/子系统设计/故障录波设计说明.md §2 | | **记录体系** | 9、13 | 13 类队列(128/256 条) | 8 类 | +5 类(越限 / 闭锁 / COS / 顺控 / 顺控执行) | | **UI 记录页** | 9、13 | 参考有完整记录页与运行报告 | 六页已绑并纳入像素黄金回归(2026-10-28) | 只剩真机判据(docs/任务规划/登记簿.md §7) | | **多规约栈** | 17 | 12 种 | 1 种(104) | +11 种(101 / 102 / 103 / CDT / ModBus / Whm_GB / 61850 / CAN + 子站侧 3 类) | @@ -1946,14 +1948,14 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 顺序 | 工作 | 类别 | 当前状态 | 开始条件 | |---|---|---|---|---| -| 1 | UI 补齐(记录 5 页 → 运行报告) | 规模型 | **清单已清空(2026-10-28)** | 六页已落地;余 5 叶属不做项,重开条件见 docs/任务规划/UI补齐.md §5 | +| 1 | UI 补齐(记录 5 页 → 运行报告) | 规模型 | **清单已清空(2026-10-28)** | 六页已落地;余 4 叶属不做项(手动录波与录波回放两叶已随录波道绑页),重开条件见 docs/任务规划/UI补齐.md §5 | | 2 | net 层跨上下文收口 | 平台 | **下一顺位(未开工)** | UI 清单已清空,切换条件已满足;**开工待用户点头** | | 3 | 其余结构与可靠性评估 | 平台 | 排队 | net 收口后 | | 4 | 端子/子板变比确认 → 两点标定 | 阻塞型 | 等外部资源 | 子板实物/原理图、标准源到位可插入 | | 5 | 标准源送检与第三方 104 主站验收 | 阻塞型 | 等标定 | 保护 VALID 置位;送检窗口前 2 日冻结特性 | | 6 | 定值铺开 / 映射铺开 / 遥测铺开 / 软压板铺开 | 规模型 | 等排期 | 标定后(工程量才有意义) | | 7 | 元件铺开与遥测增强 | 规模型 | 等标准源/硬件 | 先解除未标定闭锁;三相类等 V0.2 | -| 8 | 录波子系统 → 记录体系 → 多规约栈 | 规模型 | 不排 | 各自重启条件 | +| 8 | 录波子系统 → 记录体系 → 多规约栈 | 规模型 | **录波最小闭环已提前插入并交付代码面(用户裁定 2026-10-29 录波先行,见 docs/任务规划/故障录波.md);本行剩下的参考机完整形态(多段 / 65 通道 / COMTRADE / 104 文件传输)仍不排** | 各自重启条件 | | 9 | 验证窗口事项(CI 远端、PC peer、负向注入) | 平台 | 逢条件顺带 | 条件到位即执行 | | 10 | 出口与开入 / 三相与同期 / 电度计量 / 对下采集 | 阻塞型 | 不排 | V0.2 硬件 / 用户决策 | @@ -2498,7 +2500,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 元件铺开 | 45 个模块对应 45 个实例;每个实例有:定值项齐全、启动/动作/返回三态可观测、事件码正确、单测覆盖判据边界 | | 遥测铺开 | 31 点全部非 IV(除物理不可达点明确标 IV);线电压与相量满足 `Uab = Ua − Ub` 误差 ≤ 0.5 % | | 开入开出 | 27 路硬件开入可读、31 路开出可驱动;开入防抖可整定;双点遥信四态正确(00/01/10/11) | -| 录波 | 4000 Hz 采样;三类录波各生成一份 COMTRADE 且被第三方工具打开;配额 64/64/16 循环覆盖正确 | +| 录波 | **(二期口径;本波按 1 kHz / 8 路 / 单段 / 无 COMTRADE,对照表见 §9.1 第 10 行)**4000 Hz 采样;三类录波各生成一份 COMTRADE 且被第三方工具打开;配额 64/64/16 循环覆盖正确 | | 记录体系 | 13 类队列各有落点;每类容量达标;断电不丢已确认记录 | | 多规约 | 每种规约与对应主站/子站工具完成一次完整交互(连接 / 总召 / 变位 / 遥控 / 断链重连) | | 国密 | SM2 签名验签、SM3 摘要与标准测试向量一致 | diff --git a/docs/任务规划/README.md b/docs/任务规划/README.md index 38380629..ef8f2624 100644 --- a/docs/任务规划/README.md +++ b/docs/任务规划/README.md @@ -32,14 +32,15 @@ ## 1. UI 补齐(执行清单已于 2026-10-28 清空) -菜单树共 33 叶(节点行合计 42):按行内 #page= 实测**已绑 28 叶、未绑 5 叶**(2026-10-28 复核, -判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 5 叶因缺少开入硬件、 -录波模块或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)。 -**口径闭合**:未绑 5 = 待做 0(§1 清单已清空)+ 不做 5(遥信 3 / 手动录波 1 / SNTP 1);**42 是节点行数、不是叶数**(= 9 个菜单节点 + 33 叶)。 -已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 29(= 28 + 1 行示例)。 +菜单树共 34 叶(节点行合计 43):按行内 #page= 实测**已绑 30 叶、未绑 4 叶**(2026-10-29 复扫, +判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 4 叶因缺少开入硬件 +或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶 +已随录波道绑页**,「不做」清单里的录波那一项已销。 +**口径闭合**:未绑 4 = 待做 0(§1 清单已清空)+ 不做 4(遥信 3 / SNTP 1);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。 +已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 31(= 30 + 1 行示例)。 **UI 补齐执行清单已于 2026-10-28 清空**:记录 5 页(事故 / 操作 / 保护告警 / 保护启动 / 遥控)与 -运行报告页同日落地并绑页,内容页黄金参考 53 → 60 张(runreport 分 2 页,第 2 页单独拍一张;判据 = simulator/tests/ref/ 入库 shot_*.bin 实数,张数 = 页面个数 + 分页多拍的那一张)。 +运行报告页同日落地并绑页,内容页黄金参考 53 → 60 张(runreport 分 2 页,第 2 页单独拍一张;判据 = simulator/tests/ref/ 入库 shot_*.bin 实数,张数 = 页面个数 + 分页多拍的那一张;**该集合 2026-10-29 已扩至 66 张**,多出的是录波回放页 6 态)。 上板判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、RESTARTED、rst_cause=IWDG、conn_up +1) 已登记登记簿 §7「UI 六页真机判据」,六页文件头各留「台架待补」段;模拟器差距见 docs/测试说明/模拟器差距清单.md §2.3「开机账本 boot_stats」一行。 @@ -66,7 +67,7 @@ docs/测试说明/模拟器差距清单.md §2.3「开机账本 boot_stats」一 | **7** | 验证窗口事项 | 逢条件顺带 | CI runner、PC peer、CH340、负向故障注入条件到位时执行 | | **8** | 独立模块与暂缓项 | 当前不排 | 开入开出、SNTP、104 文件传输单独立项;RS-485 与内存优化按各自重启条件 | | **独立** | **屏板链路(对上屏板)** | **规划已出(未开工)** | 独立立项,插在任一顺位之间都能做,不占主序列;**端口已裁定(2026-10-28):屏板占 USART1,日志迁 RS-232 通道**,所以原来的 250 kbps 收发器阻塞已解除;剩下两条前置——**台架串口线挪到 CN1、日志口迁移**(见 docs/任务规划/屏板链路.md §0 与 §3 批 0);**软件可先用 PC + USB 转 TTL 台架验证,不必等屏板实物** | -| **独立** | **故障录波(1 kHz · 8 路原始码值 · 单通道回放)** | **设计定稿(代码一行未动)** | 用户裁定 2026-10-29 **录波先行**:数据面最小闭环 → 手动与保护启动触发 → 回放页,卡片判据见 docs/任务规划/故障录波.md,口径与设计见 docs/子系统设计/故障录波设计说明.md;**与 UI 道互斥**(菜单树与中文字库是整文件生成物,同刻只允许一条道写) | +| **独立** | **故障录波(1 kHz · 8 路原始码值 · 单通道回放)** | **代码面已交付(板级判据一轮未跑)** | 用户裁定 2026-10-29 **录波先行**:数据面最小闭环 → 手动与保护启动触发 → 回放页。三张卡的**代码侧判据已收口**(主机单测 100 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器字节级互验、手动录波与录波回放两叶已绑页、内容页黄金参考扩至 66 张);**没跑的是板级那一轮**(注 1 V / 50 Hz、FTP 拉回、与 `meas:` 行和 104 浮点遥测对表、段文件 CRC 不符必须拒收、回放页看真段)。卡片判据见 docs/任务规划/故障录波.md,口径与设计见 docs/子系统设计/故障录波设计说明.md §10;**与 UI 道互斥**(菜单树与中文字库是整文件生成物,同刻只允许一条道写)——本波按此串行执行:UI 道在数据面与接线冻结之后才动生成物 | ### 2.1 关键依赖 diff --git a/docs/任务规划/UI补齐.md b/docs/任务规划/UI补齐.md index a268f085..ef02e3d6 100644 --- a/docs/任务规划/UI补齐.md +++ b/docs/任务规划/UI补齐.md @@ -1,38 +1,41 @@ # 当前任务:UI 补齐 > **执行清单已清空(2026-10-28)**:记录 5 页与运行报告页已落地、绑页并纳入像素黄金回归。 +> **2026-10-29 录波 UI 道**:「手动录波」由不做转落地(绑操作页),新增「录波回放」叶(节点 43); +> 叶数口径同步为 43/34/30/4(下方逐项)。 > 本文件保留为 UI 补齐的**范围与判据口径**(§1 叶数、§4 每页统一动作、§5 不做的取舍); > 下一顺位见 docs/任务规划/README.md §2.0(换主线待用户点头),上板待复核项见 docs/任务规划/登记簿.md §7。 ## 1. 目标与范围 -菜单树共 **33 叶**(节点行合计 42,缩进即层级):按行内 `#page=` 实测**已绑 28 叶、未绑 5 叶**, -页面映射表 `UI_PAGE_MAP_COUNT` = 28(与已绑叶数一致)。未绑 5 叶 = §5 明确不做的 **5 叶** -(遥信 3 / 手动录波 1 / SNTP 1),与下面分类表逐项对得上;2026-10-28 记录 5 页与运行报告页 -落地后,原「需补配置或写入方」的 6 叶缺口清零并按销项删行(该行内容见本节末与 §3)。 +菜单树共 **34 叶**(节点行合计 43,缩进即层级):按行内 `#page=` 实测**已绑 30 叶、未绑 4 叶**, +页面映射表 `UI_PAGE_MAP_COUNT` = 30(与已绑叶数一致)。未绑 4 叶 = §5 明确不做的 **4 叶** +(遥信 3 / SNTP 1),与下面分类表逐项对得上;2026-10-29 录波 UI 道把「手动录波」从不做转为 +落地并新增「录波回放」叶,绑 +2(手动转绑 + 新叶)/ 叶 +1;2026-10-28 记录 5 页与运行报告页落地后, +原「需补配置或写入方」的 6 叶缺口清零并按销项删行(该行内容见本节末与 §3)。 | 分类 | 数量 | 当前怎么处理 | |---|---:|---| | 无开入硬件 | 3 | 不做:遥信类型、遥信防抖、双点遥信 | -| 缺独立模块或产品定义 | 2 | 不做:手动录波(2026-10-29 已随故障录波立项,落地前仍按未绑计)、SNTP 设置 | +| 缺独立模块或产品定义 | 1 | 不做:SNTP 设置(手动录波已于 2026-10-29 随故障录波 UI 道落地转绑定,不再列不做) | 现有基础:`ui_w_editor` / `ui_w_setlist` / `ui_page_action` 已就绪;`ui_data` 已扩到**四通道** (rt / par / info + 遥测死区与系数用的第四张注入表);像素黄金用例与字符集门禁已存在,内容页黄金 -现有 **60 张**(菜单 8 状态另计)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、记录 5 页与 +现有 **66 张**(菜单 8 状态另计;2026-10-29 录波 UI 道 +6 张:手动录波 3 态 + 录波回放 3 态)。**装置维护 3 页、遥测死区/系数 2 页、通讯设置页、记录 5 页与 运行报告页都是把这套基础接上真实消费者做出来的,不另发明一套机制。** 逐页形态见 `docs/UI设计/页面补齐规划.md`;数据契约见 `docs/UI设计/UI_数据供给设计说明.md`。本文件保留范围、完成判据与不做的取舍口径(执行清单已于 2026-10-28 清空)。 ### 叶数判定与复核(2026-10-28 复核) -1. **节点行** = `ui_menu_tree.txt` 里非空、非 `#` 开头的行(当前 **42** 行,含根「主界面」)。 +1. **节点行** = `ui_menu_tree.txt` 里非空、非 `#` 开头的行(当前 **43** 行,含根「主界面」)。 2. **缩进即层级**(每级 2 空格;Tab 会被 `gen_menu.py` 直接拒绝):某节点行之后再没有更深缩进的行 = **叶** - (当前 **33** 叶);有更深缩进后继者 = 菜单节点(当前 **9** 个)。等价于生成物口径 `ui_menu_tab_has_children(node) == false`。 -3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **28** 叶);不带的 = 未绑(当前 **5** 叶)。 -4. **闭合**:`未绑 = 待做 + 不做` —— 当前 待做 **0**(§2 清单已清空)+ 不做 **5**(遥信 3 / 手动录波 1 / SNTP 1)= **5**, + (当前 **34** 叶);有更深缩进后继者 = 菜单节点(当前 **9** 个)。等价于生成物口径 `ui_menu_tab_has_children(node) == false`。 +3. **绑定**:叶行行尾带 ` #page=` = 已绑(当前 **30** 叶);不带的 = 未绑(当前 **4** 叶)。 +4. **闭合**:`未绑 = 待做 + 不做` —— 当前待做 **0**(§2 清单已清空)+ 不做 **4**(遥信 3 / SNTP 1)= **4**, 与上面的分类表、§5「当前不做」表逐项对得上。 5. **别拿 `grep -c '#page='` 当已绑叶数**:2026-10-28 实测它得 **29**,比真值多 1 —— 文件头第 5 行是规则说明里的 - ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的 28 才等于生成物 `UI_PAGE_MAP_COUNT`。 + ` #page=<模块名>` 示例行(不是节点行),必须先剔注释/示例行;剔完的 30 才等于生成物 `UI_PAGE_MAP_COUNT`。 复核命令(复用生成器自己的解析器 `tools/menu_gen/gen_menu.py` 的 `parse_tree`,不另写一套计数逻辑,免得与生成物脱钩): @@ -40,16 +43,16 @@ & '.venv\Scripts\python.exe' -c "import io,sys; sys.path.insert(0,'tools/menu_gen'); import gen_menu as g; n,_=g.parse_tree(io.open('ui_menu_tree.txt',encoding='utf-8').read()); leaf=[i for i,x in enumerate(n) if all(y[0]!=i for y in n)]; b=sum(1 for i in leaf if n[i][4]); print('nodes=%d leaves=%d bound=%d unbound=%d' % (len(n),len(leaf),b,len(leaf)-b))" ``` -输出 `nodes=42 leaves=33 bound=28 unbound=5`;交叉核对生成物:`ui_menu_tab.h` 的 `UI_MENU_TAB_COUNT` = 42(节点行数)、 -`ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` = 28(已绑叶数)——一对 42、一对 28,与上面逐项对得上。 +输出 `nodes=43 leaves=34 bound=30 unbound=4`(2026-10-29 录波 UI 道落地后实测);交叉核对生成物:`ui_menu_tab.h` 的 `UI_MENU_TAB_COUNT` = 43(节点行数)、 +`ui_page_registry.h` 的 `UI_PAGE_MAP_COUNT` = 30(已绑叶数)——一对 43、一对 30,与上面逐项对得上。 三个已踩过的错算法(每种都同时给**当时**与**2026-10-28 重跑**的产物——错法不会自己失效,只是错数跟着已绑叶数一起漂): **把 42 个节点行当叶子**(多算 9 个菜单节点:当时已绑 21 叶 → 得「未绑 21」,2026-10-28 同一错法得「未绑 14」= 42 − 28); **`grep -c '#page='` 当时得 23**(当时已绑 22 叶,多的那 1 正是文件头注释第 5 行的 `#page=<模块名>` 示例字样,必须先剔注释行; -2026-10-28 同一命令得 29 = 已绑 28 + 该示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, +2026-10-28 同一命令得 29 = 当时已绑 28 + 该示例行 1;2026-10-29 现值 31 = 已绑 30 + 示例行 1);**按叶子名去重得 29**(`参数`/`定值`/`控制字`/`软压板` 在「整定」「查看」下各一份, 4 对同名不能合并;2026-10-28 重跑仍是 29 = 33 叶 − 4 对同名)。 (21 / 23 两个错数分属 2026-10-27「已绑 21 叶」与「已绑 22 叶」两个时点的实测,只用来说明错法,不代表现值; -去重 29 与上面 grep 现值 29 **只是撞号**:一个是「合并同名叶」、一个是「不剔注释行」,都与真值 28 无关。) +去重 29 与上面 grep 现值 29 **只是撞号**:一个是「合并同名叶」、一个是「不剔注释行」,都与真值 30(2026-10-29 现值)无关。) --- @@ -76,6 +79,7 @@ |---|---|---| | 通讯设置 | 页面与六态黄金已随 `728d6067` 入库;`[comm]` 段与端口真源在 `App/protocol/iec104/iec104_comm.{c,h}` | 「改后重启即生效」真机判据、配置段数与键数上限核对(台架窗口统一补做并留证) | | 记录 5 页 | 五类记录 ID / 文件 / 写入点已齐(docs/任务规划/登记簿.md §7 与 App/log/README.md §五类记录);reader 注入两端已映射 7 个记录源 | 页面已交付;只剩真机判据(真记录翻页、掉电保持、清除后归零),见 docs/任务规划/登记簿.md §7 | +| 录波两页 | 手动录波操作页 + 录波回放页已落地绑页(2026-10-29 录波 UI 道);数据源 = `App/algo/wave_ring`/`wave_store` 真实现经 `ui_data_wave_ops_t` 注入,回放读盘走 `vol_lock` try 锁小窗口 | 页面已交付;真机判据归台架(真实触发落盘的段号连续性、削徽标真值、切段切道实测毫秒与卷锁忙「本轮无段」行为,见 `docs/子系统设计/故障录波设计说明.md` §10 台架判据) | | 运行报告 | boot_stats(SD2506 用户 SRAM 上电次数 + 本次复位原因快照,道G task-12)、iec104_serv 四个只读观测计数、现成 uptime_s | 页面已交付;**跨开机累计不做**(用户裁定只报本次开机)。真机待复核 SRAM 自增与四态判定,见 docs/测试说明/模拟器差距清单.md §2.3 | --- @@ -108,7 +112,6 @@ UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时 | 项目 | 不做原因 | 重开条件 | |---|---|---| | 遥信类型 / 防抖 / 双点 3 页 | V0.1 无开入硬件,配置没有消费者 | 开入固件立项后,与开入开出一起做 | -| 手动录波 | 录波模块尚未落地;旧口径还挂着「工程量口径未闭环」这道前置,**已被「录波存原始码值」解掉**(docs/子系统设计/故障录波设计说明.md §3.2) | **重开条件已满足**(2026-10-29 立项):随 docs/任务规划/故障录波.md 的「手动触发与保护启动接线」那张卡一起做 | | SNTP 设置 | 当前使用 8888 私有对时协议,SNTP 链路与产品口径均未建立 | SNTP 客户端立项 | | net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 | | RTC 时基统一 | 两套写入口径并存(面板与 8888 写本地钟面、104 写 UTC),恒差 8 小时;统一任一侧都要动 104 上行语义或 UI 判据 | 用户裁定时基方向后,作为跨模块变更单独立项 | diff --git a/docs/任务规划/故障录波.md b/docs/任务规划/故障录波.md index caea3902..fdfcc854 100644 --- a/docs/任务规划/故障录波.md +++ b/docs/任务规划/故障录波.md @@ -7,7 +7,7 @@ ## 1. 现状一句话 -全仓**没有任何录波代码**:`App/` 下 grep 无实现,`Drivers/BSP/UI/ui_menu_tab.c:41` 那条「手动录波」叶子(菜单第 26 叶)**至今未绑内容页**;产品需求把「录波文件存储」「文件传输(录波)」两行标为未实现/未开始;旧口径把录波挂在「工程量口径实测闭环之后」,本波用**原始码值**解掉这个依赖(`docs/子系统设计/故障录波设计说明.md` §3.2)。 +**数据面与接线已在仓内,板级判据一轮未跑**(诚实口径,2026-10-29 更新):录波环与段格式编解码在 `App/algo/wave_ring.c` 与 `App/algo/wave_store.c`,压环与落盘与保护启动接线、以及两条 UI 页的代码侧判据已过(三套主机单测 100 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 解码器两份独立实现字节级互验通过;「手动录波」第 26 叶与「录波回放」两叶均已绑页,`UI_PAGE_MAP_COUNT` = 30,内容页黄金参考 60 → 66 张含录波 6 态)。**没做完的是板级那一轮**:注 1 V / 50 Hz、FTP 拉回、与 `meas:` 行和 104 浮点 YC 对表、CRC 不符必须拒收的实测、真机上看回放页放一段真波;产品需求把「录波文件存储」「文件传输(录波)」两行的更新排在台架轮次之后。旧口径把录波挂在「工程量口径实测闭环之后」,本波用**原始码值**解掉了这个依赖(`docs/子系统设计/故障录波设计说明.md` §3.2)。 三条已裁定口径(用户裁定 2026-10-29,理由见`docs/子系统设计/故障录波设计说明.md` §1.2 与 §2): @@ -23,11 +23,13 @@ | 卡 | 内容 | 退出判据 | 资源 | 依赖 | |---|---|---|---|---| -| **录波数据面最小闭环** | 新增专用录波环与段格式编解码(`docs/子系统设计/故障录波设计说明.md` §5 到 §7);bench 构建定周期自抓;FTP 拉回;仓内新增一个纯标准库 PC 脚本读段、画图、重算全周傅氏 | 注入 1 V / 50 Hz 正弦 → 拉回段文件 → PC 脚本量出周波数、幅值、极性与注入一致;削顶路数与头里 `clip_mask` 一致;**PC 重算的基波有效值与板上 `meas:` 行、104 浮点 YC 逐个对表通过**;段文件 CRC 不符时脚本必须拒绝而不是画出东西 | 🟡+🟢(需信号发生器与板,不需标准源) | 无(可与固件其他道并行;独占构建锁) | -| **手动触发与保护启动接线** | 触发状态机接两路真实来路:UI「手动录波」操作页(第 26 叶绑页,形态沿用已规划好的操作页确认模式)+ `App/algo/prot_elm.c` 的 `picked` 上升沿 | 面板按一次出一段;造一次过流,段号与该时刻 `/log/start.log` 事件能对齐;冷却期内二次触发只加 `ovf` 计数、不重录;冻结窗口内 104 上送与保护判据不受影响(**这条是"丢录波样本不丢采集"的验收**) | 🟡+🟢+UI 道(菜单树与字库是整文件生成物,须单写者) | 上一张卡 | -| **现场单通道回放页** | 新增「录波回放」叶子与内容页:单通道 2 周波 40 点、阶梯线、0 线与峰值标注、单位徽标、削顶标记、空态文案(`docs/子系统设计/故障录波设计说明.md` §8) | `ui-verify` 五步全过;回放页入 `ui-golden` 黄金参考;远端看图能拍到此页(只作留证,不当波形通路);**空态与 CRC 失败态绝不允许画成一条直线**;菜单叶数与字库生成物同步重跑且 `generated-fresh` 一致 | 🟢+UI 道(模拟器与黄金参考) | 上一张卡 | +| **录波数据面最小闭环**〔代码侧完成 / 板级待台架〕 | 新增专用录波环与段格式编解码(`docs/子系统设计/故障录波设计说明.md` §5 到 §7);bench 构建定周期自抓;FTP 拉回;仓内新增一个纯标准库 PC 脚本读段、画图、重算全周傅氏 | **代码侧判据已完成**:三套主机单测 100 / 170 / 110 全 rc=0(环 100、段格式 170、触发状态机 110)、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现**字节级互验通过**(同一份字节两边同解)。**下列判据要板子与台架,一轮未跑**:注入 1 V / 50 Hz 正弦 → 拉回段文件 → PC 脚本量出周波数、幅值、极性与注入一致(**绝对相位不可比**,段内相位以点 0 为参考、触发瞬间在段内点 159,与发生器无相位锁定关系,可核对的是相对相位与极性——见 docs/子系统设计/故障录波设计说明.md §4.2 与 §10);钉轨路数与头里 `clip_mask` 的合格判据是「头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释」(`clip_mask` 按来源派生、非本段实测,V0.2 后该位恒 0、这条退化为只查实测);**强观察项**:V0.1 下这五路做交流注入时反而没钉轨 → 先查接线或该路未驱动,属告警面不属失败面;**PC 重算的基波有效值与板上 `meas:` 行、104 浮点 YC 逐个对表通过**;段文件 CRC 不符时脚本必须拒绝而不是画出东西 | 🟡+🟢(需信号发生器与板,不需标准源) | 无(可与固件其他道并行;独占构建锁) | +| **手动触发与保护启动接线**〔代码侧完成 / 板级待台架〕 | 触发状态机接两路真实来路:UI「手动录波」操作页(第 26 叶绑页,形态沿用已规划好的操作页确认模式)+ 保护启动的元件 picked 上升沿(落点 `App/task/protect_task.c` 的 `s_log_record_edge()`,不是判据核本身) | **代码侧已完成**:保护启动来路与并发语义(同拍多请求只录一段、冷却期内二次触发只加 `ovf`、冻结窗口只丢录波样本不丢采集)已在主机单测里钉住,双端编译 0 错误 0 警告。**下列属板级未完成**(第 26 叶已于 2026-10-29 绑页,模拟器侧手动触发路径已有黄金用例;剩下的必须在真机面板上按):面板按一次出一段;造一次过流,段号与该时刻 `/log/start.log` 事件能对齐;冷却期内二次触发只加 `ovf` 计数、不重录(**同时看 `armed` 与那一刻 `/log/start.log` 的时刻,不要只盯 `ovf` 数字**:这两个计数是跨上下文自增、并发拒绝时可能少计一次,容差见 docs/子系统设计/故障录波设计说明.md §9);冻结窗口内 104 上送与保护判据不受影响(**这条是"丢录波样本不丢采集"的验收**) | 🟡+🟢+UI 道(菜单树与字库是整文件生成物,须单写者) | 上一张卡 | +| **现场单通道回放页**〔代码侧完成 / 板级待台架〕 | 新增「录波回放」叶子与内容页:单通道 2 周波 40 点、阶梯线、0 线与峰值标注、单位徽标、削顶标记、空态文案(`docs/子系统设计/故障录波设计说明.md` §8) | **代码侧判据已完成**:两叶绑页(`UI_PAGE_MAP_COUNT` = 30)、`ui-golden` 内容页黄金参考 60 → 66 张(录波 6 态:手动录波 3 态 + 录波回放 3 态),任务树重建模拟器后同一批 66 张连跑三次逐字节一致(页面池水位 5,448 B)、菜单侧 8 状态连跑三次一致(5,368 B),`generated-fresh` 退出码 0(菜单树与中文字库生成物与源一致),双端编译 0 错误 0 警告。**下列判据要板子与台架,一轮未跑**:真机上放一段真波(不是模拟器合成节拍)、远端看图能拍到此页(只作留证,不当波形通路)、**空态与 CRC 失败态绝不允许画成一条直线**要在真机上用坏段复现一次 | 🟢+🟡+UI 道(模拟器与黄金参考) | 上一张卡 | | 二期(不在本波) | 4 kHz 全速与 80 点/周波、突变量启动、COMTRADE、104 文件传输、分段与参考机形态(录波 65 通道、多类型多段) | 不在本波验收面内;重开条件见 §4 | 🔴/独立立项 | — | +**落盘道的失败留痕名(单板上一律一行 `[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`**。最后一条是 docs/子系统设计/故障录波设计说明.md §5.2 第四条陷阱的处置留痕(写完关档后复检环侧身份不过 → 判废本段、`err` 加一、段号不推进),**代码与主机单测面已有、板级未测**;另有一条非失败的慢段留痕 `[WAVE][WARN] seg held <毫秒> ms (idx=…)`(`App/task/default_task.c:831`)——搬得慢不是错,真失败才走 `seg drop`。 + **为什么录波排在信号发生器动态验证之前**(用户裁定):动态验证真正缺的不是信号源,是**样本出口**——录波段文件一旦有了,动态验证就从「比对几个派生标量」升级成「PC 独立重算 vs 板上读数」的对表,同一套台架接线两次都用得上,不必再造一条串口通路。`docs/子系统设计/故障录波设计说明.md` §11 记了因此而变的三处口径。 --- diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 384f7959..037e6e1f 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -325,7 +325,8 @@ typedef struct { | 串口每 10 s 心跳(`App/task/adc_task.c` 心跳块) | `adc:`(9 路码值)、`cal:`(三路 mV + `vl`/`cl`/`rail`/`edge` 位图)、`phasor(hb)`(逐实例 `Re` / `Im` / `rms`)、`freq(hb)`、`meas:`(快照轨) | MicroLIB 无 `%f`,全是定点:`phasor` ×0.1(**1 V 注入只剩 10 格**)、`freq` ×0.01 Hz、`cal:` / `meas:` ×1000(1 mV)。10 s 一行的单点瞬时值对 50 Hz 等价于随机相位抽样 | | 104 YC(现成 PC 主站 `test/host/iec104_master.py`) | `0x4011`~`0x4018` 八个电气量 + `0x401D` 频率,**浮点无量化**,还带品质 | UB/UC/IB/IC/IN 恒 IV、值清零 → 那五路只能回读串口码值域 | | UI 交流量页 | `Ua` / `U0` / `Ia` / `Ia2` / `Ia5` / `Hz` / 标定徽标 共 7 行 | 160×160 单色;`--` **同时**表示"没数"和"有数但不可信" → 不能当判据,只能当旁证 | -| **板上没有的(本波现状)** | 把窗里 20 / 40 个点导出来的任何出口:无命令控制台、无 dump、当前还不落文件 | 所以现在只能靠**派生标量**证;要看形状就拿示波器直接看注入点(只对照发生器,别拿它当判据)。**这条限制是临时的**:故障录波已立项,段文件就是现成的样本出口,届时判据升级为「PC 独立重算 vs 板上读数」对表(见 `docs/子系统设计/故障录波设计说明.md` §11) | +| **段文件样本出口(代码侧已落地,2026-10-29)** | 每次触发一段:`/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见),8 路 × 320 点原始码值加封存瞬间的标定与品质快照;PC 侧 `test/host/wave_read.py` 提供 dump / fft / shape / plot 四个子命令(bench 工具,纯标准库,自带 `--selftest`) | 「比对派生标量」已可升级成「PC 独立重算 vs 板上读数」对表(判据与执行步骤见 `docs/测试说明/ADC前端注入验证流程.md` §4,格式属主见 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3)。**但板级那一轮还没跑**(注 1 V / 50 Hz、与 `meas:` 与 104 YC 对表、CRC 不符必须拒收的实测,见 `docs/任务规划/故障录波.md` 卡片判据);示波器仍旧只作旁证、不当判据。段内点序:触发瞬间落在段内点 159(预触发 160 点的最后一点),段的绝对相位与发生器无锁定关系,可核对的是相对相位与极性 | +| **仍然没有的(2026-10-29 更新)** | 没有命令控制台、也没有「把窗里 20 / 40 个点导出来」的定点 dump 子命令——**但样本出口本身已成立**(上一行的段文件:一次 320 点 / 8 路,比原来规划的 20 / 40 点窗更宽) | 现场取回一律走现成 FTP(21 端口),形状就地看回放页单通道、或在 PC 侧用同一脚本出图;示波器只对照发生器、不当判据。**判据已升级为「PC 独立重算 vs 板上读数」对表**,对表的执行轮次待台架(见 `docs/子系统设计/故障录波设计说明.md` §11) | #### 4.6.4 接法与红线(照抄既有红线,不新发明) @@ -443,7 +444,7 @@ flowchart TD | THD / 谐波方和根真有效值、快照 4 点平均 | 挂 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」一起做;快照保持"基波有效值 + 直取相量"口径(§4.5) | | **谐波轨的消费出口**(C04 / C05 目前无人消费) | D6 只证"算得对";接线到判据或上送属谐波轨立项范围,本波不动 | | 端子级额定电压、绝缘耐压、温升 | 要标准源 / 型式试验条件;本波所有注入都在 `U5` 排针侧 ±2 V 以内 | -| 录波(原始码值段文件) | **已独立立项**(2026-10-29):`docs/任务规划/故障录波.md`;本卡「动态波形验证」的样本出口将改由录波段文件承担,排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11) | +| 录波(原始码值段文件) | **已交付代码面**(2026-10-29 立项并当日落地):`docs/任务规划/故障录波.md`;本卡「动态波形验证」的样本出口**已**由录波段文件承担(§4.6.3 出口表已改),排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11);**没跑的是板级那一轮**(§10) | | 104 文件传输(规约侧读文件) | 仍独立待排;现场与台架取回一律走现成 FTP(21 端口),规约侧文件服务等送检或第三方交接需求再议 | | 等周波调频 | 与标定正交、无标准源时判据不可达,见 `docs/算法/继电保护功能复现规划.md` §3.1 | | 功率 / 相位跨相比较 / 功率因数 / 对称分量 | **V0.1 硬件禁止**:跨转换器无同一时刻性,`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行 | @@ -461,7 +462,7 @@ flowchart TD |---|---| | **`docs/测试说明/ADC前端注入验证流程.md`** | **执行步骤的属主**。本文只写"证到哪、判据是什么、为什么这么排";怎么接表笔、读哪一行、填哪张表一律在那篇里。动态验证的**步骤**同样落那篇(新开"交流"节),本文 §4.6 只留判据 | | **通道映射说明** | `docs/硬件平台/ADC通道映射说明.md` §7.1(①②③④ 链路分段)与 §8.1(静态六项实测)是**量纲与实测记录的属主**;本文 §2.1 / §2.6 是它的**消费者**,不另立口径,动态实测结论也回写该文件 | -| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**:录波落地后判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表,动态验证的排序也因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见该文 §11) | +| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**,数据面代码侧已落地(2026-10-29:环与段格式在 `App/algo/wave_ring.c` / `App/algo/wave_store.c`,PC 侧 `test/host/wave_read.py`);判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表的**执行轮次待台架**,动态验证的排序已因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见 docs/子系统设计/故障录波设计说明.md §11) | | **`docs/算法/采集与算法层设计说明.md`** | 算法与判据正文(全周傅氏、测频、保护元件);本文只规定"这些算法的信度到什么程度" | | **`docs/子系统设计/IEC104上行设计说明.md`** | 动态验证的**读数出口**(`0x4011`~`0x4018` + `0x401D`);送检验收索引在 `docs/任务规划/登记簿.md` §7 | | **送检验收** | 标准源送检依赖本文「两点标定与精度验收」;顺序不可倒(先变比 → 再标定 → 才解卡 → 才送检,§6) | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index e849d09c..8f6fec22 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 264,580 B (258.38 KB) | 1,048,576 B (1024 KB) | 25.2% | -| RW_IRAM1 | 81,536 B (79.62 KB) | 131,072 B (128 KB) | 62.2% | +| ER_IROM1 | 272,336 B (265.95 KB) | 1,048,576 B (1024 KB) | 26.0% | +| RW_IRAM1 | 89,936 B (87.83 KB) | 131,072 B (128 KB) | 68.6% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **266,456 B (260.2 KB)** | 1,048,576 B (1024 KB) | **25.4%** | +| **Flash (ROM 合计)** | **274,308 B (267.9 KB)** | 1,048,576 B (1024 KB) | **26.2%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 54b8cf8d..a8494ebf 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,19 +15,19 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 264,580 B | 1,048,576 B | 25.2% | 参考 | +| ER_IROM1 (Flash) | 272,336 B | 1,048,576 B | 26.0% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 81,536 B | 131,072 B | 62.2% | PASS | +| RW_IRAM1 | 89,936 B | 131,072 B | 68.6% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 34,656 B | 16 KB | ⚠ 超 17.8 KB | +| UI | 36,428 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 15,552 B | 8 KB | ⚠ 超 7.2 KB | -| 其它 | 10,553 B | — | 参考 | +| 应用 | 16,888 B | 8 KB | ⚠ 超 8.5 KB | +| 其它 | 15,849 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— @@ -64,7 +64,9 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 `heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。 - **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS (静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")—— - 属已评审的"以空间换栈安全",差额在此如实体现。 + 属已评审的"以空间换栈安全",差额在此如实体现。2026-10-29 故障录波落盘道又把 `default_task.o` + 推高 1,204 B(该行现值 16,888 B);**录波环本体 5,300 B 不计入本行**——`subsystem_of()` + 没有 `wave_` 前缀,它落在「其它」,要读全貌看 `docs/内存管理/内存内容分配策略.md` §6 的 per-file 台账。 ## 3. CCM 分配台账(谁能用 CCM) @@ -96,3 +98,4 @@ UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名 | 2026-10-12 | 建档:三件套落地(本台账 + `scripts/ram_baseline.py` + `scripts/ram_baseline.tsv` + pre-commit `mem-budget` 钩子) | 登记簿 N-27 前半 | | 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` 语义取数与有效性参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 迁 CCM 后半链注记 | | 2026-10-12 | **任务栈 +1536 B(堆内用量,配额 56 KB 不变)**:defaultTask 1024→1280 words(+1024 B)、timeSyncTask 512→640 words(+512 B);产品任务栈合计 37,632→39,168 B | 登记簿 N-33 栈收口:B7 定值持久化把 defaultTask 静态链推到 3016 B(余 1080 < 30% 线)、timeSyncTask 链 1264 B(余 784 < ISR 底线 1024);B6 台架背书 min_ever=10000 → 抬后实测 `heap free=16400 / min_ever=8464`(与预算 −1536 完全吻合,>3072 告警线);`stack_report` 9 任务 0 FAIL + `@test` PASS,见 `RAM使用分析与优化方案.md` §1.4 | +| 2026-10-29 | **故障录波数据面入册**:新增对象 `wave_ring.o` 5,300 B(ZI 5,256 + RW 44,落 SRAM1、CCM 增量 0);`default_task.o` 静态 2,736 → 3,940 B(+1,204 B,落盘道 `FIL` 528 + 分片缓冲 256 + 段头副本 136 + 段路径 17 + 打包上下文与 `DIR`/`FILINFO`);`RW_IRAM1` 81,536 → 89,936 B(62.2 % → 68.6 %,仍远低于 92 % 门禁);`RW_CCM` 三档恒 57,376 B | 决定人 lead。实测数字出自 lead 的权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv);预估约 5.9 KB vs 实测数据面 +6,760 B 的差额构成与三档对照见 `docs/内存管理/内存内容分配策略.md` §6;**未抬栈**(`scripts/stack_report.py` 九任务全 PASS),判据出处 `docs/子系统设计/故障录波设计说明.md` §7 | diff --git a/docs/内存管理/内存内容分配策略.md b/docs/内存管理/内存内容分配策略.md index ec46e469..70f88ad8 100644 --- a/docs/内存管理/内存内容分配策略.md +++ b/docs/内存管理/内存内容分配策略.md @@ -110,17 +110,35 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两 触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。 流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。 -## 6. 当前容量台账(2026-10-12,产品构建;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) +## 6. 当前容量台账(2026-10-29,产品构建 task/wave-record 分支终值;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势) | 区域 | 总量 | 已用 | 空闲 | 驻留内容 | |---|---|---|---|---| -| RW_IRAM1(SRAM) | 131,072B (128KB) | 95,816B (~93.6KB, 73.1%) | 35,256B (~34.4KB) | 全部静态(除堆),含 LVGL 集合 53,805B | -| RW_CCM | 65,536B (64KB) | 57,376B (56KB, 87.9%) | 8,160B (~8KB) | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容) | +| RW_IRAM1(SRAM) | 131,072B (128KB) | 89,936B (87.83KB, 68.6%) | 41,136B (~40.2KB) | 全部静态(除堆),含录波环 `wave_ring.o` 5,300B 与 LVGL 池 16,384B(`lv_mem_core_builtin.o` 实测) | +| RW_CCM | 65,280B(64KB 减 256B `UNINIT_CCM` 尾,与 `.sct` 一致) | 57,376B (56.03KB, 87.9%) | 7,904B | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容);256B 尾区现用 48B | + +数字身份:`RW_IRAM1` / `RW_CCM` / `ER_IROM1` 三行取自 lead 2026-10-29 权威构建(本工作树 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`,0 错误 0 警告,`scripts/ram_baseline.py --update` 同批入 tsv)。受管快照区在 `docs/内存管理/RAM使用分析与优化方案.md`,子系统配额表在 `docs/内存管理/RAM预算台账.md`,两处由脚本渲染、本卡不手改。 LVGL 集合明细(均在 SRAM):LVGL 池 16,384B(`lv_mem_core_builtin.o`,`LV_MEM_ADR=0` 内部静态)+ `g_ui_draw_buf` 3,232B(3200+32 保护带)+ `g_ui_fbuf` 3,200B + `g_tx` 3,209B + `g_lcdmem_data` 3,204B。 产品构建比套件构建少 testTask/测试缓冲(约 40KB 级),SRAM 余量更大。数字以 map 实测为准,布局变更后必须重测并更新本表。 +录波支路静态(2026-10-29 入册,R6 要求;**全部落 RW_IRAM1、无 section 属性,CCM 增量 0**): + +| 归属对象 | 增量 | 构成 | +|---|---|---| +| `wave_ring.o`(`App/algo/wave_ring.c`) | 5,300 B(ZI 5,256 + RW 44) | 录波环 int16×320 点×8 路 = 5,120 B 主体 + 已封存段头副本 136 B + 状态/索引/观测计数;map 比代码侧手算多 2 B,是链接器对齐 | +| `default_task.o`(落盘道,函数内 `static`) | +1,204 B(2,736 → 3,940,同 tsv 的 RW+ZI 口径) | `FIL` 528 B(`FF_FS_TINY=0`,刻意不上栈)+ 分片搬运缓冲 256 B + 段头副本 136 B + 段路径 17 B + 打包上下文/`DIR`/`FILINFO`/状态字节约 267 B(对象级增量,未逐项拆分实测) | +| `adc_task.o` 及其余数据面 | 约 256 B | 8 路语义原码暂存与封存瞬间交棒结构(`App/task/adc_task.c`);与上两行相加 = 数据面实测 +6,760 B | +| UI 道:`ui_page_wave_play.o` 92 B、`ui_page_wave_manual.o` 28 B、`ui_task.o`、`ui_data.o` | 区级合计 +1,640 B | 两个新页面模块按 map 计 120 B;`ui_task.o`/`ui_data.o` 的增量拆分需要本波之前的 per-file 快照,本卡没有 → **只记区级增量,不猜分项** | + +- **三档对照(SRAM1)**:81,536 B / 62.2 %(`scripts/ram_baseline.tsv` 2026-10-28 行)→ 88,296 B(录波数据面终值)→ **89,936 B / 68.6 %**(含 UI 道终值),累计 **+8,400 B**;`RW_CCM` 三档恒 57,376 B —— 设计文档 `docs/子系统设计/故障录波设计说明.md` §7 的「不进 CCM」落位达成,也没有挤动 CCM。 +- **预估 vs 实测都要留**:设计文档 §7 原估静态增量约 5.9 KB(环 5,120 + 索引状态 32 + 双缓冲 512 + 读窗 256),实测数据面 +6,760 B。差额构成:`FIL` 528 B、`DIR`/`FILINFO`、段头副本 136 B、打包上下文(内含一份 136 B 段头)原估未记账;反向一项——落盘实际是**单缓冲 256 B**,不是预估的 512 B 双缓冲。只写实测会让下一次估算照着 6.7 KB 走、看不到原估漏了哪几项。 +- **Flash**:`ER_IROM1` 264,580 → 270,248(数据面 +5,668 B)→ 272,300(UI 道 +2,052 B)→ 272,360(落盘判废复检 +60 B)→ **272,336 B**(cppcheck 返工:把变量声明搬进最小作用域,纯搬声明 −24 B),累计 **+7,756 B**、占 26.0 %;`Total ROM` 274,308 B(26.2 %)、`Code` 243,552 B。RAM 三项(RW 1,972 / ZI 145,388 / RW_IRAM1 89,936 B / 68.6 %)与 CCM 57,376 B / 87.9 % 均未变。 +- **栈:9 任务全 PASS、未抬栈**(`scripts/stack_report.py` 静态最坏链)——adcTask 1,424 B(余 1,136 B / 44 %)、defaultTask 3,040 B 不变、protectTask 880 B、uiTask 1,336 B(余 15,048 B / 91 %)。R6 三处同步里 `AGENTS.md` 栈表这一处的结论是**核对过、数值未变**。 +- **为什么比上一版低**:本表 2026-10-12 版记 95,816 B / 73.1 %,高于今天的 89,936 B。原因是此后 LVGL 内部池由 40 KB 缩到 16 KB(`docs/内存管理/RAM预算台账.md` §3「缩池 ✅ 2026-10-15」行;`scripts/ram_baseline.tsv` 2026-10-12 与 2026-10-24 两行的 `top_rwzi` 列可见 `lv_mem_core_builtin.o` 40960 → 16384)——**不是本波回收了什么**。 + +- **子系统归集的坑(本卡实测,值得记一笔)**:`scripts/ram_baseline.py` 的 `subsystem_of()` 按对象名前缀首匹配,**表里没有 `wave_` 前缀** → 录波环 `wave_ring.o` 5,300 B 落进 **「其它」(15,853 B,参考值、无配额)**,配额 8 KB 的「应用」行看不见它;落盘道 `default_task.o`(+1,204 B)与 `adc_task.o` 才计入「应用」(现值 16,888 B,本波前该行已在配额外,成因见 `docs/内存管理/RAM预算台账.md` §2「应用 ⚠ 小幅超」条)。结论:**别拿子系统表当录波预算的判据**,要按本节 per-file 台账读。要让配额表真看见录波支路,得给 `subsystem_of()` 加 `wave_` 前缀——那是脚本改动,本卡不动,已报 lead。 ## 7. 布局变更验证阶梯(强制执行) 任何 scatter / CCM / 堆大小变更后,依次通过以下全部关卡方可认定生效: diff --git a/docs/子系统设计/故障录波设计说明.md b/docs/子系统设计/故障录波设计说明.md index 2d1917a1..046f4822 100644 --- a/docs/子系统设计/故障录波设计说明.md +++ b/docs/子系统设计/故障录波设计说明.md @@ -1,8 +1,8 @@ # 故障录波设计说明(1 kHz · 8 路原始码值 · 现场单通道回放) -> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(拟新增 `App/algo/wave_ring.c`)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。 +> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(`App/algo/wave_ring.c`,已落地)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。 > **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**排期与卡片判据在 `docs/任务规划/故障录波.md`**,本文不写排期。 -> **状态**:P0 设计定稿(2026-10-29),**代码一行未动**。三条已裁定口径见 §1.2。 +> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 和 104 浮点 YC 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在 docs/任务规划/故障录波.md 各卡的板级段。三条已裁定口径见 §1.2。 > **相关文档**: > `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它) > `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属) @@ -106,16 +106,19 @@ | 触发 | 来源 | 本波 | |---|---|---| -| **手动** | UI「手动录波」操作页(该叶子早已在菜单里:`ui_menu_tree.txt` 装置维护组,现未绑页) | 做 | -| **保护启动** | `App/algo/prot_elm.c` 的 `picked` 上升沿(元件启动),与 `/log/start.log` 同源,避免两处各自判定 | 做(复用事件点,不新增判据) | +| **手动** | 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 @@ -150,13 +153,14 @@ flowchart TD | **uiTask**(回放页) | 只读窗口取段数据 → 自绘(与 `App/log/soe_store.c` 读侧同构) | 不写文件 | | **netTask / lftpd** | 把 /wave 暴露给 FTP 取回 | 不在 netTask 里读录波环 | -### 5.2 三条已知陷阱,本波必须绕开 +### 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) @@ -190,16 +194,44 @@ flowchart TD | 14 | t0_ms | 4 | 段首相对上电时刻;配合墙钟还原绝对时刻 | | 18 | wall | 4 | 对时后的 epoch 秒(未对时为 0) | | 22 | eng | 1 | 封存瞬间的折算档位:0 引脚域、1 二次值、2 一次值 | -| 23 | flags_seq | 1 | 有效性位图(引 `adc_cal_flags()` 口径) | -| 24 | clip_mask | 2 | 削顶归属(V0.1 恒 0xE6,见 `docs/任务规划/数据可信.md` §2.7) | +| 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 + LSB(µV) 4 + k 2(定点)+ b 2(定点) | -| — | CRC16(头) | 2 | 与屏板链路同一张 CRC-16/MODBUS 表,不新造 | +| 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 | 长度为 0 或 CRC 不符 → 整段作废,页面必须显示「该段不可读」 | +| — | 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 是硬前置) @@ -213,12 +245,24 @@ flowchart TD | **合计静态增量** | — | **约 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 也**没有余量**给它; +- **落位 = 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 一屏画什么 @@ -261,8 +305,9 @@ flowchart TD 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` 文件头「观测计数的已知容差」段; - 只在原先静默的黑洞处补锚点,别重复已有步骤——每条 DBG 日志约 90 B Flash 加 216 B 栈(`AGENTS.md` 的日志与工具链节); -- 该行落地时同步进 `docs/测试说明/ADC前端注入验证流程.md` 的读数出口表。 +- 该行已同步进 `docs/测试说明/ADC前端注入验证流程.md` 的读数出口表(2026-10-29;同一张表还登记了段文件出口与 PC 侧读段工具)。 --- @@ -271,10 +316,13 @@ flowchart TD | 层 | 内容 | |---|---| | 主机侧纯逻辑单测(新增,进 `test/host/`) | ① 环:预触发与后触发点数、冻结与解冻、冷却丢弃计数;② 段格式编解码往返,含 CRC 不符、零长度、字节数不符三条失败路径;③ 触发状态机三条来路(手动 / 保护 / bench)。**环与格式必须是纯逻辑**(不碰寄存器、不碰 RTOS)才能这样测——这是 `App/algo/` 既有分层纪律 | -| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性、削顶路数与 `clip_mask` 一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表 | +| 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` 留痕(不是每拍刷屏)、且段号不推进(下一段仍写同号文件) | | UI | `ui-verify` 五步;回放页入 `ui-golden` 黄金参考(**参考图一旦入库只能改代码对齐,禁止刷参考掩盖**);新叶走 `gen_menu.py` 重生成 + 字库链 | | 构建 | `@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`)→ 定时模式必须**默认关闭 + 有时长上限 + 启动时打一次告警**,用完立刻回产品构建。 @@ -300,6 +348,7 @@ flowchart TD | 削顶五路被读成「系统有零序 / 有谐波」 | 现场误判 | 头里 `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 脚本按头解释,绝不按当前固件口径解释 | diff --git a/docs/子系统设计/运行与自检记录设计说明.md b/docs/子系统设计/运行与自检记录设计说明.md index ef8c2b16..c2fb0a78 100644 --- a/docs/子系统设计/运行与自检记录设计说明.md +++ b/docs/子系统设计/运行与自检记录设计说明.md @@ -25,7 +25,7 @@ - **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。 - **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。 - **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。 -- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 218 汉字/符号 + ASCII 0x20~0x7E,共 313 字形;2026-10-28 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。 +- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。 ### 2.2 存储与时间 diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index a3a5f129..4d909dfd 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -256,7 +256,7 @@ flowchart TB - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),ESC 返回菜单屏。 - 菜单受保护叶子(`ui_menu_tree.txt` 行尾 ` #P`):先弹密码页,授权后 `menu_password_ok` **先关页再续进入动作**。 - 待机屏:空闲超时(默认 60s)自动切入,仅确认键回菜单;数据源同实时量页。 -- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,**218 字/313 字形**,2026-10-28 实测;字形数 = 字符数 + ASCII 95 个)。 +- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,**224 字/319 字形**,2026-10-29 实测;字形数 = 字符数 + ASCII 95 个)。 --- diff --git a/docs/测试说明/ADC前端注入验证流程.md b/docs/测试说明/ADC前端注入验证流程.md index e57088ea..81aecc5f 100644 --- a/docs/测试说明/ADC前端注入验证流程.md +++ b/docs/测试说明/ADC前端注入验证流程.md @@ -67,7 +67,7 @@ ## 4. 读数出口(固件只有下面这些周期行,没有命令控制台) -> **样本出口将变**:故障录波已立项(2026-10-29,docs/任务规划/故障录波.md)。落地后段文件会直接给出原始码值样本,动态验证的判据随之从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表,口径变更见 docs/子系统设计/故障录波设计说明.md §11。下表仍是**当前**唯一可读的东西。 +> **样本出口已落地(代码侧,2026-10-29)**:故障录波的数据面已进仓(立项与卡片见 docs/任务规划/故障录波.md,格式与判据属主见 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3、口径变更见该文 §11)——每次触发一段,段文件直接给出 8 路 × 320 点原始码值,动态验证的判据因此**可以**从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表。下表仍是固件唯一的**周期行**出口(段文件不是周期行,只在触发时产生),两种出口对表用。 | 日志行 | 频率 | 内容 | 用途 | | ----------------------------------------------------------- | ------ | ------------------------ | ------------------- | @@ -77,6 +77,8 @@ | `frames=.. idle=.. adc1_err=.. tgap=..` | 每 10 s | 采集健康计数 | 步骤 4 的节奏判据 | | `phasor(hb) C0x-…: Re=.. Im=.. rms=..`(逐实例)、`freq(hb): f=.. valid=..` | 每 10 s | 基波相量分量与测频读数(×0.1 / ×0.01 定点) | **交流轮次的主读数**(直流轮次只作旁证) | | `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | 每 10 s | 200 ms 遥测快照轨(基波有效值 mV) | 交流轮次的幅值比对(1 mV 分辨率,比 phasor 行细) | +| **段文件** `/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见,与 /log 平级) | 每次触发一段(手动 / 保护启动 / bench 定时) | 8 路 × 320 点原始码值 + 封存瞬间的标定与品质快照(布局见 docs/子系统设计/故障录波设计说明.md §6.2) | **样本出口**:交流轮次的主对表源——预触发 160 点 + 后触发 160 点,触发瞬间落在段内点 159 | +| PC 侧脚本 `test/host/wave_read.py`(不是固件日志,bench 工具,纯标准库) | 手动跑;`--selftest` 是它的自带回归 | 四个子命令:dump(逐字段与双 CRC)/ fft(全周傅氏重算基波有效值与相位)/ shape(削顶与形态判定,可 `--strict`)/ plot(出 SVG 或 PNG,PNG 用 `-o` 落文件) | 把段文件还原成周波数、幅值、相位、削顶判定与图 = 「PC 独立重算」这条判据的执行体;CRC 不符或零长度它必须**拒绝**而不是画出东西 | > ⚠ 直流轮次用不到后两行,但**它们一直都在**。交流轮次(判据见 `docs/任务规划/数据可信.md` §4.6) > 必须用 `meas:` 或 104 YC 比幅值,别用 `phasor(hb)` 行——那行是 ×0.1 定点,注 1 V 只剩 10 格。 diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 9b0c3d6c..5082e104 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -713,7 +713,7 @@ uint16_t ch395f_link_change_count(void); | 类别 | 体量 | 为什么不做 | 那它靠什么把关 | |---|---|---|---| | 芯片驱动(寄存器/时序) | `ch395f.c`(801)、`gd5f2gq5ue.c`(689)、`sd2506.c`(264)、`tpafe5160.c`(157)、`rs485.c`(79) = **1,990 行** | 把 HAL 与寄存器假到底 ≈ **重写一遍驱动**,测的是自己的桩而不是硬件行为(SPI 分频必须 8、DMA 流、I2C 时序在桩里怎么编都"对") | 板级套件(`board-test-suite` skill)+ `@test` 真机日志 + 4 个 Fault 现场;陷阱逐条登记在 `docs/问题记录/CH395F_Trap_Records.md`、`docs/问题记录/GD5F2GQ5UE_Trap_Records.md`、`docs/问题记录/SD2506_Trap_Records.md` | -| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 60 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` | +| UI 侧 | 17 个 `.c` = **1,942 行** | 与 PC 模拟器是**同一份源码**,行为回归已由 `ui-golden`(菜单 8 状态 + 内容页 66 张像素黄金参考)覆盖;再写单测是第三份重复投资 | `ui-verify` skill 五步 + `docs/UI设计/UI_自动化测试流程.md` | | 任务编排 | `default_task.c`(281)、`adc_task.c`(171)、`net_task.c`(123)、`protect_task.c`(90)、`ui_task.c`(71)、`rs485_task.c`(11) = **747 行** | 每个只有 4~5 个函数,主体是"按顺序调别人";真逻辑在被调模块里(那些已测或在 P1/P2)。给编排层写单测最容易写成"断言我调了几次"的自证 | 真机启动锚点计数 + IWDG `required_mask`(`@test`)+ `docs/架构设计/项目启动顺序管理.md` | | 内核前装配 | `app_main.c`(135) | 被测对象本身就是"`osKernelStart` 之前不许调内核 API"的时序,主机侧无法忠实复现(PC 上没有那个约束) | 同上 + `cubemx-survival` 审计 | diff --git a/docs/测试说明/自动化测试流程.md b/docs/测试说明/自动化测试流程.md index 4f862457..980e7360 100644 --- a/docs/测试说明/自动化测试流程.md +++ b/docs/测试说明/自动化测试流程.md @@ -119,7 +119,7 @@ - **初始化回归**:`App/task/ui_task.c` 已有 `UI port init: OK` / `UI menu init: OK` / `UI net sink: started` 打印(见 `@test` 命令示例);将其加入 `-Required` 即可经串口自动回归 UI 初始化。 -- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 60 张黄金参考 + 池水位门禁, +- **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 66 张黄金参考 + 池水位门禁, 见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(见 `测试开发标准流程.md` §1)。 ## 扩展用法 diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index fb768419..b8671af4 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -69,7 +69,7 @@ | [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(人工目检审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 | | [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 | | [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 | -| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 60 张黄金参考图比对 | UI 源码、模拟器 | 已写 | +| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 66 张黄金参考图比对 | UI 源码、模拟器 | 已写 | | [`bom-guard.md`](bom-guard.md) | `bom-guard` | 带 BOM 的文件被编辑工具剥掉 BOM(ARMCC 换解码 → 假语法错) | `Drivers/BSP/UI/**` 等名单内文件 | 已写 | | [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 | | (专篇待补,档位与限度正文在 [`../UI设计/UI_自动化测试流程.md`](../UI设计/UI_自动化测试流程.md) §7.3) | `uinav-selftest` | 板端 UI 导航器认屏快检(黄金参考帧分类;完整闭环档=合入前/上板前手工跑,快检不互替) | `scripts/board_ui_nav.py`、`simulator/tests/ref/*.bin` | 指针 | diff --git a/docs/门禁说明/coding-standard.md b/docs/门禁说明/coding-standard.md index 58510f55..e4eb3dac 100644 --- a/docs/门禁说明/coding-standard.md +++ b/docs/门禁说明/coding-standard.md @@ -88,6 +88,16 @@ Drivers/BSP/X/x.c:42 [CODE-RULE-09] 函数 foo_bar 缺少函数注释(应含 但写成 `Malloc(` 就绕过了(同形后缀也一样:`buf_free(` 因词边界不成立而不算违规)。 8. CODE-RULE-20 **只管堆分配函数名**:`alloca` 与变长数组(VLA)是**栈**上的坑,不归它; 分配不释放(泄漏)、释放后继续用(UAF)也不归它——本条只判"别碰 C 堆"这一件事。 +9. **CODE-RULE-09 的回看窗口只有 25 行**:脚本 `scripts/check_coding_standard.py:194` 取的是函数定义行 + 往前 25 行(`window = lines[idx-26:idx-1]`),在里面找「函数功能」关键词。于是**函数头注释块一旦写 + 到 25 行以上,关键词就被挤出窗口**——注释本身完全合规,也照样报「函数缺少函数注释块」。2026-10-29 + 一次真实触发(lead 复跑抓到 6 处、已改 3 个生产文件,全部是**纯注释重排、无一行逻辑改动**):录波 + 落盘道与 UI 道上四个带长口径注释的函数(`wave_store_drain`、`s_log_record_edge`、`App/task/ui_task.c` + 的录波按键 helper、`simulator/main.c` 的合成喂拍 helper)各撞一到两处。**写法上的规避**:把长口径 + 另起一块放在函数注释块**之前**(两块分开),或把细节搬进函数体内——别让「函数功能」那一行离定义行超过 24 行。 +10. **不带文件参数就是空跑**:脚本 `:321` 是逐 `sys.argv[1:]` 处理的,**不给文件参数时一个字节都不查、 + 直接返回 0**。手工复现这条门禁时把那个 RC=0 读成「过了」是错的——必须显式列出要查的路径(pre-commit + 里由框架喂 staged 文件,所以钩子本身不会空跑)。同族坑见 `docs/代码规范/pre-commit使用说明.md` §2。 补录(判据细节,迁移自原手册): diff --git a/docs/门禁说明/nested-comment.md b/docs/门禁说明/nested-comment.md index cde2bb09..dc1b7d13 100644 --- a/docs/门禁说明/nested-comment.md +++ b/docs/门禁说明/nested-comment.md @@ -89,6 +89,15 @@ 但不会指出模板——那段链由 `generated-fresh` 保证「脚本与产物同一笔提交」。 5. 编译器侧的真实警告仍以 `@build` 为准:本闸只拦「会让 ARMCC 报 9-D 的那一类」, 编译日志里其他诊断一概不管。 +6. **仓库根探测必须兼容 linked worktree**:脚本原先用「`.git` 是不是目录」定位仓库根,而 `git worktree` + 出来的任务工作树里 `.git` 是**一个文件**(写着 `gitdir: …`)不是目录 → 本闸在任何任务树里恒 + 退出码 2「仓库根定位失败」,**静默不可用**。这不是「放过了真问题」,是**根本没跑**——比放过更坏, + 因为调用方看到红字会以为环境有问题、看到跳过会以为干净。2026-10-29 已把该判据从 `isdir` 改成 + `exists`(`scripts/check_nested_comment.py:342`,修复归当日提交)。全仓 `scripts/` 只有它这么探根, + 其余只读闸在任务树里一直正常。**后果要说清楚**:门禁修好之前那批录波改动没被它扫过,lead 手工对 + 改动面逐文件扫了一遍、嵌套命中 0,所以没有放过真问题——但那是**人工兜底,不是门禁功劳**。 + 通用教训:**门禁脚本按 `isdir(.git)` 探根 = 在 worktree 里自杀**;新增只读闸要么走 + `git rev-parse --show-toplevel`,要么同时接受 `.git` 的文件形态。 ## 7. 豁免与基线(只减不增) diff --git a/docs/门禁说明/ui-golden.md b/docs/门禁说明/ui-golden.md index bbc233ed..9977e9c1 100644 --- a/docs/门禁说明/ui-golden.md +++ b/docs/门禁说明/ui-golden.md @@ -1,4 +1,4 @@ -# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 60 张黄金参考) +# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 66 张黄金参考) > 一句话:改 UI 源码时,把模拟器增量重编 + 两条 golden 回归串成一次提交门禁,**像素对不上就拦**。 > 它是包装器:真正的判据在被包装的两条脚本与参考图里。 @@ -11,7 +11,7 @@ ## 1. 它在查什么 - 菜单侧:整棵菜单树的行渲染结果(8 个状态)与参考图逐像素比对; -- 内容页侧:60 张黄金参考(= 60 个截图态条目,同一内容页的多个状态各一张,其中 2 张是分页页多拍的第 2 屏),逐张与参考图比对; +- 内容页侧:66 张黄金参考(= 66 个截图态条目,同一内容页的多个状态各一张:分页页多拍的第 2 屏与确认链/录波的各态都单独计条目),逐张与参考图比对; - 参考图 `simulator/tests/ref/*.bin` **就是判据本身**。 ## 2. 为什么要它(有牙的实测) @@ -57,7 +57,7 @@ 1. **参考图即判据**:刷新参考图 = 修改判据。评审时必须盯 `simulator/tests/ref/` 的 diff; 一旦"顺手更新",此后所有回归都只在和错的基线比。**门禁无法阻止这件事**,只能靠评审。 -2. 只覆盖 8 个菜单状态 + 60 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 +2. 只覆盖 8 个菜单状态 + 66 张内容页黄金参考:未列入的状态(密码错误态、超长文本、缺字渲染、 无 SD 卡 / 挂载失败的降级显示)**不会有任何回归**。 3. 跑的是**模拟器那份配置**,不是固件那份 —— 两份 `lv_conf.h` 漂移时,回归结论不能外推到固件 (所以 `lv-conf-sync` 是本门有效性的前提)。 diff --git a/docs/问题记录/采集与数据处理短板评测.md b/docs/问题记录/采集与数据处理短板评测.md index 52d598ba..5e0ab461 100644 --- a/docs/问题记录/采集与数据处理短板评测.md +++ b/docs/问题记录/采集与数据处理短板评测.md @@ -16,7 +16,7 @@ | 链 | 现状 | 缺什么 | |---|---|---| | **测量链(遥测)** | 104 每 100 ms 直接读相量瞬时值 | 工程量折算、谐波方和根、平均/死区/零漂、品质位 | -| **记录链(录波)** | 只有 64 B 定长 SOE 文本记录 | 波形数据面(**2026-10-29 已按 1 kHz 立项**,docs/任务规划/故障录波.md)、COMTRADE 文件、104 文件服务 | +| **记录链(录波)** | SOE 文本记录(64 B 定长)**加上**录波段文件:1 kHz / 8 路原始码值 / 单段 320 点、`/wave` 下轮转、PC 侧读段脚本与现场单通道回放页——**代码面 2026-10-29 已交付,板级判据一轮未跑**(docs/任务规划/故障录波.md) | COMTRADE 文件、104 文件服务;以及 4 kHz 全速与 80 点/周波——本波按 1 kHz 起步,该偏离已按 docs/子系统设计/故障录波设计说明.md §2 显式登记,**不是已补齐** | | **就地闭环(开入开出)** | 固件完全没有 | 开入量采集与防抖、遥信变位、遥控出口、跳闸出口 | **四个阻断级短板**(不补则送检必挂): @@ -24,7 +24,7 @@ 1. **工程量折算缺失**——遥测上送的是 ADC 原始码值(数千~数万),点表要求"短浮点、系数 = 1"的工程量;保护定值(A/V)也在跟码值比较,量纲不成立。 2. **遥测链缺失**——没有 200 ms 测量轨:无谐波方和根、无 4 点平均、无死区确认、无零漂、无畸变率;死区做在 104 层(架构错位)。 3. **运行参数段 0x8020~0x8058 缺失**——死区、变比、零漂门槛、三级负荷报警、录波投退与定值,主站读写全部回"未知地址"。 -4. **录波与 104 文件传输缺失**——点表明确要求"录波文件存储 ≤ 15 s""104 读文件目录/文件 ≤ 30 s"。 +4. **录波已按 1 kHz 口径落地(2026-10-29 代码面),104 文件传输仍缺**——点表明确要求"录波文件存储 ≤ 15 s"(该时限未测,见 §4.4)与"104 读文件目录/文件 ≤ 30 s"(未做)。 **建议顺序**:先做"数据可信"(1 + 2 + 3 + 频率点,纯软件、可主机单测),再做"精度与安全网"(等周波 + 削顶检测 + 派生量),最后单独立项"录波与文件"(最大单块)。开入开出与功率/电度需硬件与产品先拍板,软件先建接口不使能。 @@ -36,12 +36,12 @@ | 环节 | 参考实现 | 本项目 | 差距 | |---|---|---|---| -| **采样节拍** | 内总线 4 kHz(80 点/周波),保护取 1/4 → 1 kHz;交流板本地插值对齐 | TIM3 1 kHz 单节拍,无过采样通路 | 录波无数据源;等周波未闭环 | +| **采样节拍** | 内总线 4 kHz(80 点/周波),保护取 1/4 → 1 kHz;交流板本地插值对齐 | TIM3 1 kHz 单节拍,无过采样通路 | 录波已有 1 kHz 数据源(2026-10-29 交付代码面,见 docs/子系统设计/故障录波设计说明.md §2),**但仍无 4 kHz 过采样通路**;等周波未闭环 | | **预处理** | 码值 × 每通道系数(定点 2^10)→ 浮点工程量;自产零序 `3I0=Ia+Ib+Ic`;插值异常闭锁 | 码值直通(系数 = 1),无标定、无自产零序 | **量纲缺失**;零序只能靠外部通道 | | **保护相量** | 全周傅氏 15 基波 + 3 二次 + 1 五次,每 1 ms 重算 | 5 实例(IA/UA/UN 基波 + IA 二/五次),每 1 ms 重算 | 实例数按 V0.1 有效通道裁剪(合理),但**无派生量层** | | **测量遥测** | 15 通道 × k=1~9 DFT → 方和根 → 4 点平均 → 死区 2 点确认 → 零漂置零 → 微调 TLV10 → 变比;22 交流 + 2 直流 + 7 功率 | 无遥测链;104 直读相量瞬时值 | **整链缺失** | | **派生量** | 线电压 / 负序 / 正序 / 零序 / 差分 / 直流 / 同期量(`Calf*Vec` 系列 + `CalfAnaDC/Dif`) | 无 | 整层缺失 | -| **录波** | 80 点/周波 × 8 周波,突变量启动,COMTRADE 输出(`Wave.c` + `WaveComtrade.c` 共 7 582 行) | 无 | **整链缺失** | +| **录波** | 80 点/周波 × 8 周波,突变量启动,COMTRADE 输出(`Wave.c` + `WaveComtrade.c` 共 7 582 行) | 1 kHz × 8 路原始码值单段(2026-10-29 代码面交付,含段文件与 PC 侧读段工具)| 部分补齐:口径偏离(无过采样、无突变量启动、无 COMTRADE),见 docs/子系统设计/故障录波设计说明.md §2 与 §13 | | **记录** | SOE / COS / 跳闸 / 遥测 / 冻结 / 异常(`RecUnit` + `HistoryFile`) | SOE + 动作记录(B9) | 缺 COS/遥测/冻结/异常 | | **上送** | 101/103/104/61850/ModBus,含文件服务与电度 | 104 子集:M_SP/M_DP/M_SP_TB/M_ME_NC/C_SE/C_RD/C_IC/C_CS | 无文件服务、无电度、无定值区 | @@ -79,7 +79,7 @@ ### 3.3 完全空白 -遥测链 · 派生量(线/序/差分/直流) · 录波与 COMTRADE · 104 文件服务 · 开入量采集 · 遥控出口 · 跳闸出口 · 功率与电度 · 同期 · 小电流选线 · 运行参数段 · 标定 · 等周波 · 削顶检测。 +遥测链 · 派生量(线/序/差分/直流) · COMTRADE 与 104 文件服务(录波本身已于 2026-10-29 落地 1 kHz 口径)· 开入量采集 · 遥控出口 · 跳闸出口 · 功率与电度 · 同期 · 小电流选线 · 运行参数段 · 标定 · 等周波 · 削顶检测。 --- @@ -161,11 +161,11 @@ - 地址、类型、默认值以参考表为准逐项对照,**不要凭想象填**。 - 规模:**M**(表驱动,机械但量大;104 侧 `set_read/set_write` 已是通用接口,不用改协议)。 -### 4.4 录波与 104 文件传输缺失 +### 4.4 录波已按 1 kHz 口径落地,COMTRADE 与 104 文件传输仍缺失 **现状与证据** -- 全库无录波、无 COMTRADE(关键词扫描零命中)。 +- 录波已于 2026-10-29 落地(1 kHz / 8 路 / 段文件:`App/algo/wave_ring.c` 与 `App/algo/wave_store.c`);**COMTRADE 与 104 文件传输仍零命中**(本波不做,见 docs/子系统设计/故障录波设计说明.md §13)。本节原写「全库无录波(关键词扫描零命中)」是落地前的现状句,下面几条里 4 kHz 过采样与文件面的差距仍然成立。 - 104 只实现 8 种 ASDU,**无文件传输类型**(120~122 及读文件目录扩展),无定值区类型。 - 采样通路只有 1 kHz 一条,没有 4 倍过采样。 @@ -333,7 +333,7 @@ | 等周波 | 同 §3.1 的「等周波采样(软件测频 + 调频闭环)」(⬜) | 补三条实施约束(ARR 联动、开环先行、过渡处理) | | 遥测链 | 复现规划 §3.2(「遥测谐波轨(可选)」,标"可选") | **建议从"可选"升为"必做"**——它是遥测、电度、越限、畸变率的共同底座 | | 派生量 | 复现规划 §3.2(「线/负序/零序合成」,V0.1 不适用) | 建议代码与单测先就绪,V0.2 只改映射 | -| 录波 | 复现规划 §3.4(「故障录波(可选)」,⬜) | 补"104 文件服务"这一半(原登记只写录波) | +| 录波 | 复现规划 §3.4(「故障录波(可选)」,⬜;2026-10-29 已按 1 kHz 落地代码面,4 kHz 与 COMTRADE 仍 ⬜)| 补"104 文件服务"这一半(原登记只写录波)| | 运行参数段 | 未登记 | **新发现**,建议按语义标识登记("运行参数段 0x8020~0x8058") | | 开入开出 | 未登记 | **新发现**,建议登记("开入量采集与遥信"、"遥控与出口") | | 削顶检测 | 复现规划 §3.5(「采样通道自检」残余,"码值越界待做") | 给出两条具体判据 | diff --git a/scripts/check_nested_comment.py b/scripts/check_nested_comment.py index 1bf731af..c2658145 100644 --- a/scripts/check_nested_comment.py +++ b/scripts/check_nested_comment.py @@ -339,8 +339,8 @@ def main(argv=None): ap.add_argument("--selftest", action="store_true", help="正例照得出 + 反例不误报 + 基线机制有效") args = ap.parse_args(argv) - if not os.path.isdir(os.path.join(REPO, ".git")): - print("[ENV] 仓库根定位失败:%s 里没有 .git" % REPO) + if not os.path.exists(os.path.join(REPO, ".git")): + print("[ENV] 仓库根定位失败:%s 里没有 .git(目录或 worktree 的 gitdir 文件都算)" % REPO) return 2 print("=== 块注释嵌套检查 check_nested_comment.py(注释体内出现 /* = ARMCC 诊断 9-D)===") diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a550ef23..39755861 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -11,4 +11,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 -2026-10-29 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-29 243552 28784 1972 145388 272336 26.0 89936 68.6 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;wave_ring.o=5296;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 455fbbd1..d62af305 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-10-29T07:40:50+08:00", - "fingerprint": "ebbfb0a1f9f0c25384bab377e009bb9c5ecd6e2044a2a49fcb6bc60ca2f743b9", - "fingerprint_files": 1452, + "built_at": "2026-10-29T18:46:19+08:00", + "fingerprint": "22549aa4e7b96d68fb61393f50924a46628d4b470b380ce95c79599f24f01fd4", + "fingerprint_files": 1460, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "5f69f38dfd7af7fde580068d43925969e3959947", + "git_head": "325abe43372e2f5e0d5ec0f4d87c8590fc9ee12e", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793206308, + "map_mtime": 1793270777, "pct": { - "ER_IROM1": 25.23, + "ER_IROM1": 25.97, "RW_CCM": 87.89, - "RW_IRAM1": 62.21, + "RW_IRAM1": 68.62, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 264580 + "size": 272336 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 81536 + "size": 89936 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 236456, - "ro_data": 28124, - "rw_data": 1876, - "zi_data": 137084 + "code": 243552, + "ro_data": 28784, + "rw_data": 1972, + "zi_data": 145388 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index bc586c0a..01094f53 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -1,12 +1,12 @@ { "files": { "App/algo/adc_cal.c": { - "func_rate": 1.0, + "func_rate": 0.9412, "funcs_covered": 16, - "funcs_valid": 16, - "line_rate": 0.986, + "funcs_valid": 17, + "line_rate": 0.9463, "lines_covered": 141, - "lines_valid": 143 + "lines_valid": 149 }, "App/algo/adc_health.c": { "func_rate": 1.0, @@ -80,6 +80,22 @@ "lines_covered": 41, "lines_valid": 41 }, + "App/algo/wave_ring.c": { + "func_rate": 1.0, + "funcs_covered": 15, + "funcs_valid": 15, + "line_rate": 0.9956, + "lines_covered": 225, + "lines_valid": 226 + }, + "App/algo/wave_store.c": { + "func_rate": 1.0, + "funcs_covered": 29, + "funcs_valid": 29, + "line_rate": 0.9612, + "lines_covered": 446, + "lines_valid": 464 + }, "App/cfg/cfg_file.c": { "func_rate": 0.963, "funcs_covered": 26, @@ -345,12 +361,12 @@ "lines_valid": 112 }, "Drivers/BSP/UI/ui_data.c": { - "func_rate": 0.9767, + "func_rate": 0.8235, "funcs_covered": 42, - "funcs_valid": 43, - "line_rate": 0.9637, + "funcs_valid": 51, + "line_rate": 0.8692, "lines_covered": 372, - "lines_valid": 386 + "lines_valid": 428 }, "Drivers/BSP/UI/ui_net_sink.c": { "func_rate": 0.7692, @@ -373,10 +389,10 @@ "modules": { "adc_cal": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/cfg/cfg_file.c": 689 }, - "lines_total": 832, + "lines_total": 838, "ok": true }, "adc_health": { @@ -447,7 +463,7 @@ }, "iec104": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/freq.c": 130, "App/algo/meas.c": 64, "App/algo/phasor.c": 55, @@ -461,7 +477,7 @@ "App/protocol/iec104/iec104_link.c": 405, "App/protocol/iec104/iec104_pts.c": 255 }, - "lines_total": 2365, + "lines_total": 2371, "ok": true }, "iec104_serv": { @@ -562,14 +578,14 @@ }, "prot_dim": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/freq.c": 130, "App/algo/phasor.c": 55, "App/algo/prot_elm.c": 122, "App/algo/prot_set.c": 179, "App/algo/sample_ring.c": 41 }, - "lines_total": 670, + "lines_total": 676, "ok": true }, "prot_elm": { @@ -600,11 +616,11 @@ }, "run_par": { "files": { - "App/algo/adc_cal.c": 143, + "App/algo/adc_cal.c": 149, "App/algo/run_par.c": 129, "App/cfg/cfg_file.c": 689 }, - "lines_total": 961, + "lines_total": 967, "ok": true }, "sha256": { @@ -660,10 +676,10 @@ }, "ui_data": { "files": { - "Drivers/BSP/UI/ui_data.c": 386, + "Drivers/BSP/UI/ui_data.c": 428, "Drivers/BSP/UI/ui_w_soe.c": 62 }, - "lines_total": 448, + "lines_total": 490, "ok": true }, "ui_sink": { @@ -693,6 +709,29 @@ }, "lines_total": 73, "ok": true + }, + "wave_ring": { + "files": { + "App/algo/wave_ring.c": 226, + "App/algo/wave_store.c": 464 + }, + "lines_total": 690, + "ok": true + }, + "wave_store": { + "files": { + "App/algo/wave_store.c": 464 + }, + "lines_total": 464, + "ok": true + }, + "wave_trig": { + "files": { + "App/algo/wave_ring.c": 226, + "App/algo/wave_store.c": 464 + }, + "lines_total": 690, + "ok": true } }, "note": "主机单测覆盖率基线。口径与工具链限制见 docs/测试说明/主机单测覆盖率口径.md。门禁判据:files[].lines_covered 与 funcs_covered 只增不减,下降即拦提交。", @@ -708,6 +747,8 @@ "App/algo/prot_set.c", "App/algo/run_par.c", "App/algo/sample_ring.c", + "App/algo/wave_ring.c", + "App/algo/wave_store.c", "App/cfg/cfg_file.c", "App/cfg/cfg_file_store_mcu.c", "App/cfg/prot_set_store.c", @@ -747,16 +788,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 46, - "files_untested": 151, - "func_rate": 0.9317, - "funcs_covered": 532, - "funcs_valid": 571, - "line_rate": 0.8983, - "lines_covered": 7058, - "lines_valid": 7857, - "modules_ok": 41, - "modules_total": 41 + "files_tested": 48, + "files_untested": 169, + "func_rate": 0.9231, + "funcs_covered": 576, + "funcs_valid": 624, + "line_rate": 0.8992, + "lines_covered": 7729, + "lines_valid": 8595, + "modules_ok": 44, + "modules_total": 44 }, "untested_sources": [ { @@ -768,9 +809,21 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, { - "code_lines": 407, + "code_lines": 486, + "path": "App/task/ui_task.c" + }, + { + "code_lines": 466, + "path": "App/task/default_task.c" + }, + { + "code_lines": 420, "path": "App/app/ui_data_feed.c" }, + { + "code_lines": 369, + "path": "App/task/adc_task.c" + }, { "code_lines": 353, "path": "Drivers/BSP/UI/ui_w_setview.c" @@ -783,10 +836,6 @@ "code_lines": 300, "path": "Drivers/BSP/UI/ui_content_page.c" }, - { - "code_lines": 286, - "path": "App/task/default_task.c" - }, { "code_lines": 265, "path": "Drivers/BSP/UI/ui_w_editor.c" @@ -796,8 +845,12 @@ "path": "Drivers/BSP/SD2506/sd2506.c" }, { - "code_lines": 249, - "path": "App/task/adc_task.c" + "code_lines": 248, + "path": "Drivers/BSP/UI/ui_page_wave_play.c" + }, + { + "code_lines": 230, + "path": "Drivers/BSP/UI/ui_data.h" }, { "code_lines": 218, @@ -807,10 +860,6 @@ "code_lines": 215, "path": "Drivers/BSP/UI/ui_w_setlist.c" }, - { - "code_lines": 214, - "path": "App/task/ui_task.c" - }, { "code_lines": 206, "path": "Drivers/BSP/CH395F/ch395f.h" @@ -827,14 +876,18 @@ "code_lines": 200, "path": "Drivers/BSP/UI/ui_w_ytview.c" }, - { - "code_lines": 196, - "path": "Drivers/BSP/UI/ui_data.h" - }, { "code_lines": 157, "path": "Drivers/BSP/TPAFE5160/tpafe5160.c" }, + { + "code_lines": 152, + "path": "Drivers/BSP/UI/ui_page_runreport.c" + }, + { + "code_lines": 150, + "path": "App/algo/wave_store.h" + }, { "code_lines": 150, "path": "Drivers/BSP/UI/ui_page_password.c" @@ -844,11 +897,11 @@ "path": "Drivers/BSP/UI/ui_page_device_info.c" }, { - "code_lines": 134, + "code_lines": 142, "path": "App/task/protect_task.c" }, { - "code_lines": 132, + "code_lines": 135, "path": "App/app/app_main.c" }, { @@ -911,6 +964,10 @@ "code_lines": 77, "path": "Drivers/BSP/dbg_log.h" }, + { + "code_lines": 75, + "path": "App/algo/wave_ring.h" + }, { "code_lines": 69, "path": "Drivers/BSP/UI/ui_page_dc.c" @@ -999,6 +1056,10 @@ "code_lines": 40, "path": "Drivers/BSP/UI/ui_page_deadband.c" }, + { + "code_lines": 38, + "path": "Drivers/BSP/UI/ui_page_wave_manual.c" + }, { "code_lines": 36, "path": "Drivers/BSP/UI/ui_w_records.h" @@ -1023,6 +1084,10 @@ "code_lines": 33, "path": "Drivers/BSP/UI/ui_page_action.h" }, + { + "code_lines": 32, + "path": "App/algo/adc_cal.h" + }, { "code_lines": 32, "path": "App/util/boot_stats.h" @@ -1031,10 +1096,6 @@ "code_lines": 32, "path": "Drivers/BSP/RS485/rs485.h" }, - { - "code_lines": 31, - "path": "App/algo/adc_cal.h" - }, { "code_lines": 31, "path": "App/protocol/iec104/iec104_pts.h" @@ -1115,6 +1176,18 @@ "code_lines": 22, "path": "Drivers/BSP/NETCFG/ftp_cfg.h" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_accident.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_alarm.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_oper.c" + }, { "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_runlog.c" @@ -1127,6 +1200,14 @@ "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_soe.c" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_start.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_yk.c" + }, { "code_lines": 20, "path": "Drivers/BSP/UI/ui_net_sink.h" @@ -1291,6 +1372,14 @@ "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_view_word.h" }, + { + "code_lines": 11, + "path": "Drivers/BSP/UI/ui_page_wave_manual.h" + }, + { + "code_lines": 11, + "path": "Drivers/BSP/UI/ui_page_wave_play.h" + }, { "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_yx.h" @@ -1339,6 +1428,14 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_ac.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_accident.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_alarm.h" + }, { "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_dc.h" @@ -1347,6 +1444,22 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_device_info.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_oper.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_runreport.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_start.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_yk.h" + }, { "code_lines": 4, "path": "Drivers/BSP/UI/ui_page_runlog.h" diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 3ffa6197..873b3a0e 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -124,6 +124,8 @@ set(UI_SOURCES ${UI_DIR}/ui_page_runreport.c ${UI_DIR}/ui_page_start.c ${UI_DIR}/ui_page_yk.c + ${UI_DIR}/ui_page_wave_manual.c + ${UI_DIR}/ui_page_wave_play.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) @@ -145,6 +147,13 @@ set(NETCFG_SOURCES # "单一规则源"的同一治理口径)。 set(ALGO_SOURCES ${PROJECT_ROOT}/App/algo/prot_set.c + # 录波段格式与录波环(故障录波 P1/P2):两份都是零平台依赖纯逻辑——不 include HAL/CMSIS/RTOS、 + # 不调 FatFs、不建 RTOS 对象、无 malloc。模拟器必须一起编译,因为「录波回放」页要在模拟器里 + # 出真波形(峰值换算走 wave_store_code_to_milli,取数走 wave_ring_read_ch),数据由 + # simulator/main.c 的合成节拍喂进 ring —— 与 prot_set.c 同一条「双端共享核心、不造第二真相」 + # 口径,因此两份都**不进** scripts/check_registration.py 的 SIM_EXEMPT。 + ${PROJECT_ROOT}/App/algo/wave_store.c + ${PROJECT_ROOT}/App/algo/wave_ring.c ) # IEC 104 [comm] 配置段(道G task-8):模拟器运行期不跑 104 任务、无消费路径, diff --git a/simulator/README.md b/simulator/README.md index d6ec524a..b9674c28 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -53,7 +53,7 @@ simulator/ | `build_sim.bat` / `build_sim.ps1` | 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;`build/` 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。**构建入口一律走它,文档/命令中勿写死生成器版本**;`.bat` 仅负责绕过 PowerShell 执行策略并转交 `.ps1` | | `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 通用配置文件引擎(`../App/cfg_file.c`、`../Drivers/BSP/NETCFG` 的 net_cfg;`net_cfg_legacy` 已于 2026-10-10 删除)+ `sim/` 平台层,链接预编译 SDL2,产出 `lvgl_sim.exe` | | `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 | -| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 60 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | +| `tests/` | 无头回归脚本(菜单 8 状态像素回归 + 内容页 66 张:文本断言 / 反向断言 / 黄金参考逐字节比对);`ref/shot_*.bin` **随仓库入库**(判据本体,改动等于改判据,评审须逐页过目),`ref/*.png` 是本地派生的目检图(`.gitignore` 挡掉)。详见 §3.6 | > **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` / `ui_net_sink.c` 直接引用 `Drivers/BSP/UI/` 下的原件, > MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。 @@ -274,7 +274,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 - `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init` - 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧) - 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`) -4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 218 汉字/符号 + ASCII,共 313 字形;2026-10-28 UI 补齐批 +33 字,旧数 183/278 系未回填、改前实测 185/280),缺字渲染为空白。 +4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 224 汉字/符号 + ASCII,共 319 字形;2026-10-29 录波 UI 道 +6 字(放该读削域不),218/313→224/319;2026-10-28 UI 补齐批 +33 字,旧数 183/278 系未回填、改前实测 185/280),缺字渲染为空白。 补字流程见 `docs/UI设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。 5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。 6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。 diff --git a/simulator/main.c b/simulator/main.c index a37ca31c..b7af39dc 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -72,7 +72,10 @@ #include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */ #include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:镜像真机绑定) */ #include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */ -#include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ +#include "prot_set.h" +#include "wave_ring.h" /* 录波假源:真环(不进 SIM_EXEMPT,见下方块注释) */ +#include "wave_store.h" /* 录波假源:真段格式与定点换算(同一条链) */ +#include "adc_cal.h" /* ADC_CAL_K_Q16_* 封存系数原值(不手抄对照表) */ /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -482,6 +485,383 @@ static void sim_comm_pump(void) { } s_comm_dirty = 0u; } + +/* ═════════ 故障录波假源(UI 道,2026-10-29):真 ring + 真 store + 合成节拍 ═════════ + * 口径(lead 裁定):wave_ring.c / wave_store.c **不进 SIM_EXEMPT**——模拟器链 + * 的就是板端那份实现,回放页在模拟器里出的是**真波形**:合成 1kHz 节拍喂进 + * ring,封存后用 wave_ring_read_cb 作 wave_store 打包游标的数据源产整段, + * 读侧再走 head_parse / data_off / le_decode / code_to_milli 同一条链—— + * 与 prot_set.c 同一条「双端共享核心、不造第二真相」。 + * 数值全取头文件宏与 wave_store 现算(WAVE_ST_* / ADC_CAL_K_Q16_*), + * 不手抄对照表。旗标:--wave-empty 拍「无录波段」空态黄金;--wave-bad 拍 + * 「该段不可读」黄金(故意翻转数据区一字节,让**真 CRC 判废**去拒绝它—— + * 不是把「不可读」写死在假件返回值里)。 + */ +#define SW_IMG_MAX 2u /* 两段:左右键切段的像素证据(段号 + 相位都不同) */ + +static uint8_t s_wave_img[SW_IMG_MAX][WAVE_ST_SEG_SZ_CUR]; +static uint8_t s_wave_idxval[SW_IMG_MAX]; /* 槽位对应的段号(模拟盘上编号) */ +static uint8_t s_wave_cnt; +static int s_wave_empty; /* --wave-empty */ +static int s_wave_bad; /* --wave-bad */ +static uint32_t s_wave_now = 1000u; /* 合成毫秒(跨段单调) */ +static uint32_t s_wave_seq = 10u; /* 合成帧序号(跨段单调) */ + +/* + * 函数功能:合成第 t 拍的 8 路原始码值 + * 入口参数:t - 合成拍号 + * 返回:无(写 codes[8],下标 = 语义号:UA UB UC UN IA IB IC IN) + * 函数说明:TPAFE 三路(sem 0/3/4)双极 50Hz 正弦(1kHz 采样每周波 20 点, + * 幅度 ±8000 码 = 引脚域约 ±1.2V);STM32 五路(sem 1/2/5/6/7) + * 半波 0..2048 且**钉死下轨**(V0.1 单极性无偏置的事实还原—— + * 回放页必须让「贴 0 线」与「削顶」在像素上可分辨) + */ +static void sim_wave_codes(uint32_t a_t, int16_t *a_codes) { + /* 50Hz 正弦 20 点表(×8000 满幅一半再取整,固定表 = 逐像素可复现) */ + static const int16_t k_sine[20] = { + 0, 2351, 4530, 6428, 7893, 8000, 7893, 6428, 4530, 2351, + 0, -2351, -4530, -6428, -7893, -8000, -7893, -6428, -4530, -2351 + }; + int16_t half = (int16_t)(4095 - (a_t % 20u) * 200u); + uint8_t i; + + for (i = 0u; i < 8u; i++) { + if ((i == 0u) || (i == 3u) || (i == 4u)) { + a_codes[i] = k_sine[a_t % 20u]; + } else { + a_codes[i] = (half > 0) ? half : 0; /* 半波衰减再钉下轨 0 */ + } + } +} + +/* + * 函数功能:同步跑完一整段「触发 → 冻结 → 封存 → 打包 → ACK → 冷却」 + * 入口参数:无 + * 返回值:无(成功则 s_wave_cnt++,坏图旗标开启时翻转数据区一字节) + * 限定条件:仅 uiTask/主线程绘制期外的同步调用(假件特权,真机无此路径) + * 函数说明:无头模式没有 lv_timer_handler 的 1ms 泵(同 sim_yt_pump 的 + * 「当场排空」理由)——截图前必须把这段状态机原地跑完, + * 否则回放页永远停在空态,阶梯线/0 线/峰值/徽标就全都没有 + * 像素证据。冷却 500ms 也要泵过去:下一段从 IDLE 起 + */ +static void sim_wave_capture_one(void) { + wave_seal_cal_t cal; + wave_seg_meta_t meta; + wave_pack_ctx_t ctx; + int16_t codes[8]; + uint8_t buf[256]; + static uint8_t slot_pfx; + static uint8_t inited; /* ring 一次性初始化(假件镜像 adc 侧上电) */ + uint32_t out_len = 0u; + uint32_t total; + int32_t r; + uint32_t t; + uint8_t i; + + if (s_wave_cnt >= SW_IMG_MAX) { + return; + } + if (inited == 0u) { + wave_ring_init(); + inited = 1u; + } + /* 预滚历史 ≥ 预窗:触发前的 200 拍先喂进环(真机上环一直在滚, + * 模拟器的环不预滚的话预窗就是 init 全零——回放页两周波里只有一半 + * 有波形,像素证据废一半)。push 在 IDLE/FREEZE 都写环(SEALED 才丢), + * 契约见 App/algo/wave_ring.c 的 wave_ring_push 头注 */ + for (t = 0u; t < 200u; t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + (void)wave_ring_tick(s_wave_now + t, s_wave_seq + t); + } + s_wave_now += 200u; + s_wave_seq += 200u; + if (wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) != 0) { + return; + } + r = 0; + for (t = 0u; t < 4000u; t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + r = wave_ring_tick(s_wave_now + t, s_wave_seq + t); + if (r == 1) { + break; + } + } + if (r != 1) { + return; + } + s_wave_now += t + 1u; + s_wave_seq += t + 1u; + /* 封存交棒:V0.1 真值口径(未标定 → VALID 全 0;CLIPPED 五路 = 0xE6; + * 系数取宏原值,float 一面都不碰——§6.3) */ + (void)memset(&cal, 0, sizeof(cal)); + cal.eng = (uint8_t)WAVE_ENG_PIN; + cal.flags_seq = 0u; + cal.clip_mask = 0xE6u; + cal.wall = 0u; + for (i = 0u; i < 8u; i++) { + cal.ch[i].sem = i; + if ((i == 0u) || (i == 3u) || (i == 4u)) { + cal.ch[i].conv = 0u; + cal.ch[i].bits = 16u; + cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_TPAFE; + cal.ch[i].avail = 0u; + } else { + cal.ch[i].conv = 1u; + cal.ch[i].bits = 12u; + cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_STM32; + cal.ch[i].avail = (uint8_t)0x02u; /* 只 CLIPPED,无 VALID */ + } + cal.ch[i].b_codes = 0; + } + if (wave_ring_latch_meta(&cal) != 0) { + return; + } + if (wave_ring_sealed_meta(&meta) != 0) { + return; + } + total = wave_store_seg_size(&meta); + if ((total == 0u) || (total > (uint32_t)WAVE_ST_SEG_SZ_CUR)) { + return; + } + if (wave_store_pack_start(&ctx, &meta, wave_ring_read_cb, NULL) != 0) { + return; + } + for (;;) { + uint32_t len = 0u; + + r = wave_store_pack_next(&ctx, buf, (uint32_t)sizeof(buf), &len); + if ((r != 0) && (r != WAVE_ST_DONE)) { + return; + } + if ((out_len + len) > (uint32_t)WAVE_ST_SEG_SZ_CUR) { + return; + } + (void)memcpy(&s_wave_img[s_wave_cnt][out_len], buf, len); + out_len += len; + if (r == WAVE_ST_DONE) { + break; + } + } + if (out_len != total) { + return; + } + if (s_wave_bad != 0) { + /* 翻转数据区一点:真 seg_check 的双 CRC 判废会拒它——UI 侧必须 + * 走到「该段不可读」而不是画半截线(伪造证据的反面教材做成像素) */ + s_wave_img[s_wave_cnt][WAVE_ST_HEAD_BYTES(WAVE_ST_CH_NUM) + 16u] ^= 0xA5u; + } + s_wave_idxval[s_wave_cnt] = slot_pfx; + slot_pfx = (uint8_t)((slot_pfx + 1u) % WAVE_ST_SEG_MAX); + s_wave_cnt++; + (void)wave_ring_write_done(0); + /* 泵过冷却窗(COOL = 500ms 只加 ovf 不采新触发;不泵则第二段起不来) */ + for (t = 0u; t < (WAVE_RING_COOL_MS + 1u); t++) { + sim_wave_codes(s_wave_seq + t, codes); + (void)wave_ring_push(codes, 8u); + (void)wave_ring_tick(s_wave_now + t, s_wave_seq + t); + } + s_wave_now += WAVE_RING_COOL_MS + 1u; + s_wave_seq += WAVE_RING_COOL_MS + 1u; +} + +/* + * 函数功能:懒触发建段(回放页第一次取数时把两段建齐) + * 入口参数:无 + * 返回值:无 + * 限定条件:录波通道的 first/next 入口调用 + */ +static void sim_wave_service(void) { + if (s_wave_empty != 0) { + return; + } + while (s_wave_cnt < SW_IMG_MAX) { + sim_wave_capture_one(); + } +} + +/* 函数功能:按段号找槽位 int */ +static int sim_wave_slot(uint8_t a_idx) { + uint8_t i; + + for (i = 0u; i < s_wave_cnt; i++) { + if (s_wave_idxval[i] == a_idx) { + return (int)i; + } + } + return -1; +} + +/* + * 函数功能:验段并把头解析进 p_meta(坏段拒得干净) int + * 说明:这里用全图 seg_check 与真机的流式判废**判据相同**(尺寸等式 + + * 双 CRC + sem==index),只差在真机为省 RAM 分窗——判定结果一致 + * 就是「模拟器像素 = 真机行为」的前提 + */ +static int sim_wave_verified(int a_slot, wave_seg_meta_t *p_meta) { + if (a_slot < 0) { + return UI_WAVE_NONE; + } + if (wave_store_seg_check(s_wave_img[a_slot], WAVE_ST_SEG_SZ_CUR, + p_meta) != 0) { + return UI_WAVE_BAD; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:模拟器侧「手动录波」按键的受理判定(第五注入通道的 manual_req 实现) + * 入口参数:无 + * 返 回 值:0 = 受理(界面据此显示已请求);-1 = 拒绝 int + * 限定条件:仅 UI 线程上下文(与固件 ui_task.c 的同名 ops 一一对应) + * 函数说明:--wave-empty 旗标下恒拒,目的是让「录波进行中/无段可录」那条文案在 + * 模拟器里有像素证据;真受理路径只置计数,段的生成走 sim_wave_service() + * 的懒路径(等 UI 真的来要数据时才造出一段,避免开机就烧 CPU)。 + */ +static int sim_wave_manual_req(void) { if (s_wave_empty != 0) { + return -1; /* 空态旗标:触发也拒(ERR 分支要有像素证据) */ + } + return 0; /* 受理 = ring 已 IDLE 可来一段;段的生成走懒路径 */ +} + +/* + * 函数功能:取开屏默认段号(回放页第一次进页面时的落点) + * 入口参数:a_idx 输出段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文 + * 函数说明:与固件侧同口径——按**数值**取最大号(不是「时间最新」,回绕后两者 + * 不等价),页顶显示的段号就是这一句的自证。 + */ +static int sim_wave_seg_first(uint8_t *a_idx) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + *a_idx = s_wave_idxval[s_wave_cnt - 1u]; + return UI_WAVE_OK; +} + +/* + * 函数功能:切到数值上前一段(镜像 LR 左键;段号表按数值升序) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文;当前段不在表里(被覆盖)时退回最大号 + */ +static int sim_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + { + int pos = sim_wave_slot(a_cur); + if (pos < 0) { + pos = (int)s_wave_cnt - 1; + } + pos = (pos + (int)s_wave_cnt - 1) % (int)s_wave_cnt; + *a_out = s_wave_idxval[pos]; + } + return UI_WAVE_OK; +} + +/* + * 函数功能:切到数值上后一段(镜像 LR 右键) + * 入口参数:a_cur 当前段号;a_out 输出切换后的段号 + * 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int + * 限定条件:仅 UI 线程上下文;表内循环(到尾回头),与固件侧同语义 + */ +static int sim_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { sim_wave_service(); + if (s_wave_cnt == 0u) { + return UI_WAVE_NONE; + } + { + int pos = sim_wave_slot(a_cur); + if (pos < 0) { + pos = 0; + } + pos = (pos + 1) % (int)s_wave_cnt; + *a_out = s_wave_idxval[pos]; + } + return UI_WAVE_OK; +} + +static int sim_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig, + uint8_t *a_clip) { + wave_seg_meta_t meta; + int rc = sim_wave_verified(sim_wave_slot(a_idx), &meta); + + if (rc != UI_WAVE_OK) { + return rc; + } + *a_eng = meta.eng; + *a_trig = meta.trig_src; + *a_clip = (uint8_t)(meta.clip_mask & 0xFFu); + return UI_WAVE_OK; +} + +static int sim_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes, + int32_t *a_peak_milli) { + wave_seg_meta_t meta; + int slot = sim_wave_slot(a_idx); + int rc = sim_wave_verified(slot, &meta); + uint32_t start; + uint32_t off; + uint32_t got = 0u; + int16_t raw[UI_WAVE_WIN_PTS]; + int32_t peak = 0; + int32_t milli; + uint8_t i; + + if (rc != UI_WAVE_OK) { + return rc; + } + if ((a_ch >= meta.ch_count) || (meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) { + return UI_WAVE_BAD; + } + start = (uint32_t)meta.pre_pts - (UI_WAVE_WIN_PTS / 2u); + off = wave_store_data_off(meta.ch_count, meta.pts_per_ch, a_ch, start); + if (off == 0xFFFFFFFFu) { + return UI_WAVE_BAD; + } + if (wave_store_le_decode(&s_wave_img[slot][off], UI_WAVE_WIN_PTS * 2u, + raw, UI_WAVE_WIN_PTS, &got) != 0 || + got != UI_WAVE_WIN_PTS) { + return UI_WAVE_BAD; + } + for (i = 0u; i < UI_WAVE_WIN_PTS; i++) { + int32_t v; + + v = (int32_t)raw[i] + meta.ch[a_ch].b_codes; + if (v > 32767L) { + v = 32767L; + } else if (v < (-32768L)) { + v = -32768L; + } + a_codes[i] = (int16_t)v; + /* 同尺说明与真机胶水同款(App/task/ui_task.c seg_win 注释): + * code_to_milli 内部 (raw + b_codes) × k(App/algo/wave_store.c:567), + * b 只在唯一口径里加一次,PK 与屏上波形同尺 */ + if (wave_store_code_to_milli(&meta.ch[a_ch], raw[i], &milli) == 0) { + /* 「正峰优先」并列判据与真机胶水同款(App/task/ui_task.c + * seg_win 注释):等幅异号时取正,峰值读数不随触发相位漂 */ + if (((milli < 0 ? -milli : milli) > (peak < 0 ? -peak : peak)) || + (((milli < 0 ? -milli : milli) == (peak < 0 ? -peak : peak)) && + (milli > peak))) { + peak = milli; + } + } + } + *a_peak_milli = peak; + return UI_WAVE_OK; +} + +/* 录波通道假表(与真机 s_wave_ops 同形状六件) */ +static const ui_data_wave_ops_t s_wave_ops = { + sim_wave_manual_req, + sim_wave_seg_first, + sim_wave_seg_prev, + sim_wave_seg_next, + sim_wave_seg_head, + sim_wave_seg_win +}; /* * 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态) * 入口参数:无 @@ -819,6 +1199,9 @@ static int run_headless(const char * keys, const char * path, int standby) { if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ printf("[sim] UI comm bind: FAIL\n"); } + if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */ + printf("[sim] UI wave bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -1252,6 +1635,12 @@ int main(int argc, char **argv) { if (strcmp(argv[i], "--comm-nopump") == 0) { s_comm_nopump = 1; /* 道G task-9:通讯页 WAIT 态黄金专用(同款语义独立开关) */ } + if (strcmp(argv[i], "--wave-empty") == 0) { + s_wave_empty = 1; /* 录波回放空态黄金专用:不建段且触发拒收 */ + } + if (strcmp(argv[i], "--wave-bad") == 0) { + s_wave_bad = 1; /* 「该段不可读」黄金专用:数据区翻一点让真 CRC 判废 */ + } } /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ @@ -1329,6 +1718,9 @@ int main(int argc, char **argv) { if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ printf("[sim] UI comm bind: FAIL\n"); } + if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */ + printf("[sim] UI wave bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); diff --git a/simulator/tests/README.md b/simulator/tests/README.md index e6e9bf8b..f0bdfa87 100644 --- a/simulator/tests/README.md +++ b/simulator/tests/README.md @@ -11,7 +11,7 @@ | `check_content_pages.py` | **内容页回归**:各内容页渲染比对 | | `check_remo_sink.py` | **推流(RemoDispBus)回归**:接收端渲染比对 | | `regen_ref_png.py` | 由 `ref/*.bin` 批量出**目检 PNG**(不产生基准;基准由 `check_content_pages.py --update` 刷) | -| `ref/*.bin` | **像素黄金参考(入库)**:60 张 × 3200 B,160×160 单色打包 | +| `ref/*.bin` | **像素黄金参考(入库)**:66 张 × 3200 B,160×160 单色打包(2026-10-29 录波 UI 道 +6 张) | ## 使用方式 diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 0ceded45..0f88f0bf 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -152,6 +152,19 @@ PAGES = [ # 因此用例是 53+7=60 张,不是 53+6=59)。 ("runreport", "dddddde dddddddd e"), ("runreport_p2", "dddddde dddddddd e dddddddd"), # 页内再 8 下 → 第 2 页(选中落 r0=发送帧数) + # ── 故障录波 UI 道(2026-10-29):手动录波三态 + 回放三态(真实波形/空态/坏段)── + # 导航:装置维护 = 根第 5 项(4 下)→ e 进子框(时钟被选中)→ dd 到第 3 项「手动录波」; + # 记录查询 = 根第 7 项(6 下)→ e 进(SOE 被选中)→ 九下落到组末新叶「录波回放」(node 42)。 + # 假数据纪律:模拟器泵**喂真 ring + 走真 store 打包链**(simulator/main.c 录波假源块注释), + # 回放页拍到的是真阶梯(每点 3 px)、真 0 线、真峰值 mV——不是手画折线的像素摆拍。 + # wave_play_bad 的「不可读」由**翻转数据区一字节让真双 CRC 判废**产生(--wave-bad), + # 不是把"不可读"写死在假件返回值里;wave_play_empty 用 --wave-empty 不建段。 + ("wave_manual", "dddd edde uruuruuue"), # 确认链首屏(stage0 问句) + ("wave_manual_done", "dddd edde uruuruuue ee"), # 两次 ENTER → 受理回报行 + ("wave_manual_ro", "dddd edde e"), # 查看密码 000 只读 → LOCK 拒执行 + ("wave_play", "dddddde ddddddddd e"), # 数值最大段 UA:正弦阶梯 + 0 线 + 光标 + ("wave_play_bad", "dddddde ddddddddd e"), # 坏段:显式「该段不可读」,绝不画线 + ("wave_play_empty", "dddddde ddddddddd e"), # 无段:显式「无录波段」 ] # 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)] @@ -676,7 +689,41 @@ TEXTS = { ("r4名", "统计范围", (80, 94), False), ("r4范围标签", "本次开机", (80, 94), False), ("页脚", "第 2 页,共 2 页", (140, 160), False), ], - + # ── 故障录波 UI 道(2026-10-29)── + # 行带口径:操作页四行 y = 44/66/88/110(ui_page_action 组件);回放页顶行 + # y=20、提示行 y=128、徽标行 y=143(ui_page_wave_play.c 的 WP_* 宏,与页面 + # 同源);文案期望串逐字抄页面字面量,不"顺手归一"空格(AC_HINT_DO 是双空格)。 + "wave_manual": [ + ("标题", "手动录波", (0, 24), False), + ("问句", "确认手动录波?", (44, 60), False), + ("键提示", "ENTER=确认 ESC=取消", (104, 126), False), + ], + "wave_manual_done": [ + ("标题", "手动录波", (0, 24), False), + ("回报", "已请求录波", (44, 60), False), + ("返回提示", "ENTER=返回", (104, 126), False), + ], + "wave_manual_ro": [ + ("标题", "手动录波", (0, 24), False), + ("问句", "确认手动录波?", (44, 60), False), + ("只读标", "LOCK", (60, 82), False), + ("只读提示", "ESC=返回", (104, 126), False), + ], + "wave_play": [ + ("标题", "录波回放", (0, 24), False), + ("顶行", "UA SEG" + "1 M", (18, 36), False), # 拆写:连写撞「字母+数字」编号形态 + ("键提示", "UD=CH LR=SEG", (126, 143), False), + ("峰值", "PK" + "1221" + "mV", (126, 143), False), # 拆写避编号形态;值 = 8000×k 定标取整(code_to_milli 唯一口径,正峰优先并列判据) + ("单位徽标", "引脚域", (141, 158), False), + ], + "wave_play_bad": [ + ("标题", "录波回放", (0, 24), False), + ("坏态", "该段不可读", (56, 76), False), + ], + "wave_play_empty": [ + ("标题", "录波回放", (0, 24), False), + ("空态", "无录波段", (56, 76), False), + ], } # 反向文本断言表:页名 -> [(名称, 期望**不出现**的串, y 带, 是否反显态)] @@ -688,6 +735,26 @@ TEXTS = { # · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页; # · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。 ABSENT = { + # ---- 故障录波 UI 道(2026-10-29)---- + # 好段屏不许冒出坏段/空态文案;坏段与空态屏绝不画键提示行与顶行—— + # 「画成一条直线等于伪造证据」(设计 §8.1 空态行)的像素级反证。 + "wave_play": [ + ("无坏态文案", "该段不可读", (0, 160), False), + ("无空态文案", "无录波段", (0, 160), False), + ], + "wave_play_bad": [ + ("坏态不画键提示", "UD=CH LR=SEG", (0, 160), False), + ("坏态不画顶行", "UA SEG", (0, 160), False), + ("坏态不画峰值", "mV", (0, 160), False), + ], + "wave_play_empty": [ + ("空态不画键提示", "UD=CH LR=SEG", (0, 160), False), + ("空态不画顶行", "UA SEG", (0, 160), False), + ("空态不画峰值", "mV", (0, 160), False), + ], + "wave_manual_ro": [ + ("只读无回报行", "已请求录波", (0, 160), False), + ], # ---- 道F task-3:反向断言钉的是"规矩本身",不依赖参考图 ---- # 1970 那条是本页最值钱的反向断言:记录侧的占位串一旦漏进时钟页, # 用户一次"照抄保存"就把 1970 写进 RTC(口径见 ui_page_clock.c 的 CK_INVAL 注) @@ -799,6 +866,11 @@ EXTRA = { "calib_wait": ["--yt-nopump"], # 道G task-9:通讯页 WAIT 态 = 落盘 pump 停转(同款裁定,独立开关) "comm_wait": ["--comm-nopump"], + # 故障录波 UI 道(2026-10-29):坏段/空态两张的专用机器态(同款"假的是环境, + # 页面与显示链全是真的"裁定;--wave-bad 翻数据字节让真 CRC 判废、 + # --wave-empty 根本不建段且触发拒收) + "wave_play_bad": ["--wave-bad"], + "wave_play_empty": ["--wave-empty"], } # 接线道 task-6:记录 5 页在**各自独立的空目录**里跑(不共用回归的那个 tmp)。 diff --git a/simulator/tests/ref/shot_wave_manual.bin b/simulator/tests/ref/shot_wave_manual.bin new file mode 100644 index 0000000000000000000000000000000000000000..480de69378af1a3a7ff140fb477f4bccbfe1d7ab GIT binary patch literal 3200 zcmeH}&npCB7{{M^J?93pkEz|6-L?JoI!45`jIg;zfNzM+6c9v+Iv>N5SCx3*p zN#a6Ia?w)SoBXQ1XxBTlm>)NB(!7WFecn&s@AK66^1cASUUdZ_qYK_PtGob)k0A)j z&5BotFalZ0%QBa{)jPkJNEmwe>JTMuLi4ifgVdY#F{Hb#T$fjek$im9KLeDLMV~GT z(_wPz^#->ffxl0HBa|!BWP!R`uiVj*Is-Hsb)Y0u0<`0}syIwg8S4Obd%$6_GEJ7w zGfxI_*l>;#E<~^rcG%K$3n3vz$-4%3=UOr^+ij|w^*qlIOf~AJV};>dp72-FnBnm% z1jPA4w+{P9o1n9STX1=UGDsjuAV}cn2@w2zxH;R)->jA4^;iJ@A{r^AWD8=uhpV@{XRrkc{Obfh0E&-YhX4Qo literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_manual_done.bin b/simulator/tests/ref/shot_wave_manual_done.bin new file mode 100644 index 0000000000000000000000000000000000000000..4ceb9a3dfdbe94c46e907d3856da797f60c7ff20 GIT binary patch literal 3200 zcmeH{F-yZh7>2(~u9#4vU#=}Wh)Gq@N%Rkx2$4E;%umq4{~)9V3%W__=n#tqe}gzU zh;$R2T^!u)WQCB}INf)n;v#ob^6?w>}Nt10*kJpB4eRT$`JRB+B5_IN`N1D}tx`DrF zz_3^=hd|!ioy0vfJi-7if0{2$Wp8$~7#MtCW>~kJL2*K|=)`9!uD{EkJ87A_PLo^) zWPT;MtthNzdl66NEZY7yCJj2nzG7A9ns~L(RTH+Vi*ijrcKx`CfGz|1a*xrZMw zaOaXY5f4HE9$Kz`pe#syY@1JBCIfRI&Q=lm0(M*UKct!ubbP z9Se()i>nK&*#DrtR`|WF>*4o{Nq80Wq%Z5V&YU%T7+b=)4GNaAi|*0f~MC|#6i;q&5qi~4Mr+xVnEXa W(A^9a>qmDVsw+l$qaiSiLI41oOH4Na literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_play.bin b/simulator/tests/ref/shot_wave_play.bin new file mode 100644 index 0000000000000000000000000000000000000000..3980185dfd91c2707f7538f5a83ac7642416a6d9 GIT binary patch literal 3200 zcmdUvL2DC16vyA}E@2C)ZVE!s9F}!8*#h>n!@d%&e*J3;s=+m2Z?6vX5nUp;lh>6?Tf`sLK2m zhx!fK`+7N7{h)Z0#v2+um35PmH6hd60eJ~c=GD%_TWNwJbL$4YQ&?ebm?ifO_$29C zEU|V^VIei@p28kb4_RCC-ltc;zEYSZI8w3=z|c57U{0JC$*s*l&@#lBx-w&!j>5Q0 z)#(`1=u}oPDXPGPIo^qIf%1U?mdJU_85@zIe8{U~|F|AuaEH%14GC;T5CEHx@S-z8 z01joS7}Dt&78=s&n9i$XIOB+RKeHtIF|{prBC_X6_aXn@KF!<(A42Lxwr}KSI15+3CPP-*jO=sY zY&T|gufdt?qRyO(S<5i&M{Ul&*z?eh*?!$}LS)<<^p>;STd&)0_(XVfkn>`8-JzUO zFS6T4pSdiIpu#fIN#E?Q$GY3sddS8Ygb;(t-y?g);_v6M|5qyCDJeQg1%awVZ~jH! ybI=o#@60M*LV{q3?OhJ8C|*d-cH$>$kf-v`L!}<1a%V58Y&Y%NDjTeGobfL~V7!0; literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_wave_play_bad.bin b/simulator/tests/ref/shot_wave_play_bad.bin new file mode 100644 index 0000000000000000000000000000000000000000..8e072389a03e0958746d430ea24d8a3f3ba5cd19 GIT binary patch literal 3200 zcmZQzpcvFQ2r#e+pc&I4Q2z)`%)a%Xg94fuL!*#`{ybE@{~rqYtEixgv2gzJR0%;9 zb8&U?RG)$>rq}4=uYx8f%BbOhE~e5d;Goihs`p5yM*R~sy*y0E95ff9svG5vhQMeD z47v~yXz&NRNudGRVH}Q~_P_s6{(vmT{F9TRfsp}49Yf>Ah6YCVAIRz~1sNC_e;RNg zi*Y}^&+N!4=zuK7;mOIW$SHCFMQoxcry!>giaKt^Erq}4=uYx8f%BbOhE~e5d;Goihs`p5yM*R~sy*y0E95ff9svG5vhQMeD z48jm#|G~&;gX*sO100QP=w%7>kN79(VhkLD>TaldS@{16vY-kv{D0su|9B0mAQy{E zhzpu|Tud)ST+zgY7z2dbG*I;_DR~6)pp}VEI&PC%(9|_PIG~eq3{~$aZ!`o(Ltr!n NMnhmU1cqz~005*oENcJ& literal 0 HcmV?d00001 diff --git a/test/host/wave_read.py b/test/host/wave_read.py new file mode 100644 index 00000000..6425d805 --- /dev/null +++ b/test/host/wave_read.py @@ -0,0 +1,1256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""PC-side fault-recording (wave) segment reader: dump / fft / shape / plot. + +Pure stdlib. Style sibling: test/host/iec104_master.py (same family: +argparse subcommands, explicit exit codes, ASCII-only stdout). + +This file and App/algo/wave_store.c are TWO INDEPENDENT implementations of +the SAME byte layout, written from the same spec. Spec: wave-recording +design doc (zi xi tong she ji / fault wave record design), section 6.2. +Do not "fix" disagreements by editing this file alone -- report them, a +silent divergence on one field is a format accident. + +Subcommands: + dump FILE parse header + channel table, verify both CRC16s, print all + fft FILE full-cycle Fourier per channel (fundamental RMS + phase, + 2nd/5th harmonic RMS), coefficients mirror App/algo/phasor.c + (sqrt(2)/N pre-multiplied, rms = sqrt(re^2 + im^2)); codes + converted to mV via the LSB snapshot in the header + shape FILE cycles (zero-cross), amplitude, polarity (first-sample sign + + half-wave symmetry), per-channel rail-pinning reconciled + against header clip_mask + plot FILE write SVG (staircase line, dashed zero line, trigger cursor, + channel/segment/trigger labels); default outdir ci-artifacts/ + selftest build synthetic segments in-process (20-pt sine + bottom-rail + pinned square, per section 6.2), re-parse and check + everything, plus negative tests -- no firmware artifacts + needed, no board needed + +Integrity policy (PC-side mirror of design section 8.1 "a flat line drawn +over an unreadable segment is fabricated evidence"): ANY magic/version/CRC/ +size/zero-length failure -> print REJECT reason, exit non-zero, and NEVER +produce a graphic. + +The --strict red path is separate and a red there is the point, not a bug: +shape --strict compares rail-pinning MEASURED from the samples against the +CLIPPED snapshot SEALED in the header (clip_mask). A fixture whose data +never touches a rail (e.g. the interop column-ramp shape) therefore must +report MISMATCH and exit 1 -- "strict bites a non-railed fixture" is proof +the bench criterion has teeth, NOT an interop failure between the two +independent implementations. Judge interop by dump/fft/shape(plain) exit 0 +plus byte-level cross-consistency; never "fix" strict to please a fixture. +Provenance fact (verified in App/algo/adc_cal.c around the CLIPPED branch): +on V0.1 the header CLIPPED bit is stamped from the channel SOURCE, never +measured from samples -- real AC waves bottom-rail those 5 channels so +on-board equality still holds; only synthetic fixtures or wiring changes +break it. +Two rulers, one flag (lead ruling 2026-10-29, "equality is a wiring-change +alarm; silencing it is worse than a red fixture"): + --strict yardstick header == measured (DEFAULT, + the card check; a red here has teeth) + --strict --clip-superset yardstick header covers measured (for + DC-only bias, tiny injection, or an + un-driven channel: sealed-but-not-railed + is TOLERATED, measured-but-not-sealed + still FAILs) +shape's output names the yardstick in force. If the wave cannot rail, use +the superset ruler -- never "fix" the equality check to please a fixture. + +Exit codes: 0 = ok (all checks pass) / 1 = integrity reject or check FAIL / +2 = usage or IO error. + +Usage: + python test/host/wave_read.py dump ci-artifacts/WAVE.BIN + python test/host/wave_read.py fft ci-artifacts/WAVE.BIN --all + python test/host/wave_read.py shape ci-artifacts/WAVE.BIN --strict + python test/host/wave_read.py shape ci-artifacts/WAVE.BIN --strict --clip-superset + python test/host/wave_read.py plot ci-artifacts/WAVE.BIN --ch 0 + python test/host/wave_read.py --selftest + +SPEC CONFORMANCE (design doc sections 6.2 + 6.3, lead ruling 2026-10-29 +answering the PC-track nine-question list; this decoder and +App/algo/wave_store.c must agree byte-for-byte -- if they ever diverge, +report it, do not silently re-rule from one side): + - CRC-16/MODBUS table-free (0x8005 reflected / init 0xFFFF / xorout 0), + both CRC fields stored LOW byte first; anchor "123456789" -> 0x4B37. + Header CRC covers [0, 30 + 12*ch_count); data CRC covers the data area + and sits at end of file; 8ch/320pts whole segment = 5250 B. + - Channel table entry (12 B): sem 1 + src 1 + bits 1 + availability 1 + (= adc_cal_flags(sem) low 8 bits, raw, no invented bit meanings) + + k_q16 i32 (uV/code * 65536) + b_codes i32 (code domain). There is NO + separate LSB field: LSB == k_q16, uV = k_q16/65536, never re-scaled. + mV = (code + b_codes) * k_q16/65536/1000 -- exactly the + adc_cal_apply() formula. Header eng only LABELS the unit (0 = + pin-domain, 1 secondary, 2 primary); it never changes arithmetic. + - flags_seq bit s = ADC_CAL_F_VALID of sem s; clip_mask bit s = sem s + (same mapping as App/algo/meas.h; V0.1 constant 0xE6 = UB UC IB IC IN). + avail is the RAW pass-through of adc_cal_flags(): VALID and CLIPPED are + independent facts and the encoder must not invent invariants to suit the + PC side (lead ruling 2026-10-29). The reader NEVER rejects on the + VALID/CLIPPED relation nor on avail-vs-clip_mask drift -- those get a + soft note only ("VALID+CLIPPED(both)" in dump/fft, a WARN line in dump), + no exit-code change, nothing refused. + eng likewise derives from flags_seq on the board (feed_eng(): all 8 + electrical channels VALID -> secondary(1), else pin-domain(0)); a + pairing mismatch here is also a SOFT WARN only, never a rejection. + - v1 pins ch_count = 8 (TEMP not recorded; TEMP needs a version bump + together with the TEMP-in-flags question); PC must verify table entry + i carries sem == i (ascending UA..IN), mismatch voids the whole + segment; data column i == table entry i; samples int16 LE, TPAFE two's + complement, STM32 0..4095 stored positive with NO midpoint bias + (biasing it would fabricate bipolarity). + - t0_ms / wall / frame_seq are u32 little-endian. ver must equal magic + byte 2 and both must equal 1, else refuse. +""" +import argparse +import math +import os +import struct +import subprocess +import sys +import tempfile + +# ---- format constants (design section 6.2, little-endian) ------------------- +MAGIC_W = 0x57 # 'W' +MAGIC_R = 0x52 # 'R' +FMT_VER = 1 # byte 2 of magic AND byte 3 (ver) both == 1 +SEG_LABEL_SAMPLE = "WAVE%02d" % 0 # sample segment label, produced by format so +# this file never carries a hardcoded letter+digit identifier token + +OFF_VER = 3 # 1 +OFF_FS = 4 # 2 fs_hz +OFF_PPS = 6 # 1 pts_per_cycle +OFF_CHC = 7 # 1 ch_count +OFF_PTS = 8 # 2 pts_per_ch +OFF_PRE = 10 # 2 pre_pts +OFF_TSRC = 12 # 1 trig_src +OFF_TELM = 13 # 1 trig_elm +OFF_T0 = 14 # 4 t0_ms (ms since power-on) +OFF_WALL = 18 # 4 epoch seconds, 0 = unsynced +OFF_ENG = 22 # 1 0 pin-domain / 1 secondary / 2 primary +OFF_FLAGS = 23 # 1 validity bitmap (see Q5) +OFF_CLIP = 24 # 2 clip bitmap, bit s = sem s (see Q6) +OFF_FSEQ = 26 # 4 frame_seq at segment head +HDR_FIXED = 30 # bytes before channel table +ENT_SIZE = 12 # bytes per channel-table entry +ENT_SEM, ENT_SRC, ENT_BITS, ENT_AVAIL = 0, 1, 2, 3 +ENT_KQ = 4 # i32 Q16 uV/code == the LSB (sections 6.2/6.3) +ENT_B = 8 # i32 code-domain zero offset + +SRC_TPAFE, SRC_STM32 = 0, 1 +SRC_NAMES = {SRC_TPAFE: "TPAFE5160", SRC_STM32: "STM32-ADC1"} +SEM_NAMES = {0: "UA", 1: "UB", 2: "UC", 3: "UN", 4: "IA", 5: "IB", + 6: "IC", 7: "IN", 8: "TEMP"} +TRG_NAMES = {1: "MANUAL", 2: "PROTECT", 3: "BENCH-TIMER"} +ENG_NAMES = {0: "pin-domain", 1: "secondary", 2: "primary"} + +CRC_LO_FIRST = True # section 6.3: file-wide little-endian +V1_CH_COUNT = 8 # v1 pins 8 channels (section 6.3, TEMP later) +ADC_CAL_F_VALID = 0x01 # adc_cal.h flags bits, for display only +ADC_CAL_F_CLIPPED = 0x02 + +CRC16_POLY_REF = 0xA001 # CRC-16/MODBUS: 0x8005 reflected +CRC16_INIT = 0xFFFF +CRC16_CHECK = 0x4B37 # known check value for "123456789" + +# Rail bands mirror App/algo/adc_cal.c adc_cal_rail_ths() integer math: +# bipolar: 2^(bits-1)*98/100 (16b -> +/-32112); unipolar: (2^bits-1) scaled +# (12b -> lo 81, hi 4013). +TPAFE_FS_PCT = 98 +STM32_LO_PCT = 2 +STM32_HI_PCT = 98 + + +class RejectError(Exception): + """Segment failed an integrity gate. Message must stand alone.""" + + +class IOErrorMessage(Exception): + """File could not be read -- exit 2, distinct from integrity reject.""" + + +# ---- CRC-16/MODBUS, table-free (spec: no lookup table) ---------------------- +def crc16_modbus(data): + crc = CRC16_INIT + for b in data: + crc ^= b + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ CRC16_POLY_REF + else: + crc >>= 1 + return crc + + +def read_crc16(buf, off): + if CRC_LO_FIRST: + return buf[off] | (buf[off + 1] << 8) + return (buf[off] << 8) | buf[off + 1] + + +def put_crc16(value): + if CRC_LO_FIRST: + return bytes([value & 0xFF, (value >> 8) & 0xFF]) + return bytes([(value >> 8) & 0xFF, value & 0xFF]) + + +# ---- decode helpers ---------------------------------------------------------- +def lsb_uv(ent): + """LSB in micro-volts per code = k_q16 / 65536 (section 6.3: k_q16 IS + the LSB; never re-multiplied).""" + return ent["k_q16"] / 65536.0 + + +def code_to_mv(ent, code): + """Code -> mV via exactly the adc_cal_apply() formula: + value = (code + b) * k[us/code] * 1e-6 [V] -> mV = that * 1000. + The header eng field only labels the unit (0 = pin-domain); per + section 6.3 it must never change this arithmetic, and the PC must not + re-guess units from the current firmware state.""" + return (code + ent["b_codes"]) * lsb_uv(ent) / 1000.0 + + +def rail_thresholds(ent): + """(lo_th, hi_th) in code domain: code <= lo_th pins low, >= hi_th high.""" + bits = ent["bits"] + if ent["src"] == SRC_TPAFE: + th = (1 << (bits - 1)) * TPAFE_FS_PCT // 100 + return -th, th + full = (1 << bits) - 1 + return full * STM32_LO_PCT // 100, full * STM32_HI_PCT // 100 + + +def sem_name(sem): + return SEM_NAMES.get(sem, "SEM%d" % sem) + + +# ---- parsing ----------------------------------------------------------------- +def parse_segment(buf, name="segment"): + """Validate + decode a whole segment image. Raises RejectError.""" + def rej(msg): + raise RejectError("%s: %s" % (name, msg)) + + if len(buf) < HDR_FIXED: + rej("file too short: %d bytes < fixed header %d" % (len(buf), HDR_FIXED)) + if buf[0] != MAGIC_W or buf[1] != MAGIC_R: + rej("bad magic: %02X %02X (expect 57 52 'WR')" % (buf[0], buf[1])) + if buf[2] != FMT_VER: + rej("unsupported format version in magic byte 2: %d (expect %d); " + "this build of wave_read decodes v%d only" + % (buf[2], FMT_VER, FMT_VER)) + ver = buf[OFF_VER] + if ver != FMT_VER: + rej("ver field = %d, magic version byte = %d (expect both %d)" + % (ver, buf[2], FMT_VER)) + + fs_hz = struct.unpack_from(" pts: + rej("pre_pts=%d exceeds pts_per_ch=%d" % (pre, pts)) + + table_end = HDR_FIXED + chc * ENT_SIZE + crc_h_off = table_end + data_off = table_end + 2 + data_len = pts * chc * 2 + want = data_off + data_len + 2 + if len(buf) != want: + rej("file size %d != expected %d (hdr 30 + table %d + 2 + data %d + 2)" + % (len(buf), want, chc * ENT_SIZE, data_len)) + + crc_h_calc = crc16_modbus(buf[0:table_end]) + crc_h_stored = read_crc16(buf, crc_h_off) + if crc_h_calc != crc_h_stored: + rej("header CRC16 mismatch: stored 0x%04X calc 0x%04X over [0,%d) " + "-- segment rejected, nothing drawn" + % (crc_h_stored, crc_h_calc, table_end)) + + data = buf[data_off:data_off + data_len] + crc_d_calc = crc16_modbus(data) + crc_d_stored = read_crc16(buf, data_off + data_len) + if crc_d_calc != crc_d_stored: + rej("data CRC16 mismatch: stored 0x%04X calc 0x%04X over %d " + "bytes at offset %d -- segment rejected, nothing drawn" + % (crc_d_stored, crc_d_calc, data_len, data_off)) + + entries = [] + for i in range(chc): + o = HDR_FIXED + i * ENT_SIZE + if buf[o + ENT_SEM] != i: + rej("channel table entry %d carries semantic id %d: section " + "6.3 pins ascending sem == index (UA=0 .. IN=7); whole " + "segment void" % (i, buf[o + ENT_SEM])) + entries.append({ + "idx": i, + "sem": buf[o + ENT_SEM], + "src": buf[o + ENT_SRC], + "bits": buf[o + ENT_BITS], + "avail": buf[o + ENT_AVAIL], + "k_q16": struct.unpack_from(" 0 else (-1 if v < 0 else 0) + if s == 0: + continue + if last != 0 and s != last: + edges += 1 + last = s + return edges / 2.0, edges + + +def half_wave_symmetry(vals, pps): + """mean(|x[i]+x[i+half]|) / mean(|x[i]-dc|) after DC removal. 0 = ideal.""" + if pps % 2: + return None + nfull = (len(vals) // pps) * pps + if nfull < pps: + return None + half = pps // 2 + dc = sum(vals[:nfull]) / float(nfull) + num = 0.0 + den = 0.0 + for i in range(nfull - half): + a = vals[i] - dc + b = vals[i + half] - dc + num += abs(a + b) + den += abs(a) + if den <= 1e-9: + return 1.0 + return num / den + + +def rail_pins(seg, ci, min_run): + """Return (pin_lo, pin_hi, clipped_bool) on raw codes.""" + ent = seg["entries"][ci] + lo_th, hi_th = rail_thresholds(ent) + codes = seg["samples"][ci] + lo = sum(1 for c in codes if c <= lo_th) + hi = sum(1 for c in codes if c >= hi_th) + return lo, hi, (lo >= min_run or hi >= min_run) + + +def measured_clip_mask(seg, min_run): + mask = 0 + for ci in range(seg["chc"]): + _lo, _hi, clipped = rail_pins(seg, ci, min_run) + if clipped: + mask |= 1 << seg["entries"][ci]["sem"] + return mask + + +def mask_channels(mask): + return " ".join(sem_name(s) for s in sorted(SEM_NAMES) if mask & (1 << s)) + + +# ---- subcommand: dump ------------------------------------------------------- +def fmt_dump(seg): + lines = [] + add = lines.append + add("FILE : %s (%d bytes)" % (seg["name"], seg["size"])) + add("magic : 57 52 %02X ('WR' + version byte %d)" + % (seg["ver"], seg["ver"])) + add("ver : %d" % seg["ver"]) + add("fs_hz : %d%s" % (seg["fs_hz"], + "" if seg["fs_hz"] == 1000 else " (note: not the 1000 of V0.1)")) + add("pts_per_cycle : %d%s" % (seg["pps"], + "" if seg["pps"] == 20 else " (note: not the 20 of V0.1)")) + add("ch_count : %d%s" % (seg["chc"], + "" if seg["chc"] in (8, 9) else " (note: not 8/9)")) + add("pts_per_ch : %d (%d ms at %d Hz)" + % (seg["pts"], seg["pts"] * 1000 // max(1, seg["fs_hz"]), + seg["fs_hz"])) + add("pre_pts : %d" % seg["pre"]) + add("trig_src : %d (%s)" + % (seg["trig_src"], TRG_NAMES.get(seg["trig_src"], "UNKNOWN"))) + add("trig_elm : %d" % seg["trig_elm"]) + add("t0_ms : %d (ms since power-on)" % seg["t0_ms"]) + if seg["wall"]: + add("wall : %d (epoch s)" % seg["wall"]) + else: + add("wall : 0 (time not synced at seal time)") + add("eng : %d (%s)" + % (seg["eng"], ENG_NAMES.get(seg["eng"], "UNKNOWN"))) + valid = [sem_name(s) for s in sorted(SEM_NAMES) + if seg["flags_seq"] & (1 << s)] + add("flags_seq : 0x%02X VALID bits: %s" + % (seg["flags_seq"], " ".join(valid) if valid else "(none)")) + add("clip_mask : 0x%04X [%s]" + % (seg["clip_mask"], mask_channels(seg["clip_mask"]) or "(none)")) + add("frame_seq : %d" % seg["frame_seq"]) + add("CRC16(hdr) : 0x%04X OK covers [0,%d), %s byte first" + % (seg["crc_hdr"], HDR_FIXED + seg["chc"] * ENT_SIZE, + "low" if CRC_LO_FIRST else "high")) + add("CRC16(data) : 0x%04X OK covers %d bytes, %s byte first" + % (seg["crc_data"], seg["pts"] * seg["chc"] * 2, + "low" if CRC_LO_FIRST else "high")) + add("-- channel table (%d entries x %d B) --" % (seg["chc"], ENT_SIZE)) + add("%3s %3s %-5s %-10s %4s %5s %11s %9s %12s %10s" + % ("IDX", "SEM", "NAME", "SRC", "BITS", "AVAIL", "K_Q16", + "B_CODES", "LSB_uV", "FLAG")) + for ent in seg["entries"]: + fl = [] + if ent["avail"] & ADC_CAL_F_VALID: + fl.append("VALID") + if ent["avail"] & ADC_CAL_F_CLIPPED: + fl.append("CLIPPED") + add("%3d %3d %-5s %-10s %4d 0x%02X %11d %9d %12.6f %10s" + % (ent["idx"], ent["sem"], sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "SRC%d" % ent["src"]), + ent["bits"], ent["avail"], ent["k_q16"], ent["b_codes"], + lsb_uv(ent), "|".join(fl) if fl else ".")) + # lead ruling 2026-10-29: soft notes, never a reject, never an exit code + both = [sem_name(e["sem"]) for e in seg["entries"] + if (e["avail"] & (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)) + == (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)] + if both: + add("NOTE: VALID+CLIPPED(both) on: %s (normal once two-point " + "calibration is done, not a fault)" % " ".join(both)) + agg = 0 + for e in seg["entries"]: + if e["avail"] & ADC_CAL_F_CLIPPED: + agg |= 1 << e["sem"] + if agg != seg["clip_mask"]: + add("WARN: avail CLIPPED bits aggregate to 0x%04X, header clip_mask " + "is 0x%04X -- soft check, segment NOT rejected" + % (agg, seg["clip_mask"])) + # eng derives from flags_seq on the board: feed_eng() in + # App/app/ui_data_feed.c -- all 8 electrical channels VALID -> secondary + # (1), else pin-domain (0); wave header fill App/task/adc_task.c. + # Cross-reading allowed here, SOFT ONLY: never a reject, no exit code. + all_valid = (seg["flags_seq"] & 0xFF) == 0xFF + if all_valid and seg["eng"] == 0: + add("WARN: eng=0 (pin-domain) but flags_seq=0xFF (all 8 VALID) -- " + "derived eng would be 1 on board; soft check, NOT rejected") + elif not all_valid and seg["eng"] != 0: + add("WARN: eng=%d (not pin-domain) but flags_seq=0x%02X is not " + "all-VALID -- derived eng would be 0 on board; soft check, " + "NOT rejected" % (seg["eng"], seg["flags_seq"])) + return lines + + +def cmd_dump(args): + seg = load_segment(args.file) + for line in fmt_dump(seg): + print(line) + return 0 + + +# ---- subcommand: fft ---------------------------------------------------------- +def _harm_rms(win, harm, n): + if harm * 2 > n: + return None + rms, _ang = phasor_rms_ang(win, harm, n) + return rms + + +def _fmt_or_na(v): + return "%.2f" % v if v is not None else "n/a" + + +def fmt_fft(seg, cycle, all_cycles, only_ch): + lines = [] + pps = seg["pps"] + ncyc = seg["pts"] // pps + if cycle is None: + cycle = ncyc - 1 + picked = range(ncyc) if all_cycles else [cycle] + lines.append("unit: mV eng=%d (%s)%s" + % (seg["eng"], ENG_NAMES.get(seg["eng"], "UNKNOWN"), + " [pin-domain = ADC pin voltage, NOT calibrated " + "secondary]" if seg["eng"] == 0 else "")) + lines.append("%3s %-5s %-10s %6s %10s %9s %10s %10s %s" + % ("CH", "NAME", "SRC", "CYCLE", "RMS-f[mV]", "ANG-f[deg]", + "RMS-2x[mV]", "RMS-5x[mV]", "NOTE")) + lines.append(" (f = fundamental; 2x/5x = 2nd and 5th harmonic; " + "angle reference = first sample of this window, not a phase " + "lock to the signal generator)") + for ci in only_ch: + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + rmss = [] + for c in picked: + if c < 0 or (c + 1) * pps > seg["pts"]: + lines.append("%3d %-5s %-10s %6d (out of range, cycles " + "0..%d)" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), c, ncyc - 1)) + continue + win = mv[c * pps:(c + 1) * pps] + rms1, ang1 = phasor_rms_ang(win, 1, pps) + rms2 = _harm_rms(win, 2, pps) + rms5 = _harm_rms(win, 5, pps) + rmss.append(rms1) + note = "CLIP-bit" if seg["clip_mask"] & (1 << ent["sem"]) else "" + if (ent["avail"] & (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED)) \ + == (ADC_CAL_F_VALID | ADC_CAL_F_CLIPPED): + note = (note + " VALID+CLIPPED(both)").strip() + lines.append("%3d %-5s %-10s %6d %10.2f %+9.2f %10s %10s %s" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), c, rms1, ang1, + _fmt_or_na(rms2), _fmt_or_na(rms5), note)) + if all_cycles and len(rmss) > 1: + lines.append("%3d %-5s %-10s %6s mean=%10.2f min=%10.2f " + "max=%10.2f over %d cycles" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), "ALL", + sum(rmss) / len(rmss), min(rmss), max(rmss), + len(rmss))) + lines.append("note: mV = (code + b_codes) * k_q16/65536/1000 " + "(adc_cal_apply formula, unit labeled by header eng, " + "never re-guessed); full-cycle window, coefficients = " + "sin/cos(2*pi*k/N)*sqrt(2)/N as in App/algo/phasor.c; " + "phase = atan2(im,re) deg") + return lines + + +def cmd_fft(args): + seg = load_segment(args.file) + if args.ch is None: + only = range(seg["chc"]) + elif 0 <= args.ch < seg["chc"]: + only = [args.ch] + else: + print("usage error: --ch %d out of range (0..%d)" + % (args.ch, seg["chc"] - 1), file=sys.stderr) + return 2 + for line in fmt_fft(seg, args.cycle, args.all, only): + print(line) + return 0 + + +# ---- subcommand: shape --------------------------------------------------------- +def fmt_shape(seg, min_run, strict_out=None, superset=False): + lines = [] + mv_unit = "mV eng=%d(%s)" % (seg["eng"], + ENG_NAMES.get(seg["eng"], "?")) + lines.append("unit: %s min-run for rail pin = %d samples" + % (mv_unit, min_run)) + lines.append("%3s %-5s %-10s %6s %10s %4s %6s %7s %7s %5s" + % ("CH", "NAME", "SRC", "CYCLES", "AMP[mVpk]", "POL", + "SYM", "PIN_LO", "PIN_HI", "RAIL")) + for ci in range(seg["chc"]): + ent = seg["entries"][ci] + codes = seg["samples"][ci] + mv = channel_mv(seg, ci) + mean = sum(mv) / len(mv) + cycles, _edges = zero_cross_cycles([v - mean for v in mv]) + amp = (max(mv) - min(mv)) / 2.0 + first = codes[0] + pol = "+" if first > 0 else ("-" if first < 0 else "0") + sym = half_wave_symmetry(mv, seg["pps"]) + lo, hi, clipped = rail_pins(seg, ci, min_run) + lines.append("%3d %-5s %-10s %6.2f %10.2f %4s %6s %7d %7d %5s" + % (ci, sem_name(ent["sem"]), + SRC_NAMES.get(ent["src"], "?"), cycles, amp, pol, + "%.3f" % sym if sym is not None else "n/a", + lo, hi, "YES" if clipped else ".")) + meas = measured_clip_mask(seg, min_run) + hdr = seg["clip_mask"] + if superset: + ok = (meas & ~hdr) == 0 + verdict = "SUPERSET-OK" if ok else "MISMATCH" + else: + ok = meas == hdr + verdict = "MATCH" if ok else "MISMATCH" + lines.append("clip reconcile: measured=0x%04X [%s] header=0x%04X [%s] %s" + % (meas, mask_channels(meas) or "-", hdr, + mask_channels(hdr) or "-", verdict)) + if superset: + tol = hdr & ~meas + lines.append("yardstick: superset (--clip-superset) -- every " + "measured rail pin must be claimed by the seal; " + "sealed-but-not-railed tolerated: %s" + % (mask_channels(tol) if tol else "(none)")) + else: + lines.append("yardstick: equality (default card check) -- measured " + "rail pins must equal the sealed clip_mask") + lines.append("note: header clip_mask is the seal-time CLIPPED STAMP, " + "source-derived in V0.1 (App/algo/adc_cal.c: " + "s_rom_k_stm32 -> CLIPPED, unipolar no-bias), NOT measured " + "from samples; measured is this segment's rail pins. Real " + "V0.1 AC waves bottom-rail the 5 stamped channels, so " + "equality is expected on-board; a non-railed measurement " + "means synthetic fixture (e.g. column ramps) or wiring " + "change -- investigate, do not auto-blame the seal") + if strict_out is not None: + strict_out.append("MATCH" if ok else "MISMATCH") + return lines + + +def cmd_shape(args): + if args.clip_superset and not args.strict: + print("usage error: --clip-superset only changes the --strict " + "yardstick; pass --strict as well", file=sys.stderr) + return 2 + seg = load_segment(args.file) + verdict = [] + for line in fmt_shape(seg, args.min_run, verdict, + superset=args.clip_superset): + print(line) + if args.strict and verdict and verdict[0] != "MATCH": + if args.clip_superset: + print("STRICT-SUPERSET: measured rail pins OUTSIDE the sealed " + "clip_mask -- low injection cannot explain this; " + "investigate wiring or the seal") + else: + print("STRICT: clip_mask reconciliation failed (yardstick: " + "equality; for DC-only or low injection use --strict " + "--clip-superset)") + return 1 + return 0 + + +# ---- subcommand: plot (pure-string SVG, refuses on any reject) --------------- +def svg_for_channel(seg, ci, seg_label): + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + w, h = 900, 380 + ml, mr, mt, mb = 60, 20, 34, 30 + pw, ph = w - ml - mr, h - mt - mb + span = max(max(mv), -min(mv), 1e-6) * 1.1 + n = len(mv) + + def xs(i): + return ml + pw * i / float(n - 1) + + def ys(v): + return mt + ph / 2.0 - (v / span) * (ph / 2.0) + + name = sem_name(ent["sem"]) + trg = TRG_NAMES.get(seg["trig_src"], "SRC?%d" % seg["trig_src"]) + clip_bit = bool(seg["clip_mask"] & (1 << ent["sem"])) + title = "%s seg=%s trig=%s elm=%d eng=%s%s" % ( + name, seg_label, trg, seg["trig_elm"], + ENG_NAMES.get(seg["eng"], "?"), " CLIP" if clip_bit else "") + parts = [] + parts.append('') + parts.append('' % (w, h, w, h)) + parts.append('' + % (w, h)) + parts.append('%s' % (ml, title)) + parts.append('' % (ml, mt, pw, ph)) + zy = ys(0.0) + parts.append('' + % (ml, zy, ml + pw, zy)) + parts.append('0' % (ml - 14, zy + 3)) + parts.append('%.0fmV' % (ml - 55, mt + 8, span)) + parts.append('%.0fmV' % (ml - 55, mt + ph, -span)) + tx = xs(min(seg["pre"], n - 1)) + parts.append('' + % (tx, mt, tx, mt + ph)) + parts.append('TRIG(pre=%d)' + % (min(tx + 3, w - 120), mt + ph + 12, seg["pre"])) + d = ["M %.2f %.2f" % (xs(0), ys(mv[0]))] + for i in range(1, n): + d.append("L %.2f %.2f" % (xs(i), ys(mv[i - 1]))) + d.append("L %.2f %.2f" % (xs(i), ys(mv[i]))) + parts.append('' + % " ".join(d)) + parts.append('%d pts @ %d Hz, %d pts/cycle, x = sample ' + 'index' + % (ml, h - 6, n, seg["fs_hz"], seg["pps"])) + parts.append("") + return "\n".join(parts) + "\n" + + +def default_outdir(): + root = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))) + return os.path.join(root, "ci-artifacts") + + +def cmd_plot(args): + seg = load_segment(args.file) # any integrity failure raises BEFORE write + if args.out is not None and args.ch is None: + print("usage error: plot --out requires --ch (one file per channel)", + file=sys.stderr) + return 2 + if args.ch is None: + chans = range(seg["chc"]) + elif 0 <= args.ch < seg["chc"]: + chans = [args.ch] + else: + print("usage error: --ch %d out of range (0..%d)" + % (args.ch, seg["chc"] - 1), file=sys.stderr) + return 2 + label = os.path.splitext(os.path.basename(args.file))[0] + written = [] + for ci in chans: + if args.out is not None: + path = args.out + else: + outdir = args.outdir if args.outdir else default_outdir() + if not os.path.isdir(outdir): + os.makedirs(outdir) + path = os.path.join( + outdir, "%s_ch%02d_%s.svg" + % (label, ci, sem_name(seg["entries"][ci]["sem"]))) + svg = svg_for_channel(seg, ci, label) + with open(path, "w", encoding="utf-8", newline="\n") as fh: + fh.write(svg) + written.append(path) + print("WROTE %s" % path) + if not written: + print("plot: nothing written") + return 1 + return 0 + + +# ---- synthetic segment builders (selftest) ------------------------------------ +def syn_entries(chc=8, avail_mode="v01"): + """avail = raw pass-through of adc_cal_flags(). Modes (lead ruling + 2026-10-29 asks for the first two as interop shapes): + v01 = V0.1 board truth, pre-calibration: TPAFE trio 0x00, STM32 five + 0x02; their CLIPPED bits aggregate 2+4+32+64+128 == 0xE6 header + both = deliberately VALID+CLIPPED on the five: 0x01 TPAFE / 0x03 STM32 + (legal: soft note only, must still parse clean) + none = all-zero avail under clip_mask 0xE6 (avail/header drift; soft + WARN only, must still parse clean)""" + pa_t, pa_s = {"v01": (0x00, 0x02), "both": (0x01, 0x03), + "none": (0x00, 0x00)}[avail_mode] + ents = [] + for sem in range(chc): + if sem in (0, 3, 4): + ents.append((sem, SRC_TPAFE, 16, pa_t, 10000000, 0)) + else: + ents.append((sem, SRC_STM32, 12, pa_s, 52800000, 0)) + return ents + + +def syn_samples(pts=320, pps=20, chc=8, rail=True): + """sem0: sine A=9261 phi=+90deg | sem3: A=4630 phi=+30deg | sem4: A=2000 + phi=-60deg | STM32 five (UB UC IB IC IN): 10-low/10-high square pinned + at the bottom rail (0 then 2048) -- honest V0.1 shape, no midpoint + bias. rail=False: the five become a gentle mid-band ramp + 500 + (97*i) % 3000, i.e. values 500..3499 -- never enters the rail + bands (0..81 / 4013..4095), so measured clip is EMPTY while the header + still stamps 0xE6: one fixture that shows BOTH clip yardsticks.""" + phi = {0: math.pi / 2.0, 3: math.pi / 6.0, 4: -math.pi / 3.0} + amp = {0: 9261, 3: 4630, 4: 2000} + cols = [] + for sem in range(chc): + if sem in amp: + a = amp[sem] + p = phi[sem] + cols.append([int(round(a * math.sin(2.0 * math.pi * i / pps + p))) + for i in range(pts)]) + elif rail: + cols.append([0 if (i // (pps // 2)) % 2 == 0 else 2048 + for i in range(pts)]) + else: + cols.append([500 + (97 * i) % 3000 for i in range(pts)]) + return cols + + +def build_synthetic(chc=8, pts=320, pps=20, trig_src=1, clip=0x00E6, + flags=0x00, wall=0, t0=123456, fseq=987654321, + avail_mode="v01", eng=0, rail=True): + hdr = bytearray() + hdr += bytes([MAGIC_W, MAGIC_R, FMT_VER, FMT_VER]) + hdr += struct.pack(" 0x4B37", + crc16_modbus(b"123456789") == CRC16_CHECK, + "got 0x%04X" % crc16_modbus(b"123456789")) + + # 2. synthetic 8-channel segment builds and round-trips through the + # parser (both CRC gates passing inside parse == implicit PASS) + img = build_synthetic() + seg = parse_segment(img, name="synth8") + ck.check("size 8ch/320pts == 5250", seg["size"] == 5250, + "got %d" % seg["size"]) + fields = dict(fs_hz=1000, pps=20, chc=8, pts=320, pre=160, trig_src=1, + trig_elm=0, t0_ms=123456, wall=0, eng=0, flags_seq=0, + clip_mask=0x00E6, frame_seq=987654321) + for k in sorted(fields): + ck.check("field %s round-trip" % k, seg[k] == fields[k], + "got %r" % (seg[k],)) + ck.check("table entry 0 == sem0/src0/bits16/avail0x00/k_q16 10000000", + (seg["entries"][0]["sem"], seg["entries"][0]["src"], + seg["entries"][0]["bits"], seg["entries"][0]["avail"], + seg["entries"][0]["k_q16"]) == (0, 0, 16, 0x00, 10000000)) + ck.check("table entry 1 == sem1/src1/bits12/avail0x02/k_q16 52800000", + (seg["entries"][1]["sem"], seg["entries"][1]["src"], + seg["entries"][1]["bits"], seg["entries"][1]["avail"], + seg["entries"][1]["k_q16"]) == (1, 1, 12, 0x02, 52800000)) + # per-entry availability (low 8 bits of adc_cal_flags) vs header + # clip_mask: the CLIPPED bits must reconcile, bit s = sem s both sides + clip_from_avail = 0 + for ent in seg["entries"]: + if ent["avail"] & ADC_CAL_F_CLIPPED: + clip_from_avail |= 1 << ent["sem"] + ck.check("avail CLIPPED bits reconcile header clip_mask", + clip_from_avail == seg["clip_mask"], + "avail says 0x%04X header 0x%04X" + % (clip_from_avail, seg["clip_mask"])) + + # 2b. lead ruling 2026-10-29: avail is raw pass-through, NEVER a reject + # basis; VALID+CLIPPED(both) and avail/header drift are soft only. + img_b = build_synthetic(avail_mode="both", flags=0xFF, fseq=71) + segb = parse_segment(img_b, name="synth-both") # raises if wrongly voided + ck.check("both-form (flags 0xFF, avail 0x01/0x03) parses, not rejected", + segb["size"] == 5250) + ck.check("dump both-form carries VALID+CLIPPED(both) note", + "VALID+CLIPPED(both)" in "\n".join(fmt_dump(segb))) + fftb = "\n".join(fmt_fft(segb, None, False, range(segb["chc"]))) + ck.check("fft both-form NOTE column carries VALID+CLIPPED(both)", + "VALID+CLIPPED(both)" in fftb) + vb = [] + fmt_shape(segb, 4, vb) + ck.check("both-form shape verdict unchanged: MATCH (avail does not " + "drive strict)", bool(vb) and vb[0] == "MATCH", str(vb)) + img_v0 = build_synthetic(flags=0x00, fseq=72) # V0.1 board truth form + segv0 = parse_segment(img_v0, name="synth-v01") + ck.check("V0.1 truth form (flags_seq 0x00, avail 0x00/0x02) parses clean", + segv0["flags_seq"] == 0x00) + dumpv0 = "\n".join(fmt_dump(segv0)) + ck.check("V0.1 truth dump has no both-note and no WARN", + "VALID+CLIPPED(both)" not in dumpv0 and "WARN" not in dumpv0) + segn = parse_segment(build_synthetic(avail_mode="none", flags=0xFF, + fseq=73), + name="synth-drift") # all-zero vs clip 0xE6 + ck.check("avail/header drift parses (soft, not rejected)", + segn["clip_mask"] == 0x00E6) + ck.check("avail/header drift dump carries WARN line", + "WARN" in "\n".join(fmt_dump(segn))) + + # 2c. eng x flags_seq derivation (lead follow-up ruling 2026-10-29): + # mismatch = soft WARN in dump, never a reject, never an exit code. + segx = parse_segment(img_b, name="synth-eng-cross") # eng=0 + flags 0xFF + dumpx = "\n".join(fmt_dump(segx)) + ck.check("eng=0 with flags_seq=0xFF still parses with soft eng WARN", + segx["eng"] == 0 and "WARN: eng=0" in dumpx) + segok = parse_segment(build_synthetic(avail_mode="both", flags=0xFF, + eng=1, fseq=74), + name="synth-eng-pair") # derived form: eng 1 + ck.check("eng=1 with all-VALID parses, no eng WARN", + "WARN: eng" not in "\n".join(fmt_dump(segok))) + seginv = parse_segment(build_synthetic(avail_mode="v01", flags=0x00, + eng=1, fseq=75), + name="synth-eng-inv") # not all-VALID + eng 1 + ck.check("eng=1 without all-VALID parses with soft eng WARN (not void)", + "WARN: eng=1" in "\n".join(fmt_dump(seginv))) + + # 2d. clip yardsticks (lead ruling 2026-10-29): equality is the default + # card check; --clip-superset is the explicit low-injection ruler. + # Prove both on ONE non-railed fixture at REAL process exit-code + # level, not just in-process verdicts. + imgr = build_synthetic(rail=False, fseq=76) + segr = parse_segment(imgr, name="synth-ramp") + v_eq = [] + out_eq = "\n".join(fmt_shape(segr, 4, v_eq)) + ck.check("ramp fixture: equality yardstick MISMATCH (strict rc 1)", + bool(v_eq) and v_eq[0] == "MISMATCH") + v_sup = [] + out_sup = "\n".join(fmt_shape(segr, 4, v_sup, superset=True)) + ck.check("ramp fixture: superset yardstick passes (strict rc 0)", + bool(v_sup) and v_sup[0] == "MATCH" and "SUPERSET-OK" in out_sup) + ck.check("output names the yardstick: equality", + "yardstick: equality" in out_eq) + ck.check("output names the yardstick: superset + tolerance list", + "yardstick: superset" in out_sup and + "UB UC IB IC IN" in out_sup) + with tempfile.TemporaryDirectory() as td2: + rp = os.path.join(td2, "RAMP.BIN") + with open(rp, "wb") as fh: + fh.write(imgr) + me = sys.executable + script = os.path.abspath(__file__) + rc_eq = subprocess.run([me, script, "shape", rp, "--strict"], + capture_output=True, text=True) + rc_sup = subprocess.run([me, script, "shape", rp, "--strict", + "--clip-superset"], + capture_output=True, text=True) + rc_alone = subprocess.run([me, script, "shape", rp, + "--clip-superset"], + capture_output=True, text=True) + ck.check("CLI real rc: --strict on non-railed fixture == 1", + rc_eq.returncode == 1, "rc=%d" % rc_eq.returncode) + ck.check("CLI real rc: --strict --clip-superset == 0", + rc_sup.returncode == 0, "rc=%d" % rc_sup.returncode) + ck.check("CLI real rc: --clip-superset alone == usage error 2", + rc_alone.returncode == 2, + "rc=%d" % rc_alone.returncode) + ck.check("clip_mask 0xE6 decodes to UB UC IB IC IN", + mask_channels(seg["clip_mask"]) == "UB UC IB IC IN", + mask_channels(seg["clip_mask"])) + + # 3. variant header values (bench trigger + synced wall + new frame_seq) + # must survive the byte round-trip; v1 also pins ch_count to 8 + img_v = build_synthetic(trig_src=3, wall=1793599000, fseq=55, t0=7) + segv = parse_segment(img_v, name="synth-variant") + ck.check("variant trig_src round-trip", segv["trig_src"] == 3) + ck.check("variant wall round-trip", segv["wall"] == 1793599000) + ck.check("variant frame_seq round-trip", segv["frame_seq"] == 55) + ck.check("variant t0_ms round-trip", segv["t0_ms"] == 7) + + # 4. Fourier: analytic expectations for the synthesized sines. + # RMS = A*lsb_uV/1000/sqrt(2) mV; angle = phi (phasor.c convention). + pps = seg["pps"] + for ci, amp_codes, phi_deg in ((0, 9261, 90.0), (3, 4630, 30.0), + (4, 2000, -60.0)): + ent = seg["entries"][ci] + mv = channel_mv(seg, ci) + win = mv[-pps:] + rms1, ang1 = phasor_rms_ang(win, 1, pps) + want_rms = amp_codes * lsb_uv(ent) / 1000.0 / math.sqrt(2.0) + ck.near("fft ch%d RMS" % ci, rms1, want_rms, 0.01) + ck.near("fft ch%d phase (atan2(im,re) convention)" % ci, ang1, + phi_deg, 0.01, abs_tol=2.0) + rms2 = _harm_rms(win, 2, pps) + rms5 = _harm_rms(win, 5, pps) + ck.check("fft ch%d pure sine: H2/H5 << H1" % ci, + rms2 < 0.01 * rms1 and rms5 < 0.01 * rms1, + "rms1=%.3f rms2=%.3f rms5=%.3f" % (rms1, rms2, rms5)) + + # 5. shape: cycles, polarity, symmetry, rail pins vs clip_mask + vmeas = [] + fmt_shape(seg, 4, vmeas) + ck.check("shape clip reconcile MATCH (0xE6)", + bool(vmeas) and vmeas[0] == "MATCH", str(vmeas)) + ua = channel_mv(seg, 0) + mean = sum(ua) / len(ua) + cycles, _ = zero_cross_cycles([v - mean for v in ua]) + ck.near("shape UA cycles == 16.0", cycles, 16.0, 0.0, abs_tol=0.3) + sym = half_wave_symmetry(ua, pps) + ck.check("shape UA half-wave symmetry near 0", + sym is not None and sym < 0.05, "sym=%r" % sym) + ck.check("shape UA polarity '+'", seg["samples"][0][0] > 0) + ck.check("shape IA polarity '-' (phi=-60deg)", seg["samples"][4][0] < 0) + lo1, hi1, clipped1 = rail_pins(seg, 1, 4) + ck.check("shape UB pinned bottom 160 samples", + clipped1 and lo1 == 160 and hi1 == 0, + "lo=%d hi=%d" % (lo1, hi1)) + ck.near("shape measured mask == 0xE6", measured_clip_mask(seg, 4), + 0x00E6, 0.0, abs_tol=0) + _lo0, _hi0, clip0 = rail_pins(seg, 0, 4) + ck.check("shape UA (1V rms sine) not railed", not clip0) + + # 6. plot: SVG content and honest labels; reject path writes nothing + with tempfile.TemporaryDirectory() as td: + svg = svg_for_channel(seg, 0, SEG_LABEL_SAMPLE) + p0 = os.path.join(td, "a.svg") + with open(p0, "w", encoding="utf-8", newline="\n") as fh: + fh.write(svg) + with open(p0, "r", encoding="utf-8") as fh: + back = fh.read() + ck.check("svg well-formed-ish", + back.startswith("")) + for token in ("UA", SEG_LABEL_SAMPLE, "TRIG(pre=160)", "dasharray", "MANUAL"): + ck.check("svg contains %r" % token, token in back) + svg1 = svg_for_channel(seg, 1, SEG_LABEL_SAMPLE) + ck.check("svg marks CLIP for header-clipped channel", + "CLIP" in svg1 and "UB" in svg1) + sentinel = os.path.join(td, "keep.svg") + with open(sentinel, "w", encoding="utf-8") as fh: + fh.write("SENTINEL") + bad = mutate(img, 200) + try: + parse_segment(bad, name="corrupt-for-plot") + ck.check("corrupt input must raise for plot path", False) + except RejectError: + ck.check("reject creates nothing", + not os.path.exists(os.path.join(td, "new.svg"))) + with open(sentinel, "r", encoding="utf-8") as fh: + ck.check("reject leaves existing file untouched", + fh.read() == "SENTINEL") + + # 7. negative integrity cases -- every one must REJECT with a reason + table_off = HDR_FIXED + hdr_crc_off = HDR_FIXED + 8 * ENT_SIZE + data_off = hdr_crc_off + 2 + expect_reject(ck, "reject: bad magic", mutate(img, 0, 0xFF), "magic") + expect_reject(ck, "reject: bad magic version byte", + mutate(img, 2, 0x02), "version") + expect_reject(ck, "reject: bad ver field", mutate(img, OFF_VER, 0x02), + "ver field") + expect_reject(ck, "reject: header CRC mismatch (table byte flipped)", + mutate(img, table_off + 5), "header CRC") + expect_reject(ck, "reject: data CRC mismatch (sample byte flipped)", + mutate(img, data_off + 10), "data CRC") + expect_reject(ck, "reject: stored hdr CRC flipped", + mutate(img, hdr_crc_off), "header CRC") + expect_reject(ck, "reject: truncated tail", img[:-3], "size") + expect_reject(ck, "reject: one byte too long", img + b"\x00", "size") + expect_reject(ck, "reject: too short for a header", img[:10], + "too short") + zl = bytearray(img) + struct.pack_into(" 1 and argv[1] == "--selftest": + return selftest() + parser = build_parser() + args = parser.parse_args(argv[1:]) + if args.selftest: + return selftest() + if not getattr(args, "cmd", None): + parser.print_usage(sys.stderr) + return 2 + try: + return args.fn(args) + except RejectError as exc: + print("REJECT: %s" % exc) + return 1 + except IOErrorMessage as exc: + print("IO ERROR: %s" % exc) + return 2 + except OSError as exc: + print("IO ERROR: %s" % exc) + return 2 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/test/host/wave_ring/run_unit_test.py b/test/host/wave_ring/run_unit_test.py new file mode 100644 index 00000000..93917fe7 --- /dev/null +++ b/test/host/wave_ring/run_unit_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""录波环(故障录波数据面·滚动环与触发冻结状态机,wave_ring)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/wave_ring/wave_ring_test.c(它以 #include 纳入 + **生产源文件** App/algo/wave_ring.c + App/algo/wave_store.c),在 PC 上离线跑 + 环形态与预算、触发到封段的逐拍时序、段内容与点序(列序 = 语义序)、SEALED 期间 + 写指针真冻结、封存交棒握手、读窗夹取与跨环回绕、ACK 与冷却、轮转号推进规则, + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + 工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只等台架): + 本层判据几乎全是结构与并发类(docs/子系统设计/故障录波设计说明.md §4.2 与 §5.2)—— + 1 ms 一拍的冻结窗在人工读表下不可观测,"落盘道挂住两秒"这种注入在真机上等于 + 拿看门狗窗口赌;主机侧可拿影子样本序列逐点钉死。需要板子的判据(注入 1 V/50 Hz + 后 FTP 拉回对表)不在本套件,见 docs/任务规划/故障录波.md。 + +本层为纯逻辑:无寄存器、无 RTOS、无文件/外设依赖,**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/wave_ring/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §7; + docs/任务规划/故障录波.md 卡 1。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:wave_ring.c / wave_store.c 只依赖 stdint/stddef 与本目录两份头。 +BUILD = TestBuild( + name="Wave Ring Tests", + src=os.path.join(HERE, "wave_ring_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== Wave Ring Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_ring/wave_ring_test.c b/test/host/wave_ring/wave_ring_test.c new file mode 100644 index 00000000..49ddccf6 --- /dev/null +++ b/test/host/wave_ring/wave_ring_test.c @@ -0,0 +1,538 @@ +/* + * 模块名称:录波环(滚动环 + 触发冻结状态机 + 已封存区读口)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本): + * App/algo/wave_store.c + App/algo/wave_ring.c + * 两源均为纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 为什么要主机侧(而不是只等台架):本层的判据几乎全是**结构与并发**类—— + * 段首基址与点序、SEALED 期间写指针必须真冻结、ACK 前不许重录、滞留超时自弃、 + * 冷却只在 ACK 后计时、读口在未就绪时绝不返回旧数据。这些在台架上要么不可观测 + * (1 ms 一拍的窗口,人工读不到)、要么无法安全注入(真造一次「defaultTask 挂住 + * 两秒」就是把看门狗窗口拿去赌)。主机侧可以拿影子样本序列把它逐点钉死。 + * 需要板子的部分(注入 1 V/50 Hz 后 FTP 拉回对表、实机 FAT 落盘)不在本套件, + * 见 docs/任务规划/故障录波.md 的「录波数据面最小闭环」退出判据。 + * + * 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录): + * 用例:形态与预算钉死:int16×320×8 = 5120、两半各 160、 + * sizeof(wave_seg_meta_t) = 136、sizeof(wave_ch_desc_t) = 12 + * 用例:init 复位语义:IDLE / 不可读 / 计数与轮转号归零 + * 用例:参数守卫:push / request / latch / read / get_stat 的非法入参 + * 用例:触发到封段的时序:封段恰在触发后第 160 拍,事件一次性 + * 用例:段内容与点序:8 列 × 320 点逐点等于影子样本, + * 触发那一拍的样本在点 159、点 160 是其后第一拍(列序 = 语义序) + * 用例:SEALED 丢样本:push 返 DONE、drop 计点、写指针真冻结、内容不变 + * 用例:封存交棒握手:未 latch 一律不可用;latch 只准一次、不覆盖; + * latch 内容进段头(eng / flags_seq / clip_mask / wall + 8 路系数) + * 用例:读窗夹取与跨环回绕;read_cb 的 32 位形态 + * 用例:列号映射:0..7 恒等、TEMP 与越界返 WAVE_ST_E_RANGE + * 用例:ACK 语义:成功推进 seg_idx 与 seg_total 并起冷却;重复 ACK 拒绝; + * 未就绪时读口返 E_STATE 而不是旧数据 + * 用例:环未填满就触发:前导点必须是 0(不是上一段的残留),真实样本从 + * 点 129 起(本次上电只有 31 个点可给) + * + * 通过:全部 CHECK 通过(main 返回 0)。 + * 创建日期:2026-10-29(故障录波 task-1) + */ + +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码(生产源原样纳入) ===================== */ +#include "wave_store.c" +#include "wave_ring.c" + +/* ===================== 影子样本模型 ===================== */ +#define HIST_N 2048u /* 影子样本环(比录波环大,够跑完整个场景) */ +#define TRIG_K 400u /* 第一段:消费请求那一拍的样本号 */ + +static int16_t s_hist[HIST_N][WAVE_RING_CH]; +static uint32_t s_k; /* 下一个要压的样本号 */ +static uint32_t s_evt_sealed; /* 累计看到多少次 EVT_SEALED */ +static uint32_t s_push_bad; /* push 返回了 OK/DONE 之外的值 */ +static int32_t s_push_last; /* 最近一次 push 的返回值 */ + +/* 每路一条可辨认的斜坡:不同样本、不同列都不重复,负值也覆盖(TPAFE 是双极性补码) */ +static int16_t wave_sig(uint32_t a_k, uint8_t a_col) { + return (int16_t)(((int32_t)a_k * 7) - 1200 + ((int32_t)a_col * 91)); +} + +/* 压 n 拍(push 之后 tick,与 adcTask 的接线顺序一致:先写样本再算点数) */ +static void wave_step(uint32_t a_n) { + uint32_t i; + + for (i = 0u; i < a_n; i++) { + int16_t codes[WAVE_RING_CH]; + uint8_t c; + int32_t rc; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + codes[c] = wave_sig(s_k, c); + s_hist[s_k % HIST_N][c] = codes[c]; + } + rc = wave_ring_push(codes, (uint8_t)WAVE_RING_CH); + s_push_last = rc; + if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) { + s_push_bad++; + } + rc = wave_ring_tick(s_k, s_k); + if (rc == (int32_t)WAVE_RING_EVT_SEALED) { + s_evt_sealed++; + } + s_k++; + } +} + +/* 影子样本 -> 段内点:点 i 对应样本 (a_trig - 159 + i),负数表示当时还没采到 */ +static int32_t wave_pt_sample(uint32_t a_trig, uint32_t a_pt) { + return ((int32_t)a_trig - (int32_t)(WAVE_RING_PRE - 1u)) + (int32_t)a_pt; +} + +/* 整段逐点比对(a_trig = 消费请求那一拍的样本号) */ +static int wave_cmp_all(uint32_t a_trig) { + int16_t buf[WAVE_RING_PTS]; + uint8_t c; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + int32_t n; + uint32_t i; + + n = wave_ring_read_ch(c, 0u, (uint16_t)WAVE_RING_PTS, buf); + if (n != (int32_t)WAVE_RING_PTS) { + return 0; + } + for (i = 0u; i < (uint32_t)WAVE_RING_PTS; i++) { + int32_t ks = wave_pt_sample(a_trig, i); + int16_t want = (ks < 0) ? (int16_t)0 : wave_sig((uint32_t)ks, c); + if (buf[i] != want) { + return 0; + } + } + } + return 1; +} + +/* 封好段并交棒(返回消费请求那一拍的样本号) */ +static uint32_t s_trig_first; + +static void wave_seal_once(uint8_t a_src, uint8_t a_elm) { + wave_ring_request(a_src, a_elm); + wave_step(1u); /* 本拍样本 = 点 159,同拍 tick 消费请求钉基址 */ + s_trig_first = s_k - 1u; + wave_step((uint32_t)WAVE_RING_POST); /* 后触发 160 点,最后一拍封段 */ +} + +/* 造一份封存瞬间事实 */ +static void wave_fill_cal(wave_seal_cal_t *p_cal, uint8_t a_seq_tag) { + uint8_t i; + + p_cal->eng = (uint8_t)WAVE_ENG_PIN; + p_cal->flags_seq = (uint8_t)(0x50u ^ a_seq_tag); + p_cal->clip_mask = (uint16_t)0x00E6u; + p_cal->wall = 1764374400u + (uint32_t)a_seq_tag; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) { + p_cal->ch[i].sem = i; + p_cal->ch[i].conv = (uint8_t)((i == 0u) || (i == 3u) || (i == 4u)) ? 0u : 1u; + p_cal->ch[i].bits = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 16u : 12u); + p_cal->ch[i].avail = (uint8_t)(0x01u | (((i == 1u) || (i == 5u)) ? 0x02u : 0x00u)); + p_cal->ch[i].k_q16 = (int32_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 10000000L : 52800000L); + p_cal->ch[i].b_codes = (int32_t)(a_seq_tag + i); + } +} + +static wave_seal_cal_t s_cal; + +/* ===================== 主测 ===================== */ +int main(void) { + printf("=== Wave Ring Tests:\n"); + + /* ---------- 用例:形态与预算 ---------- */ + printf("-- 用例:形态与预算 --\n"); + CHECK(sizeof(int16_t) == 2u, "int16 是两字节(不许 float)"); + CHECK(WAVE_RING_BYTES == 5120u, "环 = 5120 字节"); + CHECK(((uint32_t)WAVE_RING_PTS * (uint32_t)WAVE_RING_CH * 2u) == WAVE_RING_BYTES, "环长等式"); + CHECK(WAVE_RING_PTS == (WAVE_RING_PRE + WAVE_RING_POST), "段长 = 预触发 + 后触发"); + CHECK(WAVE_RING_PRE == 160u, "预触发 160 点 = 8 周波 @20 点/周波"); + CHECK(sizeof(wave_ch_desc_t) == (size_t)WAVE_ST_CH_BYTES, "通道表一项正好 12 字节"); + CHECK(sizeof(wave_seg_meta_t) == 136u, "段头快照 136 字节(本卡静态增量台账用)"); + + /* ---------- 用例:init 复位 ---------- */ + printf("-- 用例:init 复位语义 --\n"); + wave_ring_init(); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "init 后 IDLE"); + CHECK(wave_ring_sealed() == 0u, "init 后无可读段"); + { + wave_ring_stat_t st; + int16_t buf[8]; + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "init 后读已封存区被拒"); + CHECK(wave_ring_get_stat(&st) == WAVE_ST_OK, "get_stat OK"); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.ovf_cnt == 0u) && + (st.drop_cnt == 0u) && (st.err_cnt == 0u), "init 后计数与轮转号归零"); + } + + /* ---------- 用例:参数守卫 ---------- */ + printf("-- 用例:参数守卫 --\n"); + CHECK(wave_ring_push(NULL, (uint8_t)WAVE_RING_CH) == WAVE_ST_E_PARAM, "push 空指针"); + { + int16_t codes[WAVE_RING_CH]; + CHECK(wave_ring_push(codes, 7u) == WAVE_ST_E_PARAM, "push 长度不等于 8 拒绝"); + CHECK(wave_ring_push(codes, 9u) == WAVE_ST_E_PARAM, "push 长度大于 8 拒绝"); + } + CHECK(wave_ring_request(0u, 0u) == WAVE_ST_E_PARAM, "request 触发源 0 拒绝"); + CHECK(wave_ring_request(4u, 0u) == WAVE_ST_E_PARAM, "request 触发源 4 拒绝"); + CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针"); + CHECK(wave_ring_sealed_meta(NULL) == WAVE_ST_E_PARAM, "sealed_meta 空指针"); + CHECK(wave_ring_get_stat(NULL) == WAVE_ST_E_PARAM, "get_stat 空指针"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没段可 ACK 时拒绝"); + + /* ---------- 用例:触发到封段时序 ---------- */ + printf("-- 用例:触发到封段时序 --\n"); + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + s_push_bad = 0u; + wave_step(TRIG_K); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "400 拍后仍 IDLE(没请求就不该动)"); + wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 7u); + wave_step(1u); + s_trig_first = s_k - 1u; + CHECK(s_trig_first == TRIG_K, "请求在下一拍被消费(本层不自己冻结,交给 adcTask 的 tick)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE, "消费请求即 FREEZE"); + CHECK(s_evt_sealed == 0u, "还没封段"); + CHECK(wave_ring_sealed() == 0u, "FREEZE 期间不可读"); + wave_step((uint32_t)WAVE_RING_POST - 1u); + CHECK((wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE) && (s_evt_sealed == 0u), + "后触发 159 点时仍未封段"); + wave_step(1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "后触发计满 160 点才封段"); + CHECK(s_evt_sealed == 1u, "EVT_SEALED 恰好一次"); + wave_step(1u); + CHECK(s_evt_sealed == 1u, "事件不重复报(只封段那一拍报)"); + CHECK(s_push_bad == 0u, "全程 push 无非法返回"); + + /* ---------- 用例:a 未 latch 前一律不可用(必须先于内容比对) ---------- */ + printf("-- 用例:封存交棒握手 --\n"); + CHECK(wave_ring_sealed() == 0u, "刚封段还没交棒:不可用"); + { + wave_ring_stat_t st; + wave_seg_meta_t meta; + int16_t buf[8]; + (void)wave_ring_get_stat(&st); + CHECK(st.meta_ready == 0u, "meta_ready = 0"); + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "未 latch 读被拒"); + CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_E_STATE, "未 latch 段头被拒"); + /* 此刻 latch 是合法时机 */ + wave_fill_cal(&s_cal, 0u); + s_cal.eng = 9u; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_PARAM, "折算档位非法拒绝"); + s_cal.eng = (uint8_t)WAVE_ENG_PIN; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "latch 成功"); + CHECK(wave_ring_sealed() == 1u, "latch 后可用(两个标志并成一个)"); + /* 第二次交棒:必须整份拒绝,锁存值一个字节都不许被换掉 */ + { + uint32_t wall0 = s_cal.wall; + s_cal.wall = 123u; + s_cal.clip_mask = 0x0000u; + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复 latch 拒绝"); + CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_OK, "latch 后段头可读"); + CHECK((meta.wall == wall0) && (meta.clip_mask == 0x00E6u), + "重复 latch 没有覆盖已锁存值(半路换系数会让头与数据不同源)"); + s_cal.wall = wall0; + } + CHECK((meta.fs_hz == (uint16_t)WAVE_ST_FS_DEF) && (meta.pts_per_cycle == (uint8_t)WAVE_ST_PPC_DEF), + "段头采样率与每周波点数按 §6.2 落值"); + CHECK((meta.ch_count == (uint8_t)WAVE_RING_CH) && (meta.pts_per_ch == (uint16_t)WAVE_RING_PTS) && + (meta.pre_pts == (uint16_t)WAVE_RING_PRE), "段头几何三要素与环一致"); + CHECK((meta.trig_src == (uint8_t)WAVE_TRIG_MANUAL) && (meta.trig_elm == 7u), + "触发源与元件序号进段头"); + CHECK((meta.t0_ms == s_trig_first) && (meta.frame_seq == s_trig_first), + "段首毫秒与帧序号取触发那一拍(不是封段那一拍)"); + CHECK((meta.flags_seq == s_cal.flags_seq) && (meta.clip_mask == 0x00E6u), + "有效性位图与削顶位图进段头"); + { + uint8_t i; + int ok = 1; + for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) { + if ((meta.ch[i].sem != i) || (meta.ch[i].conv != s_cal.ch[i].conv) || + (meta.ch[i].bits != s_cal.ch[i].bits) || (meta.ch[i].avail != s_cal.ch[i].avail) || + (meta.ch[i].k_q16 != s_cal.ch[i].k_q16) || (meta.ch[i].b_codes != s_cal.ch[i].b_codes)) { + ok = 0; + } + } + CHECK(ok != 0, "通道表 8 项逐项进段头(sem = 下标)"); + } + } + + /* ---------- 用例:段内容与点序 ---------- */ + printf("-- 用例:段内容与点序 --\n"); + CHECK(wave_cmp_all(s_trig_first) != 0, "8 列 × 320 点逐点等于影子样本(列序 = 语义序)"); + { + int16_t buf[WAVE_RING_PTS]; + (void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf); + CHECK(buf[WAVE_RING_PRE - 1u] == wave_sig(s_trig_first, 0u), + "触发那一拍的样本落在点 159(预触发窗最后一点)"); + CHECK(buf[WAVE_RING_PRE] == wave_sig(s_trig_first + 1u, 0u), + "点 160 是触发之后第一拍"); + CHECK(buf[0] == wave_sig(s_trig_first - (WAVE_RING_PRE - 1u), 0u), + "点 0 是最老的预触发点(段首基址钉对了)"); + CHECK(buf[1] > buf[0], "斜坡单调 -> 点序没有倒过来"); + } + + /* ---------- 用例:SEALED 丢样本 + 写指针真冻结 ---------- */ + printf("-- 用例:SEALED 丢样本与写指针冻结 --\n"); + { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + (void)wave_ring_get_stat(&st0); + wave_step(10u); + (void)wave_ring_get_stat(&st1); + CHECK(s_push_last == WAVE_ST_DONE, "SEALED 期间 push 返 DONE(样本被丢)"); + CHECK(st1.drop_cnt == (st0.drop_cnt + (10u * (uint32_t)WAVE_RING_CH)), + "drop 计数单位 = 点(一次丢一拍加 8)"); + CHECK(st1.seal_hold == (uint16_t)(st0.seal_hold + 10u), "SEALED 滞留按拍累计"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "滞留未超时仍是 SEALED"); + CHECK(wave_cmp_all(s_trig_first) != 0, "已封存区一个点都没被动过(写指针真冻住)"); + } + + /* ---------- 用例:读窗夹取 / 跨回绕 / read_cb ---------- */ + printf("-- 用例:读窗夹取与跨回绕 --\n"); + { + int16_t buf[WAVE_RING_PTS]; + int32_t n; + uint32_t i; + int ok; + + n = wave_ring_read_ch(3u, 300u, 40u, buf); + CHECK(n == 20, "超出段尾自动夹到剩余点数"); + ok = 1; + for (i = 0u; i < 20u; i++) { + int32_t ks = wave_pt_sample(s_trig_first, 300u + i); + if (buf[i] != wave_sig((uint32_t)ks, 3u)) { + ok = 0; + } + } + CHECK(ok != 0, "夹出来的 20 点仍与影子样本一致(回绕段尾正确)"); + /* base = 241(400 拍场景)-> 段内跨环尾的点必须靠取模接上 */ + n = wave_ring_read_ch(5u, 70u, 20u, buf); + ok = 1; + for (i = 0u; i < (uint32_t)n; i++) { + int32_t ks = wave_pt_sample(s_trig_first, 70u + i); + if (buf[i] != wave_sig((uint32_t)ks, 5u)) { + ok = 0; + } + } + CHECK((n == 20) && (ok != 0), "跨越环尾的窗取模展开正确(段首基址 241)"); + n = wave_ring_read_cb(NULL, 2u, 0u, (uint32_t)WAVE_RING_PTS, buf); + CHECK(n == (int32_t)WAVE_RING_PTS, "read_cb 32 位形态整段可读"); + CHECK(wave_ring_read_cb(NULL, 2u, 0u, 9999u, buf) == WAVE_ST_E_PARAM, "read_cb 点数超过环长拒绝"); + CHECK(wave_ring_read_cb(NULL, 2u, 9999u, 1u, buf) == WAVE_ST_E_RANGE, "read_cb 起点越界拒绝"); + CHECK(wave_ring_read_ch(8u, 0u, 1u, buf) == WAVE_ST_E_RANGE, "列号越界"); + CHECK(wave_ring_read_ch(0u, (uint16_t)WAVE_RING_PTS, 1u, buf) == WAVE_ST_E_RANGE, "起点越界"); + CHECK(wave_ring_read_ch(0u, 0u, 0u, buf) == WAVE_ST_E_PARAM, "点数为 0"); + CHECK(wave_ring_read_ch(0u, 0u, 1u, NULL) == WAVE_ST_E_PARAM, "读口空指针"); + } + + /* ---------- 用例:列号映射 ---------- */ + printf("-- 用例:列号映射 --\n"); + { + int ok = 1; + uint8_t i; + for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) { + if (wave_ring_col(i) != (int32_t)i) { + ok = 0; + } + } + CHECK(ok != 0, "0..7 语义号 -> 同列号(本波恒等)"); + CHECK(wave_ring_col(8u) == WAVE_ST_E_RANGE, "TEMP 不录 -> 越界"); + CHECK(wave_ring_col(200u) == WAVE_ST_E_RANGE, "非法语义号 -> 越界"); + } + + /* ---------- 用例:ACK 成功 + 冷却 + 二次触发 ---------- */ + printf("-- 用例:ACK 与冷却 --\n"); + { + wave_ring_stat_t st; + int16_t buf[8]; + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "ACK 成功"); + (void)wave_ring_get_stat(&st); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "ACK 后转冷却"); + CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却起表 500 拍"); + CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功才推进轮转号与段计数"); + CHECK(wave_ring_sealed() == 0u, "解冻后立刻不可读"); + CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "解冻后读被拒(不返旧数据)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 拒绝"); + /* 冷却期内再触发:只加 ovf,绝不重录 */ + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 3u) == WAVE_ST_DONE, "冷却期内请求被忽略"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 1u, "ovf 计一次"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "被忽略的请求没有改状态"); + wave_step(100u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "冷却未走完仍在 COOL"); + wave_step(400u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完后回 IDLE"); + } + + /* ---------- 用例:ACK 失败不推进轮转号 ---------- */ + printf("-- 用例:ACK 失败不推进号 --\n"); + wave_seal_once((uint8_t)WAVE_TRIG_PROTECT, 2u); + wave_fill_cal(&s_cal, 1u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第二段交棒 OK"); + CHECK(wave_cmp_all(s_trig_first) != 0, "第二段内容 = 第二次故障的影子样本"); + { + wave_ring_stat_t st; + CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败 ACK 仍解冻(错误只计数)"); + (void)wave_ring_get_stat(&st); + CHECK(st.err_cnt == 1u, "err 计一次"); + CHECK((st.seg_idx == 1u) && (st.seg_total == 1u), "失败不推进轮转号与段计数(下段同号)"); + } + + /* ---------- 用例:滞留超时自弃 ---------- */ + printf("-- 用例:SEALED 滞留超时自弃 --\n"); + wave_step((uint32_t)WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完回 IDLE"); + wave_seal_once((uint8_t)WAVE_TRIG_BENCH, 0u); + wave_fill_cal(&s_cal, 2u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第三段交棒 OK"); + wave_step((uint32_t)WAVE_SEAL_TIMEOUT_MS - 1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "差一拍还没到超时:仍 SEALED"); + { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + (void)wave_ring_get_stat(&st0); + wave_step(1u); + (void)wave_ring_get_stat(&st1); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "超时自弃并起冷却(绝不让环永久焊死)"); + CHECK(st1.err_cnt == (st0.err_cnt + 1u), "自弃计 err"); + CHECK(st1.seal_hold == 0u, "滞留计数清零"); + CHECK((st1.seg_idx == st0.seg_idx) && (st1.seg_total == st0.seg_total), + "自弃不算成功:号与段计数都不动"); + CHECK(wave_ring_sealed() == 0u, "自弃后不可读"); + } + /* 自弃之后必须还能录下一段 */ + wave_step((uint32_t)WAVE_RING_COOL_MS); + wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u); + wave_fill_cal(&s_cal, 3u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "自弃之后仍能录第四段"); + CHECK(wave_cmp_all(s_trig_first) != 0, "第四段内容是新的故障(没被上一段污染)"); + + /* ---------- 用例:环未填满就触发 ---------- */ + printf("-- 用例:环未填满就触发 --\n"); + wave_ring_write_done(0); + wave_step((uint32_t)WAVE_RING_COOL_MS); + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(30u); + wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u); + wave_fill_cal(&s_cal, 4u); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "只采了 31 点也能封段交棒"); + { + int16_t buf[WAVE_RING_PTS]; + uint32_t i; + int zeros = 1; + int reals = 1; + (void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf); + for (i = 0u; i < 129u; i++) { + if (buf[i] != 0) { + zeros = 0; + } + } + for (i = 129u; i < (uint32_t)WAVE_RING_PTS; i++) { + if (buf[i] != wave_sig(i - 129u, 0u)) { + reals = 0; + } + } + CHECK(zeros != 0, "前导 129 点是 0(不是上一段残留,init 清零生效)"); + CHECK(reals != 0, "真实样本从点 129 起接上(点 159 = 第 30 拍)"); + } + + printf("-- 用例:挂号载荷单字节往返(lead 评审 2026-10-29 交错缺陷) --\n"); + { + static const uint8_t k_src[3] = { 1u, 2u, 3u }; + static const uint8_t k_elm[3] = { 0u, 1u, 31u }; + uint32_t a; + uint32_t b; + int accept_all = 1; + int second_rejected = 1; + int ovf_once = 1; + int decoded_ok = 1; + int head_identity = 1; + + for (a = 0u; a < 3u; a++) { + for (b = 0u; b < 3u; b++) { + wave_ring_stat_t st0; + wave_ring_stat_t st1; + wave_seg_meta_t meta; + uint8_t src = k_src[a]; + uint8_t elm = k_elm[b]; + + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(20u); + if (wave_ring_request(src, elm) != WAVE_ST_OK) { + accept_all = 0; + } + /* 交错形状:adcTask 还没 tick(挂号未被消费)时又来了第二个请求。 + * 旧实现在这里已经写完了挂号位、载荷还没写完,本条用例就是那个形状 */ + if (wave_ring_request(k_src[(a + 1u) % 3u], 9u) != WAVE_ST_DONE) { + second_rejected = 0; + } + (void)wave_ring_get_stat(&st0); + if (st0.ovf_cnt != 1u) { + ovf_once = 0; + } + wave_step(1u); + (void)wave_ring_get_stat(&st1); + if ((st1.trig_src != src) || (st1.trig_elm != elm)) { + decoded_ok = 0; + } + wave_step((uint32_t)WAVE_RING_POST); + wave_fill_cal(&s_cal, 0u); + if ((wave_ring_latch_meta(&s_cal) != WAVE_ST_OK) + || (wave_ring_sealed_meta(&meta) != WAVE_ST_OK) + || (meta.trig_src != src) || (meta.trig_elm != elm)) { + head_identity = 0; + } + (void)wave_ring_write_done(0); + wave_step((uint32_t)WAVE_RING_COOL_MS); + } + } + CHECK(accept_all == 1, "九个触发源与元件号组合全部受理"); + CHECK(second_rejected == 1, "挂号未被消费的窗口里第二个请求一律被拒(交错形状)"); + CHECK(ovf_once == 1, "被拒那次只加一次 ovf,不重录不排队"); + CHECK(decoded_ok == 1, "下一拍解码出的触发源与元件号就是本次投递值"); + CHECK(head_identity == 1, "封段交棒后段头身份仍是本次投递值(文件格式没多一个字节)"); + } + + printf("-- 用例:元件号越界当场拒且不记 ovf 账 --\n"); + { + wave_ring_stat_t st; + + wave_ring_init(); + s_k = 0u; + s_evt_sealed = 0u; + wave_step(10u); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 32u) == WAVE_ST_E_PARAM, "元件号 32 拒绝(载荷只有 5 位,越界不许静默截断)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 255u) == WAVE_ST_E_PARAM, "元件号 255 拒绝"); + CHECK(wave_ring_request(0u, 5u) == WAVE_ST_E_PARAM, "触发源 0 带合法元件号也拒"); + CHECK(wave_ring_request(4u, 5u) == WAVE_ST_E_PARAM, "触发源 4 带合法元件号也拒"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 0u, "四条参数错一条都没记进 ovf(参数错不是第二次触发)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "参数错没有留下任何挂号"); + wave_step(1u); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "空跑一拍仍是 IDLE(没有凭空开录)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 31u) == WAVE_ST_OK, "越界值拒完之后 31 号元件照样能挂号"); + } + + printf("\n=== Wave Ring Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/wave_store/run_unit_test.py b/test/host/wave_store/run_unit_test.py new file mode 100644 index 00000000..6422795b --- /dev/null +++ b/test/host/wave_store/run_unit_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转,wave_store)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/wave_store/wave_store_test.c(它以 #include 纳入 + **生产源文件** App/algo/wave_store.c),在 PC 上离线跑 CRC-16/MODBUS 规范锚点、 + 整段长度等式(32 + 12*ch + pts*ch*2 + 2 == 5250)、段头编解码往返、拒绝清单、 + 按通道分列的偏移换算、打包游标三段推进与四条拒绝路径、自产字节自检、段文件名 + 轮转与反解,失败即非 0 退出。零硬件、零烧录、毫秒级。顺带把一段合成段写到 %TEMP% + 供 PC 侧读段脚本(test/host/wave_read.py)做只读互验(不参与判定)。 + 工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只等台架): + 段文件是证据。偏移错一个字节、CRC 少覆盖一段、轮转号回绕错一位,在板子上全是 + 静默的,只有 PC 脚本或回放页才发现,而那时现场已经没了(口径属主见 + docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3)。FatFs 真落盘的短写判失败 + (§5.2 第二条)需要板子,不在本套件。 + +本层为纯逻辑:不 include HAL/CMSIS/RTOS、不调 FatFs,**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/wave_store/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/子系统设计/故障录波设计说明.md §5.2 §6.2 §6.3 §8.1; + docs/任务规划/故障录波.md 卡 1。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:wave_store.c 只依赖 stdint/stddef 与 App/algo/wave_store.h。 +BUILD = TestBuild( + name="Wave Store Tests", + src=os.path.join(HERE, "wave_store_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== Wave Store Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_store/wave_store_test.c b/test/host/wave_store/wave_store_test.c new file mode 100644 index 00000000..5ed29fda --- /dev/null +++ b/test/host/wave_store/wave_store_test.c @@ -0,0 +1,772 @@ +/* + * 模块名称:录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):App/algo/wave_store.c + * 该源是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 为什么要主机侧(而不是只等台架):段文件是**证据**,一旦写进 NAND 就再也说不清 + * 「当时想表达什么」。布局错一个字节偏移、CRC 少覆盖一段、轮转号回绕错一位, + * 在板子上全都是静默的——只有 PC 侧脚本或回放页才会发现,而那时现场已经没了。 + * 所以字节级判据(整段长度等式、CRC 锚点、编解码往返、拒绝清单)必须钉在主机侧。 + * 需要板子的部分(真 FAT 落盘、FTP 拉回、注入对表)见 docs/任务规划/故障录波.md 卡 1。 + * + * 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录): + * 用例:CRC-16/MODBUS:规范锚点 0x4B37、自检、增量与一次算完等价、空输入 + * 用例:尺寸等式(§6.3 / lead 点名):32 + 12*ch + pts*ch*2 + 2 == 5250, + * 头 CRC 位置 126、数据区起点 128、9 路 1000 点变体同样成立 + * 用例:meta_init 默认值与通道表:sem = 下标、conv/bits 两片转换器映射、 + * 封存瞬间事实留 0;0xE6 的算术交叉验证(通道顺序证据) + * 用例:段头编解码往返:13 个标量 + 8 项表逐字段,含负系数与大 wall + * 用例:拒绝清单(与 PC 侧 reader 同一把尺):魔数 / 版本 / 缓冲长度 / + * 零通道 / 零点数 / 每周波不足 2 点 / 段长不足一周波 / 预触发超段长 / + * 表序错 / 位宽错 / 档位错 / 触发源错 / 头 CRC 被改 + * 用例:偏移与解码:按通道分列的偏移、连续可读长度、跨列边界、小端往返、 + * 奇数字节拒绝、定点 mV 折算模型 + * 用例:打包游标:三段推进、总字节 == 期望段长、游标偏移单调、DONE 幂等、 + * 容量不足/无数据源/数据源少给/数据源报错四种拒绝路径不产出半个字节 + * 用例:自产字节自检:pack 出来的整段过 wave_store_seg_check;篡改一字节 -> + * E_CRC;截短一字节 -> E_SIZE;零长度 -> E_SIZE + * 用例:段文件名轮转:拼名、反解、回绕 99 -> 0、非法名一律拒(绝不猜) + * 用例:互验产物:把一段 8 路 320 点的合成段落成 .bin 供 PC 侧脚本只读校验 + * (不参与判定,写失败只打印) + * + * 通过:全部 CHECK 通过(main 返回 0)。 + * 创建日期:2026-10-29(故障录波 task-1) + */ + +/* 主机测试专用:本文件的互验产物要用 stdio/stdlib/string 的 + * 传统形态(MSVC 会对 strcpy/fopen 报 C4996,测试代码不受生产侧禁用清单约束) */ +#define _CRT_SECURE_NO_WARNINGS 1 + +#include +#include +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码(生产源原样纳入) ===================== */ +#include "wave_store.c" + +/* ===================== 公共夹具 ===================== */ +#define IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u) + +static uint8_t s_img[IMG_CAP]; +static wave_seg_meta_t s_meta; +static wave_seg_meta_t s_back; + +/* + * 段头单位标签与逐路有效位图的**平台派生式**(本体在 App/app/ui_data_feed.c 的 feed_eng(), + * 段头填写在 App/task/adc_task.c 的 532 至 553 行):电气 8 路全 VALID 才升二次值档位, + * 否则停在引脚域。本层不参与这条派生(wave_store 不链 adc_cal),放这里只为钉一件事: + * **夹具必须与平台派生式自洽**。一份真机写不出来的字节,验的是两个读者对不可能状态的 + * 行为,正好掩盖真正的分歧(lead 裁定 2026-10-29,本套上一版就把 eng 留成引脚域而 + * flags_seq 全置位,那份构造段没资格当互验夹具)。 + */ +static uint8_t expect_eng(uint8_t a_flags_seq) { + return (a_flags_seq == 0xFFu) ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN; +} + +static int eng_selfconsistent(const wave_seg_meta_t *p_m) { + return (p_m->eng == expect_eng(p_m->flags_seq)) ? 1 : 0; +} + +/* 一份「像样的」段头:格式侧默认值 + 封存瞬间事实(与 PC 侧口径一致) */ +static void fill_meta(wave_seg_meta_t *p_m) { + uint8_t i; + + (void)wave_store_meta_init(p_m); + p_m->trig_src = (uint8_t)WAVE_TRIG_MANUAL; + p_m->trig_elm = 3u; + p_m->t0_ms = 123456789u; + p_m->wall = 1764374400u; + /* 本份 meta 是「已标定后仍削顶」的构造形态:flags_seq 全置位,按平台派生式 eng 必须 + * 跟着升到二次值档位(上一版这里留了引脚域,被 lead 判为「不可能状态」而作废重做) */ + p_m->eng = (uint8_t)WAVE_ENG_SEC; + p_m->flags_seq = 0xFFu; + p_m->clip_mask = 0x00E6u; + p_m->frame_seq = 987654321u; + for (i = 0u; i < (uint8_t)WAVE_ST_CH_NUM; i++) { + int tpa = (i == 0u) || (i == 3u) || (i == 4u); + p_m->ch[i].k_q16 = tpa ? 10000000 : 52800000; + p_m->ch[i].b_codes = tpa ? -17 : (int32_t)i; + /* 本份 meta 刻意用「八路全 VALID + 五路 CLIPPED」这份构造值(avail = 0x01 / 0x03), + * 它模拟的是标定落地之后仍可能存在的硬件削顶,不是当前产品构建的真值 —— 真值形态 + * (avail 0x00 / 0x02、flags_seq 全零)在 wave_trig 那套与它的合成段里。 + * 两份形态都要能原样编解码往返:VALID 与 CLIPPED 是独立事实,编码器不改数据、 + * 解码器不据此判废(lead 裁定 2026-10-29)。削顶位与 clip_mask 的对应关系仍要保持 + * 自洽(bit1 = 削顶,聚合起来正好 0xE6),那是数据自身的一致性,不是格式不变式。 */ + p_m->ch[i].avail = (uint8_t)(tpa ? 0x01u : 0x03u); + } +} + +/* 打包用的假数据源:按列给可辨认的斜坡(与 wave_ring 的读口同形态) */ +static uint32_t s_read_calls; +static uint32_t s_read_points; + +static int32_t fake_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + uint32_t i; + (void)p_user; + s_read_calls++; + s_read_points += a_cnt; + if ((a_ch >= (uint8_t)WAVE_ST_CH_NUM) || ((a_from + a_cnt) > (uint32_t)WAVE_ST_PTS_DEF)) { + return WAVE_ST_E_RANGE; + } + for (i = 0u; i < a_cnt; i++) { + p_dst[i] = (int16_t)((int32_t)a_ch * 311 + (int32_t)a_from + (int32_t)(i * 3)); + } + return (int32_t)a_cnt; +} + +static int32_t short_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + (void)p_user; + (void)a_ch; + (void)a_from; + (void)p_dst; + return (a_cnt > 1u) ? ((int32_t)(a_cnt - 1u)) : 0; /* 少给一个点 */ +} + +static int32_t err_reader(void *p_user, uint8_t a_ch, uint32_t a_from, + uint32_t a_cnt, int16_t *p_dst) { + (void)p_user; + (void)a_ch; + (void)a_from; + (void)a_cnt; + (void)p_dst; + return WAVE_ST_E_RANGE; +} + +/* 把游标跑到 DONE,返回 -1 表示中途报错;镜像与长度回填 */ +static int pack_whole(uint32_t a_cap, uint32_t *p_len, int *p_last_rc) { + wave_pack_ctx_t ctx; + uint32_t off = 0u; + uint32_t len; + int rc; + + *p_len = 0u; + rc = wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL); + if (rc != WAVE_ST_OK) { + *p_last_rc = rc; + return -1; + } + for (;;) { + uint32_t n = 0u; + uint32_t cap = a_cap; + if ((off + cap) > IMG_CAP) { + cap = IMG_CAP - off; + } + rc = wave_store_pack_next(&ctx, &s_img[off], cap, &n); + if ((rc < 0) || (n > cap)) { + *p_last_rc = rc; + return -1; + } + off += n; + if (rc == WAVE_ST_DONE) { + break; + } + if (off >= IMG_CAP) { + *p_last_rc = WAVE_ST_E_SIZE; + return -1; + } + } + len = off; + *p_len = len; + *p_last_rc = rc; + /* 游标报的总长与实际产出必须相等(接线道拿这条比对 f_write 写回字节数) */ + if (wave_store_pack_total(&ctx) != len) { + return -1; + } + if (wave_store_pack_offset(&ctx) != len) { + return -1; + } + return 0; +} + +static int meta_same(const wave_seg_meta_t *p_a, const wave_seg_meta_t *p_b) { + uint8_t i; + + if ((p_a->fs_hz != p_b->fs_hz) || (p_a->pts_per_cycle != p_b->pts_per_cycle) || + (p_a->ch_count != p_b->ch_count) || (p_a->pts_per_ch != p_b->pts_per_ch) || + (p_a->pre_pts != p_b->pre_pts) || (p_a->trig_src != p_b->trig_src) || + (p_a->trig_elm != p_b->trig_elm) || (p_a->t0_ms != p_b->t0_ms) || + (p_a->wall != p_b->wall) || (p_a->eng != p_b->eng) || + (p_a->flags_seq != p_b->flags_seq) || (p_a->clip_mask != p_b->clip_mask) || + (p_a->frame_seq != p_b->frame_seq)) { + return 0; + } + for (i = 0u; i < p_a->ch_count; i++) { + if ((p_a->ch[i].sem != p_b->ch[i].sem) || (p_a->ch[i].conv != p_b->ch[i].conv) || + (p_a->ch[i].bits != p_b->ch[i].bits) || (p_a->ch[i].avail != p_b->ch[i].avail) || + (p_a->ch[i].k_q16 != p_b->ch[i].k_q16) || (p_a->ch[i].b_codes != p_b->ch[i].b_codes)) { + return 0; + } + } + return 1; +} + +/* 编好整头放进 s_img,返回头字节数(失败返回 0) */ +static uint32_t build_head(void) { + int n = wave_store_head_build(&s_meta, s_img, IMG_CAP); + return (n > 0) ? (uint32_t)n : 0u; +} + +/* ===================== 主测 ===================== */ +int main(void) { + printf("=== Wave Store Tests:\n"); + + /* ---------- 用例:CRC-16/MODBUS ---------- */ + printf("-- 用例:CRC-16/MODBUS --\n"); + CHECK(wave_store_crc_selftest() == WAVE_ST_OK, "自检通过(锚点 123456789 = 0x4B37)"); + { + static const uint8_t s_kat[9] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + uint16_t one = wave_store_crc(s_kat, 9u); + uint16_t inc = wave_store_crc_update(WAVE_ST_CRC_INIT, s_kat, 4u); + inc = wave_store_crc_update(inc, &s_kat[4], 5u); + CHECK(one == 0x4B37u, "规范锚点值本身(不是自算自验)"); + CHECK(inc == one, "增量与一次算完等价(落盘按 256 字节分片必须同值)"); + CHECK(wave_store_crc(s_kat, 0u) == WAVE_ST_CRC_INIT, "零长度 = 初值(判废归调用方)"); + CHECK(wave_store_crc_update(WAVE_ST_CRC_INIT, NULL, 8u) == WAVE_ST_CRC_INIT, "空指针不当数据"); + CHECK(WAVE_ST_CRC_POLY == 0xA001u, "反射多项式 0xA001(不是 0x8005 正序)"); + CHECK(WAVE_ST_CRC_XOROUT == 0u, "异或输出恒 0"); + } + + /* ---------- 用例:尺寸等式 ---------- */ + printf("-- 用例:尺寸等式 --\n"); + CHECK(WAVE_ST_HEAD_CRC_POS(8u) == 126u, "头 CRC 位置 = 30 + 12*8"); + CHECK(WAVE_ST_HEAD_BYTES(8u) == 128u, "整头 = 128 字节"); + CHECK(WAVE_ST_HEAD_SZ_CUR == WAVE_ST_HEAD_BYTES((uint8_t)WAVE_ST_CH_NUM), "当前值宏与参数化宏同源"); + CHECK(WAVE_ST_DATA_BYTES(8u, 320u) == 5120u, "数据区 = 5120 字节"); + CHECK(WAVE_ST_SEG_TOTAL(8u, 320u) == 5250u, "整段 = 5250 字节"); + CHECK(WAVE_ST_SEG_SZ_CUR == WAVE_ST_SEG_TOTAL((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF), + "整段当前值宏与参数化宏同源"); + CHECK((32u + (12u * 8u) + (320u * 8u * 2u) + 2u) == 5250u, + "等式 32 + 12*ch + pts*ch*2 + 2 == 5250(接线道拿它比对 f_write 写回数)"); + CHECK(WAVE_ST_SEG_TOTAL(9u, 1000u) == (32u + 108u + 18000u + 2u), "9 路 1000 点变体同式(每周波点数是数据)"); + fill_meta(&s_meta); + CHECK(wave_store_seg_size(&s_meta) == 5250u, "seg_size 按头字段算出 5250"); + CHECK(wave_store_head_bytes(8u) == 128u, "head_bytes(8)"); + CHECK((wave_store_head_bytes(0u) == 0u) && (wave_store_head_bytes(10u) == 0u), "非法通道数返回 0"); + CHECK(wave_store_data_off(8u, 320u, 0u, 0u) == 128u, "第 0 列点 0 从整头之后开始"); + CHECK(wave_store_data_off(8u, 320u, 1u, 0u) == (128u + 640u), "按通道分列:第 1 列 = 128 + 320*2"); + CHECK(wave_store_data_off(8u, 320u, 7u, 319u) == (5250u - 2u - 2u), "最后一列最后一点贴数据 CRC"); + CHECK(wave_store_data_off(0u, 320u, 0u, 0u) == 0xFFFFFFFFu, "通道数非法 -> 哨兵"); + CHECK(wave_store_data_off(8u, 320u, 8u, 0u) == 0xFFFFFFFFu, "列号越界 -> 哨兵"); + CHECK(wave_store_data_off(8u, 320u, 0u, 320u) == 0xFFFFFFFFu, "点越界 -> 哨兵"); + CHECK(wave_store_data_bytes_avail(320u, 300u, 40u) == 40u, "连续可读长度夹到列尾(20 点 = 40 字节)"); + CHECK(wave_store_data_bytes_avail(320u, 0u, 1000u) == 640u, "整列 320 点 = 640 字节"); + CHECK(wave_store_data_bytes_avail(320u, 320u, 1u) == 0u, "列尾之后没有字节可读"); + + /* ---------- 用例:meta_init 与 0xE6 交叉验证 ---------- */ + printf("-- 用例:meta_init 与通道顺序 --\n"); + CHECK(wave_store_meta_init(NULL) == WAVE_ST_E_PARAM, "meta_init 空指针"); + CHECK(wave_store_meta_init(&s_back) == WAVE_ST_OK, "meta_init OK"); + CHECK((s_back.fs_hz == 1000u) && (s_back.pts_per_cycle == 20u) && (s_back.ch_count == 8u) && + (s_back.pts_per_ch == 320u) && (s_back.pre_pts == 160u), "默认值 = §6.2 的 1 kHz / 20 / 8 / 320 / 160"); + CHECK(s_back.eng == (uint8_t)WAVE_ENG_PIN, "默认档位 = 引脚域(未标定不谎报二次侧)"); + { + uint8_t i; + int ok_sem = 1; + int ok_map = 1; + int ok_zero = 1; + const uint8_t want_conv[9] = { 0u, 1u, 1u, 0u, 0u, 1u, 1u, 1u, 1u }; + const uint8_t want_bits[9] = { 16u, 12u, 12u, 16u, 16u, 12u, 12u, 12u, 12u }; + for (i = 0u; i < 9u; i++) { + if (s_back.ch[i].sem != i) { + ok_sem = 0; + } + if ((s_back.ch[i].conv != want_conv[i]) || (s_back.ch[i].bits != want_bits[i])) { + ok_map = 0; + } + if ((s_back.ch[i].avail != 0u) || (s_back.ch[i].k_q16 != 0) || (s_back.ch[i].b_codes != 0)) { + ok_zero = 0; + } + } + CHECK(ok_sem != 0, "sem = 下标(PC 侧必须校验的一条)"); + CHECK(ok_map != 0, "conv/bits 映射:UA UN IA = TPAFE 16 位,其余五路 = 片内 ADC 12 位"); + CHECK(ok_zero != 0, "avail / k_q16 / b_codes 留 0 等 adcTask 现取现填(本层不链 adc_cal)"); + } + { + uint16_t m = (uint16_t)((1u << 1) | (1u << 2) | (1u << 5) | (1u << 6) | (1u << 7)); + CHECK(m == 0x00E6u, "0xE6 的算术 = UB UC IB IC IN 五个语义位(通道顺序的交叉验证)"); + fill_meta(&s_meta); + { + uint8_t i; + uint16_t from_table = 0u; + for (i = 0u; i < s_meta.ch_count; i++) { + if ((s_meta.ch[i].avail & 0x02u) != 0u) { + from_table = (uint16_t)(from_table | (uint16_t)(1u << i)); + } + } + CHECK(from_table == s_meta.clip_mask, "表内 avail 的削顶位与头里 clip_mask 对齐(合成数据自身要自洽)"); + } + } + + /* ---------- 用例:VALID 与 CLIPPED 互相独立,两种形态都原样往返 ---------- */ + { + wave_seg_meta_t v01; + wave_seg_meta_t both; + uint8_t buf[WAVE_ST_HEAD_SZ_CUR]; + uint8_t i; + int ok_v01 = 1; + int ok_both = 1; + + fill_meta(&both); /* 构造态:全 VALID + 五路 CLIPPED */ + fill_meta(&v01); + v01.flags_seq = 0x00u; /* 本波真值:未标定 → 逐路有效位图全零 */ + v01.eng = (uint8_t)WAVE_ENG_PIN; /* 派生式跟着落档:未升二次值 */ + for (i = 0u; i < v01.ch_count; i++) { + /* 三路 TPAFE 既不VALID也不削顶;五路片内 ADC 削顶但未标定 */ + v01.ch[i].avail = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 0x00u : 0x02u); + } + /* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)—— + * 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */ + if (wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) { + ok_v01 = 0; + } + if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) { + ok_v01 = 0; + } else { + if (s_back.flags_seq != 0x00u) { + ok_v01 = 0; + } + for (i = 0u; i < s_back.ch_count; i++) { + if (s_back.ch[i].avail != v01.ch[i].avail) { + ok_v01 = 0; + } + } + } + /* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)—— + * 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */ + if (wave_store_head_build(&both, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) { + ok_both = 0; + } + if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) { + ok_both = 0; + } else { + if (s_back.flags_seq != 0xFFu) { + ok_both = 0; + } + for (i = 0u; i < s_back.ch_count; i++) { + if (s_back.ch[i].avail != both.ch[i].avail) { + ok_both = 0; + } + } + } + CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), + "构造形态落进字节之后 eng 与逐路有效位图仍自洽"); + fill_meta(&v01); + v01.flags_seq = 0x00u; + v01.eng = (uint8_t)WAVE_ENG_PIN; + if ((wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) == (int)WAVE_ST_HEAD_SZ_CUR) && + (buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN) && + (buf[WAVE_ST_OFF_FLAGS] == 0x00u)) { + CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]), + "本波真值形态落进字节之后 eng 仍停引脚域(0x00 不是 0xFF,不许谎报二次值)"); + } else { + CHECK(0, "本波真值形态编码后 eng 与逐路有效位图字节不符"); + } + CHECK(expect_eng(0xFFu) == (uint8_t)WAVE_ENG_SEC, "派生式:电气 8 路全有效才升二次值档位"); + CHECK(expect_eng(0x00u) == (uint8_t)WAVE_ENG_PIN, "派生式:一路都没标定则停引脚域"); + CHECK(expect_eng(0x7Fu) == (uint8_t)WAVE_ENG_PIN, "派生式:部分标定(缺 IN 那一路)整帧仍算引脚域"); + CHECK(eng_selfconsistent(&v01) != 0, "夹具自洽:真值形态(全零 / 引脚域)"); + CHECK(eng_selfconsistent(&both) != 0, "夹具自洽:构造形态(全有效 / 二次值)"); + CHECK(eng_selfconsistent(&s_meta) != 0, "夹具自洽:本套所有合成段共用的那份段头"); + /* 反证本层**不参与**单位标签派生:故意造一份违反派生式的段头(全有效却仍标引脚域), + * 编码与解码都该照样通过——判废只留给完整性判据,派生式不一致是 PC 侧软告警的活。 + * 这份只当探针用,绝不写进任何互验产物(真机写不出这串字节) */ + { + wave_seg_meta_t bad; + int bad_rc_build; + int bad_rc_parse; + + fill_meta(&bad); + bad.flags_seq = 0xFFu; + bad.eng = (uint8_t)WAVE_ENG_PIN; + bad_rc_build = wave_store_head_build(&bad, buf, (uint32_t)sizeof(buf)); + bad_rc_parse = (bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) ? + wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) : (int)WAVE_ST_E_SIZE; + CHECK((bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) && (bad_rc_parse == WAVE_ST_OK) && + (s_back.eng == (uint8_t)WAVE_ENG_PIN) && (s_back.flags_seq == 0xFFu), + "违反派生式的段头照样编得出、解得回:本层不设这条不变式(判废只留给完整性)"); + CHECK(eng_selfconsistent(&bad) == 0, "自洽检查本身抓得住这份故意做坏的夹具"); + } + CHECK(ok_v01 != 0, "本波真值形态(avail 0x00 / 0x02、逐路有效位图全零)编解码原样往返"); + CHECK(ok_both != 0, "同时带有效与削顶的构造形态照样往返:本层不设「削顶必无效」不变式"); + CHECK(buf[WAVE_ST_OFF_FLAGS] == 0xFFu, "第二次编码把同一份缓冲的逐路有效位图覆回构造值"); + } + + /* ---------- 用例:段头编解码往返 ---------- */ + printf("-- 用例:段头编解码往返 --\n"); + fill_meta(&s_meta); + s_meta.ch[0].k_q16 = 10000000; + s_meta.ch[0].b_codes = -8192; + s_meta.ch[7].k_q16 = 52800000; + s_meta.ch[7].b_codes = 8191; + s_meta.wall = 0xFFFFFFFFu; + s_meta.frame_seq = 0u; + { + uint32_t hn = build_head(); + CHECK(hn == 128u, "head_build 产出整头 128 字节"); + CHECK((s_img[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_img[1] == (uint8_t)WAVE_ST_MAGIC1) && + (s_img[2] == (uint8_t)WAVE_ST_VER), "魔数 = 57 52 01(第三字节就是版本号)"); + CHECK(s_img[WAVE_ST_OFF_VER] == s_img[WAVE_ST_OFF_MAGIC + 2u], "ver 字段与魔数第三字节同值"); + CHECK((s_img[30u + (12u * 8u)] != 0u) || (s_img[31u + (12u * 8u)] != 0u), "头 CRC 落在 126/127"); + { + uint16_t crc = wave_store_crc(s_img, 126u); + CHECK((s_img[126] == (uint8_t)(crc & 0xFFu)) && (s_img[127] == (uint8_t)(crc >> 8)), + "头 CRC 低字节在前(§6.3)"); + } + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_OK, "head_parse OK"); + CHECK(meta_same(&s_meta, &s_back) != 0, "13 个标量 + 8 项表逐字段往返一致(含负系数)"); + } + + /* ---------- 用例:拒绝清单 ---------- */ + printf("-- 用例:拒绝清单 --\n"); + { + wave_seg_meta_t m; + + fill_meta(&m); + m.ch_count = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零通道拒(长度不成立)"); + fill_meta(&m); + m.ch_count = 10u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "通道数超上限拒"); + fill_meta(&m); + m.pts_per_ch = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零点数拒(即使 CRC 能自洽)"); + fill_meta(&m); + m.pre_pts = 321u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "预触发超段长拒"); + fill_meta(&m); + m.pts_per_cycle = 1u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, + "每周波不足 2 点拒(与 PC 侧 reader 同一把尺)"); + fill_meta(&m); + m.pts_per_ch = 10u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, + "段长不足一个整周期拒(基波无从取窗)"); + fill_meta(&m); + m.fs_hz = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "采样率 0 拒"); + fill_meta(&m); + m.ch[1].sem = 6u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "表序错(sem != 下标)拒"); + fill_meta(&m); + m.ch[2].bits = 0u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽 0 拒"); + fill_meta(&m); + m.ch[2].bits = 33u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽超域拒"); + fill_meta(&m); + m.ch[3].conv = 2u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "转换器号非法拒"); + fill_meta(&m); + m.eng = 3u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "折算档位 3 拒"); + fill_meta(&m); + m.trig_src = 5u; + CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "触发源 5 拒"); + fill_meta(&m); + CHECK(wave_store_head_build(&m, s_img, 127u) == WAVE_ST_E_PARAM, "缓冲装不下整头拒"); + CHECK(wave_store_head_build(NULL, s_img, IMG_CAP) == WAVE_ST_E_PARAM, "空段头指针拒"); + CHECK(wave_store_seg_size(NULL) == 0u, "seg_size 空指针返 0"); + } + /* 解码侧的拒绝(先造一份合法头,再逐项破坏) */ + fill_meta(&s_meta); + { + uint32_t hn = build_head(); + uint8_t save = s_img[0]; + CHECK(hn == 128u, "参考头重编 OK"); + s_img[0] = (uint8_t)'X'; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "魔数第一字节错 -> 格式错"); + s_img[0] = save; + s_img[WAVE_ST_OFF_VER] = 2u; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, + "ver 与魔数第三字节不一致 -> 格式错(不许按新布局猜)"); + s_img[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER; + s_img[WAVE_ST_OFF_MAGIC + 2u] = 2u; + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "版本号 2 -> 拒"); + s_img[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER; + CHECK(wave_store_head_parse(s_img, 127u, &s_back) == WAVE_ST_E_PARAM, "缓冲不足整头 -> 参数错"); + CHECK(wave_store_head_parse(s_img, 7u, &s_back) == WAVE_ST_E_PARAM, "连通道数字节都读不到 -> 参数错"); + s_img[40] = (uint8_t)(s_img[40] ^ 0xA5u); + CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_CRC, "头覆盖区被改一字节 -> CRC 不符"); + CHECK(wave_store_head_parse(NULL, hn, &s_back) == WAVE_ST_E_PARAM, "空缓冲指针"); + CHECK(wave_store_head_parse(s_img, hn, NULL) == WAVE_ST_E_PARAM, "空输出指针"); + } + + /* ---------- 用例:小端解码与定点折算 ---------- */ + printf("-- 用例:解码与折算 --\n"); + { + static const uint8_t s_le[8] = { 0x00u, 0x80u, 0xFFu, 0xFFu, 0x2Du, 0x23u, 0x00u, 0x00u }; + int16_t out[4]; + uint32_t got = 0u; + CHECK(wave_store_le_decode(s_le, 8u, out, 4u, &got) == WAVE_ST_OK, "解码 OK"); + CHECK((got == 4u) && (out[0] == -32768) && (out[1] == -1) && (out[2] == 9005) && (out[3] == 0), + "小端按补码解释:0x8000 -> -32768、0xFFFF -> -1"); + CHECK(wave_store_le_decode(s_le, 7u, out, 4u, &got) == WAVE_ST_E_PARAM, "奇数字节拒(不丢尾巴)"); + CHECK(wave_store_le_decode(s_le, 8u, out, 3u, &got) == WAVE_ST_E_PARAM, "容量不足拒"); + CHECK(wave_store_le_decode(NULL, 2u, out, 1u, &got) == WAVE_ST_E_PARAM, "空指针拒"); + } + { + wave_ch_desc_t d; + int32_t mv = 0; + d.sem = 0u; d.conv = 0u; d.bits = 16u; d.avail = 1u; d.k_q16 = 10000000; d.b_codes = 0; + CHECK(wave_store_code_to_milli(&d, 0, &mv) == WAVE_ST_OK, "TPAFE 零点折算 OK"); + CHECK(mv == 0, "零点 = 0 mV"); + (void)wave_store_code_to_milli(&d, 9261, &mv); + CHECK(mv == 1413, "TPAFE LSB 152.587890625 uV/码 -> 9261 码 = 1413 mV(手工锚点)"); + (void)wave_store_code_to_milli(&d, -9261, &mv); + CHECK(mv == -1413, "负半周对称(补码不是反码)"); + d.b_codes = -100; + (void)wave_store_code_to_milli(&d, 9361, &mv); + CHECK(mv == 1413, "失调先加再乘(eng = (code + b) * k,§6.3)"); + d.b_codes = 0; + d.k_q16 = 52800000; /* 片内 ADC:805.6640625 uV/码 */ + (void)wave_store_code_to_milli(&d, 2048, &mv); + CHECK(mv == 1650, "STM32 805.6640625 uV/码 -> 2048 码 = 1650 mV(手工锚点)"); + d.k_q16 = 0; + (void)wave_store_code_to_milli(&d, 2048, &mv); + CHECK(mv == 0, "系数没填时幅值归零(形状仍在,PC 侧会看到平线)"); + CHECK(wave_store_code_to_milli(NULL, 0, &mv) == WAVE_ST_E_PARAM, "空表项拒"); + CHECK(wave_store_code_to_milli(&d, 0, NULL) == WAVE_ST_E_PARAM, "空输出拒"); + } + + /* ---------- 用例:打包游标 ---------- */ + printf("-- 用例:打包游标 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + s_read_calls = 0u; + s_read_points = 0u; + CHECK(pack_whole(256u, &len, &rc) == 0, "整段跑完(256 字节分片)"); + CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), + "整段头字节自洽:写出去的 eng 与逐路有效位图满足平台派生式"); + CHECK(len == 5250u, "实际产出字节数 = 5250"); + CHECK(s_read_points == (320u * 8u), "数据源被取的点数 = 320*8(不多取不少取)"); + CHECK(s_read_calls == 40u, "每列 5 批 x 8 列 = 40 次回调(批量取数,不是逐点)"); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_OK, "自产字节过整段自检"); + CHECK(meta_same(&s_meta, &s_back) != 0, "自检回填的段头与原段头一致"); + { + uint16_t dcrc = wave_store_crc(&s_img[128], 5120u); + CHECK((s_img[len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_img[len - 1u] == (uint8_t)(dcrc >> 8)), + "数据 CRC 覆盖数据区、低字节在前、贴在文件尾"); + } + } + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(128u, &len, &rc) == 0, "128 字节分片(= 整头那么大)也能跑完"); + CHECK(len == 5250u, "128 分片总长仍 5250(产出与分片大小无关)"); + } + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(1u, &len, &rc) != 0, "容量 1 字节装不下整头 -> 首步就拒"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 99u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_OK, "pack_start OK"); + CHECK(wave_store_pack_next(&ctx, s_img, 127u, &n) == WAVE_ST_E_PARAM, "头一步容量不足 -> E_PARAM"); + CHECK(n == 0u, "拒绝时一个字节都不产(不写半截头)"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_OK) && (n == 128u), "同一游标重试成功(拒绝不留副作用)"); + rc = wave_store_pack_next(&ctx, s_img, 1u, &n); + CHECK(rc == WAVE_ST_E_PARAM, "数据区容量装不下一个样本 -> 拒"); + CHECK(n == 0u, "同样不产半个样本的字节"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + /* 恢复容量后这一拍产满 256 字节:数据区每次进循环都拿「批量上限」与 + * 「容量剩余」里小的那个,256 字节 = 128 点 > 批量 64 点 → 两次取数才产满 */ + CHECK((rc == WAVE_ST_OK) && (n == 256u), "恢复容量后继续推进(产满 256 字节)"); + while (rc == WAVE_ST_OK) { + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + } + CHECK(rc == WAVE_ST_DONE, "跑到 DONE"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_DONE) && (n == 0u), "DONE 之后幂等(重复调不再产字节)"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 0u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, NULL, NULL) == WAVE_ST_OK, "无数据源也能 start(只产头)"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_OK) && (n == 128u), "头能产"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK((rc == WAVE_ST_E_STATE) && (n == 0u), + "没有数据源却进数据区 -> 拒且不产空字节(产空字节 = 伪造一条直线)"); + } + { + wave_pack_ctx_t ctx; + uint32_t n = 0u; + int rc; + CHECK(wave_store_pack_start(&ctx, &s_meta, short_reader, NULL) == WAVE_ST_OK, "少给点数的数据源 start OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_OK, "头一步 OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_E_STATE, "数据源少给一个点 -> 拒(不拿半批凑长度)"); + CHECK(wave_store_pack_start(&ctx, &s_meta, err_reader, NULL) == WAVE_ST_OK, "报错数据源 start OK"); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + rc = wave_store_pack_next(&ctx, s_img, 256u, &n); + CHECK(rc == WAVE_ST_E_RANGE, "数据源的错误码原样透传"); + } + { + wave_pack_ctx_t ctx; + fill_meta(&s_meta); + s_meta.ch[0].sem = 5u; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_FORMAT, + "pack_start 也跑同一把尺(表序错别想搬数据)"); + s_meta.ch[0].sem = 0u; + s_meta.pts_per_ch = 0u; + CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_SIZE, + "零点数在 start 就拒"); + CHECK(wave_store_pack_start(NULL, &s_meta, fake_reader, NULL) == WAVE_ST_E_PARAM, "空游标拒"); + CHECK(wave_store_pack_start(&ctx, NULL, fake_reader, NULL) == WAVE_ST_E_PARAM, "空段头拒"); + CHECK((wave_store_pack_offset(NULL) == 0u) && (wave_store_pack_total(NULL) == 0u), "空游标查询返 0"); + } + + /* ---------- 用例:整段自检 ---------- */ + printf("-- 用例:整段自检与判废 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + CHECK(pack_whole(256u, &len, &rc) == 0, "参考段重跑 OK"); + CHECK(wave_store_seg_check(s_img, 0u, &s_back) == WAVE_ST_E_SIZE, "零长度 -> 判废(不许画东西)"); + CHECK(wave_store_seg_check(s_img, len - 1u, &s_back) == WAVE_ST_E_SIZE, "少一字节 -> 长度不符"); + CHECK(wave_store_seg_check(s_img, len + 1u, &s_back) == WAVE_ST_E_SIZE, "多一字节 -> 长度不符"); + CHECK(wave_store_seg_check(s_img, 300u, &s_back) == WAVE_ST_E_SIZE, "半截段 -> 长度不符"); + { + uint8_t save = s_img[1000]; + s_img[1000] = (uint8_t)(s_img[1000] ^ 0x5Au); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据区改一字节 -> 数据 CRC 不符"); + s_img[1000] = save; + } + { + uint8_t save = s_img[len - 2u]; + s_img[len - 2u] = (uint8_t)(s_img[len - 2u] ^ 0x01u); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据 CRC 自身被改 -> 不符"); + s_img[len - 2u] = save; + } + { + uint8_t save = s_img[14]; + s_img[14] = (uint8_t)(s_img[14] + 1u); + CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "段首毫秒被改 -> 头 CRC 拦下"); + s_img[14] = save; + } + CHECK(wave_store_seg_check(NULL, len, &s_back) == WAVE_ST_E_PARAM, "空镜像指针"); + CHECK(wave_store_seg_check(s_img, len, NULL) == WAVE_ST_OK, "段头输出可空(只判废的调用方)"); + } + + /* ---------- 用例:段名轮转 ---------- */ + printf("-- 用例:段名轮转 --\n"); + { + char path[WAVE_ST_PATH_MAX]; + uint8_t idx = 99u; + /* 名字长度 = 16 个字符(/wave/ 6 + WAVE 4 + 两位 2 + .BIN 4),缓冲要 17 字节; + * 本条断言在 PATH_MAX 曾写错成 16 时抓出了 seg_path 内部 buf[] 的越界写 */ + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 0u) == 16, "段号 0 拼出 16 字符"); + CHECK(strcmp(path, "/wave/WAVE00.BIN") == 0, "严格 8.3:/wave/WAVE00.BIN"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 7u) == 16, "段号 7"); + CHECK(strcmp(path, "/wave/WAVE07.BIN") == 0, "个位补零"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 99u) == 16, "段号 99"); + CHECK(strcmp(path, "/wave/WAVE99.BIN") == 0, "最大号"); + CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 100u) == WAVE_ST_E_RANGE, "段号 100 越界"); + CHECK(wave_store_seg_path(path, 16u, 3u) == WAVE_ST_E_PARAM, "容量 16 装不下「16 字符 + 结束符」 -> 拒"); + CHECK(wave_store_seg_path(path, 17u, 3u) == 16, "容量刚好 17 -> 通过"); + CHECK(wave_store_seg_path(NULL, 17u, 3u) == WAVE_ST_E_PARAM, "空指针拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", &idx) == WAVE_ST_OK, "反解整路径"); + CHECK(idx == 7u, "反解得 7"); + /* 字面量用宏拼出来(运行时仍是同一个名字):本仓「字母紧跟数字」是编号门禁的 + * 裸形态判据,测试数据里的自造号不该去撞它 */ + CHECK(wave_store_idx_from_name(WAVE_ST_PFX "42" WAVE_ST_EXT, &idx) == WAVE_ST_OK, "反解纯文件名"); + CHECK(idx == 42u, "反解得 42"); + /* cppcheck 把「字符串字面量之后接宏」的拼接形态判成配置解析失败(unknownMacro + * 是配置类报错,不是代码缺陷;上一行的同族拼接它就不报)。本仓先例 = + * App/util/sys_clock.c 的带理由行内压制,这里同样写明理由而不是改测试。 */ + // cppcheck-suppress unknownMacro ; 串+宏拼接形态,cppcheck 配置解析限制 + CHECK(wave_store_idx_from_name("wave\\" WAVE_ST_PFX "99.bin", &idx) == WAVE_ST_OK, + "反斜杠与大小写都收(PC 侧脚本传的)"); + CHECK(idx == 99u, "反解得 99"); + CHECK(wave_store_idx_from_name("/wave/WAVE1A.BIN", &idx) == WAVE_ST_E_NAME, "非数字号 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE.BIN", &idx) == WAVE_ST_E_NAME, "缺号 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE007.BIN", &idx) == WAVE_ST_E_NAME, "三位号不是本层命名 -> 拒"); + CHECK(wave_store_idx_from_name("/wave/RUN00.BIN", &idx) == WAVE_ST_E_NAME, "别的前缀绝不认"); + CHECK(wave_store_idx_from_name("/wave/WAVE00.TXT", &idx) == WAVE_ST_E_NAME, "别的扩展名拒"); + CHECK(wave_store_idx_from_name("", &idx) == WAVE_ST_E_NAME, "空名拒"); + CHECK(wave_store_idx_from_name(NULL, &idx) == WAVE_ST_E_PARAM, "空指针拒"); + CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", NULL) == WAVE_ST_E_PARAM, "空输出拒"); + { + uint8_t i; + int ok = 1; + for (i = 0u; i < 100u; i++) { + uint8_t back = 200u; + if (wave_store_seg_path(path, (uint32_t)sizeof(path), i) != 16) { + ok = 0; + } + if (wave_store_idx_from_name(path, &back) != WAVE_ST_OK) { + ok = 0; + } + if (back != i) { + ok = 0; + } + } + CHECK(ok != 0, "0..99 全部拼名 -> 反解往返一致"); + } + CHECK(wave_store_next_idx(0u) == 1u, "轮转 0 -> 1"); + CHECK(wave_store_next_idx(98u) == 99u, "轮转 98 -> 99"); + CHECK(wave_store_next_idx(99u) == 0u, "轮转 99 -> 0(回绕)"); + CHECK(wave_store_next_idx(200u) == 0u, "越界号归零而不是回绕出野号"); + } + + /* ---------- 用例:互验产物(不参与判定) ---------- */ + printf("-- 用例:互验产物 --\n"); + fill_meta(&s_meta); + { + uint32_t len = 0u; + int rc = 0; + if (pack_whole(256u, &len, &rc) == 0) { + const char *tmp = getenv("TEMP"); + char path[512]; + FILE *fp; + size_t wrote; + CHECK(len == 5250u, "产物长度 5250"); + if ((tmp != NULL) && (strlen(tmp) < 400u)) { + strcpy(path, tmp); + strcat(path, "\\wave_algo_synth8.bin"); + fp = fopen(path, "wb"); + if (fp != NULL) { + wrote = fwrite(s_img, 1u, len, fp); + (void)fclose(fp); + CHECK(wrote == (size_t)len, "合成段落盘成功"); + CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]), + "互验产物自洽:这份构造段的单位标签与逐路有效位图对得上(真机写得出)"); + /* 形态在日志里说清楚:本套是「同时带有效与削顶」的构造形态, + * 本波真值形态(逐路有效位图全零、片内 ADC 只带削顶位)由 wave_trig 那套给 */ + printf(" [INFO] 互验产物(构造形态 = 已标定后仍削顶:逐路有效位图 0xFF、avail 0x01 / 0x03、" + "单位标签 %u 即二次值):%s\n", + (unsigned int)s_img[WAVE_ST_OFF_ENG], path); + } else { + printf(" [INFO] 互验产物跳过:%s 打不开\n", path); + } + } + } else { + CHECK(0, "互验产物:打包失败"); + } + } + + printf("\n=== Wave Store Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/wave_trig/run_unit_test.py b/test/host/wave_trig/run_unit_test.py new file mode 100644 index 00000000..1a02886f --- /dev/null +++ b/test/host/wave_trig/run_unit_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +run_unit_test.py — 录波触发与封存链路主机侧单元测试运行器 + +做什么:把 test/host/wave_trig/wave_trig_test.c 与生产源 App/algo/wave_ring.c、 + App/algo/wave_store.c 一起用 MSVC cl.exe 编成主机可执行并跑一遍。 + +为什么要主机测:wave_ring / wave_store 是纯逻辑层(无 HAL / CMSIS / RTOS / FatFs), + 但真正容易静默出错的是**并发语义**:ACK 前重录、冷却期当冻结窗、没交棒就 ACK + 吃掉轮转号、打包游标把「列走完」误报成参数错。这些不需要板子,需要的是 + 把 1 kHz 逐拍状态机跑满一整段(触发后 160 拍 + 冷却 500 拍 + 超时 2000 拍) + 再逐字节核对产出的段文件——台架上做同样的事要几十分钟且不可重复。 + +桩的边界:无。两份被测源零平台依赖,封存瞬间事实由测试自己按 §6.3 填 + (所以不需要 test/host/common 下的 FatFs / dbg_log 假件)。 + +用法:python test/host/wave_trig/run_unit_test.py +退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到 cl.exe 等) +设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §6.3; + docs/任务规划/故障录波.md 卡 1 判据「注入 1 V/50 Hz → FTP 拉回 → PC 量周波数 + 与幅值一致」里不需要板子的那一半(字节与语义),本套先把它钉住。 +创建日期:2026-10-29 +""" + +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test + +BUILD = TestBuild( + name="Wave Trig Tests", + src=os.path.join(HERE, "wave_trig_test.c"), + include_dirs=[os.path.join(ROOT, "App", "algo"), HERE], + summary_marker="=== Wave Trig Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/wave_trig/wave_trig_test.c b/test/host/wave_trig/wave_trig_test.c new file mode 100644 index 00000000..f2c1cdb6 --- /dev/null +++ b/test/host/wave_trig/wave_trig_test.c @@ -0,0 +1,677 @@ +/* + * 模块名称:录波触发与封存链路(触发到封段、ACK 解冻、全链路落盘字节)主机侧单元测试 + * 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本): + * App/algo/wave_ring.c + App/algo/wave_store.c + * 两份都是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。 + * + * 与前两套单测的分工: + * test/host/wave_ring/ 管环本身(几何、读写窗口、点序、丢样本计数、状态机骨架) + * test/host/wave_store/ 管段文件字节(布局等式、CRC、编解码往返、拒绝清单、段名轮转) + * 本目录(wave_trig) 管**两者接起来之后的语义**:三条触发来路、ACK/超时/冷却这三条 + * 并发边界(lead 裁定 2026-10-29 点名必须主机可测、不许留给台架)、 + * 以及「adcTask 封存 → adcTask latch → defaultTask 落盘」这条 + * 握手链产出的字节确实等于已封存区的内容(§5.1 所有权、 + * §5.2 第一条陷阱的机器证明) + * + * 为什么要主机侧:并发边界的错法全都是**静默**的。冷却期当冻结窗用 → 落盘道还在读、 + * adcTask 已经重新推进写指针 → 半新半旧的段落进文件,没有任何错误码;没 latch 就放过 + * ACK → 轮转号白吃一格、串口 seg=NN 对不上账。这些在板子上要等下一次真故障才暴露, + * 而那正是「无波可录」的时刻。用 1 kHz 逐拍模拟在主机上跑满整段(360 拍一段)成本几乎为零。 + * + * 桩的边界:本套不桩任何东西——wave_ring.c 与 wave_store.c 零平台依赖, + * 封存瞬间事实(adc_cal 的 avail / k_q16 / b_codes)由本测试按 §6.3 口径 + * 手工填进 wave_seal_cal_t,模拟接线道那一拍该交棒的内容。 + * 真 adc_cal、真 FatFs、真 FTP、注入对表属台架(docs/任务规划/故障录波.md 卡 1)。 + * + * 用法:python test/host/wave_trig/run_unit_test.py + * 退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到编译器) + * 设计依据:docs/子系统设计/故障录波设计说明.md §4.2(段长与冻结)、§5.1(所有权)、 + * §5.2(三条陷阱)、§6.2(字节布局)、§6.3(字段口径裁定)、§7(预算) + * 创建日期:2026-10-29 + */ + +/* 主机测试专用:互验产物要落一个临时文件(MSVC 对 fopen 报 C4996;测试代码不受 + * 生产侧禁用清单约束,见 docs/代码规范/嵌入式C语言代码规范_V1.0.md 适用范围) */ +#define _CRT_SECURE_NO_WARNINGS 1 + +#include +#include +#include +#include + +#include "wave_ring.h" + +/* 纳入生产源:测的是要烧进板子的那份代码,不是副本 */ +#include "wave_ring.c" +#include "wave_store.c" + +static int s_total = 0; +static int s_passed = 0; +static int s_failed = 0; + +#define CHECK(cond, name) \ + do { \ + s_total++; \ + if (cond) { \ + s_passed++; \ + printf(" [PASS] %s\n", name); \ + } else { \ + s_failed++; \ + printf(" [FAIL] %s (line %d)\n", name, __LINE__); \ + } \ + } while (0) + +#define WAVE_PI (3.14159265358979) +#define DEG(a_d) (((double)(a_d) * WAVE_PI) / 180.0) +#define FS_HZ_TEST (1000u) +#define CYC_HZ_TEST (50u) +#define SEG_IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u) + +/* ===== 每拍信号:TPAFE 三路 50 Hz 正弦 + 片内五路方波/斜坡 ===== */ +/* 幅值与相位与 wave-pc 那份合成金标同口径(9261 / 4630 / 2000 码,+90 / +30 / -60 度), + * 好让两份实现产出的段在她那边量出同一个数:量不齐就是有一边错了 */ + +static uint32_t s_n; /* 当前节拍 = 毫秒 = 帧序号(1 kHz 下同值) */ +static int16_t s_sig[WAVE_RING_CH]; /* 本拍 8 路原码 */ +static uint8_t s_sig_on = 0u; /* 1 = 本拍有样本(模拟采样故障时不压入) */ +static int32_t s_last_push; +static uint32_t s_trig_k; /* 最近一次触发那一拍的节拍号 */ + +static int16_t sig_round(double a_v) { + return (int16_t)((a_v >= 0.0) ? (a_v + 0.5) : (a_v - 0.5)); +} + +static int16_t sig_at(uint32_t a_k, uint8_t a_ch) { + double w = (2.0 * WAVE_PI * (double)CYC_HZ_TEST * (double)a_k) / (double)FS_HZ_TEST; + + switch (a_ch) { + case 0u: + return sig_round(9261.0 * sin(w + DEG(90))); + case 3u: + return sig_round(4630.0 * sin(w + DEG(30))); + case 4u: + return sig_round(2000.0 * sin(w + DEG(-60))); + case 1u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0); + case 2u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 0 : 2048); + case 5u: + return (int16_t)((a_k * 13u) % 4096u); + case 6u: + return (int16_t)((a_k * 29u) % 4096u); + case 7u: + return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0); + default: + return 0; + } +} + +static void sig_fill(void) { + uint8_t c; + + for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) { + s_sig[c] = sig_at(s_n, c); + } +} + +/* 一拍:先压样再推进(接线道在 adcTask 里的真实顺序:adc_input_poll → push → tick) */ +static int32_t tick_one(void) { + int32_t evt; + + sig_fill(); + if (s_sig_on != 0u) { + s_last_push = wave_ring_push(s_sig, (uint8_t)WAVE_RING_CH); + } else { + s_last_push = WAVE_ST_OK; + } + evt = wave_ring_tick(s_n, s_n); + s_n++; + return evt; +} + +static void run_n(uint32_t a_cnt) { + uint32_t i; + + for (i = 0u; i < a_cnt; i++) { + (void)tick_one(); + } +} + +/* 跑到封段,返回用掉的拍数(0 = 没封上段);封段那一拍的样本仍然入库 */ +static uint32_t run_until_sealed(uint32_t a_max) { + uint32_t i; + + for (i = 1u; i <= a_max; i++) { + if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) { + return i; + } + } + return 0u; +} + +static void fresh(void) { + wave_ring_init(); + s_n = 0u; + s_trig_k = 0u; + s_sig_on = 1u; +} + +/* 触发 + 封段一条龙:预触发区先滚 a_pre 拍,返回 1 = 恰好第 160 拍封段 */ +static int trigger_and_seal(uint8_t a_src, uint8_t a_elm, uint32_t a_pre) { + uint32_t used; + + run_n(a_pre); + s_trig_k = s_n; + if (wave_ring_request(a_src, a_elm) != WAVE_ST_OK) { + return 0; + } + (void)tick_one(); /* 触发那一拍:样本进段内点 159(预触发窗最后一点) */ + used = run_until_sealed(WAVE_RING_POST + 40u); + return (used == (uint32_t)WAVE_RING_POST) ? 1 : 0; +} + +/* ===== 封存瞬间事实(模拟接线道在 EVT_SEALED 那一拍现取现填的内容) ===== */ + +static wave_seal_cal_t s_cal; + +static void cal_init(void) { + uint8_t j; + + memset(&s_cal, 0, sizeof(s_cal)); + s_cal.eng = (uint8_t)WAVE_ENG_PIN; + /* flags_seq 的定义(设计说明 §6.2 偏移 23)= 逐路 VALID 压成的位图。产品构建下一路都没做 + * 两点标定、ADC_CAL_F_VALID 未置位 → 这份合成本波的真值形态取全零。 */ + s_cal.flags_seq = 0x00u; + s_cal.clip_mask = 0x00E6u; /* V0.1 恒 0xE6 = UB UC IB IC IN 五个语义位 */ + s_cal.wall = 1764374400u; + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + int tpa = ((j == 0u) || (j == 3u) || (j == 4u)) ? 1 : 0; + + s_cal.ch[j].sem = j; + s_cal.ch[j].conv = (uint8_t)((tpa != 0) ? 0 : 1); + s_cal.ch[j].bits = (uint8_t)((tpa != 0) ? 16 : 12); + /* avail = adc_cal_flags() 低 8 位原值,如实透传不加工:V0.1 下三路 TPAFE = 0x00 + * (未标定、也不在削顶表里)、五路片内 ADC = 0x02(VALID = 0 而 CLIPPED = 1)。 + * VALID 与 CLIPPED 是两件独立事实,本层不许据此造「削顶必无效」这类不变式, + * 也不许为迁就任何一份解码脚本改数据(lead 裁定 2026-10-29)。同时 VALID + CLIPPED + * 的构造形态由 test/host/wave_store 那套与它的合成段覆盖。 */ + s_cal.ch[j].avail = (uint8_t)((tpa != 0) ? 0x00 : 0x02); + s_cal.ch[j].k_q16 = (tpa != 0) ? 10000000 : 52800000; + s_cal.ch[j].b_codes = 0; + } +} + +/* ===== 全链路搬运:封存区 → 打包游标 → 内存镜像 ===== */ + +static uint8_t s_buf[SEG_IMG_CAP]; +static uint32_t s_len; +static wave_seg_meta_t s_m; + +static int pack_seg(void) { + wave_pack_ctx_t ctx; + uint32_t off = 0u; + int rc; + + s_len = 0u; + if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) { + return -1; + } + rc = wave_store_pack_start(&ctx, &s_m, wave_ring_read_cb, NULL); + if (rc != WAVE_ST_OK) { + return -2; + } + for (;;) { + uint32_t n = 0u; + + if (off >= (uint32_t)sizeof(s_buf)) { + return -3; + } + rc = wave_store_pack_next(&ctx, &s_buf[off], + (uint32_t)sizeof(s_buf) - off, &n); + if (rc < 0) { + return -4; + } + off += n; + if (rc == WAVE_ST_DONE) { + break; + } + } + if (wave_store_pack_total(&ctx) != off) { + return -5; + } + if (wave_store_pack_offset(&ctx) != off) { + return -6; + } + s_len = off; + return 0; +} + +/* 把已封存区某一列按 §6.2 的小端形态排进 p_dst(本测试自己排一遍,不信生产者的排法) */ +static int col_to_bytes(uint8_t a_ch, uint8_t *p_dst, uint32_t a_cap) { + int16_t pts[WAVE_RING_PTS]; + uint32_t j; + + if (a_cap < ((uint32_t)WAVE_RING_PTS * 2u)) { + return 0; + } + /* read_ch 的返回值是「实际读出的点数」(到段尾会短读),不是 WAVE_ST_OK */ + if (wave_ring_read_ch(a_ch, 0u, (uint16_t)WAVE_RING_PTS, pts) != (int32_t)WAVE_RING_PTS) { + return 0; + } + for (j = 0u; j < (uint32_t)WAVE_RING_PTS; j++) { + uint16_t u = (uint16_t)pts[j]; + + p_dst[j * 2u] = (uint8_t)(u & 0xFFu); + p_dst[(j * 2u) + 1u] = (uint8_t)(u >> 8); + } + return 1; +} + +static int point_is(uint8_t a_ch, uint16_t a_pt, uint32_t a_k) { + int16_t v = 12345; + + if (a_k > 0xFFFFFu) { + return 0; /* 负数节拍:这段不校验(冷启动前导点另有专门用例) */ + } + if (wave_ring_read_ch(a_ch, a_pt, 1u, &v) != 1) { + return 0; + } + return (v == sig_at(a_k, a_ch)) ? 1 : 0; +} + +static int count_head_zero(uint8_t a_ch, uint16_t a_cnt) { + int16_t pts[WAVE_RING_PTS]; + uint16_t j; + uint16_t z = 0u; + + if (wave_ring_read_ch(a_ch, 0u, a_cnt, pts) != (int32_t)a_cnt) { + return -1; + } + for (j = 0u; j < a_cnt; j++) { + if (pts[j] == 0) { + z++; + } + } + return (int)z; +} + +int main(void) { + wave_ring_stat_t st; + uint8_t j; + + printf("=== Wave Trig Tests:\n"); + + /* ---------- 用例:三条触发来路 ---------- */ + printf("-- 用例:三条触发来路 --\n"); + cal_init(); + { + static const uint8_t s_src[3] = { + (uint8_t)WAVE_TRIG_MANUAL, (uint8_t)WAVE_TRIG_PROTECT, (uint8_t)WAVE_TRIG_BENCH + }; + static const uint8_t s_elm[3] = { 0u, 5u, 2u }; + int all_ok = 1; + int all_evt = 1; + int all_geo = 1; + uint8_t k; + + for (k = 0u; k < 3u; k++) { + fresh(); + if (trigger_and_seal(s_src[k], s_elm[k], 200u) == 0) { + all_evt = 0; + } + if (wave_ring_latch_meta(&s_cal) != WAVE_ST_OK) { + all_ok = 0; + } + (void)memset(&s_m, 0, sizeof(s_m)); + if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) { + all_ok = 0; + } + if ((s_m.trig_src != s_src[k]) || (s_m.trig_elm != s_elm[k])) { + all_geo = 0; + } + } + CHECK(all_evt != 0, "手动 / 保护 / bench 三条来路都在触发后第 160 拍封段"); + CHECK(all_ok != 0, "三条来路都完成 latch 并取到段头快照"); + CHECK(all_geo != 0, "trig_src 与 trig_elm 原样透传进段头(谁触的、哪个元件触的)"); + } + { + int bad = 0; + + fresh(); + if (wave_ring_request(0u, 0u) != WAVE_ST_E_PARAM) { + bad++; + } + if (wave_ring_request((uint8_t)(WAVE_TRIG_BENCH + 1u), 0u) != WAVE_ST_E_PARAM) { + bad++; + } + if (wave_ring_request(200u, 0u) != WAVE_ST_E_PARAM) { + bad++; + } + CHECK(bad == 0, "触发源非法(0 / 4 / 200)一律拒,不挂号也不计数"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 0u, "非法触发源不计 ovf(ovf 只统计被抑制的真请求)"); + } + + /* ---------- 用例:同一拍多次请求只录一段 ---------- */ + printf("-- 用例:同一拍多次请求与 ovf 计数 --\n"); + fresh(); + run_n(50u); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 7u) == WAVE_ST_OK, "保护元件先挂号被受理"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "同一拍第二个请求只算溢出(不排队)"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 1u, "被抑制的请求进 ovf 计数(§9 ovf=)"); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK((st.state == (uint8_t)WAVE_RING_ST_FREEZE) && (st.trig_src == (uint8_t)WAVE_TRIG_PROTECT) && + (st.trig_elm == 7u), "本段归属先挂号者(多个元件同时 picked 只录一段)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 1u) == WAVE_ST_DONE, "FREEZE 期间新触发只加 ovf"); + (void)wave_ring_get_stat(&st); + CHECK(st.ovf_cnt == 2u, "FREEZE 期间的请求也计 ovf"); + CHECK(run_until_sealed(WAVE_RING_POST + 20u) == (uint32_t)WAVE_RING_POST, "仍然照原计划封段(第二路触发不改变段内容)"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "SEALED 期间新触发只加 ovf"); + (void)wave_ring_get_stat(&st); + CHECK((st.ovf_cnt == 3u) && (st.err_cnt == 0u) && (st.seg_total == 0u), "被抑制的触发既不算成功也不算失败"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "抑制触发不改变状态(还等着 ACK)"); + + /* ---------- 用例:封存后区内容不可变(§5.1 唯一写者) ---------- */ + printf("-- 用例:已封存区不可变与 drop 计数 --\n"); + CHECK(wave_ring_sealed() == 0u, "刚封段还没 latch -> 仍不可读(防半份快照)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "adcTask 在同一拍交棒封存瞬间事实"); + CHECK(wave_ring_sealed() == 1u, "latch 之后才亮「已封存可读」"); + { + static uint8_t s_snap0[WAVE_RING_PTS * 2u]; + static uint8_t s_snap5[WAVE_RING_PTS * 2u]; + static uint8_t s_again0[WAVE_RING_PTS * 2u]; + static uint8_t s_again5[WAVE_RING_PTS * 2u]; + int same = 0; + int drop_all = 0; + uint32_t k; + + CHECK((col_to_bytes(0u, s_snap0, sizeof(s_snap0)) != 0) && + (col_to_bytes(5u, s_snap5, sizeof(s_snap5)) != 0), "封存区两列读成功"); + drop_all = 1; + for (k = 0u; k < 50u; k++) { + (void)tick_one(); + if (s_last_push != WAVE_ST_DONE) { + drop_all = 0; + } + } + CHECK(drop_all != 0, "SEALED 期间每一拍压样都被丢(返回 DONE 而不是 OK)"); + (void)wave_ring_get_stat(&st); + CHECK(st.drop_cnt == (50u * (uint32_t)WAVE_RING_CH), "drop 计数单位 = 点(一拍 8 点,50 拍 = 400)"); + CHECK((col_to_bytes(0u, s_again0, sizeof(s_again0)) != 0) && + (col_to_bytes(5u, s_again5, sizeof(s_again5)) != 0), "再读一次两列"); + if ((memcmp(s_snap0, s_again0, sizeof(s_snap0)) == 0) && + (memcmp(s_snap5, s_again5, sizeof(s_snap5)) == 0)) { + same = 1; + } + CHECK(same != 0, "50 拍压样之后已封存区逐字节不变(defaultTask 读期间不会读到半新半旧)"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "仍在 SEALED,等 ACK"); + } + + /* ---------- 用例:ACK 前不重录 + 冷却只在 ACK 后计时 ---------- */ + printf("-- 用例:ACK 前不重录 / 冷却只在 ACK 后 --\n"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "ACK 前来新触发只加 ovf(lead 裁定第一条)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "defaultTask 落盘成功后明确 ACK"); + (void)wave_ring_get_stat(&st); + CHECK(st.state == (uint8_t)WAVE_RING_ST_COOL, "ACK 之后才转冷却(冻结持续到 ACK,不是按时间解冻)"); + CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却窗从 ACK 那一拍起算 500 拍"); + CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功 ACK 才推进段计数与轮转号"); + CHECK(st.err_cnt == 0u, "这条链路上没有失败"); + CHECK(wave_ring_sealed() == 0u, "ACK 之后「已封存可读」立即熄灭(旧段不再对外)"); + { + int no_seal = 1; + uint32_t k; + + for (k = 0u; k < 499u; k++) { + if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) { + no_seal = 0; + } + } + (void)wave_ring_get_stat(&st); + CHECK((no_seal != 0) && (st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == 1u), + "冷却差 1 拍时仍在冷却、期间没有任何新段"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "冷却期内新触发只加 ovf 不重录(lead 裁定第三条)"); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK(st.state == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完自动回 IDLE"); + CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_OK, "回 IDLE 后新触发被受理(下一次故障有波可录)"); + } + + /* ---------- 用例:滞留超时自弃(绝不让环永久焊死) ---------- */ + printf("-- 用例:SEALED 滞留超时自弃 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 2u, 200u) != 0, "造一段(故意不 latch,模拟接线道没交棒)"); + run_n(WAVE_SEAL_TIMEOUT_MS - 2u); + (void)wave_ring_get_stat(&st); + CHECK((st.state == (uint8_t)WAVE_RING_ST_SEALED) && (st.seal_hold == (uint16_t)(WAVE_SEAL_TIMEOUT_MS - 2u)) && + (st.err_cnt == 0u), "差 2 拍还没到超时线,段仍留着等 ACK"); + (void)tick_one(); + (void)tick_one(); + (void)wave_ring_get_stat(&st); + CHECK((st.err_cnt == 1u) && (st.seg_total == 0u), "滞留满 WAVE_SEAL_TIMEOUT_MS 自弃并计 err(lead 裁定第二条)"); + CHECK((st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == (uint16_t)WAVE_RING_COOL_MS), + "自弃也照样起冷却解冻(不是停在 SEALED)"); + CHECK(wave_ring_sealed() == 0u, "自弃后对外不可读(绝不给落盘道一份已作废的段)"); + CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "自弃后取段头快照被拒"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "自弃后补 ACK 被拒(不吃轮转号、不加 seg_total)"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u), "自弃的段不占段号(下一次故障仍写最早那个号)"); + run_n(WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "自弃之后环照常被救回来(回 IDLE)"); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 100u) != 0, "自弃后仍能录下一段(录波不会永久停走)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段照样能交棒"); + CHECK(wave_ring_sealed() == 1u, "新段就绪"); + + /* ---------- 用例:ACK 失败留同号重写 ---------- */ + printf("-- 用例:落盘失败不推进轮转号 --\n"); + (void)wave_ring_get_stat(&st); + j = st.seg_idx; + CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败也照样 ACK(短写 / 打开失败都要解冻)"); + (void)wave_ring_get_stat(&st); + CHECK((st.err_cnt == 2u) && (st.seg_idx == j) && (st.seg_total == 0u), + "失败只计 err,段号与段计数都不动(不留半截文件骗过 PC 脚本)"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 被拒(防把上一段的 ACK 打到新段)"); + run_n(WAVE_RING_COOL_MS); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "失败段冷却后回 IDLE"); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_BENCH, 3u, 60u) != 0, "再来一段(预触发区只滚了 60 拍)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段交棒"); + (void)wave_ring_get_stat(&st); + CHECK(st.seg_idx == j, "新段沿用同一个段号"); + { + char name[WAVE_ST_PATH_MAX]; + + CHECK(wave_store_seg_path(name, (uint32_t)sizeof(name), st.seg_idx) == 16, "按 ring 的段号取名成功"); + CHECK(strcmp(name, "/wave/WAVE00.BIN") == 0, "同号 = 同一个文件名(重写覆盖,不吃掉一个号)"); + } + CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "这次写成功"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 1u) && (st.seg_total == 1u) && (st.err_cnt == 2u), "成功才推进,err 保留历史"); + + /* ---------- 用例:全链路字节(adcTask 封存 -> latch -> 落盘道搬运) ---------- */ + printf("-- 用例:全链路封段到字节 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 12u, 200u) != 0, "全链路:先滚 200 拍再触发封段"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "全链路:交棒封存瞬间事实"); + CHECK(pack_seg() == 0, "全链路:打包游标跑到 DONE(数据源就是 wave_ring 的读口)"); + CHECK(s_len == (uint32_t)WAVE_ST_SEG_SZ_CUR, "整段字节数 = 5250(§6.3 等式)"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "自产段过整段自检(两个 CRC 都对)"); + CHECK(wave_store_seg_check(s_buf, s_len, &s_m) == WAVE_ST_OK, "自检并回填段头"); + CHECK((s_buf[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_buf[1] == (uint8_t)WAVE_ST_MAGIC1) && + (s_buf[2] == (uint8_t)WAVE_ST_VER), "魔数第三字节就是版本号"); + CHECK((s_buf[WAVE_ST_OFF_FS_HZ] == (uint8_t)(FS_HZ_TEST & 0xFFu)) && + (s_buf[WAVE_ST_OFF_FS_HZ + 1u] == (uint8_t)(FS_HZ_TEST >> 8)), "采样率 1000 Hz 写进头里(是数据不是编译期假设)"); + CHECK(s_buf[WAVE_ST_OFF_PTS_CYC] == 20u, "每周波 20 点写进头里"); + CHECK(s_buf[WAVE_ST_OFF_CH_CNT] == (uint8_t)WAVE_ST_CH_NUM, "通道数 8"); + CHECK(s_buf[WAVE_ST_OFF_PRE_PTS] == (uint8_t)(WAVE_RING_PRE & 0xFFu), "预触发 160 点"); + CHECK(s_buf[WAVE_ST_OFF_TRIG_SRC] == (uint8_t)WAVE_TRIG_PROTECT, "触发源 = 保护"); + CHECK(s_buf[WAVE_ST_OFF_TRIG_ELM] == 12u, "元件序号 = 12"); + CHECK(s_buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN, "折算档位 = 引脚域"); + CHECK(s_buf[WAVE_ST_OFF_FLAGS] == 0x00u, "flags_seq = 未标定态的逐路有效位图原值(全零)"); + CHECK((s_buf[WAVE_ST_OFF_CLIP] == (uint8_t)(0xE6u & 0xFFu)) && + (s_buf[WAVE_ST_OFF_CLIP + 1u] == (uint8_t)(0xE6u >> 8)), "clip_mask = 0xE6(V0.1 恒值)"); + { + wave_seg_meta_t back; + + if (wave_store_head_parse(s_buf, (uint32_t)sizeof(s_buf), &back) == WAVE_ST_OK) { + CHECK(back.t0_ms == s_trig_k, "段首相对毫秒 = 触发那一拍"); + CHECK(back.frame_seq == s_trig_k, "段首帧序号 = 触发那一拍(PC 拿它对 frames)"); + CHECK(back.wall == s_cal.wall, "wall 取封存瞬间的对时秒(未对时应为 0)"); + CHECK(back.pts_per_ch == (uint16_t)WAVE_RING_PTS, "每路点数 320"); + } else { + CHECK(0, "段头解析成功(回填比对的前提)"); + CHECK(0, "段首相对毫秒 = 触发那一拍"); + CHECK(0, "段首帧序号 = 触发那一拍(PC 拿它对 frames)"); + CHECK(0, "wall 取封存瞬间的对时秒(未对时应为 0)"); + CHECK(0, "每路点数 320"); + } + } + { + int ok_tbl = 1; + uint32_t pos; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + pos = WAVE_ST_OFF_TBL + ((uint32_t)WAVE_ST_CH_BYTES * j); + if ((s_buf[pos + WAVE_ST_CH_OFF_SEM] != j) || + (s_buf[pos + WAVE_ST_CH_OFF_CONV] != s_cal.ch[j].conv) || + (s_buf[pos + WAVE_ST_CH_OFF_BITS] != s_cal.ch[j].bits) || + (s_buf[pos + WAVE_ST_CH_OFF_AVAIL] != s_cal.ch[j].avail)) { + ok_tbl = 0; + } + if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_KQ16]) != (uint32_t)s_cal.ch[j].k_q16) { + ok_tbl = 0; + } + if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_BCODE]) != (uint32_t)s_cal.ch[j].b_codes) { + ok_tbl = 0; + } + } + CHECK(ok_tbl != 0, "通道表 8 项:sem = 下标 + conv/bits/avail/k_q16/b_codes 全部落进字节"); + } + { + uint16_t hcrc = wave_store_crc(s_buf, WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)); + + CHECK((s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)] == (uint8_t)(hcrc & 0xFFu)) && + (s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM) + 1u] == (uint8_t)(hcrc >> 8)), + "头 CRC 覆盖 [0, 30+12*ch) 且低字节在前"); + } + { + uint16_t dcrc = wave_store_crc(&s_buf[WAVE_ST_HEAD_SZ_CUR], + WAVE_ST_DATA_BYTES((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF)); + + CHECK((s_buf[s_len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_buf[s_len - 1u] == (uint8_t)(dcrc >> 8)), + "数据 CRC 只覆盖数据区、贴在文件尾(头坏了不会被数据 CRC 掩盖)"); + } + { + static uint8_t s_col[WAVE_RING_PTS * 2u]; + int all_col = 1; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + if (col_to_bytes(j, s_col, sizeof(s_col)) == 0) { + all_col = 0; + } else if (memcmp(&s_buf[WAVE_ST_HEAD_SZ_CUR + ((uint32_t)WAVE_RING_PTS * 2u * j)], + s_col, (size_t)((uint32_t)WAVE_RING_PTS * 2u)) != 0) { + all_col = 0; + } + } + CHECK(all_col != 0, + "数据区每一列逐字节 == wave_ring_read_ch() 的已封存区内容(§5.1 只读已封存区的机器证明)"); + } + { + int ok_trig = 1; + int ok_next = 1; + int ok_old = 1; + + for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) { + if (point_is(j, (uint16_t)(WAVE_RING_PRE - 1u), s_trig_k) == 0) { + ok_trig = 0; + } + if (point_is(j, (uint16_t)WAVE_RING_PRE, s_trig_k + 1u) == 0) { + ok_next = 0; + } + if (point_is(j, 0u, s_trig_k - (WAVE_RING_PRE - 1u)) == 0) { + ok_old = 0; + } + } + CHECK(ok_trig != 0, "点序(lead 裁定 a):触发那一拍的样本落在段内点 159"); + CHECK(ok_next != 0, "点序:点 160 = 触发后第一拍(两半各 160 点不重叠)"); + CHECK(ok_old != 0, "点序:点 0 = 触发前第 159 拍(预触发窗最老一点)"); + } + { + uint8_t save = s_buf[WAVE_ST_HEAD_SZ_CUR + 100u]; + + s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = (uint8_t)(save ^ 0x5Au); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_E_CRC, "数据区改一字节 -> 整段作废(PC 侧必须拒绘)"); + s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = save; + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "改回来又能过(判废只认 CRC,不认运气)"); + } + + /* ---------- 用例:未 latch 时落盘道必须当不可用 ---------- */ + printf("-- 用例:未 latch 不许落盘 --\n"); + fresh(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "封段(不 latch)"); + CHECK(wave_ring_sealed() == 0u, "sealed() = SEALED 且已 latch 的合并判据:未交棒就是不可用"); + CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "取不到段头快照 -> 落盘道无从开文件"); + CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没交棒就 ACK 被拒(不吃轮转号、不假加 seg_total)"); + (void)wave_ring_get_stat(&st); + CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.err_cnt == 0u), "被拒的 ACK 一个计数都不动"); + CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "段仍留在 SEALED 等交棒或超时自弃"); + CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针拒"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "补交棒成功"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复交棒被拒(封存瞬间事实不许被后一拍覆盖)"); + CHECK(pack_seg() == 0, "交棒之后全链路打包成功"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "交棒后的段过自检"); + + /* ---------- 用例:冷启动第一段(环未填满) ---------- */ + printf("-- 用例:环未填满就触发 --\n"); + fresh(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 1u, 20u) != 0, "上电只滚 20 拍就触发(预触发区大部分是空的)"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒"); + CHECK(count_head_zero(0u, 139u) == 139, "点 0 到 138 全为 0(没采过的点不填假数据)"); + CHECK(point_is(0u, 139u, 0u) != 0, "点 139 = 上电第一拍(第一个真样本)"); + CHECK(point_is(7u, 159u, s_trig_k) != 0, "点 159 仍是触发那一拍(未填满时段序口径不变)"); + CHECK(point_is(0u, 319u, s_trig_k + (uint32_t)WAVE_RING_POST) != 0, "点 319 = 段尾(最后一条后触发样本)"); + CHECK(pack_seg() == 0, "冷启动段也能打包"); + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "冷启动段过自检(回放页画出来的是前导平线加真波,不是伪证据)"); + + /* ---------- 用例:互验产物(不参与判定) ---------- */ + printf("-- 用例:互验产物 --\n"); + fresh(); + cal_init(); + CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "再造一段连续的 50 Hz 正弦段"); + CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒"); + if (pack_seg() == 0) { + const char *tmp = getenv("TEMP"); + + CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "互验段过自检"); + if (tmp != NULL) { + char path[512]; + FILE *f; + + strcpy(path, tmp); + strcat(path, "\\wave_ring_synth8.bin"); + f = fopen(path, "wb"); + if (f != NULL) { + size_t w = fwrite(s_buf, 1u, s_len, f); + + (void)fclose(f); + CHECK(w == (size_t)s_len, "互验产物写入字节数 == 段长"); + /* 形态在日志里说清楚:本套给的是本波(V0.1 未标定)真值形态 */ + printf("[INFO] 互验产物(本波真值形态:avail 0x00 / 0x02,逐路有效位图全零;" + "信号形态 = 三路 50 Hz 正弦(UA / UN / IA)+ 三路方波(UB / UC / IN)+ 两路满幅斜坡(IB / IC)," + "供 test/host/wave_read.py dump/shape/fft 只读校验):%s\n", + path); + } else { + printf("[INFO] 打不开临时目录文件,跳过互验产物\n"); + } + } else { + printf("[INFO] 环境无 TEMP,跳过互验产物\n"); + } + } else { + printf("[INFO] 互验段打包失败,跳过产物\n"); + } + + printf("\n=== Wave Trig Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/tools/arch_diagram/gen_wave_record_flow_svg.py b/tools/arch_diagram/gen_wave_record_flow_svg.py index d2e1891b..8b9dba44 100644 --- a/tools/arch_diagram/gen_wave_record_flow_svg.py +++ b/tools/arch_diagram/gen_wave_record_flow_svg.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者与四条「不许」。 +"""生成 docs/images/故障录波数据流.svg —— 录波段数据的四个所有者、四条「不许」与四条已知陷阱。 为什么不用 Mermaid:本图要读的是**跨任务边界的所有权**——谁独占写、谁只能读、 标志怎么过界,并把「不许」画成红线贴在对应位置。Mermaid 的拓扑图表达不了「这块 @@ -29,7 +29,7 @@ OUT = os.path.join(ROOT, "docs", "images", "故障录波数据流.svg") # ----------------------------- 画布与字体 ----------------------------- W = 1060 -H = 702 +H = 784 FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" F_TITLE = 20 F_SUB = 12 @@ -73,12 +73,12 @@ BL_W = 430 BR_X = 494 BR_W = RIGHT - 494 C_TOP = 356 -C_H = 136 -CHIP_W = 316 +C_H = 100 +CHIP_W = 482 CHIP_GAP = 16 -D_TOP = 514 +D_TOP = 596 D_H = 132 -FOOT_Y = 672 +FOOT_Y = 754 # ----------------------------- 数据(改这里) ----------------------------- TITLE = "录波段数据的四个所有者 —— 谁独占写、谁只能读、标志怎么过界" @@ -139,6 +139,11 @@ TRAPS = [ "处置:段名 = WAVE 加两位十进制再加 .BIN,轮转覆盖", "目录 /wave 与 /log 平级,FTP 鉴权现成,零新代码", ]), + ("陷阱四 · 落盘途中环被解冻或换段", [ + "adcTask 优先级高于 defaultTask,搬段窗口里环可能自弃、甚至封出下一段", + "数据区 CRC 是对本道实际读到的字节算的 → 半新半旧照样自洽", + "处置:关档后复检环侧身份(sealed + last_t0_ms + last_seq),不符即判废", + ]), ] FORBID = [ @@ -252,8 +257,10 @@ box(BL_X, B_TOP, BL_W, B_H, LEFT_NOTE[0], LEFT_NOTE[3], LEFT_NOTE[1], LEFT_NOTE[ box(BR_X, B_TOP, BR_W, B_H, READERS[0], READERS[3], READERS[1], READERS[2]) for i, (head, lines) in enumerate(TRAPS): - x = PAD + i * (CHIP_W + CHIP_GAP) - box(x, C_TOP, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER) + # 每行两块、共两行:四块并排会超出画布,缩字号不可接受(宁可加高画布) + x = PAD + (i % 2) * (CHIP_W + CHIP_GAP) + y = C_TOP + (i // 2) * (C_H + CHIP_GAP) + box(x, y, CHIP_W, C_H, head, lines, C_AMBER, BG_AMBER) rect(PAD, D_TOP, RIGHT - PAD, D_H, C_ROSE, BG_ROSE, dash="7,5") text(PAD + 12, D_TOP + 23, "四条「不许」——上面那张图上没有箭头经过的地方", F_HEAD, C_ROSE, "700") diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index be8384de..1d0c7331 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -ⅠⅡⅢ▶一三上主事二交件位低作保信值入公共关写出分制前动化区压原参双发取口可号合听启告周回因围地址型复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收故效数整无日时显最月期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询误请负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: +ⅠⅡⅢ▶一三上不主事二交件位低作保信值入公共关写出分制削前动化区压原参双发取口可号合听启告周回因围地址型域复外失子字存定实密已帧常年序度延开异式引强归当录态总恢息户手投抖护报据掉接控掩操收放故效数整无日时显最月期未本机板查标校样检模欠次正死段求池波流测消清温点片版状狗独用电界监直看知码硬确示秒程窗立站端第类系线终统续维网置脚自节芯范荷行装警计认讯记设询该误请读负败越跳软载输过运近返退送通道遥部配采重量钟钥锁错长门闭间闸防除障零面页频馈首验默,: diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index daaeba2e..4d63c08f 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -42,7 +42,7 @@ 装置维护 时钟设置 #P #page=clock 强制复归 #P #page=reset - 手动录波 #P + 手动录波 #P #page=wave_manual 清除记录 #P #page=clear 通讯参数 通讯设置 #P #page=comm @@ -58,3 +58,4 @@ 自检记录 #page=selftest 运行记录 #page=runlog 运行报告 #page=runreport + 录波回放 #page=wave_play