简介:这份资料面向电力系统自动化、配电终端及工控通信方向的开发者与调试人员,聚焦IEC-104规约常用报文的逐字节解析。内容围绕启动字符、APDU长度、控制域、类型标识、传送原因、公共地址与信息对象地址等关键字段展开,帮助读者快速理解遥测、遥信、遥控、总召唤及对时等典型报文的组成规则,解决开发与现场调试中报文看不懂、定位难的问题。资源包共11个文件,以PDF文档与HTML网页为主,辅以JavaScript脚本、字体图标及说明文本,压缩后约838KB,可直接打开PDF阅读,也可通过index.html在浏览器中浏览,便于随时查阅。目前已有8257人学习下载,适合需要系统掌握104报文结构、提升协议解析与排错效率的初、中级技术人员参考。
1. 从一条 104 报文说起:为什么它值得单独拆开看
很多做电力自动化的工程师第一次抓 104 报文时,都会愣一下:明明叫“规约”,抓出来的却是一串68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14这样的十六进制。它既不像 Modbus 那样一眼能看出功能码,也不像普通 TCP 那样有可读的 HTTP 头。原因在于 IEC-104 是“应用层规约 + TCP 承载”的组合体,报文本身被拆成了 APCI 和 ASDU 两层,前者管链路,后者管业务。
这套规约在国内变电站、配电自动化、新能源场站里用得极广,主站和 RTU、测控装置、保护装置之间的遥测、遥信、遥控、遥调基本都跑在它上面。标题里说的“常用报文详解”,落到实际工作就是三件事:能看懂抓包里的每一段字节、能判断链路为什么起不来、能定位遥测值为什么对不上。适合刚接触 104 调试的新人,也适合已经会调但没系统梳理过报文结构的老手。
2. IEC-104 报文结构拆解:APCI 与 ASDU 到底怎么分
2.1 APCI 六个字节固定头:启动字符、长度与四种控制域
IEC-104 的每一帧都以0x68开头,这是启动字符,抓包时看到它就说明这是一条 104 报文。紧接着一个字节是 APDU 长度,注意它统计的是“控制域 4 字节 + ASDU 长度”,不包含启动字符和长度字节本身。所以一条最短的链路报文长度是 4,一条带单点遥信的报文长度通常是 14 左右。
控制域 4 个字节决定了这帧报文是干什么的,常见有四种格式:
| 控制域首字节 | 报文类型 | 用途 |
|---|---|---|
| 0x01 / 0x03 | I 格式 | 带 ASDU 的信息传输,遥测遥信遥控都走它 |
| 0x07 | S 格式 | 确认收到对方的 I 帧,只回计数 |
| 0x0B | U 格式 STARTDT | 启动数据传输 |
| 0x43 | U 格式 STOPDT | 停止数据传输 |
| 0x83 | U 格式 TESTFR | 链路测试 |
判断方法很简单:看控制域第一个字节的最低位。最低位为 0 是 I 格式,为 1 且次低位为 0 是 S 格式,两个低位都为 1 是 U 格式。这个规则在写解析脚本时是第一个要落地的判断。
2.2 ASDU 结构:类型标识、可变结构限定词与传送原因
ASDU 才是业务本体,它的头部固定 6 个字节,顺序是:类型标识 TI、可变结构限定词 VSQ、传送原因 COT(2 字节)、公共地址 CA(2 字节),之后才是信息对象地址和具体数据。
类型标识决定这条报文是遥测还是遥信。常用的几个值:0x01单点遥信、0x03双点遥信、0x09归一化遥测、0x0B标度化遥测、0x0D短浮点遥测、0x2D单点遥信带时标、0x64总召唤、0x67时钟同步、0x2E双点遥信带 CP56Time2a 时标。
可变结构限定词这个字节容易被忽略,它高位的 SQ 位表示“信息对象是否连续寻址”,低 7 位表示“本帧包含多少个信息对象”。SQ=0 时每个对象都带自己的信息对象地址,SQ=1 时只写第一个地址,后续地址依次加一。总召唤响应里 SQ 经常是 1,因为遥测点号是连续的。
传送原因 2 字节里,低字节是原因值,高字节是源发站地址。常见原因值:0x03突发、0x05被请求、0x06激活、0x07激活确认、0x0A激活终止、0x14响应站召唤。调试时如果看到原因值对不上,比如总召唤回的是突发而不是被请求,基本可以判断装置侧配置有问题。
2.3 用 Python 把一条 104 报文拆成字段
光看表记不住,写个最小解析脚本最快。下面这段代码只处理 I 格式帧,把 APCI 和 ASDU 头部拆出来打印:
# iec104_parse.py # 解析单条 IEC-104 I 格式报文,输入为十六进制字符串 def parse_104(hex_str): data = bytes.fromhex(hex_str.replace(" ", "")) if data[0] != 0x68: return "非 104 报文:启动字符不是 0x68" apdu_len = data[1] ctrl = data[2:6] # 控制域最低位为 0 表示 I 格式 if ctrl[0] & 0x01 != 0: return "非 I 格式帧,控制域首字节=0x%02X" % ctrl[0] # 发送序号 15 位,接收序号 15 位 tx = ((ctrl[0] | (ctrl[1] << 8)) >> 1) & 0x7FFF rx = ((ctrl[2] | (ctrl[3] << 8)) >> 1) & 0x7FFF asdu = data[6:6 + apdu_len - 4] ti = asdu[0] # 类型标识 vsq = asdu[1] # 可变结构限定词 sq = (vsq >> 7) & 0x01 # 是否连续寻址 num = vsq & 0x7F # 信息对象个数 cot = asdu[2] | (asdu[3] << 8) # 传送原因 ca = asdu[4] | (asdu[5] << 8) # 公共地址 return { "APDU长度": apdu_len, "发送序号": tx, "接收序号": rx, "类型标识": hex(ti), "SQ": sq, "对象个数": num, "传送原因": hex(cot & 0xFF), "源发站": cot >> 8, "公共地址": ca, "ASDU原始": asdu.hex(" ") } if __name__ == "__main__": # 一条典型的单点遥信 I 帧 print(parse_104("68 0E 00 00 00 00 01 01 14 00 01 00 01 00 00 00"))逻辑上先校验启动字符,再判断控制域格式,然后按 15 位右移取序号。ASDU 部分按固定偏移取头部字段,apdu_len - 4是因为长度里含了 4 字节控制域。参数上要注意:cot & 0xFF取原因值,cot >> 8取源发站地址,这两个别搞反,否则总召唤的源发站会显示成原因值。实际调试时把抓到的十六进制串丢进去,几秒钟就能确认类型标识和传送原因是否符合预期。
3. 常用报文类型实战:总召唤、遥测、遥信、遥控怎么读
3.1 总召唤交互全过程与报文时序
总召唤是主站和装置建立数据同步的标准动作,交互顺序固定:主站发总召唤激活(TI=0x64,COT=0x06),装置回激活确认(COT=0x07),然后装置用若干帧 I 格式把遥测遥信送上来(COT=0x14 响应站召唤),最后装置发激活终止(COT=0x0A)。如果中间任何一步缺失,主站侧就会显示“总召超时”。
抓包时判断总召是否正常,看三个点:激活确认有没有在几百毫秒内回来、数据帧的公共地址是否和主站配置一致、激活终止有没有出现。常见故障是装置回了激活确认但数据帧一直不来,多半是装置侧点表没配或者信息对象地址越界。
3.2 遥测报文:归一化、标度化、短浮点三种值怎么换算
遥测是 104 里最容易算错的部分,因为同一个物理量可能用三种编码传。归一化值(TI=0x09)是 2 字节有符号整数,范围 -32768 到 32767,对应 -1 到 1 之间的比例,实际值 = 原始值 / 32768 × 量程。标度化值(TI=0x0B)是 2 字节有符号整数,直接就是工程量,不用再乘比例。短浮点(TI=0x0D)是 4 字节 IEEE 754,直接读就行。
下面这段代码演示三种遥测的取值:
import struct def decode_measured(ti, payload): # payload 为信息对象地址之后的数值字节 if ti == 0x09: # 归一化 raw = struct.unpack(">h", payload[:2])[0] return raw / 32768.0 # 返回 -1~1 的比例 elif ti == 0x0B: # 标度化 return struct.unpack(">h", payload[:2])[0] elif ti == 0x0D: # 短浮点 return struct.unpack(">f", payload[:4])[0] return None # 归一化原始值 0x4000 = 16384,比例约 0.5 print(decode_measured(0x09, bytes.fromhex("4000"))) # 短浮点 0x41480000 约等于 12.5 print(decode_measured(0x0D, bytes.fromhex("41480000")))参数说明:>h是大端有符号短整型,104 全部用大端;>f是大端单精度浮点。归一化值一定要确认量程系数,很多现场遥测偏差一倍就是量程配错。标度化值虽然直观,但要注意装置侧是否做了系数转换,否则主站显示的是原始码值。
3.3 遥信与遥控:单点双点区别、选择执行与返校
遥信单点(TI=0x01)用 1 字节表示分合,双点(TI=0x03)用 2 位表示分、合、中间态、无效四种状态。双点遥信在断路器位置判断上更可靠,因为能识别中间态。带时标的遥信(TI=0x2D、0x2E)会多 7 字节 CP56Time2a 时间,格式是毫秒低字节、毫秒高字节、分、时、日、月、年,解析时注意年是两位。
遥控走的是“选择-执行”两阶段:主站先发选择(COT=0x06,类型 0x2D 单点遥控或 0x2E 双点遥控),装置回选择确认(COT=0x07),主站再发执行(COT=0x06),装置回执行确认(COT=0x07)并动作。如果选择阶段就失败,说明遥控点号或公共地址不对;如果执行阶段失败,多半是装置侧五防闭锁或遥控压板没投。调试遥控时一定要先确认选择确认里的信息对象地址和主站下发的一致。
4. 104 链路调试与排错:从抓包到定位的完整路径
4.1 用 tcpdump 和 Wireshark 抓 104 报文
104 默认端口是 2404,抓包命令很直接:
# 抓取 2404 端口报文并保存,供 Wireshark 分析 tcpdump -i eth0 -nn port 2404 -w iec104.pcap # 实时看十六进制,确认链路是否在交互 tcpdump -i eth0 -nn -X port 2404参数说明:-i指定网卡,-nn不做端口和地址解析,-X打印十六进制和 ASCII。Wireshark 自带 IEC 60870-5-104 解析器,但默认可能把 2404 当普通 TCP,需要在 Decode As 里手动指定为 IEC104。解析出来后能直接看到类型标识、传送原因、信息对象地址,比手工拆字节快得多。
4.2 链路起不来的四类典型原因
第一类是 TCP 连不上,表现为三次握手失败,检查 IP、端口、防火墙。第二类是 TCP 通了但 STARTDT 没交互,抓包能看到主站发68 04 07 00 00 00但装置不回68 04 0B 00 00 00,通常是装置侧 104 服务没启动或链路地址配错。第三类是 STARTDT 成功但总召无响应,看公共地址和 ASDU 地址是否匹配。第四类是数据能上来但序号乱,I 帧的发送和接收序号不连续,说明有丢包或装置侧序号管理异常。
提示:序号不连续时先看是不是网络抖动导致丢包,再排查装置侧是否有多主站连接抢占。
4.3 常见异常报文与序号回绕处理
104 的发送序号和接收序号都是 15 位,范围 0 到 32767,超过后回绕到 0。解析脚本里做序号连续性判断时,不能简单用后一帧减前一帧等于 1,要处理回绕:如果当前序号小于前一帧且差值接近 32767,说明发生了回绕。另外 S 帧只确认接收序号,不携带数据,如果长时间只看到 S 帧没有 I 帧,说明对端没有数据要发,链路是空闲而非断开。
异常报文里还有一类是 TESTFR,主站或装置在链路空闲时会发测试帧保活,收到后要回确认。如果测试帧一直没被确认,链路会被判定为断开。调试长连接场景时,这个保活机制要确认双方都开启。
5. 进阶技巧:把 104 解析脚本做成可复用的诊断工具
前面几段代码都是单条解析,实际排错时更需要的是一边抓包一边自动统计。一个实用做法是把解析逻辑挂到 pcap 文件上,逐帧输出类型标识、传送原因、信息对象地址和数值,最后汇总各类型报文数量。这样一眼就能看出总召有没有走完、遥测帧有多少、有没有异常原因值。
具体可以这样组织:先用 scapy 或 dpkt 读 pcap,过滤出 2404 端口的 TCP 载荷,按0x68切分 APDU,再调用前面的解析函数。统计维度建议至少包含:I/S/U 帧数量、各类型标识计数、各传送原因计数、公共地址分布。如果发现某个公共地址的报文突然消失,基本就是那台装置掉线了。
另一个技巧是处理粘包。TCP 是流式的,一次 recv 可能拿到半条报文或多条报文,解析时必须先读长度字节再切分,不能假设一次读到的就是完整 APDU。判断方法是:缓冲区里如果剩余字节数小于长度字节声明的 APDU 长度加 2,就继续等下一批数据。这个细节在写实时解析工具时最容易踩坑,抓包文件里看不出来,但接实时流就会暴露。
最后给一个校验思路:把解析出来的遥测值和主站画面显示值对一遍,如果偏差固定倍数,查量程;如果偏差随机,查时标和刷新周期;如果某些点一直不变,查信息对象地址是否和点表一致。104 的排错说到底就是字节、地址、原因值三样东西对齐,对齐了链路就通,数据就准。
本文还有配套的精品资源,点击获取