浅谈西门子Prodave6.0在C#中的应用

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

Prodave版本: PRODAVE6.0 - W95_S7.DLL
PLC模拟环境: PLCSIM V5.4
开发环境: VS.NET 2005
附件1:Prodave6.0手册[English] ,下载地址:https://files.cnblogs.com/J0YANG/Prodave.pdf
附件2:Prodave6.0动态库[W95_S7.DLL],下载地址: https://files.cnblogs.com/J0YANG/W95_S7.rar
附件3:DCProdave.cs[C#封装的源代码],下载地址: https://files.cnblogs.com/J0YANG/DCProdave.rar

一.从w95_s7.dll中导入PLC通讯函数的方法[DllImport]
在使用DllImport之前,必须引入InteropServices, 代码如下: using System.Runtime.InteropServices;
具体使用方法可以参考我的博客中转载的一篇文章 《C#(.net)中的DllImport用法[转] 》写的很不错,千万要注意C++数据类型到C#的对应关系,选用合适的类型。比如 char* 可以用string来转换,指针类型可以ref  或者数组。
原文地址:http://www.cnblogs.com/xumingming/archive/2008/10/10/1308248.html

二.定义结构体类型
2.1 PLC连接参数结构体
1//定义结构体[连接PLC所需参数]
2public struct PLCConnParam 
3{
4    public byte Addres;           // 定义CPU的MPI/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地址分别为2和3,槽号均为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">连接号connNo为1-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的个数,因为有可能2个PLC共用1个CPU,之间通过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 激活连接,当前连接列中某个时刻有且只有1个PLC是激活状态.建立连接的时候,默认激活第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地址为2的PLC通讯,类似的new_ss(2)则激活与MPI地址为3的PLC通讯,在数采系统中,为了读取所有PLC的数据,采用定时循环激活每个PLC的连接,然后读取其数据.
C#封装如下:
1public static int ActiveConn(int connNO)
2{
3    return 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">返回值,BYTE型buffer</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}
      这个函数是用的最多的一个函数,在数采系统中,习惯一次性的将所有需要用到数据,全部读到字节数组中,统一处理,避免不同时期凌乱读取造成的数据不一致.需要提醒的是,必须保证数据处理函数得到的数据,是PLC一次扫描周期内的.

3.5 从DB中读取整数值(int32型)
1/// <summary>从DB中读取INT数据(DBW:INT16 或者 DBD:INT32),最多4个字节的整数
2/// </summary>
3/// <param name="dbno">DB块号</param>
4/// <param name="dwno">DBW起始编号,0表示DBW0,1表示DBW2,跨度为WORD</param>
5/// <param name="anzahl">读取的WORD长度(1个WORD==2个BYTE) 2:DBW , 4:DBD</param>
6/// <param name="buffer">返回值,int型整数(十进制)</param>
7/// <returns>0正常返回,非0为错误号</returns>

8[DllImport("w95_s7.dll")]
9private extern static int db_read(int dbno, int dwno, ref int anzahl, ref int buffer);
C#封装
 1/// <summary>从DB块中读取整型数据
 2/// 要读取DB2.DBW6,则DB块号为2,DBB号为6,字节长度为2
 3/// 要读取DB2.DBD6,则DB块号为2,DBB号为6,字节长度为4
 4/// </summary>
 5/// <param name="DBBlockNO">DB块号,如:DB2</param>
 6/// <param name="DBBNO">DBB的起始字节号,如DBW2则从2开始读,由于是WORD(2个BYTE),DBB号必须为偶数</param>
 7/// <param name="DBByteAmount">要读取的BYTE数,必须是偶数(这里只能是2和4,在PLC中只有DBW,DBD两种整数)</param>
 8/// <param name="buffer">INT32型缓存区,存储读取的十进制数据</param>
 9/// <returns>返回值 0:成功  非0:错误代码</returns>

10public static int GetDBInt32Data(int DBBlockNO, int DBBNO, int DBByteAmount, ref int buffer)
11{
12    int DBWNO = DBBNO / 2;
13    int DBWordAmount = DBByteAmount / 2;
14    int errCode = db_read(DBBlockNO, DBWNO, ref DBWordAmount, ref buffer);
15    byte[] bbuf = new byte[4];
16    GetByteFromInt32(buffer, bbuf, true);
17    buffer = bbuf[0* 0x1000000 + bbuf[1* 0x10000 + bbuf[2* 0x100 + bbuf[3];
18    return errCode;
19}
这个函数读取的是一个整数,因为DB中有DBB,DBW,DBD3种数据类型,最大的DBD是4个字节,所以设计了这个函数,读取单个的整型值,不用再进行BYTE[]到INT的转换了.这里值得一提的是整数高位优先,还是低位优先的问题,字节数组的顺序切记要矫正,另外buffer = bbuf[0* 0x1000000 + bbuf[1* 0x10000 + bbuf[2* 0x100 + bbuf[3];这行代码很有意思,16进制的字节进位是0x100.

3.6 M,I,Q 3种块的读取函数类似(参数都是一样的),这里放在一起进行说明
M区读取函数
1/// <summary>读取PLC中的M字节数据
2/// </summary>
3/// <param name="no">指定M字节号,譬如要读取MB10的值,则指定no等于10</param>
4/// <param name="anzahl">指定读取的字节数,譬如需要读取MB10至MB14之间的值,则可指定为5</param>
5/// <param name="buffer">返回获取的值,这是一个十进制的值,如果需要获取某一个M位的状态,需要把它转换成二进制</param>
6/// <returns>0正常返回,非0为错误号</returns>

7[DllImport("w95_s7.dll")]
8private extern static int m_field_read(int no, int anzahl, byte[] buffer);
I区读取函数,一直不明白为什么输入区(单词input)简称为I,而函数名却为A, 后来才晓得,这个A是德文的表示方法,(Pordave是西门子公司的东东).
1/// <summary>读取Output值
2/// </summary>
3/// <param name="no">QB号</param>
4/// <param name="anzahl">读出多少个QB字节</param>
5/// <param name="buffer">返回读出的值,十进制</param>
6/// <returns>0正常返回,非0为错误号</returns>

7[DllImport("w95_s7.dll")]
8private extern static int a_field_read(int no, int anzahl, byte[] buffer);
Q区读取函数,参数与I一样.
1[DllImport("w95_s7.dll")]
2private extern static int e_field_read(int no, int anzahl, byte[] buffer);

C#封装,  M,I,Q 3种块的读取函数类似,这里放在一个函数里,利用枚举类型PLCBlockType进行区分
 1/// <summary>从M,I,Q区中读取字节数组
 2/// </summary>
 3/// <param name="blockType">Block类别,在枚举PLCBlockType中定义,如要读取M区的值,则blockType=PLCBlockType.M</param>
 4/// <param name="BlockNO">区号,如IB10,MB10</param>
 5/// <param name="ByteAmount">要读取的字节数量,如IB10--IB14共5个字节</param>
 6/// <param name="bbuf">byte[]类型的buffer</param>
 7/// <param name="StartIndex">byte[]存储的起始位置</param>
 8/// <returns>0正常返回,非0为错误号</returns>

 9public static int GetMIQByteData(PLCBlockType blockType,  int BlockNO, int ByteAmount, byte[] bbuf, int StartIndex)
10{
11    int errCode = 0;
12    byte[] bBufTemp = new byte[ByteAmount]; //局部变量,不用担心内存释放的问题. C++程序员看到"new"估计很谨慎.
13    switch (blockType) //根据块类别,调用相应的块读取函数.
14    {
15        case PLCBlockType.M: errCode = m_field_read(BlockNO, ByteAmount, bBufTemp); break;
16        case PLCBlockType.I : errCode = e_field_read(BlockNO, ByteAmount, bBufTemp); break;
17        case PLCBlockType.Q: errCode = a_field_read(BlockNO, ByteAmount, bBufTemp); break;
18    }

19    for (int i = 0; i < ByteAmount; i++)  //由于C#中对指针有所限制,从数组指定的起始位置,逐个赋值.
20    {
21        bbuf[i + StartIndex] = bBufTemp[i];
22    }

23    return errCode;
24}

四, BYTE,INT,BOOL几种类型的数据转换函数
4.1 从32位整数中提取字节数组(4个byte)
 1/// <summary>从INT32型数据中提取byte字节数组
 2/// </summary>
 3/// <param name="lbuf">源数据(long型)</param>
 4/// <param name="bbuf">字节数组,存放提取的Byte数据</param>
 5/// <param name="startIndex">起始位置</param>
 6/// <param name="ByteAmount">提取的字节数</param>
 7/// <param name="isBigEndian">long型源数据是否高位优先,如果不是,则进行反向提取</param>

 8public static void GetByteFromInt32(int ibuf, byte[] bbuf , bool isBigEndian)
 9{
10    if (isBigEndian) //高位优先,则反向提取.
11    {
12        for (int i = 0; i <=3; i++//Int32只有4个字节
13        {
14            bbuf[i] = (byte)(ibuf & 0x000000ff); //取低位字节
15            ibuf >>= 8;  //右移8位
16        }

17    }

18    else //低位优先,按顺序提取.
19    {
20        for (int i = 3; i >= 0; i--)
21        {
22            bbuf[i] = (byte)(ibuf & 0x000000ff);
23            ibuf >>= 8;
24        }

25    }

26}

4.2 从字节数据中提取bit数组(8个bit),以bool型数据代替位表示.
 1/// <summary>从Byte数据中取得所有bit的值(1Byte=8Bit , false:0 , true:1)
 2/// </summary>
 3/// <param name="byteData">源数据(Byte型),其中的8个bit位,从右到左0--7编号</param>
 4/// <param name="bitArray">bit数组,存放Byte中的8个bit的值,0:false, 1:true</param>
 5/// <param name="startIndex">在bit数组中存放的起始位置</param>

 6public static void GetBitFromByte(byte byteData, bool[] bitArray, int startIndex)
 7{
 8    byte[] byteArray = new byte[1];
 9    byteArray[0= byteData;
10    System.Collections.BitArray BA = new System.Collections.BitArray(byteArray);
11    for (int i = 0; i <= 7; i++//依次取8个位,逐个赋值
12    {
13        bitArray[startIndex + i] = BA.Get(i);
14    }

15}

4.3 从字节数据中提取某一位的状态,以bool型返回
 1/// <summary>从Byte数据中取得某一位bit的值(false:0 , true:1)
 2/// </summary>
 3/// <param name="byteData">源数据(Byte型),其中的8个bit位,从右到左0--7编号</param>
 4/// <param name="bitNo">bit位编号,从右到左以0--7编号</param>
 5/// <param name="bitData">bit值,以bool型返回,false:0 , true:1</param>

 6public static void GetBitFromByte(byte byteData, int bitNo, ref bool bitData)
 7{
 8    if (bitNo >= 0 && bitNo <= 7//位号必须在0~7之间
 9    {
10        byte[] byteArray = new byte[1];
11        byteArray[0= byteData;
12        System.Collections.BitArray BA = new System.Collections.BitArray(byteArray);
13        bitData = BA.Get(bitNo);
14    }

15}


五.错误代码字典
 1/// <summary>根据错误代码返回错误信息
 2/// 例如int errCode=ActiveConn(1);  sring errInfo = GetErrInfo(err);
 3/// </summary>
 4/// <param name="errCode">错误码</param>
 5/// <returns>错误信息</returns>

 6public static string GetErrInfo(int errCode)
 7{
 8    switch (errCode)
 9    {
10        case -1return "User-Defined  Error!";  //自定义错误,主要是参数传递错误!
11        case 0x0000return "Success";
12        case 0x0001return "Load dll failed";
13        case 0x00E1return "User max";
14        case 0x00E2return "SCP entry";
15        defaultreturn "Unkonw error";
16    }

17}
      由于错误代码比较多,这里只罗列了几个,详细信息请下载源代码DCProdave.cs进行查看,这里不再详述.

六. DCProdave.cs应用举例
      为保证数据的一致性,可以使用一个定时器,触发时间设为PLC扫描周期,在其触发事件中,把需要用到的PLC变量一次性读取.建立与PLC的连接,示例如下
1PLCConnParam[] Conn=new PLCConnParam[2]; //MPI网中有2个PLC,地址分别为2,3
2Conn[0] .Addres=2;  Conn[0].Slot=2;  Conn[0].Rack=0;  
3Conn[1] .Addres=3;  Conn[1].Slot=2;  Conn[1].Rack=0;
4errCode= DCProdave.Open(1,Conn);  //建立连接
5errCode= DCProdave.ActiveConn(1); //激活第一个连接
6errCode= DCProdave.GetDBByteData(206, buf, 0); //DB2.DBW0--DBW5 共6个字节的变量,从buf的0位存储
7if(errCode!=0){//DCLog.Write(DCProdave.GetErrInfo(errCode),"log.txt");}//如果返回值不=0,则将错误写入日志

还有很多往PLC写入数据的函数,这里没有介绍,文中有很多不足之处,欢迎希望路过的各位XDJM在此留言.
邮箱: J0YANG@163.com

posted on 2009-08-12 23:47  J0YANG  阅读(8294)  评论(12编辑  收藏  举报

导航