一、前言:为什么是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语言实现为主线,完整剖析了:
-
帧结构:I/S/U三类帧的字节级解析,以及从实际抓包数据中还原协议语义的方法
-
连接管理:STARTDT握手→激活上送→TESTFR心跳→断线降级的完整生命周期状态机
-
四遥实现:
-
遥信:单点信息(M_SP_NA_1)的指针映射与品质描述管理
-
遥测:标度化值(M_ME_NB_1)与短浮点(M_ME_NC_1)双模式,及电芯数据分块策略
-
遥控:C_SC_NA_1单步遥控与C_SE_NC_1浮点设定的IOA分段处理
-
对时:CP56Time2a 7字节毫秒级时标的解析与系统时钟同步
-
断线重连:延时降级策略保障储能系统在主站失联时的本地安全运行
-
AI融合:Isolation Forest无监督检测与LSTM预测偏差两种范式,及三类典型异常的特征工程方案
104协议在技术细节上并不"时髦",却在每一个电网边缘节点默默守护着数据的准确传递。真正的工程价值,往往就藏在这些看似朴素的字节序列和状态机跳转之中。