ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

lib60870协议栈实战:IEC 60870-5-104通信从编译到排错

lib60870协议栈实战:IEC 60870-5-104通信从编译到排错 简介一套面向电力系统自动化开发的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服务端和客户端是同一组 ASDUIEC 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_ConnectionTCP 客户端连接主站主动连从站CS104_ServerTCP 服务端从站等待主站连接CS101_ASDUASDU 编解码、信息对象操作两套协议共用实际开发时最常见的是后面三个。比如做后台主站就用 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_TYPERelease .. make -j$(nproc)CMake 配置完成后默认会生成静态库和示例程序。-DCMAKE_BUILD_TYPERelease开启编译优化调试阶段可以换成 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结构体里五个超时参数分别管不同阶段调错一个就可能在特定网络环境下断链。我把它们列成一张表方便对着改参数默认值作用设置建议t010sTCP 连接建立超时本机/局域网设 5s跨公网可放宽到 30st115s发送后等待确认的超时按链路往返时间估算取 1.2 倍t210s接收方延迟确认的最长时间必须小于 t1t320s空闲时发送测试帧的周期大于 t1一般 15-30sk/w12/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抓完导入 WiresharkWireshark 自带 IEC 60870-5-104 解析器能直接识别 APCI 帧。看包的时候只关注几个点TCP 握手是否完成、有没有 STARTDT 激活、I 帧的序号是否连续、有没有周期性的测试帧。过滤栏里可以输tcp.port2404然后在「分析」菜单里看 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 设置是否合理或者两侧参数不一致。我一般会在回调函数的人口加一条日志把类型标识和传送原因打出来再和抓包结果对照。如果回调没触发但抓包有数据问题就在库的解析层如果回调触发了但数据不对问题就在你自己的类型判断上。把这条对照规则写进排错流程比对着代码猜快得多。本文还有配套的精品资源点击获取
返回列表