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:
2026-10-29 19:23:15 +08:00
parent 325abe4337
commit d64bbdc57f
81 changed files with 8868 additions and 424 deletions
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""录波环(故障录波数据面·滚动环与触发冻结状态机,wave_ring)主机侧纯逻辑单元测试运行器(纯标准库)。
做什么:
用本机 MSVC 编译 test/host/wave_ring/wave_ring_test.c(它以 #include 纳入
**生产源文件** App/algo/wave_ring.c + App/algo/wave_store.c),在 PC 上离线跑
环形态与预算、触发到封段的逐拍时序、段内容与点序(列序 = 语义序)、SEALED 期间
写指针真冻结、封存交棒握手、读窗夹取与跨环回绕、ACK 与冷却、轮转号推进规则,
失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只等台架):
本层判据几乎全是结构与并发类(docs/子系统设计/故障录波设计说明.md §4.2 与 §5.2)——
1 ms 一拍的冻结窗在人工读表下不可观测,"落盘道挂住两秒"这种注入在真机上等于
拿看门狗窗口赌;主机侧可拿影子样本序列逐点钉死。需要板子的判据(注入 1 V/50 Hz
后 FTP 拉回对表)不在本套件,见 docs/任务规划/故障录波.md。
本层为纯逻辑:无寄存器、无 RTOS、无文件/外设依赖,**不需要任何桩目录**。
用法(仓库根目录):
python test/host/wave_ring/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §7;
docs/任务规划/故障录波.md 卡 1。
创建日期:2026-10-29
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 无桩目录:wave_ring.c / wave_store.c 只依赖 stdint/stddef 与本目录两份头。
BUILD = TestBuild(
name="Wave Ring Tests",
src=os.path.join(HERE, "wave_ring_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Wave Ring Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+538
View File
@@ -0,0 +1,538 @@
/*
* 模块名称:录波环(滚动环 + 触发冻结状态机 + 已封存区读口)主机侧单元测试
* 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):
* App/algo/wave_store.c + App/algo/wave_ring.c
* 两源均为纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。
*
* 为什么要主机侧(而不是只等台架):本层的判据几乎全是**结构与并发**类——
* 段首基址与点序、SEALED 期间写指针必须真冻结、ACK 前不许重录、滞留超时自弃、
* 冷却只在 ACK 后计时、读口在未就绪时绝不返回旧数据。这些在台架上要么不可观测
* (1 ms 一拍的窗口,人工读不到)、要么无法安全注入(真造一次「defaultTask 挂住
* 两秒」就是把看门狗窗口拿去赌)。主机侧可以拿影子样本序列把它逐点钉死。
* 需要板子的部分(注入 1 V/50 Hz 后 FTP 拉回对表、实机 FAT 落盘)不在本套件,
* 见 docs/任务规划/故障录波.md 的「录波数据面最小闭环」退出判据。
*
* 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录):
* 用例:形态与预算钉死:int16×320×8 = 5120、两半各 160、
* sizeof(wave_seg_meta_t) = 136、sizeof(wave_ch_desc_t) = 12
* 用例:init 复位语义:IDLE / 不可读 / 计数与轮转号归零
* 用例:参数守卫:push / request / latch / read / get_stat 的非法入参
* 用例:触发到封段的时序:封段恰在触发后第 160 拍,事件一次性
* 用例:段内容与点序:8 列 × 320 点逐点等于影子样本,
* 触发那一拍的样本在点 159、点 160 是其后第一拍(列序 = 语义序)
* 用例:SEALED 丢样本:push 返 DONE、drop 计点、写指针真冻结、内容不变
* 用例:封存交棒握手:未 latch 一律不可用;latch 只准一次、不覆盖;
* latch 内容进段头(eng / flags_seq / clip_mask / wall + 8 路系数)
* 用例:读窗夹取与跨环回绕;read_cb 的 32 位形态
* 用例:列号映射:0..7 恒等、TEMP 与越界返 WAVE_ST_E_RANGE
* 用例:ACK 语义:成功推进 seg_idx 与 seg_total 并起冷却;重复 ACK 拒绝;
* 未就绪时读口返 E_STATE 而不是旧数据
* 用例:环未填满就触发:前导点必须是 0(不是上一段的残留),真实样本从
* 点 129 起(本次上电只有 31 个点可给)
*
* 通过:全部 CHECK 通过(main 返回 0)。
* 创建日期:2026-10-29(故障录波 task-1)
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码(生产源原样纳入) ===================== */
#include "wave_store.c"
#include "wave_ring.c"
/* ===================== 影子样本模型 ===================== */
#define HIST_N 2048u /* 影子样本环(比录波环大,够跑完整个场景) */
#define TRIG_K 400u /* 第一段:消费请求那一拍的样本号 */
static int16_t s_hist[HIST_N][WAVE_RING_CH];
static uint32_t s_k; /* 下一个要压的样本号 */
static uint32_t s_evt_sealed; /* 累计看到多少次 EVT_SEALED */
static uint32_t s_push_bad; /* push 返回了 OK/DONE 之外的值 */
static int32_t s_push_last; /* 最近一次 push 的返回值 */
/* 每路一条可辨认的斜坡:不同样本、不同列都不重复,负值也覆盖(TPAFE 是双极性补码) */
static int16_t wave_sig(uint32_t a_k, uint8_t a_col) {
return (int16_t)(((int32_t)a_k * 7) - 1200 + ((int32_t)a_col * 91));
}
/* 压 n 拍(push 之后 tick,与 adcTask 的接线顺序一致:先写样本再算点数) */
static void wave_step(uint32_t a_n) {
uint32_t i;
for (i = 0u; i < a_n; i++) {
int16_t codes[WAVE_RING_CH];
uint8_t c;
int32_t rc;
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
codes[c] = wave_sig(s_k, c);
s_hist[s_k % HIST_N][c] = codes[c];
}
rc = wave_ring_push(codes, (uint8_t)WAVE_RING_CH);
s_push_last = rc;
if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) {
s_push_bad++;
}
rc = wave_ring_tick(s_k, s_k);
if (rc == (int32_t)WAVE_RING_EVT_SEALED) {
s_evt_sealed++;
}
s_k++;
}
}
/* 影子样本 -> 段内点:点 i 对应样本 (a_trig - 159 + i),负数表示当时还没采到 */
static int32_t wave_pt_sample(uint32_t a_trig, uint32_t a_pt) {
return ((int32_t)a_trig - (int32_t)(WAVE_RING_PRE - 1u)) + (int32_t)a_pt;
}
/* 整段逐点比对(a_trig = 消费请求那一拍的样本号) */
static int wave_cmp_all(uint32_t a_trig) {
int16_t buf[WAVE_RING_PTS];
uint8_t c;
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
int32_t n;
uint32_t i;
n = wave_ring_read_ch(c, 0u, (uint16_t)WAVE_RING_PTS, buf);
if (n != (int32_t)WAVE_RING_PTS) {
return 0;
}
for (i = 0u; i < (uint32_t)WAVE_RING_PTS; i++) {
int32_t ks = wave_pt_sample(a_trig, i);
int16_t want = (ks < 0) ? (int16_t)0 : wave_sig((uint32_t)ks, c);
if (buf[i] != want) {
return 0;
}
}
}
return 1;
}
/* 封好段并交棒(返回消费请求那一拍的样本号) */
static uint32_t s_trig_first;
static void wave_seal_once(uint8_t a_src, uint8_t a_elm) {
wave_ring_request(a_src, a_elm);
wave_step(1u); /* 本拍样本 = 点 159,同拍 tick 消费请求钉基址 */
s_trig_first = s_k - 1u;
wave_step((uint32_t)WAVE_RING_POST); /* 后触发 160 点,最后一拍封段 */
}
/* 造一份封存瞬间事实 */
static void wave_fill_cal(wave_seal_cal_t *p_cal, uint8_t a_seq_tag) {
uint8_t i;
p_cal->eng = (uint8_t)WAVE_ENG_PIN;
p_cal->flags_seq = (uint8_t)(0x50u ^ a_seq_tag);
p_cal->clip_mask = (uint16_t)0x00E6u;
p_cal->wall = 1764374400u + (uint32_t)a_seq_tag;
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
p_cal->ch[i].sem = i;
p_cal->ch[i].conv = (uint8_t)((i == 0u) || (i == 3u) || (i == 4u)) ? 0u : 1u;
p_cal->ch[i].bits = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 16u : 12u);
p_cal->ch[i].avail = (uint8_t)(0x01u | (((i == 1u) || (i == 5u)) ? 0x02u : 0x00u));
p_cal->ch[i].k_q16 = (int32_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 10000000L : 52800000L);
p_cal->ch[i].b_codes = (int32_t)(a_seq_tag + i);
}
}
static wave_seal_cal_t s_cal;
/* ===================== 主测 ===================== */
int main(void) {
printf("=== Wave Ring Tests:\n");
/* ---------- 用例:形态与预算 ---------- */
printf("-- 用例:形态与预算 --\n");
CHECK(sizeof(int16_t) == 2u, "int16 是两字节(不许 float)");
CHECK(WAVE_RING_BYTES == 5120u, "环 = 5120 字节");
CHECK(((uint32_t)WAVE_RING_PTS * (uint32_t)WAVE_RING_CH * 2u) == WAVE_RING_BYTES, "环长等式");
CHECK(WAVE_RING_PTS == (WAVE_RING_PRE + WAVE_RING_POST), "段长 = 预触发 + 后触发");
CHECK(WAVE_RING_PRE == 160u, "预触发 160 点 = 8 周波 @20 点/周波");
CHECK(sizeof(wave_ch_desc_t) == (size_t)WAVE_ST_CH_BYTES, "通道表一项正好 12 字节");
CHECK(sizeof(wave_seg_meta_t) == 136u, "段头快照 136 字节(本卡静态增量台账用)");
/* ---------- 用例:init 复位 ---------- */
printf("-- 用例:init 复位语义 --\n");
wave_ring_init();
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "init 后 IDLE");
CHECK(wave_ring_sealed() == 0u, "init 后无可读段");
{
wave_ring_stat_t st;
int16_t buf[8];
CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "init 后读已封存区被拒");
CHECK(wave_ring_get_stat(&st) == WAVE_ST_OK, "get_stat OK");
CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.ovf_cnt == 0u) &&
(st.drop_cnt == 0u) && (st.err_cnt == 0u), "init 后计数与轮转号归零");
}
/* ---------- 用例:参数守卫 ---------- */
printf("-- 用例:参数守卫 --\n");
CHECK(wave_ring_push(NULL, (uint8_t)WAVE_RING_CH) == WAVE_ST_E_PARAM, "push 空指针");
{
int16_t codes[WAVE_RING_CH];
CHECK(wave_ring_push(codes, 7u) == WAVE_ST_E_PARAM, "push 长度不等于 8 拒绝");
CHECK(wave_ring_push(codes, 9u) == WAVE_ST_E_PARAM, "push 长度大于 8 拒绝");
}
CHECK(wave_ring_request(0u, 0u) == WAVE_ST_E_PARAM, "request 触发源 0 拒绝");
CHECK(wave_ring_request(4u, 0u) == WAVE_ST_E_PARAM, "request 触发源 4 拒绝");
CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针");
CHECK(wave_ring_sealed_meta(NULL) == WAVE_ST_E_PARAM, "sealed_meta 空指针");
CHECK(wave_ring_get_stat(NULL) == WAVE_ST_E_PARAM, "get_stat 空指针");
CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没段可 ACK 时拒绝");
/* ---------- 用例:触发到封段时序 ---------- */
printf("-- 用例:触发到封段时序 --\n");
wave_ring_init();
s_k = 0u;
s_evt_sealed = 0u;
s_push_bad = 0u;
wave_step(TRIG_K);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "400 拍后仍 IDLE(没请求就不该动)");
wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 7u);
wave_step(1u);
s_trig_first = s_k - 1u;
CHECK(s_trig_first == TRIG_K, "请求在下一拍被消费(本层不自己冻结,交给 adcTask 的 tick)");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE, "消费请求即 FREEZE");
CHECK(s_evt_sealed == 0u, "还没封段");
CHECK(wave_ring_sealed() == 0u, "FREEZE 期间不可读");
wave_step((uint32_t)WAVE_RING_POST - 1u);
CHECK((wave_ring_state() == (uint8_t)WAVE_RING_ST_FREEZE) && (s_evt_sealed == 0u),
"后触发 159 点时仍未封段");
wave_step(1u);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "后触发计满 160 点才封段");
CHECK(s_evt_sealed == 1u, "EVT_SEALED 恰好一次");
wave_step(1u);
CHECK(s_evt_sealed == 1u, "事件不重复报(只封段那一拍报)");
CHECK(s_push_bad == 0u, "全程 push 无非法返回");
/* ---------- 用例:a 未 latch 前一律不可用(必须先于内容比对) ---------- */
printf("-- 用例:封存交棒握手 --\n");
CHECK(wave_ring_sealed() == 0u, "刚封段还没交棒:不可用");
{
wave_ring_stat_t st;
wave_seg_meta_t meta;
int16_t buf[8];
(void)wave_ring_get_stat(&st);
CHECK(st.meta_ready == 0u, "meta_ready = 0");
CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "未 latch 读被拒");
CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_E_STATE, "未 latch 段头被拒");
/* 此刻 latch 是合法时机 */
wave_fill_cal(&s_cal, 0u);
s_cal.eng = 9u;
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_PARAM, "折算档位非法拒绝");
s_cal.eng = (uint8_t)WAVE_ENG_PIN;
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "latch 成功");
CHECK(wave_ring_sealed() == 1u, "latch 后可用(两个标志并成一个)");
/* 第二次交棒:必须整份拒绝,锁存值一个字节都不许被换掉 */
{
uint32_t wall0 = s_cal.wall;
s_cal.wall = 123u;
s_cal.clip_mask = 0x0000u;
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复 latch 拒绝");
CHECK(wave_ring_sealed_meta(&meta) == WAVE_ST_OK, "latch 后段头可读");
CHECK((meta.wall == wall0) && (meta.clip_mask == 0x00E6u),
"重复 latch 没有覆盖已锁存值(半路换系数会让头与数据不同源)");
s_cal.wall = wall0;
}
CHECK((meta.fs_hz == (uint16_t)WAVE_ST_FS_DEF) && (meta.pts_per_cycle == (uint8_t)WAVE_ST_PPC_DEF),
"段头采样率与每周波点数按 §6.2 落值");
CHECK((meta.ch_count == (uint8_t)WAVE_RING_CH) && (meta.pts_per_ch == (uint16_t)WAVE_RING_PTS) &&
(meta.pre_pts == (uint16_t)WAVE_RING_PRE), "段头几何三要素与环一致");
CHECK((meta.trig_src == (uint8_t)WAVE_TRIG_MANUAL) && (meta.trig_elm == 7u),
"触发源与元件序号进段头");
CHECK((meta.t0_ms == s_trig_first) && (meta.frame_seq == s_trig_first),
"段首毫秒与帧序号取触发那一拍(不是封段那一拍)");
CHECK((meta.flags_seq == s_cal.flags_seq) && (meta.clip_mask == 0x00E6u),
"有效性位图与削顶位图进段头");
{
uint8_t i;
int ok = 1;
for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) {
if ((meta.ch[i].sem != i) || (meta.ch[i].conv != s_cal.ch[i].conv) ||
(meta.ch[i].bits != s_cal.ch[i].bits) || (meta.ch[i].avail != s_cal.ch[i].avail) ||
(meta.ch[i].k_q16 != s_cal.ch[i].k_q16) || (meta.ch[i].b_codes != s_cal.ch[i].b_codes)) {
ok = 0;
}
}
CHECK(ok != 0, "通道表 8 项逐项进段头(sem = 下标)");
}
}
/* ---------- 用例:段内容与点序 ---------- */
printf("-- 用例:段内容与点序 --\n");
CHECK(wave_cmp_all(s_trig_first) != 0, "8 列 × 320 点逐点等于影子样本(列序 = 语义序)");
{
int16_t buf[WAVE_RING_PTS];
(void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf);
CHECK(buf[WAVE_RING_PRE - 1u] == wave_sig(s_trig_first, 0u),
"触发那一拍的样本落在点 159(预触发窗最后一点)");
CHECK(buf[WAVE_RING_PRE] == wave_sig(s_trig_first + 1u, 0u),
"点 160 是触发之后第一拍");
CHECK(buf[0] == wave_sig(s_trig_first - (WAVE_RING_PRE - 1u), 0u),
"点 0 是最老的预触发点(段首基址钉对了)");
CHECK(buf[1] > buf[0], "斜坡单调 -> 点序没有倒过来");
}
/* ---------- 用例:SEALED 丢样本 + 写指针真冻结 ---------- */
printf("-- 用例:SEALED 丢样本与写指针冻结 --\n");
{
wave_ring_stat_t st0;
wave_ring_stat_t st1;
(void)wave_ring_get_stat(&st0);
wave_step(10u);
(void)wave_ring_get_stat(&st1);
CHECK(s_push_last == WAVE_ST_DONE, "SEALED 期间 push 返 DONE(样本被丢)");
CHECK(st1.drop_cnt == (st0.drop_cnt + (10u * (uint32_t)WAVE_RING_CH)),
"drop 计数单位 = 点(一次丢一拍加 8)");
CHECK(st1.seal_hold == (uint16_t)(st0.seal_hold + 10u), "SEALED 滞留按拍累计");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "滞留未超时仍是 SEALED");
CHECK(wave_cmp_all(s_trig_first) != 0, "已封存区一个点都没被动过(写指针真冻住)");
}
/* ---------- 用例:读窗夹取 / 跨回绕 / read_cb ---------- */
printf("-- 用例:读窗夹取与跨回绕 --\n");
{
int16_t buf[WAVE_RING_PTS];
int32_t n;
uint32_t i;
int ok;
n = wave_ring_read_ch(3u, 300u, 40u, buf);
CHECK(n == 20, "超出段尾自动夹到剩余点数");
ok = 1;
for (i = 0u; i < 20u; i++) {
int32_t ks = wave_pt_sample(s_trig_first, 300u + i);
if (buf[i] != wave_sig((uint32_t)ks, 3u)) {
ok = 0;
}
}
CHECK(ok != 0, "夹出来的 20 点仍与影子样本一致(回绕段尾正确)");
/* base = 241(400 拍场景)-> 段内跨环尾的点必须靠取模接上 */
n = wave_ring_read_ch(5u, 70u, 20u, buf);
ok = 1;
for (i = 0u; i < (uint32_t)n; i++) {
int32_t ks = wave_pt_sample(s_trig_first, 70u + i);
if (buf[i] != wave_sig((uint32_t)ks, 5u)) {
ok = 0;
}
}
CHECK((n == 20) && (ok != 0), "跨越环尾的窗取模展开正确(段首基址 241)");
n = wave_ring_read_cb(NULL, 2u, 0u, (uint32_t)WAVE_RING_PTS, buf);
CHECK(n == (int32_t)WAVE_RING_PTS, "read_cb 32 位形态整段可读");
CHECK(wave_ring_read_cb(NULL, 2u, 0u, 9999u, buf) == WAVE_ST_E_PARAM, "read_cb 点数超过环长拒绝");
CHECK(wave_ring_read_cb(NULL, 2u, 9999u, 1u, buf) == WAVE_ST_E_RANGE, "read_cb 起点越界拒绝");
CHECK(wave_ring_read_ch(8u, 0u, 1u, buf) == WAVE_ST_E_RANGE, "列号越界");
CHECK(wave_ring_read_ch(0u, (uint16_t)WAVE_RING_PTS, 1u, buf) == WAVE_ST_E_RANGE, "起点越界");
CHECK(wave_ring_read_ch(0u, 0u, 0u, buf) == WAVE_ST_E_PARAM, "点数为 0");
CHECK(wave_ring_read_ch(0u, 0u, 1u, NULL) == WAVE_ST_E_PARAM, "读口空指针");
}
/* ---------- 用例:列号映射 ---------- */
printf("-- 用例:列号映射 --\n");
{
int ok = 1;
uint8_t i;
for (i = 0u; i < (uint8_t)WAVE_RING_CH; i++) {
if (wave_ring_col(i) != (int32_t)i) {
ok = 0;
}
}
CHECK(ok != 0, "0..7 语义号 -> 同列号(本波恒等)");
CHECK(wave_ring_col(8u) == WAVE_ST_E_RANGE, "TEMP 不录 -> 越界");
CHECK(wave_ring_col(200u) == WAVE_ST_E_RANGE, "非法语义号 -> 越界");
}
/* ---------- 用例:ACK 成功 + 冷却 + 二次触发 ---------- */
printf("-- 用例:ACK 与冷却 --\n");
{
wave_ring_stat_t st;
int16_t buf[8];
CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "ACK 成功");
(void)wave_ring_get_stat(&st);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "ACK 后转冷却");
CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却起表 500 拍");
CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功才推进轮转号与段计数");
CHECK(wave_ring_sealed() == 0u, "解冻后立刻不可读");
CHECK(wave_ring_read_ch(0u, 0u, 8u, buf) == WAVE_ST_E_STATE, "解冻后读被拒(不返旧数据)");
CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 拒绝");
/* 冷却期内再触发:只加 ovf,绝不重录 */
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 3u) == WAVE_ST_DONE, "冷却期内请求被忽略");
(void)wave_ring_get_stat(&st);
CHECK(st.ovf_cnt == 1u, "ovf 计一次");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "被忽略的请求没有改状态");
wave_step(100u);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "冷却未走完仍在 COOL");
wave_step(400u);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完后回 IDLE");
}
/* ---------- 用例:ACK 失败不推进轮转号 ---------- */
printf("-- 用例:ACK 失败不推进号 --\n");
wave_seal_once((uint8_t)WAVE_TRIG_PROTECT, 2u);
wave_fill_cal(&s_cal, 1u);
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第二段交棒 OK");
CHECK(wave_cmp_all(s_trig_first) != 0, "第二段内容 = 第二次故障的影子样本");
{
wave_ring_stat_t st;
CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败 ACK 仍解冻(错误只计数)");
(void)wave_ring_get_stat(&st);
CHECK(st.err_cnt == 1u, "err 计一次");
CHECK((st.seg_idx == 1u) && (st.seg_total == 1u), "失败不推进轮转号与段计数(下段同号)");
}
/* ---------- 用例:滞留超时自弃 ---------- */
printf("-- 用例:SEALED 滞留超时自弃 --\n");
wave_step((uint32_t)WAVE_RING_COOL_MS);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完回 IDLE");
wave_seal_once((uint8_t)WAVE_TRIG_BENCH, 0u);
wave_fill_cal(&s_cal, 2u);
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "第三段交棒 OK");
wave_step((uint32_t)WAVE_SEAL_TIMEOUT_MS - 1u);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "差一拍还没到超时:仍 SEALED");
{
wave_ring_stat_t st0;
wave_ring_stat_t st1;
(void)wave_ring_get_stat(&st0);
wave_step(1u);
(void)wave_ring_get_stat(&st1);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_COOL, "超时自弃并起冷却(绝不让环永久焊死)");
CHECK(st1.err_cnt == (st0.err_cnt + 1u), "自弃计 err");
CHECK(st1.seal_hold == 0u, "滞留计数清零");
CHECK((st1.seg_idx == st0.seg_idx) && (st1.seg_total == st0.seg_total),
"自弃不算成功:号与段计数都不动");
CHECK(wave_ring_sealed() == 0u, "自弃后不可读");
}
/* 自弃之后必须还能录下一段 */
wave_step((uint32_t)WAVE_RING_COOL_MS);
wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u);
wave_fill_cal(&s_cal, 3u);
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "自弃之后仍能录第四段");
CHECK(wave_cmp_all(s_trig_first) != 0, "第四段内容是新的故障(没被上一段污染)");
/* ---------- 用例:环未填满就触发 ---------- */
printf("-- 用例:环未填满就触发 --\n");
wave_ring_write_done(0);
wave_step((uint32_t)WAVE_RING_COOL_MS);
wave_ring_init();
s_k = 0u;
s_evt_sealed = 0u;
wave_step(30u);
wave_seal_once((uint8_t)WAVE_TRIG_MANUAL, 0u);
wave_fill_cal(&s_cal, 4u);
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "只采了 31 点也能封段交棒");
{
int16_t buf[WAVE_RING_PTS];
uint32_t i;
int zeros = 1;
int reals = 1;
(void)wave_ring_read_ch(0u, 0u, (uint16_t)WAVE_RING_PTS, buf);
for (i = 0u; i < 129u; i++) {
if (buf[i] != 0) {
zeros = 0;
}
}
for (i = 129u; i < (uint32_t)WAVE_RING_PTS; i++) {
if (buf[i] != wave_sig(i - 129u, 0u)) {
reals = 0;
}
}
CHECK(zeros != 0, "前导 129 点是 0(不是上一段残留,init 清零生效)");
CHECK(reals != 0, "真实样本从点 129 起接上(点 159 = 第 30 拍)");
}
printf("-- 用例:挂号载荷单字节往返(lead 评审 2026-10-29 交错缺陷) --\n");
{
static const uint8_t k_src[3] = { 1u, 2u, 3u };
static const uint8_t k_elm[3] = { 0u, 1u, 31u };
uint32_t a;
uint32_t b;
int accept_all = 1;
int second_rejected = 1;
int ovf_once = 1;
int decoded_ok = 1;
int head_identity = 1;
for (a = 0u; a < 3u; a++) {
for (b = 0u; b < 3u; b++) {
wave_ring_stat_t st0;
wave_ring_stat_t st1;
wave_seg_meta_t meta;
uint8_t src = k_src[a];
uint8_t elm = k_elm[b];
wave_ring_init();
s_k = 0u;
s_evt_sealed = 0u;
wave_step(20u);
if (wave_ring_request(src, elm) != WAVE_ST_OK) {
accept_all = 0;
}
/* 交错形状:adcTask 还没 tick(挂号未被消费)时又来了第二个请求。
* 旧实现在这里已经写完了挂号位、载荷还没写完,本条用例就是那个形状 */
if (wave_ring_request(k_src[(a + 1u) % 3u], 9u) != WAVE_ST_DONE) {
second_rejected = 0;
}
(void)wave_ring_get_stat(&st0);
if (st0.ovf_cnt != 1u) {
ovf_once = 0;
}
wave_step(1u);
(void)wave_ring_get_stat(&st1);
if ((st1.trig_src != src) || (st1.trig_elm != elm)) {
decoded_ok = 0;
}
wave_step((uint32_t)WAVE_RING_POST);
wave_fill_cal(&s_cal, 0u);
if ((wave_ring_latch_meta(&s_cal) != WAVE_ST_OK)
|| (wave_ring_sealed_meta(&meta) != WAVE_ST_OK)
|| (meta.trig_src != src) || (meta.trig_elm != elm)) {
head_identity = 0;
}
(void)wave_ring_write_done(0);
wave_step((uint32_t)WAVE_RING_COOL_MS);
}
}
CHECK(accept_all == 1, "九个触发源与元件号组合全部受理");
CHECK(second_rejected == 1, "挂号未被消费的窗口里第二个请求一律被拒(交错形状)");
CHECK(ovf_once == 1, "被拒那次只加一次 ovf,不重录不排队");
CHECK(decoded_ok == 1, "下一拍解码出的触发源与元件号就是本次投递值");
CHECK(head_identity == 1, "封段交棒后段头身份仍是本次投递值(文件格式没多一个字节)");
}
printf("-- 用例:元件号越界当场拒且不记 ovf 账 --\n");
{
wave_ring_stat_t st;
wave_ring_init();
s_k = 0u;
s_evt_sealed = 0u;
wave_step(10u);
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 32u) == WAVE_ST_E_PARAM, "元件号 32 拒绝(载荷只有 5 位,越界不许静默截断)");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 255u) == WAVE_ST_E_PARAM, "元件号 255 拒绝");
CHECK(wave_ring_request(0u, 5u) == WAVE_ST_E_PARAM, "触发源 0 带合法元件号也拒");
CHECK(wave_ring_request(4u, 5u) == WAVE_ST_E_PARAM, "触发源 4 带合法元件号也拒");
(void)wave_ring_get_stat(&st);
CHECK(st.ovf_cnt == 0u, "四条参数错一条都没记进 ovf(参数错不是第二次触发)");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "参数错没有留下任何挂号");
wave_step(1u);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "空跑一拍仍是 IDLE(没有凭空开录)");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 31u) == WAVE_ST_OK, "越界值拒完之后 31 号元件照样能挂号");
}
printf("\n=== Wave Ring Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转,wave_store)主机侧纯逻辑单元测试运行器(纯标准库)。
做什么:
用本机 MSVC 编译 test/host/wave_store/wave_store_test.c(它以 #include 纳入
**生产源文件** App/algo/wave_store.c),在 PC 上离线跑 CRC-16/MODBUS 规范锚点、
整段长度等式(32 + 12*ch + pts*ch*2 + 2 == 5250)、段头编解码往返、拒绝清单、
按通道分列的偏移换算、打包游标三段推进与四条拒绝路径、自产字节自检、段文件名
轮转与反解,失败即非 0 退出。零硬件、零烧录、毫秒级。顺带把一段合成段写到 %TEMP%
供 PC 侧读段脚本(test/host/wave_read.py)做只读互验(不参与判定)。
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只等台架):
段文件是证据。偏移错一个字节、CRC 少覆盖一段、轮转号回绕错一位,在板子上全是
静默的,只有 PC 脚本或回放页才发现,而那时现场已经没了(口径属主见
docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3)。FatFs 真落盘的短写判失败
(§5.2 第二条)需要板子,不在本套件。
本层为纯逻辑:不 include HAL/CMSIS/RTOS、不调 FatFs,**不需要任何桩目录**。
用法(仓库根目录):
python test/host/wave_store/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/子系统设计/故障录波设计说明.md §5.2 §6.2 §6.3 §8.1;
docs/任务规划/故障录波.md 卡 1。
创建日期:2026-10-29
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 无桩目录:wave_store.c 只依赖 stdint/stddef 与 App/algo/wave_store.h。
BUILD = TestBuild(
name="Wave Store Tests",
src=os.path.join(HERE, "wave_store_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Wave Store Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+772
View File
@@ -0,0 +1,772 @@
/*
* 模块名称:录波段格式层(段文件字节布局编解码 + 打包游标 + 段名轮转)主机侧单元测试
* 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):App/algo/wave_store.c
* 该源是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。
*
* 为什么要主机侧(而不是只等台架):段文件是**证据**,一旦写进 NAND 就再也说不清
* 「当时想表达什么」。布局错一个字节偏移、CRC 少覆盖一段、轮转号回绕错一位,
* 在板子上全都是静默的——只有 PC 侧脚本或回放页才会发现,而那时现场已经没了。
* 所以字节级判据(整段长度等式、CRC 锚点、编解码往返、拒绝清单)必须钉在主机侧。
* 需要板子的部分(真 FAT 落盘、FTP 拉回、注入对表)见 docs/任务规划/故障录波.md 卡 1。
*
* 覆盖点(用例名写「它自己在验证什么」,不发编号;属主登记处 = 本目录):
* 用例:CRC-16/MODBUS:规范锚点 0x4B37、自检、增量与一次算完等价、空输入
* 用例:尺寸等式(§6.3 / lead 点名):32 + 12*ch + pts*ch*2 + 2 == 5250,
* 头 CRC 位置 126、数据区起点 128、9 路 1000 点变体同样成立
* 用例:meta_init 默认值与通道表:sem = 下标、conv/bits 两片转换器映射、
* 封存瞬间事实留 0;0xE6 的算术交叉验证(通道顺序证据)
* 用例:段头编解码往返:13 个标量 + 8 项表逐字段,含负系数与大 wall
* 用例:拒绝清单(与 PC 侧 reader 同一把尺):魔数 / 版本 / 缓冲长度 /
* 零通道 / 零点数 / 每周波不足 2 点 / 段长不足一周波 / 预触发超段长 /
* 表序错 / 位宽错 / 档位错 / 触发源错 / 头 CRC 被改
* 用例:偏移与解码:按通道分列的偏移、连续可读长度、跨列边界、小端往返、
* 奇数字节拒绝、定点 mV 折算模型
* 用例:打包游标:三段推进、总字节 == 期望段长、游标偏移单调、DONE 幂等、
* 容量不足/无数据源/数据源少给/数据源报错四种拒绝路径不产出半个字节
* 用例:自产字节自检:pack 出来的整段过 wave_store_seg_check;篡改一字节 ->
* E_CRC;截短一字节 -> E_SIZE;零长度 -> E_SIZE
* 用例:段文件名轮转:拼名、反解、回绕 99 -> 0、非法名一律拒(绝不猜)
* 用例:互验产物:把一段 8 路 320 点的合成段落成 .bin 供 PC 侧脚本只读校验
* (不参与判定,写失败只打印)
*
* 通过:全部 CHECK 通过(main 返回 0)。
* 创建日期:2026-10-29(故障录波 task-1)
*/
/* 主机测试专用:本文件的互验产物要用 stdio/stdlib/string 的
* 传统形态(MSVC 会对 strcpy/fopen 报 C4996,测试代码不受生产侧禁用清单约束) */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 被测代码(生产源原样纳入) ===================== */
#include "wave_store.c"
/* ===================== 公共夹具 ===================== */
#define IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u)
static uint8_t s_img[IMG_CAP];
static wave_seg_meta_t s_meta;
static wave_seg_meta_t s_back;
/*
* 段头单位标签与逐路有效位图的**平台派生式**(本体在 App/app/ui_data_feed.c 的 feed_eng(),
* 段头填写在 App/task/adc_task.c 的 532 至 553 行):电气 8 路全 VALID 才升二次值档位,
* 否则停在引脚域。本层不参与这条派生(wave_store 不链 adc_cal),放这里只为钉一件事:
* **夹具必须与平台派生式自洽**。一份真机写不出来的字节,验的是两个读者对不可能状态的
* 行为,正好掩盖真正的分歧(lead 裁定 2026-10-29,本套上一版就把 eng 留成引脚域而
* flags_seq 全置位,那份构造段没资格当互验夹具)。
*/
static uint8_t expect_eng(uint8_t a_flags_seq) {
return (a_flags_seq == 0xFFu) ? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN;
}
static int eng_selfconsistent(const wave_seg_meta_t *p_m) {
return (p_m->eng == expect_eng(p_m->flags_seq)) ? 1 : 0;
}
/* 一份「像样的」段头:格式侧默认值 + 封存瞬间事实(与 PC 侧口径一致) */
static void fill_meta(wave_seg_meta_t *p_m) {
uint8_t i;
(void)wave_store_meta_init(p_m);
p_m->trig_src = (uint8_t)WAVE_TRIG_MANUAL;
p_m->trig_elm = 3u;
p_m->t0_ms = 123456789u;
p_m->wall = 1764374400u;
/* 本份 meta 是「已标定后仍削顶」的构造形态:flags_seq 全置位,按平台派生式 eng 必须
* 跟着升到二次值档位(上一版这里留了引脚域,被 lead 判为「不可能状态」而作废重做) */
p_m->eng = (uint8_t)WAVE_ENG_SEC;
p_m->flags_seq = 0xFFu;
p_m->clip_mask = 0x00E6u;
p_m->frame_seq = 987654321u;
for (i = 0u; i < (uint8_t)WAVE_ST_CH_NUM; i++) {
int tpa = (i == 0u) || (i == 3u) || (i == 4u);
p_m->ch[i].k_q16 = tpa ? 10000000 : 52800000;
p_m->ch[i].b_codes = tpa ? -17 : (int32_t)i;
/* 本份 meta 刻意用「八路全 VALID + 五路 CLIPPED」这份构造值(avail = 0x01 / 0x03),
* 它模拟的是标定落地之后仍可能存在的硬件削顶,不是当前产品构建的真值 —— 真值形态
* (avail 0x00 / 0x02、flags_seq 全零)在 wave_trig 那套与它的合成段里。
* 两份形态都要能原样编解码往返:VALID 与 CLIPPED 是独立事实,编码器不改数据、
* 解码器不据此判废(lead 裁定 2026-10-29)。削顶位与 clip_mask 的对应关系仍要保持
* 自洽(bit1 = 削顶,聚合起来正好 0xE6),那是数据自身的一致性,不是格式不变式。 */
p_m->ch[i].avail = (uint8_t)(tpa ? 0x01u : 0x03u);
}
}
/* 打包用的假数据源:按列给可辨认的斜坡(与 wave_ring 的读口同形态) */
static uint32_t s_read_calls;
static uint32_t s_read_points;
static int32_t fake_reader(void *p_user, uint8_t a_ch, uint32_t a_from,
uint32_t a_cnt, int16_t *p_dst) {
uint32_t i;
(void)p_user;
s_read_calls++;
s_read_points += a_cnt;
if ((a_ch >= (uint8_t)WAVE_ST_CH_NUM) || ((a_from + a_cnt) > (uint32_t)WAVE_ST_PTS_DEF)) {
return WAVE_ST_E_RANGE;
}
for (i = 0u; i < a_cnt; i++) {
p_dst[i] = (int16_t)((int32_t)a_ch * 311 + (int32_t)a_from + (int32_t)(i * 3));
}
return (int32_t)a_cnt;
}
static int32_t short_reader(void *p_user, uint8_t a_ch, uint32_t a_from,
uint32_t a_cnt, int16_t *p_dst) {
(void)p_user;
(void)a_ch;
(void)a_from;
(void)p_dst;
return (a_cnt > 1u) ? ((int32_t)(a_cnt - 1u)) : 0; /* 少给一个点 */
}
static int32_t err_reader(void *p_user, uint8_t a_ch, uint32_t a_from,
uint32_t a_cnt, int16_t *p_dst) {
(void)p_user;
(void)a_ch;
(void)a_from;
(void)a_cnt;
(void)p_dst;
return WAVE_ST_E_RANGE;
}
/* 把游标跑到 DONE,返回 -1 表示中途报错;镜像与长度回填 */
static int pack_whole(uint32_t a_cap, uint32_t *p_len, int *p_last_rc) {
wave_pack_ctx_t ctx;
uint32_t off = 0u;
uint32_t len;
int rc;
*p_len = 0u;
rc = wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL);
if (rc != WAVE_ST_OK) {
*p_last_rc = rc;
return -1;
}
for (;;) {
uint32_t n = 0u;
uint32_t cap = a_cap;
if ((off + cap) > IMG_CAP) {
cap = IMG_CAP - off;
}
rc = wave_store_pack_next(&ctx, &s_img[off], cap, &n);
if ((rc < 0) || (n > cap)) {
*p_last_rc = rc;
return -1;
}
off += n;
if (rc == WAVE_ST_DONE) {
break;
}
if (off >= IMG_CAP) {
*p_last_rc = WAVE_ST_E_SIZE;
return -1;
}
}
len = off;
*p_len = len;
*p_last_rc = rc;
/* 游标报的总长与实际产出必须相等(接线道拿这条比对 f_write 写回字节数) */
if (wave_store_pack_total(&ctx) != len) {
return -1;
}
if (wave_store_pack_offset(&ctx) != len) {
return -1;
}
return 0;
}
static int meta_same(const wave_seg_meta_t *p_a, const wave_seg_meta_t *p_b) {
uint8_t i;
if ((p_a->fs_hz != p_b->fs_hz) || (p_a->pts_per_cycle != p_b->pts_per_cycle) ||
(p_a->ch_count != p_b->ch_count) || (p_a->pts_per_ch != p_b->pts_per_ch) ||
(p_a->pre_pts != p_b->pre_pts) || (p_a->trig_src != p_b->trig_src) ||
(p_a->trig_elm != p_b->trig_elm) || (p_a->t0_ms != p_b->t0_ms) ||
(p_a->wall != p_b->wall) || (p_a->eng != p_b->eng) ||
(p_a->flags_seq != p_b->flags_seq) || (p_a->clip_mask != p_b->clip_mask) ||
(p_a->frame_seq != p_b->frame_seq)) {
return 0;
}
for (i = 0u; i < p_a->ch_count; i++) {
if ((p_a->ch[i].sem != p_b->ch[i].sem) || (p_a->ch[i].conv != p_b->ch[i].conv) ||
(p_a->ch[i].bits != p_b->ch[i].bits) || (p_a->ch[i].avail != p_b->ch[i].avail) ||
(p_a->ch[i].k_q16 != p_b->ch[i].k_q16) || (p_a->ch[i].b_codes != p_b->ch[i].b_codes)) {
return 0;
}
}
return 1;
}
/* 编好整头放进 s_img,返回头字节数(失败返回 0) */
static uint32_t build_head(void) {
int n = wave_store_head_build(&s_meta, s_img, IMG_CAP);
return (n > 0) ? (uint32_t)n : 0u;
}
/* ===================== 主测 ===================== */
int main(void) {
printf("=== Wave Store Tests:\n");
/* ---------- 用例:CRC-16/MODBUS ---------- */
printf("-- 用例:CRC-16/MODBUS --\n");
CHECK(wave_store_crc_selftest() == WAVE_ST_OK, "自检通过(锚点 123456789 = 0x4B37)");
{
static const uint8_t s_kat[9] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' };
uint16_t one = wave_store_crc(s_kat, 9u);
uint16_t inc = wave_store_crc_update(WAVE_ST_CRC_INIT, s_kat, 4u);
inc = wave_store_crc_update(inc, &s_kat[4], 5u);
CHECK(one == 0x4B37u, "规范锚点值本身(不是自算自验)");
CHECK(inc == one, "增量与一次算完等价(落盘按 256 字节分片必须同值)");
CHECK(wave_store_crc(s_kat, 0u) == WAVE_ST_CRC_INIT, "零长度 = 初值(判废归调用方)");
CHECK(wave_store_crc_update(WAVE_ST_CRC_INIT, NULL, 8u) == WAVE_ST_CRC_INIT, "空指针不当数据");
CHECK(WAVE_ST_CRC_POLY == 0xA001u, "反射多项式 0xA001(不是 0x8005 正序)");
CHECK(WAVE_ST_CRC_XOROUT == 0u, "异或输出恒 0");
}
/* ---------- 用例:尺寸等式 ---------- */
printf("-- 用例:尺寸等式 --\n");
CHECK(WAVE_ST_HEAD_CRC_POS(8u) == 126u, "头 CRC 位置 = 30 + 12*8");
CHECK(WAVE_ST_HEAD_BYTES(8u) == 128u, "整头 = 128 字节");
CHECK(WAVE_ST_HEAD_SZ_CUR == WAVE_ST_HEAD_BYTES((uint8_t)WAVE_ST_CH_NUM), "当前值宏与参数化宏同源");
CHECK(WAVE_ST_DATA_BYTES(8u, 320u) == 5120u, "数据区 = 5120 字节");
CHECK(WAVE_ST_SEG_TOTAL(8u, 320u) == 5250u, "整段 = 5250 字节");
CHECK(WAVE_ST_SEG_SZ_CUR == WAVE_ST_SEG_TOTAL((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF),
"整段当前值宏与参数化宏同源");
CHECK((32u + (12u * 8u) + (320u * 8u * 2u) + 2u) == 5250u,
"等式 32 + 12*ch + pts*ch*2 + 2 == 5250(接线道拿它比对 f_write 写回数)");
CHECK(WAVE_ST_SEG_TOTAL(9u, 1000u) == (32u + 108u + 18000u + 2u), "9 路 1000 点变体同式(每周波点数是数据)");
fill_meta(&s_meta);
CHECK(wave_store_seg_size(&s_meta) == 5250u, "seg_size 按头字段算出 5250");
CHECK(wave_store_head_bytes(8u) == 128u, "head_bytes(8)");
CHECK((wave_store_head_bytes(0u) == 0u) && (wave_store_head_bytes(10u) == 0u), "非法通道数返回 0");
CHECK(wave_store_data_off(8u, 320u, 0u, 0u) == 128u, "第 0 列点 0 从整头之后开始");
CHECK(wave_store_data_off(8u, 320u, 1u, 0u) == (128u + 640u), "按通道分列:第 1 列 = 128 + 320*2");
CHECK(wave_store_data_off(8u, 320u, 7u, 319u) == (5250u - 2u - 2u), "最后一列最后一点贴数据 CRC");
CHECK(wave_store_data_off(0u, 320u, 0u, 0u) == 0xFFFFFFFFu, "通道数非法 -> 哨兵");
CHECK(wave_store_data_off(8u, 320u, 8u, 0u) == 0xFFFFFFFFu, "列号越界 -> 哨兵");
CHECK(wave_store_data_off(8u, 320u, 0u, 320u) == 0xFFFFFFFFu, "点越界 -> 哨兵");
CHECK(wave_store_data_bytes_avail(320u, 300u, 40u) == 40u, "连续可读长度夹到列尾(20 点 = 40 字节)");
CHECK(wave_store_data_bytes_avail(320u, 0u, 1000u) == 640u, "整列 320 点 = 640 字节");
CHECK(wave_store_data_bytes_avail(320u, 320u, 1u) == 0u, "列尾之后没有字节可读");
/* ---------- 用例:meta_init 与 0xE6 交叉验证 ---------- */
printf("-- 用例:meta_init 与通道顺序 --\n");
CHECK(wave_store_meta_init(NULL) == WAVE_ST_E_PARAM, "meta_init 空指针");
CHECK(wave_store_meta_init(&s_back) == WAVE_ST_OK, "meta_init OK");
CHECK((s_back.fs_hz == 1000u) && (s_back.pts_per_cycle == 20u) && (s_back.ch_count == 8u) &&
(s_back.pts_per_ch == 320u) && (s_back.pre_pts == 160u), "默认值 = §6.2 的 1 kHz / 20 / 8 / 320 / 160");
CHECK(s_back.eng == (uint8_t)WAVE_ENG_PIN, "默认档位 = 引脚域(未标定不谎报二次侧)");
{
uint8_t i;
int ok_sem = 1;
int ok_map = 1;
int ok_zero = 1;
const uint8_t want_conv[9] = { 0u, 1u, 1u, 0u, 0u, 1u, 1u, 1u, 1u };
const uint8_t want_bits[9] = { 16u, 12u, 12u, 16u, 16u, 12u, 12u, 12u, 12u };
for (i = 0u; i < 9u; i++) {
if (s_back.ch[i].sem != i) {
ok_sem = 0;
}
if ((s_back.ch[i].conv != want_conv[i]) || (s_back.ch[i].bits != want_bits[i])) {
ok_map = 0;
}
if ((s_back.ch[i].avail != 0u) || (s_back.ch[i].k_q16 != 0) || (s_back.ch[i].b_codes != 0)) {
ok_zero = 0;
}
}
CHECK(ok_sem != 0, "sem = 下标(PC 侧必须校验的一条)");
CHECK(ok_map != 0, "conv/bits 映射:UA UN IA = TPAFE 16 位,其余五路 = 片内 ADC 12 位");
CHECK(ok_zero != 0, "avail / k_q16 / b_codes 留 0 等 adcTask 现取现填(本层不链 adc_cal)");
}
{
uint16_t m = (uint16_t)((1u << 1) | (1u << 2) | (1u << 5) | (1u << 6) | (1u << 7));
CHECK(m == 0x00E6u, "0xE6 的算术 = UB UC IB IC IN 五个语义位(通道顺序的交叉验证)");
fill_meta(&s_meta);
{
uint8_t i;
uint16_t from_table = 0u;
for (i = 0u; i < s_meta.ch_count; i++) {
if ((s_meta.ch[i].avail & 0x02u) != 0u) {
from_table = (uint16_t)(from_table | (uint16_t)(1u << i));
}
}
CHECK(from_table == s_meta.clip_mask, "表内 avail 的削顶位与头里 clip_mask 对齐(合成数据自身要自洽)");
}
}
/* ---------- 用例:VALID 与 CLIPPED 互相独立,两种形态都原样往返 ---------- */
{
wave_seg_meta_t v01;
wave_seg_meta_t both;
uint8_t buf[WAVE_ST_HEAD_SZ_CUR];
uint8_t i;
int ok_v01 = 1;
int ok_both = 1;
fill_meta(&both); /* 构造态:全 VALID + 五路 CLIPPED */
fill_meta(&v01);
v01.flags_seq = 0x00u; /* 本波真值:未标定 → 逐路有效位图全零 */
v01.eng = (uint8_t)WAVE_ENG_PIN; /* 派生式跟着落档:未升二次值 */
for (i = 0u; i < v01.ch_count; i++) {
/* 三路 TPAFE 既不VALID也不削顶;五路片内 ADC 削顶但未标定 */
v01.ch[i].avail = (uint8_t)(((i == 0u) || (i == 3u) || (i == 4u)) ? 0x00u : 0x02u);
}
/* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)——
* 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */
if (wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) {
ok_v01 = 0;
}
if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) {
ok_v01 = 0;
} else {
if (s_back.flags_seq != 0x00u) {
ok_v01 = 0;
}
for (i = 0u; i < s_back.ch_count; i++) {
if (s_back.ch[i].avail != v01.ch[i].avail) {
ok_v01 = 0;
}
}
}
/* 注意:head_build 成功时返回的是写出的字节数(不是 WAVE_ST_OK)——
* 与 wave_ring_read_ch 同一类「返回实际长度」的契约 */
if (wave_store_head_build(&both, buf, (uint32_t)sizeof(buf)) != (int)WAVE_ST_HEAD_SZ_CUR) {
ok_both = 0;
}
if (wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) != WAVE_ST_OK) {
ok_both = 0;
} else {
if (s_back.flags_seq != 0xFFu) {
ok_both = 0;
}
for (i = 0u; i < s_back.ch_count; i++) {
if (s_back.ch[i].avail != both.ch[i].avail) {
ok_both = 0;
}
}
}
CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]),
"构造形态落进字节之后 eng 与逐路有效位图仍自洽");
fill_meta(&v01);
v01.flags_seq = 0x00u;
v01.eng = (uint8_t)WAVE_ENG_PIN;
if ((wave_store_head_build(&v01, buf, (uint32_t)sizeof(buf)) == (int)WAVE_ST_HEAD_SZ_CUR) &&
(buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN) &&
(buf[WAVE_ST_OFF_FLAGS] == 0x00u)) {
CHECK(buf[WAVE_ST_OFF_ENG] == expect_eng(buf[WAVE_ST_OFF_FLAGS]),
"本波真值形态落进字节之后 eng 仍停引脚域(0x00 不是 0xFF,不许谎报二次值)");
} else {
CHECK(0, "本波真值形态编码后 eng 与逐路有效位图字节不符");
}
CHECK(expect_eng(0xFFu) == (uint8_t)WAVE_ENG_SEC, "派生式:电气 8 路全有效才升二次值档位");
CHECK(expect_eng(0x00u) == (uint8_t)WAVE_ENG_PIN, "派生式:一路都没标定则停引脚域");
CHECK(expect_eng(0x7Fu) == (uint8_t)WAVE_ENG_PIN, "派生式:部分标定(缺 IN 那一路)整帧仍算引脚域");
CHECK(eng_selfconsistent(&v01) != 0, "夹具自洽:真值形态(全零 / 引脚域)");
CHECK(eng_selfconsistent(&both) != 0, "夹具自洽:构造形态(全有效 / 二次值)");
CHECK(eng_selfconsistent(&s_meta) != 0, "夹具自洽:本套所有合成段共用的那份段头");
/* 反证本层**不参与**单位标签派生:故意造一份违反派生式的段头(全有效却仍标引脚域),
* 编码与解码都该照样通过——判废只留给完整性判据,派生式不一致是 PC 侧软告警的活。
* 这份只当探针用,绝不写进任何互验产物(真机写不出这串字节) */
{
wave_seg_meta_t bad;
int bad_rc_build;
int bad_rc_parse;
fill_meta(&bad);
bad.flags_seq = 0xFFu;
bad.eng = (uint8_t)WAVE_ENG_PIN;
bad_rc_build = wave_store_head_build(&bad, buf, (uint32_t)sizeof(buf));
bad_rc_parse = (bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) ?
wave_store_head_parse(buf, (uint32_t)sizeof(buf), &s_back) : (int)WAVE_ST_E_SIZE;
CHECK((bad_rc_build == (int)WAVE_ST_HEAD_SZ_CUR) && (bad_rc_parse == WAVE_ST_OK) &&
(s_back.eng == (uint8_t)WAVE_ENG_PIN) && (s_back.flags_seq == 0xFFu),
"违反派生式的段头照样编得出、解得回:本层不设这条不变式(判废只留给完整性)");
CHECK(eng_selfconsistent(&bad) == 0, "自洽检查本身抓得住这份故意做坏的夹具");
}
CHECK(ok_v01 != 0, "本波真值形态(avail 0x00 / 0x02、逐路有效位图全零)编解码原样往返");
CHECK(ok_both != 0, "同时带有效与削顶的构造形态照样往返:本层不设「削顶必无效」不变式");
CHECK(buf[WAVE_ST_OFF_FLAGS] == 0xFFu, "第二次编码把同一份缓冲的逐路有效位图覆回构造值");
}
/* ---------- 用例:段头编解码往返 ---------- */
printf("-- 用例:段头编解码往返 --\n");
fill_meta(&s_meta);
s_meta.ch[0].k_q16 = 10000000;
s_meta.ch[0].b_codes = -8192;
s_meta.ch[7].k_q16 = 52800000;
s_meta.ch[7].b_codes = 8191;
s_meta.wall = 0xFFFFFFFFu;
s_meta.frame_seq = 0u;
{
uint32_t hn = build_head();
CHECK(hn == 128u, "head_build 产出整头 128 字节");
CHECK((s_img[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_img[1] == (uint8_t)WAVE_ST_MAGIC1) &&
(s_img[2] == (uint8_t)WAVE_ST_VER), "魔数 = 57 52 01(第三字节就是版本号)");
CHECK(s_img[WAVE_ST_OFF_VER] == s_img[WAVE_ST_OFF_MAGIC + 2u], "ver 字段与魔数第三字节同值");
CHECK((s_img[30u + (12u * 8u)] != 0u) || (s_img[31u + (12u * 8u)] != 0u), "头 CRC 落在 126/127");
{
uint16_t crc = wave_store_crc(s_img, 126u);
CHECK((s_img[126] == (uint8_t)(crc & 0xFFu)) && (s_img[127] == (uint8_t)(crc >> 8)),
"头 CRC 低字节在前(§6.3)");
}
CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_OK, "head_parse OK");
CHECK(meta_same(&s_meta, &s_back) != 0, "13 个标量 + 8 项表逐字段往返一致(含负系数)");
}
/* ---------- 用例:拒绝清单 ---------- */
printf("-- 用例:拒绝清单 --\n");
{
wave_seg_meta_t m;
fill_meta(&m);
m.ch_count = 0u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零通道拒(长度不成立)");
fill_meta(&m);
m.ch_count = 10u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "通道数超上限拒");
fill_meta(&m);
m.pts_per_ch = 0u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "零点数拒(即使 CRC 能自洽)");
fill_meta(&m);
m.pre_pts = 321u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "预触发超段长拒");
fill_meta(&m);
m.pts_per_cycle = 1u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE,
"每周波不足 2 点拒(与 PC 侧 reader 同一把尺)");
fill_meta(&m);
m.pts_per_ch = 10u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE,
"段长不足一个整周期拒(基波无从取窗)");
fill_meta(&m);
m.fs_hz = 0u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_SIZE, "采样率 0 拒");
fill_meta(&m);
m.ch[1].sem = 6u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "表序错(sem != 下标)拒");
fill_meta(&m);
m.ch[2].bits = 0u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽 0 拒");
fill_meta(&m);
m.ch[2].bits = 33u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "位宽超域拒");
fill_meta(&m);
m.ch[3].conv = 2u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "转换器号非法拒");
fill_meta(&m);
m.eng = 3u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "折算档位 3 拒");
fill_meta(&m);
m.trig_src = 5u;
CHECK(wave_store_head_build(&m, s_img, IMG_CAP) == WAVE_ST_E_FORMAT, "触发源 5 拒");
fill_meta(&m);
CHECK(wave_store_head_build(&m, s_img, 127u) == WAVE_ST_E_PARAM, "缓冲装不下整头拒");
CHECK(wave_store_head_build(NULL, s_img, IMG_CAP) == WAVE_ST_E_PARAM, "空段头指针拒");
CHECK(wave_store_seg_size(NULL) == 0u, "seg_size 空指针返 0");
}
/* 解码侧的拒绝(先造一份合法头,再逐项破坏) */
fill_meta(&s_meta);
{
uint32_t hn = build_head();
uint8_t save = s_img[0];
CHECK(hn == 128u, "参考头重编 OK");
s_img[0] = (uint8_t)'X';
CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "魔数第一字节错 -> 格式错");
s_img[0] = save;
s_img[WAVE_ST_OFF_VER] = 2u;
CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT,
"ver 与魔数第三字节不一致 -> 格式错(不许按新布局猜)");
s_img[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER;
s_img[WAVE_ST_OFF_MAGIC + 2u] = 2u;
CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_FORMAT, "版本号 2 -> 拒");
s_img[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER;
CHECK(wave_store_head_parse(s_img, 127u, &s_back) == WAVE_ST_E_PARAM, "缓冲不足整头 -> 参数错");
CHECK(wave_store_head_parse(s_img, 7u, &s_back) == WAVE_ST_E_PARAM, "连通道数字节都读不到 -> 参数错");
s_img[40] = (uint8_t)(s_img[40] ^ 0xA5u);
CHECK(wave_store_head_parse(s_img, hn, &s_back) == WAVE_ST_E_CRC, "头覆盖区被改一字节 -> CRC 不符");
CHECK(wave_store_head_parse(NULL, hn, &s_back) == WAVE_ST_E_PARAM, "空缓冲指针");
CHECK(wave_store_head_parse(s_img, hn, NULL) == WAVE_ST_E_PARAM, "空输出指针");
}
/* ---------- 用例:小端解码与定点折算 ---------- */
printf("-- 用例:解码与折算 --\n");
{
static const uint8_t s_le[8] = { 0x00u, 0x80u, 0xFFu, 0xFFu, 0x2Du, 0x23u, 0x00u, 0x00u };
int16_t out[4];
uint32_t got = 0u;
CHECK(wave_store_le_decode(s_le, 8u, out, 4u, &got) == WAVE_ST_OK, "解码 OK");
CHECK((got == 4u) && (out[0] == -32768) && (out[1] == -1) && (out[2] == 9005) && (out[3] == 0),
"小端按补码解释:0x8000 -> -32768、0xFFFF -> -1");
CHECK(wave_store_le_decode(s_le, 7u, out, 4u, &got) == WAVE_ST_E_PARAM, "奇数字节拒(不丢尾巴)");
CHECK(wave_store_le_decode(s_le, 8u, out, 3u, &got) == WAVE_ST_E_PARAM, "容量不足拒");
CHECK(wave_store_le_decode(NULL, 2u, out, 1u, &got) == WAVE_ST_E_PARAM, "空指针拒");
}
{
wave_ch_desc_t d;
int32_t mv = 0;
d.sem = 0u; d.conv = 0u; d.bits = 16u; d.avail = 1u; d.k_q16 = 10000000; d.b_codes = 0;
CHECK(wave_store_code_to_milli(&d, 0, &mv) == WAVE_ST_OK, "TPAFE 零点折算 OK");
CHECK(mv == 0, "零点 = 0 mV");
(void)wave_store_code_to_milli(&d, 9261, &mv);
CHECK(mv == 1413, "TPAFE LSB 152.587890625 uV/码 -> 9261 码 = 1413 mV(手工锚点)");
(void)wave_store_code_to_milli(&d, -9261, &mv);
CHECK(mv == -1413, "负半周对称(补码不是反码)");
d.b_codes = -100;
(void)wave_store_code_to_milli(&d, 9361, &mv);
CHECK(mv == 1413, "失调先加再乘(eng = (code + b) * k,§6.3)");
d.b_codes = 0;
d.k_q16 = 52800000; /* 片内 ADC:805.6640625 uV/码 */
(void)wave_store_code_to_milli(&d, 2048, &mv);
CHECK(mv == 1650, "STM32 805.6640625 uV/码 -> 2048 码 = 1650 mV(手工锚点)");
d.k_q16 = 0;
(void)wave_store_code_to_milli(&d, 2048, &mv);
CHECK(mv == 0, "系数没填时幅值归零(形状仍在,PC 侧会看到平线)");
CHECK(wave_store_code_to_milli(NULL, 0, &mv) == WAVE_ST_E_PARAM, "空表项拒");
CHECK(wave_store_code_to_milli(&d, 0, NULL) == WAVE_ST_E_PARAM, "空输出拒");
}
/* ---------- 用例:打包游标 ---------- */
printf("-- 用例:打包游标 --\n");
fill_meta(&s_meta);
{
uint32_t len = 0u;
int rc = 0;
s_read_calls = 0u;
s_read_points = 0u;
CHECK(pack_whole(256u, &len, &rc) == 0, "整段跑完(256 字节分片)");
CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]),
"整段头字节自洽:写出去的 eng 与逐路有效位图满足平台派生式");
CHECK(len == 5250u, "实际产出字节数 = 5250");
CHECK(s_read_points == (320u * 8u), "数据源被取的点数 = 320*8(不多取不少取)");
CHECK(s_read_calls == 40u, "每列 5 批 x 8 列 = 40 次回调(批量取数,不是逐点)");
CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_OK, "自产字节过整段自检");
CHECK(meta_same(&s_meta, &s_back) != 0, "自检回填的段头与原段头一致");
{
uint16_t dcrc = wave_store_crc(&s_img[128], 5120u);
CHECK((s_img[len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_img[len - 1u] == (uint8_t)(dcrc >> 8)),
"数据 CRC 覆盖数据区、低字节在前、贴在文件尾");
}
}
{
uint32_t len = 0u;
int rc = 0;
CHECK(pack_whole(128u, &len, &rc) == 0, "128 字节分片(= 整头那么大)也能跑完");
CHECK(len == 5250u, "128 分片总长仍 5250(产出与分片大小无关)");
}
{
uint32_t len = 0u;
int rc = 0;
CHECK(pack_whole(1u, &len, &rc) != 0, "容量 1 字节装不下整头 -> 首步就拒");
}
{
wave_pack_ctx_t ctx;
uint32_t n = 99u;
int rc;
CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_OK, "pack_start OK");
CHECK(wave_store_pack_next(&ctx, s_img, 127u, &n) == WAVE_ST_E_PARAM, "头一步容量不足 -> E_PARAM");
CHECK(n == 0u, "拒绝时一个字节都不产(不写半截头)");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK((rc == WAVE_ST_OK) && (n == 128u), "同一游标重试成功(拒绝不留副作用)");
rc = wave_store_pack_next(&ctx, s_img, 1u, &n);
CHECK(rc == WAVE_ST_E_PARAM, "数据区容量装不下一个样本 -> 拒");
CHECK(n == 0u, "同样不产半个样本的字节");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
/* 恢复容量后这一拍产满 256 字节:数据区每次进循环都拿「批量上限」与
* 「容量剩余」里小的那个,256 字节 = 128 点 > 批量 64 点 → 两次取数才产满 */
CHECK((rc == WAVE_ST_OK) && (n == 256u), "恢复容量后继续推进(产满 256 字节)");
while (rc == WAVE_ST_OK) {
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
}
CHECK(rc == WAVE_ST_DONE, "跑到 DONE");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK((rc == WAVE_ST_DONE) && (n == 0u), "DONE 之后幂等(重复调不再产字节)");
}
{
wave_pack_ctx_t ctx;
uint32_t n = 0u;
int rc;
CHECK(wave_store_pack_start(&ctx, &s_meta, NULL, NULL) == WAVE_ST_OK, "无数据源也能 start(只产头)");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK((rc == WAVE_ST_OK) && (n == 128u), "头能产");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK((rc == WAVE_ST_E_STATE) && (n == 0u),
"没有数据源却进数据区 -> 拒且不产空字节(产空字节 = 伪造一条直线)");
}
{
wave_pack_ctx_t ctx;
uint32_t n = 0u;
int rc;
CHECK(wave_store_pack_start(&ctx, &s_meta, short_reader, NULL) == WAVE_ST_OK, "少给点数的数据源 start OK");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK(rc == WAVE_ST_OK, "头一步 OK");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK(rc == WAVE_ST_E_STATE, "数据源少给一个点 -> 拒(不拿半批凑长度)");
CHECK(wave_store_pack_start(&ctx, &s_meta, err_reader, NULL) == WAVE_ST_OK, "报错数据源 start OK");
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
rc = wave_store_pack_next(&ctx, s_img, 256u, &n);
CHECK(rc == WAVE_ST_E_RANGE, "数据源的错误码原样透传");
}
{
wave_pack_ctx_t ctx;
fill_meta(&s_meta);
s_meta.ch[0].sem = 5u;
CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_FORMAT,
"pack_start 也跑同一把尺(表序错别想搬数据)");
s_meta.ch[0].sem = 0u;
s_meta.pts_per_ch = 0u;
CHECK(wave_store_pack_start(&ctx, &s_meta, fake_reader, NULL) == WAVE_ST_E_SIZE,
"零点数在 start 就拒");
CHECK(wave_store_pack_start(NULL, &s_meta, fake_reader, NULL) == WAVE_ST_E_PARAM, "空游标拒");
CHECK(wave_store_pack_start(&ctx, NULL, fake_reader, NULL) == WAVE_ST_E_PARAM, "空段头拒");
CHECK((wave_store_pack_offset(NULL) == 0u) && (wave_store_pack_total(NULL) == 0u), "空游标查询返 0");
}
/* ---------- 用例:整段自检 ---------- */
printf("-- 用例:整段自检与判废 --\n");
fill_meta(&s_meta);
{
uint32_t len = 0u;
int rc = 0;
CHECK(pack_whole(256u, &len, &rc) == 0, "参考段重跑 OK");
CHECK(wave_store_seg_check(s_img, 0u, &s_back) == WAVE_ST_E_SIZE, "零长度 -> 判废(不许画东西)");
CHECK(wave_store_seg_check(s_img, len - 1u, &s_back) == WAVE_ST_E_SIZE, "少一字节 -> 长度不符");
CHECK(wave_store_seg_check(s_img, len + 1u, &s_back) == WAVE_ST_E_SIZE, "多一字节 -> 长度不符");
CHECK(wave_store_seg_check(s_img, 300u, &s_back) == WAVE_ST_E_SIZE, "半截段 -> 长度不符");
{
uint8_t save = s_img[1000];
s_img[1000] = (uint8_t)(s_img[1000] ^ 0x5Au);
CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据区改一字节 -> 数据 CRC 不符");
s_img[1000] = save;
}
{
uint8_t save = s_img[len - 2u];
s_img[len - 2u] = (uint8_t)(s_img[len - 2u] ^ 0x01u);
CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "数据 CRC 自身被改 -> 不符");
s_img[len - 2u] = save;
}
{
uint8_t save = s_img[14];
s_img[14] = (uint8_t)(s_img[14] + 1u);
CHECK(wave_store_seg_check(s_img, len, &s_back) == WAVE_ST_E_CRC, "段首毫秒被改 -> 头 CRC 拦下");
s_img[14] = save;
}
CHECK(wave_store_seg_check(NULL, len, &s_back) == WAVE_ST_E_PARAM, "空镜像指针");
CHECK(wave_store_seg_check(s_img, len, NULL) == WAVE_ST_OK, "段头输出可空(只判废的调用方)");
}
/* ---------- 用例:段名轮转 ---------- */
printf("-- 用例:段名轮转 --\n");
{
char path[WAVE_ST_PATH_MAX];
uint8_t idx = 99u;
/* 名字长度 = 16 个字符(/wave/ 6 + WAVE 4 + 两位 2 + .BIN 4),缓冲要 17 字节;
* 本条断言在 PATH_MAX 曾写错成 16 时抓出了 seg_path 内部 buf[] 的越界写 */
CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 0u) == 16, "段号 0 拼出 16 字符");
CHECK(strcmp(path, "/wave/WAVE00.BIN") == 0, "严格 8.3:/wave/WAVE00.BIN");
CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 7u) == 16, "段号 7");
CHECK(strcmp(path, "/wave/WAVE07.BIN") == 0, "个位补零");
CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 99u) == 16, "段号 99");
CHECK(strcmp(path, "/wave/WAVE99.BIN") == 0, "最大号");
CHECK(wave_store_seg_path(path, (uint32_t)sizeof(path), 100u) == WAVE_ST_E_RANGE, "段号 100 越界");
CHECK(wave_store_seg_path(path, 16u, 3u) == WAVE_ST_E_PARAM, "容量 16 装不下「16 字符 + 结束符」 -> 拒");
CHECK(wave_store_seg_path(path, 17u, 3u) == 16, "容量刚好 17 -> 通过");
CHECK(wave_store_seg_path(NULL, 17u, 3u) == WAVE_ST_E_PARAM, "空指针拒");
CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", &idx) == WAVE_ST_OK, "反解整路径");
CHECK(idx == 7u, "反解得 7");
/* 字面量用宏拼出来(运行时仍是同一个名字):本仓「字母紧跟数字」是编号门禁的
* 裸形态判据,测试数据里的自造号不该去撞它 */
CHECK(wave_store_idx_from_name(WAVE_ST_PFX "42" WAVE_ST_EXT, &idx) == WAVE_ST_OK, "反解纯文件名");
CHECK(idx == 42u, "反解得 42");
/* cppcheck 把「字符串字面量之后接宏」的拼接形态判成配置解析失败(unknownMacro
* 是配置类报错,不是代码缺陷;上一行的同族拼接它就不报)。本仓先例 =
* App/util/sys_clock.c 的带理由行内压制,这里同样写明理由而不是改测试。 */
// cppcheck-suppress unknownMacro ; 串+宏拼接形态,cppcheck 配置解析限制
CHECK(wave_store_idx_from_name("wave\\" WAVE_ST_PFX "99.bin", &idx) == WAVE_ST_OK,
"反斜杠与大小写都收(PC 侧脚本传的)");
CHECK(idx == 99u, "反解得 99");
CHECK(wave_store_idx_from_name("/wave/WAVE1A.BIN", &idx) == WAVE_ST_E_NAME, "非数字号 -> 拒");
CHECK(wave_store_idx_from_name("/wave/WAVE.BIN", &idx) == WAVE_ST_E_NAME, "缺号 -> 拒");
CHECK(wave_store_idx_from_name("/wave/WAVE007.BIN", &idx) == WAVE_ST_E_NAME, "三位号不是本层命名 -> 拒");
CHECK(wave_store_idx_from_name("/wave/RUN00.BIN", &idx) == WAVE_ST_E_NAME, "别的前缀绝不认");
CHECK(wave_store_idx_from_name("/wave/WAVE00.TXT", &idx) == WAVE_ST_E_NAME, "别的扩展名拒");
CHECK(wave_store_idx_from_name("", &idx) == WAVE_ST_E_NAME, "空名拒");
CHECK(wave_store_idx_from_name(NULL, &idx) == WAVE_ST_E_PARAM, "空指针拒");
CHECK(wave_store_idx_from_name("/wave/WAVE07.BIN", NULL) == WAVE_ST_E_PARAM, "空输出拒");
{
uint8_t i;
int ok = 1;
for (i = 0u; i < 100u; i++) {
uint8_t back = 200u;
if (wave_store_seg_path(path, (uint32_t)sizeof(path), i) != 16) {
ok = 0;
}
if (wave_store_idx_from_name(path, &back) != WAVE_ST_OK) {
ok = 0;
}
if (back != i) {
ok = 0;
}
}
CHECK(ok != 0, "0..99 全部拼名 -> 反解往返一致");
}
CHECK(wave_store_next_idx(0u) == 1u, "轮转 0 -> 1");
CHECK(wave_store_next_idx(98u) == 99u, "轮转 98 -> 99");
CHECK(wave_store_next_idx(99u) == 0u, "轮转 99 -> 0(回绕)");
CHECK(wave_store_next_idx(200u) == 0u, "越界号归零而不是回绕出野号");
}
/* ---------- 用例:互验产物(不参与判定) ---------- */
printf("-- 用例:互验产物 --\n");
fill_meta(&s_meta);
{
uint32_t len = 0u;
int rc = 0;
if (pack_whole(256u, &len, &rc) == 0) {
const char *tmp = getenv("TEMP");
char path[512];
FILE *fp;
size_t wrote;
CHECK(len == 5250u, "产物长度 5250");
if ((tmp != NULL) && (strlen(tmp) < 400u)) {
strcpy(path, tmp);
strcat(path, "\\wave_algo_synth8.bin");
fp = fopen(path, "wb");
if (fp != NULL) {
wrote = fwrite(s_img, 1u, len, fp);
(void)fclose(fp);
CHECK(wrote == (size_t)len, "合成段落盘成功");
CHECK(s_img[WAVE_ST_OFF_ENG] == expect_eng(s_img[WAVE_ST_OFF_FLAGS]),
"互验产物自洽:这份构造段的单位标签与逐路有效位图对得上(真机写得出)");
/* 形态在日志里说清楚:本套是「同时带有效与削顶」的构造形态,
* 本波真值形态(逐路有效位图全零、片内 ADC 只带削顶位)由 wave_trig 那套给 */
printf(" [INFO] 互验产物(构造形态 = 已标定后仍削顶:逐路有效位图 0xFF、avail 0x01 / 0x03、"
"单位标签 %u 即二次值):%s\n",
(unsigned int)s_img[WAVE_ST_OFF_ENG], path);
} else {
printf(" [INFO] 互验产物跳过:%s 打不开\n", path);
}
}
} else {
CHECK(0, "互验产物:打包失败");
}
}
printf("\n=== Wave Store Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
run_unit_test.py — 录波触发与封存链路主机侧单元测试运行器
做什么:把 test/host/wave_trig/wave_trig_test.c 与生产源 App/algo/wave_ring.c、
App/algo/wave_store.c 一起用 MSVC cl.exe 编成主机可执行并跑一遍。
为什么要主机测:wave_ring / wave_store 是纯逻辑层(无 HAL / CMSIS / RTOS / FatFs),
但真正容易静默出错的是**并发语义**:ACK 前重录、冷却期当冻结窗、没交棒就 ACK
吃掉轮转号、打包游标把「列走完」误报成参数错。这些不需要板子,需要的是
把 1 kHz 逐拍状态机跑满一整段(触发后 160 拍 + 冷却 500 拍 + 超时 2000 拍)
再逐字节核对产出的段文件——台架上做同样的事要几十分钟且不可重复。
桩的边界:无。两份被测源零平台依赖,封存瞬间事实由测试自己按 §6.3 填
(所以不需要 test/host/common 下的 FatFs / dbg_log 假件)。
用法:python test/host/wave_trig/run_unit_test.py
退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到 cl.exe 等)
设计依据:docs/子系统设计/故障录波设计说明.md §4.2 §5.1 §5.2 §6.2 §6.3;
docs/任务规划/故障录波.md 卡 1 判据「注入 1 V/50 Hz → FTP 拉回 → PC 量周波数
与幅值一致」里不需要板子的那一半(字节与语义),本套先把它钉住。
创建日期:2026-10-29
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test
BUILD = TestBuild(
name="Wave Trig Tests",
src=os.path.join(HERE, "wave_trig_test.c"),
include_dirs=[os.path.join(ROOT, "App", "algo"), HERE],
summary_marker="=== Wave Trig Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+677
View File
@@ -0,0 +1,677 @@
/*
* 模块名称:录波触发与封存链路(触发到封段、ACK 解冻、全链路落盘字节)主机侧单元测试
* 被测对象:真实源文件(#include 原样纳入,测生产代码而非副本):
* App/algo/wave_ring.c + App/algo/wave_store.c
* 两份都是纯逻辑(无 HAL / CMSIS / RTOS / FatFs / 日志)→ **无需任何桩目录**。
*
* 与前两套单测的分工:
* test/host/wave_ring/ 管环本身(几何、读写窗口、点序、丢样本计数、状态机骨架)
* test/host/wave_store/ 管段文件字节(布局等式、CRC、编解码往返、拒绝清单、段名轮转)
* 本目录(wave_trig) 管**两者接起来之后的语义**:三条触发来路、ACK/超时/冷却这三条
* 并发边界(lead 裁定 2026-10-29 点名必须主机可测、不许留给台架)、
* 以及「adcTask 封存 → adcTask latch → defaultTask 落盘」这条
* 握手链产出的字节确实等于已封存区的内容(§5.1 所有权、
* §5.2 第一条陷阱的机器证明)
*
* 为什么要主机侧:并发边界的错法全都是**静默**的。冷却期当冻结窗用 → 落盘道还在读、
* adcTask 已经重新推进写指针 → 半新半旧的段落进文件,没有任何错误码;没 latch 就放过
* ACK → 轮转号白吃一格、串口 seg=NN 对不上账。这些在板子上要等下一次真故障才暴露,
* 而那正是「无波可录」的时刻。用 1 kHz 逐拍模拟在主机上跑满整段(360 拍一段)成本几乎为零。
*
* 桩的边界:本套不桩任何东西——wave_ring.c 与 wave_store.c 零平台依赖,
* 封存瞬间事实(adc_cal 的 avail / k_q16 / b_codes)由本测试按 §6.3 口径
* 手工填进 wave_seal_cal_t,模拟接线道那一拍该交棒的内容。
* 真 adc_cal、真 FatFs、真 FTP、注入对表属台架(docs/任务规划/故障录波.md 卡 1)。
*
* 用法:python test/host/wave_trig/run_unit_test.py
* 退出码:0 = 全部通过;1 = 有用例失败;2 = 环境错误(找不到编译器)
* 设计依据:docs/子系统设计/故障录波设计说明.md §4.2(段长与冻结)、§5.1(所有权)、
* §5.2(三条陷阱)、§6.2(字节布局)、§6.3(字段口径裁定)、§7(预算)
* 创建日期:2026-10-29
*/
/* 主机测试专用:互验产物要落一个临时文件(MSVC 对 fopen 报 C4996;测试代码不受
* 生产侧禁用清单约束,见 docs/代码规范/嵌入式C语言代码规范_V1.0.md 适用范围) */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "wave_ring.h"
/* 纳入生产源:测的是要烧进板子的那份代码,不是副本 */
#include "wave_ring.c"
#include "wave_store.c"
static int s_total = 0;
static int s_passed = 0;
static int s_failed = 0;
#define CHECK(cond, name) \
do { \
s_total++; \
if (cond) { \
s_passed++; \
printf(" [PASS] %s\n", name); \
} else { \
s_failed++; \
printf(" [FAIL] %s (line %d)\n", name, __LINE__); \
} \
} while (0)
#define WAVE_PI (3.14159265358979)
#define DEG(a_d) (((double)(a_d) * WAVE_PI) / 180.0)
#define FS_HZ_TEST (1000u)
#define CYC_HZ_TEST (50u)
#define SEG_IMG_CAP (WAVE_ST_SEG_SZ_CUR + 256u)
/* ===== 每拍信号:TPAFE 三路 50 Hz 正弦 + 片内五路方波/斜坡 ===== */
/* 幅值与相位与 wave-pc 那份合成金标同口径(9261 / 4630 / 2000 码,+90 / +30 / -60 度),
* 好让两份实现产出的段在她那边量出同一个数:量不齐就是有一边错了 */
static uint32_t s_n; /* 当前节拍 = 毫秒 = 帧序号(1 kHz 下同值) */
static int16_t s_sig[WAVE_RING_CH]; /* 本拍 8 路原码 */
static uint8_t s_sig_on = 0u; /* 1 = 本拍有样本(模拟采样故障时不压入) */
static int32_t s_last_push;
static uint32_t s_trig_k; /* 最近一次触发那一拍的节拍号 */
static int16_t sig_round(double a_v) {
return (int16_t)((a_v >= 0.0) ? (a_v + 0.5) : (a_v - 0.5));
}
static int16_t sig_at(uint32_t a_k, uint8_t a_ch) {
double w = (2.0 * WAVE_PI * (double)CYC_HZ_TEST * (double)a_k) / (double)FS_HZ_TEST;
switch (a_ch) {
case 0u:
return sig_round(9261.0 * sin(w + DEG(90)));
case 3u:
return sig_round(4630.0 * sin(w + DEG(30)));
case 4u:
return sig_round(2000.0 * sin(w + DEG(-60)));
case 1u:
return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0);
case 2u:
return (int16_t)(((a_k / 40u) % 2u) != 0u ? 0 : 2048);
case 5u:
return (int16_t)((a_k * 13u) % 4096u);
case 6u:
return (int16_t)((a_k * 29u) % 4096u);
case 7u:
return (int16_t)(((a_k / 40u) % 2u) != 0u ? 2048 : 0);
default:
return 0;
}
}
static void sig_fill(void) {
uint8_t c;
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
s_sig[c] = sig_at(s_n, c);
}
}
/* 一拍:先压样再推进(接线道在 adcTask 里的真实顺序:adc_input_poll → push → tick) */
static int32_t tick_one(void) {
int32_t evt;
sig_fill();
if (s_sig_on != 0u) {
s_last_push = wave_ring_push(s_sig, (uint8_t)WAVE_RING_CH);
} else {
s_last_push = WAVE_ST_OK;
}
evt = wave_ring_tick(s_n, s_n);
s_n++;
return evt;
}
static void run_n(uint32_t a_cnt) {
uint32_t i;
for (i = 0u; i < a_cnt; i++) {
(void)tick_one();
}
}
/* 跑到封段,返回用掉的拍数(0 = 没封上段);封段那一拍的样本仍然入库 */
static uint32_t run_until_sealed(uint32_t a_max) {
uint32_t i;
for (i = 1u; i <= a_max; i++) {
if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) {
return i;
}
}
return 0u;
}
static void fresh(void) {
wave_ring_init();
s_n = 0u;
s_trig_k = 0u;
s_sig_on = 1u;
}
/* 触发 + 封段一条龙:预触发区先滚 a_pre 拍,返回 1 = 恰好第 160 拍封段 */
static int trigger_and_seal(uint8_t a_src, uint8_t a_elm, uint32_t a_pre) {
uint32_t used;
run_n(a_pre);
s_trig_k = s_n;
if (wave_ring_request(a_src, a_elm) != WAVE_ST_OK) {
return 0;
}
(void)tick_one(); /* 触发那一拍:样本进段内点 159(预触发窗最后一点) */
used = run_until_sealed(WAVE_RING_POST + 40u);
return (used == (uint32_t)WAVE_RING_POST) ? 1 : 0;
}
/* ===== 封存瞬间事实(模拟接线道在 EVT_SEALED 那一拍现取现填的内容) ===== */
static wave_seal_cal_t s_cal;
static void cal_init(void) {
uint8_t j;
memset(&s_cal, 0, sizeof(s_cal));
s_cal.eng = (uint8_t)WAVE_ENG_PIN;
/* flags_seq 的定义(设计说明 §6.2 偏移 23)= 逐路 VALID 压成的位图。产品构建下一路都没做
* 两点标定、ADC_CAL_F_VALID 未置位 → 这份合成本波的真值形态取全零。 */
s_cal.flags_seq = 0x00u;
s_cal.clip_mask = 0x00E6u; /* V0.1 恒 0xE6 = UB UC IB IC IN 五个语义位 */
s_cal.wall = 1764374400u;
for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) {
int tpa = ((j == 0u) || (j == 3u) || (j == 4u)) ? 1 : 0;
s_cal.ch[j].sem = j;
s_cal.ch[j].conv = (uint8_t)((tpa != 0) ? 0 : 1);
s_cal.ch[j].bits = (uint8_t)((tpa != 0) ? 16 : 12);
/* avail = adc_cal_flags() 低 8 位原值,如实透传不加工:V0.1 下三路 TPAFE = 0x00
* (未标定、也不在削顶表里)、五路片内 ADC = 0x02(VALID = 0 而 CLIPPED = 1)。
* VALID 与 CLIPPED 是两件独立事实,本层不许据此造「削顶必无效」这类不变式,
* 也不许为迁就任何一份解码脚本改数据(lead 裁定 2026-10-29)。同时 VALID + CLIPPED
* 的构造形态由 test/host/wave_store 那套与它的合成段覆盖。 */
s_cal.ch[j].avail = (uint8_t)((tpa != 0) ? 0x00 : 0x02);
s_cal.ch[j].k_q16 = (tpa != 0) ? 10000000 : 52800000;
s_cal.ch[j].b_codes = 0;
}
}
/* ===== 全链路搬运:封存区 → 打包游标 → 内存镜像 ===== */
static uint8_t s_buf[SEG_IMG_CAP];
static uint32_t s_len;
static wave_seg_meta_t s_m;
static int pack_seg(void) {
wave_pack_ctx_t ctx;
uint32_t off = 0u;
int rc;
s_len = 0u;
if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) {
return -1;
}
rc = wave_store_pack_start(&ctx, &s_m, wave_ring_read_cb, NULL);
if (rc != WAVE_ST_OK) {
return -2;
}
for (;;) {
uint32_t n = 0u;
if (off >= (uint32_t)sizeof(s_buf)) {
return -3;
}
rc = wave_store_pack_next(&ctx, &s_buf[off],
(uint32_t)sizeof(s_buf) - off, &n);
if (rc < 0) {
return -4;
}
off += n;
if (rc == WAVE_ST_DONE) {
break;
}
}
if (wave_store_pack_total(&ctx) != off) {
return -5;
}
if (wave_store_pack_offset(&ctx) != off) {
return -6;
}
s_len = off;
return 0;
}
/* 把已封存区某一列按 §6.2 的小端形态排进 p_dst(本测试自己排一遍,不信生产者的排法) */
static int col_to_bytes(uint8_t a_ch, uint8_t *p_dst, uint32_t a_cap) {
int16_t pts[WAVE_RING_PTS];
uint32_t j;
if (a_cap < ((uint32_t)WAVE_RING_PTS * 2u)) {
return 0;
}
/* read_ch 的返回值是「实际读出的点数」(到段尾会短读),不是 WAVE_ST_OK */
if (wave_ring_read_ch(a_ch, 0u, (uint16_t)WAVE_RING_PTS, pts) != (int32_t)WAVE_RING_PTS) {
return 0;
}
for (j = 0u; j < (uint32_t)WAVE_RING_PTS; j++) {
uint16_t u = (uint16_t)pts[j];
p_dst[j * 2u] = (uint8_t)(u & 0xFFu);
p_dst[(j * 2u) + 1u] = (uint8_t)(u >> 8);
}
return 1;
}
static int point_is(uint8_t a_ch, uint16_t a_pt, uint32_t a_k) {
int16_t v = 12345;
if (a_k > 0xFFFFFu) {
return 0; /* 负数节拍:这段不校验(冷启动前导点另有专门用例) */
}
if (wave_ring_read_ch(a_ch, a_pt, 1u, &v) != 1) {
return 0;
}
return (v == sig_at(a_k, a_ch)) ? 1 : 0;
}
static int count_head_zero(uint8_t a_ch, uint16_t a_cnt) {
int16_t pts[WAVE_RING_PTS];
uint16_t j;
uint16_t z = 0u;
if (wave_ring_read_ch(a_ch, 0u, a_cnt, pts) != (int32_t)a_cnt) {
return -1;
}
for (j = 0u; j < a_cnt; j++) {
if (pts[j] == 0) {
z++;
}
}
return (int)z;
}
int main(void) {
wave_ring_stat_t st;
uint8_t j;
printf("=== Wave Trig Tests:\n");
/* ---------- 用例:三条触发来路 ---------- */
printf("-- 用例:三条触发来路 --\n");
cal_init();
{
static const uint8_t s_src[3] = {
(uint8_t)WAVE_TRIG_MANUAL, (uint8_t)WAVE_TRIG_PROTECT, (uint8_t)WAVE_TRIG_BENCH
};
static const uint8_t s_elm[3] = { 0u, 5u, 2u };
int all_ok = 1;
int all_evt = 1;
int all_geo = 1;
uint8_t k;
for (k = 0u; k < 3u; k++) {
fresh();
if (trigger_and_seal(s_src[k], s_elm[k], 200u) == 0) {
all_evt = 0;
}
if (wave_ring_latch_meta(&s_cal) != WAVE_ST_OK) {
all_ok = 0;
}
(void)memset(&s_m, 0, sizeof(s_m));
if (wave_ring_sealed_meta(&s_m) != WAVE_ST_OK) {
all_ok = 0;
}
if ((s_m.trig_src != s_src[k]) || (s_m.trig_elm != s_elm[k])) {
all_geo = 0;
}
}
CHECK(all_evt != 0, "手动 / 保护 / bench 三条来路都在触发后第 160 拍封段");
CHECK(all_ok != 0, "三条来路都完成 latch 并取到段头快照");
CHECK(all_geo != 0, "trig_src 与 trig_elm 原样透传进段头(谁触的、哪个元件触的)");
}
{
int bad = 0;
fresh();
if (wave_ring_request(0u, 0u) != WAVE_ST_E_PARAM) {
bad++;
}
if (wave_ring_request((uint8_t)(WAVE_TRIG_BENCH + 1u), 0u) != WAVE_ST_E_PARAM) {
bad++;
}
if (wave_ring_request(200u, 0u) != WAVE_ST_E_PARAM) {
bad++;
}
CHECK(bad == 0, "触发源非法(0 / 4 / 200)一律拒,不挂号也不计数");
(void)wave_ring_get_stat(&st);
CHECK(st.ovf_cnt == 0u, "非法触发源不计 ovf(ovf 只统计被抑制的真请求)");
}
/* ---------- 用例:同一拍多次请求只录一段 ---------- */
printf("-- 用例:同一拍多次请求与 ovf 计数 --\n");
fresh();
run_n(50u);
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, 7u) == WAVE_ST_OK, "保护元件先挂号被受理");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "同一拍第二个请求只算溢出(不排队)");
(void)wave_ring_get_stat(&st);
CHECK(st.ovf_cnt == 1u, "被抑制的请求进 ovf 计数(§9 ovf=)");
(void)tick_one();
(void)wave_ring_get_stat(&st);
CHECK((st.state == (uint8_t)WAVE_RING_ST_FREEZE) && (st.trig_src == (uint8_t)WAVE_TRIG_PROTECT) &&
(st.trig_elm == 7u), "本段归属先挂号者(多个元件同时 picked 只录一段)");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 1u) == WAVE_ST_DONE, "FREEZE 期间新触发只加 ovf");
(void)wave_ring_get_stat(&st);
CHECK(st.ovf_cnt == 2u, "FREEZE 期间的请求也计 ovf");
CHECK(run_until_sealed(WAVE_RING_POST + 20u) == (uint32_t)WAVE_RING_POST, "仍然照原计划封段(第二路触发不改变段内容)");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "SEALED 期间新触发只加 ovf");
(void)wave_ring_get_stat(&st);
CHECK((st.ovf_cnt == 3u) && (st.err_cnt == 0u) && (st.seg_total == 0u), "被抑制的触发既不算成功也不算失败");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "抑制触发不改变状态(还等着 ACK)");
/* ---------- 用例:封存后区内容不可变(§5.1 唯一写者) ---------- */
printf("-- 用例:已封存区不可变与 drop 计数 --\n");
CHECK(wave_ring_sealed() == 0u, "刚封段还没 latch -> 仍不可读(防半份快照)");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "adcTask 在同一拍交棒封存瞬间事实");
CHECK(wave_ring_sealed() == 1u, "latch 之后才亮「已封存可读」");
{
static uint8_t s_snap0[WAVE_RING_PTS * 2u];
static uint8_t s_snap5[WAVE_RING_PTS * 2u];
static uint8_t s_again0[WAVE_RING_PTS * 2u];
static uint8_t s_again5[WAVE_RING_PTS * 2u];
int same = 0;
int drop_all = 0;
uint32_t k;
CHECK((col_to_bytes(0u, s_snap0, sizeof(s_snap0)) != 0) &&
(col_to_bytes(5u, s_snap5, sizeof(s_snap5)) != 0), "封存区两列读成功");
drop_all = 1;
for (k = 0u; k < 50u; k++) {
(void)tick_one();
if (s_last_push != WAVE_ST_DONE) {
drop_all = 0;
}
}
CHECK(drop_all != 0, "SEALED 期间每一拍压样都被丢(返回 DONE 而不是 OK)");
(void)wave_ring_get_stat(&st);
CHECK(st.drop_cnt == (50u * (uint32_t)WAVE_RING_CH), "drop 计数单位 = 点(一拍 8 点,50 拍 = 400)");
CHECK((col_to_bytes(0u, s_again0, sizeof(s_again0)) != 0) &&
(col_to_bytes(5u, s_again5, sizeof(s_again5)) != 0), "再读一次两列");
if ((memcmp(s_snap0, s_again0, sizeof(s_snap0)) == 0) &&
(memcmp(s_snap5, s_again5, sizeof(s_snap5)) == 0)) {
same = 1;
}
CHECK(same != 0, "50 拍压样之后已封存区逐字节不变(defaultTask 读期间不会读到半新半旧)");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "仍在 SEALED,等 ACK");
}
/* ---------- 用例:ACK 前不重录 + 冷却只在 ACK 后计时 ---------- */
printf("-- 用例:ACK 前不重录 / 冷却只在 ACK 后 --\n");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "ACK 前来新触发只加 ovf(lead 裁定第一条)");
CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "defaultTask 落盘成功后明确 ACK");
(void)wave_ring_get_stat(&st);
CHECK(st.state == (uint8_t)WAVE_RING_ST_COOL, "ACK 之后才转冷却(冻结持续到 ACK,不是按时间解冻)");
CHECK(st.cool_left == (uint16_t)WAVE_RING_COOL_MS, "冷却窗从 ACK 那一拍起算 500 拍");
CHECK((st.seg_total == 1u) && (st.seg_idx == 1u), "成功 ACK 才推进段计数与轮转号");
CHECK(st.err_cnt == 0u, "这条链路上没有失败");
CHECK(wave_ring_sealed() == 0u, "ACK 之后「已封存可读」立即熄灭(旧段不再对外)");
{
int no_seal = 1;
uint32_t k;
for (k = 0u; k < 499u; k++) {
if (tick_one() == (int32_t)WAVE_RING_EVT_SEALED) {
no_seal = 0;
}
}
(void)wave_ring_get_stat(&st);
CHECK((no_seal != 0) && (st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == 1u),
"冷却差 1 拍时仍在冷却、期间没有任何新段");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_DONE, "冷却期内新触发只加 ovf 不重录(lead 裁定第三条)");
(void)tick_one();
(void)wave_ring_get_stat(&st);
CHECK(st.state == (uint8_t)WAVE_RING_ST_IDLE, "冷却走完自动回 IDLE");
CHECK(wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) == WAVE_ST_OK, "回 IDLE 后新触发被受理(下一次故障有波可录)");
}
/* ---------- 用例:滞留超时自弃(绝不让环永久焊死) ---------- */
printf("-- 用例:SEALED 滞留超时自弃 --\n");
fresh();
cal_init();
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 2u, 200u) != 0, "造一段(故意不 latch,模拟接线道没交棒)");
run_n(WAVE_SEAL_TIMEOUT_MS - 2u);
(void)wave_ring_get_stat(&st);
CHECK((st.state == (uint8_t)WAVE_RING_ST_SEALED) && (st.seal_hold == (uint16_t)(WAVE_SEAL_TIMEOUT_MS - 2u)) &&
(st.err_cnt == 0u), "差 2 拍还没到超时线,段仍留着等 ACK");
(void)tick_one();
(void)tick_one();
(void)wave_ring_get_stat(&st);
CHECK((st.err_cnt == 1u) && (st.seg_total == 0u), "滞留满 WAVE_SEAL_TIMEOUT_MS 自弃并计 err(lead 裁定第二条)");
CHECK((st.state == (uint8_t)WAVE_RING_ST_COOL) && (st.cool_left == (uint16_t)WAVE_RING_COOL_MS),
"自弃也照样起冷却解冻(不是停在 SEALED)");
CHECK(wave_ring_sealed() == 0u, "自弃后对外不可读(绝不给落盘道一份已作废的段)");
CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "自弃后取段头快照被拒");
CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "自弃后补 ACK 被拒(不吃轮转号、不加 seg_total)");
(void)wave_ring_get_stat(&st);
CHECK((st.seg_idx == 0u) && (st.seg_total == 0u), "自弃的段不占段号(下一次故障仍写最早那个号)");
run_n(WAVE_RING_COOL_MS);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "自弃之后环照常被救回来(回 IDLE)");
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 100u) != 0, "自弃后仍能录下一段(录波不会永久停走)");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段照样能交棒");
CHECK(wave_ring_sealed() == 1u, "新段就绪");
/* ---------- 用例:ACK 失败留同号重写 ---------- */
printf("-- 用例:落盘失败不推进轮转号 --\n");
(void)wave_ring_get_stat(&st);
j = st.seg_idx;
CHECK(wave_ring_write_done(-5) == WAVE_ST_OK, "失败也照样 ACK(短写 / 打开失败都要解冻)");
(void)wave_ring_get_stat(&st);
CHECK((st.err_cnt == 2u) && (st.seg_idx == j) && (st.seg_total == 0u),
"失败只计 err,段号与段计数都不动(不留半截文件骗过 PC 脚本)");
CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "重复 ACK 被拒(防把上一段的 ACK 打到新段)");
run_n(WAVE_RING_COOL_MS);
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_IDLE, "失败段冷却后回 IDLE");
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_BENCH, 3u, 60u) != 0, "再来一段(预触发区只滚了 60 拍)");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "新段交棒");
(void)wave_ring_get_stat(&st);
CHECK(st.seg_idx == j, "新段沿用同一个段号");
{
char name[WAVE_ST_PATH_MAX];
CHECK(wave_store_seg_path(name, (uint32_t)sizeof(name), st.seg_idx) == 16, "按 ring 的段号取名成功");
CHECK(strcmp(name, "/wave/WAVE00.BIN") == 0, "同号 = 同一个文件名(重写覆盖,不吃掉一个号)");
}
CHECK(wave_ring_write_done(0) == WAVE_ST_OK, "这次写成功");
(void)wave_ring_get_stat(&st);
CHECK((st.seg_idx == 1u) && (st.seg_total == 1u) && (st.err_cnt == 2u), "成功才推进,err 保留历史");
/* ---------- 用例:全链路字节(adcTask 封存 -> latch -> 落盘道搬运) ---------- */
printf("-- 用例:全链路封段到字节 --\n");
fresh();
cal_init();
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 12u, 200u) != 0, "全链路:先滚 200 拍再触发封段");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "全链路:交棒封存瞬间事实");
CHECK(pack_seg() == 0, "全链路:打包游标跑到 DONE(数据源就是 wave_ring 的读口)");
CHECK(s_len == (uint32_t)WAVE_ST_SEG_SZ_CUR, "整段字节数 = 5250(§6.3 等式)");
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "自产段过整段自检(两个 CRC 都对)");
CHECK(wave_store_seg_check(s_buf, s_len, &s_m) == WAVE_ST_OK, "自检并回填段头");
CHECK((s_buf[0] == (uint8_t)WAVE_ST_MAGIC0) && (s_buf[1] == (uint8_t)WAVE_ST_MAGIC1) &&
(s_buf[2] == (uint8_t)WAVE_ST_VER), "魔数第三字节就是版本号");
CHECK((s_buf[WAVE_ST_OFF_FS_HZ] == (uint8_t)(FS_HZ_TEST & 0xFFu)) &&
(s_buf[WAVE_ST_OFF_FS_HZ + 1u] == (uint8_t)(FS_HZ_TEST >> 8)), "采样率 1000 Hz 写进头里(是数据不是编译期假设)");
CHECK(s_buf[WAVE_ST_OFF_PTS_CYC] == 20u, "每周波 20 点写进头里");
CHECK(s_buf[WAVE_ST_OFF_CH_CNT] == (uint8_t)WAVE_ST_CH_NUM, "通道数 8");
CHECK(s_buf[WAVE_ST_OFF_PRE_PTS] == (uint8_t)(WAVE_RING_PRE & 0xFFu), "预触发 160 点");
CHECK(s_buf[WAVE_ST_OFF_TRIG_SRC] == (uint8_t)WAVE_TRIG_PROTECT, "触发源 = 保护");
CHECK(s_buf[WAVE_ST_OFF_TRIG_ELM] == 12u, "元件序号 = 12");
CHECK(s_buf[WAVE_ST_OFF_ENG] == (uint8_t)WAVE_ENG_PIN, "折算档位 = 引脚域");
CHECK(s_buf[WAVE_ST_OFF_FLAGS] == 0x00u, "flags_seq = 未标定态的逐路有效位图原值(全零)");
CHECK((s_buf[WAVE_ST_OFF_CLIP] == (uint8_t)(0xE6u & 0xFFu)) &&
(s_buf[WAVE_ST_OFF_CLIP + 1u] == (uint8_t)(0xE6u >> 8)), "clip_mask = 0xE6(V0.1 恒值)");
{
wave_seg_meta_t back;
if (wave_store_head_parse(s_buf, (uint32_t)sizeof(s_buf), &back) == WAVE_ST_OK) {
CHECK(back.t0_ms == s_trig_k, "段首相对毫秒 = 触发那一拍");
CHECK(back.frame_seq == s_trig_k, "段首帧序号 = 触发那一拍(PC 拿它对 frames)");
CHECK(back.wall == s_cal.wall, "wall 取封存瞬间的对时秒(未对时应为 0)");
CHECK(back.pts_per_ch == (uint16_t)WAVE_RING_PTS, "每路点数 320");
} else {
CHECK(0, "段头解析成功(回填比对的前提)");
CHECK(0, "段首相对毫秒 = 触发那一拍");
CHECK(0, "段首帧序号 = 触发那一拍(PC 拿它对 frames)");
CHECK(0, "wall 取封存瞬间的对时秒(未对时应为 0)");
CHECK(0, "每路点数 320");
}
}
{
int ok_tbl = 1;
uint32_t pos;
for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) {
pos = WAVE_ST_OFF_TBL + ((uint32_t)WAVE_ST_CH_BYTES * j);
if ((s_buf[pos + WAVE_ST_CH_OFF_SEM] != j) ||
(s_buf[pos + WAVE_ST_CH_OFF_CONV] != s_cal.ch[j].conv) ||
(s_buf[pos + WAVE_ST_CH_OFF_BITS] != s_cal.ch[j].bits) ||
(s_buf[pos + WAVE_ST_CH_OFF_AVAIL] != s_cal.ch[j].avail)) {
ok_tbl = 0;
}
if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_KQ16]) != (uint32_t)s_cal.ch[j].k_q16) {
ok_tbl = 0;
}
if (wave_st_get32(&s_buf[pos + WAVE_ST_CH_OFF_BCODE]) != (uint32_t)s_cal.ch[j].b_codes) {
ok_tbl = 0;
}
}
CHECK(ok_tbl != 0, "通道表 8 项:sem = 下标 + conv/bits/avail/k_q16/b_codes 全部落进字节");
}
{
uint16_t hcrc = wave_store_crc(s_buf, WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM));
CHECK((s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM)] == (uint8_t)(hcrc & 0xFFu)) &&
(s_buf[WAVE_ST_HEAD_CRC_POS((uint8_t)WAVE_ST_CH_NUM) + 1u] == (uint8_t)(hcrc >> 8)),
"头 CRC 覆盖 [0, 30+12*ch) 且低字节在前");
}
{
uint16_t dcrc = wave_store_crc(&s_buf[WAVE_ST_HEAD_SZ_CUR],
WAVE_ST_DATA_BYTES((uint8_t)WAVE_ST_CH_NUM, (uint16_t)WAVE_ST_PTS_DEF));
CHECK((s_buf[s_len - 2u] == (uint8_t)(dcrc & 0xFFu)) && (s_buf[s_len - 1u] == (uint8_t)(dcrc >> 8)),
"数据 CRC 只覆盖数据区、贴在文件尾(头坏了不会被数据 CRC 掩盖)");
}
{
static uint8_t s_col[WAVE_RING_PTS * 2u];
int all_col = 1;
for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) {
if (col_to_bytes(j, s_col, sizeof(s_col)) == 0) {
all_col = 0;
} else if (memcmp(&s_buf[WAVE_ST_HEAD_SZ_CUR + ((uint32_t)WAVE_RING_PTS * 2u * j)],
s_col, (size_t)((uint32_t)WAVE_RING_PTS * 2u)) != 0) {
all_col = 0;
}
}
CHECK(all_col != 0,
"数据区每一列逐字节 == wave_ring_read_ch() 的已封存区内容(§5.1 只读已封存区的机器证明)");
}
{
int ok_trig = 1;
int ok_next = 1;
int ok_old = 1;
for (j = 0u; j < (uint8_t)WAVE_ST_CH_NUM; j++) {
if (point_is(j, (uint16_t)(WAVE_RING_PRE - 1u), s_trig_k) == 0) {
ok_trig = 0;
}
if (point_is(j, (uint16_t)WAVE_RING_PRE, s_trig_k + 1u) == 0) {
ok_next = 0;
}
if (point_is(j, 0u, s_trig_k - (WAVE_RING_PRE - 1u)) == 0) {
ok_old = 0;
}
}
CHECK(ok_trig != 0, "点序(lead 裁定 a):触发那一拍的样本落在段内点 159");
CHECK(ok_next != 0, "点序:点 160 = 触发后第一拍(两半各 160 点不重叠)");
CHECK(ok_old != 0, "点序:点 0 = 触发前第 159 拍(预触发窗最老一点)");
}
{
uint8_t save = s_buf[WAVE_ST_HEAD_SZ_CUR + 100u];
s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = (uint8_t)(save ^ 0x5Au);
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_E_CRC, "数据区改一字节 -> 整段作废(PC 侧必须拒绘)");
s_buf[WAVE_ST_HEAD_SZ_CUR + 100u] = save;
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "改回来又能过(判废只认 CRC,不认运气)");
}
/* ---------- 用例:未 latch 时落盘道必须当不可用 ---------- */
printf("-- 用例:未 latch 不许落盘 --\n");
fresh();
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "封段(不 latch)");
CHECK(wave_ring_sealed() == 0u, "sealed() = SEALED 且已 latch 的合并判据:未交棒就是不可用");
CHECK(wave_ring_sealed_meta(&s_m) == WAVE_ST_E_STATE, "取不到段头快照 -> 落盘道无从开文件");
CHECK(wave_ring_write_done(0) == WAVE_ST_E_STATE, "没交棒就 ACK 被拒(不吃轮转号、不假加 seg_total)");
(void)wave_ring_get_stat(&st);
CHECK((st.seg_idx == 0u) && (st.seg_total == 0u) && (st.err_cnt == 0u), "被拒的 ACK 一个计数都不动");
CHECK(wave_ring_state() == (uint8_t)WAVE_RING_ST_SEALED, "段仍留在 SEALED 等交棒或超时自弃");
CHECK(wave_ring_latch_meta(NULL) == WAVE_ST_E_PARAM, "latch 空指针拒");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "补交棒成功");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_E_STATE, "重复交棒被拒(封存瞬间事实不许被后一拍覆盖)");
CHECK(pack_seg() == 0, "交棒之后全链路打包成功");
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "交棒后的段过自检");
/* ---------- 用例:冷启动第一段(环未填满) ---------- */
printf("-- 用例:环未填满就触发 --\n");
fresh();
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_PROTECT, 1u, 20u) != 0, "上电只滚 20 拍就触发(预触发区大部分是空的)");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒");
CHECK(count_head_zero(0u, 139u) == 139, "点 0 到 138 全为 0(没采过的点不填假数据)");
CHECK(point_is(0u, 139u, 0u) != 0, "点 139 = 上电第一拍(第一个真样本)");
CHECK(point_is(7u, 159u, s_trig_k) != 0, "点 159 仍是触发那一拍(未填满时段序口径不变)");
CHECK(point_is(0u, 319u, s_trig_k + (uint32_t)WAVE_RING_POST) != 0, "点 319 = 段尾(最后一条后触发样本)");
CHECK(pack_seg() == 0, "冷启动段也能打包");
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "冷启动段过自检(回放页画出来的是前导平线加真波,不是伪证据)");
/* ---------- 用例:互验产物(不参与判定) ---------- */
printf("-- 用例:互验产物 --\n");
fresh();
cal_init();
CHECK(trigger_and_seal((uint8_t)WAVE_TRIG_MANUAL, 0u, 200u) != 0, "再造一段连续的 50 Hz 正弦段");
CHECK(wave_ring_latch_meta(&s_cal) == WAVE_ST_OK, "交棒");
if (pack_seg() == 0) {
const char *tmp = getenv("TEMP");
CHECK(wave_store_seg_check(s_buf, s_len, NULL) == WAVE_ST_OK, "互验段过自检");
if (tmp != NULL) {
char path[512];
FILE *f;
strcpy(path, tmp);
strcat(path, "\\wave_ring_synth8.bin");
f = fopen(path, "wb");
if (f != NULL) {
size_t w = fwrite(s_buf, 1u, s_len, f);
(void)fclose(f);
CHECK(w == (size_t)s_len, "互验产物写入字节数 == 段长");
/* 形态在日志里说清楚:本套给的是本波(V0.1 未标定)真值形态 */
printf("[INFO] 互验产物(本波真值形态:avail 0x00 / 0x02,逐路有效位图全零;"
"信号形态 = 三路 50 Hz 正弦(UA / UN / IA)+ 三路方波(UB / UC / IN)+ 两路满幅斜坡(IB / IC),"
"供 test/host/wave_read.py dump/shape/fft 只读校验):%s\n",
path);
} else {
printf("[INFO] 打不开临时目录文件,跳过互验产物\n");
}
} else {
printf("[INFO] 环境无 TEMP,跳过互验产物\n");
}
} else {
printf("[INFO] 互验段打包失败,跳过产物\n");
}
printf("\n=== Wave Trig Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}