欧姆龙PLC HostLink通讯 C-MODE格式

协议说明

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了。连接图示如下所示:

欧姆龙PLC HostLink通讯 C-MODE格式

命令发送和响应帧描述

        数据帧一般包含:

        1、起始符 '@'    1byte        

        2、站号  BCD格式 0-31的数  2byte

        3、头部 一般为命令的类型 2byte

        4、内容   命令的参数

        5、FCS  校验码  对FCS之前的字节数组进行异或  2byte

        6、结束码  PLC回复时带有,表示是否有异常 

        7、结束符  "*/r" 2byte  注意'/r'是回车符,与'/n'是不同的

 Command Frame Format

欧姆龙PLC HostLink通讯 C-MODE格式

Response Frame Format

欧姆龙PLC HostLink通讯 C-MODE格式

 C-MODE协议关于分帧的规定

        当发送或接受的数据帧超过131字节时,将会对发送和结束的数据帧进行分割,会分割多个不同的帧来进行发送。最后如果不是最后一帧,每个帧的最后一个字符为'\r',用于分割不同的帧。结束的帧与正常的帧一样结尾"*\r"。因为1:1和1:N发送的帧有所不同,故在分帧上发送的字数也不尽相同。我们可以知道1:1有可能会多发一个数。

        未发送完成的,即没有Terminator,只有'\r',那么回应方需要先发一个'\r'才会继续发送下一个帧。直到发送了Terminator或接收到Terminator。

       PC发送数据时产生分帧时的规定,如下图所示:欧姆龙PLC HostLink通讯 C-MODE格式

         PC发送数据时产生分帧时的规定,如下图所示:

欧姆龙PLC HostLink通讯 C-MODE格式

 C-MODE可以读写的寄存器

欧姆龙PLC HostLink通讯 C-MODE格式

C-MODE读取寄存器时的具体协议

        读取规则除了RG和RE之外,其他的Memory读取协议都是一样的。这两个的读取可查阅官方文档。

欧姆龙PLC HostLink通讯 C-MODE格式 C-MODE寄存器写  -Monitor模式下才能正确写入

        常用的寄存器除了WE之外其他都差不多的帧内容

欧姆龙PLC HostLink通讯 C-MODE格式

 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#代码仅供参考。

上一篇:《OPC UA实践》 - 阅读笔记 3 OPC UA与产业升级


下一篇:Maven工具的使用