Bootstrap

C#上位机和松下PLC通讯

C#上位机和松下PLC通讯

1、前言
也好久没回来看博客了,看大家对于其他的PLC的需求都是有的,说明搞我们工业控制这一行的人也是很多的。刚好最近基于项目需要,一些新的东西,需要和松下PLC的FP系列的CPU进行通讯然后使用松下的NewTocol协议进行通讯,对对应的寄存器/线圈进行读写操作。于是,就随笔记录下相应的程序代码。
松下FP系列PLC与工控机之间的通信方式可以采用串口通信,与工控机连接的RS232电缆(长度有限485的长度会比较长很多)必须按照松下的产品手册所给的连线图进行制作,否则通信将无法实现。电缆连线图如下:
串口接线图
至于怎么接线,需要的可以去了解下,不过一般在项目过程中,PLC的编程人员会把线接好,直接接入到工控机就可以使用了。
既然是串口通讯,我们就需要了解下串口通讯的一些概念。我们往下走。
2、概念
2.1 波特率
在电子通信领域,波特(Baud)即调制速率,指的是有效数据讯号调制载波的速率,即单位时间内载波调制状态变化的次数。通俗一点,就是一个单位时间内数据传递的符号的个数
2.2 奇偶校验位
奇偶校验位 (Parity)通常用在数据通信中来保证数据的有效性。分为偶校验、奇校验、或非校验。如果偶校验在使用,校验位将这些位置为偶数;如果奇校验在使用,校验位将这些位置为奇数。
2.3 数据位
数据位一般为8位一个字节的数据(也有6位、7位的情况),低位(LSB)在前,高位(MSB)在后。
2.4 停止位
用于表示单个包的最后一位。典型的值为1,1.5和2位。
2.5 SerialPort类
需要引入System.IO.Port命名空间。
在这里插入图片描述
3、NewTocol协议
至于协议文档,大家可以去官方或者百度查询了解,该协议由以下特点:

  1. 数据传输采用ASCII的形式。
  2. 应答式协议,首先由工控机发送指令,然后PLC会自动对指令进行响应。也就是说,不需要编写任何PLC程序,只要PLC和工控机连接正常,工控机给PLC发送指令,都能得到PLC的响应回复。

4、上位机发送数据帧格式
指令是以帧为单位进行,工控机向PLC发送命令帧,然后PLC作出响应,向工控机发送响应帧。
格式内容如下: 在这里插入图片描述
其中:

  1. %为起始码,这是固定不变的。
  2. AD(H)和 AD(L)是目标站号的高位和低位。一般如果只有一个PLC的话,那么就填写01,高位是0,低位是1。
  3. #也是固定不变的。
  4. 指令代码。每个指令会有不同的指令代码,下面会讲。
    文本代码。指令的内容,不同的指令,内容也不同。
  5. BCC(H)和BCC(L),是帧的数据校验的高低位,数据校验范围是BCC前面的所有字符;下面我们会展开介绍说明。
  6. CR,回车键,ASCII为0x0D,不可见字符。

5、PLC响应帧格式
响应帧有两种,一种是正确响应,一种是错误响应。也就是说,如果工控机给PLC发送的指令是正确的,那么PLC就会返回正确的响应帧,否则就返回错误的响应帧。 在这里插入图片描述
6、工作站的配置
以下是电气工程师提供的PLC配置图为例,我们可以看到PLC内部的设备预设站号是1,那么对于上位机而言目标站号就是1,此配置可以找电气工程师告知即可。在这里插入图片描述
7、指令代码
工控机可以给PLC发送的指令一共有20多种,不过我们常用的指令一般有9种。如下图所示:
在这里插入图片描述
其中常用的:
在这里插入图片描述

8、BCC的计算方式
BCC校验码的计算方式是将指令中的各个ASCII字符的16进制(00~FF)进行异或求和后生成的. 该校验码也以两个ASCII码字符表示(高位在前,低位在后)。
例如这条指令:
%01#RCSX00001DCR
注意:CR不是两个字符,是一个字符,回车键,但是是不可显示字符,所以这里用CR来表示。
计算方式:在这里插入图片描述
在此贴上BCC校验的代码:

/// <summary>
        /// BCC校验码
        /// 计算方式是将指令中的各个ASCII字符的16进制(00~FF)进行异或求和后生成的. 
        /// 该校验码也以两个ASCII码字符表示(高位在前,低位在后)
        /// </summary>
        /// <param name="cmd"></param>
        /// <returns></returns>
        public static string Bcc(string cmd)
        {
            cmd = cmd.Trim();
            byte bcc = 0;
            byte[] cmdArr = System.Text.Encoding.ASCII.GetBytes(cmd);
            for (int i = 0; i < cmdArr.Length; i++)
            {
                bcc = (byte)(bcc ^ cmdArr[i]);
            }
            return bcc.ToString("X2");
        }

9、常用指令举例
例1:写入单触点状态(指令代码:WCS) 在这里插入图片描述
例如我们往触点R12写入1,则命令帧为:
%01#WCSR0012120CR
拆开成各个部分:% 01 # WCS R 0012 1 20 CR
正常通信情况下,PLC会返回正确的响应帧:%01$WC14CR

例2:读取单触点状态(指令代码:RCS)

在这里插入图片描述
例如我们读取触点R12的值,则命令帧为:
%01#RCSR001214CR
拆分成各个部分:% 01 # RCS R 0012 14 CR
正常通信情况下,假如R12触点的值为1,那么PLC返回的响应帧为
%01$RC120**CR
例3:写入数据寄存器值(指令代码:WD)
在这里插入图片描述
例如我们写入字数值到PLC的 DT1到DT3,其中:DT1=05H,DT2=1507H,DT3=900H,则命令帧为:
这里需要注意的是:
写入的数值是按字写入,也就是说,每个值占2个字节。
低位在前,高位在后。
写入的字符是16进制的。
所以,需要代码进行处理。例如此处的DT2=1507H,1507H的10进制值是5383,则处理步骤为:
先将5383转换为16进制的字符1507
将字符转换顺序,低位在前,高位在后,也就是转换成:0715
我们写一个函数来进行处理:

private string ConvertShortToPlcFormat(short value)
        {
            string temp = value.ToString("X4");
            return temp.Substring(2, 2) + temp.Substring(0, 2);
        }

正常通信情况下,PLC的响应帧为: 在这里插入图片描述
10、代码实现
1、先定义松下通讯配置用的数据模型
以下是配置文件内容和模型代码:
在这里插入图片描述

 public class PanasonicParaModel
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string PlcName { get; set; }
        /// <summary>
        /// 站号
        /// </summary>
        public int PlcStationNo { get; set; }
        /// <summary>
        /// COM口号 比如COM1
        /// </summary>
        public string PlcComNo { get; set; }
        /// <summary>
        /// 波特率
        /// </summary>
        public int PlcBoardRate { get; set; }
        /// <summary>
        /// 奇偶校验1奇校验 2偶校验 0不校验 
        /// </summary>
        public Parity PlcParity { get; set; }
        /// <summary>
        /// 数据位 正常是8
        /// </summary>
        public int PlcDataLen { get; set; }
        /// <summary>
        /// 停止位1 2 1.5 默认是1
        /// </summary>
        public StopBits PlcStopBit { get; set; }
        /// <summary>
        /// 连接标志位
        /// </summary>
        public bool LinkRes { get; set; }
    }

2、定义一个串口帮助方法类

 public class SerialPortHelper
    {
        #region 单例实现
        private static SerialPortHelper instance = null;
        private Dictionary<int, SerialPort> _dicSerialPort = null;  //保存串口对象的集合

        private SerialPortHelper()
        {
            if (this._dicSerialPort == null)
            {
                this._dicSerialPort = new Dictionary<int, SerialPort>();
            }
        }

        public static SerialPortHelper Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new SerialPortHelper();
                }
                return instance;
            }
        }
        #endregion

        #region 获得奇偶校验
        /// <summary>
        /// 获得奇偶校验
        /// </summary>
        /// <param name="num"></param>
        /// <returns></returns>
        public static Parity GetParityByConfigNum(int num)
        {
            switch(num)
            {
                case 1:
                    return Parity.Odd;
                case 2:
                    return Parity.Even;
                case 0:
                default:
                    return Parity.None;
            }
        }
        #endregion
        #region 获得停止位
        /// <summary>
        /// 获得停止位
        /// </summary>
        /// <param name="num"></param>
        /// <returns></returns>
        public static StopBits GetStopBitByConfigNum(double num)
        {
            switch (num)
            {
                case 1:
                    return StopBits.One;
                case 2:
                    return StopBits.Two;
                case 1.5:
                    return StopBits.OnePointFive;
                default:
                    return StopBits.One;
            }
        }
        #endregion
    }

3、定义连接对象类
话不多说了,直接上代码

		/// <summary>
    /// 松下PLC Newtocol协议
    /// </summary>
    public class Panasonic_Newtocol : IDisposable
    {

        /// <summary>
        /// 232串口对象
        /// </summary>
        public SerialPort serialPort { get; set; }
        /// <summary>
        /// 起始码%固定不变
        /// </summary>
        private const string headStr = "%";
        /// <summary>
        /// 目标站号 高位和低位
        /// </summary>
        private static string stationCode { get; set; }
        /// <summary>
        /// 分隔符#固定不变
        /// </summary>
        public const string fixCode = "#";
        /// <summary>
        /// 结束符\r 固定不变
        /// </summary>
        public const string endStr = "\r";
        /// <summary>
        /// 正确响应时的字符串内容
        /// </summary>
        private string successResponseHead = "";
        /// <summary>
        /// 失败响应时的字符串内容
        /// </summary>
        private string failResponseHead = "";
        public double[] DTValue = null;
        public bool[] arrXYMValue = null;
        /// <summary>
        /// 保存日志的委托
        /// </summary>
        /// <param name="logStr"></param>
        public delegate void SaveLogForRecord(string logStr);
        /// <summary>
        /// 记录日志事件实现
        /// </summary>
        public event SaveLogForRecord SaveLog;

        /// <summary>
        /// 写入单触点标志位
        /// </summary>
        bool writeCoilSingleResult = false;
        /// <summary>
        /// 写入寄存器数据结果
        /// </summary>
        bool writeDataResult = false;
        /// <summary>
        /// 读取寄存器数据结果
        /// </summary>
        bool readDataResult = false;
        /// <summary>
        /// 读取多个/单个触点结果
        /// </summary>
        bool readCoilMany_SingleResult = false;
        public Panasonic_Newtocol(PanasonicParaModel _serialPort)
        {
            stationCode = _serialPort.PlcStationNo.ToString("X2");
            serialPort = new SerialPort()
            {
                PortName = _serialPort.PlcComNo.ToString(),
                BaudRate = _serialPort.PlcBoardRate,
                Parity = _serialPort.PlcParity,
                DataBits = _serialPort.PlcDataLen,
                StopBits = _serialPort.PlcStopBit,
                ReceivedBytesThreshold = 8
            };
            successResponseHead = headStr + stationCode + "$";
            failResponseHead = headStr + stationCode + "!";

            SaveLog += Panasonic_Newtocol_SaveLog;
        }

        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            Thread.Sleep(50);//让缓存区数据接收完成
            int n = serialPort.BytesToRead;
            byte[] buf = new byte[n];
            serialPort.Read(buf, 0, n);

            string readText = Encoding.ASCII.GetString(buf).Replace("\r", "").Replace("\n", "");
            if (readText.StartsWith("%") && readText.Length >= serialPort.ReceivedBytesThreshold)
            {
                #region 字节流数据正确
                int len = readText.Length;
                string sHead = readText.Substring(0, 4);
                string sComm = readText.Substring(4, 2);
                string sComm1 = readText.Substring(4, 3);
                string sValues = "";
                string tValue = "";
                if (sHead == successResponseHead)
                {
                    switch (sComm)
                    {
                        case "RD":
                            {
                                //%01$RD XXXX_XXXX_XXXX_XXXX_XXXX_XXXX 文本代码 BCC高低位 结束符
                                if (!readDataResult)
                                {
                                    #region 读取D,转换为十进制
                                    sValues = readText.Substring(6, len - 8);
                                    DTValue = new double[sValues.Length / 4];
                                    for (int i = 0; i < (sValues.Length / 4); i++)
                                    {
                                        tValue = sValues.Substring(i * 4, 4);
                                        DTValue[i] = (double)Convert.ToInt32(tValue.Substring(2, 2) + tValue.Substring(0, 2), 16);
                                    }
                                    #endregion
                                    readDataResult = true;
                                }
                                break;
                            }
                        case "RC":
                            {
                                if (!readCoilMany_SingleResult)
                                {
                                    #region 读多个/单个触点
                                    sValues = readText.Substring(6, len - 8);
                                    arrXYMValue = new bool[sValues.Length];
                                    for (int i = 0; i < arrXYMValue.Length; i++)
                                    {
                                        tValue = sValues.Substring(i, 1);
                                        arrXYMValue[i] = tValue == "1";
                                    }
                                    #endregion
                                    readCoilMany_SingleResult = true;
                                }
                                break;
                            }
                        case "WD":
                            {
                                //写入D成功,没返回值
                                if (!writeDataResult)
                                {
                                    writeDataResult = true;
                                }
                                break;
                            }
                        case "WC":
                            {
                                //写入触点成功,没返回值
                                if (!writeCoilSingleResult)
                                {
                                    writeCoilSingleResult = true;
                                }
                                break;
                            }
                        default:
                            break;
                    }
                }
                #endregion
            }
        }

        #region 记录日志
        /// <summary>
        /// 记录日志
        /// </summary>
        /// <param name="logStr"></param>
        private void Panasonic_Newtocol_SaveLog(string logStr)
        {
            System.Diagnostics.Debug.WriteLine(logStr);
        }
        #endregion

        #region 打开串口连接
        /// <summary>
        /// 打开串口连接
        /// </summary>
        /// <returns></returns>
        public bool OpenLinkSerial()
        {
            try
            {
                if (serialPort != null)
                {
                    CloseLinkSerial();
                    Thread.Sleep(10);
                    serialPort.DataReceived += SerialPort_DataReceived;
                    serialPort.Open();
                    Panasonic_Newtocol_SaveLog("串口打开成功");
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                Panasonic_Newtocol_SaveLog(ex.StackTrace.ToString());
                return false;
            }
        }
        #endregion

        #region 关闭串口连接
        /// <summary>
        /// 关闭串口连接
        /// </summary>
        public void CloseLinkSerial()
        {
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.DiscardInBuffer();
                serialPort.DiscardOutBuffer();
                serialPort.Close();
            }
        }
        #endregion


        #region 写入单触点值
        /// <summary>
        /// 写入单触点值
        /// </summary>
        /// <param name="XYMAddr">例如R12 X触点 Y触点 M状态寄存器</param>
        /// <param name="value">0=off 1=on</param>
        /// <returns></returns>
        public bool WriteCoilSingle(string XYMAddr, bool value)
        {
            writeCoilSingleResult = false;
            string writeStr = WriteCoilSingle_CommStr(XYMAddr, value);
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Write(writeStr);
                int numPro = 0;
                while (!writeCoilSingleResult && numPro < 100)
                {
                    Thread.Sleep(1);
                    numPro++;
                }
                if (numPro < 100)
                {
                    return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
        #endregion

        #region 写入单触点值发送的字符串内容 WCS
        /// <summary>
        /// 写入单触点值发送的字符串内容 WCS X Y M
        /// </summary>
        /// <param name="XYMAddr">例如R12 X触点 Y触点 M状态寄存器</param>
        /// <param name="value">0=off 1=on</param>
        public string WriteCoilSingle_CommStr(string XYMAddr, bool value)
        {
            string commmandCode = "WCS";
            //发送
            string outStr = "";
            string sReg = XYMAddr.Substring(0, 1);
            string sAddr = XYMAddr.Substring(1).PadLeft(4, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr + string.Format(value ? "1" : "0");
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 读单触点状态
        /// <summary>
        /// 读单触点状态
        /// </summary>
        /// <param name="XYMAddr">X Y M地址 比如M1</param>
        /// <param name="value">读取的结果</param>
        /// <returns>TRUE表示读取成功 false表示读取失败</returns>
        public bool ReadCoilSingle(string XYMAddr, out bool value)
        {
            readCoilMany_SingleResult = false;
            string writeStr = ReadCoilSingle_CommStr(XYMAddr);
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Write(writeStr);
                int numPro = 0;
                while (!readCoilMany_SingleResult && numPro < 100)
                {
                    Thread.Sleep(1);
                    numPro++;
                }
                if (numPro < 100)
                {
                    value = arrXYMValue[0];
                    return true;
                }
                value = false;
                return false;
            }
            else
            {
                value = false;
                return false;
            }
        }
        #endregion

        #region 读单触点状态发送的字符串内容 RCS
        /// <summary>
        /// 读单触点状态发送的字符串内容 RCS
        /// </summary>
        /// <param name="XYMAddr">例如R12 X触点 Y触点 M状态寄存器</param>
        /// <returns></returns>
        public string ReadCoilSingle_CommStr(string XYMAddr)
        {
            string commmandCode = "RCS";
            //发送
            string outStr = "";
            string sReg = XYMAddr.Substring(0, 1);
            string sAddr = XYMAddr.Substring(1).PadLeft(4, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr;
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 读多个单触点状态结果 RCP
        /// <summary>
        /// 读多个单触点状态结果 RCP
        /// </summary>
        /// <param name="startAddr"></param>
        /// <param name="endAddr"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool ReadManyCoilData(List<string> xymAddr, out bool[] value)
        {
            readCoilMany_SingleResult = false;
            string writeStr = ReadCoilMany_CommStr(xymAddr);
            value = new bool[xymAddr.Count];
            for (int i = 0; i < value.Length; i++)
            {
                value[i] = false;
            }
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Write(writeStr);
                int numPro = 0;
                while (!readCoilMany_SingleResult && numPro < 100)
                {
                    Thread.Sleep(1);
                    numPro++;
                }
                if (numPro < 100)
                {
                    value = arrXYMValue;
                    Thread.Sleep(1);
                    return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
        #endregion

        #region 读多个单触点状态发送的字符串内容 RCP
        /// <summary>
        /// 读多个单触点状态发送的字符串内容 RCP
        /// </summary>
        /// <param name="XYMAddr">长度为1-8的触点集合</param>
        /// <returns></returns>
        public string ReadCoilMany_CommStr(List<string> XYMAddr)
        {
            string commmandCode = "RCP";
            //发送
            string outStr = "";
            outStr = headStr + stationCode + fixCode + commmandCode + XYMAddr.Count.ToString();
            for (int i = 0; i < XYMAddr.Count; i++)
            {
                string sReg1 = XYMAddr[i].Substring(0, 1);
                string sAddr1 = XYMAddr[i].Substring(1).PadLeft(4, '0');
                outStr += sReg1 + sAddr1;
            }
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 读取数据寄存器值
        /// <summary>
        /// 读取数据寄存器值
        /// </summary>
        /// <param name="startAddr"></param>
        /// <param name="endAddr"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool ReadDT_Data(string startAddr, string endAddr, out double[] value)
        {
            readDataResult = false;
            string writeStr = ReadData_CommStr(startAddr, endAddr);
            string sAddr1 = startAddr.Substring(1).PadLeft(5, '0');
            string sAddr2 = endAddr.Substring(1).PadLeft(5, '0');
            int readLength = int.Parse(sAddr2) - int.Parse(sAddr1) + 1;
            value = new double[readLength];
            for (int i = 0; i < value.Length; i++)
            {
                value[i] = 0;
            }
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Write(writeStr);
                int numPro = 0;
                while (!readDataResult && numPro < 100)
                {
                    Thread.Sleep(1);
                    numPro++;
                }
                if (numPro < 100)
                {
                    value = DTValue;
                    return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
        #endregion

        #region 读取数据寄存器值发送的字符串内容 RD
        /// <summary>
        /// 读取数据寄存器值发送的字符串内容 RD
        /// </summary>
        /// <param name="startAddr">起始地址D/L/F XXXX</param>
        /// <param name="endAddr">结束地址D/L/F XXXX</param>
        /// <returns></returns>
        public string ReadData_CommStr(string startAddr, string endAddr)
        {
            string commmandCode = "RD";
            //发送
            string outStr = "";
            string sReg = startAddr.Substring(0, 1);
            string sAddr1 = startAddr.Substring(1).PadLeft(5, '0');
            string sAddr2 = endAddr.Substring(1).PadLeft(5, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr1 + sAddr2;
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion




        #region 写入数据寄存器值
        /// <summary>
        /// 写入数据寄存器值
        /// </summary>
        /// <param name="startAddr">D/L/F XXXX</param>
        /// <param name="endAddr">D/L/F XXXX</param>
        /// <param name="writeContent">写入的数据内容</param>
        /// <returns></returns>
        public bool WriteData(string startAddr, string endAddr, int[] writeContent)
        {
            writeDataResult = false;
            string writeStr = WriteData_CommStr(startAddr, endAddr, writeContent);
            if (serialPort != null && serialPort.IsOpen)
            {
                serialPort.Write(writeStr);
                int numPro = 0;
                while (!writeDataResult && numPro < 100)
                {
                    Thread.Sleep(1);
                    numPro++;
                    //等待正确响应 或者超时
                }
                if (numPro < 100)
                {
                    return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
        #endregion

        #region 写入数据寄存器值发送的字符串内容 WD
        /// <summary>
        /// 写入数据寄存器值发送的字符串内容 WD
        /// 写入的数值是按字写入,也就是说,每个值占2个字节
        /// 低位在前,高位在后。
        /// </summary>
        /// <param name="startAddr">D/L/F XXXX</param>
        /// <param name="endAddr">D/L/F XXXX</param>
        /// <param name="writeContent">写入的数据内容</param>
        /// <returns></returns>
        public string WriteData_CommStr(string startAddr, string endAddr, int[] writeContent)
        {
            string commmandCode = "WD";
            //发送
            string outStr = "";
            string sReg = startAddr.Substring(0, 1);
            string sAddr1 = startAddr.Substring(1).PadLeft(5, '0');
            string sAddr2 = endAddr.Substring(1).PadLeft(5, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr1 + sAddr2;
            int writeLength = int.Parse(sAddr2) - int.Parse(sAddr1) + 1;
            if (writeLength == 1)
            {
                string strTemp = ConvertShortToPlcFormat(writeContent[0]);
                outStr += strTemp + strTemp;
            }
            else
            {
                for (int i = 0; i < writeLength; i++)
                {
                    string strTemp = ConvertShortToPlcFormat(writeContent[i]);
                    outStr += strTemp;
                }
            }
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 写入字单位的触点的状态信息发送的字符串内容 WCC
        /// <summary>
        /// 写入字单位的触点的状态信息发送的字符串内容 WCC
        /// </summary>
        /// <param name="startAddr">起始地址</param>
        /// <param name="endAddr">结束地址</param>
        /// <param name="writeValues">写入的值数组 1=on 0=off</param>
        /// <returns></returns>
        public string WriteCoilCoils_CommStr(string startAddr, string endAddr, bool[] writeValues)
        {
            string commmandCode = "WCC";
            //发送
            string outStr = "";
            string sReg = startAddr.Substring(0, 1);//Y R L
            string sAddr1 = startAddr.Substring(1).PadLeft(4, '0');
            string sAddr2 = endAddr.Substring(1).PadLeft(4, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr1 + sAddr2;
            int readLength = int.Parse(sAddr2) - int.Parse(sAddr1) + 1;
            for (int i = 0; i < readLength; i++)
            {
                string strTemp = ConvertShortToPlcFormat(writeValues[i] ? 1 : 0);
                outStr += strTemp;
            }
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 读取字单位的触点的状态信息发送的字符串内容 RCC
        /// <summary>
        /// 读取字单位的触点的状态信息发送的字符串内容 RCC
        /// </summary>
        /// <param name="startAddr">M1</param>
        /// <param name="endAddr">M4</param>
        /// <returns></returns>
        public string ReadCoilCoils_CommStr(string startAddr, string endAddr)
        {
            string commmandCode = "RCC";
            //发送
            string outStr = "";
            string sReg = startAddr.Substring(0, 1);
            string sAddr1 = startAddr.Substring(1).PadLeft(4, '0');
            string sAddr2 = endAddr.Substring(1).PadLeft(4, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr1 + sAddr2;
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 读多个触点发送的字符串内容
        /// <summary>
        /// 读多个触点发送的字符串内容
        /// </summary>
        /// <param name="XYMAddr"></param>
        /// <returns></returns>
        public string ReadCoilPlural_CommStr(string XYMAddr)
        {
            string commmandCode = "RCP";
            //发送
            string outStr = "";
            string sReg = XYMAddr.Substring(0, 1);
            string sAddr = XYMAddr.Substring(1).PadLeft(4, '0');
            outStr = headStr + stationCode + fixCode + commmandCode + sReg + sAddr;
            outStr = outStr + Bcc(outStr) + endStr;
            return outStr;
        }
        #endregion

        #region 获得BCC校验码
        /// <summary>
        /// BCC校验码
        /// 计算方式是将指令中的各个ASCII字符的16进制(00~FF)进行异或求和后生成的. 
        /// 该校验码也以两个ASCII码字符表示(高位在前,低位在后)
        /// </summary>
        /// <param name="cmd"></param>
        /// <returns></returns>
        public static string Bcc(string cmd)
        {
            cmd = cmd.Trim();
            byte bcc = 0;
            byte[] cmdArr = System.Text.Encoding.ASCII.GetBytes(cmd);
            for (int i = 0; i < cmdArr.Length; i++)
            {
                bcc = (byte)(bcc ^ cmdArr[i]);
            }
            return bcc.ToString("X2");
        }
        #endregion

        #region ASC
        /// <summary>
        /// ASC
        /// </summary>
        /// <param name="cmd"></param>
        /// <returns></returns>
        public static int Asc(string cmd)
        {
            if (cmd.Length == 1)
            {
                ASCIIEncoding ascii = new ASCIIEncoding();
                int intAscii = (int)ascii.GetBytes(cmd)[0];
                return intAscii;
            }
            else
            {
                return -1;
            }
        }
        #endregion

        #region 将整形转换为16进制 然后低位在前 高位在后
        /// <summary>
        /// 将整形转换为16进制 然后低位在前 高位在后
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private string ConvertShortToPlcFormat(int value)
        {
            string temp = value.ToString("X4");
            return temp.Substring(2, 2) + temp.Substring(0, 2);
        }
        #endregion

        #region 释放
        public void Dispose()
        {
            if (serialPort != null)
            {
                if (serialPort.IsOpen)
                {
                    serialPort.Close();
                    Thread.Sleep(10);
                    serialPort.Dispose();
                }
            }
        }
        #endregion

        #region 获得所有串口名数组
        /// <summary>
        /// 所有串口名数组
        /// </summary>
        /// <returns></returns>
        public static List<string> GetPortsName()
        {
            return SerialPort.GetPortNames().ToList<string>();
        }
        #endregion   
    }

11、项目案例
以下是PLC定义的值表内容在这里插入图片描述
来看看我的项目截图
在这里插入图片描述

在这里插入图片描述
12、总结
只要我们明白NewTocol协议的机制和内容,其他的逻辑部分的处理对于我们程序猿来说就是洒洒水的事的,祝大家看完我的博客可以有所收获,也欢迎大家踊跃来交流交流技术。

ps:部分截图和文字内容来自网上,程序是自己写的,如有侵权,请告知删除,写写。

;