☰
IEC101规约报文解析实战:从帧结构到总召唤与Python实现
2026/11/3 17:49:19 网站建设 项目流程

简介:面向电力自动化从业者与通信调试人员的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 回应时为 0;bit6 是 FCB(帧计数位),主站每发一次新请求翻转一次,用来防重发;bit5 是 FCV(帧计数有效位),只有 PRM=1 且需要防重时才置 1;bit4 是 ACD(要求访问位),RTU 有一级数据待上送时置 1;bit3 是 DFC(数据流控制位),RTU 缓冲区满时置 1 让主站缓一缓;低 4 位是功能码。

拿正文里的握手报文举例:主站发10 49 01 4A 16,控制域49H二进制是0100 1001,PRM=0、FCB=0、FCV=0、ACD=0、DFC=0、功能码 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?不对,是10+49+01=5A,但报文里是4A——注意固定帧校验不含启动字符,只算控制域+链路地址,49+01=4A,对上了。这个“不含启动字符”的规则在可变帧里也一样,后面会再提。

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 = 9);68第二启动;53控制域,PRM=1、FCB=1、FCV=1、功能码 3(用户数据);01链路地址;64类型标识(总召唤);01可变结构限定词(SQ=0,信息体个数 1);06传送原因(激活);01公共地址;00 00信息体地址;14QOI(总召唤限定词);xx校验;16结束。

可变结构限定词这个字节值得单独说:bit7 是 SQ(顺序位),SQ=1 表示信息体地址连续递增、后续信息体省略地址;SQ=0 表示每个信息体都带独立地址。bit0~6 是信息体个数。正文里遥信帧14 08 14 01 01 00 80 04 ...中,08就是 8 个信息体、SQ=0,所以后面每个遥信组都重复写了信息体地址01 00、11 00、21 00……一直到71 00。如果 SQ=1,地址只写第一个,后面靠递增推,报文能短一截,但解析代码得跟着改。

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 16,RTU10 0B 01 0C 16,链路完好。第二步复位:主站10 40 01 41 16,RTU 回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 16,RTU 先回链路层确认10 20 01 21 16(注意这里 ACD 又置 1),主站发10 7A 01 7B 16,RTU 回总召唤确认帧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 个信息体,SQ=0,然后循环 8 次,每次读 2 字节信息体地址 + 2 字节遥信值 + 2 字节状态变位检出 + 1 字节品质描述,共 7 字节一组。第一组地址01 00对应 1 号遥信,值80 04按位展开是 16 个遥信(bit0 对应 1 号,bit15 对应 16 号),第二组地址11 00即 17 号,依此类推。遥信序号 = 信息体地址 - 基地址 + 1,97 版遥信基地址是01H,2002 版是4000H,这个差值直接决定你算出来的点号对不对。

遥测帧类型标识15(不带品质的遥测)、可变结构限定词E0即 96 个信息体、传送原因14、起始地址01 07。97 版遥测基地址701H,所以遥测号 =0x0701 - 0x0701 = 0,从 0 号开始,每个遥测值 2 字节,96 个就是 192 字节,加上帧头帧尾正好对上长度C8H(200 字节)。如果现场配的是 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解析为毫秒0x5822=22562即 22.562 秒、分0x14=20、时0x0F=15、日0x62低 5 位0x02=2号、月09、年05即 2005 年。注意毫秒是低字节在前,别按大端读。

电度召唤分冻结和读两步,类型标识都是65,靠 QCC 区分:冻结 QCC=45,读 QCC=05。电度帧类型标识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规约配置表.ini,DF8002/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 + 6(68 + 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:i+5] if frame[4] == 0x16: frames.append(("fixed", frame)) i += 5 continue elif data[i] == 0x68 and i + 2 < len(data): L = data[i+1] total = L + 6 frame = data[i:i+total] 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:4+L]) & 0xFF return calc == frame[4+L]

split_frames里i += 1那行是后悔药:现场抓包常混入噪声字节或半帧,不跳过会卡死。checksum_ok对可变帧只累加frame[4:4+L],正好是控制域到信息元素,不含两个68和长度字节,和规约定义一致。校验不过的帧直接丢弃并计数,别硬解,硬解出来的点号全是错的。

5.2 应用层字段提取:按配置偏移读,别写死

有了合法帧,按配置对象读字段。下面这段把可变帧拆成类型标识、可变结构限定词、传送原因、公共地址、信息体列表,信息体按 SQ 位决定是否重复读地址:

def parse_variable(frame: bytes, cfg: dict): L = frame[1] body = frame[4:4+L] # 控制域起 ctrl = body[0] off = 1 link_addr = int.from_bytes(body[off:off+cfg["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:off+cfg["cause"]], "little"); off += cfg["cause"] common = int.from_bytes(body[off:off+cfg["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:off+cfg["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 用正文报文做回归测试:三帧跑通再上现场

拿正文三帧当测试用例:总召唤命令、遥信帧、遥测帧。总召唤命令解析出 type=0x64、cause=6、count=1、addr=[0],遥信帧解析出 type=0x14、count=8、addrs=[0x0001,0x0011,...,0x0071],遥测帧解析出 type=0x15、count=96、addrs 从 0x0701 起连续。三帧都对上,说明切帧、校验、偏移、SQ 处理四条链路都通。上现场前再补两类用例:E5 单字节确认(应被切帧函数跳过且不报错)、长度字节两遍不一致的坏帧(应被校验丢弃)。这套回归用例我每次改解析器都跑一遍,比在现场拿万用表量串口快得多。

从那以后我每次对接新 RTU,都先把握手、复位、总召唤、对时四步的原始报文抓下来存成 hex 文本,跑一遍解析器回归,再谈点表对不对——报文层没过,上层点表怎么调都是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询