d64bbdc57f
数据面(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 条仍全过
407 lines
30 KiB
C
407 lines
30 KiB
C
/*
|
||
* 模块名称:录波环形缓冲层(故障录波 · 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 */
|