feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(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 条仍全过
This commit is contained in:
+93
-3
@@ -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` 已在 `<IncludePath>` 内。
|
||||
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在
|
||||
- 十一个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
|
||||
- 模拟器侧分两类(Lead 裁定 2026-10-29):`wave_ring.c` 与 `wave_store.c` **真实参与**
|
||||
`simulator/CMakeLists.txt`——回放页要在模拟器里出真波形,两份又零平台依赖,与「双端共享核心、不造
|
||||
第二真相」同一条口径,故**不进** `SIM_EXEMPT`;其余九个采集链文件(UI 模拟器不跑采集链路)仍登记在
|
||||
`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`(新建子目录还要补 `<IncludePath>`)。
|
||||
模拟器侧二选一:有消费路径 → 加 `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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <stddef.h>
|
||||
|
||||
/* 触发挂号的载荷编码(v1.1):挂号量必须**一次单字节存写**才原子,所以把载荷本身编进
|
||||
* 那一个字节 —— 0 = 无请求,非 0 = (触发源 << 位移) | 元件号。触发源取值 1 到 3 占高 3 位、
|
||||
* 元件号取 prot_elm 的 picked 位号(uint32 位图 → 0 到 31)占低 5 位,合起来 8 位够用。
|
||||
* 本层双端编译(板端 ARMCC 与模拟器),不能引 FreeRTOS / CMSIS 的临界区内在函数,
|
||||
* 所以「把三个字节压成一个字节」是唯一既原子又不依赖平台的写法。 */
|
||||
#define WAVE_RG_REQ_SHIFT (5u) /* 载荷里触发源的低位置 */
|
||||
#define WAVE_RG_REQ_ELM_MSK (0x1Fu) /* 载荷里元件号的位掩码(低 5 位) */
|
||||
#define WAVE_RG_REQ_ELM_MAX (32u) /* 元件号合法上界(不含),picked 位图只有 32 位 */
|
||||
|
||||
/* ===== 一、静态对象(全部文件作用域 static,禁堆分配;落位与字节数见头注第七节) ===== */
|
||||
|
||||
/* 数据区:按通道分列,8 列 × 320 点 × 2 字节 = 5120 字节(§7 预算的大头)。
|
||||
* 不许改成 float、不许按帧交错(§12:形态错一次 RAM 翻倍 + 跨步取数)。
|
||||
* 不放在 CCM:DMA 看不见它,且 .sct 里 RW_CCM 只收 heap_4.o,普通 .bss 天然落 SRAM1 */
|
||||
static int16_t s_ring[WAVE_RING_CH][WAVE_RING_PTS];
|
||||
|
||||
/* 已封存段的段头:latch 之前只有环侧字段有效,latch 之后才是完整段头。
|
||||
* 这是 defaultTask 与 uiTask 能看到的唯一「段头视图」——内部指针一律不外露 */
|
||||
static wave_seg_meta_t s_sealed;
|
||||
|
||||
/* 观测计数(§9 周期行的 seg= / ovf= / drop= / err= 全从这里取,落盘道不数第二份账) */
|
||||
static uint32_t s_seg_total; /* 成功 ACK 的段数 */
|
||||
static uint32_t s_ovf_cnt; /* 被忽略的触发数 */
|
||||
static uint32_t s_drop_cnt; /* SEALED 窗口丢弃的点数(单位 = 点,一次丢一拍 = 加 8) */
|
||||
static uint32_t s_err_cnt; /* 落盘失败 + 滞留超时自弃 */
|
||||
static uint32_t s_last_t0_ms; /* 最近一段段首毫秒 */
|
||||
static uint32_t s_last_seq; /* 最近一段段首帧序号 */
|
||||
|
||||
/* 状态机与索引 */
|
||||
static uint16_t s_head; /* 下一个写入点(滚动位置) */
|
||||
static uint16_t s_base; /* 已封存 / 正在录的段首在环内的下标(段内点 0) */
|
||||
static uint16_t s_post_left; /* 后触发还差多少点封段 */
|
||||
static uint16_t s_cool_left; /* 冷却剩余拍数 */
|
||||
static uint16_t s_seal_hold; /* SEALED 已滞留拍数 */
|
||||
static uint8_t s_state; /* wave_ring_state_t */
|
||||
/* 触发请求挂号:0 = 无请求;非 0 = (触发源 << WAVE_RG_REQ_SHIFT) | 元件号。
|
||||
* 刻意只用这一个字节承载「是谁要录波」:旧实现分三个字节写(先写挂号位、后写载荷),
|
||||
* adcTask 抢在两条指令之间就会把上一段的旧触发源 / 旧元件号抄进本段 —— 而这两个字段是
|
||||
* 这段记录唯一的溯源信息(现场靠它与 /log/start.log 对表),错一次事后无从发现。
|
||||
* 单字节存写在 Cortex-M4 上原子,写序不再有意义,所以不需要任何顺序假设或屏障。 */
|
||||
static uint8_t s_req;
|
||||
static uint8_t s_trig_src; /* 本段实际用的触发源(钉基址时从请求抄过来) */
|
||||
static uint8_t s_trig_elm; /* 本段实际用的元件序号 */
|
||||
static uint8_t s_meta_ready; /* 1 = 段头已 latch */
|
||||
static uint8_t s_seg_idx; /* 本次上电的轮转下标(跨上电接号由落盘道自持,见头注) */
|
||||
|
||||
/* 语义号 → 列号(本波恒等,但把这条恒等式收在一处:二期带 TEMP 时只改这张表,
|
||||
* 写死下标的调用点会静默错位,走本函数的不会) */
|
||||
static const int8_t s_col_of_sem[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, -1
|
||||
};
|
||||
|
||||
/* ===== 二、内部小工具 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:环内下标 +n 后取模
|
||||
* 入口参数:a_idx 当前下标;a_step 步长(小于环长)
|
||||
* 返 回 值:取模后的下标 uint16_t
|
||||
* 限定条件:仅本文件;a_idx 与 a_step 均在环长量级,无溢出风险
|
||||
* 函数说明:环长 320 是 2 的幂吗?不是(320 = 64×5),所以老老实实取模,
|
||||
* 不用位与——写错一次就是「看起来能跑、偶尔串段」的那种 bug。
|
||||
*/
|
||||
static uint16_t wave_rg_wrap(uint16_t a_idx, uint16_t a_step) {
|
||||
uint16_t v = (uint16_t)(a_idx + a_step);
|
||||
|
||||
if (v >= (uint16_t)WAVE_RING_PTS) {
|
||||
v = (uint16_t)(v - (uint16_t)WAVE_RING_PTS);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把已封存段的「环侧字段」初始化(封存瞬间调,标定字段一律清零待 latch)
|
||||
* 入口参数:a_now_ms 段首毫秒;a_frame_seq 段首帧序号
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:仅本文件;只在钉基址那一拍调一次
|
||||
* 函数说明:先把 ch[] 全部清成「sem = 下标、其余 0」,是为了让**没有 latch** 的段
|
||||
* 不可能带着上一段的系数被写进文件:sealed() 在未 latch 时本就是 0,
|
||||
* 但清零让「万一有人绕过握手直接读 s_sealed」也读不出旧真值。
|
||||
*/
|
||||
static void wave_rg_seal_reset(uint32_t a_now_ms, uint32_t a_frame_seq) {
|
||||
uint8_t i;
|
||||
|
||||
s_sealed.fs_hz = (uint16_t)WAVE_ST_FS_DEF;
|
||||
s_sealed.pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
|
||||
s_sealed.ch_count = (uint8_t)WAVE_RING_CH;
|
||||
s_sealed.pts_per_ch = (uint16_t)WAVE_RING_PTS;
|
||||
s_sealed.pre_pts = (uint16_t)WAVE_RING_PRE;
|
||||
s_sealed.trig_src = s_trig_src;
|
||||
s_sealed.trig_elm = s_trig_elm;
|
||||
s_sealed.t0_ms = a_now_ms;
|
||||
s_sealed.wall = 0u;
|
||||
s_sealed.eng = (uint8_t)WAVE_ENG_PIN;
|
||||
s_sealed.flags_seq = 0u;
|
||||
s_sealed.clip_mask = 0u;
|
||||
s_sealed.frame_seq = a_frame_seq;
|
||||
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
|
||||
s_sealed.ch[i].sem = i;
|
||||
s_sealed.ch[i].conv = 0u;
|
||||
s_sealed.ch[i].bits = 0u;
|
||||
s_sealed.ch[i].avail = 0u;
|
||||
s_sealed.ch[i].k_q16 = 0;
|
||||
s_sealed.ch[i].b_codes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 三、初始化与压环 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:复位环与全部状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:只能在 adcTask 调,且在开始压环之前(见头文件同函数注释)
|
||||
* 函数说明:数据区显式清零而不是依赖 .bss 清零:本函数也可能在运行中被重新调
|
||||
* (显式重配 / 单测复用),留旧数据就等于留下「上一段的尾巴混进下一段」的口子。
|
||||
*/
|
||||
void wave_ring_init(void) {
|
||||
uint8_t c;
|
||||
uint16_t i;
|
||||
|
||||
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
|
||||
for (i = 0u; i < (uint16_t)WAVE_RING_PTS; i++) {
|
||||
s_ring[c][i] = 0;
|
||||
}
|
||||
}
|
||||
s_head = 0u;
|
||||
s_base = 0u;
|
||||
s_post_left = 0u;
|
||||
s_cool_left = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
s_req = 0u;
|
||||
s_trig_src = (uint8_t)WAVE_TRIG_NONE;
|
||||
s_trig_elm = 0u;
|
||||
s_meta_ready = 0u;
|
||||
s_seg_idx = 0u;
|
||||
s_seg_total = 0u;
|
||||
s_ovf_cnt = 0u;
|
||||
s_drop_cnt = 0u;
|
||||
s_err_cnt = 0u;
|
||||
s_last_t0_ms = 0u;
|
||||
s_last_seq = 0u;
|
||||
wave_rg_seal_reset(0u, 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍的 8 路原始码值
|
||||
* 入口参数:p_codes 语义序数组(前 8 项);a_n 必须等于 WAVE_RING_CH
|
||||
* 返 回 值:WAVE_ST_OK 已写入;WAVE_ST_DONE 本拍被丢弃(SEALED 窗口);WAVE_ST_E_PARAM 参数错 int32_t
|
||||
* 限定条件:只能在 adcTask 调,每拍一次
|
||||
* 函数说明:写的是「列优先」的 8 个分散地址(每列同一行)——这就是 §6.2 分列形态的代价,
|
||||
* 换来的是落盘与回放都能按列连续搬,不用跨步长取数。
|
||||
*/
|
||||
int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n) {
|
||||
uint8_t c;
|
||||
|
||||
if ((p_codes == NULL) || (a_n != (uint8_t)WAVE_RING_CH)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (s_state == (uint8_t)WAVE_RING_ST_SEALED) {
|
||||
/* 冻结窗口丢样本(§4.2):计数单位是点,一次丢一整拍 = WAVE_RING_CH 点 */
|
||||
s_drop_cnt += (uint32_t)a_n;
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
|
||||
s_ring[c][s_head] = p_codes[c];
|
||||
}
|
||||
s_head = wave_rg_wrap(s_head, 1u);
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 四、触发、推进、交棒、ACK ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:投递一次录波请求
|
||||
* 入口参数:a_src 触发源(1 手动 / 2 保护 / 3 bench);a_elm 元件序号(0 到 31)
|
||||
* 返 回 值:WAVE_ST_OK 受理;WAVE_ST_DONE 被忽略(ovf 已加一);WAVE_ST_E_PARAM 触发源
|
||||
* 非法或元件号越界 int32_t
|
||||
* 限定条件:任意上下文(**只写一个字节**,理由见 s_req 声明处)
|
||||
* 函数说明:这里判「非 IDLE 就拒」而不是「非 IDLE 就排队」——本波不做多段排队(已定裁定),
|
||||
* 而且排队会把「最近一次故障」降级成「上上次故障」。
|
||||
* s_req 已挂号时也拒:同一拍内多个元件同时 picked 只该录一段。
|
||||
* 两条入参校验排在状态判断之前:非法调用是调用方的错,不该记进 ovf 账 ——
|
||||
* ovf 的语义是「真来了第二次触发」。
|
||||
* 元件号上界写死在这里:今天没有调用方会越界(picked 是 uint32 位图),这条是给
|
||||
* 二期加元件的人留的牙 —— 载荷只有 5 位,越界必须当场拒,不许静默截断。
|
||||
*/
|
||||
int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm) {
|
||||
if ((a_src < (uint8_t)WAVE_TRIG_MANUAL) || (a_src > (uint8_t)WAVE_TRIG_BENCH)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_elm >= (uint8_t)WAVE_RG_REQ_ELM_MAX) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((s_state != (uint8_t)WAVE_RING_ST_IDLE) || (s_req != 0u)) {
|
||||
s_ovf_cnt++;
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
/* 一次存写:触发源与元件号同字节落地。别的上下文要么看到「没请求」、要么看到
|
||||
* 「完整的这次请求」,不存在读到半份载荷的中间态 */
|
||||
s_req = (uint8_t)(((uint32_t)a_src << (uint32_t)WAVE_RG_REQ_SHIFT) | (uint32_t)a_elm);
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:推进一拍状态机
|
||||
* 入口参数:a_now_ms 当前相对上电毫秒;a_frame_seq 当前帧序号
|
||||
* 返 回 值:wave_ring_evt_t(本拍刚封段返回 WAVE_RING_EVT_SEALED) int32_t
|
||||
* 限定条件:只能在 adcTask 调,每拍在 wave_ring_push 之后一次
|
||||
* 函数说明:五个分支各管一件事,互不重叠:
|
||||
* IDLE —— 消费挂号、钉段首基址、转 FREEZE
|
||||
* FREEZE —— 后触发计点,计满转 SEALED
|
||||
* SEALED —— 只计滞留,超时自弃(绝不让环永久焊死)
|
||||
* COOL —— 递减冷却,到 0 回 IDLE
|
||||
* 段首基址 = 触发那一拍写入点的「往前 160 点」,于是段内点 0 是最老的预触发点、
|
||||
* 点 160 正好是触发后第一点;后触发的 160 点落在环的另一半(本来就即将被覆盖的
|
||||
* 最老那 160 槽),所以「冻结」在物理上是**停止滚动覆盖**,不是停止写入。
|
||||
*/
|
||||
int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq) {
|
||||
int32_t evt = (int32_t)WAVE_RING_EVT_NONE;
|
||||
|
||||
switch (s_state) {
|
||||
case (uint8_t)WAVE_RING_ST_IDLE:
|
||||
s_cool_left = 0u;
|
||||
s_seal_hold = 0u;
|
||||
{
|
||||
/* 只读一次快照再解码:分两次读就会把两次请求的字段拼成一段的身份 */
|
||||
uint8_t req = s_req;
|
||||
|
||||
if (req != 0u) {
|
||||
/* 钉基址:本拍的样本已写进 (head-1),段首 = head - 预触发点数 */
|
||||
s_trig_src = (uint8_t)((uint32_t)req >> (uint32_t)WAVE_RG_REQ_SHIFT);
|
||||
s_trig_elm = (uint8_t)((uint32_t)req & (uint32_t)WAVE_RG_REQ_ELM_MSK);
|
||||
s_req = 0u;
|
||||
s_base = wave_rg_wrap(s_head, (uint16_t)((uint32_t)WAVE_RING_PTS - (uint32_t)WAVE_RING_PRE));
|
||||
s_post_left = (uint16_t)WAVE_RING_POST;
|
||||
s_meta_ready = 0u;
|
||||
s_last_t0_ms = a_now_ms;
|
||||
s_last_seq = a_frame_seq;
|
||||
wave_rg_seal_reset(a_now_ms, a_frame_seq);
|
||||
s_state = (uint8_t)WAVE_RING_ST_FREEZE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_FREEZE:
|
||||
if (s_post_left > 0u) {
|
||||
s_post_left--;
|
||||
}
|
||||
if (s_post_left == 0u) {
|
||||
/* 封段:此刻起 s_head 正好回到 s_base,再写就覆盖段内点 0 —— 所以 push 开始丢 */
|
||||
s_state = (uint8_t)WAVE_RING_ST_SEALED;
|
||||
s_seal_hold = 0u;
|
||||
evt = (int32_t)WAVE_RING_EVT_SEALED;
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_SEALED:
|
||||
/* 兜底:defaultTask 挂住 / 卷没挂载 / 落盘道被更高优先级的事挡住,都不能让环
|
||||
* 永久冻结——下一次真故障无波可录是比丢一段更贵的代价(lead 裁定) */
|
||||
s_seal_hold++;
|
||||
if (s_seal_hold >= (uint16_t)WAVE_SEAL_TIMEOUT_MS) {
|
||||
s_err_cnt++;
|
||||
s_meta_ready = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
|
||||
s_state = (uint8_t)WAVE_RING_ST_COOL;
|
||||
}
|
||||
break;
|
||||
|
||||
case (uint8_t)WAVE_RING_ST_COOL:
|
||||
/* 冷却只在 ACK(或自弃)之后计时:它是二次抑制窗,不是冻结窗 */
|
||||
if (s_cool_left > 0u) {
|
||||
s_cool_left--;
|
||||
}
|
||||
if (s_cool_left == 0u) {
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
/* 状态字节被写坏(跑飞 / 越界写)时回到 IDLE 而不是卡死:录波宁可多录也不能停 */
|
||||
s_state = (uint8_t)WAVE_RING_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
return evt;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:封存交棒——把封存瞬间事实锁进段头
|
||||
* 入口参数:p_cal 封存瞬间事实(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK 已锁存;WAVE_ST_E_STATE 不在待交棒态;WAVE_ST_E_PARAM 参数错 int32_t
|
||||
* 限定条件:只能在 adcTask 调,只在 EVT_SEALED 那一拍调一次
|
||||
* 函数说明:只 copy 封存瞬间的事实,不动环侧字段(点数 / 采样率 / 预触发这些
|
||||
* 在封段那刻已按 §6.2 定死)。重复调用直接拒,不覆盖已有锁存:
|
||||
* 半路换系数会让落盘道写进去的头与它已经在搬的数据不同源。
|
||||
*/
|
||||
int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal) {
|
||||
uint8_t i;
|
||||
|
||||
if (p_cal == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (p_cal->eng > (uint8_t)WAVE_ENG_PRI) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((s_state != (uint8_t)WAVE_RING_ST_SEALED) || (s_meta_ready != 0u)) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
s_sealed.eng = p_cal->eng;
|
||||
s_sealed.flags_seq = p_cal->flags_seq;
|
||||
s_sealed.clip_mask = p_cal->clip_mask;
|
||||
s_sealed.wall = p_cal->wall;
|
||||
for (i = 0u; i < s_sealed.ch_count; i++) {
|
||||
s_sealed.ch[i].conv = p_cal->ch[i].conv;
|
||||
s_sealed.ch[i].bits = p_cal->ch[i].bits;
|
||||
s_sealed.ch[i].avail = p_cal->ch[i].avail;
|
||||
s_sealed.ch[i].k_q16 = p_cal->ch[i].k_q16;
|
||||
s_sealed.ch[i].b_codes = p_cal->ch[i].b_codes;
|
||||
}
|
||||
s_meta_ready = 1u;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘 ACK
|
||||
* 入口参数:a_result 0 = 成功;非 0 = 失败(短写 / 打开失败 / CRC 自弃)
|
||||
* 返 回 值:WAVE_ST_OK 已解冻并起冷却;WAVE_ST_E_STATE 当前不在 SEALED int32_t
|
||||
* 限定条件:只能由 defaultTask 调,一段一次
|
||||
* 函数说明:无论成败都解冻 + 起冷却:错误只计数不冻结(环冻着的代价是下一次故障无波可录)。
|
||||
* 成功才推进轮转号与段计数(失败段留同号,下次重写覆盖,不留半截文件)。
|
||||
*/
|
||||
int32_t wave_ring_write_done(int32_t a_result) {
|
||||
if (s_state != (uint8_t)WAVE_RING_ST_SEALED) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (s_meta_ready == 0u) {
|
||||
/* 没 latch 就没有「段」可 ACK:这时候段头从未就绪,落盘道根本拿不到
|
||||
* wave_ring_sealed_meta(),绝不可能真写出文件。若在这里放过 ACK,
|
||||
* 就会白推进轮转号(号 +1 而盘上没那个文件 = 凭空吃掉一个号)并把
|
||||
* seg_total 报成一个没发生过的数(串口周期行的 seg=NN 从此对不上账)。
|
||||
* 拒绝而不是记账,把「没交棒就 ACK」变成调用方看得见的错误。
|
||||
* 本段的出口只有一个:滞留到 WAVE_SEAL_TIMEOUT_MS 自弃(那一路照实计 err)。 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (a_result == 0) {
|
||||
s_seg_idx = wave_store_next_idx(s_seg_idx);
|
||||
s_seg_total++;
|
||||
} else {
|
||||
s_err_cnt++;
|
||||
}
|
||||
s_meta_ready = 0u;
|
||||
s_seal_hold = 0u;
|
||||
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
|
||||
s_state = (uint8_t)WAVE_RING_ST_COOL;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 五、状态查询与已封存区读口 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:查当前状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:wave_ring_state_t 取值 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_state(void) {
|
||||
return s_state;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查「有已封存段可读」——跨任务唯一握手量
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = SEALED 且段头已 latch;0 = 不可用 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_sealed(void) {
|
||||
if ((s_state == (uint8_t)WAVE_RING_ST_SEALED) && (s_meta_ready != 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取已封存段的段头快照
|
||||
* 入口参数:p_out 输出(整份拷出)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文(defaultTask / uiTask 只读)
|
||||
*/
|
||||
int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out) {
|
||||
uint16_t i;
|
||||
const uint8_t *p_src;
|
||||
uint8_t *p_dst;
|
||||
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (wave_ring_sealed() == 0u) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
/* 逐字节拷出而不是 *p_out = s_sealed:结构体赋值在 ARMCC 上会调 __aeabi_memcpy,
|
||||
* 这里没必要为一个赋值多拉一层依赖,量也就 136 字节 */
|
||||
p_src = (const uint8_t *)&s_sealed;
|
||||
p_dst = (uint8_t *)p_out;
|
||||
for (i = 0u; i < (uint16_t)sizeof(wave_seg_meta_t); i++) {
|
||||
p_dst[i] = p_src[i];
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通道读已封存区的连续点窗
|
||||
* 入口参数:a_ch 列号;a_from 段内起点;a_cnt 期望点数(超出段尾自动夹);p_dst 输出
|
||||
* 返 回 值:实际点数;WAVE_ST_E_PARAM / WAVE_ST_E_RANGE / WAVE_ST_E_STATE int32_t
|
||||
* 限定条件:任意上下文,只读已封存区(本层没有任何读活缓冲的入口)
|
||||
* 函数说明:按 s_base 取模展开,读者看到的是 320 点的线性数组。
|
||||
*/
|
||||
int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst) {
|
||||
uint16_t left;
|
||||
uint16_t n;
|
||||
uint16_t idx;
|
||||
uint16_t i;
|
||||
|
||||
if ((p_dst == NULL) || (a_cnt == 0u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((a_ch >= (uint8_t)WAVE_RING_CH) || (a_from >= (uint16_t)WAVE_RING_PTS)) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
if (wave_ring_sealed() == 0u) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
left = (uint16_t)((uint16_t)WAVE_RING_PTS - a_from);
|
||||
n = (a_cnt < left) ? a_cnt : left;
|
||||
idx = wave_rg_wrap(s_base, a_from);
|
||||
for (i = 0u; i < n; i++) {
|
||||
p_dst[i] = s_ring[a_ch][idx];
|
||||
idx = wave_rg_wrap(idx, 1u);
|
||||
}
|
||||
return (int32_t)n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读口的 wave_reader_fn 形态适配(落盘道直接把它交给打包游标)
|
||||
* 入口参数:p_user 未用;其余同 wave_ring_read_ch(点数与起点是 uint32 形态)
|
||||
* 返 回 值:实际点数;负 = WAVE_ST_E_* int32_t
|
||||
* 限定条件:任意上下文,只读已封存区
|
||||
* 函数说明:32 位入参先夹到 16 位形态再转调,越界一律报错不夹成 0 ——
|
||||
* 夹成 0 会让落盘道以为「这一列读完了」而写出一个短段。
|
||||
*/
|
||||
int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst) {
|
||||
(void)p_user;
|
||||
|
||||
if (a_cnt > (uint32_t)WAVE_RING_PTS) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_from > (uint32_t)WAVE_RING_PTS) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
return wave_ring_read_ch(a_ch, (uint16_t)a_from, (uint16_t)a_cnt, p_dst);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取观测计数快照
|
||||
* 入口参数:p_out 输出(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
int32_t wave_ring_get_stat(wave_ring_stat_t *p_out) {
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
p_out->state = s_state;
|
||||
p_out->trig_src = s_trig_src;
|
||||
p_out->trig_elm = s_trig_elm;
|
||||
p_out->meta_ready = s_meta_ready;
|
||||
p_out->seg_idx = s_seg_idx;
|
||||
/* 对外字段是 8 位:本波后触发最多 160 点,不会溢出;真溢出说明环长被改过,
|
||||
* 饱和在 255 让它在日志里显眼,而不是绕回 0 假装快封段了 */
|
||||
p_out->post_left = (s_post_left > 255u) ? 255u : (uint8_t)s_post_left;
|
||||
p_out->rsv1 = 0u;
|
||||
p_out->rsv2 = 0u;
|
||||
p_out->cool_left = s_cool_left;
|
||||
p_out->seal_hold = s_seal_hold;
|
||||
p_out->seg_total = s_seg_total;
|
||||
p_out->ovf_cnt = s_ovf_cnt;
|
||||
p_out->drop_cnt = s_drop_cnt;
|
||||
p_out->err_cnt = s_err_cnt;
|
||||
p_out->last_t0_ms = s_last_t0_ms;
|
||||
p_out->last_seq = s_last_seq;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:语义通道号 → 本环列号
|
||||
* 入口参数:a_sem 语义号
|
||||
* 返 回 值:列号;WAVE_ST_E_RANGE = 该语义不录 int32_t
|
||||
* 限定条件:任意上下文(纯计算)
|
||||
*/
|
||||
int32_t wave_ring_col(uint8_t a_sem) {
|
||||
if (a_sem >= (uint8_t)WAVE_ST_CH_MAX) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
if (s_col_of_sem[a_sem] < 0) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
return (int32_t)s_col_of_sem[a_sem];
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
#include "wave_store.h" /* 同层配对头:wave_seg_meta_t / wave_ch_desc_t / wave_reader_fn /
|
||||
WAVE_ST_* 返回码(本层复用同一套 int 返回码通道,不另发明) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ===== 一、环几何(数值属主 = docs/子系统设计/故障录波设计说明.md §4.2 与 §7) ===== */
|
||||
|
||||
/* 每通道点数 = 预触发 8 周波 + 后触发 8 周波 = 160 + 160 = 320 点(1 kHz 下 320 ms) */
|
||||
#define WAVE_RING_PTS (320u)
|
||||
/* 预触发点数:触发点之前保留 8 周波;回放画触发光标的位点就是这个数(也是段内触发点下标) */
|
||||
#define WAVE_RING_PRE (160u)
|
||||
/* 后触发点数:触发点之后采满 8 周波才封段 */
|
||||
#define WAVE_RING_POST (160u)
|
||||
/* 通道数 = 8(TEMP 不录,理由见本文件头注);必须与 WAVE_ST_CH_NUM 相等,单测里钉住 */
|
||||
#define WAVE_RING_CH (8u)
|
||||
/* 冷却抑制窗:ACK 之后这么多拍内新触发只加 ovf 不重录(§4.2「只记第一次」)。
|
||||
* 500 拍 = 500 ms @1 kHz。注意:「覆盖一轮落盘窗口」这件事由 ACK 保证,本数值只是
|
||||
* ACK 之后的二次抑制(防同一次故障的抖动重复触发),待台架核。 */
|
||||
#define WAVE_RING_COOL_MS (500u)
|
||||
/* 已封存滞留上限:SEALED 状态超过这么多拍仍没人来取(卷未挂载、落盘链卡死都算)
|
||||
* → 自弃本段 + err 计数加一 + 走冷却解冻。2000 拍 = 2 s @1 kHz,待台架核。
|
||||
* 判据是 tick 的推进次数(也就是 adcTask 的存活),不是墙上时间:adcTask 自己挂了
|
||||
* 就轮到看门狗处理,本层不越权。 */
|
||||
#define WAVE_SEAL_TIMEOUT_MS (2000u)
|
||||
|
||||
/* 环数据区字节数:int16 × 320 × 8 = 5120(§7 预算里最大的一项;动它等于动预算) */
|
||||
#define WAVE_RING_BYTES (WAVE_RING_PTS * WAVE_RING_CH * (uint32_t)sizeof(int16_t))
|
||||
|
||||
/* ===== 二、类型 ===== */
|
||||
|
||||
/* wave_ring_tick 的事件:返回值语义是「本拍发生了什么」,不是错误 */
|
||||
typedef enum {
|
||||
WAVE_RING_EVT_NONE = 0, /* 正常推进,无事件 */
|
||||
WAVE_RING_EVT_SEALED = 1 /* 本拍刚计满后触发:**adcTask 必须在本拍调
|
||||
* wave_ring_latch_meta() 交棒封存瞬间事实**;
|
||||
* 不调则本段永不就绪,最终由 WAVE_SEAL_TIMEOUT_MS
|
||||
* 自弃(err 加一),可从串口周期行看到 */
|
||||
} wave_ring_evt_t;
|
||||
|
||||
/* 环状态。迁移只发生在 wave_ring_tick 与 wave_ring_write_done 里(单写者) */
|
||||
typedef enum {
|
||||
WAVE_RING_ST_IDLE = 0, /* 滚动:新样本覆盖最旧点,接受触发 */
|
||||
WAVE_RING_ST_FREEZE = 1, /* 已触发:段首基址已钉死(预触发区不再被覆盖),
|
||||
后触发区继续写入,直到计满 WAVE_RING_POST 点 */
|
||||
WAVE_RING_ST_SEALED = 2, /* 已封存:整段只读,新样本一律丢弃并计 drop;
|
||||
等 meta latch、等 defaultTask 落盘并 ACK */
|
||||
WAVE_RING_ST_COOL = 3 /* 冷却:已 ACK 解冻、恢复正常滚动,但新触发只加 ovf */
|
||||
} wave_ring_state_t;
|
||||
|
||||
/* 封存瞬间必须由 adcTask 交棒的事实(本层不链 App/algo/adc_cal,全靠这份入参)。
|
||||
* 段长 / 采样率 / 每周波点数 / 预触发点数 / 触发源与序号 / t0_ms / frame_seq 由本层自己填
|
||||
* (环就是这些数的物理实现,让调用方填只会多一个「忘了填」的失败面)。 */
|
||||
typedef struct {
|
||||
uint8_t eng; /* 折算档位(wave_eng_t:0 引脚域 / 1 二次值 / 2 一次值) */
|
||||
uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */
|
||||
uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */
|
||||
uint32_t wall; /* 对时后的 epoch 秒(未对时 0;未对时就写 0,不许拿上电毫秒冒充) */
|
||||
wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表前 8 项的 avail / k_q16 / b_codes:
|
||||
必须取 adc_cal_k_q16() 的 int32 原值,
|
||||
不许经 adc_cal_k_uv() 的 float 回算(§6.3:
|
||||
float 在 5.28e7 量级掉尾数);
|
||||
conv 与 bits 也在这里给(本层无从知道两片
|
||||
转换器的归属);sem 由本层按「表内语义号 == 下标」
|
||||
自己钉,传进来的 sem 被忽略 */
|
||||
} wave_seal_cal_t;
|
||||
|
||||
/* 观测快照(给 §9 那条周期行 wave: 与主机单测;字段序 ≈ 日志序,别随手换) */
|
||||
typedef struct {
|
||||
uint8_t state; /* wave_ring_state_t */
|
||||
uint8_t trig_src; /* 当前/最近一段的触发源(wave_trig_src_t;未触发为 0) */
|
||||
uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与 bench 为 0) */
|
||||
uint8_t meta_ready; /* 1 = 段头已 latch 可落盘;SEALED 期间为 0 就是「本段不可用」 */
|
||||
uint8_t seg_idx; /* 下一个要写的段号 0..99(轮转号由本层持有,defaultTask 照它取名) */
|
||||
uint8_t post_left; /* 后触发还差多少点封段(FREEZE 期间有意义;>255 时饱和在 255) */
|
||||
uint8_t rsv1; /* 对齐补齐 */
|
||||
uint8_t rsv2; /* 对齐补齐 */
|
||||
uint16_t cool_left; /* 冷却剩余拍数 */
|
||||
uint16_t seal_hold; /* 本段已在 SEALED 滞留多少拍(与 WAVE_SEAL_TIMEOUT_MS 对照) */
|
||||
uint32_t seg_total; /* 累计成功 ACK 的段数(给日志 seg=NN 与对账) */
|
||||
uint32_t ovf_cnt; /* 被丢弃的触发数:ACK 前重复触发 + 冷却期内触发(§9 ovf=) */
|
||||
uint32_t drop_cnt; /* 冻结窗口丢弃的录波点数,单位 = 点不是拍(§9 drop=) */
|
||||
uint32_t err_cnt; /* 落盘失败(write_done 非 0,含短写与 CRC 自弃)
|
||||
+ SEALED 滞留超时自弃 的次数(§9 err=) */
|
||||
uint32_t last_t0_ms; /* 最近一段段首相对上电毫秒 */
|
||||
uint32_t last_seq; /* 最近一段段首帧序号(与串口周期行的 frames 对账) */
|
||||
} wave_ring_stat_t;
|
||||
|
||||
/* ===== 三、生命周期(写者侧) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:复位环与全部状态(清 5120 字节数据区 + 索引 + 状态机 + 观测计数 + 轮转号)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:**只能在 adcTask 调**,且只能在开始压环之前调(启动期或显式重配)。
|
||||
* 内核启动前调用也安全:本函数不调任何 RTOS API、不建对象,纯数组清零
|
||||
* (平台约束「osKernelStart 之前禁止创建类 API」对本层不适用)
|
||||
* 函数说明:观测计数(seg_total / ovf_cnt / drop_cnt / err_cnt)与轮转号一并归零:
|
||||
* 重启录波就是重新起账,不留旧账,免得串口周期行的累计数与段文件对不上。
|
||||
* 轮转号归零意味着**本层的 seg_idx 只是本次上电的本地计数**(供日志与首轮参考),
|
||||
* 不是盘上真值:跨上电的权威下标必须由落盘道自持——启动扫目录用
|
||||
* wave_store_idx_from_name 重建、每次写成功后用 wave_store_next_idx 推进。
|
||||
* 若照本层归零后的 0 直接取名,重启后第一笔覆盖的就是上次上电的**最新**段,
|
||||
* 与轮转「新证据优先」的初衷正相反(§6.2 轮转语义)。
|
||||
*/
|
||||
void wave_ring_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍的 8 路原始码值(数据面的唯一写入路径)
|
||||
* 入口参数:p_codes 8 路码值数组,顺序 = adc_semantic_t 的前 8 项
|
||||
* (UA UB UC UN IA IB IC IN,即语义号升序,与段文件通道表列序
|
||||
* 同一口径,§6.3)
|
||||
* a_n 数组长度,必须等于 WAVE_RING_CH
|
||||
* 返 回 值:WAVE_ST_OK = 已写入;WAVE_ST_DONE = 本拍样本被丢弃(SEALED 窗口,drop 已加一);
|
||||
* WAVE_ST_E_PARAM = 空指针或长度不对 int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,每拍一次(1 kHz);不得从其它任务或 ISR 调。
|
||||
* 尤其禁止图省事挂在 Src/freertos.c 的 TIM3 回调里(adc_tick_irq_notify()
|
||||
* 旁边):那是 ISR 上下文,且 ISR 比调度器早约 100 ms 启动
|
||||
* (docs/子系统设计/看门狗设计说明.md §10.1),本层没有「消费者就绪」门,
|
||||
* 挂上去就是拿 ISR 改状态机。
|
||||
* 函数说明:IDLE、FREEZE、COOL 三态都写入;SEALED 一律丢弃并计数 —— 这就是 §4.2
|
||||
* 「冻结期间丢的是录波样本」,不是丢采样链路:sample_ring、相量、保护照常被
|
||||
* adcTask 喂,本层无权也不该影响它们。
|
||||
* 码值是转换器原始输出:TPAFE 为 16 位补码双极性、片内 ADC 为 0..4095 原样
|
||||
* 正数,**不做中点偏置、不做折算**(§6.3「样本编码」行:加偏置等于伪造双极性)。
|
||||
*/
|
||||
int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n);
|
||||
|
||||
/* ===== 四、触发请求、每拍推进、封存交棒、落盘 ACK ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:投递一次录波请求(单字投递,不碰环、不碰索引)
|
||||
* 入口参数:a_src 触发源(wave_trig_src_t 的 1 / 2 / 3;0 与其它值按参数错拒绝)
|
||||
* a_elm 触发元件序号(prot_elm 的 picked 位号,**必须小于 32**;
|
||||
* 手动与 bench 定时传 0。越界当场拒,不许静默截断 —— 载荷只有 5 位)
|
||||
* 返 回 值:WAVE_ST_OK = 已受理(下一拍由 adcTask 自己钉段首基址);
|
||||
* WAVE_ST_DONE = 被忽略(处于 FREEZE 或 SEALED 或 COOL,或请求还没被消费;
|
||||
* ovf 计数已加一,§9 的 ovf=);
|
||||
* WAVE_ST_E_PARAM = 触发源非法或元件号越界(参数错**不记 ovf 账**) int32_t
|
||||
* 限定条件:**任意上下文可调**(protectTask 的 picked 上升沿、uiTask 的手动确认、
|
||||
* bench 定周期、adcTask 自己),它只写**一个字节**(载荷编码见文件头
|
||||
* 「跨任务安全」段),与 App/algo/prot_elm.c 的 prot_elm_request_reset()
|
||||
* 同一模式(§5.1)
|
||||
* 函数说明:本函数不产生「立刻冻结」的效果 —— 段首基址由 adcTask 在下一拍
|
||||
* wave_ring_tick 里钉,避免跨上下文改环索引。最坏延迟一拍 = 1 ms,
|
||||
* 相对 160 点的预触发窗可以忽略。
|
||||
* **ACK 前一律只加 ovf**(lead 裁定):SEALED 期间再来触发不重录、不排队,
|
||||
* 本波不做多段排队。
|
||||
* **返回 WAVE_ST_E_PARAM 的两条路径不触碰任何计数**(ovf / err / drop 一个都不加,
|
||||
* 也不留挂号):那是调用方传错参数,与「第二次真触发」是两回事。台架不得拿 ovf
|
||||
* 当「谁误用了接口」的证据——误用只在返回值里体现(lead 裁定 2026-10-29)。
|
||||
*/
|
||||
int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm);
|
||||
|
||||
/*
|
||||
* 函数功能:推进一拍状态机(消费请求钉基址、计后触发点数、到点进 SEALED、
|
||||
* 冷却递减、SEALED 滞留超时自弃)
|
||||
* 入口参数:a_now_ms 当前相对上电毫秒(填段头 t0_ms;用 adcTask 已有的时基即可)
|
||||
* a_frame_seq 当前帧序号(填段头 frame_seq,与串口周期行的 frames 对账)
|
||||
* 返 回 值:wave_ring_evt_t(WAVE_RING_EVT_NONE / WAVE_RING_EVT_SEALED) int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,且每拍在 wave_ring_push 之后调一次
|
||||
* (先写样本再算点数,后触发才正好 160 个点)
|
||||
* 函数说明:FREEZE 期间写入行为与滚动无异,只是段首基址已钉死、预触发区不再被覆盖
|
||||
* —— 这就是 §4.2「触发即冻结写指针」的准确含义:冻住的是覆盖窗口,
|
||||
* 不是停止采集。计满 WAVE_RING_POST 点的本拍返回 WAVE_RING_EVT_SEALED。
|
||||
* SEALED 期间本函数只做「滞留计时 + 超时自弃」,不动数据、不动基址。
|
||||
*/
|
||||
int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq);
|
||||
|
||||
/*
|
||||
* 函数功能:封存交棒 —— 把「环已知字段 + adcTask 现取的封存瞬间事实」拼成完整段头并锁存
|
||||
* 入口参数:p_cal 封存瞬间事实(折算档位 / 有效性位图 / 削顶位图 / epoch 秒 / 通道表的
|
||||
* conv、bits、avail、k_q16、b_codes;不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK = 已锁存,wave_ring_sealed() 从此为 1;
|
||||
* WAVE_ST_E_STATE = 不在「刚封段待交棒」态(含已 latch 过第二次、非 SEALED);
|
||||
* WAVE_ST_E_PARAM = 空指针或折算档位非法 int32_t
|
||||
* 限定条件:**只能在 adcTask 调**,且只在 wave_ring_tick 返回 WAVE_RING_EVT_SEALED 的
|
||||
* 那一拍调一次(重复调返回 WAVE_ST_E_STATE 且不覆盖已有锁存值 —— 半路覆盖
|
||||
* 会让落盘道读到的头与数据不同源)
|
||||
* 函数说明:为什么系数要调用方传、而不让本层自己读 adc_cal —— 本层要能被主机单测零桩
|
||||
* 纳入;更根本的是 §12 的要求:标定一旦落地,老文件必须还能按**当时**的系数
|
||||
* 解释,所以 eng / k / b / clip 属于段头、不属于当前固件状态。
|
||||
* 取值只能在第 3 步那一拍做一次(不每拍读、不低频刷),理由见本文件头注。
|
||||
*/
|
||||
int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘 ACK —— defaultTask 明确告知本段已搬完(成功或失败),据此解冻并起冷却
|
||||
* 入口参数:a_result 0 = 落盘成功(打开、写入字节数相符、目录可用);
|
||||
* 非 0 = 失败(含 f_write 短写、打开/建目录失败、CRC 自弃)——
|
||||
* 失败时段已丢,但环必须解冻,错误只计数不冻结
|
||||
* 返 回 值:WAVE_ST_OK = 已 ACK 并转入冷却;WAVE_ST_E_STATE = 当前不在 SEALED
|
||||
* (重复 ACK 或没有段可 ACK —— 不静默改状态,防落盘道把上一段的 ACK 打到新段),
|
||||
* 或段头还没 latch(第 3 步没做):这时候落盘道拿不到 sealed_meta,根本没可能
|
||||
* 写出文件,放过 ACK 等于白吃一个轮转号并把 seg=NN 报成没发生过的数,
|
||||
* 故一律拒绝。这种段唯一的出口是滞留到 WAVE_SEAL_TIMEOUT_MS 自弃并计 err int32_t
|
||||
* 限定条件:**只能由文件系统唯一写者 defaultTask 调**,一段一次,调之前先看
|
||||
* wave_ring_sealed()(它就是「SEALED 且已 latch」的合并判据)
|
||||
* 函数说明:失败不推进轮转号(下一段仍用同号重写,同号文件按 FA_CREATE_ALWAYS 截断,
|
||||
* 不留半截文件骗过 PC 脚本);成功才推进 seg_idx 与 seg_total。
|
||||
* 本函数不删文件、不碰 FatFs:那些都在落盘道(§5.1 所有权)。
|
||||
*/
|
||||
int32_t wave_ring_write_done(int32_t a_result);
|
||||
|
||||
/* ===== 五、状态查询与已封存区读口(跨任务安全的唯一入口) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:查当前状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:wave_ring_state_t 的取值 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
*/
|
||||
uint8_t wave_ring_state(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查「有已封存段可读」标志 —— 跨任务的唯一握手量(§5.2 第一条陷阱)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = 状态为 SEALED **且段头已 latch**,此刻起 wave_ring_sealed_meta 与
|
||||
* wave_ring_read_ch 可用;0 = 不可用 uint8_t
|
||||
* 限定条件:任意上下文(只读单字节)
|
||||
* 函数说明:为什么把「已 latch」并进来而不是单开一个查询:省得 defaultTask 自己
|
||||
* 拼两个标志、拼错就是拿半份快照落盘(第 4 步明写的禁忌)。
|
||||
* 为 1 期间已封存区对 adcTask 只读不写,读者可以慢慢分次取、不需要临界区;
|
||||
* 为 0 时所有读口一律返 WAVE_ST_E_STATE 而**不是返回旧数据** ——
|
||||
* 把上一段的字节当这一段,等于伪造证据。
|
||||
*/
|
||||
uint8_t wave_ring_sealed(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取已封存段的段头快照(落盘道编头、回放页画标题都用它)
|
||||
* 入口参数:p_out 输出(整份拷出,约 136 字节;不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文(defaultTask / uiTask 只读)。整份拷贝、不暴露内部指针,
|
||||
* 免得调用方攥着指针跨到下一段去看
|
||||
* 函数说明:这里的 fs_hz / pts_per_cycle / pre_pts 是**数据**,回放页与 PC 脚本必须
|
||||
* 从这里读,不许当编译期常量(§2.1 第 1 条)。
|
||||
*/
|
||||
int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:按通道读已封存区的连续点窗(回放页画一路、落盘道搬一路都走它)
|
||||
* 入口参数:a_ch 列号 = 通道表下标 = 语义号(0 到 WAVE_RING_CH-1)
|
||||
* a_from 段内起始点下标(0 = 段首 = 最早的一点。点序口径:触发那一拍的
|
||||
* 样本落在点 WAVE_RING_PRE-1(预触发窗的最后一点),段内点
|
||||
* WAVE_RING_PRE 起是触发之后的第一拍——预触发 8 周波与后触发
|
||||
* 8 周波各 160 点,故障时刻正好是两半的分界(§4.2)
|
||||
* a_cnt 点数(1 起;超出段尾时函数内部夹到剩余点数,不报错)
|
||||
* p_dst 输出 int16 数组(不可为 NULL;画窗 40 点或搬运 128 点按调用方给的容量)
|
||||
* 返 回 值:不小于 0 = 实际读出的点数(到段尾会少于 a_cnt);
|
||||
* WAVE_ST_E_PARAM 空指针或 a_cnt 为 0;WAVE_ST_E_RANGE 列号或起始点越界;
|
||||
* WAVE_ST_E_STATE 无就绪段 int32_t
|
||||
* 限定条件:任意上下文,但**只读已封存区**:本层不提供任何读活缓冲的接口,
|
||||
* IDLE / FREEZE / COOL 下一律 WAVE_ST_E_STATE(§5.2 第一条:不给跨任务读活环的路径)
|
||||
* 函数说明:内部按段首基址取模展开,对读者来说已封存区就是一段 320 点的线性数组。
|
||||
* 回放页画 2 周波 = 40 点(§8.1)一次调用即可;字节形态 = 段文件数据区的
|
||||
* 字节形态(int16 小端),所以落盘道能直接把它交给 wave_store 的打包游标。
|
||||
*/
|
||||
int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/*
|
||||
* 函数功能:同上,形态适配 App/algo/wave_store.h 的 wave_reader_fn(落盘道直接传它)
|
||||
* 入口参数:p_user 上下文(本层是单例,忽略,传 NULL 即可)
|
||||
* a_ch 列号
|
||||
* a_from 段内起始点下标
|
||||
* a_cnt 点数
|
||||
* p_dst 输出 int16 数组
|
||||
* 返 回 值:实际读出的点数(不小于 0);负 = WAVE_ST_E_*(同上三条) int32_t
|
||||
* 限定条件:任意上下文;只读已封存区(同上)
|
||||
* 函数说明:存在的唯一理由是让 wave_store_pack_start 的 p_reader 直接指过来,
|
||||
* 省掉接线索里一个只为转发的包装函数。
|
||||
*/
|
||||
int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/*
|
||||
* 函数功能:取观测计数快照(给 §9 的周期行与主机单测)
|
||||
* 入口参数:p_out 输出(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t
|
||||
* 限定条件:任意上下文(只读;一次拷约 44 字节,个别字段可能与 adcTask 的推进错开一拍,
|
||||
* 这是观测量的固有精度,不构成判据错误)
|
||||
* 函数说明:err_cnt 的两条来源都在本层收口(write_done 非 0、SEALED 滞留超时自弃),
|
||||
* 所以 §9 日志行的 err= 直接取它即可 —— 落盘道不需要自己数第二份账。
|
||||
*/
|
||||
int32_t wave_ring_get_stat(wave_ring_stat_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:把「语义通道号」换算成本环的列号(列序 = 语义号升序,§6.3)
|
||||
* 入口参数:a_sem 语义号(adc_semantic_t 值)
|
||||
* 返 回 值:0 到 WAVE_RING_CH-1 = 列号;WAVE_ST_E_RANGE = 该语义不录(TEMP 与越界值) int32_t
|
||||
* 限定条件:任意上下文(纯计算)
|
||||
* 函数说明:本波只录前 8 个语义通道,所以「列号 == 语义号」这条恒等式成立;
|
||||
* 但调用方必须走这个函数而不是自己写死下标 —— 二期带 TEMP 时列号与语义号
|
||||
* 就会分叉,写死下标的地方会静默错位。
|
||||
*/
|
||||
int32_t wave_ring_col(uint8_t a_sem);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WAVE_RING_H */
|
||||
@@ -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 <stddef.h>
|
||||
|
||||
/* 打包游标一次最多从数据源取多少个点(128 字节栈上临时区,落盘道在 defaultTask 上跑,
|
||||
* 不在 1 kHz 采集链上;取批是为了把「每点一次回调」压成「每 64 点一次回调」) */
|
||||
#define WAVE_ST_BATCH (64u)
|
||||
/* 数据区点下标非法时的内部哨兵(对外一律走 wave_store_data_off 的 0xFFFFFFFFu) */
|
||||
#define WAVE_ST_BAD_OFF (0xFFFFFFFFu)
|
||||
|
||||
/* ===== 内部:小端读写(显式移位拼装,不依赖宿主字节序,板端与 PC 侧同一判据) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:写 16 位小端
|
||||
* 入口参数:p 目标位置(至少 2 字节);v 值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 2 字节
|
||||
*/
|
||||
static void wave_st_put16(uint8_t *p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写 32 位小端
|
||||
* 入口参数:p 目标位置(至少 4 字节);v 值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 4 字节
|
||||
*/
|
||||
static void wave_st_put32(uint8_t *p, uint32_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFFu);
|
||||
p[2] = (uint8_t)((v >> 16) & 0xFFu);
|
||||
p[3] = (uint8_t)((v >> 24) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 16 位小端
|
||||
* 入口参数:p 源位置(至少 2 字节)
|
||||
* 返 回 值:值 uint16_t
|
||||
* 限定条件:调用方保证 p 可读 2 字节
|
||||
*/
|
||||
static uint16_t wave_st_get16(const uint8_t *p) {
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 32 位小端
|
||||
* 入口参数:p 源位置(至少 4 字节)
|
||||
* 返 回 值:值 uint32_t
|
||||
* 限定条件:调用方保证 p 可读 4 字节
|
||||
*/
|
||||
static uint32_t wave_st_get32(const uint8_t *p) {
|
||||
return ((uint32_t)p[0]) | (((uint32_t)p[1]) << 8) |
|
||||
(((uint32_t)p[2]) << 16) | (((uint32_t)p[3]) << 24);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把 int16 写成小端两字节
|
||||
* 入口参数:p 目标位置;v 码值
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:调用方保证 p 可写 2 字节
|
||||
* 函数说明:先转 uint16 再移位——负数右移是实现定义行为,转成无符号后行为确定。
|
||||
*/
|
||||
static void wave_st_put_code(uint8_t *p, int16_t v) {
|
||||
wave_st_put16(p, (uint16_t)v);
|
||||
}
|
||||
|
||||
/* ===== 内部:段头字段一致性自检(编码前与解码后各跑一次,同一把尺子) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:校验段头元数据的**几何与表序**是否自相矛盾
|
||||
* 入口参数:p_meta 段头元数据(非空)
|
||||
* 返 回 值:WAVE_ST_OK = 合法;WAVE_ST_E_SIZE 长度类不成立(零通道 / 零点数 / 预触发超段长);
|
||||
* WAVE_ST_E_FORMAT 档位 / 触发源 / 表序非法 int
|
||||
* 限定条件:p_meta 由调用方保证非空
|
||||
* 函数说明:为什么编码侧也要跑:写一个「看起来像样的坏段文件」比拒写糟得多——落盘道会
|
||||
* 把 CRC 与长度都算自洽的垃圾写进 NAND,等到回放页或 PC 脚本才发现,那时
|
||||
* 现场已经没了(§8.1「画成一条直线等于伪造证据」是同一条忌讳的另一半)。
|
||||
*/
|
||||
static int wave_st_meta_check(const wave_seg_meta_t *p_meta) {
|
||||
uint8_t i;
|
||||
|
||||
/* 零通道 / 零点数:这一段连长度都不成立(§6.2 末行「长度为 0 判废」) */
|
||||
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->pts_per_ch == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 预触发点数不能大于一段的点数;后触发 = pts_per_ch - pre_pts,允许为 0(手动触发
|
||||
* 撞上落盘窗口等极端情形),但不允许负数——那是把段长算错 */
|
||||
if (p_meta->pre_pts > p_meta->pts_per_ch) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 采样率与每周波点数是数据,但为 0 的数据没法解释(回放与 PC 都要拿它做除数) */
|
||||
if ((p_meta->fs_hz == 0u) || (p_meta->pts_per_cycle == 0u)) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
/* 与 PC 侧 reader 的拒绝清单对齐(谁都不能产出一段「自己写得出、对面读不了」的文件):
|
||||
* 每周波不足 2 点连一个周期都描不出来;段长不足一个整周期则基波算法无从取窗。 */
|
||||
if (p_meta->pts_per_cycle < 2u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->pts_per_ch < (uint32_t)p_meta->pts_per_cycle) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (p_meta->eng > (uint8_t)WAVE_ENG_PRI) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
if (p_meta->trig_src > (uint8_t)WAVE_TRIG_BENCH) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* 表序:第 i 项的语义号必须等于下标(§6.3「通道表序与列序」行)——
|
||||
* 数据区第 i 列归第 i 项解释,错一位就是拿 IB 的波形配 IC 的系数 */
|
||||
for (i = 0u; i < p_meta->ch_count; i++) {
|
||||
if (p_meta->ch[i].sem != i) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
if (p_meta->ch[i].conv > (uint8_t)1u) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* 位宽留个合理域:本波只有 16(TPAFE)与 12(片内 ADC),但格式层不写死这两个值,
|
||||
* 只挡明显不成立的(0 位、超过 32 位)——二期换转换器不必改这里 */
|
||||
if ((p_meta->ch[i].bits == 0u) || (p_meta->ch[i].bits > 32u)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== CRC-16/MODBUS ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 增量计算
|
||||
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
|
||||
* p_data 数据(NULL 视为空操作)
|
||||
* a_len 字节数
|
||||
* 返 回 值:新的中间值 uint16_t
|
||||
* 限定条件:任意上下文;纯计算
|
||||
* 函数说明:反射形态 = 右移、异或 0xA001、初值 0xFFFF、异或输出 0;
|
||||
* 写进文件时低字节在前(§6.3)。无表实现的理由见 wave_store.h 头注。
|
||||
*/
|
||||
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len) {
|
||||
uint16_t crc = a_crc;
|
||||
uint32_t i;
|
||||
|
||||
if (p_data == NULL) {
|
||||
return crc;
|
||||
}
|
||||
for (i = 0u; i < a_len; i++) {
|
||||
uint8_t b = p_data[i]; /* 身兼两职:本路字节 + 下面内层 8 次的计数器 */
|
||||
|
||||
crc = (uint16_t)(crc ^ (uint16_t)b);
|
||||
for (b = 0u; b < 8u; b++) {
|
||||
if ((crc & 0x0001u) != 0u) {
|
||||
crc = (uint16_t)((crc >> 1) ^ WAVE_ST_CRC_POLY);
|
||||
} else {
|
||||
crc = (uint16_t)(crc >> 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 异或输出恒 0:写成宏引用,换异或输出时只改一处 */
|
||||
return (uint16_t)(crc ^ WAVE_ST_CRC_XOROUT);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 一次算完
|
||||
* 入口参数:p_data 数据;a_len 字节数
|
||||
* 返 回 值:CRC;空输入返回初值定义 uint16_t
|
||||
* 限定条件:任意上下文。零长度的判废由调用方做(见 wave_store.h 同函数注释)
|
||||
*/
|
||||
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len) {
|
||||
return wave_store_crc_update(WAVE_ST_CRC_INIT, p_data, a_len);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CRC 实现自检(对规范锚点,不是自算自验)
|
||||
* 入口参数:无
|
||||
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符 int
|
||||
* 限定条件:任意上下文;开机或首次落盘前一次,不在每拍路径上
|
||||
* 函数说明:锚点串「123456789」的 CRC-16/MODBUS = 0x4B37(§6.3)。字面量必须纯 ASCII,
|
||||
* 所以这里用显式字节数组而不是把中文塞进字符串。
|
||||
*/
|
||||
int wave_store_crc_selftest(void) {
|
||||
static const uint8_t s_anchor[9] = {
|
||||
(uint8_t)0x31u, (uint8_t)0x32u, (uint8_t)0x33u,
|
||||
(uint8_t)0x34u, (uint8_t)0x35u, (uint8_t)0x36u,
|
||||
(uint8_t)0x37u, (uint8_t)0x38u, (uint8_t)0x39u
|
||||
};
|
||||
uint16_t got;
|
||||
|
||||
got = wave_store_crc(s_anchor, (uint32_t)sizeof(s_anchor));
|
||||
if (got != WAVE_ST_CRC_CHECK) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 段头编解码 ===== */
|
||||
|
||||
/* 语义通道的转换器归属与位宽(0 = TPAFE5160,1 = 片内 ADC1;口径属主是
|
||||
* docs/硬件平台/ADC通道映射说明.md,本表是其抄本,改硬件接线时两处一起改)。
|
||||
* 第 9 项 TEMP 本波不录入段文件(ch_count 恒 8),仍按实际归属填,
|
||||
* 免得将来放开 TEMP 时这里是个 0。 */
|
||||
static const uint8_t s_sem_conv[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
(uint8_t)0u, (uint8_t)1u, (uint8_t)1u,
|
||||
(uint8_t)0u, (uint8_t)0u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u
|
||||
};
|
||||
static const uint8_t s_sem_bits[(uint8_t)WAVE_ST_CH_MAX] = {
|
||||
16u, 12u, 12u,
|
||||
16u, 16u, 12u, 12u, 12u, 12u
|
||||
};
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据填成本波默认值(只填格式侧静态事实)
|
||||
* 入口参数:p_meta 输出
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:填三样格式侧的静态事实:语义号 = 下标(§6.3 表序口径,也是 PC 侧判据)、
|
||||
* conv / bits 按两片转换器的归属(下面那张表的口径属主是
|
||||
* docs/硬件平台/ADC通道映射说明.md 的抄本,本层刻意不带 adc_input.h,
|
||||
* 这样格式层在主机侧不需要任何假件就能编译;单测里 0xE6 的算术交叉验证
|
||||
* 会同时钉住这套顺序,抄错就会红)。
|
||||
* 不填 avail / k_q16 / b_codes:那些是封存瞬间的事实,必须由调用方现取现填
|
||||
* (adc_cal_k_q16 / adc_cal_flags / sys_clock),本层不链它们;
|
||||
* 调用方忘填 k_q16 与 avail 不会让编码失败(0 是合法整数),
|
||||
* 但会让 PC 侧把波形解释成一条 0 线——所以单测里有一条专门检查
|
||||
* 「填了系数的往返」与「没填系数时段的形状仍然正确、只是幅值归零」。
|
||||
*/
|
||||
int wave_store_meta_init(wave_seg_meta_t *p_meta) {
|
||||
uint8_t i;
|
||||
|
||||
if (p_meta == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
p_meta->fs_hz = (uint16_t)WAVE_ST_FS_DEF;
|
||||
p_meta->pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
|
||||
p_meta->ch_count = (uint8_t)WAVE_ST_CH_NUM;
|
||||
p_meta->pts_per_ch = (uint16_t)WAVE_ST_PTS_DEF;
|
||||
p_meta->pre_pts = (uint16_t)WAVE_ST_PRE_DEF;
|
||||
p_meta->trig_src = (uint8_t)WAVE_TRIG_NONE;
|
||||
p_meta->trig_elm = 0u;
|
||||
p_meta->t0_ms = 0u;
|
||||
p_meta->wall = 0u;
|
||||
p_meta->eng = (uint8_t)WAVE_ENG_PIN;
|
||||
p_meta->flags_seq = 0u;
|
||||
p_meta->clip_mask = 0u;
|
||||
p_meta->frame_seq = 0u;
|
||||
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
|
||||
p_meta->ch[i].sem = i;
|
||||
p_meta->ch[i].conv = s_sem_conv[i];
|
||||
p_meta->ch[i].bits = s_sem_bits[i];
|
||||
p_meta->ch[i].avail = 0u;
|
||||
p_meta->ch[i].k_q16 = 0;
|
||||
p_meta->ch[i].b_codes = 0;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:内部——把固定头 + 通道表写进缓冲(不含头 CRC)
|
||||
* 入口参数:p_meta 段头;p_out 缓冲(容量须 >= 头 CRC 覆盖区)
|
||||
* 返 回 值:写入字节数(= 头 CRC 覆盖区长度) uint32_t
|
||||
* 限定条件:p_meta 已过 wave_st_meta_check、p_out 容量已核过
|
||||
*/
|
||||
static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) {
|
||||
uint8_t i;
|
||||
const uint32_t tbl_n = (uint32_t)p_meta->ch_count * WAVE_ST_CH_BYTES;
|
||||
|
||||
p_out[WAVE_ST_OFF_MAGIC] = (uint8_t)WAVE_ST_MAGIC0;
|
||||
p_out[WAVE_ST_OFF_MAGIC + 1u] = (uint8_t)WAVE_ST_MAGIC1;
|
||||
/* 魔数第三字节 = 版本号:与 ver 字段同源同值(§6.2 第 0 行 + §6.3 判废口径) */
|
||||
p_out[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER;
|
||||
p_out[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_FS_HZ], p_meta->fs_hz);
|
||||
p_out[WAVE_ST_OFF_PTS_CYC] = p_meta->pts_per_cycle;
|
||||
p_out[WAVE_ST_OFF_CH_CNT] = p_meta->ch_count;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_PTS_CH], p_meta->pts_per_ch);
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_PRE_PTS], p_meta->pre_pts);
|
||||
p_out[WAVE_ST_OFF_TRIG_SRC] = p_meta->trig_src;
|
||||
p_out[WAVE_ST_OFF_TRIG_ELM] = p_meta->trig_elm;
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_T0_MS], p_meta->t0_ms);
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_WALL], p_meta->wall);
|
||||
p_out[WAVE_ST_OFF_ENG] = p_meta->eng;
|
||||
p_out[WAVE_ST_OFF_FLAGS] = p_meta->flags_seq;
|
||||
wave_st_put16(&p_out[WAVE_ST_OFF_CLIP], p_meta->clip_mask);
|
||||
wave_st_put32(&p_out[WAVE_ST_OFF_SEQ], p_meta->frame_seq);
|
||||
|
||||
for (i = 0u; i < p_meta->ch_count; i++) {
|
||||
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)i * WAVE_ST_CH_BYTES);
|
||||
|
||||
p_out[pos + WAVE_ST_CH_OFF_SEM] = p_meta->ch[i].sem;
|
||||
p_out[pos + WAVE_ST_CH_OFF_CONV] = p_meta->ch[i].conv;
|
||||
p_out[pos + WAVE_ST_CH_OFF_BITS] = p_meta->ch[i].bits;
|
||||
p_out[pos + WAVE_ST_CH_OFF_AVAIL] = p_meta->ch[i].avail;
|
||||
/* int32 系数按小端原样落 4 字节:右移到 24 位前把符号扩展掉,
|
||||
* 保证板端 ARMCC 与主机 CRT 写出同一串字节 */
|
||||
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_KQ16], (uint32_t)p_meta->ch[i].k_q16);
|
||||
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_BCODE], (uint32_t)p_meta->ch[i].b_codes);
|
||||
}
|
||||
return (uint32_t)WAVE_ST_OFF_TBL + tbl_n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:编码段头(固定头 + 通道表 + 头 CRC)
|
||||
* 入口参数:p_meta 段头元数据;p_out 输出缓冲;a_cap 容量
|
||||
* 返 回 值:>= 0 写入字节数;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文;无静态缓冲、可重入
|
||||
* 函数说明:头 CRC 覆盖 [0, 30+12×ch_count)、自身紧随其后、低字节在前(§6.3)。
|
||||
*/
|
||||
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap) {
|
||||
uint32_t crc_pos;
|
||||
uint16_t crc;
|
||||
int rc;
|
||||
|
||||
if ((p_meta == NULL) || (p_out == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_st_meta_check(p_meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
crc_pos = WAVE_ST_HEAD_CRC_POS(p_meta->ch_count);
|
||||
if (a_cap < (crc_pos + 2u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
(void)wave_st_head_raw(p_meta, p_out);
|
||||
crc = wave_store_crc(p_out, crc_pos);
|
||||
/* 低字节在前(§6.3 首行):put16 本来就是小端,这里不额外翻转 */
|
||||
wave_st_put16(&p_out[crc_pos], crc);
|
||||
return (int)(crc_pos + 2u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解码 + 全套判废(魔数 / 版本 / 缓冲长度 / 几何 / 表序 / 头 CRC)
|
||||
* 入口参数:p_buf 头字节流;a_len 可读字节数;p_meta 输出
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:顺序刻意是「先验魔数与版本,再算整头长度,再验 CRC,最后验语义」——
|
||||
* 前两步决定我们**有没有资格**按这份布局去数偏移;反过来先按 ch_count 算长度,
|
||||
* 一个被写坏的 ch_count 就能让我们读到缓冲外面去。
|
||||
*/
|
||||
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta) {
|
||||
uint8_t ch_count;
|
||||
uint32_t crc_pos;
|
||||
uint32_t head_sz;
|
||||
uint16_t want;
|
||||
uint16_t got;
|
||||
int rc;
|
||||
wave_seg_meta_t tmp;
|
||||
|
||||
if ((p_buf == NULL) || (p_meta == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
/* 固定头是 30 + 1 字节通道数才能算出整头长度,至少得读到偏移 7 */
|
||||
if (a_len < ((uint32_t)WAVE_ST_OFF_CH_CNT + 1u)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if ((p_buf[WAVE_ST_OFF_MAGIC] != (uint8_t)WAVE_ST_MAGIC0) ||
|
||||
(p_buf[WAVE_ST_OFF_MAGIC + 1u] != (uint8_t)WAVE_ST_MAGIC1)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
/* ver 必须等于魔数第三字节、且等于本层认识的版本(§6.2 末行) */
|
||||
if ((p_buf[WAVE_ST_OFF_VER] != p_buf[WAVE_ST_OFF_MAGIC + 2u]) ||
|
||||
(p_buf[WAVE_ST_OFF_VER] != (uint8_t)WAVE_ST_VER)) {
|
||||
return WAVE_ST_E_FORMAT;
|
||||
}
|
||||
ch_count = p_buf[WAVE_ST_OFF_CH_CNT];
|
||||
head_sz = wave_store_head_bytes(ch_count);
|
||||
if (head_sz == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_len < head_sz) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
crc_pos = WAVE_ST_HEAD_CRC_POS(ch_count);
|
||||
want = wave_st_get16(&p_buf[crc_pos]);
|
||||
got = wave_store_crc(p_buf, crc_pos);
|
||||
if (want != got) {
|
||||
return WAVE_ST_E_CRC;
|
||||
}
|
||||
|
||||
/* 头 CRC 过了才开始解释字段:下面填的是一份可交回调用方的完整元数据 */
|
||||
tmp.fs_hz = wave_st_get16(&p_buf[WAVE_ST_OFF_FS_HZ]);
|
||||
tmp.pts_per_cycle = p_buf[WAVE_ST_OFF_PTS_CYC];
|
||||
tmp.ch_count = ch_count;
|
||||
tmp.pts_per_ch = wave_st_get16(&p_buf[WAVE_ST_OFF_PTS_CH]);
|
||||
tmp.pre_pts = wave_st_get16(&p_buf[WAVE_ST_OFF_PRE_PTS]);
|
||||
tmp.trig_src = p_buf[WAVE_ST_OFF_TRIG_SRC];
|
||||
tmp.trig_elm = p_buf[WAVE_ST_OFF_TRIG_ELM];
|
||||
tmp.t0_ms = wave_st_get32(&p_buf[WAVE_ST_OFF_T0_MS]);
|
||||
tmp.wall = wave_st_get32(&p_buf[WAVE_ST_OFF_WALL]);
|
||||
tmp.eng = p_buf[WAVE_ST_OFF_ENG];
|
||||
tmp.flags_seq = p_buf[WAVE_ST_OFF_FLAGS];
|
||||
tmp.clip_mask = wave_st_get16(&p_buf[WAVE_ST_OFF_CLIP]);
|
||||
tmp.frame_seq = wave_st_get32(&p_buf[WAVE_ST_OFF_SEQ]);
|
||||
for (ch_count = 0u; ch_count < (uint8_t)WAVE_ST_CH_MAX; ch_count++) {
|
||||
tmp.ch[ch_count].sem = 0u;
|
||||
tmp.ch[ch_count].conv = 0u;
|
||||
tmp.ch[ch_count].bits = 0u;
|
||||
tmp.ch[ch_count].avail = 0u;
|
||||
tmp.ch[ch_count].k_q16 = 0;
|
||||
tmp.ch[ch_count].b_codes = 0;
|
||||
}
|
||||
for (ch_count = 0u; ch_count < p_buf[WAVE_ST_OFF_CH_CNT]; ch_count++) {
|
||||
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)ch_count * WAVE_ST_CH_BYTES);
|
||||
tmp.ch[ch_count].sem = p_buf[pos + WAVE_ST_CH_OFF_SEM];
|
||||
tmp.ch[ch_count].conv = p_buf[pos + WAVE_ST_CH_OFF_CONV];
|
||||
tmp.ch[ch_count].bits = p_buf[pos + WAVE_ST_CH_OFF_BITS];
|
||||
tmp.ch[ch_count].avail = p_buf[pos + WAVE_ST_CH_OFF_AVAIL];
|
||||
tmp.ch[ch_count].k_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_KQ16]);
|
||||
tmp.ch[ch_count].b_codes = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_BCODE]);
|
||||
}
|
||||
rc = wave_st_meta_check(&tmp);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
*p_meta = tmp;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 长度 / 偏移换算 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整头字节数
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* 返 回 值:字节数;通道数非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_head_bytes(uint8_t a_ch_count) {
|
||||
if ((a_ch_count == 0u) || (a_ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return 0u;
|
||||
}
|
||||
return WAVE_ST_HEAD_BYTES(a_ch_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按头部字段算整段应有字节数
|
||||
* 入口参数:p_meta 段头
|
||||
* 返 回 值:期望字节数;字段非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:等式 = 32 + 12×ch_count + pts_per_ch×ch_count×2 + 2(§6.3;
|
||||
* 8 路 320 点 = 5250)。落盘道拿它比对 f_write 写回字节数与 f_size。
|
||||
*/
|
||||
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta) {
|
||||
if (p_meta == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
|
||||
return 0u;
|
||||
}
|
||||
if (p_meta->pts_per_ch == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return WAVE_ST_SEG_TOTAL(p_meta->ch_count, p_meta->pts_per_ch);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:数据区某点的文件绝对偏移
|
||||
* 入口参数:a_ch_count 通道数;a_pts_ch 每通道点数;a_ch 列号;a_pt 段内点下标
|
||||
* 返 回 值:偏移;非法返回 0xFFFFFFFFu 哨兵 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:按通道分列 → 偏移 = 整头长 + (列 × 点数 + 点下标) × 2。单通道整列是连续的,
|
||||
* 这正是 §6.2「只读那一路」能一次连续读的原因。
|
||||
*/
|
||||
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
|
||||
uint8_t a_ch, uint32_t a_pt) {
|
||||
uint32_t head_sz = wave_store_head_bytes(a_ch_count);
|
||||
|
||||
if ((head_sz == 0u) || (a_pts_ch == 0u) || (a_ch >= a_ch_count)) {
|
||||
return WAVE_ST_BAD_OFF;
|
||||
}
|
||||
if (a_pt >= (uint32_t)a_pts_ch) {
|
||||
return WAVE_ST_BAD_OFF;
|
||||
}
|
||||
return head_sz + (((uint32_t)a_ch * (uint32_t)a_pts_ch) + a_pt) * 2u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从某点起当前列还能连续读多少字节
|
||||
* 入口参数:a_pts_ch 每通道点数;a_pt 起始点下标;a_want 想读点数
|
||||
* 返 回 值:可连续读字节数(参数非法返回 0) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want) {
|
||||
uint32_t left;
|
||||
uint32_t n;
|
||||
|
||||
if ((a_pts_ch == 0u) || (a_want == 0u)) {
|
||||
return 0u;
|
||||
}
|
||||
if (a_pt >= (uint32_t)a_pts_ch) {
|
||||
return 0u;
|
||||
}
|
||||
left = (uint32_t)a_pts_ch - a_pt;
|
||||
n = (a_want < left) ? a_want : left;
|
||||
return n * 2u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:小端字节 → int16 点序列
|
||||
* 入口参数:p_raw 原始字节;a_len 字节数;p_out 输出;a_max_pts 容量;p_pts 实际点数(可空)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:奇数字节按参数错处理而不是「丢掉尾巴」——半截样本被静默吞掉,
|
||||
* 回放页画出来的波形会比真的少最后一个点,而这种误差正好最难查。
|
||||
*/
|
||||
int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len,
|
||||
int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts) {
|
||||
uint32_t n;
|
||||
uint32_t i;
|
||||
|
||||
if ((p_raw == NULL) || (p_out == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (((a_len & 0x00000001u) != 0u) || (a_len > (a_max_pts * 2u))) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
n = a_len / 2u;
|
||||
for (i = 0u; i < n; i++) {
|
||||
/* 先攒成 uint16 再转 int16:补码回绕在 C 里是实现定义但两大编译器一致,
|
||||
* 而这里需要的正是「按补码位型解释」——TPAFE 的 16 位补码就是这么存的 */
|
||||
p_out[i] = (int16_t)wave_st_get16(&p_raw[i * 2u]);
|
||||
}
|
||||
if (p_pts != NULL) {
|
||||
*p_pts = n;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:码值 → 定点毫单位(mV)
|
||||
* 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出 mV
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;用 int64 中间量)
|
||||
* 函数说明:模型 eng = (code + b) × k × 1e-6(§6.3,与 adc_cal_apply() 同式),
|
||||
* k = k_q16 / 65536 µV/码 → mV = (code + b) × k_q16 / 65536000,四舍五入。
|
||||
* 中间量最大约 (32767 + 8192) × 5.28e7 ≈ 2.2e12,int64 宽裕。
|
||||
*/
|
||||
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
|
||||
int32_t *p_milli) {
|
||||
int64_t num;
|
||||
int64_t den = 65536000;
|
||||
int64_t v;
|
||||
|
||||
if ((p_desc == NULL) || (p_milli == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
num = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16;
|
||||
if (num >= 0) {
|
||||
v = (num + (den / 2)) / den;
|
||||
} else {
|
||||
v = -(((-num) + (den / 2)) / den);
|
||||
}
|
||||
*p_milli = (int32_t)v;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 整段校验 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整段镜像自检(头 + 数据区 + 数据 CRC)
|
||||
* 入口参数:p_img 镜像;a_len 长度;p_meta 输出段头(可空)
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
|
||||
* 限定条件:任意上下文;整段驻留时用(本波 5250 字节)
|
||||
* 函数说明:三条判废一条都不能省:长度 0 或不等、头 CRC、数据 CRC。
|
||||
* 期望总长用**头里的字段**算,不用编译期常量——文件是自己说它多长,
|
||||
* 与镜像实际长度不一致就是坏段(§6.2 末行)。
|
||||
*/
|
||||
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta) {
|
||||
wave_seg_meta_t meta;
|
||||
uint32_t want_total;
|
||||
uint32_t data_off;
|
||||
uint32_t data_len;
|
||||
uint16_t want;
|
||||
uint16_t got;
|
||||
int rc;
|
||||
|
||||
if (p_img == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_len == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
rc = wave_store_head_parse(p_img, a_len, &meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
return rc;
|
||||
}
|
||||
want_total = wave_store_seg_size(&meta);
|
||||
if (want_total == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_len != want_total) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
data_off = wave_store_head_bytes(meta.ch_count); /* 数据区起点 = 整头字节数 */
|
||||
data_len = a_len - 2u - data_off;
|
||||
if (data_len == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
want = wave_st_get16(&p_img[a_len - 2u]);
|
||||
got = wave_store_crc(&p_img[data_off], data_len);
|
||||
if (want != got) {
|
||||
return WAVE_ST_E_CRC;
|
||||
}
|
||||
if (p_meta != NULL) {
|
||||
*p_meta = meta;
|
||||
}
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* ===== 打包游标 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化打包游标
|
||||
* 入口参数:p_ctx 游标;p_meta 段头;p_reader 数据源(可空 = 只产头);p_user 上下文
|
||||
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文(纯计算);一段一个游标
|
||||
* 函数说明:段头整份拷进游标——搬运窗口几十毫秒,期间调用方完全可以去改它自己那份
|
||||
* (比如 adcTask 又跑了几拍),本段的字节不能受影响。
|
||||
*/
|
||||
int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta,
|
||||
wave_reader_fn p_reader, void *p_user) {
|
||||
int rc;
|
||||
|
||||
if ((p_ctx == NULL) || (p_meta == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_st_meta_check(p_meta);
|
||||
if (rc != WAVE_ST_OK) {
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
|
||||
return rc;
|
||||
}
|
||||
p_ctx->total = wave_store_seg_size(p_meta);
|
||||
if (p_ctx->total == 0u) {
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_HEAD;
|
||||
p_ctx->ch = 0u;
|
||||
p_ctx->head_done = 0u;
|
||||
p_ctx->ch_done = 0u;
|
||||
p_ctx->tail_done = 0u;
|
||||
p_ctx->rsv = 0u;
|
||||
p_ctx->crc_data = WAVE_ST_CRC_INIT;
|
||||
p_ctx->sent = 0u;
|
||||
p_ctx->p_reader = p_reader;
|
||||
p_ctx->p_user = p_user;
|
||||
p_ctx->meta = *p_meta;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取整段字节流的下一片
|
||||
* 入口参数:p_ctx 游标;p_out 缓冲;a_cap 容量;p_len 实际产出字节数
|
||||
* 返 回 值:WAVE_ST_OK 还有后续;WAVE_ST_DONE 整段产完;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文,同一游标不得并发使用
|
||||
* 函数说明:三段推进——头(一次给完,要求容量 >= 整头)、数据区(按列连续、整样本切,
|
||||
* 绝不切半个 int16,也不跨列,便于回调按列读)、尾 CRC 两字节。
|
||||
* 数据 CRC 边产边算(增量中间值),所以全程不需要整段驻留 RAM。
|
||||
*/
|
||||
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
|
||||
uint32_t a_cap, uint32_t *p_len) {
|
||||
uint32_t n = 0u;
|
||||
|
||||
if ((p_ctx == NULL) || (p_out == NULL) || (p_len == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
*p_len = 0u;
|
||||
if ((p_ctx->phase == (uint8_t)WAVE_PACK_IDLE) || (a_cap == 0u)) {
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) {
|
||||
return WAVE_ST_DONE;
|
||||
}
|
||||
|
||||
/* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 128 字节整头) --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_HEAD) {
|
||||
uint32_t head_sz;
|
||||
int rc;
|
||||
|
||||
head_sz = wave_store_head_bytes(p_ctx->meta.ch_count);
|
||||
if (head_sz == 0u) {
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
if (a_cap < head_sz) {
|
||||
/* 不拆着头给:半截头意味着游标要存 128 字节中间态,纯浪费 */
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
rc = wave_store_head_build(&p_ctx->meta, p_out, a_cap);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
n = (uint32_t)rc;
|
||||
p_ctx->head_done = (uint16_t)n;
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_DATA;
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
|
||||
/* --- 数据区:按列推进,一次最多一批 --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_DATA) {
|
||||
int16_t batch[WAVE_ST_BATCH];
|
||||
|
||||
if (p_ctx->p_reader == NULL) {
|
||||
/* 没有数据源却进了数据区:宁可报错也不产空字节。
|
||||
* 产空字节会写出「长度对、内容是 0」的段文件,回放页会画出一条直线——
|
||||
* 那正是 §8.1 明令禁止的伪造证据形态。 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
while ((n < a_cap) && (p_ctx->ch < p_ctx->meta.ch_count)) {
|
||||
uint32_t room_pts = (a_cap - n) / 2u;
|
||||
uint32_t col_left = (uint32_t)p_ctx->meta.pts_per_ch - (uint32_t)p_ctx->ch_done;
|
||||
uint32_t take;
|
||||
int32_t got;
|
||||
uint32_t i;
|
||||
|
||||
if (col_left == 0u) {
|
||||
p_ctx->ch++;
|
||||
p_ctx->ch_done = 0u;
|
||||
continue;
|
||||
}
|
||||
if (room_pts == 0u) {
|
||||
break;
|
||||
}
|
||||
take = (room_pts < col_left) ? room_pts : col_left;
|
||||
if (take > (uint32_t)WAVE_ST_BATCH) {
|
||||
take = (uint32_t)WAVE_ST_BATCH;
|
||||
}
|
||||
got = p_ctx->p_reader(p_ctx->p_user, p_ctx->ch, (uint32_t)p_ctx->ch_done,
|
||||
take, batch);
|
||||
if (got < 0) {
|
||||
return (int)got;
|
||||
}
|
||||
if ((uint32_t)got != take) {
|
||||
/* 数据源给不满就是数据源的问题(越界 / 未封存),不拿半批凑长度 */
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
uint32_t base_n = n;
|
||||
for (i = 0u; i < take; i++) {
|
||||
wave_st_put_code(&p_out[n], batch[i]);
|
||||
n += 2u;
|
||||
}
|
||||
p_ctx->crc_data = wave_store_crc_update(p_ctx->crc_data, &p_out[base_n], take * 2u);
|
||||
p_ctx->ch_done = (uint16_t)((uint32_t)p_ctx->ch_done + take);
|
||||
}
|
||||
if (p_ctx->ch >= p_ctx->meta.ch_count) {
|
||||
/* 最后一列的最后一点产出后,「列走完了」要到下一次进循环才发现,
|
||||
* 所以这一拍可能一个字节都没产。这时候不许当参数错报出去:整段就差
|
||||
* 最后两片 CRC,报 E_PARAM 会让落盘道把一次真故障判成落盘失败(白丢)。
|
||||
* 让它落到下面的尾段分支,在同一次调用里把尾产完。 */
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_TAIL;
|
||||
}
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
if (n > 0u) {
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
if (p_ctx->phase != (uint8_t)WAVE_PACK_TAIL) {
|
||||
return WAVE_ST_E_PARAM; /* 容量连一个整样本都装不下 */
|
||||
}
|
||||
}
|
||||
|
||||
/* --- 尾:数据区 CRC,低字节在前 --- */
|
||||
if (p_ctx->phase == (uint8_t)WAVE_PACK_TAIL) {
|
||||
if (p_ctx->tail_done == 0u) {
|
||||
p_out[n] = (uint8_t)(p_ctx->crc_data & 0xFFu);
|
||||
n++;
|
||||
p_ctx->tail_done = 1u;
|
||||
}
|
||||
if ((p_ctx->tail_done == 1u) && (n < a_cap)) {
|
||||
p_out[n] = (uint8_t)((p_ctx->crc_data >> 8) & 0xFFu);
|
||||
n++;
|
||||
p_ctx->tail_done = 2u;
|
||||
p_ctx->phase = (uint8_t)WAVE_PACK_DONE;
|
||||
}
|
||||
p_ctx->sent += n;
|
||||
*p_len = n;
|
||||
return (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) ? WAVE_ST_DONE : WAVE_ST_OK;
|
||||
}
|
||||
return WAVE_ST_E_STATE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:游标已产出字节数
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:字节数;空指针返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx) {
|
||||
return (p_ctx == NULL) ? 0u : p_ctx->sent;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:游标对应整段的期望总长
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:字节数;空指针返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx) {
|
||||
return (p_ctx == NULL) ? 0u : p_ctx->total;
|
||||
}
|
||||
|
||||
/* ===== 段文件名轮转(只算名字,不碰文件系统) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:内部——把两位十进制写进缓冲
|
||||
* 入口参数:p 目标(至少 2 字节);a_idx 0..99
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:a_idx 已由调用方限制在 0..99
|
||||
*/
|
||||
static void wave_st_put_idx2(char *p, uint8_t a_idx) {
|
||||
p[0] = (char)('0' + (a_idx / 10u));
|
||||
p[1] = (char)('0' + (a_idx % 10u));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按段号生成段文件路径
|
||||
* 入口参数:p_out 缓冲;a_cap 容量;a_idx 段号 0..99
|
||||
* 返 回 值:>= 0 写入字符数(不含结束符);WAVE_ST_E_PARAM / WAVE_ST_E_RANGE int
|
||||
* 限定条件:任意上下文;纯字符拼装(不用格式化函数),板端与 PC 侧逐字节一致
|
||||
* 函数说明:形态 = 目录 / 前缀 + 两位十进制 + 扩展名,严格 8.3(§5.2 第三条)。
|
||||
*/
|
||||
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx) {
|
||||
static const char s_dir[] = WAVE_ST_DIR;
|
||||
static const char s_pfx[] = WAVE_ST_PFX;
|
||||
static const char s_ext[] = WAVE_ST_EXT;
|
||||
uint32_t pos = 0u;
|
||||
uint32_t i;
|
||||
char buf[WAVE_ST_PATH_MAX];
|
||||
|
||||
if (p_out == NULL) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
|
||||
return WAVE_ST_E_RANGE;
|
||||
}
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_dir) - 1u); i++) {
|
||||
buf[pos++] = s_dir[i];
|
||||
}
|
||||
buf[pos++] = '/';
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
|
||||
buf[pos++] = s_pfx[i];
|
||||
}
|
||||
wave_st_put_idx2(&buf[pos], a_idx);
|
||||
pos += 2u;
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_ext) - 1u); i++) {
|
||||
buf[pos++] = s_ext[i];
|
||||
}
|
||||
if (pos >= (uint32_t)sizeof(buf)) {
|
||||
/* 宏与拼装长度漂移时的兜底:宁可给不出名字,也绝不把结束符写到别人缓冲外 */
|
||||
return WAVE_ST_E_SIZE;
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
/* 容量必须容得下结束符:写成 a_cap < pos 会放过「刚好差一个字节」的调用方,
|
||||
* 于是把 p_out[pos] 写到别人缓冲外面(越界写在板子上是静默的) */
|
||||
if (a_cap <= pos) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
for (i = 0u; i <= pos; i++) {
|
||||
p_out[i] = buf[i];
|
||||
}
|
||||
return (int)pos;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:下一个轮转段号
|
||||
* 入口参数:a_idx 当前段号(越界按 0 处理)
|
||||
* 返 回 值:下一个段号 uint8_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint8_t wave_store_next_idx(uint8_t a_idx) {
|
||||
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
|
||||
return 0u;
|
||||
}
|
||||
if (a_idx >= ((uint8_t)WAVE_ST_SEG_MAX - 1u)) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint8_t)(a_idx + 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:内部——大小写无关地比对一个字符
|
||||
* 入口参数:a 待测字符(视作 ASCII);b 期望字符(取自段名宏)
|
||||
* 返 回 值:1 相同;0 不同 uint8_t
|
||||
* 限定条件:两侧都是 ASCII;非字母字符原样比
|
||||
* 函数说明:**两侧都**归一化成小写再比。只归一化一侧是个真踩过一次的坑——
|
||||
* 段名宏 WAVE_ST_PFX / WAVE_ST_EXT 是大写形态(FatFs 8.3 短名就是大写),
|
||||
* 只把待测串转小写等于「小写 vs 大写」永不相等,反解会拒绝掉全部合法段名
|
||||
* (单测 用例:首轮 100% 拒绝就是这个)。盘上大小写与宏写法
|
||||
* 谁都不该去猜,故两侧各归一一次。
|
||||
*/
|
||||
static uint8_t wave_st_ci_eq(char a, char b) {
|
||||
char x = a;
|
||||
char y = b;
|
||||
|
||||
if ((x >= 'A') && (x <= 'Z')) {
|
||||
x = (char)(x + ('a' - 'A'));
|
||||
}
|
||||
if ((y >= 'A') && (y <= 'Z')) {
|
||||
y = (char)(y + ('a' - 'A'));
|
||||
}
|
||||
return (x == y) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从文件名或路径反解段号
|
||||
* 入口参数:p_path 路径或纯文件名;p_idx 输出
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:只认 basename 上的「wave + 两位数字 + .bin」(大小写都收)。
|
||||
* 不匹配就返回错误,**绝不**把陌生名字当录波段——扫目录重建段账时误认一个
|
||||
* 别的文件,等于把别人的文件当波形解释(名字/参数类错误码严禁升级成卷操作,
|
||||
* 存储架构说明 §10.1 同一条教训)。
|
||||
*/
|
||||
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx) {
|
||||
static const char s_pfx[] = WAVE_ST_PFX;
|
||||
static const char s_ext[] = WAVE_ST_EXT;
|
||||
const char *base = p_path;
|
||||
const char *q = p_path;
|
||||
uint32_t i;
|
||||
uint32_t len = 0u;
|
||||
uint8_t hi;
|
||||
uint8_t lo;
|
||||
|
||||
if ((p_path == NULL) || (p_idx == NULL)) {
|
||||
return WAVE_ST_E_PARAM;
|
||||
}
|
||||
/* 找 basename:支持反斜杠形态(PC 侧脚本拼出来的路径可能被原样传进来) */
|
||||
while (*q != '\0') {
|
||||
if ((*q == '/') || (*q == '\\')) {
|
||||
base = q + 1;
|
||||
}
|
||||
q++;
|
||||
}
|
||||
q = base;
|
||||
while ((*q != '\0') && (len < (uint32_t)WAVE_ST_PATH_MAX)) {
|
||||
q++;
|
||||
len++;
|
||||
}
|
||||
if ((*q != '\0') || (len != ((uint32_t)sizeof(s_pfx) - 1u) + 2u +
|
||||
((uint32_t)sizeof(s_ext) - 1u))) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
|
||||
if (wave_st_ci_eq(base[i], s_pfx[i]) == 0u) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
}
|
||||
hi = (uint8_t)base[i];
|
||||
lo = (uint8_t)base[i + 1u];
|
||||
if ((hi < '0') || (hi > '9') || (lo < '0') || (lo > '9')) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
for (i = (i + 2u); i < len; i++) {
|
||||
if (wave_st_ci_eq(base[i], s_ext[i - ((uint32_t)sizeof(s_pfx) - 1u) - 2u]) == 0u) {
|
||||
return WAVE_ST_E_NAME;
|
||||
}
|
||||
}
|
||||
*p_idx = (uint8_t)(((hi - '0') * 10u) + (lo - '0'));
|
||||
return WAVE_ST_OK;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ===== 一、段文件布局常量(逐字段照 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3) ===== */
|
||||
|
||||
/* 魔数前两字节固定 'W' 与 'R',第三字节 = 版本号(设计说明 §6.2 偏移 0 行) */
|
||||
#define WAVE_ST_MAGIC0 (0x57u) /* 'W' */
|
||||
#define WAVE_ST_MAGIC1 (0x52u) /* 'R' */
|
||||
/* 版本:本波恒 1。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行) */
|
||||
#define WAVE_ST_VER (1u)
|
||||
|
||||
/* 固定头字段偏移(字节,小端) */
|
||||
#define WAVE_ST_OFF_MAGIC (0u) /* 3 字节:'W' 'R' ver */
|
||||
#define WAVE_ST_OFF_VER (3u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_FS_HZ (4u) /* 2 字节 本波 1000 */
|
||||
#define WAVE_ST_OFF_PTS_CYC (6u) /* 1 字节 本波 20 */
|
||||
#define WAVE_ST_OFF_CH_CNT (7u) /* 1 字节 本波 8 */
|
||||
#define WAVE_ST_OFF_PTS_CH (8u) /* 2 字节 本波 320 */
|
||||
#define WAVE_ST_OFF_PRE_PTS (10u) /* 2 字节 本波 160 */
|
||||
#define WAVE_ST_OFF_TRIG_SRC (12u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_TRIG_ELM (13u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_T0_MS (14u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_WALL (18u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_ENG (22u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_FLAGS (23u) /* 1 字节 */
|
||||
#define WAVE_ST_OFF_CLIP (24u) /* 2 字节 */
|
||||
#define WAVE_ST_OFF_SEQ (26u) /* 4 字节 u32 */
|
||||
#define WAVE_ST_OFF_TBL (30u) /* 通道表起点 */
|
||||
|
||||
/* 通道表一路 12 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4
|
||||
* 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) */
|
||||
#define WAVE_ST_CH_OFF_SEM (0u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_CONV (1u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_BITS (2u) /* 1 字节 */
|
||||
#define WAVE_ST_CH_OFF_AVAIL (3u) /* 1 字节 = adc_cal_flags(sem) 低 8 位原值,不自造位义 */
|
||||
#define WAVE_ST_CH_OFF_KQ16 (4u) /* 4 字节 int32 */
|
||||
#define WAVE_ST_CH_OFF_BCODE (8u) /* 4 字节 int32 */
|
||||
#define WAVE_ST_CH_BYTES (12u)
|
||||
|
||||
/* 本波通道数固定 8(TEMP 不录:理由见 §6.3 末段与 docs/任务规划/数据可信.md §7) */
|
||||
#define WAVE_ST_CH_NUM (8u)
|
||||
/* 通道数上限:给二期「带 TEMP 且同时升版本」留位;真正的口径是 meta 里的 ch_count 字段 */
|
||||
#define WAVE_ST_CH_MAX (9u)
|
||||
/* 每通道点数默认(= 预触发 + 后触发 = 160 + 160;长度是文件里的数据,宏只作默认值) */
|
||||
#define WAVE_ST_PTS_DEF (320u)
|
||||
#define WAVE_ST_PRE_DEF (160u)
|
||||
#define WAVE_ST_FS_DEF (1000u)
|
||||
#define WAVE_ST_PPC_DEF (20u)
|
||||
|
||||
/* 尺寸一律由字段算出,不写死字面量(改通道数/点数时自动跟着走):
|
||||
* 头 CRC 覆盖区 = 30 + 12 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 12 × ch 起
|
||||
* ch_count = 8 时:覆盖区 126、整头 128、数据区 5120、整段 5250(§6.3「CRC 覆盖范围」行) */
|
||||
#define WAVE_ST_HEAD_CRC_POS(ch) ((uint32_t)(WAVE_ST_OFF_TBL) + \
|
||||
((uint32_t)(ch) * WAVE_ST_CH_BYTES))
|
||||
#define WAVE_ST_HEAD_BYTES(ch) ((uint32_t)(WAVE_ST_HEAD_CRC_POS(ch)) + 2u)
|
||||
#define WAVE_ST_DATA_BYTES(ch, pts) ((uint32_t)(pts) * (uint32_t)(ch) * 2u)
|
||||
#define WAVE_ST_SEG_TOTAL(ch, pts) (WAVE_ST_HEAD_BYTES(ch) + \
|
||||
WAVE_ST_DATA_BYTES(ch, pts) + 2u)
|
||||
/* 本波实际值(只给注释与单测引用,代码里一律用上面的参数化形态) */
|
||||
#define WAVE_ST_HEAD_SZ_CUR (128u)
|
||||
#define WAVE_ST_SEG_SZ_CUR (5250u)
|
||||
|
||||
/* 段目录与轮转(设计说明 §5.2 第三条:只能严格 8.3 短名;目录与 /log 平级、FTP 根下可见)
|
||||
* 名字形态 = 目录 + 前缀 + 两位十进制 + 扩展名,号 0..99 共 100 个轮转覆盖 */
|
||||
#define WAVE_ST_DIR "/wave"
|
||||
#define WAVE_ST_PFX "WAVE"
|
||||
#define WAVE_ST_EXT ".BIN"
|
||||
#define WAVE_ST_SEG_MAX (100u)
|
||||
#define WAVE_ST_PATH_MAX (17u) /* /wave/WAVE00.BIN = 16 字符 + 结束符 = 17 字节 */
|
||||
|
||||
/* CRC-16/MODBUS:多项式 0x8005 反射形态 = 0xA001、初值 0xFFFF、异或输出 0、低字节在前 */
|
||||
#define WAVE_ST_CRC_POLY (0xA001u)
|
||||
#define WAVE_ST_CRC_INIT (0xFFFFu)
|
||||
#define WAVE_ST_CRC_XOROUT (0x0000u)
|
||||
/* 自检锚点:串 123456789 的 CRC(§6.3 首行钉的常量) */
|
||||
#define WAVE_ST_CRC_CHECK (0x4B37u)
|
||||
|
||||
/* ===== 二、返回码(int 通道;调用方必须接住或显式 (void),不得裸丢) ===== */
|
||||
|
||||
#define WAVE_ST_OK (0) /* 成功 */
|
||||
#define WAVE_ST_DONE (1) /* 打包游标:整段已产完(不是错误) */
|
||||
#define WAVE_ST_E_PARAM (-1) /* 空指针 / 容量不足 / 字段非法 */
|
||||
#define WAVE_ST_E_CRC (-2) /* CRC 不符(头或数据区):整段判废 */
|
||||
#define WAVE_ST_E_FORMAT (-3) /* 魔数不符 / 版本不认识 / 表内语义号不等于下标 */
|
||||
#define WAVE_ST_E_SIZE (-4) /* 长度为 0 或长度与头部字段矛盾:整段判废 */
|
||||
#define WAVE_ST_E_RANGE (-5) /* 列号 / 点下标 / 段号越界 */
|
||||
#define WAVE_ST_E_STATE (-6) /* 状态错:未封存就读、未 start 就步进、缺数据源 */
|
||||
#define WAVE_ST_E_NAME (-7) /* 文件名不是本层命名形态(扫目录重建段账时用) */
|
||||
|
||||
/* ===== 三、类型 ===== */
|
||||
|
||||
/* 触发源(§6.2 偏移 12 的取值;0 表示「无触发」,只出现在段外的状态查询里) */
|
||||
typedef enum {
|
||||
WAVE_TRIG_NONE = 0, /* 无 */
|
||||
WAVE_TRIG_MANUAL = 1, /* 手动(UI「手动录波」确认,task-2 接线) */
|
||||
WAVE_TRIG_PROTECT = 2, /* 保护启动(App/algo/prot_elm.c 的 picked 上升沿) */
|
||||
WAVE_TRIG_BENCH = 3 /* bench 定周期自抓(只在 DTU_BENCH 构建里有这条来路) */
|
||||
} wave_trig_src_t;
|
||||
|
||||
/* 折算档位(§6.2 偏移 22;单位一律以头里的 eng 为准,PC 不许按当前固件口径猜) */
|
||||
typedef enum {
|
||||
WAVE_ENG_PIN = 0, /* 引脚域(未标定;V0.1 产品构建停在这档) */
|
||||
WAVE_ENG_SEC = 1, /* 二次值 */
|
||||
WAVE_ENG_PRI = 2 /* 一次值 */
|
||||
} wave_eng_t;
|
||||
|
||||
/* 通道表一路的内存形态(字段序 = §6.2 通道表字节序) */
|
||||
typedef struct {
|
||||
uint8_t sem; /* 语义号(adc_semantic_t 值;必须等于表内下标,§6.3) */
|
||||
uint8_t conv; /* 来源转换器(adc_source_t 值:0 = TPAFE,1 = STM32) */
|
||||
uint8_t bits; /* 转换器输出位宽(TPAFE 16 / STM32 12) */
|
||||
uint8_t avail; /* 可用性 = adc_cal_flags(sem) 低 8 位原值(不自造位义) */
|
||||
int32_t k_q16; /* 封存瞬间生效系数:µV/码 × 65536,就是 LSB */
|
||||
int32_t b_codes; /* 封存瞬间生效零点偏移:码值 */
|
||||
} wave_ch_desc_t;
|
||||
|
||||
/* 段头元数据 = 固定头 + 通道表的内存形态。
|
||||
* 这是「待编码的段头」,不是「已写盘的证据」:打包游标只读它、不改它。 */
|
||||
typedef struct {
|
||||
uint16_t fs_hz; /* 采样率 Hz(从文件读出来的数据,不是编译期常量) */
|
||||
uint8_t pts_per_cycle; /* 每周波点数 */
|
||||
uint8_t ch_count; /* 通道数(本波 8) */
|
||||
uint16_t pts_per_ch; /* 每通道点数(本波 320) */
|
||||
uint16_t pre_pts; /* 预触发点数(本波 160;回放画触发光标用) */
|
||||
uint8_t trig_src; /* 触发源(wave_trig_src_t) */
|
||||
uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与定时为 0) */
|
||||
uint32_t t0_ms; /* 段首相对上电毫秒 */
|
||||
uint32_t wall; /* 对时后的 epoch 秒(未对时 0) */
|
||||
uint8_t eng; /* 折算档位(wave_eng_t) */
|
||||
uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */
|
||||
uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */
|
||||
uint32_t frame_seq; /* 段首帧序号(与串口周期行的 frames 对账) */
|
||||
wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表(前 ch_count 项有效) */
|
||||
} wave_seg_meta_t;
|
||||
|
||||
/* 打包游标阶段(对外只用于「已搬完」判定与观测日志;推进全在 wave_store_pack_next 内部) */
|
||||
typedef enum {
|
||||
WAVE_PACK_IDLE = 0, /* 未初始化 */
|
||||
WAVE_PACK_HEAD = 1, /* 正在产头(固定头 + 通道表 + 头 CRC) */
|
||||
WAVE_PACK_DATA = 2, /* 正在产数据区(按通道分列,一列一列产) */
|
||||
WAVE_PACK_TAIL = 3, /* 只剩数据区 CRC 两字节 */
|
||||
WAVE_PACK_DONE = 4 /* 整段已产完 */
|
||||
} wave_pack_phase_t;
|
||||
|
||||
/* 数据源回调:把「第 a_ch 列、段内第 a_from 点起的 a_cnt 个点」取进 p_dst。
|
||||
* 返 回 值:实际拷贝的点数(等于 a_cnt 表示全给到);负 = 错误码 int32_t
|
||||
* 只允许读已封存区:本层不提供、也不允许任何跨任务读活缓冲的路径
|
||||
* (设计说明 §5.2 第一条陷阱:App/algo/sample_ring.c 的 sample_ring_window() 无临界区,
|
||||
* 本波不复用它,跨任务只传「已封存」标志)。
|
||||
* 生产实现 = wave_ring_read_cb()(签名一致,可直接传给 wave_store_pack_start)。 */
|
||||
typedef int32_t (*wave_reader_fn)(void *p_user, uint8_t a_ch, uint32_t a_from,
|
||||
uint32_t a_cnt, int16_t *p_dst);
|
||||
|
||||
/* 打包游标:段文件字节流的「拉模式」状态机。
|
||||
* 为什么拉模式 + 小缓冲:defaultTask 是文件系统唯一写者,落盘窗口几十毫秒量级,
|
||||
* 一次心跳最多一段;整段驻留 RAM 既超预算也没必要(设计说明 §7)。
|
||||
* 游标不校验写回字节数 —— 那是调用方对着 f_write 做的事(§5.2 第二条陷阱:本移植
|
||||
* f_write 簇分配失败会返 FR_OK + 短写、没有错误码,必须靠比对写回字节数判失败)。 */
|
||||
typedef struct {
|
||||
uint8_t phase; /* wave_pack_phase_t */
|
||||
uint8_t ch; /* 数据区当前列 */
|
||||
uint16_t head_done; /* 头区已产出字节数(含头 CRC 两字节) */
|
||||
uint16_t ch_done; /* 当前列已产出点数 */
|
||||
uint8_t tail_done; /* 尾部数据 CRC 已产出字节数(0..2) */
|
||||
uint8_t rsv; /* 补齐用,保持结构体偶数字节 */
|
||||
uint16_t crc_data; /* 数据区运行 CRC(增量中间值,避免整段驻留) */
|
||||
uint32_t total; /* 整段期望字节数 */
|
||||
uint32_t sent; /* 已产出字节数(= 下一片的文件偏移) */
|
||||
wave_seg_meta_t meta; /* 段头快照(start 时拷入;搬运期间调用方改自己那份不影响) */
|
||||
wave_reader_fn p_reader; /* 数据源;NULL = 只产头 */
|
||||
void *p_user; /* 数据源上下文 */
|
||||
} wave_pack_ctx_t;
|
||||
|
||||
/* ===== 四、CRC-16/MODBUS(无表位运算) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 增量计算(输入输出均低位在前)
|
||||
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
|
||||
* p_data 数据指针(NULL 且 a_len = 0 视为空操作,原样返回 a_crc)
|
||||
* a_len 字节数
|
||||
* 返 回 值:新的中间值(写文件时低字节在前,见 §6.3 首行) uint16_t
|
||||
* 限定条件:任意上下文(含 adcTask 之外的 defaultTask / uiTask);纯计算、无静态状态、
|
||||
* 不碰外设、不取临界区
|
||||
* 函数说明:为什么自带实现 —— 全仓没有可复用的 CRC-16 表(依据见本文件头注)。
|
||||
* 无表 = 零新增表 RAM 与表 Flash;数据区 5120 字节每字节 8 次移位,在 168 MHz
|
||||
* 上量级是几十微秒,且只在 defaultTask 的落盘链上出现,不在 1 kHz 采集链上。
|
||||
*/
|
||||
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len);
|
||||
|
||||
/*
|
||||
* 函数功能:CRC-16/MODBUS 一次算完(从初值起步)
|
||||
* 入口参数:p_data 数据指针
|
||||
* a_len 字节数
|
||||
* 返 回 值:CRC uint16_t
|
||||
* 限定条件:任意上下文。p_data 为 NULL 或 a_len = 0 时返回 WAVE_ST_CRC_INIT —— 这是
|
||||
* 空输入的定义值,不是「算好的段 CRC」:零长度一律判废,调用方必须先判长度
|
||||
* (设计说明 §6.2 末行:长度为 0 → 整段作废)
|
||||
*/
|
||||
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len);
|
||||
|
||||
/*
|
||||
* 函数功能:CRC 自检 —— 用规范锚点验算实现是否仍是 CRC-16/MODBUS
|
||||
* 入口参数:无
|
||||
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符(实现或编译配置被改坏) int
|
||||
* 限定条件:任意上下文;开机或首次落盘前调一次即可,不在每拍路径上
|
||||
* 函数说明:锚点 = 串 123456789 的 CRC = 0x4B37(§6.3)。这条是机器可执行的口径锚,
|
||||
* 防的是「自算自验永远相符」:只有对已知常数才咬得住(换机、换编译器、
|
||||
* 反射方向写反都在这里暴露)。落盘前失败就自弃本段并计入 err(§9 观测行)。
|
||||
*/
|
||||
int wave_store_crc_selftest(void);
|
||||
|
||||
/* ===== 五、段头编解码 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据填成本波默认值(只填格式侧的静态事实)
|
||||
* 入口参数:p_meta 输出(调用方持有的结构;本层不返回静态地址)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文;不碰文件系统
|
||||
* 函数说明:填 fs_hz = 1000、pts_per_cycle = 20、ch_count = 8、pts_per_ch = 320、
|
||||
* pre_pts = 160,以及通道表的 sem = 下标 + conv / bits 按两片转换器
|
||||
* (UA / UN / IA 走 TPAFE 16 位;UB / UC / IB / IC / IN 走片内 ADC 12 位 ——
|
||||
* 通道归属口径属主是 docs/硬件平台/ADC通道映射说明.md,本函数只是它的抄本,
|
||||
* 单测里对 0xE6 的算术交叉验证会同时钉住这套顺序)。
|
||||
* 不填 avail / k_q16 / b_codes / trig_* / t0_ms / wall / eng / flags_seq /
|
||||
* clip_mask / frame_seq:那些是封存瞬间的事实,必须由调用方现取现填
|
||||
* (adc_cal_k_q16 / adc_cal_flags / sys_clock / wave_ring),本层不链它们。
|
||||
*/
|
||||
int wave_store_meta_init(wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:把段头元数据编码成段文件头字节流(固定头 + 通道表 + 头 CRC,全小端)
|
||||
* 入口参数:p_meta 段头元数据(只读)
|
||||
* p_out 输出缓冲
|
||||
* a_cap 缓冲容量(须不小于按 ch_count 算出的整头字节数)
|
||||
* 返 回 值:不小于 0 = 实际写入字节数;负 = WAVE_ST_E_* int
|
||||
* 限定条件:任意上下文;无静态缓冲、可重入;不碰文件系统
|
||||
* 函数说明:编码前先自检字段一致性 —— ch_count 为 0 或越界、pts_per_ch = 0、
|
||||
* pre_pts 大于 pts_per_ch、表内语义号不等于下标(§6.3「通道表序与列序」那条
|
||||
* PC 侧判据在编码侧同样成立)一律拒绝:不把坏头写成看起来像样的文件。
|
||||
*/
|
||||
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap);
|
||||
|
||||
/*
|
||||
* 函数功能:把段文件头字节流解回元数据,并做全套判废检查(魔数 / 版本 / 长度 / 表序 / 头 CRC)
|
||||
* 入口参数:p_buf 头字节流
|
||||
* a_len 可读字节数
|
||||
* p_meta 输出元数据(判废时内容不保证有效,调用方不得消费)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针或缓冲不足;WAVE_ST_E_FORMAT 魔数 / 版本 /
|
||||
* 表序不符;WAVE_ST_E_SIZE 零长度或长度与字段矛盾;WAVE_ST_E_CRC 头 CRC 不符 int
|
||||
* 限定条件:任意上下文(uiTask 与 defaultTask 都可调)。UI 与 PC 侧必须用同一套判废语义:
|
||||
* 解不过就只能显示「该段不可读」,绝不允许退化画一条直线
|
||||
* (设计说明 §8.1 空态行:画直线等于伪造证据)
|
||||
* 函数说明:a_len 不足「按头里 ch_count 推出来的整头长度」时按缓冲不足返回,不猜没读到
|
||||
* 的字节;ver 不等于魔数第三字节、或不等于 WAVE_ST_VER 一律判废。
|
||||
*/
|
||||
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta);
|
||||
|
||||
/* ===== 六、整段校验与长度 / 偏移换算(读侧) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:整段镜像自检 —— 头 + 数据区 + 数据 CRC 三道全过才算「这一段可读」
|
||||
* 入口参数:p_img 整段文件镜像(调用方已读进内存)
|
||||
* a_len 镜像长度
|
||||
* p_meta 输出段头(可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_SIZE 长度为 0 或与期望总长不符;
|
||||
* WAVE_ST_E_CRC 任一 CRC 不符;WAVE_ST_E_FORMAT 头本身判废 int
|
||||
* 限定条件:任意上下文;只在整段驻留(本波 5250 字节)时用。回放页不要走这条 ——
|
||||
* 用 wave_store_head_parse + wave_store_data_off + wave_store_crc_update 分段查,
|
||||
* 才符合 §7 的 256 字节读窗预算
|
||||
* 函数说明:这是「段文件 CRC 不符时脚本必须拒绝而不是画出东西」那条判据在板侧的同一份
|
||||
* 实现,也是本层与 PC 解码器逐字节同解的对照物(两套主机单测互验)。
|
||||
*/
|
||||
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:按头部字段算整段文件应有的字节数(写侧记账与读侧长度判废共用)
|
||||
* 入口参数:p_meta 段头元数据
|
||||
* 返 回 值:期望字节数(8 路 320 点 = 5250);字段非法(含零长度)返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:返回 0 就是「这一段连长度都不成立」,据此判废。不要拿 0 去开缓冲,
|
||||
* 也不要拿 0 去比对 f_write 的写回字节数(那会把失败读成成功)。
|
||||
*/
|
||||
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta);
|
||||
|
||||
/*
|
||||
* 函数功能:整头字节数(固定头 + 通道表 + 头 CRC 两字节)
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* 返 回 值:字节数;通道数为 0 或越界返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_head_bytes(uint8_t a_ch_count);
|
||||
|
||||
/*
|
||||
* 函数功能:数据区某一点在段文件里的绝对偏移(按通道分列 → 单通道是一整段连续区)
|
||||
* 入口参数:a_ch_count 通道数
|
||||
* a_pts_ch 每通道点数
|
||||
* a_ch 列号(= 通道表下标 = 语义号,0 到 a_ch_count-1)
|
||||
* a_pt 段内点下标(0 到 a_pts_ch-1)
|
||||
* 返 回 值:文件偏移;参数非法返回 0xFFFFFFFFu 哨兵(这个值不是任何合法偏移) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:回放页据此定位「只读那一路」,配合 wave_store_data_bytes_avail 一次连续读;
|
||||
* PC 侧同一算法,两份实现互验。
|
||||
*/
|
||||
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
|
||||
uint8_t a_ch, uint32_t a_pt);
|
||||
|
||||
/*
|
||||
* 函数功能:从某点起当前列还能连续读多少字节(不跨列、不越段尾)
|
||||
* 入口参数:a_pts_ch 每通道点数
|
||||
* a_pt 起始点下标
|
||||
* a_want 想读的点数
|
||||
* 返 回 值:可连续读字节数 = 2 × 实际可给点数;参数非法返回 0 uint32_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want);
|
||||
|
||||
/*
|
||||
* 函数功能:把段文件读出的小端字节解码成 int16 点序列(按时间序,旧到新)
|
||||
* 入口参数:p_raw 原始字节(来自文件,2 × 点数)
|
||||
* a_len 字节数(须为偶数,且不大于 a_max_pts × 2)
|
||||
* p_out 输出 int16 数组
|
||||
* a_max_pts 输出容量(点数)
|
||||
* p_pts 输出实际点数(可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:显式按小端逐字节拼装,不依赖宿主字节序,所以板端与 PC 侧同一判据。
|
||||
* 片内 ADC 那几路的值本就是 0..4095 的原样正数,不做中点偏置
|
||||
* (§6.3「样本编码」行:加偏置等于伪造双极性)。
|
||||
*/
|
||||
int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len,
|
||||
int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts);
|
||||
|
||||
/*
|
||||
* 函数功能:把一路的 int16 码值折成定点毫单位(回放页峰值标注用;纯整数,不用 float)
|
||||
* 入口参数:p_desc 该路通道表项(k_q16 / b_codes 来自段文件头,不来自当前固件)
|
||||
* a_code 原始码值
|
||||
* p_milli 输出四舍五入后的毫单位(k_q16 是 µV/码 → 输出单位 mV)(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
|
||||
* 限定条件:任意上下文(只在回放页与单测里用,不在 1 kHz 采集链上;内部用 int64 中间量,
|
||||
* 落在这条路径上时可接受)
|
||||
* 函数说明:算式 eng = (code + b) × k × 1e-6,与 App/algo/adc_cal.c 的 adc_cal_apply()
|
||||
* 同一模型(§6.3)。输出单位是 mV;单位含义由段头 eng 字段决定(eng = 0 是
|
||||
* 引脚域,别读成二次值)。必须用段文件里的 k / b,不许拿当前固件的系数解释
|
||||
* 老文件(§12 风险表「标定落地后老文件被解释错」)。
|
||||
*/
|
||||
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
|
||||
int32_t *p_milli);
|
||||
|
||||
/* ===== 七、打包游标(分次搬运,给文件系统唯一写者) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化打包游标(段头拷进游标,随后逐次拉整段字节流)
|
||||
* 入口参数:p_ctx 游标(调用方持有,栈上或静态都行;本层不记它的地址)
|
||||
* p_meta 段头元数据(内部拷贝;搬运期间调用方改自己那份不影响本段)
|
||||
* p_reader 数据源回调;NULL = 只产头(回放页只校验头 / 主机单测只测头)
|
||||
* p_user 数据源上下文(生产传 NULL,wave_ring 是单例不需要)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_FORMAT 段头字段非法;
|
||||
* WAVE_ST_E_SIZE 算不出合法长度 int
|
||||
* 限定条件:任意上下文(纯计算,无临界区);一段一个游标,搬完再 start 下一段
|
||||
* 函数说明:p_reader 为 NULL 而 ch_count 大于 0 时,游标仍能产头,但进数据区那一步
|
||||
* 返回 WAVE_ST_E_STATE —— 不静默跳过数据区,否则调用方会写出一个
|
||||
* 「长度看着对、内容是空的」段文件(那正是 §8.1 要防的伪造证据形态)。
|
||||
*/
|
||||
int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta,
|
||||
wave_reader_fn p_reader, void *p_user);
|
||||
|
||||
/*
|
||||
* 函数功能:取整段字节流的下一片(分次搬运的唯一推进口)
|
||||
* 入口参数:p_ctx 已 start 的游标
|
||||
* p_out 输出缓冲
|
||||
* a_cap 本片容量(建议 256 字节,见 §7;数据区不切半点,奇数容量会少产 1 字节)
|
||||
* p_len 输出本片实际字节数(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK 产出本片且还有后续;WAVE_ST_DONE 整段已产完(*p_len 可为 0);
|
||||
* 负 = WAVE_ST_E_*(未 start / 容量为 0 / 数据源回调返错 / 游标状态错) int
|
||||
* 限定条件:任意上下文,但同一游标不得并发使用(文件系统唯一写者单线程推进)
|
||||
* 函数说明:头区字节流与 wave_store_head_build 完全一致(内部同一份写字段函数);
|
||||
* **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 128),否则返回
|
||||
* WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 128 字节
|
||||
* 中间态;§7 的 256 字节搬运缓冲天然满足。
|
||||
* 数据区 CRC 走增量中间值,整段最后两片是数据 CRC 的低字节、高字节。
|
||||
* 调用方每写完一片都必须比对 f_write 写回字节数,短写按失败处理(§5.2)。
|
||||
*/
|
||||
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
|
||||
uint32_t a_cap, uint32_t *p_len);
|
||||
|
||||
/*
|
||||
* 函数功能:读游标已产出字节数(= 当前文件偏移;供记账、观测与「写没写全」判定)
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:已产出字节数;p_ctx 为 NULL 返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:读游标对应整段文件的期望总长(写完与 f_size / 累计写回字节数比对)
|
||||
* 入口参数:p_ctx 游标
|
||||
* 返 回 值:期望总字节数;p_ctx 为 NULL 返回 0 uint32_t
|
||||
* 限定条件:任意上下文(只读)
|
||||
*/
|
||||
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx);
|
||||
|
||||
/* ===== 八、段文件名轮转(只算名字,不碰文件系统) ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:按段号生成段文件路径(严格 8.3 短名:目录 + 前缀 + 两位十进制 + 扩展名)
|
||||
* 入口参数:p_out 输出缓冲(容量须不小于 WAVE_ST_PATH_MAX)
|
||||
* a_cap 容量
|
||||
* a_idx 段号 0 到 WAVE_ST_SEG_MAX-1
|
||||
* 返 回 值:不小于 0 = 写入的字符数(不含结束符;当前布局恒为 16,故缓冲至少要 17 字节);
|
||||
* WAVE_ST_E_PARAM 空指针或容量不足;
|
||||
* WAVE_ST_E_RANGE 段号越界 int
|
||||
* 限定条件:任意上下文;纯字符拼装(不用 sprintf,见 CODE-RULE-06 系),逐字节可预期
|
||||
* 函数说明:号 0..99 轮转覆盖,共 100 个名字,写满回绕盖最老的(§5.2 第三条)。
|
||||
* 本函数不检查文件是否存在、不建目录:那些都是接线道的职责。
|
||||
*/
|
||||
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx);
|
||||
|
||||
/*
|
||||
* 函数功能:下一个轮转段号(到 99 后回 0)
|
||||
* 入口参数:a_idx 当前段号(越界输入按 0 处理,不猜)
|
||||
* 返 回 值:下一个段号 uint8_t
|
||||
* 限定条件:任意上下文
|
||||
*/
|
||||
uint8_t wave_store_next_idx(uint8_t a_idx);
|
||||
|
||||
/*
|
||||
* 函数功能:从文件名或路径反解段号(掉电后靠扫目录重建段账用)
|
||||
* 入口参数:p_path 路径或纯文件名(只看最后一段,大小写都收)
|
||||
* p_idx 输出段号(不可为 NULL)
|
||||
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:只认「前缀 + 两位数字 + 扩展名」,其余(目录名、长名、以点开头)一律判不匹配
|
||||
* —— 扫目录重建账时不许把陌生名字当录波段;名字/参数类错误码严禁升级成卷操作
|
||||
* (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 的同一条教训)。
|
||||
*/
|
||||
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WAVE_STORE_H */
|
||||
Reference in New Issue
Block a user