简介:一套面向电力系统自动化开发的IEC 60870协议库完整源码包,对应lib60870-2.2.0版本,专为需要集成IEC 60870-5-101/104等通信服务的C/C++开发者设计,可用于远程终端单元、变电站自动化等场景的客户端与服务端实现。压缩包共121个文件,大小约291KB,以41个C源码和34个头文件为核心,配合Makefile等构建脚本、示例程序、README等文档及测试用例,结构清晰便于二次开发与编译调试。目前已有469人学习参考,适合具备基础网络编程能力、希望深入理解电力规约底层实现的开发者。通过阅读源码可掌握ASDU解析、链路层处理、连接管理等关键机制,同时借助自带示例快速搭建协议通信模块,显著降低从零实现的工作量与出错风险,并保障与标准设备的互操作性。
1. lib60870 解决的,是 IEC 60870 协议栈里最磨人的那一段
做变电站后台、配电终端或者发电厂远动接入的人,迟早会撞到同一件事:装置侧的测点要通过 IEC 60870-5-101/104 送上来,而协议细节比想象中多得多——ASDU 类型、传送原因、公共地址、信息对象地址、启动帧和序号确认,任何一个对不上,后台就是一片灰。lib60870 这个 C 语言实现的开源协议栈,把 101 的串口链路和 104 的 TCP 传输都封装成了可调用的 C API,省掉的是从零拼报文、维护状态机、处理重传确认这些脏活。2.2.0 这个版本在电力自动化集成项目里很常见,适合三类人:做 SCADA 接入的工程师、写装置模拟器的测试开发、以及要理解 104 连接细节的运维。这篇我就顺着 lib60870 的代码组织方式,讲清楚从编译到调参、再到抓包排错的完整路径。
2. 先看懂 IEC 60870-5-101/104 在 lib60870 里的对应关系
2.1 101 走串口、104 走 TCP,服务端和客户端是同一组 ASDU
IEC 60870-5-101 和 104 的差别,很多人以为是「串口 vs 网口」这么简单,其实真正的差别在传输层和链路层的处理方式。101 面向串口链路,有平衡式和非平衡式两种传输模式,非平衡式下从站只能被动应答,主站轮询时从站才能上送数据。104 直接跑在 TCP 之上,用 APCI(应用协议控制信息)做启动、停止、测试和序号确认,从站可以主动上送,也支持总召唤、时钟同步、遥控、遥调这些通用服务。
lib60870 的代码树把这两套协议分开实现,但 ASDU 这一层是共用的。也就是说,你在 101 里组的一条遥测 ASDU,和 104 里组的一条遥测 ASDU,信息对象的结构完全一致,差异只在链路层的封装方式。实际项目中,很多厂站是 104 进后台、101 出调度,两套协议处理的是同一批测点。写代码时只需要关心 ASDU 内容,传输方式交给 CS101 或 CS104 的 API 去处理,这也是 lib60870 比很多自研协议栈省事的地方。
2.2 lib60870 的模块拆分:CS101、CS104 和公共的 ASDU 层
先说代码目录的结构。拿到 2.2.0 源码后,核心代码在iec60870目录下,里面有cs101、cs104、cs101_asdu、apci这些模块。CS101 模块管串口链路层,CS104 模块管 TCP 连接和 APCI 状态机,而cs101_asdu是所有信息对象、ASDU 编解码的公共层。头文件按功能拆得很细,iec60870/cs104_connection.h是 104 客户端连接、iec60870/cs104_server.h是 104 服务端、iec60870/cs101_asdu.h是 ASDU 通用接口。
| 模块 | 职责 | 对应场景 |
|---|---|---|
| CS101_Master / CS101_Slave | 串口链路层的主从站 | 101 非平衡/平衡式通信 |
| CS104_Connection | TCP 客户端连接 | 主站主动连从站 |
| CS104_Server | TCP 服务端 | 从站等待主站连接 |
| CS101_ASDU | ASDU 编解码、信息对象操作 | 两套协议共用 |
实际开发时,最常见的是后面三个。比如做后台主站,就用 CS104_Connection 去连接各个装置;做装置模拟器,就用 CS104_Server 监听 2404 端口;101 串口场景使用 CS101_Master。判断一个项目该用哪个模块,先问一句:对端是主动连我还是等我连它。连不上、收不到数据的时候,多半是这一层选错了。
2.3 一个信息对象在内存里长什么样
ASDU 由类型标识、传送原因、公共地址和若干个信息对象组成。比如一条单点遥信,类型标识是 M_SP_NA_1(值 1),信息对象里带一个对象地址和一个布尔值。lib60870 把信息对象封装成不透明指针InformationObject,用的时候通过类型判断后转成具体结构体。看代码时记住这个模式就够了:拿元素、判类型、转结构体、取字段。
InformationObject io = CS101_ASDU_GetElement(asdu, i); if (InformationObject_GetType(io) == M_SP_NA_1) { SinglePointInformation spi = (SinglePointInformation) io; bool value = SinglePointInformation_GetValue(spi); int ioa = InformationObject_GetObjectAddress(io); }这段代码里,CS101_ASDU_GetElement按索引取出信息对象,InformationObject_GetType判断类型标识,类型匹配后才能安全转换成SinglePointInformation。SinglePointInformation_GetValue返回遥信值,InformationObject_GetObjectAddress取对象地址。这里最容易犯的错是拿到一个浮点遥测(M_ME_NC_1)却按单点遥信去解析,库不会帮你拦,取出来的值完全是垃圾数据。所以判类型那一步不能省。
2.4 数据从对端到回调函数的路径
104 的数据到达本机后,要经过 TCP 缓冲、APCI 帧解析、ASDU 校验,最后才进到你注册的回调函数里。这条路径上,lib60870 已经把序号校验、重复帧、格式错误都处理完了。你只需要做一件事:告诉库,收到 ASDU 之后调用哪个函数。
static void asduReceivedHandler(void* parameter, CS104_Connection con, CS101_ASDU asdu); CS104_Connection_SetASDUReceivedHandler(con, asduReceivedHandler, NULL);回调函数的三个参数分别是自定义上下文、连接对象和解析后的 ASDU。SetASDUReceivedHandler的第三个参数会原样传回回调的第一个参数,一般用来传设备指针或配置结构体,省得用全局变量。我在实际项目里习惯把整个设备上下文塞进去,这样回调里拿到 ASDU 后可以直接定位到这台装置对应的数据表。值得留意的是,回调运行在库的接收线程里,不要在回调里做阻塞操作,否则后续帧的处理会被拖住。
3. 用 CMake 编译 lib60870-2.2.0,再从官方 example 跑通一次 104 通信
3.1 拿到源码之后先做静态库
lib60870 用 CMake 构建,整个编译过程在 Linux 上很简单。从 MZ Automation 的 GitHub 仓库拿到 2.2.0 源码后,进入根目录执行:
mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j$(nproc)CMake 配置完成后,默认会生成静态库和示例程序。-DCMAKE_BUILD_TYPE=Release开启编译优化,调试阶段可以换成 Debug,方便跟代码。-j$(nproc)用满 CPU 核心数加快编译。构建完成后,静态库通常在build/src/下,头文件在lib60870目录里保持原结构。自己项目引用时,不需要把源码整个拷进去,只需要链接静态库并包含lib60870这个头文件目录。
如果编译过程中报缺少依赖,先检查系统有没有装 cmake 和 gcc。这个库不依赖第三方库,纯标准 C,出问题的概率很低。真正需要注意的是交叉编译场景:嵌入式设备上跑 104 从站时,要改用交叉编译工具链,CMake 里通过-DCMAKE_C_COMPILER指定。
3.2 官方 example 里 server 和 client 是怎么对上话的
源码的examples目录下面有现成的 104 服务端和客户端示例。先启动服务端,再启动客户端,就能看到一次完整的 104 通信过程。服务端示例启动后会监听默认的 2404 端口,客户端连接成功后,双方先交换启动帧,然后客户端会发起总召唤,服务端返回一组模拟的遥测和遥信数据。
跑这个示例时,终端输出里应该能看到连接成功、总召唤完成之类的日志。如果客户端打印了数据但服务端没有任何反应,先确认防火墙有没有放行 2404 端口。我自己调试时习惯在服务端机器上先ss -lntp | grep 2404确认端口在监听。这个 example 是很好的参考模板:服务端的回调函数写法、客户端的参数设置、总召唤怎么发起,都能直接抄。
3.3 写一个 50 行的最小主站
官方 example 代码量偏大,我们写一个最精简的主站,把连接、启动、总召唤、收数据这几步走完。以 lib60870 2.2.0 的 API 为准,代码大致如下:
#include <stdio.h> #include "iec60870/cs104_connection.h" static void asduReceivedHandler(void* parameter, CS104_Connection con, CS101_ASDU asdu) { int type = CS101_ASDU_GetTypeID(asdu); int cot = CS101_ASDU_GetCOT(asdu); printf("ASDU type=%d COT=%d elements=%d\n", type, cot, CS101_ASDU_GetNumberOfElements(asdu)); int i; for (i = 0; i < CS101_ASDU_GetNumberOfElements(asdu); i++) { InformationObject io = CS101_ASDU_GetElement(asdu, i); if (type == M_ME_NC_1) { float v = MeasuredValueShort_GetValue((MeasuredValueShort) io); printf(" IOA=%d value=%f\n", InformationObject_GetObjectAddress(io), v); } } } int main(void) { CS104_Connection con = CS104_Connection_Create("127.0.0.1"); CS104_Connection_SetASDUReceivedHandler(con, asduReceivedHandler, NULL); if (CS104_Connection_Connect(con)) { printf("connected\n"); CS104_Connection_SendStartDT(con); CS104_Connection_SendInterrogationCommand(con, CS101_COT_ACTIVATION, 1, 20); while (1) { Thread_sleep(1000); } } else { printf("connect failed\n"); } CS104_Connection_Destroy(con); return 0; }流程拆开看:CS104_Connection_Create传入对端 IP,端口默认 2404,如果对端不是默认端口,需要看当前版本的参数接口去改。SetASDUReceivedHandler注册接收回调,Connect建立 TCP 连接。连接成功之后,先发SendStartDT激活传输,这一步不能省,104 规定 TCP 建立后必须经过 STARTDT 激活才能传数据。然后发总召唤,第三个参数1是公共地址,第四个参数20是总召唤限定词 QOI,这不是信息对象地址,别填成 0。回调里按类型标识区分数据类型,这里只处理了浮点遥测M_ME_NC_1。
4. CS104 连接参数怎么调:k/w 窗口、t0-t3 超时与 ASDU 地址
4.1 五个超时参数:连接建立、发送确认与保活测试
lib60870 的 CS104 连接参数存在CS104_APCIParameters结构体里,五个超时参数分别管不同阶段,调错一个就可能在特定网络环境下断链。我把它们列成一张表,方便对着改:
| 参数 | 默认值 | 作用 | 设置建议 |
|---|---|---|---|
| t0 | 10s | TCP 连接建立超时 | 本机/局域网设 5s,跨公网可放宽到 30s |
| t1 | 15s | 发送后等待确认的超时 | 按链路往返时间估算,取 1.2 倍 |
| t2 | 10s | 接收方延迟确认的最长时间 | 必须小于 t1 |
| t3 | 20s | 空闲时发送测试帧的周期 | 大于 t1,一般 15-30s |
| k/w | 12/8 | 未确认 I 帧窗口 | 见 4.2,不要随意改大 |
t2 和 t1 的关系是 104 协议里最容易踩的坑。t2 是接收方的确认延迟上限,t1 是发送方等待确认的上限,如果 t2 大于等于 t1,接收方还没发确认,发送方就已经判定超时了,连接会反复中断。默认值 10 和 15 留了余量,自己调的时候记住:t2 一定要比 t1 小一个量级。t3 是保活测试帧的周期,链路空闲超过 t3 就会发 TESTFR 帧确认对端还活着,收不到响应就按 t1 超时断开。
4.2 k 和 w 不是随便填的窗口
k 和 w 控制的是未确认 I 帧的数量。发送方最多连续发 k 个 I 帧而不等确认,接收方累计收到 w 个 I 帧后必须回一个 S 帧确认。这两个值直接影响吞吐量和链路稳定性,调度主站和装置通信时,默认的 12/8 已经够用,改成 240 这种大窗口反而会在丢包时造成大量重传。
CS104_APCIParameters params = CS104_APCIParameters_create(); params->k = 12; params->w = 8; params->t0 = 10; params->t1 = 15; params->t2 = 10; params->t3 = 20; CS104_Connection_SetAPCIParameters(con, params);这段代码展示了创建参数结构体、逐个赋值、应用参数的过程。参数结构体在连接创建之后、连接建立之前设置才有效。w必须小于k,一般取 k 的一半左右,写反了会导致对端迟迟收不到确认,链路一忙就窗口打满。如果你在抓包里看到对端连发 8 帧后戛然而止,多半就是本地的 w 参数和被确认帧数不匹配。
4.3 ASDU 地址和 IOA 地址,设错一个就静默丢弃
参数里还有一类更隐蔽的配置:ASDU 公共地址和 IOA 信息对象地址。104 报文里,公共地址用于区分不同的从站设备,IOA 用于区分设备内部的不同测点。主站发总召唤时指定公共地址,从站只响应匹配自己地址的报文。如果你设的公共地址和装置里配的不一致,TCP 连接正常、启动帧也正常,但总召唤就是没有数据返回。
这类问题在回调函数里看得很清楚:CS101_ASDU_GetCA取到的公共地址和装置配置比对一下就知道。很多新手在回调里看到数据就开心了,忘记校验公共地址,结果把 A 装置的数据写进了 B 装置的测点表。lib60870 不会帮你过滤不匹配的公共地址,校验逻辑要自己写在回调里。
4.4 参数配置的完整顺序
实际项目里,参数设置有个固定顺序:创建连接、设置参数、设置回调、连接、启动激活、发总召唤。顺序错了会出现各种奇怪现象,比如先 Connect 再 SetAPCIParameters,参数不会生效,因为连接已经建立了。
一个常见误配案例是把 t3 设成 3 秒。链路空闲时 3 秒就发一次测试帧,如果对端实现不完整,很可能直接断开。还有一个容易忽略的点:共享同一个 CS104_Connection 时,多个线程同时发命令要加锁,库内部不保证并发安全。最后,每次修改参数后,不要只靠日志判断,用抓包软件看实际报文的 k/w 窗口和测试帧间隔,这才是参数生效的直接证据。
5. 用抓包和回调快速定位 lib60870 连接故障的实操套路
5.1 抓包怎么抓、看哪几列
遇到 104 通信异常,第一件事不是看代码,是抓包。在客户端或服务端机器上执行:
tcpdump -i eth0 -s 0 -w iec104.pcap port 2404抓完导入 Wireshark,Wireshark 自带 IEC 60870-5-104 解析器,能直接识别 APCI 帧。看包的时候只关注几个点:TCP 握手是否完成、有没有 STARTDT 激活、I 帧的序号是否连续、有没有周期性的测试帧。过滤栏里可以输tcp.port==2404,然后在「分析」菜单里看 104 层的详细信息。
5.2 问题一:TCP 通着但 STARTDT 一直不激活
最典型的现象是抓包只有 TCP 握手,之后没有任何 104 数据。原因十有八九是客户端没发 STARTDT_ACT,或者服务端没回 STARTDT_CON。检查客户端的代码里有没有调用SendStartDT,再看服务端有没有正确启动并进入接收状态。还有一种情况是服务端连接数达到上限,新连接被拒绝。
5.3 问题二:收到数据后没有回确认,w 窗口积压
如果抓包看到对端连发多个 I 帧后突然停顿,检查自己的确认帧。w 窗口打满后,对端会停止发送等待 S 帧。这类问题在回调里做耗时操作时尤其高发——接收线程被卡住,确认帧来不及发。解决办法是把数据拷贝出来,放到独立线程处理,回调里只做轻量工作。
5.4 问题三:把序号和测试帧逻辑搞混
104 的 I 帧序号是 15 位,从 0 到 32767 循环。排查时看到序号跳到 0,不一定是错误,可能是绕回了。另外,区分 TESTFR_ACT 和 TESTFR_CON:主动方发 ACT,对端回 CON,只看到 ACT 没有 CON,说明对端没响应保活,考虑 t3 设置是否合理或者两侧参数不一致。
我一般会在回调函数的人口加一条日志,把类型标识和传送原因打出来,再和抓包结果对照。如果回调没触发但抓包有数据,问题就在库的解析层;如果回调触发了但数据不对,问题就在你自己的类型判断上。把这条对照规则写进排错流程,比对着代码猜快得多。
本文还有配套的精品资源,点击获取