ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

IEC 104/101规约报文解析与组帧实战:从源码到避坑指南

IEC 104/101规约报文解析与组帧实战:从源码到避坑指南 简介本资源面向电力自动化、配网通信及规约开发方向的工程师与学习者围绕DL/T634.5101-2002101规约与DL/T634.5104-2009104规约提供一套基于Java的解析与组装实现可用于实际场景中发送报文的生成、规约内容解析与交互调试帮助读者理解电力规约的报文结构与编解码逻辑。压缩包共43个文件以34个Java源码为核心辅以3个XML配置、2份规约实施细则文档、1份规约解析细则表格及README说明等整体约1.79MB目录按解析与交互模块划分结构清晰便于按需查阅。目前已有661人学习下载。读者可从中获得可运行的规约解析与组装代码、配套的101/104规约实施细则与解析细则参考以及报文生成与排错的实现思路适合作为电力规约开发与学习的实践素材。1. 从一份 iec-master 源码说起104 规约报文到底怎么抓、怎么解、怎么造手里拿到一份标注着 iec-master_104规约报文_101规约_IEC101_电力104规约_ 的代码包多数人的第一反应是翻目录找入口然后被一堆 0x68、0x0B、0x03 之类的十六进制常量劝退。IEC 60870-5-104下称 104 规约和它的串口兄弟 IEC 60870-5-101下称 101 规约是电力自动化里最常被拿来对接的两种远动通信协议前者跑在 TCP 上后者跑在串口上报文结构高度同源。真正让人头疼的不是协议本身而是「抓到的报文对不上文档」「主站发总召唤从站不回」「遥测值跳变但报文看着没问题」这类现场问题。这篇笔记面向需要把 104/101 规约跑通、调稳、排错的工程师从 iec-master 这类实现的结构切入把报文怎么组、参数怎么设、坑在哪一条条讲清楚新手能照着复现熟手能对着边界参数核对。2. 104 与 101 规约的报文骨架APCI、ASDU 和那三个关键字段2.1 为什么 104 报文总以 0x68 开头104 规约的每一帧都从 0x68 开始这个字节叫启动字符作用是让接收端在字节流里找到帧边界。紧接着的一个字节是 APDU 长度表示从第四个字节控制域第一个字节到帧尾的字节数注意它不包含启动字符和长度字节本身。很多人第一次解析时把长度理解成整帧长度结果偏移全部错位这是最典型的翻车点。APCI应用规约控制信息固定 6 字节1 字节启动字符 1 字节长度 4 字节控制域。控制域决定这一帧是 I 帧、S 帧还是 U 帧。I 帧携带 ASDU用于传输遥测、遥信、遥控等实际数据S 帧只做确认不带 ASDUU 帧负责链路控制比如启动、停止、测试。判断依据是控制域第一个字节的最低两位bit00 是 I 帧bit01 且 bit10 是 S 帧bit01 且 bit11 是 U 帧。ASDU应用服务数据单元才是业务载荷它的结构是类型标识1 字节 可变结构限定词1 字节 传送原因2 字节 公共地址2 字节 信息对象地址3 字节 信息元素。类型标识决定这条报文是总召唤、遥测上送还是遥控命令可变结构限定词的最高位表示信息对象是顺序还是离散低 7 位是信息对象个数。2.2 101 规约和 104 的差异到底在哪101 规约跑在串口上帧结构比 104 多了一层链路层。它的固定帧格式是启动字符 0x10 控制域 链路地址 校验和 结束字符 0x16。可变帧格式则是启动字符 0x68 长度 控制域 链路地址 ASDU 校验和 结束字符 0x16。可以看到 101 的可变帧和 104 的 I 帧在 ASDU 部分几乎一致差异在于 101 多了链路地址和校验和且没有 TCP 的流控和确认机制。把 101 的 ASDU 直接搬到 104 上通常能解析但反过来不行因为 104 没有链路地址字段公共地址在 ASDU 里。现场调试时如果发现 104 报文解析出来公共地址对不上先确认是不是把 101 的链路地址当成了公共地址。2.3 用 Python 解析一帧 104 报文的最小实现下面这段代码只做一件事给一帧完整的 104 报文拆出 APCI 和 ASDU 的关键字段。它不依赖任何第三方库方便直接嵌到调试脚本里。def parse_104_frame(raw: bytes): # raw 是从 socket 或抓包文件里读到的完整一帧 if raw[0] ! 0x68: raise ValueError(不是 104 帧启动字符错误) apdu_len raw[1] # 长度字段不包含启动字符和长度字节本身 if len(raw) ! apdu_len 2: raise ValueError(f帧长度不匹配声明 {apdu_len 2}实际 {len(raw)}) ctrl raw[2:6] frame_type I if (ctrl[0] 0x01) 0 else (S if (ctrl[0] 0x03) 0x01 else U) result {type: frame_type, apci_len: apdu_len, ctrl: ctrl.hex()} if frame_type I: asdu raw[6:] type_id asdu[0] vsq asdu[1] cot int.from_bytes(asdu[2:4], little) ca int.from_bytes(asdu[4:6], little) ioa int.from_bytes(asdu[6:9], little) result.update({ type_id: hex(type_id), vsq: vsq, num_objects: vsq 0x7F, is_sequence: bool(vsq 0x80), cot: cot, common_addr: ca, ioa: ioa, }) return result逻辑说明先校验启动字符和长度再根据控制域最低两位判断帧类型。I 帧才继续解析 ASDUS 帧和 U 帧直接返回控制域。参数说明apdu_len是 APDU 长度等于整帧长度减 2cot是传送原因低 6 位是原因值高 2 位是源发站地址标志ioa是信息对象地址104 里固定 3 字节小端。如果现场报文里num_objects和实际解析出的对象数对不上先检查 VSQ 最高位是不是 1顺序模式下只给首地址后续地址靠递增。提示解析前先确认字节序。104 规约的 ASDU 里多字节字段一律小端但部分老设备的实现会在大端和小端之间摇摆遇到数值离谱时先怀疑字节序。3. 用 iec-master 跑通主站与从站环境、配置和最小联调3.1 拿到 iec-master 后先确认的三件事第一确认代码包里的协议实现覆盖了你要用的类型标识。104 规约的类型标识有几十种常见的有 M_SP_NA_1单点遥信类型 1、M_ME_NA_1归一化遥测类型 9、C_SC_NA_1单点遥控类型 45、C_IC_NA_1总召唤类型 100。如果代码里只实现了遥测上送遥控相关类型缺失那它只能当从站模拟器用不能当主站。第二确认 TCP 角色。104 规约里主站是客户端从站是服务端主站主动发起连接。有些实现把角色写反了导致连不上。看代码里是connect还是listen就能判断。第三确认端口。104 规约默认端口是 2404但现场经常改成其他端口。代码里如果硬编码了 2404要么改代码要么在配置里覆盖。3.2 从站模拟器的启动参数怎么设以常见的 Python 实现为例启动一个从站模拟器通常需要指定监听地址、端口、公共地址和遥测遥信初始值。下面是一个典型的启动命令和对应配置。python iec104_server.py --host 0.0.0.0 --port 2404 --common-addr 1 --points 100参数说明--host 0.0.0.0表示监听所有网卡联调时如果主站和从站在同一台机器上用 127.0.0.1 更安全--port 2404是标准端口改成其他端口时主站侧要同步改--common-addr 1是 ASDU 公共地址主站和从站必须一致不一致时从站会丢弃报文且通常不报错这是最隐蔽的坑之一--points 100表示模拟 100 个信息对象用于测试主站的点表容量。启动后从站会等待主站连接。此时用ss -tlnp | grep 2404确认端口处于监听状态。如果端口没起来先看是不是被防火墙拦了再看代码里有没有绑定失败但异常被吞掉的情况。3.3 主站侧发起总召唤并解析上送报文主站连上从站后第一步通常是发总召唤类型 100让从站把所有遥测遥信上送一遍。下面这段代码演示主站如何组一帧总召唤并发送。import socket def build_total_interrogation(common_addr1): # 类型标识 100可变结构限定词 1传送原因 6激活公共地址信息对象地址 0 asdu bytes([100, 1, 6, 0, common_addr 0xFF, (common_addr 8) 0xFF, 0, 0, 0]) apdu_len 4 len(asdu) # 控制域 4 字节 ASDU frame bytes([0x68, apdu_len, 0x01, 0x00, 0x00, 0x00]) asdu return frame s socket.create_connection((127.0.0.1, 2404), timeout5) s.send(build_total_interrogation(common_addr1)) resp s.recv(4096) print(resp.hex())逻辑说明控制域用01 00 00 00表示 I 帧且发送序号为 0实际实现里发送序号和接收序号要按规约递增维护这里为了最小演示做了简化。参数说明传送原因 6 表示激活从站回送时传送原因通常是 7激活确认或 20响应总召唤信息对象地址填 0 表示总召唤不针对特定点。收到响应后用第 2 章的解析函数逐帧拆开确认类型标识是不是 1 或 9传送原因是不是 20。如果主站发了总召唤但从站不回按这个顺序排查公共地址是否一致、传送原因是否被从站接受、从站是否要求先发 U 帧启动链路。104 规约里链路启动用 U 帧的 STARTDT 激活部分从站实现要求主站先发0x68 0x04 0x07 0x00 0x00 0x00才会响应总召唤。注意总召唤的响应可能分多帧上送不要只 recv 一次就认为拿全了。按信息对象个数和帧里的 VSQ 判断是否收完或者等从站发来传送原因 10激活终止。4. 报文解析与组帧的避坑清单从字节序到序号翻转4.1 公共地址不一致导致从站静默丢弃现象主站能连上从站发总召唤后从站 TCP 层有响应但应用层没有任何 ASDU 返回抓包看到从站直接不回或回 S 帧确认。原因104 规约的 ASDU 公共地址是 2 字节主站发的公共地址和从站配置的不一致时从站按规约应当丢弃该 ASDU且不产生错误响应。很多从站实现连日志都不打看起来就像没收到。解决联调第一步就核对公共地址。主站侧和从站侧的配置项名称可能不同有的叫 common address有的叫 ASDU address有的叫站地址值必须一致。改完后重启从站不要指望热加载。4.2 发送序号和接收序号不递增导致链路断开现象主站和从站连上后正常收发几帧然后突然断开重连后重复同样过程。原因104 规约的 I 帧控制域里发送序号和接收序号各占 15 位每发一个 I 帧发送序号加 1每收一个 I 帧接收序号加 1到 32767 后回绕到 0。如果实现里忘了递增或者递增后没有对 32768 取模从站会认为序号异常并断开链路。解决在发送和接收路径上分别维护序号变量每次操作后(seq 1) 0x7FFF。抓包时看控制域的第 2、3 字节和第 4、5 字节正常应该连续递增。如果看到序号跳变或重复就是这里的问题。4.3 遥测值跳变但报文看着正常现象主站显示的遥测值和从站实际值对不上有时差一个固定倍数有时正负号相反。原因104 规约的遥测类型有归一化值类型 9、标度化值类型 11、短浮点数类型 13。归一化值是 2 字节有符号数范围 -1 到 1 减 2 的负 15 次方实际工程值需要乘以量程系数标度化值是 2 字节有符号整数直接对应工程值短浮点数是 4 字节 IEEE 754。如果从站发的是归一化值主站按标度化值解析就会差一个系数。解决先确认类型标识。类型 9 按归一化处理类型 11 按整数处理类型 13 按浮点处理。归一化值的解析公式是value raw / 32768.0 * range其中 range 是量程上限。如果正负号相反检查是不是把有符号数当成了无符号数。4.4 总召唤响应丢帧导致点表不全现象总召唤后主站收到的点数少于从站配置的点数但链路正常后续变化上送也能收到。原因总召唤响应帧数较多时从站可能受限于发送缓冲区或主站接收缓冲区导致部分帧丢失。104 规约基于 TCP理论上不丢帧但如果主站 recv 缓冲区设得太小或者从站发送太快触发 TCP 窗口关闭应用层可能读到不完整的帧。解决主站侧把 socket 接收缓冲区调大s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)。从站侧在每帧之间加小延时或者按规约用 S 帧做流控。收完后用总召唤激活终止传送原因 10判断结束不要靠超时。4.5 101 规约校验和算错导致串口通信失败现象101 规约串口通信时从站完全不响应或者响应但主站校验失败。原因101 规约的校验和是从控制域到 ASDU 末尾逐字节累加后取低 8 位不包括启动字符、长度、校验和本身和结束字符。很多人把启动字符也算进去或者忘了取低 8 位。解决写一个独立的校验和函数单独测试。校验和计算范围是固定帧从控制域到链路地址可变帧从控制域到 ASDU 最后一个字节。算完后和报文里的校验和字节比对不一致就逐字节打印累加过程。def checksum_101(data: bytes) - int: # data 是从控制域开始到 ASDU 结束的字节 return sum(data) 0xFF参数说明data不包含启动字符、长度、校验和、结束字符。固定帧的 data 是控制域 链路地址可变帧的 data 是控制域 链路地址 ASDU。如果现场校验总是不对先确认串口参数波特率、数据位、停止位、校验位101 规约常用 9600 8E1 或 9600 8N1参数不匹配时收到的字节本身就是错的。5. 进阶用报文回放和模糊测试验证规约实现的边界5.1 把抓包文件变成可回放的测试用例现场抓到的报文是最真实的测试素材。把 tcpdump 或 Wireshark 抓到的 104 流量导出为十六进制文本按帧切分后逐帧喂给解析函数可以快速验证实现是否覆盖了所有类型标识和边界情况。我一般会写一个回放脚本把每帧的解析结果和预期结果做比对不一致的帧单独打印出来。def replay_frames(hex_file: str): with open(hex_file) as f: for line in f: raw bytes.fromhex(line.strip()) try: info parse_104_frame(raw) print(info) except Exception as e: print(f解析失败: {raw.hex()} - {e})逻辑说明每行一个完整帧的十六进制字符串解析失败不中断继续跑完所有帧。参数说明hex_file里不要有空行和注释帧与帧之间用换行分隔。如果某帧解析失败先确认它是不是完整的帧抓包文件里可能包含 TCP 握手包或半帧。5.2 模糊测试改一个字节看实现怎么崩规约实现的健壮性往往在异常输入下才暴露。拿一帧正常的 I 帧逐字节修改后喂给解析函数观察是否抛异常、是否死循环、是否返回错误结果。重点改这几个位置长度字段改成比实际大或小、类型标识改成未实现的值、VSQ 的个数改成超过实际字节数、公共地址改成 0 或 65535。我踩过的一个坑是某实现里 VSQ 个数直接用来做循环次数没有校验剩余字节是否够结果收到一个恶意构造的帧后循环读越界进程直接崩。修复方式是在循环前先算剩余字节数 // 每个信息对象长度取小值。5.3 用 Wireshark 过滤 104 流量的实用技巧Wireshark 默认能识别 104 规约但需要确认端口。如果现场端口不是 2404在 Decode As 里手动指定。常用过滤表达式tcp.port 2404看所有流量iec60870_104.type 100只看总召唤iec60870_104.cot 20只看总召唤响应。如果 Wireshark 解析出来的字段和你的实现不一致以规约原文为准Wireshark 的 dissector 也有版本差异。提示回放和模糊测试只在实验室环境做不要对着运行中的生产系统发异常帧。生产系统上做验证时先用从站模拟器接主站确认主站行为正常后再接真实从站。5.4 一个我坚持了多年的习惯每次接手新的 104/101 实现第一件事不是读代码而是用模拟器发一帧总召唤抓包看响应。响应正常说明链路和基本解析没问题响应异常从公共地址、传送原因、序号三个地方依次排查。这个习惯帮我省掉了大量读代码的时间也避免了一上来就陷进实现细节里。规约调试的本质是对着字节流做排除法工具和脚本只是让排除更快。希望帮到你。本文还有配套的精品资源点击获取
返回列表