news 2026/10/5 1:20:32

IEC 101/104 规约实战:从 iec-master 源码到报文调试与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IEC 101/104 规约实战:从 iec-master 源码到报文调试与避坑

简介:本资源围绕电力系统101规约(DL/T634.5101-2002)与104规约(DL/T634.5104-2009)展开,基于Java语言实现,面向从事配网自动化、变电站通信及电力规约开发的工程师与学习者。项目可完成规约报文的解析与组装,适用于实际场景中发送报文的生成与交互处理,帮助读者理解规约帧结构、信息体地址与传输流程。压缩包共43个文件,以34个Java源码为核心,辅以3个XML配置、2份规约实施细则文档、1份规约解析细则表格及README说明,整体约1.79MB,目录按解析与交互模块划分,结构清晰。目前已有661人学习下载。通过源码与配套细则文档对照,读者可掌握101/104规约的编解码逻辑,快速搭建报文生成与解析环境,并借助附件中的广东电网实施细则与解析细则表,深入理解实际工程中的规约应用与调试要点。

1. 从一份 iec-master 源码说起:101/104 规约到底在解决什么问题

如果你手上正好有一份叫iec-master的代码包,打开目录大概率能看到iec101、iec104两个并列的模块,外加一堆asdu、apci、cp56time2a之类的命名。很多人第一次接触电力104规约,是从「为什么调度主站能实时看到我这条线路的遥测值」这个问题开始的。答案就藏在 IEC 60870-5 这套标准里:101 规约跑在串口上,104 规约跑在 TCP 上,两者共享同一套 ASDU(应用服务数据单元)结构,区别只在链路层和传输层。iec-master这类工程的价值,是把这套又长又碎的报文格式,变成能编译、能抓包、能对着字节调试的代码。它适合做变电站自动化、配网终端、规约网关的工程师,也适合想搞懂「四遥」数据到底怎么在网线上跑的开发者。下面我按自己踩过的顺序,把 101 和 104 从报文结构讲到能跑通的代码。

2. 101 与 104 的报文骨架:从 FT1.2 到 APCI 的差异在哪

2.1 为什么同一套 ASDU 要配两种链路层

IEC 60870-5-101 的定位是串行链路,典型场景是变电站内的 RTU 通过 RS-485 或 RS-232 接到通信管理机。串口是字节流,没有连接概念,所以 101 用 FT1.2 帧格式来划界:固定帧长、可变帧长、单字符三种帧型,靠起始字节0x10、0x68和校验和来保证同步。104 则把链路层换成了 TCP/IP,用 APCI(应用规约控制信息)的 6 字节头68 04 xx xx xx xx来标识报文长度和发送序号。这里有个容易翻车的点:104 的 APCI 里那个68和 101 可变帧的68长得一样,但含义完全不同,抓包时如果按 101 的逻辑去解析 104 的 TCP 流,序号字段会被当成链路地址,整包数据全乱。

我一般会先让读者记住一张对照表,再去看代码:

维度IEC 101IEC 104
传输介质串口(RS-232/485)TCP/IP
链路帧格式FT1.2(固定/可变/单字符)APCI(6 字节头)
链路地址1~2 字节无,靠 IP 区分
发送序号无2 字节,模 32768
典型端口无2404
启动方式链路复位STARTDT 激活

这张表不是背的,是调试时用来定位问题的。比如你发现 104 连接建立后主站不发总召,先看 STARTDT 有没有回,而不是去查 ASDU 类型。

2.2 ASDU 的公共地址与传送原因怎么读

不管 101 还是 104,真正承载遥测、遥信、遥控的是 ASDU。一个 ASDU 由数据单元标识和信息对象两部分组成。数据单元标识里,类型标识(TypeID)决定后面跟的是什么数据,可变结构限定词(VSQ)告诉你信息对象的个数和是否连续,传送原因(COT)说明这是周期上送、响应总召还是突发。公共地址(CA)在 101 里常用来区分不同 RTU,在 104 里通常填 1 或按主站约定。

以最常见的单点遥信为例,TypeID 是 1,VSQ 的 bit7 为 0 表示不连续,低 7 位是信息对象个数。COT 占两个字节,低字节是原因,高字节是源发站地址。信息对象地址(IOA)三个字节,小端排列。这些字段在iec-master里通常对应一个struct,但不同作者的字段顺序和字节序处理不一样,直接拿来用之前一定要对着标准原文核一遍。

提示:101 和 104 的 ASDU 结构完全一致,所以一份 ASDU 编解码代码可以同时服务两个规约,前提是把链路层抽象干净。

3. 用 iec-master 跑通 104 主站与从站的最小闭环

3.1 环境准备与目录结构确认

拿到iec-master后,先别急着编译。我习惯先看三样东西:有没有CMakeLists.txt或Makefile、iec104目录下是不是分了master和slave、有没有示例配置文件。常见做法是主站和从站各编一个可执行文件,主站监听或主动连接,从站绑定 2404 端口。如果代码里用了epoll或select,说明是单线程事件循环,调试时可以用strace跟一下系统调用。

# 查看目录结构,确认主从站入口 find . -maxdepth 3 -type f \( -name "*.c" -o -name "*.cpp" -o -name "*.h" \) | head -40 # 如果存在 CMakeLists,按标准流程构建 mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Debug make -j4

上面这段命令的作用是先摸清代码组织,再决定编译方式。-DCMAKE_BUILD_TYPE=Debug是为了后面能用 gdb 看报文组装过程,Release 模式下有些变量会被优化掉,单步调试时看不到中间值。如果项目只有 Makefile,直接make即可,但要留意有没有-lpthread之类的链接选项缺失。

3.2 从站启动与 STARTDT 握手

104 从站启动后,第一件事是等主站发 STARTDT 激活。APCI 的 U 格式帧里,0x07是 STARTDT 请求,0x0B是 STARTDT 确认。很多新手写的从站一上来就主动上送遥测,结果主站还没激活链路,数据全被丢弃。正确顺序是:TCP 连接建立 → 主站发 STARTDT → 从站回 STARTDT 确认 → 主站发总召 → 从站响应总召 → 进入周期上送。

// 简化的 STARTDT 处理逻辑,U 格式帧控制域为 1 字节 if (apci_len == 6 && (buf[2] & 0x03) == 0x03) { // U 格式帧,buf[2] 低两位为 11 if (buf[2] == 0x07) { // 收到 STARTDT 请求,回复确认 uint8_t ack[6] = {0x68, 0x04, 0x0B, 0x00, 0x00, 0x00}; send(fd, ack, 6, 0); startdt_active = 1; } }

这段代码的关键是判断 APCI 长度和控制域。0x68 0x04表示 APCI 总长 6 字节,后面四个字节是控制域。U 格式帧的控制域第一个字节低两位为11,0x07和0x0B分别对应 STARTDT 的请求和确认。startdt_active这个标志位很重要,后续所有 I 格式帧的上送都要先检查它,否则主站会认为从站行为异常。

3.3 总召响应与遥测上送

总召是主站用 I 格式帧发来的,TypeID 为 100(总召唤命令),COT 为 6(激活)。从站收到后,要按信息对象地址顺序把遥信、遥测逐个上送,最后再发一个 TypeID 100、COT 为 10(激活终止)的帧表示总召结束。遥测的 TypeID 常用 9(归一化值)或 11(标度化值),每个信息对象后面跟 2 字节数值和 1 字节品质位。

// 组装一个归一化遥测 ASDU,TypeID=9,COT=3(突发)或 20(响应总召) uint8_t asdu[32]; int idx = 0; asdu[idx++] = 9; // 类型标识:归一化遥测 asdu[idx++] = 0x01; // VSQ:1 个信息对象,不连续 asdu[idx++] = 20; // COT 低字节:响应总召 asdu[idx++] = 0; // COT 高字节:源发站地址 asdu[idx++] = 1; // 公共地址 asdu[idx++] = ioa & 0xFF; // IOA 低字节 asdu[idx++] = (ioa >> 8) & 0xFF; asdu[idx++] = (ioa >> 16) & 0xFF; int16_t norm = (int16_t)(value * 32767 / rated); // 归一化到 -1~1 asdu[idx++] = norm & 0xFF; asdu[idx++] = (norm >> 8) & 0xFF; asdu[idx++] = 0; // 品质位:有效 // 再封装 APCI,发送序号和接收序号按当前状态填

这里value是实际工程值,rated是额定值,归一化公式按标准是value / rated * 32767,注意溢出和符号。IOA 三字节小端,品质位 0 表示有效,如果遥测越限或无效,要置对应的 bit。发送序号每发一帧加一,模 32768,接收序号是对端已确认的序号,这两个序号维护错了,主站会断开连接。

4. 101 规约在串口上的调试要点与 FT1.2 帧解析

4.1 可变帧长的校验和与结束符

101 的 FT1.2 可变帧格式是68 L L 68 C A CS 16,其中两个68之间是长度字段重复一次,C是控制域,A是链路地址,CS是从控制域到信息域的校验和,最后16是结束符。固定帧格式是10 C A CS 16,没有信息域。调试串口时,最常见的问题是校验和算错,导致从站直接丢帧。校验和的算法是控制域、链路地址、信息域所有字节的算术和取低 8 位,不取反。

# 计算 101 可变帧校验和 def checksum(data): s = 0 for b in data: s = (s + b) & 0xFF return s # 假设控制域 0x53,链路地址 0x01,信息域为 asdu frame_body = bytes([0x53, 0x01]) + asdu cs = checksum(frame_body) frame = bytes([0x68, len(asdu)+2, len(asdu)+2, 0x68]) + frame_body + bytes([cs, 0x16])

这段 Python 用来验证你手算的校验和对不对。len(asdu)+2是因为长度字段包含控制域和链路地址。实际用 C 写的时候,注意uint8_t溢出是自动取模的,但如果你用了int累加,最后要& 0xFF。串口参数一般是 9600 或 19200 波特率,8 数据位,1 停止位,偶校验,具体看设备手册。

4.2 链路复位与召唤的时序

101 从站上电后,主站会先发链路复位帧(固定帧,控制域0x40),从站回确认(控制域0x00)。然后主站发总召,从站响应。和 104 不同的是,101 没有 STARTDT 这个概念,链路复位就相当于激活。如果从站一直不回确认,先查串口线是不是 A/B 接反,再查波特率。我遇到过一批 RTU,出厂默认波特率是 9600,但现场通信管理机设成了 19200,结果就是能收到帧但校验全错,因为采样点对不上。

注意:101 的链路地址在有些设备上是 0,有些是 1,主站配置里必须和从站一致,否则从站会认为不是发给自己的帧而丢弃。

5. 避坑与排查:报文对不上时先查这五处

5.1 现象:104 连接建立后主站不发总召

原因:从站没有正确回复 STARTDT 确认,或者回复的 U 格式帧控制域写错。有些代码把0x0B写成了0x03,主站认为链路没激活。解决:抓包看从站回的第二个字节是不是0x0B,同时确认 APCI 长度是 6。

5.2 现象:遥测值跳变或恒为 0

原因:归一化公式用错,或者字节序搞反。104 的遥测值是小端,低字节在前。如果从站用大端发送,主站解析出来就是另一个数。解决:用计算器把收到的两个字节按小端拼成int16_t,再除以 32767 乘以额定值,看是否和实际相符。

5.3 现象:总召响应到一半连接断开

原因:发送序号或接收序号没有按模 32768 递增,或者收到主站的 S 格式帧后没有更新接收序号。104 的序号是强校验的,序号错一次主站就可能断开。解决:在代码里把每次发送和接收的序号打印出来,对比抓包工具的序号字段。

5.4 现象:101 串口能收到数据但解析全是乱码

原因:串口参数不匹配,最常见的是校验位设成了无校验,而设备要求偶校验。偶校验下每个字节的最高位是校验位,如果按无校验解析,数据会整体偏移。解决:用示波器或串口助手确认波特率和校验位,再核对帧头的0x68或0x10是否出现在正确位置。

5.5 现象:遥控命令下发后从站不动作

原因:遥控的 TypeID 是 45(单命令)或 46(双命令),COT 是 6(激活),从站要回 COT 为 7(激活确认)的帧,然后执行,再回 COT 为 10(激活终止)。如果从站只回了确认没执行,或者执行后没回终止,主站会认为超时。解决:检查遥控处理流程里有没有漏掉激活终止帧,以及遥控输出是否被闭锁。

6. 进阶:用一份代码同时支持 101 和 104 的抽象技巧

写到后面你会发现,101 和 104 的差异其实只在链路层,ASDU 层完全可以复用。我一般会把代码分成三层:link层负责帧的收发和校验,asdu层负责编解码,app层负责业务逻辑。link层定义一个接口,101 实现 FT1.2,104 实现 APCI,上层不关心底下是串口还是 TCP。

// 链路层抽象接口 typedef struct { int (*init)(void *cfg); int (*send)(const uint8_t *asdu, int len); int (*recv)(uint8_t *buf, int maxlen); void (*close)(void); } link_ops_t; // 101 和 104 各自实现这套接口,app 层只调 link_ops

这样做的直接好处是,调试 104 时可以用 101 的 ASDU 测试用例来验证编解码,反过来也一样。另一个好处是,现场如果从串口升级到网络,业务代码一行不用改。我自己的习惯是,每加一个 TypeID,先在 101 的串口助手上发一遍,确认 ASDU 字节对了,再切到 104 走 TCP。这个顺序能省掉很多「到底是链路问题还是应用问题」的纠结。

最后说一个验证方法:用 Wireshark 抓 104 的包,过滤tcp.port == 2404,然后对着iec-master的日志逐字段比对。如果 Wireshark 能正确解析出 TypeID 和 IOA,说明你的 APCI 和 ASDU 都没问题;如果 Wireshark 显示 malformed,那一定是长度字段或序号错了。这个习惯我保持了几年,比任何文档都快。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 1:18:49

RPG Maker MV游戏逆向:从加密资源到核心数值的静态拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:18:47

基于Yolov5的裂缝检测系统:从数据标注到部署全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:18:47

OpenLayers多区域动态遮罩实战:Canvas挖洞与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:18:17

AI CTF实战:提示词注入与意图偏离攻防全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:17:56

工业嵌入式存储选型:MRAM与FRAM芯片SPI驱动及掉电保护实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:17:37

ES8388音频Codec寄存器配置实战:从录音到播放的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华