资讯动态

IEC60870开源库实战:104规约编解码、避坑与二次开发

发布时间:2026/10/16 3:28:23 来源:尧图企业网站定制
简介lib60870-2.0.1 是 IEC60870-5 系列协议的开源实现库面向电力自动化、SCADA 与智能电网领域的开发者帮助其快速集成 101、102、104 协议构建主站或子站应用无需从零实现底层通信逻辑。压缩包共 114 个文件约 186KB以 39 个 c 源文件与 34 个 h 头文件为核心覆盖链路层、ASDU 编解码、主从站连接等模块另含 9 个 makefile、3 个 mk 构建脚本、12 个 txt 说明及 pem 证书、readme、changelog 等辅助文件结构紧凑便于移植。目前已有 2496 人学习下载。库内提供 API 接口、错误检测与恢复机制并支持串口、以太网等多种传输介质读者可据此掌握 101/102/104 的编码解码流程与主从站双向通信实现快速搭建调试环境降低协议开发成本提升软件可靠性与兼容性。1. 从一次变电站调试翻车说起IEC60870 开源实现库到底能干什么去年帮一个做配电自动化的朋友排查问题现场装置用的是 IEC 60870-5-104 规约主站那边死活收不到遥测数据。抓包一看链路层握手正常但总召唤报文发出去之后从站回的 ASDU 里传送原因字节被解析错了——对方主站用的是某商业库对 CP56Time2a 时间戳的毫秒字段处理有偏差。那次之后我就养成了一个习惯手头常备一套能读能改的开源 IEC60870 实现出问题直接翻源码对字节比对着规约文档猜快得多。IEC60870 这套规约在电力监控、轨道交通、水利调度里用得极广尤其是 104 规约IEC 60870-5-104跑在 TCP 之上是厂站和调度主站之间的标准语言。但规约文档厚、字段多、字节序和时标格式容易踩坑商业协议栈又是个黑匣子出了问题只能等厂商支持。这个开源实现库的价值就在这儿它把 101串口、104网络两套链路层的编解码、ASDU 类型解析、总召唤/时钟同步/遥控等典型流程都摊开给你看代码可读、可改、可嵌到自己项目里。适合谁做电力/工控上位机、协议网关、规约测试工具的开发者以及需要把 104 从站塞进嵌入式设备的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆一遍。2. 架构与目录拆解链路层、ASDU、应用层怎么分层2.1 整体分层设计这套库的分层思路很清晰基本照着规约本身的层次来切。最底下是链路层负责 101 的 FT1.2 帧格式和 104 的 APCI启动字符 0x68、长度、控制域四字节往上是传输层/会话层处理 104 的发送序号 N(S)、接收序号 N(R) 和 k/w 窗口流控再往上是ASDU 层也就是应用服务数据单元承载类型标识、可变结构限定词、传送原因、公共地址和信息对象最上面是应用层封装总召唤、时钟同步、遥控、遥调这些业务动作。这种分法好处是改一层不影响其他层。比如你要把 104 换成 101 串口链路层换掉ASDU 和应用层几乎不用动。我见过不少项目把三层揉在一个文件里加个新类型标识就得全局搜字符串维护起来是血泪经验。2.2 目录结构说明源码包解压后大致是这么个结构不同实现命名略有差异但职责划分一致iec60870/ ├── src/ │ ├── link/ # 链路层FT1.2 帧、APCI 控制域 │ │ ├── ft12.c # 101 串口帧编解码 │ │ └── apci.c # 104 的 I/S/U 帧格式 │ ├── asdu/ # ASDU 编解码 │ │ ├── asdu.c # 类型标识分发 │ │ └── types.c # 各信息对象类型单点、双点、归一化值等 │ ├── app/ # 应用层业务 │ │ ├── interrogation.c # 总召唤 │ │ ├── clock_sync.c # 时钟同步 │ │ └── command.c # 遥控/遥调 │ ├── session/ # 会话管理、序号维护 │ └── port/ # 平台适配层socket、串口、内存 ├── include/ # 对外头文件 ├── examples/ # 示例主站/从站 demo ├── tests/ # 单元测试 └── CMakeLists.txtport/这层是移植的关键它把 socket、串口读写、内存分配、时间获取都抽象成函数指针换平台只改这一层。examples/里通常有一个 104 从站和一个 104 主站的 demo跑起来就能看到完整交互。2.3 关键数据结构ASDU 是核心几乎所有业务都围绕它转。典型定义长这样typedef struct { uint8_t type_id; // 类型标识如 1单点信息, 9归一化测量值 uint8_t vsq; // 可变结构限定词最高位SQ低7位是信息对象个数 uint8_t cot; // 传送原因如 6激活, 7激活确认, 20响应总召唤 uint8_t ca; // 公共地址区分不同厂站 uint8_t oa; // 源发站地址101 用104 常为 0 void *objects; // 信息对象数组按 type_id 决定元素类型 int obj_count; // 对象个数 } iec_asdu_t;vsq这个字段最容易翻车最高位 SQ1 表示后面是「一个信息对象 连续地址序列」SQ0 表示「多个独立信息对象」。很多新手解析时不管 SQ 直接按个数读遇到 SQ1 的报文就错位。cot的取值也要对着规约表查6/7/8/9/10 是激活相关20 是响应总召唤这些在asdu.c里通常有个 switch 分发。2.4 104 的 APCI 控制域104 的帧分 I、S、U 三种靠控制域第一个字节的低两位区分// 控制域四字节解析 // I 帧bit00, bit10含发送序号和接收序号 // S 帧bit00, bit11只有接收序号 // U 帧bit01, bit11用于 STARTDT/STOPDT/TESTFR static int apci_frame_type(const uint8_t *ctrl) { if ((ctrl[0] 0x01) 0) return I_FRAME; if ((ctrl[0] 0x03) 0x01) return S_FRAME; return U_FRAME; }I 帧的发送序号在 ctrl[0..1] 的低 15 位接收序号在 ctrl[2..3] 的低 15 位都是小端。这里有个坑序号是 15 位循环到 32767 回绕到 0比较序号时不能直接用大于小于得用模运算。session/里维护序号的那段代码值得细看。3. 跑起来从编译到主从站联调的完整步骤3.1 编译与依赖这套库一般用 CMake 构建依赖很少标准 C 库加平台 socket 就够。Linux 下mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4如果开了测试make完会生成iec_test之类的可执行文件。Windows 下用 MinGW 或 MSVC 都行port/层里通常有port_win32.c和port_posix.c两套适配CMake 里按平台自动选。要是编译报找不到sys/socket.h说明平台宏没配对去CMakeLists.txt里看if(WIN32)那段。3.2 启动一个 104 从站examples/里的从站 demo 是理解整个流程的最好入口。核心就三步初始化、绑定端口、进事件循环。#include iec60870.h int main(void) { // 1. 创建从站实例指定公共地址和端口 iec_slave_t *slave iec_slave_create(1 /*CA*/, 2404 /*端口*/); if (!slave) return -1; // 2. 注册回调收到总召唤时怎么回数据 iec_slave_set_interrogation_cb(slave, on_interrogation); // 收到遥控时怎么处理 iec_slave_set_command_cb(slave, on_command); // 3. 启动监听进循环 iec_slave_listen(slave); while (iec_slave_run(slave, 100 /*ms*/) 0) { // 周期性上送变化遥测 iec_slave_send_measurement(slave, 1, 0x4001, 12345); } iec_slave_destroy(slave); return 0; }iec_slave_create的第二个参数是监听端口104 默认 2404。iec_slave_run每次跑 100ms内部处理收包、超时、心跳TESTFR。回调on_interrogation里你要把当前所有遥测遥信按 ASDU 格式填回去传送原因填 20。这里注意总召唤响应可能分多帧发因为一帧 ASDU 的信息对象个数有限受 APDU 最大长度 253 字节限制库一般会自动分包但你要保证回调里把数据一次性给全。3.3 启动一个 104 主站去连它主站侧对称iec_master_t *m iec_master_create(127.0.0.1, 2404, 1 /*CA*/); iec_master_set_asdu_cb(m, on_asdu_received); iec_master_connect(m); // 发起总召唤 iec_master_interrogation(m, 1 /*CA*/, 20 /*QOI: 站总召唤*/); while (iec_master_run(m, 100) 0) { // 主循环 }iec_master_interrogation的第三个参数是召唤限定词 QOI20 表示站总召唤21~36 是分组召唤。发完之后从站会回一系列 ASDUon_asdu_received里按type_id分发处理。联调时先看能不能收到 STARTDT 确认再发总召唤最后看遥测值对不对。3.4 用抓包工具验证跑起来之后别急着写业务先用 Wireshark 抓一下 2404 端口。过滤tcp.port 2404能看到 U 帧的 STARTDT 激活、I 帧的总召唤和响应。Wireshark 自带 104 解析器能把 APCI 和 ASDU 字段都展开。对照抓包和源码里的编解码函数哪个字节对不上立刻就能定位。我一般会先构造一个已知内容的 ASDU用库编码成字节再和抓包里的原始字节逐位比对这是最快的验证方式。提示如果从站收不到主站的连接先确认防火墙和端口占用如果连上了但总召唤无响应检查公共地址 CA 是否匹配很多现场问题就出在 CA 配错。4. 避坑与排查那些年我们在 104 上踩过的雷4.1 序号回绕导致连接莫名断开现象跑几个小时或几天后主从站突然断链重连后又正常周而复始。原因104 的发送/接收序号是 15 位到 32767 回绕。如果代码里用if (send_seq ack_seq)判断是否超窗回绕后就判断反了k 窗口默认 12被误判超限触发断链。解决序号比较一律用模运算(send_seq - ack_seq 32768) % 32768得到未确认帧数再和 k 比较。session/里如果有现成的窗口判断函数直接复用别自己写。4.2 CP56Time2a 时标解析差 8 小时现象主站显示的遥测时间比实际早或晚 8 小时。原因CP56Time2a 的时标是本地时间还是 UTC规约里没强制不同厂商实现不一样。有的库默认按 UTC 解析现场装置发的是本地时间就差出时区偏移。解决在配置里加一个时区/时标模式开关解析时按需加减偏移。types.c里解析 CP56Time2a 的函数通常长这样毫秒在低 16 位分钟在 bit0~5小时在 bit0~4日和月年依次排。改之前先抓包确认装置发的是哪种。4.3 SQ 位没处理导致信息对象错位现象解析带多个信息对象的 ASDU 时第一个值对后面全乱。原因vsq最高位 SQ1 时报文里只有一个信息对象后面跟的是连续地址SQ0 才是多个独立对象。代码里如果统一按「对象个数」循环读SQ1 的报文就会把地址当成值读。解决解析前先判 SQ 位分两条路径。asdu.c里一般有asdu_parse_objects函数进去先看vsq 0x80。4.4 总召唤响应超时现象主站发了总召唤从站没回或只回了一部分。原因一是从站回调里数据没填全二是分包逻辑有问题三是 APDU 超过 253 字节被截断。解决先确认回调被调用了加日志再看返回的 ASDU 个数。如果数据量大库应该自动分包每包传送原因都是 20最后一包带激活终止COT10。如果库没自动分包得自己在回调里分批调发送函数。4.5 遥控返校流程走不通现象主站发遥控选择从站没回选择确认或直接执行了。原因104 遥控是「选择-执行」两阶段选择COT6和执行COT6但 S/E 位不同要分开处理。有的实现把两个阶段揉在一起或者 S/E 位在信息对象地址里没解析对。解决command.c里按 S/E 位分发选择阶段只校验不动作回 COT7 确认执行阶段才真正操作回 COT7 或 10。抓包看 S/E 位是 0 还是 1。5. 二次开发加一个自定义 ASDU 类型和移植到新平台5.1 扩展一个新的类型标识规约里类型标识是 1~127有些私有实现会用 128~255 做扩展。假设你要加一个类型标识 200 的自定义遥测步骤是// 1. 在 types.h 里定义类型和结构 #define IEC_TYPE_CUSTOM_MEASURE 200 typedef struct { uint16_t address; float value; uint8_t quality; } custom_measure_t; // 2. 在 types.c 的编解码分发里注册 static int custom_measure_encode(const void *obj, uint8_t *buf, int len) { const custom_measure_t *m (const custom_measure_t *)obj; // 地址 3 字节小端 buf[0] m-address 0xFF; buf[1] (m-address 8) 0xFF; buf[2] 0; // 值 4 字节浮点按需转字节序 memcpy(buf[3], m-value, 4); buf[7] m-quality; return 8; // 返回编码后字节数 } // 3. 在 asdu.c 的分发表里挂上 static const iec_type_handler_t type_handlers[] { // ... 已有类型 { IEC_TYPE_CUSTOM_MEASURE, custom_measure_encode, custom_measure_decode }, };关键是编解码函数要返回实际字节数分发层靠这个推进缓冲区指针。加完之后主从站两侧都要更新否则一边发一边不认。测试时先单元测试编解码再联调。5.2 移植到新平台移植只动port/层需要实现这几个函数函数作用注意事项port_tcp_listen创建监听 socket设置 SO_REUSEADDRport_tcp_accept接受连接非阻塞或带超时port_tcp_send/recv收发数据处理 EAGAIN 和部分写port_serial_open打开串口101 用波特率、校验位配置port_get_time_ms取毫秒时间用于超时和时标port_malloc/free内存管理嵌入式可换成静态池嵌入式平台如果没 socket104 就用不了只能走 101 串口。串口那套 FT1.2 帧有固定头 0x68 和校验和ft12.c里编解码是独立的移植时重点测校验和计算和转义0x68 在数据里要转义。5.3 一个调试技巧字节级对照我改协议栈有个习惯每次改完编解码先写个最小测试构造一个已知 ASDU编码成字节数组打印出来再手动对着规约文档逐字节核。比如总召唤响应的单点信息类型标识 1VSQ 0x01COT 20CA 1然后信息对象地址 3 字节加 SIQ 1 字节。核完再解码回来看是否还原。这个习惯帮我省了无数次现场返工——协议这东西差一个字节就是收不到数据抓包也未必一眼看出来。从那以后我每次动协议栈代码都强制走一遍「编码-打印-对照-解码」的闭环哪怕只改了一个字段。希望这套拆解能帮到你把 IEC60870 这个开源库跑起来、改起来少走点我当年走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑