C#与PLC通信开发之三菱FX系列PLC
前言
本文讲解的是上位机如何与三菱FX系列PLC进行通信,开发语言使用的是C#语言,代码不多,主要是讲解通信协议。
刚做了一个转盘式翻斗分拣机项目,采用的是三菱PLC,之前没有接触过三菱的PLC,查看了一些资料,但是这些资料要么不完整,或者写的不够清楚,导致自己实际开发的时候,还是碰到一些问题,所以想花点时间总结一下,写一篇比较完整而且得到实际项目验证的文章记录下来。
简单介绍一下项目:项目的功能是按波次分拣订单,订单的特点是:每个订单的数量较多,商品的尺寸较小,大部分是胶带,小部分是很轻很薄的纸片式的商品,还有部分是比较大的比如记事本啊什么的。
分拣机由51个分拣口(含一个异常口),每个波次可以分拣50个订单,每个波次2000-3500件商品。由于商品的特殊性,没办法采用相机顶扫的方式,所以采用人工扫码,这台分拣机两个熟练的操作员操作的话,可以达到每秒1件商品的分拣速度,一小时能分拣3000多件商品,效率还是杠杠的。
好了,闲话少说,下面切入正题。
三菱FX系列PLC
通信接口
三菱FX系列PLC,采用串口与PC机进行通信,使用 SC-09 编程电缆作为计算机与 PLC 通信的连线。电缆的 9 芯 D 形插头连接在计算机串口上,另一端连接 PLC 的 RS-422 编程口。
FX-PLC
SC-09-DB9线
串口参数设置如下:
波特率:9600
奇偶校验:Even(偶校验)
数据位:7位
停止位:1位
串口参数设置好了以后,连接PLC,使用串口调试助手,按16进制发送05给PLC,如果连接正常的话,PLC会返回06。
通信协议
三菱FX系列PLC的通信协议有三个特点:
- 上位机(我们的PC机)和PLC之间的通信是应答式的,也就是说,上位机给PLC发送指令,PLC作出相应的应答,PLC不会主动给上位机发送任何指令。注意:每次给PLC发送指令后,必须等待PLC的应答完成才能发送下一条指令,否则下一条指令将会失败!
- 指令都是ASCII码的形式,而且都是十六进制的。比如你要往某个地址写入数值100,如果用二进制表示,那么只需要一个字节就行了,但是用ASCII码表示,就需要3个字节,在内存里面分别是:31H、30H、30H。整条指令,包括起始符、终止符、地址、值、校验都是使用ASCII码。
- 指令简单。除了上文所说的05H通信测试指令之外,还有另外4条指令:
我在项目中,主要用到的就是读写这两个指令,置位和复位这两个指令没有用到,不过下面将会逐一的讲解这4个指令(置位和复位这两个指令的内容从别人的文章里复制~)
校验
这4条指令都涉及到校验,所以在逐一讲解这4条指令之前,先说一下校验。三菱FX系列PLC的指令校验方式,采用的是和校验方式,也就是累加求和,然后取最后两位。很简单,直接上代码吧:
public string CheckSum(byte[] cmd)
{
byte sum = 0;
for (int i = 0; i < cmd.Length; i++)
{
sum = (byte)(sum + cmd[i]);
}
return sum.ToString("X2");
}
地址
地址需要进行转换,转算算法为:
address = address*2 + 1000H
比如要往D134这个地址写入数据,那么地址为:
address = 134*2 + 4096
值得注意的是,FX系列PLC,读写的基本单元是字,也就是说每个地址对应的是两个字节,如图所示:
读指令
上位机指令
先来看看读指令的指令格式,一条完整的读指令,包含起始符(ASCII码里面的STX)、命令(30H)、首地址、读取的字节数、终止符(ASCII码里面的ETX)、校验这6个部分。下面的图示里,我们以读取首地址为D123为例,读取2个字节,也就是读一个short数据出来:
下面逐一讲解各个部分:
-
STX:起始符,ASCII码里面的STX,值为02H。一个字节。
-
CMD:指令,读指令为ASCII码的’0’,值为30H。一个字节。
-
首地址:读取数据的起始地址,4个字节表示。地址的计算方式,上文有讲解,这个图里我们读取的是D123的地址,根据计算公式address=123*2 + 4096 = 10F6H,转换成ASCII码,就是31H,30H,46H,36H。
-
字节数:要去读的字节数,2个字节表示。需要注意的是,一条读指令,最多能读取64个字节。
-
ETX:指令终止符,ASCII码里面的ETX,值为03H。一个字节。
-
校验:2个字节表示。校验在前面也讲解过,采用的是累加求和,取最后两个字节。从CMD到ETX这部分参与运算:
PLC响应
如果一切正常的话,那么PLC会对读指令进行响应,返回要读取的数据,我们假设PLC返回的short值是30,那么响应格式为:
基本上,PLC响应的各部分和指令是相同的。需要注意的是值这个部分,我们的指令是读取2个字节,但是这个部分有4个字节,是因为FX系列通信协议里,指令和响应都是ASCII码表示的,每个字节用两个ASCII码字符表示,所以一共有4个字节。如果我们是按字读取值的话,也就是读取一个short的值,那么有两个地方需要注意:
- 字节顺序是:低位在前,高位在后,解析的时候需要调整顺序。
- 里面存储的是ASCII码,需要进行转换。
如何把这4个字节,还原成一个short呢?
首先,将31H45H30H30H,高低位调整顺序变成30H30H31H45H,然后把ASCII码转换成值001EH,也就是1*16 + 14 = 30。
其中,ASCII码转换成数值的代码如下:
public static int AsciiToInt(byte ascVal)
{
if (ascVal >= 0x30 && ascVal <= 0x39) // ASCII字符0-9之间
{
return (ascVal - 0x30);
}
else if (ascVal >= 0x41 && ascVal <= 0x46) // ASCII字符A-F之间
{
return (ascVal - 0x41 + 10);
}
else if (ascVal >= 0x61 && ascVal <= 0x66) // ASCII字符a-f之间
{
return (ascVal - 0x61 + 10);
}
else
{
return -1;
}
}
完整的转换一个short值的代码如下:
public static short TranslateToShortValue(byte[] buf)
{
int lowByte = AsciiToInt(buf[0]) * 16 + AsciiToInt(buf[1]);
int highByte = AsciiToInt(buf[2]) * 16 + AsciiToInt(buf[3]);
int intVal = highByte * 256 + lowByte;
return (short)intVal;
}
其中,buf数组里面,存储的就是从PLC里面读取的数据31H45H30H30H。
写指令
上位机指令
假设我们往地址D123里写入一个short值98(注意,不是16进制),指令格式为:
大体上和读指令的格式是一样的,只是有两个地方需要注意:
- 字节数:这里我们是按short类型写入,也就是写入一个字,所以字节数是2个。但是在数据部分,却占了4个字节,是因为指令全部由ASCII码组成。
- 数据部分:首先数值98要转换成16进制0063H,然后低位在前,高位在后,然后再转换成ASCII码。
PLC响应
写指令因为不需要返回数据,所以PLC的响应很简单,只返回一个字节。
- 如果写入正确,则返回ACK(06H)
- 如果写入错误,则返回NAK(15H)
完整的代码如下:
private static string STX = "\x02";
private static string ETX = "\x03";
private SerialPort _serialPort;
public bool WriteData(int addr, short value)
{
if (null == _serialPort || !_serialPort.IsOpen)
{
return false;
}
StringBuilder sb = new StringBuilder();
sb.Append("1"); // CMD
addr = addr * 2 + 4096;
sb.Append(addr.ToString("X4")); // 首地址
sb.Append("02"); //字节数
string strValue = value.ToString("X4"); // 数据
sb.Append(strValue.Substring(2, 2)); // 低字节在先
sb.Append(strValue.Substring(0, 2)); // 高字节在后
sb.Append(ETX); // 结束符
// 计算SUM
byte[] cmd = System.Text.Encoding.ASCII.GetBytes(sb.ToString());
string sum = CheckSum(cmd);
sb.Append(sum); // SUM
sb.Insert(0, STX); // 插入起始符
try
{
// 转换成字节并写入串口
byte[] cmdArr = System.Text.Encoding.ASCII.GetBytes(sb.ToString());
_serialPort.Write(cmdArr, 0, cmdArr.Length);
}
catch (System.ServiceProcess.TimeoutException ex)
{
Logger.WriteLog("写入plc超时");
return false;
}
catch (Exception ex)
{
Logger.WriteLog("写入plc失败,异常:" + ex.Message);
return false;
}
try
{
// 读取PLC响应
int ret = _serialPort.ReadByte();
if (ret == 6)
{
// 正确应答
return true;
}
}
catch (System.ServiceProcess.TimeoutException ex)
{
Logger.WriteLog("读取plc应答超时");
}
catch (InvalidOperationException ex)
{
Logger.WriteLog("读取plc应答失败,串口未打开");
}
catch (Exception ex)
{
Logger.WriteLog("读取plc应答失败,异常:" + ex.Message);
}
return false;
}
置位指令
上位机指令
这里需要注意的就是:地址计算方式,address = address / 8 + 100H
PLC响应
PLC的响应很简单,只返回一个字节。
- 如果写入正确,则返回ACK(06H)
- 如果写入错误,则返回NAK(15H)
复位指令
上位机指令
这里需要注意的就是:地址计算方式,address = address / 8 + 100H
PLC响应
PLC的响应很简单,只返回一个字节。
- 如果写入正确,则返回ACK(06H)
是:地址计算方式,address = address / 8 + 100H
PLC响应
PLC的响应很简单,只返回一个字节。
- 如果写入正确,则返回ACK(06H)
- 如果写入错误,则返回NAK(15H)
复位指令
上位机指令
[外链图片转存中…(img-9eSEcDCT-1603044910210)]
这里需要注意的就是:地址计算方式,address = address / 8 + 100H
PLC响应
PLC的响应很简单,只返回一个字节。
- 如果写入正确,则返回ACK(06H)
- 如果写入错误,则返回NAK(15H)