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:
+4
-2
@@ -196,8 +196,10 @@ flowchart TD
|
||||
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
|
||||
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)。
|
||||
零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。
|
||||
- **尚未接线**:无任务调用 `prot_elm_run()`(链接器整对象移除,零足迹);
|
||||
板端接线随 protectTask 立项(栈预算 + IWDG 心跳门禁,AGENTS E-59 同款流程)。
|
||||
- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask,
|
||||
20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检;
|
||||
跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。
|
||||
判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7
|
||||
|
||||
## 4. 关键约定
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ typedef struct {
|
||||
|
||||
static pe_rt_t s_rt[PE_COUNT];
|
||||
|
||||
/* 跨上下文复归请求(单字标志,volatile;B5:104 YK11 在 iec104Task 落请求) */
|
||||
static volatile uint8_t s_reset_req;
|
||||
|
||||
/*
|
||||
* 函数功能:按名前缀在 phasor 实例表里找索引(开机解析一次)
|
||||
* 入口参数:prefix - 相量实例名前缀(如 "C01")
|
||||
@@ -90,6 +93,16 @@ void prot_elm_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,见 prot_elm.h)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 函数说明:只置单字标志;真正的状态复位在下一拍 eval 头部(消费者上下文)执行。
|
||||
*/
|
||||
void prot_elm_request_reset(void) {
|
||||
s_reset_req = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
|
||||
@@ -103,6 +116,13 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
|
||||
uint8_t locked = prot_set_locked();
|
||||
uint32_t i;
|
||||
|
||||
/* 跨上下文复归请求在此消费(YK11 经 request_reset,见 prot_elm.h) */
|
||||
if (s_reset_req != 0u) {
|
||||
s_reset_req = 0u;
|
||||
prot_elm_init();
|
||||
locked = prot_set_locked();
|
||||
}
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
const pe_cfg_t *cfg = &k_pe[i];
|
||||
uint8_t cond;
|
||||
|
||||
+13
-2
@@ -18,8 +18,10 @@
|
||||
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
|
||||
* - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。
|
||||
*
|
||||
* 线程约定:init 内核启动前;eval/run 仅保护消费者任务上下文(P1 阶段 = 单测;
|
||||
* 板端接线随 protectTask 立项,走栈预算 + IWDG 心跳门禁)。
|
||||
* 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务
|
||||
* 上下文(B5 起 = protectTask);request_reset 任意上下文(跨上下文复归请求,
|
||||
* 如 104 遥控 YK11 在 iec104Task 落请求、下一拍 eval 头部消费)。板端接线
|
||||
* 走栈预算 + IWDG 心跳门禁。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
@@ -50,9 +52,18 @@ typedef struct {
|
||||
|
||||
/*
|
||||
* 函数功能:复位全部元件状态(定值不动;幂等,内核启动前可调)
|
||||
* 限定条件:内核启动前,或保护消费者任务上下文(跨上下文请用 request_reset)
|
||||
*/
|
||||
void prot_elm_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,单字标志)
|
||||
* 函数说明:下一拍 prot_elm_eval() 头部消费并执行 prot_elm_init()。
|
||||
* 104 遥控 YK11(iec104Task 上下文)必须走本接口,不得直接调
|
||||
* prot_elm_init()(会与 protectTask 的 eval 竞态)。
|
||||
*/
|
||||
void prot_elm_request_reset(void);
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值(prot_set RAM)+ 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,回绕安全:差值法)
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
|
||||
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
|
||||
#include "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
|
||||
@@ -240,6 +242,14 @@ int app_main_init(void) {
|
||||
(void)phasor_init();
|
||||
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
|
||||
|
||||
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
|
||||
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
|
||||
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
DBG_INFO("protection init: OK (set=%u elm=%u)",
|
||||
(unsigned)PS_ID_COUNT, (unsigned)PE_COUNT);
|
||||
|
||||
/* 5) SD2506 RTC 诊断信息 */
|
||||
{
|
||||
/* 读取 ID */
|
||||
|
||||
+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 */
|
||||
+10
-2
@@ -62,6 +62,10 @@ extern osThreadId_t timeSyncTaskHandle;
|
||||
extern const osThreadAttr_t timeSyncTask_attributes;
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
extern const osThreadAttr_t adcTask_attributes;
|
||||
extern osThreadId_t iec104TaskHandle;
|
||||
extern const osThreadAttr_t iec104Task_attributes;
|
||||
extern osThreadId_t protectTaskHandle;
|
||||
extern const osThreadAttr_t protectTask_attributes;
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
|
||||
@@ -69,9 +73,11 @@ extern const osThreadAttr_t adcTask_attributes;
|
||||
#define STACK_PROBE_FIRST_MS 5000u
|
||||
#define STACK_PROBE_PERIOD_MS 2000u
|
||||
/* S11(2026-10-03):堆低水位告警阈值(B)。
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(configTOTAL_HEAP_SIZE
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(当时 configTOTAL_HEAP_SIZE
|
||||
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。 */
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。
|
||||
* 2026-10-12(B5):堆扩至 56KB(.ioc configTOTAL_HEAP_SIZE=57344,CCM 腾给
|
||||
* protectTask/iec104Task 栈),阈值**不随堆缩放**——它是绝对字节安全线。 */
|
||||
#define HEAP_LOW_WATER_B 3072u
|
||||
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
|
||||
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
|
||||
@@ -93,6 +99,8 @@ static const stack_probe_task_t s_stack_probe[] = {
|
||||
{ "ui", &uiTaskHandle, &uiTask_attributes },
|
||||
{ "timesync", &timeSyncTaskHandle, &timeSyncTask_attributes },
|
||||
{ "adc", &adcTaskHandle, &adcTask_attributes },
|
||||
{ "iec104", &iec104TaskHandle, &iec104Task_attributes },
|
||||
{ "protect", &protectTaskHandle, &protectTask_attributes },
|
||||
};
|
||||
|
||||
#define STACK_PROBE_COUNT (sizeof(s_stack_probe) / sizeof(s_stack_probe[0]))
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
|
||||
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
|
||||
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||||
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
|
||||
* 本文件只做 I/O 编排(A-4 约束 1)。
|
||||
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_serv.h"
|
||||
|
||||
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
|
||||
#define DBG_TAG "[I104]"
|
||||
|
||||
#include "iec104_link.h"
|
||||
#include "iec104_pts.h"
|
||||
#include "net_socket.h"
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
|
||||
#define IEC104_POLL_MS 100u
|
||||
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
|
||||
#define IEC104_RX_SZ 128u
|
||||
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
|
||||
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
|
||||
#define IEC104_LIVENESS_DIV 32u
|
||||
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
|
||||
#define IEC104_SEND_FAIL_MAX 3u
|
||||
|
||||
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
|
||||
static iec104_link_t s_link;
|
||||
static int s_listen = -1;
|
||||
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
|
||||
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
|
||||
static uint8_t s_rx[IEC104_RX_SZ];
|
||||
static uint8_t s_liveness_ctr;
|
||||
static uint8_t s_send_fail_run;
|
||||
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
|
||||
|
||||
/* 前向声明(cfg 初始化引用) */
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
|
||||
|
||||
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
|
||||
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync };
|
||||
|
||||
/*
|
||||
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
|
||||
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
|
||||
* 返回值:无
|
||||
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
|
||||
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
|
||||
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
|
||||
*/
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
int ret;
|
||||
|
||||
(void)ctx;
|
||||
if (s_client < 0) {
|
||||
return;
|
||||
}
|
||||
ret = net_send(s_client, apdu, (int)len, 0);
|
||||
if (ret == (int)len) {
|
||||
s_send_fail_run = 0u;
|
||||
return;
|
||||
}
|
||||
if (s_send_fail_run < 255u) {
|
||||
s_send_fail_run++;
|
||||
}
|
||||
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
|
||||
(unsigned)s_send_fail_run);
|
||||
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
|
||||
s_conn_dead = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||||
* 返回值:0 成功(s_listen 写回)/ -1 失败
|
||||
* 限定条件:net 子系统可用(NET_READY 屏障后)
|
||||
*/
|
||||
static int s_bind_listen(void) {
|
||||
int ls;
|
||||
struct net_sockaddr_in addr;
|
||||
|
||||
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (ls < 0) {
|
||||
return -1;
|
||||
}
|
||||
(void)memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons(IEC104_SERV_PORT);
|
||||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
if (net_listen(ls) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
s_listen = ls;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
|
||||
* 入口参数:why = 日志原因串(ASCII)
|
||||
* 返回值:无
|
||||
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
|
||||
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
|
||||
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
|
||||
*/
|
||||
static void s_drop_conn(const char *why) {
|
||||
DBG_ERROR("i104 drop: %s", why);
|
||||
if (s_client >= 0) {
|
||||
net_close(s_client);
|
||||
}
|
||||
s_client = -1;
|
||||
s_listen = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
if (s_bind_listen() < 0) {
|
||||
s_listen = -1; /* 主循环下轮重试 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
|
||||
* 入口参数:now = 注入时基(tick ms)
|
||||
* 返回值:无
|
||||
*/
|
||||
static void s_service_link(uint32_t now) {
|
||||
iec104_link_poll(&s_link, now);
|
||||
if (s_conn_dead != 0u) {
|
||||
s_drop_conn("send fail");
|
||||
return;
|
||||
}
|
||||
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
|
||||
s_drop_conn("protocol fault");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
|
||||
* 返回值:无
|
||||
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
|
||||
* 函数说明:门控(net_can_accept/net_can_read 纯 RAM 读)+ 兜底节拍照
|
||||
* ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层
|
||||
* 已 auto_relisten,close 会连监听一起毁)
|
||||
*/
|
||||
static void s_serv_poll(void) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
int n;
|
||||
|
||||
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
|
||||
if (net_get_generation() != s_gen) {
|
||||
if ((s_client >= 0) && (s_client != s_listen)) {
|
||||
net_close(s_client);
|
||||
}
|
||||
if (s_listen >= 0) {
|
||||
net_close(s_listen);
|
||||
s_listen = -1;
|
||||
}
|
||||
s_client = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
s_gen = net_get_generation();
|
||||
(void)s_bind_listen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_listen < 0) {
|
||||
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_client < 0) {
|
||||
int c;
|
||||
|
||||
if (net_can_accept(s_listen) == 0) {
|
||||
if (net_get_sock(s_listen) == NULL) {
|
||||
s_listen = -1; /* 控制块消失:交回上方重建 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
c = net_accept(s_listen, NULL, NULL);
|
||||
if (c >= 0) {
|
||||
s_client = c;
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_open(&s_link, now);
|
||||
DBG_INFO("i104: accepted fd=%d", c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
|
||||
if (net_can_read(s_client) == 0) {
|
||||
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
|
||||
s_service_link(now);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
s_liveness_ctr = 0u;
|
||||
}
|
||||
|
||||
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
|
||||
if (n > 0) {
|
||||
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
|
||||
} else if (n == 0) {
|
||||
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
|
||||
if (s_client != s_listen) {
|
||||
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
|
||||
}
|
||||
s_client = -1;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_close(&s_link);
|
||||
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
|
||||
s_listen = -1;
|
||||
}
|
||||
return;
|
||||
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||||
s_drop_conn("recv err");
|
||||
return;
|
||||
}
|
||||
|
||||
s_service_link(now);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
|
||||
* 入口参数:argument 未用
|
||||
* 返回值:无(不返回)
|
||||
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
|
||||
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
|
||||
*/
|
||||
void iec104_serv_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
if (sys_evtHandle != NULL) {
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* 就绪位是状态位(NoClear),不消费 */
|
||||
}
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
}
|
||||
}
|
||||
|
||||
iec104_pts_init();
|
||||
iec104_link_init(&s_link, &s_cfg, NULL);
|
||||
s_gen = net_get_generation();
|
||||
if (s_bind_listen() < 0) {
|
||||
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
|
||||
(unsigned)IEC104_SERV_PORT);
|
||||
} else {
|
||||
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
s_serv_poll();
|
||||
osDelay(IEC104_POLL_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(对外接口)
|
||||
* 模块功能:声明 iec104Task 任务体与监听端口;协议实现在 App/protocol/iec104/,
|
||||
* 本接口仅供 Src/freertos.c 创建任务使用。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_SERV_H
|
||||
#define IEC104_SERV_H
|
||||
|
||||
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口) */
|
||||
#define IEC104_SERV_PORT 2404u
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(在 Src/freertos.c USER CODE 区 osThreadNew 创建)
|
||||
* 入口参数:argument - 未用
|
||||
* 返回值:无(任务不返回)
|
||||
*/
|
||||
void iec104_serv_task(void *argument);
|
||||
|
||||
#endif /* IEC104_SERV_H */
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)——实现
|
||||
* 模块功能:见 protect_task.h。每拍三件事:
|
||||
* ① L5-4 采样故障闭锁:adc_health_flags() 电平位命中 → 元件状态
|
||||
* 复位(防陈旧判据)+ 本拍不 eval;
|
||||
* ② prot_set_selfcheck()(L5-1 定值完整性巡检,31 项 sum32,开销
|
||||
* 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件);
|
||||
* ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志。
|
||||
* 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。
|
||||
* 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h);
|
||||
* 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#include "protect_task.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#define DBG_TAG "[PROT]"
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "adc_health.h"
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "task_watchdog.h"
|
||||
|
||||
/* L5-4 采样故障电平闭锁位:stall(可自愈)+ backward(锁存)都须停判据。
|
||||
* 锁存类(FRSTDATA/HW_ERR 等)不在此列——它们是一次性事件痕迹,不构成
|
||||
* "当前测量不可信";持续 stall/backward 才是。 */
|
||||
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | ADC_HF_SEQ_BACKWARD)
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
|
||||
/*
|
||||
* 函数功能:动作位图变化沿日志(逐变化元件打名字与状态)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()
|
||||
* 函数说明:V0.1 无出口硬件(规划 R7),动作 = 遥信 + 日志;SOE 落盘随 L4。
|
||||
*/
|
||||
static void s_log_active_edge(uint32_t active) {
|
||||
uint32_t chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (chg != 0u); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if ((chg & bit) != 0u) {
|
||||
prot_elm_state_t st;
|
||||
|
||||
chg &= ~bit;
|
||||
prot_elm_state((prot_elm_id_t)i, &st);
|
||||
DBG_INFO("elm %s asserted=%u picked=%u",
|
||||
prot_elm_name((prot_elm_id_t)i),
|
||||
(unsigned)st.asserted, (unsigned)st.picked);
|
||||
}
|
||||
}
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
* 返 回 值:无(不返回)
|
||||
* 函数说明:无就绪屏障(判据只依赖 phasor 使用区,未就绪时 prot_elm_run
|
||||
* 整拍自跳;采样闭锁另有 adc_health 电平位兜底)→ 上电即跑。
|
||||
* 每拍喂看门狗(TASK_WD_PROTECT,产品 required_mask 0x1F7)。
|
||||
*/
|
||||
void protect_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
DBG_INFO("protect task start: cycle=%ums elm=%u",
|
||||
(unsigned)PROTECT_TASK_CYCLE_MS, (unsigned)PE_COUNT);
|
||||
|
||||
for (;;) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t hf = adc_health_flags();
|
||||
|
||||
if ((hf & PROT_HF_LOCK_MASK) != 0u) {
|
||||
/* L5-4:测量链不可信 → 复位元件(本任务上下文,直接 init 合法)
|
||||
* 并跳过判据;stall 自愈后下一拍自动恢复 */
|
||||
if (s_prev_hf_lock == 0u) {
|
||||
DBG_ERROR("sampling FAULT -> protection blocked (hf=0x%08lX)",
|
||||
(unsigned long)(hf & PROT_HF_LOCK_MASK));
|
||||
}
|
||||
s_prev_hf_lock = 1u;
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_hf_lock != 0u) {
|
||||
DBG_INFO("sampling recovered -> protection resumed");
|
||||
s_prev_hf_lock = 0u;
|
||||
}
|
||||
/* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起
|
||||
* eval 自动复位全元件)。⚠ 与 104 定值写(iec104Task set+seal)
|
||||
* 的竞态窗口为 µs 级:selfcheck 误锁后同一次写的 seal 即清锁
|
||||
* (prot_set.c),V0.1 如实登记,P4 定值持久化时收口。 */
|
||||
(void)prot_set_selfcheck();
|
||||
prot_elm_run(now);
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)
|
||||
* 模块功能:protectTask 线程入口——以固定节拍驱动保护判据
|
||||
* (`prot_elm_run()`:phasor rms → 元件步进),并落实 L5-4 采样
|
||||
* 故障闭锁(`adc_health_flags()` 电平位 → 元件状态复位 + 本拍不判据)。
|
||||
* 判据本体在 `App/algo/prot_elm.c`(纯逻辑,L0.5 已覆盖),本文件
|
||||
* 只做消费/编排(AGENTS 目录地图:task 不装算法)。
|
||||
* 线程约定:仅本任务调 prot_elm_run / prot_set_selfcheck(prot_elm.h 线程约定);
|
||||
* prot_set 写在 P4 前无第二写者(104 定值写经 pts→prot_set,见注)。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#ifndef PROTECT_TASK_H
|
||||
#define PROTECT_TASK_H
|
||||
|
||||
/* 判据节拍(ms):V0.1 元件吃 50 Hz 基波 rms(phasor 每工频周更新)→ 20 ms
|
||||
* = 一个工频周;延时精度由 prot_elm 的 now_ms 差值法保证,与节拍无关。
|
||||
* 规划 §4 的"1 ms 高优先级"是参考系统瞬时值元件口径,随 P2 瞬时值元件再评。 */
|
||||
#define PROTECT_TASK_CYCLE_MS 20u
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口(freertos.c USER CODE 区创建)
|
||||
*/
|
||||
void protect_task(void *argument);
|
||||
|
||||
#endif /* PROTECT_TASK_H */
|
||||
@@ -191,7 +191,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC); /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_ADC) /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_IEC104) /* iec104Task 同 adcTask:任何构建都创建 */
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);/* protectTask 同:判据节拍必须心跳 */
|
||||
|
||||
#if defined(ENABLE_HW_LAYER_TESTS)
|
||||
/* 见函数说明:CH395F 硬件层测试构建 = default + 屏障等待者(等待分片喂位)
|
||||
@@ -200,7 +202,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC);
|
||||
| (1u << (uint32_t)TASK_WD_ADC)
|
||||
| (1u << (uint32_t)TASK_WD_IEC104)
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);
|
||||
#endif
|
||||
|
||||
return m;
|
||||
|
||||
@@ -31,6 +31,8 @@ typedef enum {
|
||||
TASK_WD_UI = 4,
|
||||
TASK_WD_TIMESYNC = 5,
|
||||
TASK_WD_ADC = 6, /* adcTask:1ms 采样节拍消费者(B1,2026-10-10) */
|
||||
TASK_WD_IEC104 = 7, /* iec104Task:104 上行服务(B4,2026-10-12) */
|
||||
TASK_WD_PROTECT = 8, /* protectTask:保护判据节拍(B5,2026-10-12) */
|
||||
TASK_WD_ID_MAX
|
||||
} task_wd_id_t;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user