简介:lib60870-2.0.1是IEC60870标准的开源C语言实现库,面向电力自动化、SCADA及智能电网通信开发者,可直接集成101、102、104协议,支持主站/子站双向通信,省去从零实现复杂协议栈的代价。资源包共114个文件,压缩后仅186KB,以C源文件(39个)和头文件(34个)为核心实现,另含makefile构建脚本、adoc用户指南、readme说明及测试用txt等,便于编译、阅读与调试。已有2489人学习下载,适合需要快速接入IEC60870协议或深入理解协议报文编码细节的工程师。通过阅读源码与示例,可掌握CS101/CS104链路层、ASDU编解码、连接管理等核心机制,并利用提供的测试代码与构建配置快速搭建主站或子站应用,降低协议集成门槛。
1. IEC60870到底是什么?为什么电力自动化离不开它
1.1 一个协议家族,不止是104
做电力自动化、SCADA、变电站监控或者分布式光伏接入的朋友,对IEC60870这套协议应该都不陌生。它不是一个单独的标准,而是一整个协议家族,日常接触最多的是IEC 60870-5-101和IEC 60870-5-104。简单说,101走串口,常见于RS232/RS485链路,适合带宽小、实时性要求高的本地通信;104走TCP/IP网络,把101的应用数据搬到以太网上,适合主站与子站之间的远动通信,端口固定是2404。
这套协议在国内电力行业几乎是无处不在。调度自动化、配网自动化、变电站综合自动化、新能源电站远动信息上传,底层基本都是104或101。它定义的模型很清晰,信息体地址(IOA)、公共地址、类型标识,一层层把遥信、遥测、遥控、遥调数据组织起来,主站和从站之间只要按这个格式交互,设备厂商不同也能顺利对接。
如果你接触过电力项目,大概率遇到过跟不同厂家联调时的"糟心事"——协议不一致、点位表理解偏差、规约实现有差异。这时候如果能有一个稳定可靠的开源实现库做参照,甚至直接作为项目基础代码,能省掉大量重复开发和联调时间。这也是我今天想聊的重点:IEC60870的开源实现库,到底怎么选、怎么用、有哪些坑。
1.2 为什么说"开源实现库"是刚需
有人可能会问,IEC104协议本身并不复杂,自己照着标准写一套不行吗?说实话,能写,但没必要。协议的核心难点不在正常流程,而在各种边界情况和对端设备的非标实现。举个例子,同一个遥信变位报文,有的厂家用M_SP_NA_1(类型标识1,单点遥信),有的厂家用M_SP_TB_1(带时标),如果你只实现了前者,联调时就会遇到"数据能上送但时间戳对不上"的问题。这类细节标准文档里都有,但实际踩一遍坑的成本很高。
更关键的是,IEC60870涉及电厂、变电站这类对稳定性要求极高的场景,代码里一个时序问题就可能造成误遥信或遥控失败。开源实现库的好处在于,核心逻辑经过了大量项目验证,社区里各种边界问题基本都被踩过了,你拿到手可以直接用,遇到问题还能翻issue、看commit记录,比自己从零开始写要靠谱得多。
2. 开源库选型:lib60870、j60870,怎么选
2.1 主流开源库横向对比
目前电力圈子里用得多、口碑也比较稳定的IEC60870开源实现库,主要有这么几个:
| 库名称 | 语言 | 协议支持 | 授权方式 | 适用场景 |
|---|---|---|---|---|
| lib60870 | C/C++ | 101/104 | GPLv3/商用授权 | 嵌入式终端、从站、高性能主站 |
| j60870 | Java | 104 | GPLv2/商用授权 | Java系SCADA主站、后台服务 |
| openmuc | Java | 101/104 | Apache 2.0 | 科研、教育、内部工具 |
| node-red-contrib-iec60870 | JavaScript | 104 | MIT | 快速原型、轻量接入 |
| qinghuaIEC104(各厂自研) | C/C++/Java | 104 | 不定 | 企业内自用 |
如果你的项目是嵌入式设备,比如用STM32、ARM9跑一个采集终端,那基本绕不开lib60870,因为它是C语言的,资源占用可控,交叉编译也方便。如果是在服务端做数据汇聚,主站后台、规约转换器这类,Java系的j60870或者openmuc更顺手,开发效率高,接数据库、接消息中间件都方便。
我自己的习惯是:终端侧一律用lib60870,后台服务优先找Apache许可证的库,避免授权风险。openmuc的Apache 2.0对商业项目很友好,但因为某些原因它在国内信息更新比较慢,用之前需要确认你需要的功能是否都覆盖了。
2.2 许可证与商用合规提醒
这里必须多说一句,因为很多人容易在这里栽跟头。lib60870本体是GPLv3授权的,也就是说,如果你的项目对外分发,并且用了lib60870的代码,那么你的整个项目就有义务开源。对很多做产品、做方案的团队来说,这可能是致命的。MZ Automation官方提供商业授权,价格需要邮件咨询,但如果你是做非商用项目、学习研究、或者在甲方内部系统使用而不对外分发,那GPLv3的约束就没有那么敏感。
还有个容易忽略的点:lib60870有多个分支和fork,GitHub上搜IEC60870会出来一大堆,有些是老版本,有些是社区魔改版,选的时候要认准MZ Automation官方仓库,或者看star数和最近commit时间,别迷迷糊糊拿了个三年前的fork当主线,跑到最后发现协议栈有bug还找不到人修。
3. 手把手:基于lib60870把104从站跑起来
3.1 编译准备与工程结构
先说说环境。我用的是Ubuntu 20.04/22.04,交叉编译到ARM平台也做过,流程基本一致。先把代码拉下来:
git clone https://github.com/mz-automation/lib60870.git cd lib60870lib60870的工程组织比较清晰,核心源码在lib60870-C目录下,包含src和inc两个子目录。编译有两种方式:一是直接用CMake构建整个库,二是把源码直接加进你的工程一起编。嵌入式场景我更推荐后者,因为裁剪灵活,编译选项一目了然。
CMake方式:
mkdir build && cd build cmake .. make编译完成后会生成静态库和一堆示例程序。看示例代码是快速上手的最好方式,比如examples/cs104_server_example.c就是最典型的104从站程序,examples/cs104_client_example.c是主站程序。这两个例子覆盖了90%的基础场景,直接改改就能用。
3.2 从站核心代码与参数解析
下面直接看从站怎么实现。我简化了官方示例,留下最核心的部分:
#include "cs104_slave.h" static void connectionHandler(CS104_Connection connection, CS104_PeerConnectionState state, void *parameter) { if (state == CS104_PEER_CONNECTION_STATE_OPENED) { printf("连接建立,对端地址:%s\n", CS104_Connection_getPeerAddress(connection)); } } int main(void) { CS104_Slave slave = CS104_Slave_create(100, 10); // 参数1: 最大同时连接数, 参数2: 启动事件队列大小 /* 设置公共地址,这里用1,必须与主站配置一致 */ CS104_Slave_setCommonAddress(slave, 1); /* 设置连接事件回调 */ CS104_Slave_setConnectionHandler(slave, connectionHandler, NULL); /* 启动从站服务,监听0.0.0.0:2404 */ CS104_Slave_start(slave); /* 模拟循环:定期刷新遥测值 */ while (1) { float temperature = 25.6; CS104_Slave_setFloatValue(slave, CS104_InformationObject_create(4001, M_ME_NC_1), temperature); Thread_sleep(1000); } CS104_Slave_destroy(slave); return 0; }这段代码看着简单,关键参数我展开说一下:
CS104_Slave_create(100, 10):第一个参数是允许的最大客户端连接数。实际调度系统里经常会有主备双主站,甚至地调、县调同时接入,所以这个值不能设得太小。第二个参数是启动事件(总召、单点命令等)的队列长度,一般10~20就够。setCommonAddress:这就是前面说的公共地址。在配置点表时,主站端填写的公共地址必须和这里一致,否则整个ASDU会被直接丢弃,表现出来就是"连接正常但一条数据都没有"。- 信息体地址4001:这是点表规划的关键。国内电力系统约定俗成的做法是遥测从4001开始编号,遥信从1开始,遥控从6001开始,不同区域会有差别,但核心原则是主站与从站必须完全对应。
从站的信息体读取还有个常见写法,就是通过回调函数按需返回数据,而不是像我上面那样在主循环里定时刷新。回调方式适合点位数量大、数据动态变化的场景:
static bool readSinglePointHandler(void *parameter, int address, bool *value) { if (address == 1) { *value = getBreakerStatus(); /* 从你自己的数据源拿开关状态 */ return true; } return false; } CS104_Slave_setReadSinglePointHandler(slave, readSinglePointHandler, NULL);这种方式的好处是,主站发起总召或者单个对象查询时,库会自动回调你的处理函数,不需要自己管理定时上报逻辑,也避免在主循环里反复遍历所有点位。
3.3 主站连接与调试
写完从站,还得有个主站来验证。官方示例的客户端代码还可以再精简:
#include "cs104_connection.h" static void connectionHandler(void *parameter, CS104_Connection connection, CS104_ConnectionEvent event) { switch (event) { case CS104_CONNECTION_EVENT_CONNECTED: printf("已连接到从站\n"); break; case CS104_CONNECTION_EVENT_STARTDT_CONFIRM: printf("STARTDT确认,可以传输数据\n"); break; default: break; } } int main(void) { CS104_Connection con = CS104_Connection_create("192.168.1.100", 2404); CS104_Connection_setConnectionHandler(con, connectionHandler, NULL); /* 建立TCP连接并启动数据传输模式 */ CS104_Connection_connect(con); CS104_Connection_sendStartDT(con); /* 发送总召命令,类型标识100 (C_IC_NA_1) */ CS104_Connection_sendInterrogationCommand(con, CS101_COT_ACTIVATION, 1, 0); /* 让程序跑一会儿,观察回调 */ Thread_sleep(5000); CS104_Connection_destroy(con); return 0; }主站连接以后要主动sendStartDT,这是因为IEC104有一个启动握手流程:客户端发STARTDT激活命令,服务端确认后双方才能开始传输应用数据。我见过不少刚开始用这个库的人,TCP连接是通了,但忘了发STARTDT,结果一条数据也收不到。
主站端接收遥测数据,需要注册ASDU接收回调:
static void asduReceivedHandler(void *parameter, CS104_Connection connection, CS101_ASDU asdu) { if (CS101_ASDU_getTypeID(asdu) == M_ME_NC_1) { InformationObject io = CS101_ASDU_getElement(asdu, 0); float val = CS101_InformationObject_getFloatValue(io); int ioa = CS101_InformationObject_getObjectAddress(io); printf("遥测: IOA=%d, 值=%f\n", ioa, val); } CS101_ASDU_destroy(asdu); }注意,接收回调里的ASDU需要自己调用CS101_ASDU_destroy释放,否则长时间跑必然内存泄漏。这个在官方文档里有写,但很多人会漏。
4. 现场实战:常见故障与排错清单
4.1 连接层问题
用这套开源库做了不少项目,现场调试时遇到最多的问题集中在连接建立阶段。
第一个常见现象是TCP能通,但连接建了又断、反复重连。这种情况八成是t0、t1、t2、t3超时参数不匹配。IEC104定义了四个关键超时:t0是建立TCP连接的超时,默认10秒;t1是发送或测试APDU后的确认超时,默认15秒;t2是接收APDU后的确认延迟,默认10秒;t3是链路空闲时的测试帧周期,默认20秒。如果对端设备设的t3特别短,而你的库默认t3比较长,就会触发对端主动断开。
解决办法是在创建连接或从站后显式设置这些参数:
CS104_Connection_setParameter(con, CS104_PARAM_TIMEOUT_T0, 10000); CS104_Connection_setParameter(con, CS104_PARAM_TIMEOUT_T1, 15000); CS104_Connection_setParameter(con, CS104_PARAM_TIMEOUT_T2, 10000); CS104_Connection_setParameter(con, CS104_PARAM_TIMEOUT_T3, 20000);第二个常见问题是端口填错。IEC104固定是2404,有的设备会改端口,但很多调试人员习惯性地用101协议里的串口参数思维,忘了检查TCP端口,抓包一看SYN发出去,对端根本不回应,最后发现是对端监听端口不是2404。
4.2 应用层与数据问题
连接正常、但数据不对,这种情况更让人头疼。优先级最高的排查手段是Wireshark。IEC104的dissector是内置的,抓包以后能看到APCI和ASDU的详细字段,比看代码日志高效得多。
举几个典型的例子:
- 公共地址不一致:现象是所有报文都有响应,但从站就是不回数据。抓包可以看到主站发送的总召请求里带的是公共地址1,而从站配置的公共地址是2,此时从站直接丢弃该ASDU,不做任何应答。
- 信息体地址错位:比如从站IOA是从1开始排的遥信,主站配置的遥信表从0或者从1001开始,就会导致数据串位。这种问题靠抓包特别容易定位,对照点表一条条看IOA即可。
- 类型标识不匹配:主站请求总召所有遥信,从站回的是带时标的版本或者数据类型不对,主站解析不出来。多见于只实现了部分类型标识的简化版设备。
另外有个非常容易踩的坑:浮点数的传输格式。IEC104里的浮点遥测默认用的是32位IEEE754小端序,但有些保护装置、测控装置出自不同的历史背景,会用短浮点或者定点数传输。如果你发现遥测值差好几个数量级、或者小数点位置不对,优先怀疑这个,别急着改业务代码。
4.3 项目收尾建议
最后分享几个我做项目时养成的习惯,都是折腾出来的经验:
第一,不管时间多紧,先把点表明确再动代码。IEC104的联调几乎全是点表问题,点位编号、类型、单位、死区、变化上送条件,这些必须跟对端负责人一条条核对清楚。点表做扎实了,联调能快一半。
第二,保留抓包习惯。跑通以后不要立刻关掉Wireshark,保存一份完整交互记录,至少包含启动握手、总召、正常上送、遥控命令这几类报文。后面出了问题,这份抓包文件就是最有力的证据。
第三,给自己的实现库打个内部版本号,记录修改过哪些源码。开源库虽说稳定,但难免要根据项目微调,比如修改默认最大ASDU长度、调整缓冲区大小。如果你不记录版本变更,三个月后出了bug,你可能完全想不起来改过哪里。
第四,遥控和遥调命令一定要加确认机制。使用开源库时,命令发送的API调用本身很快,但现场设备是否真正执行,必须通过COI(传输原因)和执行结果的回执来判断。代码里不要只发命令不监听确认,否则事故回放时你会非常被动。
我的经验是,IEC60870的开源实现库最大的价值不是省掉那几千行代码,而是把一个经过大量现场验证的协议状态机交到你手里,让你可以集中精力去处理业务逻辑和对接问题。但协议栈只是基础,真正决定项目成败的,还是点表规划、异常处理和现场联调的细致程度。
本文还有配套的精品资源,点击获取