协议说明
HostLink C-mode可以直接通过PC连接欧姆龙PLC,可以直接读取/写入欧姆龙PLC寄存器的协议。
其中分为1对1,以及1对N模式,1对1表示1台PC只能连接一个PLC,1对N表示1台PC可以通过协议连接多个PLC。而1:1与1:N在数据帧上也有所不同,其中1:1不需要带有PLC站号,这点比较好理解,毕竟只有一个PLC就无所谓区别是哪一个PLC了。连接图示如下所示:
命令发送和响应帧描述
数据帧一般包含:
1、起始符 '@' 1byte
2、站号 BCD格式 0-31的数 2byte
3、头部 一般为命令的类型 2byte
4、内容 命令的参数
5、FCS 校验码 对FCS之前的字节数组进行异或 2byte
6、结束码 PLC回复时带有,表示是否有异常
7、结束符 "*/r" 2byte 注意'/r'是回车符,与'/n'是不同的
Command Frame Format
Response Frame Format
C-MODE协议关于分帧的规定
当发送或接受的数据帧超过131字节时,将会对发送和结束的数据帧进行分割,会分割多个不同的帧来进行发送。最后如果不是最后一帧,每个帧的最后一个字符为'\r',用于分割不同的帧。结束的帧与正常的帧一样结尾"*\r"。因为1:1和1:N发送的帧有所不同,故在分帧上发送的字数也不尽相同。我们可以知道1:1有可能会多发一个数。
未发送完成的,即没有Terminator,只有'\r',那么回应方需要先发一个'\r'才会继续发送下一个帧。直到发送了Terminator或接收到Terminator。
PC发送数据时产生分帧时的规定,如下图所示:
PC发送数据时产生分帧时的规定,如下图所示:
C-MODE可以读写的寄存器
C-MODE读取寄存器时的具体协议
读取规则除了RG和RE之外,其他的Memory读取协议都是一样的。这两个的读取可查阅官方文档。
C-MODE寄存器写 -Monitor模式下才能正确写入
常用的寄存器除了WE之外其他都差不多的帧内容
C#实现协议
读取寄存器部分代码
目前不支持RE,RG。方法主体使用泛型来编写,返回值设计了一个泛型类型表示通讯是否成功、通讯时间、以及读取到的寄存器内容。返回的类型设计为:
public class HostLinkResult<T> where T : unmanaged
{
public bool IsSuccessful { get; set; }
public IList<T> Results { get; set; }
public long CommunicationTime { get; set; }
}
方法主体,分为发送命令和解析返回帧。返回帧为16进制的多段字,可以先将及写入Ilist<short>中,在全部写入IList<T>中。
public HostLinkResult<T> Read<T>(int begin, int length, RegisterType readType) where T : unmanaged
{
Stopwatch sw = new Stopwatch();
sw.Restart();
HostLinkResult<T> result = new HostLinkResult<T>();
var size = Marshal.SizeOf(default(T));
string commandStr = "";
commandStr += _firstChar;
if (_unitNumber >= 0)
{
commandStr += _unitNumber.ToString("X2");
}
commandStr += $"{GetReadHeaderCode(readType)}";
commandStr += $"{begin:0000}";
var registerNumber = (size / 16 * length);
if (registerNumber < 1) registerNumber = 1; //寄存器数量必须大于1
commandStr += $"{registerNumber:0000}";
Tuple<bool, List<short>> r = SendReadCommand(commandStr, readType);
result.IsSuccessful = r.Item1;
result.CommunicationTime = sw.ElapsedMilliseconds;
if (!r.Item1) return result;
byte[] bytes = BytesHelper.StructsToBytes(r.Item2);
result.Results = BytesHelper.BytesToStructs<T>(bytes);
result.CommunicationTime = sw.ElapsedMilliseconds;
return result;
}
读取寄存器帧的命令,发送字符串的生成方法SendReadCommand如下:
/// <summary>
/// 处理读取命令
/// </summary>
/// <param name="commandStr"></param>
/// <returns></returns>
private Tuple<bool, List<short>> SendReadCommand(string commandStr, RegisterType readType)
{
commandStr += $"{GetChecksum(commandStr)}";
commandStr += "*\r";
string resultStr = "";
List<short> result = new List<short>();
#if DEBUG
WriteToLog($"PC=>PLC\t:{commandStr}");
#endif
_serialPort.Write(commandStr);
try
{
string curFrame = "";
while (true)
{
curFrame = _serialPort.ReadTo("\r"); //接收到一帧 读后就没有\r了
#if DEBUG
WriteToLog($"PLC=>PC\t:{curFrame}");
#endif
var checksum = GetChecksum(curFrame.Substring(0, curFrame.Length - 3));
var fcs = curFrame.Substring(curFrame.Length - 3, 2);
if (checksum != fcs)//检测校验码
{
_lastErrorMessage = "The received data FCS error";
#if DEBUG
WriteToLog($"Error\t:{_lastErrorMessage}");
#endif
return new Tuple<bool, List<short>>(false, result);
}
if (curFrame.IndexOf("*") == -1) //判断是否是结束符 当前不是结束符
{
#if DEBUG
WriteToLog($"Info\t:Current frame isn't end.");
#endif
resultStr += curFrame.Substring(0, curFrame.Length - 3); //去掉FCS和Terminator fcs效验过留着没用了
}
else
{
WriteToLog($"Info\t:Current frame is end.");
resultStr += curFrame.Substring(0, curFrame.Length - 3);
break; //这里退出
}
_serialPort.Write("\r"); //发送一个回车
}
string errCode = "";
if (_unitNumber >= 0)
{
errCode = resultStr.Substring(5, 2);
if (!CheckErrorCode(errCode)) return new Tuple<bool, List<short>>(false, result);
resultStr = resultStr.Remove(0, 7); //已经验证过ErrCode 可以把头部去掉
}
else
{
errCode = resultStr.Substring(3, 2);
if (!CheckErrorCode(errCode)) return new Tuple<bool, List<short>>(false, result);
resultStr = resultStr.Remove(0, 5); //已经验证过ErrCode 可以把都去掉
}
#if DEBUG
WriteToLog($"resultStr is\t:{resultStr}");
#endif
if (readType == RegisterType.TC_Status)
{
foreach (var c in resultStr)
{
result.Add(c == '0' ? (short)0 : (short)1);
}
}
else
{
for (int i = 0; i <= resultStr.Length - 4; i += 4)
{
result.Add(short.Parse(resultStr.Substring(i, 4), System.Globalization.NumberStyles.HexNumber));
}
}
return new Tuple<bool, List<short>>(true, result);
}
catch (Exception exp) //一般是超时的异常
{
_lastErrorMessage = exp.StackTrace;
#if DEBUG
WriteToLog($"Error\t:{_lastErrorMessage}");
#endif
return new Tuple<bool, List<short>>(false, result);
}
}
C-MODE写入寄存器的具体代码
写入代码返回比较简单,主要是验证是否正确的写入。具体代码如下
public bool Write<T>(int begin, IList<T> datas, RegisterType writeType) where T : unmanaged
{
Stopwatch sw = new Stopwatch();
sw.Restart();
string commandStr = "";
commandStr += _firstChar;
if (_unitNumber >= 0)
{
commandStr += _unitNumber.ToString("X2");
}
commandStr += $"{GetWriteHeaderCode(writeType)}";
commandStr += $"{begin:0000}";
var bytes = BytesHelper.StructsToBytes(datas);
var words = BytesHelper.BytesToStructs<short>(bytes);
foreach (var word in words)
{
commandStr += $"{word:X4}";
}
if (!SendWriteCommand(commandStr, writeType)) return false;
else return true;
}
读取寄存器帧的命令,发送字符串的生成方法SendWriteCommand如下:
private bool SendWriteCommand(string commandStr, RegisterType writeType)
{
commandStr += GetChecksum(commandStr); //添加验证码
string reply = ""; //欧姆龙回复的字符串
string replyCheckSum = "";
string errorCode = "";
if (commandStr.Length <= 129)
{
commandStr += "*\r"; //所有要发送的数据
_serialPort.Write(commandStr);
#if DEBUG
WriteToLog($"PC=>PLC\t:{commandStr}");
#endif
reply = _serialPort.ReadTo("\r");
#if DEBUG
WriteToLog($"PLC=>PC\t:{reply}");
#endif
replyCheckSum = reply.Substring(reply.Length - 3, 2);
if (replyCheckSum != GetChecksum(reply.Substring(0, reply.Length - 3)))
{
_lastErrorMessage = "The received data FCS error";
#if DEBUG
WriteToLog($"Error\t:{_lastErrorMessage}");
#endif
return false;
}
errorCode = "";
if (_unitNumber >= 0)
errorCode = reply.Substring(5, 2);
else errorCode = reply.Substring(3, 2);
#if DEBUG
WriteToLog($"Error code\t:{errorCode}\r");
#endif
if (!CheckErrorCode(errorCode)) return false;
return true;
}
else
{
string firstFrame = "";
string residueStr = "";
if (_unitNumber > 0)
{
firstFrame = commandStr.Substring(0, 127); //写入头部和前30个字
residueStr = commandStr.Remove(0, 127);
}
else
{
firstFrame = commandStr.Substring(0, 125); //写入头部和前30个字
residueStr = commandStr.Remove(0, 125);
}
firstFrame += GetChecksum(firstFrame + "\r");
_serialPort.Write(firstFrame);
#if DEBUG
WriteToLog($"PC=>PLC\t:{firstFrame}\r");
#endif
while (true)
{
reply = _serialPort.ReadTo("\r");
if (reply.IndexOf("*") != -1) //已经是结束帧 其他的帧只是回车符
{
replyCheckSum = reply.Substring(reply.Length - 3, 2);
if (replyCheckSum != GetChecksum(reply.Substring(0, reply.Length - 3)))
{
_lastErrorMessage = "The received data FCS error";
#if DEBUG
WriteToLog($"Error\t:{_lastErrorMessage}\r");
#endif
return false;
}
errorCode = "";
if (_unitNumber >= 0)
errorCode = reply.Substring(5, 2);
else errorCode = reply.Substring(3, 2);
#if DEBUG
WriteToLog($"Error code\t:{errorCode}\r");
#endif
if (!CheckErrorCode(errorCode)) return false;
return true;
}
else //不是结束帧 只回复回车。 没有FCS
{
string sendCurFrame = "";//需要发送的当前帧
if (residueStr.Length / 4 <= 31) //需要发送的最后一帧 31 * 4 = 124
{
sendCurFrame = residueStr; //全部发送
sendCurFrame += GetChecksum(sendCurFrame);
_serialPort.Write(sendCurFrame + "*\r");
WriteToLog($"PC=>PLC\t:{sendCurFrame}");
}
else
{
sendCurFrame = residueStr.Substring(0, 124); //发送31字
residueStr.Remove(0, 124);
sendCurFrame += GetChecksum(sendCurFrame);
_serialPort.Write(sendCurFrame + "\r");
WriteToLog($"PC=>PLC\t:{sendCurFrame}");
}
}
}
}
}
NOTE
由于本人没有欧姆龙PLC做验证,只是在朋友的帮助下验证了几个命令。所以C#代码仅供参考。