Files
STM32F4-Base/test/host/prot_set/prot_set_test.c
T
wandering1 0264b49489 feat(algo): B8 频率测量层 + 过频/低频保护(L3-5)
新增 App/algo/freq.c/h:由 L2 基波相量导出系统频率。相邻两拍共轭
乘积 P=Z(m)·conj(Z(m-1)) 取角 = 每拍相位增量(免显式解缠绕),再按
自适应窗长 K_eff=round(8·Fs/(2·f_hat))(45~55 Hz 段 73~89 拍)做滑动
平均。定长窗受 20 点矩形窗负序泄漏纹波限制(实测扫频最坏 0.317 Hz;
60 拍 Kay 复数和 0.197 Hz,交叉项和角不按 1/K^2 衰减),自适应窗使
窗长恰为纹波周期 fs/(2f) 整数倍 -> 最坏 0.0101 Hz。推导与两轮弯路
写在 freq.c 文件头。

假值钳制带取 35~65 Hz,刻意宽于系统合理带 40.5~60.5:单拍相位差本身
带 ±5 Hz 泄漏纹波,贴边钳制会非对称丢弃纹波半周 -> 均值偏置(实测
45 Hz 点 +0.16 Hz)。系统合理带由 OF/UF 定值范围在消费侧把关。
幅值门 FREQ_AMP_MIN=50 码(拟定,待 L1-5 标定复核):低于门判无有效
波形,停更新、valid 清零、读数保持 -> 浮空/PT 断线不发散。源优先级
UA(C02)->IA(C01),按名前缀解析、每拍动态回退,切换即废相位基准。

prot_set:尾追加 8 项(PS_ID_COUNT 32->40,编号不回收)
OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF。OF/UF 逐项通讯
地址不在本仓库 -> 全部 comm_addr=0 本地项(104 C_SE 不可寻址,待对照
参考表补)。频率返回系数独立(0.99/1.01),勿与电气量 0.95/1.05 共用。
bad_mask 随项数扩为 uint64_t。PROT_SET_BLOB_VER 1->2。

prot_set_store:换代与损坏分流。load 返回 2 = 版本/项数换代(本文件
打 INFO "prot set blob version change, defaults in use"),-1 = 真损坏
(defaultTask 按 CORRUPT 报 ERROR + 自检留痕)。B7 旧档(ver=1/32 项)
升级首启属预期路径,不得报成损坏。语义分流收在属主文件内,
default_task.c 不动。

prot_elm:实例表加 src 字段(0=phasor rms,1=freq),PE_OF/PE_UF 尾
追加(PE_COUNT 7->9)。freq 无有效读数按方向填"必不启动"值(过量 0 /
欠量 1e30 -> 滞回比较恒假 -> 元件复位),不整拍跳过——频率源缺失不得
饿死电流/电压类元件(B5 行为不回归)。OF/UF 暂未映射独立 104 遥信点,
动作只经 YX15 保护总聚合可见(送检点表 = B10)。

板端接线:adcTask 在 phasor_run() 之后同拍调 freq_run();心跳/首拍日志
追加 freq(<tag>): f=%ld valid=%u (x0.01)(MicroLIB 无 %f)。app_main
内核前 freq_init()。

主机单测:freq 46 用例(J1 扫频 45~55 Hz <=0.02 Hz 实测最坏 0.0101;
J2 噪声 1% <=0.10 实测 0.0160;J3 启动收敛 <=40 ms 实测 20;J4 阶跃
50->51 <=120 ms 实测 98;J5 浮空 valid=0 不发散;J6 跳变 60 ms 内回
±0.05;另直流 8000 码 / 3-5 次谐波 20%/10% / UA->IA 回退)。
prot_set 90(+TC-PS-011 OF/UF 行 + 64 位 bad_mask bit39 + 旧档拒绝)、
prot_elm 47(+OF/UF 滞回与方向隔离 + run() 测频接线)。回归:phasor
57、iec104 94 全绿。

J4=120 ms 是"精度优先于响应"的显式取舍(Lead 批准):4 周波窗换
0.317->0.0101 Hz,测频响应 98 ms << OF/UF 定时限默认 1000 ms(占
总动作时间 <10%),暂态另有 J6 <=60 ms 兜底。

注册:freq.c 入 uvprojx algo 组 + check_registration.py SIM_EXEMPT
(纯算法、模拟器无消费路径,照 prot_set.c 先例)。

@build 0 错误 0 警告(Code=207468 RO=18304 RW=1068 ZI=153084)。
RAM:RW_IRAM1 96096->96728 B(+632 B,73.3%->73.8%)、RW_CCM 不变
57376 B(87.9%)、Flash 224596->226840 B(+2244 B,21.4%->21.6%);
台账随合并由 Lead 回填。stack_report:adcTask 静态最坏链 1320 B /
余量 1240 B(48%)改前后完全不变(freq_run 帧未进最坏链,无需抬栈);
protectTask 链 872 B / 余 1176 B(57%)同样不变。

gate: only mem-budget/mem-numbers pending lead ledger backfill
--------------------------------------------------------------
门禁实跑(pre-commit run --all-files,Lead 批准 --no-verify 提交):
28 Passed / 2 Failed,失败仅 mem-numbers 与 mem-budget 两条——均为
docs/内存管理 受管快照与本 .map 不一致(台账由 Lead 每次合并后
--write 回填,避免多分支同改受管表冲突)。其余含 doc-links /
coding-standard / no-mojibake / cppcheck / registration-check /
13 条主机单测钩子全 Passed(gitlint 属 commit-msg 阶段,不在该次
--all-files 内)。ci_local L0.5 全绿(freq 46/46 等)。
字节卫生自查(--no-verify 跳过卫生钩子,故手工核):17 个暂存文件
全部 BOM=无 / CR=0(纯 LF)/ U+FFFD=0;自研 .c/.h 去注释后字符串
字面量 100% ASCII(唯一非 ASCII 字面量在 test/host/iec104/iec104_test.c
的既有用例名,非本次改动行;test/ 不在 coding-standard 钩子范围)。
2026-10-12 19:16:37 +08:00

342 lines
16 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.
/*
* 模块名称:保护定值模型(P1)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/prot_set.c 做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入)。纯逻辑,无需桩。
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §5.2 / §3.4 L5-1):
* TC-PS-001 init:默认值装载、越界位图 0、selfcheck 通过、未闭锁
* TC-PS-002 set:范围内写入生效;越界拒写且原值不动
* TC-PS-003 校验和封存语义:set 后未 seal → selfcheck 失败并闭锁;
* seal → 通过并解锁(L5-1 核心闭环)
* TC-PS-004 load_raw:合法整表装载并封存;含越界项 → 整表不装载 +
* bad_mask 指认越界项(定值一致性:宁可全旧不可半新)
* TC-PS-005 Q10 往返精度(±1 LSB)
* TC-PS-006 元数据表自检:min<=max、投退项 0..1、非零通讯地址不重复
* TC-PS-007 越界编号守卫(get/set)
* TC-PS-008 全局软压板 PS_GLOBAL_EN(B7):默认投入、set_sealed 原子写
* 不破坏 selfcheck、脏标志三件套(peek/take/mark)
* TC-PS-009 持久化 blob 往返:serialize → init 清场 → parse 恢复
* (位精确 + 封存自洽 + parse 不置脏)
* TC-PS-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/
* 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动
* TC-PS-011 OF/UF 定值行(B8):40 项 / blob 版本 2 / 本地项地址 0 /
* 默认值与范围 / 返回系数独立性 / 64 位 bad_mask(bit 39)/
* B7 旧档(32 项)换代拒绝
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11;2026-10-12 扩 OF/UF(B8)
*/
#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 "prot_set.c"
/* TC-PS-001 */
static void tc_init(void) {
prot_set_init();
CHECK(prot_set_bad_mask() == 0u, "TC-PS-001 init: no bad items");
CHECK(prot_set_selfcheck() == 0, "TC-PS-001 init: selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PS-001 init: not locked");
CHECK(prot_set_get_f(PS_OC1_CUR) > 9.99f && prot_set_get_f(PS_OC1_CUR) < 10.01f,
"TC-PS-001 default OC1 current = 10.0 A");
CHECK(prot_set_get_ctl(PS_OC3_TRIP) == 0u,
"TC-PS-001 default OC3 = alarm (trip off)");
}
/* TC-PS-002 */
static void tc_set_range(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(7.5)) == 0,
"TC-PS-002 in-range set accepted");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PS-002 value readable after set");
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(101.0)) == -1,
"TC-PS-002 over-max set rejected");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PS-002 rejected set leaves value unchanged");
CHECK(prot_set_set(PS_OC1_EN, PROT_SET_Q10(2)) == -1,
"TC-PS-002 ctl item rejects value 2");
}
/* TC-PS-003 */
static void tc_seal_selfcheck(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC1_CUR, PROT_SET_Q10(8.0)) == 0, "TC-PS-003 set ok");
CHECK(prot_set_selfcheck() == -1,
"TC-PS-003 set-without-seal => selfcheck FAIL (tamper detect)");
CHECK(prot_set_locked() == 1u, "TC-PS-003 selfcheck failure latches lock");
prot_set_seal();
CHECK(prot_set_selfcheck() == 0, "TC-PS-003 seal => selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PS-003 seal clears lock");
}
/* TC-PS-004 */
static void tc_load_raw(void) {
int32_t tbl[PS_ID_COUNT];
uint32_t i;
prot_set_init();
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
tbl[i] = prot_set_get((prot_set_id_t)i);
}
tbl[PS_OL_CUR] = PROT_SET_Q10(1.5);
CHECK(prot_set_load_raw(tbl) == 0, "TC-PS-004 valid table loads");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PS-004 loaded value visible");
CHECK(prot_set_selfcheck() == 0, "TC-PS-004 load_raw auto-seals");
tbl[PS_OC3_DLY] = PROT_SET_Q10(60001.0); /* 越上限 */
tbl[PS_OL_CUR] = PROT_SET_Q10(2.5); /* 合法改动 */
CHECK(prot_set_load_raw(tbl) == -1, "TC-PS-004 bad table rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_OC3_DLY),
"TC-PS-004 bad_mask points at the offending item");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PS-004 rejected load keeps OLD table (all-or-nothing)");
CHECK(prot_set_load_raw(0) == -1, "TC-PS-004 NULL table rejected");
}
/* TC-PS-005 */
static void tc_q10_roundtrip(void) {
float v = 12.345f;
int32_t q;
prot_set_init();
q = PROT_SET_Q10(v);
CHECK(prot_set_set(PS_OC1_CUR, q) == 0, "TC-PS-005 set ok");
{
float back = prot_set_get_f(PS_OC1_CUR);
float err = back - v;
if (err < 0.0f) { err = -err; }
CHECK(err < 0.001f, "TC-PS-005 Q10 roundtrip error < 1 LSB");
}
}
/* TC-PS-006 */
static void tc_meta_sanity(void) {
uint32_t i, j;
int ok_range = 1, ok_ctl = 1, ok_addr = 1;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
const prot_set_meta_t *m = &g_prot_set_meta[i];
if (m->min > m->max) { ok_range = 0; }
if ((m->def < m->min) || (m->def > m->max)) { ok_range = 0; }
if ((m->is_ctl != 0u) &&
((m->min != 0) || (m->max != PROT_SET_Q10(1)))) { ok_ctl = 0; }
if (m->comm_addr == 0u) { continue; }
for (j = i + 1u; j < (uint32_t)PS_ID_COUNT; j++) {
if (g_prot_set_meta[j].comm_addr == m->comm_addr) { ok_addr = 0; }
}
}
CHECK(ok_range, "TC-PS-006 meta: min<=def<=max for all items");
CHECK(ok_ctl, "TC-PS-006 meta: ctl items are 0..1");
CHECK(ok_addr, "TC-PS-006 meta: comm addresses unique");
CHECK((uint32_t)PS_ID_COUNT <= 64u, "TC-PS-006 item count fits 64-bit bad_mask (B8: 40)");
}
/* TC-PS-007 */
static void tc_id_guard(void) {
prot_set_init();
CHECK(prot_set_get(PS_ID_COUNT) == 0, "TC-PS-007 get out-of-range id -> 0");
CHECK(prot_set_set(PS_ID_COUNT, 0) == -1, "TC-PS-007 set out-of-range id -> -1");
CHECK(prot_set_set_sealed(PS_ID_COUNT, 0) == -1, "TC-PS-007 set_sealed id guard");
}
/* TC-PS-008(B7:全局软压板 + 原子写 + 脏标志) */
static void tc_global_plate_dirty(void) {
prot_set_init();
CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 init not dirty");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-PS-008 global plate default IN");
CHECK(prot_set_set(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 plate rejects 2");
CHECK(prot_set_set_sealed(PS_GLOBAL_EN, 0) == 0, "TC-PS-008 plate OUT via set_sealed");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 plate OUT readable");
CHECK(prot_set_selfcheck() == 0, "TC-PS-008 set_sealed leaves selfcheck PASS (no unsealed window)");
CHECK(prot_set_locked() == 0u, "TC-PS-008 set_sealed no lock");
CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 set_sealed marks dirty");
CHECK(prot_set_dirty_take() == 1u, "TC-PS-008 dirty take consumes");
CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 dirty cleared after take");
prot_set_dirty_mark();
CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 dirty mark re-arms (write-fail retry)");
CHECK(prot_set_set_sealed(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 set_sealed range check");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 rejected set_sealed keeps value");
}
/* TC-PS-009(B7:blob 往返) */
static void tc_store_roundtrip(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
int32_t cur;
prot_set_init();
(void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(12.5));
(void)prot_set_set_sealed(PS_GLOBAL_EN, 0);
(void)prot_set_dirty_take();
cur = prot_set_get(PS_OC1_CUR);
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-009 serialize ok");
CHECK(len == (uint32_t)PROT_SET_BLOB_SZ, "TC-PS-009 blob exact size");
prot_set_init();
CHECK(prot_set_get(PS_OC1_CUR) != cur, "TC-PS-009 init wiped to defaults");
CHECK(prot_set_store_parse(blob, len) == 0, "TC-PS-009 parse ok");
CHECK(prot_set_get(PS_OC1_CUR) == cur, "TC-PS-009 value round-trip bit-exact");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-009 plate state round-trip");
CHECK(prot_set_selfcheck() == 0, "TC-PS-009 parsed table sealed");
CHECK(prot_set_dirty_peek() == 0u, "TC-PS-009 parse does not mark dirty");
}
/* 伪造辅助:按 blob 值区重算尾部 sum/sumn(构造"自洽但越界"的对抗样本) */
static void s_forge_tail(uint8_t *b, uint32_t len) {
uint32_t sum = 0u;
uint32_t inv;
uint32_t i;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
sum += s_get32(b + PROT_SET_BLOB_HDR_SZ + 4u * i);
}
inv = ~sum;
s_put32(b + len - 8u, sum);
s_put32(b + len - 4u, inv);
}
/* TC-PS-010(B7:损坏 blob 整表拒绝,all-or-nothing) */
static void tc_store_corrupt(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
int32_t keep;
prot_set_init();
(void)prot_set_set_sealed(PS_OC2_CUR, PROT_SET_Q10(6.0));
keep = prot_set_get(PS_OC2_CUR);
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-010 serialize ok");
blob[PROT_SET_BLOB_HDR_SZ] ^= 0x01u; /* 值区一位翻转:sum 对不符 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 flipped value byte rejected (sum pair)");
CHECK(prot_set_get(PS_OC2_CUR) == keep, "TC-PS-010 reject leaves values untouched");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
blob[0] ^= 0xFFu; /* magic 破坏 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 bad magic rejected");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
CHECK(prot_set_store_parse(blob, len - 4u) == -1, "TC-PS-010 truncated blob rejected");
CHECK(prot_set_store_parse(blob, len + 4u) == -1, "TC-PS-010 oversized blob rejected");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
blob[5] ^= 0x02u; /* version 高字节:1 -> 0x200 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 version mismatch rejected");
/* 对抗样本:越限定值 + 重算 sum 对(校验全过、范围必拦) */
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_OC1_CUR,
(uint32_t)(g_prot_set_meta[PS_OC1_CUR].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 forged consistent out-of-range rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item");
CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PS-010 defaults still intact");
CHECK(prot_set_selfcheck() == 0, "TC-PS-010 active table still self-consistent after all rejects");
}
/* TC-PS-011(B8:OF/UF 定值行 + 40 项 + 64 位 bad_mask + 换代拒绝) */
static void tc_of_uf_meta(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
prot_set_init();
/* 结构:40 项、blob 版本 2、尺寸随项数自适应 */
CHECK((uint32_t)PS_ID_COUNT == 40u, "TC-PS-011 PS_ID_COUNT == 40 (32 + 8 OF/UF)");
CHECK(PROT_SET_BLOB_VER == 2u, "TC-PS-011 blob version bumped to 2 (B8)");
CHECK((uint32_t)PROT_SET_BLOB_SZ == (PROT_SET_BLOB_HDR_SZ + 4u * 40u + 8u),
"TC-PS-011 blob size = hdr + 4*40 + tail");
/* 地址:参考逐项地址不在本仓库 -> 8 项全部本地项(comm_addr=0,104 不可寻址) */
CHECK((g_prot_set_meta[PS_OF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_OF].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_UF].comm_addr == 0u),
"TC-PS-011 OF/UF items all local (comm_addr=0, pending reference table)");
/* 投退 = ctl 项(0/1),其余 = 模拟量 Q10 */
CHECK((g_prot_set_meta[PS_OF_EN].is_ctl == 1u) && (g_prot_set_meta[PS_UF_EN].is_ctl == 1u)
&& (g_prot_set_meta[PS_OF_HZ].is_ctl == 0u) && (g_prot_set_meta[PS_RET_UF].is_ctl == 0u),
"TC-PS-011 EN items are ctl, HZ/DLY/RET are Q10 analog");
/* 出厂默认(拟定值)与范围 */
CHECK(prot_set_get(PS_OF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 OF EN default 1");
CHECK(prot_set_get(PS_UF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 UF EN default 1");
CHECK(prot_set_get(PS_OF_HZ) == PROT_SET_Q10(51.0), "TC-PS-011 OF default 51.0 Hz");
CHECK(prot_set_get(PS_UF_HZ) == PROT_SET_Q10(49.0), "TC-PS-011 UF default 49.0 Hz");
CHECK(prot_set_get(PS_OF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 OF default 1000 ms");
CHECK(prot_set_get(PS_UF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 UF default 1000 ms");
CHECK(prot_set_get(PS_RET_OF) == PROT_SET_Q10(0.99), "TC-PS-011 RET_OF default 0.99");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-011 RET_UF default 1.01");
CHECK((g_prot_set_meta[PS_OF_HZ].min == PROT_SET_Q10(50.0))
&& (g_prot_set_meta[PS_OF_HZ].max == PROT_SET_Q10(55.0)),
"TC-PS-011 OF range 50.0~55.0 Hz");
CHECK((g_prot_set_meta[PS_UF_HZ].min == PROT_SET_Q10(45.0))
&& (g_prot_set_meta[PS_UF_HZ].max == PROT_SET_Q10(50.0)),
"TC-PS-011 UF range 45.0~50.0 Hz");
/* 频率返回系数与电气量系数独立:返回**门槛** = 定值×系数(Q10 乘积
* /1024)——OF 门槛 < 动作门槛、UF 门槛 > 动作门槛(方向相反) */
CHECK(((int64_t)prot_set_get(PS_OF_HZ) * (int64_t)prot_set_get(PS_RET_OF)) / 1024
< (int64_t)prot_set_get(PS_OF_HZ),
"TC-PS-011 OF return threshold (set x 0.99) below pickup");
CHECK(((int64_t)prot_set_get(PS_UF_HZ) * (int64_t)prot_set_get(PS_RET_UF)) / 1024
> (int64_t)prot_set_get(PS_UF_HZ),
"TC-PS-011 UF return threshold (set x 1.01) above pickup");
/* 投退可经 set_sealed 投/退(YK 口径同 GLOBAL_EN;Q10(2)=2048 越界拒) */
CHECK(prot_set_set_sealed(PS_OF_EN, 0) == 0, "TC-PS-011 OF EN set_sealed OK");
CHECK(prot_set_get_ctl(PS_OF_EN) == 0u, "TC-PS-011 OF EN readable after seal");
CHECK(prot_set_set_sealed(PS_OF_EN, PROT_SET_Q10(2)) == -1, "TC-PS-011 OF EN rejects Q10(2)");
CHECK(prot_set_selfcheck() == 0, "TC-PS-011 no lock after OF/UF seal ops");
/* 64 位 bad_mask:末项(bit 39 > 31)越界可指认 */
(void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(11.0));
(void)prot_set_dirty_take();
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-011 serialize ok");
s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_RET_UF,
(uint32_t)(g_prot_set_meta[PS_RET_UF].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-011 out-of-range RET_UF rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_RET_UF),
"TC-PS-011 bad_mask bit 39 addressable (64-bit mask, B8)");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-011 default RET_UF intact");
/* 换代语义:B7 旧档(ver=1 / 32 项 = 144 B)在新固件必被拒 -> 出厂默认 */
CHECK(prot_set_store_parse(blob, 8u + 4u * 32u + 8u) == -1,
"TC-PS-011 legacy 32-item blob length rejected");
CHECK(prot_set_selfcheck() == 0, "TC-PS-011 active table intact after legacy reject");
}
int main(void) {
printf("=== Prot Set Tests ===\n");
tc_init();
tc_set_range();
tc_seal_selfcheck();
tc_load_raw();
tc_q10_roundtrip();
tc_meta_sanity();
tc_id_guard();
tc_global_plate_dirty();
tc_store_roundtrip();
tc_store_corrupt();
tc_of_uf_meta();
printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}