ARTICLE DETAIL

资讯详情

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

IEC-104 报文解析实战:APCI 控制域与 ASDU 类型标识详解

IEC-104 报文解析实战:APCI 控制域与 ASDU 类型标识详解 简介这份资源面向电力系统通信开发、调试与运维人员以及需要快速掌握IEC-104规约的初学者系统梳理了104报文中常用类型的作用与每个字节的含义帮助读者在开发与排错中迅速定位问题、理解报文规则。压缩包共11个文件约838KB包含PDF文档、HTML页面、JavaScript脚本、字体与图标等资源其中PDF适合离线精读HTML页面可直接在浏览器中打开浏览脚本与样式文件支撑页面交互与展示效果。目前已有8257人学习下载说明其在电力自动化领域具有较高的参考价值。读者可借助这份资料建立对104报文结构的整体认知掌握常用报文类型的字段解析方法并对照实际抓包数据验证理解从而在开发调试中减少试错成本提升协议对接与故障排查效率。1. 电力系统 IEC-104 报文到底在传什么从四遥点到主站的一条链路厂站侧 RTU 上电主站前置服务起来104 链路一建立最先跑起来的往往不是遥测而是总召唤。很多刚接手 104 调试的人会盯着 Wireshark 里那一串十六进制发懵68 开头、两个长度字节、四个控制域字节后面跟着 ASDU看着像乱码其实每一个字节都有明确归属。IEC-104 就是 IEC 60870-5-104它把 60870-5-101 的链路层搬到 TCP/IP 上用 2404 端口承载远动报文国内变电站、配电自动化、新能源场站到调度的数据上送绝大多数走的就是它。它传的东西说白了就是四遥遥测、遥信、遥控、遥调外加电度、定值、文件传输这些扩展。这篇文章不空谈标准而是按一线调试的顺序把 104 报文的类型标识、传送原因、控制域、信息对象地址、时标这些真正决定你能不能抓对包、解对点的东西拆开讲让新手能照着抓包复现熟手能对着参数边界判断问题出在链路层还是应用层。104 的报文分两大类一类是链路控制报文只有 APCI没有 ASDU比如启动、停止、测试、确认另一类是应用报文APCI 后面挂 ASDU承载具体数据。判断一条 104 链路是否健康第一步不是看数据对不对而是看控制域里的发送序号和接收序号是否连续、t1/t2/t3 定时器是否正常。很多现场“数据不刷新”的锅最后查出来是链路层序号对不上而不是点表配错。所以理解 104必须先把 APCI 这层黑匣子打开再去看 ASDU 里到底装了什么。2. IEC-104 报文结构拆解APCI 四个控制域字节怎么读2.1 启动字符、长度与 2404 端口一条 104 报文的最外层结构非常固定第一个字节是启动字符 0x68第二个字节是 APDU 长度注意这个长度只统计它后面的字节数也就是 4 个控制域字节加上 ASDU 的长度不包含 0x68 和长度字节本身。所以当你看到68 04 07 00 00 00长度是 04后面正好四个控制域字节没有 ASDU这就是一条典型的 U 格式链路报文。端口方面标准默认 2404但现场经常被改成 2405、2406 甚至 9999抓包前先跟主站和厂站确认端口否则过滤器写错抓半天一个包都没有。用 Wireshark 抓 104 最省事的过滤写法是tcp.port 2404如果端口被改过就换成实际端口。想只看应用报文可以再加tcp.len 6因为纯链路报文 TCP 载荷只有 6 字节。下面这条命令是我在 Linux 前置机上常用的抓包方式把包存下来再拖到 Wireshark 里分析# 在 2404 端口抓 104 报文-s 0 抓完整帧-w 存成 pcap 供 Wireshark 分析 tcpdump -i eth0 -s 0 -w iec104.pcap tcp port 2404参数说明-i eth0指定网卡现场如果是 bond 口就换成 bond0-s 0表示不截断104 报文虽然短但截断后 ASDU 可能不完整-w直接落盘避免终端刷屏看不清。抓完用 Wireshark 打开在过滤栏输入iec60870_104可以直接调用内置解析器比人肉数字节快得多。2.2 I 帧、S 帧、U 帧控制域第一个字节就能分辨控制域四个字节是 104 的精华也是最容易看错的地方。判断帧类型只看第一个控制域字节的最低位和次低位帧类型第一个控制域字节特征作用I 帧bit0 0承载 ASDU带发送序号和接收序号S 帧bit0 1, bit1 0只确认接收不带 ASDUU 帧bit0 1, bit1 1链路启动、停止、测试、确认I 帧格式里第一个字节和第二个字节组成发送序号 N(S)第三个字节和第四个字节组成接收序号 N(R)每个序号占 15 位低字节在前。比如68 0E 00 00 02 00 ...发送序号是 0接收序号是 2说明本端已经收到对方 2 帧。S 帧只填接收序号U 帧则用第一个字节的低两位区分具体功能常见的有 STARTDT act0x07、STARTDT con0x0B、TESTFR act0x43、TESTFR con0x83、STOPDT act0x13、STOPDT con0x23。现场排查链路断没断最直接的办法就是看有没有 TESTFR 心跳。t3 默认 20 秒如果 20 秒内没有任何报文主站或厂站会发 TESTFR act对方必须回 TESTFR con。如果只看到 act 看不到 con链路就是单向通的问题多半在防火墙或对端进程。这里有个血泪经验有些厂站设备 t3 被改成 60 秒甚至更长主站侧却按 20 秒判断结果主站频繁报链路中断实际链路是好的只是心跳节奏没对齐。所以对接前一定把 t1、t2、t3 三个定时器白纸黑字确认下来。2.3 用 Python 解析控制域验证序号连续性光靠眼睛数序号容易翻车尤其是报文一多。我一般会写个小脚本把 pcap 里的 104 控制域抽出来检查 N(S) 和 N(R) 是否连续。下面这段代码用 scapy 读取 pcap只解析 TCP 载荷的前六个字节够用了from scapy.all import rdpcap, TCP, Raw def parse_apci(payload): # payload 至少 6 字节0x68 len 4 控制域 if len(payload) 6 or payload[0] ! 0x68: return None ctrl payload[2:6] if ctrl[0] 0x01 0: # I 帧发送序号 ctrl[0..1] 1接收序号 ctrl[2..3] 1 ns ((ctrl[1] 8) | ctrl[0]) 1 nr ((ctrl[3] 8) | ctrl[2]) 1 return (I, ns, nr) elif ctrl[0] 0x03 0x01: nr ((ctrl[3] 8) | ctrl[2]) 1 return (S, None, nr) else: return (U, ctrl[0], None) pkts rdpcap(iec104.pcap) for p in pkts: if p.haslayer(TCP) and p.haslayer(Raw): r parse_apci(bytes(p[Raw].load)) if r: print(r)逻辑说明先判断启动字符再按控制域第一个字节的最低位区分帧类型。I 帧的序号要右移一位因为最低位是标志位不是序号的一部分。参数上ctrl[1] 8 | ctrl[0]是小端拼接104 规定低字节在前。跑完如果发现 N(S) 跳号比如从 5 直接跳到 8中间两帧丢了那就要看 TCP 层有没有重传或者对端是不是真的没发。这个脚本不依赖 Wireshark 的解析器适合在只有命令行环境的前置机上快速定位。3. ASDU 类型标识与传送原因遥测遥信遥控怎么区分3.1 类型标识决定这条报文装的是什么数据APCI 之后就是 ASDUASDU 第一个字节是类型标识 TypeID它直接告诉你这条报文是干什么的。现场最常打交道的几个类型标识必须背下来单点遥信是 1M_SP_NA_1带时标单点遥信是 30M_SP_TB_1双点遥信是 3M_DP_NA_1带时标双点遥信是 31M_DP_TB_1归一化遥测是 9M_ME_NA_1标度化遥测是 11M_ME_NB_1短浮点遥测是 13M_ME_NC_1带时标短浮点遥测是 36M_ME_TF_1遥控是 45C_SC_NA_1单点遥控和 46C_DC_NA_1双点遥控总召唤是 100C_IC_NA_1电度是 15M_IT_NA_1。看到 100就知道这是总召唤后面会跟一批 1、3、9、13 的数据上送。类型标识后面紧跟一个字节最高位是 SQ 位低七位才是可变结构限定词 VSQ。SQ0 表示 ASDU 里每个信息对象都带自己的信息对象地址SQ1 表示连续多个信息对象共用第一个地址后续地址依次加一。这个位非常关键解析时如果忽略 SQ地址就会全错。比如一条 SQ1 的遥测报文信息对象地址只出现一次后面跟多个值你按 SQ0 去解第二个值就会把第一个值后面的字节当成地址整条报文全乱。3.2 传送原因为什么同一条数据会重复上送传送原因 COT 占两个字节低字节是原因高字节是源发站地址。常见原因值3 是突发spontaneous5 是请求或被请求6 是激活7 是激活确认8 是停止激活9 是停止激活确认10 是激活终止20 是响应总召唤1 是周期上送。现场经常有人问为什么这个遥信一会儿来一条一会儿又来一条看 COT 就明白了3 是变位主动上送20 是总召唤响应两者都会让同一个点出现在报文里但触发机制完全不同。总召唤的交互流程是固定的主站发 TypeID100、COT6 的激活报文厂站回 COT7 激活确认然后厂站用 COT20 把全部数据分批上送送完再发一个 TypeID100、COT10 的激活终止。如果主站发了总召唤却收不到激活确认先查 ASDU 里的公共地址对不对公共地址不匹配厂站会直接丢弃连拒绝都不回。公共地址通常 1 字节也有 2 字节的这个在点表对接时就要和厂站确认清楚属于典型的“配错一个字调一整天”。3.3 信息对象地址与时标点表对不上的根因信息对象地址 IOA 占三个字节低字节在前范围 0 到 16777215。国内很多厂站习惯把 IOA 按 遥信 1 开头、遥测 4 开头、遥控 6 开头来编但这只是习惯不是标准实际以点表为准。时标是 7 个字节毫秒 2 字节、分钟 1 字节、小时 1 字节、日 1 字节、月 1 字节、年 1 字节。带时标的类型标识30、31、36才有这 7 个字节不带时标的1、3、9、13没有。解析时如果类型标识判断错把不带时标的当带时标的解后面所有字节都会错位这是新手最常见的翻车点。下面这段代码演示如何从 ASDU 里取出类型标识、传送原因、公共地址和第一个信息对象地址帮助快速核对点表def parse_asdu(asdu): type_id asdu[0] sq (asdu[1] 0x80) 7 num asdu[1] 0x7F cot asdu[2] | (asdu[3] 8) # 低字节原因高字节源发站 ca asdu[4] # 公共地址1 字节场景 ioa asdu[5] | (asdu[6] 8) | (asdu[7] 16) return { type_id: type_id, sq: sq, count: num, cot: cot 0xFF, ca: ca, first_ioa: ioa }参数说明asdu[1] 0x80取 SQ 位 0x7F取对象个数COT 两个字节里真正的原因在低字节高字节是源发站地址调试时如果源发站地址不为 0说明报文经过了多级转发。公共地址这里按 1 字节处理如果现场是 2 字节公共地址ca要取两个字节后面 IOA 的起始位置也要顺延一位。这个函数只取第一个 IOASQ1 时后续地址依次加一即可。4. 避坑与排查104 调试现场最容易翻车的五件事4.1 链路起来了但总召唤没数据现象TCP 连接建立成功STARTDT 也确认了主站发总召唤但收不到任何数据上送。原因通常有三个公共地址不匹配、点表里 IOA 和厂站实际不一致、厂站侧总召唤功能没使能。解决顺序是先确认公共地址再抓包看厂站有没有回激活确认。如果连激活确认都没有基本是公共地址或端口问题如果有激活确认但没有后续数据查厂站点表是否为空或总召唤被禁用。4.2 遥测值跳变或明显偏大现象主站显示的遥测值偶尔跳一下或者整体偏大几十倍。原因多半是类型标识和系数没对上。归一化值 M_ME_NA_1 是 -1 到 1 之间的小数需要乘量程系数标度化值 M_ME_NB_1 是整数系数在点表里配短浮点 M_ME_NC_1 本身就是浮点不需要再乘。把归一化值当标度化值解或者系数配错都会导致数值异常。解决方法是抓一条原始报文按类型标识手动算一遍和主站显示值对比。4.3 遥控下发后无返校现象主站发遥控选择厂站不回选择确认或者回了确认但执行后没有执行确认。原因可能是遥控类型标识用错单点遥控 45 还是双点遥控 46、遥控输出方式直接执行还是选择执行没对齐、遥控点号在厂站侧被闭锁。解决时先看 ASDU 里的类型标识和 COT选择执行流程应该是 COT6 选择、COT7 选择确认、COT6 执行、COT7 执行确认、COT10 激活终止。少任何一步都要查厂站侧遥控配置。4.4 序号不连续导致链路复位现象链路运行一段时间后自动断开重连抓包看到 N(S) 或 N(R) 跳号。原因是 TCP 层丢包或对端处理不过来104 的 k 参数未确认 I 帧最大数默认 12w 参数收到多少帧必须确认默认 8如果厂站侧处理慢主站发满 k 帧还没收到确认就会主动断开。解决办法是适当调大 k 和 w或者查网络质量。注意 k 和 w 必须两端匹配一端调了一端没调反而更容易断。4.5 时标对不上导致顺序错乱现象带时标的遥信上送后主站按时间排序发现顺序不对。原因是时标解析时字节顺序或时区处理错了。104 时标是本地时间不带时区7 个字节依次是毫秒低、毫秒高、分、时、日、月、年。如果厂站和主站不在同一时区或者厂站时钟本身不准时标就会乱。解决方法是先对时再核对时标字节解析顺序尤其是毫秒两个字节的低高顺序。5. 进阶技巧用脚本批量校验 104 点表与报文一致性调试到后期最耗时间的不是抓包而是核对几百上千个点。我的习惯是写一个校验脚本把点表 CSV 和抓包解析结果做比对自动找出“点表里有但报文里没出现”和“报文里有但点表里没有”的点。下面这个思路可以直接套用import csv from collections import defaultdict # 读取点表ioa, name, type point_table {} with open(points.csv, encodingutf-8) as f: for row in csv.DictReader(f): point_table[int(row[ioa])] row[name] # 假设 parse_pcap 返回 [(type_id, ioa), ...] seen defaultdict(int) for type_id, ioa in parse_pcap(iec104.pcap): seen[ioa] 1 # 点表有但报文没出现 missing [ioa for ioa in point_table if ioa not in seen] # 报文有但点表没有 extra [ioa for ioa in seen if ioa not in point_table] print(点表缺失上送:, missing[:20]) print(报文多余点号:, extra[:20])逻辑说明parse_pcap可以复用前面解析 ASDU 的函数把每条报文的类型标识和 IOA 抽出来。seen统计每个 IOA 出现次数出现 0 次的就是点表里有但没上送的。参数上点表 CSV 至少要有 ioa 和 name 两列type 列可选用来区分遥信遥测。这个脚本跑一遍比人工翻点表快得多尤其适合总召唤后做全点核对。还有一个实用技巧是看总召唤的激活终止报文。厂站发完所有数据后会发 TypeID100、COT10 的报文这条报文里的 VSQ 如果是 0说明没有对象只是终止信号。如果一直等不到这条终止报文说明厂站侧总召唤流程没走完可能是某个数据上送卡住了。这时候可以看最后一条数据报文的 N(S)和厂站侧日志对比定位卡在哪一批。我自己的习惯是每次新站调试先抓一段完整的总召唤过程存成 pcap 归档后面点表变更或数据异常时拿出来对比。104 报文看着复杂但结构极其规整把 APCI 四个控制域字节和 ASDU 前八个字节吃透剩下的就是耐心核对。希望帮到你。本文还有配套的精品资源点击获取
返回列表