1.概述

Modbus是由Schneider Electric(施耐德电气)为PLC(可编程逻辑控制器)通信而研发的一种串行通信协议,是应用于电子控制器上的一种通用语言。常用的有两种ModbusRTU和ModbusTCP,还有一种Modbus ASCII,但是相对来说要用的少一些,如需了解,可参考我另一篇文章“C# Modbus ASCII命令功能码”。 Modbus RTU 协议是一种主从式串行异步半双工通信协议,是基于RS485\RS422\RS232物理层的通信的,在协议通信中每个字符通信格式规定为1个起始位、8个数据位、1个校验位、1\2个停止位,并且有CRC错误校验。 所谓主从,我们直接可以理解为发送请求的是主,也就是Master,常指我们用的上位机(电脑);那么响应请求的就是从,即Slave也就是PLC等485硬件设备,ModbusRTU指令中从站地址就是指的这些设备。 有人说Modbus服务器端和客户端,实际用的是网络通信用语,和上面说的主从一回事,服务器端是响应请求的,自然指的就是PLC等硬件设备,客户端是发送请求的,也就是指我们用的电脑了。在Modbus中,Slave和Server意思相同,Master和Client意思相同。

2.Modbus RTU 通信协议寄存器种类

寄存器种类

说明

与PLC比较

举例说明

线圈状态 Coil Status

数字量输出、继电器输出,可读可写

DO数字量输出或内部读写位变量

电磁阀输出、继电器

离散量输入 Input Status

数字量输入,只读

DI数字量输入或内部只读位量

按钮输入、拨码开关、接近开关

保持寄存器 Holding Register

输出参数、保持参数,可读可写

AO模拟量输出或内部读写寄存器

模拟量输出设定、变量阀输出大小

输入寄存器 Input Register

输入参数,只读

AI模拟量输入或内部只读寄存器

模拟量输入、现场工程量信号采集

3.ModbusRTU的八条主要功能码命令

功能码

作 用

0x01

读线圈

0x02

读离散量输入

0x03

读保持寄存器

0x04

读输入寄存器

0x05

写单个线圈

0x06

写单个寄存器

0x0F

写多个线圈

0x10

写多个寄存器

4.ModbusRTU的八条命令生成代码

上面我们简单描述了ModbusRTU的一些基本知识,是为了我们生成ModbusRTU指令做准备,至于更多关于ModbusRTU的技术内容,大家可以自己网上查询,这里不再赘述。 ModbusRTU这8条常用指令,其实与ModbusTCP大致相同,只不过ModbusTCP多了一个7个字节的MBAP报文头和少了CRC校验,有兴趣了解的朋友可以参看我另一篇“C# ModbusTCP命令功能码”的文章。 本文介绍的是生成ModbusRTU主要八条命令的代码,它是在总线传输前的指令代码集,类型是二进制数组,不包含串口传输部分(如果需要串口传输代码,请参考我的文章"C# 串口发送ModbusRTU、ModbusASCII的8条主要功能代码并获取返回的响应信息"),但它是是串口传输的必不可少的一项,同样适用于采用ModbusRTU协议的485/422/232转网络的采集器(有人叫集中器),这种产品在多数环境下比Modbus总线更容易部署,对于熟悉网络的技术人员来说更加容易实现,避免了Modbus总线无法实现的星形连接方式,也不用担心增加120欧的终端电阻等方式,当然并不是说Modbus总线连接方式不好,Modbus总线连接有它独有的优势,这里只是就某种环境而言的。 在写这八条ModbusRTU指令代码之前,我们得先声明两个枚举,其中有几条枚举项现在用不上,但是我写上它是为了能列出来更多的Modbus指令和在取ModbusRTU返回数据时使用的。

/// <summary>
    /// Modbus指令代码
    /// </summary>
    public enum ModbusCodes
    {
        READ_COILS = 0x01,                      //读取线圈
        READ_DISCRETE_INPUTS = 0x02,            //读取离散量输入
        READ_HOLDING_REGISTERS = 0x03,          //读取保持寄存器
        READ_INPUT_REGISTERS = 0x04,            //读取输入寄存器
        WRITE_SINGLE_COIL = 0x05,               //写单个线圈
        WRITE_SINGLE_REGISTER = 0x06,           //写单个保持寄存器
        READ_EXCEPTION_STATUS = 0x07,           //读取异常状态
        DIAGNOSTIC = 0x08,                      //回送诊断校验
        GET_COM_EVENT_COUNTER = 0x0B,           //读取事件计数
        GET_COM_EVENT_LOG = 0x0C,               //读取通信事件记录
        WRITE_MULTIPLE_COILS = 0x0F,            //写多个线圈
        WRITE_MULTIPLE_REGISTERS = 0x10,        //写多个保持寄存器
        REPORT_SLAVE_ID = 0x11,                 //报告从机标识(ID)
        READ_FILE_RECORD = 0x14,                //读文件记录
        WRITE_FILE_RECORD = 0x15,               //写文件记录
        MASK_WRITE_REGISTER = 0x16,             //屏蔽写寄存器
        READ_WRITE_MULTIPLE_REGISTERS = 0x17,   //读写多个寄存器
        READ_FIFO_QUEUE = 0x18,                 //读取队列
        READ_DEVICE_IDENTIFICATION = 0x2B       //读取设备标识
    }

    /// <summary>
    /// 错误代码
    /// </summary>
    public enum Errors
    {
        NO_ERROR = 0,//无错误
        EXCEPTION_UNKNOWN = 1,//未知异常
        EXCEEDING_MODBUSCODE_RANGE = 2,//超出ModbusCode范围
        UNPROCESSED_MODBUSCODE = 3,//没有处理的ModbusCode
        WRONG_RESPONSE_ADDRESS = 4,//响应地址错误
        WRONG_RESPONSE_REGISTERS = 5,//响应寄存器错误
        WRONG_RESPONSE_VALUE = 6,//响应值错误
        WRONG_CRC = 7,//CRC16校验错误
        TOO_MANY_REGISTERS_REQUESTED = 8,//请求的寄存器数量太多
        ZERO_REGISTERS_REQUESTED = 9,//零寄存器请求
        EXCEPTION_ILLEGAL_FUNCTION = 20,//非法的功能码
        EXCEPTION_ILLEGAL_DATA_ADDRESS = 21,//非法的数据地址
        EXCEPTION_ILLEGAL_DATA_VALUE = 22,//非法的数据值
        EXCEPTION_SLAVE_DEVICE_FAILURE = 23,//从站(服务器)故障
        
    }

下面就是ModbusRTU的主要八条命令代码,返回类型均是byte[],可直接代入程序运行。

#region Modbus RTU
    
    public class ModbusRTU
    {
        #region 常量
        /// <summary>
        /// 可读取的线圈的最大数量
        /// </summary>
        public const ushort MAX_COILS_IN_READ_NUM = 2000;

        /// <summary>
        /// 可读取的离散量的最大数量
        /// </summary>
        public const ushort MAX_DISCRETE_INPUTS_IN_READ_NUM = 2000;

        /// <summary>
        /// 可读取的保持寄存器的最大数量
        /// </summary>
        public const ushort MAX_HOLDING_REGISTERS_IN_READ_NUM = 125;

        /// <summary>
        /// 可读取的输入寄存器的最大数量
        /// </summary>
        public const ushort MAX_INPUT_REGISTERS_IN_READ_NUM = 125;

        /// <summary>
        /// 可写入的线圈的最大数量
        /// </summary>
        public const ushort MAX_COILS_IN_WRITE_NUM = 1968;

        /// <summary>
        /// 可写入的保持寄存器的最大数量
        /// </summary>
        public const ushort MAX_HOLDING_REGISTERS_IN_WRITE_NUM = 123;

        /// <summary>
        /// 可读取读/写的保持寄存器的最大数量
        /// </summary>
        public const ushort MAX_HOLDING_REGISTERS_TO_READ_IN_READWRITE_NUM = 125;

        /// <summary>
        /// 可写取读/写的保持寄存器的最大数量
        /// </summary>
        public const ushort MAX_HOLDING_REGISTERS_TO_WRITE_IN_READWRITE_NUM = 121;
        #endregion

        #region 变量
        /// <summary>
        /// Modbus错误
        /// </summary>
        public Errors error;
        #endregion

        #region 读取线圈,功能码0x01
        /// <summary>
        /// 生成ModbusRTU读取线圈状态指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Numbers">读取线圈的数量</param>
        /// <returns>读取线圈状态的指令byte[]</returns>
        public byte[] ReadCoilStatus_0x01(byte SlaveId, ushort StartAddress, ushort Numbers)
        {
            error = Errors.NO_ERROR;
            if (Numbers < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Numbers > MAX_COILS_IN_READ_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            byte[] command = new byte[8];
            try
            {

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x01
                command[1] = (byte)ModbusCodes.READ_COILS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是线圈数量
                Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;
        }
        #endregion

        #region 读取离散量,功能码0x02
        /// <summary>
        /// 生成ModbusRTU读取离散量状态指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Numbers">读取离散量的数量</param>
        /// <returns>读取离散量状态的指令byte[]</returns>
        public byte[] ReadInputStatus_0x02(byte SlaveId, ushort StartAddress, ushort Numbers)
        {
            error = Errors.NO_ERROR;
            if (Numbers < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Numbers > MAX_DISCRETE_INPUTS_IN_READ_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            byte[] command = new byte[8];
            try
            {

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x02
                command[1] = (byte)ModbusCodes.READ_DISCRETE_INPUTS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是线圈数量
                Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;

        }
        #endregion

        #region 读取保持寄存器,功能码0x03
        /// <summary>
        /// 生成ModbusRTU读取保持寄存器指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Numbers">读取保持寄存器的数量</param>
        /// <returns>读取保持寄存器的指令byte[]</returns>
        public byte[] ReadHoldingRegister_0x03(byte SlaveId, ushort StartAddress, ushort Numbers)
        {
            error = Errors.NO_ERROR;
            if (Numbers < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Numbers > MAX_HOLDING_REGISTERS_IN_READ_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            byte[] command = new byte[8];
            try
            {

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x03
                command[1] = (byte)ModbusCodes.READ_HOLDING_REGISTERS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是线圈数量
                Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;
        }
        #endregion

        #region 读取输入寄存器,功能码0x04
        /// <summary>
        /// 生成ModbusRTU读取输入寄存器指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Numbers">读取输入寄存器的数量</param>
        /// <returns>读取输入寄存器的指令byte[]</returns>
        public byte[] ReadInputRegister_0x04(byte SlaveId, ushort StartAddress, ushort Numbers)
        {
            error = Errors.NO_ERROR;
            if (Numbers < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Numbers > MAX_INPUT_REGISTERS_IN_READ_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            byte[] command = new byte[8];
            try
            {

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x04
                command[1] = (byte)ModbusCodes.READ_INPUT_REGISTERS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是线圈数量
                Array.Copy(BitConverter.GetBytes(Numbers).Reverse().ToArray(), 0, command, 4, 2);
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;
        }
        #endregion

        #region 写单个线圈,功能码0x05
        /// <summary>
        /// 生成ModbusRTU写单个线圈指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Value">单个线圈是开启(true)还是关闭(false)</param>
        /// <returns>写单个线圈的指令byte[]</returns>
        public byte[] WriteSingleCoil_0x05(byte SlaveId, ushort StartAddress, bool Value)
        {
            error = Errors.NO_ERROR;
            byte[] command = new byte[8];
            try
            {

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x05
                command[1] = (byte)ModbusCodes.WRITE_SINGLE_COIL;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是开关标识,0xFF00为开,0x0000为关
                command[4] = Value ? (byte)0xFF : (byte)0x00;
                command[5] = 0x00;
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;
        }
        #endregion

        #region 写单个保持寄存器,功能码0x06
        /// <summary>
        /// 生成ModbusRTU写单个保持寄存器指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Value">单个保持寄存器的值,类型是无符号16位整数</param>
        /// <returns>写单个保持寄存器的指令byte[]</returns>
        public byte[] WriteSingleRegister_0x06(byte SlaveId, ushort StartAddress, ushort Value)
        {
            error = Errors.NO_ERROR;
            byte[] command = new byte[8];
            try
            {
                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x06
                command[1] = (byte)ModbusCodes.WRITE_SINGLE_REGISTER;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是开关标识,0xFF00为开,0x0000为关
                Array.Copy(BitConverter.GetBytes(Value).Reverse().ToArray(), 0, command, 4, 2);
                //第7位和第8位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 6);
                Array.Copy(crc16, 0, command, 6, 2);
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

            return command;
        }
        #endregion

        #region 写多个线圈,功能码0x0F
        /// <summary>
        /// 生成ModbusRTU写多个线圈指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Values">多个线圈的写入状态,从起始地址线圈开始依次排列</param>
        /// <returns>写多个线圈的指令byte[]</returns>
        public byte[] WriteMultipleCoil_0x0F(byte SlaveId, ushort StartAddress, bool[] Values)
        {
            error = Errors.NO_ERROR;
            if (Values == null)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Values.Length < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Values.Length > MAX_COILS_IN_WRITE_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            try
            {
                //线圈数值长度(按字节算)
                byte ValuesBytesCount = Convert.ToByte(Math.Ceiling((double)Values.Count() / 8));
                //byte ValueBytesCount = (byte)((Value.Length / 8) + ((Value.Length % 8) == 0 ? 0 : 1));
                byte[] command = new byte[8 + 1 + ValuesBytesCount];

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x06
                command[1] = (byte)ModbusCodes.WRITE_MULTIPLE_COILS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是线圈数值长度(接位数算)
                Array.Copy(BitConverter.GetBytes((ushort)Values.Length).Reverse().ToArray(), 0, command, 4, 2);
                //第7位是线圈数值长度(字节数)
                command[6] = ValuesBytesCount;

                /*   后面是线圈ValueBytesCount个字节的数值,在Modbus中是以大端法排列,每个字节的二进值对应的线圈顺序是
                 *   【87654321】 【16 15 14 13 12 11 10 9】 【24 23 22 21 20 19 18 17】 ......(每个【】代表1个字节)。
                 *   我们使用BitArray类将传入的Values(bool数组)转换为ModbusRTU所需要的大端byte数组。BitArray是位数组,
                 *   位的排列是小端法,所以当用参数Value以bool数组初始化时,它会跟bool数组一样,索引0对应bool数组的索引0,
                 *   1对应1,依次往后排列。当它用CopyTo方法转换为byte数组时,byte数组二进制值就会变成上面说的那样的顺序---
                 *   【87654321】 【16 15 14 13 12 11 10 9】 【24 23 22 21 20 19 18 17】 ......
                 *   例如:我们输入一个bool数组,new bool[]{false,ture,false,false,false,false,false,true},
                 *   相当于0100 0001,用BitArray转换为byte[]后,会成为1000 0010这样的值。
                 */
                byte[] dataValue = new byte[ValuesBytesCount];
                BitArray Ba = new BitArray(Values);
                Ba.CopyTo(dataValue, 0);
                //把线圈值按字节加入command中
                Array.Copy(dataValue, 0, command, 7, ValuesBytesCount);

                //最后两位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 7 + ValuesBytesCount);
                Array.Copy(crc16, 0, command, 7 + ValuesBytesCount, 2);

                return command;
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }
        }
        #endregion

        #region 写多个保持寄存器,功能码0x10
        /// <summary>
        /// 生成ModbusRTU写多个保持寄存器指令byte数组
        /// </summary>
        /// <param name="SlaveId">从站号(服务器端)</param>
        /// <param name="StartAddress">起始地址</param>
        /// <param name="Values">多个保持寄存器的值(无符号16位),从起始地址开始依次排列</param>
        /// <returns>写多个保持寄存器的指令byte[]</returns>
        public byte[] WriteMultipleRegister_0x10(byte SlaveId, ushort StartAddress, ushort[] Values)
        {
            error = Errors.NO_ERROR;
            if (Values == null)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Values.Length < 1)
            {
                error = Errors.ZERO_REGISTERS_REQUESTED;
                return null;
            }
            if (Values.Length > MAX_HOLDING_REGISTERS_IN_WRITE_NUM)
            {
                error = Errors.TOO_MANY_REGISTERS_REQUESTED;
                return null;
            }

            try
            {
                //保持寄存器的字节数
                byte ValuesBytesCount = (byte)(Values.Length * 2);

                byte[] command = new byte[8 + 1 + ValuesBytesCount];

                //第1位是从站站号
                command[0] = SlaveId;
                //第2位是功能码0x06
                command[1] = (byte)ModbusCodes.WRITE_MULTIPLE_REGISTERS;
                //第3和第4位起始地址(大端法)
                Array.Copy(BitConverter.GetBytes(StartAddress).Reverse().ToArray(), 0, command, 2, 2);
                //第5位和第6位是写入保持寄存器的数量
                Array.Copy(BitConverter.GetBytes((ushort)Values.Length).Reverse().ToArray(), 0, command, 4, 2);
                //第7位是写入保持寄存器的字节数
                command[6] = ValuesBytesCount;

                //将写入寄存器的值加入command中
                for (int i = 0; i < Values.Length; i++)
                {
                    Array.Copy(BitConverter.GetBytes(Values[i]).Reverse().ToArray(), 0, command, 7 + i * 2, 2);
                }

                //最后两位是CRC16校验
                byte[] crc16 = CRC16.CRCCalc(command, 0, 7 + ValuesBytesCount);
                Array.Copy(crc16, 0, command, 7 + ValuesBytesCount, 2);

                return command;
            }
            catch
            {
                error = Errors.EXCEPTION_UNKNOWN;
                return null;
            }

        }
        #endregion


    }
    #endregion

5.代码说明

以上代码需要注意几点:1、如果代码返回null,请查看error的值,如果是Errors.EXCEPTION_UNKNOWN,那么多数是由于数组Copy产生的,说明输入参数有问题。2、功能码0x0F(写多个线圈)的参数Values是bool数组,是对一组线圈操作的,顺序应按线圈地址从低到高进行赋值,即Values[0]是线圈的起始地址的开关量,Values[N]是线圈的最高地址的开关量。3、代码中的CRC校验的方法CRC16.CRCCalc()本文没有列出来,如果需要,可以参考我另一篇“C# Modbus的CRC16校验代码”的文章。