简介面向电力系统自动化领域研发与调试人员的 IEC 104 报文速查资料帮助读者理清主站与子站间常用报文类型、帧格式及每个字节的详细含义尤其适合刚接触 104 规约、需要在开发或联调中快速定位报文问题的工程师。资料内置 PDF 与 HTML 两种阅读形态PDF 便于离线精读HTML 页面支持在浏览器中直接检索查看并配合页面演示辅助理解。资源包共 11 个文件主要包含 1 份 PDF 详解文档、2 个 HTML 页面、3 个 JavaScript 脚本以及字体、文本说明等辅助文件整体约 838KB轻量便携。PDF 文档可离线打开HTML 页面适合快速浏览配套的 JS 脚本支撑网页中的演示效果文本说明文件则提供简要使用指引。目前已有 8258 人浏览学习。内容从开发痛点出发逐条拆解常用报文的作用和关键字节含义让学习者避免反复翻查协议文档通过对照页面展示能更快掌握 104 报文规则提升实际项目中的报文解析、联调与排错效率。无论课堂自学还是现场调试都可作为随手查阅的速查手册。1. 一条不刷新的遥测逼我重新读懂了IEC-104报文IEC-104报文平时看着不难出问题时却能逼疯一屋子人。某天现场A同学拿着抓包文件来找我主站画面上一条遥测半小时没刷新而从站那边仪表显示值早就变了。我把包里那几帧IEC-104报文逐字节拆开发现根本不是网络问题从站一直在按突发上送主站也确实收到了只是主站按错误的类型标识把它当成了另一个点数据进了隔壁的遥控信息体。这让我意识到IEC-104报文平时不出事感觉简单一出事就绕不开APCI控制域、ASDU体这几层基本功。IEC-104是电力调度自动化里最常用的远动规约它把远动应用层搬到TCP/IP上遥信、遥测、遥控、遥调、总召唤、时钟同步全在这一套报文里。这篇笔记不打算复述协议原文而是按「报文长什么样→怎么解析→怎么联调→踩过哪些坑→怎么把抓包变成自动化用例」的顺序把现场真正用得上的东西一次讲清。适合刚接手变电站调试的运维、做配电主站开发的工程师以及总被报文搞到半夜的调试人员。2. IEC-104报文的基本盘四字节控制域与三类帧的判定2.1 帧格式骨架起始符、长度字段与APCI一条IEC-104报文在TCP流里以APDU为单位存在每个APDU的结构是固定的起始符0x68占一个字节长度字段占一个字节后面跟着4字节控制域和可变长的ASDU。这里有个新手最容易数错的细节长度字段表示的是「控制域4字节加ASDU」的总长度并不包含0x68和长度字段自身。比如68 0E意味着后面一共14字节拆出来是4字节APCI加10字节ASDU如果你按14字节去读ASDU必然越界。控制域也就是APCI固定4字节。第一个字节的最低两位用来区分帧类型00表示I帧01表示S帧11表示U帧。I帧负责携带ASDU业务数据同时靠接收序号确认对方发来的I帧S帧是纯确认帧只回接收序号、不带ASDUU帧则用来做启动传输、停止传输和链路测试也没有ASDU。这个分类是看懂后续所有报文交互的前提。I帧的发送序号和接收序号各占两个字节但真正的序号只有15位因为最低位被帧类型标志占用了。所以解析时要把取到的16位数值右移一位才是真实序号。举个例子控制域02 00 06 00第一字节低两位是00判定为I帧发送序号是0x0002右移一位等于1接收序号是0x0006右移一位等于3。序号是模32768循环的超过32767后回绕到0很多主站在这里没做回绕处理导致链路跑一两个月后莫名卡死。2.2 三类帧怎么配合从握手、确认到心跳U帧的命令集中在第一个字节上靠特征位区分0x07是STARTDT act启动传输请求0x0B是STARTDT con启动传输确认0x13是STOPDT act0x23是STOPDT con0x43是TESTFR act0x83是TESTFR con。链路建立后主站必须先发STARTDT act从站回STARTDT con之后双方才允许传输I帧。这个先后顺序卡得很死有的从站实现里没收到STARTDT就直接丢弃所有I帧。很多系统集成商第一次连104链路上来就发总召唤结果石沉大海原因就在这里。S帧不背业务数据它干的活就是确认。当接收方短时间内收到多帧I帧而来不及处理时先回一个S帧把接收序号确认掉避免对方的发送窗口被占满。识别S帧很简单控制域第一字节的低两位是01典型样子是68 04 01 00 06 00长度4说明只有APCI没有ASDU接收序号同样是右移一位。调试时如果发现某侧从不回S帧那对端的发送窗口迟早被撑爆表现就是链路还连着、业务数据不更新。I帧承载真正的遥测遥信遥控。I帧里的接收序号必须严格递增接收方一旦发现中间少了序号就会按丢帧处理并丢弃后续I帧直到序号重新对上。实际现场常见的情况是主站和从站之间有防火墙做了TCP劫持或缓冲导致一批I帧乱序到达从站全拒收主站屏幕上那一批数据全部不动。遇到这种「一串数据同时卡住」的现象优先怀疑I帧序号不连续而不是单个点表配错。2.3 三个必查字段类型标识、传送原因与品质位ASDU的固定头部依次是类型标识TI占1字节、可变结构限定词VSQ占1字节、传送原因COT占2字节、公共地址占2字节之后才是信息体地址和信息体数据。类型标识决定了这条报文里信息体的长度和语义TI1是单点遥信TI13是短浮点遥测TI45是单点遥控TI100是总召唤。解析报文的第一个动作永远是看TITI认错了后面所有字段都会跟着错当初我排查那条不刷新的遥测最后就是发现主站把TI13的帧按TI9去解析了。可变结构限定词VSQ的bit7是SQ位bit0到bit6是信息体个数。SQ位决定信息体地址是否连续SQ1表示连续后一个信息体地址自动加一报文里只写第一个IOASQ0表示不连续每个信息体都带完整的三字节IOA。解析时一定要先看SQ位再决定读取方式否则信息体个数对不上整帧长度就算不齐。公共地址占2字节是站地址多数从站配成1COT的常见取值是1周期、3突发、6激活、7激活确认、10激活终止、20响应站召唤。COT高字节的bit7是P/N位置1表示负确认命令被拒绝时多半能在这里看到。遥信和遥测信息体末尾会跟一个品质描述符QDSbit0是IV无效bit1是NT非当前值bit2是SB被取代bit3是BL被闭锁bit4是OV溢出。品质位全0才是正常值。主站画面数据不刷新很多时候不是链路断了而是从站把IV或NT位置1主站按品质策略直接拒绝更新画面。排查时把QDS从报文里抠出来看一眼比在界面上反复对点表快得多。下面两个速查表建议贴在调试台上比翻几百页协议原文方便。类型标识速查类型标识名称信息体典型长度1单点遥信 M_SP_NA_11字节状态品质3双点遥信 M_DP_NA_11字节13短浮点遥测 M_ME_NC_14字节浮点1字节品质45单点遥控 C_SC_NA_11字节S/EQU46双点遥控 C_DC_NA_11字节100总召唤 C_IC_NA_1无信息体数据103时钟同步 C_CS_NA_17字节CP56Time2a传送原因速查传送原因含义3突发变位上送6激活命令或召唤7激活确认10激活终止20响应站召唤3. 把一条报文从hex拆成人话手写解析与抓包验证3.1 一条上行遥测的逐字节拆解拿到一段抓包比如这条上行遥测68 12 02 00 06 00 0D 01 03 00 01 00 01 00 00 00 CD CC 4C 3E 00先看头两个字节68 12长度0x12等于18说明后面有18字节。接着是控制域02 00 06 00低两位是00I帧发送序号1接收序号3。从第7个字节开始读ASDU0D是类型13即短浮点遥测01是VSQ信息体个数1个、SQ003 00是传送原因3突发上送01 00是公共地址101 00 00是信息体地址1注意IOA是三字节小端01 00 00表示地址1不是0x010000。后面的CD CC 4C 3E按小端拼成0x3E4CCCCD用IEEE754单精度解出来约等于0.2最后一个字节00是品质位表示数据有效。把整帧从68数到末尾4字节控制域加14字节ASDU正好18字节长度字段吻合。拆过这一帧你对IEC-104就不再有「黑匣子」的感觉后面所有联调问题都可以回到这条拆解线上找原因。拆的时候有两点要注意。第一短浮点遥测TI13的数值就是标准IEEE754单精度不需要缩放按字节直接解就行而TI9归一化遥测是两个字节的整数需要除以一个系数才能得到工程值很多主站把TI9和TI13混用解析出来数值差一大截。第二品质位字节一定要解析出来看而不是跳过。上行数据里品质位是0、1、2、4、8这些值都有特殊含义以前我遇到过一段数据的QDS一直为0x01IV置位主站全丢弃排除好久才发现是从站采集板没初始化完成。3.2 用Python写一个最小解析器批量把hex变成结构化数据调试时手上往往是一大段连续hex一帧帧手数太容易出错而且TCP粘包可能让一包数据里挤了好几条APDU。我一般用脚本先按长度字段循环切帧再逐帧解析字段。下面这个简化版解析器只处理I帧S帧和U帧打印类型后跳过核心逻辑是把每个字段读出来并格式化输出。import struct def parse_104(raw: bytes): 解析单条IEC-104 APDU返回字段字典或None if len(raw) 6 or raw[0] ! 0x68: return None length raw[1] # 长度含控制域4字节ASDU不含68和长度自身 frame raw[2:2 length] # 按长度字段切出完整帧体 ctrl frame[:4] # 控制域4字节 c0 ctrl[0] # 低两位00I帧 01S帧 11U帧 if c0 0x03 0x00: send ((ctrl[1] 8) | ctrl[0]) 1 # 16位右移1位才是真实序号 recv ((ctrl[3] 8) | ctrl[2]) 1 asdu frame[4:] if len(asdu) 6: return {type: I, send: send, recv: recv, note: ASDU不完整} type_id asdu[0] # 类型标识TI vsq asdu[1] # 可变结构限定词 num vsq 0x7F # 信息体个数 sq (vsq 7) 0x01 # 0不连续地址 1连续地址 cot struct.unpack(H, asdu[2:4])[0] # 传送原因2字节小端 ca struct.unpack(H, asdu[4:6])[0] # 公共地址2字节小端 ioa asdu[6] | (asdu[7] 8) | (asdu[8] 16) # 3字节小端 body asdu[9:] # 信息体原始数据 return {type: I, send: send, recv: recv, type_id: type_id, num: num, sq: sq, cot: cot, ca: ca, ioa: ioa, body: body.hex()} elif c0 0x01 0x01: recv ((ctrl[3] 8) | ctrl[2]) 1 return {type: S, recv: recv} else: return {type: U, cmd: f0x{c0:02X}} raw_hex 6812020006000D010300010001000000CDCC4C3E00 raw bytes.fromhex(raw_hex) print(parse_104(raw))这段代码有四个关键点。第一先用长度字段切帧而不是按TCP包边界读这样能正确处理一包多个APDU的粘包情况。第二I帧的序号必须右移一位这个细节错一次会连锁错一大堆。第三IOA是三字节小端COT和公共地址是两字节小端不能复用同一个解析函数。第四返回的body字段是信息体原始hex下一步按TI类型解析时直接从这里取数据就行比如TI13时读前4字节为浮点、第5字节为品质TI1时只读1字节。运行后输出大概是{type: I, send: 1, recv: 3, type_id: 13, num: 1, sq: 0, cot: 3, ca: 1, ioa: 1, body: cdcc4c3e00}。拿这个结果和Wireshark协议树对比能快速验证你理解的字段边界对不对。实际用的时候把抓包文件里每帧的data.data字段导出来循环喂给这个函数几秒钟就能把几千帧报文变成一张结构化表格。3.3 用Wireshark快速定位过滤端口与核对协议树现场抓包最快的方式还是Wireshark过滤器先写 tcp.port 2404把IEC-104流量全拉出来这是104的默认端口。新版Wireshark自带104解析器抓到后在协议树里能看到APCI和ASDU两层Type ID、COT、公共地址、IOA都有可读显示。看协议树时重点核对四列Type ID、COT、公共地址、IOA。如果你发现Wireshark显示正常、主站日志显示类型不对那就是两边的解析口径差异通常出在厂商自定义类型上。Wireshark里还有个有用的习惯抓包保存成pcap后把包含总召唤、变位、遥控执行的帧单独标记。后面从站升级或换装置重放这份抓包对比行为比盯着一堆在线数据要快得多。对于I帧右键选择Follow TCP Stream按时间排序一帧帧核对序号是否连续。序号跳变意味着期间有丢包或从站重启这是主站侧日志看不到的底层信息。注意抓包时上游交换机会丢弃一些错误帧抓到的包不完整时先怀疑抓包口镜像配置不要上来就怀疑从站。4. 用最小链路跑通全流程启动、总召唤与遥控的报文对照4.1 最小链路怎么搭从站模拟器、信息体表与端口参数联调前手头没有真实装置常见做法是找一开源IEC-104从站模拟器配一张最小信息体表。信息体表建议至少三列信息体地址IOA、类型标识TI、初始值。比如IOA1到10配TI1单点遥信IOA20到30配TI13短浮点遥测公共地址统一填1。这个配置的目的不是模拟真实容量而是把「从站发什么、主站按什么收」的对应关系先立起来。后面换真实装置时把IP和端口一改就能沿用这套验证逻辑。模拟器监听2404端口主站侧用一个支持104的客户端或另一个模拟器连过去。启动前先核对五个参数公共地址是不是1、端口是不是2404、T0连接超时是不是30秒、k值是不是12、w值是不是8。多数开源模拟器默认值就是标准值但有些打包版本改过不核对直接跑链路反复断都不知道是哪边的参数问题。链路通断看U帧TCP建立后主站发STARTDT act从站回STARTDT con这时才允许I帧。没有STARTDT就没有I帧这个顺序在模拟器日志里看得一清二楚。4.2 启动时序从TCP握手到总召唤结束的报文对照一条标准链路启动顺序固定为TCP三次握手、STARTDT act/con、主站发总召唤TI100且COT6、从站回TI100且COT7激活确认、从站上送全量数据COT20、从站再发TI100且COT10激活终止。整个过程在抓包里一目了然报文对照表如下。方向关键字段含义主站→从站68 04 07 00 00 00STARTDT启动传输从站→主站68 04 0B 00 00 00STARTDT确认主站→从站TI100 COT6总召唤激活从站→主站TI100 COT7总召唤激活确认从站→主站TI1/13 COT20全量数据响应从站→主站TI100 COT10总召唤激活终止总召唤报文的信息体地址固定为0、VSQ0表示对整个站的召唤。COT7的确认帧和COT10的终止帧之间才是数据段如果误把COT7当成数据开始读到的会是类型100的召唤帧体自然提取不到任何业务数据。常见误用是拿COT6的召唤帧去对数据响应然后抱怨从站不回数据其实从站早就把数据夹在COT20的帧里发过来了只是你找错了帧。除了总召唤主站启动后还会做时钟同步。时钟同步用TI103COT6信息体地址0信息体是7字节的CP56Time2a时间格式精确到毫秒。从站收到后回COT7确认校时完成。时标不准会直接影响事件顺序记录SOE如果现场发现两个站的事件顺序对不上先查主站有没有做时钟同步而不是查通道。时钟同步在IEC-104里是常规操作很多从站规定启动后必须校时否则带时标的报文不按正确时间上送。4.3 遥控操作选择与执行的报文差异及常见歧义遥控分两步先选择再执行。以单点遥控TI45为例选择命令的信息体是一个字节bit0是S/E位置1表示选择执行命令同样一字节bit0清零表示执行。完整报文举例如下。选择帧68 0E 04 00 06 00 2D 01 06 00 01 00 05 00 00 01 执行帧68 0E 06 00 04 00 2D 01 06 00 01 00 05 00 00 00拆开看选择帧控制域04 00 06 00是I帧发送序号2、接收序号32D是类型45单点遥控VSQ01一个信息体COT06激活公共地址01IOA05 00 00即5号对象最后一个字节01bit0为1表示选择。执行帧最后一个字节00bit0为0表示执行其余字段完全一致。从站正常反馈顺序是收到选择先回COT7确认收到执行再回COT7执行完成回COT10。如果从站不支持选择/执行模式会直接回COT10拒绝这时要看第二字节P/N位P/N1代表负确认。有些装置把选择/执行模式合并只认执行帧不认选择帧主站照样按两步走就会看到选择被拒。这类兼容问题只能靠抓包确认先发选择帧看返回如果是COT44未知类型标识或COT10负确认说明这个从站实现不走标准两步遥控。解决办法是在主站侧关闭选择功能直接下发执行帧。另外QU字段是限定词QU0无条件执行QU1短脉冲QU2双脉冲。不少从站只支持短脉冲主站却填0导致装置拒动。拿不准时先在模拟器上试三种QU值看哪个能通过再改主站配置。5. IEC-104报文调试避坑5个真实翻车现场5.1 现象链路反复断开重连现象是主站日志里TCP连接不断建立又断开从站侧模拟器显示连接存活时间经常不到1分钟。原因十有八九是T0/T1/T2/T3四组超时参数两端不一致。常见组合是主站T1设15秒从站T1设30秒主站发完I帧后15秒内没收到确认就断开而从站一直傻等主站继续发。T2是监视超时T3是测试帧周期任何一个参数不匹配都会导致误判死链。解决方法是把T030秒、T115秒、T210秒、T320秒作为基准值在两端统一配置并确认从站周期发TESTFR act、主站能正确回TESTFR con。抓包里如果只有TESTFR act没有con问题在主站的U帧处理逻辑上而不是从站。这四组参数虽然只是数字却是链路稳定性的地基别偷懒按默认值省事必须逐项核对。5.2 现象遥测偶发丢点刷新停在一个值上现象是数据列表里个别点长时间不更新重启从站后恢复过一会儿又卡住。原因通常是k参数和w参数配置过小。k是未确认I帧的最大个数标准默认12w是收到多少帧未确认就发S帧确认默认8。有些厂家为了省带宽把w改成1结果主站或从站稍有延迟发送窗口就满了后续I帧全被缓存或丢弃表现就是某几个点不刷新。解决方法是恢复标准值k12、w8同时确认接收侧及时回S帧不要攒批确认。排查时抓一段正常上送的数据流数一下连续I帧数量和S帧间隔是否符合w参数。如果连续12帧都没有S帧回来那窗口必然溢出这个锅不在从站在主站的确认策略。5.3 现象遥控选择成功、执行后从站不动现象是主站下发遥控抓包看选择帧和执行帧都收到了COT7确认但从站现场设备无动作几秒后收到COT10。原因通常是S/E位写反或QU限定词不匹配。最常见的是厂家自定义网关把选择/执行做了合并选择帧信息体字节必须为0x81而不是0x01导致从站识别成普通置位还有一种情况是执行帧晚发了3秒超过从站2秒的选择有效期从站返回COT10终止。解决方法是先查从站说明书确认信息体bit0的S/E判定和QU取值再调整主站选择执行间隔。选择后执行要快建议控制在1秒以内如果业务上做不到就在从站侧把选择有效期调大。这种问题在模拟器上验证比在现场试安全得多先在模拟器发一组标准帧看从站返回什么再比照现场主站的报文差异。5.4 现象总召唤后数据全是历史值画面不刷新现象是总召唤完成后主站画面所有点显示上次值后续变化也不上送。原因多数是品质位QDS被置IV或NT。很多从站初始化后数据源还没采到有效值就把IV置1打包上送主站按品质策略直接丢弃保持旧值。总召唤的COT20响应数据里如果没把IV清掉整个站的画面都会是旧值不是单个点的问题。解决方法是抓包里检索所有QDS非0的报文定位到具体信息体再查从站侧该点的采集链路是否正常。如果单点正常而全站都旧值问题在主站的品质处理策略可以暂时关闭品质过滤来验证。还有一种情况是TI不匹配从站发TI9归一化遥测主站按TI13短浮点解析读出来的数值驴唇不对马嘴。这种要先对点表确认TI定义再改主站侧类型标识。5.5 现象A开关变位B开关动作现象是操作A开关画面B开关变位或者遥测数据串到相邻信息点。原因多是IOA偏移从站按信息体地址十六进制排列主站点表从1开始按十进制累加两边差了整整一段也有公共地址从0开始配主站按1配导致全站错位。这类问题最坑的地方是链路完全正常报文也合法错的是地址映射本身。解决方法是总召唤后把抓包里的IOA列表导出来和主站数据库逐条比对。我一般写个小脚本把pcap里TI1和TI13的IOA全提出来排序后和点表文件做diff一次就能把偏移量找出来。比对时注意IOA的三字节小端格式别把05 00 00当成0x050000那是地址5。这个习惯能省掉大半数据串位的排查时间也让点表核对从人工变流程。6. 进阶把抓包文件变成回归测试用例6.1 从pcap提取基线报文建立回归基准从站固件升级后最容易出的问题不是功能消失而是某个报文字段悄悄变了比如类型标识换了、传送原因从3变成20、信息体长度多了个字节。把联调通过时抓的pcap存成基线用tshark按帧导出hex之后每次升级或改配置重新抓一份同样场景的包用脚本把两份hex逐帧比对。字段不一致的帧直接标红不用重新全量联调半小时就能覆盖主要链路。我实际是把基线pcap放在固定目录下文件名带日期和从站版本号。升级后跑一次同样的联调用例导出的新抓包和基线对比输出差异帧清单。这个流程对验证从站固件修改尤其有用因为固件改动经常影响报文的填充字节和时标字段而这些在主站界面上根本看不到。别小看这个动作有一次模拟项目X就是靠它抓到新固件把总召唤的COT7错误地发成了COT20导致主站把确认帧当数据帧处理。6.2 用回放验证从站行为tcpreplay与自制校验器比对比hex更进一步的是用tcpreplay把基线pcap按原时序回放到测试从站观察从站响应是否和基线一致。回放前把pcap里的IP和端口改成测试环境的值再确认TCP序列号和时间戳不影响从站判断。回放后对照两边日志如果新固件对同一总召唤返回的TI、COT、IOA和基线不同基本能定位到改动点。这个流程我固定成了发布前必做项宁可多花20分钟回放也不在投运当天被叫起来查报文。回放之外还可以做一个简单的断言脚本把「总召唤后必须收到COT7再收到COT10」这类链路规则写成检查项跑完用例自动输出通过/失败。断言脚本不需要复杂框架几十行Python就能完成先按长度字段切帧再把每帧的类型标识、COT、公共地址、IOA顺序存进列表最后用一组预置规则去匹配。第一次写这个脚本大概花一个下午但之后每次联调省下的时间远不止一个下午。IEC-104报文调试到最后拼的其实是耐心和记录习惯。每个坑第一次踩都是玄学把现象、抓包、处置写进一张表第二次就变成了流程。我现在每处理一个现场问题都会把关键帧存一份pcap、写一段现象描述、留一条处置结论。这个习惯救过我很多次希望你也能用上。本文还有配套的精品资源点击获取