通信协议:西门子
2026-03-08 21:13:34一、西门子S7协议
1. 西门子PLC设备
PLC:系列 LOGO、200、200Smart、300、400、1200、1500
2. 西门子PLC存储区
存储区分类
- I:输入、Q:输出、AI:模拟量输入、V/DB:变量存储区 Modbus 线圈状态Q 输入线圈I 输入寄存器AI 保持型寄存V
- AQ:模拟量输出、M:位存储区、T:定时器存储区、C:计数器存储区、HC:高速计数器、AC:累加器、SM:特殊存储器、L:局部存储区、S:顺序控制继电器 S7
访问规则
类型:bit、Byte、Word、Dword 对应数组
访问地址:最小存储是字节
I0.0 Q1.3 MB10 MW10 M10.0 VW100 V100.1 DB1.DBX100.5 位地址最大:7
3. 通信协议
Modbus协议/OPC
功能有限 NModbus4
S7
- 私有协议,非公开
- 功能强大,大部分功能都能完成
- 通信模式:主从(客/服,单边通信)、伙伴(双边通信,PLC->PLC)
请求流程,可以使用 Wireshark 监控通信过程,S7.NET库测试使用/sharp7
建立TCP连接 Socket.Connect
发送访问请求 COTP
交换通信信息 Setup Communication
执行相关操作 ….

4. S7COMM-COTP报文
COTP报文
- 第一次交互
- 请求与响应

byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x16,
// COTP
0x11,
0xe0, // 表示连接请求
0x00,0x00,
0x00,0x01,
0x00,
// 0xc0
0xc0,
0x01,
0x0a,// 2的10次方 1024
// 0xc1 通信源对应的相关配置 PC
0xc1,
0x02,
0x10, // S7双边模式 也可以选择01- PG 02 OP
0x00, // 不需要设置机架和插槽
// 0xc2
0xc2,
0x02,
0x03, // S7单边模式
0x01 // 如果机架插槽为0,0 0x00 机架*32+插槽 (机架<<5)+插槽
};
socket.Send(bytes);
// 接收COTP CC结果
socket.Receive(bytes, 0, bytes.Length, SocketFlags.None);
关于TSAP:
- 2Byte,绝大部分都是2Byte
- 28Byte,如果遇到28Byte,下面内容可以参考,主要针对目的设备
- byte[4] 表示是S7子网ID第1部分的高字节
- byte[5]表示这个ID的低字节
- byte[8]表示S7子网ID第2部分高字节
- byte[9]第10个字节表示这个ID的低字节(一般子网ID是这个格式:0000-0000)
- byte[10]表示Profibus DP地址
- byte[26]表示连接资源的价值
- byte[17]表示机架号和插槽号的计算结果
关于插槽和机架:
| plc | rack | slot |
|---|---|---|
| s7-200smart | 0 | 1 |
| s7-300 | 0 | 2 |
| s7-400/WIN AC | 见硬件组态 | 见硬件组态 |
| s7-1200/1500 | 0 | 0/1 |
5. S7COMM-SetupCommunication报文
Setup Communication报文
- 第二次交互
- 请求与响应
PDU

bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x19,
// COTP
0x02, // 在COTP这个环节,当前字节以后的字节数
0xf0, // 表示数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x00, // 累加序号
// Parameter 长度
0x00,0x08,
// Data 长度
0x00,0x00,
// S7 - Parameter
0xf0, // Setup Communication Funcion
0x00,
0x00,0x01, // 任务处理队列长度
0x00,0x01, // 任务处理队列长度
0x03,0xc0 // PDU 长度 960 PLC 反馈 240 480 960
};
socket.Send(bytes);
byte[] resp = new byte[27];
socket.Receive(resp);
ushort pdu = BitConverter.ToUInt16(new byte[] { resp[26], resp[25] });
6. S7COMM-Read报文
数据请求与响应
- Parameter:地址/类型描述
- Data:类型描述
地址请求
- I0.5 Q0.4
- MB10 M5.5
- VW100 DB1.DBD0
- DB1.DBX0.3

static void S7Read()
{
// 1、读取单个连续地址
// 读取 DB1.DBW100 - 10 地址的数据请求报文 连续读取同区域数据
byte[] bytes = new byte[] {
// TPKT - 4bytes
0x03,
0x00,
0x00,0x1f, // 十进制:31
// COTP
0x02,
0xf0,//数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度
0x00,0x0e,
// Data 长度
0x00,0x00,
// S7 - Parameter
0x04, // 向PLC发送一个读变量的请求
0x01, // Item的数量 Item中包含了请求的地址以及类型相关信息
// S7 - Parameter - Item
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x02,// 传输数据类型 02:BYTE 04:Word
0x00,0x14, // 读取的数量
// DB100 0x00,0x64
// V-》DB1
0x00,0x01, // 对应DB块的编号,如果区域不是DB,这里写0
// 存储区
0x84,// DataBlock -> V 0x81 I区,
// 变量地址 100 Byte -2 100 101 占三个字节
0x00,0x03,0x20
// 0000 0000 0000 0011 0010 0000 // 表达字节+位信息
// DB1.DBX100.5 0x64 0110 0100 0-7 101
// 0x00 0x03 0x20
};
// 2、读取多个不同区域地址
// 读取 DB1.DBW100 - 10 Byte
// 读取 DB1.DBW150 - 5 Word
// 读取 Q0.5-3 位 Bit 01
// 读取不同区域的数据可不可以?
// 读取不同类型的数据可不可以?
// 支持做不同区域的读取,但是这里读取位数据的时候 只能是一个
bytes = new byte[] {
// TPKT - 4bytes
0x03,
0x00,
0x00,0x37, // 十进制:31+12 43 37
// COTP
0x02,
0xf0,//数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度
0x00,0x26,
// Data 长度
0x00,0x00,
// S7 - Parameter
0x04, // 向PLC发送一个读变量的请求
0x03, // Item的数量 Item中包含了请求的地址以及类型相关信息
// S7 - Parameter - Item - 1
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x02,// 传输数据类型 02:BYTE 04:Word
0x00,0x14, // 读取的数量
// DB100 0x00,0x64
// V-》DB1
0x00,0x01, // 对应DB块的编号,如果区域不是DB,这里写0
// 存储区
0x84,// DataBlock -> V 0x81 I区,
// 变量地址 100 Byte -2 100 101 占三个字节
0x00,0x03,0x20,
// S7 - Parameter - Item -3
// 如果请求的位数据在中间Item而不在最的话,那么这个数据返回的时候会多一个Fill Bytes字节
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x01,// 传输数据类型 01:BIT 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区
0x82,
// 变量地址 0.5 Bit
// 0000 0000 0000 0000 0000 0101
0x00,0x00,0x06,
// S7 - Parameter - Item -2
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x04,// 传输数据类型 02:BYTE 04:Word
0x00,0x05, // 读取的数量
0x00,0x01,
// 存储区
0x84,
// 变量地址 150 Byte
0x00,0x04,0xB0
};
// 3、单独请求位地址
// 单独请求Q0.5地址数据
// 注意:就算是单个位区域数据请求,也得是一个状态一个状态的获取,但是可以按照字节获取
bytes = new byte[] {
// TPKT - 4bytes
0x03,
0x00,
0x00,0x37, // 十进制:31+12 43 37 55-24=31 1F
// COTP
0x02,
0xf0,//数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度
0x00,0x26,
// Data 长度
0x00,0x00,
// S7 - Parameter
0x04, // 向PLC发送一个读变量的请求
0x03, // Item的数量 Item中包含了请求的地址以及类型相关信息
// S7 - Parameter - Item
// 如果请求的位数据在中间Item而不在最的话,那么这个数据返回的时候会多一个Fill Bytes字节
// Q0.5
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x01,// 传输数据类型 01:BIT 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区
0x82,
// 变量地址 0.5 Bit
// 0000 0000 0000 0000 0000 0101
0x00,0x00,0x05,
// Q0.6
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x01,// 传输数据类型 01:BIT 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区
0x82,
// 变量地址 0.5 Bit
// 0000 0000 0000 0000 0000 0101
0x00,0x00,0x06,
//Q0.7
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x01,// 传输数据类型 01:BIT 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区
0x82,
// 变量地址 0.5 Bit
// 0000 0000 0000 0000 0000 0101
0x00,0x00,0x07
};
// 4、请求字符串数据
// "Hello" -> 0x48 0x65 0x6C 0x6C 0x6F
bytes = new byte[] {
// TPKT - 4bytes
0x03,
0x00,
0x00,0x1F,
// COTP
0x02,
0xf0,//数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度
0x00,0x0E,
// Data 长度
0x00,0x00,
// S7 - Parameter
0x04, // 向PLC发送一个读变量的请求
0x01, // Item的数量 Item中包含了请求的地址以及类型相关信息
// S7 - Parameter - Item
// 如果请求的位数据在中间Item而不在最的话,那么这个数据返回的时候会多一个Fill Bytes字节
0x12,
0x0a,// 当前Item部分,此字节往后还有10个字节
0x10,
0x02,// 传输数据类型 01:BIT 02:BYTE 04:Word
0x00,0x0C, // 读取的数量
0x00,0x01,
// 存储区
0x84,
// 变量地址 0.5 Bit
// 0000 0000 0000 0000 0101 0000
0x00,0x00,0x00
};
socket.Send(bytes);// 读取地址变量数据的请求
}
7. S7COMM-Write报文
数据请求与响应
- Parameter:地址/类型描述
- Data:类型描述

static void S7Write()
{
// 1、修改任务类型 04读 05写
// 2、相关长度Item个数 DBW20
// Data长度
// 整个报文长度
byte[] bytes = new byte[] {
// TPKT - 4bytes
0x03,
0x00,
0x00,0x4A, // 十进制:38+17=56 56+17=73 49**************************
// COTP
0x02,
0xf0,
0x80,
// S7 - Header
0x32,
0x01,
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度***************
0x00,0x26, //26+12=38 -32 26
// Data 长度********************
0x00,0x13, // 5
// S7 - Parameter
0x05, // 向PLC发送一个写变量的请求 04:读 05:写
0x03, // Item的数量 **********************************
// S7 - Parameter - Item-1 DBW20 - 22
0x12,
0x0a,
0x10,
0x04,// 传输数据类型 02:BYTE 04:Word
0x00,0x02, // 读取的数量
0x00,0x01,
// 存储区
0x84,
0x00,0x00,0xA0, // 20.0
// 0000 0000 0000 0000 1010 0000
// S7 - Parameter - Item-2 Q0.5
0x12,
0x0a,
0x10,
0x01,// 传输数据类型 01:Bit 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区 Q0.5
0x82,
0x00,0x00,0x05,
// S7 - Parameter - Item-3 Q0.6
0x12,
0x0a,
0x10,
0x01,// 传输数据类型 01:Bit 02:BYTE 04:Word
0x00,0x01, // 读取的数量
0x00,0x00,
// 存储区 Q0.6
0x82,
0x00,0x00,0x06,
// S7 - Data - Item-1
0x00,
0x04,
0x00, 0x20, // 长度按照位长度来计算 16
0x02,0x9C,0x02,0x9D,
// S7 - Data - Item-2 0.5
0x00,
0x03, // 数据类型
0x00, 0x01, // 长度 字节数*8
0x01,
0x00, // Fill bytes
// S7 - Data - Item-3 0.6
0x00,
0x03, // 数据类型
0x00, 0x01, // 长度 字节数*8
0x01
// 这里是否可以写多个状态? 尽量一次发送多个请求
// ***这里不能做多个位的写入,会报0x03的异常***
};
socket.Send(bytes);
}
8. S7COMM-Run报文

byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x25, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x01,
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x14, // 10#20
// Data Length
0x00,0x00,
// S7-Parameter
0x28,// 控制PLC启动
0x00,0x00,0x00,0x00,0x00,0x00,0xfd,
0x00,0x00,
0x09,
// P_PROGRAM
0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
};
socket.Send(bytes);
9. S7COMM-Stop报文

byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x21, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x01,
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x10, // 10#20
// Data Length
0x00,0x00,
// S7-Parameter
0x29,// 控制PLC停止
0x00,0x00,0x00,0x00,0x00,
//0x00,0xfd, 相对启动来讲,多了这4个字节
//0x00,0x00,
0x09,
// P_PROGRAM
0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
};
socket.Send(bytes);
10. S7COMM-时间读写报文

byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x1d, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x07,//UserData
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x8, // 10#20
// Data Length
0x00,0x04,
// S7-Parameter
0x00,0x01,0x12,
0x04,// Parameter部分后续还有4个字节的内容
0x11,
// 0100 0111
0x47,
0x01,// Read Clock
0x00,
// S7-Data
0x0a,
0x00,
0x00,0x00
};
socket.Send(bytes);

// 2025-10-01 20:05:00 4
byte[] bytes = new byte[] {
// TPKT
0x03,0x00,
0x00,0x27, // 39
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x07,// UserData
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x08,
// Data Length
0x00,0x0E, // 14
// S7-Paramter
0x00,0x01,0x12,
0x04,
0x11,
0x47,
0x02,// Set Clock
0x00,
// S7-Data
0xff,
0x09,
0x00,0x0a,// 后续有10个长度的字节
0x00,
0x19,
// Year:25
0x25,
// Month:10
0x10,
// Day:01
0x01,
// Hour:20
0x20,
// Minute:05
0x05,
// Second:00
0x00,
// 毫秒:0 和星期
0x00,0x04
// 0000 0000 0000 0 100
};
socket.Send(bytes);
11. S7COMM-SZL报文
系统状态列表(德语:System-ZustandsListen,英语:System-Status-Lists)
系统数据、CPU中的模块状态数据、模拟的诊断数据、诊断缓冲区
- 消息服务
- 诊断信息
- 告警信息

12. S7COMM-其他
S7COMM-Userdata-获取系统块
列举一下块的类型
读取块的信息
S7COMM-上传下载
文件处理
StartUpload、Upload、EndUpload
StartDownload、Download、EndDownload
程序块激活
控制支持情况

附录
- 附录一,COTP->PDU type已知枚举值
| 0xe0 | 连接请求 |
|---|---|
| 0xd0 | 连接确认 |
| 0x08 | 断开请求 |
| 0x0c | 断开确认 |
| 0x05 | 拒绝访问 |
| 0x01 | 加急数据 |
| 0x02 | 加急数据确认 |
| 0x04 | 用户数据 |
| 0x07 | TPDU错误 |
| 0xf0 | 数据传输 |
- 附录二,S7Header->ROSCTR已知枚举值
| 0x01 | Job request。主站发送请求 |
|---|---|
| 0x02 | Ack。从站响应请求不带数据 没有专门的Data部分 |
| 0x03 | Ack_Data。从站响应请求并带有数据 带专门的Data部分报文 |
| 0x07 | Userdata。原始协议的扩展。读取编程/调试、SZL读取、安全功能、时间设置等 |
- 附录三、S7Header->Error class已知枚举值
| 0x00 | 无错误 |
|---|---|
| 0x81 | 应用程序关系错误 |
| 0x82 | 对象定义错误 |
| 0x83 | 无资源可用错误 |
| 0x84 | 服务处理错误 |
| 0x85 | 请求错误(如果有错,此码较多) |
| 0x87 | 访问错误 |
- 附录四,S7Parameter->Error code已知枚举值
| 0x0000 | 无错误 | 0x8500 | L7PDU大小错误 |
|---|---|---|---|
| 0x0110 | 无效块类型编号 | 0xD401 | L7无效SZL ID |
| 0x0112 | 无效参数 | 0xD402 | L7无效索引 |
| 0x011A | PG资源错误 | 0xD403 | L7 DGS连接已宣布 |
| 0x011B | PLC重新外包错误 | 0xD404 | L7 最大用户NB |
| 0x011C | 协议错误 | 0xD405 | L7 DGS功能参数语法错误 |
| 0x011F | 用户缓冲区太短 | 0xD406 | L7无信息 |
| 0x0141 | 请求错误 | 0xD601 | L7 PRT 函数参数语法错误 |
| 0x01C0 | 版本不匹配 | 0xD801 | L7 无效变量地址 |
| 0x01F0 | 末实施 | 0xD802 | L7 未知请求 |
| 0x8001 | L7无效CPU状态 | 0xD803 | L7 无效请求状态 |
- 附录五,S7Parameter->Function已知枚举值
| 0x00 | CPU服务 |
|---|---|
| 0xF0 | 设置通信 |
| 0x04 | 读取变量 |
| 0x05 | 写变量 |
| 0x1A | 请求下载 |
| 0x1B | 下载块 |
| 0x1C | 下载结束 |
| 0x1D | 开始上传 |
| 0x1E | 上传 |
| 0x1F | 结束上传 |
| 0x28 | PLC 控制 启动 |
| 0x29 | PLC 停止 |
- 附录六,S7Parameter->Item->Syntax Id已知枚举值
| 0x10 | S7ANY:Address data S7-Any pointer-like DB1.DBX10.2 |
|---|---|
| 0x13 | PBC-R_ID:R_ID for PBC |
| 0x15 | ALARM_LOCKFREE:Alarm lock/free dataset |
| 0x16 | ALARM_IND:Alarm indication dataset |
| 0x19 | ALARM_ACK:Alarm acknowledge message dataset |
| 0x1a | ALARM_QUERYREQ:Alarm query request dataset |
| 0x1c | NOTIFY_IND:Notify indication dataset |
| 0xa2 | DRIVEESANY:seen on Drive ES Starter with routing over S7 |
| 0xb2 | 1200SYM:Symbolic address mode of S7-1200 |
| 0xb0 | DBREAD:Kind of DB block read, seen only at an S7-400 |
| 0x82 | NCK:Sinumerik NCK HMI access |
- 附录七,S7Parameter->Item->Transport size常见值
| Parameter ITEM | |||
|---|---|---|---|
| 0x01 | BIT | 0x0A | TOD(Time of day 32位) |
| 0x02 | Byte | 0x0B | TIME(IEC时间32位) |
| 0x03 | CHAR | 0x0C | S5TIME(Simatic时间16位) |
| 0x04 | WORD | 0x0F | DATE AND TIME |
| 0x05 | INT | 0x1C | COUNTER |
| 0x06 | DWORD | 0x1D | TIMER |
| 0x07 | DINT | 0x1E | IEC TIMER |
| 0x08 | REAL | 0x1F | IEC COUNTER |
| 0x09 | DATE | 0x20 | HS COUNTER |
| DATA Item | |
|---|---|
| 0x00 | NULL |
| 0x03 | BIT |
| 0x04 | BYTE/WORD/DWORD |
| 0x05 | INTEGER |
| 0x07 | REAL |
| 0x09 | OCTET STRING |
- 附录八,S7Parameter->Item->Area常见值
| 0x03 | System info of 200 family | 200系列系统信息 |
|---|---|
| 0x05 | System flags of 200 family | 200系列系统标志 |
| 0x06 | Analog inputs of 200 family | 200系列模拟量输入 |
| 0x07 | Analog outputs of 200 family | 200系列模拟量输出 |
| 0x80 | Direct peripheral access (P) | 直接访问外设 |
| 0x81 | Inputs (I) | 输入(I) |
| 0x82 | Outputs (Q) | 输出(Q) |
| 0x83 | M |
| 0x84 | Data blocks (DB) | 数据块(DB) V |
| 0x85 | Instance data blocks (DI) | 背景数据块(DI) |
| 0x86 | Local data (L) | 局部变量(L) |
| 0x87 | Unknown yet (V) | 全局变量(V) |
| 0x1c | S7 counters (C) | S7计数器(C) |
| 0x1d | S7 timers (T) | S7定时器(T) |
| 0x1e | IEC counters (200 family) | IEC计数器(200系列) |
| 0x1f | IEC timers (200 family) | IEC定时器(200系列) |
- 附录九,S7Data->Item->Return code已知枚举值
| 0xff | 成功 |
|---|---|
| 0x00 | Reserved | 未定义,预留 |
| 0x01 | 硬件错误 |
| 0x03 | 对象不允许访问 |
| 0x05 | 地址越界,无效地址,所需的地址超出此PLC的极限 |
| 0x06 | 请求的数据类型与存储类型不一致 |
| 0x07 | 日期类型不一致 |
| 0x0a | 对象不存在 |
- 附录十,Userdata已知枚举值
| 0x0 | 转换工作模式(Mode-transition) |
|---|---|
| 0x1 | 工程师命令调度(Programmer commands) |
| 0x2 | 循环读取(Cyclic data) |
| 0x3 | 块功能(Block functions) |
| 0x4 | CPU功能(CPU functions) |
| 0x5 | 安全功能(Security) |
| 0x6 | PBC BSEND/BRECV |
| 0x7 | 时间功能(Time functions) |
| 0xf | NC编程(NC programming) |
- 附录十一,PI service names已知枚举值
| _INSE | PI-Service_INSE(Activates a PLC module)。激活设备上下载的块,参数是块的名称 |
|---|---|
| _DELE | 工程师命令调度(Programmer commands)。从设备的文件系统中删除一个块,该参数是块的名称 |
| P_PROGRAM | 循环读取(Cyclic data)。设置设备的运行状态(启动、停止、复位) |
| _MODU | 块功能(Block functions)。压缩PLC内存 |
| _GARB | CPU功能(CPU functions)。将RAM复制到ROM,参数包含文件系统标识符(A/E/P) |
- 附录十二,文件系统
| P | 被动模块。Passive (copied,but not chained) module |
|---|---|
| A | 有源嵌入式模块。Active embedded module |
| B | 有源和无源模块。Active as well as passive module |
二、西门子S7通信库封装
1. 功能清单
- 连接、断开
- 读取、写入
- 启动、停止
- 获取时间、设置时间
2. 定义枚举与数据字典
- 定义枚举
public enum Areas
{
I = 0x81,
Q = 0x82,
DB = 0x84,
V = 0x84,
M = 0x83
}
public enum ParameterVarType
{
BIT = 0x01,
BYTE = 0x02,
CHAR = 0x03,
WORD = 0x04,
INT = 0x05,
DWORD = 0x06,
DINT = 0x07,
REAL = 0x08,
DATE = 0x09
}
public enum DataVarType
{
NULL = 0x00,
BIT = 0x03,
BYTE = 0x04,
WORD = 0x04,
DWORD = 0x04,
INTERGER = 0x05,
REAL = 0x07,
OCTETSTRING = 0x09
}
- 定义2个数据字典,用于判断返回状态和错误信息
public static class Status
{
public static Dictionary<ushort, string> HeaderErrors = new Dictionary<ushort, string>()
{
{ 0x0110,"无效块类型编号" },
{ 0x0112,"无效参数" },
{ 0x011A,"PG资源错误" },
{ 0x011B,"PLC重新外包错误" },
{ 0x011C,"协议错误" },
{ 0x011F,"用户缓冲区太短" },
{ 0x0141,"请求错误" },
{ 0x01C0,"版本不匹配" },
{ 0x01F0,"末实施" },
{ 0x8001,"L7无效CPU状态" },
// .......自行补齐
};
public static Dictionary<byte, string> DataReturnCode = new Dictionary<byte, string>()
{
{ 0xff,"请求成功"},
{ 0x01,"硬件错误"},
{ 0x03,"对象不允许访问"},
{ 0x05,"地址越界,所需的地址超出此PLC的极限"},
{ 0x06,"请求的数据类型与存储类型不一致"},
{ 0x07,"日期类型不一致"},
{ 0x0a,"对象不存在"}
};
}
3. 封装实体
西门子S7通信过程中,无论是读取还是写入,它是支持批量通信的,而每个读取或写入都至少需要知道“存储区、地址、类型、数量或数据”等基本信息。
因此我们可以为读或写,定义一个统一的数据实体对象
// 需要读取一个地址下的特定类型的数据,读N个
public class DataParameter
{
public Areas Area { get; set; }
public ushort DBNumber { get; set; } = 0;
// 请求的时候 byte
public ParameterVarType PVarType { get; set; }
public DataVarType DVarType { get; set; }
public int ByteAddress { get; set; }
public byte BitAddress { get; set; }
public int Count { get; set; } = 1;
public byte[] DataBytes { get; set; } = new byte[0];
}
4. 连接与断开
我们知道连接到PLC,需要经过3次握手、COTP和Communication。这3步需要一气呵成,任何一个环节出问题都会导致连接不上。
连上之后,后续的操作也需要使用同一个socket对象。
因此,在写通信库前,我们需要准备全局的私有socket对象,以及pduSize的长度(用来判断是否消息过长)。
public class S7Client
{
private Socket? socket;
//通信方PLC的PDU长度
private int pduSize = 0;
/// <summary>
/// 连接到PLC
/// </summary>
public void Connect(string ip, byte rack, byte slot, int timeout = 5000)
{
socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.ReceiveTimeout = timeout;
socket.Connect(ip, 102); //102是固定端口
// COTP
byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x16,
// COTP
0x11,
0xe0,
0x00,0x00,
0x00,0x01,
0x00,
// 0xc0
0xc0,
0x01,
0x0a,
// 0xc1
0xc1,
0x02,
0x10, // S7双边通信
0x00,
// 0xc2
0xc2,
0x02,
0x03, // S7单边模式
// 如果机架插槽为0,0 0x00 机架*32+插槽 (机架<<5)+插槽
(byte)(rack*32+slot)
};
this.SendAndReceive(bytes);
// Setup Communication
bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x19,
// COTP
0x02,
0xf0,
0x80,
// S7 - Header
0x32,
0x01,
0x00,0x00,
0x00,0x00,
// Parameter 长度
0x00,0x08,
// Data 长度
0x00,0x00,
// S7 - Parameter
0xf0, // Setup Communication Funcion
0x00,
0x00,0x01, // 任务处理队列长度
0x00,0x01, // 任务处理队列长度
0x03,0xc0 // PDU 长度 960 PLC 反馈 240 480 960
};
byte[] resp = this.SendAndReceive(bytes);
if (resp[14] != 0x00)
throw new Exception("连接异常:Setup Communication失败");
pduSize = BitConverter.ToUInt16(new byte[] { resp[22], resp[21] });
}
public void Disconnect()
{
if (socket == null) return;
socket.Disconnect(true);
socket.Close();
socket = null;
}
private byte[] SendAndReceive(byte[] reqBytes)
{
socket.Send(reqBytes);
byte[] resp = new byte[4];
socket.Receive(resp, 0, resp.Length, SocketFlags.None);
int len = resp[2] * 256 + resp[3] - 4;
resp = new byte[len];
socket.Receive(resp, 0, resp.Length, SocketFlags.None);
return resp;
}
}
5. 读取数据
- 实体转Byte,根据我们前面封装的DataParameter实体,读取数据时会传入不定长度的DataParameter对象,我们先提供一个实体对象转Byte数组的方法
private byte[] GetS7Parameters(DataParameter[] variable)
{
List<byte> result = new List<byte>();
int sum = 0;
foreach (DataParameter dp in variable)
{
int num = 1;
if (dp.PVarType == ParameterVarType.WORD)
num = 2;
if (dp.PVarType == ParameterVarType.DWORD)
num = 4;
num *= dp.Count;
num += num % 2;
sum += num;
//每一个item的报文
byte[] dp_bytes = new byte[] {
0x12,
0x0a,
0x10,
(byte)dp.PVarType,
(byte)(dp.Count / 256 % 256),
(byte)(dp.Count % 256),
(byte)(dp.DBNumber / 256 % 256),
(byte)(dp.DBNumber % 256),
// 存储区
(byte)dp.Area,
(byte)(((dp.ByteAddress << 3) + dp.BitAddress) / 256 / 256 % 256),
(byte)(((dp.ByteAddress << 3) + dp.BitAddress) / 256 % 256),
(byte)(((dp.ByteAddress << 3) + dp.BitAddress) % 256)
};
result.AddRange(dp_bytes);
}
// 报异常/自动分组
if (sum > (pduSize - 50))
throw new Exception("请确认请求数据量");
return result.ToArray();
}
- 实现读取方法,核心理解
- 长度的计算
- 异常检查
public void Read(params DataParameter[] variable)
{
byte[] ps = this.GetS7Parameters(variable);
byte[] bytes = new byte[] {
// COTP
0x02,
0xf0,//数据传输
0x80,
// S7 - Header
0x32, // 协议ID 默认0x32
0x01, // 向PLC发送一个Job请求
0x00,0x00,
0x00,0x01, // 累加序号
// Parameter 长度
//0x00,0x0e,
(byte)((ps.Length+2)/256%256),
(byte)((ps.Length+2)%256),
// Data 长度
0x00,0x00,
// S7 - Parameter
0x04, // 向PLC发送一个读变量的请求
(byte)variable.Length, // Item的数量 Item中包含了请求的地址以及类型相关信息
};
List<byte> req_bytes = new List<byte>()
{
0x03,
0x00,
(byte)((ps.Length+bytes.Length+4)/256%256),
(byte)((ps.Length+bytes.Length+4)%256),
};
req_bytes.AddRange(bytes);
req_bytes.AddRange(ps);
byte[] resp = this.SendAndReceive(req_bytes.ToArray());
/// 做异常检查
/// 0x0112
ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
int offset = 17;
for (int i = 0; i < variable.Length; i++)
{
// 第一个ITEM的返回状态
if (resp[offset] != 0xff)//21
{
throw new Exception(Status.DataReturnCode[resp[offset]]);
}
offset++;//22
// 后续有多少个数据字节 位数 / 8 = 字节数
// byte Word DWord bit
int data_bytes_len = resp[offset + 1] * 256 + resp[offset + 2];
if (resp[offset] == 0x04)
data_bytes_len /= 8;
byte[] data_bytes = resp.ToList().GetRange(offset + 3, data_bytes_len).ToArray();
variable[i].DataBytes = data_bytes;
// 包含了Item1的的有字节数 再往后偏移一个字节
// 数据字节数 + 状态字节和字节数字节 + 往后偏移一个 + FillBytes字节
// (data_bytes.Length % 2)当数据字节为奇数个时,添加一个FillBytes字节
offset += data_bytes_len + 2 + 1 + (data_bytes.Length % 2);
}
}
- 测试读取
static void CustomLib_ReadTest()
{
S7Lib.S7Client client = new S7Lib.S7Client();
client.Connect("192.168.1.100", 0, 0);
{
// 一交性读取多个不同区域的数据
// DB1.DBW100
DataParameter dataParameter = new DataParameter();
dataParameter.Area = Areas.DB;
dataParameter.DBNumber = 1;
dataParameter.PVarType = ParameterVarType.DWORD;
dataParameter.Count = 2;
dataParameter.ByteAddress = 12;
// DB1.DBB10
DataParameter dataParameter2 = new DataParameter();
dataParameter2.Area = Areas.DB;
dataParameter2.DBNumber = 1;
dataParameter2.PVarType = ParameterVarType.BYTE;
dataParameter2.Count = 3;
dataParameter2.ByteAddress = 10;
// Q0.5
DataParameter dataParameter3 = new DataParameter();
dataParameter3.Area = Areas.Q;
dataParameter3.PVarType = ParameterVarType.BIT;
//dataParameter3.Count = 1;// 对于位的操作,数据只能是1,多了是处理不了的
//dataParameter3.ByteAddress = 0;
dataParameter3.BitAddress = 5;
//这里顺序可以交换
DataParameter[] dps = new DataParameter[] {
dataParameter3,
dataParameter,
dataParameter2,
};
client.Read(dps);
foreach (var item in dps)
{
Console.WriteLine(string.Join(" ", item.DataBytes.Select(b => b.ToString("X2"))));
}
}
Console.WriteLine("读取完成");
}
实现按地址读取,我们只实现一个地址的读取,读取格式如:
- I I0.0 IB0 IW0 ID0
- Q Q0.0 QB.....
- M
- V V10.5 VB10 VW10 VD10
- DB DB1.DBX0.0 DB1.DBB0 DB1.DBW0 DB1.DBD0
关键步骤:
- Read方法内部直接调用前面实现好的Read方法,关键是解析DataParameter
- 定义2个数据字典,用来处理数据类型
- 核心是GetVariable方法
public byte[] Read(string variable, int count = 1)
{
DataParameter parameter = GetVariable(variable, count);
this.Read(parameter);
return parameter.DataBytes;
}
// 只对一个地址进行处理
// DB1.DBW100
// Read("DB1.DBW100",2)
/// I I0.0 IB0 IW0 ID0
/// Q Q0.0 QB.....
/// M
/// V V10.5 VB10 VW10 VD10
/// DB DB1.DBX0.0 DB1.DBB0 DB1.DBW0 DB1.DBD0
private Dictionary<string, ParameterVarType> ParamVarDic = new Dictionary<string, ParameterVarType>
{
{ "X",ParameterVarType.BIT},
{ "B",ParameterVarType.BYTE},
{ "W",ParameterVarType.WORD},
{ "D",ParameterVarType.DWORD},
};
private Dictionary<char, Areas> AreaDic = new Dictionary<char, Areas>
{
{'I',Areas.I },
{'Q',Areas.Q },
{'M',Areas.M },
{'V',Areas.DB }
};
private DataParameter GetVariable(string variable, int count)
{
DataParameter parameter = new DataParameter();
parameter.Count = count;
string str = variable.Substring(0, 2);
if (str.ToUpperInvariant() == "DB")
{
parameter.Area = Areas.DB;
string[] vs = variable.Split(".");
// vs[0]:DB1 DB100 1000 123
// vs[1]:DBX0
// vs[2]:0
// DB编号出来
if (!ushort.TryParse(vs[0].Substring(2), out ushort db_num))
throw new Exception("地址错误,无法解析");
parameter.DBNumber = db_num;
// 获取请求的数据类型
string st = vs[1].Substring(2, 1);
//ParameterVarType pv = ParamVarDic[st];
parameter.PVarType = ParamVarDic[st];
// 获取请求的Byte地址
if (!int.TryParse(vs[1].Substring(3), out int byte_addr))
throw new Exception("地址错误,无法解析");
parameter.ByteAddress = byte_addr;
// 获取请求的位地址
if (vs.Length == 3)
{
if (!byte.TryParse(vs[2], out byte bit_addr))
throw new Exception("地址错误,无法解析");
parameter.BitAddress = bit_addr;
parameter.Count = 1;
}
}
else if ("IQMV".Contains(variable[0]))
{
// 根据第一个字符进行区域判断
parameter.Area = AreaDic[variable[0]];
// 如果是针对V区 这里设置为1 其他需要为0
parameter.DBNumber = (ushort)(variable[0] == 'V' ? 1 : 0);
string[] vs = variable.Split(".");
if (vs.Length == 2)
{
// 按位操作
parameter.PVarType = ParameterVarType.BIT;
//vs[0].Substring(1);// Byte地址
if (!int.TryParse(vs[0].Substring(1), out int byte_addr))
throw new Exception("地址错误,无法解析");
parameter.ByteAddress = byte_addr;
//vs[1];// Bit地址
if (!byte.TryParse(vs[1], out byte bit_addr))
throw new Exception("地址错误,无法解析");
parameter.BitAddress = bit_addr;
parameter.Count = 1;
}
else
{
// VB10 VW10 VD10
// 按Byte/Word/DWord
string st = variable.Substring(1, 1);
parameter.PVarType = ParamVarDic[st];
// Byte地址
if (!int.TryParse(variable.Substring(2), out int byte_addr))
throw new Exception("地址错误,无法解析");
parameter.ByteAddress = byte_addr;
}
}
else
{
throw new Exception("地址格式不支持,无法解析");
}
return parameter;
}
- 测试按地址读取
byte[] data_bytes = client.Read("VW100", 2);
//byte[] data_bytes = client.Read("DB1.DBB100", 6);
//byte[] data_bytes = client.Read("Q0.5");
Console.WriteLine(string.Join(" ", data_bytes.Select(b => b.ToString("X2"))));
6. 写入数据
数据写入跟读取一下,参数也是DataParameter实体。区别是写入数据除了需要解析Parameter,还需要解析Data。
- 实现GetS7Datas方法
private byte[] GetS7Datas(DataParameter[] variable)
{
List<byte> data = new List<byte>();
for (int i = 0; i < variable.Length; i++)
{
DataParameter parameter = variable[i];
int bit = 1;
if ((byte)parameter.DVarType == 0x04)
bit = 8;
List<byte> d_tmep = new List<byte> {
// S7 - Data - Item-1
0x00,
(byte)parameter.DVarType, // 类型 BIT/BYTE/WORD/DWORD
// 04 长度*8 03 不需要*8
(byte)(parameter.DataBytes.Length * bit / 256),
(byte)(parameter.DataBytes.Length * bit % 256)
};
d_tmep.AddRange(parameter.DataBytes);
// 考虑Fill Byte
// 但是如果这是最后一个Item,就不需要加这个Fill Byte
if ((parameter.DataBytes.Length % 2) == 1 &&
i < variable.Length - 1)
{
d_tmep.Add(0x00);
}
data.AddRange(d_tmep);
}
return data.ToArray();
}
- 实现数据写入
public void Write(params DataParameter[] variable)
{
byte[] ps = this.GetS7Parameters(variable);
byte[] ds = this.GetS7Datas(variable);
byte[] bytes = new byte[] {
// COTP
0x02,
0xf0,
0x80,
// S7 - Header
0x32,
0x01,
0x00,0x00,
0x00,0x01,
// Parameter 长度***************
(byte)((ps.Length+2)/256%256),
(byte)((ps.Length+2)%256),
// Data 长度********************
(byte)(ds.Length/256),
(byte)(ds.Length%256),
// S7 - Parameter
0x05,
(byte)variable.Length, // Item的数量 **********************************
};
List<byte> req_bytes = new List<byte>()
{
0x03,
0x00,
(byte)((4+bytes.Length+ps.Length+ds.Length)/256%256),
(byte)((4+bytes.Length+ps.Length+ds.Length)%256),
};
req_bytes.AddRange(bytes);
req_bytes.AddRange(ps);
req_bytes.AddRange(ds);
byte[] resp = this.SendAndReceive(req_bytes.ToArray());
ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
int offset = 17;
for (int i = 0; i < variable.Length; i++)
{
// 第一个ITEM的返回状态
if (resp[offset] != 0xff)//21
{
throw new Exception(Status.DataReturnCode[resp[offset]]);
}
offset++;//22
}
}
- 测试写入
static void CustomLib_WriteTest()
{
S7Lib.S7Client client = new S7Lib.S7Client();
client.Connect("192.168.1.100", 0, 0);
client.Connect("192.168.174.128", 0, 0);
// 一交性写入多个不同区域的数据
// DB1.DBW100 : 123
DataParameter dataParameter = new DataParameter();
dataParameter.Area = Areas.DB;
dataParameter.DBNumber = 1;
dataParameter.PVarType = ParameterVarType.BYTE;
dataParameter.Count = 2;
dataParameter.ByteAddress = 100;
dataParameter.DataBytes = new byte[] { 0x00, 0x7B };//123
dataParameter.DVarType = DataVarType.BYTE;
// DB1.DBB10 : 123\124\125
DataParameter dataParameter2 = new DataParameter();
dataParameter2.Area = Areas.DB;
dataParameter2.DBNumber = 1;
dataParameter2.PVarType = ParameterVarType.WORD;
dataParameter2.Count = 3;
dataParameter2.ByteAddress = 10;
dataParameter2.DataBytes = new byte[] { 0x00, 0x7B, 0x00, 0x7C, 0x00, 0x7D };//123-124-125
dataParameter2.DVarType = DataVarType.BYTE;
// Q0.5 : 0
DataParameter dataParameter3 = new DataParameter();
dataParameter3.Area = Areas.Q;
dataParameter3.PVarType = ParameterVarType.BIT;
dataParameter3.BitAddress = 5;
dataParameter3.DataBytes = new byte[] { 0x00 };
dataParameter3.DVarType = DataVarType.BIT;
DataParameter[] dps = new DataParameter[] {
dataParameter,
dataParameter2,
dataParameter3,
};
client.Write(dps);
Console.WriteLine("写入完成");
}
- 按地址写入,跟按地址读取一样,我们扩展一个按地址写入的方法。由于基础方法已经封装好,按地址写入的实现就非常简单
public void Write(string variable, byte[] datas)
{
DataParameter parameter = GetVariable(variable, 1);
if (parameter.PVarType == ParameterVarType.BYTE)
parameter.Count = datas.Length;
else if (parameter.PVarType == ParameterVarType.WORD)
parameter.Count = datas.Length / 2;
else if (parameter.PVarType == ParameterVarType.DWORD)
parameter.Count = datas.Length / 4;
parameter.DVarType = DataVarType.BYTE;
if (parameter.PVarType == ParameterVarType.BIT)
parameter.DVarType = DataVarType.BIT;
parameter.DataBytes = datas;
this.Write(parameter);
}
- 测试按地址写入
client.Write("VW20", new byte[] { 0x00, 0x0A });
client.Write("VB22", new byte[] { 0x0B, 0x0C });
client.Write("DB1.DBB22", new byte[] { 0x0B, 0x0C });
client.Write("Q0.6", new byte[] { 0x01 });
7. 报文内容解析
之前实现modbus解析的时候,实现了一套报文内容的解析方法。我们这里也为S7库扩展这两个方法,实现原理跟modbus一样。
// 提供一些数据解析功能(1、读取出来的字节进行数据转换;2、将数据转换成字节)
public List<T> GetDatas<T>(byte[] bytes)
{
List<T> datas = new List<T>();
if (typeof(T) == typeof(bool))
{
foreach (byte b in bytes)
{
dynamic d = (b == 0x01);
datas.Add(d);
}
}
// 字符串 长度 _ XXXXXXXXXXX
// 这个处理逻辑不支持200Smart的字符串处理
else if (typeof(T) == typeof(string))
{
//byte[0] 254 字符串的有效空间
//byte[1] 有效字符的字节数
byte[] str_bytes = bytes.ToList().GetRange(2, bytes[1]).ToArray();
dynamic d = Encoding.UTF8.GetString(str_bytes);
datas.Add(d);
}
// 严格的字节2、4、8个数的这种情况
// 这里一次将统一数据类型进行处理,不支持多类型的转
else
{
// short ushort int16 uint16 = 2
// int uint int32 uint32 float = 4
// double int64 uint64 = 8
int size = Marshal.SizeOf<T>();
Type tBitConverter = typeof(BitConverter);
MethodInfo[] mis = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static);
if (mis.Count() <= 0) return datas;
MethodInfo? mi = mis.FirstOrDefault(m => m.ReturnType == typeof(T) && m.GetParameters().Count() == 2);
if(mi == null)
throw new Exception($"类型错误:无法解析{typeof(T)}类型");
// 作用直接根据这个长度进行原始字节的截取 然后进行相应类型的转换
for (int i = 0; i < bytes.Length; i += size)
{
byte[] data_bytes = bytes.ToList().GetRange(i, size).ToArray();
// 根据当前指定的数据类型进行动态选择方法[反射]
//
// BitConverter 小端
if (BitConverter.IsLittleEndian)
{
Array.Reverse(data_bytes);
}
dynamic v = mi.Invoke(tBitConverter, new object[] { data_bytes, 0 })!;
datas.Add(v);
}
}
return datas;
}
public byte[] GetBytes<T>(params T[] values)
{
List<byte> bytes = new List<byte>();
if (typeof(T) == typeof(bool))
{
foreach (var v in values)
{
bytes.Add((byte)(bool.Parse(v.ToString()) ? 0x01 : 0x00));
}
}
else if (typeof(T) == typeof(string))
{
byte[] str_bytes = Encoding.UTF8.GetBytes(values[0].ToString());
bytes.AddRange(str_bytes);
}
else
{
foreach (var v in values)
{
dynamic d = v;
byte[] v_bytes = BitConverter.GetBytes(d);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(v_bytes);
}
bytes.AddRange(v_bytes);
}
}
return bytes.ToArray();
}
使用
List<float> str = client.GetDatas<float>(dataParameter.DataBytes.ToArray());
byte[] datas1 = client.GetBytes<short>(-20, 1000, 666);
byte[] datas2 = client.GetBytes<float>(-20.2f, 6.5f, 66.6f);
8. 启动与停止
// 功能有限制 200Smart/300/400有用
public void Run()
{
byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x25, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x01,
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x14, // 10#20
// Data Length
0x00,0x00,
// S7-Parameter
0x28,// 控制PLC启动
0x00,0x00,0x00,0x00,0x00,0x00,0xfd,
0x00,0x00,
0x09,
// "P_PROGRAM"
0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
};
byte[] resp = this.SendAndReceive(bytes);
ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
}
public void Stop()
{
byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x21, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x01,
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x10, // 10#20
// Data Length
0x00,0x00,
// S7-Parameter
0x29,// 控制PLC停止
0x00,0x00,0x00,0x00,0x00,
//0x00,0xfd, 相对启动来讲,多了这4个字节
//0x00,0x00,
0x09,
// P_PROGRAM
0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
};
byte[] resp = this.SendAndReceive(bytes);
ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
}
9. 获取和设置时间
public void SetTime(DateTime time)
{
// "25" 0x25
// "2026" : 0x20 0x26 // 这个方法在Framework环境下无效,只能一个一个转
byte year = Convert.FromHexString((time.Year - 2000).ToString("00"))[0];
byte month = Convert.FromHexString(time.Month.ToString("00"))[0];
byte day = Convert.FromHexString(time.Day.ToString("00"))[0];
byte hour = Convert.FromHexString(time.Hour.ToString("00"))[0];
byte minute = Convert.FromHexString(time.Minute.ToString("00"))[0];
byte second = Convert.FromHexString(time.Second.ToString("00"))[0];
byte[] bytes = new byte[] {
// TPKT
0x03,0x00,
0x00,0x27, // 39
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x07,// UserData
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x08,
// Data Length
0x00,0x0E, // 14
// S7-Paramter
0x00,0x01,0x12,
0x04,
0x11,
0x47,
0x02,// Set Clock
0x00,
// S7-Data
0xff,
0x09,
0x00,0x0a,// 后续有10个长度的字节
0x00,
0x19,
// Year:25
year,
// Month:10
month,
// Day:01
day,
// Hour:20
hour,
// Minute:05
minute,
// Second:00
second,
// 毫秒:0 和星期
0x00,0x00
// 0000 0000 0000 0 100
};
byte[] resp = this.SendAndReceive(bytes);
ushort header_error_code = (ushort)(resp[23] << 8 + resp[24]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
else if (resp[25] != 0xff)
{
// 这里虽说有异常 但是时间已经设置成功,这里不做检查
//throw new Exception(Status.DataReturnCode[resp[25]]);
}
}
public DateTime GetTime()
{
byte[] bytes = new byte[] {
// TPKT
0x03,
0x00,
0x00,0x1d, // 37
// COTP
0x02,
0xf0,
0x80,
// S7-Header
0x32,
0x07,//UserData
0x00,0x00,0x00,0x00,
// Parameter Length
0x00,0x8, // 10#20
// Data Length
0x00,0x04,
// S7-Parameter
0x00,0x01,0x12,
0x04,// Parameter部分后续还有4个字节的内容
0x11,
// 0100 0111
0x47,
0x01,// Read Clock
0x00,
// S7-Data
0x0a,
0x00,
0x00,0x00
};
byte[] resp = this.SendAndReceive(bytes);
ushort header_error_code = (ushort)(resp[23] << 8 + resp[24]);
if (header_error_code != 0)
{
throw new Exception(Status.HeaderErrors[header_error_code]);
}
else if (resp[25] != 0xff)
{
throw new Exception(Status.DataReturnCode[resp[25]]);
}
else
{
// 年
byte b_year = resp[31];
// 0x0A "0A" "10"
if (!int.TryParse("20" + b_year.ToString("X2"), out int year))
{
throw new Exception("时间转换失败");
}
// 月
byte m_byte = resp[32];
if (!int.TryParse(m_byte.ToString("X2"), out int month))
{
throw new Exception("时间转换失败");
}
// 日
byte dayByte = resp[33];
if (!int.TryParse(dayByte.ToString("X2"), out int day))
{
throw new Exception("时间转换失败");
}
// 时
byte hourByte = resp[34];
if (!int.TryParse(hourByte.ToString("X2"), out int hour))
{
throw new Exception("时间转换失败");
}
// 分
byte minuteByte = resp[35];
if (!int.TryParse(minuteByte.ToString("X2"), out int minute))
{
throw new Exception("时间转换失败");
}
// 秒
byte secondByte = resp[36];
if (!int.TryParse(secondByte.ToString("X2"), out int second))
{
throw new Exception("时间转换失败");
}
DateTime dt = new DateTime(year, month, day, hour, minute, second);
return dt;
}
}
