IEC-104 报文解析实战:帧结构、ASDU 类型与调试排错指南
2026/9/23 17:46:20 网站建设 项目流程

简介:这份资料聚焦电力系统 IEC-104 规约的报文解析,面向从事电力自动化、SCADA 通信开发与调试的工程师及初学者。内容围绕常用报文类型展开,逐字节说明其作用与含义,帮助读者在开发中快速定位问题、理解报文规则,减少反复查阅标准文档的时间成本。资源包共 11 个文件,约 838KB,包含 PDF 文档与 HTML 页面两种浏览方式,辅以 js 脚本、字体图标及样式文件,可直接打开 index.html 在浏览器中查看,也可阅读 PDF 版本。目前已有 8257 人学习下载,说明其在电力通信领域具有较高的参考价值。读者可借此掌握 104 报文的帧结构、类型标识与传输原因等关键知识点,形成从报文捕获到含义解读的完整认知,适合作为日常开发调试的速查手册与入门学习材料。

1. 为什么一张 IEC-104 报文表能省掉半天抓包

很多做电力自动化的工程师第一次接触 104 规约,都是在变电站后台和主站对不上点的时候。抓包工具里刷出一屏68开头的十六进制,盯着68 0E 00 00 00 00 46 01 03 00 01 00 00 00 00这种串,完全不知道从哪看起。IEC-104 本质是 IEC-60870-5-101 的 TCP/IP 封装版本,把串口上的 FT1.2 帧塞进 TCP 载荷,用 APCI 做链路控制、ASDU 做业务数据。它跑在 2404 端口,一个连接就是一条链路,主站是控制方,RTU 或测控装置是被控方。

真正让人头疼的不是协议本身,而是报文类型太多:总召唤、遥测、遥信、遥控、时钟同步、电度召唤,每种 ASDU 的类型标识(TypeID)不同,信息对象地址(IOA)的编排规则也不同。手里有一份按类型整理好的报文详解,配合抓包对照,定位问题的时间能从半天压到十几分钟。这篇内容适合刚接手 104 调试的自动化工程师、做规约测试的软件开发者,以及需要写 104 主站或从站模拟程序的人。

2. IEC-104 帧结构与 APCI 链路层报文拆解

2.1 三种帧格式的判别方法

104 的 APCI 只有 6 个字节,但三种帧格式的区分全在这 6 个字节里。第一个字节固定是0x68,第二个字节是 APDU 长度(不含启动符和长度字节本身)。关键看第 3、4 字节的控制域:

帧类型控制域第 1 字节特征用途典型报文
I 格式bit0 = 0传输 ASDU 数据68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14
S 格式bit0=1, bit1=0确认收到 I 帧68 04 01 00 00 00
U 格式bit0=1, bit1=1链路启停、测试68 04 07 00 00 00(STARTDT act)

判别顺序很简单:先看控制域第一字节最低位,为 0 就是 I 帧;为 1 再看次低位,为 0 是 S 帧,为 1 是 U 帧。U 帧的六种功能码(STARTDT、STOPDT、TESTFR 的 act/confirm)都落在控制域第一字节的高 6 位。

2.2 用 Python 解析 APCI 头部

抓包拿到的原始字节流,第一步就是把 APCI 剥出来。下面这段代码处理 TCP 流里粘包的情况,按长度字段切分 APDU:

import struct def parse_apci(data: bytes): """解析 IEC-104 APCI 头部,返回帧类型和控制域信息""" frames = [] offset = 0 while offset < len(data): if data[offset] != 0x68: offset += 1 continue length = data[offset + 1] apdu = data[offset: offset + 2 + length] if len(apdu) < 2 + length: break # 半包,等下次数据 ctrl1 = apdu[2] if ctrl1 & 0x01 == 0: ftype = "I" send_seq = (apdu[2] | (apdu[3] << 8)) >> 1 recv_seq = (apdu[4] | (apdu[5] << 8)) >> 1 info = {"type": ftype, "tx": send_seq, "rx": recv_seq, "asdu": apdu[6:]} elif ctrl1 & 0x03 == 0x01: info = {"type": "S", "rx": (apdu[4] | (apdu[5] << 8)) >> 1} else: info = {"type": "U", "func": ctrl1 & 0xFC} frames.append(info) offset += 2 + length return frames

length字段只统计控制域加 ASDU 的字节数,不含启动符和它自己,所以切片时是2 + length。I 帧的发送序号和接收序号各占 15 位,低字节在前,右移一位去掉格式位。U 帧的功能码用ctrl1 & 0xFC取出高 6 位,0x07是 STARTDT act,0x0B是 STARTDT con,0x13是 STOPDT act,0x23是 STOPDT con,0x43是 TESTFR act,0x83是 TESTFR con。

2.3 链路建立与序号确认机制

链路刚连上时双方都处于停止状态,主站必须先发68 04 07 00 00 00请求激活传输,从站回68 04 0B 00 00 00确认后,才允许发 I 帧。这个握手不做,后面所有总召唤都会被丢弃,这是新手最常见的坑。

I 帧的序号是 15 位循环,0 到 32767。发送方每发一个 I 帧 tx 加一,接收方收到后用自己的 rx 记录期望的下一个序号。当接收方累计收到 8 个 I 帧(默认参数 k=12、w=8)就要回一个 S 帧确认,否则发送方窗口耗尽会停发。调试时如果看到 I 帧发到某个序号后卡住,先查 S 帧有没有正常回。

提示:k 和 w 是可配置参数,k 是发送方未确认 I 帧的最大数量,w 是收到多少个 I 帧后必须发 S 帧确认。不同厂家默认值可能不同,对接前先确认。

3. ASDU 类型标识与常用报文实战解析

3.1 总召唤与遥测遥信上送

总召唤是调试时第一个要跑通的流程。主站发类型标识 100(C_IC_NA_1)的 ASDU,从站回 100 确认,然后依次上送类型 1(单点遥信)、9(归一化遥测)、11(标度化遥测)、13(短浮点遥测),最后再发一个 100 表示总召唤结束。

一条典型的单点遥信报文:68 0E 00 00 00 00 01 01 03 00 01 00 00 00 00 00。拆开看,01是 TypeID(单点信息),01是可变结构限定词(VSQ),高 1 位表示是否连续,低 7 位是信息对象个数,这里 1 个;03是传送原因,3 表示突发(spontaneous);00 01是公共地址;00 01 00是信息对象地址 IOA;最后一个00是遥信值,bit0 表示分合。

归一化遥测的 TypeID 是 9,值是两个字节的有符号数,实际工程值 = 原始值 × 系数。短浮点遥测 TypeID 是 13,值占 4 字节,IEEE 754 格式,直接struct.unpack('<f', ...)就能还原。

3.2 遥控与时钟同步报文

遥控分四步:选择、选择确认、执行、执行确认。TypeID 是 45(C_SC_NA_1 单点遥控)或 46(C_DC_NA_1 双点遥控)。选择和执行靠传送原因区分,6 是激活,7 是激活确认,8 是停止激活,10 是激活终止。

def build_control_cmd(ioa: int, value: bool, select: bool): """构造单点遥控 ASDU,select=True 为选择,False 为执行""" type_id = 45 vsq = 0x01 cot = 6 if select else 6 # 激活 ca = struct.pack('<H', 1) ioa_bytes = struct.pack('<I', ioa)[:3] sco = (0x01 if value else 0x00) | (0x80 if select else 0x00) asdu = bytes([type_id, vsq, cot]) + ca + ioa_bytes + bytes([sco]) apci = bytes([0x68, len(asdu) + 4, 0x00, 0x00, 0x00, 0x00]) return apci + asdu

SCO 字节的 bit7 是选择/执行标志,1 为选择,0 为执行;bit0 是合分闸,1 为合。时钟同步用 TypeID 103(C_CS_NA_1),ASDU 里带 7 字节 CP56Time2a 时间,前两字节是毫秒,后面依次是分、时、日、月、年。

3.3 传送原因与公共地址的对应关系

传送原因(COT)是排查问题的关键字段,常见值如下:

COT 值含义典型场景
3突发遥信变位主动上送
5被请求响应总召唤
6激活主站下发命令
7激活确认从站确认收到命令
10激活终止命令执行完成
20响应站召唤总召唤数据

公共地址(CA)一般 2 字节,标识不同的 RTU 或装置。如果主站收到数据但点表对不上,先核对 CA 和 IOA 是否和点表一致,这两个字段错一个,数据就映射到别的点位上。

4. 104 报文调试排错与性能优化技巧

4.1 常见异常报文定位

链路建不起来,先看 STARTDT 有没有回 confirm。如果主站发了68 04 07 00 00 00但从站没回68 04 0B 00 00 00,检查从站是否配置了允许建立连接、端口是否被占用、防火墙是否放行 2404。

总召唤超时,看从站有没有回类型 100 的激活确认。如果确认回了但数据不上送,多半是点表配置为空或 IOA 越界。遥控失败,重点看选择确认和执行确认的 COT 是不是 7,如果从站回的是否定确认(COT=47),说明命令被拒绝,查遥控闭锁或权限配置。

序号错乱是另一个高频问题。如果主站收到的 I 帧 rx 序号和本地期望的不一致,说明有丢包或重复。104 本身不带重传,靠 TCP 保证可靠,但 TCP 断开重连后序号会重置,主站必须重新发 STARTDT。

4.2 用 tcpdump 和 Wireshark 过滤 104 流量

现场没有专用工具时,tcpdump 加 Wireshark 是最快的组合。在装置侧抓包:

# 抓取 2404 端口流量,保存为 pcap 供 Wireshark 分析 tcpdump -i eth0 -w iec104.pcap 'tcp port 2404' # 实时查看 104 报文十六进制 tcpdump -i eth0 -X 'tcp port 2404 and tcp[13] & 8 != 0'

第一条命令把流量存下来,Wireshark 打开后直接能识别 IEC 60870-5-104 协议,自动解析出 TypeID、COT、IOA。第二条只抓 PSH 标志置位的包,过滤掉纯 ACK,减少噪音。Wireshark 里可以用iec60870_asdu.typeid == 100这样的过滤器只看总召唤。

4.3 批量测试与参数调优

做规约测试时,经常需要模拟从站批量上送数据。用 Python 起一个简易从站,循环构造遥测帧:

import socket, time, struct def send_measure(sock, ioa, value): """发送短浮点遥测,TypeID=13""" asdu = bytes([13, 0x01, 0x03]) + struct.pack('<H', 1) asdu += struct.pack('<I', ioa)[:3] asdu += struct.pack('<f', value) + bytes([0]) apci = bytes([0x68, len(asdu) + 4, 0x00, 0x00, 0x00, 0x00]) sock.send(apci + asdu) s = socket.create_connection(('127.0.0.1', 2404)) for i in range(100): send_measure(s, 0x4001 + i, 220.5 + i * 0.1) time.sleep(0.05)

struct.pack('<f', value)把浮点数转成 4 字节小端,IOA 取 3 字节。批量发送时注意 k 窗口,发太快会触发流控,实际测试中每发 8 帧插一个 S 帧确认更稳。参数调优上,k 值调大能提高吞吐但增加内存占用,w 值调小确认更及时但增加网络开销,一般保持默认即可,除非链路质量差需要调整。

注意:模拟从站时公共地址和 IOA 必须和主站点表严格对应,否则主站会丢弃或映射错误。测试前先导出点表核对一遍。

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

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

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

立即咨询