简介:面向电力自动化通信开发者的IEC 60870-5-104协议仿真工具,提供Master主站与Slave从站两种角色,可在VS2015环境下模拟真实远动通信过程,用于验证SCADA系统报文解析、连接建立与断开、I帧/S帧/U帧交互及错误校验逻辑,适合从事变电站自动化、配电自动化开发与调试的工程师进行联调测试。压缩包共104个文件、约14.18MB,以h头文件与cpp源文件为核心,配合sbr、obj、dll、exe等编译产物及vcproj、sln工程文件,另含aps、clw、ncb等开发辅助文件,可直接在VS2015中打开编译运行。目前已有1510人学习下载。借助主从双端模拟,读者可快速搭建测试环境,验证ASDU与APDU报文结构、信息对象类型及服务质量等级,排查通信延迟与丢包问题,缩短协议对接与兼容性验证周期。
1. 一套能同时跑主站和从站的 IEC104 模拟器,到底解决什么问题
做电力自动化、配网终端或者工业网关对接的人,几乎都绕不开 IEC 60870-5-104 这个规约。它不像 Modbus 那样随便抓个串口助手就能看,也不像 MQTT 那样有现成的可视化客户端。真正上手时最尴尬的局面是:手上有主站软件,但没有从站设备;或者有 RTU 样机,却找不到一个能稳定发总召、能改遥测值的主站。这套 IEC104 模拟器(master & slave)就是冲着这个空档来的——同一个工具里既能当主站发命令,也能当从站应答,把一条 104 链路的两端都握在自己手里。它适合规约开发、现场调试、教学演示和联调前的自测,尤其适合那些需要在没有真实设备的情况下把点表、时标、品质位这些细节跑通的人。下面按“它是什么、怎么配、怎么验、坑在哪”的顺序拆开讲。
2. IEC104 的链路模型与模拟器的角色分工
2.1 为什么 104 不能像 Modbus 那样“随便连”
IEC 60870-5-104 是把 101 规约的 ASDU 承载在 TCP/IP 上的版本,默认端口 2404。它和 Modbus TCP 最大的区别在于:104 有一条明确的“链路建立”过程,主站发起 TCP 连接后,还要通过 U 帧(STARTDT)激活传输,从站回 STARTDT CON 之后,才允许传 I 帧(带 ASDU 的数据帧)。这中间还夹着 S 帧做确认、K 帧做序号窗口控制。很多新手第一次抓包看到一堆 68 开头、长度 4 或 6 的短帧,会以为是心跳,其实那是链路层的握手和确认。
模拟器的价值就在这里:它把 U 帧、S 帧、I 帧的收发和序号维护都封装好了,你只需要关心 ASDU 里的类型标识、传送原因、公共地址和信息对象地址。主站模式下,它替你发总召(C_IC_NA_1,类型标识 100)、时钟同步(C_CS_NA_1,103)、遥控(C_SC_NA_1,45);从站模式下,它替你回总召确认、回遥测/遥信数据、处理遥控选择与执行。换句话说,它把 104 里最容易写错的那层“壳”做掉了,留下业务层给你调。
2.2 主站与从站两种模式的能力边界
主站模式的核心能力是“发得出去、收得回来”。典型操作包括:建立 TCP 连接、发 STARTDT 激活、发总召并解析从站上送的 ASDU、按点号读取遥测遥信、下发单点/双点遥控并处理选择确认和执行确认。从站模式的核心是“站得住、答得对”:监听 2404 端口、接受主站连接、响应 STARTDT、按配置的点表回总召数据、对遥控命令做选择/执行两步应答、维护接收序号和发送序号。
这里要提醒一句:模拟器不是协议栈库,它的定位是“可操作的调试面板”,不是给你嵌到产品代码里的 SDK。如果你的目标是给自己的网关做 104 从站,模拟器适合用来当对端主站做验证;反过来,如果你在写主站,模拟器当从站能帮你把点表和应答逻辑先跑顺。选型时先想清楚你要验证的是哪一端,别指望一个工具把两端的产品化都包了。
2.3 点表配置是模拟器的“数据底座”
无论主站还是从站,模拟器都依赖一份点表来描述“有哪些信息对象”。常见做法是用 CSV 或界面表格配置:信息对象地址(IOA)、类型标识(TI)、是否带时标、初始值、品质位。比如遥测用 M_ME_NC_1(13,短浮点)或 M_ME_NA_1(9,归一化值),遥信用 M_SP_NA_1(1,单点)或 M_SP_TB_1(30,带 CP56Time2a 时标)。点表配错,后面所有现象都会变得“玄学”——主站收到了数据但对不上点,或者总召回来一堆但界面不显示。所以第一步永远是把点表的 IOA 和 TI 对齐对端设备的规约点表文档。
3. 用模拟器搭一条可复现的 104 链路
3.1 从站模式:先把“被总召”跑通
从站模式是最容易验证的起点,因为你可以用模拟器自己当主站去连它,也可以抓包看应答。启动从站前,先确认本机 2404 端口没被占用,然后加载点表、设置公共地址(Common Address of ASDU,通常 1)、设置站地址。下面是一段用 Python 模拟“主站侧最小验证”的代码,用来确认从站是否正常响应 STARTDT 和总召——实际用模拟器界面操作也行,但脚本能让你看清帧的往返。
import socket import struct HOST, PORT = "127.0.0.1", 2404 def build_u_frame(control): # U帧:控制域4字节,STARTDT act = 0x07 0x00 0x00 0x00 return b"\x68\x04" + control def build_total_interrogation(common_addr=1): # I帧:类型标识100(总召)、可变结构限定词0x01、传送原因0x06(激活)、公共地址2字节、IOA 3字节=0 asdu = struct.pack("<BBBHB", 100, 0x01, 0x06, common_addr, 0) # 控制域:发送序号和接收序号先置0,真实环境需按窗口维护 control = struct.pack("<HH", 0x0000, 0x0000) return b"\x68" + bytes([4 + len(asdu)]) + control + asdu with socket.create_connection((HOST, PORT), timeout=5) as s: s.sendall(build_u_frame(b"\x07\x00\x00\x00")) # STARTDT act print("STARTDT resp:", s.recv(64).hex()) s.sendall(build_total_interrogation()) print("总召应答:", s.recv(1024).hex())这段代码的逻辑说明:build_u_frame构造的是链路激活帧,控制域第一个字节 0x07 表示 STARTDT 激活;build_total_interrogation里类型标识 100 是总召,传送原因 0x06 是“激活”,公共地址按 2 字节小端打包,信息对象地址 3 字节置 0 表示站总召。参数上要注意:控制域的发送/接收序号在真实交互中必须递增维护,这里为了演示置 0,只适合单次握手验证;如果从站严格校验序号,你会看到它不回数据或者直接断链,这时候就要把序号逻辑补上。跑通这一步,说明从站的链路层和总召应答是活的,接下来再在模拟器界面上看遥测值是否按点表上送。
3.2 主站模式:总召、读点、遥控的完整闭环
主站模式建议按“连接 → 激活 → 总召 → 解析 → 遥控”五步走。连接阶段填对端 IP 和 2404 端口;激活阶段发 STARTDT;总召阶段发类型标识 100、传送原因 6;解析阶段重点看 ASDU 里的可变结构限定词(VSQ)——最高位是 SQ 标志,低 7 位是信息对象个数,SQ=0 表示每个对象都带独立 IOA,SQ=1 表示连续 IOA 只写第一个。很多解析错误就出在没处理 SQ 标志,把连续地址当成了重复地址。
遥控是主站模式里最容易翻车的一环。以单点遥控 C_SC_NA_1(45)为例,流程是“选择 → 选择确认 → 执行 → 执行确认”。选择和执行的区别在传送原因:选择是 6(激活),执行也是 6,但信息对象里的 S/E 位不同(选择 S/E=1,执行 S/E=0)。模拟器一般会提供选择/执行两个按钮,操作时先点选择,看到对端回的选择确认(传送原因 7,激活确认)再点执行。如果跳过选择直接执行,有的从站会拒绝,有的会直接动作——这就是现场必须按规约走两步的原因。
3.3 用抓包和日志定位“数据对不上”的问题
104 的问题十有八九表现为“主站收到了但显示不对”或者“总召没反应”。这时候别急着改代码,先抓包。Wireshark 对 104 有内置解析,过滤tcp.port == 2404就能看到每一帧的类型标识、传送原因、公共地址和 IOA。对照模拟器的发送日志,重点看三处:一是公共地址是否一致(有的设备 ASDU 公共地址和链路地址不是一回事);二是传送原因是否用了“响应”而不是“突发”(总召应答的传送原因通常是 20,响应总召);三是时标格式,CP56Time2a 是 7 字节,年只占 7 位,月份和日期的位域如果按普通整数解析就会错位。模拟器的日志面板一般会把这些字段拆开显示,抓包和日志对着看,比盲猜快得多。
4. 参数、点表与时标:那些决定成败的细节
4.1 K/W 窗口与 t1/t2/t3 超时怎么设
104 的链路层有几个关键参数:K 是发送方未确认 I 帧的最大数量(默认 12),W 是接收方收到多少个 I 帧后必须发 S 帧确认(默认 8),t1 是发送后等确认的超时(默认 15 秒),t2 是无数据时发确认的超时(默认 10 秒),t3 是链路空闲时发测试帧的超时(默认 20 秒)。模拟器一般会把这些做成可调项。调试阶段建议先用默认值,等链路稳定后再按现场网络质量微调。如果 K 设得太大而 W 太小,会出现“发出去一堆但确认跟不上”,抓包能看到大量重传;如果 t1 设得太短,弱网下会频繁断链重连,现象是总召刚发出去连接就掉了。这些参数没有万能值,但“先默认、再观察、后微调”的顺序不会错。
4.2 点表里 TI 与 IOA 的对应关系
点表是模拟器的核心配置,下面这张表是常见类型标识的对照,配置时按对端文档选,别凭记忆填。
| 类型标识 | 名称 | 典型用途 | 是否带时标 |
|---|---|---|---|
| 1 | M_SP_NA_1 | 单点遥信 | 否 |
| 3 | M_DP_NA_1 | 双点遥信 | 否 |
| 9 | M_ME_NA_1 | 归一化遥测 | 否 |
| 13 | M_ME_NC_1 | 短浮点遥测 | 否 |
| 30 | M_SP_TB_1 | 带时标单点遥信 | 是 |
| 36 | M_ME_TF_1 | 带时标短浮点遥测 | 是 |
| 45 | C_SC_NA_1 | 单点遥控 | 否 |
| 100 | C_IC_NA_1 | 总召 | 否 |
配置时最容易犯的错是“遥测用了类型 9 却填了浮点值”。类型 9 是归一化值,范围 -1 到 1 之间的定点数,填 220.5 这种工程值进去,主站解析出来会是完全不同的数。要传工程值就用类型 13(短浮点)。另一个坑是 IOA 重复:同一个公共地址下 IOA 必须唯一,重复了模拟器可能只回其中一个,主站那边就表现为“少点”。
4.3 时标与品质位:别让“看起来对”骗了你
带时标的类型(30、36)在 ASDU 末尾附 7 字节 CP56Time2a。它的结构是:毫秒 2 字节(低 16 位,0~59999)、分钟 1 字节(低 6 位)、小时 1 字节(低 5 位)、日 1 字节(低 5 位)、月 1 字节(低 4 位)、年 1 字节(低 7 位,表示 2000 年起的偏移)。如果你用普通的时间戳直接塞进去,主站显示的时间会差几十年,这种“看起来有数据但时间不对”的问题最耗时间。品质位(QDS)也别忽略:IV 位表示值无效,NT 位表示非当前值,主站界面可能会因为 IV=1 而把数值灰掉,让人误以为没收到。
5. 避坑与排查:五条来自联调现场的血泪经验
5.1 现象:TCP 连上了,但总召没有任何回应
原因通常是 STARTDT 没发或者发错。104 规定 TCP 连接建立后必须由主站发 STARTDT 激活,从站才允许传 I 帧。有些模拟器默认自动发,有些需要手动点“激活”。排查时先抓包看有没有 68 04 07 00 00 00 这一帧,没有就补发;有但没回 68 04 0B 00 00 00,说明从站侧没接受激活,检查从站的公共地址和端口配置。
5.2 现象:总召回来了,但主站只显示部分点
先看 VSQ 的 SQ 标志。SQ=1 时后续对象共用连续 IOA,只解析第一个地址就会漏掉后面的点。再看点表的 IOA 是否重复、TI 是否和对端一致。还有一种情况是传送原因用了“突发”(3)而不是“响应”(20),某些主站只认响应帧,突发帧会被丢弃。解决方法是把模拟器从站的应答传送原因改成 20,并确认 SQ 处理逻辑。
5.3 现象:遥控选择成功,执行却超时
多半是 S/E 位没翻转。选择帧的信息对象里 S/E=1,执行帧 S/E=0,如果两次都发成 1,从站会认为你在重复选择,执行确认永远不来。另一个可能是遥控点号在点表里没配成类型 45,模拟器按遥信处理了。排查时抓包对比选择和执行两帧的 ASDU 字节,看第 7 个字节(信息对象第一个字节)的最低位是否变化。
5.4 现象:链路跑一段时间就断,重连后序号错乱
这是 K/W 窗口和序号维护的问题。104 的发送序号和接收序号是 15 位循环的,断链重连后必须从 0 重新开始,如果模拟器复用了旧序号,从站会认为帧乱序而丢弃。解决方法是重连时重置收发序号,并把 K 调小一点观察是否还断。弱网环境下 t1 适当放大到 20 秒以上,能减少误判断链。
5.5 现象:遥测值显示成天文数字或负数
先确认 TI 和数值类型匹配。类型 9 是归一化值(-1~1 的定点),类型 13 才是短浮点。如果点表配了 9 却填了 220,解析出来就是错的值。反过来,配了 13 却按整数打包,浮点位模式会被解释成极大的数。解决方法是统一按“工程值用 13、归一化用 9”的规则配点表,并在模拟器里用原始字节视图核对一遍打包结果。
6. 进阶:把模拟器当成回归测试的固定对端
模拟器真正省时间的地方,不是手动点几下看数据,而是把它固定成回归测试里的“对端”。我的习惯是:把点表导出成固定文件,从站模式启动后不关,主站侧用脚本跑一组固定用例——总召、单点遥控选择/执行、带时标遥测上送,每次改完代码就跑一遍,比对抓包里的类型标识和传送原因是否符合预期。下面这段是校验总召应答里传送原因是否为 20 的片段,思路可以复用到其他用例。
def check_interrogation_response(frame: bytes): # 跳过 68、长度、4字节控制域,进入 ASDU asdu = frame[6:] ti = asdu[0] cot = asdu[2] & 0x3F # 传送原因低6位 if ti == 100 and cot == 20: return "总召响应正常" return f"异常: TI={ti}, COT={cot}"参数说明:asdu[2] & 0x3F取传送原因的低 6 位,因为高 2 位是 P/N 和测试标志;TI 为 100 表示总召,COT 为 20 表示“响应总召”。这个校验函数可以挂在每次总召之后,失败就打印原始 hex,比人眼看日志可靠。再进一步,可以把“选择确认 COT=7、执行确认 COT=7、执行后遥信变位 COT=3”这几条做成断言,形成一个小型回归集。
还有一个容易被忽略的技巧:模拟器的从站模式支持改值后主动上送(突发),这可以用来验证主站的变位处理。做法是改一个遥信点的值,观察主站是否在不等总召的情况下收到 COT=3 的帧。如果主站没反应,问题往往在主站的变位订阅逻辑,而不是模拟器。用这种方式把“总召”和“突发”两条路径都走一遍,联调时心里就有底了。
从那以后我每次接 104 的活,都先把模拟器的从站拉起来、点表固定、抓包开着,再动主站代码——先确认对端是“讲规矩”的,再去怀疑自己。希望帮到你。
本文还有配套的精品资源,点击获取