c# modbus的TCP数据协议
参考:C#实现MODBUS TCP 通信 第二章 (程序内实现) - 『编程语言区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
modbus通讯协议详解 - 孤情剑客 - 博客园 (cnblogs.com)
只要了解这个modubs的数据格式
常用的命令
功能码(16进制) 功能说明 0x01 读取输出线圈 1 0x02 读取输入线圈 2 0x03 读取保持寄存器 3 0x04 读取输入寄存器 4 0x05 写入单个线圈 5 0x06 写入单个寄存器 6 0x0F 写入多个线圈 15 0x10 写入多个寄存器 16
解释
线圈寄存器:实际上就可以类比为开关量(继电器状态),每一个bit对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。对应上面的功能码也就是:0x01 0x05 0x0f 离散输入寄存器:如果线圈寄存器理解了这个自然也明白了。离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。所以功能码也简单就一个读的 0x02 保持寄存器:这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。一般对应参数设置,比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写,所以功能码有对应的三个:0x03 0x06 0x10 输入寄存器:这个和保持寄存器类似,但是也是只支持读而不能写,一般是读取各种实时数据。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值。对应的功能码也就一个 0x04
modubs数据协议格式
1. ModbusRtu的报文格式如下(PDU): 第一部分:从站地址,占1个字节 第二部分:功能码,占1个字节 第三部分:数据部分,占N个字节 第四部分:校验部分,CRC校验,占2个字节 2. ModbusAscii的报文格式如下: 第一部分:开始字符(:) 第二部分:从站地址,占2个字节 第三部分:功能码,占2个字节 第四部分:数据部分,占N个字节 第五部分:校验部分,LRC校验,占2个字节 第六部分:结束字符(CR LF) 3. ModbusTcp的报文格式如下: 第一部分:事务处理标识符,占2个字节 第二部分:协议标识符,占2个字节 第三部分:长度,占2个字节 第四部分:单元标识符,占1个字节 第五部分:功能码,占1个字节 第六部分:数据部分,占N个字节
修改modbus数据的格式(tcp)
(1)、0x01:读线圈 在从站中读1~2000个连续线圈状态,ON=1,OFF=0 请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节) 响应:MBAP 功能码 数据长度 数据(一个地址的数据为1位) 如:在从站0x01中,读取开始地址为0x0002的线圈数据,读0x0008位------------------00 01 00 00 00 06 01 01 00 02 00 08 回:数据长度为0x01个字节,数据为0x01,第一个线圈为ON,其余为OFF------------00 01 00 00 00 04 01 01 01 01 (2)、0x05:写单个线圈 将从站中的一个输出写成ON或OFF,0xFF00请求输出为ON,0x000请求输出为OFF 请求:MBAP 功能码 输出地址H 输出地址L 输出值H 输出值L(共12字节) 响应:MBAP 功能码 输出地址H 输出地址L 输出值H 输出值L(共12字节) 如:将地址为0x0003的线圈设为ON---------------00 01 00 00 00 06 01 05 00 03 FF 00 回:写入成功--------------------------------------------00 01 00 00 00 06 01 05 00 03 FF 00 (3)、0x0F:写多个线圈 将一个从站中的一个线圈序列的每个线圈都强制为ON或OFF,数据域中置1的位请求相应输出位ON,置0的位请求响应输出为OFF 请求:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L 字节长度 输出值H 输出值L 响应:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L (4)、0x02:读离散量输入 从一个从站中读1~2000个连续的离散量输入状态 请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节) 响应:MBAP 功能码 数据长度 数据(长度:9+ceil(数量/8)) 如:从地址0x0000开始读0x0012个离散量输入-------------------------------------------------------------------------------------------------00 01 00 00 00 06 01 02 00 00 00 12 回:数据长度为0x03个字节,数据为0x01 04 00,表示第一个离散量输入和第11个离散量输入为ON,其余为OFF----------00 01 00 00 00 06 01 02 03 01 04 00 (5)、0x04:读输入寄存器 从一个远程设备中读1~2000个连续输入寄存器 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节) 响应:MBAP 功能码 数据长度 寄存器数据(长度:9+寄存器数量×2) 如:读起始地址为0x0002,数量为0x0005的寄存器数据----------------------------00 01 00 00 00 06 01 04 00 02 00 05 回:数据长度为0x0A,第一个寄存器的数据为0x0c,其余为0x00----------------00 01 00 00 00 0D 01 04 0A 00 0C 00 00 00 00 00 00 00 00 (6)、0x03:读保持寄存器 从远程设备中读保持寄存器连续块的内容 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节) 响应:MBAP 功能码 数据长度 寄存器数据(长度:9+寄存器数量×2) 如:起始地址是0x0000,寄存器数量是 0x0003----------------------------------00 01 00 00 00 06 01 03 00 00 00 03 回:数据长度为0x06,第一个寄存器的数据为0x21,其余为0x00-----------00 01 00 00 00 09 01 03 06 00 21 00 00 00 00 (7)、0x06:写单个保持寄存器 在一个远程设备中写一个保持寄存器 请求:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节) 响应:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节) 如:向地址是0x0000的寄存器写入数据0x000A------------------00 01 00 00 00 06 01 06 00 00 00 0A 回:写入成功--------------------------------------------------------------00 01 00 00 00 06 01 06 00 00 00 0A (8)、0x10:写多个保持寄存器 在一个远程设备中写连续寄存器块(1~123个寄存器) 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L 字节长度 寄存器值(13+寄存器数量×2) 响应:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节) 如:向起始地址为0x0000,数量为0x0001的寄存器写入数据,数据长度为0x02,数据为0x000F-----------00 01 00 00 00 09 01 10 00 00 00 01 02 00 0F 回:写入成功------------------------------------------------------------------------------------------------------------------------00 01 00 00 00 06 01 10 00 00 00 01
请求示例
请求内容:00 0B 00 00 00 06 06 01 00 02 00 08 事务处理标识(2字节):00 0B 协议标识符(2字节):00 00 长度(2字节):00 06 单元标识符(1字节):01 功能码(1字节):06 数据地址(2字节):00 02 数据内容(2字节):00 08 响应内容:00 0B 00 00 00 06 06 01 00 02 00 08 事务处理标识(2字节):00 0B 协议标识符(2字节):00 00 长度(2字节):00 06 单元标识符(1字节):01 功能码(1字节):06 数据地址(2字节):00 02 数据内容(2字节):00 08
请求测试数据
代码实现
//拼接 List<byte> SendCommand = new List<byte>(); //事物处理标识符 SendCommand.AddRange(new byte[] { 0x00, 0x0B }); //协议标识符 SendCommand.AddRange(new byte[] { 0x00, 0x00 }); //长度 SendCommand.AddRange(new byte[] { 0x00, 0x06 }); //单元标识符 SendCommand.Add(0x01); ////功能码 //SendCommand.Add(0x03); //功能码 SendCommand.Add(0x06); ////起始地址 //SendCommand.Add((byte)(start / 256)); //SendCommand.Add((byte)(start % 256)); ////结束地址-长度 //SendCommand.Add((byte)(length / 256)); //SendCommand.Add((byte)(length % 256)); ////起始地址 //SendCommand.AddRange(new byte[] { 0x00, 0x00 }); ////结束地址-长度 //SendCommand.AddRange(new byte[] { 0x00, 0x0A }); //起始地址 SendCommand.AddRange(new byte[] { 0x00, 0x01 }); //具体数据 SendCommand.AddRange(new byte[] { 0x00, 0x12 }); var sendData = Encoding.Default.GetString(SendCommand.ToArray()); Task.Factory.StartNew(new Action(() => { Thread.Sleep(1000); _client.SendData(sendData); }));
返回的结果处理
private void HandelByte(byte[] buffer) { int count = buffer.Count(); if (buffer[7] == 0x03)//读取多个 { int length = buffer[8] / 2; if (count == 9 + 2 * length) { ShowData(buffer, length); } } else if (buffer[7] == 0x06)//写入单个寄存器 { //解析 ushort[] data = new ushort[1]; data[0] = BitConverter.ToUInt16(new byte[] { buffer[11], buffer[10] }, 0); Console.WriteLine(data); } } /// <summary> /// /// </summary> /// <param name="buffer"></param> /// <param name="length"></param> /// <param name="j"></param> private void ShowData(byte[] buffer, int length) { //解析 ushort[] data = new ushort[length]; for (int i = 0; i < data.Length; i++) { data[i] = BitConverter.ToUInt16(new byte[] { buffer[9 + 2 * i + 1], buffer[9 + 2 * i] }, 0); } Console.WriteLine(data); }
效果图(这样就修改好了一个地址数据)