目录
西门子s7协议
西门子PLC数据类型
PLC中类型与C#对应类型
特殊说明:
S7协议帧结构
示例代码(C#访问PLC数据):
上位机和西门子PLC的通讯
西门子PLC的存储区
S7协议通讯网络模型
S7协议栈基于ISO/OSI模型,但主要实现以下4层:
S7协议的主要通信方式
s7协议通讯流程
COTP连接(第一次握手)报文
完整报文示例(Hex格式)
S7连接(第二次握手)报文
读取格式
关键差异说明
写入格式
关键差异说明
完整代码
西门子S7通信协议(S7 Communication Protocol)是西门子为其SIMATIC S7系列PLC(如S7-200、S7-300、S7-400、S7-1200、S7-1500)开发的一套工业自动化通信协议,主要用于PLC与HMI(人机界面)、SCADA系统、上位机(如PC)、其他PLC或工业设备之间的数据交换。
西门子s7协议
S7Comm(S7 Communication)是西门子专有的协议,是西门子S7通讯协议簇里的一种。
S7通信协议是西门子S7系列PLC内部集成的一种通信协议,是S7系列PLC的精髓所在。它是一种运行在传输层之上的(会话层/表示层/应用层)、经过特殊优化的通信协议,其信息传输可以基于MPI网络、PROFIBUS网络或者以太网S7在TCP连接上后还需要进行两次握手,S7协议的TCP/IP实现依赖于面向块的ISO传输服务。S7协议被封装在TPKT和ISO-COTP协议中,这使得PDU(协议数据单元) 能够通过TCP传送。
西门子PLC数据类型
PLC 数据类型
基本数据类型:包括位、位字符串、整数、浮点数、定时器、日期&时间、字符、数组和结构
PLC中类型与C#对应类型
PLC数据类型 (西门子) | C# 对应类型 | 存储大小 | 值范围 | 示例 |
---|---|---|---|---|
BOOL (位) | bool | 1 bit | true/false | I0.0, Q1.1 |
BYTE (字节) | byte | 8 bits | 0-255 | IB0 |
WORD (字) | ushort | 16 bits | 0-65535 | MW10 |
DWORD (双字) | uint | 32 bits | 0-4,294,967,295 | MD20 |
INT (短整数) | short | 16 bits | -32,768~32,767 | DB1.DBW0 |
DINT (双整数) | int | 32 bits | -2,147,483,648~2,147,483,647 | DB2.DBD4 |
USINT (无符号短整数) | byte | 8 bits | 0-255 | - |
UINT (无符号整数) | ushort | 16 bits | 0-65,535 | - |
UDINT (无符号双整数) | uint | 32 bits | 0-4,294,967,295 | - |
REAL (浮点数) | float | 32 bits | ±1.5×10⁻⁴⁵~±3.4×10³⁸ | DB3.DBD8 |
LREAL (长浮点数) | double | 64 bits | ±5.0×10⁻³²⁴~±1.7×10³⁰⁸ | DB4.DBD12 |
CHAR (字符) | char | 8 bits | ASCII字符 | 'A' |
STRING (字符串) | string | 可变长度 | 最大254字符 | "Hello" |
TIME (时间间隔) | TimeSpan | 32 bits | 0~2,147,483,647 ms | T#1D2H3M4S |
DATE_AND_TIME (日期时间) | DateTime | 64 bits | 1970-01-01~2106-02-07 | DT#2023-10-01-14:30:00 |
特殊说明:
-
PLC数组 → C#数组(如
REAL[10]
→float[]
) -
PLC结构体(STRUCT) → C#
class
或struct
-
TIME 类型在西门子PLC中以毫秒存储,C#用
TimeSpan
表示时间间隔 -
DATE_AND_TIME 在C#中对应完整的
DateTime
结构 -
字符串处理需注意:西门子STRING前2字节存储长度信息(如
[长度][最大长度][字符数据]
)
S7协议帧结构
1 TPKT 会话层 主要设置版本号 预留号 报文总长度
2 COPT 表示层 设置PDU类型
3 s7协议 应用层 设置协议头和协议参数等
示例代码(C#访问PLC数据):
// 使用S7.Net库读取不同类型数据
var plc = new Plc(CpuType.S71500, "192.168.1.10", 0, 1);
plc.Open();// 读取不同类型
bool boolVal = plc.Read("I0.0"); // BOOL
byte byteVal = plc.Read("MB0"); // BYTE
ushort wordVal = plc.Read("MW2"); // WORD
int intVal = plc.Read("DB1.DBW4"); // INT
float realVal = plc.Read("DB1.DBD8"); // REAL
DateTime dtVal = plc.Read("DB1.DBD12"); // DATE_AND_TIMEplc.Close();
上位机和西门子PLC的通讯
西门子PLC的存储区
-
I:输入
-
Q:输出
-
AI:模拟量输入
-
V/DB:变量存储区
可以使用modbus协议和s7协议和plc进行通讯,但是modbus协议只能请求上面四个区,西门子plc还有M:位区、T:定时器...
,使用S7协议可以操作所有的区
S7协议通讯网络模型
S7协议栈基于ISO/OSI模型,但主要实现以下4层:
-
物理层(第1层):
-
支持多种物理介质:RS485、工业以太网(PROFINET)、MPI等
-
典型传输速率:9.6Kbps-100Mbps(取决于具体实现)
-
-
数据链路层(第2层):
-
对于MPI/PROFIBUS网络使用西门子专有的第2层协议
-
对于PROFINET/工业以太网使用标准的IEEE 802.3协议
-
-
网络层(第3层):
-
使用西门子专有的网络层协议
-
处理设备寻址和路由
-
-
传输层(第4层):
-
使用西门子专有的传输协议
-
提供可靠的端到端数据传输
-
-
应用层(第7层):
-
S7通信协议本身
-
包含多种服务功能如读写变量、控制PLC等
-
S7协议的主要通信方式
-
S7基本通信:
-
用于简单的数据交换
-
基于OSI模型的第1-3层
-
-
S7通信(扩展通信):
-
更复杂的数据交换
-
基于OSI模型的第1-4层
-
提供更多服务功能
-
-
S7函数通信:
-
用于远程函数调用
-
基于OSI模型的完整7层
-
s7协议通讯流程
TCP三次握手(TCP连接时进行) => COTP连接(第一次握手连接) => S7连接(第二次握手) => 数据的读写
COTP连接(第一次握手)报文
字段分类 | 字段名称 | 字节位置 | 长度(字节) | 示例值(Hex) | 说明 |
---|---|---|---|---|---|
TPKT头部 | 版本号 | 0 | 1 | 0x03 | TPKT协议版本,固定为 0x03 |
保留位 | 1 | 1 | 0x00 | 保留字段,默认为0 | |
报文总长度 | 2-3 | 2 | 0x0016 | 整个报文长度(22字节) | |
COTP会话层 | COTP数据长度 | 4 | 1 | 0x11 | 后续COTP数据长度(17字 节) |
PDU类型 | 5 | 1 | 0xE0 | 0xE0表示连接请求(CR) | |
目标引用 | 6-7 | 2 | 0x0000 | 初始为0,由服务器分配 | |
源引用 | 8-9 | 2 | 0x0001 | 客户端生成的标识 | |
扩展格式 | 10 | 1 | 0x00 | 流控制标志(默认0) | |
COTP参数部分 | 源TSAP标识 | 11 | 1 | 0xC1 | 0xC1表示源TSAP(上位机) |
源TSAP长度 | 12 | 1 | 0x02 | 源TSAP参数长度(2字节) | |
源TSAP值 | 13-14 | 2 | 0x1000 | 连接模式:0x10(S7双边通信) | |
目标TSAP标识 | 15 | 1 | 0xC2 | 0xC2表示目标TSAP(PLC) | |
目标TSAP长度 | 16 | 1 | 0x02 | 目标TSAP参数长度(2字节) | |
目标TSAP值 | 17-18 | 2 | 0x0301 | 机架号0x03(0),槽 号0x01(1) | |
TPDU大小标识 | 19 | 1 | 0xC0 | 0xC0表示TPDU大小参数 | |
TPDU大小长度 | 20 | 1 | 0x01 | TPDU大小参数长度(1字节) | |
TPDU大小值 | 21 | 1 | 0x0A | 2^10=1024字节(最大传 输单元) |
完整报文示例(Hex格式)
03 00 00 16 11 E0 00 00 00 01 00 C1 02 10 00 C2 02 03 01 C0 01 0A
S7连接(第二次握手)报文
使用tcp五次握手链接
public partial class Form1 : Form
{public Form1(){InitializeComponent();}/// <summary>/// 五次握手/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void button1_Click(object sender, EventArgs e){// s7协议// 1 需要通过socket三次握手,不用写握手过程// 目前提供设备型号s71200 cpu:1212c 电压是24vDCTcpClient client = new TcpClient();client.Connect("192.168.107.202",102); // 连接服务器receiveData(client);// 2 第一请求连接 发送请求帧为// 总共22个字节byte[] bs1 = new byte[]{0x03, // 1字节版本号 默认是030x00, // 1字节 保留值 默认00x00, 0x16, // 2 字节 报文的总长度0x11, // 1字节从该字节往后字节个数 十进制是170xE0, // PDU 类型0x00,0x00, // DST引用 默认值0x00,0x01, // src引用0x00, // 采用默认值0xc1, // 上位机擦书0x02, // 上位机长度0x10,0x00, // 0x01代表双边通信 0x00机架号和插槽号0xC2, // plc参数0x02, // 长度0x03,0x01, // 0x01和0x00 共同控制机架号和插槽0xC0,0x01,0x0a};client.GetStream().Write(bs1,0,bs1.Length); // 发送第一次请求帧 // 3 第二次请求连接 发送请求帧为bs1 = new byte[]{0x03, // 1字节版本号 默认是030x00, // 1字节 保留值 默认00x00, 0x19, // 2 字节 报文的总长度0x02, // 当前字节后的字节数0xF0, // PUD类型 数据传输0x80, // 最高是十进制1280x32, // 协议ID,固定值0x01, // 工作类型 0x01 主站发送请求0x00,0x00,0x00,0x00,0x00,0x08, // 参数长度0x00,0x00, // 数据长度0xF0, // 功能码0x00, // Reserved保留值0x00,0x03, // 允许操作最大工作队列0x00,0x03, 0x03,0xc0, // 允许处理最大字节数组};client.GetStream().Write(bs1,0,bs1.Length);MessageBox.Show("连接成功");}/// <summary>/// 接收响应数据集/// </summary>/// <param name="tcpClient"></param>public void receiveData(TcpClient tcpClient){Task.Run(() =>{byte[] bytes = new byte[1024];while (tcpClient.Connected){// ONEint count = tcpClient.GetStream().Read(bytes,0,bytes.Length);if (count == 0) return;Console.WriteLine(BitConverter.ToString(bytes, 0, count) + "\r\n");// TWObyte[] s = new byte[count];Array.Copy(bytes,s,count);Console.WriteLine(string.Join(",",s));}});}
}
读取格式
读取报文
关键差异说明
-
报文长度:
-
请求报文总长度为31字节(0x001F),响应报文为29字节(0x001D)。
-
-
ROSCTR字段:
-
请求报文为作业请求(0x01),响应报文为确认响应(0x03)。
-
-
数据长度:
-
请求报文数据长度为0(0x0000),响应报文为8字节(0x0008)。
-
-
返回数据:
-
响应报文包含实际读取的数据(0x000000-N),长度为32字节(0x0020)。
-
/// <summary>
/// 读取M区
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{// 发送请求帧 请求M区地址从00开始 读取一个数据// 读取数据时的请求帧byte[] data = new byte[] {// TPKT: 版本号 预留号 总字节长度0x03, // 版本号 0x00, // 预留号0x00,0x01F, // 总字节长度// COTP: 0x02, // 往下的长度0xF0, // PDU类型0x80, // 目标引用// s7-header s7头0x32, // 协议ID 默认0x01, // 主站开始发请求0x00,0x00, // 预留位置0x03,0x7b, // 随机生成的数字 每次在基础之上递增0x00,0x0e, // 参数长度0x00,0x00, // 数据长度// s7-参数部分0x04, // 功能码 读取功能 重点0x01, // 如果涉及多读时候 设置为1,0x12, // 结构表示 一般默认120x0a, // 往后的字节长度0x10, // 寻址模式0x02, // 读取的数据类型 02是字节类型0x00,0x01, // 读取长度 重点0x00,0x00, // 读取不是DB区 重点0x83, // 0x83 M存储区,0x84DB块 重点0x00,0x00,0x70, // 开始数据起始地址 重点// 列如M30000, 实际地址是30000*8=24 00000,把转成山歌字节,转成16进制3a980 对应三个地址,0x03,0xA9 0x80// 列如数据DB块的数据,DB21234,4000,其中DB号是21234 转成16进制0x52F2,DB区改为0x52,0xF2// 40000*8=,2000,转成16进制7d00 转成三个字节变成 0x00,0x7d,0x00};socket.Send(data);
}
收取数据响应
/// <summary>
/// 接收响应数据
/// </summary>
void startReceive()
{Task.Run(() =>{byte[] bytes = new byte[1024];while (true){int count = socket.Receive(bytes);if (count == 0) break;// 转为16进制的字符串Console.WriteLine("十六进制打印:"+BitConverter.ToString(bytes,0,count));// 转为十进制打印byte[] datas = new byte[count];Array.Copy(bytes,datas,count);Console.WriteLine("十进制打印:" + string.Join(",",datas));Invoke(new Action(() =>{try{this.label1.Text = datas[25].ToString();}catch (Exception ex){Console.WriteLine(ex);}}));/* 响应的数据* 连接的响应* 03-00-00-16-11-D0-00-01-00-08-00-C0-01-0A-C1-02-10-00-C2-02-03-01* 03-00-00-1B-02-F0-80-32-03-00-00-00-00-00-08-00-00-00-00-F0-00-00-03-00-03-00-F0* * 读取数据的响应* 03-00-00-1A-02-F0-80-32-03-00-00-03-7B-00-02-00-05-00-00-04-01-FF(读取成功的标志)-04(读取的数据类型)-00-08(数据的长度)-0C(数据)*/}});
}
写入格式
写入报文
关键差异说明
-
报文长度:
-
请求报文总长度为36字节(0x0024),响应报文为29字节(0x001D)。
-
-
功能码:
-
请求报文为写入操作(0x05),响应报文可能复用读取功能码(0x04)或应为写入响应(需确认协议规范)。
-
-
数据内容:
-
请求报文包含写入地址(0x000000)和数据(0xFF),响应报文返回操作状态和可能的回读数据。
-
数据写入
/// <summary>
/// 写入M14
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{byte[] value = BitConverter.GetBytes(uint.Parse(textBox1.Text));// 生成写的报文byte[] bs = new byte[]{// TPKT部分0x03, // 版本号0x00, // 预留号// 0x00,0x24, // 报文总长度360x00,0x27, // 报文总长度39// TOPT0x02, // 长度0xF0, // PDU类型0xB0, // 目标引用// s7 header0x32, // 协议id0x01, // 主站开始请求0x00,0x00, // 预留部分0x03,0x7d, // 随机生成0x00,0x0E, // 参数长度0x00,0x08, // 参数数据长度// s7 参数0x05, // 05代表写入,04代表读取0x01, // 通信项数 可以支持多写0x12, // 变量指定0x0A, // 后面的长度0x10,0x02, // 传输数据类型 字节// 0x00,0x01, // 操作数据的长度0x00,0x04, // 操作数据的长度0x00,0x00, // M区 不是DB区0x83, // M区// 0x00,0x00,0x70, // 开始写入的地址M140x00,0x3e,0x80, // 开始写入的地址M20000x00,0x04, // 字节类型// 0x00,0x80, // 写入的长度 8位=1字节0x00,0x20, // 写入的长度 8位=1字节 (写入4个数据 4*8=32转16进制为20)//byte.Parse(textBox1.Text)value[3],value[2],value[1],value[0]};tcp.Send(bs);Type = RequestType.Write;
}
完整代码
public partial class Form1 : Form
{TcpClientHelper tcp;public Form1(){InitializeComponent();}private void Tcp_OnClose(TcpClientHelper obj){MessageBox.Show("客户端关闭");}enum RequestType{Write, // 写入请求Read, // 读取请求Connect // 连接的请求}RequestType Type;private void Tcp_OnMessage(byte[] arg1, TcpClientHelper arg2){// 获取数据即可 自动触发BeginInvoke(new Action(() =>{switch (Type){case RequestType.Write:// 写入数据的响应Console.WriteLine("写入数据的响应"+BitConverter.ToString(arg1) );break;case RequestType.Read:// 读取数据的响应Console.WriteLine("读取数据的响应" + BitConverter.ToString(arg1));this.label1.Text = arg1[arg1.Length-1].ToString();break;case RequestType.Connect:Console.WriteLine("连接时的响应" + BitConverter.ToString(arg1));break;}}));}/// <summary>/// 写入M14/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void button1_Click(object sender, EventArgs e){byte[] value = BitConverter.GetBytes(uint.Parse(textBox1.Text));// 生成写的报文byte[] bs = new byte[]{// TPKT部分0x03, // 版本号0x00, // 预留号// 0x00,0x24, // 报文总长度360x00,0x27, // 报文总长度39// TOPT0x02, // 长度0xF0, // PDU类型0xB0, // 目标引用// s7 header0x32, // 协议id0x01, // 主站开始请求0x00,0x00, // 预留部分0x03,0x7d, // 随机生成0x00,0x0E, // 参数长度0x00,0x08, // 参数数据长度// s7 参数0x05, // 05代表写入,04代表读取0x01, // 通信项数 可以支持多写0x12, // 变量指定0x0A, // 后面的长度0x10,0x02, // 传输数据类型 字节// 0x00,0x01, // 操作数据的长度0x00,0x04, // 操作数据的长度0x00,0x00, // M区 不是DB区0x83, // M区// 0x00,0x00,0x70, // 开始写入的地址M140x00,0x3e,0x80, // 开始写入的地址M20000x00,0x04, // 字节类型// 0x00,0x80, // 写入的长度 8位=1字节0x00,0x20, // 写入的长度 8位=1字节 (写入4个数据 4*8=32转16进制为20)//byte.Parse(textBox1.Text)value[3],value[2],value[1],value[0]};tcp.Send(bs);Type = RequestType.Write;}/// <summary>/// 读取M14/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void button2_Click(object sender, EventArgs e){// 读取的报文31字节byte[] bs = new byte[]{0x03,0x00,0x00,0x1f,0x02,0xf0,0xb0,// 协议参数0x32,0x01,0x00,0x00,0x03,0x7d,0x00,0x0e,0x00,0x00, // 读取操作 为00x04,0x01,0x12,0x0a,0x10,0x02,//0x00,0x01, // 读取长度0x00,0x04, // M2000 读取四个字节0x00,0x00,0x83,// 0x00, 0x00,0x700x00,0x3e,0x80,};tcp.Send(bs);Type = RequestType.Read;}/// <summary>/// 连接/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void Form1_Load(object sender, EventArgs e){// 1 创建客户端对象tcp = new TcpClientHelper();tcp.Connect("192.168.107.202", 102); // 连接服务器// 2 获取数据tcp.OnMessage += Tcp_OnMessage;// 3 关闭连接tcp.OnClose += Tcp_OnClose;// 第一次连接请求byte[] bs = new byte[]{0x03, // 1字节版本号 默认是030x00, // 1字节 保留值 默认00x00, 0x16, // 2 字节 报文的总长度0x11, // 1字节从该字节往后字节个数 十进制是170xE0, // PDU 类型0x00,0x00, // DST引用 默认值0x00,0x01, // src引用0x00, // 采用默认值0xc1, // 上位机擦书0x02, // 上位机长度0x10,0x00, // 0x01代表双边通信 0x00机架号和插槽号0xC2, // plc参数0x02, // 长度0x03,0x01, // 0x01和0x00 共同控制机架号和插槽0xC0,0x01,0x0a};tcp.Send(bs);// 第二次请求连接bs = new byte[]{0x03, // 1字节版本号 默认是030x00, // 1字节 保留值 默认00x00, 0x19, // 2 字节 报文的总长度0x02, // 当前字节后的字节数0xF0, // PUD类型 数据传输0x80, // 最高是十进制1280x32, // 协议ID,固定值0x01, // 工作类型 0x01 主站发送请求0x00,0x00,0x00,0x00,0x00,0x08, // 参数长度0x00,0x00, // 数据长度0xF0, // 功能码0x00, // Reserved保留值0x00,0x03, // 允许操作最大工作队列0x00,0x03,0x03,0xc0, // 允许处理最大字节数组};tcp.Send(bs);Type = RequestType.Connect;MessageBox.Show("连接成功");}
}
本文部分借鉴于网络,如有侵权请联系删除!!!