ae09c5737a
任务规划目录从 5 个文件收敛到 3 个:只留导航(README)、待办(登记簿)、 暂缓专项(暂缓-RS485)。 删除的两个文件都不再有现行读者: - UI补齐.md 执行清单 2026-10-28 清空、上板判据 2026-11-06 台架轮已复核; 叶数口径迁 docs/UI设计/菜单页面设计.md §3.9,三条内容页硬规矩迁 docs/UI设计/内容页架构.md §7.1,两条未闭环待办迁登记簿 §7/§8。 - 数据可信.md 不是完成记录,而是被约 130 处代码头注释引用的设计约束, 因此按内容分三处落位后删除,不整篇丢弃。 数据可信内容的新归属(旧节号 → 新家): - §2/§4 设计口径 → docs/算法/采集与算法层设计说明.md 新增 §13(13 小节), §7 前置决策表 → §13.12 已定案的前置决策。 - §3 台阶与波次退出判据 → docs/硬件平台/ADC通道映射说明.md 新增 §9。 - §5 任务卡 → docs/任务规划/登记簿.md §7(拆成变比确认 / 两点标定两行)。 引用改写 34 文件 77 处路径引用 + 14 文件 24 处昵称引用 + 双重节号 35 处, 覆盖代码头注释、测试、生成器与文档;归档区历史路径改纯文本提及。 四个 SVG 生成器文案同步并重跑,产物随脚本同批提交。 同批处理两件事: - 9 个 UTF-16 LE 证据文件转 UTF-8。end-of-file-fixer 按字节判末尾换行, UTF-16 的换行是 0A 00、末字节 00,钩子会追加裸 0x0a 使文件变奇数长度, 8 个已实际损坏。转码后逐文件比对 HEAD 原文验证正文零丢失。 - 修回 App/cfg/cfg_file.c 被剥掉的 UTF-8 BOM(补 3 字节,未整文件重写)。 验证:/keil-build 0 错误 0 警告(Code=255160);check_doc_links --all --sections-strict 221 篇全通过、无法判定 0;pre-commit 全量全 Passed。
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 §13 已定案),进录波只多占 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 */
|