PLC内部数据的传统读写方式是利用触摸屏组态。PLC可以和HMI轻易建立起连接并传输数据。而S7netplus为我们提供了另一种连接思路,让我们可以利用C#读写PLC内部的数据。这样做的优点是显而易见的:可以做出比WinCC更漂亮更强大的界面;可以对数据更好地利用;可以以更高频率获取PLC数据而不是10Hz等等。

上图是我用S7netplus做的调试小工具。虽然只有简单的功能,却可以在现场调试时省的抱着笔记本等博途启动。顺便吐槽一下西门子,自己的软件互相之间都不兼容,安装麻烦得要死。就算运气好都安上了,稍微差点的电脑还带不动,一启动就等很久。不过谁让人家是业界巨头呢,难用也只能忍着。
回到正题。S7netplus使用起来非常简单,但是它的文档不太好。只有英文版倒是问题不大,主要是不全,只介绍了一部分内容,还和实际操作不太一样。也可能是我没找到最新版的文档吧。
安装与连接
前提
硬件方面只需要用网线把PLC与电脑相连,处于同一网段即可。本次测试使用的是西门子S7-1200 1215C DC/DC/DC以及西门子Tp700 Comfort面板。
与PLC通讯前首先要让PLC处于可访问状态。设置方法如下:
第一步:打开博途,在项目树找到PLC打开属性。将保护设置为完全访问权限,并将下面连接机制的选项打钩。

第二步:在PLC下面依次打开每一个用到的DB块的属性。关闭“优化块的访问”这个选项。否则没有偏移量不能访问。点击确定后编译DB块即可。

更改完成之后需要在设备组态中编译并下载才能使设置生效。

S7netplus目前最新的版本是0.4.0,需要Visual Studio 2015及以上版本。框架要求.Net Framework 4.5.2。支持使用Nuget安装:
PM> Install-Package S7netplus -Version 0.4.0
安装好之后不需要其他操作,添加:
using S7.Net;
之后就可以用了。
连接
S7netplus支持的CPU包括:LOGO!0BA8,S7-200,S7-300,S7-400,S7-1200,S7-1500。(后来SMART支持PN通讯之后也可以连SMART了,更新了可能有好一阵子了,我最近才发现_2022.3.11)
plc连接的定义为:
public Plc(CpuType cpu, string ip, short rack, short slot);
四个参数分别是:CPU类型,CPU的IP地址,CPU的机架号(Rack)与插槽号(Slot)。

连接与断开方式如下:
namespace s7netplus
{
public partial class Form1 : Form
{
private Plc plc = null;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
toolStripStatusLabel2.Text = "就绪";
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (plc.IsConnected)
{
plc.Close();
}
}
private void connbtn_Click(object sender, EventArgs e)
{
try
{
plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1);
plc.Open();
if (plc.IsConnected)
{
toolStripStatusLabel2.Text = "已建立连接";
}
else
{
toolStripStatusLabel2.Text = "连接失败";
}
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message);
}
}
private void cutbtn_Click(object sender, EventArgs e)
{
if (plc.IsConnected)
{
plc.Close();
toolStripStatusLabel2.Text = "连接已断开";
}
}
}
}
文档中的Open()
函数类型为ErrorCode
,可以使用ErrorCode state = plc.Open();
来获取连接的状态码。但在0.4.0版本实际使用中发现状态码仍然存在,但Open()
函数声明为void
。因为查不到更多资料,此处获取状态码的方式未知。
数据操作
数字读写
S7netplus提供了对五种数据的读写方法:Word,Int,DWord,DInt,Real。每一种类型需要不同的转换方式。具体方法如下:
private void readbtn_Click(object sender, EventArgs e)
{
if (plc == null)
{
MessageBox.Show("尚未建立与PLC的连接");
}
else
{
if (plc != null)
{
switch (typebox.Text)
{
case "Word":
ushort result1 = (ushort)plc.Read("DB1.DBW0");
readtxt.Text = result1.ToString();
break;
case "Int":
short result2 = ((ushort)plc.Read("DB1.DBW0")).ConvertToShort();
readtxt.Text = result2.ToString();
break;
case "DWord":
uint result3 = (uint)plc.Read("DB1.DBD0");
readtxt.Text = result3.ToString();
break;
case "DInt":
int result4 = ((uint)plc.Read("DB1.DBD0")).ConvertToInt();
readtxt.Text = result4.ToString();
break;
case "Real":
double result5 = ((uint)plc.Read("DB1.DBD0")).ConvertToFloat();
readtxt.Text = result5.ToString();
break;
default:
break;
}
toolStripStatusLabel2.Text = "读取成功";
}
}
}
private void writebtn_Click(object sender, EventArgs e)
{
if (plc == null)
{
MessageBox.Show("尚未建立与PLC的连接");
}
else
{
if (plc != null)
{
switch (typebox.Text)
{
case "Word":
ushort value1 = ushort.Parse(writetxt.Text);
plc.Write("DB1.DBW0", value1);
break;
case "Int":
short value2 = short.Parse(writetxt.Text);
plc.Write("DB1.DBW0", value2);
break;
case "DWord":
uint value3 = uint.Parse(writetxt.Text);
plc.Write("DB1.DBD0", value3);
break;
case "DInt":
int value4 = int.Parse(writetxt.Text);
plc.Write("DB1.DBD0", value4);
break;
case "Real":
double value5 = double.Parse(writetxt.Text);
plc.Write("DB1.DBD0", value5);
break;
default:
break;
}
toolStripStatusLabel2.Text = "数据已写入";
}
}
}
布尔读写
S7netplus并没有直接提供单个位的查询。但是提供了字节查询,并附带了直接从字节提取位的方法:
byte[] myByte = plc.ReadBytes(...);
byte myByte[0] = 5; // 0000 0101
myByte.SelectBit(0) // true
myByte.SelectBit(1) // false
读写的定义如下:
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count);
public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value);
int db
默认为0,startByteAdr
为开始地址,bitAdr
为位。
//读辅助继电器M(从MB0读一个字节)
byte[] result = plc.ReadBytes(DataType.Memory, 0, 0, 1);
//读输入I(IB0)
byte[] result = plc.ReadBytes(DataType.Input, 0, 0, 1);
//读输出Q(QB0)
byte[] result = plc.ReadBytes(DataType.Output, 0, 0, 1);
//写辅助继电器M(置位M0.1)
plc.WriteBit(DataType.Memory, 0, 0, 1, true);
//写辅助继电器M(复位M0.2)
plc.WriteBit(DataType.Memory, 0, 0, 2, false);
字符(串)读写
字符串读取比较麻烦,因为其长度是不定的,S7netplus好像也没提供一个便捷的读写方法。我自己瞎琢磨出了读写方法,不算复杂。
DB块存String类型时默认分配256个字节,使用时只占用一部分。以DB1偏移量0.0为例:
该数据的第一个字节DB1.DBB0
为16#FE
。第二个字节DB1.DBB1
存储的byte型数据为字符串长度n。从DB1.DBB2
开始的n个字节是字符串的内容。依次读取这n字节,获取的是十进制数据,转化为字符后连接成字符串即可。Char类型读取方法类似:
private string GetString(int db, int addr)
{
string addr1 = "DB" + db.ToString() + ".DBB" + (addr + 1).ToString();
int len = int.Parse(plc.Read(addr1).ToString());
string getstring = "";
for (int i = 0; i < len; i++)
{
string addr2 = "DB" + db.ToString() + ".DBB" + (addr + 2 + i).ToString();
string result = plc.Read(addr2).ToString();
int value = Convert.ToInt32(result, 10);
string stringValue = Char.ConvertFromUtf32(value);
getstring += stringValue;
}
return getstring;
}
Char写入就比较简单了,不需要转换,直接对地址写入string类型的单个字符即可。String需要先写入长度,再依次写入每一个字符。第一位的16#FE
至今不知含义,但不影响读写。String的拆分和写入如下:
private void PutString(int db, int addr, string data)
{
byte len = Convert.ToByte(data.Length);
string addr1 = "DB" + db.ToString() + ".DBB" + (addr + 1).ToString();
plc.Write(addr1, len);
char[] single = data.ToCharArray();
for (int i = 0; i < data.Length; i++)
{
string addr2 = "DB" + db.ToString() + ".DBB" + (addr + 2 + i).ToString();
plc.Write(addr2, single[i].ToString());
}
}
偷偷放一个自己做的调试小工具
下载 “西门子PLC简易调试工具v2.0.0” Siemens PLC S7netplus.msi – 16 MB
附:含中文字符串处理
此方法由 @小白 提供。
据 @小白 考证,数据第一字节的含义为整个字符串的长度,参考来源见此。读取含中文字符串需要引入using System.Web;
,直接输入using
指令会无效,需要在项目资源管理器引用System.Web,读取方法如下:
private string GetStringGBK(Plc plc, int db, int addr)
{
//测试字符串为"测试abc123测试@#"
string getstring = string.Empty;
string addr1 = "DB" + db.ToString() + ".DBB" + (addr + 1).ToString();
int len = int.Parse(plc.Read(addr1).ToString());
var bytes = plc.ReadBytes(DataType.DataBlock, db, addr + 2, len);
int i = 0;
while (i < len)
{
if (bytes[i] > 127)
{
byte[] b = new byte[] { bytes[i], bytes[i + 1] };
//当字符小于127位时,与ASCII字符相同,但当两个大于127位的字符连接在一起时,表示一个汉字
string result = HttpUtility.UrlEncode(b);
string a = HttpUtility.UrlDecode(result, Encoding.GetEncoding("GB2312"));
getstring += a;
i += 2;
}
else
{
//除中文外字符为单个字节,且其编码与ASCII,相同,直接用ASCII转换就可以正确读取字符了。
byte[] b = new byte[] { bytes[i] };
getstring += Encoding.ASCII.GetString(b);
i++;
}
}
return getstring;
}
写入含中文字符串:
private void PutStringGBK(Plc plc, int db, int addr, string data)
{
string addr1 = "DB" + db.ToString() + ".DBB" + (addr + 1).ToString();
byte[] gbk = Encoding.GetEncoding("GBK").GetBytes(data);
byte len2 = Convert.ToByte(gbk.Length);
plc.Write(addr1, len2);
plc.WriteBytes(DataType.DataBlock, db, addr+2, gbk);
}
小白寄语:
这个测试读取我写入的中文字符串是没办法正确读取的,原因是
@小白s7.net
的源码为return System.Text.Encoding.ASCII.GetString(bytes);
他用了ASCII解码,而GB2312是基于ASCII上的(算是拓展吧),那就很奇怪为什么,在PLC直接输入中文字符,能用这个方法读取了。。。。,不过为了弥补读取我的写入方法的读取方法(感觉有点绕口哈哈),方法也提供在了评论区了哦。希望大家共同进步
再次感谢小白,呱唧呱唧