news 2026/9/8 14:41:28

吃透IEC104协议:从学习版源码到电力规约开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
吃透IEC104协议:从学习版源码到电力规约开发实战

简介:面向电力自动化学习者的IEC 104协议C语言源码包,对应IEC 60870-5-104远动通信标准,基于lib60870-C实现,包含客户端与服务器端完整代码,可用于理解智能电网设备间的数据交换机制。压缩包共111个文件,大小仅877KB,核心为39个C源文件和32个头文件,覆盖应用服务数据单元解析、传输控制、连接管理与超时重传等模块;同时附带构建脚本、说明文档、文档配置以及用于TLS加密通信的证书密钥文件,便于在Linux环境下交叉编译和二次开发。目前已有429人浏览学习。通过细致阅读源码,可以掌握报文的编码与解析流程,了解TCP/IP Socket编程在电力远动中的应用,并可从协议栈中附带的安全传输实现学习为传统规约添加TLS加密的方法。代码包目录结构清晰,源码注释较为规范,适合作为课程设计或毕业设计的参考资料;通过阅读连接管理和定时器实现,还能深化对TCP状态机及可靠传输的理解,对希望进入电力物联网或智能电网方向的技术人员也是一份难得的实战素材。 做电力自动化的人,电脑里没存过一份IEC104源代码,都不好意思说自己是干这行的。IEC104协议,即IEC 60870-5-104,是电力调度远动通信领域绕不开的核心规范,它把变电站里的遥测、遥信、遥控数据,通过TCP/IP网络送到调度主站。标题里“仅供自己学习使用”这个说法很实在,说明你拿到手的不是商用闭源库,而是一份能逐行读、能自由修改的学习素材。市面上能跑的IEC104源码不少,但大部分要么封装太厚、要么注释太少,真正适合用来理解协议本质的反而难得。

这篇文章我想从一个实际研读过、改写过IEC104源码的从业者角度,聊聊怎么把一份学习版源代码吃透。不会只丢一堆标准文档链接,而是直接讲代码里那些关键函数在做什么、为什么这么写、报文在哪一层被组装和拆解,以及我自己踩过的坑。适合刚接触电力规约开发的工程师、自动化专业的在校生,以及那些准备做电网协议适配但又不知道从哪下手的同学。

1. 为什么一份“学习版”IEC104源码值得逐行读

1.1 源码是最快的协议学习路径

很多人学IEC104,第一反应是去啃IEC 60870-5-104标准原文,或者买一本电力规约的书从头看到尾。说实话,这个路子效率很低。标准文档为了保证严谨性,术语密度极高,一句话能拆出七八个限定条件,读完了脑子里还是一团浆糊。代码不一样,代码是把协议标准翻译成机器逻辑的最终产物,每一个结构体、每一个switch分支、每一次memcpy,都是对标准条款的一次具体实现。你对着源代码去反推协议条款,远比对着条款去想象代码怎么写要轻松得多。

而且“仅供自己学习使用”的代码,通常意味着作者没有做过度封装。商用库为了通用性,会抽象出各种层、各种回调、各种线程模型,看半天都不知道报文在哪一刻被拼出来的。学习版代码一般就是直线思维——收包、解析、处理、组包、发送,一条流走到底,这种代码最适合用来建立协议的整体认知。我手上的这份源码大概三千多行,核心文件也就几个,一个周末认真捋一遍就能把主链路串起来。

1.2 拿到源码后先别急着编译,先把工程翻一遍

这是我反复强调的习惯:拿到学习版代码,第一件事不是make,也不是点IDE里的运行按钮,而是先把文件列表完整看一遍。一个规范的IEC104源码工程,通常会有这样几个模块:链路层处理(socket收发、TCP连接管理)、APCI解析(启动、停止、测试帧)、ASDU编解码(不同类型报文的组装与拆分)、应用层逻辑(总召处理、遥信变化上报、遥控命令执行)。你不需要记住每个文件的每一行,但要在心里画出一张地图——哪个文件负责收数据,哪个文件负责解析报文,哪个文件对应四遥里的哪一类。

这一步的价值在于,后续你调试问题时能快速定位。比如发现遥测数据上不去,你就知道该去ASDU解码那块找;如果链路反复掉线,说明是TCP保活或者序号校验的逻辑出了问题。没这份地图,你只能一个文件一个文件地打断点,效率低到让人想摔键盘。

2. IEC104协议的核心机制与源码里的对应关系

2.1 先认识一下APDU:协议的最小工作单元

IEC104所有的通信,本质上都是在传输APDU(应用协议数据单元)。每个APDU由APCI(应用协议控制信息)和可选的ASDU(应用服务数据单元)组成。源码里一般会有一个结构体,比如叫iec104_apdu_t,里面包含起始字节、长度、控制域,以及紧跟其后的一段数据缓冲区。固定起始字节是0x68,第二个字节是后面内容的长度,这两个字段几乎是所有IEC104代码里第一个被检查的东西。

我自己刚上手时犯过一个很低级的错误:以为收到的每个TCP包就是一个完整的APDU。实际上TCP是流协议,一次recv可能拿到半个APDU,也可能拿到三个半APDU。学习版源码通常会在这一层做缓冲处理,把收到的数据累积到一个环形缓冲区或者动态缓冲区里,然后循环检查里面有没有完整的APDU。这个处理逻辑值得重点看,因为很多实际运行中的通信问题,根源就是拆包粘包没处理好。

// 伪代码示意:缓冲区中提取完整APDU while (buf_len >= 2 && buf[0] == 0x68) { int apdu_len = buf[1] + 2; // 长度字段不含起始字节和长度字段自身 if (buf_len < apdu_len) break; // 数据不完整,等下一次收包 process_apdu(buf, apdu_len); buf += apdu_len; buf_len -= apdu_len; }

理解这段逻辑后,再看TCP收发线程就会通透很多。学习版代码里,收发通常各有一个线程,或者用select/epoll驱动,但不管哪种模型,缓冲区处理这块都是共通的。

2.2 控制域里的序号机制:U帧、S帧、I帧

IEC104的控制域只有一个字节或者四个字节,区分三种帧类型:U帧(编号0x03、0x23等)、S帧(0x01开头)、I帧(序号从0开始递增)。学习源码时,我最建议先把这三个宏定义找出来:

  • U帧:控制域为0x07表示STARTDT act,0x0B表示STARTDT con,0x13表示STOPDT act,0x23表示STOPDT con,0x43表示TESTFR act,0x83表示TESTFR con。
  • S帧:控制域第一个字节低两位为01,用于确认对方发送的I帧,自身不带数据。
  • I帧:控制域前两个字节是发送序号,后两个字节是接收序号,真正承载ASDU数据。

有生活化类比可以帮助理解:链路建立后的首次通信,很像打电话前的“你在吗?我在呢”。U帧就是这种链路控制信号,负责建立和维持会话;S帧是“我收到你说的了”的纯确认;I帧才是真正聊天内容。序号机制则是双方各自维护一个计数器,防止消息丢失或重复。

源码里序号处理那段,建议重点关注取模运算。因为IEC104的序号是12位,范围0到4095,超出后回绕。很多学习版代码会用(send_seq + 1) % 4096这种写法,简单有效。但要注意,真实场景里可能同时存在多个未确认的I帧,所以接收窗口大小的管理也是一个考点。学习版代码通常不会实现完整的滑动窗口,但至少会记录recv_seqsend_seq两个变量,调试时打印这两个值,基本就能定位数据不同步的问题。

2.3 四遥报文的解析思路:ASDU才是真正干活的

ASDU是IEC104里最贴近业务的部分,学习版源码的核心工作也大多集中在这里。一个ASDU包含类型标识、可变结构限定词(VSQ)、传送原因(COT)、公共地址(厂站地址)和信息体。类型标识决定了这条报文是遥信(如0x01单点遥信)、遥测(如0x09短浮点遥测)、遥控(如0x2D单点命令)还是总召(0x64)。

拿到一份源码后,可以先画出这样一个对照表,把所有支持的类型标识整理出来:

类型标识含义传输方向常见场景
0x01单点遥信 (M_SP_NA_1)站端→主站开关位置、保护动作信号
0x09短浮点遥测 (M_ME_NC_1)站端→主站电压、电流、有功功率
0x0D累计量 (M_IT_NA_1)站端→主站电度表读数
0x2D单点遥控 (C_SC_NA_1)主站→站端分合闸命令
0x64总召唤 (C_IC_NA_1)主站→站端全数据初始化

看ASDU编解码代码时,最容易懵的是字节序和数据类型转换。IEC104里信息体地址是三个字节,部分老代码会把它们当作int来读,但如果主机是大端序,就会得到完全错误的值。学习版代码如果考虑了跨平台,一般会用移位运算手动组地址,比如addr = (buf[0] | (buf[1] << 8) | (buf[2] << 16)),而不是直接memcpy。这个细节值得留意,因为很多莫名其妙的“地址串位”问题都是字节序没处理好。

3. 把代码跑起来:编译、配置与链路联调

3.1 编译前的准备工作

学习版IEC104源码通常依赖libpcap(抓包)、pthread(多线程)等基础库。在Linux下,先确认环境里装好了gcc、make和这些依赖。Windows下如果用MinGW,需要注意socket API的差异,不过多数学习版代码已经用条件编译把Winsock和BSD socket的差异处理掉了。

sudo apt-get install gcc make libpcap-dev

编译过程一般很顺利,真正花时间的是配置。源码里通常有一个配置文件,或者几个宏定义,用来设置本地监听端口、远动站地址、公共地址等。端口默认是2404,这是IEC104的标准TCP端口。厂站地址这个参数一定要好好确认,它对应ASDU里的公共地址,主站和站端不一致时,报文会被直接丢弃,但你会发现TCP连接是正常的,这种问题极难排查。

3.2 启动流程到底走哪几步

IEC104链路启动是一个固定的六步握手过程。主站侧(调度端)主动发起TCP连接,连接建立后,主站发送U帧STARTDT act,站端回复STARTDT con,然后双方才能正式传输I帧。如果站端认为链路异常,可能先发STOPDT act,等收到确认后再重新走STARTDT流程。测试帧TESTFR用于链路保活,周期性地确认对方还活着。

我在源码里定位这六步时,一般直接搜控制域的宏定义,然后顺着宏找到处理函数。学习版代码通常会在收到STARTDT act后置一个状态标志,比如link_started = 1,只有这个标志为真时,后续收到的I帧才会被继续解析,否则直接丢弃。看懂了这段,你就明白为什么有时候TCP连接是通的,数据却一股都不往上送——很可能就是STARTDT流程没走完,或者状态标志被意外重置了。

3.3 用模拟器做一次完整链路验证

推荐一个我常用的联调方式:用IEC104 client simulator作为对端,和你的学习代码互通。模拟器可以模拟主站或者站端,支持手动下发总召、遥控等命令,还能按周期自动发送遥测数据,非常适合验证代码的正确性。

具体操作可以这样:先用模拟器作为主站,连接你的站端代码。主站发出STARTDT act,观察你的代码是否回复STARTDT con。然后主站下发总召(类型标识0x64,传送原因6),你的代码要能正确解析并回复带数据的总召确认帧(传送原因7),接着上送全部点位的遥信和遥测。如果模拟器界面上的数据点开始刷新,说明整条链路已经打通。

这里分享一个心得:不要把模拟器窗口放在前台死死盯着。先在代码里加好日志,把收发的原始报文按十六进制打印出来,再配合Wireshark抓包对比,效率会高很多。一条报文从模拟器发出,到Wireshark里看到,再到日志里输出解析结果,三个地方一比对,问题出在收、解析、还是发送,一目了然。

# Wireshark过滤IEC104流量的标准写法 tcp.port == 2404

4. 研读源码时最常踩的坑与排查思路

4.1 连接建立了,却不传任何数据

这是最典型的新手迷惑现场。TCP三次握手已经完成,2404端口也通,但双方就是“尬聊”。出现这种情况,八成是U帧握手没走完。检查你的代码是否在收到STARTDT act之后正确回复了STARTDT con;检查主站侧是否主动发起了STARTDT,因为有些模拟器默认不会自动启动链路,需要手动点一下“启动”按钮。这个坑我在第一次用源码时也踩过,当时一头扎进ASDU解析里找原因,折腾了一下午才发现是模拟器界面有个启动按钮没点。

4.2 总召下发后一条遥信都没回来

总召的处理逻辑一般分布在两个地方:收到总召命令后的应答,以及组织数据上送的回调函数。很多学习版代码为了简化,会用一个全局数组模拟遥信遥测的当前值,你需要把点位总数、起始地址、每个点位对应的类型标识都配置正确。如果总召命令解析成功,但返回的数据长度是0,很可能就是点位表为空,或者生成报文的循环条件写错了。

还有一个容易被忽略的点:总召的应答有严格顺序。先回一个总召确认帧(传送原因7),然后按地址从小到大把所有数据帧发完,最后再回一个总召结束帧(传送原因10)。很多学习版代码只实现了中间的数据帧,确认帧和结束帧都没发,导致主站认为总召没有执行成功。调试时抓包看三个阶段的帧是否完整,能快速缩小问题范围。

4.3 控制命令发下去现场没动作

遥控命令(类型标识0x2D/0x2E)和其他报文不太一样,它需要带一个限定词(QU,0x00表示执行,0x01表示撤销)和一个选择/执行标志(S/E)。有些学习版代码虽然能收到遥控报文,但解析时把S/E字段当成了普通数据忽略掉,导致命令内容不完整,现场设备自然没反应。排查方法是打印收到的遥控ASDU原始字节,对照标准逐字节核对,看看限定词和处理标志是不是被正确提取。

4.4 收发序号对不上,链路反复重建

如果日志里频繁出现序号超差的记录,并且链路不断重建,很可能是在并发环境下序号自增操作没有加锁。IEC104要求每个I帧都带发送序号和接收序号,两个线程同时发送报文时,如果序号变量不是原子操作,就会出现序号跳变、重复等异常。学习版代码里常见的修复方式是在发送函数里加互斥锁,或者用__sync_fetch_and_add这类原子操作。这个问题在单线程循环里不会暴露,一旦改成多线程发送就立刻现出原形。

5. 从“看懂”到“能改”:我的学习体会

把一份学习版IEC104代码从头到尾捋清楚,和能独立修改它,中间还有一道坎。我的做法是从小改动开始:先给代码增加一个自定义扩展报文,比如把设备厂商信息打包进一个自定义ASDU,发给模拟器验证;再把原本写死的点位表改成从外部配置文件加载,每次启动时读取点位定义。这些小改动覆盖了组包、解析、配置加载、状态管理等多个环节,每完成一个,对协议的理解都会深一层。

在后续扩展时,有几个方向值得尝试:把这套代码移植到ARM嵌入式平台(比如RK3506这类工业级芯片上),或者用Python重新实现一个简化版IEC104客户端用于自动化测试,再或者为它加上TLS加密,满足电力监控系统网络安全防护的要求。这些方向每一条都能延伸出不少实战经验,但前提都是先把基础协议玩透。

最后再分享一个个人习惯:我会把研读过程中画的协议流程图、报文交互时序、各结构体字段含义整理成一份自己的笔记,连同源码的注释一起提交到git仓库。三个月后再回来看,这份笔记比任何标准文档都更有价值,因为它是你自己踩过坑、填过洞之后沉淀下来的理解。学习版IEC104源码只是起点,真正值钱的是你从源代码里长出来的那一整套调试思路和工程直觉。

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

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

Unity动画系统笔记

动画系统的本质 每帧执行的&#xff0c;对有关键帧的属性&#xff0c;根据动画曲线值进行写入的系统。本职工作是写入骨骼位置旋转&#xff0c;也可以写入各种组件字段。写入时机在Update()之后&#xff0c;LateUpdate()之前&#xff0c;也就是会覆盖Update()效果&#xff0c;…

作者头像 李华
网站建设 2026/9/8 14:39:53

DeepSeek LeetCode 56. 合并区间 JavaScript实现

以下是 JavaScript 实现 LeetCode 56. 合并区间 的代码&#xff0c;包含详细注释&#xff1a; /*** param {number[][]} intervals* return {number[][]}*/ var merge function(intervals) {// 边界条件&#xff1a;空数组直接返回if (intervals.length 0) return [];// 按区…

作者头像 李华
网站建设 2026/9/8 14:38:20

10周玩转Triton编译器:从零解剖GPU编程与编译原理

我一直觉得&#xff0c;编译原理是一门被学院派教学耽误的硬核知识。很多人一听到“编译器”三个字&#xff0c;第一反应就是那本厚厚的龙书&#xff0c;以及里面晦涩的正规表达式、LR分析表和寄存器分配算法。这种劝退感我太懂了&#xff0c;因为我一开始也是这么被吓跑的。直…

作者头像 李华
网站建设 2026/9/8 14:37:51

基于Simulink的双馈风机仿真:自励他励模式与MPPT控制实现

Simulink/matlab2019 双馈风机仿真&#xff1a;自励和他励模式实现与MPPT控制全解析做风电仿真的人应该都有同感&#xff1a;双馈风机&#xff08;DFIG&#xff09;的仿真模型在网上能找到不少&#xff0c;但大多数都是“能用”级别——波形出来就算成功&#xff0c;想进一步改…

作者头像 李华
网站建设 2026/9/8 14:37:21

MCP协议实战:用自然语言驱动Unity与UE的AI游戏开发工具链

最近帮团队搭了一条AI辅助游戏开发的工具链&#xff0c;从Unity到UE都有覆盖&#xff0c;核心思路就是用自然语言直接驱动游戏引擎。这套东西不是概念演示&#xff0c;是真能跑到项目里的。这次把完整实践写出来&#xff0c;从MCP协议本身的逻辑&#xff0c;到Unity MCP和Unrea…

作者头像 李华
网站建设 2026/9/8 14:37:19

ethers.js智能合约部署实战:从原理到脚本编写

最近在给一个链上小项目写部署脚本时&#xff0c;我又把 ethers.js 的部署链路完整走了一遍。很多人习惯直接用 Hardhat 的run命令一条龙部署&#xff0c;这当然省事&#xff0c;但一旦你想把部署能力嵌进后端服务、CI 流程&#xff0c;或者想精细控制 gas、nonce、签名者这些细…

作者头像 李华