Files
STM32F4-Base/test/host/iec104/iec104_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +08:00

945 lines
33 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:死区内不发 / 越死区发
* 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 暂存,窗开后补发)
*
* 通过:全部 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 "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();
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] = 105.0f; /* 死区 10 内 */
iec104_link_poll(&g_lk, now + 100u);
CHECK(cap_i_count() == 0, "TC-008 inside the deadband nothing is uploaded");
s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */
iec104_link_poll(&g_lk, now + 200u);
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) == 130.0f), "TC-008 crossing the deadband uploads");
}
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");
}
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();
printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}