IEC60870-5-104通信规约 | 报文解析 | 组织报文与解析报文(C++)

CSDN 2024-08-08 08:05:04 阅读 93

文章目录

一、IEC60870-5-104通信规约1.IEC104的报文结构2.IEC104的报文格式--I/U/S格式2.1 I帧2.2 U帧2.3 S帧

3.应用服务数据单元ASDU

二、IEC60870-5-104规约通信过程报文帧解析三、组织报文与解析报文(C++)

一、IEC60870-5-104通信规约

IEC60870-5-104规约,简称IEC104,IEC104规约由国际电工委员会制定。IEC104规约把IEC101的应用服务数据单元(ASDU)用网络规约TCP/IP进行传输的标准,该标准为远动信息的网络传输提供了通信规约依据。采用104规约组合101规约的ASDU的方式后,可很好的保证规约的标准化和通信的可靠性。

TCP端口号为2404,站端为Server 控端为Client,平衡式传输

IEC 104规约是一种电力自动化通信协议,通常采用的是平衡传输模式。在平衡传输模式下,所有站都可以启动报文传输,即主站和从站都可以独立地发起通信会话,进行数据的发送和接收。

平衡模式传输与非平衡模式传输

平衡模式传输

平衡方式传输是一种通信传输方式,它允许通信双方在没有规定谁先发起通信的情况下进行会话。在这种传输方式中,通信的两个实体都具备发起和接收信息的能力,从而实现双向通信。这种传输方式可以提高通信的灵活性和效率,因为它不依赖于固定的通信发起方。

非平衡模式传输

在非平衡传输模式下,主站可以顺序地召唤各被控站,而被控站只在被查询时才传输数据。这种模式适用于点对点、星形、多点等多种配置

IEC 60870-5-104 标准中的平衡模式传输过程允许通信双方在没有规定谁先发起通信的情况下进行会话,即主站和从站都可以独立地发起通信会话。以下是平衡模式传输过程的一般步骤

建立连接:通信双方建立一个稳定的通信连接。在 TCP/IP 环境中,这涉及到建立一个 TCP 连接。初始化:在开始数据交换之前,双方可能需要进行初始化过程,以确保它们都处于正确的工作状态。数据传输:在平衡模式下,主站和从站都可以独立地发送数据。数据可以是请求、命令、状态更新或其他类型的信息。确认和响应:接收方收到数据后,会发送确认信息或相应的响应。这确保了数据的可靠传输。事件触发:在平衡模式下,任何站都可以独立地触发事件,例如,当检测到特定条件或异常时,从站可以主动向主站发送事件通知。周期性更新:被控站可以周期性地向主站发送过程变量的最新值,以保持数据的时效性。总召唤:控制站可以使用总召唤命令请求从站发送所有过程变量的实际值,以刷新控制站的数据。时钟同步:为了确保时间一致性,控制站可以发送时钟同步命令给被控站,以同步它们的时钟。错误处理:如果在传输过程中出现错误,比如数据包丢失或损坏,双方都可以采取措施进行重传或错误纠正。通信结束:当通信双方完成数据交换后,可以结束通信会话。这通常涉及到关闭 TCP 连接。

1.IEC104的报文结构

IEC 60870-5-104 协议的报文结构为应用规约数据单元(APDU)。主要包括两个部分,分别是应用规约控制信息(APCI)、应用服务数据单元(ASDU)。

在这里插入图片描述

APCI应用规约控制信息:它是所有发送、接收的报文头并且可以单独发送的。

ASDU 应用服务数据单元:

ASDU 是报文的有效载荷部分,包含了实际传输的数据,如遥信、遥测、遥控等信息。ASDU 由类型标识、可变结构限定词、传送原因、ASDU 公共地址、信息对象地址等组成。ASDU 的结构和内容取决于传输的数据类型和上下文。

APDU 是由 APCI 和 ASDU 组成的完整报文,是 IEC 60870-5-104 协议中传输的基本单元。

APDU 应用规约数据单元(整个数据) = APCI 应用规约控制信息(固定6个字节) + ASDU 应用服务数据单元(长度可变)

APDU的长度域定义了APDU体的长度,它包括APCI的四个控制域八位位组和ASDU(ASDU长度加4)。APDU应用规约数据单元长度最大253(255减去启动符与本身)

控制域定义了确保报文不丢失和重复传送的控制信息(也就是发送序列号和接收序列号),报文传输启动/停止,以及传输连接的监视等。

2.IEC104的报文格式–I/U/S格式

控制域对应不同类型的格式(I帧、U帧、S帧),意义和格式都不相同。

在这里插入图片描述

2.1 I帧

I格式(信息传输格式类型—Information transmit format):

I格式报文用于传递信息,包含了应用服务数据单元(ASDU)。==I帧为信息帧,用于传输具体的通信数据。==遥信、遥测、遥控、遥调、总召、对时等都需要使用<code>I格式传送。I帧用于传输含有信息体的报文和确认对方I格式的信息报文

I帧的基本格式如下:

启动字符:固定为 68H,用于标识报文的开始。

APDU的后续长度:这个长度是变长的

控制域:

I帧报文控制域格式如下:

img

包含ASDU

后续会对应用服务数据单元ASDU进行详细分析。

2.2 U帧

U格式(不计数的控制功能类型—Unnumbered control function):

U格式报文用于数据传输的过程控制主站实现子站进行数据传输(STARTPDT),停止子站的数据传输(STOPDT),和TCP链路测试(TESTER),它不包含ASDU(应用服务数据单元)。在同一时刻TESTFR、STOPDT、STARTDT中只能有一个功能可以被激活。U帧为控制帧,用于控制启动、停止和测试U帧是用于传输链路控制命令的报文

U帧的基本格式如下:

启动字符:固定为 <code>68H,用于标识报文的开始。

APDU的后续长度:这个长度通常是固定的,为4个字节。

控制域:

U帧报文控制域格式如下:

img

控制域1的六种情形:

U格式报文 控制域 语义
<code>68 04 07 00 00 00 <code>00000111 启动命令
<code>68 04 0B 00 00 00 <code>00001011 启动确认
<code>68 04 13 00 00 00 <code>00010011 停止命令
<code>68 04 23 00 00 00 <code>00100011 停止确认
<code>68 04 43 00 00 00 <code>01000011 测试命令
<code>68 04 83 00 00 00 <code>10000011 测试确认

控制域2~4为<code>00 00 00

不包含ASDU

U帧的主要用途:

连接的建立和断开:U帧用于在通信开始前进行握手,建立连接,并在通信结束后进行断开。它可以用来发送连接请求、确认连接、发送心跳信号、请求断开连接等

测试链路连通性:U帧还用于测试链路的连通性。在一段时间内如果没有数据传输,主站或子站可以发送测试U帧(TESTFR)来检查对方是否仍然在线,并期待收到测试确认U帧(TESTFR ACK)作为响应

维护链路活动状态:当双方都没有数据发送时,U帧可以用来维持TCP连接的活动状态,防止因长时间无活动而导致的连接断开

U帧报文解析(举例):

当首次建立通信连接时,需要使用U帧来启动传输

传输启动命令

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>07 00 00 00

传输启动确认(响应)

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>0B 00 00 00

在数据传输过程中,为了确保链路的连通性,通信双方可以周期性地发送测试U帧。或者如果主站超过一定时间没有下发报文或者RTU也没有上送任何报文,双方都可以按频率发送U帧,测试帧(U帧)

测试命令

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>43 00 00 00

测试确认(响应)

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>83 00 00 00

当要断开通信连接时,需要使用U帧来停止传输

停止命令

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>13 00 00 00

停止确认(响应)

启动字符 APDU后续长度 控制域
<code>68 <code>04 <code>23 00 00 00

2.3 S帧

S格式(计数的监视功能类型—Numbered supervisory functions):

S帧用于当没有I格式报文回应的情况下,回应确认报文的接收,它不包含ASDU(应用服务数据单元)。S帧为监视帧,主要作用是向发送方确认已经成功接收并处理了特定的I帧S帧传送的没有具体的信息内容,是用来对站端所发信息报文的确认。S帧的主要作用是对I帧进行确认,它不包含任何实际的数据负载,仅用于确保数据传输的可靠性和顺序性。通过S帧,接收方可以告知发送方已经成功接收到特定序列号的I帧,从而允许发送方进行流量控制和错误恢复。

S帧的基本格式如下:

启动字符:固定为 68H,用于标识报文的开始。

APDU后续长度:这个长度通常是固定的,为4个字节。

控制域:

S帧报文控制域格式如下:

img

控制域1与控制域2:固定为<code>01 00

控制域3与控制域4 (接收序列号):已接收的I格式报文的发送序列号 + 2

不包含ASDU

S帧的主要用途:

确认接收:接收方通过发送S帧来确认已经成功接收并处理了发送方的I帧。流量控制:通过调整发送S帧的频率,接收方可以对发送方的数据流量进行控制,防止因接收方处理能力有限而导致的数据溢出。错误检测:如果发送方发现接收方的S帧中确认的序列号与预期不符,可以采取重传等措施。

S帧的使用策略可以根据实际的通信需求和系统设计来定制。例如:

固定频率确认:无论接收到多少I帧,接收方都按照固定频率发送S帧。比如接收8帧I帧回答一帧S帧按需确认:接收方只有在接收到特定数量的I帧后,才发送一个S帧进行确认。即时确认:接收方在接收到每一个I帧后都立即发送S帧进行确认。

S帧报文解析(举例):

已接收的I

启动字符 APDU后续长度 控制域1~2(发送序列号) 控制域3~4(接收序列号) ASDU
<code>68 <code>FA <code>6C 67 <code>84 00 ...

确认的S帧

启动字符 APDU后续长度 控制域1~2 控制域3~4(接收序列号)
<code>68 <code>04 <code>01 00 <code>6E 67

已接收的I格式报文的发送序列号 + 2 = 0x676C+ 2 = 0x676E,表明接收方已经成功接收并处理了发送方的I帧。

3.应用服务数据单元ASDU

应用服务数据单元(ASDU)是IEC 60870-5-104协议中用于传输具体应用数据的单元。ASDU是I帧(Information Frame)的有效载荷部分,包含了实际传输的数据,如遥信、遥测、遥控、遥调、总召、对时等信息。

其结构可以简化为:

类型标识 可变结构限定词 传送原因 ASDU公共地址 信息体地址 数据和品质描述
1个字节 1个字节 2个字节 2个字节 3个字节 变长

类型标识(1个字节)

标识出后面的信息体的数据类型,不同的类型标识对应不同的数据结构和意义。

完整的类型标识与每一种类型标识特点如下:

类型标识特点

监视方向过程信息

标度化值与归一化值占2个字节,短浮点数占4个字节一般为从站发送给主站

控制方向过程信息

标度化值与归一化值占2个字节,短浮点数占4个字节一般为主站发送给从站

监视方向系统信息

当厂站(从站)在重新上电、初始化参数、重新分配缓存区等情况下,厂站需要给主站发送该类型,而主站收到该类型的APDU包,主站一般会做一次总召唤;一般为从站发送给主站

控制方向的系统信息

一般为主站发送给从站

常用的类型标识

image-20240521163411555

完整的类型标识

在监视方向的过程信息(上行)

<1> 0x01:M-SP-NA-1 =单点信息 (总召唤遥信、变位遥信)

<2> 0x02:M-SP-TA-1 =带时标单点信息 (SOE事项)

<3> 0x03:M-DP-TA-1 =双点信息 (遥信)

<4> 0x04:M-DP-TA-1 =带时标双点信息

<5> 0x05:M-ST-NA-1 =步位置信息

<6> 0x06:M-ST-TA-1 =带时标步位置信息

<7> 0x07:M-BO-NA-1 =32比特串

<8> 0x08:M-BO-TA-1 =带时标32比特串

<9> 0x09:M-ME-NA-1 =测量值,归一化值 (遥测)

<10> 0x0A:M-ME-TA-1 =测量值,带时标归一化值

<11> 0x0B:M-ME-NB-1 =测量值,标度化值 (遥测)

<12> 0x0C:M-ME-TB-1 =测量值,带时标标度化值

<13> 0x0D:M-ME-NC-1 =测量值,短浮点数(遥测)

<14> 0x0E:M-ME-TC-1 =测量值,带时标短浮点数

<15> 0x0F:M-IT-NA-1 =累计量 (电度量)

<16> 0x10:M-IT-TA-1 =带时标累计量

<17> 0x11:M-EP-TA-1 =带时标继电保护装置事件

<18> 0x12:M-EP-TB-1 =带时标继电保护装置成组启动事件

<19> 0x13:M-EP-TC-1 =带时标继电保护装置成组输出电路信息

<20> 0x14:M-SP-NA-1 =具有状态变位检出的成组单点信息

<21> 0x15:M-ME-ND-1 =测量值,不带品质描述的归一化值 (总召唤遥测量)

<30> 0x1E:M-SP-TB-1 =带时标CP56TimE2A的单点信息(遥信带时标)

<31> 0x1F:M-DP-TB-1 =带时标CP56TimE2A的双点信息(遥信带时标)

<32> 0x20:M-ST-TB-1 =带时标CP56TimE2A的步位信息

<33> 0x21:M-BO-TB-1 =带时标CP56TimE2A的32位串

<34> 0x22:M-ME-TD-1 =带时标CP56TimE2A的归一化测量值

<35> 0x23:M-ME-TE-1 =测量值,带时标CP56TimE2A的标度化值

<36> 0x24:M-ME-TF-1 =测量值,带时标CP56TimE2A的短浮点数

<37> 0x25:M-IT-TB-1 =带时标CP56TimE2A的累计值

<38> 0x26:M-EP-TD-1 =带时标CP56TimE2A的继电保护装置事件

<39> 0x27:M-EP-TE-1 =带时标CP56TimE2A的成组继电保护装置成组启动事件

<40> 0x28:M-EP-TF-1 =带时标CP56TimE2A的继电保护装置成组输出电路信息

<41~44> 为将来的兼容定义保留

在控制方向的过程信息(下行)

<45> 0x2D:C-SC-NA-1 =单命令 (遥控)

<46> 0x2E:C-DC-NA-1 =双命令 (遥控)

<47> 0x2F:C-RC-NA-1 =升降命令

<48> 0x30:C-SE-NA-1 =设定值命令,归一化值 (遥调)

<49> 0x31:C-SE-NB-1 =设定值命令,标度化值

<50> 0x32:C-SE-NC-1 =设定值命令,短浮点数

<51> 0x33:C-BO-NA-1 =32比特串

<52~57> 为将来的兼容定义保留

<58> 0x3A:C-SC-TA-1 =带时标CP56TimE2A的单命令

<59> 0x3B:C-DC-TA-1 =带时标CP56TimE2A的双命令

<60> 0x3C:C-RC-TA-1 =带时标CP56TimE2A的升降命令

<61> 0x3D:C-SE-TA-1 =带时标CP56TimE2A的设定值命令,归一化值

<62> 0x3E:C-SE-TB-1 =带时标CP56TimE2A的设定值命令,标度化值

<63> 0x3F:C-SE-TC-1 =带时标CP56TimE2A的设定值命令,短浮点数

<64> 0x40:C-BO-TA-1 =带时标CP56TimE2A的32比特串

<65~69> 为将来的兼容定义保留

在监视方向的系统信息(上行)

<70> 046x:M-EI-NA-1 =初始化结束

<71~99> 为将来的兼容定义保留

在控制方向的系统信息(下行)

<100> 0x64:C-IC-NA-1 =总召唤命令 (总召唤)

<101> 0x65:C-CI-NA-1 =电能脉冲召唤命令 (召唤电度量)

<102> 0x66:C-RD-NA-1 =读单个参数命令 (参数设置)

<103> 0x67:C-CS-NA-1 =时钟同步命令 (校时)

<104>0x68:C-TS-NA-1 =测试命令

<105> 0x69:C-RP-NA-1 =复位进程命令

<106> 0x6A:C-CD-NA-1 =延时传输命令

<107> 0x6B:C-TS-TA-1 =带时标CP56TimE2A的测试命令

<108~109> 为将来的兼容定义保留

在控制方向的参数命令(下行)

<110> 0x6E:P-ME-NA-1 =测量值参数,归一化值

<111> 0x6F:P-ME-NB-1 =测量值参数,标度化值

<112> 0x70:P-ME-NC-1 =测量值参数,短浮点数

<113> 0x71:P-AC-NA-1 =参数激活

<114~119> 为将来的兼容定义保留

文件传输

<120> 0x78:F-FR-NA-1 =文件准备好

<121> 0x79:F-SR-NA-1 =节已准备好

<122> 0x7A:F-SC-NA-1 =召唤目录,选择文件,召唤文件,召唤节

<123> 0x7B:F-LS-NA-1 =最后的节,最后的度

<124> 0x7C:F-AF-NA-1 =确认文件,确认节

<125> 0x7D:F-SG-NA-1 =段

<126> 0x7E:F-DR-TA-1 =目录{空白或×,只在监视(标准)方向有效}

可变结构限定词(1个字节)

bit7 bit6~bit0
SQ 信息对象数目

SQ控制信息对象地址是否连续

<code>SQ=1表示连续,即当有多个信息对象时,可以依据信息体地址推导出所有信息对象的地址(只有第一个信息对象有地址,其他对象的地址就是累加1)SQ=0表示不连续,即每个信息对象必须给出其地址

总召唤时,为了压缩信息传输时间SQ=1;而在从站主动上传变化数据时,因为地址不连续,采用SQ=0

信息对象数目:如:召唤的遥测、遥信等点位的数目,最多为127个

传送原因(2个字节)

bit7 bit6 bit5~bit0 bit7~bit0
T P/N 传送原因 源发地址

传输原因可以是一个或两个字节,根据需要可以选择带或不带源发地址。

<code>T:T=0未实验,T=1实验(一般T=0)

P/NP/N=0肯定确认,P/N=1否定确认(正常为P/N=0;P/N=1 说明该报文无效)

源发地址:用来表明来自哪个主站的召唤,一般情况下不适用。规定源发地址不使用时,应置零。

传送原因:

十进制 十六进制 cause 方向
1 0X01 周期、循环(遥测) 上行(从站->主站)
2 0x02 背景扫描(遥信)(遥测) 上行
3 0x03 突发信息(遥信)(遥测) 上行
4 0x04 初始化 上行
5 0x05 请求、被请求(遥信被请求)(遥测被请求) 上行、下行
6 0x06 激活(遥控、参数设置 控制方向) 下行(主站->从站)
7 0x07 激活确认(遥控、参数设置 监视方向) 上行
8 0x08 停止激活(遥控、参数设置 控制方向) 下行
9 0x09 停止激活确认 (遥控、参数设置 监视方向) 上行
10 0x0A 激活终止(遥控 监视方向) 上行
20 0x14 响应站总召唤(遥信响应总召唤)(遥测响应总召唤) 上行
21-36 0x15-0x24 响应第1组召唤-响应第16组召唤 上行
37 0x25 响应计数量召唤 上行
38-41 0x26-0x29 响应第1组计数量-响应第4组计数量 上行
44 0x2c 未知的类型标识(遥控、参数设置 监视方向) 上行
45 0x2d 未知的传送原因(遥控、参数设置 监视方向) 上行
46 0x2e 未知的应用服务数据单元公共地址(遥控、参数设置 监视方向) 上行
47 0x2f 未知的信息对象地址(遥控、参数设置 监视方向) 上行
48 0x30 遥控执行软压板状态错误 上行
49 0x31 遥控执行时间戳错误 上行
50 0x32 遥控执行数字签名认证错误 上行

ASDU公共地址–厂站地址(2个字节)

一般为厂站地址,每一个配电终端对应唯一的一个值。规定:高位字节为<code>0x00,低位字节中 ,1-254 为站地址;255为全局地址。

信息体地址(3个字节)

image-20240522094011637

总召唤,信息地址为<code>00 00 00

主站向子站发送的总召唤命令帧、子站向总站发送的总召唤确认帧、子站信息传送完毕向主站发送的总召唤结束帧,这三个帧的信息体地址均为00 00 00。而这个过程中子站向主站发送的全遥测、全遥信等的信息体地址为后续遥测/遥信数据的起始地址,如:01 4C 00。每个遥测/遥信地址在该地址的基础上依次加1。

时钟同步,信息体地址为00 00 00

主站向子站发送的对时报文帧、主站收到来自子站的对时返回帧,这两个帧的信息体地址均为00 00 00

复位进程,信息体地址为00 00 00

初始化,信息体地址为00 00 00

如果RTU有变化遥测数据时,主动上传,主站收到的变化遥测报文帧中的信息体地址为变化遥测对应的设备点位地址,如:01 4C 00

数据与品质描述(变长)

数据(变长,数据类型由前面的类型标识所决定)

举例信息体的情形:

连续信息传输

带时标CP56TimE2A的遥测

image-20240522134534134

不带时标CP56TimE2A的遥测

image-20240522134615738

带时标CP56TimE2A的遥信

image-20240522134749364

不带时标CP56TimE2A的遥信

image-20240522134819124

非连续信息传输型

带时标CP56TimE2A的遥测

image-20240522134916095

不带时标CP56TimE2A的遥测

image-20240522134944924

带时标CP56TimE2A的遥信

image-20240522135031300

不带时标CP56TimE2A的遥信

image-20240522135056480

遥控

单点遥控

image-20240522135154975

单点遥控信息;

image-20240522135225825

S/E = 0 遥控执行命令;S/E=1 遥控选择命令;QU 被控站内部确定遥控输出方式

1 短脉冲方式输出2 长脉冲方式输出3 持续脉冲方式输出 RES :保留位SCS : 设置值; 0 = 控开 ;1 = 控合

双点遥控

image-20240522135507294

双点遥控信息:

image-20240522135542500

S/E = 0 遥控执行命令;S/E=1 遥控选择命令QU 被控站内部确定遥控输出方式

1 短脉冲方式输出2 长脉冲方式输出3 持续脉冲方式输出其他值没有定义 DCS 控制

1 控分2 控合3 无效控制

设定值(遥测):

image-20240522135947934

QOS:设定命令限定词

image-20240522140037246

S/E : 0 设定执行;1 设定选择设定命令限定词: 基本就是 0 ,因为其他并没有定义

这里需要关注的几个点:

归一化值、标度化值、短浮点数对遥测信息体数据的长度的影响

归一化值(占2个字节)

标度化值(占2个字节)

短浮点数(占4个字节)

遥信数据用一个字节表示

带时标CP56TimE2A与不带时标对数据的影响

image-20240522130931740

带时标CP56TimE2A的数据,

如果是遥测,则会在重复信息体数据+品质描述词,并在最后加7个字节的时标如果是遥信,则信息体数据与品质描述词会合在一个字节中,再在最后加7个字节的时标

连续数据上送(SQ=1)与变量数据上送(SQ=0)对数据的影响

SQ=1,连续数据上送,如:上送全遥测/全遥信报文,此时的数据部分不包含每个信息对象的地址,只要有前一个字段“信息体地址”就可以。例如:

image-20240522132430353

SQ=0,离散数据上送,如:上送变化遥测/变化遥信报文,此时的数据部分会包含每个信息对象的地址,例如:

image-20240522132633671

品质描述词(1个字节)

分为遥信品质描述词与遥测品质描述词

image-20240522105418789

IV(有效标志):IV = 0 状态有效;IV = 1 状态无效;

若值被正确采集就是有效,在采集功能确认了信息源的反常状态(例如:装置的启动过程中或者一些配置错误),那么值就是无效的。信息对象的值在这些条件下没有被定义。标上无效用以提醒使用者,此值不正确而不能使用。

NT(刷新标志):NT=0 刷新成功;NT=1 刷新未成功;

若最近的刷新成功则值就称为当前值,若一个指定的时间间隔内刷新不成功或者其值不可用,值就称为非当前值。设备处于调试态或装置通讯中断都有可能造成非当前值。、

SB(取代标志位):SB=0 未被取代;SB = 1 被取代;

当信息对象的值由值班员(调度员)输入(即人工置数)或者由当地自动原因(模拟遥信)所提供时,即为被取代。

BL(封锁标志位):BL=0 未被封锁;BL=1 封锁;

信息对象的值为传输而被封锁,值保持封锁前被采集的状态。封锁和解锁可以由当地联锁机构或当地自动原因启动。

RES(保留位)

SPI(遥信状态位)

单点遥信,0=开;1=合,占一个bit位。

双点遥信,1=开,2=合,0和3为中间状态,占两个bit位。

OV(溢出标志):OV=0 未溢出;OV=1 遥测超出量程,发生溢出

信息对象的值超出了预先定义值的范围(主要适用模拟量值)。仅在遥测品质结构词中出现。

二、IEC60870-5-104规约通信过程报文帧解析

要想理解IEC60870-5-104规约通信过程,需要先了解多种时间间隔,这些时间间隔用于确保数据的完整性、系统的同步以及通信链路的监控。

总召唤周期(遥测、遥信)

控制站(主站)向所有受控站(从站)发送总召唤命令的周期,总召唤可以确保控制站获得所有相关设备的当前状态信息。

站对时周期

站对时周期是指控制站与受控站之间进行时钟同步的频率。这个周期确保了系统中所有设备的时间同步。

电度召唤时间间隔

召唤电度周期是指控制站请求受控站发送电度量(如电能表读数)信息的时间间隔。

测试间隔

测试间隔是指控制站或受控站发送测试帧(如TESTFR U帧)以检测和维护通信链路状态的时间间隔。如果在指定的测试间隔内没有收到对方的响应,可能表明通信链路存在问题。

主从站间的通信过程

传输启动过程

主站向从站发送启动命令(STARTDT U帧),来启动数据传输

报文帧:68 04 07 00 00 00

解析:68(启动符) 04(长度4) 07(控制域0000 0111) 00 00 00

主站收到来自从站的启动确认

报文帧:68 04 0B 00 00 00

解析:68(启动符) 04(长度4)0B(控制域0000 1011) 00 00 00

总召唤过程:总召唤功能是在初始化以后进行,或者是定期进行总召唤

主站向从站发送的总召唤命令帧,会设置总召唤周期如:5分钟,来定时发送总召唤命令帧。总召唤的内容包括子站的遥测、遥信等。

报文帧:68 0E 00 00 00 00 64 01 06 00 01 00 00 00 00 14

解析:68(启动符) 0E(长度14) 00 00(发送序号) 00 00(接受序号) 64(类型标示召唤全部数据) 01(可变结构限定词 sq=0:离散的信息报告) 06 00 (传输原因 0006:激活)01 00 (公共地址即RTU地址)00 00 00(信息体地址) 14(区分是总召唤还是分组召唤,02年修改后的规约中没有分组召唤)

子站收到后,如果确认,则主站会收到来自子站的总召唤确认帧;如果否认,则子站会送否定确认

报文帧:68 0E 00 00 02 00 64 01 07 00 01 00 00 00 00 14

解析:68(启动符) 0E(长度14) 00 00(发送序号) 02 00(接受序号) 64(类型标示召唤全部数据) 01(可变结构限定词 sq=0:离散的信息报告)07 00(传输原因 0007:激活确认)01 00(公共地址即RTU地址) 00 00 00 (信息体地址) 14(区分是总召唤还是分组召唤,02年修改后的规约中没有分组召唤)

子站连续地向主站传送数据,包括遥测 、遥信等

主站收到的全遥测报文

报文帧:68 F3 02 00 02 00 0D AE 14 00 01 00 01 4C 00 第1点遥测的4字节浮点数值 第1点遥测的品质描述 第2点遥测的4字节浮点数值 第2点遥测的品质描述 ……

解析:68(启动符) F3(长度243)02 00(发送序号)02 00 (接受序号)0D (带品质描述的浮点值,每个遥测值占5个字节)AE(可变结构限定词10101110 SQ=1:表示顺序的信息报文;101110=46:表示后面有46个遥测) 14 00(0014:响应总召唤) 01 00 (公共地址即RTU地址)01 4C 00 (3字节的第1点遥测信息体地址,每个遥测地址在该地址的基础上依次加1)

9A 99 41 41 00 (yc1)

34 33 97 41 00 (yc2)

67 66 08 C2 00 (yc3)

33 33 03 42 00 (yc4)

2E 33 23 41 00 (yc5)

67 66 92 C1 00 (yc6)

66 66 AA C1 00(yc7)

9A 99 19 B6 00(yc8)

9A 99 11 C1 00(yc9)

36 33 29 42 00(yc10)

9A 19 47 43 00 (yc40)

CE 4C 78 C3 00 (yc41)

00 00 C9 42 00(yc42)

35 33 7D C3 00 (yc43)

9A 99 AE 42 00 (yc44)

CA 2C 4B 44 00(yc45)

CD CC 8C 36 00 (yc46)

主站收到的全遥信报文

报文帧:68 71 04 00 02 00 01 E4 14 00 01 00 01 00 00 01 00 00 01 00 00 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 01 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 01 00 00 01 00 00 00 01 00 00 00 00 00 01 00 00 00 00 00 00 00 0100 00 00 00 00 00 00 00 00 01

解析:68(启动符) F3(长度243) 04 00(发送序号) 02 00(接受序号) 01(不带时标的单点遥信) E4可变结构限定词11100100 SQ=1:表示顺序的信息报文;1100100=100:表示后面有100个遥信 14 00(0014:相应总召唤) 01 00(公共地址即RTU地址) 01 00 00(3字节的第1点遥信信息体地址,每个遥信地址在该地址的基础上依次加1)

01 (yx1)

00 (yx2)

00 01 00 00 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 01 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 01 00 00 01 00 00 00 01 00 00 00 00 00 01 00 00 00 00 00 00 00 0100 00 00 00 00 00 00 00 00 01

子站信息传送完毕后,发送总召唤结束帧

报文帧:68 0E 06 00 02 00 64 01 0A 00 01 00 00 00 00 14

解析:68(启动符) 0E(长度14) 0C 00 (发送序号)02 00 (接受序号)64 (类型标示召唤全部数据)01(可变结构限定词 sq=0:离散的信息报告) 0A 00 (传输原因 000A:激活结束)01 00(公共地址即RTU地址) 00 00 00 (信息体地址)14(区分是总召唤还是分组召唤,02年修改后的规约中没有分组召唤)

召唤电度量(电能脉冲)过程

主站向从站发送电度召唤命令,会设置电度召唤间隔,如:10分钟。召唤电度量的内容为电能脉冲数据

报文帧:68 0E 02 00 00 00 65 01 06 00 01 00 00 00 00 05

解析:68(启动符) 0E(长度14) 02 00(发送序号) 00 00(接受序号) 65(类型标识:召唤电度量) 01(可变结构限定词 sq=0:离散的信息报告) 06 00(传输原因 0006:激活) 01 00(公共地址即RTU地址) 00 00 00(信息体地址) 05

子站收到后,如果确认,则主站会收到来自子站的召唤电度量确认帧;如果否认,则子站会送否定确认

报文帧:68 0E 08 00 04 00 65 01 07 00 01 00 00 00 00 05

解析:68(启动符) 0E(长度14) 08 00(发送序号) 04 00(接受序号) 65(类型标识:召唤电度量) 01(可变结构限定词 sq=0:离散的信息报告) 07 00(传输原因 0007:激活确认) 01 00(公共地址即RTU地址) 00 00 00(信息体地址) 05

子站连续地向主站传送电能量数据

报文帧:68 FD 0A 00 04 00 0F B0 25 00 01 00 01 64 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 02 00 00 00 00 03 00 00 00 00 04 00 00 00 00 05 00 00 00 00 06 00 00 00 00 07 00 00 00 00 08 00 00 00 00 09 00 00 00 00 0A 00 00 00 00 0B 00 00 00 00 0C 00 00 00 00 0D 00 00 00 00 0E 00 00 00 00 0F 00 00 00 00 10 00 00 00 00 11 00 00 00 00 12 00 00 00 00 13 00 00 00 00 14 00 00 00 00 15 00 00 00 00 16 00 00 00 00 17 00 00 00 00 18 00 00 00 00 19 00 00 00 00 1A 00 00 00 00 1B 00 00 00 00 1C 00 00 00 00 1D 00 00 00 00 1E 00 00 00 00 1F 00 00 00 00 20 00 00 00 00 21 00 00 00 00 22 00 00 00 00 23 00 00 00 00 24 00 00 00 00 25 00 00 00 00 26 00 00 00 00 27 00 00 00 00 28 00 00 00 00 29 00 00 00 00 2A 00 00 00 00 2B 00 00 00 00 2C 00 00 00 00 2D 00 00 00 00 2E 00 00 00 00 2F

解析:68(启动符) FD(长度253) 0A 00(发送序号) 04 00(接受序号) 0F(累计量–电度量,每个占5个字节) B0(可变结构限定词10110000 SQ=1:表示顺序的信息报文;110000=48:表示后面有48个电度量) 25 00(0025:响应计数量召唤) 01 00(公共地址即RTU地址) 01 64 00(3字节的第1点电度量信息体地址,每个电度量地址在该地址的基础上依次加1)

00 00 00 00 00 00 00 00 00 01 00 00 00 00 02 00 00 00 00 03 00 00 00 00 04 00 00 00 00 05 00 00 00 00 06 00 00 00 00 07 00 00 00 00 08 00 00 00 00 09 00 00 00 00 0A 00 00 00 00 0B 00 00 00 00 0C 00 00 00 00 0D 00 00 00 00 0E 00 00 00 00 0F 00 00 00 00 10 00 00 00 00 11 00 00 00 00 12 00 00 00 00 13 00 00 00 00 14 00 00 00 00 15 00 00 00 00 16 00 00 00 00 17 00 00 00 00 18 00 00 00 00 19 00 00 00 00 1A 00 00 00 00 1B 00 00 00 00 1C 00 00 00 00 1D 00 00 00 00 1E 00 00 00 00 1F 00 00 00 00 20 00 00 00 00 21 00 00 00 00 22 00 00 00 00 23 00 00 00 00 24 00 00 00 00 25 00 00 00 00 26 00 00 00 00 27 00 00 00 00 28 00 00 00 00 29 00 00 00 00 2A 00 00 00 00 2B 00 00 00 00 2C 00 00 00 00 2D 00 00 00 00 2E 00 00 00 00 2F

子站信息传送完毕后,发送召唤电度量结束帧

报文帧:68 0E 10 00 04 00 65 01 0A 00 01 00 00 00 00 05

解析:68(启动符) 0E(长度14) 10 00(发送序号) 04 00(接受序号) 65(类型标识:召唤电度量) 01(可变结构限定词 sq=0:离散的信息报告) 0A 00(传输原因 000A:激活结束) 01 00(公共地址即RTU地址) 00 00 00(信息体地址) 05

站对时过程

主站向从站发送对时报文,会设置站对时间隔,如:20分钟。

报文帧:68 14 02 00 0E 00 67 01 06 00 01 00 00 00 00 8E 6D 2C 0B 2F 0B 0A

解析:68(启动符)14(长度20) 02 00 (发送序号)0E 00 (接受序号)67 (类型标示:67时钟同步)01 (可变结构限定词 sq=0:离散的信息报告)06 00 (传输原因 0006:激活)01 00(公共地址即RTU地址)

00 00 00 (信息体地址)

8E(毫秒低位) 6D(毫秒高位)

2C(分钟)

0B(时)

2F(日与星期)

0B (月)

0A(年)

主站收到来自从站的对时返回

报文帧:68 14 0E 00 04 00 67 01 07 00 01 00 00 00 00 8E 6D 2C 0B 2F 0B 0A

解析:68 (启动符)14 (长度20) 0E 00(发送序号) 04 00 (接受序号)67(类型标示:67时钟同步) 01 (可变结构限定词 sq=0:离散的信息报告)07 00(传输原因 0007:激活确认)01 00(公共地址即RTU地址)

00 00 00(信息体地址)

8E(毫秒低位)

6D(毫秒高位) 6D8E = 28046/1000 = 28秒 28046%1000 = 46毫秒

2C(分钟) 0010 1100 取 0-5 01100 = 12分

0B(时) 0000 1011 取 0-4 01011 = 11时

2F(日与星期) = 101111 取0-4 01111 = 15日

0B (月)= 0000 1011 取0-3 1011 = 11月

0A(年)= 0000 1010 取0-6 001010 = 10年

变化数据主动上传过程:如果RTU有变化数据则主动上传(从站向主站主动上传变化数据)

主站收到的变化遥测报文

报文帧:68 EA 04 00 00 00 0D 1C 03 00 01 00

变化遥测的3字节信息体地址 第1个变化遥测的4字节浮点数值 第1个变化遥测的品质描述

变化遥测的3字节信息体地址 第2个变化遥测的4字节浮点数值 第2个变化遥测的品质描述

……

解析:68(启动符) EA(长度234) 04 00 (发送序号)00 00(接受序号) 0D (带品质描述的浮点值,每个遥测值占5个字节)1C (可变结构限定词(00011100 sq=0: 离散的信息报告; 11100 = 28:表示有28个遥测值;) 03 00 (传输原因 0003:突发)01 00 (公共地址即RTU地址)

01 4C 00 (信息体地址)CE CC 64 41 00 ——1

02 4C 00 (信息体地址)CE CC B8 C1 00 ——2

04 4C 00 (信息体地址)33 33 03 C2 00 ——3

4C 00 67 (信息体地址) 66 92 C1 00 07 ——4

00 32 33 (信息体地址) 63 41 00 08 4C ——5

00 97 99 (信息体地址) 01 41 00 0A 4C——6

CE 8C C6 (信息体地址)C3 00 26 4C 00——24

00 E0 0E (信息体地址) 44 00 29 4C 00 ——25

34 F3 B8 (信息体地址)C3 00 2A 4C 00 ——26

9B 99 FC (信息体地址)C3 00 2C 4C 00 ——27

6C 66 B0(信息体地址) C2 00 4C 06 41——28

主站收到的SOE报文

SOE是为厂站端RTU采集的现场遥信变位信息,它包含了遥信变位的准确发生时间(精细到毫秒),RTU将其打包成SOE报文发送给主站系统,主站系统将规约报文解析成SOE报表

报文帧:68 20 12 00 04 00 1E 02 03 00 01 00

03 00 00 00 99 AF 3A 13 1E 03 00

03 00 01 00 99 AF 3A 13 1E 03 00

解析:68(启动符)20 (长度32)12 00(发送序号)04 00 (接受序号)1E (类型标示:带时标的遥信)02 (可变结构限定词 sq=0:离散的信息报告,2个SOE)03 00 (传输原因 0003:突发)01 00 (公共地址即RTU地址)

03 00 00 (信息体地址)

00 (分)99 AF(毫秒) 3A(分钟)13(时) 1E(日与星期) 03(月)00 (年)03 00 01 (信息体地址)

00 (分)99 AF(毫秒) 3A (分钟)13 (时)1E(日与星期) 03 (月)00(年)

补充

在总召唤时间间隔内,会用测试帧(TESTDT帧,U帧)来检测与维护通信链路状态,如:

24-05-22 14:22:16 : 104子站–U帧:测试

68 04 43 00 00 00

24-05-22 14:22:16 : 104主站–U帧:测试确认

68 04 83 00 00 00

24-05-22 14:22:56 : 104子站–U帧:测试

68 04 43 00 00 00

24-05-22 14:22:56 : 104主站–U帧:测试确认

主站会发送S帧,告诉从站已经成功接收并处理了特定的<code>I帧(主站已接收并处理的I帧,S帧)。

24-05-22 14:21:56 : 104主站–S帧

68 04 01 00 06 00

24-05-22 14:21:56 : 104主站–S帧

68 04 01 00 0C 00

24-05-22 14:21:56 : 104主站–S帧

68 04 01 00 12 00

三、组织报文与解析报文(C++)

首先,从数据库表中获取规约的编号,如:ptl_104_cj

然后,加载规约动态库,得到该规约对应的组织数据与解析数据的函数

通道通信发送数据处理

调用规约报文组织函数组织报文

m_pFuncbuildData(m_SPTLRecvSenddata, dynamic_cast<CUnitBase*>(m_pCurrentUnit)); //调用规约报文组织函数组织报文

PTL_API void buildData(S_RecvSendData &sRecvSendData, CUnitBase* pUnit) //组织报文

{ -- -->

S_PtlVariant sVariant;

memset(&sVariant, 0, sizeof(S_PtlVariant));

if(pUnit != NULL)

{

sVariant.pUnit = pUnit;

sVariant.pBySendBuffer = sRecvSendData.pSendBuf;

sVariant.uwSendDataLen = sRecvSendData.uiSendLen;

sVariant.bMainChannel = sRecvSendData.bMainChannel;

sVariant.byChannelMode = sRecvSendData.byChannelMode;

sVariant.byChannelIndex = sRecvSendData.byChannelIndex;

sVariant.pPtlFlag = (S_PtlFlag*)(pUnit->getTUFlagBuffer()); // 获取规约标志位使用的缓冲区

sendFrame(sVariant); //组织数据

sRecvSendData.uiSendLen = sVariant.uwSendDataLen;

}

}

基于IEC60870-5-104的通信过程来组织报文

发送STARTDT(U帧)是在规约初始化完成后、重新建立连接后、三分钟内没有通信默认通道是断的时。发送TESTDT(U帧)是在15秒内无数据通信时

调用buildU()接口来组织U帧

发送S帧是在主站解析完来自从站的I帧报文后

调用buildS()接口来组织S帧

针对遥控与遥调类型,首先这些数据的来源是站端解析来自云端的MQTT报文得到的

遥控

单点遥控,调用buildYKDataASDU45()接口双点遥控,调用buildYKDataASDU46()接口

遥调:

遥调模式-归一化值,调用buildYTCmdASDU48()接口遥调模式-标度化值,调用buildYTCmdASDU49()接口遥调模式-短浮点数,调用buildYTCmdASDU50()接口

超过总召时间间隔,调用buildCallAllASDU100()接口组织发送总召命令

超过遥脉总召时间间隔,调用buildDDASDU101()接口组织发送遥脉总召时间间隔

超过校时时间间隔,调用buildSyncTimeASDU103()接口组织发送校时报文

void sendFrame(S_PtlVariant &sVariant) //组织数据

{

sVariant.uwMaxSendLen = MAX_FRAME_LEN;

if(!sVariant.pPtlFlag->bPtlInitOK)

{

if(initPtl(sVariant))

{

sVariant.pPtlFlag->bPtlInitOK = true;

qDebug() << "initptl";

}

else

{

return;

}

}

if(sVariant.pUnit->isNeedRestartLink())

{

sVariant.pPtlFlag->bPtlInitOK = false;

sVariant.pPtlFlag->bLinkOK = 0;

sVariant.pUnit->setRestartLink(false);

return;

}

S_DateTime nowTime = qGetCurrentTime();

//轮询周期

quint32 uiScancYC = sVariant.pPtlFlag->pSPtl104->uiScancYCLen;

uiScancYC = 0 ? 5: uiScancYC;

//规约初始化

if(!sVariant.pPtlFlag->bPtlInitOK)

{

sVariant.pPtlFlag->bPtlInitOK = true;

}

//规约初始化完成

if(!sVariant.pPtlFlag->bLinkOK)

{

sVariant.pPtlFlag->bLinkOK = true;

sVariant.pPtlFlag->byUStart_V = 1;

}

S_DateTime time = qGetCurrentTime();

sVariant.pPtlFlag->tLastRecvTime = time; //上次接收数据时间

//作为数据接收方,要控制链路

if(sVariant.pPtlFlag->bLinkOK)

{

if(time - sVariant.pPtlFlag->tLastRecvTime > NODATA_LMT)

{

//三分钟没收到数据,通道是断的,发启动帧

sVariant.pPtlFlag->bLinkOK = 0;

sVariant.pPtlFlag->byUStart_V = 1;

}

//15秒无数据发送测试帧

else if(time - sVariant.pPtlFlag->tLastRecvTime > TESTDATA_LMT)

{

//测试ASDU超时

if(time - sVariant.pPtlFlag->tLastSendTime > T1)

{

sVariant.pPtlFlag->byTest_V = 1;

}

}

}

else

//重新建立连接

{

sVariant.pPtlFlag->uwRecvSN = 0;

sVariant.pPtlFlag->uwSendSN = 0;

if(time - sVariant.pPtlFlag->tLastSendTime >T0)

{

sVariant.pPtlFlag->byUStart_V = 1;

}

}

//判断链路连接成功,是否发送总召

if((sVariant.pPtlFlag->bLinkOK)&& (sVariant.pPtlFlag->bSendCallAll)

&& sVariant.pPtlFlag->byDowmFrame == FRAME_NULL)

{

sVariant.pPtlFlag->byDowmFrame = ASDU_100; //站总召命令

}

//链路成功,设置时钟

if((sVariant.pPtlFlag->bLinkOK) && (sVariant.pPtlFlag->bSendTimeSynchro)

&& sVariant.pPtlFlag->byDowmFrame == FRAME_NULL)

{

sVariant.pPtlFlag->byDowmFrame = ASDU_103; //时钟同步命令

}

if ((sVariant.pPtlFlag->bLinkOK) && (sVariant.pPtlFlag->bSendDDCall)

&& sVariant.pPtlFlag->byDowmFrame == FRAME_NULL)

{

sVariant.pPtlFlag->byDowmFrame = ASDU_101; //电能量召唤命令

}

if(uiScancYC > 0)

{

if(sVariant.pPtlFlag->bSendSFrame || (sVariant.pPtlFlag->bSendSFrame && nowTime - sVariant.pPtlFlag->tLastSFrameTime >= 1000))

{

buildS(sVariant);

}

if(sVariant.pPtlFlag->byUStart_V)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_STARTDT_V; //启动生效

}

if(sVariant.pPtlFlag->byUStart_C)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_STARTDT_C;

}

if(sVariant.pPtlFlag->byStop_V)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_STOPDT_V;

}

if(sVariant.pPtlFlag->byStop_C)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_STOPDT_C;

}

if(sVariant.pPtlFlag->byTest_V)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_TESTFR_V;

}

if(sVariant.pPtlFlag->byTest_C)

{

sVariant.pPtlFlag->byDowmFrame = UFRAME_TYPE_TESTFR_C;

}

if(sVariant.pPtlFlag->byDowmFrame != FRAME_NULL)

{

switch (sVariant.pPtlFlag->byDowmFrame) //根据启动控制信息,组织不同形式的帧

{

case UFRAME_TYPE_STARTDT_V:

buildU(sVariant,UFRAME_TYPE_STARTDT_V);

sVariant.pPtlFlag->byUStart_V = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_STARTDT_C:

buildU(sVariant,UFRAME_TYPE_STARTDT_C);

sVariant.pPtlFlag->byUStart_C = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_STOPDT_V:

buildU(sVariant,UFRAME_TYPE_STOPDT_V);

sVariant.pPtlFlag->byStop_V = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_STOPDT_C:

buildU(sVariant,UFRAME_TYPE_STOPDT_C);

sVariant.pPtlFlag->byStop_C = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_TESTFR_V:

buildU(sVariant,UFRAME_TYPE_TESTFR_V);

sVariant.pPtlFlag->byTest_V = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_TESTFR_C:

buildU(sVariant,UFRAME_TYPE_TESTFR_C);

sVariant.pPtlFlag->byTest_C = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

default:

break;

}

}

if(sVariant.pPtlFlag->bLinkOK)

{

if(sVariant.pUnit->getHaveCmd()&&!sVariant.pUnit->isCmdProcing())

{

sVariant.pUnit->setCmdProcing(true);

switch (sVariant.pUnit->getCmdType()) //采集装置:命令类型;转发装置:命令响应的类型

{

case CMD_TYPE_YK: //遥控

{

S_YKCmd* pYKCmd = static_cast<S_YKCmd*>(sVariant.pUnit->getCmd(CMD_TYPE_YK));

quint8 byType = 0;

sVariant.pUnit->getValue(S_DataID(TABLE_PREYK, pYKCmd->uiZFYKID, COL_PREYK_YKTYPE), &byType); //遥控类型:单点遥信与双点遥信

if(byType == EYKT_SP)

{

sVariant.pPtlFlag->byDowmFrame = ASDU_45;

}

else if (byType == EYKT_DP)

{

sVariant.pPtlFlag->byDowmFrame = ASDU_46;

}

}

break;

case CMD_TYPE_YT: //遥调

{

switch(sVariant.pPtlFlag->pSPtl104->byYTMode)

{

case YT104_MODE_ASDU48: //104遥调模式-归一化值

buildYTCmdASDU48(sVariant);

break;

case YT104_MODE_ASDU49: //遥调模式-标度化值

buildYTCmdASDU49(sVariant);

break;

case YT104_MODE_ASDU50: //遥调模式-短浮点数

buildYTCmdASDU50(sVariant);

break;

default:

break;

}

}

break;

case CMD_TYPE_SET_TIME: //时钟同步命令

sVariant.pPtlFlag->byDowmFrame = ASDU_103;

break;

case CMD_TYPE_CALL_DATA: //站总召命令

sVariant.pPtlFlag->byDowmFrame = ASDU_100;

break;

default:

break;

}

}

}

if(nowTime - sVariant.pPtlFlag->tLastSendCallAllTime >= sVariant.pPtlFlag->pSPtl104->uwAllCallInterval)

{

buildCallAllASDU100(sVariant);

}

if(nowTime - sVariant.pPtlFlag->tLastSendCallDDTime >= sVariant.pPtlFlag->pSPtl104->uwYMInterval)

{

// sVariant.pPtlFlag->bSendDDCall = 1;

buildDDASDU101(sVariant);

}

if(nowTime - sVariant.pPtlFlag->tLastCheckTime >= sVariant.pPtlFlag->pSPtl104->uwCheckTimeInterval)

{

buildSyncTimeASDU103(sVariant);

}

switch (sVariant.pPtlFlag->byDowmFrame)

{

case S_FRAME:

buildS(sVariant);

break;

case ASDU_100:

buildCallAllASDU100(sVariant);

break;

case ASDU_103:

buildSyncTimeASDU103(sVariant);

break;

case ASDU_101:

buildDDASDU101(sVariant);

break;

case UFRAME_TYPE_TESTFR_V:

buildU(sVariant,UFRAME_TYPE_TESTFR_V);

sVariant.pPtlFlag->byTest_V = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case UFRAME_TYPE_TESTFR_C:

buildU(sVariant,UFRAME_TYPE_TESTFR_C);

sVariant.pPtlFlag->byTest_C = 0;

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

break;

case ASDU_45:

buildYKDataASDU45(sVariant);

break;

case ASDU_46:

buildYKDataASDU46(sVariant);

break;

default:

break;

}

}

sVariant.pPtlFlag->byDowmFrame = FRAME_NULL;

putSendData(sVariant); //把发送数据放入RTU发送缓冲区中

}

以组织总召唤命令为例:

void buildCallAllASDU100(S_PtlVariant &sVariant)

{

if(sVariant.uwMaxSendLen - sVariant.uwSendDataLen < 16)

{

return;

}

S_IframeHead *pIframeHead = (S_IframeHead*)(sVariant.pBySendBuffer + sVariant.uwSendDataLen);

S_ASDUHead *pASDUHead = (S_ASDUHead *)(sVariant.pBySendBuffer + sVariant.uwSendDataLen+6);

pIframeHead->byStart = 0x68;

pIframeHead->byLen = 0x0E;

pIframeHead->uwDoublens = sVariant.pPtlFlag->uwSendSN;

pIframeHead->uwDoublens = qBigLittleEndianConvert(pIframeHead->uwDoublens);

pIframeHead->uwDoublenr = sVariant.pPtlFlag->uwRecvSN;

pIframeHead->uwDoublenr = qBigLittleEndianConvert(pIframeHead->uwDoublenr);

pASDUHead->byType = ASDU_100;

pASDUHead->bSq = 0;

pASDUHead->bNum = 1;

pASDUHead->uwReason = 6;

pASDUHead->uwReason = qBigLittleEndianConvert(pASDUHead->uwReason);

pASDUHead->uwCa = sVariant.pUnit->getTUAddress();

pASDUHead->uwCa = qBigLittleEndianConvert(pASDUHead->uwCa);

memset(sVariant.pBySendBuffer + sVariant.uwSendDataLen+12,0,3);

sVariant.pBySendBuffer[sVariant.uwSendDataLen + 15] = 20;

sVariant.uwSendDataLen += 16;

sVariant.pPtlFlag->uwSendSN += 2;

sVariant.pPtlFlag->bSendCallAll = 0;

S_DateTime time = qGetCurrentTime();

sVariant.pPtlFlag->tLastSendCallAllTime = time;

sVariant.pPtlFlag->tLastSendTime = time;

//sVariant.pPtlFlag->tLastSendCallDDTime = time;

sVariant.pPtlFlag->bSendDDCall = 1;

}

组织好报文后,基于通道通讯类型,分别调用串口或网络的发送数据接口

switch(m_pChannel->byChannelCOMType)

{

case CHANNEL_TYPE_TCP_CLIENT:

case CHANNEL_TYPE_TCP_SERVER:

case CHANNEL_TYPE_UDP_CLIENT:

case CHANNEL_TYPE_UDP_SERVER:

m_SocketMutex.lock();

m_SPTLRecvSenddata.iSendedLen = m_pNetComm->sendData(m_pSocketHandle, m_SPTLRecvSenddata.pSendBuf, m_SPTLRecvSenddata.uiSendLen);

m_SocketMutex.unlock();

if(m_SPTLRecvSenddata.iSendedLen > 0)

{

if(m_qsPtlLibName.contains("_zf"))

{

//ljx debug

qMSleep(50);

}

else

{

// qMSleep(30);

}

}

break;

case CHANNEL_TYPE_COM_485:

case CHANNEL_TYPE_COM_232:

{

m_SPTLRecvSenddata.iSendedLen = m_pSerial->sendData(m_SPTLRecvSenddata.pSendBuf,static_cast<int>(m_SPTLRecvSenddata.uiSendLen));

}

break;

case CHANNEL_TYPE_CAN:

break;

default:

break;

}

通道通信接收数据处理

基于通道类型,采用不同的处理方法将接收到的数据存放到接收缓存区中

switch(m_pChannel->byChannelCOMType) //通道类型

{

case CHANNEL_TYPE_TCP_CLIENT:

case CHANNEL_TYPE_TCP_SERVER:

case CHANNEL_TYPE_UDP_CLIENT:

case CHANNEL_TYPE_UDP_SERVER:

m_NewRecvedDataMutex.lock();

while(m_sNewRecvedDataList.size() > 0)

{

S_NewRecvedData& sNewRecvedData = m_sNewRecvedDataList.first();

quint32 uiFreeRecvBufLen = MAX_BUF_SIZE - m_SPTLRecvSenddata.uiRecvedLen; //剩余的接收缓冲区长度

if(uiFreeRecvBufLen >= sNewRecvedData.uiRecvedLen) //若剩余的缓冲区长度大于新接收的缓冲区长度,则新接收到的数据全部复制

{

memcpy(m_SPTLRecvSenddata.pRecvBuf + m_SPTLRecvSenddata.uiRecvedLen, sNewRecvedData.pRecvBuf, sNewRecvedData.uiRecvedLen);

m_SPTLRecvSenddata.uiRecvedLen += sNewRecvedData.uiRecvedLen;

m_sNewRecvedDataList.removeFirst();

}

else if(uiFreeRecvBufLen > 0) //若剩余的接收缓冲区长度大于0,则只复制剩余的接收缓冲区长度

{

memcpy(m_SPTLRecvSenddata.pRecvBuf + m_SPTLRecvSenddata.uiRecvedLen, sNewRecvedData.pRecvBuf, uiFreeRecvBufLen);

m_SPTLRecvSenddata.uiRecvedLen += uiFreeRecvBufLen;

sNewRecvedData.uiRecvedLen -= uiFreeRecvBufLen;

memmove(sNewRecvedData.pRecvBuf, sNewRecvedData.pRecvBuf + uiFreeRecvBufLen, sNewRecvedData.uiRecvedLen);

break;

}

else

{

break;

}

}

m_NewRecvedDataMutex.unlock();

break;

case CHANNEL_TYPE_COM_485:

case CHANNEL_TYPE_COM_232:

m_uiMaxRecvLen = MAX_BUF_SIZE - m_SPTLRecvSenddata.uiRecvedLen;

if(m_uiMaxRecvLen > 0)

{

m_SPTLRecvSenddata.iRecvedLen = m_pSerial->recvData(m_SPTLRecvSenddata.pRecvBuf + m_SPTLRecvSenddata.uiRecvedLen,

static_cast<int>(m_uiMaxRecvLen)); //从串口中接收数据

if(m_SPTLRecvSenddata.iRecvedLen > 0)

{

m_tRecvedTime = time(NULL);

addCommMsg(true, static_cast<quint16>(m_SPTLRecvSenddata.iRecvedLen), m_SPTLRecvSenddata.pRecvBuf + m_SPTLRecvSenddata.uiRecvedLen); //添加通信报文

setUpCommFault(false);

setDownCommFault(false);

m_SPTLRecvSenddata.uiRecvedLen += static_cast<quint32>(m_SPTLRecvSenddata.iRecvedLen);

}

else if(m_SPTLRecvSenddata.iRecvedLen < 0)

{

setCommState(COMM_ERROR);

}

}

break;

case CHANNEL_TYPE_CAN:

break;

default:

break;

}

调用规约报文解析函数来解析报文

m_pFuncParseData(m_SPTLRecvSenddata, dynamic_cast<CUnitBase*>(m_pCurrentUnit)

PTL_API bool parseData(S_RecvSendData &sRecvSendData, CUnitBase* pUnit) //解析报文

{

S_PtlVariant sVariant;

memset(&sVariant, 0, sizeof(S_PtlVariant));

if(pUnit != NULL)

{

sVariant.pUnit = pUnit;

sVariant.pByReadBuffer = sRecvSendData.pRecvBuf;

sVariant.uwRecvDataLen = sRecvSendData.uiRecvedLen;

sVariant.bMainChannel = sRecvSendData.bMainChannel;

sVariant.byChannelMode = sRecvSendData.byChannelMode;

sVariant.byChannelIndex = sRecvSendData.byChannelIndex;

memset(sVariant.pParsedCommMsg, 0, MAX_BUF_SIZE);

sVariant.uwParsedCommMsgLen = 0;

sVariant.pPtlFlag = (S_PtlFlag*)(pUnit->getTUFlagBuffer());

if(readFrame(sVariant)) //解析数据

{

sRecvSendData.uiRecvedLen = sVariant.uwRecvDataLen;

return true;

}

}

sRecvSendData.uiRecvedLen = sVariant.uwRecvDataLen;

return false;

}

解析报文

首先,判断当前主站接收到的报文帧是U帧S帧还是I帧,并设置对应的帧状态与帧类型。若帧的第一个字符不是启动字符,则偏移缓冲区中 的数据

接着,根据帧类型调用通道报文添加函数,将报文转发给上位机。此时帧状态为接收帧已同步

最后,根据帧类型调用不同的报文解析函数

U帧S帧:调用parseUOrSFrame()接口

I帧:调用parseIFrame()接口

bool readFrame(S_PtlVariant &sVariant) //解析数据

{

bool bResult = false;

while(sVariant.uwRecvDataLen >= sizeof (S_Uframe))

{

S_IframeHead sIFrame;

memcpy(&sIFrame, sVariant.pByReadBuffer, sizeof (S_IframeHead));

if(FRAME_STATE_NO == sVariant.byCurrentRecvedFrameState) //当前接收帧的状态为无有效帧

{

quint16 uwAddr;

S_ASDUHead sASDUHead;

uwAddr = sVariant.pUnit->getTUAddress();

int i;

for(i = 0;i <= sVariant.uwRecvDataLen - 6; i++)

{

//找到启动符68 和第二个字长度为04,确定为短帧:U帧或S帧

if(0x68 == sVariant.pByReadBuffer[i] && 0x04 == sVariant.pByReadBuffer[i+1]

&& 0x00 == sVariant.pByReadBuffer[i+3] && 0x00 == sVariant.pByReadBuffer[i+5]) //短帧

{

if(sVariant.uwRecvDataLen >= i+6)

{

//为u或s帧

sVariant.byCurrentRecvedFrameState = FRAME_STATE_SYNING;

sVariant.byCurrentRecvedFrameType = FRAME_TYPE_UORS;

break;

}

else

{

if(i > 0)

{

offsetRecvBuffer(sVariant,static_cast<quint16>(i));

}

return bResult;

}

}

else //长帧:I帧

{

if(0x68 == sVariant.pByReadBuffer[i])

{

if(sVariant.uwRecvDataLen > i + 10 && uwAddr == sVariant.pByReadBuffer[i + 10])

{

sVariant.byCurrentRecvedFrameState = FRAME_STATE_SYNING;

sVariant.byCurrentRecvedFrameType = FRAME_TYPE_I;

break;

}

else

{

if(i > 0)

{

offsetRecvBuffer(sVariant,static_cast<quint16>(i));

}

return bResult;

}

}

}

}

if(i > 0)

{

offsetRecvBuffer(sVariant,static_cast<quint16>(i));

}

}

if(FRAME_STATE_SYNING == sVariant.byCurrentRecvedFrameState)

{

if(FRAME_TYPE_UORS == sVariant.byCurrentRecvedFrameType)

{

if(sVariant.uwRecvDataLen >= 6)

{

sVariant.byCurrentRecvedFrameState = FRAME_STATE_SYN;

sVariant.uwCurrentRecvedFrameLen = 6;

sVariant.pUnit->addCommMsg(true, sVariant.uwCurrentRecvedFrameLen, sVariant.pByReadBuffer, sVariant.byChannelMode, sVariant.byChannelIndex, sVariant.bMainChannel);

// qDebug()<< "装置通信短帧" <<sVariant.uwCurrentRecvedFrameLen << (quint8*)pCommMsg->chCommMsg;

}

}

if(FRAME_TYPE_I == sVariant.byCurrentRecvedFrameType)

{

if(sVariant.uwRecvDataLen >= sVariant.pByReadBuffer[1])

{

sVariant.byCurrentRecvedFrameState = FRAME_STATE_SYN;

sVariant.uwCurrentRecvedFrameLen = sVariant.pByReadBuffer[1] + 2;

sVariant.pUnit->addCommMsg(true, sVariant.uwCurrentRecvedFrameLen, sVariant.pByReadBuffer, sVariant.byChannelMode, sVariant.byChannelIndex, sVariant.bMainChannel); //添加通信报文到队列(包括转发给上位机的队列,以及要保存到本地的队列)

//qDebug()<< "装置通信长帧" <<sVariant.uwCurrentRecvedFrameLen << (quint8*)pCommMsg->chCommMsg;

}

}

}

if(FRAME_STATE_SYN != sVariant.byCurrentRecvedFrameState)

{

return bResult;

}

S_DateTime time = qGetCurrentTime();

sVariant.pPtlFlag->tLastRecvTime = time;

switch (sVariant.byCurrentRecvedFrameType)

{

case FRAME_TYPE_UORS:

parseUOrSFrame(sVariant); //解析U帧或S帧

break;

case FRAME_TYPE_I:

parseIFrame(sVariant); // 解析I帧数据

sVariant.pPtlFlag->uwRecvSN = sIFrame.uwDoublens;

sVariant.pPtlFlag->uwRecvSN += 2;

sVariant.pPtlFlag->uwRecvSN = qBigLittleEndianConvert(sVariant.pPtlFlag->uwRecvSN);

break;

default:

break;

}

sVariant.pPtlFlag->bWaitDataReturn = 0;

sVariant.byCurrentRecvedFrameState = FRAME_STATE_NO;

offsetRecvBuffer(sVariant,sVariant.uwCurrentRecvedFrameLen);

sVariant.uwCurrentRecvedFrameLen = 0;

bResult = true;

}

return bResult

解析U帧、S帧、I帧的函数接口分别为:

void parseUFrame(S_PtlVariant &sVariant)

{

S_Uframe* pSUFrame = (S_Uframe *)sVariant.pByReadBuffer;

//主站发送→激活传输启动

if(pSUFrame ->byCtrlOctets1 == 0x07)

{

sVariant.pPtlFlag->uwRecvSN = 0; //接收计数

sVariant.pPtlFlag->uwSendSN = 0; //发送计数

sVariant.pPtlFlag->bLinkOK = true; //链路状态

}

else if(pSUFrame ->byCtrlOctets1 == 0x43)

{

sVariant.pPtlFlag->byTest_C = 1;

}

//从站发送→确认激活传输启动

else if(pSUFrame ->byCtrlOctets1 == 0x0B)

{

sVariant.pPtlFlag->uwRecvSN = 0; //接收计数

sVariant.pPtlFlag->uwSendSN = 0; //发送计数

sVariant.pPtlFlag->bLinkOK = 1; //链路状态

sVariant.pPtlFlag->bSendCallAll = 1;//总召状态

}

//主站发送→停止链路

else if(pSUFrame ->byCtrlOctets1 == 0x13)

{

sVariant.pPtlFlag->bLinkOK = 0; //链路状态

}

//从站发送→确认停止链路

else if(pSUFrame ->byCtrlOctets1 == 0x23)

{

sVariant.pPtlFlag->bLinkOK = 0; //链路状态

}

//重新同步

sVariant.byCurrentRecvedFrameState = FRAME_STATE_NO;

}

void parseSFrame(S_PtlVariant &sVariant)

{

quint16 uwRecv = 0;

memcpy(&uwRecv,&sVariant.pByReadBuffer[4],sizeof(quint16));

sVariant.pPtlFlag->uwConfirmRecvSn = uwRecv;

sVariant.pPtlFlag->byRecvSFrameSendFlag = 1;

S_DateTime time = qGetCurrentTime();

sVariant.pPtlFlag->tLastRecvSFrame = time;

//重新同步

sVariant.byCurrentRecvedFrameState = FRAME_STATE_NO;

}

void parseIFrame(S_PtlVariant &sVariant)

{

S_ASDUHead sASDUHead;

memcpy(&sASDUHead,sVariant.pByReadBuffer + sizeof (S_IframeHead),sizeof(S_ASDUHead));

//判断公共地址

sASDUHead.uwCa = qBigLittleEndianConvert(sASDUHead.uwCa);

if(sASDUHead.uwCa != sVariant.pUnit->getTUAddress())

{

return;

}

switch (sASDUHead.byType)

{

case ASDU_01: //单点信息(遥信)

parseYXDataASDU1(sVariant);

break;

case ASDU_03: //双点信息(遥信)

parseYXDataASDU3(sVariant);

break;

case ASDU_09: //解析遥测归一化值

parseYCDataASDU9(sVariant);

break;

case ASDU_21: //解析遥测归一化值 测试 debug 白

parseYCDataASDU21(sVariant);

break;

case ASDU_11: //解析遥测标度化值

parseYCDataASDU11(sVariant);

break;

case ASDU_13: //解析遥测短浮点数

parseYCDataASDU13(sVariant);

break;

case ASDU_30: //解析带时标单点遥信

parseYXDataASDU30(sVariant);

break;

case ASDU_31: //解析带时标单点遥信

parseYXDataASDU31(sVariant);

break;

case ASDU_45: //解析遥控单命令

if(sVariant.pByReadBuffer[1] != 0x0E)

{

return;

}

parseYKDataASDU45(sVariant);

break;

case ASDU_46: //解析遥控单命令

if(sVariant.pByReadBuffer[1] != 0x0E)

{

return;

}

parseYKDataASDU46(sVariant);

break;

case ASDU_48:

if(sVariant.pByReadBuffer[1] != 16)

{

return;

}

parseYTCmdResultASDU48(sVariant);

break;

case ASDU_49:

if(sVariant.pByReadBuffer[1] != 16)

{

return;

}

parseYTCmdResultASDU49(sVariant);

break;

case ASDU_50:

if(sVariant.pByReadBuffer[1] != 18)

{

return;

}

parseYTCmdResultASDU50(sVariant);

break;

case ASDU_100: //解析总召唤数据

if(sVariant.pByReadBuffer[1] != 0x0E)

{

return;

}

parseZHDataASDU100(sVariant);

break;

case ASDU_15: //解析遥脉值

parseYMDataASDU206(sVariant);

break;

case ASDU_206: //解析遥脉值

parseYMDataASDU206(sVariant);

break;

case ASDU_207: //解析遥脉值

parseYMDataASDU207(sVariant);

break;

case ASDU_103: //解析时钟同步

parseTimeSynASDU103(sVariant);

default:

break;

}

sVariant.pPtlFlag->bSendSFrame = 1;

//debug白11/26

}

在解析I帧中,以解析遥测归一化值为例

void parseYCDataASDU9(S_PtlVariant &sVariant)

{

S_ASDUHead sASDUHead;

S_Addr sAddr;

S_ASDU9YC sYCValue;

quint16 uwIndex;

bool bIsCalc = false;

memcpy(&sASDUHead,sVariant.pByReadBuffer + sizeof (S_IframeHead),sizeof(S_ASDUHead));

sASDUHead.uwReason = qBigLittleEndianConvert(sASDUHead.uwReason);

//类型不为9,原因不为1/2/3/5/20

// if( sASDUHead.uwReason != 1 &&sASDUHead.uwReason != 2 &&sASDUHead.uwReason != 3

// && sASDUHead.uwReason != 5 &&sASDUHead.uwReason != 20)

// {

// return;

// }

if(sASDUHead.bSq)//连续

{

memcpy(&sAddr,&sVariant.pByReadBuffer[12],sizeof (sAddr));

sAddr.uwAddrOrder = qBigLittleEndianConvert(sAddr.uwAddrOrder);

//地址合法性

if((sAddr.uwAddrOrder > sVariant.pPtlFlag->pSPtl104->uwYCEndAddr)||

(sAddr.uwAddrOrder < sVariant.pPtlFlag->pSPtl104->uwYCStartAddr))

{

return;

}

uwIndex = sAddr.uwAddrOrder - sVariant.pPtlFlag->pSPtl104->uwYCStartAddr;

for(quint8 i = 0;i < sASDUHead.bNum; i++)

{

memcpy(&sYCValue,&sVariant.pByReadBuffer[15 + i*2],sizeof (S_ASDU9YC));

sYCValue.wValue = qBigLittleEndianConvert(sYCValue.wValue);

double dValue = (double)sYCValue.wValue;

sVariant.pUnit->getValue(S_DataIndex(TABLE_PREYC, uwIndex + i, COL_PREYC_ISCALC), &bIsCalc);

if(!bIsCalc)

{

sVariant.pUnit->setValue(S_DataIndex(TABLE_PREYC,uwIndex + i,COL_PREYC_YCVALUE) ,&dValue);

}

}

}

else //不连续

{

for(quint8 i = 0;i < sASDUHead.bNum; i++)

{

memcpy(&sAddr,&sVariant.pByReadBuffer[12 + i*6],sizeof (sAddr));

sAddr.uwAddrOrder = qBigLittleEndianConvert(sAddr.uwAddrOrder);

//合法性检查

if((sAddr.uwAddrOrder > sVariant.pPtlFlag->pSPtl104->uwYCEndAddr)||

sAddr.uwAddrOrder < sVariant.pPtlFlag->pSPtl104->uwYCStartAddr)

{

return;

}

uwIndex = sAddr.uwAddrOrder - sVariant.pPtlFlag->pSPtl104->uwYCStartAddr;

memcpy(&sYCValue,&sVariant.pByReadBuffer[15 + i*6],sizeof (sYCValue));

sYCValue.wValue = qBigLittleEndianConvert(sYCValue.wValue);

double dValue = (double)sYCValue.wValue;

sVariant.pUnit->getValue(S_DataIndex(TABLE_PREYC, uwIndex, COL_PREYC_ISCALC), &bIsCalc);

if(!bIsCalc)

{

sVariant.pUnit->setValue(S_DataIndex(TABLE_PREYC, uwIndex, COL_PREYC_YCVALUE) ,&dValue);

}

}

}

}

解析报文后,依据连续数据点位与非连续数据点位来讨论分别设置数据库表中对应点位的值。


参考文献:

IEC60870协议是什么?IEC60870协议介绍

从零开始理解IEC104协议之一——104规约帧格式

IEC60870开源库

IEC 60870-5-104 详细解读

IEC104规约(一)协议结构阐述

IEC104/101 主站/客户端 模拟器



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。