云计算百科
云计算领域专业知识百科平台

Modbus-RTU详解

目录

Modbus-RTU协议

Modbus-RTU 核心特性

数据帧格式

帧结构示例

应用场景与示例

1. PLC 控制电机启停

2. 读取温度传感器数据

CRC16校验算法

CRC16算法的过程

modbus-rtu的使用

发送数据

接收数据

tcp网口完整实现modbus-rtu协议

Modbus-RTU和Modbus-TCP的趋区别

使用NModbus4实现modbus-rtu协议

安装NModbus4库。

串口实现NModbus4


Modbus-RTU协议

Modbus RTU 协议是一种开放的串行协议,广泛应用于当今的工业监控设备中。该协议使用 RS-232 或 RS-485 串行接口进行通信,并得到市场上几乎所有商业 SCADA、HMI、OPC 服务器和数据采集软件程序的支持。因此,很容易将 Modbus 兼容设备集成到新的或现有的监控应用程序中,并具有即时的软件支持。


Modbus-RTU 核心特性

特性说明
传输方式 二进制数据传输(紧凑高效)
物理接口 RS-485(多设备总线)或 RS-232(点对点)
校验方式 循环冗余校验(CRC-16),校验范围包括整个数据帧
传输效率 高(无字符转换开销)
最大设备数 RS-485 总线支持最多 32~247 个设备(依赖驱动能力)
典型应用 工业实时控制、设备监控、高速数据采集

数据帧格式

帧结构:设备地址、功能码、数据和CRC校验字段。

常用功能码:Modbus-RTU协议定义了一系列常用的功能码,用于执行不同的操作,如读取保持寄存器、写入单个寄存器、写入多个寄存器等。

(1). 功能码-0x03读保持寄存器:该功能码用于从设备中读取一个或多个保持寄存器的值。 (2). 功能码-0x06写单个寄存器:该功能码用于向设备中写入一个保持寄存器的值。 (3). 功能码-0x10写多个寄存器:该功能码用于向设备中写入多个连续的保持寄存器的值。

帧结构示例

Modbus RTU 功能 01 用于从 Modbus 从站数据采集设备读取线圈状态或数字输出状态。请参阅下面的典型命令和响应以及使用说明。

    主机发送:  01 03 00 00 00 01 84 0A     从机响应:  01 03 02 19 98 B2 7E

该例子中,主机发送的数据为`地址 + 功能码 + 数据 + 校验`​,CRC校验码是根据前面的数据计算得出的

回复的数据格式


应用场景与示例

1. PLC 控制电机启停
  • 请求帧(主机 → 从机地址 0x02): 02 05 00 01 FF 00 8C 09

    • 功能码 0x05:写单个线圈。

    • 地址 0x0001:电机控制寄存器。

    • 值 FF 00:ON(FF00 表示 ON,0000 表示 OFF)。

2. 读取温度传感器数据
  • 请求帧(主机 → 从机地址 0x03): 03 04 00 00 00 01 70 0B

    • 功能码 0x04:读输入寄存器。

    • 地址 0x0000:温度传感器寄存器。

    • 数量 0x0001:读取 1 个寄存器。


CRC16校验算法

CRC全称循环冗余校验(Cyclic Redundancy Check, CRC),是通信领域数据传输技术中常用的检错方法,用于保证数据传输的可靠性。

CRC校验的基本思路是数据发送方发送数据之前,先生成一个CRC校验码,可以是单bit也可以是多bit,并附在有效数据末尾,以串行方式发送到接收方。接收方接收到数据后,进行CRC校验,根据校验结果就可以知道数据是否有误。

CRC校验码的生成:将有效数据**扩展后**作为被除数,使用一个指定的**多项式**作为除数,进行模二除法,得到的**余数**就是校验码。

数据接收方的CRC校验:将接受的数据(**有效数据+CRC校验码**)扩展后作为被除数,用指定的多项式作为除数,进行模二除法,得到**余数为0**,则表示校验正确。

我们使用代码向设备发送命令帧时需要使用CRC算法计算校验值,当设备响应数据时使用CRC算法校验该数据是否正确,

CRC16算法的过程

1 初始化一个16位的寄存器地址 用作初始值 2 遍历数据字节,从最高位到最低位,  3 将数据字节与寄存器异或 4 对寄存器进行8次迭代,每一次迭代将寄存器右移一位 5 如果最低位位1,将寄存器与生成多项式0x8005异或,否则只进行右移操作 6 重复上述步骤直到遍历完所有的字节 7 最终寄存器的值就是crc16校验码 8 crc计算之后高低位进行互换

以下是封装的CRC16效验算法类:

    public static class CRC16
    {
        /// <summary>
        /// CRC校验,参数data为byte数组
        /// </summary>
        /// <param name="data">校验数据,字节数组</param>
        /// <returns>字节0是高8位,字节1是低8位</returns>
        public static byte[] CRCCalc(byte[] data)
        {
            //crc计算赋初始值
            int crc = 0xffff;
            for (int i = 0; i < data.Length; i++)
            {
                crc = crc ^ data[i];
                for (int j = 0; j < 8; j++)
                {
                    int temp;
                    temp = crc & 1;
                    crc = crc >> 1;
                    crc = crc & 0x7fff;
                    if (temp == 1)
                    {
                        crc = crc ^ 0xa001;
                    }
                    crc = crc & 0xffff;
                }
            }
            //CRC寄存器的高低位进行互换
            byte[] crc16 = new byte[2];
            //CRC寄存器的高8位变成低8位,
            crc16[1] = (byte)((crc >> 8) & 0xff);
            //CRC寄存器的低8位变成高8位
            crc16[0] = (byte)(crc & 0xff);
            return crc16;
        }
    
        /// <summary>
        /// CRC校验,参数为空格或逗号间隔的字符串
        /// </summary>
        /// <param name="data">校验数据,逗号或空格间隔的16进制字符串(带有0x或0X也可以),逗号与空格不能混用</param>
        /// <returns>字节0是高8位,字节1是低8位</returns>
        public static byte[] CRCCalc(string data)
        {
            //分隔符是空格还是逗号进行分类,并去除输入字符串中的多余空格
            IEnumerable<string> datac = data.Contains(",") ? data.Replace(" ", "").Replace("0x", "").Replace("0X", "").Trim().Split(',') : data.Replace("0x", "").Replace("0X", "").Split(' ').ToList().Where(u => u != "");
            List<byte> bytedata = new List<byte>();
            foreach (string str in datac)
            {
                bytedata.Add(byte.Parse(str, System.Globalization.NumberStyles.AllowHexSpecifier));
            }
            byte[] crcbuf = bytedata.ToArray();
            //crc计算赋初始值
            return CRCCalc(crcbuf);
        }
    
        /// <summary>
        ///  CRC校验,截取data中的一段进行CRC16校验
        /// </summary>
        /// <param name="data">校验数据,字节数组</param>
        /// <param name="offset">从头开始偏移几个byte</param>
        /// <param name="length">偏移后取几个字节byte</param>
        /// <returns>字节0是高8位,字节1是低8位</returns>
        public static byte[] CRCCalc(byte[] data, int offset, int length)
        {
            byte[] Tdata = data.Skip(offset).Take(length).ToArray();
            return CRCCalc(Tdata);
        }
    }


modbus-rtu的使用

发送数据

现要读取变送器设备(地址 0x01)的风速值,文档如图所示

我们发送的命令帧应为

根据命令帧计算校验码(3种方式)

    byte[] buffer = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
    byte[] crc16 = CRC16.CRCCalc(buffer);  // 根据字节数组计算
    Console.WriteLine($"{crc16[0]:X2} {crc16[1]:X2}");
    
    string data1 = "0x01,0x03,0x00,0x00,0x00,0x02";
    string data1 = "0x01 0x03 0x00 0x00 0x00 0x02";
    byte[] crc16 = CRC16.CRCCalc(data1);    // 根据字符串计算
    Console.WriteLine($"{crc16[0]:X2} {crc16[1]:X2}");
    
     byte[] buffer = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B };
     byte[] crc16 = CRC16Calc(buffer,0,6);  // 从字节数组中截取某部分计算
     Console.WriteLine($"{crc16[0]:X2} {crc16[1]:X2}");

将数据和校验码数组进行合并然后发送

    byte[] buffer = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
    byte[] crc16 = CRC16.CRCCalc(buffer);
    byte[] data = buffer.Concat(crc16).ToArray();
    serialPort.Write(data, 0, data.Length);

接收数据

我们将命令帧(请求帧、问询码)发送后,如果没有错误,从设备会返回对应的数据,如下读取变送器设备(地址 0x01)的实时风力等级值,将会返回如图所示的数据,我们需要将数据读取、校验、计算、展示

    // 假设这是从设备响应的数据
    // 0x01:设备地址码
    // 0x03:功能码
    // 0x02:读取到的数据字节
    // 0x00, 0x01:当前风力等级
    // 0x79, 0x84:校验码
    byte[] value = new byte[] { 0x01, 0x03, 0x02, 0x00, 0x01, 0x79, 0x84 };
    // 1、验证校验码是否正确
    byte[] crc = CRC16.CRCCalc(value, 0, value.Length – 2);
    if (crc[0] != value[value.Length – 2] || crc[1] != value[value.Length-1] ) {
        MessageBox.Show("数据校验错误,应忽略");
        return;
    }
    // 2、验证设备地址
    if (value[0] !=  0x01)
    {
        MessageBox.Show("设备地址不正确");
        return;
    }
    // 3、计算数据
    // int v = value[3] * 256 + value[4];  // 因为这个数据占两个字节,每个字节最大255,相当于256进制,转换为10进制
    int v = (value[3] << 8) + value[4];       // 也可以使用左移运算符,高位左移8位,相当于乘2的8次方
    
    // 4、数据展示
    MessageBox.Show("风力等级:" + v);

tcp网口完整实现modbus-rtu协议

public partial class Form1 : Form
{
/// <summary>
/// 套接字
/// </summary>
Socket socket;

/// <summary>
/// IP地址
/// </summary>
string Ip = "192.168.107.5";

/// <summary>
/// 端口
/// </summary>
string Dk = "8016";

/// <summary>
/// 命令帧
/// </summary>
string Icommand = "01 03 00 00 00 02";

public Form1()
{
InitializeComponent();
button1.Enabled = false;
checkBox1.Enabled = false;
}

/// <summary>
/// 打开连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
if (button2.Text == "连接网口")
{
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(Ip, int.Parse(Dk));

button1.Enabled = true;
checkBox1.Enabled = true;
button2.Text = "断开";
this.timer1.Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
else
{
if (socket == null) return;
socket.Close();
button1.Enabled = false;
checkBox1.Enabled = false;
checkBox1.Checked = false;
button2.Text = "连接网口";
this.timer1.Stop();
}

}

/// <summary>
/// 刷新风速风向数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
byte[] bs = new byte[1024];
bs = StringToByte(Icommand);
byte[] bb = CRCCalc(bs);
bs = bs.Concat(bb).ToArray();
await Task.Run(() =>
{
socket.Send(bs); // 发送请求帧

byte[] body = new byte[1024];
int length = socket.Receive(body); // 获取响应帧

double value = (body[3] * 256+ body[4]) *0.01;
double value2 = (body[5] * 256 + body[6]);
this.Invoke(new Action(() =>
{
this.textBox1.Text = value.ToString() + "m/s";
this.textBox2.Text = value2.ToString();
}));

});

}

/// <summary>
/// 字符串转字节
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
byte[] StringToByte(string s)
{
string[] strings = s.Split(' ') ;
byte[] bs = new byte[strings.Length];
for (int i = 0; i < strings.Length; i++)
{
bs[i] = Convert.ToByte(strings[i],16);
}
return bs;
}

/// <summary>
/// CRC效验
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static byte[] CRCCalc(byte[] data)
{
//crc计算赋初始值
int crc = 0xffff;
for (int i = 0; i < data.Length; i++)
{
//XOR
//(1) 0^0=0,0^1=1 0异或任何数=任何数
//(2) 1 ^ 0 = 1,1 ^ 1 = 0 1异或任何数-任何数取反
//(3) 1 ^ 1 = 0,0 ^ 0 = 0 任何数异或自己=把自己置0
//异或操作符是^。异或的特点是相同为false,不同为true。
crc = crc ^ data[i]; //和^表示按位异或运算。
//0x0fff ^ 0x01 Console.WriteLine(result.ToString("X"));
// 输出结果为4094,即十六进制数1001
for (int j = 0; j < 8; j++)
{
int temp;
temp = crc & 1; // & 运算符(与) 1 & 0 为 0 ;0 & 0 为0;1 & 1 为1

//右移 (>>) 将第一个操作数向右移动第二个操作数所指定的位数,空出的位置补0。右移相当于整除. 右移一位相当于除以2;右移两位相当于除以4;右移三位相当于除以8。
//int i = 7;
//int j = 2;
//Console.WriteLine(i >> j); //输出结果为1
crc = crc >> 1;
crc = crc & 0x7fff;
if (temp == 1)
{
crc = crc ^ 0xa001;
}
crc = crc & 0xffff;
}
}
//CRC寄存器的高低位进行互换
byte[] crc16 = new byte[2];
//CRC寄存器的高8位变成低8位,
crc16[1] = (byte)((crc >> 8) & 0xff);
//CRC寄存器的低8位变成高8位
crc16[0] = (byte)(crc & 0xff);
return crc16;
}
}


Modbus-RTU和Modbus-TCP的趋区别

特性Modbus-RTUModbus-TCP
物理层 RS-485/RS-232 以太网(RJ45)
传输速率 最高 115.2 kbps 100 Mbps ~ 1 Gbps
拓扑结构 总线型 星型/树型/网状
实时性 高(确定性延迟) 依赖网络状况
扩展性 有限(受总线长度限制)

高(支持跨网段通信)


使用NModbus4实现modbus-rtu协议

NModbus4是一个C#实现的Modbus库,它允许开发者以Modbus RTU的方式与工业设备进行通信。

安装NModbus4库。

通过NuGet安装NModbus4

串口实现NModbus4

public partial class Form1 : Form
{
// 创建对象
ModbusSerialMaster master;
public Form1()
{
InitializeComponent();
this.serialPort1.PortName = "COM2"; // 串口名
this.serialPort1.BaudRate = 9600;
this.serialPort1.DataBits = 8;
this.serialPort1.Parity =System.IO.Ports.Parity.None;
this.serialPort1.StopBits = System.IO.Ports.StopBits.One;

serialPort1.Open();
master = ModbusSerialMaster.CreateRtu(serialPort1);
}

/// <summary>
/// 读取数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
// ReadHoldingRegistersAsync 异步读取数据
// await 等待异步任务执行完之后 再往下执行
// 参数1 从站地址, 参数2 起始地址 参数3:寄存器个数
// values 元素个数和寄存器个数有关
ushort[] values = await master.ReadHoldingRegistersAsync(1,0×00,3);
comboBox1.DataSource = values;
}

/// <summary>
/// 写入数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button2_Click(object sender, EventArgs e)
{
// 写入 单个的寄存器
// 参数1 从站地址
// 参数2 写入的地址
// 参数3 写入的数据
// short 短整型
// ushort 无符号的短整型
await master.WriteSingleRegisterAsync(1,0×04,14);
}
}

赞(0)
未经允许不得转载:网硕互联帮助中心 » Modbus-RTU详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!