简介:面向C#工业自动化开发人员及西门子PLC初学者,这是一份可直接运行的S7.Net与Sharp7连接PLC实例源码。资源实现C#与S7-1200 PLC通信,覆盖DB块数据读写,并补充了bool变量、string及Wstring类型读取,从基础连接到特定数据类型访问均有示例可循;适合新手快速上手,也能给有经验的开发者提供工程结构参考。压缩包共35个文件,以C#源码、项目与解决方案文件、配置文件为主,同时包含S7.Net.dll、英文说明文档及可执行程序,包体仅805KB,轻量便于直接导入学习。资源包内项目工程完整,采用WinForm界面展示通信过程,工程结构清晰,方便对照学习连接建立、DB块寻址与多类型变量转换等处理逻辑;附带说明文档也免去到处寻找DLL的麻烦。目前已有4066人学习下载,是工控程序员学习C#与西门子PLC通信时值得参考的实例。 做上位机开发的朋友,十有八九绕不开和西门子PLC打交道。我上一个项目是设备数据采集与MES对接,现场清一色S7-1200,上位机用C#,当时在S7.Net和Sharp7之间来回折腾,最后两条路都走通了。今天干脆把这两个库怎么连接、怎么读写、各自有哪些坑,一次性说清楚,附带可以直接抄的实例源码。如果你正在做C#上位机、准备接西门子PLC做数据采集或者设备控制,这篇内容应该能帮你少走不少弯路。
先聊一个核心结论:S7.Net和Sharp7都是通过西门子的S7协议(TCP 102端口)与PLC通信,不是OPC UA,也不是Modbus TCP,而是西门子工业环境里最高效、最直接的以太网通信方式之一。两者解决的问题相同,但设计思路完全不同,实际用起来差别不小。下面我会从选型思路讲起,再把环境、连接、读写、工业场景落地、排障全部走一遍。
1. 整体设计与技术选型思路
1.1 S7.Net和Sharp7是什么,通信原理简单过一遍
先理解通信本质。C#上位机要和西门子PLC交换数据,底层其实是一条TCP连接,端口是102。PLC作为TCP服务器,上位机作为客户端发起连接,然后通过ISO-on-TCP协议栈发起S7通信请求,格式是一串二进制数据包。这个协议如果自己从Socket层写,工作量不小,而且容易踩字节序、PDU长度、ACK机制这些坑。
S7.Net和Sharp7就是在这一层帮我们封装好的库。它们把“连接握手、读取请求、写入请求、响应解析”这些底层细节全部处理好,我们只需要给出变量地址,比如DB1.DBD0或者M10.0,就能直接读回一个对象。这样做的最大好处是开发效率高,代码看起来非常直观,适合项目周期紧、需要快速上线的场景。
S7.Net的封装程度更高,喜欢用字符串地址描述变量,比如Read("DB1.DBD0"),读回来直接是object,需要自己做类型转换。Sharp7则走的是“函数调用+字节缓冲区”风格,比如ReadArea方法配合byte[]数组,低层API暴露得更充分,数据收发性能也更好,但代码写起来更啰嗦。二者本质都是TCP客户端,都在.NET平台上运行,没有谁绝对优于谁,只有适不适合当前项目。
1.2 两个库怎么选:接口友好vs底层高效
我自己的选择标准很简单:项目里如果变量数量不多、逻辑偏业务、主要做一些按钮控制和数据显示,那么S7.Net是首选,因为它写起来太舒服了。比如要读一个DB块的浮点数,float val = (float)plc.Read("DB10.DBD4");,一行代码搞定,新手看一遍就会。
但如果要做高频采集,比如每100ms轮询几十个DB块,或者要和MES大量交互、传上百个字节,那Sharp7更合适。它的ReadArea/WriteArea让你直接操作字节数组,没有多余的装箱拆箱开销,而且自带多实例连接管理,对吞吐量和稳定性要求高的场景更有底。老外做过不少对比,Sharp7在大量读写时的CPU占用和内存抖动确实比S7.Net更小。
我还观察到一个细节:S7.Net的Plc类在内部维护了请求序列,虽然使用方便,但多线程同时调用同一个实例会出问题,必须加锁。Sharp7的S7Client每个实例相对独立,做多连接、多线程轮询时心理负担小一些。一句话总结:求快用S7.Net,求稳求快用Sharp7,两个都学会,现场遇到什么问题都不慌。
1.3 为什么不是OPC UA、Modbus TCP
有人会问,现在OPC UA不是很流行吗,为什么还用S7协议?这个问题的答案在现场很现实。OPC UA功能强大、跨平台、安全性好,但它需要OPC Server,需要配置证书和节点模型,对中小型设备和数据采集项目来说明显偏重。而且西门子PLC通过S7协议开放PUT/GET通信,是官方硬件支持的能力,不需要额外装软件,延迟低,实现简单。
Modbus TCP看起来也很轻,但西门子S7-1200/1500的Modbus TCP通常需要在程序中调用Modbus TCP指令块,并且自己规划保持寄存器区,不像S7协议那样可以随意读DB、M、I、Q区。更重要的是,很多既有设备程序里根本没有做Modbus映射,只有S7通信可用。所以基于TCP 102端口的S7协议,反而是C#上位机与西门子PLC通信的“最低门槛”方案。
2. 开发环境与PLC侧准备
2.1 环境搭建和NuGet包安装
我用的是Visual Studio 2022,项目框架是.NET 6.0,因为新项目不想再用.NET Framework了。但要注意,S7.Net和Sharp7都支持.NET Framework 4.6.2及以上,所以老项目也能用,不用担心迁移问题。
安装包很简单,NuGet搜索S7.Net或Sharp7,直接装最新稳定版。S7.Net目前版本是S7.Net,注意不要和另一个早期库S7.NetPlus混淆了。Sharp7的包名就是Sharp7,安装后引用里会出现Sharp7命名空间。
dotnet add package S7.Net dotnet add package Sharp7装完后先检查using是否正常:S7.Net用的是using S7.Net;,Sharp7用的是using Sharp7;。两个库只要不混用类型,放在同一个项目里不冲突。我实际项目中就是同时装了这两个包,S7.Net负责几个操作界面上的手动读写,Sharp7负责后台的批量数据采集。
2.2 PLC端必须打开的“远程访问开关”
很多新手最容易忽略的一点:PLC程序能跑、IP也能Ping通,但C#连接就是超时,十有八九是PLC侧的“允许远程访问”没打开。S7-1200/1500默认是不开放PUT/GET通信的,必须到PLC程序里手动设置。
在TIA Portal中,选中CPU,右键进入“属性”,找到“防护与安全”->“连接机制”,勾选“允许来自远程对象的PUT/GET通信访问”。然后重新编译下载硬件配置。这一步不做,S7.Net和Sharp7连上就会报“连接超时”或者“无法协商S7连接”。
S7-200 SMART有些差异,它需要在系统块里勾选“允许远程访问”,一般默认是开启的。S7-300/400老设备相对宽松,但也建议确认一下CPU的防护等级不是“完全保护”。
另外,PLC和上位机的IP必须在同一网段,比如PLC是192.168.0.1,上位机就设成192.168.0.10,掩码一致。把Windows防火墙临时关掉测试一下也是排查步骤之一。
2.3 IP、机架号和槽号这些参数到底怎么填
S7.Net和Sharp7建立连接时都需要几个参数:IP、机架号(Rack)、槽号(Slot)。很多人被这两个数字折磨过,其实记住经验值就够了:
- S7-1200:Rack=0,Slot=1
- S7-1500:Rack=0,Slot=1
- S7-300:Rack=0,Slot=2(常见)
- S7-400:Rack根据机架来,Slot一般为3或4
- S7-200 SMART:Rack=0,Slot=1,但部分旧版本直接用TCP/IP连接,不校验Rack/Slot
我实测S7-1200和S7-1500用Rack=0,Slot=1是稳定可靠的。如果填错,连接阶段通常直接报错,不会等到读写才暴露。
3. 核心读写实操:从连接到最后一个字节
3.1 建立连接:S7.Net和Sharp7的两种打开方式
先看S7.Net的写法。它很简单,定义一个Plc对象,参数是CPU类型、IP、Rack、Slot,然后Open:
using S7.Net; var plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1); plc.Open(); if (plc.IsConnected) { Console.WriteLine("S7.Net连接成功"); }这里CpuType枚举要和你实际的CPU型号匹配,如果PLC是S7-1500,就填CpuType.S71500。类型填错有时也能连上,但读写DB时可能表现异常,还是按实际填稳妥。
Sharp7的连接代码风格明显更底层:
using Sharp7; var client = new S7Client(); int result = client.ConnectTo("192.168.0.1", 0, 1); if (result == 0) { Console.WriteLine("Sharp7连接成功"); } else { Console.WriteLine("连接失败,错误码:" + client.ErrorText(result)); }注意Sharp7的ConnectTo返回值是0表示成功,非0是错误码。ErrorText方法能直接把错误码转成字符串,排障很有用。S7.Net的Open如果失败会直接抛异常,没有返回码,所以习惯用try-catch包住。
3.2 读I/Q/M位和写输出
连接建立后,先看最简单的位操作。S7.Net用地址字符串直接读写:
// 读输入位I0.0 bool inputBit = (bool)plc.Read("I0.0"); Console.WriteLine("I0.0 = " + inputBit); // 写输出位Q0.0 plc.Write("Q0.0", true); // 写M区位 plc.Write("M20.0", false);这段代码非常适合做手动控制画面里的按钮。比如界面上一个“启动”按钮,点击后Write("Q0.0", true),PLC里如果写了对应输出,设备就动了。但要注意:写入Q区是直接输出到物理点,实际操作前一定要确认设备安全,最好通过PLC程序里的逻辑互锁来控制,而不是上位机直接写Q点。
Sharp7在点位读写上没有字符串地址,需要自己拼写内存地址和位偏移。读M20.0的代码是这样的:
byte buffer; int area = S7AreaMK; int dbNumber = 0; int start = 20; int size = 1; int err = client.ReadArea(area, dbNumber, start, size, out buffer); bool bitValue = (buffer & 0x01) != 0;如果要写位,需要先读回一个字节,改对应位,再写回去。Sharp7在这点上确实麻烦,但胜在灵活,适合批量操作。实际项目里我用Sharp7读M区连续字节,再用位运算取出某些状态位,性能很好。
3.3 读写DB块的完整示例(含字符串和数组)
DB块是PLC程序中最常用的数据存储区,也是上位机读写的重点。S7.Net读写单个变量非常直观:
// 读DB1.DBD0,这是个浮点数 float speed = (float)plc.Read("DB1.DBD0"); // 读DB1.DBW4,这是个无符号整数 ushort value = (ushort)plc.Read("DB1.DBW4"); // 写DB1.DBD10,浮点数 plc.Write("DB1.DBD10", 123.456f);这里要特别强调字节偏移的计算。DBD0表示从DB块起始地址算起,第0字节开始,占4字节的Double Word(32位浮点数)。DBW4表示第4字节开始的Word(16位整数)。在PLC侧开发的同事通常用符号名,而在上位机这边,我们只能通过偏移地址来访问,所以拿到DB块结构表是第一要务。每改一次PLC程序,这些偏移地址都可能变,建议专门维护一份地址映射表。
字符串读写比较特殊。PLC中的String类型不是普通C#字符串,它内部结构是:第1个字节是最大长度,第2个字节是当前实际长度,从第3个字节开始才是字符内容。所以读DB1中的字符串之前,一定要知道它存放的最大长度和起始字节。S7.Net读字符串不能直接用字符串地址,需要自己读字节数组再解码:
// 假设DB1.DBB20是字符串起始地址 int startByte = 20; int maxLength = 254; // S7默认最大254 byte[] buffer = new byte[maxLength + 2]; plc.ReadBytes(DataType.DataBlock, 1, startByte, buffer.Length, buffer); int actualLength = buffer[1]; string text = Encoding.ASCII.GetString(buffer, 2, actualLength); Console.WriteLine(text);Sharp7处理字符串和字节数组就更顺手了,它内置了S7.GetStringAt方法:
byte[] buffer = new byte[256]; int err = client.ReadArea(S7AreaDB, 1, 20, 256, buffer); if (err == 0) { string text = S7.GetStringAt(buffer, 0); Console.WriteLine(text); }数组的读写思路也类似。如果PLC里定义了一个ARRAY[0..9] OF REAL,那么它实际就是连续的40个字节。用ReadBytes或ReadArea一次性读回来,再用BitConverter解析即可。千万不要一个一个变量去读,效率低,还会增加PLC通信负载。
3.4 异步读写与工业场景下的并发处理
工业上位机最怕界面卡死。S7.Net从旧版本开始就提供了一些异步方法,比如ReadAsync、WriteAsync,内部基于Task封装,UI线程调用时不会阻塞:
// 异步读浮点数 float speed = (float)await plc.ReadAsync("DB1.DBD0");注意ReadAsync返回的是object,还是要拆箱。Sharp7没有内置异步方法,但可以自己包一层Task.Run:
float speed = await Task.Run(() => { byte[] buffer = new byte[4]; int err = client.ReadArea(S7AreaDB, 1, 0, 4, buffer); if (err == 0) return S7.GetRealAt(buffer, 0); return float.NaN; });多线程并发是另一个大坑。S7.Net的Plc对象不是线程安全的,多个线程同时读同一个实例,轻则数据错乱,重则直接异常。我一开始没注意,在后台轮询线程和界面刷新线程同时调plc.Read,结果程序跑十几分钟就开始抛Socket异常。解决办法也很直接:要么所有访问都加同一把锁,要么每个线程单独创建自己的Plc实例。后一种方式更推荐,因为通信开销不大,却省去很多锁竞争。
Sharp7的S7Client在并发场景下相对安全,但我也建议一个连接只给一个线程用。多连接模式性能最好,不过需要自己维护连接池,项目复杂度允许时可以这么干。
4. 工业现场的综合落地:采集、扫码与UI维护
4.1 扫描枪触发事件后写入PLC
很多产线项目里,扫码枪并不是直接连PLC的,而是先到上位机,再由上位机把条码内容写入PLC。这里的关键是事件触发。扫码枪通过串口或TCP发送数据,C#这边用SerialPort.DataReceived或TcpClient异步接收,触发事件后解析条码,然后调用S7.Net或Sharp7写入PLC的DB块字符串区。
我做过一个实际案例:扫码枪通过串口连接到工控机,扫码后触发事件里取出条码字符串,写入PLC的DB10.DBB0开始的字符串变量,同时把M100.0置位,通知PLC“新条码已到达”。PLC处理完后再复位M100.0。这样做的好处是上位机和PLC之间有一个简单的握手协议,不会丢数据。
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { string barcode = serialPort.ReadLine().Trim(); // 假设使用S7.Net plc.Write("DB10.DBB2", barcode); plc.Write("M100.0", true); }注意S7.Net的Write对字符串写入有长度限制,而且PLC字符串变量最大长度要足够。串口事件里不要直接做耗时操作,建议把条码放进ConcurrentQueue,再由后台线程负责写入PLC,这样通信更稳定。
4.2 循环数据采集业务数据不卡UI的正确思路
“C# 循环数据采集和UI刷新卡顿”是热词,也是现场开发最典型的痛点。很多新手在定时器里直接plc.Read,然后又直接textBox.Text = value,跑起来一会儿界面就卡死。正确的做法必须遵循两个原则:PLC通信远离UI线程,UI刷新只在需要时进行。
我通常用一个后台Task做While循环,每隔100ms读取一批数据,读完后把数据封装成一个快照对象,放进Volatile或ConcurrentDictionary。UI侧用System.Windows.Forms.Timer或DispatcherTimer,每200ms从快照里取一次值,更新到控件上。
private async Task PollingLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { var snapshot = await Task.Run(() => ReadAllVariables()); _latest = snapshot; await Task.Delay(100, token); } }这样即使PLC响应慢,也只是后台任务排队,UI不会卡。还要注意UI控件的跨线程访问,更新时用BeginInvoke,不要频繁调用,批量赋值比逐个赋值快很多。实测下来,100ms轮询几十个变量,UI完全流畅。
4.3 延伸:ABB变频器、康耐视相机等设备也能这么玩
很多现场设备看似和PLC通信无关,实际上只要支持S7协议,都能用同一套思路接入。比如ABB变频器通过Profinet接入西门子PLC,PLC程序里把变频器的运行频率、电流映射到DB块,那么上位机读这些DB块就等于间接读了变频器数据,不再需要单独和变频器通信。
康耐视InSight相机同样可以通过Profinet与西门子PLC交互,相机结果写入PLC的DB区,C#上位机再从PLC读回判定结果。这就是“相机+PLC+上位机”的常见集成模式。作为上位机开发,我们不需要关心Profinet怎么配置,只要能从PLC的DB块里拿到结果,整个链路就是通的。这个思路可以帮你少做很多无用的驱动开发。
5. 常见问题与排障实录
5.1 连接不上:IP、防火墙、PUT/GET和安全设置
这是出现频率最高的问题。排查顺序我建议是:先ping通PLC的IP,再检查上位机与PLC是否同网段,然后确认PLC的PUT/GET通信是否打开,最后关掉Windows防火墙测试。如果PLC是S7-1500,还要注意CPU的“防护等级”不能选“完全保护”,否则任何S7通信都会被拒绝。
有时候连接时好时坏,很可能是PLC侧连接资源被占满。S7-1200/1500的可用连接资源有限,多个上位机或HMI同时连接时,新连接会被拒绝。这种情况可以尝试降低轮询频率,或者干脆给上位机单独分一个“连接资源”,在PLC硬件组态里设置。
5.2 读出来的数据不对:偏移和类型长度是重灾区
读回来的数据完全不对,比如应该是0.5,读出来却是1.084e-19,这通常是偏移地址错位。PLC里的DBD0是0~3字节,DBD4是4~7字节,如果实际变量在4字节位置,你读了DBD0,拿到的就是别的变量的数据。类型长度不一致也会乱掉,比如把Word当DWord读、把Real当Int读,数据当然不对。
解决这件事最可靠的办法是让PLC工程师导出一份DB块结构表,或者自己在TIA Portal里看偏移。别凭记忆写,后面对接MES出了问题排查很费时间。字符串读出来乱码,多半是最大长度与实际内容长度处理不对,按前面讲的buffer[1]作为实际长度解析就没问题。
5.3 程序运行一段时间后卡死:线程安全与连接复用
我在项目里遇到过连续运行两小时后,上位机所有读写都超时,但PLC和网络都正常。后来定位到是S7.Net的Plc对象被多个线程并发调用,内部状态被搞乱了。从那以后,我定下两个规矩:一是每个线程单独创建连接,二是所有对同一个连接实例的访问必须加锁。
另外要注意长时间不通信的连接可能被防火墙或者交换机断开。S7.Net没有自动重连机制,所以轮询循环里如果捕获到PlcException或Socket异常,应该关闭连接并重新Open。Sharp7同理,检测到返回值非0,就断开重连。写一个EnsureConnected方法,每轮调用时检查一下状态,是稳定运行的关键。
private void EnsureConnected() { if (!plc.IsConnected) { plc.Open(); } }5.4 轮询太快拖累PLC?调整协议开销
上位机采集太频繁,PLC CPU负载会明显上升,严重时甚至影响设备逻辑执行。S7协议本身很轻,但每条请求都有TCP握手之外的协议开销,如果每100ms读取几十个变量,实际上产生了大量数据包。
最好的优化是合并读取:把连续的DB字节一次性读回来,再在本地解析,而不是一次读一个变量。比如要读DB1中20个浮点数,一次ReadBytes(DataType.DataBlock, 1, 0, 80, buffer)就够了,比20次单变量读取高效得多。如果必须高频轮询,建议把轮询周期降到200ms以上,并只在数据变化时刷新UI,这样对PLC和上位机双方都友好。
我在实际项目中的体会是,S7.Net和Sharp7都不是万能钥匙,但它们组合起来基本覆盖了90%的西门子PLC数据交互场景。最烦人的往往不是库本身,而是现场网络、PLC设置和数据结构理解。开发前花半天时间把PLC侧的地址表、连接机制和网络环境摸清楚,比卡在实现阶段再反复试错要省太多时间。最后再分享一个小技巧:正式上设备前,先在一台笔记本上模拟PLC侧通信,把两套库的读写逻辑全部跑通,再带到现场联调,这样效率最高,也最不容易在客户现场翻车。
本文还有配套的精品资源,点击获取