找回密码
 立即注册
首页 业界区 业界 C#实现三菱MC通讯协议库(4C帧-格式1)

C#实现三菱MC通讯协议库(4C帧-格式1)

简千叶 8 小时前
C#实现三菱MC通讯协议库(4C帧-格式1)

运行环境:VS2022 .net Standard2.0
通讯库项目地址(Gitee):通讯库项目Gitee 仓库
Melsec通讯手册链接(蓝奏云):三菱Q系列与L系列MELSEC通讯协议手册
C24模块用户手册链接(蓝奏云):三菱Q系列串行通信模块用户手册(基础篇)
QnA兼容4C帧格式1报文分析:QnA兼容4C帧格式1报文分析
通讯工具(蓝奏云):Commix 1.4
概要:根据三菱的 Melsec 通讯协议(本文称MC协议)手册内容,使用串口实现了 PC 与 PLC 的通讯,能够通过QnA兼容4C帧的格式1实现 PC 读写 PLC 的软元件存储器内容(异步方法),最后用一个 C#控制台项目测试了通讯库功能
背景介绍

MC协议是三菱 PLC 与主机通讯的一种公开协议,PC 可通过三菱C24或者E71模块读取 PLC 的运行状态和I/O点位
以下是MC协议的两种模块和适用的通信帧和通信格式代码表格
对象模块可使用的通信帧通信数据代码
C24QnA兼容3C帧格式1~4ASCII代码
QnA兼容4C帧格式5二进制代码
QnA兼容2C帧格式1~4ASCII代码
A兼容1C帧ASCII代码
E714E帧ASCII代码或二进制代码
QnA兼容3E帧ASCII代码或二进制代码
A兼容1E帧ASCII代码或二进制代码
通过MC协议进行的数据通信是以半双工通信进行,在对PLC发送指令报文后会接收到来自PLC的响应报文,接收完全后才能再次发送下一个指令报文
在没接收完全响应报文就发送下一个指令报文会发生错误!
示意图如下所示

本文主要介绍QnA兼容4C帧格式1,需要使用RS232线连接PC主机与PLC,连接示意图与RS232线序图如下所示


QnA兼容4C帧(格式1)报文分析

QnA兼容4C帧的格式1通过ASCII代码进行通信,通信报文如下表
以QnA兼容4C帧(格式1)读写M0~M4、D0~D1的两个例子,通过表格说明
报文表格文件:QnA兼容4C帧格式1报文分析
读写M0~M4报文例子


  • 读取M0~M4


  • 写入M0~M4

读写D0~D1报文例子


  • 读取D0~D1


  • 写入D0~D1

QnA兼容4C帧的通用数据内容说明

此部分在官方的协议手册有详细说明,相关内容通过下列图片表示
控制代码


数据字节数(格式5用)


帧识别编号


站号


网络编号与可编程控制器编号


请求目标模块I/O编号


请求目标模块站号



本站编号


和校验代码

在报文中参与和校验的部分在各个格式中不同,需要自行查阅协议手册

出错代码

C24模块与E71模块的错误代码可能不相同,需要自行查阅协议手册
C24模块用户手册:三菱Q系列串行通信模块用户手册(基础篇)

软元件的批量读写指令

指令的部分内容说明


位单元的读写指令



字单元的读写指令



MC协议的功能很强大,本文的内容只是分享了其中的一小部分,如果大家有需要的话,可以通过它的通讯手册更深入的了解MC协议
MC协议手册:三菱Q系列与L系列MELSEC通讯协议手册
MC通讯库的C#实现

和校验实现

根据上文提供的和校验代码规则制作了一个和校验的方法,以下代码可以用于手动调试校验代码的内容
  1. Console.WriteLine("Start Test!!");//测试用,Frame1的和校验代码应为"0x31,0x43";或者十进制的"49,67"List Frame1 = new List { 0x46, 0x39, 0x30, 0x30, 0x30, 0x30, 0x46, 0x46, 0x30, 0x30, 0x30, 0x34, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x58, 0x2A, 0x30, 0x30, 0x30, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x35 };List Frame2 = new List { 0x46, 0x38, 0x30, 0x30, 0x30, 0x30, 0x46, 0x46, 0x30, 0x33, 0x46, 0x46, 0x30, 0x30, 0x30, 0x30, 0x30, 0x34, 0x30, 0x31, 0x30, 0x30, 0x30, 0x31, 0x4d, 0x2a, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x35 };List result = CheckSum(Frame2);System.Console.WriteLine($"{result[0]},{result[1]}");System.Console.WriteLine("Over!!");public static List CheckSum(List frame){    try    {        List checkResult = new List();        //取和        int sum = 0;        foreach (byte b in frame)        {            sum += b;        }        //截取最后后两位        byte lowByte = (byte)(sum & 0xFF);        //转为十六进制字符串        string hexString = lowByte.ToString("X2");        if (hexString.Length >= 2)        {            char num1 = hexString[hexString.Length - 2];            char num2 = hexString[hexString.Length - 1];            //按照高位在前顺序添加            checkResult.Add((byte)num1);            checkResult.Add((byte)num2);        }        else        {            checkResult.Add(0x30);            checkResult.Add((byte)hexString[0]);        }        return checkResult;    }    catch (Exception)    {        throw;    }}
复制代码
串口通讯实现

通讯库使用了Serial Port进行串口通讯,通过使用SemaphoreSlim(信号量限制)、DataReceived(串口接收数据方法)和TaskCompletionSource(异步任务传输串口数据)等内容实现串口通讯
以下是部分代码
  1. //构造函数public Melsec4CClient(string portname, int baudrate, System.IO.Ports.Parity parity, int databits, System.IO.Ports.StopBits stopbits)        {            this.PortName = portname;            this.BaudRate = baudrate;            this.Parity = parity;            this.DataBits = databits;            this.StopBits = stopbits;        }
复制代码
  1. //串口接收数据方法private void Melsec_4C_ReadIO_DataReceived(object sender, SerialDataReceivedEventArgs e)        {            try            {                //获取并添加到缓冲区                int bytesToRead = serialPort.BytesToRead;                byte[] buffer = new byte[bytesToRead];                serialPort.Read(buffer, 0, bytesToRead);                receiveBuffer.AddRange(buffer);                //处理数据                //查找完整的报文                while (true)                {                    if (format == Melsec_4C_FormatEnum.Format1)                    {                        int startIndex = receiveBuffer.IndexOf((byte)0x02);//STX                        int errIndex = receiveBuffer.IndexOf((byte)0x15);//NAK                        if (startIndex != -1 && errIndex == -1)//只找到STX,正常结束                        {                            if (receiveBuffer.Count > 17 + startIndex)//接收保文到Data部分                            {                                int seqStartIndex = FindSeq(receiveBuffer, new byte[] { 0x02, 0x46, 0x38 });                                if (seqStartIndex == -1)                                {                                    //继续接收数据                                    break;                                }                                if (seqStartIndex != startIndex)                                {                                    startIndex = seqStartIndex;                                }                                int endIndex = receiveBuffer.IndexOf((byte)0x03, startIndex + 17);//ETX                                if (endIndex == -1)                                {                                    //继续接收数据                                    break;                                }                                //找到和校验位置                                if (isCheckSum)//有和校验                                {                                    if (receiveBuffer.Count >= (endIndex + 3))                                    {                                        int frameEnd = endIndex + 3;                                        //提取完整报文(从STX到和校验代码)                                        List completeFrame = receiveBuffer.GetRange(startIndex, frameEnd - startIndex);                                        //读取到的和校验数值                                        List receivedCheckSum = new List() { completeFrame[completeFrame.Count - 2], completeFrame[completeFrame.Count - 1] };                                        //用于计算和校验的数据                                        List dataForCheckSum = new List();                                        for (int i = 1; i < completeFrame.Count - 2; i++)//待测试                                        {                                            dataForCheckSum.Add(completeFrame[i]);                                        }                                        List calculatedCheckSum = Melsec_4C_Check.CheckSum(format, dataForCheckSum);                                        if (!calculatedCheckSum.SequenceEqual(receivedCheckSum))                                        {                                            throw new InvalidOperationException("读取的和校验数值与计算的不符");                                        }                                        //完成结果                                        receiveTcs.TrySetResult(completeFrame);                                        //清除缓冲区中已处理的报文                                        receiveBuffer.RemoveRange(0, frameEnd);                                    }                                    else                                    {                                        //继续接收数据                                        break;                                    }                                }                                else//无和校验                                {                                    List completeFrame = receiveBuffer.GetRange(startIndex, endIndex - startIndex);//待测试,是否要+1?                                    //完成结果                                    receiveTcs.TrySetResult(completeFrame);                                    //清除缓冲区中已处理的报文                                    receiveBuffer.RemoveRange(0, endIndex);                                }                            }                            else                            {                                //继续接收数据                                break;                            }                        }                        else if (startIndex == -1 && errIndex != -1)//只找到NAK,异常结束                        {                            //到达固定字数                            if (receiveBuffer.Count >= 21 + errIndex)                            {                                int seqErrIndex = FindSeq(receiveBuffer, new byte[] { 0x15, 0x46, 0x38 });                                if (seqErrIndex == -1)                                {                                    //继续接收数据                                    break;                                }                                if (seqErrIndex != errIndex)                                {                                    errIndex = seqErrIndex;                                }                                int frameEnd = errIndex + 21;                                List completeFrame = receiveBuffer.GetRange(errIndex, frameEnd - errIndex);//待测试,是否要+1?                                                                                                                 //完成结果                                receiveTcs.TrySetResult(completeFrame);                                //清除缓冲区中已处理的报文                                receiveBuffer.RemoveRange(0, frameEnd);                            }                            else                            {                                //继续接收数据                                break;                            }                        }                        else //两个开头都没找到                        {                            //继续接收数据                            break;                        }                    }                    else                    {                        //format数值错误,抛出异常                        throw new ArgumentOutOfRangeException("format error!");                    }                }            }            catch (Exception ex)            {                if (receiveTcs.Task.IsCompleted == false)                {                    receiveTcs.TrySetException(ex);                }            }        }
复制代码
  1. //读取位单位的异步方法public async Task ReadIOBitAsync(Melsec_4C_IOAreaEnum IOArea, uint IOAdr, uint ReadCount)        {            try            {                Melsec_4C_FrameConfig config = new Melsec_4C_FrameConfig();                if (format == Melsec_4C_FormatEnum.Format1)                {                    config.IDCode = Melsec_4C_ControlCode.IDCode_ASCII_4C;//F8                    config.SNCode = new List { 0x30, 0x30 };//00                    config.NetCode = new List { 0x30, 0x30 };//00                    config.CPUCode = new List { 0x46, 0x46 };//FF                    config.TargetModuleIOCode = new List { 0x30, 0x33, 0x46, 0x46 };//03FF                    config.TargetModuleSNCode = new List { 0x30, 0x30 };//00                    config.ThisSNCode = new List { 0x30, 0x30 };//00                    config.Command = new List { 0x30, 0x34, 0x30, 0x31 };//0401                    config.SonCommand = new List { 0x30, 0x30, 0x30, 0x31 };//0001                    List datas = new List();                    //选择IO区域代码                    switch (IOArea)                    {                        case Melsec_4C_IOAreaEnum.IO_X:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_X);                            break;                        case Melsec_4C_IOAreaEnum.IO_Y:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_Y);                            break;                        case Melsec_4C_IOAreaEnum.IO_M:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_M);                            break;                        case Melsec_4C_IOAreaEnum.IO_L:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_L);                            break;                        case Melsec_4C_IOAreaEnum.IO_F:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_F);                            break;                        case Melsec_4C_IOAreaEnum.IO_V:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_V);                            break;                        case Melsec_4C_IOAreaEnum.IO_B:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_B);                            break;                        case Melsec_4C_IOAreaEnum.IO_TC:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_T);                            break;                        case Melsec_4C_IOAreaEnum.IO_CC:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_C);                            break;                        case Melsec_4C_IOAreaEnum.IO_S:                            datas.AddRange(Melsec_4C_IOAreaCode.IOArea_ASCII.IOArea_S);                            break;                        default:                            throw new ArgumentOutOfRangeException("IO区域选择出错");                    }                    datas.AddRange(MelsecConverter.Uint_D6String_ByteList(IOAdr));                    datas.AddRange(MelsecConverter.Uint_D4String_ByteList(ReadCount));                    config.Datas = datas;                    var result = await ReadIOAreaAsync(config);                    //result解析                    if (result.IsSuccessed)                    {                        List listResult = MelsecConverter.ByteList_ASCII_BoolList(result.Datas);                        return listResult;                    }                    else                    {                        throw new Exception(result.ExMessage);                    }                }                else                {                    throw new ArgumentOutOfRangeException("format选择出错");                }            }            catch (Exception)            {                throw;            }        }
复制代码
详细代码可参考:通讯库项目Gitee 仓库
控制台试验

使用控制台进行通讯库试验,以下是试验使用的代码和试验结果图
  1. using Mitsubishi.MelsecLib;using Mitsubishi.MelsecLib.Melsec4CBase;using System.IO.Ports;namespace MelsecTest{    internal class Program    {        private static Melsec4CClient? plc;        private static Melsec_4C_FormatEnum format;        static async Task Main(string[] args)        {            plc = new Melsec4CClient("COM6",9600, Parity.Even,7,StopBits.Two);            var isConnect = await plc.ConnectAsync(Melsec_4C_FormatEnum.Format1,true,false);            if (isConnect)            {                Console.WriteLine("读取X0~X6");                var result1 = await plc.ReadIOBitAsync(Melsec_4C_IOAreaEnum.IO_X, 0, 6);                foreach (var b in result1)                {                    Console.WriteLine(b.ToString());                }                await Task.Delay(1000);                Console.WriteLine("读取M300~M306");                var result2 = await plc.ReadIOBitAsync(Melsec_4C_IOAreaEnum.IO_M, 300, 6);                foreach (var b in result2)                {                    Console.WriteLine(b.ToString());                }                await Task.Delay(1000);                Console.WriteLine("读取D3000~D3006");                var result3 = await plc.ReadIOWordAsync(Melsec_4C_IOAreaEnum.IO_D, 3000, 6);                foreach (var b in result3)                {                    Console.WriteLine(b.ToString());                }                await Task.Delay(1000);                Console.WriteLine("写入M300~M306");                var result4 = await plc.WriteIOBitAsync(Melsec_4C_IOAreaEnum.IO_M, 300, new List { true, false, true, true, false, true });                if (result4)                {                    Console.WriteLine("写入M300~M306:OK");                }                else                {                    Console.WriteLine("写入M300~M306:NG");                }                await Task.Delay(1000);                Console.WriteLine("读取M300~M306");                var result5 = await plc.ReadIOBitAsync(Melsec_4C_IOAreaEnum.IO_M, 300, 6);                foreach (var b in result5)                {                    Console.WriteLine(b.ToString());                }                await Task.Delay(1000);                Console.WriteLine("写入D3000~D3006");                var result6 = await plc.WriteIOWordAsync(Melsec_4C_IOAreaEnum.IO_D, 3000, new List { 11,22,33,44,55,66 });                if (result6)                {                    Console.WriteLine("写入D3000~D3006:OK");                }                else                {                    Console.WriteLine("写入D3000~D3006:NG");                }                await Task.Delay(1000);                Console.WriteLine("读取D3000~D3006");                var result7 = await plc.ReadIOWordAsync(Melsec_4C_IOAreaEnum.IO_D, 3000, 6);                foreach (var b in result7)                {                    Console.WriteLine(b.ToString());                }                await Task.Delay(1000);                Console.WriteLine("恢复M300~M306");                await plc.WriteIOBitAsync(Melsec_4C_IOAreaEnum.IO_M, 300, new List { false, false, false, false, false, false });                await Task.Delay(1000);                Console.WriteLine("恢复D3000~D3006");                await plc.WriteIOWordAsync(Melsec_4C_IOAreaEnum.IO_D, 3000, new List { 0, 0, 0, 0, 0, 0 });                await Task.Delay(1000);            }            else            {                Console.WriteLine("连接错误");            }            await plc.DisconnectAsync();            Console.ReadKey();        }    }}
复制代码
详细代码可参考:通讯库项目Gitee 仓库
试验结果图如下图

后续


项目还有很多值得改进的地方,例如使用ConcurrentQueue多线程队列来实现串口通讯的队列;开发4C帧的其他格式和E71模块的通讯代码等,这些后续进行改进了都会在仓库上进行更新。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册