基于C#的S7-1200上位机通讯测试

采用S7.NET库开发S7-1200上位机软件。软件采用VS2022。具体开发流程如下.

1. S7.NET介绍:

How to download s7.Net
The official repository is on GitHub (https://github.com/killnine/s7netplus), you can also download the library directly from NuGet (https://www.nuget.org/packages/S7netplus/).
What is S7.Net
S7.Net is a plc driver that works only with Siemens PLC and only with Ethernet connection. This means that your plc must have a Profinet CPU or a profinet external card (CPxxx card).
S7.Net is written entirely in C#, so you can debug it easily without having to go through native dlls.
Supported PLC
S7.Net is compatible with S7-200, S7-300, S7-400, S7-1200, S7-1500.

2. VS创建C#窗体项目:

(1)创建项目:

注意:创建过程中,选取自己需要的框架,这里选取.NET 8.0。

(2)添加S7.NET的NuGet程序包。
右键->管理NuGet程序包

浏览S7net, 选择S7netplus,版本选择最新的0.20.0,点击安装。

等待安装结束。

(3)编辑Form1界面,添加需要的控件,如显示窗口TextBox,Label,Button等。

根据自己需求设计好Form1,

(4)编写代码

program.cs //一般不需要修改,可以创建全局类、全局变量等。

Form1.cs //主要集中在这里面。

Form1.Designer.cs //一般不需要修改。

FromS7.cs、NumberFromString.cs //包含在Types文件夹中,是我自己创建的数据处理的方法、类等的命名空间。

以下是各个源码。

Program.cs

//Program.cs
namespace S7ComTest
{
    public static class Global
    {
            public static int Result = 0;
            public static bool Connecting = false;
    }
    public class Db1Read : ICloneable
    {
        public bool Bool1 { get; set; }
        public bool Bool2 { get; set; }
        public bool Bool3 { get; set; }
        public bool Bool4 { get; set; }
        public byte Byte1 { get; set; }
        public byte Byte2 { get; set; }
        public short Int { get; set; }
        public int Dint { get; set; }
        public float Real { get; set; }
        public string String { get; set; } = string.Empty;
        public ushort Word { get; set; }

        public object Clone()
        {
            return new Db1Read
            {
                Bool1 = this.Bool1,
                Bool2 = this.Bool2,
                Bool3 = this.Bool3,
                Bool4 = this.Bool4,
                Byte1 = this.Byte1,
                Byte2 = this.Byte2,
                Int = this.Int,
                Dint = this.Dint,
                Real = this.Real,
                String = this.String,
                Word = this.Word
            };
        }
    }

    public class Db1Write : ICloneable
    {
        public bool Bool1 { get; set; }
        public bool Bool2 { get; set; }
        public bool Bool3 { get; set; }
        public bool Bool4 { get; set; }
        public byte Byte1 { get; set; }
        public byte Byte2 { get; set; }
        public short Int { get; set; }
        public int Dint { get; set; }
        public float Real { get; set; }
        public string String { get; set; } = string.Empty;
        public ushort Word { get; set; }

        public object Clone()
        {
            return new Db1Write
            {
                Bool1 = this.Bool1,
                Bool2 = this.Bool2,
                Bool3 = this.Bool3,
                Bool4 = this.Bool4,
                Byte1 = this.Byte1,
                Byte2 = this.Byte2,
                Int = this.Int,
                Dint = this.Dint,
                Real = this.Real,
                String = this.String,
                Word = this.Word
            };
        }
    }

    public partial class Form1 : Form
    {
        internal static class Program
        {
            /// <summary>
            ///  The main entry point for the application.
            /// </summary>
            [STAThread]
            static void Main()
            {
                // To customize application configuration such as set high DPI settings or default font,
                // see https://aka.ms/applicationconfiguration.
                ApplicationConfiguration.Initialize();
                Application.Run(new Form1());
            }
        }
    }
}

Form1.cs

//Form1.cs
using S7.Net;//S7通讯,需要开启西门子PUT/GET
using S7.Net.Types;//数据转换函数包含其中
using S7ComTest.Types;
using System.Diagnostics;
namespace S7ComTest
{

    public partial class Form1 : Form
    {
        
        private Plc plc;//
        private System.Windows.Forms.Timer refreshTimer;//定义一个新的刷新计时器
        private Db1Read db1Read = new Db1Read();
        private Db1Write lastDb1Write = new Db1Write(); // 缓存上次写入区值
        private Db1Read lastDb1Read = new Db1Read(); // 缓存上次读取区值
        private Db1Write db1Write = new Db1Write();
        //private Db1Write lastWriteValues = new Db1Write();
        private bool isFirstRead = true;
        private bool _isPLCUpdate = false;
        private CancellationTokenSource? debounceTokenSource;
        private Dictionary<string, string> pendingWrites = new Dictionary<string, string>();//定义字典,包含文本框名字和实际string型值
        //private Dictionary<string, bool> userModfiedFlags = new Dictionary<string, bool>();//定义字典,包含文本框名字,和是否被修改标志
        private Dictionary<string, bool> userModfiedFlags = new Dictionary<string, bool>();
        private Dictionary<string, bool> isEditingFlags = new Dictionary<string, bool>();
        private List<string> readOnlyTextBoxNames = new List<string>();//只读取文本框名称列表
        private List<string> readWriteTextBoxNames = new List<string>();//读写区文本框名称列表
        private readonly object _lockObject = new object(); // 用于线程同步的锁对象
        private Dictionary<string, object> lastPLCValues = new Dictionary<string, object>()
            {
        { nameof(textBox14), false },
        { nameof(textBox15), false },
        { nameof(textBox16), false },
        { nameof(textBox17), false },
        { nameof(textBox18), (byte)0 },
        { nameof(textBox19), (byte)0 },
        { nameof(textBox20), (short)0 },
        { nameof(textBox21), 0 },
        { nameof(textBox22), 0f },
        { nameof(textBox23), string.Empty },
        { nameof(textBox24), (ushort)0 }
    };

        public Form1()
        {
            //从这里运行
            InitializeComponent();
            plc = new Plc(CpuType.S71200, "192.168.10.20", 0, 1);//创建PLC连接接口

            InitializeTextBoxLists();//初始化文本框列表
            InitializeUserModfiedFlags();//初始化用户修改标示
            InitializeEditingFlags();
            SetupReadWriteTextBoxEvents();//为读写文本框添加事件处理

            refreshTimer = new System.Windows.Forms.Timer { Interval = 1000 };//创建刷新计时器,设定时间为1000ms
            refreshTimer.Tick += RefreshTimer_Tick;

        }

        // 修改更新方法
        private void UpdateLastPLCValues()
        {
            lastPLCValues[nameof(textBox14)] = db1Write.Bool1;
            lastPLCValues[nameof(textBox15)] = db1Write.Bool2;
            lastPLCValues[nameof(textBox16)] = db1Write.Bool3;
            lastPLCValues[nameof(textBox17)] = db1Write.Bool4;
            lastPLCValues[nameof(textBox18)] = db1Write.Byte1;
            lastPLCValues[nameof(textBox19)] = db1Write.Byte2;
            lastPLCValues[nameof(textBox20)] = db1Write.Int;
            lastPLCValues[nameof(textBox21)] = db1Write.Dint;
            lastPLCValues[nameof(textBox22)] = db1Write.Real;
            lastPLCValues[nameof(textBox23)] = db1Write.String;
            lastPLCValues[nameof(textBox24)] = db1Write.Word;
        }

        private void InitializeTextBoxLists()
        {
            //设置只读取文本框
            readOnlyTextBoxNames.AddRange(new[] {
                nameof(textBox1), nameof(textBox2), nameof(textBox3), nameof(textBox4),
                nameof(textBox5), nameof(textBox6), nameof(textBox7), nameof(textBox8),
                nameof(textBox9), nameof(textBox10), nameof(textBox11), nameof(textBox12),
                nameof(textBox13)
            });

            //设置读写区文本框
            readWriteTextBoxNames.AddRange(new[] {
                nameof(textBox14), nameof(textBox15), nameof(textBox16), nameof(textBox17),
                nameof(textBox18), nameof(textBox19), nameof(textBox20), nameof(textBox21),
                nameof(textBox22), nameof(textBox23), nameof(textBox24)
            });
        }

        private void InitializeUserModfiedFlags()
        {
            //为所有读写区域文本框初始化用户修改标志
            foreach (var textBoxName in readWriteTextBoxNames)
            {
                userModfiedFlags[textBoxName] = false;
            }
        }

        private void InitializeEditingFlags()
        {
            foreach(var textBoxName in readWriteTextBoxNames)
            {
                isEditingFlags[textBoxName] = false;
            }
        }

        private void SetupReadWriteTextBoxEvents()
        {
            //为读写区的每个TextBox添加TextChanged和LostFocus事件处理
            foreach (var textBoxName in readWriteTextBoxNames)
            {
                if (Controls.Find(textBoxName, true).FirstOrDefault() is TextBox textBox)
                {
                    textBox.TextChanged += WriteTextBox_TextChanged;
                    textBox.LostFocus += WriteTextBox_LostFocus;
                    textBox.GotFocus += WriteTextBox_GotFocus;
                }
            }
        }

        private void WriteTextBox_TextChanged(object? sender, EventArgs e)
        {
            var textBox = sender as TextBox;
            if (textBox == null) return;

            // 如果是PLC主动更新导致的文本变化,不标记为用户修改
            if (_isPLCUpdate)
                return;

            // 记录用户修改的值的标志位
            userModfiedFlags[textBox.Name] = true;

            // 使用防抖机制避免频繁触发
            Debounce(() => {
                pendingWrites[textBox.Name] = textBox.Text;
                ApplyPendingWrites();
                CheckAndWriteData();
            }, 500);

            textBox.BackColor = Color.LightYellow;
        }

        private void WriteTextBox_LostFocus(object? sender, EventArgs e)
        {
            var textBox = sender as TextBox;
            if (textBox == null) return;

            string textBoxName = textBox.Name;
            isEditingFlags[textBoxName] = false;

            // 安全获取当前值
            object? currentValue = GetValueFromText(textBoxName, textBox.Text);
            if (currentValue == null) return; // 转换失败

            // 获取最后已知PLC值
            object lastPLCValue = lastPLCValues[textBoxName];

            // 比较值是否变化
            if (!ValuesEqual(currentValue, lastPLCValue))
            {
                // 值有变化,添加到待写入
                pendingWrites[textBoxName] = textBox.Text;
                ApplyPendingWrites();
                CheckAndWriteData();
            }
            else
            {
                // 值未变化,重置修改标志
                userModfiedFlags[textBoxName] = false;
                textBox.BackColor = SystemColors.Window; // 恢复背景色
            }

        }

        private void WriteTextBox_GotFocus(object? sender, EventArgs e)
        {
            var textBox = sender as TextBox;
            if (textBox == null) return;

            isEditingFlags[textBox.Name] = true;
            userModfiedFlags[textBox.Name] = true;
        }

        private bool ValuesEqual(object value1, object value2)
        {
            // 处理浮点数特殊比较
            if (value1 is float f1 && value2 is float f2)
            {
                return Math.Abs(f1 - f2) < 0.0001;
            }

            // 处理字符串比较
            if (value1 is string s1 && value2 is string s2)
            {
                return string.Equals(s1, s2, StringComparison.Ordinal);
            }

            // 默认比较
            return Equals(value1, value2);
        }

        private void Debounce(Action action, int delayMs)
        {
            //取消之前的延时操作
            debounceTokenSource?.Cancel();
            debounceTokenSource = new CancellationTokenSource();

            //启用新的延时操作
            Task.Delay(delayMs, debounceTokenSource.Token).ContinueWith(t =>
            {
                if (!t.IsCanceled)
                {
                    Invoke(action);
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                if (plc.IsConnected)
                {
                    // 断开连接
                    refreshTimer.Stop();
                    plc.Close();
                    Global.Connecting = false;

                    // 重置所有状态
                    ResetAllStates();

                    MessageBox.Show("已断开与PLC的连接");
                }
                else
                {
                    // 建立连接
                    plc.Open();
                    Global.Connecting = plc.IsConnected;

                    if (plc.IsConnected)
                    {
                        // 重置所有状态
                        ResetAllStates();

                        refreshTimer.Start();
                        MessageBox.Show("已经成功连接PLC,开始自动刷新");
                    }
                    else
                    {
                        MessageBox.Show("连接失败,请检查PLC地址和网络");
                    }
                }
                UpdateReadOnlyUI();
                UpdateReadWriteUI();
            }
            catch (Exception ex)
            {
                MessageBox.Show($"操作出错:{ex.Message}");
                Global.Connecting = false;
                refreshTimer.Stop();
                ResetAllStates();
            }
        }

        private void ResetAllStates()
        {
            // 重置标志
            isFirstRead = true;

            // 重置用户修改标志
            foreach (var key in userModfiedFlags.Keys.ToList())
            {
                userModfiedFlags[key] = false;
            }

            // 重置编辑状态标志
            foreach (var key in isEditingFlags.Keys.ToList())
            {
                isEditingFlags[key] = false;
            }

            // 清空待写入队列
            pendingWrites.Clear();
            debounceTokenSource?.Cancel();

            // 重置背景色
            foreach (var textBoxName in readWriteTextBoxNames)
            {
                if (Controls.Find(textBoxName, true).FirstOrDefault() is TextBox textBox)
                {
                    textBox.BackColor = SystemColors.Window;
                }
            }

            // 重置缓存
            lastPLCValues[nameof(textBox14)] = false;
            lastPLCValues[nameof(textBox15)] = false;
            lastPLCValues[nameof(textBox16)] = false;
            lastPLCValues[nameof(textBox17)] = false;
            lastPLCValues[nameof(textBox18)] = (byte)0;
            lastPLCValues[nameof(textBox19)] = (byte)0;
            lastPLCValues[nameof(textBox20)] = (short)0;
            lastPLCValues[nameof(textBox21)] = 0;
            lastPLCValues[nameof(textBox22)] = 0f;
            lastPLCValues[nameof(textBox23)] = string.Empty;
            lastPLCValues[nameof(textBox24)] = (ushort)0;

            // 清空数据对象
            db1Read = new Db1Read();
            db1Write = new Db1Write();
            lastDb1Write = new Db1Write();
        }

        private async void RefreshTimer_Tick(object? sender, EventArgs e)
        {
            if (!plc.IsConnected)
            {
                refreshTimer.Stop();
                Global.Connecting = false;

                // 重置状态
                ResetAllStates();

                ClearDataDisplay();
                UpdateReadOnlyUI();
                UpdateReadWriteUI();
                return;
            }
            try
            {
                Db1Read oldDb1Read = (Db1Read)db1Read.Clone();
                Db1Write oldDb1Write = (Db1Write)db1Write.Clone();

                await ReadFromPLCAsync();
                await ReadDbWriteFromPLCAsync();

                if (isFirstRead)
                {
                    lastDb1Write = (Db1Write)db1Write.Clone();
                    UpdateLastPLCValues();
                    UpdateReadOnlyUI();
                    UpdateReadWriteUI();
                    isFirstRead = false;
                }
                else
                {
                    if (IsDb1ReadChanged(oldDb1Read))
                    {
                        UpdateReadOnlyUI();
                    }

                    if (IsDb1WriteChanged(oldDb1Write))
                    {
                        UpdateReadWriteUI();
                        lastDb1Write = (Db1Write)db1Write.Clone();
                        UpdateLastPLCValues();
                    }
                }
            }
            catch (PlcException ex)
            {
                //返回PLC读取错误,显示弹窗
                MessageBox.Show($"PLC读取错误:{ex.Message}");
                refreshTimer.Stop();
                Global.Connecting = false;
                //清空所有数据显示
                ClearDataDisplay();

                UpdateReadOnlyUI();
                UpdateReadWriteUI();
            }
            catch (Exception ex)
            {
                MessageBox.Show($"刷新出错:{ex.Message}");
                refreshTimer.Stop();
            }
        }

        private bool HasUserModifiedValue()
        {
            //检查是否有用户修改的值
            return userModfiedFlags.ContainsValue(true);
        }

        private async Task ReadFromPLCAsync()
        {
            await Task.Run(() =>
            {
                byte[] buffer = new byte[272];
                //读取PLC数据, 以字节形式批量读取
                plc.ReadBytes(buffer: buffer.AsSpan(), dataType: DataType.DataBlock, db: 1, startByteAdr: 0);

                //解析数据
                db1Read.Bool1 = Bit.FromByte(buffer[0], 0);
                db1Read.Bool2 = Bit.FromByte(buffer[0], 1);
                db1Read.Bool3 = Bit.FromByte(buffer[0], 2);
                db1Read.Bool4 = Bit.FromByte(buffer[0], 3);
                db1Read.Byte1 = buffer[1];
                db1Read.Byte2 = buffer[2];
                db1Read.Int = Int.FromByteArray(buffer.AsSpan(4, 2).ToArray());
                db1Read.Dint = DInt.FromByteArray(buffer.AsSpan(6, 4).ToArray());
                db1Read.Real = Real.FromByteArray(buffer.AsSpan(10, 4).ToArray());
                db1Read.String = S7String.FromByteArray(buffer.AsSpan(14, 256).ToArray());
                db1Read.Word = Word.FromByteArray(buffer.AsSpan(270, 2).ToArray());
            });
        }

        private async Task ReadDbWriteFromPLCAsync()
        {
            await Task.Run(() =>
            {
                byte[] buffer = new byte[272];
                //读取PLC数据, 以字节形式批量读取
                plc.ReadBytes(buffer: buffer.AsSpan(), dataType: DataType.DataBlock, db: 1, startByteAdr: 300);

                //解析数据
                db1Write.Bool1 = Bit.FromByte(buffer[0], 0);
                db1Write.Bool2 = Bit.FromByte(buffer[0], 1);
                db1Write.Bool3 = Bit.FromByte(buffer[0], 2);
                db1Write.Bool4 = Bit.FromByte(buffer[0], 3);
                db1Write.Byte1 = buffer[1];
                db1Write.Byte2 = buffer[2];
                db1Write.Int = Int.FromByteArray(buffer.AsSpan(4, 2).ToArray());
                db1Write.Dint = DInt.FromByteArray(buffer.AsSpan(6, 4).ToArray());
                db1Write.Real = Real.FromByteArray(buffer.AsSpan(10, 4).ToArray());
                db1Write.String = S7String.FromByteArray(buffer.AsSpan(14, 256).ToArray());
                db1Write.Word = Word.FromByteArray(buffer.AsSpan(270, 2).ToArray());
            });
        }

        private async Task WriteToPLCAsync()
        {
            await Task.Run(() =>
            {
                byte[] buffer = new byte[272];

                //填充数据
                FromS7.Bit.ToByteArray(db1Write.Bool1, buffer, 0, 0);
                FromS7.Bit.ToByteArray(db1Write.Bool2, buffer, 0, 1);
                FromS7.Bit.ToByteArray(db1Write.Bool3, buffer, 0, 2);
                FromS7.Bit.ToByteArray(db1Write.Bool4, buffer, 0, 3);
                buffer[1] = db1Write.Byte1;
                buffer[2] = db1Write.Byte2;
                FromS7.Int.ToByteArray(db1Write.Int, buffer.AsSpan(4));
                FromS7.Dint.ToByteArray(db1Write.Dint, buffer.AsSpan(6));
                FromS7.Real.ToByteArray(db1Write.Real, buffer.AsSpan(10));

                FromS7.S7String.ToByteArray(db1Write.String, buffer.AsSpan(), 14);
                FromS7.Word.ToByteArray(db1Write.Word, buffer.AsSpan(270));
                //执行PLC写入
                plc.WriteBytes(DataType.DataBlock, 1, 300, buffer.AsSpan());
            });
        }

        private void ApplyPendingWrites()
        {
            //将待写入的值应用到UI和DBWrite
            foreach (var entry in pendingWrites)
            {
                //if (Controls.Find(entry.Key, true).FirstOrDefault() is TextBox textBox)
                //{
                //    textBox.Text = entry.Value;

                    //根据TextBox名称更新对应的Db1Write属性
                    switch (entry.Key)
                    {
                        case nameof(textBox14):
                            db1Write.Bool1 = numberFromString.ParseBool(entry.Value);
                            break;
                        case nameof(textBox15):
                            db1Write.Bool2 = numberFromString.ParseBool(entry.Value);
                            break;
                        case nameof(textBox16):
                            db1Write.Bool3 = numberFromString.ParseBool(entry.Value);
                            break;
                        case nameof(textBox17):
                            db1Write.Bool4 = numberFromString.ParseBool(entry.Value);
                            break;
                        case nameof(textBox18):
                            db1Write.Byte1 = numberFromString.ParseByte(entry.Value);
                            break;
                        case nameof(textBox19):
                            db1Write.Byte2 = numberFromString.ParseByte(entry.Value);
                            break;
                        case nameof(textBox20):
                            db1Write.Int = numberFromString.ParseInt(entry.Value);
                            break;
                        case nameof(textBox21):
                            db1Write.Dint = numberFromString.ParseDint(entry.Value);
                            break;
                        case nameof(textBox22):
                            db1Write.Real = numberFromString.ParseReal(entry.Value);
                            break;
                        case nameof(textBox23):
                            db1Write.String = entry.Value;
                            break;
                        case nameof(textBox24):
                            db1Write.Word = numberFromString.ParseWord(entry.Value);
                            break;
                        default:
                            break;
                    }
                //}
            }
            pendingWrites.Clear();
        }

        private async void CheckAndWriteData()
        {
            if (!plc.IsConnected) return;

            try
            {
                await WriteToPLCAsync();
                UpdateStatusMessage("写入成功");

                // 写入成功后,只重置当前控件的修改标志
                foreach (var entry in pendingWrites)
                {
                    userModfiedFlags[entry.Key] = false;
                }

                // 更新PLC值缓存
                UpdateLastPLCValues();
            }
            catch (Exception ex)
            {
                UpdateStatusMessage($"写入失败:{ex.Message}");
            }
        }

        private object? GetValueFromText(string textBoxName, string text)
        {
            switch (textBoxName)
            {
                case nameof(textBox14): return numberFromString.ParseBool(text);
                case nameof(textBox15): return numberFromString.ParseBool(text);
                case nameof(textBox16): return numberFromString.ParseBool(text);
                case nameof(textBox17): return numberFromString.ParseBool(text);
                case nameof(textBox18): return numberFromString.ParseByte(text);
                case nameof(textBox19): return numberFromString.ParseByte(text);
                case nameof(textBox20): return numberFromString.ParseInt(text);
                case nameof(textBox21): return numberFromString.ParseDint(text);
                case nameof(textBox22): return numberFromString.ParseReal(text);
                case nameof(textBox23): return text;
                case nameof(textBox24): return numberFromString.ParseWord(text);
                default: return null;
            }
        }

        private bool IsDb1ReadChanged(Db1Read lastRead)
        {
            return db1Read.Bool1 != lastRead.Bool1 ||
                   db1Read.Bool2 != lastRead.Bool2 ||
                   db1Read.Bool3 != lastRead.Bool3 ||
                   db1Read.Bool4 != lastRead.Bool4 ||
                   db1Read.Byte1 != lastRead.Byte1 ||
                   db1Read.Byte2 != lastRead.Byte2 ||
                   db1Read.Int != lastRead.Int ||
                   db1Read.Dint != lastRead.Dint ||
                   db1Read.Real != lastRead.Real ||
                   db1Read.String != lastRead.String ||
                   db1Read.Word != lastRead.Word;

        }

        private bool IsDb1WriteChanged(Db1Write oldValues)
        {
            return oldValues.Bool1 != db1Write.Bool1 ||
               oldValues.Bool2 != db1Write.Bool2 ||
               oldValues.Bool3 != db1Write.Bool3 ||
               oldValues.Bool4 != db1Write.Bool4 ||
               oldValues.Byte1 != db1Write.Byte1 ||
               oldValues.Byte2 != db1Write.Byte2 ||
               oldValues.Int != db1Write.Int ||
               oldValues.Dint != db1Write.Dint ||
               Math.Abs(oldValues.Real - db1Write.Real) > 0.001 || // 浮点数比较容差
               oldValues.String != db1Write.String ||
               oldValues.Word != db1Write.Word;
        }

        private void UpdateStatusMessage(string message)
        {
            richTextBox1.AppendText($"[{System.DateTime.Now:HH:mm:ss}] {message}\n");
            richTextBox1.ScrollToCaret();
        }

        private void UpdateReadOnlyUI()
        {
            //更新只读区文本框
            textBox3.Text = db1Read.Bool1.ToString();
            textBox4.Text = db1Read.Bool2.ToString();
            textBox5.Text = db1Read.Bool3.ToString();
            textBox6.Text = db1Read.Bool4.ToString();
            textBox7.Text = "16#" + db1Read.Byte1.ToString("x2");
            textBox8.Text = "16#" + db1Read.Byte2.ToString("x2");
            textBox9.Text = db1Read.Int.ToString();
            textBox10.Text = db1Read.Dint.ToString();
            textBox11.Text = db1Read.Real.ToString();
            textBox12.Text = db1Read.String.ToString();
            textBox13.Text = "16#" + db1Read.Word.ToString("x2");

            //更新其他只读文本
            textBox1.Text = System.DateTime.Now.ToString("HH:mm:ss");
            textBox2.Text = Global.Connecting ? "已经连接" : "未连接";
            //更新按钮文本
            button1.Text = plc.IsConnected ? "断开连接" : "连接PLC";
        }

        private void UpdateReadWriteUI()
        {
            _isPLCUpdate = true;

            foreach (var textBoxName in readWriteTextBoxNames)
            {
                // 只更新未被用户修改的控件
                if (!userModfiedFlags[textBoxName] && !isEditingFlags[textBoxName])
                {
                    TextBox? textBox = Controls.Find(textBoxName, true).FirstOrDefault() as TextBox;
                    if (textBox != null)
                    {
                        // 获取当前PLC值
                        string? plcValue = GetPLCValueByTextBoxName(textBoxName);

                        // 只有当值变化时才更新UI
                        if (textBox.Text != plcValue)
                        {
                            textBox.Text = plcValue;
                        }
                    }
                }
            }

            _isPLCUpdate = false;
        }

        private string? GetPLCValueByTextBoxName(string textBoxName)
        {
            lock (_lockObject)
            {
                return textBoxName switch
                {
                    nameof(textBox14) => db1Write.Bool1.ToString(),
                    nameof(textBox15) => db1Write.Bool2.ToString(),
                    nameof(textBox16) => db1Write.Bool3.ToString(),
                    nameof(textBox17) => db1Write.Bool4.ToString(),
                    nameof(textBox18) => db1Write.Byte1.ToString(),
                    nameof(textBox19) => db1Write.Byte2.ToString(),
                    nameof(textBox20) => db1Write.Int.ToString(),
                    nameof(textBox21) => db1Write.Dint.ToString(),
                    nameof(textBox22) => db1Write.Real.ToString("0.####"), // 控制精度
                    nameof(textBox23) => db1Write.String,
                    nameof(textBox24) => db1Write.Word.ToString(),
                    _ => null
                };
            }
        }

        private void ClearDataDisplay()
        {
            // 只清除UI显示,不重置状态
            textBox3.Text = string.Empty;
            textBox4.Text = string.Empty;
            textBox5.Text = string.Empty;
            textBox6.Text = string.Empty;
            textBox7.Text = string.Empty;
            textBox8.Text = string.Empty;
            textBox9.Text = string.Empty;
            textBox10.Text = string.Empty;
            textBox11.Text = string.Empty;
            textBox12.Text = string.Empty;
            textBox13.Text = string.Empty;
            textBox14.Text = string.Empty;
            textBox15.Text = string.Empty;
            textBox16.Text = string.Empty;
            textBox17.Text = string.Empty;
            textBox18.Text = string.Empty;
            textBox19.Text = string.Empty;
            textBox20.Text = string.Empty;
            textBox21.Text = string.Empty;
            textBox22.Text = string.Empty;
            textBox23.Text = string.Empty;
            textBox24.Text = string.Empty;

            richTextBox1.Text = string.Empty;
        }

    }
}

Form1.Designer.cs

//Form1.Designer.cs
namespace S7ComTest
{
    partial class Form1
    {
        /// <summary>
        ///  Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        ///  Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        ///  Required method for Designer support - do not modify
        ///  the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            button1 = new Button();
            label1 = new Label();
            label2 = new Label();
            textBox1 = new TextBox();
            textBox2 = new TextBox();
            label3 = new Label();
            label5 = new Label();
            textBox3 = new TextBox();
            textBox4 = new TextBox();
            label6 = new Label();
            textBox5 = new TextBox();
            label7 = new Label();
            textBox6 = new TextBox();
            label8 = new Label();
            textBox7 = new TextBox();
            label9 = new Label();
            textBox8 = new TextBox();
            label10 = new Label();
            textBox9 = new TextBox();
            label11 = new Label();
            textBox10 = new TextBox();
            label12 = new Label();
            textBox11 = new TextBox();
            label13 = new Label();
            textBox12 = new TextBox();
            label14 = new Label();
            textBox13 = new TextBox();
            label4 = new Label();
            textBox14 = new TextBox();
            label15 = new Label();
            textBox15 = new TextBox();
            label16 = new Label();
            textBox16 = new TextBox();
            label17 = new Label();
            textBox17 = new TextBox();
            label18 = new Label();
            textBox18 = new TextBox();
            label19 = new Label();
            textBox19 = new TextBox();
            label20 = new Label();
            textBox20 = new TextBox();
            label21 = new Label();
            textBox21 = new TextBox();
            label22 = new Label();
            textBox22 = new TextBox();
            label23 = new Label();
            textBox23 = new TextBox();
            label24 = new Label();
            textBox24 = new TextBox();
            label25 = new Label();
            label26 = new Label();
            richTextBox1 = new RichTextBox();
            SuspendLayout();
            // 
            // button1
            // 
            button1.Location = new Point(283, 38);
            button1.Name = "button1";
            button1.Size = new Size(141, 61);
            button1.TabIndex = 0;
            button1.Text = "启用连接";
            button1.UseVisualStyleBackColor = true;
            button1.Click += button1_Click;
            // 
            // label1
            // 
            label1.AutoSize = true;
            label1.Location = new Point(20, 79);
            label1.Name = "label1";
            label1.Size = new Size(69, 20);
            label1.TabIndex = 2;
            label1.Text = "连接状态";
            // 
            // label2
            // 
            label2.AutoSize = true;
            label2.Location = new Point(20, 38);
            label2.Name = "label2";
            label2.Size = new Size(69, 20);
            label2.TabIndex = 3;
            label2.Text = "当前时间";
            // 
            // textBox1
            // 
            textBox1.Location = new Point(107, 77);
            textBox1.Name = "textBox1";
            textBox1.ReadOnly = true;
            textBox1.Size = new Size(121, 27);
            textBox1.TabIndex = 4;
            // 
            // textBox2
            // 
            textBox2.Location = new Point(107, 35);
            textBox2.Name = "textBox2";
            textBox2.ReadOnly = true;
            textBox2.Size = new Size(121, 27);
            textBox2.TabIndex = 5;
            // 
            // label3
            // 
            label3.AutoSize = true;
            label3.Location = new Point(20, 123);
            label3.Name = "label3";
            label3.Size = new Size(39, 20);
            label3.TabIndex = 6;
            label3.Text = "读取";
            // 
            // label5
            // 
            label5.AutoSize = true;
            label5.Location = new Point(20, 165);
            label5.Name = "label5";
            label5.Size = new Size(59, 20);
            label5.TabIndex = 8;
            label5.Text = "BOOL1";
            // 
            // textBox3
            // 
            textBox3.Location = new Point(107, 162);
            textBox3.Name = "textBox3";
            textBox3.ReadOnly = true;
            textBox3.Size = new Size(121, 27);
            textBox3.TabIndex = 9;
            // 
            // textBox4
            // 
            textBox4.Location = new Point(107, 199);
            textBox4.Name = "textBox4";
            textBox4.ReadOnly = true;
            textBox4.Size = new Size(121, 27);
            textBox4.TabIndex = 11;
            // 
            // label6
            // 
            label6.AutoSize = true;
            label6.Location = new Point(20, 202);
            label6.Name = "label6";
            label6.Size = new Size(59, 20);
            label6.TabIndex = 10;
            label6.Text = "BOOL2";
            // 
            // textBox5
            // 
            textBox5.Location = new Point(107, 236);
            textBox5.Name = "textBox5";
            textBox5.ReadOnly = true;
            textBox5.Size = new Size(121, 27);
            textBox5.TabIndex = 13;
            // 
            // label7
            // 
            label7.AutoSize = true;
            label7.Location = new Point(20, 239);
            label7.Name = "label7";
            label7.Size = new Size(59, 20);
            label7.TabIndex = 12;
            label7.Text = "BOOL3";
            // 
            // textBox6
            // 
            textBox6.Location = new Point(107, 273);
            textBox6.Name = "textBox6";
            textBox6.ReadOnly = true;
            textBox6.Size = new Size(121, 27);
            textBox6.TabIndex = 15;
            // 
            // label8
            // 
            label8.AutoSize = true;
            label8.Location = new Point(20, 276);
            label8.Name = "label8";
            label8.Size = new Size(59, 20);
            label8.TabIndex = 14;
            label8.Text = "BOOL4";
            // 
            // textBox7
            // 
            textBox7.Location = new Point(107, 310);
            textBox7.Name = "textBox7";
            textBox7.ReadOnly = true;
            textBox7.Size = new Size(121, 27);
            textBox7.TabIndex = 17;
            // 
            // label9
            // 
            label9.AutoSize = true;
            label9.Location = new Point(20, 313);
            label9.Name = "label9";
            label9.Size = new Size(53, 20);
            label9.TabIndex = 16;
            label9.Text = "BYTE1";
            // 
            // textBox8
            // 
            textBox8.Location = new Point(107, 347);
            textBox8.Name = "textBox8";
            textBox8.ReadOnly = true;
            textBox8.Size = new Size(121, 27);
            textBox8.TabIndex = 19;
            // 
            // label10
            // 
            label10.AutoSize = true;
            label10.Location = new Point(20, 350);
            label10.Name = "label10";
            label10.Size = new Size(53, 20);
            label10.TabIndex = 18;
            label10.Text = "BYTE2";
            // 
            // textBox9
            // 
            textBox9.Location = new Point(345, 162);
            textBox9.Name = "textBox9";
            textBox9.ReadOnly = true;
            textBox9.Size = new Size(121, 27);
            textBox9.TabIndex = 21;
            // 
            // label11
            // 
            label11.AutoSize = true;
            label11.Location = new Point(258, 165);
            label11.Name = "label11";
            label11.Size = new Size(34, 20);
            label11.TabIndex = 20;
            label11.Text = "INT";
            // 
            // textBox10
            // 
            textBox10.Location = new Point(345, 199);
            textBox10.Name = "textBox10";
            textBox10.ReadOnly = true;
            textBox10.Size = new Size(121, 27);
            textBox10.TabIndex = 23;
            // 
            // label12
            // 
            label12.AutoSize = true;
            label12.Location = new Point(258, 202);
            label12.Name = "label12";
            label12.Size = new Size(45, 20);
            label12.TabIndex = 22;
            label12.Text = "DINT";
            // 
            // textBox11
            // 
            textBox11.Location = new Point(345, 236);
            textBox11.Name = "textBox11";
            textBox11.ReadOnly = true;
            textBox11.Size = new Size(121, 27);
            textBox11.TabIndex = 25;
            // 
            // label13
            // 
            label13.AutoSize = true;
            label13.Location = new Point(258, 239);
            label13.Name = "label13";
            label13.Size = new Size(46, 20);
            label13.TabIndex = 24;
            label13.Text = "REAL";
            // 
            // textBox12
            // 
            textBox12.Location = new Point(345, 273);
            textBox12.Name = "textBox12";
            textBox12.ReadOnly = true;
            textBox12.Size = new Size(121, 27);
            textBox12.TabIndex = 27;
            // 
            // label14
            // 
            label14.AutoSize = true;
            label14.Location = new Point(258, 276);
            label14.Name = "label14";
            label14.Size = new Size(64, 20);
            label14.TabIndex = 26;
            label14.Text = "STRING";
            // 
            // textBox13
            // 
            textBox13.Location = new Point(345, 310);
            textBox13.Name = "textBox13";
            textBox13.ReadOnly = true;
            textBox13.Size = new Size(121, 27);
            textBox13.TabIndex = 29;
            // 
            // label4
            // 
            label4.AutoSize = true;
            label4.Location = new Point(258, 313);
            label4.Name = "label4";
            label4.Size = new Size(57, 20);
            label4.TabIndex = 28;
            label4.Text = "WORD";
            // 
            // textBox14
            // 
            textBox14.Location = new Point(590, 162);
            textBox14.Name = "textBox14";
            textBox14.Size = new Size(121, 27);
            textBox14.TabIndex = 52;
            // 
            // label15
            // 
            label15.AutoSize = true;
            label15.Location = new Point(741, 313);
            label15.Name = "label15";
            label15.Size = new Size(57, 20);
            label15.TabIndex = 51;
            label15.Text = "WORD";
            // 
            // textBox15
            // 
            textBox15.Location = new Point(590, 199);
            textBox15.Name = "textBox15";
            textBox15.Size = new Size(121, 27);
            textBox15.TabIndex = 50;
            // 
            // label16
            // 
            label16.AutoSize = true;
            label16.Location = new Point(741, 276);
            label16.Name = "label16";
            label16.Size = new Size(64, 20);
            label16.TabIndex = 49;
            label16.Text = "STRING";
            // 
            // textBox16
            // 
            textBox16.Location = new Point(590, 236);
            textBox16.Name = "textBox16";
            textBox16.Size = new Size(121, 27);
            textBox16.TabIndex = 48;
            // 
            // label17
            // 
            label17.AutoSize = true;
            label17.Location = new Point(741, 239);
            label17.Name = "label17";
            label17.Size = new Size(46, 20);
            label17.TabIndex = 47;
            label17.Text = "REAL";
            // 
            // textBox17
            // 
            textBox17.Location = new Point(590, 273);
            textBox17.Name = "textBox17";
            textBox17.Size = new Size(121, 27);
            textBox17.TabIndex = 46;
            // 
            // label18
            // 
            label18.AutoSize = true;
            label18.Location = new Point(741, 202);
            label18.Name = "label18";
            label18.Size = new Size(45, 20);
            label18.TabIndex = 45;
            label18.Text = "DINT";
            // 
            // textBox18
            // 
            textBox18.Location = new Point(590, 310);
            textBox18.Name = "textBox18";
            textBox18.Size = new Size(121, 27);
            textBox18.TabIndex = 44;
            // 
            // label19
            // 
            label19.AutoSize = true;
            label19.Location = new Point(741, 165);
            label19.Name = "label19";
            label19.Size = new Size(34, 20);
            label19.TabIndex = 43;
            label19.Text = "INT";
            // 
            // textBox19
            // 
            textBox19.Location = new Point(590, 347);
            textBox19.Name = "textBox19";
            textBox19.Size = new Size(121, 27);
            textBox19.TabIndex = 42;
            // 
            // label20
            // 
            label20.AutoSize = true;
            label20.Location = new Point(503, 350);
            label20.Name = "label20";
            label20.Size = new Size(53, 20);
            label20.TabIndex = 41;
            label20.Text = "BYTE2";
            // 
            // textBox20
            // 
            textBox20.Location = new Point(828, 162);
            textBox20.Name = "textBox20";
            textBox20.Size = new Size(121, 27);
            textBox20.TabIndex = 40;
            // 
            // label21
            // 
            label21.AutoSize = true;
            label21.Location = new Point(503, 313);
            label21.Name = "label21";
            label21.Size = new Size(53, 20);
            label21.TabIndex = 39;
            label21.Text = "BYTE1";
            // 
            // textBox21
            // 
            textBox21.Location = new Point(828, 199);
            textBox21.Name = "textBox21";
            textBox21.Size = new Size(121, 27);
            textBox21.TabIndex = 38;
            // 
            // label22
            // 
            label22.AutoSize = true;
            label22.Location = new Point(503, 276);
            label22.Name = "label22";
            label22.Size = new Size(59, 20);
            label22.TabIndex = 37;
            label22.Text = "BOOL4";
            // 
            // textBox22
            // 
            textBox22.Location = new Point(828, 236);
            textBox22.Name = "textBox22";
            textBox22.Size = new Size(121, 27);
            textBox22.TabIndex = 36;
            // 
            // label23
            // 
            label23.AutoSize = true;
            label23.Location = new Point(503, 239);
            label23.Name = "label23";
            label23.Size = new Size(59, 20);
            label23.TabIndex = 35;
            label23.Text = "BOOL3";
            // 
            // textBox23
            // 
            textBox23.Location = new Point(828, 273);
            textBox23.Name = "textBox23";
            textBox23.Size = new Size(121, 27);
            textBox23.TabIndex = 34;
            // 
            // label24
            // 
            label24.AutoSize = true;
            label24.Location = new Point(503, 202);
            label24.Name = "label24";
            label24.Size = new Size(59, 20);
            label24.TabIndex = 33;
            label24.Text = "BOOL2";
            // 
            // textBox24
            // 
            textBox24.Location = new Point(828, 310);
            textBox24.Name = "textBox24";
            textBox24.Size = new Size(121, 27);
            textBox24.TabIndex = 32;
            // 
            // label25
            // 
            label25.AutoSize = true;
            label25.Location = new Point(503, 165);
            label25.Name = "label25";
            label25.Size = new Size(59, 20);
            label25.TabIndex = 31;
            label25.Text = "BOOL1";
            // 
            // label26
            // 
            label26.AutoSize = true;
            label26.Location = new Point(503, 123);
            label26.Name = "label26";
            label26.Size = new Size(39, 20);
            label26.TabIndex = 30;
            label26.Text = "写入";
            // 
            // richTextBox1
            // 
            richTextBox1.Location = new Point(-1, 390);
            richTextBox1.Name = "richTextBox1";
            richTextBox1.ReadOnly = true;
            richTextBox1.Size = new Size(983, 150);
            richTextBox1.TabIndex = 54;
            richTextBox1.Text = "";
            // 
            // Form1
            // 
            AutoScaleDimensions = new SizeF(9F, 20F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new Size(985, 541);
            Controls.Add(richTextBox1);
            Controls.Add(textBox14);
            Controls.Add(label15);
            Controls.Add(textBox15);
            Controls.Add(label16);
            Controls.Add(textBox16);
            Controls.Add(label17);
            Controls.Add(textBox17);
            Controls.Add(label18);
            Controls.Add(textBox18);
            Controls.Add(label19);
            Controls.Add(textBox19);
            Controls.Add(label20);
            Controls.Add(textBox20);
            Controls.Add(label21);
            Controls.Add(textBox21);
            Controls.Add(label22);
            Controls.Add(textBox22);
            Controls.Add(label23);
            Controls.Add(textBox23);
            Controls.Add(label24);
            Controls.Add(textBox24);
            Controls.Add(label25);
            Controls.Add(label26);
            Controls.Add(textBox13);
            Controls.Add(label4);
            Controls.Add(textBox12);
            Controls.Add(label14);
            Controls.Add(textBox11);
            Controls.Add(label13);
            Controls.Add(textBox10);
            Controls.Add(label12);
            Controls.Add(textBox9);
            Controls.Add(label11);
            Controls.Add(textBox8);
            Controls.Add(label10);
            Controls.Add(textBox7);
            Controls.Add(label9);
            Controls.Add(textBox6);
            Controls.Add(label8);
            Controls.Add(textBox5);
            Controls.Add(label7);
            Controls.Add(textBox4);
            Controls.Add(label6);
            Controls.Add(textBox3);
            Controls.Add(label5);
            Controls.Add(label3);
            Controls.Add(textBox2);
            Controls.Add(textBox1);
            Controls.Add(label2);
            Controls.Add(label1);
            Controls.Add(button1);
            Name = "Form1";
            Text = "S7通讯测试";
            ResumeLayout(false);
            PerformLayout();
        }

        #endregion

        private Button button1;
        private Label label1;
        private Label label2;
        private TextBox textBox1;
        private TextBox textBox2;
        private Label label3;
        private Label label5;
        private TextBox textBox3;
        private TextBox textBox4;
        private Label label6;
        private TextBox textBox5;
        private Label label7;
        private TextBox textBox6;
        private Label label8;
        private TextBox textBox7;
        private Label label9;
        private TextBox textBox8;
        private Label label10;
        private TextBox textBox9;
        private Label label11;
        private TextBox textBox10;
        private Label label12;
        private TextBox textBox11;
        private Label label13;
        private TextBox textBox12;
        private Label label14;
        private TextBox textBox13;
        private Label label4;
        private TextBox textBox14;
        private Label label15;
        private TextBox textBox15;
        private Label label16;
        private TextBox textBox16;
        private Label label17;
        private TextBox textBox17;
        private Label label18;
        private TextBox textBox18;
        private Label label19;
        private TextBox textBox19;
        private Label label20;
        private TextBox textBox20;
        private Label label21;
        private TextBox textBox21;
        private Label label22;
        private TextBox textBox22;
        private Label label23;
        private TextBox textBox23;
        private Label label24;
        private TextBox textBox24;
        private Label label25;
        private Label label26;
        private RichTextBox richTextBox1;
    }
}

FromS7.cs
 

//FromS7.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace S7ComTest.Types
{
    public class FromS7
    {
        public class Bit
        {
            /// <summary>
            /// 将布尔值写入到字节跨度的指定位置(按位操作)
            /// </summary>
            /// <param name="value">要写入的布尔值</param>
            /// <param name="destination">目标字节跨度</param>
            /// <param name="byteIndex">字节跨度中的索引位置</param>
            /// <param name="bitIndex">字节内的位索引(0-7),0表示最低位(最右侧),7表示最高位</param>
            /// <exception cref="ArgumentOutOfRangeException">
            /// 当byteIndex超出跨度范围,或bitIndex超出0-7范围时抛出
            /// </exception>
            /// <remarks>
            /// 此方法会直接修改目标跨度中的指定字节。
            /// 位操作原理:
            /// - 设置特定位:使用按位或 (targetByte | (1 << bitIndex))
            /// - 清除特定位:使用按位与 (targetByte & ~(1 << bitIndex))
            /// </remarks>
            public static void ToByteArray(bool value, Span<byte> destination, int byteIndex, int bitIndex)
            {
                // 验证参数有效性
                if (byteIndex < 0 || byteIndex >= destination.Length)
                    throw new ArgumentOutOfRangeException(nameof(byteIndex),
                        $"字节索引必须在0-{destination.Length - 1}范围内");

                if (bitIndex < 0 || bitIndex > 7)
                    throw new ArgumentOutOfRangeException(nameof(bitIndex), "位索引必须在0-7范围内");

                // 获取目标字节
                ref byte targetByte = ref destination[byteIndex];

                // 修改位值
                if (value)
                {
                    // 设置指定位置的位为1
                    targetByte = (byte)(targetByte | (1 << bitIndex));
                }
                else
                {
                    // 清除指定位置的位为0
                    targetByte = (byte)(targetByte & ~(1 << bitIndex));
                }
            }
        }

        public class Int
        {
            /// <summary>
            /// 将 short (Int16) 转换为 S7 Int 格式,并写入到指定的字节跨度中
            /// </summary>
            /// <param name="value">要转换的 Int16 值</param>
            /// <param name="destination">目标字节跨度,必须至少有 2 字节的空间</param>
            /// <exception cref="ArgumentException">当目标跨度长度不足时抛出</exception>
            public static void ToByteArray(Int16 value, Span<byte> destination)
            {
                if (destination.Length < 2)
                    throw new ArgumentException("目标跨度长度必须至少为 2 字节。", nameof(destination));

                // 大端字节序(高字节在前)
                destination[0] = (byte)(value >> 8);      // 高字节
                destination[1] = (byte)value;             // 低字节
            }
        }

        public class Dint
        {
            /// <summary>
            /// 将 int (Int32) 转换为 S7 DInt 格式,并写入到指定的字节跨度中
            /// </summary>
            /// <param name="value">要转换的 Int32 值</param>
            /// <param name="destination">目标字节跨度,必须至少有 4 字节的空间</param>
            /// <exception cref="ArgumentException">当目标跨度长度不足时抛出</exception>
            public static void ToByteArray(Int32 value, Span<byte> destination)
            {
                if (destination.Length < 4)
                    throw new ArgumentException("目标跨度长度必须至少为 4 字节。", nameof(destination));

                // 大端字节序(高字节在前)
                destination[0] = (byte)(value >> 24);      // 最高字节
                destination[1] = (byte)(value >> 16);      // 次高字节
                destination[2] = (byte)(value >> 8);       // 次低字节
                destination[3] = (byte)value;              // 最低字节
            }
        }

        public class Real
        {
            /// 将 float 转换为 S7 Real 格式,并写入到指定的字节跨度中
            /// </summary>
            /// <param name="value">要转换的 float 值</param>
            /// <param name="destination">目标字节跨度,必须至少有 4 字节的空间</param>
            /// <exception cref="ArgumentException">当目标跨度长度不足时抛出</exception>
            public static void ToByteArray(float value, Span<byte> destination)
            {
                if (destination.Length < 4)
                    throw new ArgumentException("目标跨度长度必须至少为 4 字节。", nameof(destination));

                // 将 float 转换为字节(平台原生字节序)
                byte[] temp = BitConverter.GetBytes(value);

                // 根据平台字节序调整
                if (BitConverter.IsLittleEndian)
                {
                    // S7 使用大端字节序,而当前平台是小端,需要反转
                    destination[0] = temp[3];
                    destination[1] = temp[2];
                    destination[2] = temp[1];
                    destination[3] = temp[0];
                }
                else
                {
                    // 平台字节序与 S7 一致(大端),直接复制
                    temp.AsSpan().CopyTo(destination);
                }
            }
        }

        public class S7String
        {
            private static Encoding stringEncoding = Encoding.ASCII;
            /// <summary>
            /// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
            /// </summary>
            /// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
            public static Encoding StringEncoding
            {
                get => stringEncoding;
                set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
            }

            /// <summary>
            /// 将字符串转换为S7协议格式的字符串(带2字节头部),并写入目标字节跨度
            /// S7字符串格式:
            /// - 第1个字节:预留长度(字符串最多可包含的字符数)
            /// - 第2个字节:实际长度(当前字符串的字符数)
            /// - 后续字节:字符串内容的字节表示
            /// </summary>
            /// <param name="value">要转换的字符串,不能为null</param>
            /// <param name="destinationSpan">目标字节跨度,用于存储转换后的S7字符串</param>
            /// <param name="startIndex">在目标跨度中开始写入的起始索引</param>
            /// <exception cref="ArgumentNullException">当value为null时抛出</exception>
            /// <exception cref="ArgumentException">
            /// 当预留长度超过254字符,或字符串实际长度超过预留长度时抛出
            /// </exception>
            public static void ToByteArray(string? value, Span<byte> destinationSpan, int startIndex)
            {
                // 验证输入字符串不为null
                if (value is null)
                {
                    throw new ArgumentNullException(nameof(value), "输入字符串不能为null");
                }

                // 计算预留长度(使用字符串实际长度)
                int reservedLength = value.Length;

                // 检查预留长度是否超出S7协议限制(最大254字符)
                if (reservedLength > 254)
                {
                    throw new ArgumentException($"S7协议支持的最大字符串长度为254字符,当前字符串长度为{reservedLength}");
                }

                // 将字符串编码为字节数组
                byte[] bytes = StringEncoding.GetBytes(value);

                // 验证编码后的字节长度是否符合预留长度
                if (bytes.Length > reservedLength)
                {
                    throw new ArgumentException($"编码后的字符串长度({bytes.Length})超过了预留长度({reservedLength})");
                }

                // 写入S7字符串头部信息
                destinationSpan[startIndex] = (byte)reservedLength;         // 预留长度
                destinationSpan[startIndex + 1] = (byte)bytes.Length;      // 实际长度

                // 写入字符串内容(从startIndex+2位置开始)
                bytes.AsSpan().CopyTo(destinationSpan.Slice(startIndex + 2));
            }

        }

        public class Word
        {
            /// <summary>
            /// 将 ushort (UInt16) 转换为 S7 Word 格式,并写入到指定的字节跨度中
            /// </summary>
            /// <param name="value">要转换的 UInt16 值</param>
            /// <param name="destination">目标字节跨度,必须至少有 2 字节的空间</param>
            /// <exception cref="ArgumentException">当目标跨度长度不足时抛出</exception>
            public static void ToByteArray(UInt16 value, Span<byte> destination)
            {
                if (destination.Length < 2)
                    throw new ArgumentException("目标跨度长度必须至少为 2 字节。", nameof(destination));

                // 大端字节序(高字节在前)
                destination[0] = (byte)(value >> 8);      // 高字节
                destination[1] = (byte)value;             // 低字节
            }
        }

        public class Dword
        {
            /// <summary>
            /// 将 uint (UInt32) 转换为 S7 DWord 格式,并写入到指定的字节跨度中
            /// </summary>
            /// <param name="value">要转换的 UInt32 值</param>
            /// <param name="destination">目标字节跨度,必须至少有 4 字节的空间</param>
            /// <exception cref="ArgumentException">当目标跨度长度不足时抛出</exception>
            public static void ToByteArray(UInt32 value, Span<byte> destination)
            {
                if (destination.Length < 4)
                    throw new ArgumentException("目标跨度长度必须至少为 4 字节。", nameof(destination));

                // 大端字节序(高字节在前)
                destination[0] = (byte)(value >> 24);      // 最高字节
                destination[1] = (byte)(value >> 16);      // 次高字节
                destination[2] = (byte)(value >> 8);       // 次低字节
                destination[3] = (byte)value;              // 最低字节
            }
        }

    }
}

NumberFromString.cs

//NumberFromString.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace S7ComTest.Types
{
    public static class numberFromString
    {
        public static bool ParseBool(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return false;

            // 标准布尔解析
            if (bool.TryParse(text, out bool result))
                return result;

            // 扩展格式支持
            string lowerText = text.ToLowerInvariant();
            switch (lowerText)
            {
                case "1":
                case "on":
                case "yes":
                case "true":
                case "enabled":
                    return true;
                case "0":
                case "off":
                case "no":
                case "false":
                case "disabled":
                    return false;
                default:
                    errorCallback?.Invoke($"无效的布尔值: '{text}',已使用默认值 false");
                    return false;
            }
        }

        public static byte ParseByte(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return 0;

            // 处理十六进制格式
            if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            {
                // 修复:使用不同的变量名避免冲突
                if (byte.TryParse(text.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out byte hexResult))
                    return hexResult;
            }

            // 处理十进制格式
            if (byte.TryParse(text, out byte decResult))
                return decResult;

            errorCallback?.Invoke($"无效的字节值: '{text}',已使用默认值 0");
            return 0;
        }

        public static short ParseInt(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return 0;

            // 处理十六进制格式
            if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            {
                // 修复:使用不同的变量名避免冲突
                if (short.TryParse(text.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out short hexResult))
                    return hexResult;
            }

            // 处理十进制格式
            if (short.TryParse(text, out short decResult))
                return decResult;

            errorCallback?.Invoke($"无效的整数: '{text}',已使用默认值 0");
            return 0;
        }

        public static int ParseDint(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return 0;

            // 处理十六进制格式
            if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            {
                // 修复:使用不同的变量名避免冲突
                if (int.TryParse(text.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out int hexResult))
                    return hexResult;
            }

            // 处理十进制格式
            if (int.TryParse(text, out int decResult))
                return decResult;

            errorCallback?.Invoke($"无效的长整数: '{text}',已使用默认值 0");
            return 0;
        }

        public static float ParseReal(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return 0;

            // 使用固定的不变文化格式解析浮点数,确保小数点始终为'.'
            if (float.TryParse(text, System.Globalization.NumberStyles.Float,
                              System.Globalization.CultureInfo.InvariantCulture,
                              out float result))
                return result;

            errorCallback?.Invoke($"无效的浮点数: '{text}',已使用默认值 0");
            return 0;
        }

        public static ushort ParseWord(string text, Action<string>? errorCallback = null)
        {
            if (string.IsNullOrWhiteSpace(text))
                return 0;

            // 处理十六进制格式
            if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
            {
                // 修复:使用不同的变量名避免冲突
                if (ushort.TryParse(text.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out ushort hexResult))
                    return hexResult;
            }

            // 处理十进制格式
            if (ushort.TryParse(text, out ushort decResult))
                return decResult;

            errorCallback?.Invoke($"无效的字值: '{text}',已使用默认值 0");
            return 0;
        }
    }
}

以上代码实现与S7-1200通讯,左边是只读取,右边是读写区,这两个区域每隔1S从PLC读取数据,存储在缓存中,再有程序判断是否跟上次数据有变化,若有,则更新UI,若没有,则不更新UI。读写区还可以写入数据,当写入数据的过程时,读写区不更新相应的数据,直到写入完成。

注意,西门子1200需要开启PUT/GET通讯允许。

在Form1中,很多方法都有注释,请自行理解,后续有空的时候,再分析具体方法。

另外,在下载下来的C#版本的S7.NET源码可以自行修改编辑,编辑完再重新生成,然后在对应框架下找到S7.Net.dll、S7.Net.xml复制到指定地方替换即可。

源包可以自行去网上下载。这里不建议修改它,如果需要添加转换方法,可以自行在项目中创建,比如,我自己创建的FromS7.cs、NumberFromString.cs //包含在Types文件夹中,是我自己创建的数据处理的方法、类等的命名空间。

修改后生成的.dll文件所在位置

替换位置

到此结束,整个过程其实很简单,通讯配置也很简单。主要是这个处理逻辑、多线程等比较麻烦,需要自己去建。如果C#水平不咋地,可以直接把这个搬过去,自己再扩充。这个代码里,采用的是批量通讯,只需要放在附近,规划好自己,就能拿来用了。这个只是做简单的画面和数据监控,当然,如果需要开发更丰富复杂的画面,也可以采用WPF应用程序,S7.NET配置基本一致。区别就在两个应用程序的语法上,请各位自行选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值