W1 功能主线完成:内存台账三件套(W1-0) + IEC104 上行(B4) + protectTask 板端接线(B5)
W1-0 台账三件套:scripts/ram_baseline.py(四模式)+ ram_baseline.tsv 趋势 + docs/内存管理/RAM预算台账.md + pre-commit mem-budget 钩子(RW_IRAM1/RW_CCM >92% 即 FAIL + Top-10 附表);审查报告 §5.5 三条闭环。 B4 IEC 60870-5-104 上行(被控站,TCP 2404):App/protocol/iec104/ 三模块 (asdu 编解码 / pts 点表+数据源 vtable / link APCI+FSM,纯逻辑零 I/O)+ App/task/iec104_serv.c(iec104Task,NET_READY 屏障 100ms 分片喂位)。 82 断言 L0.5 全绿 + iec104-unit-test 钩子(第 31 钩子); 设计说明 docs/子系统设计/IEC104上行设计说明.md 全量回填。 B5 protectTask(L3 板端接线):App/task/protect_task.c——20ms 判据节拍 prot_elm_run + L5-4 采样故障闭锁(adc_health 电平位→元件复位)+ L5-1 定值 校验和巡检;新增 prot_elm_request_reset() 收口跨上下文复归(104 YK11 落请求、 eval 头部消费);定值/元件初始化移入 app_main_init(内核前)。TC-PE-010 (prot_elm 28 用例)。 门禁与内存:TASK_WD_PROTECT=8 入 required_mask(产品 0x77→0x1F7、HW 层 0x1F5,看门狗单测 39 断言同步);FreeRTOS 堆 48→56KB(.ioc L1 参数 + FreeRTOSConfig.h 同步,RAM预算台账 §6 登记配额 48→56KB)。 ZI 144,008→152,204(+8,196 B = 堆 +8KB,B5 扩容);Code=204,348。 stack_report:protectTask 512 words 静态链 872 B/余 57%、iec104Task 640 words 1456 B/余 43% 均 PASS;RW_IRAM1 73.1% / RW_CCM 87.9%(<92%)。 验证:@build 0 错 0 警;ci_local 全绿(31 钩子 + 13 主机侧单测); doc-links 135 文档 PASS。登记簿 B4/B5 销项、W1 波次完成;保护规划/看门狗 说明/AGENTS(9 任务)/RAM 三文档全部同步。 遗留(验收窗口,非代码空洞):台架第三方 104 主站往返(送检窗口)、 标准源注入动作判据、@test PROT 锚点 + protect 栈水位 + 新堆 min_ever 复测。
This commit is contained in:
+11
-4
@@ -1,11 +1,18 @@
|
||||
# App/protocol/ — 通信协议(预留)
|
||||
# App/protocol/ — 通信协议(纯逻辑层)
|
||||
|
||||
> **占位目录**:规划用于存放自定义通信协议实现(解析 / 组帧 / 状态机)。
|
||||
> 当前为空(仅 `.gitkeep`),协议相关实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c` + `App/util/crc.c` + `App/util/ringbuf.c`
|
||||
> 存放**跨接口复用的协议逻辑**(解析 / 组帧 / 状态机),零 I/O、依赖注入,
|
||||
> 可被任意任务/接口承载;板端 I/O 胶水在对应 task(如 `App/task/iec104_serv.c`)。
|
||||
> 其余协议实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c`(CRC 在驱动内)
|
||||
> - 网络侧:`Drivers/BSP/NET/`
|
||||
> - 远端显示:`docs/UI设计/RemoDispBus协议与接收设计说明.md`
|
||||
|
||||
## 已落地协议
|
||||
|
||||
| 子目录 | 协议 | 说明 |
|
||||
|---|---|---|
|
||||
| `iec104/` | IEC 60870-5-104 被控站(TCP 2404 上行) | **纯逻辑**三件套(asdu/pts/link,零 I/O,数据源 vtable 注入)→ 全栈 L0.5 主机单测;板端胶水与点表决策 → `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
|
||||
## 何时使用本目录
|
||||
|
||||
当出现**跨接口复用的协议逻辑**(如统一的帧编解码器、协议状态机)时,落到此处,
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:实现 iec104_asdu.h——ASDU 头解析/组装、IOA 3B 小端、IEEE754
|
||||
* 短浮点小端、CP56Time2a ↔ epoch 毫秒(UTC 分解,Hinnant civil
|
||||
* 日历算法,不依赖 time.h → ARMCC/MSVC 双端可移植)。
|
||||
* 线程约定:全部纯函数,无状态 → 任意上下文可调。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* COT 首字节:bit0..5 = 值,bit6 = PN,bit7 = T;RBT 复用 bit4(C_IC 语义) */
|
||||
#define COT_VAL_MASK 0x3FU
|
||||
#define COT_FLAG_MASK 0xC0U
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(6B),信息体起点经 obj/obj_len 指出
|
||||
* 入口参数:buf/len = ASDU 首字节与总长;hdr = 输出
|
||||
* 返 回 值:0 成功 / -1 空指针或长度不足 6B
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr) {
|
||||
if ((buf == NULL) || (hdr == NULL) || (len < 6u)) {
|
||||
return -1;
|
||||
}
|
||||
hdr->type = buf[0];
|
||||
hdr->vsq = buf[1];
|
||||
hdr->cot = (uint8_t)(buf[2] & COT_VAL_MASK);
|
||||
hdr->cot_hi = (uint8_t)(buf[2] & 0xC0u);
|
||||
hdr->oa = buf[3];
|
||||
hdr->ca = (uint16_t)(buf[4] | ((uint16_t)buf[5] << 8));
|
||||
hdr->obj = &buf[6];
|
||||
hdr->obj_len = (uint16_t)(len - 6u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装 ASDU 头(OA 恒 0,EVI 由调用方置 buf[3];RBT 位透传)
|
||||
* 入口参数:cot = 6bit 原因值;cot_hi = 标志位(T/PN/RBT);ca = 公共地址
|
||||
* 返 回 值:固定 6(头字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca) {
|
||||
buf[0] = type;
|
||||
buf[1] = vsq;
|
||||
buf[2] = (uint8_t)(((uint8_t)(cot & COT_VAL_MASK))
|
||||
| (uint8_t)(cot_hi & COT_FLAG_MASK)
|
||||
| (uint8_t)(cot_hi & 0x10u)); /* RBT 位透传 */
|
||||
buf[3] = 0u; /* OA = 0(EVI 由调用方按需置位) */
|
||||
buf[4] = (uint8_t)(ca & 0xFFu);
|
||||
buf[5] = (uint8_t)(ca >> 8);
|
||||
return 6u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端写入
|
||||
*/
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa) {
|
||||
buf[0] = (uint8_t)(ioa & 0xFFu);
|
||||
buf[1] = (uint8_t)((ioa >> 8) & 0xFFu);
|
||||
buf[2] = (uint8_t)((ioa >> 16) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端读取
|
||||
*/
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf) {
|
||||
return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IEEE754 短浮点 → 小端字节序(104 恒小端,宿主字节序无关)
|
||||
*/
|
||||
void iec104_put_float(uint8_t *buf, float v) {
|
||||
/* 位模式小端:memcpy 不假设宿主字节序,逐字节拆给目标(104 恒小端) */
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
|
||||
(void)memcpy(tmp, &v, sizeof(tmp));
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
buf[i] = tmp[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
buf[i] = tmp[i];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:小端字节序 → IEEE754 短浮点
|
||||
*/
|
||||
float iec104_get_float(const uint8_t *buf) {
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
float v;
|
||||
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
tmp[i] = buf[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
tmp[i] = buf[i];
|
||||
#endif
|
||||
}
|
||||
(void)memcpy(&v, tmp, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---------------- CP56Time2a ↔ epoch ---------------- */
|
||||
|
||||
/* 2000-01-01 00:00:00 UTC 的 epoch 毫秒(钳位下限) */
|
||||
#define EPOCH_2000_MS 946684800000ULL
|
||||
|
||||
/*
|
||||
* 函数功能:公历(y>=1)→ 距 1970-01-01 天数(Hinnant civil_from_days 正向)
|
||||
* 说明:纯整数算法;m 取 1..12。
|
||||
*/
|
||||
static uint32_t s_days_from_civil(uint32_t y, uint32_t m, uint32_t d) {
|
||||
uint32_t yy = y - ((m <= 2u) ? 1u : 0u);
|
||||
uint32_t era = yy / 400u;
|
||||
uint32_t yoe = yy - (era * 400u);
|
||||
uint32_t mp = (m > 2u) ? (m - 3u) : (m + 9u); /* 3 月 = 月序 0 */
|
||||
uint32_t doy = ((153u * mp) + 2u) / 5u + d - 1u;
|
||||
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy;
|
||||
|
||||
return (era * 146097u) + doe - 719468u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:距 1970-01-01 天数 → 公历(Hinnant 反向)
|
||||
* 入口参数:z_in = 天数;y/m/d = 输出
|
||||
*/
|
||||
static void s_civil_from_days(uint32_t z_in, uint32_t *y, uint32_t *m, uint32_t *d) {
|
||||
uint32_t z = z_in + 719468u;
|
||||
uint32_t era = z / 146097u;
|
||||
uint32_t doe = z - (era * 146097u);
|
||||
uint32_t yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u;
|
||||
uint32_t yy = yoe + (era * 400u);
|
||||
uint32_t doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
|
||||
uint32_t mp = ((5u * doy) + 2u) / 153u;
|
||||
uint32_t mon = mp + ((mp < 10u) ? 3u : 0u);
|
||||
|
||||
*d = doy - (((153u * mp) + 2u) / 5u) + 1u;
|
||||
if (mp >= 10u) {
|
||||
mon = mp - 9u;
|
||||
}
|
||||
*m = mon;
|
||||
*y = yy + ((mon <= 2u) ? 1u : 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CP56Time2a → epoch 毫秒(UTC 分解)
|
||||
* 入口参数:buf = 7B;epoch_ms = 输出
|
||||
* 返 回 值:0 成功 / -2 IV(时间无效,调用方应拒绝)/ -1 字段越界
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms) {
|
||||
uint32_t ms_min, minute, hour, day, mon, year;
|
||||
|
||||
if ((buf == NULL) || (epoch_ms == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((buf[2] & 0x80u) != 0u) {
|
||||
return -2; /* IV:时间无效,调用方应拒绝 */
|
||||
}
|
||||
ms_min = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8); /* 0..59999 */
|
||||
minute = (uint32_t)buf[2] & 0x3Fu;
|
||||
hour = (uint32_t)buf[3] & 0x1Fu;
|
||||
day = (uint32_t)buf[4] & 0x1Fu;
|
||||
mon = (uint32_t)buf[5] & 0x0Fu;
|
||||
year = 2000u + ((uint32_t)buf[6] & 0x7Fu);
|
||||
if ((day == 0u) || (day > 31u) || (mon == 0u) || (mon > 12u)
|
||||
|| (hour > 23u) || (minute > 59u) || (ms_min > 59999u)) {
|
||||
return -1;
|
||||
}
|
||||
*epoch_ms = ((uint64_t)s_days_from_civil(year, mon, day) * 86400000ULL)
|
||||
+ ((uint64_t)hour * 3600000ULL)
|
||||
+ ((uint64_t)minute * 60000ULL)
|
||||
+ (uint64_t)ms_min;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(钳位 [2000-01-01, 2127-12-31],IV 恒 0)
|
||||
* 返 回 值:固定 7(字节数)
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf) {
|
||||
uint64_t days, rem, clamped;
|
||||
uint32_t y, m, d, ms_min;
|
||||
|
||||
clamped = (epoch_ms < EPOCH_2000_MS) ? EPOCH_2000_MS : epoch_ms;
|
||||
days = clamped / 86400000ULL;
|
||||
rem = clamped % 86400000ULL;
|
||||
s_civil_from_days((uint32_t)days, &y, &m, &d);
|
||||
if (y > 2127u) { /* 年字段 7bit 上限(2000+127) */
|
||||
y = 2127u;
|
||||
m = 12u;
|
||||
d = 31u;
|
||||
rem = 86399999ULL;
|
||||
}
|
||||
ms_min = (uint32_t)(rem % 60000ULL);
|
||||
buf[0] = (uint8_t)(ms_min & 0xFFu);
|
||||
buf[1] = (uint8_t)(ms_min >> 8);
|
||||
buf[2] = (uint8_t)((rem / 60000ULL) % 60ULL); /* min,IV=0 */
|
||||
buf[3] = (uint8_t)((rem / 3600000ULL) % 24ULL); /* hour */
|
||||
buf[4] = (uint8_t)(d & 0x1Fu); /* day */
|
||||
buf[5] = (uint8_t)(m & 0x0Fu); /* month */
|
||||
buf[6] = (uint8_t)((y - 2000u) & 0x7Fu); /* year */
|
||||
return 7u;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:104 规约应用服务数据单元(ASDU)的类型/传送原因常量、组帧与解析。
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧 L0.5 可离线单测
|
||||
* (登记簿 A-4 约束 1"协议解析与硬件 I/O 分离")。
|
||||
* 口径来源:docs/算法/继电保护功能复现规划.md §1.5(B0 产品定义)——
|
||||
* 公共地址 2 字节、传送原因 2 字节、信息体地址 IOA **3 字节**(101 为 2 字节);
|
||||
* 遥测上送**短浮点** M_ME_NC_1(系数 = 1);遥信双点上双点、其余单点;
|
||||
* 遥控**双点命令**(选择 82H / 执行 02H,即 QDC 的 S 位 + 值 2)。
|
||||
* 帧结构(本层只管 ASDU,APCI 6 字节头归 iec104_link):
|
||||
* [type 1B][vsq 1B][cot 2B][ca 2B]( [ioa 3B][元素...] )*n
|
||||
* 多信息体地址对象(SQ=0)每对象带独立 IOA;顺序对象(SQ=1)单 IOA + n 元素。
|
||||
* 创建日期:2026-10-12(B4,登记簿 §2.1 W1)
|
||||
*/
|
||||
#ifndef IEC104_ASDU_H
|
||||
#define IEC104_ASDU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 类型标识(DL/T 634.5104,仅定义本期子集) ---------------- */
|
||||
#define IEC104_T_M_SP_NA_1 1U /* 单点遥信 SIQ */
|
||||
#define IEC104_T_M_DP_NA_1 3U /* 双点遥信 DIQ */
|
||||
#define IEC104_T_M_ME_NC_1 13U /* 遥测短浮点 float+QDS(§1.5 口径) */
|
||||
#define IEC104_T_C_SE_NC_1 50U /* 定值写(短浮点)float+QDS */
|
||||
#define IEC104_T_C_DC_NA_1 52U /* 双点遥控 QDC(82H 选 / 02H 执行) */
|
||||
#define IEC104_T_C_IC_NA_1 100U /* 总召 RQT */
|
||||
#define IEC104_T_C_RD_NA_1 102U /* 读单个测量值 IOA only */
|
||||
#define IEC104_T_C_CS_NA_1 103U /* 时钟同步 CP56Time2a */
|
||||
#define IEC104_T_C_TS_NA_1 104U /* 试验 FBP 2B */
|
||||
#define IEC104_T_C_RP_NA_1 105U /* 进程复位 RPA 1B */
|
||||
|
||||
/* ---------------- 传送原因 COT(低字节 6bit;高字节 T|PN|RBT/…) ---------- */
|
||||
#define IEC104_COT_PERIODIC 1U /* 周期/循环 */
|
||||
#define IEC104_COT_BG_SCAN 2U /* 背景扫描(总召响应) */
|
||||
#define IEC104_COT_SPONT 3U /* 突发(cos) */
|
||||
#define IEC104_COT_INIT 4U /* 初始化(总召 + RBT=1 时用) */
|
||||
#define IEC104_COT_REQ 5U /* 请求(C_RD 响应) */
|
||||
#define IEC104_COT_ACT 6U /* 激活 */
|
||||
#define IEC104_COT_ACTCON 7U /* 激活确认 */
|
||||
#define IEC104_COT_ACTTERM 8U /* 激活终止 */
|
||||
#define IEC104_COT_DEACT 9U /* deactivate */
|
||||
#define IEC104_COT_DEACTCON 10U /* 停止激活确认 */
|
||||
#define IEC104_COT_UNK_COT 12U /* 未知传送原因 */
|
||||
#define IEC104_COT_UNK_CA 13U /* 未知公共地址 */
|
||||
#define IEC104_COT_UNK_IOA 14U /* 未知信息体地址(+EVI) */
|
||||
#define IEC104_COT_BIT_T 0x80U /* COT 高字节 bit7:试验 */
|
||||
#define IEC104_COT_BIT_PN 0x40U /* COT 高字节 bit6:否定确认 */
|
||||
#define IEC104_COT_BIT_RBT 0x10U /* COT 高字节 bit4:请求链路仿真(总召初始化) */
|
||||
|
||||
/* ---------------- VSQ ---------------- */
|
||||
#define IEC104_VSQ_SQ 0x80U /* 1 = 顺序地址(单 IOA + n 元素) */
|
||||
#define IEC104_VSQ_N(n) ((uint8_t)((n) & 0x7FU))
|
||||
|
||||
/* ---------------- 遥信/遥测品质描述位(本期只用溢出/异常/非当前) --------- */
|
||||
#define IEC104_QDS_OV 0x01U
|
||||
#define IEC104_QDS_BL 0x10U /* blocked(闭锁) */
|
||||
#define IEC104_QDS_NT 0x20U /* not top(暂态品质,遥测未就绪用) */
|
||||
#define IEC104_QDS_IV 0x80U /* invalid(无效,通道不可用用) */
|
||||
#define IEC104_DIQ_IV 0x80U
|
||||
#define IEC104_SIQ_IV 0x80U
|
||||
|
||||
/* ---------------- 双点值(DIQ/QDC 低 2bit) ---------------- */
|
||||
#define IEC104_DPC_DONTCARE 0U
|
||||
#define IEC104_DPC_OFF 1U
|
||||
#define IEC104_DPC_ON 2U
|
||||
#define IEC104_QDC_S 0x80U /* 1 = 选择,0 = 执行(§1.5:82H/02H) */
|
||||
|
||||
/* ---------------- 总召 RQT ---------------- */
|
||||
#define IEC104_RQT_GROUP1 20U /* 总查询(组 1) */
|
||||
|
||||
/* ---------------- ASDU 头(解析产物) ---------------- */
|
||||
typedef struct {
|
||||
uint8_t type; /* 类型标识 */
|
||||
uint8_t vsq; /* 可变结构限定词 */
|
||||
uint8_t cot; /* 传送原因低 6bit */
|
||||
uint8_t cot_hi; /* COT 首字节的高位标志(T=0x80 / PN=0x40 / RBT=0x10 原样保留) */
|
||||
uint8_t oa; /* 源发站地址(第 2 COT 字节;bit7 = EVI) */
|
||||
uint16_t ca; /* 公共地址(链路地址 = ASDU 公共地址,2B) */
|
||||
const uint8_t *obj; /* 信息体区首址(指向调用方缓冲,生命周期同 on_rx) */
|
||||
uint16_t obj_len; /* 信息体区长度 */
|
||||
} iec104_asdu_hdr_t;
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(不解析信息体——各命令的体解析在 iec104_link 内做)
|
||||
* 入口参数:buf - ASDU 首址(不含 APCI);len - ASDU 长度
|
||||
* hdr - 输出头
|
||||
* 返 回 值:0 成功 / -1 参数或长度错(len < 6)
|
||||
* 函数说明:纯只读,不改动 buf。
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr);
|
||||
|
||||
/*
|
||||
* 函数功能:写 ASDU 头
|
||||
* 入口参数:buf - 目标缓冲(容量须 ≥ 6);type/vsq/cot/cot_hi/ca 见常量
|
||||
* 返 回 值:固定 6(已写字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca);
|
||||
|
||||
/* ---------------- 小工具(IOA 3B 小端 / IEEE 短浮点小端) ---------------- */
|
||||
|
||||
/* IOA(3 字节小端)写入 */
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa);
|
||||
/* IOA(3 字节小端)读取 */
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf);
|
||||
/* IEEE754 float 按 104 小端写入(memcpy 位模式,不做字节序假设) */
|
||||
void iec104_put_float(uint8_t *buf, float v);
|
||||
/* IEEE754 float 按 104 小端读取 */
|
||||
float iec104_get_float(const uint8_t *buf);
|
||||
|
||||
/* ---------------- CP56Time2a(C_CS 时钟同步用) ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:解析 CP56Time2a(7 字节)为 epoch 毫秒
|
||||
* 入口参数:buf - 7B 首址;epoch_ms - 输出(UTC 毫秒)
|
||||
* 返 回 值:0 成功 / -1 参数错 / -2 IV 位置起(时间无效,调用方应拒绝)
|
||||
* 函数说明:年字段仅 7bit(0..127,基准 2000);月/日/时/分按 1.. 语义;
|
||||
* 不处理 SU(夏令时)位——忽略。
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(7 字节,UTC 分解)
|
||||
* 入口参数:epoch_ms - UTC 毫秒;buf - 目标 7B
|
||||
* 返 回 值:固定 7
|
||||
* 函数说明:epoch_ms < 2000-01-01 时钳到 2000(年字段下限);IV 清 0。
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf);
|
||||
|
||||
#endif /* IEC104_ASDU_H */
|
||||
@@ -0,0 +1,777 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(实现)
|
||||
* 模块功能:实现 iec104_link.h——APCI 流重组与 U/S/I 编解码、STARTDT/STOPDT/
|
||||
* TESTFR、k/w 窗与 t1/t2/t3(差值法,tick 回绕安全)、总召分片、
|
||||
* COV 突发(k 窗满暂存不丢)、周期上送、遥控/定值两段式、C_RD、
|
||||
* C_CS 对时、错误应答(COT 12/13/14 + EVI)。
|
||||
* 失败语义:坏帧/序号错/N(R) 越窗/缓冲溢出 → 实例置 IDLE(链路故障),
|
||||
* 由胶水层检测 IDLE 跳变断开 socket;本层不感知 I/O。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_link.h"
|
||||
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ---------------- APCI 常量 ---------------- */
|
||||
#define APCI_START 0x68u
|
||||
#define APCI_CTRL_LEN 4u /* len 字段最小值(纯控制域) */
|
||||
#define APCI_LEN_MIN 4u
|
||||
#define APCI_LEN_MAX 252u
|
||||
#define SEQ_MOD 32768u /* 序号 15bit */
|
||||
|
||||
/* U 帧控制字节(DL/T 634.5104 表:低 2 位恒 11) */
|
||||
#define U_STARTDT_ACT 0x07u
|
||||
#define U_STARTDT_CON 0x0Bu
|
||||
#define U_STOPDT_ACT 0x13u
|
||||
#define U_STOPDT_CON 0x23u
|
||||
#define U_TESTFR_ACT 0x43u
|
||||
#define U_TESTFR_CON 0x83u
|
||||
|
||||
/* 本实现单 ASDU 信息体区上限(背景 YC 组 8×5=40 B 最大) */
|
||||
#define BODY_MAX 64u
|
||||
/* 周期上送周期(COT=1) */
|
||||
#define PERIOD_MS 60000UL
|
||||
|
||||
/*
|
||||
* 函数功能:超时判定(now 到达/越过 deadline,tick 回绕安全)
|
||||
* 返 回 值:1 已到期 / 0 未到期
|
||||
*/
|
||||
static int s_expired(uint32_t now, uint32_t deadline) {
|
||||
return (((int32_t)(now - deadline)) >= 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---------------- 发送原语 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:发 U 帧(STARTDT/STOPDT/TESTFR act|con)
|
||||
* 说明:t1 只约束"需确认的发送"(标准语义)——TESTFR act 置挂起并起 t1;
|
||||
* STARTDT/STOPDT con 不启动 t1(主站可合法静默,靠 t3 探活)。
|
||||
*/
|
||||
static void s_send_u(iec104_link_t *lk, uint8_t q) {
|
||||
uint8_t buf[6];
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = q;
|
||||
buf[3] = 0u;
|
||||
buf[4] = 0u;
|
||||
buf[5] = 0u;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
/* t1 只约束"需确认的发送"(标准语义):TESTFR act 等 con;
|
||||
* STARTDT/STOPDT con 不启动 t1——主站可合法静默,靠 t3 探活 */
|
||||
if (q == U_TESTFR_ACT) {
|
||||
lk->testfr_pending = 1u;
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
}
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发 S 帧(确认已收序号,rx_unacked 清零)
|
||||
* 说明:S 不需对端确认 → 不起 t1,只刷 t3。
|
||||
*/
|
||||
static void s_send_s(iec104_link_t *lk) {
|
||||
uint8_t buf[6];
|
||||
uint16_t r = (uint16_t)(lk->rx_seq << 1);
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = 0x01u;
|
||||
buf[3] = 0u;
|
||||
buf[4] = (uint8_t)(r & 0xFFu);
|
||||
buf[5] = (uint8_t)(r >> 8);
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
lk->rx_unacked = 0u;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS; /* S 不需确认 → 不起 t1 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发一个 I 帧(asdu 由调用方组好,N(S)/N(R) 由本层填)
|
||||
* 返 回 值:0 已发出 / -1 k 窗满或超长(调用方保留数据下次重试)
|
||||
*/
|
||||
static int s_send_i(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t s16;
|
||||
uint16_t r16;
|
||||
|
||||
if ((lk->cfg->send_apdu == NULL) || (len > BODY_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (lk->tx_unacked >= IEC104_K_MAX) {
|
||||
return -1;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = (uint8_t)(APCI_CTRL_LEN + len);
|
||||
s16 = (uint16_t)(lk->tx_seq << 1);
|
||||
r16 = (uint16_t)(lk->rx_seq << 1);
|
||||
buf[2] = (uint8_t)(s16 & 0xFFu);
|
||||
buf[3] = (uint8_t)(s16 >> 8);
|
||||
buf[4] = (uint8_t)(r16 & 0xFFu);
|
||||
buf[5] = (uint8_t)(r16 >> 8);
|
||||
(void)memcpy(&buf[6], asdu, len);
|
||||
lk->tx_seq = (uint16_t)((lk->tx_seq + 1u) % SEQ_MOD);
|
||||
lk->tx_unacked++;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, (uint16_t)(6u + len));
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组 ASDU(hdr [+ ioa] + body)并发 I 帧
|
||||
* 说明:evi=1 → OA bit7(EVI,未知 IOA/类型/地址类应答)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_asdu(iec104_link_t *lk, 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 body_len, uint8_t evi) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t n;
|
||||
|
||||
n = iec104_asdu_put_hdr(buf, type, vsq, cot, cot_hi, lk->cfg->ca);
|
||||
if (evi != 0u) {
|
||||
buf[3] |= 0x80u; /* OA bit7 = EVI */
|
||||
}
|
||||
if (with_ioa != 0u) {
|
||||
iec104_put_ioa(&buf[n], ioa);
|
||||
n += 3u;
|
||||
}
|
||||
if (body_len > 0u) {
|
||||
if ((uint32_t)n + (uint32_t)body_len > (uint32_t)sizeof(buf)) {
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(&buf[n], body, body_len);
|
||||
n += body_len;
|
||||
}
|
||||
return s_send_i(lk, buf, n);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:命令应答(actcon/deactcon 等)——原样回显信息体(ioa+限定词)
|
||||
* 入口参数:pn = 1 置否定确认位
|
||||
*/
|
||||
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
|
||||
uint16_t obj_len, uint8_t cot, uint8_t pn) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint16_t n = obj_len;
|
||||
|
||||
if (n > BODY_MAX) {
|
||||
n = 0u;
|
||||
}
|
||||
if (n > 0u) {
|
||||
(void)memcpy(body, obj, n);
|
||||
}
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:单点上送遥测(M_ME_NC_1 SQ=0,值+QDS)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_yc_single(iec104_link_t *lk, uint8_t idx, uint8_t cot) {
|
||||
uint8_t body[5];
|
||||
float v;
|
||||
uint8_t qds;
|
||||
|
||||
(void)iec104_pts_yc_read(idx, &v, &qds);
|
||||
iec104_put_float(body, v);
|
||||
body[4] = qds;
|
||||
return s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, cot, 0u, 1u,
|
||||
iec104_pts_yc_ioa(idx), body, 5u, 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总召/周期扫描一步(phase 游标 = interp_yx_idx:
|
||||
* 0 双点组→1 单点组→2 YC 组→3 频率→4 actterm)
|
||||
* 入口参数:budget = 本 poll 发送预算(成功一步减一);periodic = 周期扫描
|
||||
* 返 回 值:0 本步已发出 / -1 k 窗满原地保留(poll 循环必须就此停步,
|
||||
* 否则死循环——2026-10-12 单测抓到)
|
||||
*/
|
||||
static int s_scan_step(iec104_link_t *lk, uint8_t *budget, uint8_t periodic) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint8_t n = 0u;
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t qds;
|
||||
float v;
|
||||
int rc = -1;
|
||||
|
||||
switch (lk->interp_yx_idx) {
|
||||
case 0u: /* YX 双点组 SQ=1,IOA 1..4 */
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_DP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 4u),
|
||||
lk->interp_cot, 0u, 1u, 1u, body, n, 0u);
|
||||
break;
|
||||
case 1u: /* YX 单点组 SQ=1,IOA 5..33 */
|
||||
for (i = 4u; i < 33u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_SP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 29u),
|
||||
lk->interp_cot, 0u, 1u, 5u, body, n, 0u);
|
||||
break;
|
||||
case 2u: /* YC 组 SQ=1,IOA 0x4011..0x4018 */
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
(void)iec104_pts_yc_read(i, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, (uint8_t)(IEC104_VSQ_SQ | 8u),
|
||||
lk->interp_cot, 0u, 1u, 0x4011u, body, n, 0u);
|
||||
break;
|
||||
case 3u: /* 频率单点 SQ=0,IOA 0x401D */
|
||||
(void)iec104_pts_yc_read(8u, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, lk->interp_cot, 0u,
|
||||
1u, 0x401Du, body, n, 0u);
|
||||
break;
|
||||
default: /* actterm(仅总召;周期扫描不走这步) */
|
||||
iec104_put_ioa(&body[n], 0u);
|
||||
n += 3u;
|
||||
body[n++] = IEC104_RQT_GROUP1;
|
||||
rc = s_send_asdu(lk, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACTTERM, 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
break;
|
||||
}
|
||||
if (rc != 0) {
|
||||
return -1; /* k 窗满:下次 poll 重试本 phase */
|
||||
}
|
||||
(*budget)--;
|
||||
lk->interp_yx_idx++;
|
||||
if (periodic != 0u) {
|
||||
if (lk->interp_yx_idx >= 4u) {
|
||||
lk->interp_step = 0u; /* 周期只发 YC(phase 2..3),无 actterm */
|
||||
}
|
||||
} else if (lk->interp_yx_idx >= 5u) {
|
||||
lk->interp_step = 0u;
|
||||
lk->init_pending = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:突发(COV)上送——pend 暂存逐点 SQ=0 COT=3,发成功才清位
|
||||
*/
|
||||
static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[5];
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[0] = d;
|
||||
rc = s_send_asdu(lk,
|
||||
(i < 4u) ? IEC104_T_M_DP_NA_1
|
||||
: IEC104_T_M_SP_NA_1,
|
||||
1u, IEC104_COT_SPONT, 0u, 1u,
|
||||
(uint32_t)i + 1u, body, 1u, 0u);
|
||||
if (rc != 0) {
|
||||
return; /* k 窗满:pend 位保留,下次续发 */
|
||||
}
|
||||
lk->pend_yx &= ~(1uLL << i);
|
||||
} else {
|
||||
i = 0u;
|
||||
while (i < 16u) {
|
||||
if ((lk->pend_yc & (uint16_t)(1u << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
rc = s_send_yc_single(lk, i, IEC104_COT_SPONT);
|
||||
if (rc != 0) {
|
||||
return;
|
||||
}
|
||||
lk->pend_yc &= (uint16_t)~(1u << i);
|
||||
}
|
||||
(*budget)--;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 命令处理 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:C_IC_NA_1 总召处理(ACT → actcon + 启动扫描;DEACT → deactcon)
|
||||
* 说明:RBT(bit4)落在 COT 6bit 值域内,ACT+RBT 上收为 22——先剥再判;
|
||||
* 首召/RBT → COT=4(init),后续 COT=2(bg)
|
||||
*/
|
||||
static void s_h_ic(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
/* RBT(bit4)落在 COT 6bit 值域内:ACT+RBT 上收为 22——先剥 RBT 再判值 */
|
||||
uint8_t cot_val = (uint8_t)(h->cot & 0xEFu);
|
||||
|
||||
if (cot_val == IEC104_COT_DEACT) {
|
||||
lk->interp_step = 0u;
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_DEACTCON, 0u);
|
||||
return;
|
||||
}
|
||||
if (cot_val != IEC104_COT_ACT) {
|
||||
return;
|
||||
}
|
||||
if ((h->obj_len < 4u) || (h->obj[3] != IEC104_RQT_GROUP1)) {
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_ACTCON, 1u);
|
||||
return;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON, 0u);
|
||||
lk->interp_step = 1u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->interp_cot = ((lk->init_pending != 0u)
|
||||
|| ((h->cot & IEC104_COT_BIT_RBT) != 0u))
|
||||
? (uint8_t)IEC104_COT_INIT : (uint8_t)IEC104_COT_BG_SCAN;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_CS_NA_1 对时(CP56 → epoch → cfg->clock_sync)
|
||||
* 说明:IV/字段坏/系统侧拒绝 → actcon + PN
|
||||
*/
|
||||
static void s_h_cs(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint64_t epoch = 0u;
|
||||
int rc = -1;
|
||||
|
||||
if (h->obj_len >= 10u) {
|
||||
rc = iec104_cp56_to_epoch_ms(&h->obj[3], &epoch);
|
||||
if ((rc == 0) && (lk->cfg->clock_sync != NULL)) {
|
||||
rc = lk->cfg->clock_sync(lk->ctx, epoch);
|
||||
}
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_CS_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_DC_NA_1 遥控两段式(选择记 ioa+dpc;执行须匹配,否则 PN)
|
||||
*/
|
||||
static void s_h_dc(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t qdc;
|
||||
uint8_t sel;
|
||||
uint8_t dpc;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 4u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
qdc = h->obj[3];
|
||||
sel = (uint8_t)(((qdc & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
dpc = (uint8_t)(qdc & 0x03u);
|
||||
if (sel != 0u) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 1u);
|
||||
if (rc == 0) {
|
||||
lk->yk_sel_ioa = ioa;
|
||||
lk->yk_sel_dpc = dpc;
|
||||
}
|
||||
} else {
|
||||
if ((lk->yk_sel_ioa == ioa) && (lk->yk_sel_dpc == dpc)) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 0u);
|
||||
} else {
|
||||
rc = -1; /* 无选择/失配 → 否定确认 */
|
||||
}
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_DC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_SE_NC_1 定值两段式写(选择段只校验;执行段落盘)
|
||||
* 说明:越界/未知地址 → actcon + PN
|
||||
*/
|
||||
static void s_h_se(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t addr;
|
||||
float v;
|
||||
uint8_t sel;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 8u) {
|
||||
return;
|
||||
}
|
||||
addr = iec104_get_ioa(h->obj);
|
||||
v = iec104_get_float(&h->obj[3]);
|
||||
sel = (uint8_t)(((h->obj[7] & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
rc = iec104_pts_set_write(addr, v, sel);
|
||||
s_cmd_con(lk, IEC104_T_C_SE_NC_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_RD_NA_1 读单个信息体(YC 命中 → M_ME COT5;定值命中 → 同;
|
||||
* 未知 IOA → 原样回 C_RD + COT14 + EVI)
|
||||
*/
|
||||
static void s_h_rd(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t i;
|
||||
float v;
|
||||
|
||||
if (h->obj_len < 3u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
for (i = 0u; i < iec104_pts_yc_count(); i++) {
|
||||
if (iec104_pts_yc_ioa(i) == ioa) {
|
||||
s_send_yc_single(lk, i, IEC104_COT_REQ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (iec104_pts_set_read(ioa, &v) == 0) {
|
||||
uint8_t body[5];
|
||||
|
||||
iec104_put_float(body, v);
|
||||
body[4] = 0u;
|
||||
(void)s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, IEC104_COT_REQ, 0u,
|
||||
1u, ioa, body, 5u, 0u);
|
||||
return;
|
||||
}
|
||||
(void)s_send_asdu(lk, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_UNK_IOA, 0u,
|
||||
1u, ioa, NULL, 0u, 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ASDU 分派(公共地址校验 → 类型路由)
|
||||
* 说明:错 ca → COT13+EVI;未知类型 → COT12+EVI(原类型回)
|
||||
*/
|
||||
static void s_h_asdu(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
iec104_asdu_hdr_t h;
|
||||
|
||||
if (iec104_asdu_parse_hdr(asdu, len, &h) != 0) {
|
||||
return;
|
||||
}
|
||||
if (h.ca != lk->cfg->ca) {
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_CA, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
return;
|
||||
}
|
||||
switch (h.type) {
|
||||
case IEC104_T_C_IC_NA_1:
|
||||
s_h_ic(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_CS_NA_1:
|
||||
s_h_cs(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_DC_NA_1:
|
||||
s_h_dc(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_SE_NC_1:
|
||||
s_h_se(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_RD_NA_1:
|
||||
s_h_rd(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_TS_NA_1:
|
||||
s_cmd_con(lk, IEC104_T_C_TS_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
case IEC104_T_C_RP_NA_1:
|
||||
iec104_pts_cov_check(NULL); /* 进程复位:重建快照(清历史) */
|
||||
s_cmd_con(lk, IEC104_T_C_RP_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
default:
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_COT, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- APDU 分派 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:U 帧处理(STARTDT/STOPDT act → con + 态迁移;TESTFR act → con;
|
||||
* TESTFR con → 清挂起)
|
||||
*/
|
||||
static void s_h_u(iec104_link_t *lk, uint8_t q) {
|
||||
switch (q) {
|
||||
case U_STARTDT_ACT:
|
||||
s_send_u(lk, U_STARTDT_CON);
|
||||
lk->state = IEC104_ST_RUN;
|
||||
lk->interp_step = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
break;
|
||||
case U_STOPDT_ACT:
|
||||
s_send_u(lk, U_STOPDT_CON);
|
||||
lk->state = IEC104_ST_STOPPED;
|
||||
lk->interp_step = 0u;
|
||||
break;
|
||||
case U_TESTFR_ACT:
|
||||
s_send_u(lk, U_TESTFR_CON);
|
||||
break;
|
||||
case U_TESTFR_CON:
|
||||
lk->testfr_pending = 0u;
|
||||
break;
|
||||
default:
|
||||
break; /* con/未知 U:t1/t3 已由外层刷新 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:N(R) 校验并更新 tx_unacked(V(A) ≤ V(A)' ≤ V(S),即 0 ≤ d ≤ 窗内)
|
||||
* 说明:N(R) 倒退(早于上次确认)= 协议违例 → 链路故障(标准语义);
|
||||
* tx_unacked 恒 ≤ k(发送侧门控),故 d ≤ tx_unacked 已蕴含窗校验
|
||||
* 返 回 值:0 / -1 违例
|
||||
*/
|
||||
static int s_apply_nr(iec104_link_t *lk, uint16_t nr) {
|
||||
uint16_t d = (uint16_t)(((uint32_t)lk->tx_seq + SEQ_MOD - (uint32_t)nr)
|
||||
% SEQ_MOD);
|
||||
|
||||
if ((uint32_t)d > (uint32_t)lk->tx_unacked) {
|
||||
return -1;
|
||||
}
|
||||
lk->tx_unacked = d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单个完整 APDU 分派(U/S/I);任何接收只刷 t3(不刷 t1)
|
||||
* 说明:OPEN 态只认 U;I 帧 N(S) 不连续 → IDLE;RUN 态才处理 ASDU;
|
||||
* 收满 w 个未确认 → 回 S
|
||||
*/
|
||||
static void s_h_apdu(iec104_link_t *lk, const uint8_t *p, uint16_t total) {
|
||||
const uint8_t *c = &p[2]; /* 控制域 4B */
|
||||
uint16_t ns;
|
||||
uint16_t nr;
|
||||
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
if ((c[0] & 0x03u) == 0x03u) {
|
||||
s_h_u(lk, c[0]);
|
||||
return;
|
||||
}
|
||||
if (lk->state == IEC104_ST_OPEN) {
|
||||
return; /* OPEN 态只认 U 帧 */
|
||||
}
|
||||
if ((c[0] & 0x01u) != 0u) { /* S 格式 */
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* I 格式 */
|
||||
ns = (uint16_t)(((uint16_t)c[0] | ((uint16_t)c[1] << 8)) >> 1);
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (ns != lk->rx_seq) {
|
||||
lk->state = IEC104_ST_IDLE; /* 序号错 → 链路故障 */
|
||||
return;
|
||||
}
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
return;
|
||||
}
|
||||
lk->rx_seq = (uint16_t)((ns + 1u) % SEQ_MOD);
|
||||
lk->rx_unacked++;
|
||||
if (lk->rx_unacked == 1u) {
|
||||
lk->t2_deadline = lk->now_ms + IEC104_T2_MS;
|
||||
}
|
||||
if (lk->state == IEC104_ST_RUN) {
|
||||
s_h_asdu(lk, &p[6], (uint16_t)(total - 6u));
|
||||
}
|
||||
if (lk->rx_unacked >= IEC104_W_MAX) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 公开接口 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:链路实例初始化(全清零 + 绑配置与上下文,态 = IDLE)
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg,
|
||||
void *ctx) {
|
||||
(void)memset(lk, 0, sizeof(*lk));
|
||||
lk->cfg = cfg;
|
||||
lk->ctx = ctx;
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立 → OPEN(序号/窗口/定时/暂存全部复位,init_pending=1)
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms) {
|
||||
lk->state = IEC104_ST_OPEN;
|
||||
lk->tx_seq = 0u;
|
||||
lk->rx_seq = 0u;
|
||||
lk->tx_unacked = 0u;
|
||||
lk->rx_unacked = 0u;
|
||||
lk->testfr_pending = 0u;
|
||||
lk->init_pending = 1u;
|
||||
lk->rx_len = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->now_ms = now_ms;
|
||||
lk->t1_deadline = now_ms + IEC104_T1_MS;
|
||||
lk->t2_deadline = now_ms + IEC104_T2_MS;
|
||||
lk->t3_deadline = now_ms + IEC104_T3_MS;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:链路断开/故障 → IDLE(清重组缓冲与所有暂存)
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
lk->rx_len = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:喂入收到的原始字节(APCI 流重组:半帧/粘帧逐处)
|
||||
* 说明:坏起始符/坏长度/缓冲溢出 → IDLE(胶水层检测后断开 socket)
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms) {
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state == IEC104_ST_IDLE) || (data == NULL)) {
|
||||
return;
|
||||
}
|
||||
if ((uint32_t)len > (uint32_t)(IEC104_RXBUF_SZ - lk->rx_len)) {
|
||||
lk->state = IEC104_ST_IDLE; /* 重组缓冲溢出 = 坏流 */
|
||||
lk->rx_len = 0u;
|
||||
return;
|
||||
}
|
||||
(void)memcpy(&lk->rx_buf[lk->rx_len], data, len);
|
||||
lk->rx_len = (uint16_t)(lk->rx_len + len);
|
||||
|
||||
while (lk->rx_len >= 2u) {
|
||||
uint16_t lf;
|
||||
uint16_t need;
|
||||
|
||||
if (lk->rx_buf[0] != APCI_START) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
lf = lk->rx_buf[1];
|
||||
if ((lf < APCI_LEN_MIN) || (lf > APCI_LEN_MAX)) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
need = (uint16_t)(lf + 2u);
|
||||
if (lk->rx_len < need) {
|
||||
break; /* 半帧:等下次 on_rx */
|
||||
}
|
||||
s_h_apdu(lk, lk->rx_buf, need);
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
break;
|
||||
}
|
||||
if (need < lk->rx_len) {
|
||||
(void)memmove(lk->rx_buf, &lk->rx_buf[need],
|
||||
(uint16_t)(lk->rx_len - need));
|
||||
}
|
||||
lk->rx_len = (uint16_t)(lk->rx_len - need);
|
||||
}
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
lk->rx_len = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:周期节拍(胶水层每次服务循环调用一次,时基由调用方注入)
|
||||
* 说明:t1 超时 → close;t3 到期 → TESTFR act;t2 到期 → S;
|
||||
* RUN 态按优先级:总召续扫 > 周期续扫 > COV 突发 > 周期触发
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms) {
|
||||
uint8_t budget = IEC104_POLL_BUDGET;
|
||||
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state != IEC104_ST_RUN) && (lk->state != IEC104_ST_STOPPED)) {
|
||||
return;
|
||||
}
|
||||
if (((lk->tx_unacked > 0u) || (lk->testfr_pending != 0u))
|
||||
&& (s_expired(now_ms, lk->t1_deadline) != 0)) {
|
||||
iec104_link_close(lk); /* t1:未确认 I/未答 TESTFR 超时 = 故障 */
|
||||
return;
|
||||
}
|
||||
if (s_expired(now_ms, lk->t3_deadline) != 0) {
|
||||
s_send_u(lk, U_TESTFR_ACT);
|
||||
}
|
||||
if ((lk->rx_unacked > 0u) && (s_expired(now_ms, lk->t2_deadline) != 0)) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
if (lk->state != IEC104_ST_RUN) {
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 1u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 1u)) {
|
||||
if (s_scan_step(lk, &budget, 0u) != 0) {
|
||||
break; /* k 窗满:本 poll 停步(防死循环) */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 3u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((lk->pend_yx != 0u) || (lk->pend_yc != 0u)) {
|
||||
s_spont_step(lk, &budget);
|
||||
if (budget == 0u) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (s_expired(now_ms, lk->period_next) != 0) {
|
||||
lk->interp_step = 3u;
|
||||
lk->interp_yx_idx = 2u; /* 周期只扫 YC(phase 2..3) */
|
||||
lk->interp_cot = IEC104_COT_PERIODIC;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
{
|
||||
iec104_pts_cov_t cov;
|
||||
|
||||
iec104_pts_cov_check(&cov);
|
||||
lk->pend_yx |= cov.yx_changed;
|
||||
lk->pend_yc |= cov.yc_changed;
|
||||
s_spont_step(lk, &budget);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询链路状态(胶水层检测 IDLE 跳变以断开/重建 socket)
|
||||
*/
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk) {
|
||||
return lk->state;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(APCI + 状态机,纯逻辑)
|
||||
* 模块功能:TCP 流 → APDU 重组、U/S/I 帧编解码、STARTDT/STOPDT/TESTFR、
|
||||
* k/w 流量窗与 t1/t2/t3 超时、发送序号 N(S)/N(R) 管理,以及应用层
|
||||
* 流程编排:总召(act→背景扫描→actterm,首召 RBT 用 COT=4)、
|
||||
* 突发上送(COV,COT=3)、周期上送(COT=1)、读单个(C_RD→COT=5)、
|
||||
* 双点遥控两段式(选择/执行,失配否定确认 PN)、定值读(C_RD)/
|
||||
* 定值写(C_SE_NC 选择=范围校验 / 执行=写入+封存)、时钟同步(C_CS
|
||||
* → clock_sync 回调)、试验(C_TS)与进程复位(C_RP,清快照)。
|
||||
* **零 I/O**:收 = on_rx 喂 TCP 流片段,发 = send_apdu 回调,
|
||||
* 时基 = 调用方注入 now_ms(tick ms,回绕安全:差值法)
|
||||
* → 全栈(asdu+pts+link)主机侧 L0.5 可离线跑(登记簿 A-4 约束 1)。
|
||||
* 参数(DL/T 634.5104 标准默认,§1.5 未另定):k=12 w=8 t1=15s t2=10s t3=20s。
|
||||
* 端口:TCP **2404**(§1.5 B0 定义;本 DTU = 被控站/服务器,主站连入)。
|
||||
* 线程约定:单实例单任务上下文(板端 = 104 服务在 netTask 消息循环内驱动,
|
||||
* 见 iec104_serv 胶水层);非线程安全,禁止多任务共享一实例。
|
||||
* 失败语义:send_apdu 内**不得阻塞**;发送失败由胶水层断链(主站会重连),
|
||||
* 本层不感知发送失败。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_LINK_H
|
||||
#define IEC104_LINK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "iec104_pts.h"
|
||||
|
||||
/* 服务端口(§1.5:以太网 TCP 2404) */
|
||||
#define IEC104_PORT 2404U
|
||||
|
||||
/* 链路参数(标准默认值) */
|
||||
#define IEC104_K_MAX 12U /* 未确认 I 帧上限 */
|
||||
#define IEC104_W_MAX 8U /* 收到 I 帧未发确认的强制 S 阈值 */
|
||||
#define IEC104_T1_MS 15000UL /* 发送/确认超时(超时 = 链路故障) */
|
||||
#define IEC104_T2_MS 10000UL /* 无 I 发送时强制 S 确认超时(< t1) */
|
||||
#define IEC104_T3_MS 20000UL /* 空闲发 TESTFR 周期(< t1) */
|
||||
|
||||
/* TCP 流重组缓冲:APDU 上限 253 + 半帧余量 */
|
||||
#define IEC104_RXBUF_SZ 512U
|
||||
|
||||
/* 单次 poll 发送预算(ASDU 数)——防总召大扫描饿死宿主任务 */
|
||||
#define IEC104_POLL_BUDGET 4U
|
||||
|
||||
/* 链路状态 */
|
||||
typedef enum {
|
||||
IEC104_ST_IDLE = 0, /* TCP 未建立(或已断开) */
|
||||
IEC104_ST_OPEN, /* TCP 已建立,等 STARTDT act */
|
||||
IEC104_ST_RUN, /* STARTDT 已确认,数据服务开 */
|
||||
IEC104_ST_STOPPED /* STOPDT 已确认:停数据,保 S/TESTFR */
|
||||
} iec104_state_t;
|
||||
|
||||
/* 配置与回调(ctx 原样回传给回调) */
|
||||
typedef struct {
|
||||
uint16_t ca; /* 公共地址 = 链路地址(2B,§1.5) */
|
||||
/* 发一个完整 APDU(含 6B APCI 头)。不得阻塞;生命周期仅回调内有效 */
|
||||
void (*send_apdu)(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
/* C_CS 时钟同步:epoch 毫秒;NULL 或返回 -1 → 否定确认 */
|
||||
int (*clock_sync)(void *ctx, uint64_t epoch_ms);
|
||||
} iec104_link_cfg_t;
|
||||
|
||||
/* 链路实例(胶水层静态持有,零堆) */
|
||||
typedef struct {
|
||||
const iec104_link_cfg_t *cfg;
|
||||
void *ctx;
|
||||
iec104_state_t state;
|
||||
uint8_t init_pending; /* 1 = 下次总召按初始化(COT=4 + RBT) */
|
||||
/* 序号(0..32767,帧内 ×2) */
|
||||
uint16_t tx_seq;
|
||||
uint16_t rx_seq;
|
||||
uint16_t tx_unacked; /* 已发 I 未获认(k 窗) */
|
||||
uint16_t rx_unacked; /* 已收 I 未确认(w 阈) */
|
||||
uint8_t testfr_pending; /* 1 = TESTFR act 未获 con(t1 同样约束它) */
|
||||
/* 定时器到期时刻(now_ms 域;差值法判超时,回绕安全) */
|
||||
uint32_t t1_deadline;
|
||||
uint32_t t2_deadline;
|
||||
uint32_t t3_deadline;
|
||||
uint32_t now_ms;
|
||||
/* TCP 流重组 */
|
||||
uint8_t rx_buf[IEC104_RXBUF_SZ];
|
||||
uint16_t rx_len;
|
||||
/* 总召进度:0 无 / 1 背景扫描中 / 2 待发 actterm */
|
||||
uint8_t interp_step;
|
||||
uint8_t interp_yc_idx; /* 背景扫描 YC 游标 */
|
||||
uint8_t interp_yx_idx; /* 背景扫描 YX 游标 */
|
||||
uint8_t interp_cot; /* 本次总召用的 COT(BG_SCAN 或 INIT) */
|
||||
/* 遥控选择暂存(执行段比对) */
|
||||
uint32_t yk_sel_ioa; /* 0 = 无选择 */
|
||||
uint8_t yk_sel_dpc;
|
||||
/* 周期上送(COT=1)下次触发时刻 */
|
||||
uint32_t period_next;
|
||||
/* COV 待发暂存(k 窗满时保留到下次 poll,变化不许丢) */
|
||||
uint64_t pend_yx;
|
||||
uint16_t pend_yc;
|
||||
} iec104_link_t;
|
||||
|
||||
/*
|
||||
* 函数功能:初始化实例(置 IDLE、清序号;不发送任何帧)
|
||||
* 入口参数:lk - 实例;cfg/ctx - 配置与回调上下文
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg, void *ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立(新连接或重连后调用)
|
||||
* 入口参数:now_ms - 当前时基
|
||||
* 函数说明:序号清零、init_pending 置位(首召 = 初始化总召)、进 OPEN。
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 断开(对端断开或胶水层发送失败主动断链时调用)
|
||||
* 函数说明:回 IDLE、清重组缓冲与选择暂存;不发送任何帧。
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk);
|
||||
|
||||
/*
|
||||
* 函数功能:喂入 TCP 收到的流片段(任意切分/粘帧)
|
||||
* 入口参数:data/len - 流片段;now_ms - 当前时基
|
||||
* 函数说明:内部重组,凑齐 APDU 即处理;坏帧(起始符/长度非法)→ 视同链路
|
||||
* 故障,实例回 IDLE(胶水层检测到 IDLE 跳变应断开 socket)。
|
||||
* 返回后实例仍为 RUN 不代表对端合法——仅表示未触发断链条件。
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:周期驱动(定时器 + COV + 总召分片 + 周期上送)
|
||||
* 入口参数:now_ms - 当前时基(建议 ≤100 ms 调一次)
|
||||
* 函数说明:仅 RUN 态产生数据帧;STOPPED 态只处理 t2/t3(S/TESTFR)。
|
||||
* 单次调用发送量 ≤ IEC104_POLL_BUDGET 个 ASDU。
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/* 当前状态(日志/看门狗用) */
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk);
|
||||
|
||||
#endif /* IEC104_LINK_H */
|
||||
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(实现)
|
||||
* 模块功能:实现 iec104_pts.h——YC/YX/YK/定值点表(ROM)、板端默认源
|
||||
* (phasor / prot_elm / prot_set)、COV 快照比对(遥信变位 +
|
||||
* 遥测死区)。点表口径见 iec104_pts.h 文件头与设计文档 §3。
|
||||
* 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_pts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */
|
||||
#include "phasor.h" /* 板端源:遥测 = 基波有效值 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区 = 拟定码值,L1-5 后复核) -- */
|
||||
typedef struct {
|
||||
uint32_t ioa;
|
||||
int8_t sem; /* 绑定的 phasor 语义通道(-1 = 无 phasor 源,恒无效) */
|
||||
float deadband; /* 死区(工程量绝对值) */
|
||||
} pts_yc_def_t;
|
||||
|
||||
static const pts_yc_def_t k_yc[] = {
|
||||
{ 0x4011u, (int8_t)ADC_SEM_UA, 10.0f }, /* 1Ua */
|
||||
{ 0x4012u, -1, 10.0f }, /* 1Ub:V0.1 削顶 → IV */
|
||||
{ 0x4013u, -1, 10.0f }, /* 1Uc:同上 */
|
||||
{ 0x4014u, (int8_t)ADC_SEM_UN, 10.0f }, /* 1U0(外部零序专用通道) */
|
||||
{ 0x4015u, (int8_t)ADC_SEM_IA, 10.0f }, /* 1Ia */
|
||||
{ 0x4016u, -1, 10.0f }, /* 1Ib */
|
||||
{ 0x4017u, -1, 10.0f }, /* 1Ic */
|
||||
{ 0x4018u, -1, 10.0f }, /* 1I0 */
|
||||
{ 0x401Du, -1, 0.5f }, /* 1f:L1-7 未实现 → IV */
|
||||
};
|
||||
#define YC_N ((uint8_t)(sizeof(k_yc) / sizeof(k_yc[0])))
|
||||
|
||||
/* phasor 实例下标缓存(init 时按 sem+harm==1 查表;-1 = 未找到) */
|
||||
static int8_t s_yc_phasor_idx[YC_N];
|
||||
|
||||
/* ---------------- YX 表(IOA = 1..33;双点 = 开关位置类) ------------------ */
|
||||
#define YX_N 33u
|
||||
#define YX_DP_N 4u /* idx 0..3 = 双点(开关位置,V0.1 无硬件 → IV) */
|
||||
|
||||
/* 有效位图:YX15~18(idx14~17)+ YX23(idx22)+ YX31(idx30) */
|
||||
#define YX_VALID_MASK (((1uLL << 14u) | (1uLL << 15u) | (1uLL << 16u) \
|
||||
| (1uLL << 17u) | (1uLL << 22u) | (1uLL << 30u)))
|
||||
|
||||
/* ---------------- 数据源(默认 = 板端;bind_src 可整体替换) -------------- */
|
||||
static int s_src_yc_read(uint8_t idx, float *out);
|
||||
static uint64_t s_src_yx_bits(void);
|
||||
static uint64_t s_src_yx_valid(void);
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
static int s_src_set_read(uint32_t addr, float *out);
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
static const iec104_pts_src_t k_default_src = {
|
||||
s_src_yc_read, s_src_yx_bits, s_src_yx_valid,
|
||||
s_src_yk_exec, s_src_set_read, s_src_set_write
|
||||
};
|
||||
static const iec104_pts_src_t *s_src;
|
||||
|
||||
/* ---------------- COV 快照 ---------------- */
|
||||
static float s_yc_last[YC_N];
|
||||
static uint16_t s_yc_have; /* bit i = 已有上次快照 */
|
||||
static uint64_t s_yx_last;
|
||||
static uint8_t s_yx_snap_ok;
|
||||
|
||||
/* ============================ 公开接口 ================================== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化点表模块(扫 phasor 基波实例映射、恢复默认源、清快照)
|
||||
*/
|
||||
void iec104_pts_init(void) {
|
||||
uint8_t i;
|
||||
uint8_t n;
|
||||
|
||||
(void)memset(s_yc_phasor_idx, -1, sizeof(s_yc_phasor_idx));
|
||||
n = phasor_inst_count();
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
uint8_t j;
|
||||
|
||||
if (k_yc[i].sem < 0) {
|
||||
continue;
|
||||
}
|
||||
for (j = 0u; j < n; j++) {
|
||||
const phasor_inst_t *inst = phasor_inst(j);
|
||||
|
||||
if ((inst != NULL) && ((uint8_t)inst->sem == (uint8_t)k_yc[i].sem)
|
||||
&& (inst->harm == 1u)) {
|
||||
s_yc_phasor_idx[i] = (int8_t)j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_src = &k_default_src;
|
||||
s_yc_have = 0u;
|
||||
s_yx_snap_ok = 0u;
|
||||
s_yx_last = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:替换数据源(NULL = 恢复板端默认源;单测注入假源)
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src) {
|
||||
s_src = (src != NULL) ? src : &k_default_src;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点数
|
||||
*/
|
||||
uint8_t iec104_pts_yc_count(void) {
|
||||
return YC_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点 IOA(按下标;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx) {
|
||||
return (idx < YC_N) ? k_yc[idx].ioa : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥测(值 + 品质 descriptor;源 -2 → NT,其余失败 → IV)
|
||||
* 返 回 值:0 有效 / -1 无效
|
||||
*/
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
int rc;
|
||||
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if ((idx >= YC_N) || (s_src == NULL) || (s_src->yc_read == NULL)) {
|
||||
if (qds != NULL) {
|
||||
*qds = IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
rc = s_src->yc_read(idx, v);
|
||||
if (rc == 0) {
|
||||
if (qds != NULL) {
|
||||
*qds = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if (qds != NULL) {
|
||||
*qds = (rc == -2) ? IEC104_QDS_NT : IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点数
|
||||
*/
|
||||
uint8_t iec104_pts_yx_count(void) {
|
||||
return (uint8_t)YX_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点 IOA(idx + 1;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_N) ? ((uint32_t)idx + 1u) : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:该遥信是否双点型(idx < 4 = 开关位置类)
|
||||
*/
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_DP_N) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥信(无效通道 → DIQ_IV;双点 → DPC 编码)
|
||||
*/
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out) {
|
||||
uint64_t bits;
|
||||
uint64_t valid;
|
||||
uint8_t on;
|
||||
|
||||
if (out == NULL) {
|
||||
return;
|
||||
}
|
||||
if ((idx >= YX_N) || (s_src == NULL) || (s_src->yx_bits == NULL)
|
||||
|| (s_src->yx_valid == NULL)) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
valid = s_src->yx_valid();
|
||||
if (((valid >> idx) & 1u) == 0u) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
bits = s_src->yx_bits();
|
||||
on = (uint8_t)((bits >> idx) & 1u);
|
||||
if (iec104_pts_yx_is_dp(idx) != 0u) {
|
||||
*out = (on != 0u) ? (uint8_t)IEC104_DPC_ON : (uint8_t)IEC104_DPC_OFF;
|
||||
} else {
|
||||
*out = on;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:COV 快照比对(遥信 = 有效位翻转;遥测 = 死区)
|
||||
* 入口参数:cov = 输出变位位图(NULL = 只刷快照)
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
iec104_pts_cov_t local;
|
||||
|
||||
(void)memset(&local, 0, sizeof(local));
|
||||
|
||||
/* YX:变位 = 有效位上的位翻转(无效位不参与,避免 IV 通道抖动成"变位") */
|
||||
if ((s_src != NULL) && (s_src->yx_bits != NULL)
|
||||
&& (s_src->yx_valid != NULL)) {
|
||||
uint64_t bits = s_src->yx_bits() & s_src->yx_valid();
|
||||
|
||||
if (s_yx_snap_ok != 0u) {
|
||||
local.yx_changed = bits ^ s_yx_last;
|
||||
}
|
||||
s_yx_last = bits;
|
||||
s_yx_snap_ok = 1u;
|
||||
}
|
||||
|
||||
/* YC:死区比对(仅对"本次有效 且 上次有快照"的点判定) */
|
||||
if ((s_src != NULL) && (s_src->yc_read != NULL)) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
float v;
|
||||
|
||||
if (s_src->yc_read(i, &v) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (((s_yc_have >> i) & 1u) != 0u) {
|
||||
float d = v - s_yc_last[i];
|
||||
|
||||
if (d < 0.0f) {
|
||||
d = -d;
|
||||
}
|
||||
if (d > k_yc[i].deadband) {
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
s_yc_last[i] = v;
|
||||
s_yc_have |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
|
||||
if (cov != NULL) {
|
||||
*cov = local;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥控两段式(转发数据源;select=1 选择段 / 0 执行段)
|
||||
* 返 回 值:0 接受 / -1 拒绝(调用方回 PN)
|
||||
*/
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->yk_exec == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->yk_exec(ioa, dpc, select);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址读定值(工程量浮点)
|
||||
* 返 回 值:0 / -1 未知地址
|
||||
*/
|
||||
int iec104_pts_set_read(uint32_t addr, float *out) {
|
||||
if ((s_src == NULL) || (s_src->set_read == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_read(addr, out);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址写定值(select=1 只校验;0 执行写入)
|
||||
* 返 回 值:0 / -1 越界或未知地址
|
||||
*/
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->set_write == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_write(addr, v, select);
|
||||
}
|
||||
|
||||
/* ============================ 板端默认源 ================================= */
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥测 = 基波相量有效值(fAmSQ)
|
||||
* 返 回 值:0 / -1 无 phasor 源(削顶通道/频率未实现)/ -2 未满一周波(NT)
|
||||
*/
|
||||
static int s_src_yc_read(uint8_t idx, float *out) {
|
||||
phasor_t p;
|
||||
int8_t pi;
|
||||
|
||||
if ((idx >= YC_N) || (out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
pi = s_yc_phasor_idx[idx];
|
||||
if (pi < 0) {
|
||||
return -1; /* 无 phasor 源(削顶通道/频率未实现) */
|
||||
}
|
||||
if (phasor_get((uint8_t)pi, &p) != 0) {
|
||||
return -2; /* 未满一周波:未就绪(NT) */
|
||||
}
|
||||
*out = p.fAmSQ; /* 有效值口径(系数已折算) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信位图(保护元件动作态 + 定值闭锁)
|
||||
*/
|
||||
static uint64_t s_src_yx_bits(void) {
|
||||
prot_elm_state_t oc1;
|
||||
prot_elm_state_t oc2;
|
||||
prot_elm_state_t oc3;
|
||||
prot_elm_state_t ol;
|
||||
prot_elm_state_t u0;
|
||||
uint64_t bits = 0u;
|
||||
|
||||
prot_elm_state(PE_OC1, &oc1);
|
||||
prot_elm_state(PE_OC2, &oc2);
|
||||
prot_elm_state(PE_OC3, &oc3);
|
||||
prot_elm_state(PE_OL, &ol);
|
||||
prot_elm_state(PE_3U0, &u0);
|
||||
if ((oc1.asserted != 0u) || (oc2.asserted != 0u) || (oc3.asserted != 0u)
|
||||
|| (u0.asserted != 0u)) {
|
||||
bits |= (1uLL << 14u); /* YX15 故障总 */
|
||||
}
|
||||
if (oc1.asserted != 0u) {
|
||||
bits |= (1uLL << 15u); /* YX16 过流 I 段 */
|
||||
}
|
||||
if (oc2.asserted != 0u) {
|
||||
bits |= (1uLL << 16u); /* YX17 过流 II 段 */
|
||||
}
|
||||
if (oc3.asserted != 0u) {
|
||||
bits |= (1uLL << 17u); /* YX18 过流 III 段 */
|
||||
}
|
||||
if (ol.asserted != 0u) {
|
||||
bits |= (1uLL << 22u); /* YX23 过载 */
|
||||
}
|
||||
if (prot_set_locked() != 0u) {
|
||||
bits |= (1uLL << 30u); /* YX31 保护闭锁 */
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信有效位图(V0.1 = YX15~18/23/31)
|
||||
*/
|
||||
static uint64_t s_src_yx_valid(void) {
|
||||
return YX_VALID_MASK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥控(V0.1 仅 YK11 复归 → prot_elm_request_reset)
|
||||
* 返 回 值:0 / -1 无出口硬件或未知 IOA(YK1~4/12~14 负向确认)
|
||||
* 函数说明:本回调跑在 iec104Task 上下文,**不得**直接 prot_elm_init()
|
||||
* (与 protectTask 的 eval 竞态)→ 落请求,下一拍 eval 头部消费(B5)。
|
||||
*/
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
(void)dpc; /* 本期仅 YK11 与双点值无关(复归) */
|
||||
switch (ioa) {
|
||||
case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */
|
||||
if (select == 0u) {
|
||||
prot_elm_request_reset();
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射)
|
||||
* 返 回 值:0 / -1 未知地址
|
||||
*/
|
||||
static int s_src_set_read(uint32_t addr, float *out) {
|
||||
uint32_t i;
|
||||
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if (g_prot_set_meta[i].comm_addr == (uint16_t)addr) {
|
||||
*out = prot_set_get_f((prot_set_id_t)i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set + seal)
|
||||
* 返 回 值:0 / -1 越界或未知地址
|
||||
*/
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint32_t i;
|
||||
int32_t q10;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
const prot_set_meta_t *meta = &g_prot_set_meta[i];
|
||||
|
||||
if (meta->comm_addr != (uint16_t)addr) {
|
||||
continue;
|
||||
}
|
||||
q10 = PROT_SET_Q10(v);
|
||||
if ((q10 < meta->min) || (q10 > meta->max)) {
|
||||
return -1; /* 越界:选择段即拒(执行段同样拒) */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0; /* 选择段 = 只做范围校验 */
|
||||
}
|
||||
if (prot_set_set((prot_set_id_t)i, q10) != 0) {
|
||||
return -1;
|
||||
}
|
||||
prot_set_seal(); /* 执行段:写入 + 封存校验和(L5-1) */
|
||||
return 0;
|
||||
}
|
||||
return -1; /* 未知地址 */
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(纯逻辑 + 源注入)
|
||||
* 模块功能:104 点表(遥测 YC / 遥信 YX / 遥控 YK / 定值)的定义、读取与变化检测
|
||||
* (COV:遥信变位 + 遥测死区)。**数据源经 vtable 注入**:板端绑
|
||||
* phasor/prot_elm/prot_set 真实源;主机单测绑假源 → 协议栈全链
|
||||
* (asdu+pts+link)可离线跑(登记簿 A-4 约束 1)。
|
||||
* 点表口径(docs/算法/继电保护功能复现规划.md §1.5,验收 = 省电科院点表):
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;
|
||||
* ⚠ V0.1 有效通道仅 UA/UN/IA(TPAFE 三路,§1.6)——其余点**存在但置 IV**
|
||||
* (无效位),频率随 L1-7 落地前同样置 IV;
|
||||
* - YX:IOA 1~33(§1.5 表(3));双点 = 开关位置类(YX1~4,本期无硬件 → IV),
|
||||
* 保护动作类 YX15~23 映射 prot_elm 状态,闭锁类 YX31~33 映射 prot_set;
|
||||
* - YK:IOA 0x6000+(§1.5 表(3));V0.1 无出口硬件(规划 R7)——除
|
||||
* YK11 信号复归(→ prot_elm_init)外一律否定确认(COT|PN),如实回;
|
||||
* - 定值:IOA = prot_set 元数据 comm_addr(0x8820 基址 + 告警段 0x8027~0x802A),
|
||||
* 读经 C_RD、写经 C_SE_NC(选择/执行两段,执行完成统一 seal 校验和);
|
||||
* - 固有参数 8001H~800AH:本期**不实现**(字符串类无法用短浮点上送),
|
||||
* 按未知 IOA(COT 14 + EVI)应答,登记为待办(设计文档)。
|
||||
* 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_PTS_H
|
||||
#define IEC104_PTS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 数据源 vtable(NULL 项 = 该能力不可用 → 按无效/拒绝处理) -- */
|
||||
typedef struct {
|
||||
/* 遥测读:idx = YC 表下标;out = 工程量浮点。
|
||||
* 返回 0 有效 / -1 无效(发 IV)/ -2 数据未就绪(发 NT,如相量未满一周波) */
|
||||
int (*yc_read)(uint8_t idx, float *out);
|
||||
/* 遥信位图:bit i = YX 表下标 i 的"合/动作"态(双点按 ON 计) */
|
||||
uint64_t (*yx_bits)(void);
|
||||
/* 遥信有效位图:bit i = 1 该点有效;0 → 发 IV(无硬件/未实现) */
|
||||
uint64_t (*yx_valid)(void);
|
||||
/* 遥控:select=1 选择段 / 0 执行段(dpc 已剥 S 位)。返回 0 接受 / -1 拒绝 */
|
||||
int (*yk_exec)(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
/* 定值读:addr = 通讯地址;返回 0 存在 / -1 未知地址 */
|
||||
int (*set_read)(uint32_t addr, float *out);
|
||||
/* 定值写:select=1 预置校验(范围)/ 0 执行写入。返回 0 / -1 越界或未知 */
|
||||
int (*set_write)(uint32_t addr, float v, uint8_t select);
|
||||
} iec104_pts_src_t;
|
||||
|
||||
/* ---------------- 变化检测结果(cov_check 输出) ---------------- */
|
||||
typedef struct {
|
||||
uint64_t yx_changed; /* bit i = YX 下标 i 变位(相对上次快照) */
|
||||
uint16_t yc_changed; /* bit i = YC 下标 i 越过死区(相对上次快照) */
|
||||
} iec104_pts_cov_t;
|
||||
|
||||
/*
|
||||
* 函数功能:点表层初始化(清快照、绑板端默认源)
|
||||
* 限定条件:内核启动前或 104 服务启动前,调一次
|
||||
*/
|
||||
void iec104_pts_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:覆盖数据源(主机单测注假源)
|
||||
* 入口参数:src - vtable(浅拷贝,调用方保生命周期);NULL = 恢复板端默认源
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src);
|
||||
|
||||
/* ---------------- YC ---------------- */
|
||||
uint8_t iec104_pts_yc_count(void);
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx);
|
||||
/* 读 YC:v 有效值(无效时为 0);qds 输出品质位(IV/NT 等)。返回 0/无效-1 */
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds);
|
||||
|
||||
/* ---------------- YX ---------------- */
|
||||
uint8_t iec104_pts_yx_count(void);
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx);
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx); /* 1 = 双点(M_DP) */
|
||||
/* 读 YX:out = DIQ/SIQ 值(含 IV)。 */
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out);
|
||||
|
||||
/* ---------------- COV(每 poll 一次) ---------------- */
|
||||
/*
|
||||
* 函数功能:读全表快照并与上次比对(变位 + 死区),输出变化集
|
||||
* 入口参数:cov - 输出(可为 NULL = 只刷快照不取变化)
|
||||
* 函数说明:首次调用只建快照、变化集全 0(突发由初始化/总召覆盖)。
|
||||
* 死区 = 点表内置绝对值(§1.5 运行参数 8020H 段本期取内置默认)。
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov);
|
||||
|
||||
/* ---------------- 遥控 ---------------- */
|
||||
/* 遥控两段式(转发 src->yk_exec;无源 = 一律拒绝)。返回 0 接受 / -1 拒绝 */
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
|
||||
/* ---------------- 定值 ---------------- */
|
||||
/* 按通讯地址读定值(转发 src->set_read)。返回 0 / -1 未知地址 */
|
||||
int iec104_pts_set_read(uint32_t addr, float *out);
|
||||
/* 按通讯地址写定值(两段)。返回 0 / -1 */
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
#endif /* IEC104_PTS_H */
|
||||
Reference in New Issue
Block a user