IEC 104 系列(五):开发实现——基于 lib60870 的代码实战
·
核心目标:能基于 lib60870 开源库独立开发 IEC 104 Server(从站/RTU)和 Client(主站/调度),掌握回调机制、数据处理、双网冗余和嵌入式移植。
前置知识:Part 2 的帧格式(APCI/ASDU 结构),Part 3 的通信机制(连接/总召/确认),Part 4 的数据类型(TI/品质位/COT)。
5.1 开源方案选型
5.1.1 主流开源库对比
| 库 | 语言 | 许可证 | Server | Client | 101 | 特点 |
|---|---|---|---|---|---|---|
| lib60870 | C | GPL-3.0 / 商业 | ✓ | ✓ | ✓ | 功能最完整,嵌入式首选 |
| lib60870.NET | C# | GPL-3.0 / 商业 | ✓ | ✓ | ✓ | .NET 平台,Windows 友好 |
| py60870 | Python (C 绑定) | — | ✓ | ✓ | ✓ | 快速原型 / 自动化测试 |
| j60870 | Java | Apache-2.0 | ✓ | ✓ | — | 跨平台后端集成 |
推荐:嵌入式/高性能 → lib60870 ©;原型验证 → py60870;Java 后端 → j60870。
5.1.2 lib60870 项目结构速览
lib60870/
├── lib60870-C/ # C 语言核心库
│ ├── src/
│ │ ├── inc/ # 头文件
│ │ │ ├── cs104_slave.h # Server API
│ │ │ ├── cs104_master.h # Client API
│ │ │ ├── cs101_information_elements.h # 信息元素定义
│ │ │ └── hal_socket.h # 平台抽象层 (Socket)
│ │ └── common/ # 源码实现
│ ├── examples/
│ │ ├── cs104_server.c # Server 示例
│ │ └── cs104_client.c # Client 示例
│ ├── config/ # 构建配置
│ └── Makefile
├── lib60870.NET/ # C# 实现
└── py60870/ # Python 绑定
5.2 基于 lib60870 开发 Server(从站/RTU)
5.2.1 环境搭建
Linux (x86_64):
# 克隆源码
git clone https://github.com/mz-automation/lib60870.git
cd lib60870/lib60870-C
# 创建构建目录
mkdir build && cd build
cmake ..
make -j$(nproc)
# 安装到系统
sudo make install
ARM 交叉编译:
# 以 aarch64 为例
mkdir build_arm && cd build_arm
cmake .. \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64
make -j$(nproc)
5.2.2 最小 Server 示例
先从最简单的 Server 开始——启动监听,响应总召唤和时钟同步,周期上送遥测:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include "cs104_slave.h"
/* ── 全局句柄 ── */
static CS104_Slave slave = NULL;
static bool running = true;
void sigint_handler(int signal) {
running = false;
}
/* ───────────────────────────────────────────
* 回调: 时钟同步
* ─────────────────────────────────────────── */
static bool clock_sync_handler(void *parameter,
IMasterConnection connection,
CS101_ASDU asdu,
CP56Time2a new_time)
{
printf("[对时] 收到时钟同步: %02d-%02d-%02d %02d:%02d:%02d.%03d\n",
new_time.year + 2000, new_time.month, new_time.day,
new_time.hour, new_time.minute, new_time.second,
new_time.millisecond);
/* 返回 true = 接受对时, 返回 false = 拒绝对时 */
return true;
}
/* ───────────────────────────────────────────
* 回调: 总召唤 (GI)
* ─────────────────────────────────────────── */
static bool interrogation_handler(void *parameter,
IMasterConnection connection,
CS101_ASDU asdu,
uint8_t qoi)
{
printf("[总召] QOI=%d, 主站请求全量数据\n", qoi);
if (qoi == 20) { /* 站召唤 (全局) */
CS101_AppLayerParameters al_params =
IMasterConnection_getApplicationLayerParameters(connection);
IMasterConnection_sendACT_CON(connection, asdu, false);
/* 上送遥信数据 (TI=1, 单点遥信) */
CS101_ASDU newAsdu = CS101_ASDU_create(
al_params, false, CS101_COT_INTERROGATED_BY_STATION,
0, 1, false, false);
/* 离散地址模式 (SQ=0), 一次发送多个点 */
for (int ioa = 0x0001; ioa <= 0x0010; ioa++) {
/* 从数据源读取实际遥信值 */
int siq_val = get_remote_signal(ioa); // 用户实现的读取函数
InformationObject io = (InformationObject)
SinglePointInformation_create(NULL, ioa, siq_val & 0x01,
(siq_val & 0x80) ? IEC60870_QUALITY_INVALID : IEC60870_QUALITY_GOOD);
CS101_ASDU_addInformationObject(newAsdu, io);
InformationObject_destroy(io);
}
IMasterConnection_sendASDU(connection, newAsdu);
CS101_ASDU_destroy(newAsdu);
/* 上送遥测数据 (TI=13, 短浮点) */
CS101_ASDU measAsdu = CS101_ASDU_create(
al_params, false, CS101_COT_INTERROGATED_BY_STATION,
0, 1, false, false);
for (int ioa = 0x0400; ioa <= 0x040F; ioa++) {
float value = get_analog_value(ioa); // 用户实现的读取函数
int qds_val = get_analog_quality(ioa);
InformationObject io = (InformationObject)
MeasuredValueShort_create(NULL, ioa, value,
qds_val & 0x80 ? IEC60870_QUALITY_INVALID : IEC60870_QUALITY_GOOD);
CS101_ASDU_addInformationObject(measAsdu, io);
InformationObject_destroy(io);
}
IMasterConnection_sendASDU(connection, measAsdu);
CS101_ASDU_destroy(measAsdu);
/* 总召唤结束 */
IMasterConnection_sendACT_TERM(connection, asdu);
} else {
/* 分组召唤 */
IMasterConnection_sendACT_CON(connection, asdu, true); /* 负确认 */
}
return true;
}
/* ───────────────────────────────────────────
* 回调: 遥控命令 (ASDU 处理)
* ─────────────────────────────────────────── */
static bool asdu_handler(void *parameter,
IMasterConnection connection,
CS101_ASDU asdu)
{
if (CS101_ASDU_getTypeID(asdu) == C_SC_NA_1) {
printf("[遥控] 收到单点遥控命令\n");
/* 遍历信息对象 */
int objCount = CS101_ASDU_getNumberOfElements(asdu);
for (int i = 0; i < objCount; i++) {
InformationObject io = CS101_ASDU_getElement(asdu, i);
int ioa = InformationObject_getObjectAddress(io);
if (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) {
int sco = SingleCommand_getSCO((SingleCommand)io);
bool isSelect = ((sco >> 1) & 0x01); // SE 位
bool isClose = (sco & 0x01); // SCS 位
if (isSelect) {
printf("[遥控] IOA=0x%04X: 选择 %s\n", ioa,
isClose ? "合闸" : "分闸");
IMasterConnection_sendACT_CON(connection, asdu, false);
} else {
printf("[遥控] IOA=0x%04X: 执行 %s\n", ioa,
isClose ? "合闸" : "分闸");
/* ★ 在这里执行实际的遥控操作 */
execute_control(ioa, isClose); // 用户实现
IMasterConnection_sendACT_CON(connection, asdu, false);
}
}
}
}
return true;
}
/* ───────────────────────────────────────────
* 周期上送线程
* ─────────────────────────────────────────── */
static void *cyclic_send_thread(void *arg)
{
while (running) {
/* 每秒上送一次遥测变化量 */
Thread_sleep(1000);
/* 遍历所有连接,向每个主站发送周期数据 */
IMasterConnection connection = CS104_Slave_getActiveConnections(slave);
while (connection != NULL) {
CS101_AppLayerParameters al_params =
IMasterConnection_getApplicationLayerParameters(connection);
CS101_ASDU asdu = CS101_ASDU_create(
al_params, false, CS101_COT_PERIODIC, 0, 1, false, false);
/* 发送第一个遥测点 (IOA=0x0401) */
float value = get_analog_value(0x0401);
int quality = get_analog_quality(0x0401);
InformationObject io = (InformationObject)
MeasuredValueShort_create(NULL, 0x0401, value,
IEC60870_QUALITY_GOOD);
CS101_ASDU_addInformationObject(asdu, io);
InformationObject_destroy(io);
IMasterConnection_sendASDU(connection, asdu);
CS101_ASDU_destroy(asdu);
connection = IMasterConnection_getNextActiveConnection(
slave, connection);
}
}
return NULL;
}
/* ───────────────────────────────────────────
* 主函数: 初始化并启动 Server
* ─────────────────────────────────────────── */
int main(void)
{
signal(SIGINT, sigint_handler);
/* ① 创建 Server 实例 */
slave = CS104_Slave_create(100, 100); /* 最大 100 个 TCP 连接 */
/* ② 配置连接参数 */
CS104_Slave_setLocalAddress(slave, "0.0.0.0");
CS104_Slave_setLocalPort(slave, 2404);
/* ③ 设置应用层参数 (k/w/IOA长度/COT长度/公共地址长度) */
CS101_AppLayerParameters al_params =
CS104_Slave_getAppLayerParameters(slave);
al_params->sizeOfIOA = 2; /* IOA = 2 字节 */
al_params->sizeOfCOT = 2; /* COT = 2 字节 */
al_params->sizeOfCA = 2; /* 公共地址 = 2 字节 */
al_params->maxOutstandingIPDUs = 12; /* k = 12 */
al_params->maxSupplRespAfterPDU = 8; /* w = 8 */
CS104_Slave_setAppLayerParameters(slave, al_params);
/* ④ 设置超时参数 (t1, t2, t3, 单位: ms) */
CS104_Slave_setConnectionTimeout(slave, 15000); /* t1 = 15s */
CS104_Slave_setAckTimeout(slave, 10000); /* t2 = 10s */
CS104_Slave_setTestTimeout(slave, 20000); /* t3 = 20s */
/* ⑤ 注册回调函数 */
CS104_Slave_setClockSyncHandler(slave, clock_sync_handler, NULL);
CS104_Slave_setInterrogationHandler(slave, interrogation_handler, NULL);
CS104_Slave_setASDUHandler(slave, asdu_handler, NULL);
/* ⑥ 启动 Server */
CS104_Slave_start(slave);
if (CS104_Slave_isRunning(slave)) {
printf("IEC 104 Server 启动成功, 监听端口 2404\n");
/* ⑦ 启动周期上送线程 */
Thread cyclicThread = Thread_create(cyclic_send_thread, NULL, true);
Thread_start(cyclicThread);
while (running) {
Thread_sleep(100);
}
Thread_destroy(cyclicThread);
}
/* ⑧ 停止并销毁 */
CS104_Slave_stop(slave);
CS104_Slave_destroy(slave);
printf("Server 已停止\n");
return 0;
}
5.2.3 编译与运行
# 编译 Server
gcc -o iec104_server server.c \
-I/path/to/lib60870/lib60870-C/src/inc \
-L/path/to/lib60870/lib60870-C/build \
-l60870 -lpthread -lm
# 运行
./iec104_server
# 输出:
# IEC 104 Server 启动成功, 监听端口 2404
5.2.4 Server 回调全景图
5.2.5 IOA 配置与数据存储设计
在实际工程中,数据存储需要和 IOA 映射。以下是推荐的数据结构设计:
/* ── 遥信点表定义 ── */
typedef struct {
uint16_t ioa; /* 信息对象地址 */
char name[32]; /* 点名称 */
uint8_t type; /* 0=单点, 1=双点 */
uint8_t value; /* 当前值 (含品质位) */
CP56Time2a last_change_time; /* 最后变位时间 (SOE用) */
} YX_Point;
/* ── 遥测点表定义 ── */
typedef struct {
uint16_t ioa; /* 信息对象地址 */
char name[32]; /* 点名称 */
float value; /* 当前值 (物理量) */
uint8_t quality; /* 品质描述符 QDS */
float deadband; /* 死区值 (变化量上送用) */
float last_sent_value; /* 上次上送的值 */
} YC_Point;
/* ── 遥控点表定义 ── */
typedef struct {
uint16_t ioa; /* 信息对象地址 */
char name[32];
uint8_t state; /* 当前状态 (0=分, 1=合) */
uint8_t select_state; /* 0=未选中, 1=已选中 */
int select_timeout; /* 选择超时计时器 (ms) */
} YK_Point;
/* ── 全局点表 ── */
#define MAX_YX 256
#define MAX_YC 512
#define MAX_YK 64
static YX_Point yx_table[MAX_YX];
static YC_Point yc_table[MAX_YC];
static YK_Point yk_table[MAX_YK];
static int yx_count = 0, yc_count = 0, yk_count = 0;
/* ── 从点表中查找 ── */
static YC_Point *find_yc_by_ioa(uint16_t ioa) {
for (int i = 0; i < yc_count; i++) {
if (yc_table[i].ioa == ioa) return &yc_table[i];
}
return NULL;
}
5.2.6 SOE 突发上送实现
当检测到遥信变位时,从站应立即构造带时标的 SOE 帧上送:
/* SOE 突发上送: 遥信变位时调用 */
void send_soe_event(CS104_Slave slave, uint16_t ioa,
uint8_t siq_value, CP56Time2a timestamp)
{
IMasterConnection connection =
CS104_Slave_getActiveConnections(slave);
while (connection != NULL) {
CS101_AppLayerParameters al_params =
IMasterConnection_getApplicationLayerParameters(connection);
/* 构造 M_SP_TB_1 (TI=30, 单点遥信 + CP56Time2a) */
CS101_ASDU asdu = CS101_ASDU_create(
al_params, false,
CS101_COT_SPONTANEOUS, /* COT=3, 突发 */
0, 1, false, false);
CS101_ASDU_setTypeID(asdu, M_SP_TB_1);
InformationObject io = (InformationObject)
SinglePointWithCP56Time2a_create(
NULL, ioa,
siq_value & 0x01, /* SPI */
(siq_value & 0x80) ? IEC60870_QUALITY_INVALID
: IEC60870_QUALITY_GOOD,
×tamp);
CS101_ASDU_addInformationObject(asdu, io);
InformationObject_destroy(io);
IMasterConnection_sendASDU(connection, asdu);
CS101_ASDU_destroy(asdu);
connection = IMasterConnection_getNextActiveConnection(
slave, connection);
}
}
5.3 基于 lib60870 开发 Client(主站/调度)
5.3.1 最小 Client 示例——连接 + 总召唤
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cs104_master.h"
static bool running = true;
/* ── 回调: 收到 ASDU 数据 ── */
static bool asdu_received_handler(void *parameter, CS101_ASDU asdu)
{
int ti = CS101_ASDU_getTypeID(asdu);
int cot = CS101_ASDU_getCOT(asdu);
int n = CS101_ASDU_getNumberOfElements(asdu);
printf("[收到] TI=%d, COT=%d, 对象数=%d\n", ti, cot, n);
/* 根据 TI 类型解析信息对象 */
switch (ti) {
case M_SP_NA_1: /* TI=1, 单点遥信 */
for (int i = 0; i < n; i++) {
InformationObject io = CS101_ASDU_getElement(asdu, i);
int ioa = InformationObject_getObjectAddress(io);
SinglePointInformation sp = (SinglePointInformation)io;
printf(" 遥信: IOA=0x%04X, SCS=%d, IV=%d\n",
ioa,
SinglePointInformation_getValue(sp),
SinglePointInformation_getQuality(sp) == IEC60870_QUALITY_INVALID);
}
break;
case M_ME_NC_1: /* TI=13, 短浮点遥测 */
for (int i = 0; i < n; i++) {
InformationObject io = CS101_ASDU_getElement(asdu, i);
int ioa = InformationObject_getObjectAddress(io);
MeasuredValueShort mv = (MeasuredValueShort)io;
printf(" 遥测: IOA=0x%04X, 值=%.2f, IV=%d\n",
ioa,
MeasuredValueShort_getValue(mv),
MeasuredValueShort_getQuality(mv) == IEC60870_QUALITY_INVALID);
}
break;
case M_SP_TB_1: /* TI=30, SOE */
for (int i = 0; i < n; i++) {
InformationObject io = CS101_ASDU_getElement(asdu, i);
int ioa = InformationObject_getObjectAddress(io);
SinglePointWithCP56Time2a soe = (SinglePointWithCP56Time2a)io;
CP56Time2a ts = SinglePointWithCP56Time2a_getTimestamp(soe);
printf(" SOE: IOA=0x%04X, 值=%d, "
"时间=%02d-%02d-%02d %02d:%02d:%02d.%03d\n",
ioa,
SinglePointWithCP56Time2a_getValue(soe),
ts.year + 2000, ts.month, ts.day,
ts.hour, ts.minute, ts.second, ts.millisecond);
}
break;
}
return true;
}
/* ── 回调: 连接状态变化 ── */
static void connection_handler(void *parameter,
CS104_Master master,
CS104_MasterConnectionEvent event)
{
switch (event) {
case CS104_CON_EVENT_ACTIVE:
printf("[连接] 连接已建立\n");
break;
case CS104_CON_EVENT_INACTIVE:
printf("[连接] 连接已断开\n");
break;
case CS104_CON_EVENT_STARTDT_CON:
printf("[连接] STARTDT 已确认, 可传输数据\n");
break;
}
}
/* ── 主函数 ── */
int main(void)
{
/* ① 创建 Master 实例 */
CS104_Master master = CS104_Master_create(NULL, NULL);
/* ② 配置应用层参数 (必须与 Server 一致) */
CS101_AppLayerParameters al_params =
CS104_Master_getAppLayerParameters(master);
al_params->sizeOfIOA = 2;
al_params->sizeOfCOT = 2;
al_params->sizeOfCA = 2;
/* ③ 注册回调 */
CS104_Master_setASDUReceivedHandler(master, asdu_received_handler, NULL);
CS104_Master_setConnectionHandler(master, connection_handler, NULL);
/* ④ 连接 Server */
printf("正在连接 IEC 104 Server...\n");
CS104_Master_addSlave(master, "192.168.1.100", 2404);
/* 等待连接建立 + STARTDT */
Thread_sleep(3000);
/* ⑤ 检查连接状态 */
if (CS104_Master_isRunning(master)) {
printf("连接成功! 开始总召唤...\n");
/* 发送总召唤命令 */
CS104_Master_sendInterrogationCommand(master, CS101_QOI_STATION);
/* 等待总召唤完成 (实际应根据 COT=10 标记判断) */
Thread_sleep(5000);
}
/* ⑥ 断开 */
CS104_Master_removeSlave(master);
CS104_Master_destroy(master);
return 0;
}
5.3.2 时钟同步实现
/*
* 时钟同步: 将主站的当前时间发送给从站
*/
void send_clock_sync(CS104_Master master)
{
/* 获取当前时间 */
time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
/* 构造 CP56Time2a 时间戳 */
CP56Time2a cp56_time;
CP56Time2a_createFromMsTimestamp(
&cp56_time,
(uint64_t)now * 1000); /* 秒 → 毫秒 */
/* 发送时钟同步命令 (TI=103, COT=6) */
CS104_Master_sendClockSyncCommand(master, &cp56_time);
printf("[对时] 已发送时钟同步: %04d-%02d-%02d %02d:%02d:%02d\n",
tm_now->tm_year + 1900,
tm_now->tm_mon + 1,
tm_now->tm_mday,
tm_now->tm_hour,
tm_now->tm_min,
tm_now->tm_sec);
}
5.3.3 遥控命令——Select-Execute 完整实现
/*
* 遥控: 完整的 Select-Execute 两阶段流程
*
* 返回值:
* 0 = 成功
* -1 = 选择失败
* -2 = 执行失败
*/
int send_control_command(CS104_Master master,
uint16_t ioa,
bool close) /* true=合闸, false=分闸 */
{
/* ═════════════════════════════════════
* 阶段 1: 选择 (Select)
* ═════════════════════════════════════ */
printf("[遥控] IOA=0x%04X: 开始选择...\n", ioa);
/* 构造选择命令 (SE=1) */
InformationObject select_io = (InformationObject)
SingleCommand_create(NULL, ioa,
close ? 0x01 : 0x00, /* SCS: 1=合, 0=分 */
true, /* SE=1 (选择) */
0); /* QU=0 */
if (!CS104_Master_sendControlCommand(master,
CS101_COT_ACTIVATION, /* COT=6 */
1, /* 公共地址 */
select_io)) {
printf("[遥控] ★ 选择命令发送失败\n");
InformationObject_destroy(select_io);
return -1;
}
InformationObject_destroy(select_io);
/* 等待选择确认 (COT=7)
* 在实际工程中,应通过回调异步等待,
* 这里简化为同步等待 */
Thread_sleep(500);
/* ═════════════════════════════════════
* 阶段 2: 执行 (Execute)
* ═════════════════════════════════════ */
printf("[遥控] IOA=0x%04X: 开始执行...\n", ioa);
/* 构造执行命令 (SE=0) */
InformationObject exec_io = (InformationObject)
SingleCommand_create(NULL, ioa,
close ? 0x01 : 0x00, /* SCS */
false, /* SE=0 (执行) */
0); /* QU=0 */
if (!CS104_Master_sendControlCommand(master,
CS101_COT_ACTIVATION, /* COT=6 */
1,
exec_io)) {
printf("[遥控] ★ 执行命令发送失败\n");
InformationObject_destroy(exec_io);
return -2;
}
InformationObject_destroy(exec_io);
printf("[遥控] IOA=0x%04X: 遥控完成 (等待 SOE 确认)\n", ioa);
return 0;
}
/* ── 使用示例 ── */
void control_example(CS104_Master master)
{
/* 合闸: 断路器 IOA=0x0801 */
send_control_command(master, 0x0801, true);
Thread_sleep(2000);
/* 分闸: 断路器 IOA=0x0801 */
send_control_command(master, 0x0801, false);
}
5.3.4 Client-Server 交互全景
5.3.5 遥测死区变化量上送
/*
* 周期上送: 仅上送变化超过死区的遥测值
*/
void send_cyclic_deadband(CS104_Slave slave)
{
static CP56Time2a last_send_time;
CP56Time2a now;
CP56Time2a_createFromMsTimestamp(&now, currentTimeMillis());
IMasterConnection connection =
CS104_Slave_getActiveConnections(slave);
while (connection != NULL) {
CS101_AppLayerParameters al_params =
IMasterConnection_getApplicationLayerParameters(connection);
bool has_data = false;
CS101_ASDU asdu = CS101_ASDU_create(
al_params, false, CS101_COT_PERIODIC, 0, 1, false, false);
CS101_ASDU_setTypeID(asdu, M_ME_NC_1);
/* 遍历遥测点表,筛选变化量 */
for (int i = 0; i < yc_count; i++) {
float current = yc_table[i].value;
float last_sent = yc_table[i].last_sent_value;
float deadband = yc_table[i].deadband;
/* 死区判断 */
if (fabs(current - last_sent) >= deadband) {
yc_table[i].last_sent_value = current;
InformationObject io = (InformationObject)
MeasuredValueShort_create(NULL,
yc_table[i].ioa, current,
(yc_table[i].quality & 0x80) ?
IEC60870_QUALITY_INVALID :
IEC60870_QUALITY_GOOD);
CS101_ASDU_addInformationObject(asdu, io);
InformationObject_destroy(io);
has_data = true;
/* 避免单帧过大 (每帧最多 50 个点) */
if (CS101_ASDU_getNumberOfElements(asdu) >= 50) {
break;
}
}
}
if (has_data) {
IMasterConnection_sendASDU(connection, asdu);
}
CS101_ASDU_destroy(asdu);
connection = IMasterConnection_getNextActiveConnection(
slave, connection);
}
last_send_time = now;
}
5.4 数据处理实战
5.4.1 构造和解析各类数据速查
/* ══════════════════════════════════════════
* 快速参考: 常用信息对象的创建与解析
* ══════════════════════════════════════════ */
/* ── 单点遥信 (TI=1, M_SP_NA_1) ── */
/* 创建 */
InformationObject sp = (InformationObject)
SinglePointInformation_create(NULL, ioa,
value, /* 0=分, 1=合 */
quality); /* IEC60870_QUALITY_GOOD / INVALID / ... */
/* 解析 */
int sp_val = SinglePointInformation_getValue((SinglePointInformation)sp);
int sp_quality = SinglePointInformation_getQuality((SinglePointInformation)sp);
/* ── 双点遥信 (TI=3, M_DP_NA_1) ── */
/* 创建 */
InformationObject dp = (InformationObject)
DoublePointInformation_create(NULL, ioa,
value, /* 0=不确定, 1=分, 2=合, 3=中间 */
quality);
/* 解析 */
int dp_val = DoublePointInformation_getValue((DoublePointInformation)dp);
/* ── 短浮点遥测 (TI=13, M_ME_NC_1) ── */
/* 创建 */
InformationObject mv = (InformationObject)
MeasuredValueShort_create(NULL, ioa,
230.5, /* IEEE 754 单精度浮点 */
quality);
/* 解析 */
float mv_val = MeasuredValueShort_getValue((MeasuredValueShort)mv);
/* ── 单点遥信 SOE (TI=30, M_SP_TB_1) ── */
/* 创建 */
CP56Time2a ts;
CP56Time2a_createFromMsTimestamp(&ts, currentTimeMillis());
InformationObject soe = (InformationObject)
SinglePointWithCP56Time2a_create(NULL, ioa, value, quality, &ts);
/* 解析 */
CP56Time2a soe_ts = SinglePointWithCP56Time2a_getTimestamp(
(SinglePointWithCP56Time2a)soe);
/* ── 单点遥控 (TI=45, C_SC_NA_1) ── */
/* 创建 */
InformationObject cmd = (InformationObject)
SingleCommand_create(NULL, ioa,
scs, /* 0=分, 1=合 */
se, /* true=选择, false=执行 */
qu); /* 命令限定词, 通常为 0 */
/* 解析 */
int sco_byte = SingleCommand_getSCO((SingleCommand)cmd);
int cmd_scs = sco_byte & 0x01;
int cmd_se = (sco_byte >> 1) & 0x01;
/* ── 短浮点设点 (TI=50, C_SE_NC_1) ── */
/* 创建 */
InformationObject sp_cmd = (InformationObject)
SetpointCommandShort_create(NULL, ioa,
100.5, /* 目标值 */
false, /* SE=false (执行) */
qu); /* 限定词 */
/* 解析 */
float sp_val = SetpointCommandShort_getValue((SetpointCommandShort)sp_cmd);
5.4.2 品质位统一处理模块
/*
* 品质位处理: 统一判断数据是否有效可用
*/
/* 品质位定义 (与标准一致) */
#define QUALITY_INVALID 0x80 /* IV: 无效 */
#define QUALITY_NON_TOPICAL 0x40 /* NT: 非当前值 */
#define QUALITY_SUBSTITUTED 0x20 /* SB: 人工置数 */
#define QUALITY_BLOCKED 0x10 /* BL: 被闭锁 */
#define QUALITY_OVERFLOW 0x0F /* OV: 溢出 (遥测专用) */
/* 判断遥测值是否可信 */
const char *get_measurement_quality_label(uint8_t qds) {
if (qds & QUALITY_INVALID) return "无效";
if (qds & QUALITY_NON_TOPICAL) return "非当前值";
if (qds & QUALITY_SUBSTITUTED) return "人工置数";
if (qds & QUALITY_BLOCKED) return "被闭锁";
if (qds & QUALITY_OVERFLOW) return "溢出";
return "正常";
}
/* 判断遥信值是否可信 */
const char *get_signal_quality_label(uint8_t siq) {
if (siq & QUALITY_INVALID) return "无效";
if (siq & QUALITY_NON_TOPICAL) return "非当前值";
if (siq & QUALITY_SUBSTITUTED) return "人工置数";
if (siq & QUALITY_BLOCKED) return "被闭锁";
return "正常";
}
/*
* 数据过滤: 在显示或存储前过滤无效数据
*/
bool is_measurement_valid(uint8_t qds) {
/* 仅 IV=0 且 BL=0 时数据有效 */
return !(qds & (QUALITY_INVALID | QUALITY_BLOCKED));
}
bool is_signal_valid(uint8_t siq) {
return !(siq & (QUALITY_INVALID | QUALITY_BLOCKED));
}
5.4.3 CP56Time2a 时间处理
/*
* CP56Time2a 工具函数
*/
/* 将 struct tm 转换为 CP56Time2a (不依赖 lib60870 API) */
void tm_to_cp56time2a(struct tm *tm, int ms, CP56Time2a *out) {
memset(out, 0, sizeof(CP56Time2a));
out->millisecond = ms;
out->second = tm->tm_sec;
out->minute = tm->tm_min;
out->hour = tm->tm_hour;
out->day = tm->tm_mday;
out->month = tm->tm_mon + 1;
out->year = tm->tm_year - 100; /* 1900+ → 2000+ */
}
/* 手动解析 CP56Time2a 原始 7 字节 (不依赖 API) */
void parse_cp56time2a_raw(const uint8_t *data, struct tm *out_tm, int *out_ms) {
int ms_low = data[0];
int ms_high = data[1] & 0x7F;
*out_ms = ms_low | (ms_high << 8);
out_tm->tm_sec = 0;
out_tm->tm_min = data[2] & 0x3F;
out_tm->tm_hour = data[3] & 0x1F;
out_tm->tm_mday = data[4] & 0x1F;
out_tm->tm_mon = (data[5] & 0x0F) - 1; /* 1~12 → 0~11 */
out_tm->tm_year = (data[6] & 0x7F) + 100; /* 2000+ → 1900+ */
/* 有效性检查: bit7 of data[2] */
if (data[2] & 0x80) {
*out_ms = -1; /* 时间无效标记 */
}
}
5.5 双网冗余实现
5.5.1 Server 端双网监听
/*
* Server 双网冗余: 同时监听两个网段
* - 网段 A: 192.168.1.0/24 (主网络)
* - 网段 B: 192.168.2.0/24 (备网络)
*/
int start_dual_network_server(void)
{
CS104_Slave slave = CS104_Slave_create(10, 10);
CS101_AppLayerParameters al_params =
CS104_Slave_getAppLayerParameters(slave);
al_params->sizeOfIOA = 2;
al_params->sizeOfCOT = 2;
al_params->sizeOfCA = 2;
/* ★ 关键: 绑定到 0.0.0.0, 监听所有网卡 */
CS104_Slave_setLocalAddress(slave, "0.0.0.0");
CS104_Slave_setLocalPort(slave, 2404);
/* 回调注册 (与单网相同) */
CS104_Slave_setInterrogationHandler(slave, gi_handler, NULL);
CS104_Slave_setASDUHandler(slave, asdu_handler, NULL);
CS104_Slave_setClockSyncHandler(slave, clock_handler, NULL);
CS104_Slave_start(slave);
printf("双网 Server 启动: 0.0.0.0:2404\n");
printf(" 网段 A: 192.168.1.100:2404\n");
printf(" 网段 B: 192.168.2.100:2404\n");
/* 主循环 */
while (running) {
/* 发送数据时遍历所有连接 */
IMasterConnection conn = CS104_Slave_getActiveConnections(slave);
while (conn != NULL) {
const char *peer_ip = IMasterConnection_getPeerAddress(conn);
printf(" 活动连接: %s\n", peer_ip);
/* 向该连接发送数据 ... */
conn = IMasterConnection_getNextActiveConnection(slave, conn);
}
Thread_sleep(5000);
}
CS104_Slave_stop(slave);
CS104_Slave_destroy(slave);
return 0;
}
5.5.2 Client 端双连接 + 温备切换
/*
* Client 双连接 温备模式:
* - 主连接: 192.168.1.100
* - 备连接: 192.168.2.100
* - 主连接故障 → 自动切换备连接
*/
typedef enum {
REDUN_STATE_PRIMARY, /* 主连接工作中 */
REDUN_STATE_BACKUP, /* 备连接工作中 */
REDUN_STATE_FAILOVER, /* 正在切换中 */
} RedundantState;
typedef struct {
CS104_Master master;
char primary_ip[32];
char backup_ip[32];
int port;
RedundantState state;
bool primary_ok;
bool backup_ok;
} DualNetworkClient;
static DualNetworkClient redun_client;
/* 连接状态变化回调 */
static void redundant_connection_handler(void *parameter,
CS104_Master master, CS104_MasterConnectionEvent event)
{
DualNetworkClient *cli = (DualNetworkClient *)parameter;
switch (event) {
case CS104_CON_EVENT_ACTIVE:
printf("[冗余] 连接建立\n");
if (cli->state == REDUN_STATE_FAILOVER) {
cli->state = REDUN_STATE_BACKUP;
}
break;
case CS104_CON_EVENT_INACTIVE:
printf("[冗余] ★ 连接断开, 触发切换\n");
if (cli->state == REDUN_STATE_PRIMARY) {
/* 主连接断开 → 切换备连接 */
cli->state = REDUN_STATE_FAILOVER;
cli->primary_ok = false;
printf("[冗余] 主连接断开, 切换到备连接: %s\n",
cli->backup_ip);
CS104_Master_addSlave(cli->master,
cli->backup_ip, cli->port);
}
break;
}
}
/* 初始化双网 Client */
void init_dual_network_client(const char *primary_ip,
const char *backup_ip, int port)
{
strncpy(redun_client.primary_ip, primary_ip, 32);
strncpy(redun_client.backup_ip, backup_ip, 32);
redun_client.port = port;
redun_client.state = REDUN_STATE_PRIMARY;
redun_client.primary_ok = false;
redun_client.backup_ok = false;
redun_client.master = CS104_Master_create(NULL, NULL);
CS104_Master_setConnectionHandler(redun_client.master,
redundant_connection_handler, &redun_client);
/* 先连接主站 */
printf("[冗余] 连接主站: %s:%d\n", primary_ip, port);
CS104_Master_addSlave(redun_client.master, primary_ip, port);
}
5.5.3 冗余连接状态机
5.6 性能优化与嵌入式移植
5.6.1 大数据量场景优化
场景: 5000 个遥测点 + 2000 个遥信点
策略:
① GI 时分批上送 (每帧 50~100 个点) → 避免单帧过大
② 周期上送启用死区过滤 → 减少无效传输
③ 增大 k 值 (12 → 60) → 增大发送窗口
④ 适当增大 t1/t2 → 减少超时误判
分批 GI 实现:
/*
* 分批总召唤: GI 响应时每帧最多 max_per_frame 个点
*/
#define GI_MAX_PER_FRAME 80
void send_gi_data_batched(IMasterConnection connection,
CS101_ASDU request_asdu,
YC_Point *points, int count)
{
CS101_AppLayerParameters al_params =
IMasterConnection_getApplicationLayerParameters(connection);
int sent = 0;
while (sent < count) {
int batch_size = (count - sent > GI_MAX_PER_FRAME)
? GI_MAX_PER_FRAME : (count - sent);
CS101_ASDU asdu = CS101_ASDU_create(
al_params, false, CS101_COT_INTERROGATED_BY_STATION,
0, 1, false, false);
/* ★ SQ=1: 顺序地址模式, 节省 (batch_size-1) × IOA长度 字节 */
for (int i = sent; i < sent + batch_size; i++) {
InformationObject io = (InformationObject)
MeasuredValueShort_create(NULL,
points[i].ioa,
points[i].value,
IEC60870_QUALITY_GOOD);
CS101_ASDU_addInformationObject(asdu, io);
InformationObject_destroy(io);
}
IMasterConnection_sendASDU(connection, asdu);
CS101_ASDU_destroy(asdu);
sent += batch_size;
/* 小幅延时, 避免网络拥塞 */
Thread_sleep(5);
}
}
5.6.2 内存管理优化
/*
* 内存策略对比:
* ┌──────────┬─────────────────┬─────────────────┐
* │ 策略 │ 优点 │ 缺点 │
* ├──────────┼─────────────────┼─────────────────┤
* │ 动态分配 │ 灵活, 按需使用 │ 碎片, malloc 开销 │
* │ 内存池 │ 无碎片, 快速 │ 固定大小, 浪费 │
* │ 栈分配 │ 最快, 自动回收 │ 大小受限 │
* └──────────┴─────────────────┴─────────────────┘
*
* 推荐: 通信帧使用内存池, 点表使用动态分配
*/
/* 内存池示例: 预分配 I 帧缓冲区 */
#define MAX_I_FRAMES 64
#define MAX_FRAME_LEN 256
typedef struct {
uint8_t data[MAX_FRAME_LEN];
bool in_use;
} FrameBuffer;
static FrameBuffer frame_pool[MAX_I_FRAMES];
static pthread_mutex_t pool_mutex = PTHREAD_MUTEX_INITIALIZER;
FrameBuffer *frame_alloc(void) {
pthread_mutex_lock(&pool_mutex);
for (int i = 0; i < MAX_I_FRAMES; i++) {
if (!frame_pool[i].in_use) {
frame_pool[i].in_use = true;
memset(frame_pool[i].data, 0, MAX_FRAME_LEN);
pthread_mutex_unlock(&pool_mutex);
return &frame_pool[i];
}
}
pthread_mutex_unlock(&pool_mutex);
return NULL; /* 池已满 */
}
void frame_free(FrameBuffer *fb) {
pthread_mutex_lock(&pool_mutex);
fb->in_use = false;
pthread_mutex_unlock(&pool_mutex);
}
5.6.3 ARM Linux 交叉编译完整流程
# ═════════════════════════════════════════
# 目标: aarch64 (ARM64) Linux, 如树莓派 4B
# ═════════════════════════════════════════
# 1. 安装交叉编译工具链
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# 2. 编译 lib60870
cd lib60870/lib60870-C
mkdir build_arm64 && cd build_arm64
cmake .. \
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=OFF \
-DBUILD_EXAMPLES=OFF
make -j$(nproc)
# 3. 编译你的 Server 程序
aarch64-linux-gnu-gcc -o iec104_server_arm \
my_server.c \
-I/path/to/lib60870/lib60870-C/src/inc \
-L/path/to/lib60870/lib60870-C/build_arm64 \
-l60870 -lpthread -lm \
-static # ★ 静态链接, 避免缺少 .so
# 4. 查看生成的文件信息
file iec104_server_arm
# iec104_server_arm: ELF 64-bit LSB executable, ARM aarch64, ...
# 5. 部署到目标机
scp iec104_server_arm root@192.168.1.200:/opt/rtu/
# 6. 在目标机上运行
ssh root@192.168.1.200
chmod +x /opt/rtu/iec104_server_arm
/opt/rtu/iec104_server_arm
5.6.4 连接参数调优速查
/*
* 不同场景的参数推荐配置
*/
typedef struct {
int w; /* 最大未确认 I 帧数 */
int k; /* 最大接收 I 帧数 */
int t1; /* 发送确认超时 (ms) */
int t2; /* 确认间隔超时 (ms) */
int t3; /* 测试帧间隔 (ms) */
} ConnectionTuning;
static ConnectionTuning tuning_table[] = {
/* 场景 w k t1(ms) t2(ms) t3(ms) */
[0] = { 12, 8, 15000, 10000, 20000 }, /* 默认 */
[1] = { 24, 16, 10000, 5000, 15000 }, /* 局域网低延迟 */
[2] = { 24, 16, 30000, 20000, 60000 }, /* 广域网高延迟 */
[3] = { 60, 32, 15000, 10000, 30000 }, /* 大量遥测点 */
[4] = { 8, 8, 60000, 30000, 120000 }, /* 卫星/4G 通道 */
};
void apply_connection_tuning(CS104_Slave slave, int scenario)
{
ConnectionTuning *t = &tuning_table[scenario];
CS101_AppLayerParameters al_params =
CS104_Slave_getAppLayerParameters(slave);
al_params->maxOutstandingIPDUs = t->w;
al_params->maxSupplRespAfterPDU = t->k;
CS104_Slave_setAppLayerParameters(slave, al_params);
CS104_Slave_setConnectionTimeout(slave, t->t1);
CS104_Slave_setAckTimeout(slave, t->t2);
CS104_Slave_setTestTimeout(slave, t->t3);
}
5.7 完整工程架构
5.8 开发检查清单
| 检查项 | 说明 | 状态 |
|---|---|---|
| IOA 长度配置 | Server 和 Client 的 sizeOfIOA 必须一致 | ☐ |
| COT 长度配置 | 2 字节 COT (sizeOfCOT=2) 是主流 | ☐ |
| 公共地址配置 | sizeOfCA=1 或 2, 双方一致 | ☐ |
| k/w 参数 | w ≤ k, 非平衡配比不宜过大 | ☐ |
| 回调线程安全 | GI/ASDU 回调中不要做阻塞操作 | ☐ |
| 遥控 Select 超时 | 选择后 15~30s 未执行应自动取消 | ☐ |
| 品质位处理 | IV=1 时忽略值, BL=1 时告警 | ☐ |
| SOE 缓冲区 | 避免变位事件丢失 | ☐ |
| 连接断开重连 | 指数退避, 最大间隔不超过 60s | ☐ |
| 双网测试 | 拔网线测试主备切换是否正常 | ☐ |
| 时钟同步 | 连接后 + 周期对时必须实现 | ☐ |
| 死区配置 | 遥测死区合理, 避免数据风暴或漏报 | ☐ |
| 日志记录 | 关键事件(连接/总召/遥控)必须有日志 | ☐ |
5.9 小结
| 知识点 | 掌握程度 | 核心要点 |
|---|---|---|
| lib60870 项目结构 | 理解 | lib60870-C 核心库, 各语言绑定 |
| Server 开发流程 | 熟练 | 创建→配置参数→注册回调→启动监听 |
| GI 回调处理 | 掌握 | 四阶段: 确认→遥信→遥测→终止 |
| ASDU 回调 (遥控处理) | 掌握 | 遍历信息对象, 区分 Select/Execute |
| SOE 突发上送 | 掌握 | TI=30, COT=3, 带 CP56Time2a |
| Client 开发流程 | 熟练 | 创建→配置→连接→总召→解析 |
| 遥控 Select-Execute | 掌握 | SE=1 选择 → SE=0 执行, 两阶段防误 |
| 品质位处理 | 掌握 | IV=1 忽略, BL=1 告警, SB=1 标记 |
| 死区变化量上送 | 理解 | 变化量 ≥ 死区 → 上送 |
| 双网冗余 (Server) | 掌握 | bind 0.0.0.0, 遍历所有连接发送 |
| 双网冗余 (Client) | 掌握 | 温备模式: 断连→切换→GI |
| ARM 交叉编译 | 掌握 | CMake toolchain + 静态链接 |
| 连接参数调优 | 理解 | 5 种场景的 w/k/t1-t3 配置 |
下期预告
[Part 6:工程实战] 将覆盖从开发到投运的完整链路:
- 系统集成流程(设备接入→点表配置→通信联调→对点测试→投运)
- 数据点表设计与管理(Excel → 配置自动生成)
- 网络架构与冗余设计
- Wireshark 深度排障(7 种典型故障案例分析)
- IEC 62351 安全加固
- 典型工程案例(配电站接入、双网切换、61850↔104 网关)
参考资源
- lib60870 官方仓库:https://github.com/mz-automation/lib60870
- lib60870 API 文档:https://www.mz-automation.de/communication/lib60870/
- IEC 60870-5-104 标准:第 9 节(互操作性配置)
推荐工具
- CMake + GCC/Clang —— 编译构建
- aarch64-linux-gnu-gcc —— ARM 交叉编译
- Wireshark —— 验证 Server/Client 通信报文
- Valgrind —— 内存泄漏检查(
valgrind --leak-check=full ./iec104_server)strace—— 跟踪系统调用(排查 Socket 相关问题)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)