Files
STM32F4-Base/test/host/iec104/iec104_test.c
T
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

1457 lines
57 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.
/*
* 模块名称:IEC 60870-5-104 被控站协议栈主机侧单元测试
* 模块功能:在 PC 上对 App/protocol/iec104 全栈(asdu + pts + link)做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入):
* iec104_asdu.c + iec104_pts.c + iec104_link.c(B4 协议核心)
* prot_set.c + prot_elm.c + phasor.c + sample_ring.c(板端默认源
* 的链接依赖;定值两段写走**真实 prot_set**)
* 数据源经 iec104_pts_bind_src 注入假源(YC/YX/YK 可控),
* 发送经 send_apdu 回调全量捕获,时基由测试注入。
*
* 覆盖点(docs/子系统设计/IEC104上行设计说明.md §7):
* TC-IEC104-001 CP56Time2a 往返 / 闰年 / IV / 钳位
* TC-IEC104-002 IOA 3B 与短浮点小端往返
* TC-IEC104-003 APCI 重组:半帧 / 粘帧 / 坏起始符 / 坏长度
* TC-IEC104-004 STARTDT 握手 → RUN
* TC-IEC104-005 OPEN 态 I 帧忽略(序号不消费)
* TC-IEC104-006 总召全流程:actcon→4 分片(首召 COT=4)→actterm;二召 COT=2;RBT→COT=4
* TC-IEC104-007 遥信变位 COV(COT=3);无效位不触发
* TC-IEC104-008 遥测死区 COV(卡 9 起 = 2 点确认):死区内不发 / 首越只挂起 /
* 连续第 2 越点上送
* TC-IEC104-009 k 窗:12 未确认停发;S 确认后恢复
* TC-IEC104-010 w 阈:收满 8 发 S
* TC-IEC104-011 t2 到期发 S
* TC-IEC104-012 t3 空闲发 TESTFR act;未答 t1 超时 → IDLE;con 清挂起不误杀
* TC-IEC104-013 遥控两段式:选择/执行成功;失配 PN;拒绝类选择段即 PN
* TC-IEC104-030 遥控 / 操作记录投递(道F task-10 的 104 侧写入点):未知 IOA、
* YK11 选择与执行、YK12 选择 / 执行 / 非法 DPC 各投哪一类哪一句,外加
* "拒绝不投操作记录""记录与压板实际状态一致""文案不超 28 字节"三条会
* 静默变坏但台架看不出的点(YK12 的 set_sealed 失败腿不可达,理由写在
* 该用例注释里,没有为它伪造定值范围)
* TC-IEC104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck
* TC-IEC104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI
* TC-IEC104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN
* TC-IEC104-017 未知类型 COT12+EVI;错公共地址 COT13+EVI
* TC-IEC104-018 STOPDT:数据停发,TESTFR 仍应答
* TC-IEC104-019 序号错 → IDLE(链路故障)
* TC-IEC104-020 pts:无效通道 IV / 未就绪 NT / 无效遥信 IV / is_dp / 表尺寸
* TC-IEC104-021 C_TS / C_RP actcon
* TC-IEC104-022 周期上送 COT=1(YC 组 + 频率)
* TC-IEC104-023 k 窗满 COV 不丢事件(pend 暂存,窗开后补发)
* —— 以下为「数据可信」卡 9/卡 11(道D,2026-10-25;板端默认源 = 真实
L2 链 ring→phasor→freq→meas,非桩)——
* TC-IEC104-025 板端源读快照(024 号已归 B7 用例):未发布
* IV|NT / 值=基波有效值 / 削顶 CLIPPED
* →线上 IV 清零 / 干净路品质 0 / 频率未新鲜 IV
* TC-IEC104-026 0x401D 绑频:收敛后 ±0.02 Hz / freq_valid==0 → IV 清零
* (卡 11 DoD 原句)
* TC-IEC104-027 死区运行时化(卡 10 后 = run_par 真源 + 转发壳):set/get
* + 非法拒 + 运行档压旧档 + 2 点确认/回摆撤销 + 壳清半截确认计数
* TC-IEC104-028 品质翻转直报(假源)+ 板端 seq 去重(同帧不计两点)+
* 换帧只变品质时按集合直报
* TC-IEC104-029 卡 10 路由:0x8030/0x8038 命中 / 0x8027 仍归 prot_set /
* 空隙 COT14 / C_SE 两段式 / k 带拒 / b 单侧 RMW 保 k
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-12(B4)
*/
#include <stdint.h>
#include <stdio.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 "iec104_asdu.c"
#include "prot_set.c"
#include "sample_ring.c"
#include "phasor.c"
#include "freq.c"
#include "meas.c" /* 卡 9:板端默认 YC 源 = L2 遥测快照(真实链,非桩) */
/* ===================== 卡 10 真链桩(同 adc_cal 套件先例) =====================
* run_par 依赖真实 cfg 引擎与 adc_cal:dbg_log 走 stub/ 计数宏,文件/堆走
* 内存虚拟盘(cfg_store_* 强符号),adc_input_desc 按 V0.1 权威映射给桩。
* 顺序敏感:桩头与强符号先于被纳入的 .c。 */
#include "dbg_log.h" /* stub/ 优先解析:DBG_* → 计数 */
#include <stdlib.h>
#include "cfg_file_store.h"
#define RP_VFS_CAP 8192u
static uint8_t rp_s_vfs[RP_VFS_CAP];
static size_t rp_s_vfs_len = 0u;
static uint32_t rp_s_vfs_mtime = 100u;
static int rp_s_vfs_exists = 0;
void *cfg_store_alloc(size_t size) { return malloc(size); }
void cfg_store_free(void *p_buf) { free(p_buf); }
uint32_t cfg_store_heap_free(void) { return 0u; }
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) {
size_t n;
(void)p_path;
if (!rp_s_vfs_exists) {
return -1;
}
n = rp_s_vfs_len;
if (n > cap) {
n = cap;
}
(void)memcpy(p_buf, rp_s_vfs, n);
*p_nread = n;
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
(void)p_path;
if (len > RP_VFS_CAP) {
return -1;
}
(void)memcpy(rp_s_vfs, p_buf, len);
rp_s_vfs_len = len;
rp_s_vfs_exists = 1;
rp_s_vfs_mtime++;
return 0;
}
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
(void)p_path;
if (!rp_s_vfs_exists) {
return -1;
}
p_st->size = (uint32_t)rp_s_vfs_len;
p_st->mtime = rp_s_vfs_mtime;
return 0;
}
int cfg_store_remove(const char *p_path) {
(void)p_path;
rp_s_vfs_exists = 0;
rp_s_vfs_len = 0u;
return 0;
}
const char *cfg_store_config_path(void) { return "/config.ini"; }
const char *cfg_store_tmp_path(void) { return "/config.tmp"; }
/* ---- sys_log 桩:只计数自检留痕 ---- */
#include "sys_log.h"
static int rp_s_syslog_count = 0;
void syslog_post(syslog_id_t id, const char *desc) {
(void)id;
(void)desc;
rp_s_syslog_count++;
}
/* ---- syslog_post_quiet 桩:记下投了什么(task-10 遥控 / 操作记录用) ----
* 真机上 quiet 与 post 的差别只在"投递侧不打日志",收/丢判据逐条相同;
* 本套件是协议栈单测,不验投递本身(那归 test/host/sys_log_mcu),只验
* **板端源在每条返回路径上投了哪一类、哪一句**——所以这里存成环形日志。 */
#define RP_REC_MAX 16u
static uint8_t rp_s_rec_id[RP_REC_MAX];
static char rp_s_rec_desc[RP_REC_MAX][SYSLOG_DESC_MAX + 1u];
static uint32_t rp_s_rec_n; /* 累计条数(不回绕,供"一条都没多投"断言) */
static uint32_t rp_s_drop_quiet; /* 本桩不模拟丢弃,恒 0 */
void syslog_post_quiet(syslog_id_t id, const char *desc) {
uint32_t slot;
size_t dl = 0u;
if (desc == NULL) {
return;
}
slot = rp_s_rec_n % RP_REC_MAX;
rp_s_rec_id[slot] = (uint8_t)id;
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
dl++;
}
(void)memcpy(rp_s_rec_desc[slot], desc, dl);
rp_s_rec_desc[slot][dl] = '\0';
rp_s_rec_n++;
}
uint32_t syslog_post_dropped(void) { return rp_s_drop_quiet; }
/* 第 k 条(0 起)记录;越界返回 -1 */
static int rp_rec_at(uint32_t k, syslog_id_t id, const char *desc)
{
if (k >= rp_s_rec_n) {
return -1;
}
return (rp_s_rec_id[k % RP_REC_MAX] == (uint8_t)id)
&& (strcmp(rp_s_rec_desc[k % RP_REC_MAX], desc) == 0) ? 1 : 0;
}
/* 某类记录一共投了几条 */
static uint32_t rp_rec_cnt(syslog_id_t id)
{
uint32_t i, n = 0u;
for (i = 0u; (i < rp_s_rec_n) && (i < RP_REC_MAX); i++) {
if (rp_s_rec_id[i] == (uint8_t)id) {
n++;
}
}
return n;
}
static void rp_rec_clear(void)
{
rp_s_rec_n = 0u;
(void)memset(rp_s_rec_id, 0, sizeof(rp_s_rec_id));
(void)memset(rp_s_rec_desc, 0, sizeof(rp_s_rec_desc));
}
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */
#include "adc_input.h"
static const adc_channel_desc_t rp_s_desc[ADC_SEM_COUNT] = {
{ ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */
{ ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */
{ ADC_SRC_STM32, 3u }, /* UC = CH3 */
{ ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */
{ ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */
{ ADC_SRC_STM32, 6u }, /* IB = CH6 */
{ ADC_SRC_STM32, 1u }, /* IC = CH1 */
{ ADC_SRC_STM32, 2u }, /* IN = CH2 */
{ ADC_SRC_STM32, 5u }, /* TEMP = CH5 */
};
const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) {
if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) {
return NULL;
}
return &rp_s_desc[(unsigned)ch];
}
/* dbg 计数定义(stub/dbg_log.h 声明 extern) */
int g_dbgstub104_err_count = 0;
int g_dbgstub104_info_count = 0;
int g_dbgstub104_debug_count = 0;
#include "cfg_file.c"
#include "adc_cal.c"
#include "run_par.c"
#include "prot_elm.c"
#include "iec104_pts.c"
#include "iec104_link.c"
/* ---------------- 假数据源(定值转发真实 prot_set) ---------------- */
static float s_f_yc[9];
static int s_f_yc_rc[9];
static uint64_t s_f_bits;
static uint64_t s_f_valid;
static uint32_t s_f_yk_ioa;
static uint8_t s_f_yk_dpc;
static uint8_t s_f_yk_sel;
static int f_yc(uint8_t idx, float *out)
{
if (idx >= 9u) {
return -1;
}
if (s_f_yc_rc[idx] != 0) {
return s_f_yc_rc[idx];
}
*out = s_f_yc[idx];
return 0;
}
static uint64_t f_bits(void) { return s_f_bits; }
static uint64_t f_valid(void) { return s_f_valid; }
static int f_yk(uint32_t ioa, uint8_t dpc, uint8_t sel)
{
s_f_yk_ioa = ioa;
s_f_yk_dpc = dpc;
s_f_yk_sel = sel;
if (ioa == 0x600Bu) {
/* YK12 全局软压板(B7):转真实板端源(prot_set_set_sealed 原子路径) */
return s_src_yk_exec(ioa, dpc, sel);
}
return (ioa == 0x600Au) ? 0 : -1;
}
static int f_set_read(uint32_t addr, float *out)
{
return s_src_set_read(addr, out);
}
static int f_set_write(uint32_t addr, float v, uint8_t sel)
{
return s_src_set_write(addr, v, sel);
}
static const iec104_pts_src_t g_fake = {
f_yc, f_bits, f_valid, f_yk, f_set_read, f_set_write
};
/* ---------------- 发送捕获 + 对时假回调 ---------------- */
#define CAP_MAX 48
#define CAP_SZ 260
static uint8_t s_cap[CAP_MAX][CAP_SZ];
static int s_cap_n;
static uint64_t s_clock_ms;
static int s_clock_rc;
static void cap_send(void *ctx, const uint8_t *apdu, uint16_t len)
{
(void)ctx;
if ((s_cap_n < CAP_MAX) && (len <= CAP_SZ)) {
(void)memcpy(s_cap[s_cap_n], apdu, len);
s_cap_n++;
}
}
static int f_clock(void *ctx, uint64_t ms)
{
(void)ctx;
s_clock_ms = ms;
return s_clock_rc;
}
/* B9 时标源假回调:s_epoch_val = UINT64_MAX → 时钟无效(CP56 IV) */
static uint64_t s_epoch_val;
static uint64_t f_epoch(void *ctx)
{
(void)ctx;
return s_epoch_val;
}
static iec104_link_t g_lk;
static const iec104_link_cfg_t g_cfg = { 1u, cap_send, f_clock, f_epoch };
/* ---------------- 主站侧模拟(N(R) 单调,照真实主站) ---------------- */
static uint16_t m_ns;
static uint16_t m_nr;
static void m_reset(void)
{
m_ns = 0u;
m_nr = 0u;
s_cap_n = 0;
}
static void feed_u(uint8_t q, uint32_t now)
{
uint8_t b[6];
b[0] = 0x68u; b[1] = 4u; b[2] = q; b[3] = 0u; b[4] = 0u; b[5] = 0u;
iec104_link_on_rx(&g_lk, b, 6u, now);
}
static void feed_i(const uint8_t *asdu, uint16_t len, uint32_t now)
{
uint8_t b[6u + 250u];
uint16_t fs = (uint16_t)(m_ns << 1);
uint16_t fr = (uint16_t)(m_nr << 1); /* N(R) 单调(feed_s 推进) */
b[0] = 0x68u;
b[1] = (uint8_t)(4u + len);
b[2] = (uint8_t)(fs & 0xFFu);
b[3] = (uint8_t)(fs >> 8);
b[4] = (uint8_t)(fr & 0xFFu);
b[5] = (uint8_t)(fr >> 8);
(void)memcpy(&b[6], asdu, len);
iec104_link_on_rx(&g_lk, b, (uint16_t)(6u + len), now);
m_ns = (uint16_t)((m_ns + 1u) % 32768u);
}
static void feed_s(uint16_t nr, uint32_t now)
{
uint8_t b[6];
uint16_t f = (uint16_t)(nr << 1);
b[0] = 0x68u; b[1] = 4u; b[2] = 1u; b[3] = 0u;
b[4] = (uint8_t)(f & 0xFFu);
b[5] = (uint8_t)(f >> 8);
m_nr = nr; /* 主站确认推进(单调) */
iec104_link_on_rx(&g_lk, b, 6u, now);
}
static uint16_t mk_asdu(uint8_t *b, uint8_t type, uint8_t vsq, uint8_t cot,
uint8_t cot_hi, uint8_t with_ioa, uint32_t ioa,
const uint8_t *body, uint16_t blen)
{
uint16_t n = iec104_asdu_put_hdr(b, type, vsq, cot, cot_hi, 1u);
if (with_ioa != 0u) {
iec104_put_ioa(&b[n], ioa);
n += 3u;
}
if (blen > 0u) {
(void)memcpy(&b[n], body, blen);
n += blen;
}
return n;
}
/* ---------------- 复位 / 握手 ---------------- */
static void reset_all(uint32_t now)
{
prot_set_init();
prot_elm_init();
run_par_init(); /* 卡 10:死区/脏复位到工厂默认(重复注册 -1 走计数桩,无害) */
iec104_pts_init();
iec104_pts_bind_src(&g_fake);
(void)memset(s_f_yc, 0, sizeof(s_f_yc));
(void)memset(s_f_yc_rc, 0, sizeof(s_f_yc_rc));
s_f_bits = 0u;
s_f_valid = (1uLL << 14) | (1uLL << 15) | (1uLL << 16) | (1uLL << 17)
| (1uLL << 22) | (1uLL << 30);
s_clock_rc = 0;
s_clock_ms = 0u;
s_epoch_val = 1760234567000ULL; /* B9:固定时标(2025-10-12 UTC) */
iec104_link_init(&g_lk, &g_cfg, NULL);
iec104_link_open(&g_lk, now);
m_reset();
}
static void handshake(uint32_t now)
{
feed_u(0x07u, now); /* STARTDT act */
}
/* ---------------- 捕获检查 ---------------- */
typedef struct {
uint8_t type, vsq, cot, cot_hi, oa;
uint32_t ca, ioa;
const uint8_t *obj; /* ioa 之后的信息体 */
uint16_t obj_len;
} cap_t;
static int cap_i_count(void)
{
int k, n = 0;
for (k = 0; k < s_cap_n; k++) {
if ((s_cap[k][2] & 0x01u) == 0u) {
n++;
}
}
return n;
}
static int cap_u_count(uint8_t q)
{
int k, n = 0;
for (k = 0; k < s_cap_n; k++) {
if ((s_cap[k][2] & 0x03u) == 0x03u && s_cap[k][2] == q) {
n++;
}
}
return n;
}
static int cap_has_s(uint16_t *nr_out)
{
int k;
for (k = s_cap_n - 1; k >= 0; k--) {
const uint8_t *p = s_cap[k];
if ((p[2] & 0x03u) == 0x01u) {
*nr_out = (uint16_t)(((uint16_t)p[4] | ((uint16_t)p[5] << 8)) >> 1);
return 1;
}
}
return 0;
}
static int cap_i(int n, cap_t *o)
{
int k, seen = 0;
for (k = 0; k < s_cap_n; k++) {
const uint8_t *p = s_cap[k];
uint16_t lf = p[1];
iec104_asdu_hdr_t h;
if ((p[2] & 0x01u) != 0u) {
continue;
}
if (seen == n) {
if (iec104_asdu_parse_hdr(&p[6], (uint16_t)(lf - 4u), &h) != 0) {
return -1;
}
o->type = h.type;
o->vsq = h.vsq;
o->cot = h.cot;
o->cot_hi = h.cot_hi;
o->oa = h.oa;
o->ca = h.ca;
if (h.obj_len >= 3u) {
o->ioa = iec104_get_ioa(h.obj);
o->obj = h.obj + 3u;
o->obj_len = (uint16_t)(h.obj_len - 3u);
} else {
o->ioa = 0u;
o->obj = h.obj;
o->obj_len = 0u;
}
return 0;
}
seen++;
}
return -1;
}
/* ============================ 测试用例 ============================ */
static void tc_cp56(void)
{
uint8_t b[7];
uint64_t e1 = 946684800000ULL; /* 2000-01-01 00:00:00.000 */
uint64_t e2 = 1709188496789ULL; /* 2024-02-29 06:34:56.789(闰日) */
uint64_t back = 0u;
int rc;
iec104_epoch_ms_to_cp56(e1, b);
rc = iec104_cp56_to_epoch_ms(b, &back);
CHECK((rc == 0) && (back == e1), "TC-IEC104-001 CP56 round trip on 2000-01-01");
iec104_epoch_ms_to_cp56(e2, b);
back = 0u;
rc = iec104_cp56_to_epoch_ms(b, &back);
CHECK((rc == 0) && (back == e2), "TC-IEC104-001 CP56 round trip on the leap day 2024-02-29");
b[2] |= 0x80u;
CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-IEC104-001 IV -> -2");
iec104_epoch_ms_to_cp56(0u, b); /* 早于 2000 → 钳位 */
back = 0u;
(void)iec104_cp56_to_epoch_ms(b, &back);
CHECK(back == e1, "TC-IEC104-001 a value before 2000 is clamped");
}
static void tc_ioa_float(void)
{
uint8_t b[8];
iec104_put_ioa(b, 0x123456u);
CHECK((iec104_get_ioa(b) == 0x123456u) && (b[0] == 0x56u) && (b[1] == 0x34u)
&& (b[2] == 0x12u), "TC-IEC104-002 IOA is 3 bytes little endian");
iec104_put_float(b, 1.0f);
CHECK((b[0] == 0x00u) && (b[1] == 0x00u) && (b[2] == 0x80u)
&& (b[3] == 0x3Fu), "TC-IEC104-002 float in IEEE754 little endian");
iec104_put_float(b, -12.5f);
CHECK(iec104_get_float(b) == -12.5f, "TC-IEC104-002 float round trip");
}
static void tc_apci(void)
{
uint32_t now = 1000u;
reset_all(now);
{
static const uint8_t b[6] = { 0x68u, 4u, 0x07u, 0u, 0u, 0u };
iec104_link_on_rx(&g_lk, b, 3u, now);
CHECK(cap_u_count(0x0Bu) == 0, "TC-IEC104-003 a half frame triggers nothing");
iec104_link_on_rx(&g_lk, &b[3], 3u, now);
CHECK(cap_u_count(0x0Bu) == 1, "TC-IEC104-003 a half frame is reassembled then con");
}
{
static const uint8_t b[12] = {
0x68u, 4u, 0x43u, 0u, 0u, 0u, /* TESTFR act */
0x68u, 4u, 0x13u, 0u, 0u, 0u /* STOPDT act */
};
iec104_link_on_rx(&g_lk, b, 12u, now);
CHECK((cap_u_count(0x83u) == 1)
&& (iec104_link_state(&g_lk) == IEC104_ST_STOPPED),
"TC-IEC104-003 a coalesced frame is handled one by one and STOPDT takes effect");
}
reset_all(now);
{
static const uint8_t b[6] = { 0x69u, 4u, 0x07u, 0u, 0u, 0u };
iec104_link_on_rx(&g_lk, b, 6u, now);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-IEC104-003 a bad start byte -> IDLE");
}
reset_all(now);
{
static const uint8_t b[6] = { 0x68u, 3u, 0x07u, 0u, 0u, 0u };
iec104_link_on_rx(&g_lk, b, 6u, now);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-IEC104-003 a bad length -> IDLE");
}
}
static void tc_startdt(void)
{
uint32_t now = 1000u;
reset_all(now);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-IEC104-004 open -> OPEN");
handshake(now);
CHECK((cap_u_count(0x0Bu) == 1)
&& (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-IEC104-004 STARTDT -> RUN");
}
static void tc_open_ignore(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint16_t n;
reset_all(now);
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i_count() == 0) && (iec104_link_state(&g_lk) == IEC104_ST_OPEN),
"TC-IEC104-005 an I frame in OPEN state is ignored");
handshake(now);
m_ns = 0u; /* OPEN 忽略不消费序号 → 主站仍从 0 起 */
feed_i(a, n, now); /* ns=0 仍有效(序号未被消费) */
CHECK(cap_i_count() == 1, "TC-IEC104-005 after RUN the same ns is answered normally");
}
static void tc_interrogation(void)
{
uint32_t now = 5000u;
uint8_t a[32];
uint8_t body[1];
uint16_t n;
cap_t c;
reset_all(now);
s_f_yc[0] = 111.0f;
s_f_yc_rc[1] = -1; /* idx1(1Ub)无效通道 */
handshake(now);
body[0] = IEC104_RQT_GROUP1;
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
feed_i(a, n, now);
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 100u)
&& (c.cot == IEC104_COT_ACTCON), "TC-IEC104-006 actcon is sent right away, COT7");
iec104_link_poll(&g_lk, now);
CHECK(cap_i_count() == 5, "TC-IEC104-006 the first poll uses the whole budget of 4 frames");
CHECK((cap_i(1, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT)
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 4u)) && (c.ioa == 1u),
"TC-IEC104-006 YX double-point group, first call COT=4");
CHECK((cap_i(2, &c) == 0) && (c.type == 1u)
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 29u)) && (c.ioa == 5u),
"TC-IEC104-006 YX single-point group with 29 points");
CHECK((cap_i(3, &c) == 0) && (c.type == 13u)
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 8u)) && (c.ioa == 0x4011u)
&& (c.obj_len == 40u), "TC-IEC104-006 YC group of 8 points, 40 bytes");
if (cap_i(3, &c) == 0) {
CHECK((iec104_get_float(c.obj) == 111.0f) && (c.obj[4] == 0u),
"TC-IEC104-006 YC value 111.0 with QDS=0");
CHECK(c.obj[9] == IEC104_QDS_IV, "TC-IEC104-006 an invalid channel sets QDS=IV");
}
CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u),
"TC-IEC104-006 frequency as a single point with SQ=0");
iec104_link_poll(&g_lk, now + 100u);
CHECK((cap_i_count() == 6) && (cap_i(5, &c) == 0) && (c.type == 100u)
&& (c.cot == IEC104_COT_ACTTERM), "TC-IEC104-006 actterm closes the sequence");
/* 二召:init 已清 → COT=2 */
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
feed_i(a, n, now + 200u);
iec104_link_poll(&g_lk, now + 200u);
CHECK((cap_i(7, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_BG_SCAN),
"TC-IEC104-006 second call COT=2");
/* RBT 位 → COT=4(actcon3 是第 12 个未确认帧 → 先 S 确认窗才开) */
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, IEC104_COT_BIT_RBT,
1u, 0u, body, 1u);
feed_i(a, n, now + 300u);
feed_s(12u, now + 300u);
iec104_link_poll(&g_lk, now + 300u);
CHECK((cap_i(12, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT),
"TC-IEC104-006 RBT -> COT=4");
}
static void tc_cov_yx(void)
{
uint32_t now = 1000u;
cap_t c;
uint64_t back;
reset_all(now);
handshake(now);
iec104_link_poll(&g_lk, now); /* 建快照 */
CHECK(cap_i_count() == 0, "TC-IEC104-007 the first poll sends nothing spontaneous");
s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */
iec104_link_poll(&g_lk, now + 100u);
/* B9:单点 YX 自发 = M_SP_TB_1(30)带 CP56Time2a 时标(SOE 上送路径) */
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 30u)
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u)
&& (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-IEC104-007 a YX16 change of state sends COT3 with TB");
if ((cap_i(0, &c) == 0) && (c.obj_len == 8u)) {
CHECK(iec104_cp56_to_epoch_ms(&c.obj[1], &back) == 0,
"TC-IEC104-007 the CP56 time tag decodes");
CHECK(back == s_epoch_val, "TC-IEC104-007 the time tag equals the epoch_ms source");
}
s_f_bits &= ~(1uLL << 15); /* 复归 */
iec104_link_poll(&g_lk, now + 200u);
CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.type == 30u)
&& (c.ioa == 16u) && (c.obj[0] == 0u), "TC-IEC104-007 YX16 is reset and changes again");
/* B9 IV 路径:时标源无效 → CP56 IV 位如实置位 */
s_epoch_val = UINT64_MAX;
s_f_bits |= (1uLL << 16); /* YX17(idx16)动作 */
iec104_link_poll(&g_lk, now + 300u);
CHECK((cap_i_count() == 3) && (cap_i(2, &c) == 0) && (c.type == 30u)
&& (c.ioa == 17u) && ((c.obj[3] & 0x80u) != 0u),
"TC-IEC104-007 an invalid clock sets the CP56 IV bit");
s_epoch_val = 1760234567000ULL;
s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */
iec104_link_poll(&g_lk, now + 400u);
CHECK(cap_i_count() == 3, "TC-IEC104-007 a change while the IV bit is set triggers nothing");
}
static void tc_cov_yc(void)
{
uint32_t now = 1000u;
cap_t c;
reset_all(now);
handshake(now);
s_f_yc[0] = 100.0f;
iec104_link_poll(&g_lk, now); /* 快照 = 100 */
s_f_yc[0] = 100.4f; /* 死区 0.5 V 内(卡 6 二次值口径:0.5 %×100 V,
* 旧拟定码值死区 10 已由「数据可信」卡 6 重定) */
iec104_link_poll(&g_lk, now + 100u);
CHECK(cap_i_count() == 0, "TC-IEC104-008 inside the deadband nothing is uploaded");
s_f_yc[0] = 101.25f; /* 101.25-100=1.25 > 0.5,第 1 越点 */
iec104_link_poll(&g_lk, now + 200u);
CHECK(cap_i_count() == 0, "TC-IEC104-008 first deadband crossing only arms (2-point confirm)");
iec104_link_poll(&g_lk, now + 300u); /* 第 2 个连续测点仍越 -> 确认上送 */
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u)
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u)
&& (iec104_get_float(c.obj) == 101.25f),
"TC-IEC104-008 second consecutive crossing uploads (Card 9 confirm)");
}
static void feed_rd(uint32_t now)
{
uint8_t a[16];
uint16_t n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u,
1u, 0x4011u, NULL, 0u);
feed_i(a, n, now);
}
static void tc_k_window(void)
{
uint32_t now = 1000u;
int i;
reset_all(now);
handshake(now);
for (i = 0; i < 12; i++) {
feed_rd(now);
}
CHECK(cap_i_count() == 12, "TC-IEC104-009 the k=12 window caps the outstanding frames");
feed_rd(now);
CHECK(cap_i_count() == 12, "TC-IEC104-009 the 13th frame is suppressed by the k window");
feed_s(12u, now); /* 全确认 → 窗开 */
feed_rd(now);
CHECK(cap_i_count() == 13, "TC-IEC104-009 sending resumes after the S acknowledgement");
}
static void tc_w_thresh(void)
{
uint32_t now = 1000u;
uint16_t nr = 0xFFFFu;
int i;
reset_all(now);
handshake(now);
for (i = 0; i < 7; i++) {
feed_rd(now);
}
CHECK(cap_has_s(&nr) == 0, "TC-IEC104-010 after 7 received frames no S has been sent yet");
feed_rd(now); /* 第 8 个 → w 阈 */
CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-IEC104-010 w=8 -> S with nr=8");
}
static void tc_t2(void)
{
uint32_t now = 1000u;
uint16_t nr = 0xFFFFu;
reset_all(now);
handshake(now);
feed_rd(now); /* rx_unacked=1,t2 起爆 */
iec104_link_poll(&g_lk, now + 9999u);
CHECK(cap_has_s(&nr) == 0, "TC-IEC104-011 no S before t2 expires");
iec104_link_poll(&g_lk, now + 10001u);
CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-IEC104-011 S is sent once t2 expires");
}
static void tc_t3_t1(void)
{
uint32_t now = 1000u;
reset_all(now);
handshake(now);
iec104_link_poll(&g_lk, now + 19999u);
CHECK(cap_u_count(0x43u) == 0, "TC-IEC104-012 no TESTFR before t3 expires");
iec104_link_poll(&g_lk, now + 20001u);
CHECK(cap_u_count(0x43u) == 1, "TC-IEC104-012 TESTFR act is sent once t3 expires");
iec104_link_poll(&g_lk, now + 20001u + 15001u);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-IEC104-012 an unanswered TESTFR hits t1 -> IDLE");
/* con 清挂起 → 不误杀 */
reset_all(now);
handshake(now);
iec104_link_poll(&g_lk, now + 20001u);
CHECK(cap_u_count(0x43u) == 1, "TC-IEC104-012 after reconnect t3 fires again");
feed_u(0x83u, now + 20100u); /* TESTFR con */
iec104_link_poll(&g_lk, now + 35000u);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-IEC104-012 once con clears the pending frame t1 does not kill it by mistake");
}
static void tc_dc(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint8_t body[1];
uint16_t n;
cap_t c;
reset_all(now);
handshake(now);
body[0] = 0x82u; /* S=1, DPC=ON */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 52u) && (c.cot == IEC104_COT_ACTCON)
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-IEC104-013 the select phase answers actcon");
CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-IEC104-013 the select phase forwards the source");
body[0] = 0x02u; /* 执行 ON */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u)
&& (s_f_yk_sel == 0u), "TC-IEC104-013 the execute phase answers actcon");
/* 失配:选 ON 执 OFF */
body[0] = 0x82u;
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
feed_i(a, n, now);
body[0] = 0x01u;
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-013 a mismatching execute phase returns PN");
/* 拒绝类(YK1):选择段即 PN */
body[0] = 0x82u;
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x6000u, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-013 a YK without output hardware returns PN in the select phase");
}
/* TC-IEC104-024(B7):YK12 全局软压板两段式转真(经真实板端源 + 原子封存) */
static void tc_dc_yk12(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint8_t body[1];
uint16_t n;
cap_t c;
reset_all(now);
handshake(now);
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-IEC104-024 soft plates are engaged by default");
body[0] = 0x81u; /* S=1, DPC=OFF:选择段 */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.cot == IEC104_COT_ACTCON)
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-IEC104-024 the YK12 select phase answers actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-IEC104-024 the select phase performs no action");
body[0] = 0x01u; /* 执行 OFF */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
"TC-IEC104-024 the YK12 execute phase answers actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-IEC104-024 the execute phase drives the soft plate to the released state");
CHECK(prot_set_selfcheck() == 0, "TC-IEC104-024 set_sealed seals atomically and selfcheck passes");
CHECK(prot_set_dirty_peek() == 1u, "TC-IEC104-024 the dirty flag is set for persistence");
body[0] = 0x82u; /* 再投入:选择 ON */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
feed_i(a, n, now);
body[0] = 0x02u; /* 执行 ON */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
"TC-IEC104-024 the engage command executes with actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-IEC104-024 the soft plate reaches the engaged state");
body[0] = 0x80u; /* DPC=00 非法:选择段即 PN */
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-024 an invalid DPC returns PN in the select phase");
}
/* TC-IEC104-030(道F task-10):遥控记录——板端默认源 s_src_yk_exec() 的三条返回
* 路径各投哪一类记录、投几句、是否顺手投了操作记录。
* 为什么直接调 s_src_yk_exec 而不是发 ASDU:本套件绑定的是**假源** f_yk,
* YK11 / 未知 IOA 在 f_yk 里就被短路(只有 YK12 转发到真板端源,见 f_yk),
* 走 ASDU 反而测不到要验的那两条腿;"ASDU -> link -> pts -> 源"这条转发链
* 已由 TC-IEC104-013/024 覆盖,不重复。**投递本身**(quiet 通道的收/丢)归
* test/host/sys_log_mcu,本用例只钉"哪条路径投了哪一条"。
* 覆盖不到的一条腿(登记,不假装):YK12 的 rc!=0 分支要 prot_set_set_sealed
* 返 -1 才进得去,而 PS_GLOBAL_EN 的 min/max 恰好就是本函数会写的 0/1 两值
* → 该产品路径上不可达,是防御性分支;不为它伪造定值范围(那是改判据)。 */
static void tc_yk_records(void)
{
uint32_t now = 1000u;
reset_all(now);
rp_rec_clear();
/* 1) 未知 IOA:拒绝,只投一条遥控记录;拒绝没改动任何东西 -> 不该有操作记录 */
CHECK(s_src_yk_exec(0x6000u, (uint8_t)IEC104_DPC_ON, 1u) == -1,
"TC-IEC104-030 an IOA without output hardware is still rejected");
CHECK((rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK unknown ioa") == 1),
"TC-IEC104-030 the reject path posts exactly one telecontrol record");
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
"TC-IEC104-030 a rejected command posts no operation record");
/* 2) YK11 选择段:受理但未动作 -> 只有遥控记录 */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 1u) == 0)
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK11 select") == 1),
"TC-IEC104-030 the YK11 select phase posts a select record and acts on nothing");
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
"TC-IEC104-030 the select phase is not an operation yet");
/* 3) YK11 执行段:遥控记录 + 操作记录各一条,顺序 = 先命令后动作 */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 0u) == 0)
&& (rp_s_rec_n == 2u)
&& (rp_rec_at(0u, SYSLOG_YK, "YK11 exec") == 1)
&& (rp_rec_at(1u, SYSLOG_OPER, "force reset") == 1),
"TC-IEC104-030 the YK11 execute phase posts one telecontrol plus one operation record");
/* 4) YK12 非法双点值:拒绝 + 一条带原因的遥控记录,压板不动 */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Bu, 0u, 0u) == -1) && (rp_s_rec_n == 1u)
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 bad dpc") == 1),
"TC-IEC104-030 an invalid DPC posts the reason and changes nothing");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u,
"TC-IEC104-030 the invalid DPC leaves the soft plate engaged");
/* 5) YK12 执行 OFF:遥控 off + 操作 plate out,且压板真的退出了(记录与事实同步) */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 0u) == 0)
&& (rp_s_rec_n == 2u)
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec off") == 1)
&& (rp_rec_at(1u, SYSLOG_OPER, "plate out") == 1),
"TC-IEC104-030 releasing the soft plate records both the command and the operation");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u,
"TC-IEC104-030 the record agrees with the plate state the settings layer holds");
/* 6) YK12 执行 ON:对称的另一半(只测 OFF 的话,两条 desc 写反也发现不了) */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_ON, 0u) == 0)
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec on") == 1)
&& (rp_rec_at(1u, SYSLOG_OPER, "plate in") == 1),
"TC-IEC104-030 engaging the soft plate records the matching pair");
/* 7) 选择段不落地:YK12 select 只投一条,压板状态不变 */
rp_rec_clear();
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 1u) == 0)
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK12 select") == 1)
&& (prot_set_get_ctl(PS_GLOBAL_EN) == 1u),
"TC-IEC104-030 the YK12 select phase records the intent without changing the plate");
/* 8) 文案长度契约:desc 全是 ROM 常量,任何一条超过 SYSLOG_DESC_MAX 都会在
* 投递侧被静默截断(屏上少几个字,没人会去数)-> 在单测里钉住。 */
CHECK((strlen(YK_REC_UNKNOWN) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK11_EXEC) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK12_OFF) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK12_FAIL) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_DPC_BAD) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK12_SEL) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK11_SEL) <= SYSLOG_DESC_MAX)
&& (strlen(YK_REC_YK12_ON) <= SYSLOG_DESC_MAX),
"TC-IEC104-030 every telecontrol description fits the 28-byte record field");
rp_rec_clear();
}
static void tc_se(void)
{
uint32_t now = 1000u;
uint8_t a[24];
uint8_t body[5];
uint16_t n;
uint32_t addr = g_prot_set_meta[PS_OC1_CUR].comm_addr; /* 0x8858 */
cap_t c;
reset_all(now);
handshake(now);
CHECK(addr == 0x8858u, "TC-IEC104-014 the OC1 current setting communication address");
iec104_put_float(body, 5.0f);
body[4] = IEC104_QDC_S; /* 选择段 */
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 50u)
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-IEC104-014 selecting a setting answers actcon");
body[4] = 0u; /* 执行段 */
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
"TC-IEC104-014 executing a setting answers actcon");
CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-IEC104-014 the setting really was written (real prot_set)");
CHECK(prot_set_selfcheck() == 0, "TC-IEC104-014 sealing after execute keeps selfcheck passing");
CHECK(prot_set_dirty_peek() == 1u, "TC-IEC104-014 the execute phase sets dirty (B7, waiting to be persisted)");
iec104_put_float(body, 100000.0f); /* 越上限 */
body[4] = IEC104_QDC_S;
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
feed_i(a, n, now);
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-014 an out-of-range select phase returns PN");
}
static void tc_rd(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint16_t n;
cap_t c;
reset_all(now);
handshake(now);
s_f_yc[0] = 222.0f;
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ)
&& (iec104_get_float(c.obj) == 222.0f), "TC-IEC104-015 C_RD hits a YC group");
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x8858u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && (c.type == 13u) && (c.ioa == 0x8858u),
"TC-IEC104-015 C_RD hits a setting");
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x9999u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i(2, &c) == 0) && (c.type == 102u)
&& (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u),
"TC-IEC104-015 an unknown IOA answers COT14 plus EVI");
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i(3, &c) == 0) && (c.cot == IEC104_COT_UNK_IOA),
"TC-IEC104-015 a local setting item (comm_addr=0) is not addressable (B7)");
}
static void tc_cs(void)
{
uint32_t now = 1000u;
uint8_t a[24];
uint8_t cp[7];
uint16_t n;
cap_t c;
uint64_t e = 1709188496789ULL;
reset_all(now);
handshake(now);
(void)iec104_epoch_ms_to_cp56(e, cp);
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 103u)
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) && (s_clock_ms == e),
"TC-IEC104-016 C_CS syncs the clock with an exact epoch");
cp[2] |= 0x80u; /* IV */
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-016 CP56 IV -> PN");
(void)iec104_epoch_ms_to_cp56(e, cp);
s_clock_rc = -1; /* 系统侧拒绝 */
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
feed_i(a, n, now);
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
"TC-IEC104-016 a clock_sync failure -> PN");
}
static void tc_err_resp(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint16_t n, m;
cap_t c;
reset_all(now);
handshake(now);
n = mk_asdu(a, 200u, 1u, IEC104_COT_ACT, 0u, 1u, 0u, NULL, 0u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 200u) && (c.cot == IEC104_COT_UNK_COT)
&& ((c.oa & 0x80u) != 0u), "TC-IEC104-017 an unknown type answers COT12 plus EVI");
m = iec104_asdu_put_hdr(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 2u);
a[m] = IEC104_RQT_GROUP1;
m++;
feed_i(a, m, now); /* ca=2 ≠ 1 */
CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA)
&& ((c.oa & 0x80u) != 0u), "TC-IEC104-017 a wrong common address answers COT13 plus EVI");
}
static void tc_stopdt(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint8_t body[1];
uint16_t n;
reset_all(now);
handshake(now);
feed_u(0x13u, now); /* STOPDT act */
CHECK((cap_u_count(0x23u) == 1)
&& (iec104_link_state(&g_lk) == IEC104_ST_STOPPED), "TC-IEC104-018 STOPDT con");
body[0] = IEC104_RQT_GROUP1;
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
feed_i(a, n, now);
CHECK(cap_i_count() == 0, "TC-IEC104-018 in STOPPED state the data service is off");
feed_u(0x43u, now);
CHECK(cap_u_count(0x83u) == 1, "TC-IEC104-018 in STOPPED state TESTFR is still answered");
}
static void tc_seq_err(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint16_t n;
reset_all(now);
handshake(now);
m_ns = 5u; /* 期望 0,投 5 */
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
feed_i(a, n, now);
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-IEC104-019 a sequence error -> IDLE");
}
static void tc_pts(void)
{
float v;
uint8_t q;
uint8_t d;
reset_all(1000u);
CHECK((iec104_pts_yc_count() == 9u) && (iec104_pts_yx_count() == 33u)
&& (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-IEC104-020 table size and IOA layout");
s_f_yc[0] = 7.5f;
CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (v == 7.5f) && (q == 0u),
"TC-IEC104-020 a valid YC");
s_f_yc_rc[1] = -1;
(void)iec104_pts_yc_read(1u, &v, &q);
CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-IEC104-020 an invalid channel gives IV");
s_f_yc_rc[2] = -2;
(void)iec104_pts_yc_read(2u, &v, &q);
CHECK(q == IEC104_QDS_NT, "TC-IEC104-020 not ready gives NT");
iec104_pts_yx_read(0u, &d);
CHECK(d == IEC104_DIQ_IV, "TC-IEC104-020 an invalid telemetry point gives IV");
CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u),
"TC-IEC104-020 the is_dp boundary");
s_f_bits = (1uLL << 15);
iec104_pts_yx_read(15u, &d);
CHECK(d == 1u, "TC-IEC104-020 a valid telemetry point gives ON");
}
static void tc_ts_rp(void)
{
uint32_t now = 1000u;
uint8_t a[16];
uint8_t body[2];
uint16_t n;
cap_t c;
reset_all(now);
handshake(now);
body[0] = 0u;
n = mk_asdu(a, IEC104_T_C_TS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(0, &c) == 0) && (c.type == 104u) && (c.cot == IEC104_COT_ACTCON),
"TC-IEC104-021 C_TS actcon");
n = mk_asdu(a, IEC104_T_C_RP_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
feed_i(a, n, now);
CHECK((cap_i(1, &c) == 0) && (c.type == 105u) && (c.cot == IEC104_COT_ACTCON),
"TC-IEC104-021 C_RP actcon");
}
static void tc_periodic(void)
{
uint32_t now = 1000u;
cap_t c;
reset_all(now);
handshake(now);
iec104_link_poll(&g_lk, now + 60001u);
CHECK(cap_i_count() == 2, "TC-IEC104-022 the cyclic upload takes 2 frames");
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_PERIODIC)
&& (c.ioa == 0x4011u), "TC-IEC104-022 the cyclic YC group uses COT=1");
CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-IEC104-022 the cyclic frequency point");
}
static void tc_cov_kfull(void)
{
uint32_t now = 1000u;
int i;
cap_t c;
reset_all(now);
handshake(now);
iec104_link_poll(&g_lk, now); /* 先建快照(首 poll 只建不算变位) */
for (i = 0; i < 12; i++) {
feed_rd(now); /* k 窗占满 */
}
s_f_bits |= (1uLL << 14); /* YX15 变位 */
iec104_link_poll(&g_lk, now + 100u);
CHECK(cap_i_count() == 12, "TC-IEC104-023 with k full nothing spontaneous can go out");
feed_s(12u, now + 200u); /* 窗开 */
iec104_link_poll(&g_lk, now + 300u);
CHECK((cap_i_count() == 13) && (cap_i(12, &c) == 0) && (c.ioa == 15u)
&& (c.cot == IEC104_COT_SPONT), "TC-IEC104-023 once the window opens the backlogged event is sent, none lost");
}
/* ========== 卡 9/卡 11:板端默认源(真实 L2 链:ring→phasor→freq→meas) ========== */
/* 与 App/task/adc_task.c 同型的单拍编排:压环 → phasor_run →(可选 freq_run)。
* 9 路同幅错相正弦(整周波 → 各路基波有效值 = A/√2,与相位无关,期望好算) */
static double s_ph2 = 0.0;
static void snap_beats(int n, double amp, int run_freq)
{
float f[ADC_SEM_COUNT];
int i;
int s;
for (i = 0; i < n; i++) {
for (s = 0; s < (int)ADC_SEM_TEMP; s++) {
f[s] = (float)(amp * sin(s_ph2 + (double)s * 0.35));
}
f[ADC_SEM_TEMP] = 1234.0f;
s_ph2 += 2.0 * 3.14159265358979323846 * 50.0 / 1000.0; /* 50 Hz */
(void)sample_ring_push_frame(f, (uint8_t)ADC_SEM_COUNT);
phasor_run();
if (run_freq != 0) {
freq_run();
}
}
}
/* 200 ms 发布拍(读同拍 freq 状态,adc_task v3.2 编排的镜像) */
static void snap_pub(uint32_t ms, uint32_t clip_mask)
{
meas_tick(ms, freq_valid(), freq_last(), clip_mask);
}
static void tc_pts_snap(void)
{
float v;
uint8_t q;
reset_all(1000u);
iec104_pts_bind_src(NULL); /* 板端默认源 = 真实快照链 */
(void)phasor_init(); /* 环 + 相量双区清零 */
(void)meas_init();
CHECK((iec104_pts_yc_read(0u, &v, &q) == -1)
&& (q == (uint8_t)(IEC104_QDS_IV | IEC104_QDS_NT)) && (v == 0.0f),
"TC-IEC104-025 before first publish -> IV|NT, value zero");
snap_beats(20, 100.0, 0);
snap_pub(200u, 0xE6u); /* 削顶位图同板端默认构建(cl=0xE6) */
CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (q == 0u)
&& (fabsf(v - 70.711f) < 0.1f),
"TC-IEC104-025 UA = snapshot fundamental RMS (engineering domain)");
CHECK((iec104_pts_yc_read(3u, &v, &q) == 0) && (q == 0u),
"TC-IEC104-025 UN (bit3 not in 0xE6) stays valid");
CHECK((iec104_pts_yc_read(4u, &v, &q) == 0) && (q == 0u),
"TC-IEC104-025 IA valid");
CHECK((iec104_pts_yc_read(1u, &v, &q) == -1) && (q == IEC104_QDS_IV)
&& (v == 0.0f), "TC-IEC104-025 clipped UB: CLIPPED maps to wire IV, value zero");
CHECK((iec104_pts_yc_read(2u, &v, &q) == -1) && (q == IEC104_QDS_IV),
"TC-IEC104-025 clipped UC IV");
CHECK((iec104_pts_yc_read(5u, &v, &q) == -1) && (q == IEC104_QDS_IV),
"TC-IEC104-025 clipped IB IV");
CHECK((iec104_pts_yc_read(8u, &v, &q) == -1) && (q == IEC104_QDS_IV),
"TC-IEC104-025 freq point published but not fresh -> IV (Card 11 rule)");
}
static void tc_pts_freq(void)
{
float v;
uint8_t q;
int k;
reset_all(1000u);
iec104_pts_bind_src(NULL);
(void)phasor_init();
(void)freq_init();
(void)meas_init();
for (k = 0; k < 5; k++) { /* 100 拍:测频启动收敛需 20 拍 */
snap_beats(20, 100.0, 1);
snap_pub((uint32_t)(k + 1) * 200u, 0u);
}
CHECK(freq_valid() == 1u, "TC-IEC104-026 drive precondition: freq converged");
CHECK((iec104_pts_yc_read(8u, &v, &q) == 0) && (q == 0u)
&& (fabsf(v - 50.0f) <= 0.02f),
"TC-IEC104-026 0x401D carries freq reading within 0.02 Hz of synthetic 50.000");
for (k = 0; k < 3; k++) { /* 浮空:幅值门落下 */
snap_beats(20, 0.0, 1);
snap_pub((uint32_t)(6 + k) * 200u, 0u);
}
CHECK((freq_valid() == 0u) && (iec104_pts_yc_read(8u, &v, &q) == -1)
&& (q == IEC104_QDS_IV) && (v == 0.0f),
"TC-IEC104-026 freq_valid==0 -> IV with zeroed value (Card 11 DoD)");
}
static void tc_db_runtime(void)
{
iec104_pts_cov_t cov;
reset_all(1000u); /* 假源在场 = 每次 cov_check 一测点 */
CHECK((iec104_pts_yc_set_deadband(0u, 2.0f) == 0)
&& (iec104_pts_yc_deadband(0u) == 2.0f),
"TC-IEC104-027 runtime deadband write + readback");
CHECK((iec104_pts_yc_set_deadband(9u, 1.0f) == -1)
&& (iec104_pts_yc_set_deadband(0u, -0.1f) == -1)
&& (iec104_pts_yc_deadband(0u) == 2.0f),
"TC-IEC104-027 bad idx / negative rejected, RAM untouched");
CHECK(iec104_pts_yc_deadband(8u) == 0.5f, "TC-IEC104-027 freq point ROM default kept");
s_f_yc[0] = 100.0f;
iec104_pts_cov_check(&cov); /* 建线 rep=100(档 2.0) */
s_f_yc[0] = 101.0f; /* 越旧档 0.5、未越运行档 2.0 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 runtime db gates the old step");
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 2nd point under 2.0 still silent");
run_par_init(); /* 恢复工厂默认档(0.5)——卡 10 复位入口 */
iec104_pts_init();
iec104_pts_bind_src(&g_fake);
s_f_yc[0] = 100.0f;
iec104_pts_cov_check(&cov); /* 建线 */
s_f_yc[0] = 100.6f; /* d=0.6 > 0.5,第 1 越 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 crossing arms only (2-point)");
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-027 second consecutive crossing confirms");
s_f_yc[0] = 101.4f; /* 新基准 100.6 起 d=0.8 第 1 越 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 pullback test step 1 armed");
s_f_yc[0] = 100.8f; /* 回摆入死区 -> 撤销候选 */
iec104_pts_cov_check(&cov);
s_f_yc[0] = 101.4f; /* 重新起算:单点不报 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 pullback cleared pending count");
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-027 two fresh crossings confirm again");
/* 壳语义钉(lead 裁⑤):iec104_pts_yc_set_deadband 成功须清零该点半截确认
* 计数——若不清,下面第一拍就凑满 2 点直发,与"跨档位重新起算"矛盾 */
s_f_yc[0] = 100.0f; /* rep=101.4 起 d=1.4 > 0.5:第 1 越挂起 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 shell pin arms first");
iec104_pts_yc_set_deadband(0u, 0.4f);
s_f_yc[0] = 100.5f; /* d=0.9 > 0.4:计数已清则仍只是第 1 越 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 shell clears pending count");
iec104_pts_cov_check(&cov); /* 新档连续第 2 越 → 确认 */
CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-027 reconfirms after shell clear");
}
static void tc_cov_qds_seq(void)
{
iec104_pts_cov_t cov;
reset_all(1000u); /* 假源段:品质翻转 */
s_f_yc[1] = 50.0f;
iec104_pts_cov_check(&cov);
s_f_yc_rc[1] = -1;
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0002u) != 0u, "TC-IEC104-028 valid->IV reported at once");
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0002u) == 0u, "TC-IEC104-028 IV steady state not repeated");
s_f_yc_rc[1] = 0;
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0002u) != 0u, "TC-IEC104-028 IV->valid recovery reported at once");
iec104_pts_init(); /* 板端段:seq 去重 */
iec104_pts_bind_src(NULL);
(void)phasor_init();
(void)meas_init();
snap_beats(20, 100.0, 0);
snap_pub(200u, 0u); /* seq=1 */
iec104_pts_cov_check(&cov); /* 建线帧 */
CHECK(cov.yc_changed == 0u, "TC-IEC104-028 baseline frame silent");
snap_beats(20, 200.0, 0);
snap_pub(400u, 0u); /* seq=2:第 1 越点 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-028 one new frame arms only");
iec104_pts_cov_check(&cov);
iec104_pts_cov_check(&cov); /* 无换帧:不得计为第 2 点 */
CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-028 same frame never counts twice");
snap_beats(20, 200.0, 0);
snap_pub(600u, 0u); /* seq=3:连续第 2 越点 */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-028 second new frame confirms");
snap_pub(800u, 0xE6u); /* 换帧仅变品质(削顶注入) */
iec104_pts_cov_check(&cov);
CHECK((cov.yc_changed & 0x00E6u) == 0x00E6u,
"TC-IEC104-028 clipped flip per frame reported as a set");
}
/* 卡 10:0x8020~0x8058 读写路由(板端默认源,run_par 表先 / prot_set 后) */
static void tc_pts_rpar_route(void)
{
float v;
reset_all(1000u);
iec104_pts_bind_src(NULL);
CHECK((iec104_pts_set_read(0x8030u, &v) == 0) && (fabsf(v - 0.5f) < 1e-4f),
"TC-IEC104-029 0x8030 deadband UA reads factory default");
CHECK((iec104_pts_set_read(0x8038u, &v) == 0) && (fabsf(v - 0.5f) < 1e-4f),
"TC-IEC104-029 0x8038 deadband freq reads factory 0.5 Hz");
/* 0x8027~0x802A = prot_set 告警段(欠压默认 20 V)——run_par 精确匹配
* 永不覆盖;路由顺序的直接证据 */
CHECK((iec104_pts_set_read(0x8027u, &v) == 0) && (fabsf(v - 20.0f) < 0.01f),
"TC-IEC104-029 0x8027 still served by prot_set table");
CHECK(iec104_pts_set_read(0x8039u, &v) == -1, "TC-IEC104-029 gap address unknown");
/* C_SE 两段式:选择段校验不提交,执行段落真源 */
CHECK((iec104_pts_set_write(0x8031u, 0.7f, 1u) == 0)
&& (fabsf(iec104_pts_yc_deadband(1u) - 0.5f) < 1e-4f),
"TC-IEC104-029 select phase validates without commit");
CHECK((iec104_pts_set_write(0x8031u, 0.7f, 0u) == 0)
&& (fabsf(iec104_pts_yc_deadband(1u) - 0.7f) < 1e-4f)
&& (run_par_dirty_peek() != 0u),
"TC-IEC104-029 execute commits and marks dirty");
CHECK(iec104_pts_set_write(0x8031u, 51.0f, 1u) == -1,
"TC-IEC104-029 over-range rejected in select");
/* k/b 覆盖直通 adc_cal(UA 合法带 = ROM x[0.5,2.0] = [76.29,305.17]) */
CHECK((iec104_pts_set_write(0x8040u, 300.0f, 0u) == 0)
&& (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) < 0.01f),
"TC-IEC104-029 k override lands in adc_cal");
CHECK(iec104_pts_set_write(0x8040u, 400.0f, 0u) == -1,
"TC-IEC104-029 k band reject (via adc_cal)");
CHECK((iec104_pts_set_write(0x8048u, -5.0f, 0u) == 0)
&& (fabsf(adc_cal_b_codes(ADC_SEM_UA) + 5.0f) < 1e-3f)
&& (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) < 0.01f),
"TC-IEC104-029 b single-side write keeps k (RMW)");
CHECK((iec104_pts_set_read(0x8048u, &v) == 0) && (fabsf(v + 5.0f) < 1e-3f),
"TC-IEC104-029 b echo via run_par read path");
run_par_init(); /* 收尾:把 k/b 脏与副本复位留给后续 TC 的干净出厂态 */
}
int main(void)
{
printf("=== IEC104 Protocol Stack Host Tests ===\n");
tc_cp56();
tc_ioa_float();
tc_apci();
tc_startdt();
tc_open_ignore();
tc_interrogation();
tc_cov_yx();
tc_cov_yc();
tc_k_window();
tc_w_thresh();
tc_t2();
tc_t3_t1();
tc_dc();
tc_dc_yk12();
tc_yk_records();
tc_se();
tc_rd();
tc_cs();
tc_err_resp();
tc_stopdt();
tc_seq_err();
tc_pts();
tc_ts_rp();
tc_periodic();
tc_cov_kfull();
tc_pts_snap();
tc_pts_freq();
tc_db_runtime();
tc_cov_qds_seq();
tc_pts_rpar_route();
printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}