Files
STM32F4-Base/test/host/wave_trig/wave_trig_test.c
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

678 lines
31 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:录波触发与封存链路(触发到封段、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;
}