Files
STM32F4-Base/App/algo/wave_ring.h
T
wandering1 d64bbdc57f 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 条仍全过
2026-10-29 19:23:15 +08:00

407 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:录波环形缓冲层(故障录波 · 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 */