C#读写三菱PLC和西门子PLC数据 使用TCP/IP 协议

时间:2024-04-17 16:31:50

引用:https://www.cnblogs.com/woxihuadabai/articles/8037872.html

本文将使用一个NuGet公开的组件技术来读写三菱PLC和西门子plc数据,使用的是基于以太网的TCP/IP实现,不需要额外的组件,读取操作只要放到后台线程就不会卡死线程,本组件支持超级方便的高性能读写操作

 

在Visual Studio 中的NuGet管理器中可以下载安装,也可以直接在NuGet控制台输入下面的指令安装:

1
Install-Package HslCommunication

 如果需要教程:Nuget安装教程:http://www.cnblogs.com/dathlin/p/7705014.html

 

技术支持QQ群:592132877  (组件的版本更新细节也将第一时间在群里发布)最后编辑日期:2017年12月7日 11:03:56

里面各种小伙伴,为您解答数据交互,编程技巧,如果对本界面提供的API有任何疑问,都可以加群咨询,如果有更好的建议,欢迎提出。

 

如果你需要在读取PLC数据之后,还要群发客户端来实现远程办公室同步监视,可以参考如下的项目(基于该组件扩展起来的,带有账户验证,版本控制,数据群发,公告管理等等功能)

https://github.com/dathlin/ClientServerProject

 

本文将展示如何配置网络参数及怎样使用代码来访问PLC数据,希望给有需要的人解决一些实际问题。主要对三菱Q系列PLC的X,Y,M,L,B,V,F,S,D,W,R区域的数据读写,对西门子PLC的M,Q,I,DB块的数据读写,亲测有效。

此处使用了网线直接的方式,如果PLC接进了局域网,就可以进行远程读写了^_^

此处使用到了2个命名空间:

1
2
using HslCommunication;
using HslCommunication.Profinet;

 

随便聊聊


当我们一个上位机需要读取100台西门子PLC设备(此处只是举个例子,凡是都是使用Modbus tcp的都是一样的)的时候,你采用服务器主动去请求100台设备的机制对性能来说是个极大的考验,如果开100个线程去轮询100台设备,那么性能损失将是非常大的,更不用说再增加设备,如果搭建Modbus tcp服务器,就可以完美的解决性能问题,因为连接的压力将会平均分摊给每一台PLC,服务器端只要新增一个时间戳就可以知道客户端有没有连接上。

我们在100台PLC里都增加发送Modbus tcp方法,将数据发送到服务器的ip和端口上去,服务器根据站号来区分设备。这样就可以搭建一个高性能总站。 本组件支持快速搭建一个高性能的Modbus tcp总站。

http://www.cnblogs.com/dathlin/p/7782315.html

 

关于两种模式


在PLC端,包括三菱和西门子篇二以及Modbus Tcp客户端的访问器上,都支持两种模式,短连接模式和长连接模式,现在就来解释下什么原理。

短连接:每次读写都是一个单独的请求,请求完毕也就关闭了,如果服务器的端口仅仅支持单连接,那么关闭后这个端口可以被其他连接复用,但是在频繁的网络请求下,容易发生异常,会有其他的请求不成功,尤其是多线程的情况下。

长连接:创建一个公用的连接通道,所有的读写请求都利用这个通道来完成,这样的话,读写性能更快速,即时多线程调用也不会影响,内部有同步机制。如果服务器的端口仅仅支持单连接,那么这个端口就被占用了,比如三菱的端口机制,西门子的Modbus tcp端口机制也是这样的。以下代码默认使用长连接,性能更高,还支持多线程同步。

在短连接的模式下,每次请求都是单独的访问,所以没有重连的困扰,在长连接的模式下,如果本次请求失败了,在下次请求的时候,会自动重新连接服务器,直到请求成功为止。另外,尽量所有的读写都对结果的成功进行判断。

 

关于日志记录


不管是三菱的数据访问类,还是西门子的,还是Modbus tcp访问类,都有一个LogNet属性用来记录日志,该属性是一个接口类,ILogNet,凡事继承该接口的都可以用来记录日志,该日志会在访问失败时,尤其是因为网络的原因导致访问失败时会进行日志记录(如果你为这个LogNet属性配置了真实的日志记录器的话):如果你想使用该记录日志的功能,请参照如下的博客进行实例化:

http://www.cnblogs.com/dathlin/p/7691693.html

 

 

演示项目


下面的三篇演示了具体如何去访问PLC的数据,我们在访问完成后,通常需要进行处理,以下的示例项目就演示了后台从PLC读取数据后,前台显示并推送给所有在线客户端的功能,客户端并进行图形化显示,具有一定的参考意义,项目地址为:

https://github.com/dathlin/RemoteMonitor

下面的图片示例中的左边程序就是服务器程序,它应该和PLC直接连接并接入局域网,然后把数据推送给客户端显示。注意:一个复杂高级的程序就应该把处理逻辑程序和界面程序分开,比如这里的服务器程序实现数据采集,推送,存储。让客户端程序去实现数据的整理,分析,显示,这样即使客户端程序因为BUG奔溃,服务器端仍然可以正常的工作。

 

 

 

三菱PLC篇(如果你的PLC内置了以太网,请配置MC协议通讯)

 


 

Q06UDV Plc的访问测试感谢网友:hwdq0012

感想网友:小懒猪雨中人  的测试,VB程序也可以调用本通讯库


环境:此处以GX Works2为示例,添加以太网模块,型号为QJ71E71-100,组态里添加完成后进行以太网的参数配置,此处需要注意的是:参数的配置对接下来的代码中配置参数要一一对应




注意:在PLC的以太网模块的配置中,无法设置网络号为0,也无法设置站号为0, 所以此处均设置为1,在C#程序中也使用上述的配置,在代码中均配置为0,如果您自定义设置为网络2, 站号8,那么在代码中就要写对应的数据。如果仍然通信失败,重新测试0,0。

打开设置:在上图中的打开设置选项,进行其他参数的配置,下图只是举了一个例子,开通了4个端口来支持读写操作:


端口号设置规则:

  • 为了不与原先存在的系统发生冲突,您在添加自己的端口时尽量使用您自己的端口。
  • 如果读写都需要,尽可能的将读取端口和写入端口区分开来,这样做比较高性能。
  • 如果您的网络状态不是特别稳定,读取端口使用2个,一个受阻切换另一个读取可以提升系统的稳定性。


本文档仅作组件的测试,所以只用了一个端口作为读写。如果你的程序也使用了一个端口,那么你在读取数据时候, 刚好也在写入(异步操作可能发生这样的情况),那么写入会失败!)(在长连接模式下没有这个问题)

三菱PLC的数据主要由两类数据组成,位数据和字数据,在位数据中,例如X,Y,M,L都是位数据,字数据例如D,W。 两类的数据在读取解码上存在一点小差别。(事实上也可以先将16个M先赋值给一个D,读取D数据再进行解析, 在读取M的数量比较多的时候,这样操作效率更高)

初始化访问PLC对象

如果想使用本组件的数据读取功能,必须先初始化数据访问对象,根据实际情况进行数据的填入。 下面仅仅是测试中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private MelsecNet melsec_net = new MelsecNet();
private void MelsecNetInitialization()
{
    //初始化
    melsec_net.PLCIpAddress = System.Net.IPAddress.Parse("192.168.0.7");  // PLC的IP地址
    melsec_net.PortRead = 6000;                                           // 端口
    melsec_net.PortWrite = 6001;                                          // 写入端口,最好和读取分开
    melsec_net.NetworkNumber = 0;                                         // 网络号
    melsec_net.NetworkStationNumber = 0;                                  // 网络站号
    melsec_net.ConnectTimeout = 500;                                      // 连接超时时间
 
    // 如果需要长连接,就取消下面这行代码的注释,对于数据读写的代码,没有影响
    melsec_net.ConnectServer();                                           // 切换长连接,这行代码可以放在其他任何地方
    // melsec_net.ConnectClose();                                         // 关闭长连接,并切换为短连接,在系统退出时可以调用
}

 
说明:对象应该放在窗体类下面,此处仅仅针对读取一台设备的plc,也可以在访问的方法中实例化局部对象, 初始化数据,然后读取,该对象几乎不损耗内存,内存垃圾由CLR进行自动回收。此处测试方便,窗体的多个按钮均连接同一台PLC 设备,所以本窗体实例化一个对象即可。

 

关于两种地址的表示方式

第一种,使用系统的类来标识,比如M200,写成(MelsecDataType.M, 200)的表示形式,这样也可以去MelsecDataType里面找到所有支持的数据类型。

第二种,使用字符串表示,这个组件里所有的读写操作提供字符串表示的重载方法,所有的支持访问的类型对应如下,字符串的表示方式存在十进制和十六进制的区别:

  • 输入继电器:"X100","X1A0"            // 字符串为十六进制机制
  • 输出继电器:"Y100" ,"Y1A0"           // 字符串为十六进制机制
  • 内部继电器:"M100","M200"           // 字符串为十进制
  • 锁存继电器:"L100"  ,"L200"           // 字符串为十进制
  • 报警器:       "F100", "F200"            // 字符串为十进制
  • 边沿继电器:"V100" , "V200"          // 字符串为十进制
  • 链接继电器:"B100" , "B1A0"          // 字符串为十六进制
  • 步进继电器:"S100" , "S200"          // 字符串为十进制
  • 数据寄存器:"D100", "D200"           // 字符串为十进制
  • 链接寄存器:"W100" ,"W1A0"         // 字符串为十六进制
  • 文件寄存器:"R100","R200"            // 字符串为十进制

 

展示一些简单实用基础数据读写,这些数据的读写没有进行严格的是否成功判断(判断方法参照后面的代码),一般网络良好的情况下都会成功,但不排除失败,以下代码仅作测试,所有没有严格判断是否成功:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void userButton22_Click(object sender, EventArgs e)
{
    bool M100 = melsec_net.ReadBoolFromPLC("M100").Content;            // 读取M100是否通,十进制地址
    bool X1A0 = melsec_net.ReadBoolFromPLC("X1A0").Content;            // 读取X1A0是否通,十六进制地址
    bool Y1A0 = melsec_net.ReadBoolFromPLC("Y1A0").Content;            // 读取Y1A0是否通,十六进制地址
    bool B1A0 = melsec_net.ReadBoolFromPLC("B1A0").Content;            // 读取B1A0是否通,十六进制地址
    short short_D1000 = melsec_net.ReadShortFromPLC("D1000").Content;   // 读取D1000的short值  ,W3C0,R3C0 效果是一样的
    ushort ushort_D1000 = melsec_net.ReadUShortFromPLC("D1000").Content; // 读取D1000的ushort值
    int int_D1000 = melsec_net.ReadIntFromPLC("D1000").Content;          // 读取D1000-D1001组成的int数据
    uint uint_D1000 = melsec_net.ReadUIntFromPLC("D1000").Content;       // 读取D1000-D1001组成的uint数据
    float float_D1000 = melsec_net.ReadFloatFromPLC("D1000").Content;    // 读取D1000-D1001组成的float数据
    long long_D1000 = melsec_net.ReadLongFromPLC("D1000").Content;       // 读取D1000-D1003组成的long数据
    double double_D1000 = melsec_net.ReadDoubleFromPLC("D1000").Content; // 读取D1000-D1003组成的double数据
    string str_D1000 = melsec_net.ReadStringFromPLC("D1000", 10).Content; // 读取D1000-D1009组成的条码数据
 
 
     
    melsec_net.WriteIntoPLC("M100"true);                        // 写入M100为通
    melsec_net.WriteIntoPLC("Y1A0"true);                        // 写入Y1A0为通
    melsec_net.WriteIntoPLC("X1A0"true);                        // 写入X1A0为通
    melsec_net.WriteIntoPLC("B1A0"true);                        // 写入B1A0为通
    melsec_net.WriteIntoPLC("D1000", (short)1234);                // 写入D1000  short值  ,W3C0,R3C0 效果是一样的
    melsec_net.WriteIntoPLC("D1000", (ushort)45678);              // 写入D1000  ushort值
    melsec_net.WriteIntoPLC("D1000", 1234566);                    // 写入D1000  int值
    melsec_net.WriteIntoPLC("D1000", (uint)1234566);               // 写入D1000  uint值
    melsec_net.WriteIntoPLC("D1000", 123.456f);                    // 写入D1000  float值
    melsec_net.WriteIntoPLC("D1000", 123.456d);                    // 写入D1000  double值
    melsec_net.WriteIntoPLC("D1000", 123456661235123534L);          // 写入D1000  long值
    melsec_net.WriteAsciiStringIntoPLC("D1000""K123456789");      // 写入D1000  string值
}

 

 

下面再分别讲解严格的操作,以及批量化的复杂的读写操作,假设你要读取1000个M,循环读取1千次可能要3秒钟,如果用了下面的批量化读取,只需要50ms,但是需要你对字节的原理比较熟悉才能得心应手的处理

X,Y,M,L,F,V,B,S位数据的读写说明

  • X 输入继电器
  • Y 输出继电器
  • M 内部继电器
  • L 锁存继电器
  • F 报警器
  • V 边沿继电器
  • B 链接继电器
  • S 步进继电器

 

本小节将展示八种位数据的读取,虽然更多的时候只是读取D数据即可,或者是将位数据批量挪到D数据中, 但是在此处仍然进行介绍单独的读取X,Y,M,L,F,V,B,S,由于这八种读取手法一致,故针对M数据进行介绍,其他的您可以自己测试。

如下方法演示读取了M200-M209这10个M的值,注意:读取长度必须为偶数,即时写了奇数,也会补齐至偶数,读取和写入的最大长度为7168,否则报错。如需实际需求确实大于7168的,请分批次读取。
返回值解析:如果读取正常则共返回10个字节的数据,以下示例数据进行批量化的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private void userButton20_Click(object sender, EventArgs e)
{
    // M200-M209读取显示
    OperateResult<byte[]> read = melsec_net.ReadFromPLC("M200", 10);
    if (read.IsSuccess)
    {
        // 成功读取,True代表通,False代表不通
        bool M200 = read.Content[0] == 1;
        bool M201 = read.Content[1] == 1;
        bool M202 = read.Content[2] == 1;
        bool M203 = read.Content[3] == 1;
        bool M204 = read.Content[4] == 1;
        bool M205 = read.Content[5] == 1;
        bool M206 = read.Content[6] == 1;
        bool M207 = read.Content[7] == 1;
        bool M208 = read.Content[8] == 1;
        bool M209 = read.Content[9] == 1;
        // 显示
    }
    else
    {
        //失败读取,显示失败信息
        MessageBox.Show(read.ToMessageShowString());
    }
}
private void userButton21_Click(object sender, EventArgs e)
{
    // X100-X10F读取显示
    OperateResult<byte[]> read = melsec_net.ReadFromPLC("X200", 16);
    if (read.IsSuccess)
    {
        // 成功读取,True代表通,False代表不通
        bool X200 = read.Content[0] == 1;
        bool X201 = read.Content[1] == 1;
        bool X202 = read.Content[2] == 1;
        bool X203 = read.Content[3] == 1;
        bool X204 = read.Content[4] == 1;
        bool X205 = read.Content[5] == 1;
        bool X206 = read.Content[6] == 1;
        bool X207 = read.Content[7] == 1;
        bool X208 = read.Content[8] == 1;
        bool X209 = read.Content[9] == 1;
        bool X20A = read.Content[10] == 1;
        bool X20B = read.Content[11] == 1;
        bool X20C = read.Content[12] == 1;
        bool X20D = read.Content[13] == 1;
        bool X20E = read.Content[14] == 1;
        bool X20F = read.Content[15] == 1;
        // 显示
    }
    else
    {
        //失败读取,显示失败信息
        MessageBox.Show(read.ToMessageShowString());
    }
}
 
private void userButton3_Click(object sender, EventArgs e)
{
    // M100-M104 写入测试 此处写入后M100:通 M101:断 M102:断 M103:通 M104:通
    bool[] values = new bool[] { truefalsefalsetruetrue };// 等同于 byte[] values = new byte[]{0x01,0x00,0x00,0x01,0x01}
    OperateResult write = melsec_net.WriteIntoPLC("M100", values);
    if (write.IsSuccess)
    {
        TextBoxAppendStringLine("写入成功");
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

 


错误说明:有可能因为站号网络号没有配置正确返回有错误代号没有错误信息, 也有可能因为网络问题导致没有连接上,此时会有连接不上的错误信息。


下面展示的是后台线程循环读取的情况,事实上在实际的使用过程中经常会碰见的情况。下面的方法需要 放到单独的线程中,同理,访问D数据时也是按照下面循环就行,此处不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//后台循环读取PLC数据 M200开始10个字 也即是M200-M209
while (true)
{
    OperateResult<byte[]> read = melsec_net.ReadFromPLC("M200", 10);
    if (read.IsSuccess)
    {
        //成功读取,委托显示
        textBox2.BeginInvoke(new Action(delegate
        {
            textBox2.Text = "M201:" + (read.Content[1] == 1 ? "通" "断");
        }));
    }
    else
    {
        //失败读取,应该对失败信息进行日志记录,不应该显示,测试访问时才适合显示错误信息
        LogHelper.save(read.ToMessageShowString());
    }
    System.Threading.Thread.Sleep(1000);//决定了访问的频率
}

  

D,W,R字数据的读写操作

此处读取针对中间存在整数数据的情况,因为两者读取方式相同,故而只演示一种数据读取, 使用该组件读取数据,一次最多读取或写入960个字,超出则失败。 如果读取的长度确实超过限制,请考虑分批读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private void button1_Click(object sender, EventArgs e)
{
    //读取PLC数据 D6000开始21个字 也即是D6000-D6020 最大长度980
    OperateResult<byte[]> read = melsec_net.ReadFromPLC("D6000", 21);
    if(read.IsSuccess)
    {
        //成功读取
        textBox2.Text = "D6000:" + melsec_net.GetShortFromBytes(read.Content, 0);
        //textBox2.Text = "D6001:" + melsec_net.GetShortFromBytes(read.Content, 1);
        //textBox2.Text = "D6002:" + melsec_net.GetShortFromBytes(read.Content, 2);
        //textBox2.Text = "D6003:" + melsec_net.GetShortFromBytes(read.Content, 3);
        //textBox2.Text = "D6004:" + melsec_net.GetShortFromBytes(read.Content, 4);
        //================================================================================
        //这两种方式一样的,
        //textBox2.Text = "D6000:" + BitConverter.ToInt16(read.Content, 0);
        //textBox2.Text = "D6001:" + BitConverter.ToInt16(read.Content, 2);
        //textBox2.Text = "D6002:" + BitConverter.ToInt16(read.Content, 4);
        //textBox2.Text = "D6003:" + BitConverter.ToInt16(read.Content, 6);
        //textBox2.Text = "D6004:" + BitConverter.ToInt16(read.Content, 8);
    }
    else
    {
        //失败读取
        MessageBox.Show(read.ToMessageShowString());
    }
}
private void button2_Click(object sender, EventArgs e)
{
    short[] values = new short[4] { 1335, 8765, 1234, 4567 };//决定了写多少长度的D
    //写入PLC数据 D6000为1234,D6001为8765,D6002为1234,D6003为4567
    OperateResult write = melsec_net.WriteIntoPLC(MelsecDataType.D, 6000, values);
    if(write.IsSuccess)
    {
        textBox2.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());//显示失败原因
    }
}

  



ASCII字符串数据的读写

在实际项目中,有可能会碰到PLC存储了规格数据,或是条码数据,这些数据是以ASCII编码形式存在, 我们需要把数据进行读取出来用于显示,保存等操作。下面演示读取指定长度的条码数据,数据的数据存放在D2000-D2004中, 长度应该为存储条码的最大长度,也即是占用了5个D,一个D可以存储2个ASCII码字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void button7_Click(object sender, EventArgs e)
{
    //读取字符串数据,共计10个字节长度
    OperateResult<byte[]> read = melsec_net.ReadFromPLC(MelsecDataType.D, 2000, 5);
    if (read.IsSuccess)
    {
        //成功读取
        textBox2.Text = Encoding.ASCII.GetString(read.Content);
    }
    else
    {
        //失败读取
        MessageBox.Show(read.ToMessageShowString());
    }
}
private void button8_Click(object sender, EventArgs e)
{
    //写字符串,如果写入K12345678这9个字符,读取出来时末尾会补0
    OperateResult write = melsec_net.WriteAsciiStringIntoPLC(MelsecDataType.D, 2000, "K123456789");
    if (write.IsSuccess)
    {
        textBox2.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  



需要注意的是,如果第一次在D2000-D2004中写入了"K123456789",第二次写入了"K6666",那么读取D2000-D2004的条码数据会读取到 K666656789,如果要避免这种情况,则需要在写入条码的时候,指定总长度,该长度必须为偶数, 不然也会自动补0,小于该长度时,自动补零,大于该长度时,自动截断数据,具体的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button8_Click(object sender, EventArgs e)
{
    //写字符串,本次写入指定了10个长度的字符,其余的D的数据将被清空,是一种安全的写入方式
    OperateResult write = melsec_net.WriteAsciiStringIntoPLC(MelsecDataType.D, 2000, "K6666", 10);
    if (write.IsSuccess)
    {
        textBox2.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  



中文及特殊字符的读写

在需要读写复杂的字符数据时,上述的ASCII编码已经不能满足要求,虽然使用读写的基础方法可以实现任意数据的读写, 但是此处为了方便,还是提供了一个方便的方法来读写中文数据,采用Unicode编码的字符, 该编码下的一个字符占用一个D或W来存储。如下将演示,读写方法,基本用途和上述 ASCII编码的读写一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void button9_Click(object sender, EventArgs e)
{
    //读中文,存储在D3000-D3009
    OperateResult<byte[]> read = melsec_net.ReadFromPLC(MelsecDataType.D, 3000, 10);
    if (read.IsSuccess)
    {
        //解析数据
        textBox2.Text = Encoding.Unicode.GetString(read.Content);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}
private void button10_Click(object sender, EventArgs e)
{
    //写中文 D3000-D3009,该10含义为中文字符数
    OperateResult write = melsec_net.WriteUnicodeStringIntoPLC(MelsecDataType.D, 3000, "测试数据test", 10);
    if (write.IsSuccess)
    {
        textBox2.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

 

一个实际中复杂的例子演示

实际中可能碰到的情况会很复杂,一台设备中需要上传的数据包含了温度,压力,产量,规格等等信息,在一串数据中 会包含各种各样的不同的数据,上述的读取D,读取M,读取条码的方式不太好用,所以此处做一个完整示例的演示,假设我们需要读取 D4000-D4009的数据,假设D4000存放了温度数据,55.1℃在D中为551,D4001存放了压力数据,1.23MPa在D中存放为123,D4002存放了 设备状态,0为停止,1为运行,D4003存放了产量,1000就是指1000个,D4004备用,D4005-D4009存放了规格,以下代码演示如何去解析数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void button29_Click(object sender, EventArgs e)
{
    //解析复杂数据
    OperateResult<byte[]> read = melsec_net.ReadFromPLC(MelsecDataType.D, 4000, 10);
    if (read.IsSuccess)
    {
        double 温度 = BitConverter.ToInt16(read.Content, 0) / 10d;//索引很重要
        double 压力 = BitConverter.ToInt16(read.Content, 2) / 100d;
        bool IsRun = BitConverter.ToInt16(read.Content, 4) == 1;
        int 产量 = BitConverter.ToInt16(read.Content, 6);
        string 规格 = Encoding.ASCII.GetString(read.Content, 10, 10);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

 

究极数据读取展示,用于测试你自己的报文以及扩展自己的更高级,更变态的API,以下演示,使用这个高级模式,写入M100,True的操作:

我们要写入的字节数组HEX表示形式为:50 00 00 FF FF 03 00 0D 00 0A 00 01 14 01 00 64 00 00 90 01 00 10

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void userButton23_Click(object sender, EventArgs e)
{
    byte[] buffer = HslCommunication.BasicFramework.SoftBasic.HexStringToBytes("50 00 00 FF FF 03 00 0D 00 0A 00 01 14 01 00 64 00 00 90 01 00 10");
    // 直接使用报文进行
    OperateResult<byte[]> operate = melsec_net.ReadFromServerCore(buffer);
    if(operate.IsSuccess)
    {
        // 返回PLC的报文反馈,需要自己对报文进行结果分析
        MessageBox.Show(HslCommunication.BasicFramework.SoftBasic.ByteToHexString(operate.Content));
    }
    else
    {
        // 网络原因导致的失败
        MessageBox.Show(operate.ToMessageShowString());
    }
}

 

 

 

西门子篇一(S5兼容的FETCH/WRITE协议,配置比较麻烦,不再维护,s300系列和1200系列参照西门子篇二)


 

环境:此处使用了STEP 7V5.5 sp4编程软件作为示例,在添加以太网模块(6GK7 343-1EX30-0E0 CP343-1)到组态中时,可以设置IP地址及子网掩码, 此处测试使用,所以不使用路由器,如果您的西门子需要连接到内网中的话,需要配置路由器。目前只支持M,I,Q数据的读写。 然后点击新建,创建一个Ethernet(1)网络。以太网参数配置如下图:



将以太网的模块添加到机架中以后,现在打开网络组态 ,打开后点击组态上的PLC模块。会出现如下界面,在箭头出进行双击操作,可以弹出对话框,并进行一系列操作:

 

 

 



按照上面一套操作下来,创建了一个读取的端口,端口号为2000,后面有用,需要记住, 按照上述的步骤再创建一个写入的端口,只有最后一步不一致,如下:



配置完之后的效果图如下,新建了两个端口,一个用于读取数据,一个用于写入数据。 <strong>注意:设置完成后一定要写入到PLC才算真的完成。</strong>



如上图所示,共配置了2000,2001两个端口号,配置完成后需要进行重启PLC,端口的配置原则如下:

端口号设置规则:

  • 为了不与原先存在的系统发生冲突,您在添加自己的端口时尽量使用您自己的端口。
  • 如果读写都需要,尽可能的将读取端口和写入端口区分开来,这样做比较高性能。
  • 如果您的网络状态不是特别稳定,读取端口使用2个,一个受阻切换另一个读取可以提升系统的稳定性。



西门子PLC的数据种类其实只有一种,就是byte数据,更大的数据用多个byte来组合,位数据就是byte上的位,所以会比三菱的简单一点点,好处理一点。

初始化访问PLC对象

如果想使用本组件的数据读取功能,必须先初始化数据访问对象,根据实际情况进行数据的填入。 下面仅仅是测试中的数据:

1
2
3
4
5
6
7
8
9
10
private SiemensNet siemens_net = new SiemensNet();
private void FormPlcTest_Load(object sender, EventArgs e)
{
    //初始化,此处的值参考了上面的配置参数
    siemens_net.ConnectTimeout = 500;
    siemens_net.PortRead = 2000;
    siemens_net.PortReadBackup = 2002;//备用读端口,也可以不指定,默认负数,不会切换负数端口
    siemens_net.PortWrite = 2001;
    siemens_net.PLCIpAddress = System.Net.IPAddress.Parse("192.168.0.6");
}

  



说明:对象应该放在窗体类下面,此处仅仅针对读取一台设备的plc,也可以在访问的方法中实例化局部对象, 初始化数据,然后读取,该对象几乎不损耗内存,内存垃圾由CLR进行自动回收。此处测试方便,窗体的多个按钮均连接同一台PLC 设备,所以本窗体实例化一个对象即可。

M,I,Q数据读写

由于西门子这三个数据种类相同,操作时一模一样,所以此处只展示读写M寄存器的例子。

如下方法演示读取了M100-M101这2个M的值,返回值解析:如果读取正常则共返回2个字节的数据,以下示例数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void button3_Click(object sender, EventArgs e)
{
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 100, 2);
    if(read.IsSuccess)
    {
        textBox4.Text = "M100:" + read.Content[0] + " M101:" + read.Content[1];
        bool M100_0 = (read.Content[0] & 0x01) == 0x01;//M100.0的通断
        bool M100_1 = (read.Content[0] & 0x02) == 0x02;//M100.1的通断
        bool M100_2 = (read.Content[0] & 0x04) == 0x04;//M100.2的通断
        bool M100_3 = (read.Content[0] & 0x08) == 0x08;//M100.3的通断
        bool M100_4 = (read.Content[0] & 0x10) == 0x10;//M100.4的通断
        bool M100_5 = (read.Content[0] & 0x20) == 0x20;//M100.5的通断
        bool M100_6 = (read.Content[0] & 0x40) == 0x40;//M100.6的通断
        bool M100_7 = (read.Content[0] & 0x80) == 0x80;//M100.7的通断
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}
private void button4_Click(object sender, EventArgs e)
{
    //写入结果是M100:81  M101:22  M102:124
    OperateResult<byte[]> write = siemens_net.WriteIntoPLC(SiemensDataType.M, 100, new byte[] { 81, 22, 124 });
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  



错误说明:有可能因为站号网络号没有配置正确返回有错误代号没有错误信息, 也有可能因为网络问题导致没有连接上,此时会有连接不上的错误信息。

下面展示的是后台线程循环读取的情况,事实上在实际的使用过程中经常会碰见的情况。下面的方法需要 放到单独的线程中,同理,访问D数据时也是按照下面循环就行,此处不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//后台循环读取PLC数据 M200开始10个字 也即是M200-M209
while (true)
{
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 100, 2);
    if (read.IsSuccess)
    {
        //成功读取,委托显示
        textBox2.BeginInvoke(new Action(delegate
        {
            textBox4.Text = "M100:" + read.Content[0] + " M101:" + read.Content[1];
        }));
    }
    else
    {
        //失败读取,应该对失败信息进行日志记录,不应该显示,测试访问时才适合显示错误信息
        LogHelper.save(read.ToMessageShowString());
    }
    System.Threading.Thread.Sleep(1000);//决定了访问的频率
}

  



整数数据读写(一个数据由2个byte组成)

虽然上述实现了M数据的读写,但是只能表示0-255的数据,想要支持更大的数据,需要自己指定规则, 这就需要你对数据和字节原理非常清晰才能实现,为了方便,此处提供了读写双字节数据的功能,先演示读取M100-M106 的数据,对应有三个双字节数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void button15_Click(object sender, EventArgs e)
{
    //整数读取
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 100, 6);
    if (read.IsSuccess)
    {
        short[] result = siemens_net.GetArrayFromBytes(read.Content);
        textBox4.Text = "M100:" + result[0] + ",M102:" + result[1] + ",M104:" + result[2];
        //或者下述的方法或许,可以发现和三菱的D数据获取是一模一样的
        //short m100 = BitConverter.ToInt16(read.Content, 0);
        //short m102 = BitConverter.ToInt16(read.Content, 2);
        //short m104 = BitConverter.ToInt16(read.Content, 4);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  

接下来介绍写入整数数据的操作,例如,我们要使得M100,M101=2456,M102,M103=4567,M104,M105=-124,那么代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button16_Click(object sender, EventArgs e)
{
    //整数写入,数组的长度为x,那么占用的M字节数为x乘以2
    OperateResult write = siemens_net.WriteIntoPLC(SiemensDataType.M, 100, new short[] { 2456, 4567, -124 });
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  




ASCII字符串数据的读写

在实际项目中,有可能会碰到PLC存储了规格数据,或是条码数据,这些数据是以ASCII编码形式存在, 我们需要把数据进行读取出来用于显示,保存等操作。下面演示读取指定长度的条码数据,数据的数据存放在M100-M109中, 长度应该为存储条码的最大长度,也即是占用了10个M,一个M可以存储1个ASCII码字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button12_Click(object sender, EventArgs e)
{
    //读取字符串,长度为10
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 100, 10);
    if (read.IsSuccess)
    {
        textBox4.Text = Encoding.ASCII.GetString(read.Content);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  


下面演示写入条码数据,地址在M100-M109中,所以需要写入10个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button14_Click(object sender, EventArgs e)
{
    //写字符串,字符串的长度决定了需要占用多少个M字节,两者是相等的
    OperateResult write = siemens_net.WriteAsciiStringIntoPLC(SiemensDataType.M, 100, "K123456789");
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  



需要注意的是,如果第一次在M100-M109中写入了"K123456789",第二次写入了"K6666",那么读取M100-M109的条码数据会读取到K666656789,如果要避免这种情况,则需要在写入条码的时候,指定总长度,该长度 可单数可偶数,具体的使用方法如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
private void button14_Click(object sender, EventArgs e)
{
    //写字符串
    OperateResult write = siemens_net.WriteAsciiStringIntoPLC(SiemensDataType.M, 100, "K6666", 10);
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  




中文及特殊字符的读写

在需要读写复杂的字符数据时,上述的ASCII编码已经不能满足要求,虽然使用读写的基础方法可以实现任意数据的读写, 但是此处为了方便,还是提供了一个方便的方法来读写中文数据,采用Unicode编码的字符, 该编码下的一个字符占用两个M来存储。如下将演示,读写方法,基本用途和上述 ASCII编码的读写一致。


1
2
3
4
5
6
7
8
9
10
11
12
13
private void button11_Click(object sender, EventArgs e)
{
    //读取中文 指定读取的长度必须双数,否则解码失败
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 200, 12);
    if (read.IsSuccess)
    {
        textBox4.Text = Encoding.Unicode.GetString(read.Content);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  


在写入的过程中,只演示写入指定长度的(实际中也应该使用这个方法),指定长度的意思为多少个中文。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button13_Click(object sender, EventArgs e)
{
    //中文写入
    OperateResult write = siemens_net.WriteUnicodeStringIntoPLC(SiemensDataType.M, 200, "测试de东西", 10);
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  


一个实际中复杂的例子演示

实际中可能碰到的情况会很复杂,一台设备中需要上传的数据包含了温度,压力,产量,规格等等信息,在一串数据中 会包含各种各样的不同的数据,所以此处做一个完整示例的演示,假设我们需要读取 M100-M116的数据,假设M100,M101存放了温度数据,55.1℃在M中为551,M102,M103存放了压力数据,1.23MPa在M中存放为123,M104存放了 设备状态,0为停止,1为运行,M105,M106存放了产量,1000就是指1000个,M107-M116存放了规格,以下代码演示如何去解析数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void button29_Click(object sender, EventArgs e)
{
    //解析复杂数据
    OperateResult<byte[]> read = siemens_net.ReadFromPLC(SiemensDataType.M, 200, 17);
    if (read.IsSuccess)
    {
        double 温度 = BitConverter.ToInt16(read.Content, 0) / 10d;//索引很重要
        double 压力 = BitConverter.ToInt16(read.Content, 2) / 100d;
        bool IsRun = read.Content[4] == 1;
        int 产量 = BitConverter.ToInt16(read.Content, 5);
        string 规格 = Encoding.ASCII.GetString(read.Content, 7, 10);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  

西门子篇二(采用S7协议下的tcp直接通讯,配置简单,一般PLC都支持)


 

测试通过的PLC:1200系列 本人亲测

                          200smart  感谢 無名①终止^^ 的测试

 

上述的西门子篇一的通讯配置相对太麻烦,采用西门子官方的库的话,又是DEMO版本,会有广告,而且携带组件过多,最新学习了西门子的TCP/IP报文格式,报文格式在官网没有找到,参考了如下地址的格式:

http://www.itpub.net/thread-2052649-1-1.html

https://wenku.baidu.com/view/d93b88b06394dd88d0d233d4b14e852459fb3912.html

如果你擅长于网络通信和组件开发,可以通过报文格式开发出自己的西门子通信库,我所做的就是基于报文格式进行了二次封装,隐藏了socket通信的细节,还包含了异常处理,提供了简单方便的API来读写数据。提供了整数数据的读写,字符串读写,来丰富各种需求,从事实上来说,只要可以读写字节,相当于任何数据了。

准备:在西门子PLC上配置好IP地址,就只有一个IP地址就够了,然后打开电脑的cmd指令,只要能ping通西门子PLC即可。

 

实例化:

1
2
3
4
5
private SiemensTcpNet siemensTcpNet = new SiemensTcpNet(SiemensPLCS.S1200)
        {
            PLCIpAddress = System.Net.IPAddress.Parse("192.168.1.195"),
            ConnectTimeout = 5000, // 连接超时时间,默认5秒,可以不设置
        };

 

切换长连接(可以根据自己的需求来确认是否切换),也可以放在窗口的Load方法中,一般建议使用长连接,速度更快,又是线程安全的:

1
2
3
4
private void userButton16_Click(object sender, EventArgs e)
{
    siemensTcpNet.ConnectServer();
}

 一行代码就可以切换到长连接模式,长连接的模式通讯更加稳定(如果网络确实好的话),这行代码可以放到Form的Load事件方法中。

只要放到form窗口下即可,实例化需要指定访问的是1200系列还是300系列,然后指定IP地址,端口号不需要指定,西门子有个默认的端口号102,支持读写操作。

 

演示一些简单使用的数据读写操作,以下代码没有进行对读写结果严格判断(判断是否读写成功,参照更下面的代码),网络良好的情况下几乎不会失败,但不保证完全没有错误,生产使用时尽可能的完善:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void userButton40_Click(object sender, EventArgs e)
{
    // 读取操作,这里的M100可以替换成I100,Q100,DB20.100效果时一样的
    bool M100_7 = siemensTcpNet.ReadBoolFromPLC("M100.7").Content;  // 读取M100.7是否通断,注意M100.0等同于M100
    byte byte_M100 = siemensTcpNet.ReadByteFromPLC("M100").Content; // 读取M100的值
    short short_M100 = siemensTcpNet.ReadShortFromPLC("M100").Content; // 读取M100-M101组成的字
    ushort ushort_M100 = siemensTcpNet.ReadUShortFromPLC("M100").Content; // 读取M100-M101组成的无符号的值
    int int_M100 = siemensTcpNet.ReadIntFromPLC("M100").Content;         // 读取M100-M103组成的有符号的数据
    uint uint_M100 = siemensTcpNet.ReadUIntFromPLC("M100").Content;      // 读取M100-M103组成的无符号的值
    float float_M100 = siemensTcpNet.ReadFloatFromPLC("M100").Content;   // 读取M100-M103组成的单精度值
    long long_M100 = siemensTcpNet.ReadLongFromPLC("M100").Content;      // 读取M100-M107组成的大数据值
    ulong ulong_M100 = siemensTcpNet.ReadULongFromPLC("M100").Content;   // 读取M100-M107组成的无符号大数据
    double double_M100 = siemensTcpNet.ReadDoubleFromPLC("M100").Content; // 读取M100-M107组成的双精度值
    string str_M100 = siemensTcpNet.ReadStringFromPLC("M100", 10).Content;// 读取M100-M109组成的ASCII字符串数据
 
    // 写入操作,这里的M100可以替换成I100,Q100,DB20.100效果时一样的
    siemensTcpNet.WriteIntoPLC("M100.7"true);                // 写位,注意M100.0等同于M100
    siemensTcpNet.WriteIntoPLC("M100", (byte)0x33);            // 写单个字节
    siemensTcpNet.WriteIntoPLC("M100", (short)12345);          // 写双字节有符号
    siemensTcpNet.WriteIntoPLC("M100", (ushort)45678);         // 写双字节无符号
    siemensTcpNet.WriteIntoPLC("M100", 123456789);             // 写双字有符号
    siemensTcpNet.WriteIntoPLC("M100", (uint)3456789123);      // 写双字无符号
    siemensTcpNet.WriteIntoPLC("M100", 123.456f);              // 写单精度
    siemensTcpNet.WriteIntoPLC("M100", 1234556434534545L);     // 写大整数有符号
    siemensTcpNet.WriteIntoPLC("M100", 523434234234343UL);     // 写大整数无符号
    siemensTcpNet.WriteIntoPLC("M100", 123.456d);              // 写双精度
    siemensTcpNet.WriteAsciiStringIntoPLC("M100""K123456789");// 写ASCII字符串
}

 

如果上面的指令不能满足你的需求,下面再分别讲解严格的操作,以及批量化的复杂的读写操作,假设你要读取1000个M,循环读取1千次可能要3秒钟,如果用了下面的批量化读取,只需要50ms,但是需要你对字节的原理比较熟悉才能得心应手的处理

 

批量位数据写入:(如果长度刚好为8的倍数,比如24个,那就刚好写3个字节的数据,如果像下面的代码写10个长度,那么实际上会改变M200-M201共16个开关点,这个一定要注意)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void userButton14_Click(object sender, EventArgs e)
        {
            bool[] data = new bool[10]
            {
                false,    // M200.0 = false
                false,    // M200.1 = false
                false,    // M200.2 = false
                true,     // M200.3 = true
                true,     // M200.4 = true
                false,    // M200.5 = false
                true,     // M200.6 = true
                false,    // M200.7 = false
                true,     // M201.0 = true
                false     // M201.1 = false
            };
 
            OperateResult write = siemensTcpNet.WriteIntoPLC("M200", data);
            if (write.IsSuccess)
            {
               textBox4.Text = "写入成功";
            }
            else
            {
                MessageBox.Show(write.ToMessageShowString());
            }
        }

 

 

 

M,I,Q的读写(此处演示批量读取,自己根据需求来解析数据):

这三个数据的读写是一致的,为了区分之前旧的一个通讯类,此处的数据地址格式变更为字符串,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void button3_Click(object sender, EventArgs e)
        {<br>            // 示例:M100,I100,Q100,DB20.100
            OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC("M100", 2);
            if(read.IsSuccess)
            {
                textBox4.Text = "M100:" + read.Content[0] + " M101:" + read.Content[1];
                bool M100_0 = (read.Content[0] & 0x01) == 0x01;//M100.0的通断
                bool M100_1 = (read.Content[0] & 0x02) == 0x02;//M100.1的通断
                bool M100_2 = (read.Content[0] & 0x04) == 0x04;//M100.2的通断
                bool M100_3 = (read.Content[0] & 0x08) == 0x08;//M100.3的通断
                bool M100_4 = (read.Content[0] & 0x10) == 0x10;//M100.4的通断
                bool M100_5 = (read.Content[0] & 0x20) == 0x20;//M100.5的通断
                bool M100_6 = (read.Content[0] & 0x40) == 0x40;//M100.6的通断
                bool M100_7 = (read.Content[0] & 0x80) == 0x80;//M100.7的通断
            }
            else
            {
                MessageBox.Show(read.ToMessageShowString());
            }
        }
        private void button4_Click(object sender, EventArgs e)
        {
            //写入结果是M100:81  M101:22  M102:124
            OperateResult write = siemensTcpNet.WriteIntoPLC("M100"new byte[] { 81, 22, 124 });
            if (write.IsSuccess)
            {
                textBox4.Text = "写入成功";
            }
            else
            {
                MessageBox.Show(write.ToMessageShowString());
            }
        }

  如果是输入的话,就是把"M100"换成"I100","Q100",效果是一样的,这样的就可以对数据进行读写了。而DB块的格式有些区别,比如我们要写DB块20的100地址数据,我们需要写成"DB20.100"这样就可以正常读写了。DB块数据并没有进行严格测试,如果你需要使用这个功能,最好需要测试下是否真的能读取数据。

DB块的读写测试感谢我的好朋友:吃饱睡好

 

整数数据读写(一个数据由2个byte组成)

虽然上述实现了M数据的读写,但是只能表示0-255的数据,想要支持更大的数据,需要自己指定规则, 这就需要你对数据和字节原理非常清晰才能实现,为了方便,此处提供了读写双字节数据的功能,先演示读取M100-M106 的数据,对应有三个双字节数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void button15_Click(object sender, EventArgs e)
    {
        //整数读取
        OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC("M100", 6);
        if (read.IsSuccess)
        {
            short[] result = siemensTcpNet.GetArrayFromBytes(read.Content);
            textBox4.Text = "M100:" + result[0] + ",M102:" + result[1] + ",M104:" + result[2];
            // 或者使用下述的方法
            //short m100 = siemensTcpNet.GetShortFromBytes(read.Content, 0);
            //short m102 = siemensTcpNet.GetShortFromBytes(read.Content, 2);
            //short m104 = siemensTcpNet.GetShortFromBytes(read.Content, 4);
        }
        else
        {
            MessageBox.Show(read.ToMessageShowString());
        }
     }

 

  接下来介绍写入整数数据的操作,例如,我们要使得M100,M101=2456,M102,M103=4567,M104,M105=-124,那么代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button16_Click(object sender, EventArgs e)
{
    //整数写入,数组的长度为x,那么占用的M字节数为x乘以2
    OperateResult write = siemensTcpNet.WriteIntoPLC("M100"new short[] { 2456, 4567, -124 });
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  

ASCII字符串数据的读写

在实际项目中,有可能会碰到PLC存储了规格数据,或是条码数据,这些数据是以ASCII编码形式存在, 我们需要把数据进行读取出来用于显示,保存等操作。下面演示读取指定长度的条码数据,数据的数据存放在M100-M109中, 长度应该为存储条码的最大长度,也即是占用了10个M,一个M可以存储1个ASCII码字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button12_Click(object sender, EventArgs e)
{
    //读取字符串,长度为10
    OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC("M100", 10);
    if (read.IsSuccess)
    {
        textBox4.Text = Encoding.ASCII.GetString(read.Content);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  


下面演示写入条码数据,地址在M100-M109中,所以需要写入10个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button14_Click(object sender, EventArgs e)
{
    //写字符串,字符串的长度决定了需要占用多少个M字节,两者是相等的
    OperateResult write = siemensTcpNet.WriteAsciiStringIntoPLC("M100""K123456789");
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  



需要注意的是,如果第一次在M100-M109中写入了"K123456789",第二次写入了"K6666",那么读取M100-M109的条码数据会读取到K666656789,如果要避免这种情况,则需要在写入条码的时候,指定总长度,该长度 可单数可偶数,具体的使用方法如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
private void button14_Click(object sender, EventArgs e)
{
    //写字符串
    OperateResult write = siemensTcpNet.WriteAsciiStringIntoPLC("M100""K6666", 10);
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  




中文及特殊字符的读写

在需要读写复杂的字符数据时,上述的ASCII编码已经不能满足要求,虽然使用读写的基础方法可以实现任意数据的读写, 但是此处为了方便,还是提供了一个方便的方法来读写中文数据,采用Unicode编码的字符, 该编码下的一个字符占用两个M来存储。如下将演示,读写方法,基本用途和上述 ASCII编码的读写一致。


1
2
3
4
5
6
7
8
9
10
11
12
13
private void button11_Click(object sender, EventArgs e)
{
    //读取中文 指定读取的长度必须双数,否则解码失败
    OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC("M200", 12);
    if (read.IsSuccess)
    {
        textBox4.Text = Encoding.Unicode.GetString(read.Content);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

  


在写入的过程中,只演示写入指定长度的(实际中也应该使用这个方法),指定长度的意思为多少个中文。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button13_Click(object sender, EventArgs e)
{
    //中文写入
    OperateResult write = siemensTcpNet.WriteUnicodeStringIntoPLC("M200""测试de东西", 10);
    if (write.IsSuccess)
    {
        textBox4.Text = "写入成功";
    }
    else
    {
        MessageBox.Show(write.ToMessageShowString());
    }
}

  

 

一个实际的复杂例子:

实际中可能碰到的情况会很复杂,一台设备中需要上传的数据包含了温度,压力,产量,规格等等信息,在一串数据中 会包含各种各样的不同的数据,所以此处做一个完整示例的演示,假设我们需要读取 M100-M116的数据,假设M100,M101存放了温度数据,55.1℃在M中为551,M102,M103存放了压力数据,1.23MPa在M中存放为123,M104存放了 设备状态,0为停止,1为运行,M105,M106存放了产量,1000就是指1000个,M107-M116存放了规格,以下代码演示如何去解析数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void button49_Click(object sender, EventArgs e)
{
    //解析复杂数据,西门子有个问题在于多字节数据的高地位是需要反转的,而内置的GetShortFromBytes已经反转了数据
    OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC("M200", 17);
    if (read.IsSuccess)
    {
        double 温度 = siemensTcpNet.GetShortFromBytes(read.Content, 0) / 10d;//索引很重要
        double 压力 = siemensTcpNet.GetShortFromBytes(read.Content, 2) / 100d;
        bool IsRun = read.Content[4] == 1;
        int 产量 = siemensTcpNet.GetShortFromBytes(read.Content, 5);
        string 规格 = Encoding.ASCII.GetString(read.Content, 7, 10);
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

 

散乱数据的读取:

实际中我们需要读取PLC中的数据,并不是连续的数据块,最好的方法是将所有需要的数据块挪到一个连续的区块,比如M块,这样可以加速数据的读取,系统的性能也能更加高效,即时需要读取1000个M点,也是毫秒级的事情,如果1000个M点分成1000次来读取,那么使用本组件的效率是非常低下的,循环1000次的siemensTcpNet.ReadFromPLC("M100", 1);非常的耗时甚至可能达到几秒的量级,因为这个方法每次调用都会重新请求网络连接,然后初始化连接,请求数据,断开连接,所以最好的方法就是所有的数据都挪到一个统一的数据区块。

但是如果你确实有需求读取多个地址的数据,比如做成访问PLC的数据是可配置的,在配置文件里追加一个M100,长度4的int型数据,这种情况就不太适合将散乱的数据进行挪到统一的区块,所以本组件提供了一个高性能数组读取(但是仍然比一次读取连续区块慢一点,基本上是同一个量级的),声明如下:

1
2
3
4
5
6
7
/// <summary>
/// 一次性从PLC获取所有的数据,按照先后顺序返回一个统一的Buffer,需要按照顺序处理,两个数组长度必须一致
/// </summary>
/// <param name="address">起始地址数组</param>
/// <param name="count">数据长度数组</param>
/// <returns></returns>
public OperateResultBytes ReadFromPLC(string[] address, ushort[] count);

address和count数组都不能为空,否则报错,两者的长度必须一致,否则报错

接下来我们举例访问PLC数据,比如我们需要读取M100开始的4个字节(这是一个int数据),M150开始的4个字节(这是一个float数据),M200开始的2个字节(这是一个short数据),I300开始的一个字节(普通的byte数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void button29_Click_1(object sender, EventArgs e)
{
    // 数组读取
    OperateResult<byte[]> read = siemensTcpNet.ReadFromPLC(
        new string[] { "M100""M150""M200""I300" },
        new ushort[] { 4, 4, 2, 1});
 
    if(read.IsSuccess)
    {
        int value1 = siemensTcpNet.GetIntFromBytes(read.Content, 0);     // 第一个int数据
        float value2 = siemensTcpNet.GetFloatFromBytes(read.Content, 4); // 第二个float数据
        short value3 = siemensTcpNet.GetShortFromBytes(read.Content, 8); // 第三个short数据
        byte value4 = read.Content[10];                                  // 第四个byte数据
    }
    else
    {
        MessageBox.Show(read.ToMessageShowString());
    }
}

 

究极数据的读取:

此处提供一个核心的报文读取机制,你可以自己传入自己的报文,然后接收服务器的报文,再自己解析操作,可以根据报文格式实现任意的操作,当然,前提是需要报文支持。假设我要实现写入M100,为0x3B,那么最终的报文为

03 00 00 24 02 F0 80 32 01 00 00 00 01 00 0E 00 05 05 01 12 0A 10 02 00 01 00 00 83 00 03 20 00 04 00 08 3B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void userButton23_Click_1(object sender, EventArgs e)
{
    byte[] buffer = HslCommunication.BasicFramework.SoftBasic.HexStringToBytes(
        "03 00 00 24 02 F0 80 32 01 00 00 00 01 00 0E 00 05 05 01 12 0A 10 02 00 01 00 00 83 00 03 20 00 04 00 08 3B");
    OperateResult<byte[]> operate = siemensTcpNet.ReadFromServerCore(buffer);
    if (operate.IsSuccess)
    {
        // 显示服务器返回的报文
        TextBoxAppendStringLine(HslCommunication.BasicFramework.SoftBasic.ByteToHexString(operate.Content));
    }
    else
    {
        // 显示网络错误
        MessageBox.Show(operate.ToMessageShowString());
    }
}