Files
STM32F4-Base/test/host/iec104/iec104_test.c
T
wandering1 8c72698666 feat(运行参数): 卡10 run_par真源收编+104段0x8020~0x8058读写映射(道D M6-①)
新增 App/algo/run_par.c/h(「数据可信」卡 10,lead 四裁落地):
- [run_par] cfg 段:遥测死区 9 键 int32 毫单位(db_ua…db_freq),工厂默认 =
  卡 6 额定 0.5% 档(500mV×4 / 5mA×4 / 500mHz);注册走 adc_cal 同款内核前
  契约(app_main_init),不挂回调 = 引擎副本即生效值;键数 9≤16 不拆段
- 真源收编(裁③):pts 层 s_yc_db 运行时副本删除,COV 每拍实时读 run_par
  (协议→算法向下依赖,同 meas 先例;无投递无缓存 = 顺序/幂等平凡成立);
  iec104_pts_yc_set_deadband()/deadband() 保留为转发壳,壳内保留"成功清
  半截确认计数"语义并由 TC-IEC104-027 追加断言钉住(裁⑤)
- 104 地址映射(裁①):0x8030~0x8038 死区 9 点 / 0x8040~0x8047 k 覆盖 /
  0x8048~0x804F b 覆盖(逐项地址本仓自定——复现规划 §5.4 权威表不在仓,
  送检点表到位后替换 run_par.c 顶部常量段一处;已在 IEC104 §3.4 表头挂账);
  路由顺序 = run_par 表先、未命中落 prot_set——0x8027~0x802A 属 prot_set
  告警段永不覆盖,TC-IEC104-029 双向往返钉死;空隙地址自然 COT14/PN
- 通道系数覆盖 = adc_cal_set 代理(裁②a):合法带/越带 [ERR] 留痕全复用,
  run_par 不存第二份系数(防双真源);单侧写 RMW 用另一侧当前生效值;
  回显 = 生效值视图(文件坏值被哨兵回退 ROM 时主站看到的就是下游吃到的);
  adc_cal 配套:+adc_cal_b_codes() 读取口(回显/RMW 用)+ set() 授权写者
  注记扩至 iec104Task 直通路径(单字原子契约同「数据可信」§4.4)
- 持久化(裁②b):新事件位 SYS_EVT_RUN_PAR_SAVE(1<<8);iec104Task 主循环
  peek 投递 → defaultTask run_par_store_save() 一条事件一趟 cfg_file_save
  整档外科(覆盖 [run_par] 9 键 + 联动 [adc_cal] 两段);失败脏保留 = 天然
  重试(B7 prot_set 同款)。C_SE 现行无"暂停/测试态"门控——文档现行如此
  不造新语义,送检若要求接线点已写进 §3.4 注(裁④)

测试:新建 test/host/run_par(TC-RUNPAR-001~008,67 断言,真实 cfg 引擎 +
真实 adc_cal + 内存虚拟盘同 adc_cal 套件先例;[run_par] 注册契约/落盘往返/
外改文件即副本生效/两段式不提交不置脏/合法带拒+RMW+生效值回显/init 幂等
重注册留痕);iec104 套扩真链 TU(cfg_file+adc_cal+run_par 非桩,141 断言:
+TC-IEC104-029 路由 + 壳清确认计数 3 断言);meas 套静态更名 s_snap*→
s_ms_snap*(与 cfg_file.c s_snap 同 TU 相撞,D1 同款改名法,226 断言不变);
iec104_serv 套补 run_par_dirty_peek 假件。id_registry 建族 TC-HOST-RUNPAR
(游标 008)+ TC-HOST-IEC104 游标 028→029;新钩子 run-par-unit-test +
iec104 钩子触发面随真链扩;uvprojx +run_par.c(合法入库)+ SIM_EXEMPT 登记
(模拟器不编 104 协议,已核实 CMake 面)。

文档:IEC104 §3.4 运行参数段表(含送检替换账/门控未来条件/代理声明)、§7、
§8 第 4 条、变更记录;网络配置说明新增 §3.5 [run_par] 键表;复现规划 §5.4
补运行参数段落实进度。**遗留(交 lead)**:数据可信 §4.4 "引擎 RAM 副本"
一节当时的措辞对象含 pts 侧副本,s_yc_db 删除后该句过时(裁③b 点名,§4.4
在写域外未动)。

gate:@build 0/0(Code=219620 RO=22036 RW=1280 ZI=132864);vs 0cbae7d:
Flash +1912B、RAM(RW+ZI) +8B(37B 新静态被链接器并洞吸收);iec104Task 栈
1456B/margin 43%、defaultTask 3032B 不变;id-check/doc-links/cppcheck/
coding-standard 实跑过;--all-files 全量 45 钩子实跑(43 绿 + 仅 mem-budget /
mem-numbers 两红 = 台账受管区 vs .map 漂移,回填属 lead 单写者区,批 1/道D-1
/D2 同口径):gate: only mem-budget/mem-numbers pending lead ledger backfill
BOM/CR/U+FFFD/字面量 ASCII 逐文件自查通过。
2026-10-25 13:15:41 +08:00

1305 lines
49 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-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++;
}
/* ---- 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-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-001 CP56 round trip on the leap day 2024-02-29");
b[2] |= 0x80u;
CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-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-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-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-002 float in IEEE754 little endian");
iec104_put_float(b, -12.5f);
CHECK(iec104_get_float(b) == -12.5f, "TC-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-003 a half frame triggers nothing");
iec104_link_on_rx(&g_lk, &b[3], 3u, now);
CHECK(cap_u_count(0x0Bu) == 1, "TC-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-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-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-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-004 open -> OPEN");
handshake(now);
CHECK((cap_u_count(0x0Bu) == 1)
&& (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-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-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-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-006 actcon is sent right away, COT7");
iec104_link_poll(&g_lk, now);
CHECK(cap_i_count() == 5, "TC-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-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-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-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-006 YC value 111.0 with QDS=0");
CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 an invalid channel sets QDS=IV");
}
CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u),
"TC-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-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-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-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-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-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-007 the CP56 time tag decodes");
CHECK(back == s_epoch_val, "TC-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-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-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-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-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-009 the k=12 window caps the outstanding frames");
feed_rd(now);
CHECK(cap_i_count() == 12, "TC-009 the 13th frame is suppressed by the k window");
feed_s(12u, now); /* 全确认 → 窗开 */
feed_rd(now);
CHECK(cap_i_count() == 13, "TC-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-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-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-011 no S before t2 expires");
iec104_link_poll(&g_lk, now + 10001u);
CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-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-012 no TESTFR before t3 expires");
iec104_link_poll(&g_lk, now + 20001u);
CHECK(cap_u_count(0x43u) == 1, "TC-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-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-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-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-013 the select phase answers actcon");
CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-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-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-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-013 a YK without output hardware returns PN in the select phase");
}
/* TC-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-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-024 the YK12 select phase answers actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-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-024 the YK12 execute phase answers actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 the execute phase drives the soft plate to the released state");
CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed seals atomically and selfcheck passes");
CHECK(prot_set_dirty_peek() == 1u, "TC-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-024 the engage command executes with actcon");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-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-024 an invalid DPC returns PN in the select phase");
}
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-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-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-014 executing a setting answers actcon");
CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 the setting really was written (real prot_set)");
CHECK(prot_set_selfcheck() == 0, "TC-014 sealing after execute keeps selfcheck passing");
CHECK(prot_set_dirty_peek() == 1u, "TC-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-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-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-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-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-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-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-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-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-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-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-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-018 in STOPPED state the data service is off");
feed_u(0x43u, now);
CHECK(cap_u_count(0x83u) == 1, "TC-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-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-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-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-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-020 not ready gives NT");
iec104_pts_yx_read(0u, &d);
CHECK(d == IEC104_DIQ_IV, "TC-020 an invalid telemetry point gives IV");
CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u),
"TC-020 the is_dp boundary");
s_f_bits = (1uLL << 15);
iec104_pts_yx_read(15u, &d);
CHECK(d == 1u, "TC-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-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-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-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-022 the cyclic YC group uses COT=1");
CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-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-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-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_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;
}