一、Modbus协议 概念Modbus协议是MODICON(莫迪康)(现施耐德品牌)在1979年开发的,是全球第一个真正用于现场的总线协议。Modbus协议是应用于电子控制器的一种通用语言。通过此协议,
一、Modbus协议
- 概念 Modbus协议是MODICON(莫迪康)(现施耐德品牌)在1979年开发的,是全球第一个真正用于现场的总线协议。 Modbus协议是应用于电子控制器的一种通用语言。通过此协议,可以实现控制器相互之间、控制器经由网络和其他设备之间的通信。
- 特点
- 标准开放、公开发表、无版税要求、无许可证费(没有费用)
- 支持多种接口(RS232\RS422\RS485\RJ45);各种传输介质(双绞线,网线)
- 格式简单、紧凑、通俗易懂,容易上手(好用)
- Modbus总线通信环境
- 基本通信
- 从机编码
二、Modbus协议的分类
- 分类
- 串口 RS485(一注多从):ModbusAscii【Ascii字符方式进行发送】、ModbusRTU
- 以太网(点对点链接)ModbusTCP、ModbusUDP
- Modbus协议下的数据存储
- 数据存储中的位、字节byte (8位)、字 word(2个字节,16位)、双字 word(4个字节 32位),C#中的数据显示:数据类型、显示格式
- 内存分区与功能
存储区 对象类型 访问类型 存储区标识 说明 可用功能码 线圈状态 单个bit 读写 0XXXX 通过应用程序改变这种类型数据 01 05 15 输入线圈 单个bit 只读 1XXXX I/O系统提供这种类型数据 02 输入寄存器 16-位字 只读 3XXXX I/O系统提供这种类型数据 04 保持寄存器 16-位字 读写 4XXXX 通过应用程序改变这种类型数据 03 06 16 - 操作存储区的命令
- 功能码:01、02、03、04、05、06、15、16
三、Modbus通信报文解读
-
读寄存器消息帧格式
- TX:发送 RX:接收
示例如下: 16进制
//01:读1号从站保持型寄存器 //03:功能码 //00 00 :起始地址 (高低位)00 00 //00 0A :读取数量 (高低位)00 0A //C5 CD:CRC校验 Tx:000662-01 03 00 00 00 0A C5 CD Rx:000663-01 03 14 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 DA 85
- 0x03 0x04
- 请求
从站地址 功能码 起始地址 读取长度(2byte - > 16bit) CRC 01 03 00(Hi)00(Lo) 00(Hi)0A(Lo) CS CD - 响应
从站地址 功能码 字节数 寄存器值(1) 寄存器值(2) ...... 寄存器值(20) CRC 01 03 14 00(Hi)00(Lo) 00(Hi)00(Lo) ..... 00(Hi)00(Lo) XX XX - 代码如下
- 实现方式一
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); Modbus.Device.ModbusMaster modbusMaster = Modbus.Device.ModbusSerialMaster.CreateRtu(serialPort); Task.Run(() => { while (true) { Task.Delay(5000).Wait(); ushort[] arry = modbusMaster.ReadInputRegisters(1, 0, 2); Console.WriteLine($"温度:{(arry[1] * 0.1).ToString("#0.0")} ℃"); Console.WriteLine($"湿度:{(arry[0] * 0.1).ToString("#0.0")}%"); } });
- 实现方式二
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> bytes = new List<byte>(); //设备号 bytes.Add(0x01); //功能码 bytes.Add(0x03); //地址 两个字节 ushort addr = 0; bytes.Add((byte)(addr / 256)); //高位 bytes.Add((byte)(addr % 256)); //低位 //数量 ushort leng = 2; bytes.Add((byte)(leng / 256)); //高位 bytes.Add((byte)(leng % 256)); //低位 //CRC校验码 bytes = CRC16(bytes); //发送报文 serialPort.Write(bytes.ToArray(), 0, bytes.Count); //接收报文 byte[] data = new byte[2 * 2 + 5]; serialPort.Read(data, 0, data.Length); //解析报文 短整型 01 03 06 00 19 00 19 00 02 6C B1 for (int i = 3; i < data.Length - 2; i = i + 2) { byte[] vb = new byte[2] { data[i + 1], data[i] }; ushort u = BitConverter.ToUInt16(vb);//无符号短整型 BitConverter 为小端处理 Console.WriteLine(u); } //解析浮点型 ABCD for (int i = 3; i < data.Length - 2; i += 4) { var v = data[i + 3]; //D var v1 = data[i + 2];//C var v3 = data[i + 1];//B var v4 = data[i];//A byte[] vb = new byte[4] { data[i + 3], data[i + 2], data[i + 1], data[i] }; float aa = BitConverter.ToSingle(vb); // Console.WriteLine(u); } static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF) { if (value == null || !value.Any()) throw new ArgumentException(""); //运算 ushort crc = crcInit; for (int i = 0; i < value.Count; i++) { crc = (ushort)(crc ^ (value[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 List<byte> buffer = new List<byte>(); buffer.AddRange(value); buffer.Add(lo); buffer.Add(hi); return buffer; }
- 实现方式一
- 请求
- TX:发送 RX:接收
示例如下: 16进制
-
写单寄存器消息帧格式
- 请求与响应
从站地址 功能码 写入地址 写入值(2) CRC 01 06 00(Hi)00(Lo) 00(Hi)00(Lo) XX XX - 代码如下
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> bytes = new List<byte>(); //设备号 bytes.Add(0x01); //功能码 bytes.Add(0x06); //地址 两个字节 ushort addr = 3; bytes.Add((byte)(addr / 256)); //高位 bytes.Add((byte)(addr % 256)); //低位 // 写入寄存器的值 ushort value = 100; bytes.Add((byte)(value / 256)); //高位 bytes.Add((byte)(value % 256)); //低位 //CRC校验码 bytes = CRC16(bytes); serialPort.Write(bytes.ToArray(), 0, bytes.Count);
- 请求与响应
-
写多寄存器消息帧格式
- 请求
从站地址 功能码 写入地址 写入数量 字节数 写入值 CRC 01 10 00(Hi)00(Lo) 00(Hi)0A(Lo) 04 0A AB 00 01 XX XX - 响应
从站地址 功能码 写入地址 写入数量 CRC 01 0F 00(Hi)00(Lo) 00(Hi)0A(Lo) XX XX - 代码如下
- 整形数据
SerialPort serialPort = new SerialPort("COM1",9600,Parity.None,8,StopBits.One); serialPort.Open(); List<byte> list = new List<byte>(); //设备号 list.Add(0x01); //功能码 list.Add(0x10); //地址 ushort addr = 0; list.Add((byte)(addr / 256));//高位 list.Add((byte)(addr % 256));//低位 //写入多个相同类型的值 List<ushort> values = new List<ushort>(); values.Add(111); values.Add(item: 222); values.Add(333); //写入数量 list.Add((byte)(values.Count / 256));//高位 list.Add((byte)(values.Count % 256));//低位 //写入字节数 6个字节 list.Add((byte)(values.Count*2)); for (int i = 0; i < values.Count; i++) { //第一种 //list.Add((byte)(values[i] / 256)); //list.Add((byte)(values[i] %256)); //第二种 //list.Add(BitConverter.GetBytes(values[i])[1]); // list.Add(BitConverter.GetBytes(values[i])[0]); //第三种 list.AddRange(BitConverter.GetBytes(values[i]).Reverse()); } list = CRC16(list); serialPort.Write(list.ToArray(),0, list.Count); //验证检验码 static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF) { if (value == null || !value.Any()) throw new ArgumentException(""); //运算 ushort crc = crcInit; for (int i = 0; i < value.Count; i++) { crc = (ushort)(crc ^ (value[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 List<byte> buffer = new List<byte>(); buffer.AddRange(value); buffer.Add(lo); buffer.Add(hi); return buffer; }
- 浮点型数据
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> list = new List<byte>(); //设备号 list.Add(0x01); //功能码 list.Add(0x10); //地址 ushort addr = 0; list.Add((byte)(addr / 256));//高位 list.Add((byte)(addr % 256));//低位 List<float> values = new List<float>(); values.Add(1.1f); values.Add(item: 2.1f); values.Add(item: 2.3f); //数量 list.Add((byte)(values.Count * 2 / 256)); list.Add((byte)(values.Count * 2 % 256)); //字节长度 list.Add((byte)(list.Count * 4)); for (int i = 0; i < values.Count; i++) { list.Add(BitConverter.GetBytes(values[i])[3]); //A list.Add(BitConverter.GetBytes(values[i])[2]); //B list.Add(BitConverter.GetBytes(values[i])[1]);//C list.Add(BitConverter.GetBytes(values[i])[0]);//D } list = CRC16(list); serialPort.Write(list.ToArray(), 0, list.Count); //验证检验码 static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF) { if (value == null || !value.Any()) throw new ArgumentException(""); //运算 ushort crc = crcInit; for (int i = 0; i < value.Count; i++) { crc = (ushort)(crc ^ (value[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 List<byte> buffer = new List<byte>(); buffer.AddRange(value); buffer.Add(lo); buffer.Add(hi); return buffer; }
- 整形数据
- 请求
-
线圈状态
- 读线圈消息帧格式:OXO1,0X02
- 请求
从站地址 功能码 起始地址 读取长度 CRC 01 01 00(HI) 00(LO) 00(HI) 0A(LO) xx xx - 响应
从站地址 功能码 字节数 输出状态15-8 输出状态15-8 CRC 01 01 02 00 00 xx xx - 代码如下
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> list = new List<byte>(); // 设备名称 list.Add(0x01); //功能码 list.Add(0x01); //起始地址 ushort addr = 0; list.Add((byte)(addr / 256));//高位 list.Add((byte)(addr % 256));//低位 //读取寄存器数量 ushort leng = 10; list.Add((byte)(leng / 256)); //高位 list.Add((byte)(leng % 256)); //低位 list = CRC16(list); serialPort.Write(list.ToArray(), 0, list.Count); //响应 byte[] data = new byte[(int)Math.Ceiling(leng * 1.0 / 8) + 5]; serialPort.Read(data, 0, data.Length); List<byte> dataList = new List<byte>(); //获取字节数据 2个字节 16 位 for (int i = 3; i < data.Length && dataList.Count < (int)Math.Ceiling(leng * 1.0 / 8); i++) { dataList.Add(data[i]); } int count = 0; //字节运算 for (int i = 0; i < dataList.Count; i++) { //按位与运算的方式 for (int k = 0; k < 8; k++) { //移位 byte temp = (byte)(1 << k % 8); //与运算 byte b = (byte)(dataList[i] & temp); //输出结果 Console.WriteLine((dataList[i] & temp) != 0); count++; if (count == leng) break; } } //CRC校验码 static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF) { if (value == null || !value.Any()) throw new ArgumentException(""); //运算 ushort crc = crcInit; for (int i = 0; i < value.Count; i++) { crc = (ushort)(crc ^ (value[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 List<byte> buffer = new List<byte>(); buffer.AddRange(value); buffer.Add(lo); buffer.Add(hi); return buffer; }
- 请求
- 写线圈状态帧 0x05
- 请求
- 响应
从站地址 功能码 写入地址 写入值 CRC 01 05 00(HI) 00(LO) FF(HI)/00(HI) 00(Lo) xx xx - 代码如下
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> data = new List<byte>(); //设备名称 data.Add(0x01); //功能码 data.Add(0x05); //地址 ushort addr = 11; data.Add((byte)(addr/256));//高位 data.Add((byte)(addr % 256));//低位 //写入值 on:0xFF 0x00 off:0x00 0x00 data.Add(0x00); data.Add(0x00); //校验 data = CRC16(data); serialPort.Write(data.ToArray(),0, data.Count); //CRC校验码 static List<byte> CRC16(List<byte> value, ushort poly = 0xA001, ushort crcInit = 0xFFFF) { if (value == null || !value.Any()) throw new ArgumentException(""); //运算 ushort crc = crcInit; for (int i = 0; i < value.Count; i++) { crc = (ushort)(crc ^ (value[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ poly) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 List<byte> buffer = new List<byte>(); buffer.AddRange(value); buffer.Add(lo); buffer.Add(hi); return buffer; }
- 写多线圈状态帧 0x0F
- 请求
从站地址 功能码 写入地址 写入数量 字节数 写入值 CRC 01 0F 00(HI) 00(LO) 00(HI) 0A(LO) 02 0A(Hi) AB(Lo) xx xx - 响应
从站地址 功能码 写入地址 写入数量 CRC 01 0F 00(HI) 00(LO) 00(HI) 00(LO) xx xx - 代码如下
SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One); serialPort.Open(); List<byte> data = new List<byte>(); //设备名称 data.Add(0x01); //功能码 data.Add(0x0F); //地址 ushort addr = 10; data.Add((byte)(addr / 256));//高位 data.Add((byte)(addr % 256));//低位 //输入值 List<bool> state = new List<bool>() { true, false, true, false, true ,true, false, true, false, true }; //写入数量 写入多少个寄存器 data.Add((byte)(state.Count / 256));//高位 data.Add((byte)(state.Count % 256));//低位 byte data1 = 0; List<byte> list = new List< byte > (); int index = 0; //0000 0000 for (int i = 0; i < state.Count; i++) { if (i % 8 == 0) list.Add(0x00); index = list.Count-1; if (state[i]) { //移位 byte temp = (byte)(1 << (i%8)); //或运算 list[index] |= temp; } } //字节数 data.Add((byte)list.Count); // 添加 data.AddRange(list); //校验 data = CRC16(data); serialPort.Write(data.ToArray(),0,data.Count);
- 请求
- 读线圈消息帧格式:OXO1,0X02