最近在做一个数据采集项目,涉及到许多与西门子S7系列PLC的通信,由于自己的VC水平属于半瓶子晃荡,所以就想利用C#来进行开发(个人比较喜欢C#的代码风格,看着很清爽),虽然C#这种高级语言与底层的设备通讯效率确实不如C++,但好在数据量不大,实时性要求不算太高,用C#还是可以应付的。在界面开发方面,高级语言确实不如WinCCIntouch之类的组态软件,但在数据处理上面,却有很大的灵活性。
      在这里很感谢吴向阳,在中国工控网上面发现了他的文章,因为我是从C#转到工控方向的,以前对PLC一点都不懂,刚接触这一行时,学习起来很吃力,看了他的这篇文章,让我对PLC的有了更加深入的了解。我的这个DCProdave.cs就是在他的基础上修改的,加了一些自己的东西。还望各位多多提意见,多多交流!

Prodave版本: PRODAVE6.0 - W95_S7.DLL
PLC模拟环境: PLCSIM V5.4
开发环境: VS.NET 2005
.w95_s7.dll中导入PLC通讯函数的方法[DllImport]
在使用DllImport之前,必须引入InteropServices, 代码如下: using System.Runtime.InteropSer
vices;
具体使用方法可以参考我的博客中转载的一篇文章 C#()中的DllImport用法[》写的很不错,千万要注意C++数据类型到C#的对应关系,选用合适的类型。比如 char* 可以用string来转换,指针类型可以ref  或者数组。

.定义结构体类型
2.1 PLC连接参数结构体
1//定义结构体[连接PLC所需参数]
2public struct PLCConnParam 
3{
4    public byte Addres;           // 定义CPUMPI/DP地址
5    //public byte SegmentId;    // 保留为0
6    public byte Rack;             // 定义CPU的机架号
7    public byte Slot;              // 定义CPU的槽号
8}

2.2 PLC存储区域类别编号
 1//定义枚举类型[PLC的存储区域编号]
 2public enum PLCBlockType
 3{
 4    I = 1//Input bytes
 5    Q = 2//Output bytes
 6    M = 3//Flag bytes
 7    T = 4//Timer words
 8    Z = 5//Counter words
 9    D = 6//Data from DB
10}

.常用函数详细讲解
3.1 建立PLC连接函数
     首先从W95_S7.DLL中导入连接函数,访问权限为私有,C#将会对此函数进行封装,供外部调用,稍后讲解.
1/**//// <summary>PLC建立连接,该函数必须在其他所有函数调用之前被调用
2/// </summary>
3/// <param name="nr">连接数,DOS,WIN3.1最多可以有4,WIN95以上最多可以有16</param>
4/// <param name="device">PLC通讯的设备名称,一般为S7ONLINE</param>
5/// <param name="adr_table">参数列表,4个值分别为MPI/DP地址,保留值=0,槽号,机架号</param>
6/// <returns>0正常返回,0为错误号</returns>
7[DllImport("w95_s7.dll")]
8private extern static int load_tool(byte nr, string device, byte[,] adr_table);
说明:
      在一个MPI/DP网络中若有多个PLC时,可指定多个连接列。最后一列的所有参数须置0,以标志参数列结束。例如一个MPI/DP网中有两个PLC,他们的MPI地址分别为23,槽号均为2,机架号均为0,则可按如下方式调用:byte[,] ba={{2,0,2,0},{3,0,2,0},{0,0,0,0}};  int err=load_tool(1, "s7online",ba); 返回值为int,如果返回0则表示执行成功,非零值,则需要根据错误号查到错误具体信息,具体参照本文第五部分:错误代码字典
      当然如果PLC使用的是DP网络时,只需要将Set PG/PC Interface中接口参数分配选为PLCSIM(PROFIBUS)即可,Prodave不需要做任何修改(当然PLC地址肯定也是DP口的地址哦),具体如下图:

个人不太习惯西门子的这种函数命名,索性就按照C#的常用习惯,进行一下简单的封装,供外部调用.
 1/**//// <summary>建立连接,同一个连接只容许调用一次
 2/// </summary>
 3/// <param name="connNo">连接号connNo1-4</param>
 4/// <param name="connParam">连接参数,PLCConnParam定义的参数结构体</param>
 5/// <returns>返回10进制错误号,0表示没有错误</returns>
 6public static int Open(byte connNo, PLCConnParam[] connParam)
 7{
 8    int PLCCPUCnt = connParam.Length;
 9    if (PLCCPUCnt <= 0//传递参数不正确
10    {
11        return -1;
12    }
13    byte[,] btr = new byte[PLCCPUCnt + 14]; //多分配1,用于存放0作为连接结束标记
14    //转换连接表
15    for (int i = 0; i < connParam.Length; i++)
16    {
17        btr[i, 0] = connParam[i].Addres;
18        btr[i, 1] = 0;
19        btr[i, 2] = connParam[i].Slot;
20        btr[i, 3] = connParam[i].Rack;
21    }
22    btr[connParam.Length, 0] = 0;
23    btr[connParam.Length, 1] = 0;
24    btr[connParam.Length, 2] = 0;
25    btr[connParam.Length, 3] = 0;
26    //调用初始化函数,打开连接
27    int errCode = load_tool(connNo, "S7ONLINE", btr);
28    return errCode;
29}
    建立于PLC的连接,只需在数采程序启动的时候调用即可,并且只能打开一次,否则报错.
动设备名称"S7ONLINE",一般情况下是不会有变化的,所以这里就写死了.特别需要指出的是,这个函数的第一个参数(连接号),是指当前连接有多少个PLC连接(严格意义上来讲,CPU的个数,因为有可能2PLC共用1CPU,之间通过IM467组态),激活连接并交换数据的时候,和这个值有点关系. 在建立连接的时候默认激活第1个连接.

3.2 断开与PLC的连接
W95_S7.DLL中导入函数,依然是私有,因为我要对所有的导入函数进行封装.
1/**//// <summary>断开与PLC的连接,必须退出数采软件之前调用,否则PLC的连接一直被占用,影响下次连接
2/// </summary>
3/// <returns>0正常返回,0为错误号</returns>
4[DllImport("w95_s7.dll")]
5private extern static int unload_tool();
关闭PLC的连接函数进行C#封装,没有改变任何代码,只是换了个函数名.
1public static int Close()
2{
3    return unload_tool();
4}

3.3 激活连接,当前连接列中某个时刻有且只有1PLC是激活状态.建立连接的时候,默认激活第1个连接.
1/**//// <summary>激活与MPI网中的哪个CPU通讯,load_tool后默认激活第一个CPU连接
2/// </summary>
3/// <param name="no">连接号,对应于参数adr_table所传递的连接参数顺序</param>
4/// <returns>0正常返回,0为错误号,若激活的连接在MPI网中没有,则返回错误号517</returns>
5[DllImport("w95_s7.dll")]
6private extern static int new_ss(byte no);
      其参数与load_tool中参数adr_table所传递的连接参数顺序对应譬如byte[,] btr={{2,0,2,0},{3,0,2,0},{0,0,0,0}} , new_ss(1)则激活第1个连接即与MPI地址为2PLC通讯,类似的new_ss(2)则激活与MPI地址为3PLC通讯,在数采系统中,为了读取所有PLC的数据,采用定时循环激活每个PLC的连接,然后读取其数据.
C#封装如下:
1public static int ActiveConn(int connNO)
2{
3    param namereturn new_ss((byte)connNO);
4}

3.4 DB块中读取字节数据(返回BYTE数组)
1/**//// <summary>DB中读取BYTE数组(字节数可以是任意长度的)
2/// </summary>
3/// <param name="blockno">DB块号</param>
4/// <param name="no">DBB起始编号,0表示DBB0,1表示DBB1,跨度为BYTE</param>
5/// <param name="amount">读取的BYTE长度(任意长度,可以为奇数)</param>
6/// <param name="buffer">返回值,BYTEbuffer</param>
7/// <returns>0正常返回,0为错误号</returns>
8[DllImport("w95_s7.dll")]
9private extern static int d_field_read(int blockno, int no, int amount, byte[] buffer);
C#封装如下:
 1/**//// <summary>读取DB块的BYTE数据
 2/// </summary>
 3/// <param name="DBBlockNO">DB块号,:DB2</param>
 4/// <param name="DBBNO">DB数据的起始字节,DBB2则从2开始读</param>
 5/// <param name="DBByteAmount">要读取的字节数,如从DBB2--DBB5,4个字节</param>
 6/// <param name="buffer">BYTE型缓存区,存储读取的数据</param>
 7/// <param name="StartIndex">数据缓存区的起始位置</param>
 8/// <returns>返回值 0:成功  0:错误代码</returns>
 9public static int GetDBByteData(int DBBlockNO, int DBBNO, int DBByteAmount, byte[] buffer, int StartIndex)
10{
11    byte[] bBufTemp = new byte[DBByteAmount];
12    int errCode=d_field_read(DBBlockNO, DBBNO, DBByteAmount, bBufTemp);
13    for(int i=0;i<DBByteAmount;i++)
14    {
15        buffer[i+StartIndex] = bBufTemp[i] ;
16    }
17    return errCode;
18}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。