/* * 模块名称:IEC 60870-5-104 被控站协议栈主机侧单元测试 * 模块功能:在 PC 上对 App/protocol/iec104 全栈(asdu + pts + link)做离线验证。 * * 被测对象:真实源文件(#include 原样纳入): * iec104_asdu.c + iec104_pts.c + iec104_link.c(B4 协议核心) * + iec104_comm.c(道G task-8 [comm] 段,真链注册) * 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(104 改读快照起 = 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 失败腿不可达,理由写在 * 该用例注释里,没有为它伪造定值范围) * —— 以下为道G task-8「[comm] 段与 104 消费」(2026-10-26;置于 main 末位, 段注册表清理不影响其余 TC)—— * TC-IEC104-031 [comm] 注册 + 无文件出厂模板落盘(含 [comm]/ca=1/port=2404, * 模板长度钉上限)+ 改档 poll 沉降重载 getter 跟变 * TC-IEC104-032 文件值带外(ca=0 / port=80)→ RAM 存原值、getter 自愈回默认; * setter 守卫(0 拒 / 1023 拒 / 65535 收 / 带内提交) * TC-IEC104-033 ca 端到端:ca=comm 值时 C_RD 正答 COT5 且 TX 头回显同值; * 旧写死值 ca=1 变错地址 → COT13+EVI(证明过滤真源已迁移) * TC-IEC104-034 [comm] 落盘脏位:置脏/清脏/空转/带外拒不置脏(道G task-9, * 通讯设置页保存行 WAIT/OK 的判据源) * 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 暂存,窗开后补发) * —— 以下为「数据可信」104 改读快照与频率点绑定(道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 清零 * (频率点绑定 DoD 原句) * TC-IEC104-027 死区运行时化(运行参数段后 = run_par 真源 + 转发壳):set/get * + 非法拒 + 运行档压旧档 + 2 点确认/回摆撤销 + 壳清半截确认计数 * TC-IEC104-028 品质翻转直报(假源)+ 板端 seq 去重(同帧不计两点)+ * 换帧只变品质时按集合直报 * TC-IEC104-029 运行参数段路由:0x8030/0x8038 命中 / 0x8027 仍归 prot_set / * 空隙 COT14 / C_SE 两段式 / k 带拒 / b 单侧 RMW 保 k * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-12(B4) */ #include #include #include 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" /* 104 改读快照:板端默认 YC 源 = L2 遥测快照(真实链,非桩) */ /* ===================== 运行参数段真链桩(同 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 #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 "iec104_comm.c" /* 道G task-8:[comm] 段(配对模块,注册走真引擎) */ #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(); /* 运行参数段:死区/脏复位到工厂默认(重复注册 -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 内(工程量重定后的二次值口径:0.5 %×100 V, * 旧拟定码值死区 10 已由「数据可信」下游常数按工程量重定) */ 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"); } /* ====== 104 改读快照与频率点绑定:板端默认源(真实 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)——运行参数段复位入口 */ 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"); } /* 运行参数段: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 的干净出厂态 */ } /* ===================== 道G task-8:[comm] 段 ===================== * 真实 cfg 引擎(上方已 #include cfg_file.c)+ 运行参数段的 RAM 虚拟盘。 * 白盒清理 = cfg_file_test 同款(生产静态无公开复位接口,白盒单测可接受)。 * ⚠ tc_comm 必须排在 main 末位:comm_reg_reset 会清空**整张**段注册表。 */ static void comm_reg_reset(void) { (void)memset(s_sections, 0, sizeof(s_sections)); (void)memset(&s_base, 0, sizeof(s_base)); (void)memset(&s_last, 0, sizeof(s_last)); s_stable = 0u; s_base_valid = 0u; (void)memset(s_snap, 0, sizeof(s_snap)); (void)memset(s_sec_hdr, 0, sizeof(s_sec_hdr)); (void)memset(s_key_line, 0, sizeof(s_key_line)); rp_s_vfs_exists = 0; rp_s_vfs_len = 0u; } /* 向 RAM 虚拟盘放一份 config.ini(mtime 令牌 +1 = 外部写盘语义) */ static void comm_put(const char *text) { size_t n = strlen(text); (void)memcpy(rp_s_vfs, text, n); rp_s_vfs_len = n; rp_s_vfs_exists = 1; rp_s_vfs_mtime++; } /* 虚拟盘前 n 字节取成 C 串(断言模板/文件内容用) */ static const char *comm_vfs_head(size_t n) { static char buf[1024]; if (n > rp_s_vfs_len) { n = rp_s_vfs_len; } if (n > sizeof(buf) - 1u) { n = sizeof(buf) - 1u; } (void)memcpy(buf, rp_s_vfs, n); buf[n] = '\0'; return buf; } static void tc_comm(void) { cap_t c; uint8_t a[24]; uint16_t m; uint32_t now = 1000u; static iec104_link_cfg_t comm_cfg; /* 生命周期覆盖整链(link 存指针) */ /* ---- TC-IEC104-031:注册 / 出厂模板 / poll 沉降重载 ---- */ printf("[TC-IEC104-031] [comm] register + factory template + settle reload\n"); comm_reg_reset(); CHECK(iec104_comm_init() == 0, "TC-IEC104-031 [comm] registers into engine"); CHECK((iec104_comm_ca() == IEC104_COMM_DEF_CA) && (iec104_comm_port() == IEC104_COMM_DEF_PORT), "TC-IEC104-031 factory defaults before any file (ca=1 port=2404)"); CHECK(cfg_file_load() == 0, "TC-IEC104-031 load without file falls back to defaults"); CHECK(rp_s_vfs_exists == 1, "TC-IEC104-031 load generates factory template"); /* 容量钉:本套单 comm 段模板必须远小于 4096(生产 6 段全量估算 ~1.3 KB, * 见 docs/子系统设计/配置文件设计说明.md;上限被顶 = 文件判损坏重建) */ CHECK(rp_s_vfs_len <= 1024u, "TC-IEC104-031 single-comm template well under 4096 cap"); CHECK(strstr(comm_vfs_head(1023u), "[comm]") != NULL, "TC-IEC104-031 template carries [comm]"); CHECK(strstr(comm_vfs_head(1023u), "ca=1") != NULL, "TC-IEC104-031 template writes ca default"); CHECK(strstr(comm_vfs_head(1023u), "port=2404") != NULL, "TC-IEC104-031 template writes port default"); cfg_file_poll(); /* 基线 = 出厂模板(首 poll 只建基线,cfg_file.c:1318) */ comm_put("[comm]\r\nca=7\r\nport=8104\r\n"); /* 外部写盘:内容变 + 令牌变 */ cfg_file_poll(); /* 检出变更(沉降 1) */ cfg_file_poll(); /* 稳定确认(沉降 2)-> 重载 */ CHECK((iec104_comm_ca() == 7u) && (iec104_comm_port() == 8104u), "TC-IEC104-031 edited file reload moves both getters"); /* ---- TC-IEC104-032:带外自愈 + setter 守卫 ---- */ printf("[TC-IEC104-032] [comm] out-of-band self-heal + setter guards\n"); comm_put("[comm]\r\nca=0\r\nport=80\r\n"); cfg_file_poll(); cfg_file_poll(); CHECK((s_comm.ca == 0u) && (s_comm.port == 80u), "TC-IEC104-032 raw out-of-band values land verbatim in RAM copy"); CHECK((iec104_comm_ca() == IEC104_COMM_DEF_CA) && (iec104_comm_port() == IEC104_COMM_DEF_PORT), "TC-IEC104-032 getters self-heal to factory defaults (adc_cal band precedent)"); CHECK(iec104_comm_ca_set(0u) == -1, "TC-IEC104-032 ca=0 rejected by setter"); CHECK(iec104_comm_ca() == IEC104_COMM_DEF_CA, "TC-IEC104-032 rejected write does not commit"); CHECK(iec104_comm_port_set(1023u) == -1, "TC-IEC104-032 reserved-band port rejected"); CHECK((iec104_comm_port_set(IEC104_COMM_PORT_MIN) == 0) && (iec104_comm_port() == IEC104_COMM_PORT_MIN), "TC-IEC104-032 band edge 1024 accepted"); CHECK((iec104_comm_ca_set(IEC104_COMM_CA_MAX) == 0) && (iec104_comm_ca() == IEC104_COMM_CA_MAX), "TC-IEC104-032 ca band edge 65535 accepted"); CHECK(iec104_comm_init() == -1, "TC-IEC104-032 re-init is idempotent-reject (dup name)"); CHECK((iec104_comm_ca() == IEC104_COMM_DEF_CA) && (iec104_comm_port() == IEC104_COMM_DEF_PORT), "TC-IEC104-032 re-init still resets RAM copy to factory defaults"); /* ---- TC-IEC104-033:ca 端到端(消费真源 = [comm],旧写死 1 变错地址) ---- */ printf("[TC-IEC104-033] comm ca reaches the 104 wire\n"); reset_all(now); /* 复位假源/链路(g_cfg ca=1) */ (void)iec104_comm_ca_set(7u); comm_cfg.ca = iec104_comm_ca(); /* 板端 s_apply_comm_ca() 的同款动作 */ comm_cfg.send_apdu = cap_send; comm_cfg.clock_sync = f_clock; comm_cfg.epoch_ms = f_epoch; iec104_link_init(&g_lk, &comm_cfg, NULL); iec104_link_open(&g_lk, now); m_reset(); handshake(now); m = iec104_asdu_put_hdr(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 7u); iec104_put_ioa(&a[m], 0x4011u); m += 3u; feed_i(a, m, now); CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ) && (c.ca == 7u), "TC-IEC104-033 C_RD accepted at comm ca, TX header echoes comm ca"); m = iec104_asdu_put_hdr(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u); iec104_put_ioa(&a[m], 0x4011u); m += 3u; feed_i(a, m, now); CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA) && ((c.oa & 0x80u) != 0u), "TC-IEC104-033 the retired hardwired ca=1 now answers COT13+EVI"); (void)iec104_comm_ca_set(IEC104_COMM_DEF_CA); /* 收尾:复位出厂 ca */ (void)iec104_comm_port_set(IEC104_COMM_DEF_PORT); /* ===== TC-IEC104-034 [comm] 落盘脏位与 store_save(道G task-9,页面链前置)===== * 契约 = iec104_comm.h 的两个新出口:setter 成功置脏 / store_save 成功清脏 / * 无脏空转返 0 / 带外拒不置脏。defaultTask 每轮无条件调 store_save(挂载点 * 在 App/task/default_task.c,run_par_store_save 同点位),屏上 WAIT/OK 全看 * 这枚脏位——脏位错 = 通讯页保存行说谎,这是本条用例钉的东西。 * 编号 034 = 向 Lead 申请发号(cursor_owner_only),cursor 现 033。 */ printf("[TC-IEC104-034] comm dirty bit drives the store_save cycle\n"); CHECK(iec104_comm_dirty_peek() == 1u, "TC-IEC104-034 successful setters leave the section dirty"); CHECK((iec104_comm_store_save() == 0) && (iec104_comm_dirty_peek() == 0u), "TC-IEC104-034 store_save clears dirty only on write success"); CHECK((strstr(comm_vfs_head(rp_s_vfs_len), "ca=1") != NULL) && (strstr(comm_vfs_head(rp_s_vfs_len), "port=2404") != NULL), "TC-IEC104-034 the saved file carries the committed values"); CHECK((iec104_comm_store_save() == 0) && (iec104_comm_dirty_peek() == 0u), "TC-IEC104-034 a save with no dirty is a silent no-op (poll cost)"); CHECK((iec104_comm_ca_set(0u) == -1) && (iec104_comm_dirty_peek() == 0u) && (iec104_comm_ca() == IEC104_COMM_DEF_CA), "TC-IEC104-034 out-of-band reject neither dirties nor moves the copy"); } 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(); tc_comm(); printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }