最近在做一个数据采集项目,涉及到许多与西门子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] ,下载地址:
http://files.cnblogs.com/J0YANG/Prodave.pdf
附件2:Prodave6.0动态库[W95_S7.DLL],下载地址:
http://files.cnblogs.com/J0YANG/W95_S7.rar
附件3:DCProdave.cs[C#封装的源代码],下载地址:
http://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所需参数]
2
public
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的存储区域编号]
2
public
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
"
)]
8
private
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>
6
public
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}
建立于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
"
)]
5
private
extern
static
int
unload_tool();
关闭PLC的连接函数进行C#封装,没有改变任何代码,只是换了个函数名.
1
public
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
"
)]
6
private
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#封装如下:
1
public
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
"
)]
9
private
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>
9
public
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
"
)]
9
private
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>
10
public
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
"
)]
8
private
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
"
)]
8
private
extern
static
int
a_field_read(
int
no,
int
anzahl,
byte
[] buffer);
Q区读取函数,参数与I一样.
1
[DllImport(
"
w95_s7.dll
"
)]
2
private
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>
9
public
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>
8
public
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>
6
public
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>
6
public
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>
6
public
static
string
GetErrInfo(
int
errCode)
7
{
8 switch (errCode)
9 {
10 case -1: return "User-Defined Error!"; //自定义错误,主要是参数传递错误!
11 case 0x0000: return "Success";
12 case 0x0001: return "Load dll failed";
13 case 0x00E1: return "User max";
14 case 0x00E2: return "SCP entry";
15 default: return "Unkonw error";
16 }
17}
由于错误代码比较多,这里只罗列了几个,详细信息请下载源代码
DCProdave.cs进行查看,这里不再详述.
六. DCProdave.cs应用举例
为保证数据的一致性,可以使用一个定时器,触发时间设为PLC扫描周期,在其触发事件中,把需要用到的PLC变量一次性读取.建立与PLC的连接,示例如下
1
PLCConnParam[] Conn
=
new
PLCConnParam[
2
];
//
MPI网中有2个PLC,地址分别为2,3
2
Conn[
0
] .Addres
=
2
; Conn[
0
].Slot
=
2
; Conn[
0
].Rack
=
0
;
3
Conn[
1
] .Addres
=
3
; Conn[
1
].Slot
=
2
; Conn[
1
].Rack
=
0
;
4
errCode
=
DCProdave.Open(
1
,Conn);
//
建立连接
5
errCode
=
DCProdave.ActiveConn(
1
);
//
激活第一个连接
6
errCode
=
DCProdave.GetDBByteData(
2
,
0
,
6
, buf,
0
);
//
DB2.DBW0--DBW5 共6个字节的变量,从buf的0位存储
7
if
(errCode
!=
0
)
{//DCLog.Write(DCProdave.GetErrInfo(errCode),"log.txt");}//如果返回值不=0,则将错误写入日志
还有很多往PLC写入数据的函数,这里没有介绍,文中有很多不足之处,欢迎希望路过的各位XDJM在此留言.
邮箱:
[email protected]