智能变电站协议系列-5、IEC 104协议细化解读(IEC 60870以及如何获取对应国标和行标)

316 阅读1小时+

一、前言

通过之前整体性的协议分析,目前确定先基于IEC104做深入分析,来结合分析电网常见的业务,以此从协议侧关联深入到业务侧。在国内该标准也应用比较稳定和广泛了,所以研究104协议相关资料也会更全一些。

二、资料及标准收集

除了一些网上找到的总结文档,主要还是要参考国标/行标来分析总结。

一些参考文章:

www.cnblogs.com/yking/p/176…

www.cnblogs.com/yking/p/176…

www.jianshu.com/p/772582a9d…

blog.csdn.net/chenyitao73…

zhuanlan.zhihu.com/p/66988575

国标、行标的下载方法及网站(现在大部分都要开会员):

www.bzfxw.com/down63_1934…

mp.weixin.qq.com/s/H6wDjbdhi…

zhuanlan.zhihu.com/p/82467306

gcbz.org/

一些标准就不具体给地址了,具体可能需要参考标准:

建议查看对照的标准:

  • DL/T 634.5104-2009 远动设备及系统 第5-104部分传输规约 采用标准传输规约集的IEC 60870-5-101
  • DL/T 634.5101-2002 远动设备及系统 第5-101部分传输规约 基本远动任务配套标准
  • GB/T 18657.5 远动设备及系统 第 5部分:传输规约 第 5篇:基本应用功能
  • GB/T 18657.4 远动设备及系统 第 5部分:传输规约 第 4篇:应用信息元素的定义和编码
  • GB/T 18657.3 远动设备及系统 第 5部分:传输规约 第 3篇:应用数据的一般结构
  • GB/T 18657.2 远动设备及系统 第 5部分:传输规约 第 2篇:链路传输规则
  • GB/T 18657.1 远动设备及系统 第 5部分:传输规约 第 1篇:传输帧格式

“电气技术”在线术语数据库(IEV):www.electropedia.org/iev/iev.nsf…

语言选择Chinese即可,包括远动、摇测等术语。

三、协议细化解读及结合报文帧再分析

1、ISO/OSI网络模型及报文帧整体情况

ISO/OSI网络模型:

IEC:

国标:

根据标准说明不难理解,104是将101的应用层与TCP/IP提供的传输功能进行了结合。由于传输层是走TCP方式,TCP为流式传输,为了区分ASDU的启动或停止,增加了APCI,用于检出ASDU的启动和结束。对于整体的TCP报文又通过启动字符68H定义起点,并增加长度字段,这段报文整体就是APDU(这样对报文的理解的就比较清楚了:报文头+报文长度+报文内容+也许还有校验字节+也许还有报文尾,不管我们是串口通信还是这类网络通信大部分时候都可以这么类比来理解,我想即使是电报时代也大概类似吧,否则一串不知道头尾的内容很难翻译)。

  • APCI:应用协议控制信息
  • ASDU:应用服务数据单元
  • APDU:应用协议 数据单元

应用数据的一般结构:APDU->APCI+ASDU

APCI:

ASDU(数据单元标识符+信息体+应用服务数据单元公共时标):

这下根据报文APDU就引入了APCI和ASDU,继续往下分别分析APCI和ASDU。

2、APCI控制域及三种帧格式(I、S、U)

  • 启动字符68H(0x68)定义了数据流中的起点。
  • APDU的长度域定义了APDU体的长度,包括APCI的四个控制域八位位组和ASDU。
  • ASDU的最大长度限制在249以内,因为APDU域的最大长度是253(APDU长度最大值=255-启动字符-长度),控制域的长度为4个八位位组。
  • 控制域定义了保护报文不至丢失和重复传送的控制信息、报文传输启动/停止以及传输连接的监视等控制信息。
  • 三种类型的控制域格式用于编号的信息传输(I格式)、编号的监视功能(S格式)和未编号的控制功能(U格式)。
  • 控制域第一个八位位组的比特1=0定义I格式,I格式的APDU总是包含一个ASDU。I格式的控制信息如下图2所示。

实际报文对照:

  • 控制域的第一个八位位组的比特1=1并且比特2=0定义S格式。S格式的APDU只包括APCI。S格式对的控制信息如图7所示。

  • 控制域的第一个八位位组的比特1=1并且2=1定义了U格式。U格式的APDU只包括APCI U格式的控制消息,如图8所示。在同一时刻,TESTFR、STOPDT或STARTDT中只有一个功能可以被激活。

3、防止报文丢失和报文重复

发送序列号N(S)和接收序列号(R)的使用域ITU-T X.25定义的方法一致。两个序列号在每个APDU和每个方向上都应按顺序加一。发送方增加发送序列号N(S),接受方增加接收序列号N(R)。接收站认可接收的每个APDU或者多个APDU,将最后一个正确接收的APDU的发送序列号加1作为接收序列号返回。发送站把一个或多个APDU保存在保存在缓存区中,直到它收到接收序列号,这个接收序列号是对所有发送序列号小于该号的APDU的有效确认,这时就可以删除缓冲区里已正确传送过的APDU。如只在一个方向进行较长的数据传输,应在另一个方向发送S格式认可这些APDU。这种方法在两个方向上都适用。在建立一个TCP连接后,发送和接收序列号都应该被置为0。

V(S)---发送状态变量

V(R)---接收状态变量

ACK ---指示DTE已经正确收到所有小于或等于这个编号的I格式的APDU

I(a,b) ---I格式的APDU,a=发送序列号,b=接收序列号

S(b) ---S格式的APDU,b=接收序列号

U ---未编号的U格式的APDU。

4、测试过程

未使用但已打开的连接可通过发送测试APDU(TESTFR=act)并由接收站发送TESTFR=con,在两个方向上进行周期性测试。发送站和接收站在规定时间段内没有数据传输(超时)均可启动测试过程。

一般可以通过发送测试信息来确认对端是否断开,如果超时时间内未收到对应的TESTFR的con就可以在应用层主动关闭连接,相当于一层应用层控制,实际上一般STARTDT和STOPDT就够用了,这个很少用,所以lib60870的demo没有实现对应的TESTFR的发送,但是其定了结构,我上节总结没有找到方法,这一节自己实现了一下,然后测试抓包发现可以正常添加使用。

在cs104_connection.c中添加该函数:

void
CS104_Connection_sendTestFR(CS104_Connection self)
{
#if (CONFIG_USE_SEMAPHORES == 1)
    Semaphore_wait(self->conStateLock);
#endif /* (CONFIG_USE_SEMAPHORES == 1) */

    self->conState = STATE_WAITING_FOR_TESTFR_CON;

#if (CONFIG_USE_SEMAPHORES == 1)
    Semaphore_wait(self->socketWriteLock);
#endif
    writeToSocket(self, TESTFR_ACT_MSG, TESTFR_ACT_MSG_SIZE);
#if (CONFIG_USE_SEMAPHORES == 1)
    Semaphore_post(self->socketWriteLock);
#endif

#if (CONFIG_USE_SEMAPHORES == 1)
    Semaphore_post(self->conStateLock);
#endif /* (CONFIG_USE_SEMAPHORES == 1) */
}

在cs104_connection.h中添加声明:

void
CS104_Connection_sendTestFR(CS104_Connection self);

在simple_client.c中调用测试:

int
main(int argc, char** argv)
{
    const char* ip = "localhost";
    uint16_t port = IEC_60870_5_104_DEFAULT_PORT;
    const char* localIp = NULL;
    int localPort = -1;

    if (argc > 1)
        ip = argv[1];

    if (argc > 2)
        port = atoi(argv[2]);

    if (argc > 3)
        localIp = argv[3];

    if (argc > 4)
        port = atoi(argv[4]);

    printf("Connecting to: %s:%i\n", ip, port);
    CS104_Connection con = CS104_Connection_create(ip, port);

    CS101_AppLayerParameters alParams = CS104_Connection_getAppLayerParameters(con);
    alParams->originatorAddress = 3;

    CS104_Connection_setConnectionHandler(con, connectionHandler, NULL);
    CS104_Connection_setASDUReceivedHandler(con, asduReceivedHandler, NULL);

    /* optional bind to local IP address/interface */
    if (localIp)
        CS104_Connection_setLocalAddress(con, localIp, localPort);

    /* uncomment to log messages */
    //CS104_Connection_setRawMessageHandler(con, rawMessageHandler, NULL);

    if (CS104_Connection_connect(con)) {
        printf("Connected!\n");

        CS104_Connection_sendStartDT(con);

        Thread_sleep(2000);

        CS104_Connection_sendInterrogationCommand(con, CS101_COT_ACTIVATION, 1, IEC60870_QOI_STATION);

        Thread_sleep(5000);

        struct sCP56Time2a testTimestamp;
        CP56Time2a_createFromMsTimestamp(&testTimestamp, Hal_getTimeInMs());

        CS104_Connection_sendTestCommandWithTimestamp(con, 1, 0x4938, &testTimestamp);

#if 0
        InformationObject sc = (InformationObject)
                SingleCommand_create(NULL, 5000, true, false, 0);

        printf("Send control command C_SC_NA_1\n");
        CS104_Connection_sendProcessCommandEx(con, CS101_COT_ACTIVATION, 1, sc);

        InformationObject_destroy(sc);

        /* Send clock synchronization command */
        struct sCP56Time2a newTime;

        CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());

        printf("Send time sync command\n");
        CS104_Connection_sendClockSyncCommand(con, 1, &newTime);
#endif

        printf("Wait ...\n");
        //TESTFR测试
        CS104_Connection_sendTestFR(con);

        Thread_sleep(1000);
        //STOPDT测试
        CS104_Connection_sendStopDT(con);
    }
    else
        printf("Connect failed!\n");

    Thread_sleep(1000);

    CS104_Connection_destroy(con);

    printf("exit\n");
}

编译并运行测试后抓包:

5、采用启/停的传输控制

控制站(如A站)可以有效的利用STARTDT(启动数据传输)和STOPDT(停止数据传输)来控制被控站(B站)的数据传输。例如,当在站间有超过一个以上打开的连接可利用时,一次只有一个连接可用于数据传输。定义STARTDT和STOPDT的功能在于从一个连接切换到另一个连接时避免数据丢失。STARTDT和STOPDT还可与站间的单个连接一起用于控制连接的通信量。

连接建立后,被控站不会自动使能连接上的用户数据传输,即当一个连接建立时,STOPDT是缺省状态。在这种状态下,除了未编号的控制功能和对这些功能的确认,被控站不通过这个连接发送任何数据。控制站应通过这个连接发送STARTDT激活指令激活这个连接中的用户数据传输。被控站用STARTDT确认响应这个命令。如果STARTDT没有被确认,被控站将关闭这个连接。站初始化之后,STARTDT必须总是在来自被控站的任何用户数据传输(例如,总召唤信息)开始发送。被控站只有在发送STARTDT确认后才能发送任何待发用户数据。

STARTDT/STOPDT是一种控制站激活/解除激活监视方向的机制,即使没有收到激活确认,被控站也可以发送命令或者设定值。发送和接收计数器继续计数,不依赖于STARTDT/STOPDT的使用。

其它的被控站的开始/停止状态切换及控制站的开始/停止状态切换流程图如下(详细的处理过程参考标准说明,描述的还是挺详细的):

6、端口号

每个TCP地址由一个IP地址和 一个端口号组成。标准端口为2404,已由IANA确认 。

7、未被确认的I格式的APDU最大数目(k)

k表示在某一特定的事件内未被DTE确认(即不被承认)的连续编号的I格式APDU的最大数目。每一I格式帧都按顺序编好号,从0到模数n减1。以n为模的操作中k值永远不会超过n-1。

  • 当未确认I格式的APDU达到k个时,发送方停止传送。
  • 接收方达到w个I格式的APDU 后确认。
  • 模n操作时k的最大值是n-1。

k值的最大范围:1到32767(-1)APDU,精确到一个APDU。

w值的最大范围:1到32767 APDU,精确到一个APDU(推荐:w不应超过2/3的k)。

8、ASDU的结构和类型

根据GB/T 18657.3(IEC 60870-5-3)中对应用数据的一般结构的描述,我们能更详细的了解ASDU(应用服务数据单元)的结构情况及各个字段的含义。

8.1 应用服务单元ASDU

应用服务数据单元(ASDU)由数据单元标识符和信息体组成:

8.2 数据单元标识符

数据单元标识符(DATA UINT IDENTIFIER)由类型标识(TYPE IDENTIFICATION),应用服务数据单元长度(LENGTH OF ASDU,选用)、可变结构限定词(VARIABLE STRUCTURE QUALIFIER,选用)、传送原因(CAUSE OF TRANSMISSION,选用)、应用服务数据单元的公共地址(COMMON ADDRESS OF ASDU,选用)组成。

类型标识(TYPE IDENTIFICATION)、应用服务数据单元长度(LENGTH OF ASDU)和可变结构限定词(VARIABLE STRUCTURE QUALIFIER)统称为数据单元类型(DATA UINT TYPE)。

类型标识是一个码,再各种协议集活系统的可能类型中明确标识应用服务数据单元的类型。如有应用服务数据单元长度,它以八位位组表示应用服务数据单元的总长度。如有可变结构限定词,它表示应用服务数据单元在各种通信情况中的结构变化。类型标识能使接收的 应用服务将各数据单元发往正确的应用进程,以便应用进程处理指明的数据单元类型。它也能使接收的应用进程观察数据单元包含哪种类型数据,并从当地表格中确定其结构。如有数据单元标识符,类型标识是唯一的非选用的域元素。

传送原因如未明确定义,也可以包含在数据单元类型中。

如定义了应用服务数据单元的公共地址,它常位于信息体的前面。

类型标识目前定义大致如下:

在控制方向传送过程信息给指定站时,可以带或者不带时标,但对某一给定的站,两者不可混合发送。

注:在控制方向上具有”CON“标记的ASDU是被确认的应用服务,在监视方向上可以用不同的传送原因镜像同样的报文内容。这些镜像ASDU用作肯定或否定认可(确定)。

8.3 信息体

应用服务数据单元可包含一个或多个信息体。信息体的一般结构如图8所示。

信息体可由信息体标识符和信息元素集组成。信息体标识符可由信息体类型和信息体地址组成。

如信息体的结构不同,又未在数据单元类型中定义,可定义信息体类型。

每个信息体可以选择地加上信息体时标。如规定信息体时标,常将它排在信息体的最后。

8.4.1 信息体标识

远动系统中信息体的标识应支持各种可能配置。简单的远动系统只需物理地址就可标识信息体 地址的结构常使它们可以代表被控制的进程的镜像。各种识别原则一般由相应的标准数据模型考虑。标准数据模型的详细定义或选择由具体应用方式的标准协议集规定。

为在各种远动过程中实现高的数据传输效率,定义了一种通用的数据结构,如图 6、图了、图 8所示。 信息体一般由数据单元类型(或信息体类型)和应用服务数据单元的公共地址(或信息体地址)标识。在 紧凑的表示方法中,公共地址可以包含在数据单元类型中,和信息元素集一起传输。也可以采用将数据 单元类型、传送原因和应用服务数据单元公共地址综合到数据单元标识符中(见图9)等其他方法。多级 的结构性地址(见图10)也是允许的 但不论采用哪种方法,都应采用图6、图 7、图8中列出的顺序。

每个信息体由数据单元标识符标识。数据单元标识符可以采用表 1的结构。信息体的标识可以采用在信息体标识表上加指针的方法。信息体组也可以由组的标识定义 标识表可以包含附加的信息体属性,为信息体规定固定的赋值,如物理地址等,如图9所示。信息体属性也可以为信息元素定义。

8.4.2 信息体地址

非结构性地址以从一个数集中选择的数区分不同的信息体。

结构性地址考虑了技术的、物理的、拓扑的或地理的结构以标识信息体。这种方式应为每一级定义充分的地址空间,以便每一级最大地扩展。

在生成系统或修改系统配置时将地址赋予信息体。

8.4.3 信息元素集

有下列三种信息元素集(图 11):

第1种信息元素集由单个信息元素组成,由相应的信息体地址或应用服务数据单元公共地址标识单个信息元素的例子是命令、事件、状态值或模拟值等。

第 2种信息元素集由定义的一组相同的信息元素集组成(例如,同一格式的测量值)。这种信息体的地址或应用服务数据单元的公共地址是该序列第 1个信息元素的相应地址,后面的信息元素由预先定义的序列地址方案标识。

第 3种信息元素集由定义的一组不同的信息元素集组成(例如,表示一个电力馈线状态的模拟值和数字值的组合)。这种情况的信息体地址或应用服务数据单元的公共地址是整个信息体的相应地址,每个信息元素由预先定义的结构方案标识。

信息元素是变量,在传输时由预先定义的数据类型和编码表示。变量的类型为布尔、整数、实数、比特串(位串)、八位位组串和综合类型等。GB/T 18657.4给出了常用的信息元素的规范建议。

8.4 构造应用服务数据单元的导则

应用服务数据单元用于包含在通过通信服务的通信中的应用过程之间的数据交换。基于本标准构成的协议集包含这些应用服务数据单元。数据交换所需的主要基本过程在GB/T 18657.5中规定。

各应用服务数据单元由域元素组成。域元素由在GB/T 18657.4中规定的整数、布尔、比特串等语法数据类型定义。此外,信息元素和时标的语义定义也在 GB/T 18657.4中描述,并在应用协议集中规定。下面的规范用文本块图和语法描述方法说明在GB/T 18657.4中定义的域元素和它们的功能目的。

基于一般结构的具体的应用服务数据单元的规范按以下步骤确定。规范不需包括 5.1中定义的所有域元素,例如,可变结构限定词可以省略。

在构造应用服务数据单元之前,分析应用服务数据单元所属的特定协议集的任务是非常重要的,即应知道定义信息类别、信息容量、需要的精度(例如,测量值的准确度:11比特+符号)、地址的结构等规范。 定义这些约束条件后,就可按下列步骤构造应用服务数据单元。

如图5所示,几个应用服务数据单元可以组成一个应用规约数据单元(APDU)。简单的情况是1个应用规约数据单元只有 1个应用服务数据单元。这意味着应用服务数据单元和应用规约数据单元是相同的。

8.4.1 步骤1:数据单元标识符域元素的选择

步骤1选择用于有关的应用服务数据单元的域元素,可以省略选用的域元素,应遵守通用结构定义的域元素的顺序。建议从一个应用协议集的所有应用服务数据单元中选择域元素的公共集。

例:一个具体的应用协议集的数据单元标识符由下列域元素组成(图 12)。

8.4.2 步骤2:数据单元标识符域元素长度的选择

步骤 2规定域元素的长度。域元素可由1个或多个八位位组组成。或者,1个八位位组可包含两个或多个域元素,或 1个域元素可能分配到几个八位位组的几部分中。不论如何,只要可能,建议规定每个域元素的八位位组的总数。一个协议集中各应用服务数据单元的类型标识的长度应相同。此外,在一个具体的协议集中,建议各应用服务数据单元的数据单元标识符的其他域元素的长度相同。

例:上例协议集的应用服务数据单元的域元素的长度为(图13):

8.4.3 步骤3:数据单元标识符数据类型的定义

步骤 3规定域元素的数据类型。数据类型为整数型、布尔型等。

注 :一个域元素可由几种数据类型组成 。在一个具体协议集中 建议只用一种数据类型定义数据单元标识符的各域元素。

例:定义下列数据类型〔图 14)。

数据单元标识符:=CP40 {类型标识,应用服务数据单元的长度,传送原因,应用服务数据单元的公共地址 }

类型标识 :=UI8[1.. 8]

应用服务数据单元的长度 := UI8[1. .8]

传送原因:=CP8{UI6[1..6],BS2[7..8]}

应用服务数据单元的公共地址:=UI16[1. .16]

8.4.4 步骤4:信息体的定义

每个信息体可由信息体类型、信息体地址、信息元素集和信息体时标组成(见图8)。 如个别信息体类型和信息体时标域元素需由特定的协议集定义,它们应在以上步骤中规定。常用的域元素和时标在GB/T 18657.4中规定。本标准5.1.5已定义信息元素集可以是单个信息元素、信息元素序列或信息元素组合。这些信息元素由应用服务数据单元的公共地址或信息体地址寻址。在下面的例子中信息元素由应用服务数据单元的公共地址寻址。信息元素的语法描述方法引自GB/T 18657.4。

  • 例 1:

单个信息元素(仅有一个信息元素,图 15)

宽度为 2的比特串:=BS2[1..2],或

8比特带符号的整数:=I8[1..8],或

7比特无符号带差错指示的整数:=CP8{UI7,BS1}

  • 例 2:

信息元素序列(几个相同数据类型的信息元素,图16).

  • 例 3:(图 17)

  • 例 4:

信息元素组合(几个不同的信息元素,图 18)。

7比特无符号整数,宽度为1的1个比特串,宽度为2的4个比特串:=CP16{U17[1..7],BS1[8],BS2[9..10],BS2[11..12],BS2[13..14],BS2[15..16]}

特定协议集中使用的所有信息体均应按这种方法规定。

8.4.5 步骤5:对信息体赋予类型标识和语义定义

步骤 5定义域元素的值的功能解释。

  • 类型标识(图 18)

如表1的规定,以上定义的信息体由该域元素选择。

例 :

类型标识:=UI8[1..8]<0.255>

<0>:=未用

<1>:=信息体1:8个单点信息

<2>:=信息体2:8个8比特的测量值

<3>:=等等

  • 应用服务数据单元的长度

该域元素规定应用服务数据单元(包括全部的域)的八位位组的数目。

例 :

应用服务数据单元的长度:=UI8[1. . 8]<0. . .255>

应用服务数据单元的长度在八位位组内由0^-255的数规定,即由一个长度八位位组UI8规定。

  • 传送原因

该域元素对相同的应用服务数据单元赋予不同的传送原因。因此请求的或自发的数据可以用相同的数据单元类型传输,由传送原因域元素区别。

例 :

6比特无符号整数和宽度为 2的 1个比特串

传送原因:=CP8{U16[1..6],BS2[7...8]}

U16[1..6]<0...63>

<0>:=自发数据

<1>:=循环数据

<2>:=请求数据

<3>:=等等

BS2[7] :=LS=当地服务 LS<0>:=远方

LS<1>:=当地

BS2[8]:=TE=测试(Test) TE<0>:=不测

TE<1>:=测试

  • 应用服务数据单元的公共地址

以该结构性或非结构性域元素(见5.1.2)作信息体地址。如信息体无具体的信息体类型和地址,则以应用服务数据单元的公共地址直接作信息元素集地址。

例 :

应用服务数据单元的公共地址:=UI16[1...16]<0...65535>

以范围为0~65535的整数作不同的信息元素集的地址。

如这些例子所示,建议用表格为应用协议集的各域元素定义。该表格应表示出可能的值的范围并定义采用的值的功能说明。

9、基本应用功能及对应字段

帧基本格式确定了之后,通过定义的基本应用功能,对应的流程和字段也有不同的标准定义。

控制站等同于客户(连接者),被控站为服务器(监听者)。

本章定义了利用标准通信服务的各种基本应用功能。用在控制站和被控站之间交换的数据单元的序列的图以及完成这些功能的数据单元的任务的方法来描述这些功能。首先描述以站初始化和用问答式(查询)方法采集数据两种基本应用功能 这两种功能是执行其他基本应用功能的基础,由具体应用和下面详细描述的链路服务配合完成。其他基本应用功能可能涉及查询过程,描述时不再重复过程的细节 。

用箭头表示传输过程的顺序,每个箭头代表一个规约数据单元(PDU)。用分层的字母结构形式为应用规约数据单元或应用服务数据单元命名。命名可由各配套标准补充完整。因为没有明确的应用规约控制信息(APCI),应用服务数据单元(ASDU)和应用规约数据单元(APDU)在 GB/T 18657规约定义中是相同的。

9.1 标号规则

下面按分层规则规定应用服务数据单元标号。它提供了这样的可能性:在本标准中采用全局标号,在不同配套标准中采用具体标号。

  • 最高一级为:
第 1信息级的种类标号
监视信息M
控制信息C
参 数P
文件传输F
  • 第2级定义:
第2信息级的种类标号
监视信息M
单点信息M_ SP
双点信息M_ DP
测量值M_ME
继电保护事件M_ EP
累计量M_IT
步位置信息M_ ST
比特和八位位组串M_ B
初始化结束M _ EI
可用的应用层M_ AA
控 制 信 息C
单点命令C_ SC
双点命令C_DC
设点命令C_ SE
步调节命令C_RC
召唤命令C_ IC
时钟同步命令C_CS
延时采集C_ CD
计数值召唤命令C_CI
测试命令C_ TS
复位进程命令C_RP
读命令C_RD
初始化结束C_ El
参 数P
测量值参数P_ME
参数激活P_AC
文件传输F
目录F_DR
文件、节或目录的选择或召唤F_SC
最后的节或段F_LS
文件或节的认可F_AF
文件准备好F_FR
节准备好F_SR
F_SG
  • 第3级为各配套标准采用,定义应用服务数据单元的具体类型、时标的应用等。第3级的第1个字母指明是否使用时标(N=无时标,T=有时标),第2个字母指明类型。每一个配套标准可从“A”开始用字母顺序定义自己的类型,例如 :
说明标号
不带时标的归一化的测量值(类型A)M_ME _NA
带时标有标度的测量值(类型 B)M_ME_TB
单点命令,类型A不带时标C_SC_NA

此外,最后的数字表示哪一个配套标准定义了应用服务数据单元的标号。例如:

说明标号
配套标准 101M_ME_NA_1或 C_SC_NA_1
配套标准 102M_ME_NA_2或 C_SC_NA_2

这标号方法是开放的,如需要,不同的配套标准可在各层次补充完整。

控制方向的应用服务数据单元可以在监视方向形成镜像。这些应用服务数据单元的镜像用于肯定或否定认可。为明确区分控制和监视两个方向,需要在标号的基础上附加说明。用下列缩写符分别标记两个方向的应用服务数据单元。

说明标号
控制方向:激活ACT
监视方向:激活确认ACTCON
控制方向:停止激活DEACT
监视方向:停止激活确认DEACTCON
监视方向:激活终止ACTTERM

还可采用下列缩写符:

说明标号
监视方向:循环传输CYCLIC
监视方向:突发(自发)传输SPONT

采用非平衡传输过程时,激活(ACT)可由发送/无回答(SEND/NO REPLY)链路服务作为广播报文传输(例如,站召唤或时钟同步)。这时,每一个接收到激活(ACT)的被控站应分别传输激活确认(ACTCON)到控制站。

9.2 站初始化(GB/T 18657.5的6.1.5~6.1.7)

和应用相关的远动操作开始工作前,需用站初始化过程将站设置成正确的工作状态。 应区分冷启动和热启动两种过程。冷启动是站的主要启动引导过程,在按实际状态刷新数据库之前先将过程变量信息清除。热启动是站复位或者重新激活的重新引导过程,不清除在重新激活前采集的过程变量信息。另一个区别是控制站的初始化和被控站的初始化。以下规定主要考虑涉及站间数据传输的初始化过程。

控制站通常配备有冗余的控制设备和数据库,以保证正在工作的控制设备因故障而切换时不丢失信息。这种情况不需要启动总召唤刷新控制站的数据库。只在刚合上电源或者整个控制站复位后,总召唤以及在一些系统中的时钟同步过程才是不可少的。

被控站可能由当地命令控制复位或接受控制站的请求而复位。

这里以DL/T 634.5104中的描述为准来具体分析。

连接的释放既可由控制站也可由被控站提出,连接的建立有两种方式:

  • 由一对控制站和被控站中的控制站建立连接。
  • 两个平等的控制站,固定选择(参数)其中一个站建立连接(端到端方式)。

图19显示关闭一个已建立的连接,首先由控制站向其TCP发出主动关闭请求,接着被控站向其TCP发出被动关闭请求。图19接着显示建立一个新连接,首先由控制主站向其TCP发送主动打开请求,接着被控站向其TCP发出被动打开请求。最后图19显示可选择由被控站主动关闭连接。

图20显示控制站初始化时依次与每一个被控站建立连接。由子站1开始,控制站向TCP发出主动打开请求,如果被控站的TCP有监听状态(状态未显示在图中),连接就建立起来了。其他的被控站也重复相同的过程。

图21显示控制站反复尝试与被控站建立连接。直到被控站完成本地的初始化,向TCP发出被动打开请求,取得监听状态,连接才成功。

图22显示控制站向TCP发出主动打开请求建立连接,然后向被控站发出复位进程命令,被控站返回确认并向TCP发出主动关闭请求。控制站向TCP发出被动关闭请求后连接被释放,然后控制站向TCP循环发出主动打开请求,试着连接被控站。当被控子站完成初始化并再次可用,被控站返回CLT=SYN,ACK。当控制站确认CTL=SYN,ACK后,连接建立。

9.3 用查询方式采集数据(GB/T 18657.5的6.2)

工作在非平衡传输过程的数据采集系统系统采用查询方式进行数据采集,以各被控站的过程变量的实际状态刷新控制站保存的数据。控制站顺序地召唤各被控站,被控站只在被查询时才传输。

请求1级和2级用户数据是GB/T 18657.2的链路功能,无法用于本标准中。但是可以按照GB/T 18657.5中图10底部所示的方法读取(请求)数据。因为循环请求数据会加重网络传输负担,因此尽管允许,也应尽量避免。

循环或非循环查询方式的各种可能查询过程如图10所示。

第1种过程为控制站的通信服务发送“请求1级用户数据”,被控站回答否定认可(NACK)。这过程发生在事件采集中没有事件等待传输时。

第2种过程为控制站发送“请求2级用户数据”到一个被控站,该站返回了数据。返回的数据在控制站以A_USER_CLASS2.ind传递给应用功能,其要求访问位(ACD-bit)=1(见GB/T 18657.2-2002的5.1-2),即被控站向控制站表示有1级用户数据,请求控制站发送“请求 1级用户数据”。

第3种过程,控制站的应用功能产生A_RD_DATA(读数据)的请求,它以C_RD_PDU(发送/确认链路服务)发送到被控站。然后请求的数据被“请求1级用户数据”查询,以M_PDU传输到控制站,并以A_M_DATA.ind传递给用户层。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_RD_DATA.req发送C_RD
A_RD_DATA.ind接收C_RD
A_M_DATA.req发送M
A_M_DATA.ind接收M

9.4 循环数据传输(GB/T 18657.5的6.3)

循环数据传输用于平衡和非平衡传输方式远动系统的传输过程,为过程变量当前值提供连续刷新功能。这过程的优先级一般较低,可被事件触发的通信请求中断。

被控站的应用进程循环地将过程变量实际值写人缓冲区。缓冲区的实际值按循环间隔时间传输到控制站,见图 11。传到控制站的数据由A CYCLIC D八TA. ind向控制站的过程传输。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_CYCLIC_DATA.req发送M CYCLIC
A_CYCLIC_DATA.ind接收M CYCLIC

9.5 事件采集(GB/T 18657.5的6.4)

事件在应用层突发地发生。将事件通知远方站的过程和通信系统的操作有关。在任何情况下,采集事件比将这些事件传输到远方站快,所以当地进程需要事件缓冲区以采集事件。

在平衡式通信系统中,被控站按给定的优先级直接传输事件,即中断低优先级的传输过程,如中断循环传输过程。

在非平衡式通信系统中,被控站的进程要等待控制站的传输请求。

顺序过程见图12 在非平衡传输系统中,控制站定期地或由被控站请求启动采集事件,即当控制站查询时,被控站通知控制站发生了事件。在平衡传输系统中,事件的传输由发送/确认过程完成。

若被控站存贮了一个或几个事件,这些信息以A_ SPONT PDU传输到控制站,并以A_ EVENT.ind原语发送给应用层,见图 12。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_EVENT.req发送M SPONT
A_EVENT.ind接收M SPONT

9.6 以快速-检验(quick-check)过程采集数据(DL标准中未选用)

该方法用于非平衡传输系统中的一些应用,以加速事件的采集。

控制站周期地向所有被控站发送全局请求PDU查询被控站的访问要求。传输这些 PDU后,有三种可能情况,见图13。

  • 第1种情况:从传输上次事件后没有发生事件。

这种情况下对全局请求无后回答,超时后这过程就终止。

  • 第2种情况:在寻址的被控站中有一个发生了事件。

该被控站向通信服务发送A_EVENT.req请求原语。被控站接收到查询访问的请求后、发送响应访问要求的PDU给控制站,然后,控制站发送请求1级用户数据的PDU给被控站,被控站以1级用户数据PDU格式向控制站发送事件。控制站接收后,以A_EVENT.ind指示原语传递给应用功能。

  • 第3种情况:多个寻址的被控站发生了事件。

这种情况下等待事件传输的各被控站同时向控制站发送响应访问请求的PDU.这时,发送帧冲突。控制站检出了冲突,被控站的全部帧传输结束后,控制站启动图10所述的事件查询过程。

9.7 总召唤-子站召唤(GB/T 18657.5的6.6)

子站召唤功能用于站初始化过程结束后或控制站检出信息丢失时刷新控制站的数据。控制站的总召唤功能请求被控站传送全部过程变量的实际值。

接收到召唤命令以后,被控站传送被召唤的信息。控制站和被控站的应用功能一般都知道被请求信息的总量。控制站校核召唤传输的信息总量,就可确定召唤过程是否结束。如召唤的信息总量没有在控制站定义,子站需发送标志召唤过程的结束的召唤服务结束(选用)。

随时发生在被控站的事件可中断子站召唤过程,应十分小心以避免混乱。这些混乱可能由于接收到已过时的召唤信息引起。

顺序过程的描述见图14 控制站的应用进程给通信服务发送召唤命令A_GENINCOM.req请求原语,通信服务传输C_IC ACT PDU(召唤命令PDU)到被控站,被控站以A_GENINCOM.ind指示原语传递给应用进程。

被控站开始应用进程的召唤过程后,传输对总召唤的确认,即传输由请求原语八_GENINCOM.req启动的C_ IC ACTCON PDU。该PDU被控制站接收后以A_GENINACK.ind指示原语传递给控制站的应用功能。这个服务是选用的。

被控站的应用功能以M(监视信息)规约数据单元向控制站传输被召唤的信息,这些信息由请求原语A_INTINF.req启动,以指示原语A_INTINF.ind传送给控制站的应用功能。

传输了最后的被召唤的信息以后,可由被控站的应用功能标明召唤过程的结束,通过由请求原语 A_ENDINT.req启动的C_IC_ACTTERM PDU传送给控制站,再以A_ENDINT.ind指示原语告知控制站的应用功能。这个服务是选用的。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_GENINCOM.req发送C_IC ACT
A_GENINCOM.ind接收C_IC ACT
A_GENINACK.req发送C_IC ACTCON
A_GENINACK.req接收C_IC ACTCON
A_INTINF.req发送M
A_INTINF.ind接收M
A_ENDINT.req发送C_IC ACTTERM
A_ENDINT.ind接收C_IC ACTTERM

9.8 时钟同步(GB/T 18657.5的6.7)

为给传输到控制站或当地登录的带时标的事件或信息体提供准确的日历时间,应将被控站的时钟和控制站的时钟同步。系统初始化后控制站先同步,然后,通过控制站传输C_ CS ACT(时钟同步命令))PDU定期地再同步。

C_CS_ACT PDU包含全部当前时钟时间,即传输C_CS ACT PDU第1比特瞬间的具有所需时间分辨率的日期和时间的信息。被控站接收该PDU时必需校正其时间信息,或在控制站发送时钟同步命令 PDU前先校正控制站的时间信息。时间校正值是时间同步帧长和传输速率的乘积加传输延时的和。被控站时钟同步操作依赖于具体的进程要求,不属标准化范围 被控站在时钟同步后产生C_CS ACTCON PDU。该PDU包含被控站同步前的当地时间信息减去时间校正值,在所有存贮在缓冲区中的等待传输的带时标的PDU传输后传输。发生在时钟同步以后的事件在被控站传输C_CS ACTCON PUD后传输。

被控站期待在规定的时间间隔内接收到时钟同步命令。该时间间隔和时钟的准确度及容许时间偏差有关。如在该时间间隔内没有接收到时间同步命令,被控站将全部带时标的信息体加上标记,表示时间信息的准确度可疑。同样,在被控站硬件复位或初始化之后,接收到有效时问同步命令C_ CS ACT PDU以前的所有带时标信息体也将加上时间信息可能不准确的标记。接收有效的C_CS ACT PDU后发生的带时标的事件不带这种标记。

C_CS ACT PDU可按发送/无回答服务发送(广播发送到多个被控站)或按链路层的发送/确认服务发送。

顺序过程的描述见图15。控制站的应用进程以CLOCKSYN.req请求原语向通信服务发送时钟同步命令,通信服务发送包含时钟时间的C_CS ACT PDU给被控站,被控站以A_CLOCKSYN.ind指示原语传递给应用进程。

执行时钟同步操作后,被控站应用进程产生由八_TIMEMESS.req请求原语启动、以C_CS CT-CON PDU传输的时间报文。该PDU包含同步前瞬间有效的时间信息减去时间校正值。该信息在控制站以A TIMEMESS.ind指示原语传递给应用进程。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_CLOCKSYN.req发送C_CS ACT
A_CLOCKSYN.ind接收C_CS ACT
A_TIMEMESS.req发送C_CS ACTCON
A_TIMEMESS.req接收C_CS ACTCON

按照GB/T 18657.2,链路层提供发送时钟命令的精确时间,因为本部分不适用该链路层,故GB/T 18657.5中定义的时钟同步过程无法应用于本部分。

但是,当最大网络延迟小于接收站要求的时钟精度时,配置中仍然可以使用时钟同步。例如,如果网络提供者保证网络延迟不大于400ms(X.25 WAN的典型值),并且被控站要求的精度为1s,时钟同步过程就可以使用,从而避免在几百甚至上千个被控站安装时钟同步接收器或类似的装置。

时钟同步过程参照GB/T 18657.5的6.7,删去”比特1“和”时间修正“要求以及链路层选项(发送/无回答或发送/确认)。

被控站的时钟必须与控制站同步,以提供具有正确的按时间顺序排列的带时标的事件和信息对象,不管发送给控制站还是记录在本地。系统初始化完成后,控制站进行初始化同步,以后每隔一段约定的时间发送C_CS ACT PDU再同步。

9.9 命令传输(GB/T 18657.5的6.8)

远动系统的命令用于改变运行设备的状态(见IEV 371-03-01)。这样,命令可使受控过程向预定方向发展。

命令可由操作人员或控制站自动监视过程启动。对未授权的访问或误动作的防止和系统或进程相关。

典型的运行设备或应用进程涉及的任务对象包括:

  • 电气接触器、隔离刀闸;
  • 断路器;
  • 当地控制进程的启动和停止;
  • 当地控制顺序中的执行步骤;
  • 点、告警限值、具体参数等的设置。

有两种标准的命令传输过程,即:

1)选择和执行命令

2)直接命令

选择和执行命令用于控制站对被控站准备控制操作。检查控制操作是否已正确准备好,然后执行命令。

直接命令用于控制站对被控站立即进行控制操作。被控站的应用功能为了安全检查接收的命令报文的允许性和有效性。如检查正确,执行操作检查。

也可由操作员或应用进程进行。直到接收到正确的执行指示后,被控站才执行控制操作。

选择和执行命令以及直接命令的顺序过程如图16所示,描述如下:

  • 选择和执行命令过程

控制站应用进程给通信服务发送请求原语A_SELECT.req,通信服务发送包含C_ACT(选择命令)的PDU给被控站,被控站接收后,以A_SELECT.ind指示原语传递给应用进程。如被控站的应用进程准备接收“选择命令”告知的控制命令,它产生“选择响应”,通过A_ SELECT. resp选择命令响应原语返送给通信服务,该命令响应以C_ACTCON PDU传输给控制站,控制站接收后通过A_SELECT.con选择确认原语产生“选择确认”。这个过程仅用于选择和执行命令,不被中断,由超时控制。

选择过程可由“撤消命令”停止执行。该命令以C_DEACT发给被控站,由C_DEACTCON响应。

如选择命令得到确认,以A_EXCO.req请求原语向通信服务发“执行命令”。控制站以C ACT PDU将执行命令传输给被控站,被控站以A_EXCO.ind指示原语传递给应用进程功能,以C ACT-CON PDU将“执行响应”返回给控制站,产生肯定或否定的确认。这样,规定的控制操作即将开始。这个过程不被中断,由超时控制。

  • 直接命令过程

被控站的应用进程检查寻址的命令输出是否被闭锁,即执行是否已准备好。如果检查结果是肯定的,被控站的应用进程向执行设备发出命令,同时返送肯定的C_ACTCON PDU,否则返送否定的C_ACTCON PDU。

当命令传递到应用进程时,寻址的运行设备将改变状态,这状态改变受到监视,并通过返回信息告知控制站(见IEV 371-02-05)。在特殊命令情况下,例如控制动作慢的隔离刀闸的双命令(见 IEV 371-03-03),当原先的合或者分状态信息还存在时,状态改变的开始可以选择地以“控制操作开始”的M_PDU返送给控制站。当命令执行完毕设备处于新状态时,被控站的应用进程以“控制操作完成”的M PDU返送给控制站,见图16。

最后,可用C_ACTTERM PDU表明控制操作结束(选用)。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_SELECT.req发送C_SC,C_DC,C_SE,C_RC,C_BO ACT
A_SELECT.ind接收C_SC,C_DC,C_SE,C_RC,C_BO ACT
A_SELECT.res发送C_SC,C_DC,C_SE,C_RC,C_BO ACTCON
A_SELECT.con接收C_SC,C_DC,C_SE,C_RC,C_BO ACTCON
A_BREAK.req发送C_SC,C_DC,C_SE,C_RC,C_BO DEACT
A_BREAK.ind接收C_SC,C_DC,C_SE,C_RC,C_BO DEACT
A_BREAK.res发送C_SC,C_DC,C_SE,C_RC,C_BO DEACTCON
A_BREAK.con接收C_SC,C_DC,C_SE,C_RC,C_BO DEACTCON
A_EXCO.req发送C_SC,C_DC,C_SE,C_RC,C_BO ACT
A_EXCO.ind接收C_SC,C_DC,C_SE,C_RC,C_BO ACT
A_EXCO.res发送C_SC,C_DC,C_SE,C_RC,C_BO ACTCON
A_EXCO.con接收C_SC,C_DC,C_SE,C_RC,C_BO ACTCON
A_RETURN_INF.req发送M_SP,M_DP,M_ST
A_RETURN_INF.ind接收M_SP,M_DP,M_ST
A_COTERM.req发送C_SC,C_DC,C_SE,C_RC,C_BO ACTTERM
A_COTERM.ind接收C_SC,C_DC,C_SE,C_RC,C_BO ACTTERM

9.10 传输累计量(远程累计)(GB/T 18657.5的6.9)

远程累计定义为“应用通信技术传输某一被测量对一特定参量(如时间)的累计值。 累计可发生在传送前或传送后。如在传送前累计,就以‘传输累计量”表述。"IEV 371-01-05) 。

累计量是一个按规定时段进行累计的值。连续采集累计量的具体时间和时间间隔都是系统参量一些系统在控制站使用命令周期激活累计量采集,还有一些系统则以被控站的当地时钟定期激活累计量采集。控制站时钟同步可由远动系统(见 6. 7 )或由外部同步时钟过程进行,例如,接收国家的或者国际的无线电广播时间信息。

采集累计量信息有两种不同的方法:

1)采集累计值

被控站周期地在特定时刻记忆(冻结)累计量,将累计量存贮到缓冲存贮器,再将记忆值传送到控制站累计计数器连续工作,不因记忆操作而复位。这种情况的每个时段的增量值由控制站计算。增量值是两次连续传输值的差。

2)采集增量信息

被控站周期地在具体时刻记忆(冻结)累计量,将累计量存贮到缓冲存贮器,再将记忆值传送到控制站。

顺序过程的描述见图17。作为一种选用方式,控制站周期地在特定时刻传输C_CI ACT PDU(可以是记忆计数命令或记忆增量命令)给被控站。这两种命令都使瞬时累计量存贮到缓冲存贮器。若为记忆增量命令,还要将累计计数器中累计量清零。这个过程也可由被控站的当地时钟激活。

上述过程执行后,记忆的累计值可以由C_CI ACT(请求累计量)请求上传.然后被控站以C_CI ACTCON响应传输记忆值。也可以将记忆值作为事件对象传输到控制站,这时控制站按事件采集记忆值(M_ IT PDU)(见6.4)。

累计量传输可由A_IBREAK. req请求原语终止。该原语以C_CIDEACT传输给被控站,被控站收到后,以C_CI DEACTCON响应。

在请求累计量情况下,累计量传完后,可用 C_CI ACTTERM PDU表示控制操作过程结束(选用)。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_MEMCNT.req发送C_CI ACT
A_MEMCNT.ind接收C_CI ACT
A_MEMCNT.res发送C_CI ACTCON
A_MEMCNT.con接收C_CI ACTCON
A_MEMINCR.req发送C_CI ACT
A_MEMINCR.ind接收C_CI ACT
A_MEMINCR.res发送C_CI ACTCON
A_MEMINCR.con接收C_CI ACTCON
A_REQINTO.req发送C_CI ACT
A_REQINTO.ind接收C_CI ACT
A_REQINTO.res发送C_CI ACTCON
A_REQINTO.con接收C_CI ACTCON
A_INTO_INF.req发送M_IT
A_INTO_INF.ind接收M_IT
A_ITERM.req发送C_CI ACTTERM
A_ITERM.ind接收C_CI ACTTERM

9.11 参数装载(GB/T 18657.5的6.10)

系统以装载参数改变被控站定义的参数,如门限值、测量值的上下限。装载参数一般分两步进行

1)以参数命令要求被控站装载一个或多个参数,被控站存贮这些参数暂不激活。

2)再以参数激活命令激活上述装载的参数

如要求在同一时间内激活一定数量的参数,需要分两个步骤。如只装载一个参数,装载参数和激活可结合起来一步完成。

顺序过程的描述见图18.控制站的应用进程向通信服务发送A_PARAM.req请求原语,通信服务传输包含P_ME ACT(多数命令)的PDU,被控站接收后,向应用进程传递 A_PARAM.ind指示原语。被控站的应用功能产生参数命令ACK,通过A_PARAM.res响应原语返送给通信服务。被控站向控制站传输命令响应P_ME ACTCON PDU,控制站通过A_PARAM.can确认原语产生认可。

如上述装载的参数分别被激活,单个参数激活命令通过A_PACTIV.req请求原语发送给通信服务,通信服务以P_AC ACT PDU向被控站传输该命令,以A_PACTIV.ind指示原语传递给被控站的应用进程。然后,通过P_AC ACTCON PDU向控制站返送认可,确认装载的参数已运行。

在当地改变参数的情况下,被控站可传送 P_ME SPONT PDU给控制站。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_PARAM.req发送P_ME ACT
A_PARAM.ind接收P_ME ACT
A_PARAM.res发送P_ME ACTCON
A_PARAM.con接收P_ME ACTCON
A_PACTIV.req发送P_AC ACT
A_PACTIV.ind接收P_AC ACT
A_PACTIV.res发送P_AC ACTCON
A_PACTIV.con接收P_AC ACTCON
A_LCPACH.req发送P_ME SPONT
A_LCPACH.ind接收P_ME SPONT

9.12 测试过程(GB/T 18657.5的6.11)

测试过程用以检查控制站到被控站、被控站回控制站、包括相应应用功能的整个环路。

顺序过程的描述见图19。控制站的应用功能向通信服务发送测试请求原语A_TEST.req,通信服务传输包括C_TS ACT(测试命令)的PDU给被控站。被控站以A_TEST.ind指示原语传递给应用功能,被控站的应用功能产生测试命令的ACK,通过A_TEST.res响应原语返送给通信服务。该命令响应以C_TSACTCON PDU传输给控制站,在控制站通过A_TEST.con原语确认响应控制站检查C_TEST PDU的镜像。若在限定的时间内接收到同样的 PDU,检查结果为肯定的。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_TEST.req发送C_TS ACT
A_TEST.ind接收C_TS ACT
A_TEST.res发送C_TS ACTCON
A_TEST.con接收C_TS ACTCON

9.13 文件传输(GB/T 18657.5的6.12)

如远动系统中某信息体的八位位组数超过应用服务数据单元 ASDU规定的最大长度,就需要采用文件传输,以分段的形式将信息体传送到目的地。在远动系统中,文件可从被控站传向控制站,也可控制站传向被控站。被控站因事件引起的大量数据记录(如故障录波器的数据记录)顺序地传送到控制站。这些文件的类型和数目在被控站登录,应以目录PDU告知控制站。

从控制站到被控站的参数表或程序的下载进程由控制站管理,不需要传输目录。

两个方向的文件结构相同(见图20)。一个文件可分成几节,一个节可分成几段,由PDU按段顺序传输。故障录波器记录的一个故障可以表示为文件的一个节。若干数值可组合一起成为文件的段。

虽然两个方向文件的结构相同,但传输过程不同,所以分别描述。

  • 1)监视方向的文件传输

从被控站到控制站的文件传输主要用于通知控制站已发生事件并已登录大量数据。文件数量、登录时间和事件类型(如继电保护跳闸命令瞬间记录的数据)用目录PDU告知控制站,由控制站决定是否传输及传输哪个文件。被控站将已成功传输文件的数据记录删除,为新文件腾出内存空间。

顺序过程描述:

顺序过程的描述见图 21。被控站因发生事件而生成一个新文件,自发地向控制站传送新文件目录 PDU,其内容包含被控站记录还没有向控制站发送的文件的数量、类型和生成时刻。

控制站还可在任何时候通过A_CALL_DIRECTORY请求采集被控站登录的文件数量和类型。

当控制站准备好接收文件时,向被控站发送SELECT_FILE PDU,被控站准备传送所选文件,并将准备状态用 FILE_ READY PDU告知控制站。

控制站用CALL_ FILE PDU请求选择的文件。被控站用SECTION_ READY PDU响应,告知控制站文件的第 1节已准备传输。

控制站发送CALL_SECTION PDU(肯定)请求第1节,或发送CALL_SECTION PDU(否定)拒绝请求第1节。在否定情况下,被控站向控制站发送SECTION_ READY PDU,表示文件第2节(下一节)已准备好。在肯定情况下,被控站用SEGMENT PDU顺序传输该节第 1段到第n段,最后一段发送完后 被控站发送LAST_ SEGMENT PDU给控制站。控制站以ACK_SECTION PDU肯定或否定地确认有关节的接收。如被控站收到否定认可,再向控制站发送SECTION_READY PDU表示准备再传输该节。如收到肯定认可,被控站发送SECTION_READY PDU,表示下一节已准备好。重复这个过程传输文件的以下各节。

传输了最后一节后,被控站以LAST_SECTION PDU表明文件传输结束。控制站以ACK_FILE PDU确认整个文件的正确接收,被控站可从缓冲存贮器和目录中删除这个文件,再通过目录 PDU将修改后的实际目录情况传送给控制站。

  • 2)控制方向的文件传输

控制方向的文件传输主要用于下载参数表或程序。控制站安排传输数据文件的类型、数量和规模,因此不需要传输目录。

顺序过程描述:

顺序过程的描述见图22。控制站以FILE_READY PDU告知被控站要传输文件。若被控站已准备接收文件,发送CALL_FILE PDU给控制站。控制站以SECTION_READY PDU告知被控站已准备好文件某节。如被控站准备接收,向控制站传输CALL_SECTION PDU。

控制站以SEGMENTS PDU向被控站传输准备好的节的各段 用LAST_SEGMENT PDU指明最后一段。如被控站正确接收了各段,传输 ACK_SECTION PDU给控制站。

如上所述,控制站依次传输文件下面各节。传输最后一节后‘控制站传输LAST_SECTION PDU,表明文件传输结束。如被控站正确接收了整个文件,以ACK_FILE PDU确认。

应用服务(GB/T 18657.5)TCP服务(RFC 793)ASDU标识(GB/T 18657.5)
A_CALL_DIRECTORY.req发送F_SC
A_CALL_DIRECTORY.ind接收F_SC
A_CALL_DIRECTORY.res发送F_DR
A_CALL_DIRECTORY.con接收F_DR
A_SELECT_FILE.req发送F_SC
A_SELECT_FILE.ind接收F_SC
A_FILE_READY.req发送F_FR
A_FILE_READY.ind接收F_FR
A_CALL_FILE.req发送F_SC
A_CALL_FILE.ind接收F_SC
A_SECTION1_READY.req发送F_SR
A_SECTION1_READY.ind接收F_SR
A_CALL_SECTION1.req发送F_SC
A_CALL_SECTION1.ind接收F_SC
A_SEGMENT1.req发送F_SG
A_SEGMENT1.ind接收F_SG
A_SEGMENTn.req发送F_SG
A_SEGMENTn.ind接收F_SG
A_LAST_SEGMENT.req发送F_LS
A_LAST_SEGMENT.ind接收F_LS
A_ACK_SECTION1.req发送F_AF
A_ACK_SECTION1.ind接收F_AF
A_SECTIONm_READY.req发送F_SR
A_SECTIONm_READY.ind接收F_SR
A_CALL_SECTIONm.req发送F_SC
A_CALL_SECTIONm.ind接收F_SC
A_ACK_SECTIONm.req发送F_AF
A_ACK_SECTIONm.ind接收F_AF
A_LAST_SECTION.req发送F_LS
A_LAST_SECTION.ind接收F_LS
A_ACK_FILE.req发送F_AF
A_ACK_FILE.ind接收F_AF
A_DIRECTORY.req发送F_DR
A_DIRECTORY.ind接收F_DR

9.14 传输延时采集(DL标准中未选用)

被控站时钟同步,包括时间校正。时间校正值决定于传输延时和设备内部延时的和。 后者决定了对设备本身的要求,不在本标准范围内。传输延时值可以通过参数分别采集,或由控制站启动动态过程采集。传输延时的动态采集过程如下。

顺序过程描述:

顺序过程的描述见图23。控制站发送C_CD ACT(延时数据采集)PDU,其中包含发送该PDU第1比特瞬间的时刻(时间SDT)。被控站以接收的PDU中的时间同步其内部时钟(或辅助时钟),从而和时间 SDT同步。被控站返送 C_CD ACTCON PDU响应时间同步。该PDU包含发送该帧的第1比特的被控站时刻(SDT+tR),控制站在RDT时刻接收到该响应PDU。这样,控制站可用下式计算传输延时 tD:

控制站用C_CD SPONTANEOUS PDU将采集的传输延时传输到被控站(选用)以校正同步时间。

注:上述方法假定控制方向和监视方向的传输时间相同。

10、控制方向带时标的过程信息ASDU

这个似乎是DL标准中新增的,新增了一些控制方向带时标CP56Time2a的ASDU。这个时标包含从毫秒到年的日期和分钟时间,在DL/T 634.5101中有定义。当使用那些可能产生较大的命令延迟的网络时,本部分建议在发送时使用带时标的ASDU,这样当被控站收到一个超过最大允许延迟(系统特定参数)的命令或设定时,不会发回一个协议上的响应(例如被控站不回复一个”肯定“确认或”否定“确认)。这是因为这个确认信息可能被明显地滞后,并难以与最初的主站请求相关联。命令被传递至被控站的应用时,这个命令将被识别出来是接收得”太迟“了,且不得执行命令中的任何操作,该时标包含了控制站的命令初始形成时的时间。

10.1 、传送原因

基于DL/T 634.5101的7.2.3:

10.2、新增的带时标的ASDU分析

  • 1)这里以第一个类型标识58:C_SC_TA_1为例:

带时标CP56Time2a的单命令见图23.

单个信息对象(SQ=0)

C_SC_TA_1:= CP{数据单元标识符,信息对象地址,SOC,CP56Time2a}

类型标识58:=C_SC_TA_1中使用的传送原因:

在控制方向:

<6>:=激活

<8>:=停止激活

在监视方向:

<9>:=停止激活确认

<10>:=激活终止

<44>:=未知的类型标识

<45>:=未知的传送原因

<46>:=未知的ASDU公共地址

<47>:=未知的信息对象地址

  • 2)其它

类新版标识59:C_DC_TA_1

类型标识60:C_RC_TA_1

类型标识61:C_SE_TA_1

类型标识62:C_SE_TB_1

类型标识63:C_SE_TC_1

类型标识64:C_BO_TA_1

类型标识107:C_TS_TA_1

类型标识127:F_SC_NB_1

整体来说,通过类型标识来区别不同的ASDU,都增加了CP56Time2a的时标,用于网络延迟可能较高的环境。

11、互操作性

对于可选性和互斥性选项做了详细说明,这对于需要配置控制站和被控站的参数有很大的帮助,如果要开发配置软件或者配置平台这里的配置互斥性和可选性就会很有帮助。这里不展开了 ,对照标准即可。

12、四摇

电气术语在线数据库(IEV):www.electropedia.org/iev/iev.nsf…

四、104客户端服务端结合电网四摇实例

我们可以了解到对应电力四摇等概念其实就是发送开关量进行远程控制或者主动上报一些模拟量或累积量,这样我们结合协议的理解以及60870的104客户端服务端实例就比较容易实现简单的例子了:

借助于其时间同步的命令同步修改服务端的时间和客户端保持一致,这样104协议也可以开发一个简单的客户端服务端时间同步功能,否则就像104协议所说大量的电网设备都需要单独部分程序进行时间同步了。如下,做两种模拟实验场景来熟悉下协议的应用开发:

实验场景1:

  • 客户端作为主机,开关量发送带时标的命令,服务端作为从机,收到命令后计算单向时延并返回
  • 客户端作为主机,发送命令启动模拟量上报,服务端作为从机,间隔上报带时标的模拟量,客户端收到后计算单向时延

实验场景2:

  • 客户端作为主机发送命令或模拟量,并获取当前ns级时间,服务端作为从机只需要回复确认指令,客户端根据回复内容获取当前ns级时间,之后算出命令或模拟量发送到服务端的双向时延

这两种应用只是臆想的应用场景,用于测试其TCP发包的单向或双向时延,是否具备参考价值不得而知,只是为了熟悉协议开发而做的实验,这其中的重点仍是对协议的熟悉以及对接口调用的熟悉,这里例子给到实验场景1的代码,实验场景2可以自行根据理解去模拟实现一下,实际的开发能帮助你快速理解掌握协议。

#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>

#include "cs104_slave.h"

#include "hal_thread.h"
#include "hal_time.h"
#include "power_def.h"

#define mcu_opi_oddsef_ms 0

static bool running = true;

void
sigint_handler(int signalId)
{
    running = false;
}

void
printCP56Time2a(CP56Time2a time)
{
    printf("%02i:%02i:%02i:%03i %02i/%02i/%04i", CP56Time2a_getHour(time),
                             CP56Time2a_getMinute(time),
                             CP56Time2a_getSecond(time),
                             CP56Time2a_getMillisecond(time),
                             CP56Time2a_getDayOfMonth(time),
                             CP56Time2a_getMonth(time),
                             CP56Time2a_getYear(time) + 2000);
}

/* Callback handler to log sent or received messages (optional) */
static void
rawMessageHandler(void* parameter, IMasterConnection conneciton, uint8_t* msg, int msgSize, bool sent)
{
    if (sent)
        printf("SEND: ");
    else
        printf("RCVD: ");

    int i;
    for (i = 0; i < msgSize; i++) {
        printf("%02x ", msg[i]);
    }

    printf("\n");
}

void opi_utc_update(uint8_t *msgBuf, uint16_t size)
{
    time_t rawTime;
    //struct tm *pNowTM;
    struct tm nowTM;
    struct timeval nowTV;
    int microSec = 0;

    if (size < 8)
        return;

    rawTime = time(NULL);
    printf("RawTime 1 is %lu\r\n", rawTime);

    /*Set my time*/
    nowTM.tm_year = 100 + (int)msgBuf[0];
    nowTM.tm_mon = msgBuf[1] - 1;
    nowTM.tm_mday = msgBuf[2];
    nowTM.tm_hour = msgBuf[3];
    nowTM.tm_min = msgBuf[4];
    nowTM.tm_sec = msgBuf[5];

    rawTime = mktime(&nowTM);
    if (nowTM.tm_gmtoff != 0)
        rawTime += nowTM.tm_gmtoff;

    printf("RawTime 3 is %lu\r\n", rawTime);
    microSec = msgBuf[6];
    microSec <<= 8;
    microSec += msgBuf[7];
    microSec += mcu_opi_oddsef_ms;
    if (microSec > 1000)
    {
        microSec %= 1000;
        rawTime += 1;
    }
    microSec *= 1000;
    /*Set time of day*/
    nowTV.tv_sec = rawTime;
    nowTV.tv_usec = microSec;
    if (settimeofday(&nowTV, NULL) != 0)
    {
        printf("Can not set system time\r\n");
        return;
    }
    /*update hardware time*/
    int rc = system("hwclock --systohc");
    printf("return value is %d \r\n", rc);
}

static bool
clockSyncHandler (void* parameter, IMasterConnection connection, CS101_ASDU asdu, CP56Time2a newTime)
{
    printf("Process time sync command with time ");
    printCP56Time2a(newTime);
    printf("\n");

    uint64_t newSystemTimeInMs = CP56Time2a_toMsTimestamp(newTime);

    /* Set time for ACT_CON message */
//    CP56Time2a_setFromMsTimestamp(newTime, Hal_getTimeInMs());

    /* update system time here */
    uint8_t set_time[8] = {CP56Time2a_getYear(newTime), CP56Time2a_getMonth(newTime),
                           CP56Time2a_getDayOfMonth(newTime), CP56Time2a_getHour(newTime),
                           CP56Time2a_getMinute(newTime), CP56Time2a_getSecond(newTime),
                           CP56Time2a_getMillisecond(newTime), mcu_opi_oddsef_ms};
    opi_utc_update(set_time, 8);

    return true;
}

static bool
interrogationHandler(void* parameter, IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi)
{
    printf("Received interrogation for group %i\n", qoi);

    if (qoi == IEC60870_QOI_STATION) { /* only handle station interrogation */
        printf("station group %i\n", qoi);
        CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);

        IMasterConnection_sendACT_CON(connection, asdu, false);

        /* The CS101 specification only allows information objects without timestamp in GI responses */

        CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_INTERROGATED_BY_STATION,
                0, 1, false, false);

        InformationObject io = (InformationObject) MeasuredValueScaled_create(NULL, 100, -1, IEC60870_QUALITY_GOOD);

        CS101_ASDU_addInformationObject(newAsdu, io);

        CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
            MeasuredValueScaled_create((MeasuredValueScaled) io, 101, 23, IEC60870_QUALITY_GOOD));

        CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
            MeasuredValueScaled_create((MeasuredValueScaled) io, 102, 2300, IEC60870_QUALITY_GOOD));

        InformationObject_destroy(io);

        IMasterConnection_sendASDU(connection, newAsdu);

        CS101_ASDU_destroy(newAsdu);

        newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_INTERROGATED_BY_STATION,
                    0, 1, false, false);

        io = (InformationObject) SinglePointInformation_create(NULL, 104, true, IEC60870_QUALITY_GOOD);

        CS101_ASDU_addInformationObject(newAsdu, io);

        CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
            SinglePointInformation_create((SinglePointInformation) io, 105, false, IEC60870_QUALITY_GOOD));

        InformationObject_destroy(io);

        IMasterConnection_sendASDU(connection, newAsdu);

        CS101_ASDU_destroy(newAsdu);

        newAsdu = CS101_ASDU_create(alParams, true, CS101_COT_INTERROGATED_BY_STATION,
                0, 1, false, false);

        CS101_ASDU_addInformationObject(newAsdu, io = (InformationObject) SinglePointInformation_create(NULL, 300, true, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 301, false, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 302, true, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 303, false, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 304, true, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 305, false, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 306, true, IEC60870_QUALITY_GOOD));
        CS101_ASDU_addInformationObject(newAsdu, (InformationObject) SinglePointInformation_create((SinglePointInformation) io, 307, false, IEC60870_QUALITY_GOOD));

        InformationObject_destroy(io);

        IMasterConnection_sendASDU(connection, newAsdu);

        CS101_ASDU_destroy(newAsdu);

        newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_INTERROGATED_BY_STATION,
                        0, 1, false, false);

        io = (InformationObject) BitString32_create(NULL, 500, 0xaaaa);

        CS101_ASDU_addInformationObject(newAsdu, io);

        InformationObject_destroy(io);

        IMasterConnection_sendASDU(connection, newAsdu);

        CS101_ASDU_destroy(newAsdu);

        IMasterConnection_sendACT_TERM(connection, asdu);
    } else if(qoi == IEC60870_QOI_GROUP_1) {
        printf("1 group %i\n", qoi);

    } else if(qoi == IEC60870_QOI_GROUP_2) {
        printf("2 group %i\n", qoi);

    }
    else {
        printf("send act con\n");
        IMasterConnection_sendACT_CON(connection, asdu, true);
    }

    return true;
}

static bool g_send_yx = false;
static bool g_send_yc = false;
static bool g_send_yk = false;
static bool g_send_yt = false;
static int g_yk_ott = 0;
static int g_yt_ott = 0;

static bool
asduHandler(void* parameter, IMasterConnection connection, CS101_ASDU asdu)
{
    printf("received command type:%d\n", CS101_ASDU_getTypeID(asdu));
    if (CS101_ASDU_getTypeID(asdu) == C_SC_NA_1) {
        printf("received single command\n");

        if  (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) {
            InformationObject io = CS101_ASDU_getElement(asdu, 0);

            if (io) {
                if (InformationObject_getObjectAddress(io) == 5000) {
                    SingleCommand sc = (SingleCommand) io;

                    printf("IOA: %i switch to %i\n", InformationObject_getObjectAddress(io),
                            SingleCommand_getState(sc));

                    CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                } else if (InformationObject_getObjectAddress(io) == TEST_TYPE_YX) {
                    SingleCommand sc = (SingleCommand) io;

                    printf("IOA: %i switch to %i\n", InformationObject_getObjectAddress(io),
                           SingleCommand_getState(sc));

                    CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                    if (SingleCommand_getState(sc)) {
                        g_send_yx = true;
                    } else {
                        g_send_yx = false;
                    }
                } else if (InformationObject_getObjectAddress(io) == TEST_TYPE_YC) {
                    SingleCommand sc = (SingleCommand) io;

                    printf("IOA: %i switch to %i\n", InformationObject_getObjectAddress(io),
                           SingleCommand_getState(sc));

                    CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                    if (SingleCommand_getState(sc)) {
                        g_send_yc = true;
                    } else {
                        g_send_yc = false;
                    }
                }
                else
                    CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);

                InformationObject_destroy(io);
            }
            else {
                printf("ERROR: message has no valid information object\n");
                return true;
            }
        }
        else
            CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_COT);

        IMasterConnection_sendASDU(connection, asdu);

        return true;
    } else if (CS101_ASDU_getTypeID(asdu) == C_SC_TA_1) {
        printf("received single command with timestamp\n");

        if  (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) {
            InformationObject io = CS101_ASDU_getElement(asdu, 0);

            if (io) {
                if (InformationObject_getObjectAddress(io) == TEST_TYPE_YK) {
                    SingleCommandWithCP56Time2a sc = (SingleCommandWithCP56Time2a ) io;
                    struct sCP56Time2a newTime;

                    CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());
                    printf("  measured scaled values with CP56Time2a timestamp:\n");

                    printf("IOA: %i switch to %i\n", InformationObject_getObjectAddress(io),
                           SingleCommand_getState(sc));

                    CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                    int time_sec_rcv = CP56Time2a_getSecond(&newTime);
                    int time_ms_rcv = CP56Time2a_getMillisecond(&newTime);
                    int time_sec_send = CP56Time2a_getSecond(SingleCommandWithCP56Time2a_getTimestamp(sc));
                    int time_ms_send = CP56Time2a_getMillisecond(SingleCommandWithCP56Time2a_getTimestamp(sc));
                    g_yk_ott = (time_sec_rcv - time_sec_send) * 1000 + (time_ms_rcv - time_ms_send);
                    printf("yk ott:%d ms\n", g_yk_ott);
                    if (SingleCommand_getState(sc)) {
                        g_send_yk = true;
                    } else {
                        g_send_yk = false;
                    }
                } else if (InformationObject_getObjectAddress(io) == TEST_TYPE_YT) {
                    SingleCommandWithCP56Time2a sc = (SingleCommandWithCP56Time2a ) io;
                    struct sCP56Time2a newTime;

                    CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());
                    printf("  measured scaled values with CP56Time2a timestamp:\n");

                    printf("IOA: %i switch to %i\n", InformationObject_getObjectAddress(io),
                           SingleCommand_getState(sc));

                    CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                    int time_sec_rcv = CP56Time2a_getSecond(&newTime);
                    int time_ms_rcv = CP56Time2a_getMillisecond(&newTime);
                    int time_sec_send = CP56Time2a_getSecond(SingleCommandWithCP56Time2a_getTimestamp(sc));
                    int time_ms_send = CP56Time2a_getMillisecond(MeasuredValueScaledWithCP56Time2a_getTimestamp(sc));
                    g_yt_ott = (time_sec_rcv - time_sec_send) * 1000 + (time_ms_rcv - time_ms_send);
                    printf("yt ott:%d ms\n", g_yt_ott);
                    if (SingleCommand_getState(sc)) {
                        g_send_yt = true;
                    } else {
                        g_send_yt = false;
                    }
                }
                else
                    CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);

                InformationObject_destroy(io);
            }
            else {
                printf("ERROR: message has no valid information object\n");
                return true;
            }
        }
        else
            CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_COT);

        IMasterConnection_sendASDU(connection, asdu);

        return true;
    }

    return false;
}

static bool
connectionRequestHandler(void* parameter, const char* ipAddress)
{
    printf("New connection request from %s\n", ipAddress);

#if 0
    if (strcmp(ipAddress, "127.0.0.1") == 0) {
        printf("Accept connection\n");
        return true;
    }
    else {
        printf("Deny connection\n");
        return false;
    }
#else
    return true;
#endif
}

static void
connectionEventHandler(void* parameter, IMasterConnection con, CS104_PeerConnectionEvent event)
{
    if (event == CS104_CON_EVENT_CONNECTION_OPENED) {
        printf("Connection opened (%p)\n", con);
    }
    else if (event == CS104_CON_EVENT_CONNECTION_CLOSED) {
        printf("Connection closed (%p)\n", con);
    }
    else if (event == CS104_CON_EVENT_ACTIVATED) {
        printf("Connection activated (%p)\n", con);
    }
    else if (event == CS104_CON_EVENT_DEACTIVATED) {
        printf("Connection deactivated (%p)\n", con);
    }
}

int
main(int argc, char** argv)
{
    /* Add Ctrl-C handler */
    signal(SIGINT, sigint_handler);

    /* create a new slave/server instance with default connection parameters and
     * default message queue size */
    CS104_Slave slave = CS104_Slave_create(10, 10);

    CS104_Slave_setLocalAddress(slave, "0.0.0.0");

    /* Set mode to a single redundancy group
     * NOTE: library has to be compiled with CONFIG_CS104_SUPPORT_SERVER_MODE_SINGLE_REDUNDANCY_GROUP enabled (=1)
     */
    CS104_Slave_setServerMode(slave, CS104_MODE_SINGLE_REDUNDANCY_GROUP);

    /* get the connection parameters - we need them to create correct ASDUs -
     * you can also modify the parameters here when default parameters are not to be used */
    CS101_AppLayerParameters alParams = CS104_Slave_getAppLayerParameters(slave);

    /* when you have to tweak the APCI parameters (t0-t3, k, w) you can access them here */
    CS104_APCIParameters apciParams = CS104_Slave_getConnectionParameters(slave);

    printf("APCI parameters:\n");
    printf("  t0: %i\n", apciParams->t0);
    printf("  t1: %i\n", apciParams->t1);
    printf("  t2: %i\n", apciParams->t2);
    printf("  t3: %i\n", apciParams->t3);
    printf("  k: %i\n", apciParams->k);
    printf("  w: %i\n", apciParams->w);

    /* set the callback handler for the clock synchronization command */
    CS104_Slave_setClockSyncHandler(slave, clockSyncHandler, NULL);

    /* set the callback handler for the interrogation command */
    CS104_Slave_setInterrogationHandler(slave, interrogationHandler, NULL);

    /* set handler for other message types */
    CS104_Slave_setASDUHandler(slave, asduHandler, NULL);

    /* set handler to handle connection requests (optional) */
    CS104_Slave_setConnectionRequestHandler(slave, connectionRequestHandler, NULL);

    /* set handler to track connection events (optional) */
    CS104_Slave_setConnectionEventHandler(slave, connectionEventHandler, NULL);

    /* uncomment to log messages */
    CS104_Slave_setRawMessageHandler(slave, rawMessageHandler, NULL);

    CS104_Slave_start(slave);

    if (CS104_Slave_isRunning(slave) == false) {
        printf("Starting server failed!\n");
        goto exit_program;
    }

    while (running) {
        Thread_sleep(1000);

        CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_PERIODIC, 0, 1, false, false);
        struct sCP56Time2a newTime;
        CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());

        if (g_send_yc) {
            //未收到停止指令一直发送时延
            InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YC, 1,
                                                                                  IEC60870_QUALITY_GOOD, &newTime);
            CS101_ASDU_addInformationObject(newAsdu, io);
            InformationObject_destroy(io);
        }
        if (g_send_yx) {
            //未收到停止指令一直发送时延
            InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YX, 1,
                                                                                                IEC60870_QUALITY_GOOD, &newTime);
            CS101_ASDU_addInformationObject(newAsdu, io);
            InformationObject_destroy(io);
        }
        if (g_send_yk) {
            InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YK, g_yk_ott,
                                                                                                IEC60870_QUALITY_GOOD, &newTime);
            CS101_ASDU_addInformationObject(newAsdu, io);
            InformationObject_destroy(io);
            //每一次时延计算及传输都需要发送指令
            g_send_yk = false;
        }
        if (g_send_yt) {
            InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YT, g_yt_ott,
                                                                                                IEC60870_QUALITY_GOOD, &newTime);
            CS101_ASDU_addInformationObject(newAsdu, io);
            InformationObject_destroy(io);
            //每一次时延计算及传输都需要发送指令
            g_send_yt = false;
        }

        /* Add ASDU to slave event queue */
        CS104_Slave_enqueueASDU(slave, newAsdu);

        CS101_ASDU_destroy(newAsdu);
    }

    CS104_Slave_stop(slave);

exit_program:
    CS104_Slave_destroy(slave);

    Thread_sleep(500);
}
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#include "cs104_connection.h"
#include "hal_time.h"
#include "hal_thread.h"

#include "power_def.h"

static bool running = true;
static bool is_send = true;

void
sigint_handler(int signalId) {
    running = false;
}

/* Callback handler to log sent or received messages (optional) */
static void
raw_message_handler(void *parameter, uint8_t *msg, int msgSize, bool sent) {
    if (sent)
        printf("SEND: ");
    else
        printf("RCVD: ");

    int i;
    for (i = 0; i < msgSize; i++) {
        printf("%02x ", msg[i]);
    }

    printf("\n");
}

/* Connection event handler */
static void
connection_handler(void *parameter, CS104_Connection connection, CS104_ConnectionEvent event) {
    switch (event) {
        case CS104_CONNECTION_OPENED:
            printf("Connection established\n");
            break;
        case CS104_CONNECTION_CLOSED:
            printf("Connection closed\n");
            break;
        case CS104_CONNECTION_FAILED:
            printf("Failed to connect\n");
            break;
        case CS104_CONNECTION_STARTDT_CON_RECEIVED:
            printf("Received STARTDT_CON\n");
            break;
        case CS104_CONNECTION_STOPDT_CON_RECEIVED:
            printf("Received STOPDT_CON\n");
            break;
    }
}

/*
 * CS101_ASDUReceivedHandler implementation
 *
 * For CS104 the address parameter has to be ignored
 */
static bool
asdu_received_handler(void *parameter, int address, CS101_ASDU asdu) {
    printf("RECVD ASDU type: %s(%i) elements: %i\n",
           TypeID_toString(CS101_ASDU_getTypeID(asdu)),
           CS101_ASDU_getTypeID(asdu),
           CS101_ASDU_getNumberOfElements(asdu));

    if (CS101_ASDU_getTypeID(asdu) == M_ME_TE_1) {
        int i;

        for (i = 0; i < CS101_ASDU_getNumberOfElements(asdu); i++) {

            MeasuredValueScaledWithCP56Time2a io =
                    (MeasuredValueScaledWithCP56Time2a) CS101_ASDU_getElement(asdu, i);

            printf("    IOA: %i value: %i\n",
                   InformationObject_getObjectAddress((InformationObject) io),
                   MeasuredValueScaled_getValue((MeasuredValueScaled) io)
            );

            switch (InformationObject_getObjectAddress((InformationObject) io)) {
                case TEST_TYPE_YX: {
                    struct sCP56Time2a newTime;

                    CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());
                    printf("  measured scaled values with CP56Time2a timestamp:\n");
                    int time_sec_rcv = CP56Time2a_getSecond(&newTime);
                    int time_ms_rcv = CP56Time2a_getMillisecond(&newTime);
                    int time_sec_send = CP56Time2a_getSecond(MeasuredValueScaledWithCP56Time2a_getTimestamp(io));
                    int time_ms_send = CP56Time2a_getMillisecond(MeasuredValueScaledWithCP56Time2a_getTimestamp(io));
                    int ott = (time_sec_rcv - time_sec_send) * 1000 + (time_ms_rcv - time_ms_send);
                    printf("yx ott:%d ms\n", ott);
                }
                    break;
                case TEST_TYPE_YC: {
                    struct sCP56Time2a newTime;

                    CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());
                    printf("  measured scaled values with CP56Time2a timestamp:\n");
                    int time_sec_rcv = CP56Time2a_getSecond(&newTime);
                    int time_ms_rcv = CP56Time2a_getMillisecond(&newTime);
                    int time_sec_send = CP56Time2a_getSecond(MeasuredValueScaledWithCP56Time2a_getTimestamp(io));
                    int time_ms_send = CP56Time2a_getMillisecond(MeasuredValueScaledWithCP56Time2a_getTimestamp(io));
                    int ott = (time_sec_rcv - time_sec_send) * 1000 + (time_ms_rcv - time_ms_send);
                    printf("yc ott:%d ms\n", ott);
                }
                    break;
                case TEST_TYPE_YK: {
                    int ott = MeasuredValueScaled_getValue((MeasuredValueScaled) io);
                    printf("yk ott:%d ms\n", ott);
                    is_send = true;
                }
                    break;
                case TEST_TYPE_YT: {
                    int ott = MeasuredValueScaled_getValue((MeasuredValueScaled) io);
                    printf("yt ott:%d ms\n", ott);
                    is_send = true;
                }
                    break;
            }

            MeasuredValueScaledWithCP56Time2a_destroy(io);
        }
    } else if (CS101_ASDU_getTypeID(asdu) == M_SP_NA_1) {
        printf("  single point information:\n");

        int i;

        for (i = 0; i < CS101_ASDU_getNumberOfElements(asdu); i++) {

            SinglePointInformation io =
                    (SinglePointInformation) CS101_ASDU_getElement(asdu, i);

            printf("    IOA: %i value: %i\n",
                   InformationObject_getObjectAddress((InformationObject) io),
                   SinglePointInformation_getValue((SinglePointInformation) io)
            );

            SinglePointInformation_destroy(io);
        }
    } else if (CS101_ASDU_getTypeID(asdu) == C_TS_TA_1) {
        printf("  test command with timestamp\n");
    }

    return true;
}

int
main(int argc, char **argv) {
    const char *ip = "localhost";
    uint16_t port = IEC_60870_5_104_DEFAULT_PORT;
    signal(SIGINT, sigint_handler);
//    const char* localIp = NULL;
//    int localPort = -1;
    if (argc < 3) {
        printf("./%s ip port type\n", argv[0]);
        return 0;
    }

    ip = argv[1];
    port = atoi(argv[2]);

    printf("Connecting to: %s:%i\n", ip, port);
    CS104_Connection con = CS104_Connection_create(ip, port);

    CS101_AppLayerParameters alParams = CS104_Connection_getAppLayerParameters(con);
    alParams->originatorAddress = 3;

    CS104_Connection_setConnectionHandler(con, connection_handler, NULL);
    CS104_Connection_setASDUReceivedHandler(con, asdu_received_handler, NULL);

    /* optional bind to local IP address/interface */
////    if (localIp)
//        CS104_Connection_setLocalAddress(con, localIp, localPort);

    /* uncomment to log messages */
    CS104_Connection_setRawMessageHandler(con, raw_message_handler, NULL);

    if (CS104_Connection_connect(con)) {
        printf("Connected!\n");

        CS104_Connection_sendStartDT(con);
        Thread_sleep(2000);

        CS104_Connection_sendInterrogationCommand(con, CS101_COT_ACTIVATION, 1, IEC60870_QOI_STATION);
        Thread_sleep(2000);

        CS104_Connection_sendTestFR(con);
        Thread_sleep(1000);

        /* Send clock synchronization command */
        struct sCP56Time2a newTime;
        CP56Time2a_createFromMsTimestamp(&newTime, Hal_getTimeInMs());
        printf("Send time sync command\n");
        CS104_Connection_sendClockSyncCommand(con, 1, &newTime);
        Thread_sleep(1000);

        while (running) {
            switch (atoi(argv[3])) {
                case TEST_TYPE_YX: {
                    //
                    if (is_send) {
                        CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_PERIODIC, 0, 1, false, false);
                        InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YX, true,
                                                                                                            IEC60870_QUALITY_GOOD, &newTime);
                        CS101_ASDU_addInformationObject(newAsdu, io);
                        InformationObject_destroy(io);

                        CS104_Connection_sendASDU(con, newAsdu);
                        is_send = false;
                    }
                }
                    break;
                case TEST_TYPE_YC: {
                    //发送命令触发从站定时传输遥测指标,传输任意值在主站来计算摇测指令传输时延
                    if (is_send) {
                        CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false, CS101_COT_PERIODIC, 0, 1, false, false);
                        InformationObject io = (InformationObject) MeasuredValueScaledWithCP56Time2a_create(NULL, TEST_TYPE_YC, true,
                                                                                                            IEC60870_QUALITY_GOOD, &newTime);
                        CS101_ASDU_addInformationObject(newAsdu, io);
                        InformationObject_destroy(io);

                        CS104_Connection_sendASDU(con, newAsdu);
                        is_send = false;
                    }
                }
                    break;
                case TEST_TYPE_YK: {
                    //发出带时标的YK指令到从站(命令的),在从站计算时延
                    if (is_send) {
                        struct sCP56Time2a local_time;
                        CP56Time2a_createFromMsTimestamp(&local_time, Hal_getTimeInMs());
                        InformationObject sc = (InformationObject) SingleCommandWithCP56Time2a_create(NULL,
                                                                                                      TEST_TYPE_YK,
                                                                                                      true, false,
                                                                                                      0, &local_time);
                        CS104_Connection_sendProcessCommandEx(con, CS101_COT_ACTIVATION, 1, sc);
                        InformationObject_destroy(sc);
                        is_send = false;
                    }
                }
                    break;
                case TEST_TYPE_YT: {
                    //发出带时标的YT指令到从站,在从站计算时延
                    if (is_send) {
                        struct sCP56Time2a local_time;
                        CP56Time2a_createFromMsTimestamp(&local_time, Hal_getTimeInMs());
                        InformationObject sc = (InformationObject) SingleCommandWithCP56Time2a_create(NULL,
                                                                                                      TEST_TYPE_YT,
                                                                                                      true, false,
                                                                                                      0, &local_time);
                        CS104_Connection_sendProcessCommandEx(con, CS101_COT_ACTIVATION, 1, sc);
                        InformationObject_destroy(sc);
                        is_send = false;
                    }
                }
                    break;
                default:
                    break;
            }
            Thread_sleep(1000);
        }

        InformationObject sc_yx = (InformationObject) SingleCommand_create(NULL, TEST_TYPE_YX, false, false,
                                                                           0);
        CS104_Connection_sendProcessCommandEx(con, CS101_COT_ACTIVATION, 1, sc_yx);
        InformationObject_destroy(sc_yx);

        InformationObject sc_yc = (InformationObject) SingleCommand_create(NULL, TEST_TYPE_YC, false, false,
                                                                           0);
        CS104_Connection_sendProcessCommandEx(con, CS101_COT_ACTIVATION, 1, sc_yc);
        InformationObject_destroy(sc_yc);

        printf("Wait ...\n");

        Thread_sleep(1000);
        CS104_Connection_sendStopDT(con);
    } else
        printf("Connect failed!\n");

    Thread_sleep(1000);

    CS104_Connection_destroy(con);

    printf("exit\n");
}
//
// Created by Administrator on 2024/1/5.
//

#ifndef TESTTERMINAL_POWER_DEF_H
#define TESTTERMINAL_POWER_DEF_H

typedef enum {
    TEST_TYPE_YX = 1,   //遥信
    TEST_TYPE_YC,   //摇测
    TEST_TYPE_YK,   //遥控
    TEST_TYPE_YT,   //摇调
} TEST_TYPE;

typedef enum {
    SIGNAL_COMMAND = 1,
    DOUBLE_COMMAND,
    STEP_COMMAND,
    SET_POINT_SCALED,
    SET_POINT_NORMALIZED,
    SET_POINT_SHORT,
    BIT_STRING_32,
} IEC60870_COMMAND_TYPE;

typedef enum {
    SELECT_COMMAND = 1,
    BREAK_COMMAND,
    EXECUTE_COMMAND,
} IEC60870_OPERATION_TYPE;

typedef enum {
    SIGNAL_POINT_INFO = 1,
    DOUBLE_POINT_INFO,
    STEP_POINT_INFO,
    BIT_STRING_32_INFO,
    MEASURED_NORMALIZED_INFO,
    MEASURED_SCALED_INFO,
    MEASURED_SHORT_INFO,
} IEC60870_INFO_TYPE;

#endif //TESTTERMINAL_POWER_DEF_H

结果:

如下为抓包内容:

1.rar

五、最后

基本上iec101、iec104协议由于应用时间比较长了,资料相对多一些,再加上开源库,所以掌握起来会相对简单一些。