IEC 60870-5-104协议解析——电力系统远动通信实战

一、前言:为什么是104?

在电力系统的神经网络中,调度主站与变电站子站之间的通信是整个电网安全运行的命脉。IEC 60870-5-104(以下简称"104协议")是IEC60870-5-101(串口远动规约)在TCP/IP网络上的延伸版本,它继承了101协议完整的应用层语义,同时借助以太网实现了更远的传输距离和更高的可靠性。

本文以 iec_104_reporter目录下的一套真实工业EMS(储能能量管理系统)C语言实现为蓝本,从帧结构解析到连接管理、从遥信遥测到遥控对时,逐层拆解104协议的工程细节,并在文末探讨如何用AI对104数据流做实时电网异常检测。

二、104协议帧结构:三类帧的完整解析

2.1 APDU总体结构

104协议的传输单元称为 APDU(Application Protocol Data Unit,应用规约数据单元),其最大长度为255字节。APDU由两部分组成:

┌───────────────────────────────────────────────────────────

│ APCI(应用规约控制信息,6字节固定头) │

│ ASDU(应用服务数据单元,可变长度) │

└───────────────────────────────────────────────────────────

APCI的结构如下:

字节0:0x68 起始字符(固定)

字节1:APDU长度 从字节2开始到APDU结束的字节数

字节2-5:控制域 决定帧类型(I/S/U)

在代码的 rawMessageHandler 回调中,可以看到系统对每一帧的原始字节进行了十六进制打印与比较:

// iec_104_handler.c - rawMessageHandler

static void rawMessageHandler(void* parameter, IMasterConnection conneciton,

uint8_t* msg, int msgSize, bool sent)

{

int i;

for (i = 0; i < msgSize; i++)

{

nOffset += sprintf(szDump + nOffset, "%02X", msg[i]);

}

// 与上次接收数据对比,避免重复记录

if(strncmp(&szDump[12], &pIEC104->szLastRecvData[12], (nOffset-12)) == 0)

return;

memcpy(pIEC104->szLastRecvData, szDump, nOffset);

LOG_INFO("IEC_104 type=[%s], Data=\n[%s]", (sent == TRUE) ? "WRITE" : "READ", szDump);

}

注意第12字节偏移(跳过前6字节APCI头和6字节序号域)的比较逻辑:这是为了过滤S帧等纯控制帧的重复日志,因为S帧数据体完全相同但序号不同。

2.2 I帧(Information Frame)------数据传输帧

I帧用于携带ASDU进行实际数据传输,是三类帧中最复杂、信息量最大的一种。

控制域结构(4字节):

字节2: N(S) 低7位 + LSB=0(标识为I帧)

字节3: N(S) 高8位

字节4: N(R) 低7位 + LSB=0

字节5: N(R) 高8位

其中 N(S) 是发送序列号,N(R) 是接收序列号(同时也是对对端的确认)。两者范围均为 0~32767(15位),超过后归零循环。

代码注释中保留了真实抓包的I帧示例:

从站发送:

68 16 92 01 00 00 0b 83 01 01 01 00 01 40 00 73 00 00 7e 00 00 89 00 80

解析如下:

  • 68:起始字节

  • 16:APDU长度=22字节

  • 92 01 00 00:I帧,N(S)=0xC9=201,N(R)=0

  • 0b:TI=11(M_ME_NB_1,带品质描述的标度化测量值)

  • 83:VSQ=0x83,SQ=1(顺序地址),个数=3

  • 01 01:COT=0x0101,传送原因=1(周期/循环)

2.3 S帧(Supervisory Frame)------监视帧

S帧仅用于确认(ACK),不携带ASDU数据。

控制域结构:

字节2: 0x01(固定,标识为S帧)

字节3: 0x00(固定)

字节4: N(R) 低7位 + LSB=0

字节5: N(R) 高8位

典型的S帧如:68 04 01 00 d8 03(长度4,N(R)=0x01EC=492)。这意味着子站确认已收到主站发送序号 < 492 的所有I帧。

S帧是104的"滑动窗口"机制的核心。协议参数 k(最大未确认I帧数)和 w(最大收到未确认I帧数)控制着发送节奏:

// 代码中的APCI参数获取(iec_104_handler.c)

CS104_APCIParameters apciParams = CS104_Slave_getConnectionParameters(slave);

// 默认参数:t0=10s t1=15s t2=10s t3=20s k=18 w=12

2.4 U帧(Unnumbered Frame)------无编号控制帧

U帧用于连接控制,分为三对命令/确认:

┌─────────────┬─────────────────┬──────────────────┐

│ 命令类型 │ 控制域(字节2) │ 用途 │

├─────────────┼─────────────────┼──────────────────┤

│ STARTDT ACT │ 0x07 │ 启动数据传输激活 │

├─────────────┼─────────────────┼──────────────────┤

│ STARTDT CON │ 0x0B │ 启动数据传输确认 │

├─────────────┼─────────────────┼──────────────────┤

│ STOPDT ACT │ 0x13 │ 停止数据传输激活 │

├─────────────┼─────────────────┼──────────────────┤

│ STOPDT CON │ 0x17 │ 停止数据传输确认 │

├─────────────┼─────────────────┼──────────────────┤

│ TESTFR ACT │ 0x43 │ 测试帧激活 │

├─────────────┼─────────────────┼──────────────────┤

│ TESTFR CON │ 0x83 │ 测试帧确认 │

└─────────────┴─────────────────┴──────────────────┘

U帧格式简洁:68 04 [控制字节] 00 00 00,总长6字节,无序列号、无ASDU。

三、连接管理:STARTDT/STOPDT/TESTFR握手机制

3.1 连接状态机

在 iec_104.h 中,工程师定义了清晰的连接状态枚举:

typedef enum tagIEC104ConnStatus

{

STAT_Connect_Opened = 0, // TCP连接已建立,但应用层未激活

STAT_Connect_Closed, // TCP连接关闭

STAT_Activated, // 应用层已激活(STARTDT握手完成)

STAT_DeActivated, // 应用层已停止

STAT_DisConnected, // 完全断开

} IEC104_CONN_STATUS;

对应的状态转换处理在 connectionEventHandler 中实现:

// iec_104_handler.c

void connectionEventHandler(void* parameter, IMasterConnection con,

CS104_PeerConnectionEvent event)

{

IEC104_DATA_ROUGH* pIEC104 = (IEC104_DATA_ROUGH*)parameter;

if (event == CS104_CON_EVENT_CONNECTION_OPENED) {

pIEC104->em104ConnStatus = STAT_Connect_Opened;

} else if (event == CS104_CON_EVENT_CONNECTION_CLOSED) {

pIEC104->em104ConnStatus = STAT_Connect_Closed;

} else if (event == CS104_CON_EVENT_ACTIVATED) {

pIEC104->em104ConnStatus = STAT_Activated;

pIEC104->bTrigger = TRUE; // ★ 激活后立即触发全数据上送

} else if (event == CS104_CON_EVENT_DEACTIVATED) {

pIEC104->em104ConnStatus = STAT_DeActivated;

}

}

STAT_Activated 时设置的 bTrigger = TRUE 是关键:它触发主线程立即将所有遥信/遥测数据推送给新激活的主站连接,相当于完成了"连接初始化上送"。

3.2 STARTDT握手完整时序

主站(Client) 子站(Server/Slave)

| |

|-------- TCP SYN -----------------------> |

|<------- TCP SYN-ACK --------------------|

|-------- TCP ACK -----------------------> | TCP连接建立

| |

|--- 68 04 07 00 00 00 (STARTDT ACT) ---->| 启动数据传输

|<-- 68 04 0B 00 00 00 (STARTDT CON) -----| 确认启动

| | ← STAT_Activated

| 开始正常I帧数据交换 |

|<-- 68 xx [I帧 遥信/遥测数据] ------------ |

|--- 68 04 01 00 xx xx (S帧 确认) -------> |

| |

|--- 68 xx 67 01 06 00 [时钟同步] -------> | 对时

|<-- 68 xx 67 01 07 00 [时钟确认] -------- |

| |

|--- 68 04 43 00 00 00 (TESTFR ACT) -----> | 心跳测试

|<-- 68 04 83 00 00 00 (TESTFR CON) ------ | 心跳确认

3.3 服务器初始化与线程架构

// iec_104_handler.c - _ThreadEntry_IEC104Comm

static DWORD _ThreadEntry_IEC104Comm(void* pArg)

{

CS104_Slave slave = CS104_Slave_create(50, 50); // 低优先级队列50, 高优先级队列50

CS104_Slave_setLocalAddress(slave, "0.0.0.0"); // 监听所有网卡

// 支持多主站并发连接(每个连接独立事件队列)

CS104_Slave_setServerMode(slave, CS104_MODE_CONNECTION_IS_REDUNDANCY_GROUP);

CS104_Slave_setMaxOpenConnections(slave, 5); // 最多5个主站同时连接

// 注册回调

CS104_Slave_setClockSyncHandler(slave, clockSyncHandler, pIEC104);

CS104_Slave_setInterrogationHandler(slave, interrogationHandler, pIEC104);

CS104_Slave_setASDUHandler(slave, asduHandler, pIEC104);

CS104_Slave_setConnectionRequestHandler(slave, connectionRequestHandler, pIEC104);

CS104_Slave_setConnectionEventHandler(slave, connectionEventHandler, pIEC104);

CS104_Slave_setRawMessageHandler(slave, rawMessageHandler, pIEC104);

CS104_Slave_start(slave); // 启动后台监听线程

while (!pReporter->bExit) {

Handle_IEC104CommStatus(pIEC104); // 监控连接状态变化

if (!Wait_IEC104_CommReady(pReporter)) {

Sleep(1000);

continue; // 未激活则等待

}

Refresh_YC_YX_SigMapValue(pIEC104); // 刷新实时数据

Handle_YC_YX_EnqueueASDU(pIEC104, slave, alParams); // 定时上送

Sleep(2000); // 2秒轮询周期

}

CS104_Slave_stop(slave);

CS104_Slave_destroy(slave);

}

这里采用了 CS104_MODE_CONNECTION_IS_REDUNDANCY_GROUP 模式(2026年3月18日由George更新),支持多个主站并发连接,每个连接有独立的事件队列,适合多个调度系统同时接入同一EMS子站的场景。

四、遥信、遥测、遥控全类型深度解析

4.1 遥信(YX)------状态量上送

遥信(Teleinformation)反映开关、告警等二值状态,对应104协议的 M_SP_NA_1(TI=1,单点遥信)。

信号点映射表设计:

// iec_104.h - 信号映射结构

typedef struct tagIEC104sigMap {

UINT nSigIdx; // 内部信号索引

UINT nIOA; // 信息对象地址(IEC104点号)

QualityDescriptorP emQuality; // 品质描述(IV/NT/SB/BL/OV)

OBJVALUE_DATATYPE emDataType; // 数据类型

UINT nScale; // 缩放因子

void* ptrValue; // 直接指向内存中的实时值

} IEC104_CONFIG_SIGMAP;

遥信数据上送代码:

// iec_104_handler.c - Create_YX_EnqueueASDU

static void Create_YX_EnqueueASDU(IEC104_DATA_ROUGH* pIEC104,

CS104_Slave slave,

CS101_AppLayerParameters alParams)

{

// 创建ASDU:TI=M_SP_NA_1,COT=1(周期循环),IOA起始地址=0x01

CS101_ASDU newAsdu = CS101_ASDU_create(alParams, true,

CS101_COT_PERIODIC, 0x01, 1, false, false);

IEC104_CONFIG_SIGMAP* pYXItem;

for (n = 0; n < pIEC104->nYXNum; n++) {

pYXItem = pIEC104->pYXEle + n;

int tempVal = *((int*)pYXItem->ptrValue); // 直接读取内存中的BOOL值

// 创建单点信息对象

InformationObject io = (InformationObject)

SinglePointInformation_create(NULL, pYXItem->nIOA,

tempVal, pYXItem->emQuality);

CS101_ASDU_addInformationObject(newAsdu, io);

InformationObject_destroy(io);

}

CS104_Slave_enqueueASDU(slave, newAsdu);

CS101_ASDU_destroy(newAsdu);

}

关键设计要点:ptrValue 是直接指向内存中实时变量的指针,实现了信号表与实时数据的"零拷贝"映射。当系统底层(BMS/PCS)更新变量时,104上送自动反映最新值,无需额外同步。

品质描述字段(Quality Descriptor)解析:

代码注释中的抓包数据揭示了品质描述的实际含义:

变位有效 IV=0 当前值 NT=0 未被取代 SB=0 未被封锁 BL=0 点号=0 分

变位有效 IV=0 当前值 NT=0 未被取代 SB=0 未被封锁 BL=0 点号=1 合

变位无效 IV=1 当前值 NT=0 未被取代 SB=0 未被封锁 BL=0 点号=2 分

IV=1 表示该点通信中断或传感器异常,调度系统应忽略此值------这正是代码中通信失败时直接 return 不上送的原因:

static void Create_YCCabMeter_EnqueueASDU(...) {

if(pIEC104->strYC_SysInfo.emCabMeter_CommStatu == 1) // 通信故障

return; // 通信失败时不上送,避免主站读到错误数据

// ...

}

BMS告警遥信的分层设计:

储能系统的告警点多达数百个,代码采用分层结构体映射:

// iec_104.h - BMS告警Part1,共127个布尔点

typedef struct strYXBmsAlmPart1SigMap {

BOOL bBmsNotPowerOn; // BMS未上电

BOOL bAL_Over_Ucell; // 单体过压告警

BOOL bAL_Under_Ucell; // 单体欠压告警

BOOL bAL_Over_Tcell; // 单体过温告警

BOOL bFireAlm; // 火灾告警

BOOL bFire_Lv1_Alm; // 火灾一级告警

// ... 共127个告警点

} STR_YX_BMS_ALM_PART1_SIGMAP;

按电池组分组上送,IOA地址分配为:

  • 第1组 BMS Part1:起始 0x1001

  • 第2组 BMS Part1:起始 0x1101(偏移 0x100)

  • 第1组 BMS Part2:起始 0x1081

4.2 遥测(YC)------模拟量上送

遥测(Telemetry)上送模拟量测量值,本工程使用了两种类型:

4.2.1 M_ME_NB_1(TI=11)------归一化/标度化测量值

用于EMS调度参数等整数类型遥测:

// 创建标度化测量值(整数型,有缩放因子)

InformationObject io = (InformationObject)

MeasuredValueScaled_create(NULL,

pYCItem_Ems->nIOA, // 信息对象地址

tempVal * (int)pYCItem_Ems->nScale, // 值 × 缩放因子

pYCItem_Ems->emQuality); // 品质描述

// 对应ASDU示例(实际抓包):

// TI=11 M_ME_NB_1 点号=16385 OV=0 未溢出 值=115

// 点号=16386 值=126,点号=16387 IV=1(无效)值=137

缩放因子 nScale 实现了工程量与协议值的转换:例如SOC百分比×100传输,主站收到后÷100还原。

4.2.2 M_ME_NC_1(TI=13)------短浮点数测量值

用于电表、BMS等需要小数精度的遥测:

// 创建短浮点测量值(IEEE 754单精度浮点)

InformationObject io = (InformationObject)

MeasuredValueShort_create(NULL,

pYCItem_CabMeter->nIOA,

tempVal, // float,保留小数精度

pYCItem_CabMeter->emQuality);

遥测数据的完整IOA地址规划:

4.2.3 电芯数据的分块传输策略

240个电芯的单体数据(电压+温度)共480个浮点值,每个I帧最大只能容纳约48个信息体(受255字节APDU限制),因此代码实现了分块循环上送:

// iec_104_handler.c - Create_YCCellGrp_EnqueueASDU

// 注释:240个电芯的单体信息:温度+电压=480个,float型占4字节,

// 信息体243/(4+1)=48,需10个APDU传送,需要循环创建

static void Create_YCCellGrp_EnqueueASDU(...) {

int i;

for(i = 0; i < 10; i++) { // 10个APDU批次

Create_YCCellG1_EnqueueASDU(pIEC104, slave, alParams, i);

Create_YCCellG2_EnqueueASDU(pIEC104, slave, alParams, i);

Create_YCCellG3_EnqueueASDU(pIEC104, slave, alParams, i);

Create_YCCellG4_EnqueueASDU(pIEC104, slave, alParams, i);

}

}

// 每个批次:

static void Create_YCCellG1_EnqueueASDU(..., int id) {

int offset = id * 48; // 每批48个点

CS101_ASDU newAsdu = CS101_ASDU_create(...,

0xA001 + offset, ...); // IOA地址随批次偏移

for (n = 0; (((n + offset) < pIEC104->nYCNum_CellG1) && (n < 48)); n++) {

// 每批最多48个信息体

}

}

4.3 遥控(YK)------控制命令

遥控是104协议中最敏感的功能,本工程实现了单点遥控(C_SC_NA_1,TI=45)。

遥控处理流程(在 asduHandler 中):

bool asduHandler(void* parameter, IMasterConnection connection, CS101_ASDU asdu)

{

if (CS101_ASDU_getTypeID(asdu) == C_SC_NA_1) { // 单点遥控

if (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) { // 激活命令

InformationObject io = CS101_ASDU_getElement(asdu, 0);

SingleCommand sc = (SingleCommand)io;

BOOL bCtrlState = SingleCommand_getState(sc); // 0=分, 1=合

int nIOA = InformationObject_getObjectAddress(io);

if (nIOA == 0x6001) { // 系统锁定/解锁

CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);

On_Hander_Lock(pIEC104, bCtrlState);

} else if (nIOA == 0x6002) { // 系统复位

CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);

On_Hander_Reset(pIEC104, bCtrlState);

} else {

CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA); // 未知点号

}

IMasterConnection_sendASDU(connection, asdu); // 回复确认

}

}

else if (CS101_ASDU_getTypeID(asdu) == C_SE_NB_1) { // 标度化设定命令

// IOA 0x6101-0x6105:上报间隔参数设定

// IOA 0x6201-0x623D:EMS调度计划设定(12个时段×5参数)

}

else if (CS101_ASDU_getTypeID(asdu) == C_SE_NC_1) { // 短浮点设定命令

// IOA 0x6191/0x6391:REMS下发充放电功率指令(+充-放/+放-充两种约定)

}

}

关键的遥控响应流程:

主站(Client) 子站(Server)

| |

|-- C_SC_NA_1, COT=6(ACT), IOA=0x6001, S=1 --> | 发送遥控命令

| | ← 执行控制操作

|<- C_SC_NA_1, COT=7(ACT_CON), IOA=0x6001 ----| 返回执行确认

| |

注意:本工程没有实现Select-Execute两步遥控(先选择后执行),而是直接执行单步遥控。这在储能EMS领域较为常见------调度主站下发充放电功率指令,EMS直接执行,然后主动上报当前状态。真实变电站的一次设备控制(断路器分合)才必须

实现Select-Execute以防止误操作。

五、CP56Time2a精确对时设计

5.1 时间格式解析

CP56Time2a是104协议使用的7字节时间戳格式,精度达到毫秒级:

字节0-1:毫秒(0~59999,包含秒值)

字节2: 分钟(0~59)+ IV位(时间有效性)+ RES1

字节3: 小时(0~23)+ RES2 + SU(夏令时标志)

字节4: 星期(1~7)+ 日(1~31)

字节5: 月(1~12)+ RES3

字节6: 年(0~99,加2000为完整年份)

代码中的时间解析与转换:

// iec_104_handler.c - CP56Time2a_toSecTimestamp

time_t CP56Time2a_toSecTimestamp(CP56Time2a time)

{

struct tm tmTime;

tmTime.tm_year = CP56Time2a_getYear(time) + 100; // +100因为tm_year是从1900开始

tmTime.tm_mon = CP56Time2a_getMonth(time) - 1; // tm_mon是0-11

tmTime.tm_mday = CP56Time2a_getDayOfMonth(time);

tmTime.tm_hour = CP56Time2a_getHour(time);

tmTime.tm_min = CP56Time2a_getMinute(time);

tmTime.tm_sec = CP56Time2a_getSecond(time); // 毫秒部分÷1000

time_t timestamp = mktime(&tmTime); // 转换为Unix时间戳

return timestamp;

}

5.2 对时处理流程

// iec_104_handler.c - clockSyncHandler

bool clockSyncHandler(void* parameter, IMasterConnection connection,

CS101_ASDU asdu, CP56Time2a newTime)

{

time_t newSystemTimeInSec = CP56Time2a_toSecTimestamp(newTime);

// 用当前系统时间更新ACT_CON响应中的时间戳

CP56Time2a_setFromMsTimestamp(newTime, Hal_getTimeInMs());

// 仅当时间差超过5秒时才执行对时(防止频繁调整)

if (pIEC104->lcCfg.lcType == LC_TYPE_1st) // 一级控制器才执行系统对时

{

if (ABS(newSystemTimeInSec - NOW) > 5)

{

VAR_VALUE varV;

varV.dtValue = newSystemTimeInSec;

pReporter->pDAI->Set((HANDLE)pReporter, DAITYPE_SETT_PARAMVALUE_RW,

1, 1, &varV, 4, 0); // 写入系统时钟

}

}

return true; // 返回true触发库自动发送ACT_CON

}

工程细节:

  • LC_TYPE_1st 判断:在主从式多机架构中,只有主控器(1st级)执行系统对时,避免从机互相干扰

  • 5秒阈值:小于5秒的偏差忽略,防止频繁写入RTC造成系统抖动

  • 对时报文示例(来自代码注释):

主站发送时钟同步(COT=6激活):

68 14 00 00 18 00 67 01 06 00 01 00 00 00 00 70 d0 1f 10 04 06 17

= TI=103(C_CS_NA_1) 360ms 53秒 31分 16时 4日 6月 2023年

子站响应(COT=7激活确认):

68 14 18 00 02 00 67 01 07 00 01 00 00 00 00 33 81 1f 10 04 06 17

= TI=103 75ms 33秒 31分 16时 4日 6月 2023年(响应时刻)

5.3 总召(Station Interrogation)中的时间戳规则

值得注意的是,代码遵循了104协议的严格规定:总召(GI)响应的ASDU不允许携带时间戳。代码中总召响应一律使用 CS101_COT_INTERROGATED_BY_STATION(COT=20),且创建ASDU时 isSequence=true 以顺序地址方式减少帧数。

六、主站-子站断线重连的工程实现

6.1 断线检测机制

// iec_104_handler.c - Handle_IEC104CommStatus

static void Handle_IEC104CommStatus(IEC104_DATA_ROUGH* pIEC104)

{

static IEC104_CONN_STATUS emLastCommStatu = STAT_DisConnected;

if(pIEC104->em104ConnStatus != emLastCommStatu) {

// 检测到从激活状态变为非激活状态

if (emLastCommStatu == STAT_Activated &&

pIEC104->em104ConnStatus != STAT_Activated)

{

pIEC104->tmLastDisconnect = NOW; // 记录断线时刻

pIEC104->bDisconTrigger = TRUE; // 设置断线触发标志

}

emLastCommStatu = pIEC104->em104ConnStatus;

// 向上层报告通信状态

VAR_VALUE varV;

varV.emValue = (pIEC104->em104ConnStatus == STAT_Activated) ? 0 : 1;

pReporter->pDAI->Set(pReporter, DAITYPE_VIRTUAL_PARAMVALUE_W, 1, 5, &varV, 4, 100);

}

}

6.2 断线后的优雅降级

断线后,REMS(远端能量管理系统)控制权需要自动回退到本地控制,以确保储能系统安全运行:

// iec_104_handler.c - _ThreadEntry_IEC104Comm主循环

while (!pReporter->bExit) {

Handle_IEC104CommStatus(pIEC104);

if (pIEC104->enableREMSCtrl &&

pIEC104->bDisconTrigger && // 曾经断过线

pIEC104->em104ConnStatus != STAT_Activated) // 当前仍是断线

{

// 断线超过 nTDisconDelayIOA6191 秒后,清零控制指令并切回本地控制

if (abs(NOW - pIEC104->tmLastDisconnect) > pIEC104->nTDisconDelayIOA6191)

{

Handle_ParamStat(pIEC104, 0x6191, 0); // 清零功率指令

if (pIEC104->emEMSCtrlMode_REMS == EMSCtrlMode_Remote) {

// 强制切换回本地控制模式

varV.emValue = EMSCtrlMode_Local;

pReporter->pDAI->Set(pReporter, DAITYPE_VIRTUAL_PARAMVALUE_W,

1001, 3902, &varV, 4, 100);

pIEC104->emEMSCtrlMode_REMS = EMSCtrlMode_Local;

}

pIEC104->bDisconTrigger = FALSE;

}

}

// ...

}

断线重连时序:

REMS主站 EMS子站

| |

| ×× TCP断开(网络故障/主站重启)×× |

| | bDisconTrigger=TRUE

| | tmLastDisconnect=now

| (等待 nTDisconDelayIOA6191 秒) |

| | 功率指令清零

| | 切回本地控制(安全状态)

| |

|-------- TCP重连 -----------------------> |

|--- STARTDT ACT -------------------------> |

|<-- STARTDT CON -------------------------- | 重新激活

| | bTrigger=TRUE(触发全量上送)

|<-- 全量遥信/遥测推送 ------------------ |

| |

|--- 重新下发 C_SE_NC_1 控制指令 -------> | 重建控制

延时时间 nTDisconDelayIOA6191 可通过IOA 0x6191 动态配置,对应代码中的参数存储:1001, 3300(设备ID=1001, 参数SubID=3300)。

七、AI结合:基于104数据流的电网异常实时检测

7.1 数据特征工程

104协议数据流天然具备时序性和多维性,非常适合机器学习异常检测。从本工程的数据结构可以提取以下特征维度:

时序特征:

  • 遥测采样频率(本工程BMS每2~5秒一帧,电表每2~5秒一帧)

  • 品质描述变化(IV位突变往往预示传感器故障)

  • 连接状态事件时间分布(断连频率异常)

物理约束特征:

储能系统物理约束检测示例

def detect_physical_violation(bms_data):

violations = []

约束1:SOC超限(0~100%)

if not (0 <= bms_data['soc'] <= 100):

violations.append(('SOC_OUT_OF_RANGE', bms_data['soc']))

约束2:功率守恒(PCS输出功率不能超过电池允许功率)

if bms_data['pcs_output'] > bms_data['max_allow_chg_pwr'] * 1.1:

violations.append(('POWER_OVERCHARGE', bms_data['pcs_output']))

约束3:单体电压一致性(极差超阈值=电芯内阻失衡)

cell_diff = bms_data['max_cell_volt'] - bms_data['min_cell_volt']

if cell_diff > 0.05: # 50mV阈值

violations.append(('CELL_IMBALANCE', cell_diff))

return violations

7.2 基于Isolation Forest的无监督异常检测

import numpy as np

from sklearn.ensemble import IsolationForest

from collections import deque

class IEC104AnomalyDetector:

"""

基于104实时数据流的储能系统异常检测器

对应iec_104_reporter中上送的遥测数据

"""

def init(self, window_size=300, contamination=0.01):

self.window_size = window_size # 300个采样点=约600秒历史

self.model = IsolationForest(

n_estimators=100,

contamination=contamination, # 预期异常比例1%

random_state=42

)

self.buffer = deque(maxlen=window_size)

self.is_trained = False

def extract_features(self, iec104_frame):

"""

从104上送的遥测数据提取特征向量

对应iec_104.h中的STR_YC_PARAM_BMS/STR_YC_PARAM_METER结构

"""

features = [

BMS特征(来自IOA 0x4301 电池组数据)

iec104_frame['bms']['soc'], # 荷电状态

iec104_frame['bms']['rack_volt'], # 总压

iec104_frame['bms']['rack_curr'], # 总流

iec104_frame['bms']['max_cell_temp'], # 最高单体温度

iec104_frame['bms']['max_cell_volt'], # 最高单体电压

iec104_frame['bms']['min_cell_volt'], # 最低单体电压

温差和电压差(衍生特征)

iec104_frame['bms']['max_cell_temp'] - iec104_frame['bms']['min_cell_temp'],

iec104_frame['bms']['max_cell_volt'] - iec104_frame['bms']['min_cell_volt'],

电表特征(来自IOA 0x4101/0x4201)

iec104_frame['meter']['active_power'], # 有功功率

iec104_frame['meter']['reactive_power'], # 无功功率

iec104_frame['meter']['freq'], # 电网频率

iec104_frame['meter']['power_factor'], # 功率因数

通信质量特征

iec104_frame['quality_invalid_count'], # 本帧无效点数量

iec104_frame['frame_interval_ms'], # 帧间隔(正常应稳定)

]

return np.array(features)

def update(self, iec104_frame):

features = self.extract_features(iec104_frame)

self.buffer.append(features)

收集足够数据后训练模型

if len(self.buffer) >= self.window_size and not self.is_trained:

X = np.array(list(self.buffer))

self.model.fit(X)

self.is_trained = True

print("Anomaly detector trained on historical data")

在线推理

if self.is_trained:

score = self.model.decision_function([features])[0]

is_anomaly = self.model.predict([features])[0] == -1

if is_anomaly:

self.trigger_alert(features, score, iec104_frame)

return is_anomaly, score

return False, 0.0

def trigger_alert(self, features, score, raw_frame):

"""

异常触发:可对接告警系统或反向通过104协议通知主站

"""

alert = {

'timestamp': raw_frame['timestamp'],

'anomaly_score': score,

'raw_soc': features[0],

'cell_volt_diff': features[7], # 电芯电压差

'power_imbalance': abs(features[8]), # 功率异常

}

print(f"[ALERT] Anomaly detected at {alert['timestamp']}: "

f"score={score:.3f}, SOC={features[0]:.1f}%, "

f"cell_diff={features[7]*1000:.1f}mV")

可在此处调用 pReporter->pDAI->Set 触发告警遥信上送

7.3 基于LSTM的时序预测异常检测

对于SOC和温度等具有强时序规律的量,LSTM预测偏差检测效果更好:

import torch

import torch.nn as nn

class SOC_Predictor(nn.Module):

"""

基于LSTM的SOC预测模型

若实测值与预测值偏差超过阈值,触发异常

"""

def init(self, input_size=8, hidden_size=64, num_layers=2):

super().init()

self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)

self.fc = nn.Linear(hidden_size, 1)

def forward(self, x):

out, _ = self.lstm(x)

return self.fc(out[:, -1, :])

class PredictiveAnomalyDetector:

"""

预测偏差型异常检测:

利用历史数据预测下一时
● 刻的SOC值,若实测与预测偏差过大则报警

"""

def init(self, threshold=3.0):

self.model = SOC_Predictor()

self.threshold = threshold # 偏差阈值(%SOC)

self.seq_len = 30 # 使用过去30个采样点(约60秒)预测

def detect(self, soc_sequence):

soc_sequence: shape (seq_len, features)

x = torch.FloatTensor(soc_sequence).unsqueeze(0)

with torch.no_grad():

predicted_soc = self.model(x).item()

actual_soc = soc_sequence[-1][0] # 最新实测SOC

deviation = abs(actual_soc - predicted_soc)

if deviation > self.threshold:

return True, deviation, predicted_soc

return False, deviation, predicted_soc

7.4 三类典型异常的104数据特征

结合本工程实际上送的数据,总结三种高频电网/储能异常:

异常类型1:电芯失衡(Cell Imbalance)

在 `STR_YC_PARAM_BMS` 结构和 IOA `0xA001`~`0xA5A1` 的电芯单体数据中可观测到:

检测规则:

max_cell_volt - min_cell_volt > 50mV(正常运行期间)

→ 预警电芯失衡,需要均衡充电

104数据来源:

IOA 0x4301+12: fMax_CellVolt(最高单体电压)

IOA 0x4301+13: fMin_CellVolt(最低单体电压)

IOA 0xA001起: 各电芯实际电压(分10批APDU上送)

异常类型2:并网点功率越限(Grid Power Violation)**

检测规则:

abs(meter_active_power) > sys_max_power * 1.05

→ 系统实发功率超出合同限值5%

104数据来源:

IOA 0x4201+6: fActive_Power(并网点有功功率)

IOA 0x4051+3: nActvPwrLmt(系统功率限值,由REMS通过0x6101下发)

异常类型3:通信链路质量劣化(Link Quality Degradation)**

```python

def detect_link_degradation(frame_history, window=60):

"""

统计窗口内品质描述无效帧(IV=1)占比

对应 rawMessageHandler 中记录的帧质量信息

"""

recent = frame_history[-window:]

invalid_ratio = sum(1 for f in recent if f['has_invalid_quality']) / len(recent)

连接状态异常频率

disconnect_count = sum(1 for f in recent if f['conn_event'] == 'CLOSED')

if invalid_ratio > 0.1: # 10%帧含无效品质描述

return 'LINK_DEGRADED', invalid_ratio

if disconnect_count > 3: # 1分钟内断连超3次

return 'LINK_UNSTABLE', disconnect_count

return 'OK', 0

7.5 AI检测结果的反向推送

检测到异常后,可利用现有104基础设施反向通知主站------在 connectionEventHandler 感知到主站在线后,构造自发性ASDU(COT=3,突发上送):

// 新增:AI告警结果上送

static void Push_AIAlarm_ASDU(IEC104_DATA_ROUGH* pIEC104,

CS104_Slave slave,

CS101_AppLayerParameters alParams,

int nAlarmIOA, float fScore)

{

// 使用 CS101_COT_SPONTANEOUS(COT=3,自发/突发)

CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false,

CS101_COT_SPONTANEOUS, 0x7001, 1, false, false);

// 将AI异常评分作为浮点遥测上送给主站

InformationObject io = (InformationObject)

MeasuredValueShort_create(NULL, nAlarmIOA, fScore,

IEC60870_QUALITY_GOOD);

CS101_ASDU_addInformationObject(newAsdu, io);

InformationObject_destroy(io);

CS104_Slave_enqueueASDU(slave, newAsdu);

CS101_ASDU_destroy(newAsdu);

}

八、工程级性能优化与注意事项

8.1 总召期间的数据保护

代码中有一个关键的并发保护设计,避免总召期间的周期数据与总召响应数据交织:

// iec_104_handler.c - Handle_YC_YX_EnqueueASDU

static void Handle_YC_YX_EnqueueASDU(...) {

// 总召期间,停止周期上送

if(pIEC104->bOn_Interrogating == TRUE) {

TRACE("Warning:--- On_Interrogating ---\n");

return;

}

// ...周期数据处理

}

// interrogationHandler中:

bool interrogationHandler(...) {

pIEC104->bOn_Interrogating = TRUE; // 上锁

// ...发送所有总召响应数据...

IMasterConnection_sendACT_TERM(connection, asdu);

pIEC104->bOn_Interrogating = FALSE; // 解锁

}

8.2 差异化上送周期

不同数据源的变化频率不同,工程中实现了分级上送策略:

// 系统状态:1秒上送(最快,反映实时状态)

if (ABS(tmNow - pIEC104->tmLastSent_Sys) >= 1)

Create_YCSysInfo_EnqueueASDU(...);

// 电表数据:max(2, nCmd_107_Interval)秒上送

if (ABS(tmNow - pIEC104->tmLastSent_Meter) >= MAX(2, nCmd_107_Interval))

Create_YCCabMeter_EnqueueASDU(...);

// BMS数据:max(2, nCmd_103_Interval)秒上送

if (ABS(tmNow - pIEC104->tmLastSent_BMS) >= MAX(2, nCmd_103_Interval))

Create_YCBatG1_EnqueueASDU(...);

// 电量统计:max(10, nCmd_204_Interval)秒上送(变化慢)

if (ABS(tmNow - pIEC104->tmLastSent_KWh) >= MAX(10, nCmd_204_Interval))

Create_YCKWh_EnqueueASDU(...);

// 电芯单体:20秒上送(数据量大,不宜频繁)

if (pIEC104->bNeedUploadCellInfo && ABS(tmNow - pIEC104->tmLastSent_Cell) >= 20)

Create_YCCellGrp_EnqueueASDU(...);

上送间隔还支持主站通过遥控命令动态调整(IOA 0x6102~`0x6105`),使主站可以根据网络状况和业务需要灵活控制子站的数据上报频率。

8.3 bTrigger触发机制

bTrigger 标志是系统中的"脏标记",触发条件包括:

  • 主站连接激活(STAT_Activated)

  • 配置参数变更(EVENT_TYPE_CFG_CHANGED)

  • EMS计划下发完成

  • REMS控制模式切换

触发后,下一个2秒轮询周期立即上送全量数据(遥信+遥测),而不是等待各自的差异化周期到来------这保证了主站在任何时刻都能获得最新完整数据快照。

九、总结

本文以一套真实储能EMS的104协议C语言实现为主线,完整剖析了:

  1. 帧结构:I/S/U三类帧的字节级解析,以及从实际抓包数据中还原协议语义的方法

  2. 连接管理:STARTDT握手→激活上送→TESTFR心跳→断线降级的完整生命周期状态机

  3. 四遥实现:

  • 遥信:单点信息(M_SP_NA_1)的指针映射与品质描述管理

  • 遥测:标度化值(M_ME_NB_1)与短浮点(M_ME_NC_1)双模式,及电芯数据分块策略

  • 遥控:C_SC_NA_1单步遥控与C_SE_NC_1浮点设定的IOA分段处理

  1. 对时:CP56Time2a 7字节毫秒级时标的解析与系统时钟同步

  2. 断线重连:延时降级策略保障储能系统在主站失联时的本地安全运行

  3. AI融合:Isolation Forest无监督检测与LSTM预测偏差两种范式,及三类典型异常的特征工程方案

104协议在技术细节上并不"时髦",却在每一个电网边缘节点默默守护着数据的准确传递。真正的工程价值,往往就藏在这些看似朴素的字节序列和状态机跳转之中。

相关推荐
无心水2 小时前
2、5分钟上手|PyPDF2 快速提取PDF文本
java·linux·分布式·后端·python·架构·pdf
ꪶꪜ4452 小时前
vlan综合实验
linux·运维·网络
咋吃都不胖lyh3 小时前
opencode在Ubuntu下无法复制
linux·运维·ubuntu
亚空间仓鼠3 小时前
OpenEuler系统常用服务(八)
linux·运维·服务器·网络
小鸡食米3 小时前
Linux-SSH
linux·运维·ssh
飞yu流星3 小时前
linux 操作系统基础知识和目录与文件管理
linux·运维·服务器
亚空间仓鼠3 小时前
OpenEuler系统常用服务(九)
linux·运维·服务器·网络
不怕犯错,就怕不做3 小时前
rk3562 buildrooot编译更新的lib库push后无效问题分析
linux·驱动开发·嵌入式硬件
a里啊里啊3 小时前
常见面试题目集合
linux·数据库·c++·面试·职场和发展·操作系统