简介面向电力自动化从业者与通信调试人员的IEC101通讯规约学习资料聚焦2002版规约的报文流程与逐字节解析帮助读者理解主站与RTU之间严格的一问一答机制、ACD位判断一级/二级数据召唤等关键差异适合需要现场调试或规约对接的初中级技术人员。压缩包内为1个doc文档约121KB集中整理了规约报文结构、字节个数配置、基地址范围与典型交互流程便于对照查阅。已有2782人学习说明其在电力系统规约学习中有一定参考价值。文档以握手请求链路状态、复位链路、总召唤三步流程为主线给出请求链路状态、链路完好、复位确认、总召唤及遥信遥测响应帧的十六进制示例并说明链路地址、公共地址、传送原因、信息体地址等字段的字节配置方式以及97版与2002版在遥信、遥测、遥控、设点、电度基地址范围上的区别可帮助读者快速建立报文解析思路并用于实际排错。1. 抓包看到 68 开头就懵先把 2002 版 IEC101 的问答骨架立住如果你在变电站后台或者调试笔记本上抓过 101 的串口报文大概率见过这样的场面满屏10 49 01 4A 16、68 09 09 68 53 01 64 01 06 01 00 00 14 xx 16来回刷看着像心跳又像乱码。这就是 2002 版 IEC101 通讯规约的日常——它跑在串口上主站和 RTU 之间严格一问一答每个长帧之前还要先过一次链路层短帧确认。这份资料把握手、复位、总召唤、对时、电度、遥控、二级数据轮循七个步骤的原始报文逐字节拆开配了 97 版与 2002 版的基地址对照和iec101规约配置表.ini的配置项说明。适合正在对接 RTU、排查总召唤收不全、或者被 ACD 位和一级/二级数据绕晕的电力自动化从业者拿它当报文对照手册用。2. 报文结构拆解固定帧与可变帧到底差在哪几个字节2.1 固定帧长10 开头的那 5 个字节各管什么固定帧是链路层的心跳和确认载体长度固定 5 字节结构是启动字符 10H 控制域 链路地址 校验和 结束字符 16H。控制域是唯一需要动脑子的字节它按位拆开看bit7 是 PRM启动报文位主站发起时为 1、RTU 回应时为 0bit6 是 FCB帧计数位主站每发一次新请求翻转一次用来防重发bit5 是 FCV帧计数有效位只有 PRM1 且需要防重时才置 1bit4 是 ACD要求访问位RTU 有一级数据待上送时置 1bit3 是 DFC数据流控制位RTU 缓冲区满时置 1 让主站缓一缓低 4 位是功能码。拿正文里的握手报文举例主站发10 49 01 4A 16控制域49H二进制是0100 1001PRM0、FCB0、FCV0、ACD0、DFC0、功能码 9请求链路状态RTU 回10 0B 01 0C 16控制域0BH是0000 1011功能码 11链路完好。复位链路时主站发10 40 01 41 16控制域40H功能码 0复位远方链路RTU 回10 00 01 01 16或10 20 01 21 16——后者控制域20H的 ACD 位为 1意思是“我复位完了但手里有一级数据等你来取”主站必须紧接着发10 7A 01 7B 16召唤一级数据RTU 才会把初始化结束帧46H吐出来。这个细节是 2002 版和 97 版最直观的差异之一很多对接翻车就翻在没看 ACD 位。校验和算法是链路地址到校验前一字节的算术累加取低 8 位比如10 49 01累加得5A不对是1049015A但报文里是4A——注意固定帧校验不含启动字符只算控制域链路地址49014A对上了。这个“不含启动字符”的规则在可变帧里也一样后面会再提。2.2 可变帧长68 开头长度字节为什么要写两遍可变帧承载应用层数据结构是68H L L 68H 控制域 链路地址 类型标识 可变结构限定词 传送原因 公共地址 信息体地址 信息元素 校验和 16H。长度 L 从控制域算到校验和前一字节不含校验和与结束符而且连写两遍——这是 101 规约的硬性要求第二遍 L 用来做长度自校验抓包时如果两个 L 不一致直接判定帧损坏不用往下解。以总召唤命令68 09 09 68 53 01 64 01 06 01 00 00 14 xx 16为例逐段对68启动09 09长度 9 字节控制域 1 链路地址 1 类型标识 1 可变结构限定词 1 传送原因 1 公共地址 1 信息体地址 2 QOI 1 968第二启动53控制域PRM1、FCB1、FCV1、功能码 3用户数据01链路地址64类型标识总召唤01可变结构限定词SQ0信息体个数 106传送原因激活01公共地址00 00信息体地址14QOI总召唤限定词xx校验16结束。可变结构限定词这个字节值得单独说bit7 是 SQ顺序位SQ1 表示信息体地址连续递增、后续信息体省略地址SQ0 表示每个信息体都带独立地址。bit0~6 是信息体个数。正文里遥信帧14 08 14 01 01 00 80 04 ...中08就是 8 个信息体、SQ0所以后面每个遥信组都重复写了信息体地址01 00、11 00、21 00……一直到71 00。如果 SQ1地址只写第一个后面靠递增推报文能短一截但解析代码得跟着改。2.3 配置项决定解析器怎么写地址字节数是第一道分水岭正文给了iec101规约配置表.ini能配的四类字节数这直接决定解析器读几个字节配置项可选值影响链路地址字节数1 或 2固定帧和可变帧都要按此偏移读链路地址公共地址字节数1 或 2可变帧传送原因之后读几个字节传送原因字节数1 或 21 字节时 bit6 为 P/N、bit0~5 为原因值2 字节时低字节同前、高字节为源发地址信息体地址字节数2 或 3遥信遥测电度的信息体地址宽度DF8900 系统走配置文件DF8002/DF1800 系统信息固定为链路地址 1、传送原因 1、公共地址 1、信息体地址 2。这意味着同一份解析代码换现场就得改偏移量硬编码字节数是最常见的返工点。我一般会把四个字节数做成配置对象解析函数里所有offset n都从配置取而不是写死。3. 总召唤全流程复现从握手到电度每一步的报文和判据3.1 握手、复位、总召唤前三步的报文序列与 ACD 判据按正文的 97 版基地址、链路地址 1 字节的配置完整走一遍。第一步握手主站10 49 01 4A 16RTU10 0B 01 0C 16链路完好。第二步复位主站10 40 01 41 16RTU 回10 00 01 01 16或10 20 01 21 16若回20主站补发10 7A 01 7B 16召唤一级数据RTU 上送初始化结束帧类型标识46。第三步总召唤主站发68 09 09 68 53 01 64 01 06 01 00 00 14 xx 16RTU 先回链路层确认10 20 01 21 16注意这里 ACD 又置 1主站发10 7A 01 7B 16RTU 回总召唤确认帧68 09 09 68 20 01 64 01 07 01 00 00 14 xx 16传送原因07激活确认主站再发10 5A 01 5B 16召唤一级数据RTU 开始上送遥信帧。这里有个容易忽略的节奏2002 版要求主站每发一个长帧RTU 都先回短帧链路层确认再回应用层数据而且主站要主动用10 7A召唤一级或10 5A召唤二级去“拉”数据。97 版没有这么严格有些 RTU 直接回数据帧。对接时如果主站侧状态机没等链路确认就发下一帧RTU 会丢帧或者回否定确认现象就是总召唤只收到前几组遥信就断了。3.2 遥信与遥测帧解析可变结构限定词和基地址怎么算遥信帧正文给了类型标识14成组单点遥信、可变结构限定词08、传送原因14响应总召唤、公共地址01、起始信息体地址01 00。解析时先读可变结构限定词得 8 个信息体SQ0然后循环 8 次每次读 2 字节信息体地址 2 字节遥信值 2 字节状态变位检出 1 字节品质描述共 7 字节一组。第一组地址01 00对应 1 号遥信值80 04按位展开是 16 个遥信bit0 对应 1 号bit15 对应 16 号第二组地址11 00即 17 号依此类推。遥信序号 信息体地址 - 基地址 197 版遥信基地址是01H2002 版是4000H这个差值直接决定你算出来的点号对不对。遥测帧类型标识15不带品质的遥测、可变结构限定词E0即 96 个信息体、传送原因14、起始地址01 07。97 版遥测基地址701H所以遥测号 0x0701 - 0x0701 0从 0 号开始每个遥测值 2 字节96 个就是 192 字节加上帧头帧尾正好对上长度C8H200 字节。如果现场配的是 2002 版基地址4001H同样的01 07地址就要按0x0701 - 0x4001算结果是负数——说明基地址配错了这是现场最常见的“遥测全错位”根因。3.3 对时、电度、遥控CP56Time2a 和 QCC 的字节序对时帧类型标识67信息体是 7 字节 CP56Time2a毫秒低字节、毫秒高字节、分bit7 为 SU 夏令时标志、bit0~5 为分钟、时bit0~4、日bit0~4 星期bit5~7、月bit0~3、年bit0~6偏移 2000。正文例子22 58 14 0F 62 09 05解析为毫秒0x582222562即 22.562 秒、分0x1420、时0x0F15、日0x62低 5 位0x022号、月09、年05即 2005 年。注意毫秒是低字节在前别按大端读。电度召唤分冻结和读两步类型标识都是65靠 QCC 区分冻结 QCC45读 QCC05。电度帧类型标识0F每个电度值 4 字节 1 字节描述信息共 5 字节97 版电度基地址C01H正文里01 0C即0x0C01电度号 0x0C01 - 0x0C01 0。遥控类型标识2E预置用82控合或81控分执行用02或01撤销传送原因08确认09。遥控号 信息体地址 - 遥控基地址97 版遥控基地址B01H正文03 0B即0x0B03遥控号 0x0B03 - 0x0B01 2。4. 避坑排查总召唤收不全、遥测错位、ACD 不置位的现场记录4.1 总召唤只收到遥信没有遥测现象主站发完总召唤遥信帧收齐了遥测帧一帧没来链路还在正常轮循。原因RTU 上送遥信后 ACD 位仍为 1主站没继续发10 7A召唤一级数据或者主站状态机把“收到遥信帧”当成总召唤结束提前发了总召唤结束确认。解决解析遥信帧后检查控制域 ACD 位只要为 1 就继续10 7A直到收到类型标识64、传送原因0A激活结束的总召唤结束帧才算完同时确认 RTU 参数表里遥测个数不为 0。4.2 遥测点号整体偏移一个固定值现象所有遥测值都对得上设备但点号整体差0x3900或类似固定偏移。原因基地址版本配错97 版遥测基地址701H、2002 版4001H解析器用了错的基地址做减法。解决先抓一帧遥测看起始信息体地址落在哪个区间701H~900H按 97 版算4001H~5000H按 2002 版算DF8900 改iec101规约配置表.iniDF8002/DF1800 只能按固定基地址改解析代码。4.3 链路层确认收到 E5 而不是 10 开头短帧现象主站发长帧后串口收到单字节E5解析器按固定帧找10启动字符找不到报帧错。原因部分 RTU 厂家用单字节E5做链路层确认这是 101 规约允许的“单字符确认”不是帧。解决串口收包层先判首字节E5直接当确认事件抛给状态机不进帧解析10和68才进对应解析器。这个分支不加现场换一家 RTU 就得改一次代码。4.4 对时后 RTU 时间差 8 小时现象对时命令发下去RTU 回激活确认但读回来时间差整 8 小时。原因CP56Time2a 的时、分是本地时间主站侧用了 UTC 直接填或者毫秒高低字节读反导致时间跳变。解决对时前把主站时间转成本地时区再拆字节毫秒按低字节在前写入对时确认帧回来后逐字节比对重点看时字节0x0F这类值有没有被当成 BCD 码误转。4.5 遥控预置成功但执行超时现象预置帧回激活确认执行帧发下去没回确认遥控失败。原因预置和执行之间主站没等链路层确认就发下一帧或者预置与执行的信息体地址、控制值不一致预置82、执行写成01。解决预置确认收到后再发执行执行帧的控制值必须与预置一致执行确认的传送原因应为07撤销用08且控制值同样保持一致超时时间按现场串口波特率留足9600bps 下一帧 20 字节左右要 20ms 以上。5. 用 Python 写一个最小解析器把抓包字节流跑成点表5.1 帧切分与校验先解决“从哪切、切得对不对”抓包拿到的是一串十六进制字节第一步按启动字符切帧。固定帧以10开头、16结尾共 5 字节可变帧以68开头读第二个字节得 L整帧长度 L 668 L L 68 数据区 L 校验 16其中数据区从控制域到信息元素共 L 字节再加 4 字节帧头帧尾和 1 字节校验。切完做校验和比对固定帧校验 控制域 链路地址可变帧校验 控制域到信息元素末字节累加取低 8 位。def split_frames(data: bytes): frames, i [], 0 while i len(data): if data[i] 0x10 and i 4 len(data): frame data[i:i5] if frame[4] 0x16: frames.append((fixed, frame)) i 5 continue elif data[i] 0x68 and i 2 len(data): L data[i1] total L 6 frame data[i:itotal] if len(frame) total and frame[-1] 0x16: frames.append((variable, frame)) i total continue i 1 # 非法字节跳过防止死循环 return frames def checksum_ok(kind, frame: bytes) - bool: if kind fixed: calc (frame[1] frame[2]) 0xFF return calc frame[3] L frame[1] calc sum(frame[4:4L]) 0xFF return calc frame[4L]split_frames里i 1那行是后悔药现场抓包常混入噪声字节或半帧不跳过会卡死。checksum_ok对可变帧只累加frame[4:4L]正好是控制域到信息元素不含两个68和长度字节和规约定义一致。校验不过的帧直接丢弃并计数别硬解硬解出来的点号全是错的。5.2 应用层字段提取按配置偏移读别写死有了合法帧按配置对象读字段。下面这段把可变帧拆成类型标识、可变结构限定词、传送原因、公共地址、信息体列表信息体按 SQ 位决定是否重复读地址def parse_variable(frame: bytes, cfg: dict): L frame[1] body frame[4:4L] # 控制域起 ctrl body[0] off 1 link_addr int.from_bytes(body[off:offcfg[link]], little); off cfg[link] type_id body[off]; off 1 vsq body[off]; off 1 count, sq vsq 0x7F, (vsq 7) 1 cause int.from_bytes(body[off:offcfg[cause]], little); off cfg[cause] common int.from_bytes(body[off:offcfg[common]], little); off cfg[common] infos, last_addr [], None for _ in range(count): if sq 0 or last_addr is None: addr int.from_bytes(body[off:offcfg[ioa]], little); off cfg[ioa] else: addr last_addr 1 last_addr addr infos.append(addr) return {ctrl: ctrl, link: link_addr, type: type_id, count: count, sq: sq, cause: cause, common: common, addrs: infos, rest: body[off:]}cfg就是前面说的四个字节数{link:1,cause:1,common:1,ioa:2}对应 DF8002/DF1800 固定配置。rest是信息元素原始字节遥信按 7 字节一组、遥测按 2 或 3 字节一组再切切法取决于类型标识。这段代码不处理信息元素的具体语义因为类型标识太多硬塞进一个函数会变成黑匣子我一般按类型标识分派到parse_yx、parse_yc、parse_kwh几个小函数每个只认自己那几种类型出问题好定位。5.3 用正文报文做回归测试三帧跑通再上现场拿正文三帧当测试用例总召唤命令、遥信帧、遥测帧。总召唤命令解析出 type0x64、cause6、count1、addr[0]遥信帧解析出 type0x14、count8、addrs[0x0001,0x0011,...,0x0071]遥测帧解析出 type0x15、count96、addrs 从 0x0701 起连续。三帧都对上说明切帧、校验、偏移、SQ 处理四条链路都通。上现场前再补两类用例E5 单字节确认应被切帧函数跳过且不报错、长度字节两遍不一致的坏帧应被校验丢弃。这套回归用例我每次改解析器都跑一遍比在现场拿万用表量串口快得多。从那以后我每次对接新 RTU都先把握手、复位、总召唤、对时四步的原始报文抓下来存成 hex 文本跑一遍解析器回归再谈点表对不对——报文层没过上层点表怎么调都是玄学。希望帮到你。本文还有配套的精品资源点击获取