ModbusTCP协议与PLC交互

文章详细介绍了Modbus通信协议的存储区(线圈和寄存器)、功能码以及数据帧结构,包括ASCII、RTU、TCP三种报文类型。同时,展示了如何使用Java程序作为主机通过ModbusTCP与ModbusSlave模拟器进行通信,以及利用modbus-master-tcp库进行数据读取操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Modbus通信协议

Modbus通信协议由Modicon公司(现在的施耐德电气Schneider Electric)于1979年为可编程逻辑控制(即PLC)通信而发表,是工业电子设备之间常用的连接方式。
它包括ASCII、RTU、TCP三种报文类型,采用master/slave方式通信。

存储区-线圈和寄存器

为了更好存储不同的数据类型,modbus会将布尔和非布尔的数据分开存储。
布尔类型:0/1 或 false/true。

  • 线圈:存储布尔值
    从电气角度来看,在电气控制回路中,一般都是靠接触器或中间继电器来实现控制,接触器或中继最终靠的是线圈的得电和失电来控制触点闭合和断开,因此用线圈表示布尔量;
  • 寄存器:存储非布尔值
    用来暂时存放参与运算的数据和运算结果,具有接收数据、存放数据和输出数据的功能。而寄存器在计算机中,就是用来存储数据的,因此非布尔的数据放在寄存器里。

对于不同类型的存储区又分为只读和读写两种情况,故存在以下四种存储区:

存储区名称存储类型读写存储区代号绝对地址范围相对地址范围
输出线圈线圈读写0区000001-0655360-65535
输入线圈线圈只读1区100001-1655360-65535
输入寄存器寄存器只读3区300001-3655360-65535
保持寄存器寄存器读写4区400001-4655360-65535

存储区范围: 无论是什么存储区,都会有一个范围的限制,Modbus规定每个存储区的最大范围是65536

在实际使用中,一般用不了这么多地址,一般情况下,10000以内就已经足够使用了;因此,为了方便有一种短的地址模型,如下图所示(反之其它地址范围剩余部分则为:长地址模型

存储区名称存储区代号绝对地址范围相对地址范围
输出线圈0区00001-099990-9998
输入线圈1区10001-199990-9998
输入寄存器3区30001-399990-9998
保持寄存器4区40001-499990-9998

功能码

对于4个存储区的不同操作,modbus分别定义了不同的功能码用来区分不同行为。
主要的8个功能码如下。(Modbus规约中的功能码其实不止这8个,还有一些功能码是用于诊断或异常码,但是一般很少使用)

代码功能码中文名称英文名位操作/字操作操作数量
010x01读输出线圈READ COIL STATUS位操作单个或多个
020x02读输入线圈READ INPUT STATUS位操作单个或多个
030x03读保持寄存器READ HOLDING REGISTER字操作单个或多个
040x04读输入寄存器READ INPUT REGISTER字操作单个或多个
050x05写单个输出线圈WRITE SINGLE COIL位操作单个
060x06写单个保持寄存器WRITE SINGLE REGISTER字操作单个
150x0F写多个输出线圈WRITE MULTIPLE COIL位操作多个
160x10写多个保持寄存器WRITE MULTIPLE REGISTER字操作多个

Modbus通信数据帧

ModbusTCP的数据帧可分为两部分:MBAP+PDU。

MBAP报文头

MBAP为报文头,长度为7字节,组成如下。

内容长度解释
事务处理标识2字节可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。
协议标识符2字节00 00表示ModbusTCP协议。
长度2字节表示接下来的数据长度,单位为字节。
单元标识符1字节可以理解为设备地址。

PDU详细结构

PDU由功能码+数据组成。 功能码为1字节,数据长度不定,由具体功能决定。
一个字节(Byte)为8个Bit(位),8Bit用2个16进制直接就能表达出来,不管阅读还是存储都比其他进制要方便,故以下均用16进制表示。

例如读线圈:
请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节,H表示高位,L表示地位)
响应:MBAP 功能码 数据长度 数据
请求:在从站0x01中,读取开始地址为0x0002的线圈数据,读0x0008位,报文如下。
在这里插入图片描述

响应:数据长度为0x01个字节,数据为0x01,第一个线圈为ON,其余为OFF,报文如下。
在这里插入图片描述
报文解释:

  • 响应报文中的数据是【01】,是16进制的,读取的是8个线圈需要将16进制的【01】转为2进制得到【0000 0001】,故只有第一个线圈为ON,其余为OFF。
  • 响应报文中的数据长度是【01】,表示1个字节,指的是响应报文中的数据【01】的长度,【01】是2个16进制 = 1个字节。
  • 接下来的数据长度,以响应报文为例接下来的数据长度是【00 04】表示4个字节,指的是【00 04】后面【01 01 01 01】这段报文的长度,【01 01 01 01】是8个16进制 = 4个字节。

高位/低位解释
比如一个8位二进制0000 0001, 0000就是高四位,0001就是低四位。

字节序(大端/小端)
指在内存中以字节为单位的排列顺序,与cpu和操作系统有关,操作系统可以选择大小端,java默认读取按大端读取。
小端(Little-Endian) 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。(低低高高,低地址低字节,高地址高字节)
大端(Big-Endian) 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(低高高低,低地址高字节,高地址低字节)
比如将0x123456存入从0x0000开始的内存中,大端/小端分别会按如下顺序存储,读取时也要按相应顺序读取。
在这里插入图片描述

Java程序作为主机时通过ModbusTCP通信

Modbus Slave模拟器

modbus slave作为从机,java程序访问从机获取数据/写入数据。
仿真软件网址:https://modbustools.com/download.html

使用方式如下。

  1. 选择TCP模式,端口是固定的502
    在这里插入图片描述在这里插入图片描述

  2. 设置功能码等信息
    在这里插入图片描述
    在这里插入图片描述

  3. 设置值
    双击单元格,设置该地址的值
    在这里插入图片描述
    这里可以选择数据类型和格式
    Signed:有符号
    Unsigned:无符号
    Hex:十六进制
    Binary:二进制
    AB CD/CD AB等等表示字节序
    在这里插入图片描述

AB CD/CD AB等字节序解释
如:32位整数1,按不同的字节序会解析得到不用的结果
在这里插入图片描述

通过modbus-master-tcp实现通信

注入依赖

<dependency>
    <groupId>com.digitalpetri.modbus</groupId>
    <artifactId>modbus-master-tcp</artifactId>
    <version>1.2.0</version>
</dependency>

读取数据

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //建立连接
    ModbusTcpMasterConfig config = new ModbusTcpMasterConfig
            //ip地址
            .Builder("localhost")
            //端口
            .setPort(502).build();
    ModbusTcpMaster master = new ModbusTcpMaster(config);
    master.connect();

    //1、读取保持寄存器,开始地址:0,读取寄存器数量:2,slaveId:1
    CompletableFuture<ReadHoldingRegistersResponse> future1 = master.sendRequest(
            new ReadHoldingRegistersRequest(0, 2), 1);
    ReadHoldingRegistersResponse response1 = future1.get();
    ByteBuf buf1 = response1.getRegisters();
    //读取十六进制数据
    String hex = ByteBufUtil.hexDump(buf1);
    //读取字节流
    byte[] bytes = ByteBufUtil.getBytes(buf1);
    //十六进制转成整数
    Integer i = Integer.parseInt(hex, 16);
    //字节流转成字符串
    String s = new String(bytes);
    //释放
    ReferenceCountUtil.release(response1);

    //2、读取输入寄存器,开始地址:10,读取寄存器数量:2,slaveId:1
    CompletableFuture<ReadInputRegistersResponse> future2 = master.sendRequest(
            new ReadInputRegistersRequest(10, 2), 1);
    ReadInputRegistersResponse response2 = future2.get();
    ByteBuf buf2 = response2.getRegisters();
    ReferenceCountUtil.release(response2);

    //3、读取输出线圈,开始地址:20,读取线圈数量:1,slaveId:1
    CompletableFuture<ReadCoilsResponse> future3 = master.sendRequest(
            new ReadCoilsRequest(20, 1), 1);
    ReadCoilsResponse response3 = future3.get();
    ByteBuf buf3 = response3.getCoilStatus();
    ReferenceCountUtil.release(response3);

    //4、读取输入线圈,开始地址:30,读取线圈数量:1,slaveId:1
    CompletableFuture<ReadDiscreteInputsResponse> future4 = master.sendRequest(
            new ReadDiscreteInputsRequest(20, 1), 1);
    ReadDiscreteInputsResponse response4 = future4.get();
    ByteBuf buf4 = response4.getInputStatus();
    ReferenceCountUtil.release(response4);

    //释放连接
    master.disconnect();
    Modbus.releaseSharedResources();
}

以读取保持寄存器为例,从地址0开始读取2个寄存器,数据设置和读取结果如下。
在这里插入图片描述
在这里插入图片描述

字节流转二进制

/**
 * 字节流转二进制
 * @param bytes 字节流
 * @return 二进制字符串
 */
public String bytesToBinarySystem(byte[] bytes){
    StringBuilder result = new StringBuilder();
    for (byte b : bytes) {
        String binary = Integer.toBinaryString(b & 0xFF);
        result.append(String.format("%8s", binary).replace(' ', '0'));
    }
    return result.toString();
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值