1d96fc9f87
- 新增 App/log/soe_rec.{c,h}:64B 定长 SOE 记录纯编解码(CP56Time2a
时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS
依赖,主机 L0.5 覆盖(32 断言,含与 iec104 CP56 解码器交叉互证)
- 新增 App/log/soe_store.{c,h}:protectTask 边沿投递(取时标 + 组记录 +
SPSC 待发环/消息队列,零文件系统);defaultTask 经 syslog_drain 尾部
排空落 /log/soe.log(卷锁 try / FA_WRITE|FA_OPEN_APPEND / 校验字节数 /
容量满 unlink 重建,全套照 run.log 实证纪律;无卷损坏归类路径)
- protect_task.c:元件动作/返回、软压板投退、采样闭锁/恢复六类边沿
→ soe_store_post;post 内禁 DBG_*(protect 静态最坏链 872 B 不变,
改前/改后 stack_report 九任务逐字节一致),丢弃留痕挪 drain 侧
- iec104:单点 YX 自发改 M_SP_TB_1(30) 带 CP56 时标(总召/周期保持
M_SP_NA_1,单测两种都断言);link cfg 新增 epoch_ms 时标源回调
(NULL/无效 → IV 位如实置位);serv 绑 sys_clock(秒域时基,
ms 域 = 秒×1000,不造假亚秒)
- 主机单测:test/host/soe/(32 断言)+ iec104 94→97(TB 时标/IV);
iec104_master.py 解析 type 30(B6/B7 流不回归);decode_soe.py
台架解码断言工具(seq 单调/校验和/UTC 交叉)
- 注册:uvprojx + check_registration SIM_EXEMPT 点名(MCU-only 胶水)
- 文档(我方属主区):`App/log/README.md` 增 soe 节(64B 记录/单写者/
post 禁 DBG/UTC 交叉坑);`docs/子系统设计/IEC104上行设计说明.md`
§1 范围行 + §3.2 时标注 + §7 台架行 + §8 第 2 条销项 + 变更记录行
台架腿(bench 锁 B9,release --bench-pass):run_boot_test PASS;
--b7-pre 21/21、--b7-post 13/13(B6/B7 回归 + 确定性动作边沿);
自发 YX 上送 M_SP_TB_1 带 ts(11:00:51 动作 / 11:00:53 复归);
FTP 下载 /log/soe.log 30 条记录 decode PASS(seq 0..28 单调、
ELM_ASSERT 14 条、UTC 交叉一致);定值/压板已恢复默认。
台账 +1088 B(SPSC 环 512 + 静态 FIL 528 + 计数器)随合并由 Lead
回填(ram_baseline --write + check_mem_numbers --write)。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(pre-commit --all-files: 29 Passed / 2 Failed / 0 Skipped;
hygiene self-check OK: no BOM/CRLF/mojibake/trailws/non-ASCII literal)
sync 追平 v2-on-ui(B8 freq + N33 抬栈)后复跑:@build 0/0
(Code=209492 RW=1104 ZI=154184);stack_report 九任务全 PASS、
protectTask 872 B/57% 不变;ci_local L0.5 15 runner 951/951
(freq 46 / soe 32 / iec104 97 / prot_set 90 / prot_elm 47)。
顺带修 protect_task.c 陈旧注释:定值巡检 31 项 → 40 项(B8 后)。
798 lines
26 KiB
C
798 lines
26 KiB
C
/*
|
||
* 模块名称: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,发成功才清位
|
||
* 函数说明:单点遥信(idx≥4)自发用 **M_SP_TB_1**(SIQ + CP56Time2a,B9:
|
||
* 变位带时标 = SOE 上送路径);双点(idx<4,V0.1 恒 IV)保持
|
||
* M_DP_NA_1;总召/周期扫描仍走 s_scan_step 的 M_SP_NA_1/M_DP_NA_1
|
||
* (时标只用于自发,标准语义——防"全量换型")。
|
||
* 时标 = cfg->epoch_ms(ctx);NULL/UINT64_MAX → CP56 IV 位如实置位。
|
||
*/
|
||
static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||
uint8_t body[8]; /* 最大 = SIQ + CP56(7)(M_SP_TB_1) */
|
||
uint8_t i;
|
||
uint8_t d;
|
||
uint8_t blen;
|
||
uint8_t type;
|
||
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;
|
||
blen = 1u;
|
||
if (i < 4u) {
|
||
type = (uint8_t)IEC104_T_M_DP_NA_1;
|
||
} else {
|
||
uint64_t ems = (lk->cfg->epoch_ms != NULL)
|
||
? lk->cfg->epoch_ms(lk->ctx)
|
||
: UINT64_MAX;
|
||
|
||
type = (uint8_t)IEC104_T_M_SP_TB_1;
|
||
(void)iec104_epoch_ms_to_cp56((ems == UINT64_MAX) ? 0u : ems,
|
||
&body[1]);
|
||
if (ems == UINT64_MAX) {
|
||
body[1 + 2] |= 0x80u; /* CP56 byte2 bit7 IV:时钟无效如实上送 */
|
||
}
|
||
blen = 8u;
|
||
}
|
||
rc = s_send_asdu(lk, type, 1u, IEC104_COT_SPONT, 0u, 1u,
|
||
(uint32_t)i + 1u, body, blen, 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;
|
||
}
|