最近在做一个数据采集项目,涉及到许多与西门子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
开发环境: VS.NET 2005
一.从w95_s7.dll中导入PLC通讯函数的方法[DllImport]
在使用DllImport之前,必须引入InteropServices, 代码如下: using System.Runtime.InteropSer
在这里很感谢吴向阳,在中国工控网上面发现了他的文章,因为我是从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连接参数结构体
具体使用方法可以参考我的博客中转载的一篇文章 《C#()中的DllImport用法[转] 》写的很不错,千万要注意C++数据类型到C#的对应关系,选用合适的类型。比如 char* 可以用string来转换,指针类型可以ref 或者数组。
二.定义结构体类型
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}
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}
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#将会对此函数进行封装,供外部调用,稍后讲解.
首先从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);
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口的地址哦),具体如下图:
在一个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 + 1, 4]; //多分配1个,用于存放0作为连接结束标记
14 //转换连接表
15 for (int i = 0; i < connParam.Length; i++)
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 + 1, 4]; //多分配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}
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中导入函数,依然是私有,因为我要对所有的导入函数进行封装.
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();
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}
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);
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#封装如下:
C#封装如下:
1public static int ActiveConn(int connNO)
2{
3 param namereturn new_ss((byte)connNO);
4}
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">返回值,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);
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);
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}
14 {
15 buffer[i+StartIndex] = bBufTemp[i] ;
16 }
17 return errCode;
18}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论