从IEC101到IEC104:用iec-master实战电力104规约主站调试
2026/9/13 11:35:41 网站建设 项目流程

简介:面向电力自动化与配网通信领域的 Java 开发者,这份资源基于 DL/T634.5101-2002 和 DL/T634.5104-2009 标准,实现了 IEC101/104 规约报文的解析与组装,可应用于实际工程中的发送报文生成、报文校验与调试等场景,也能为协议栈二次开发提供可复用的骨架。包体共 43 个文件,以 34 个 Java 源码文件为主,辅以 3 个 XML 配置、2 个规约实施细则 DOCX 文档、1 个 XLSX 解析细则表及说明与许可文件,压缩包仅 1.79MB,整体轻量且结构清晰。代码按解析、交互等模块划分,并配有 docs 目录下的广东电网配网自动化规约实施细则与解析细则表格,方便对照标准核查字段定义与组装流程。已有 658 人学习下载,适合正在从事电网规约开发、需要快速落地 101/104 通信逻辑或排查报文问题的初中级工程师参考,可以显著缩短从报文分析到编码实现的时间。

1. 用iec-master视角看电力104规约:它和IEC101是什么关系

在电力远动这一行待久了你会发现,IEC60870-5-101 和 IEC60870-5-104 的关系,像同一套词汇表配了两本语法书。101 走串口,链路层要自己处理地址、确认和重发;104 走 TCP,把链路层的活交给 TCP 的序号和确认机制。但到了信息体这一层,遥信、遥测、遥控的 ASDU 结构是同一套,所以做 104 主站收到的数据帧,用 101 的报文知识照样能读懂大半。标题里这几个关键词拼在一起,说的就是这件事:用 iec-master 做 104 主站拉数据,同时保留对 101 老站装置的兼容能力。做储能、光伏、配网终端接入,或者在 EMS 里写规约适配层的人,大概率都会撞上这个题。下面这六章按我实际调试的习惯展开,先把两种规约的边界讲清,再把 104 的帧格式、参数、解析和排错逐一落到可执行的命令上。

2. 从IEC101到IEC104:串口到TCP的映射带出报文结构差异

2.1 101和104差在哪一层:链路层职责转移

IEC60870-5-101 定位在串口通信,链路层是它自己的规约。101 的一个链路规约数据单元(LPDU)大致是:启动字符 0x68、长度 L、链路控制字、链路地址,后面再挂 ASDU。链路控制字里带着 FCB、FCV、ACD、DFC 这些位,主站和从站要靠它做收发确认、忙闲通知,一套流程走完才能保证一帧数据可靠送达。

IEC60870-5-104 把 101 的链路层整个换掉了。104 直接跑在 TCP/IP 上,链路地址不再单独出现,公共地址收编进 ASDU 里;链路控制字的职责交给 TCP 的滑动窗口和 6 字节 APCI 控制域。这个替换的结果是:101 的 ASDU 几乎原样搬进 104,但打包方式和确认机制完全不同。实际工作中经常遇到“同一个从站既出 101 又出 104”的情况,从站程序里 ASDU 编解码是共用的,只有链路层分了两套实现。

这对主站开发者的启示很直接:如果你已经写过 101 主站,104 主站要新写的是 APCI 封装、序号管理、TCP 连接维护这三块;ASDU 那层解析逻辑可以直接复用。反过来,只懂 104 的人去看 101,最容易懵的就是链路控制字那几位。

2.2 104报文的APCI四字节控制域:I帧、S帧、U帧的一个表格

104 的 APDU(应用规约数据单元)由两部分组成:4 字节 APCI 控制域 + 可选的 ASDU。完整报文的第一个字节固定是 0x68,第二个字节是 APDU 长度,表示后面的 4 字节控制域加 ASDU 一共多长。第三个字节到第六个字节就是控制域本体,它同时承担了帧类型识别、序号传递和链路控制三个功能。

68 LEN C1 C2 C3 C4 ASDU...

控制域第一字节的低两位决定了帧类型:

帧类型低两位作用典型第一字节
I帧00承载数据,带发送序号和接收序号0x00~0x02 等偶数
S帧01纯确认,只带接收序号0x01
U帧11链路控制:启动、停止、测试0x07、0x0B、0x43 等

I帧的 4 字节控制域里,第 1、2 字节是发送序号左移一位,第 3、4 字节是接收序号左移一位。为什么左移一位?因为最低位要让给帧类型标记。接收方拿到序号后右移一位,就是真实的序号 N。U帧的三个命令在调试里最常用:0x07 是启动传输 STARTDT act,0x0B 是启动确认 STARTDT con,0x43 是测试帧 TESTFR act。记住这六个字节的小表,看抓包就能一眼分帧型。

2.2.1 三种帧的识别与序号规则

I帧序号是 15 位,取值范围 0 到 32767,循环使用。主站和从站各自维护自己的发送序号,不收对方影响。S帧只携带接收序号,用来确认自己已经收到对方多少帧。U帧不需要序号,也不能携带数据,它的作用是建立传输关系。

这里有一条规则值得单独强调:U帧的 STARTDT act 必须在 TCP 连接建立之后、任何 I 帧发送之前完成。有的主站实现省掉这一步直接发总召唤,部分从站会直接丢弃,表现就是“TCP 能连上但一条遥测都收不到”。我一般会在连接成功后的第一件事就发 0x07,等收到 0x0B 再进数据流程。

2.3 一个起始报文(STARTDT act)的逐字节含义

拿最常见的启动报文做例子,主站发给从站的 STARTDT act 全长只有 6 个字节:

68 04 07 00 00 00

逐个字节说:0x68 是启动字符;0x04 表示 APCI 长度,因为后面没有 ASDU,只有 4 字节控制域;第三字节 0x07 是 STARTDT act 的标记;后面三个字节固定填 0。从站如果同意,会回一个 68 04 0B 00 00 00,启动确认。之后主站才能发 I 帧。

这个 6 字节的最小报文值得背下来,调试时用它验证 TCP 通道是否通、从站是否在线,比直接发总召唤更安全。如果连 0x0B 都收不到,问题基本在 TCP 层或对端服务没起来,后面查什么都是白搭。

3. 用iec-master做主站:K/W参数与T0~T3定时器怎么设置

3.1 主站状态机:连接、启动、总召唤、正常运行的四步

iec-master 这类主站实现,表面上是一堆报文收发,本质上是一个有限状态机。状态依次是:TCP 已连接、传输已启动、总召唤完成、正常遥测运行。每一步都有对应的报文驱动,跳步就会出问题。

第一步 TCP 连接,连上从站的 2404 端口,这一步有 T0 超时限制。第二步发 STARTDT act 等 STARTDT con,这一步有 T1 超时限制。第三步发总召唤 C_IC_NA_1(类型标识 100,传送原因 6),等从站回传送原因 7 的激活确认,再收传送原因 20 的数据。第四步进入稳态,周期收遥测、按需发遥控,空闲时用 TESTFR 保活。

实际项目里最常见的“连上 2404 端口但收不到数据”,八成都卡在第二步或第三步。第二步的启动确认没完成,从站不认你;第三步总召唤没发成功,从站不会主动推数据。很多从站一条常态遥测都不主动上送,只有总召唤响应才给全量快照,所以总召唤这步跳不得。

3.2 K和W:接收窗口与发送确认,为什么默认是12和8

K 和 W 是 104 规约流控的两个关键参数。K 表示发送方在未收到确认的情况下,最多能连续发送多少个 I 帧;W 表示接收方在收到多少个 I 帧之后,必须回一个 S 帧确认。IEC 60870-5-104 标准建议 K 默认 12,W 默认 8,二者必须满足 W 小于等于 K。

可以这样理解:主站这边每发一帧,发送计数加一;每收到一个 S 帧或 I 帧,用对方的接收序号把自己的未确认计数清零。如果连续发了 12 帧还没收到任何确认,发送方就得停下来等,防止序号溢出和接收方缓冲溢出。从站侧同理,收到 8 帧 I 帧后必须发 S 帧,否则主站会认为通道阻塞。

调参的实际场景一般是这两种:一是大容量对时场景,主站一次要突袭上千个遥信点,可以把 W 调大减少回确认的频率;二是通道质量差、频繁出现序号重叠,可以适当调小 K 值降低重传压力。但要注意,K 和 W 是连接双方协商好的能力,修改时要确认对端也支持同样的窗口,否则会出现对端按自己窗口收帧、本端按自己的窗口计数,两边序号错位。

3.3 T0~T3定时器:四个超时的默认值与调参场景

104 规约里有四个定时器,T0 到 T3,分别管连接建立、确认等待、接收确认延迟和空闲保活。它们的默认值在不同实现里略有差异,但通用的推荐值是 T0 为 30 秒,T1 为 15 秒,T2 为 10 秒,T3 为 20 秒。T2 必须小于 T1,这是规约明确要求的,因为 T2 是接收方发确认的延迟上限,如果 T2 大于 T1,发送方会在等到确认之前就认为超时了。

定时器作用触发时机默认值常见调整方向
T0TCP 连接建立超时connect 到 accept30s跨网段连接调大到 60s
T1发送后等待确认超时发 I/S/U 帧后15s通道抖动大调到 30s
T2收到 I 帧后延迟确认收帧后起算10s收多发少时调小到 5s
T3空闲时发送测试帧无收发超过 T320s保活要求高调到 10s

我一般建议在调任何定时器之前,先抓包看一轮完整的交互。如果主站反复在 T1 超时后断链,先别急着调大 T1,看是不是从站根本没回 S 帧;如果从站回了但确认晚于 15 秒,那才说明通道延迟真的大,这时调 T1 才有意义。

3.3.1 T2必须小于T1:这一条最容易配错

配错 T2 和 T1 的关系是新手主站调试里最常见的低级错误之一。有的实现里 T2、T1 是分开配置的两个文本框,默认值就配反了,结果主站发一堆 I 帧,从站还没来得及回 S 帧,主站这边 T1 先炸了,反复断链。排查时看日志全是 “T1 timeout”,很容易怀疑是网络问题,其实只是参数配错。

调参的原则是遵循 T2 < T1 < T3 这条链(T3 大于 T1 是为了避免测试帧和确认超时互相打架),在这个前提下按通道质量调整。内网直连可以把 T1 压到 5 秒,让断链更快暴露;跨运营商专线建议 T1 放到 20 秒以上,给公网抖动留余量。

3.4 Python最小主站骨架:连接2404端口并完成启动和总召唤

下面这段代码是我做原型验证时常用的最小骨架,纯 socket 实现,不依赖第三方规约库,方便在隔离环境里复现抓包里的每一条报文。它完成的是状态机的前三步:建立 TCP 连接、发送 STARTDT act、发送总召唤。

import socket import struct import time def build_u_frame(cmd): # U帧:长度固定4,控制域4字节,cmd对应STARTDT/TESTFR等 return bytes([0x68, 0x04]) + bytes([cmd, 0x00, 0x00, 0x00]) def build_i_frame(send_seq, recv_seq, asdu): # I帧:序号左移1位,保留bit0=0表示I帧;低字节在前 s = (send_seq << 1) & 0x7FFF r = (recv_seq << 1) & 0x7FFF ctrl = struct.pack('<BBBB', s & 0xFF, (s >> 8) & 0xFF, r & 0xFF, (r >> 8) & 0xFF) return bytes([0x68, len(asdu) + 4]) + ctrl + asdu def build_interrogation_asdu(common_addr=1): # 类型100总召唤;VSQ=1表示1个对象;COT=6激活;IOA=0;QOI=0站召唤 return bytes([0x64, 0x01, 0x06, 0x00, common_addr & 0xFF, (common_addr >> 8) & 0xFF, 0x00, 0x00, 0x00, 0x00]) sock = socket.create_connection(("192.168.1.20", 2404), timeout=10) sock.sendall(build_u_frame(0x07)) # STARTDT act resp = sock.recv(1024) assert resp == bytes([0x68, 0x04, 0x0B, 0x00, 0x00, 0x00]), "启动确认异常" send_n = 1 recv_n = 0 asdu = build_interrogation_asdu(common_addr=1) sock.sendall(build_i_frame(send_n, recv_n, asdu)) # 总召唤

代码里几个细节说清楚。STARTDT act 的 0x07 和确认的 0x0B 必须严格匹配,我遇到有的从站实现会回 0x0B 但不是预期字节序,这里断言能快速暴露。I 帧的序号左移一位后,发送计数每次发 I 帧才加 1,S 帧和 U 帧不占序号;总召唤 ASDU 里公共地址按小端写入,IOA 固定 3 字节且低字节在前,这几个字节序错了从站直接丢弃。这个最小主站跑通后,再往上面加收帧解析、心跳保活,就是一个可用的调试工具。

4. 104规约报文详解:类型标识、传送原因与一个通用解析器

4.1 类型标识与传送原因:读懂报文的两个坐标

104 报文里的 ASDU,第一字节类型标识决定了这条报文在说什么,第三第四字节的传送原因决定了这条报文是干嘛的。两个坐标一起看,基本就能定位报文的语义。类型标识对应具体的数据类型,比如单点遥信、双点遥信、短浮点遥测、遥控命令、总召唤;传送原因则说明是主动上送、响应召唤、激活还是激活确认。

实际调试最常用的一张对照表我列在下面,都是 IEC 60870-5-101/104 标准里的固定编号,不同厂家实现也遵循这套编号:

类型标识助记符方向说明
1M_SP_NA_1从站到主站单点遥信
3M_DP_NA_1从站到主站双点遥信
9M_ME_NA_1从站到主站归一化遥测
13M_ME_NC_1从站到主站短浮点遥测,4字节IEEE754
15M_IT_NA_1从站到主站累计量,常见于电度
30M_SP_TB_1从站到主站带时标单点遥信(SOE)
45C_SC_NA_1主站到从站单点遥控
50C_SE_NC_1主站到从站设点控制,短浮点
100C_IC_NA_1主站到从站总召唤
103C_CS_NA_1主站到从站时钟同步

传送原因也是固定编号,最常用的是:6 激活(命令/召唤已发出)、7 激活确认(从站已受理)、8 停止激活、9 停止激活确认、20 响应站召唤(从站开始批量上送数据)。看到 100 类型加传送原因 6,是总召唤请求;看到 1 类型加传送原因 20,是对总召唤的遥信响应。

4.2 逐字节拆一条总召唤确认应答

从站收到主站总召唤后,先回一条传送原因 7 的激活确认,随后批量上送数据。下面是一条典型的从站数据帧,三条单点遥信在同一个 ASDU 里连续编排:

68 10 02 00 02 00 01 83 14 00 01 00 0A 00 00 01 00 01

帧头先拆:0x68 是启动字符;0x10 表示 APCI 加 ASDU 共 16 字节;控制域 02 00 02 00,前两个字节是发送序号 1,后两个字节是接收序号 1,这是一条 I 帧。

再看 ASDU 这 12 字节。0x01 说明类型是单点遥信;0x83 是可变结构限定词,最高位是 1 表示连续编排,低 7 位是 3 表示后面有 3 个信息体;14 00 传送原因是 20,响应总召唤;01 00 公共地址是 1;0A 00 00 是起始信息体地址 10;后面 01 00 01 分别是三个信息体的值,1 表示合,0 表示分。因为连续编排,三个信息体的地址是 10、11、12,不需要逐个写地址,这是 104 压缩报文长度的主要手段。

4.3 解析函数落地:从裸字节到对象

写主站解析不能用“看到 0x01 就知道是遥信”这种思路,要把字节流变成结构化数据。下面这个解析函数覆盖了 I 帧、S 帧、U 帧的识别,以及 ASDU 公共部分的提取:

def parse_apdu(buf): if buf[0] != 0x68: raise ValueError("不是104报文") length = buf[1] if len(buf) < 2 + length: raise ValueError("报文不完整") apdu = buf[:2 + length] ctrl = apdu[2:6] if (ctrl[0] & 0x01) == 0: # I帧,序号为15位,右移1位还原 send_seq = ((ctrl[1] << 8) | ctrl[0]) >> 1 recv_seq = ((ctrl[3] << 8) | ctrl[2]) >> 1 return {"frame": "I", "send": send_seq, "recv": recv_seq, "asdu": parse_asdu(apdu[6:])} if (ctrl[0] & 0x03) == 0x01: # S帧,只有接收序号 recv_seq = ((ctrl[3] << 8) | ctrl[2]) >> 1 return {"frame": "S", "recv": recv_seq} # U帧,cmd值映射到语义 u_cmds = {0x07: "STARTDT act", 0x0B: "STARTDT con", 0x43: "TESTFR act", 0x83: "TESTFR con"} return {"frame": "U", "cmd": u_cmds.get(ctrl[0], hex(ctrl[0]))} def parse_asdu(asdu): type_id = asdu[0] vsq = asdu[1] cot = int.from_bytes(asdu[2:4], "little") ca = int.from_bytes(asdu[4:6], "little") info_elem_count = vsq & 0x7F ioa_start = int.from_bytes(asdu[6:9], "little") return {"type_id": type_id, "vsq": vsq, "cot": cot, "common_addr": ca, "count": info_elem_count, "ioa_start": ioa_start, "body": asdu[9:]}

代码逻辑基于 3 字节信息体地址、2 字节公共地址的标准 104 配置。I 帧识别后把序号和 ASDU 分开,S 帧只做确认计数,U 帧映射成可读命令。要注意 VSQ 的最高位是连续的编排标志位,低 7 位才是信息体数量;连续编排时只需要读起始 IOA,逐条递增即可,非连续编排时每个信息体都带自己的 3 字节 IOA。这套解析逻辑写清楚后,遥控下发和时钟同步只是在 type_id 和 body 上做文章,框架不动。

5. 电力104规约主站排错:T3超时、序号错位与Wireshark定位

5.1 三个最常见的交互故障与判断依据

主站调试绕不开三类故障:T3 超时断链、序号错位导致对端丢帧、总召唤发了没响应。这三类故障的表现接近,都是“连接不正常、数据不来”,但定位路径完全不同,抓包后看几个关键特征就能区分。

症状抓包特征根因方向
周期性断链,间隔稳定每过约 T3 秒发 TESTFR act 无确认对端忙或保活机制未实现
发送后长时间无回应TESTFR 有回,但 I 帧序号跳跃本端序号计算错误
总召唤无响应发 100/COT=6 后 100/COT=7 一直不来公共地址或装置状态问题

T3 超时的典型现场是:连接稳定跑几分钟,然后主站发 TESTFR act,从站不回 TESTFR con,主站等 T1 超时后断链重连。碰到这个先别调 T3,去查从站是不是被另一个主站占用了连接。104 从站在 IEC 规范里允许双连接,但很多实际装置只支持单连接,第二个主站连上来时从站直接不响应测试帧。

序号错位的典型表现是主站发了第 13 个 I 帧后对端失联,因为 K 值默认 12,超过未确认窗口对端直接丢弃。这类问题要先检查自己的发送序号是不是在重发的时候加了两次,或者把 S 帧也计入了发送序号。对端回一个带接收序号的帧时,用那个值校准本端的已确认计数,比盲目调大 K 更稳。

5.2 Wireshark看104:过滤表达式与四层检查

Wireshark 对 IEC 60870-5-104 有完整的解析器,抓包后不需要手动数字节。过滤表达式用 tcp.port==2404 或直接按协议名过滤 iec60870_104(旧版本里叫 iec60870_5_104,看本机版本选一个)。打开解析结果后,I 帧、S 帧、U 帧会被自动分开,ASDU 里的类型标识、传送原因、公共地址、IOA 都会被拆成独立的字段,这比盯着十六进制效率高一个数量级。

我检查 104 交互时习惯按四层顺序看:

  1. TCP 三次握手是否完成,连接是否稳定在 ESTABLISHED。
  2. STARTDT act 和 con 是否成对出现,有没有发送后没有确认。
  3. 有没有总召唤请求和对应的激活确认,以及随后的 COT=20 批量数据。
  4. 数据帧的序号是否连续,S 帧确认间隔是否在 K 值窗口内。

四层里任何一层断了,问题就在那一层,不要跨层去查。曾经有个现场,前三层全通,就是没有 COT=20 的数据帧,最后发现是主站总召唤的公共地址写成 0,从站公共地址是 1,地址不匹配从站直接丢弃总召唤。这种问题光看 TCP 连接是看不出来的。

5.2.1 四层检查顺序

按顺序看的好处是能快速缩小范围。TCP 层有问题,后面三层不用看;STARTDT 没确认,不用查总召唤;总召唤正常但没有数据,再看是装置没配置还是地址不匹配。实际调试里我踩过的坑是跳过第二层直接查总召唤,花半小时排查地址问题,最后发现 STARTDT 确认包被防火墙丢了,白白浪费时间。

5.3 调参验证:改T1和T2之前先确认这个前提

调 T1、T2 参数有一个前提必须先确认:当前生效的连接参数是本次运行配置的,不是上一次遗留的。104 连接参数在连接建立时由双方协商,主站改完配置要重连才生效。有的从站实现会把参数固化在启动文件里,主站改了 T1,从站没改,两边对不上。

验证方法是抓两次包对比。第一次抓包看调参前的超时时间点,记录从发包到断链的间隔;第二次抓包改参后重连,看同样场景下的间隔是否按预期变化。如果两次一样,先检查是不是连接没重来、配置没加载;确认都在生效后再谈参数值合不合理。这一步能挡住一大批“改了半天没效果”的无效排查。

6. 抓包逆向校验ASDU字节序:新接入装置唯一要防的坑

6.1 现象:解析结果出现4.6e+29这类数值

新接一台从站调试,遥测值解析出来全是 4.6e+29 或者 -3.14e-41 这种明显不是电力量的数值,十有八九是字节序的问题。104 报文里数值型数据默认低位在前,但有些老装置、或者转发的中间网关,会把浮点数的字节序搞成高位在前。更隐蔽的是浮点正常但信息体地址、公共地址的大小端不一致,导致你解析出来点位全对不上。

6.2 校验流程与代码

用一个已知量测做锚点,是拆字节序问题最可靠的办法。选一个你确定数值的量测点,比如某线路电流理论上是 100.0A,从抓包里把它的 4 字节测量值抠出来,分别按大小端解一遍:

import struct def probe_float(raw4): for fmt, tag in [("<f", "小端"), (">f", "大端")]: val = struct.unpack(fmt, raw4)[0] print(tag, val) # 顺带校验3字节IOA和2字节公共地址 ioa_bytes = raw4[:3] print("IOA小端:", int.from_bytes(ioa_bytes, "little")) print("IOA大端:", int.from_bytes(ioa_bytes, "big")) probe_float(bytes([0x9A, 0x99, 0xC8, 0x42]))

跑完这段,看哪个方向的解析结果接近实际值,就能确定装置的字节序。这里有个技巧:IOA 通常是小端,所以 3 字节的 IOA 解析结果如果是类似 65536 这种规整的大数,也值得警惕。我用这套方法判断过一台 101 转 104 的规约转换器,104 侧浮点按小端发,但内嵌的 IOA 按大端编,导致点位全部错位,用锚点验证一次就暴露了。

确定字节序后,把这份校验固化成一个启动自检函数,每次接入新装置时先跑一遍,比对已知量测点的解析值,通过后再进正常轮询。这个习惯能把新装置接入的调试时间压缩到原来的三分之一,也是区分老手和新手最明显的分水岭。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询