Files
STM32F4-Base/test/host/iec104/iec104_test.c
T
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

1619 lines
66 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 协议核心)
* + 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 <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" /* 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 <stdlib.h>
#include "cfg_file_store.h"
#define RP_VFS_CAP 8192u
static uint8_t rp_s_vfs[RP_VFS_CAP];
static size_t rp_s_vfs_len = 0u;
static uint32_t rp_s_vfs_mtime = 100u;
static int rp_s_vfs_exists = 0;
void *cfg_store_alloc(size_t size) { return malloc(size); }
void cfg_store_free(void *p_buf) { free(p_buf); }
uint32_t cfg_store_heap_free(void) { return 0u; }
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) {
size_t n;
(void)p_path;
if (!rp_s_vfs_exists) {
return -1;
}
n = rp_s_vfs_len;
if (n > cap) {
n = cap;
}
(void)memcpy(p_buf, rp_s_vfs, n);
*p_nread = n;
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
(void)p_path;
if (len > RP_VFS_CAP) {
return -1;
}
(void)memcpy(rp_s_vfs, p_buf, len);
rp_s_vfs_len = len;
rp_s_vfs_exists = 1;
rp_s_vfs_mtime++;
return 0;
}
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
(void)p_path;
if (!rp_s_vfs_exists) {
return -1;
}
p_st->size = (uint32_t)rp_s_vfs_len;
p_st->mtime = rp_s_vfs_mtime;
return 0;
}
int cfg_store_remove(const char *p_path) {
(void)p_path;
rp_s_vfs_exists = 0;
rp_s_vfs_len = 0u;
return 0;
}
const char *cfg_store_config_path(void) { return "/config.ini"; }
const char *cfg_store_tmp_path(void) { return "/config.tmp"; }
/* ---- sys_log 桩:只计数自检留痕 ---- */
#include "sys_log.h"
static int rp_s_syslog_count = 0;
void syslog_post(syslog_id_t id, const char *desc) {
(void)id;
(void)desc;
rp_s_syslog_count++;
}
/* ---- syslog_post_quiet 桩:记下投了什么(task-10 遥控 / 操作记录用) ----
* 真机上 quiet 与 post 的差别只在"投递侧不打日志",收/丢判据逐条相同;
* 本套件是协议栈单测,不验投递本身(那归 test/host/sys_log_mcu),只验
* **板端源在每条返回路径上投了哪一类、哪一句**——所以这里存成环形日志。 */
#define RP_REC_MAX 16u
static uint8_t rp_s_rec_id[RP_REC_MAX];
static char rp_s_rec_desc[RP_REC_MAX][SYSLOG_DESC_MAX + 1u];
static uint32_t rp_s_rec_n; /* 累计条数(不回绕,供"一条都没多投"断言) */
static uint32_t rp_s_drop_quiet; /* 本桩不模拟丢弃,恒 0 */
void syslog_post_quiet(syslog_id_t id, const char *desc) {
uint32_t slot;
size_t dl = 0u;
if (desc == NULL) {
return;
}
slot = rp_s_rec_n % RP_REC_MAX;
rp_s_rec_id[slot] = (uint8_t)id;
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
dl++;
}
(void)memcpy(rp_s_rec_desc[slot], desc, dl);
rp_s_rec_desc[slot][dl] = '\0';
rp_s_rec_n++;
}
uint32_t syslog_post_dropped(void) { return rp_s_drop_quiet; }
/* 第 k 条(0 起)记录;越界返回 -1 */
static int rp_rec_at(uint32_t k, syslog_id_t id, const char *desc)
{
if (k >= rp_s_rec_n) {
return -1;
}
return (rp_s_rec_id[k % RP_REC_MAX] == (uint8_t)id)
&& (strcmp(rp_s_rec_desc[k % RP_REC_MAX], desc) == 0) ? 1 : 0;
}
/* 某类记录一共投了几条 */
static uint32_t rp_rec_cnt(syslog_id_t id)
{
uint32_t i, n = 0u;
for (i = 0u; (i < rp_s_rec_n) && (i < RP_REC_MAX); i++) {
if (rp_s_rec_id[i] == (uint8_t)id) {
n++;
}
}
return n;
}
static void rp_rec_clear(void)
{
rp_s_rec_n = 0u;
(void)memset(rp_s_rec_id, 0, sizeof(rp_s_rec_id));
(void)memset(rp_s_rec_desc, 0, sizeof(rp_s_rec_desc));
}
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */
#include "adc_input.h"
static const adc_channel_desc_t rp_s_desc[ADC_SEM_COUNT] = {
{ ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */
{ ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */
{ ADC_SRC_STM32, 3u }, /* UC = CH3 */
{ ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */
{ ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */
{ ADC_SRC_STM32, 6u }, /* IB = CH6 */
{ ADC_SRC_STM32, 1u }, /* IC = CH1 */
{ ADC_SRC_STM32, 2u }, /* IN = CH2 */
{ ADC_SRC_STM32, 5u }, /* TEMP = CH5 */
};
const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) {
if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) {
return NULL;
}
return &rp_s_desc[(unsigned)ch];
}
/* dbg 计数定义(stub/dbg_log.h 声明 extern) */
int g_dbgstub104_err_count = 0;
int g_dbgstub104_info_count = 0;
int g_dbgstub104_debug_count = 0;
#include "cfg_file.c"
#include "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;
}