FX5使用SLMP与上位机通讯测试
以下使用C#进行测试,仅供参考
准备工作
这两天正在测试FX5的Slmp与上位机通讯试验,目前暂时可以实现读写操作,但是还面临很多其它问题,想与大家探讨,共同进步:
通讯结果如下图所示:
安装VS2019 Community版本
官方网址:https://visualstudio.microsoft.com/zh-hans/vs/community/
-
安装步骤略;
-
当前Demo应用程序的目标框架为.NET Framework4.6.1;
-
生成目标平台x64,由于三菱的组件MX Componet4不支持x64,此次的测试目标以x64为主;
安装 Gx Works3 1.070Y版本
官方网址:https://mitsubishielectric.yangben.cn/assets/detail/5b553ac0e4040f68ccc99707
1.安装步骤略;
2.连接PLC;
3.设置以太网端口;
4.设置对象设备连接配置设置;
5.反映设置并关闭,点击应用,并写入PLC中,断电重启,使设置生效;
查文档手册
通过三菱提供的MELSEC iQ-FFX5用户手册(SLMP篇)查询重要技术参数,以获取指定报文通过tcp/ip协议以Socket套接字进行发送/接收。
报文格式
指令
软元件范围
方法实现
定义枚举方法
1.定义PLC操作状态(PLCState)
#region<!--Eps:PLC操作状态(PLCState)-->
/// <summary>
/// PLC操作状态(PLCState)
/// </summary>
public enum Eps
{
Open,
Close,
Read,
ReadRandom,
ReadBlock,
Write,
WriteBit,
WriteRandom,
WriteBitRandom,
WriteBlock,
}
#endregion
2.定义软元件类型(SoftType),以下参数可查手册获取:
#region<!--Est:软元件类型(SoftType)-->
/// <summary>
/// 软元件类型(SoftType)
/// </summary>
public enum Est : byte
{
/// <summary>
/// 特殊继电器
/// </summary>
SM = 0x91,
/// <summary>
/// 特殊寄存器
/// </summary>
SD = 0xA9,
/// <summary>
/// 输入继电器
/// </summary>
X = 0x9C,
/// <summary>
/// 输出继电器
/// </summary>
Y = 0x9D,
/// <summary>
/// 内部继电器
/// </summary>
M = 0x90,
/// <summary>
/// 锁存继电器
/// </summary>
L = 0x92,
/// <summary>
/// 报警器
/// </summary>
F = 0x93,
/// <summary>
/// 变址继电器
/// </summary>
V = 0x94,
/// <summary>
/// 链接继电器
/// </summary>
B = 0xA0,
/// <summary>
/// 数据寄存器
/// </summary>
D = 0xA8,
/// <summary>
/// 链接寄存器
/// </summary>
W = 0xB4,
/// <summary>
/// 定时器触点
/// </summary>
TS = 0xC1,
/// <summary>
/// 定时器线圈
/// </summary>
TC = 0xC0,
/// <summary>
/// 定时器当前值
/// </summary>
TN = 0xC2,
/// <summary>
/// 计数器触点
/// </summary>
CS = 0xC4,
/// <summary>
/// 计数器线圈
/// </summary>
CC = 0xC3,
/// <summary>
/// 计数器当前值
/// </summary>
CN = 0xC5,
/// <summary>
/// 累加定时器触点
/// </summary>
SS = 0xC7,
/// <summary>
/// 累加定时器线圈
/// </summary>
SC = 0xC6,
/// <summary>
/// 累加定时器当前值
/// </summary>
SN = 0xC8,
/// <summary>
/// 链接特殊继电器
/// </summary>
SB = 0xA1,
/// <summary>
/// 链接特殊寄存器
/// </summary>
SW = 0xB5,
/// <summary>
/// 步进继电器
/// </summary>
S = 0x98,
/// <summary>
/// 文件寄存器
/// </summary>
R = 0xAF
}
#endregion
3.定义数据类型(DataType)
#region<!--Edt:数据类型(DataType)-->
/// <summary>
/// 数据类型(DataType)
/// </summary>
public enum Edt
{
/// <summary>
/// 二进制数
/// </summary>
Bit,
/// <summary>
/// 16位单精度整型
/// </summary>
Short,
/// <summary>
/// 32位双精度整型
/// </summary>
Int,
/// <summary>
/// 32位单精度浮点
/// </summary>
Float,
/// <summary>
/// 64位双精度浮点
/// </summary>
Double
}
#endregion
定义Message方法
此方法的目的是为了自动生成报文,通过输入数据类型,数据地址,等必要参数,获取我们想要的报文信息;
namespace Chustange.Functional
{
public class Message
{
public Eps Eps { get; set; }
public Address Address { get; set; }
public List<Address> Addresses = new List<Address>();
public ushort Count { get; set; }
public Message(Eps Eps, Address Address)
{
this.Eps = Eps;
this.Address = Address;
}
public Message(Eps Eps, Address Address, ushort Count)
{
this.Eps = Eps;
this.Address = Address;
this.Count = Count;
}
public Message(Eps Eps, List<Address> Addresses)
{
this.Eps = Eps;
this.Addresses = Addresses;
}
其中Address是结构体,定义如下:
public struct Address
{
public Est SoftType;
public ushort Index;
public Edt DataType;
public object Value;
public int Decimalplace;
}
地址使用声明如下,例:定义PLC中的M101,数据类型为bit,软元件为M,索引为101,值默认为1;定义PLC中的D101,数据类型为short,软元件为D,索引为101,值默认为10,保留小数点后0位:
static Address M100 = new Address { DataType = Edt.Bit, SoftType = Est.M, Index = 101, Value = 1 };
static Address D101 = new Address { DataType = Edt.Short, SoftType = Est.D, Index = 101, Value = 10,Decimalplace=0};
定义报文类如下:
internal class Smc
{
/// <summary>
/// 副头部
/// </summary
internal readonly List<byte> DeputyHead = new List<byte>() { 0x50, 0x00 };
/// <summary>
/// 网络编号
/// </summary>
internal readonly List<byte> NetworkNumber = new List<byte>() { 0x00 };
/// <summary>
/// 可编程控制器编号
/// </summary>
internal readonly List<byte> PLCNumber = new List<byte>() { 0xFF };
/// <summary>
/// 请求目标模块IO编号
/// </summary>
internal readonly List<byte> RequestDestinationModuleIONumber = new List<byte>() { 0xFF, 0x03 };
/// <summary>
/// 请求目标模块站号
/// </summary>
internal readonly List<byte> RequestDestinationModulestationNumber = new List<byte>() { 0x00 };
/// <summary>
/// 请求数据长度
/// </summary>
internal List<byte> RequestDataLenth = new List<byte>();
/// <summary>
/// CPU监视定时器
/// </summary>
internal readonly List<byte> CPUWatchdogTimer = new List<byte>() { 0x10, 0x00 };
/// <summary>
/// 指令
/// </summary>
internal List<byte> Command = new List<byte>();
/// <summary>
/// 子指令
/// </summary>
internal List<byte> SubCommand = new List<byte>() { 0x00, 0x00 };
/// <summary>
/// 请求数据
/// </summary>
internal List<byte> Data = new List<byte>();
/// <summary>
/// 软元件
/// </summary>
internal List<byte> Type = new List<byte>();
/// <summary>
/// 索引
/// </summary>
internal List<byte> Index = new List<byte>();
/// <summary>
/// 访问点数
/// </summary>
internal List<byte> Typecount = new List<byte>();
}
根据PLC操作状态(PLCState),获取相关的指令
private void Command()
{
switch (Eps)
{
case Eps.Read:
Smc.Command = new List<byte>() { 0x01, 0x04 };
break;
case Eps.Write:
Smc.Command = new List<byte>() { 0x01, 0x14 };
break;
case Eps.WriteBit:
Smc.Command = new List<byte>() { 0x02, 0x14 };
break;
case Eps.ReadRandom:
Smc.Command = new List<byte>() { 0x03, 0x04 };
break;
case Eps.WriteRandom:
Smc.Command = new List<byte>() { 0x02, 0x14 };
break;
case Eps.ReadBlock:
Smc.Command = new List<byte>() { 0x06, 0x04 };
break;
case Eps.WriteBlock:
Smc.Command = new List<byte>() { 0x06, 0x14 };
break;
}
}
获取随机读取的报文
#region<!--GetRandomReadData:随机读取数据-->
/// <summary>
/// 获取随机读取的报文
/// </summary>
private void RandomReadData()
{
byte singlecount = 0;
byte doublecount = 0;
List<byte> _data = new List<byte>();
for (int i = 0; i < Addresses.Count; i++)
{
if (Addresses[i].DataType == Edt.Bit || Addresses[i].DataType == Edt.Short)
{
singlecount++;
}
if (Addresses[i].DataType == Edt.Float || Addresses[i].DataType == Edt.Int)
{
doublecount++;
}
///软元件
List<byte> _type = new List<byte>() { (byte)Addresses[i].SoftType };
//编号
byte[] index = BitConverter.GetBytes((short)Addresses[i].Index);
List<byte> _index = new List<byte>() { index[0], index[1], 0x00 };
_data = _data.Concat(_index).Concat(_type).ToList();
}
List<byte> _count = new List<byte>() { singlecount, doublecount };
Smc.Data = _count.Concat(_data).ToList();
}
#endregion
获取随机写入的报文
#region<!--GetRandomWriteData:随机写入数据-->
/// <summary>
/// 获取随机写入的报文
/// </summary>
private void RandomWriteData()
{
byte singlecount = 0;
byte doublecount = 0;
List<byte> _singledata = new List<byte>();
List<byte> _doubledata = new List<byte>();
for (int i = 0; i < Addresses.Count; i++)
{
if (Addresses[i].DataType == Edt.Bit || Addresses[i].DataType == Edt.Short)
{
singlecount++;
}
else if (Addresses[i].DataType == Edt.Float || Addresses[i].DataType == Edt.Int)
{
doublecount++;
}
///软元件
List<byte> _type = new List<byte>() { (byte)Addresses[i].SoftType };
byte[] index = BitConverter.GetBytes((short)Addresses[i].Index);
List<byte> _index = new List<byte>() { index[0], index[1], 0x00 };
if (Addresses[i].DataType == Edt.Bit || Addresses[i].DataType == Edt.Short)
{
_singledata = _singledata.Concat(_index).Concat(_type).Concat(BitConverter.GetBytes(Convert.ToInt16(Addresses[i].Value))).ToList();
}
else if (Addresses[i].DataType == Edt.Float)
{
_doubledata = _doubledata.Concat(_index).Concat(_type).Concat(BitConverter.GetBytes(Convert.ToSingle(Addresses[i].Value))).ToList();
}
else if (Addresses[i].DataType == Edt.Int)
{
_doubledata = _doubledata.Concat(_index).Concat(_type).Concat(BitConverter.GetBytes(Convert.ToInt32(Addresses[i].Value))).ToList();
}
}
List<byte> _count = new List<byte>() { singlecount, doublecount };
Smc.Data = _count.Concat(_singledata).Concat(_doubledata).ToList();
}
#endregion
生成最终报文
public List<byte> GetCode()
{
GetData();
Command();
short count = Convert.ToInt16(Smc.CPUWatchdogTimer.Count + Smc.Command.Count + Smc.SubCommand.Count + Smc.Data.Count);
Smc.RequestDataLenth = BitConverter.GetBytes(count).ToList();
return Smc.DeputyHead.Concat(Smc.NetworkNumber)
.Concat(Smc.PLCNumber)
.Concat(Smc.RequestDestinationModuleIONumber)
.Concat(Smc.RequestDestinationModulestationNumber)
.Concat(Smc.RequestDataLenth)
.Concat(Smc.CPUWatchdogTimer)
.Concat(Smc.Command)
.Concat(Smc.SubCommand)
.Concat(Smc.Data)
.ToList();
}
定义Socket方法
获取指定生成的报文,通过socket套接字以tcp/ip的方式发送给PLC。
namespace Chustange.Functional
{
internal class Socket
{
private SOCKET _mySocket;
/// <summary>
/// 声明端口号,并默认为8080
/// </summary>
internal int Port { get; set; } = 8080;
internal string IpAddress { get; set; } = "127.0.0.1";
internal List<byte> ReadCode { get; set; }
internal List<byte> WriteCode { get; set; }
internal bool Connected => _mySocket != null && _mySocket.Connected;
private const int ReadBufferSize = 65535;
private IPAddress Ip => IPAddress.Parse(IpAddress);
private IPEndPoint IpEnd => new IPEndPoint(Ip, Port);
internal byte[] Data { get; private set; } = new byte[ReadBufferSize];
private readonly ManualResetEvent _openDone = new ManualResetEvent(false);
private readonly ManualResetEvent _closeDone = new ManualResetEvent(false);
private readonly ManualResetEvent _sendDone = new ManualResetEvent(false);
private readonly ManualResetEvent _receiveDone = new ManualResetEvent(false);
private readonly object _openLock = new object();
private readonly object _sendLock = new object();
private readonly object _receiveLock = new object();
private readonly object _readLock = new object();
private readonly object _writeLock = new object();
打开套接字方法如下:
internal void Open()
{
try
{
lock (_openLock)
{
_mySocket = new SOCKET(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_openDone?.Reset();
_mySocket?.BeginConnect(IpEnd, OpenCallback, _mySocket);
}
}
catch (SocketException se)
{
("[Socket]->Open:" + se.StackTrace + Environment.NewLine + se.Message).ToLog();
}
}
private void OpenCallback(IAsyncResult ar)
{
try
{
var hlSocket = (SOCKET)ar.AsyncState;
hlSocket?.EndConnect(ar);
}
catch (Exception e)
{
("[Socket]->ConnectCallback:" + e.StackTrace + Environment.NewLine + e.Message).ToLog();
}
finally
{
_openDone?.Set();
}
}
读取方法
internal void Read()
{
try
{
lock (_readLock)
{
var sd = Task.Factory.StartNew(() => { Send(ReadCode); },
TaskCreationOptions.RunContinuationsAsynchronously);
var re = Task.Factory.StartNew(Receive, TaskCreationOptions.RunContinuationsAsynchronously);
Task.WhenAll(sd, re);
}
}
catch (SocketException se)
{
("[Socket]->SendAndReceive:" + se.StackTrace + Environment.NewLine + se.Message).ToLog();
}
}
写入方法
internal void Write()
{
try
{
lock (_writeLock)
{
Task.Factory.StartNew(() =>
{
Send(WriteCode);
}, TaskCreationOptions.RunContinuationsAsynchronously);
}
}
catch (SocketException se)
{
("[Socket]->SendAndReceive:" + se.StackTrace + Environment.NewLine + se.Message).ToLog();
}
}
其中判定报文发送的方法如下:
private void Send(List<byte> code)
{
try
{
lock (_sendLock)
{
if (code == null) return;
if (code.Count <= 0) return;
if (_mySocket == null) return;
if (!_mySocket.Connected)
{
Open();
}
if (!_mySocket.Connected) return;
_sendDone?.Reset();
_mySocket?.BeginSend(code.ToArray(), 0, code.Count, SocketFlags.None, SendCallback, _mySocket);
}
}
catch (SocketException se)
{
("[Socket]->Send:" + se.StackTrace + Environment.NewLine + se.Message).ToLog();
}
}
private void SendCallback(IAsyncResult ar)
{
try
{
var socket = (SOCKET)ar.AsyncState;
socket?.EndSend(ar);
}
catch (Exception e)
{
("[Socket]->SendCallback:" + e.StackTrace + Environment.NewLine + e.Message).ToLog();
}
finally
{
_sendDone?.Set();
}
}
接收报文的判定结果如下
private void Receive()
{
try
{
lock (_receiveLock)
{
if (_mySocket == null) return;
if (!_mySocket.Connected) return;
_receiveDone?.Reset();
_mySocket?.BeginReceive(Data, 0, Data.Length, SocketFlags.None, ReceiveCallback, _mySocket);
}
}
catch (SocketException ex)
{
("[Socket]->Receive:" + ex.StackTrace + Environment.NewLine + ex.Message + ex.SocketErrorCode).ToLog();
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
var socket = (SOCKET)ar.AsyncState;
socket?.EndReceive(ar);
}
catch (Exception e)
{
("[Socket]->ReceiveCallback:" + e.StackTrace + Environment.NewLine + e.Message).ToLog();
}
finally
{
_receiveDone?.Set();
}
}
定义SLMP类封装随机读/写方法
将上述的方法进行再一次封装,类比MX Component的WriteDeviceRandom和ReadDeviceRandom的方法,以下是我个人的习惯,如不需要请借鉴以上方法自行处理
namespace Chustange.Mould
{
public class SLMP
{
private readonly Socket Socket = new Socket();
public int Port { get { return Socket.Port; } set { Socket.Port = value; } }
public string IPAddress { get { return Socket.IpAddress; } set { Socket.IpAddress = value; } }
public bool Connected { get { return Socket.Connected; } }
private byte[] Socketdata { get { return Socket.Data; } }
public Eps Eps { get; set; }
public int WriteResult { get; private set; }
public int ReadResult { get; private set; }
public void Open() => Socket.Open();
public void Close() => Socket.Close();
public Priority Priority { get; private set; } = Priority.Normal;
public SLMP()
{
}
public SLMP(string IPAddress, int Port)
{
this.IPAddress = IPAddress;
this.Port = Port;
}
以下是我自行封装的WriteDeviceRandom的方法;
public void WriteDeviceRandom(List<Address> writeAddresses)
{
try
{
var message = new Message(Eps.WriteRandom, writeAddresses);
Socket.WriteCode = message.GetCode();
if (Socket.WriteCode != null)
{
Socket.Write();
}
}
catch (Exception e)
{
("[SLMP]->WriteDeviceRandom:" + e.StackTrace + Environment.NewLine + e.Message).ToLog();
}
if (Socketdata[7] == 0x02)
{
WriteResult = 0;
}
else
{
WriteResult = -1;
}
}
以下是我封装的ReadDeviceRandom的方法;
public void ReadDeviceRandom(List<Address> readAddresses, ref List<object> readDatas)
{
if (readAddresses == null) return;
if (readAddresses.Count <= 0) return;
var message = new Message(Eps.ReadRandom, readAddresses);
try
{
Socket.ReadCode = message.GetCode();
if (Socket.ReadCode != null)
{
Socket.Read();
}
}
catch (Exception e)
{
("[SLMP]->ReadDeviceRandom:" + e.StackTrace + Environment.NewLine + e.Message).ToLog();
}
var socketCount = GetAddressLength(readAddresses);
if (Socketdata[7] == socketCount + 2)
{
var count = 0;
if (readDatas != null)
{
readDatas.Clear();
}
else
{
readDatas = new List<object>();
}
for (var i = 0; i < readAddresses.Count; i++)
{
switch (readAddresses[i].DataType)
{
case Edt.Bit:
count += 2;
var result = new BitArray(new byte[] { Socketdata[8 + count + 1] })[0];
readDatas.Add(result);
break;
case Edt.Short:
count += 2;
readDatas.Add((short)(Socketdata[8 + count + 2] << 8) + Socketdata[8 + count + 1]);
break;
case Edt.Int:
count += 2;
var itLow = (short)((Socketdata[8 + count + 2] << 8) + Socketdata[8 + count + 1]);
count += 2;
var itHigh = (short)((Socketdata[8 + count + 2] << 8) + Socketdata[8 + count + 1]);
readDatas.Add((itHigh << 16) + itLow);
break;
case Edt.Float:
count += 2;
var ftLow = (short)((Socketdata[8 + count + 2] << 8) + Socketdata[8 + count + 1]);
count += 2;
var ftHigh = (short)((Socketdata[8 + count + 2] << 8) + Socketdata[8 + count + 1]);
readDatas.Add(Parse.ShortToFloat(ftLow, ftHigh));
break;
}
}
}
else
{
if (readDatas != null)
{
readDatas.Clear();
}
else
{
readDatas = new List<object>();
}
}
}
在Demo中运行
在Demo项目中引用上述生成的动态链接库Chustange.dll,当前名称可自行命名:
引用及定义如下:
namespace Demo
{
public partial class MainForm : Form
{
uc_First uc_First = new uc_First();
Us_Second us_Second;
readonly SLMP slmp = new SLMP("192.168.1.220", 1106);
// readonly SLMP slmp2 = new SLMP("192.168.1.220", 1107);
static Address M100 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 100, Value = 1 };
static Address D101 = new Address() { DataType = Edt.Short, SoftType = Est.D, Index = 101, Value = 1 };
static Address M101 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 101, Value = 1 };
static Address M102 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 102, Value = 1 };
static Address M103 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 103, Value = 1 };
static Address M104 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 104, Value = 1 };
static Address M105 = new Address() { DataType = Edt.Bit, SoftType = Est.M, Index = 105, Value = 1 };
public bool ReadLock = false;
static readonly List<Address> ReadAddresses = new List<Address>() { M101, D101 };
List<object> ReadDatas = new List<object>();
List<Address> WriteAddresses = new List<Address>() { M101, D101 };
目前测试使用的代码片段如下:
private void T1_show()
{
object lc_t1 = new object();
int i = 0;
object[] value;
while (true)
{
lock (lc_t1)
{
if (slmp.Priority == Priority.Normal)
{
if (!slmp.Connected)
{
slmp.Open();
}
if (slmp.Connected)
{
M101.Value = !Convert.ToBoolean(M101.Value);
D101.Value = ++i;
WriteAddresses = new List<Address> { M101, D101 };
slmp.WriteDeviceRandom(WriteAddresses);
Task.Delay(1).Wait();
slmp.ReadDeviceRandom(ReadAddresses, ref ReadDatas);
}
if (ReadDatas != null)
{
if (ReadDatas.Count == ReadAddresses.Count)
{
value = ReadDatas.ToArray();
txt_t1.Text(value[0].ToString());
txt_t2.Text(value[1].ToString());
Console.WriteLine($@"M101:{value[0]},D101:{value[1]}");
slmp.Running();
}
}
if (i > 1000)
{
i = 0;
}
}
}
Task.Delay(1).Wait();
}
slmp.Close();
// slmp2.Close();
}
结果如下图所示:
控制台输出结果如下:
结束语
目前测试还存在很多不足,socket中有很多BUG,自动报文生成也有些问题,持续优化中,以上思路仅供思考,完整代码就不拿出来献丑了。