欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

IoTClient开发3 - ModBusTcp协议客户端实现

程序员文章站 2023-12-18 17:47:22
前言 进过前面两章的介绍,今天开始正式的实战。 进制转换 很多朋友对于进制转换可能是在刚学计算机的时候有接触,后来做高级语言开发可能就慢慢忘记了。我们做工控开发的时候需要经常进行进制转换,这里和大家一起复习下。 一个字节等8位(1byte = 8bit),可以存储2^8(0 255)共计256个数字 ......

前言

进过前面两章的介绍,今天开始正式的实战。

进制转换

很多朋友对于进制转换可能是在刚学计算机的时候有接触,后来做高级语言开发可能就慢慢忘记了。我们做工控开发的时候需要经常进行进制转换,这里和大家一起复习下。
一个字节等8位(1byte = 8bit),可以存储2^8(0-255)共计256个数字。所以我们要对8、256等数字要敏感。
int16(short), int32(int), int64(long) 分别是占用2个字节、4个字节、8个字节,single(float)也是占用4个字节。

bool       system.boolean (布尔型,其值为 true 或者 false)
byte       system.byte    (字节型,占 1 字节,表示 8 位正整数,范围 0 ~ 255)
sbyte      system.sbyte   (带符号字节型,占 1 字节,表示 8 位整数,范围 -128 ~ 127)
char       system.char    (字符型,占有 2 个字节,表示 1 个 unicode 字符)
short      system.int16   (短整型,占 2 字节,表示 16 位整数,范围 -32,768 ~ 32,767)
ushort     system.uint16  (无符号短整型,占 2 字节,表示 16 位正整数,范围 0 ~ 65,535)
uint       system.uint32  (无符号整型,占 4 字节,表示 32 位正整数,范围 0 ~ 4,294,967,295)
int        system.int32   (整型,占 4 字节,表示 32 位整数,范围 -2,147,483,648 到 2,147,483,647)
float      system.single  (单精度浮点型,占 4 个字节)
ulong      system.uint64  (无符号长整型,占 8 字节,表示 64 位正整数)
long       system.int64   (长整型,占 8 字节,表示 64 位整数)
double     system.double  (双精度浮点型,占8 个字节)

接着我们来看其他进制转十进制的计算

十进制转十进制  
1263 = 1*10^3 + 2*10^2 + 6*10^1 + 3*10^0 = 1000 + 200 + 60 + 3 = 1263
二进制转十进制 
1001 = 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0 = 8 + 0 + 0 + 1 = 9 
十六进制转十进制 
3245 = 3*16^3 + 2*16^2 + 4*16^1 + 5*16^0 = 3*4096 + 2*256 + 4*16 + 5 = 12869

十进制转二进制

第八位 第七位 第六位 第五位 第四位 第三位 第二位 第一位
2^7     2^6    2^5    2^4     2^3    2^2    2^1   2^0
128     64     32     16      8      4      2      1

以上位二进制位能存储最大十进制数,所以我们反过来也可以对照把十进制转二进制。比如86,
86小于128多以第八位是0,86大于64所以第七位是1。86-64=22,22小于32所以第六位是0,22大于16所以第五位是1。。。 

IoTClient开发3 -  ModBusTcp协议客户端实现
所以最好转成二进制是:0101 0110

二进制转十六进制

我们用二进制 0101 0110来演示,也就是上面十进制的86。
IoTClient开发3 -  ModBusTcp协议客户端实现

当然,你最好用计算器验证下
IoTClient开发3 -  ModBusTcp协议客户端实现

modbustcp协议介绍

我们在对进制转换进行复习过后,接下来讲modbustcp协议。
modbus协议是现在工控里面用的比较多比较通用的一种协议,什么可靠啊、简单啊等等一些优点就不说了,直接入正题。
modbus分为rtu、ascii、tcp三种方式进行通信,今天我们只讲tcp。
在modbus里面有站号、功能码、寄存器地址等概念。

  • 站号:多设备的标识号
  • 功能码:一些功能的标识号
    功能码详解:
01:读线圈
02:读离散量
03:读保持寄存器(每个寄存器含有两个字节)
04:读输入寄存器
05:写单个线圈
06:写单个寄存器
15:用于写多个线圈
16:写多个寄存器

modbustcp报文分析

协议的理解和实现主要就是要对协议报文理解。(注意:以下报文数据都是十六进制)

数据【读取-请求报文】:19 b2 00 00 00 06 02 03 00 04 00 01

  • 19 b2 是客户端发的检验信息,随意定义。
  • 00 00 代表是基于tcp/ip协议的modbus
  • 00 06 标识后面还有多长的字节
  • 02 表示站号地址
  • 03 为功能码(读保持寄存器)
  • 00 04 为寄存器地址
  • 00 01 为寄存器的长度(寄存器个数)

数据【读取-响应报文】(分两次获取)

第一次获取前八个字节(map报文头):19 b2 00 00 00 05 02 03 02 00 20

  • 19 b2 检验验证信息(复制的客户端发的,配件检验)
  • 00 00 代表是基于tcp/ip协议的modbus(复制的客户端发的)
  • 00 05 为当前位置到最后的长度
  • 02 表示站号地址(复制的客户端发的)
  • 03 为功能码(复制的客户端发的)

第二次获取的报文:02 00 20

  • 02 字节个数
  • 00 20 响应的数据

数据【写入-请求报文】:19 b2 00 00 00 09 02 10 00 04 00 01 02 00 20

  • 19 b2 是客户端发的检验信息,随意定义。
  • 00 00 代表是基于tcp/ip协议的modbus
  • 00 09 从本字节下一个到最后
  • 02 站号
  • 10 功能码(转十进制就是16)
  • 00 04 寄存器地址
  • 00 01 寄存器的长度(寄存器个数)
  • 02 写字节的个数
  • 00 20 要写入的值(转十进制为32)

数据【写入-响应报文】:19 b2 00 00 00 06 02 10 00 04 00 01

和请求报文的区别

  • 没有了请求报文的数据值
  • 00 09 变成了00 06 因为报文长度变了
  • 其他的报文意义和请求报文一致

modbustcp对寄存器的读取

有了上面的三个报文做参考,我们就可以用socket来实现modbustcp协议了。其实协议就是按照报文的规定来,也没有想的那么复杂,和我们前面实现的聊天通讯软件区别不大。
第一步,我们先实现数据读取报文的组装:

/// <summary>
/// 获取读取命令(此方法传入参数后就可以得到类似19 b2 00 00 00 06 02 03 00 04 00 01这样的请求报文)
/// </summary>
/// <param name="address">寄存器起始地址</param>
/// <param name="stationnumber">站号</param>
/// <param name="functioncode">功能码</param>
/// <param name="length">读取长度</param>
/// <returns></returns>
public static byte[] getreadcommand(ushort address, byte stationnumber, byte functioncode, ushort length)
{
    byte[] buffer = new byte[12];
    buffer[0] = 0x19;
    buffer[1] = 0xb2;//client发出的检验信息
    buffer[2] = 0x00;
    buffer[3] = 0x00;//表示tcp/ip 的协议的modbus的协议
    buffer[4] = 0x00;
    buffer[5] = 0x06;//表示的是该字节以后的字节长度

    buffer[6] = stationnumber;  //站号
    buffer[7] = functioncode;   //功能码
    buffer[8] = bitconverter.getbytes(address)[1];
    buffer[9] = bitconverter.getbytes(address)[0];//寄存器地址
    buffer[10] = bitconverter.getbytes(length)[1];
    buffer[11] = bitconverter.getbytes(length)[0];//表示request 寄存器的长度(寄存器个数)
    return buffer;
}

第二步,就是建立我们的socket连接,并发送请求报文

//1 创建socket
var socket = new socket(addressfamily.internetwork, sockettype.stream, protocoltype.tcp);

//2 建立连接
socket.connect(new ipendpoint(ipaddress.parse(ip), 端口));

//3 获取命令[组装请求报文](寄存器起始地址:4、站号:2、功能码:3、读取寄存器长度:1)
byte[] command = getreadcommand(4, 2, 3, 1);

//4 发送命令
socket.send(command);

第三步,解析响应报文,得到数据值

//5 读取响应
byte[] buffer1 = new byte[8];//先读取前面八个字节(map报文头)
socket.receive(buffer1, 0, buffer1.length, socketflags.none);

//5.1 获取将要读取的数据长度
int length = buffer1[4] * 256 + buffer1[5] - 2;//减2是因为这个长度数据包括了单元标识符和功能码,占两个字节

//5.2 读取数据
byte[] buffer2 = new byte[length];
var readlength2 = socket.receive(buffer2, 0, buffer2.length, socketflags.none);

byte[] buffer3 = new byte[readlength2 - 1];
//5.3  过滤第一个字节(第一个字节代表数据的字节个数)
array.copy(buffer2, 1, buffer3, 0, buffer3.length);
var buffer3reverse = buffer3.reverse().toarray();
var value = bitconverter.toint16(buffer3reverse, 0);

//6 关闭连接
socket.shutdown(socketshutdown.both);
socket.close();

modbustcp对寄存器的写入

对于数据写入就更简单了。
第一步,组装请求报文

/// <summary>
/// 获取写入命令
/// </summary>
/// <param name="address">寄存器地址</param>
/// <param name="values"></param>
/// <param name="stationnumber">站号</param>
/// <param name="functioncode">功能码</param>
/// <returns></returns>
public static byte[] getwritecommand(ushort address, byte[] values, byte stationnumber, byte functioncode)
{
    byte[] buffer = new byte[13 + values.length];
    buffer[0] = 0x19;
    buffer[1] = 0xb2;//检验信息,用来验证response是否串数据了           
    buffer[4] = bitconverter.getbytes(7 + values.length)[1];
    buffer[5] = bitconverter.getbytes(7 + values.length)[0];//表示的是header handle后面还有多长的字节

    buffer[6] = stationnumber; //站号
    buffer[7] = functioncode;  //功能码
    buffer[8] = bitconverter.getbytes(address)[1];
    buffer[9] = bitconverter.getbytes(address)[0];//寄存器地址
    buffer[10] = (byte)(values.length / 2 / 256);
    buffer[11] = (byte)(values.length / 2 % 256);//写寄存器数量(除2是一个寄存器两个字节,寄存器16位。除以256是byte最大存储255。)              
    buffer[12] = (byte)(values.length);          //写字节的个数
    values.copyto(buffer, 13);                   //把目标值附加到数组后面
    return buffer;
}

第二步,建立socket连接,并发送报文

//1 创建socket
var socket = new socket(addressfamily.internetwork, sockettype.stream, protocoltype.tcp);

//2 建立连接
socket.connect(new ipendpoint(ipaddress.parse(ip), 端口));

//值的转换
short value = 32;
var values = bitconverter.getbytes(value).reverse().toarray();

//3 获取并发送命令(寄存器起始地址、站号、功能码)
var command = getwritecommand(4, values, 2, 16);
socket.send(command);

//4 关闭连接
socket.shutdown(socketshutdown.both);
socket.close();

结束

上一篇:

下一篇: