简介:面向需要使用西门子S7-300 PLC与上位机进行以太网TCP通讯的工程师,这份C#开发的通讯程序示例提供了完整的模块化工程。程序基于.NET4.0,采用类似OPC的Tag标签方式,通过XML配置文件即可实时读写I、Q、PI、PA、M、DB等寄存器,支持断线重连与至少10路PLC并发通讯,每路读写点可达2万点,适合作为二次开发或学习参考。资源包共含26个文件,压缩后仅174KB,其中以C#源文件(9个cs)、窗体布局资源(3个resx)、可执行程序(2个exe)及动态库(2个dll)为主,另附配置文件、解决方案与使用说明文档,结构清晰便于直接编译运行。已有3489人学习下载。内容预览中可见VS2010测试Demo源代码、使用说明及PLC通讯帮助项目,读者可快速掌握基于标签的读写封装思路、断线重连机制和多PLC管理模型,省去从零搭建通讯框架的时间。
1. 西门子S7-300与上位机以太网TCP通讯:先确认硬件和协议再动手
现场一台S7-300要跟车间MES上位机做以太网通讯,很多人第一想法是下载一套现成TCP通讯程序直接刷进去。真正卡住的往往不是程序本身,而是这台CPU到底有没有PN口、走的哪种协议、上位机该用库还是手写Socket。这套西门子S7-300与上位机以太网TCP通讯程序,覆盖硬件组态、PLC侧通讯块、上位机客户端和调试文档,能帮新入场的人两小时内跑通一个完整交互,也让老工程师少踩几个已知的坑。下面按选型、组态、写码、排坑的顺序展开,每步都能对着做。
实际工作里栽在同一件事上的人不少:把带TCON的PLC程序下到一台老S7-300,编译却报指令不存在;或者上位机用普通TcpClient直接发业务报文,PLC理都不理。资源的价值不在代码本身,在于通讯路径讲清了。适合刚接手工业以太网改造的现场人员,也适合需要把MPI/DP改成TCP的老产线维护者。
2. 组态与协议选型:PN口、CP343-1与三条通讯路径怎么选
2.1 先确认硬件:你的S7-300走哪种网
拿到程序别急着烧写,先开STEP7看组态里有没有网口。S7-300的以太网方案基本三种:第一种是CPU自带PN口,型号像315-2 PN/DP、317-2 PN/DP,CPU属性里直接有Ethernet选项卡,这种情况最省事,上位机直接走这个网口;第二种是CPU不带PN,主机架上插一块CP343-1,以太网功能由CP承担,这也是老产线最常见的做法,手头这份资源里的PLC侧程序大部分对应这种结构;第三种是连CP343-1都没有,只有MPI口,那必须加硬件,否则程序包里的通讯块根本下载不进CPU。
判断方法很简单:看CPU订货号里带不带PN字样,比如6ES7 315-2EH14是PN版,6ES7 315-2AG10就不带;更直接的办法是看STEP7里CPU属性页有没有“Interface: Ethernet”标签。CP343-1又分老款CP343-1和新款CP343-1 Lean,组态时必须选对订货号和固件版本,选错了下载组态时会提示“模块类型不一致”。这个动作决定了整个通讯走哪条路,值得花十分钟确认清楚。
2.2 协议选型:S7COMM、ISO-on-TCP还是裸TCP
看到“TCP通讯”就以为只能自己写Socket裸收发的人不在少数,其实西门子S7-300上常见三条路,选错会多写很多无用代码。
第一条路S7COMM:西门子私有协议,运行在TCP 102端口之上,PLC作为被动服务端,上位机用现成协议库(比如S7.Net.Plus)直接读写DB块、M区、定时器。最省事,PLC侧不需要写任何通讯程序,硬件组态配好IP即可,适合MES要数据、报表上送、集中监控这类场景。我做过的一个项目里上位机要读30个DB块和50个M区,这条路径两天就稳定跑完。
第二条路ISO-on-TCP(开放式TCP通讯):RFC1006规范,本质是TCP之上包了一层TPKT/COTP,端口还是102。PLC侧需要调用TCON、TSEND、TRCV这些通讯块,可以主动推数据,也可以被动接收,适合自定义报文协议、上位机主动拉数据的场景。这份资源包里的主要代码就是走这条路。
第三条路裸TCP:S7-300对裸TCP的支持一直比较弱,旧CPU和CP343-1组合下很多功能根本不可用,就算能跑也不稳定,工业现场不建议碰。这里放一张对比表,方便你对着选型:
| 路径 | PLC侧工作量 | 上位机工作量 | 适用场景 | 稳定性 |
|---|---|---|---|---|
| S7COMM | 只需组态IP | 用协议库直接读写 | MES数据采集 | 高 |
| ISO-on-TCP | TCON/TSEND/TRCV | 需做COTP握手 | 自定义报文、主动推送 | 高 |
| 裸TCP | 部分模块不支持 | 需自己实现全协议 | 极少用 | 低 |
所以你先别急着抄代码,先问自己:上位机要主动读,还是PLC要主动推?读多推少就选S7COMM,PLC要主动发报文给上位机就选TCON这条ISO-on-TCP。如果资源里PLC侧程序带了TCON、TSEND、TRCV字样,那上位机必须实现COTP握手,不能用普通TcpClient直接发业务数据,这一点第四章会细讲。
2.3 硬件组态与IP规划:动手前的十分钟
半数通讯失败都发生在组态没做完。拿CP343-1举例,步骤是:打开STEP7 V5.x,新建或打开S7-300站,双击硬件组态,把电源、CPU、CP343-1按机架槽位拖上去;再双击CP343-1,新建以太网子网并分配IP和掩码,保存编译。编译通过不代表IP已经生效,必须把硬件组态下载到PLC并停机重启一次,CP343-1才会真正开始监听102端口。
IP规划建议单独划一段:PLC固定192.168.0.10、上位机固定192.168.0.100,掩码统一255.255.255.0,不设网关,避免和现场HMI、触摸屏同网段冲突。如果上位机里有工业网卡或虚拟网卡,把无关网卡禁用掉,很多连不上其实是Windows路由走错了网卡。另一点要注意:在NetPro连接表里新建TCP连接时,连接方向要把“主动”给上位机,“被动”给PLC。如果PLC侧TCON也配成主动,就会出现两边都在等对方连的尴尬局面,连接永远建立不起来。
3. PLC侧程序编写:TCON/TSEND/TRCV的调用顺序与参数
3.1 TCON连接块:建立TCP连接的固定套路
PLC如果要主动推数据,第一步不是直接把数据塞给TSEND,而是先建立连接。TCON就是干这个的,在STEP7里调用方式很固定:
CALL "TCON" REQ := M0.0 ID := 1 DONE := M0.1 BUSY := M0.2 ERROR := M0.3 STATUS := MW10 CONNECT:= DB1REQ是请求信号,用上升沿触发;ID是连接ID,必须跟组态里设置的ID一致;DONE表示连接建立完成;BUSY表示正在建立;STATUS是错误码;CONNECT指向UDT65类型的连接配置DB。这几个信号位要单独安排,不要重复使用。
CONNECT的配置要落在DB1里,按UDT65结构填写:本地端口可以设102,远端IP写上位机IP,远端端口写102或按自己协议定义。方向设置分两种:PLC主动连上位机时,把active connection置1,同时上位机侧要开一个TCP Server监听;PLC作为服务端时,TCON只需要配置本地端口102,等上位机来连即可。顺序上记住一句话:TCON先出DONE,后面TSEND才有意义。
3.2 TSEND/TRCV收发:缓冲区与长度怎么定
连接建立好之后,发送和接收分别用TSEND和TRCV。发送的调用示例:
CALL "TSEND" REQ := M0.4 ID := 1 LEN := 20 DONE := M2.0 BUSY := M2.1 ERROR := M2.2 STATUS := MW20 DATA := P#DB10.DBX0.0 BYTE 20LEN是发送字节数,这里填20;DATA指向发送缓冲区,通常放在DB10的起始地址;REQ同样用上升沿触发。TSEND完成后DONE信号M2.0会亮,BUSY为1时说明上一次还没发完,连续触发会被忽略,所以发送逻辑里要加BUSY互锁。
接收用TRCV,参数结构和TSEND类似,但有个RCV_LEN比较特殊:如果不知道对端会发多少字节,LEN填期望的最大长度,实际收到的字节数会写在RCV_LEN里。常见做法是PLC作为服务端接收上位机指令,把固定报文长度定成12字节或20字节,PLC侧LEN就填这个固定长度,这样数据边界清晰,上位机不用做粘包解析。
3.3 调用时序:OB100初始化与OB1轮询
新手最常见的问题是:把TCON放进OB1每个循环都执行,结果连接刚建立又被切断。我一般这么做:OB100暖启动里只做参数初始化和缓冲区清零,不做连接;OB1里用M0.0上升沿触发一次TCON,DONE还没亮之前,不要进第二轮。上位机断开后要重新连接,需要先复位TCON——有些固件版本提供了TCON_RESET,没有的话就把连接ID置0,等一个扫描周期再重新触发,否则会收到连接资源占用错误。
收发周期也要控制。TSEND不要每个扫描周期都触发,用一个100ms定时脉冲做REQ;一次要发几十个字,不要拆成几个小块分开发,合并在一个DB里一次TSEND出去,效率和稳定性都更好。S7-300的通讯块大多是异步执行,BUSY信号做互锁能避开大部分数据乱序。如果通讯一开OB1循环时间就明显变长,把通讯块挪到OB32这类100ms周期OB里执行,别继续塞在OB1里。老软硬件上TCON指令可能不存在,那只能走FC5/AG_SEND的写法,程序包里如果有FC5形式,上位机侧就按ISO-on-TCP处理。
4. 上位机侧连接与读写:C#代码、COTP握手与字节序细节
4.1 最省事的客户端:用现成协议库先跑通
如果选的是S7COMM路线,上位机最省事的方式是直接用开源库S7.Net.Plus。我建议下载到资源后,先把这一步跑通,再去看手写TCP代码,因为手写代码一旦连不上,你很难判断是握手协议问题还是数据格式问题。示例代码:
using S7.Net; var plc = new Plc(CpuType.S7300, "192.168.0.10", 0, 2); plc.Open(); // 读取 DB10 前 20 字节 var data = plc.ReadBytes(DataType.DataBlock, 10, 0, 20); for (int i = 0; i < data.Length; i++) { Console.WriteLine($"DB10.DBB{i} = {data[i]}"); } plc.Close();CpuType.S7300要和实际CPU型号匹配,老型号选S7300或S7300Plus,带PN口的CPU选对应型号;rack和slot默认是0和2,但CP343-1挂在槽位4的场合,slot要填4,很多连不上的问题就出在这。Open时会自动完成COTP握手和PDU协商,不用自己写协议,这是这条路径最大的价值。
4.2 手动组COTP报文:不依赖库时客户端怎么写
如果PLC侧用的是TCON/FC5这条ISO-on-TCP路线,上位机就不能直接拿TcpClient发业务数据,必须先做COTP握手。很多内网环境不准引入第三方库,手动写握手代码更让人踏实。原理是TCP连接建立后先发一个COTP连接请求,服务器回连接确认,之后才能发业务报文。
COTP握手报文是固定格式,19字节,C#写法:
// COTP Connection Request (CR TPDU) byte[] cotp = new byte[] { 0x03, 0x00, 0x00, 0x16, // TPKT头,长度22 0x11, 0xE0, 0x00, 0x00, // COTP CR TPDU 0x00, 0x01, 0x00, 0xC0, 0x01, 0x0A, 0x01, 0x02, // 本地TSAP 0xC0, 0x03, 0x06 }; client.Client.Send(cotp); byte[] resp = new byte[22]; client.Client.Receive(resp); // resp[0]==0x03 && resp[1]==0x00 说明收到CC TPDU前4字节是TPKT头,0x03 0x00加总长度;0x11是CR TPDU,0xE0表示协议类别是ISO-on-TCP;0x0001是引用号;后面是TSAP参数。不同版本细节有差异,但结构固定,调试时打开Wireshark,过滤tcp.port == 102,对着看自己发的包和PLC回包,能很快定位问题。
COTP之后,S7COMM报文由TPKT、COTP DT TPDU、S7COMM头、参数区和数据区组成。读DB10前20字节就需要组一个“Read Var”请求,包含请求ID、对象地址和长度,代码篇幅比较长。资源包里我单独写了一个构建函数,接收DB号和字节长度,返回发送字节数组,这份资源里也包含这个类的完整源码。
4.3 读写DB块的字节序与类型转换
S7和西门子的数据都是大端字节序,C#是小端,读取DINT和REAL必须做字节序交换,这是数据出现“天文数字”的头号原因。转换代码:
// REAL 读取:4字节大端转小端再按float解析 byte[] raw = new byte[4]; Array.Copy(data, offset, raw, 0, 4); if (BitConverter.IsLittleEndian) Array.Reverse(raw); float value = BitConverter.ToSingle(raw, 0); // BOOL 读取:S7的Bool在字节内按位 int byteIndex = 12; int bitIndex = 3; bool b = (data[byteIndex] & (1 << bitIndex)) != 0;offset是相对缓冲区起始的字节偏移;REAL按IEEE754存储,西门子REAL就是C#里的float;BOOL的位号从0到7,比如DB10.DBX12.3就是第12字节第3位。同一个DB区里混合了REAL和BOOL时,地址计算要按字节对齐来推,REAL占4字节,DINT也占4字节,这些偏移每写错一位,后期排查都要花几小时。
调试时最笨也最有效的办法:在PLC里写一个已知值,比如DB10.DBB0里写16#AB,上位机读出来如果和你预期不一致,就说明字节序或地址偏移有问题。这个验证动作我每次必做,比看代码更快。
5. 现场排查记录:以太网通讯最容易翻车的五个坑
5.1 连接类故障:连不上、连上就断
坑一:上位机连不上PLC的102端口。现象是TCP连接超时,telnet IP 102不通,但ping得通。原因通常是CP343-1的IP没有下载生效,或者Windows防火墙拦了入站102端口。解决步骤:先ping,再telnet,确认网络层和应用层通没通;防火墙加一条入站规则放行102;还不行就把PLC断电重启,CP343-1固件加载后约5秒才开始监听端口。我碰到过两次都是STEP7改了IP但没下载,PLC还在用上一个旧IP。
坑二:PLC主动发送时DONE一直不亮,STATUS报“连接正在建立”或连接资源错误。外观上看上位机一直在监听,但两边始终握不上手。原因是TCON方向配错了——PLC配成主动,上位机也配成主动,两个都在等对方连;或者连接ID和CONNECT配置里的ID不一致。解决方法是明确方向:上位机主动连PLC,PLC侧TCON配被动;PLC主动推数据,上位机必须有Server在听。STATUS错误码对照手册查,8xxx开头大多是连接资源占用或方向问题。
5.2 数据类故障:数据错位、丢包与CPU负载
坑三:读回REAL变成天文数字。现象是PLC里监视值是25.8,上位机读出来是1e-26这类乱数。原因就是字节序没转,S7存储用大端,C#读取前必须Array.Reverse。解决方法是把字节序交换统一封装到一个函数里,所有REAL、DINT都走这同一个转换入口,再跑一遍已知值验证。
坑四:断线重连失败,不重启PLC无法恢复。现象是上位机程序重启或网络断开后重连,始终报连接超时,PLC侧TCON一直显示连接被占用。原因是PLC侧的连接资源没有释放,旧连接还挂在TCON上。解决方法是PLC侧断开后先复位TCON,把连接ID清零,延迟5秒再重建;或者上位机断开前先主动关闭套接字,让PLC感知到TCP断开。有些现场等30秒资源会自动释放,但那不是可持续的方案。
坑五:通讯一开,OB1循环时间暴涨到几十毫秒以上。现象是没开通讯时循环10ms,通讯启动后飙到55ms甚至100多ms,其他逻辑开始卡顿。原因是TSEND/TRCV每个循环都触发,而且数据被拆成很多小份频繁发送,通讯块吃掉了太多CPU时间。解决方法是把通讯块放到OB32这类100ms周期OB里执行,REQ用脉冲触发,数据打包成DB一次发送。如果上位机要轮询,轮询周期放到200ms以上,别高频读写。我自己的习惯是发送周期低于100ms就不做了,工业交互稳定比快更重要。
5.3 排查顺序和抓包工具的使用
遇到通讯怪问题别急着翻手册,我固定顺序是:先看物理层——网口LED、ping、telnet;再看组态——IP、ID、方向;然后看数据——抓包摘要和字节序;最后才动PLC程序。Wireshark里过滤tcp.port == 102,能看到COTP和S7COMM报文,重点看PDU协商数值:客户端请求480而PLC应答240,说明固件或上位机库配置过高,要把PDU改回240。资源包里自带的文档末尾有一个错误码对应表,排查STATUS时直接翻那个表,比现场翻手册快很多。
6. 进阶用法:心跳、看门狗与收尾验收习惯
6.1 心跳帧让通讯状态变得可见
通讯能跑通只是第一步,真正的问题是数据断了你多久能发现。我每次做这类通讯都会定义一个心跳帧:PLC侧固定每500ms向上位机发一条12字节报文——前4字节是递增计数器,中间4字节是系统运行时间,最后4字节是最近一次通讯状态位。上位机连续2秒没收到心跳,就把“通讯中”置为“通讯断开”,界面上弹出明显横幅。这个设计的价值在于所有数据都带时间戳,回溯时看心跳就能判断是停电、断网还是PLC程序卡死,省掉大量猜测。实现也很简单,在OB32里调TSEND发这12字节就行,不用占用OB1周期,也不需要上位机请求。
6.2 验收清单:交付前跑一遍这六项
通讯程序写完之后,别急着说跑通了,我给自己定了一个验收清单,每次下载类似资源复现或现场交付都强制走一遍:
第一项:冷机启动,PLC停电后重新上电,上位机重启,通讯自动恢复,不需要手动干预。第二项:热拔网线60秒再插回,上位机能自动重连,PLC侧不发重复数据块。第三项:抓包确认COTP握手正常,请求和响应一一对应,没有严重重传。第四项:用已知值验证字节序和地址偏移,PLC里写0xAB,上位机读到0xAB。第五项:连续运行24小时不掉线,记录CPU循环时间不超设计余量。第六项:把上位机换成另一台电脑再跑一遍,确认不依赖特定网卡或驱动。
能跑完这六项,这套以太网通讯程序才算真在你现场落了地。数据偶发错位、重启必断这些老大难问题,几乎都能归到其中一项。从那以后我每次拿到S7-300的通讯资源,第一件事不是趴在代码里看,而是先搜索工程里的IP、连接ID和报文长度这三个参数,把硬件地址标在组态图上再烧写一次,这个方法让我少走了很多回头路,希望帮到你。
本文还有配套的精品资源,点击获取