GKMLT通讯工具箱(WPF MVVM) - 03-西门子S7通讯

目录

  • [1. 通讯协议概述](#1. 通讯协议概述)
  • [2. 协议栈结构](#2. 协议栈结构)
  • [3. 报文格式详解](#3. 报文格式详解)
  • [4. 通讯原理](#4. 通讯原理)
  • [5. 调用方法与API](#5. 调用方法与API)
  • [6. 实际应用示例](#6. 实际应用示例)
  • [7. 故障排除](#7. 故障排除)

1. 通讯协议概述

1.1 西门子S7通讯简介

西门子S7通讯协议是用于与西门子S7系列PLC(包括S7-200/300/400/1200/1500)进行数据交换的专有通讯协议。该协议基于TCP/IP网络,实现了可靠的数据传输和高效的PLC数据访问。

1.2 主要特点

  • 基于TCP/IP:利用标准以太网进行通讯
  • 客户端/服务器模式:PC作为客户端主动连接PLC
  • 支持多种数据类型:位、字节、字、双字、浮点数等
  • 高效的数据访问:支持批量读写操作
  • 多CPU类型支持:兼容不同型号的西门子PLC

2. 协议栈结构

2.1 分层架构

复制代码
┌─────────────────────────────────────┐
│   应用层 (Application Layer)         │
│   S7 Protocol (读/写请求与响应)      │
├─────────────────────────────────────┤
│   表示层 (Presentation Layer)        │
│   COTP (Connection Oriented Transport) │
├─────────────────────────────────────┤
│   会话层 (Session Layer)             │
│   TPKT (Transport Packet)            │
├─────────────────────────────────────┤
│   传输层 (Transport Layer)           │
│   TCP (Transmission Control Protocol)│
├─────────────────────────────────────┤
│   网络层 (Network Layer)             │
│   IP (Internet Protocol)             │
└─────────────────────────────────────┘

2.2 各层功能说明

2.2.1 TPKT层 (Transport Packet)
  • 功能:提供数据包长度信息和版本标识
  • 格式:固定4字节头部 + 数据部分
  • 作用:确保数据完整传输
2.2.2 COTP层 (Connection Oriented Transport)
  • 功能:建立和管理面向连接的通讯
  • 协议:ISO 8073 (COTP)
  • 作用:处理连接建立、数据传输和连接释放
2.2.3 S7协议层
  • 功能:定义具体的PLC操作指令
  • 操作类型:读、写、控制等
  • 作用:实现PLC数据访问

3. 报文格式详解

3.1 完整报文结构

复制代码
┌──────────────┬──────────────┬───────────────────┬──────────────┐
│  TPKT Header │  COTP Header │  S7 Protocol Data │   Data       │
│   (4 bytes)  │   (变长)      │    (变长)          │  (变长)       │
└──────────────┴──────────────┴───────────────────┴──────────────┘

3.2 TPKT报文头格式

cpp 复制代码
struct TPKT_Header {
    uint8_t  version;     // 版本号,通常为0x03
    uint8_t  reserved;    // 保留字段,通常为0x00
    uint16_t length;      // 整个报文长度(包括头本身)
};

示例:

复制代码
03 00 00 21    // 版本=3,长度=33字节

3.3 COTP报文头格式

3.3.1 连接请求 (CR - Connection Request)
cpp 复制代码
struct COTP_CR {
    uint8_t  length;           // COTP参数长度
    uint8_t  pdu_type;         // PDU类型:0x0E (连接请求)
    uint8_t  dst_ref;          // 目标引用(通常0x00)
    uint8_t  src_ref;          // 源引用
    uint8_t  class_option;     // 类选项
    uint8_t  ext_format;       // 扩展格式
};

典型连接请求:

复制代码
11 0E 00 00 00 01 00 C0 01 0A C1 02 01 00 C2 01 02
3.3.2 连接确认 (CC - Connection Confirm)
cpp 复制代码
struct COTP_CC {
    uint8_t  length;           // COTP参数长度
    uint8_t  pdu_type;         // PDU类型:0x0D (连接确认)
    uint8_t  dst_ref;          // 目标引用
    uint8_t  src_ref;          // 源引用
    // ... 其他参数
};
3.3.3 数据传输 (DT - Data Transfer)
cpp 复制代码
struct COTP_DT {
    uint8_t  length;           // 长度:0x02
    uint8_t  pdu_type;         // PDU类型:0x03 (数据传输)
    uint8_t  eot;              // 传输结束标志:0x80
};

3.4 S7协议报文格式

3.4.1 S7报文头
cpp 复制代码
struct S7_Header {
    uint8_t  pdu_id;           // PDU标识:0x32 (S7协议)
    uint8_t  reserved1;        // 保留字段
    uint16_t length;           // S7数据长度
    uint8_t  reserved2;        // 保留字段:0x00
    uint8_t  function;         // 功能码
};

功能码定义:

  • 0x04 - 读取变量 (Read Variable)
  • 0x05 - 写入变量 (Write Variable)
  • 0x1A - 请求下载 (Request Download)
3.4.2 读取请求报文
cpp 复制代码
struct S7_Read_Request {
    S7_Header    header;       // S7头
    uint8_t      item_count;   // 读取项数量
    S7_Item      items[];      // 读取项列表
};

struct S7_Item {
    uint8_t      spec;         // 规范标识:0x12
    uint8_t      length;       // 参数长度
    uint8_t      syntax_id;    // 语法ID
    uint16_t     area;         // 内存区域
    uint16_t     db_number;    // DB编号(如使用DB)
    uint16_t     start;        // 起始地址
    uint16_t     count;        // 读取数量
    uint16_t     data_size;    // 数据大小
};

实际读取报文示例(读取DB1.DBB0开始的10字节):

复制代码
// TPKT Header
03 00 00 1F

// COTP DT Header  
02 F0 80

// S7 Read Request
32                    // PDU ID: 0x32
01 00 00              // Length: 0x0001
00                    // Reserved: 0x00
04 01                 // Function: 0x04 (Read), Items: 1

// S7 Item (读取DB1.DBW0开始的5个字)
12 0A 10 02 00 01     // Spec=0x12, Length=10, SyntaxID=0x10
00 01 00 00          // Area=0x00(DataBlock), DB=1, Start=0
00 03 00 04          // Word access, 5 items, 2 bytes each
3.4.3 读取响应报文
cpp 复制代码
struct S7_Read_Response {
    S7_Header    header;       // S7头
    uint8_t      item_count;   // 响应项数量
    uint8_t      data_len[];   // 数据长度
    uint8_t      data[];       // 实际数据
};

实际读取响应示例:

复制代码
// TPKT Header
03 00 00 23

// COTP DT Header
02 F0 80

// S7 Read Response
32 03 00 00           // PDU ID, Length, Reserved
01 00 00 01           // Items: 1, Return code: 0x00 (Success)
0A 00                // Data length: 10 bytes
41 42 43 44 45 46 47 48 49 4A  // Actual data (ASCII: "ABCDEFGHIJ")

4. 通讯原理

4.1 连接建立过程

复制代码
Client (PC)                    Server (PLC)
    │                               │
    │─────── TCP SYN ──────────────>│
    │<────── TCP SYN+ACK ───────────│
    │─────── TCP ACK ──────────────>│
    │                               │
    │───── COTP CR (Connect Request)│
    │                             │
    │<─── COTP CC (Connect Confirm)│
    │                               │
    │        连接已建立             │

4.2 数据读取流程

复制代码
Application    │    SiemensS7 Class    │    Network    │    PLC
    │                │                    │              │
    │ ReadByteArray  │                    │              │
    │──────────────>│                    │              │
    │                │ Build S7 Request   │              │
    │                │───────────────────>│              │
    │                │                    │  TCP/IP      │
    │                │                    │─────────────>│
    │                │                    │              │ Process
    │                │                    │<─────────────│ Response
    │                │<───────────────────│              │
    │                │ Parse Response     │              │
    │<──────────────│                    │              │
    │   Data         │                    │              │

4.3 地址解析机制

4.3.1 S7地址格式
复制代码
[DB编号.][区域代码][地址][.位号]

示例:
DB1.DBW0      - DB1的字地址0
M10.0         - 位存储器M10.0
I0.0          - 输入位I0.0
QW0           - 输出字QW0
4.3.2 内存区域编码
csharp 复制代码
public enum StoreType
{
    DataBlock = 0x00,    // DB块
    Timer     = 0x01,    // 定时器
    Counter   = 0x02,    // 计数器
    Memory    = 0x03,    // 位存储器(M)
    Input     = 0x04,    // 输入(I)
    Output    = 0x05     // 输出(Q)
}
4.3.3 地址解析算法
csharp 复制代码
// 伪代码示例
function ParseAddress(addressString):
    // 解析 "DB1.DBW0" 格式
    if addressString contains "DB":
        dbNumber = extract number before first "."
        area = DataBlock
        offset = extract number after "."
    
    // 解析 "M10.0" 格式  
    if addressString matches "[A-Z]\d+\.\d+":
        area = determineArea(letter)
        wordOffset = number before "."
        bitOffset = number after "."
        
    return { dbNumber, area, offset, bitOffset }

5. 调用方法与API

5.1 SiemensS7类主要属性

csharp 复制代码
public class SiemensS7
{
    // 连接参数
    public string IPAddress { get; set; }      // PLC IP地址
    public int Port { get; set; }              // 通讯端口,默认102
    public CpuType CpuType { get; set; }       // CPU类型
    public short Rack { get; set; }            // 机架号
    public short Slot { get; set; }            // 插槽号
    
    // 超时设置
    public int ConnectTimeOut { get; set; }    // 连接超时
    public int ReceiveTimeOut { get; set; }    // 接收超时
}

5.2 连接管理API

5.2.1 建立连接
csharp 复制代码
// 创建实例
SiemensS7 plc = new SiemensS7();

// 设置参数
plc.IPAddress = "192.168.1.100";
plc.Port = 102;
plc.CpuType = CpuType.S71200;
plc.Rack = 0;
plc.Slot = 0;

// 建立连接
OperateResult result = plc.Connect();
if (result.IsSuccess)
{
    // 连接成功
}
else
{
    // 连接失败,检查result.Message
}
5.2.2 断开连接
csharp 复制代码
plc.DisConnect();

5.3 数据读取API

5.3.1 按地址读取字节数组
csharp 复制代码
// 读取DB1.DBW0开始的10字节
OperateResult<byte[]> result = plc.ReadByteArray(
    StoreType.DataBlock,  // 存储区域
    1,                    // DB编号
    0,                    // 起始地址
    10                    // 读取长度
);

if (result.IsSuccess)
{
    byte[] data = result.Content;
    // 处理数据
}
5.3.2 按变量地址读取
csharp 复制代码
// 读取变量 "DB1.DBW0"
OperateResult<object> result = plc.ReadVariable("DB1.DBW0");

if (result.IsSuccess)
{
    object value = result.Content;
    // value 可能是 int, short, float, bool 等类型
}

5.4 数据写入API

5.4.1 按变量地址写入
csharp 复制代码
// 写入整数到 "DB1.DBW0"
int value = 12345;
OperateResult result = plc.WriteVariable("DB1.DBW0", value);

if (result.IsSuccess)
{
    // 写入成功
}

// 写入浮点数
float floatValue = 123.45f;
result = plc.WriteVariable("DB1.DBD4", floatValue);

6. 实际应用示例

6.1 基础读写操作

csharp 复制代码
using NET8_CommModbusMcS7Lib;

class S7CommunicationExample
{
    SiemensS7 plc = new SiemensS7();
    
    // 初始化连接
    public bool Initialize()
    {
        plc.IPAddress = "192.168.1.100";
        plc.Port = 102;
        plc.CpuType = CpuType.S71200;
        plc.Rack = 0;
        plc.Slot = 0;
        
        var result = plc.Connect();
        return result.IsSuccess;
    }
    
    // 批量读取数据
    public byte[] ReadProductionData()
    {
        // 读取DB100.DBB0开始的100字节
        var result = plc.ReadByteArray(StoreType.DataBlock, 100, 0, 100);
        
        if (result.IsSuccess)
        {
            return result.Content;
        }
        else
        {
            // 错误处理
            Console.WriteLine($"读取失败: {result.Message}");
            return null;
        }
    }
    
    // 读取生产参数
    public int ReadParameter()
    {
        // 读取DB1.DBD10的整数
        var result = plc.ReadVariable("DB1.DBD10");
        
        if (result.IsSuccess && result.Content is int value)
        {
            return value;
        }
        return 0;
    }
    
    // 写入控制指令
    public bool WriteControlCommand(bool startCommand)
    {
        // 写入位信号
        var result = plc.WriteVariable("M0.0", startCommand);
        return result.IsSuccess;
    }
}

6.2 实际应用:数据采集系统

csharp 复制代码
public class DataAcquisitionSystem
{
    private SiemensS7 plc;
    private bool isRunning;
    
    public bool StartAcquisition()
    {
        plc = new SiemensS7();
        plc.IPAddress = "192.168.1.100";
        plc.CpuType = CpuType.S71200;
        
        if (!plc.Connect().IsSuccess)
        {
            return false;
        }
        
        isRunning = true;
        Task.Run(() => AcquisitionLoop());
        return true;
    }
    
    private void AcquisitionLoop()
    {
        while (isRunning)
        {
            try
            {
                // 读取生产数据
                var productionData = ReadProductionData();
                
                // 读取设备状态
                var deviceStatus = ReadDeviceStatus();
                
                // 处理数据
                ProcessData(productionData, deviceStatus);
                
                Thread.Sleep(1000); // 1秒采集周期
            }
            catch (Exception ex)
            {
                Console.WriteLine($"采集异常: {ex.Message}");
            }
        }
    }
    
    private ProductionData ReadProductionData()
    {
        // 读取DB100的数据块
        var rawData = plc.ReadByteArray(StoreType.DataBlock, 100, 0, 50);
        
        if (rawData.IsSuccess)
        {
            byte[] data = rawData.Content;
            
            return new ProductionData
            {
                Temperature = BitConverter.ToInt16(data, 0) / 10.0f,
                Pressure = BitConverter.ToInt16(data, 2) / 100.0f,
                FlowRate = BitConverter.ToInt32(data, 4),
                ProductCount = BitConverter.ToUInt32(data, 8)
            };
        }
        
        return null;
    }
    
    private DeviceStatus ReadDeviceStatus()
    {
        // 读取状态位
        var statusResult = plc.ReadVariable("M0.0");
        var alarmResult = plc.ReadVariable("M0.1");
        
        return new DeviceStatus
        {
            IsRunning = (bool)statusResult.Content,
            HasAlarm = (bool)alarmResult.Content
        };
    }
    
    public void StopAcquisition()
    {
        isRunning = false;
        plc?.DisConnect();
    }
}

// 数据模型
public class ProductionData
{
    public float Temperature { get; set; }
    public float Pressure { get; set; }
    public int FlowRate { get; set; }
    public uint ProductCount { get; set; }
}

public class DeviceStatus
{
    public bool IsRunning { get; set; }
    public bool HasAlarm { get; set; }
}

6.3 高级应用:数据解析

csharp 复制代码
public class S7DataParser
{
    // 解析复杂数据结构
    public RecipeData ParseRecipe(byte[] rawData)
    {
        return new RecipeData
        {
            RecipeId = BitConverter.ToUInt16(rawData, 0),
            RecipeName = Encoding.ASCII.GetString(rawData, 2, 20).TrimEnd('\0'),
            Temperature = BitConverter.ToInt16(rawData, 22) / 10.0f,
            Pressure = BitConverter.ToInt16(rawData, 24) / 100.0f,
            Speed = BitConverter.ToUInt16(rawData, 26),
            Duration = BitConverter.ToUInt16(rawData, 28),
            Enabled = rawData[30] != 0
        };
    }
    
    // 构建写入数据
    public byte[] BuildRecipeData(RecipeData recipe)
    {
        byte[] data = new byte[32];
        
        BitConverter.GetBytes((ushort)recipe.RecipeId).CopyTo(data, 0);
        Encoding.ASCII.GetBytes(recipe.RecipeName.PadRight(20, '\0')).CopyTo(data, 2);
        BitConverter.GetBytes((short)(recipe.Temperature * 10)).CopyTo(data, 22);
        BitConverter.GetBytes((short)(recipe.Pressure * 100)).CopyTo(data, 24);
        BitConverter.GetBytes((ushort)recipe.Speed).CopyTo(data, 26);
        BitConverter.GetBytes((ushort)recipe.Duration).CopyTo(data, 28);
        data[30] = (byte)(recipe.Enabled ? 1 : 0);
        
        return data;
    }
}

public class RecipeData
{
    public ushort RecipeId { get; set; }
    public string RecipeName { get; set; }
    public float Temperature { get; set; }
    public float Pressure { get; set; }
    public ushort Speed { get; set; }
    public ushort Duration { get; set; }
    public bool Enabled { get; set; }
}

7. 故障排除

7.1 常见问题与解决方案

7.1.1 连接失败

症状 :Connect()方法返回失败
可能原因

  • IP地址错误
  • PLC未开机
  • 网络不通
  • CPU类型或Rack/Slot参数错误

解决方案

csharp 复制代码
// 1. 检查网络连接
Ping ping = new Ping();
var reply = ping.Send("192.168.1.100");
if (reply.Status != IPStatus.Success)
{
    Console.WriteLine("网络不通,请检查IP地址和网络连接");
}

// 2. 验证PLC参数
// S7-1200/1500: Rack=0, Slot=0或1
// S7-300/400: 根据实际配置设置

// 3. 尝试不同的CPU类型
foreach (var cpuType in new[] { CpuType.S71200, CpuType.S71500 })
{
    plc.CpuType = cpuType;
    if (plc.Connect().IsSuccess)
    {
        Console.WriteLine($"连接成功,CPU类型: {cpuType}");
        break;
    }
}
7.1.2 读取数据错误

症状 :ReadByteArray()返回失败或数据异常
可能原因

  • DB块不存在
  • 地址超出范围
  • PLC处于STOP模式

解决方案

csharp 复制代码
// 安全读取函数
public byte[] SafeReadData(int dbNumber, int startAddress, int length)
{
    // 1. 验证参数
    if (dbNumber < 1 || dbNumber > 99999)
    {
        Console.WriteLine("DB编号超出范围");
        return null;
    }
    
    if (length < 1 || length > 65535)
    {
        Console.WriteLine("读取长度超出范围");
        return null;
    }
    
    // 2. 分批读取大数据
    const int maxBatchSize = 200;
    List<byte> allData = new List<byte>();
    
    for (int offset = 0; offset < length; offset += maxBatchSize)
    {
        int currentBatch = Math.Min(maxBatchSize, length - offset);
        var result = plc.ReadByteArray(StoreType.DataBlock, 
                                      dbNumber, 
                                      startAddress + offset, 
                                      currentBatch);
        
        if (!result.IsSuccess)
        {
            Console.WriteLine($"读取失败: {result.Message}");
            return null;
        }
        
        allData.AddRange(result.Content);
    }
    
    return allData.ToArray();
}
7.1.3 数据类型转换错误

症状 :读取的数值与PLC显示不一致
可能原因

  • 字节序问题
  • 数据类型不匹配
  • 缩放因子未考虑

解决方案

csharp 复制代码
// 数据类型转换工具类
public class S7DataTypeConverter
{
    // 考虑缩放因子的整数读取
    public static float ReadScaledValue(byte[] data, int offset, float scale)
    {
        short rawValue = BitConverter.ToInt16(data, offset);
        return rawValue / scale;
    }
    
    // 处理大端序数据
    public static int ReadBigEndianInt32(byte[] data, int offset)
    {
        return (data[offset] << 24) | 
               (data[offset + 1] << 16) | 
               (data[offset + 2] << 8) | 
               data[offset + 3];
    }
    
    // S7日期时间转换
    public static DateTime ReadS7DateTime(byte[] data, int offset)
    {
        // S7日期格式:BCD编码
        int year = BcdToDec(data[offset]);
        int month = BcdToDec(data[offset + 1]);
        int day = BcdToDec(data[offset + 2]);
        // ... 其他字段
        
        return new DateTime(year, month, day);
    }
    
    private static int BcdToDec(byte bcd)
    {
        return ((bcd >> 4) * 10) + (bcd & 0x0F);
    }
}

7.2 性能优化

7.2.1 批量操作优化
csharp 复制代码
// 不好的做法:多次单独读取
for (int i = 0; i < 100; i++)
{
    var result = plc.ReadByteArray(StoreType.DataBlock, 1, i * 2, 2);
}

// 好的做法:一次批量读取
var result = plc.ReadByteArray(StoreType.DataBlock, 1, 0, 200);
7.2.2 连接池管理
csharp 复制代码
public class S7ConnectionPool
{
    private Dictionary<string, SiemensS7> connections = new Dictionary<string, SiemensS7>();
    
    public SiemensS7 GetConnection(string ipAddress)
    {
        if (!connections.ContainsKey(ipAddress))
        {
            var plc = new SiemensS7();
            plc.IPAddress = ipAddress;
            plc.Connect();
            connections[ipAddress] = plc;
        }
        
        return connections[ipAddress];
    }
    
    public void CloseAllConnections()
    {
        foreach (var plc in connections.Values)
        {
            plc.DisConnect();
        }
        connections.Clear();
    }
}

8. 总结

西门子S7通讯协议是一个结构完整、功能强大的工业通讯协议。通过理解其分层协议栈和报文格式,开发者可以实现稳定可靠的PLC数据交换。

关键要点:

  1. 协议分层:TPKT → COTP → S7,每层都有明确的功能
  2. 报文格式:严格按照规范构造报文头和数据部分
  3. 地址解析:正确理解S7地址格式和内存区域编码
  4. 错误处理:完善的异常处理和重试机制
  5. 性能优化:合理使用批量操作和连接管理
相关推荐
雨浓YN7 小时前
GKMLT通讯工具箱(WPF MVVM) - 05-WebAPI通讯
wpf
软泡芙1 天前
【WPF 】MVVM 设计模式在 WPF 中的实战应用
设计模式·wpf
张小俊_1 天前
WPF 跨线程 UI 更新与硬编码赋值引发的 Bug 排查
c#·bug·wpf
七夜zippoe2 天前
DolphinDB在工业物联网中的优势
物联网·wpf·工业物联网·优势·dolphindb
heimeiyingwang2 天前
【架构实战】观察者模式在分布式系统中的应用
观察者模式·架构·wpf
bugcome_com2 天前
WPF + Microsoft.ToolKit.Mvvm 技术指南与实战项目
microsoft·wpf
武藤一雄3 天前
WPF中逻辑树(Logical Tree)与可视化树(Visual Tree)到底是什么
microsoft·c#·.net·wpf·.netcore
炸炸鱼.3 天前
ELK 企业级日志分析系统完整部署手册
elk·wpf
Mr_pyx4 天前
微服务可观测性实战:分布式链路追踪从入门到精通
wpf