本文将尝试从源码角度,使用Tcp/Ip的方式直接与西门子PLC进行交互通讯。
C#与西门子PLC通讯 系列文章目录
往期博客参考
C#与西门子PLC通讯——新手快速入门 C#与西门子PLC通讯——熟手快速入门 建议先看一下这两篇,了解预设背景。
文章目录
前言
知其然,知其所以然。
这篇文章,我们就尝试重复造一个轮子。通过对通讯协议的简要分析,我们能够更好地了解与西门子PLC是如何交互的。最后,我们就运用底层方法,使用Socket通讯将一个数组读取出来,再将数组反转之后写回PLC中。
一、通讯协议
1.1 S7协议位置
首先,参照 ISO-OSI 参考模型,S7 协议位置如下:
参考西门子官网介绍:S7 协议有哪些属性,优势及特征?
1.2 S7通讯协议
当C#应用程序中与西门子PLC进行通信时,需要经历一系列协议阶段,以确保有效的数据传输和通信。 这些阶段包括TCP/IP协议、TPKT协议、COTP协议和S7连接协议。
- 通信的第一阶段是建立TCP/IP连接的三次握手。这是通信的基础,它确保了数据能够可靠地在客户端和PLC之间传输。+ 一旦TCP/IP连接建立,下一步是使用TPKT协议。如果直接去尝试发送别的信息,就会被PLC踢出去。+ 接下来是COTP协议。COTP协议用于建立和管理连接,它为应用层提供了一种连接导向的通信方式。在COTP外包一层TPKT,这样就可以发送给PLC,完成协议的确认。+ 最后,我们到达S7连接协议阶段,这是与西门子PLC通信的核心协议。从源码来看,这个是一个固定的内容
{ 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 3, 192};
。
网络上有很多写得很好的关于S7通讯协议的介绍和分析,这里就不做复读机啦。
完成上述操作之后,就开启了新世界的大门,在PLC的数据海洋里自由荡漾。
1.3 S7 Net Plus源码赏析
1.3.1 声明对象
/// <summary>
/// 创建一个具备连接所需参数的 PLC 对象。
/// 对于 S7-1200 和 S7-1500,默认值为 rack = 0 和 slot = 0。
/// 如果要连接到外部以太网卡 (CP),则需要 slot > 0。
/// 对于 S7-300 和 S7-400,默认值为 rack = 0 和 slot = 2。
/// </summary>
/// <param name="cpu">PLC 的 CpuType(从枚举中选择)</param>
/// <param name="ip">PLC 的 IP 地址</param>
/// <param name="rack">PLC 的机架号,通常为 0,但请在 Step7 或 TIA Portal 的硬件配置中进行检查</param>
/// <param name="slot">PLC 的 CPU 插槽号,对于 S7-1200 和 S7-1500 通常为 0,对于 S7-300 和 S7-400 通常为 2。
/// 如果使用外部以太网卡,必须相应地设置。</param>
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
: this(cpu, ip, DefaultPort, rack, slot)
{
}
/// <summary>
/// 创建一个具备连接所需参数的 PLC 对象。
/// 对于 S7-1200 和 S7-1500,默认值为 rack = 0 和 slot = 0。
/// 如果要连接到外部以太网卡 (CP),则需要 slot > 0。
/// 对于 S7-300 和 S7-400,默认值为 rack = 0 和 slot = 2。
/// </summary>
/// <param name="cpu">PLC 的 CpuType(从枚举中选择)</param>
/// <param name="ip">PLC 的 IP 地址</param>
/// <param name="port">用于连接的端口号,默认为 102。</param>
/// <param name="rack">PLC 的机架号,通常为 0,但请在 Step7 或 TIA Portal 的硬件配置中进行检查</param>
/// <param name="slot">PLC 的 CPU 插槽号,对于 S7-1200 和 S7-1500 通常为 0,对于 S7-300 和 S7-400 通常为 2。
/// 如果使用外部以太网卡,必须相应地设置。</param>
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
: this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
{
if (!Enum.IsDefined(typeof(CpuType), cpu))
throw new ArgumentException(
$"参数 '{nameof(cpu)}' 的值 ({cpu}) 对于枚举类型 '{typeof(CpuType).Name}' 无效。",
nameof(cpu));
CPU = cpu;
Rack = rack;
Slot = slot;
}
1.3.2 建立连接TcpIp连接
同步方法:
/// <summary>
/// 连接到 PLC 并执行 COTP 连接请求和 S7 通信设置。
/// </summary>
public void Open()
{
try
{
OpenAsync().GetAwaiter().GetResult();
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ConnectionError,
$"无法建立与 {IP} 的连接。\n消息:{exc.Message}", exc);
}
}
其中,同步方法会调用异步方法。
/// <summary>
/// 连接到 PLC 并执行 COTP 连接请求和 S7 通信设置。
/// </summary>
/// <param name="cancellationToken">用于监视取消请求的令牌。默认值为 None。
/// 请注意,取消不会以任何方式影响打开套接字,只会在成功建立套接字连接后影响用于配置连接的数据传输。
/// 请注意,取消是建议性/协作性的,不会在所有情况下立即导致取消。</param>
/// <returns>表示异步打开操作的任务。</returns>
public async Task OpenAsync(CancellationToken cancellationToken = default)
{
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
try
{
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch (Exception)
{
stream.Dispose();
throw;
}
}
ConnectAsync
对应TcpIp连接方法:
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
{
tcpClient = new TcpClient();
ConfigureConnection();
#if NET5_0_OR_GREATER
await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
#else
await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
#endif
return tcpClient.GetStream();
}
.Net5 以上会调用await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
。 .Net5 以下会调用await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
。
1.3.3 通讯协议交互
OpenAsync
中EstablishConnection
就是建立TPKT协议、COTP协议和S7连接协议三个通讯握手的阶段。 其中,RequestConnection
对应TPKT协议、COTP协议,SetupConnection
对应S7连接协议。
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
{
// 发起TPKT和COTP连接请求
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
// 设置S7连接协议
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
1.3.3.1 TPKT协议和COTP协议
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
// 获取COTP连接请求数据
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
// 发送请求并等待响应
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
// 检查响应是否为连接确认类型
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{
throw new InvalidDataException("连接请求被拒绝", response.TPkt.Data, 1, 0x0d);
}
}
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
{
// 构建COTP连接请求数据
byte[] bSend1 = {
3, 0, 0, 22, // TPKT
17, // COTP 头部长度
224, // 连接请求
0, 0, // 目标参考
0, 46, // 源参考
0, // 标志位
193, // 参数代码 (源 TASP)
2, // 参数长度
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, // 源 TASP
194, // 参数代码 (目标 TASP)
2, // 参数长度
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, // 目标 TASP
192, // 参数代码 (TPDU 大小)
1, // 参数长度
10 // TPDU 大小 (2^10 = 1024)
};
return bSend1;
}
1.3.3.2 S7连接协议
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
{
// 获取S7连接设置数据
var setupData = GetS7ConnectionSetup();
// 发送设置数据并等待响应
var s7data = await NoLockRequestTsduAsync