ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

lib60870实现IEC 60870-5-104主从站:从ASDU到参数调优实战指南

lib60870实现IEC 60870-5-104主从站:从ASDU到参数调优实战指南 简介面向电力自动化与智能电网开发者的IEC60870-5-101/104通信协议库实现内置ASDU编解码、链路层、连接管理等核心模块并附带用户指南与测试代码可支撑远程测控、变电站自动化、智能电网设备互联等场景。它既适用于串行链路的101规约也支持基于TCP/IP网络的104规约可帮助开发者快速构建符合IEC60870-5系列标准的通信程序。压缩包共157个文件约1.66MB以42个C源码和38个头文件为主组成协议实现核心11个makefile与11个sample示例便于工程编译和功能演示14个txt说明、证书及文档则提供配置参考与使用指引整体结构清晰。目前已有722人学习适合需要深入理解IEC60870-5协议或进行电力通信系统二次开发的工程师。从内容预览可见资源包含CS101信息对象、CS104从站、链路层等关键实现配合unity测试框架与用户指南能帮助读者掌握协议细节并验证功能缩短项目开发周期。1. 凌晨两点半的 2404 端口我为什么盯上 lib60870SCADA 上那路 10 kV 线电压红了半小时值班长问是不是 RTU 死了。我盯着 Wireshark2404 端口上 STARTDT act 之后只剩 U 帧 TESTFR 在来回——主站根本没发总召问题不在 RTU在链路建立之后那一步。这几帧对应的就是 IEC 60870-5-104。这串标题像搜索词被粗暴拼接但它指向一个真实场景用 C 语言写的 lib60870 实现 101/104 协议栈对接变电站、储能、光伏里的 RTU 和集中器。101 与 104 共享 ASDU差别在传输层和链路层的封帧方式。适合自己写主站接 SCADA 的工程师也适合把 104 从站塞进嵌入式网关的开发者。下文从 ASDU 结构讲到 CMake 编译、主从站代码和排错全落在 I 帧、S 帧、k/w、t1t3 这些需要动手调的参数上。2. 先分清 101 和 104ASDU 是皮APCI 是肉lib60870 两个都做2.1 一条遥测数据从 RTU 到 SCADA 的完整路径RTU 把电压互感器输出的 05 V 信号做标度变换后得到一个带地址的测量值。这个地址叫信息对象地址 IOA值本身加上品质描述有效、溢出、非拓扑等构成一个信息对象 Information Object。再往上一层若干信息对象被打成一个应用服务数据单元 ASDU也就是我们在 104 报文的载荷里看到的东西。ASDU 的结构分两段数据单元标识包含类型标识 TI、可变结构限定词 VSQ、传送原因 COT、公共地址 COA数据部分是一个或多个信息对象每个信息对象由 IOA 和数据字段组成。TI 决定这一帧到底是遥信、遥测、遥控还是时钟同步COT 决定它是突发上送、激活命令还是对命令的确认。这套结构在 IEC 60870-5 系列里属于 5-3 和 5-4 定义的范畴101 和 104 完全共用。IEC 60870-5 系列里还有一个常被忽略的 -5-5它定义总召、时钟同步、链路测试这类基本应用功能。也就是说你在 104 里发总召、对时行为规则来自 -5-5而 101/104 只是承载它们的不同运输工具。lib60870 把 ASDU 编解码和这些应用功能做成了公共部分这才是它同时支持 101 和 104 而不需要写两套业务代码的根本原因。2.2 104 的 APCI 和 k/w、t0t3 定时器这是现场调包的入口101 跑在串口或 TCP 上时用 FT1.2 链路帧带链路地址、帧计数位和校验104 直接把 ASDU 装进 APDU每个 APDU 前面加 6 字节 APCI。APCI 前 4 字节是控制域I 帧带发送序号和接收序号S 帧只带接收序号做确认U 帧没有序号专门做 STARTDT、STOPDT、TESTFR 这三个控制动作。104 之所以需要 k/w 和一组定时器是因为它不靠物理链路判断对方死活靠的是一套窗口确认机制。参数表如下参数lib60870 默认值作用现场调整方向k12发送端未确认 I 帧的最大数量大吞吐但低延时链路可调大上限受序号回绕限制w8接收端收到该数量的 I 帧后必须发 S 帧必须小于 k通常取 k/2k-1t010000 msTCP 连接建立超时跨公网、4G 时调大到 30 st115000 msI/S/U 帧的响应超时超时判链路断开慢链路、卫星链路调大到 30 s 以上t210000 ms接收多帧后延迟确认的时间必须小于 t3决定了 S 帧多久发出一次t320000 ms链路空闲时发送 TESTFR 的周期NAT 环境过短会被误杀过长则链路失效检测变慢k 和 w 反映的是滑动窗口I 帧序号是 032767 循环的所以 k 的上限也就被限制在 32767 以内标准里几乎没有厂家会用到那么大的窗口。t0t3 里最容易踩的坑是 t2 和 t3 的关系t2 大于 t3 时发送端还没等到 S 帧确认就开始发 TESTFR两边误判对方掉线。调参时先固定 t2 t3再去动 t1。2.3 lib60870 的分层什么代码是 101/104 共用的lib60870 按 hal、编解码、连接对象三层组织。hal 层封装线程、时间、套接字这让同一份源码能编译进 Linux x86 网关、ARM 嵌入式板卡和 Windows 主机编解码层处理 ASDU、信息对象和 APCI 的序列化连接对象层则是主站和从站的 API。它的命名也沿用了这套逻辑主站侧用 CS101_Master104 的 TCP 连接只是其中一种 connect 方式从站侧 104 单独提供了 CS104_Slave专门管理多连接、绑定 2404 端口和主动上送。实际项目中我通常这样选型设备侧嵌入式采集器用 CS104_Slave因为要同时支持多个 SCADA 主站连接网关或上位机用 CS101_Master因为它同时覆盖了 101 串口和 104 网络两种通道一个主站对象可以接不同厂家、不同介质的 RTU。理解了这个分层后面看 API 就不会被 CS101、CS104 的前缀绕晕。3. 用 lib60870 把 104 从站跑起来从 CMake 到第一个 ASDU3.1 编译 lib60870 的最小命令序列lib60870 用 CMake 管理构建不依赖第三方库源码拉下来直接编即可。最小命令如下git clone https://github.com/mz-automation/lib60870.git cd lib60870 mkdir -p build cd build cmake -DCMAKE_BUILD_TYPERelease -DBUILD_SHARED_LIBSON .. make -j$(nproc) sudo make installcmake 的两个开关里CMAKE_BUILD_TYPE 决定调试符号和优化级别现场排查时我建议先用 Debug 编一遍库会把收到的每个 APDU 摘要打到 stderr这对于确认数据是否到达非常有用。BUILD_SHARED_LIBSON 会生成动态库 lib60870.so对需要热替换的网关程序更友好嵌入式环境通常改成 OFF和业务代码静态链接减小运行时依赖。编译产物会安装到 /usr/local/lib 和 /usr/local/include。如果系统里有多个版本的 lib60870建议不要急于 make install直接在 build 目录里把头文件路径和库路径写进你自己的 CMakeLists避免污染系统目录。Windows 上用 MSVC 打开 CMake 工程同样能编只是示例程序里默认绑定了本地 127.0.0.1跨机器联调时要改成对端 IP。3.2 一个能响应总召、周期上送变化的从站骨架104 从站要跑通核心就三件事监听端口、处理主站请求、主动上送数据。下面这段代码把三件事都覆盖了总召时打印日志另外用一个线程每 5 秒上送一路不断翻转的遥信点/* 最小 104 从站监听 2404处理总召周期上送遥信 */ #include stdio.h #include signal.h #include cs104_slave.h #include hal_thread.h static CS104_Slave slave; static volatile sig_atomic_t running 1; static CS104_AppLayerParameters aplParams; static void signalHandler(int sig) { running 0; } static bool connectionHandler(void* parameter, int connectionId, CS104_ConnectionEvent event) { if (event CS104_CONNECTION_OPENED) printf(连接打开: id%d\n, connectionId); else if (event CS104_CONNECTION_CLOSED) printf(连接关闭: id%d\n, connectionId); return true; } static bool asduHandler(void* parameter, IMasterConnection connection, CS104_ASDU asdu) { if (CS104_ASDU_getTypeID(asdu) C_IC_NA_1) { printf(收到总召\n); return true; /* 已处理不再交给后续回调 */ } return false; } static void* dataThread(void* arg) { int tick 0; while (running) { Thread_sleep(5000); CS104_ASDU asdu CS104_ASDU_create(aplParams, M_SP_NA_1, false, 3, 1, false, false); InformationObject io (InformationObject) SinglePointInformation_create(1, tick 1, IEC60870_QUALITY_GOOD); CS104_ASDU_addInformationObject(asdu, io); InformationObject_destroy(io); CS104_Slave_sendASDU(slave, asdu); CS104_ASDU_destroy(asdu); tick; } return NULL; } int main(int argc, char** argv) { signal(SIGINT, signalHandler); slave CS104_Slave_create(10, 100, connectionHandler, NULL); CS104_Slave_setASDUHandler(slave, asduHandler); CS104_Slave_start(slave); aplParams CS104_Slave_getApplicationLayerParameters(slave); aplParams-sizeOfIOA 3; /* 信息对象地址 3 字节和主站约定一致 */ aplParams-sizeOfCA 2; /* 公共地址 2 字节 */ aplParams-sizeOfCOT 2; /* 传送原因 2 字节 */ Thread t Thread_create(dataThread, NULL, true); Thread_start(t); while (running) Thread_sleep(100); CS104_Slave_stop(slave); return 0; }这个代码里CS104_Slave_create 的 10 表示最大允许 10 个主站并发连接100 是每个连接缓冲区最多缓存的 ASDU 数量。真正的业务逻辑集中在 asduHandler主站发来总召TI100时返回 true 表示我已经消费掉了如果返回 false库会再把这份 ASDU 转发给下一个已注册的处理函数。周期上送里的 COT3 是“突发”含义表示这条数据不是主站请求触发的而是从站主动上报主站侧通常不会对这个 COT 回确认帧只有 I 帧本身会被传输层确认。3.3 从站 API 里最容易理解错的三个参数CS104_ASDU_create 的签名里第三个参数 isSequence 决定信息对象是否用连续 IOA 编码。我见过不少人用 false 也能跑通就忽略了它但当 IOA 连续时置 true 可以让报文缩短三分之一对低速窄带链路是实打实的优化。不过它有个约束所有信息对象的 IOA 必须连续递增否则对端会解析出错误的地址。COT 那个位置写常量而不是魔法数字。3 是突发、6 是激活、7 是激活确认、20 是响应总召这是现场抓包时最先要记住的一组值。很多 SCADA 厂家对 COT 的校验比 lib60870 严格得多从站主动上送用了 4被请求而不用 3主站会直接丢弃遥信在画面上就是不动。最后是信息对象的生命周期。ASDU_addInformationObject 内部会拷贝数据所以 add 完之后马上 destroy 掉 io 是安全的但 ASDU_destroy 之前必须先把所有信息对象 destroy 掉库不会替你遍历释放。数据线程里我每轮都创建和销毁峰值内存完全可控。泄露的典型症状是嵌入式板卡跑几天后内存持续上涨看堆栈往往就是这个循环没释放 io。4. 主站侧怎么收遥测、发遥控CS101_Master 的常用姿势4.1 连上从站并发送总召的最小主站主站侧我习惯用 CS101_Master因为它同时支持串口 101 和 TCP 104切换通道时业务代码不用动。连接成功后先发一个总召再循环拉取从站主动上送的数据#include stdio.h #include cs101_master.h #include hal_thread.h static void errorHandler(void* parameter, CS101_Connection connection, CS101_ASDU asdu, int msgType, int errorCode) { printf(链路异常: msgType%d code%d\n, msgType, errorCode); } static void printAsdu(CS101_ASDU asdu) { int num CS101_ASDU_getNumberOfElements(asdu); uint16_t ti CS101_ASDU_getTypeID(asdu); uint8_t cot CS101_ASDU_getCOT(asdu); printf(ASDU: TI%u COT%u 对象数%d\n, ti, cot, num); for (int i 0; i num; i) { InformationObject io CS101_ASDU_getElement(asdu, i); int ioa InformationObject_getObjectAddress(io); if (ti M_SP_NA_1) { /* 单点遥信 */ bool val SinglePointInformation_getValue((SinglePointInformation) io); printf( IOA%d 遥信%s\n, ioa, val ? ON : OFF); } else if (ti M_ME_NB_1) { /* 标度化遥测16 位整数 */ int val MeasuredValueScaled_getValue((MeasuredValueScaled) io); printf( IOA%d 遥测%d\n, ioa, val); } InformationObject_destroy(io); } } int main(void) { CS101_AppLayerParameters apl; apl.sizeOfIOA 3; apl.sizeOfCA 2; apl.sizeOfCOT 2; CS101_Master master CS101_Master_create(apl, errorHandler, NULL); CS101_Connection con CS101_Master_connect(master, 127.0.0.1, 2404); if (!con) { printf(连接失败\n); return 1; } CS101_ASDU asdu CS101_ASDU_create(apl, C_IC_NA_1, false, 6, 1, false, false); InformationObject io (InformationObject) InterrogationCommand_create(0, 20); CS101_ASDU_addInformationObject(asdu, io); CS101_Connection_sendASDU(con, asdu); CS101_ASDU_destroy(asdu); for (int i 0; i 30; i) { /* 拉取 6 秒覆盖从站两个上送周期 */ CS101_ASDU recv CS101_Connection_getNextASDU(con); if (recv) { printAsdu(recv); CS101_ASDU_destroy(recv); } Thread_sleep(200); } CS101_Master_destroy(master); return 0; }总召命令用 InterrogationCommand_create(0, 20) 构造信息对象IOA 固定为 0QOI 固定为 20这是我在现场对多个厂家验证过的通用写法。发送时注意必须设置 ASDU 的 COT 为 6激活并且要释放掉这个只用于发送的 ASDU。接收侧用了 getNextASDU 非阻塞拉取拿到后要自己 destroy如果这里漏掉内存只涨不降。CS101_Master_connect 对 104 来说内部会先完成 TCP 三次握手再发起 STARTDT act直到收到 STARTDT con 后才允许发送 I 帧。这一串交互你不用自己拼库全部处理判断链路真正可用看的是 connect 成功后第一次 sendASDU 的返回值和后续是否有 errorHandler 触发。4.2 ASDU 的四个关键字段TI、COT、COA、IOA在写主站协议栈时把下面这张表贴在屏幕上比看文档快得多字段含义常见值TI 类型标识决定数据形状1 单点遥信11 标度化遥测13 短浮点遥测45 单点遥控100 总召103 时钟同步COT 传送原因说明动作属性3 突发6 激活7 激活确认20 响应总召COA 公共地址站地址与从站配置一致通常是 1IOA 信息对象地址点号和点表一一对应四者里最容易出问题的是 TI 和 IOA 的匹配。同一个 IOA230TI1 时表示遥信TI11 时就是遥测从站按点表解释主站也按点表解释两个表对不上就会出现“画面上遥信闪动但数据库里没值”这种灵异现象。调试时我会先用 printAsdu 这种打印函数把收到的 TI 和 IOA 全部打出来和点表核对核对通过再写业务逻辑。COT20 响应总召只出现在从站对总召的完整应答链路里。有些国产从站在总召后第一帧先回 COT7 激活确认再发 COT20 数据也有实现直接只发 COT20。主站侧不要把逻辑写成“必须等到 COT7”而是把两者都视为总召过程的正常帧只在超时未收到任何帧时才判定总召失败。4.3 主站侧常见的三个坑第一个是应用层参数不一致。主站和从站的 sizeOfIOA、sizeOfCA、sizeOfCOT 三项必须完全一致否则从站发来的 3 字节 IOA 被主站按 2 字节解析后面的点号全错位。现场我见过不少联调失败都是因为这个而库恰好不会在握手时暴露这个不匹配。第二个是总召里 InterrogationCommand 的 QOI20。如果漏掉信息对象只发一个空 ASDU部分从站会直接丢弃没有任何回复。这也是为什么我在主站代码里专门 add 了 InterrogationCommand——它不是可选项是总召语义的组成部分。第三个是误以为 getNextASDU 是阻塞的。它返回 NULL 不代表链路断开只是暂时没有完整 ASDU正确的判活方式是统计连续 N 秒未收到 I 帧且 errorHandler 没被触发。把 NULL 当成断线去重连会造成真实生产环境里 104 连接反复重建SCADA 上看到的现象就是“数据断断续续”。5. 现场验证与调参抓包对照再动 t1/t2 和 k/w5.1 用一张 pcap 验证链路状态从 STARTDT 到 TESTFR我验证 104 链路最常用的命令是 tcpdump过滤端口后直接把原始报文落盘tcpdump -i eth0 -s 0 -w iec104.pcap tcp port 2404抓完用 Wireshark 打开重点看三段第一段是 TCP 握手后的 STARTDT act/conU 帧里只有四个字节 68 04 07 00 00 00如果只看到 act 没有 con说明从站没起来或防火墙挡了第二段是主站发的总召 I 帧负载里能看到 68 0C 和 TI100这证明业务层已经通了第三段是周期性的 TESTFR频率应该和 t3 一致。S 帧也是必看项。接收方每收到 w 个 I 帧就会发一个 S 帧控制域后两个字节是收到序号。如果抓包里连续 I 帧数量超过你所设的 k发送方仍在发那就是 k 参数没生效或者主站侧缓存堆积。用 Wireshark 的统计功能查看同一 TCP 流里的 I/S/U 帧数量比值若 S 帧占比异常低优先检查 t2 是不是被设得过大。5.2 慢链路里 k/w 和 t1/t2 的改法跨公网或 4G 链路时TCP 的 RTT 从局域网 1 ms 级别跳到几百毫秒默认的 t115 s 在偶发拥塞时容易被误判。常见做法是只调大 t1 和 t0保持 t2 略小于 t3CS101_APCIParameters apci CS101_Connection_getAPCIParameters(con); apci-k 12; /* 保持默认不随意增大 */ apci-w 8; /* 小于 k 即可 */ apci-t0 30000; /* 跨公网建连30 秒 */ apci-t1 30000; /* 响应超时放大到 30 秒 */ apci-t2 15000; /* 必须小于 t3预留 5 秒余量 */ apci-t3 20000; /* TESTFR 周期保持 20 秒 */t1 调大后链路断开的发现时间会变长这是成本但相比误判断线导致的反复重连这个成本通常可以接受。k 不要轻易调大因为 k 越大链路恢复时重发的 I 帧越多慢链路上反而加重拥塞。卫星链路我建议 k 保持 12、w 设为 10让接收端尽早回确认减少发送窗口填满后的停顿。整个调参过程的验证方法是一致的改完参数后重放一次总召抓包确认从 STARTDT 到第一个 I 帧的时间差、TESTFR 的实际发包间隔和你设的 t3 基本吻合。如果 TESTFR 间隔漂移明显先检查系统时间同步和 TCP_NODELAY 是否被上层关掉再回来怀疑协议栈参数。本文还有配套的精品资源点击获取
返回列表