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

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

最近在做一个数据采集项目,涉及到许多与西门子S7系列PLC的通信,由于自己的VC水平属于半瓶子晃荡,所以就想利用C#来进行开发(个人比较喜欢C#的代码风格,看着很清爽),虽然C#这种高级语言与底层的设备通讯效率确实不如C++,但好在数据量不大,实时性要求不算太高,用C#还是可以应付的。在界面开发方面,高级语言确实不如WinCC,Intouch之类的组态软件,但在数据处理上面,却有很大的灵活性。

在这里很感谢吴向阳,在中国工控网上面发现了他的文章,因为我是从C#转到工控方向的,以前对PLC一点都不懂,刚接触这一行时,学习起来很吃力,看了他的这篇文章,让我对PLC的有了更加深入的了解。我的这个DCProdave.cs就是在他的基础上修改的,加了一些自己的东西。还望各位多多提意见,多多交流!

Prodave版本: PRODAVE6.0 - W95_S7.DLL

PLC模拟环境: PLCSIM V5.4

开发环境: https://www.360docs.net/doc/b28765562.html, 2005

一.从w95_s7.dll中导入PLC通讯函数的方法[DllImport]

在使用DllImport之前,必须引入InteropServices, 代码如下: using System.Runtime.InteropServices; 具体使用方法可以参考我的博客中转载的一篇文章《C#(.net)中的DllImport用法[转] 》写的很不错,千万要注意C++数据类型到C#的对应关系,选用合适的类型。比如char* 可以用string来转换,指针类型可以ref 或者数组。

二.定义结构体类型

2.1 PLC连接参数结构体

1//定义结构体[连接PLC所需参数]

2public struct PLCConnParam

3{

4public byte Addres; // 定义CPU的MPI/DP地址

5 //public byte SegmentId; // 保留为0

6public byte Rack; // 定义CPU的机架号

7public 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///

与PLC建立连接,该函数必须在其他所有函数调用之前被调用

2///

3///连接数,在DOS,WIN3.1最多可以有4个,在WIN95以上最多可以有16个< /param>

4///与PLC通讯的设备名称,一般为S7ONLINE

5///参数列表,4个值分别为MPI/DP地址,保留值=0,槽号,机架号

6///0正常返回,非0为错误号

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///

建立连接,同一个连接只容许调用一次

2///

3///连接号connNo为1-4

4///连接参数,PLCConnParam定义的参数结构体 5///返回10进制错误号,0表示没有错误

6public static int Open(byte connNo, PLCConnParam[] connParam)

7{

8int PLCCPUCnt = connParam.Length;

9if (PLCCPUCnt <= 0) //传递参数不正确

10 {

11return -1;

12 }

13byte[,] btr = new byte[PLCCPUCnt + 1, 4]; //多分配1个,用于存放0作为连接结束标记

14 //转换连接表

15for (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//调用初始化函数,打开连接

27int errCode = load_tool(connNo, "S7ONLINE", btr);

28return errCode;

29}

建立于PLC的连接,只需在数采程序启动的时候调用即可,并且只能打开一次,否则报错. 驱动设备名称"S7ONLINE",一般情况下是不会有变化的,所以这里就写死了.特别需要指出的是,这个函数的第一个参数(连接号),是指当前连接有多少个PLC连接(严格意义上来讲,是CPU的个数,因为有可能2个PLC共用1个CPU,之间通过IM467组态),激活连接并交换数据的时候,和这个值有点关系. 在建立连接的时候默认激活第1个连接.

3.2 断开与PLC的连接

从W95_S7.DLL中导入函数,依然是私有,因为我要对所有的导入函数进行封装.

1///

断开与PLC的连接,必须退出数采软件之前调用,否则PLC的连接一直被占用,影响下次连接

2///

3///0正常返回,非0为错误号

4[DllImport("w95_s7.dll")]

5private extern static int unload_tool();

关闭PLC的连接函数进行C#封装,没有改变任何代码,只是换了个函数名.

1public static int Close()

2{

3return unload_tool();

4}

3.3 激活连接,当前连接列中某个时刻有且只有1个PLC是激活状态.建立连接的时候,默认激活第1个连接. 1///

激活与MPI网中的哪个CPU通讯,load_tool后默认激活第一个CPU连接

2///

3///连接号,对应于参数adr_table所传递的连接参数顺序

4///0正常返回,非0为错误号,若激活的连接在MPI网中没有,则返回错误号517

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{

3return new_ss((byte)connNO);

4}

3.4 从DB块中读取字节数据(返回BYTE数组)

1///

从DB中读取BYTE数组(字节数可以是任意长度的)

2///

3///DB块号

4///DBB起始编号,0表示DBB0,1表示DBB1,跨度为BYTE

5///读取的BYTE长度(任意长度,可以为奇数)

6///返回值,BYTE型buffer

7///0正常返回,非0为错误号

8[DllImport("w95_s7.dll")]

9private extern static int d_field_read(int blockno, int no, int amount, byte[] buffer);

C#封装如下:

1///

读取DB块的BYTE数据

2///

3///DB块号,如:DB2

4///DB数据的起始字节,如DBB2则从2开始读

5///要读取的字节数,如从DBB2--DBB5,共4个字节 6///BYTE型缓存区,存储读取的数据

7///数据缓存区的起始位置

8///返回值 0:成功非0:错误代码

9public static int GetDBByteData(int DBBlockNO, int DBBNO, int DBByteAmount, byte[] buffe r, int StartIndex)

10{

11byte[] bBufTemp = new byte[DBByteAmount];

12int errCode=d_field_read(DBBlockNO, DBBNO, DBByteAmount, bBufTemp);

13for(int i=0;i

14 {

15 buffer[i+StartIndex] = bBufTemp[i] ;

16 }

17return errCode;

18}

这个函数是用的最多的一个函数,在数采系统中,习惯一次性的将所有需要用到数据,全部读到字节数组中,统一处理,避免不同时期凌乱读取造成的数据不一致.需要提醒的是,必须保证数据处理函数得到的数据,是PLC一次扫描周期内的.

3.5 从DB中读取整数值(int32型)

1///

从DB中读取INT数据(DBW:INT16 或者 DBD:INT32),最多4个字节的整数

2///

3///DB块号

4///DBW起始编号,0表示DBW0,1表示DBW2,跨度为WORD

5///读取的WORD长度(1个WORD==2个BYTE) 2:DBW , 4:DBD

6///返回值,int型整数(十进制)

7///0正常返回,非0为错误号

8[DllImport("w95_s7.dll")]

9private extern static int db_read(int dbno, int dwno, ref int anzahl, ref int buffer);

C#封装

1///

从DB块中读取整型数据

2///要读取DB2.DBW6,则DB块号为2,DBB号为6,字节长度为2

3///要读取DB2.DBD6,则DB块号为2,DBB号为6,字节长度为4

4///

5///DB块号,如:DB2

6///DBB的起始字节号,如DBW2则从2开始读,由于是WORD(2个BYT E),DBB号必须为偶数

7///要读取的BYTE数,必须是偶数(这里只能是2和4,在PLC中只有DBW,DBD两种整数)

8///INT32型缓存区,存储读取的十进制数据

9///返回值 0:成功非0:错误代码

10public static int GetDBInt32Data(int DBBlockNO, int DBBNO, int DBByteAmount, ref int buff

er)

11{

12int DBWNO = DBBNO / 2;

13int DBWordAmount = DBByteAmount / 2;

14int errCode = db_read(DBBlockNO, DBWNO, ref DBWordAmount, ref buffer);

15byte[] bbuf = new byte[4];

16 GetByteFromInt32(buffer, bbuf, true);

17 buffer = bbuf[0] * 0x1000000 + bbuf[1] * 0x10000 + bbuf[2] * 0x100 + bbuf[3];

18return 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///

读取PLC中的M字节数据

2///

3///指定M字节号,譬如要读取MB10的值,则指定no等于10

4///指定读取的字节数,譬如需要读取MB10至MB14之间的值,则可指定为5

5///返回获取的值,这是一个十进制的值,如果需要获取某一个M位的状态,需要把它转换成二进制

6///0正常返回,非0为错误号

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///

读取Output值

2///

3///QB号

4///读出多少个QB字节

5///返回读出的值,十进制

6///0正常返回,非0为错误号

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///

从M,I,Q区中读取字节数组

2///

3///Block类别,在枚举PLCBlockType中定义,如要读取M区的值,则b

lockType=PLCBlockType.M

4///区号,如IB10,MB10

5///要读取的字节数量,如IB10--IB14共5个字节

6///byte[]类型的buffer

7///byte[]存储的起始位置

8///0正常返回,非0为错误号

9public static int GetMIQByteData(PLCBlockType blockType, int BlockNO, int ByteAmount, by te[] bbuf, int StartIndex)

10{

11int errCode = 0;

12byte[] bBufTemp = new byte[ByteAmount]; //局部变量,不用担心内存释放的问题. C++程序员看到"new"估计很谨慎.

13switch (blockType) //根据块类别,调用相应的块读取函数.

14 {

15case PLCBlockType.M: errCode = m_field_read(BlockNO, ByteAmount, bBufTemp); brea k;

16case PLCBlockType.I : errCode = e_field_read(BlockNO, ByteAmount, bBufTemp); break; 17case PLCBlockType.Q: errCode = a_field_read(BlockNO, ByteAmount, bBufTemp); break;

18 }

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

20 {

21 bbuf[i + StartIndex] = bBufTemp[i];

23return errCode;

24}

四, BYTE,INT,BOOL几种类型的数据转换函数

4.1 从32位整数中提取字节数组(4个byte)

1///

从INT32型数据中提取byte字节数组

2///

3///源数据(long型)

4///字节数组,存放提取的Byte数据

5///起始位置

6///提取的字节数

7///long型源数据是否高位优先,如果不是,则进行反向提取

8public static void GetByteFromInt32(int ibuf, byte[] bbuf , bool isBigEndian)

9{

10if (isBigEndian) //高位优先,则反向提取.

11 {

12for (int i = 0; i <=3; i++) //Int32只有4个字节

13 {

14 bbuf[i] = (byte)(ibuf & 0x000000ff); //取低位字节

15 ibuf >>= 8; //右移8位

16 }

18else//低位优先,按顺序提取.

19 {

20for (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///

从Byte数据中取得所有bit的值(1Byte=8Bit , false:0 , true:1)

2///

3///源数据(Byte型),其中的8个bit位,从右到左0--7编号 4///bit数组,存放Byte中的8个bit的值,0:false, 1:true

5///在bit数组中存放的起始位置

6public static void GetBitFromByte(byte byteData, bool[] bitArray, int startIndex)

7{

8byte[] byteArray = new byte[1];

9 byteArray[0] = byteData;

10 System.Collections.BitArray BA = new System.Collections.BitArray(byteArray);

11for (int i = 0; i <= 7; i++) //依次取8个位,逐个赋值

12 {

13 bitArray[startIndex + i] = BA.Get(i);

14 }

15}

4.3 从字节数据中提取某一位的状态,以bool型返回

1///

从Byte数据中取得某一位bit的值(false:0 , true:1)

2///

3///源数据(Byte型),其中的8个bit位,从右到左0--7编号 4///bit位编号,从右到左以0--7编号

5///bit值,以bool型返回,false:0 , true:1

6public static void GetBitFromByte(byte byteData, int bitNo, ref bool bitData)

7{

8if (bitNo >= 0 && bitNo <= 7) //位号必须在0~7之间

9 {

10byte[] 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///

根据错误代码返回错误信息

2///例如int errCode=ActiveConn(1); sring errInfo = GetErrInfo(err);

3///

4///错误码

5///错误信息

6public static string GetErrInfo(int errCode)

7{

8switch (errCode)

9 {

10case -1: return"User-Defined Error!"; //自定义错误,主要是参数传递错误!

11case0x0000: return"Success";

12case0x0001: return"Load dll failed";

13case0x00E1: return"User max";

14case0x00E2: return"SCP entry";

15default: return"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(2, 0, 6, buf, 0); //DB2.DBW0--DBW5 共6个字节的变量,从buf的0位存储

7if(errCode!=0){//DCLog.Write(DCProdave.GetErrInfo(errCode),"log.txt");}//如果返回值不=0,则将错误写入日志

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

相关主题
相关文档
最新文档