资讯详情

IEC60870开源协议栈:主站从站双角色,快速实现电力104通信

📅 2026/10/20 6:36:56 | 华诺云谱 👁 阅读
IEC60870开源协议栈:主站从站双角色,快速实现电力104通信
简介lib60870-2.0.1 是一套面向电力自动化开发者的开源 IEC60870 协议实现库覆盖 101、102、104 三种常用规约101 面向串行链路点对点通信102 用于电能量数据传输104 基于 TCP/IP 适配广域组网支持主站与子站双向通信、编解码与多传输介质适合需要快速接入 SCADA、RTU 或智能电网系统的 C/C 工程师也可作为学习协议栈的参考范本。资源包共 114 个文件、约 186KB主体为 39 个 C 源码与 34 个头文件另有 makefile/mk 构建脚本、adoc/md/readme 使用文档、cer/pem 证书及 txt 示例说明结构清晰便于按模块阅读、裁剪和移植到嵌入式或桌面平台并附示例程序与单元测试辅助验证。已有 2499 人学习浏览。通过这套代码可获得完整的协议栈参考实现包括 cs101/cs104 主从站示例、链路层与 ASDU 编解码逻辑、时间标签处理等并从中理解规约细节和工程落地方式节省从零开发协议栈的工期同时提高通信可靠性与兼容性。1. IEC60870开源实现库一份能直接上线的电力通信协议栈做电力调度或 SCADA 场景的开发者大概率经历过这样的阶段手里一批 RTU 要接进监控后台协议是 IEC 60870-5-104文档翻了几十页报文却总对不上。自己从零写协议栈链路层状态机、ASDU 解析、序号管理、超时重传每一步都是坑。这个 IEC 60870 开源实现库把 101/104 双栈都封装好了主站和从站角色实例化就能用适合变电站监控、配网自动化、储能系统接入的开发者直接拿来做底层通信。核心代码围绕 ASDU 数据模型编解码和链路层状态机展开改造成本低接入业务系统很快。2. 整体架构与目录设计从 APCI 到业务数据的转换链路2.1 Master 与 Slave 双角色设计IEC 60870-5-104 协议框架里通信双方天然分成主站控制端和从站被控端。这个库没有把两者做成两套完全独立的代码而是在同一套抽象之上分别实现CS104Connection和CS101Slave两类对象。主站负责发起连接、周期总召唤、发送遥控命令从站负责监听端口、维护链路状态、响应总召唤并主动上报变位信息。这种双角色设计的价值在于同一份 ASDU 编解码代码被两侧共用。主站收到的遥信遥测数据和从站上报的变位事件走的是同一个解析入口区别只在于主站侧需要管理发送序号和接收序号从站侧需要处理多个并发连接。实际项目中常见的用法是一个后台服务同时打开一个主站连接去采集下辖的几个 RTU再挂一个从站服务把聚合后的数据转发给上级调度系统两侧用的是同一个库。从线程模型上看库内部为每个 TCP 连接维护一个独立的工作线程收发缓冲区和回调分发都在这个线程里串行处理。这样设计的好处是业务回调里不需要加锁坏处是回调如果执行太久会阻塞链路层的接收循环。常见做法是回调里只做数据拷贝和投递真正的业务处理放到外部线程池去这一点在第五部分的避坑记录里会展开讲。2.2 目录结构与代码分层整个代码库按功能分成四层目录结构建议阅读时按这个顺序走iec60870/ ├── core/ # 基础数据结构和编解码工具 │ ├── asdu.c # ASDU 编解码核心实现 │ ├── asdu_types.h # 信息对象类型定义与联合体 │ ├── buffer.c # 环形缓冲区实现 │ └── frame.c # 104 帧类型判断与初析 ├── linklayer/ # 链路层 │ ├── cs104_frame.c # 104 帧格式组织与解析 │ ├── cs101_link.c # 101 链路层状态机 │ └── connection.c # 连接管理与超时控制 ├── hal/ # 硬件抽象层 │ ├── thread.c # 线程封装pthread 与 Win32 两套实现 │ ├── socket.c # TCP/UDP 套接字封装 │ └── time.c # 毫秒级时间戳工具 ├── examples/ # 可直接运行的示例 │ ├── slave_demo.c # 从站示例程序 │ └── master_demo.c # 主站示例程序 └── tests/ # 单元测试与抓包辅助脚本core 层是整个库的基石把类型标识、信息对象地址、品质描述符这些协议概念全部映射成 C 结构体。linklayer 层负责把 ASDU 装进 104 帧或者把收到的字节流还原成 ASDU。hal 层把操作系统相关的线程、套接字和时钟全部抽象掉交叉编译到其他平台时只需要替换这一个目录。examples 目录是上手的捷径后面跑通部分就基于它来改。这种分层在二次开发时很舒服。业务代码不会散落到协议解析逻辑里协议层的修改也不会影响上层的业务模块。我见过不少团队直接在 core 层增加自定义 ASDU 类型然后把新增的编解码函数挂进 linklayer 的分发表里改动量通常控制在几百行以内这就是分层设计带来的直接收益。2.3 构建系统与外部依赖构建系统用 CMake最低版本 3.10支持 GCC 和 Clang 两类编译器。库本体不依赖任何第三方库TCP 通信和线程管理都走操作系统标准接口这个约束是刻意的——电力行业项目经常要部署在国产化系统或者老的 ARM 板子上依赖越少迁移成本越低。编译产物包括静态库libiec60870.a和几个示例程序。mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DENABLE_SAMPLESON .. make -j4如果只想编译库本体不带示例把-DENABLE_SAMPLES改成OFF。交叉编译时设置CMAKE_TOOLCHAIN_FILE指向工具链文件hal 层的 socket 实现会自动选择 POSIX 分支还是 Windows 分支。构建阶段值得注意的细节有三个默认 TCP 接收缓冲区大小是 8KB报文量大时要调大日志输出默认走 stderr生产环境建议挂一个日志回调函数把输出重定向到文件系统Debug 构建默认开启断言检查报文异常时直接触发断言这在开发期帮助很大但发布版本务必切到 Release。3. 核心模块拆解一个报文从网线到回调函数的完整旅程3.1 ASDU 的编解码类型标识决定一切ASDUApplication Service Data Unit是 IEC 60870 协议里真正承载业务数据的单元。报文里的每一个遥测、遥信、遥控指令都以 ASDU 的形式存在。一个完整的 ASDU 由类型标识TypeID、可变结构限定词VSQ、传送原因COT、公共地址COA和若干个信息对象组成。// 构建一个单点遥信 ASDUM_SP_NA_1上报开关分合位 ASDU asdu; ASDU_initialize(asdu, M_SP_NA_1, false); ASDU_setCOT(asdu, COT_SPONTANEOUS); // 自发原因值为 3 ASDU_setCA(asdu, 0x0001); // 公共地址 ASDU_addInformationObject(asdu, (InformationObject)SinglePointInfo_create(0x010203, 1, QualityDescriptor_getValid(true)));这段代码创建了一个类型为M_SP_NA_1的 ASDU传送原因是自发从站主动上报公共地址设为 1然后添加了一个信息对象信息对象地址0x010203值是 1表示合位品质描述符标记为有效。InformationObject是一个联合体支持单点信息、双点信息、短浮点测量值、步进命令等几十种类型具体用哪个取决于 TypeID。解析侧的逻辑是编解码的镜像。收到一个 ASDU 后先读类型标识再用一个分支语句分发到对应的解析函数。难点在变长信息对象的数量计算——VSQ 的低 7 位表示对象个数最高位 SQ 表示这批对象的地址是否连续。地址连续时只需要传第一个对象的地址再加偏移量不连续时每个对象都要带完整的三字节地址。这个细节很容易写漏一旦漏了解析出来的数据全是错的。// 解析侧根据类型标识分发给具体处理函数 static void handleASDU(ASDU asdu, void* userData) { // 短浮点遥测常见于电压、电流、功率量 if (ASDU_getTypeID(asdu) M_ME_NC) { MeasuresSequence values; ASDU_getMeasures((uint8_t*)ASDU_getData(asdu), values); for (int i 0; i values.size; i) { printf(IOA0x%06X value%.2f quality0x%02X\n, values.objects[i].ioa, values.objects[i].value, values.objects[i].quality); } } }一个完整的读取流程是先用ASDU_parseFromByteArray把裸字节流转成 ASDU 结构体再根据ASDU_getTypeID拿到类型标识最后调用对应的取值函数提取信息对象。这个库把信息对象地址统一按三字节处理高位补零。实际调试时把 IOA 直接打印出来和现场装置说明书里的点表比对能很快确认接线和配置是否正确。3.2 链路层服务状态机I 帧、S 帧与 U 帧的协作104 协议的链路层和 101 最大的不同是它跑在 TCP 之上因此多了一层连接管理和确认机制。帧类型分三种I 帧承载数据并携带收发序号S 帧只做接收确认U 帧负责建立连接、启停数据传输和测试链路。每帧固定是68 长度 控制域1~4加上紧随其后的 ASDU 载荷。// I 帧控制域构造示例 static void buildIFrame(uint8_t* buffer, uint16_t sendSeq, uint16_t recvSeq) { buffer[0] 0x68; // 起始字符固定值 buffer[1] 0x00; // 长度字段回调前回填 buffer[2] (uint8_t)((sendSeq 1) 0xFE); // 发送序号低位 buffer[3] (uint8_t)((sendSeq 7) 0x01); // 发送序号高位 buffer[4] (uint8_t)((recvSeq 1) 0xFE); // 接收序号低位 buffer[5] (uint8_t)((recvSeq 7) 0x01); // 接收序号高位 }控制域四个字节里真正有用的比特位很少发送序号占 15 位接收序号占 15 位其余是帧类型标识位。注意序号是每发一帧加一而不是按字节计数。接收方校验序号是否连续如果不连续就丢弃后续帧直到收到正确的重传。U 帧不带序号三个命令分别是 STARTDT启动数据传输、STOPDT停止数据传输、TESTFR链路测试各自的确认帧是对应控制域加 0x10 偏移。从站收到 I 帧后并不会每帧都回复 S 帧常见做法是攒够一定数量的帧或者超过一个很短的时间窗口后统一确认这个窗口默认是 5 帧或 50 毫秒。主站侧如果发出去 12 帧还没收到确认就会关闭发送窗口停止继续发送并启动超时重传。这个半窗口机制是协议性能和可靠性的关键平衡点很多现场断链问题都出在窗口和超时参数的配合上。3.3 回调机制与数据上行通道从站侧的数据上行是整个库最常用的一条路径。协议栈在收到主站的召唤命令或者检测到内部数据变化后需要把结果送给业务层。这个库用的是注册回调函数的方式而不是队列加轮询的传统模型。// 注册从站数据上报回调 CS101_Slave slave CS101_Slave_create(2404, slaveParams, linkLayerParams); CS101_Slave_setASDUHandler(slave, handleASDU, NULL); CS101_Slave_start(slave);setASDUHandler注册的handleASDU会在每个完整 ASDU 被解析出来后立即调用。参数里的那个指针可以指向自己的业务上下文比如一个会话结构体或者一个环形缓冲区。库内部保证同一个连接上的回调不会并发进入但多个连接之间是并行回调的如果业务代码要访问共享数据还是得自己加锁。还有一个更细粒度的上报接口在每一个信息对象被解析后触发适合按单个开关量或模拟量处理业务的场景。这两种回调不能同时用注册了对象级回调后ASDU 级回调就不会被触发。实际使用中数据量小用 ASDU 级回调数据量大用对象级回调直接逐条投递避免在回调里再拆一层。回调模型的选择直接影响后续业务代码的组织方式。4. 编译构建与运行验证把协议栈跑通4.1 编译参数与构建步骤源码包里的构建流程已经在主流 Linux 发行版和 Windows 上验证过。构建命令前面给过这里补充几个实际操作中经常调整的 CMake 开关cmake -DCMAKE_BUILD_TYPEDebug \ -DENABLE_SAMPLESON \ -DENABLE_LOGGINGON \ -DBUILD_SHARED_LIBSON ..ENABLE_LOGGING控制协议栈内置日志。开发阶段建议始终开着它会把每个收到的原始报文按十六进制打印出来排查问题时不至于对着裸字节发呆。发布时建议关掉因为日志输出本身对时序有影响在真实调度场景里可能干扰链路层的超时判断。还有一个开关控制是否编译 101 链路层代码如果项目只走 104 走 TCP关掉它可以减小编译产物体积。编译完成后build/examples目录下会生成master_demo和slave_demo两个可执行文件。这两个示例是理解库用法的活文档代码量不大覆盖了连接建立、总召唤、遥控下发、变位上报四条核心交互路径。我拿到一个新版本源码的习惯动作是先跑这两个示例确认环境没问题再开始看业务集成代码。4.2 快速搭建一个主站实例主站的业务逻辑围绕CS104_Connection展开。创建连接时传入从站 IP 和端口然后设置参数、注册回调、发起连接。下面这段代码基本是一个最小可用的主站模板CS104_Connection conn CS104_Connection_create(192.168.1.100, 2404); CS104_Connection_setConnectionTimeout(conn, 10); // 连接超时 10 秒 CS104_Connection_setSentIFrameTimeout(conn, 10); // 发送超时 10 秒 CS104_Connection_setMaxSentIFrames(conn, 12); // 未确认最大帧数 CS104_Connection_setASDUReceivedHandler(conn, handleASDU, appContext); CS104_Connection_connect(conn); CS104_Connection_sendInterrogationCommand(conn, 1, IEC60870_QOI_STATION);最后一行发出的是一轮站总召唤参数IEC60870_QOI_STATION表示全站数据从站收到后会按顺序把全部遥信、遥测数据以多个 ASDU 回送。实际的调度系统里总召唤通常定时执行比如每小时一次用来修复长期运行中可能出现的微小数据偏差。召唤命令本身也是一个 ASDU类型是 C_IC_NA_1不带信息对象只带一个召唤限定词。handleASDU回调里的实现可以参照上一章的解析代码把遥信、遥测分类归档到内部数据结构。这里有一个值得借鉴的做法回调里只做数据拷贝用轻量锁保护一个队列另一个线程负责消费这个队列写入数据库或者做界面刷新。这样即使从站短时间内爆发大量数据也不会拖慢链路层接收速度。4.3 模拟从站与收发验证没有真实装置的时候直接用slave_demo当模拟从站是最快的验证方式。默认配置下它会监听 2404 端口内部维护一组模拟数据点收到总召唤命令后回送 100 个遥测点和 50 个遥信点。# 终端 A启动模拟从站 ./slave_demo # 终端 B用主站示例发起总召唤并打印结果 ./master_demo 127.0.0.1 2404终端 B 会打印类似下面的输出每一行对应一个信息对象RECV ASDU type13 (M_ME_NC) cot3 (spontaneous) ca1 IOA0x000001 value220.50 quality0x00 IOA0x000002 value10.32 quality0x00如果收不到输出按顺序检查三件事两个进程是否都正常启动主站连接参数里的端口和从站监听端口是否一致系统防火墙有没有拦截 2404 端口。协议层的抓包建议用带 IEC 60870-5-104 解析器功能的抓包工具能直接看到帧类型、序号和 ASDU 内容比单纯看 HEX dump 直观得多。开发阶段养成抓包对比的习惯能省掉大量后面找人排障的时间。5. 协议调试避坑指南五个真实踩坑记录5.1 发送序号不回卷导致连接假死现象主站运行一段时间后停止发送数据日志里看不到任何新帧TCP 连接没有断开从站也没有收到数据。原因发送序号是 15 位范围 0 到 32767。长时间连续上报的数据量很大时序号达到上限后如果实现里忘了回卷序号溢出变成负值或者超出帧格式能容纳的位宽接收端校验序号时认为不连续直接丢弃所有后续帧。主站侧表现为停止发送从站侧表现为主站静默。解决检查序号递增代码确认每次加一之前判断是否等于 32767是则回零。更稳妥的方式是让序号变量用无符号类型让位运算的截断自然完成回卷。验证方法是在测试环境人为往发送循环里加一个计数器触发序号跳变观察从站是否恢复正常接收。5.2 遥信变位丢失COT 与时钟对不上现象从站明明上报了变位比如开关跳闸主站却收不到任何记录日志里只有周期性总召唤的数据。原因变位上报告要求的传送原因是自发值为 3。现场不少装置默认配置的原因码是周期值为 2主站侧的过滤逻辑遇到非自发原因的事件型 ASDU 会直接丢弃因为无法确认这是新变位还是重复数据。另一个常见原因是事件时标字段的三字节毫秒时标溢出或者使用了零时标主站收到后因为时标异常直接忽略。解决第一步查从站配置文件里事件上送的原因码设置改成 3第二步在主站回调里打印 ASDU 的 COT 和时标字段看实际收到的值。如果是时标为零尝试关闭主站侧的时标校验逻辑或者修改从站的时钟同步周期先排除协议栈本身的问题再查时钟源。5.3 多从站接入时公共地址冲突现象主站同时接十几个 RTU其中两个 RTU 的数据串到另一个设备名下遥测值张冠李戴画面上的曲线乱跳。原因104 协议里公共地址COA用于区分不同的从站或逻辑设备但默认配置下很多 RTU 出厂时 COA 都是 1。主站侧如果只按连接区分数据源而没有把连接和 COA 联合起来做映射就会出现不同设备上报的数据在同一个设备节点下堆积路由不到正确的数据点。解决所有设备接入前先做 COA 规划每个 RTU 分配一个唯一地址。主站侧建立一张连接到 COA 集合的映射表收到 ASDU 时先取 COA 再查表路由。开发期可以在回调入口打印 COA 字段用一个简单脚本扫描所有接入设备是否出现重复 COA。这个坑在集中器场景尤其常见值得提前规划地址表不要等设备挂上去了再改。5.4 结构体位域与大小端对齐问题现象跨平台编译后遥控命令的开关分合位取反品质描述符的字节错位。在开发机的 x86 上一切正常部署到 ARM 板子上就翻车。原因协议栈的某些实现直接用位域结构体去映射报文字节比如把品质描述符的 8 个 bit 用带位域的匿名结构体表示。C 语言标准不保证位域的内存布局同一套代码在不同架构上的位域分配方向可能相反。远动装置的双点遥控恰好依赖最高位一旦位序反了合位变分位分位变合位这在调度台上是严重事故。解决不用位域结构体直接映射报文全部改成手工掩码和移位操作。品质描述符的读取收敛到一个函数里内部用(byte bitIndex) 0x01逐位提取写入同理。这个问题排查起来很费时间我踩过一次之后新代码里所有跨平台报文解析一律禁止位域映射宁可多写几行移位代码。5.5 心跳超时参数设置不当引发频繁断链现象主站和从站都在线但每隔几分钟连接就断开重建一次日志里反复出现 STOPDT 和 STARTDT业务数据跟着中断。原因协议的 t3 参数控制链路空闲时的测试帧发送周期主站和从站两端如果配置不一致比如主站设 20 秒从站设 30 秒主站发 TESTFR 后从站可能不确认主站超时后判定链路异常直接断开。另一种情况是心跳周期设置太短确认帧打进业务数据洪峰里被延迟导致误判。解决两端的 t0连接超时、t1发送超时、t2确认超时、t3空闲测试周期必须完全一致且满足 t2 小于 t1、t3 大于 t2 的关系。推荐一组经过验证的配置t0 取 10 秒t1 取 15 秒t2 取 10 秒t3 取 20 秒。修改完参数后跑 24 小时稳定性测试观察日志里 TESTFR 帧的响应时间是否稳定在 1 秒以内。6. 二次开发改造从拿来用到定制自己的协议栈6.1 扩展一个私有 ASDU 类型接入某些非标装置时厂商会在标准 ASDU 类型之外自定义类型标识通常落在 100 到 127 的保留段用于传输私有数据。改造步骤是先在前面的asdu_types.h里加枚举值定义再在asdu.c的编解码函数里补一个分支解析私有信息结构体。需要留意的是新类型也要在类型名称映射表里登记否则日志和调试工具打印出来的是 UNKNOWN排查问题时全靠猜。case 100: // 私有类型报文头带设备状态字 parsePrivateType(asdu, obj); break;6.2 数据通道替换从回调打印到消息队列生产环境里数据要进时序数据库或者消息中间件直接把回调函数替换成队列生产者的实现即可。常见做法是在回调里组装一个结构体推入队列由独立的消费者线程负责批量写入。需要保证的是回调的投递操作不能阻塞队列满了就丢弃并计数等链路空闲时靠周期总召唤补偿数据空缺。6.3 性能验证与参数调优跑通之后建议压一轮性能。用并发脚本模拟多主站同时连接观察库的 CPU 占用和数据吞吐。经验上单核处理 2000 点规模的百毫秒级周期上送没有压力瓶颈基本出现在业务回调而不是协议栈本体。调优时优先调两处TCP 收发缓冲区改大一个数量级链路层确认窗口从默认的 12 调到 20前提是从站端参数同步调整否则两端的窗口不匹配反而引发重传风暴。我每次把协议栈接入新项目都会强制走一遍完整的报文抓取对比流程把每个 ASDU 的 TypeID、COT、COA、IOA 逐一记录与装置侧导出的点表比对全部核对无误后才开始写业务代码。这个习惯帮我躲过了数不清的现场调试环节。希望这份文档能帮你在 IEC 60870 这条路上少踩几个坑。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑