一、简介
Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的Modbus协议:ModbusTCP。
Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。
标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。
二、ModbusTCP数据帧
ModbusTCP的数据帧可分为两部分:MBAP+PDU。
2.1 报文头MBAP
MBAP为报文头,长度为7字节:
事务处理标识 | 协议标识 | 长度 | 单元标识符 |
---|
2字节 | 2字节 | 2字节 | 1字节 |
内容 | 解释 |
---|
事务处理标识 | 可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。 | 协议标识符 | 00 00表示ModbusTCP协议。 | 长度 | 表示接下来的数据长度,单位为字节。 | 单元标识符 | 可以理解为设备地址。 |
2.2 帧结构PDU
PDU由功能码+数据组成。功能码为1字节,数据长度不定,由具体功能决定。
2.2.1 功能码
Modbus的操作对象有四种:线圈、离散输入、保持寄存器、输入寄存器。
对象 | 含义 |
---|
线圈 | PLC的输出位,开关量,在Modbus中可读可写 | 离散量 | PLC的输入位,开关量,在Modbus中只读 | 输入寄存器 | PLC中只能从模拟量输入端改变的寄存器,在Modbus中只读 | 保持寄存器 | PLC中用于输出模拟量信号的寄存器,在Modbus中可读可写 |
根据对象的不同,Modbus的功能码有:
功能码 | 含义 |
---|
0x01 | 读线圈 | 0x05 | 写单个线圈 | 0x0F | 写多个线圈 | 0x02 | 读离散量输入 | 0x04 | 读输入寄存器 | 0x03 | 读保持寄存器 | 0x06 | 写单个保持寄存器 | 0x10 | 写多个保持寄存器 |
三、ModbusTCP通信
3.1 通信方式
Modbus设备可分为主站(poll)和从站(slave)。主站只有一个,从站有多个,主站向各从站发送请求帧,从站给予响应。在使用TCP通信时,主站为client端,主动建立连接;从站为server端,等待连接。
- 主站请求:功能码+数据
- 从站正常响应:请求功能码+响应数据
- 从站异常响应:异常功能码+异常码,其中异常功能码即将请求功能码的最高有效位置1,异常码指示差错类型
- 注意:需要超时管理机制,避免无期限的等待可能不出现的应答
IANA(Internet Assigned Numbers Authority,互联网编号分配管理机构)给Modbus协议赋予TCP端口号为502,这是目前在仪表与自动化行业中唯一分配到的端口号。
3.2 通信过程
- connect 建立TCP连接
- 准备Modbus报文
- 使用send命令发送报文
- 在同一连接下等待应答
- 使用recv命令读取报文,完成一次数据交换
- 通信任务结束时,关闭TCP连接
3.3 仿真软件
- 我是用的仿真软件是Modbus slave,可以实现Modbus RTU、TCP、串口仿真等。
- Modbus slave 作为服务器端处理请求,客户端用java代替。
- 使用软件时,需要指定功能码,在Modbus slave的Setup->slave definition中指定。
– slave ID:从站编号(事务标识符) – function:功能码,0x01对应线圈操作,0x02对应离散量操作,0x03对应保持寄存器操作,0x04对应输入寄存器操作 – address:开始地址 – quantity:寄存器/线圈/离散量 的数量
参考:
https://www.cnblogs.com/ioufev/p/10831289.html? Java实现modbustcp通信
https://blog.csdn.net/zwxuse251/article/details/24154951?数据报文结构详细
https://blog.csdn.net/lakerszhy/article/details/68927178?locationNum=4&fps=1? Modbus功能码
3.4 概念
原文链接:https://wenku.baidu.com/view/55595d0690c69ec3d5bb75ed.html
1.开关量:
一般指的是触点的“开”与“关”的状态,一般在计算机设备中也会用“0”或“1”来表示开关量的状态。开关量分为有源开关量信号和无源开关量信号,有源开关量信号指的是“开”与“关”的状态是带电源的信号,专业叫法为跃阶信号,可以理解为脉冲量,一般的都有220VAC,?110VAC,24VDC,12VDC等信号,无源开关量信号指的是“开”和“关”的状态时不带电源的信号,一般又称之为干接点。电阻测试法为电阻0或无穷大。
2.数字量:
很多人会将数字量与开关量混淆,也将其与模拟量混淆。数字量在时间和数量上都是离散的物理量,其表示的信号则为数字信号。数字量是由0和1组成的信号,经过编码形成有规律的信号,量化后的模拟量就是数字量。
3.模拟量:
模拟量的概念与数字量相对应,但是经过量化之后又可以转化为数字量。模拟量是在时间和数量上都是连续的物理量,其表示的信号则为模拟信号。模拟量在连续的变化过程中任何一个取值都是一个具体有意义的物理量,如温度,电压,电流等。
4.离散量:
离散量是将模拟量离散化之后得到的物理量。即任何仪器设备对于模拟量都不可能有个完全精确的表示,因为他们都有一个采样周期,在该采样周期内,其物理量的数值都是不变的,而实际上的模拟量则是变化的。这样就将模拟量离散化,成为了离散量。
5.脉冲量:
脉冲量就是瞬间电压或电流由某一值跃变到另一值的信号量。在量化后,其变化持续有规律就是数字量,如果其由0变成某一固定值并保持不变,其就是开关量。
四、仿真软件的使用
多处参考取自:https://www.cnblogs.com/ioufev/p/10831289.html
验证4个常用功能码,仿真软件上面有F=01,F=02,F=03、F=04来显示
- 0x01:读线圈
- 0x02:读离散量输入
- 0x03:读保持寄存器
- 0x04:读输入寄存器
代码参数的理解
- saveid:看资料"从站在modbus总线上可以有多个",仿真软件就能模拟一个从站,就是ID=1,当然可以修改成ID=2
- 功能码:4个功能码,对应写4个方法,,仿真软件上的F=1,或者F=2,3,4
- addr:一开始看代码4个方法
addr 都是从0开始,是否重复?答案是:4个功能码表示4个区域或者设备,addr表示各自区域的地址编号。
仿真软件激活后选择Connection --> Connection Setup?选择TCP模式,端口是固定的502
4.1 地址类型
?操作:新建四个不同功能码的窗口,然后运行代码,修改仿真软件上的值。
4.2 数据类型?
功能码01:false/true? 0/1
?功能码02:false/true? 0/1
功能码03:多种类型
signed:有符号 unsigned:无符号 hex:十六进制 binary:二进制
big-endian:大端,将高序字节存储在起始地址(高位编址) little-endian:小端,将低序字节存储在起始地址(低位编址)
swap:交换
?注意:双击第一个地址输入数据,会提示输入数据的类型,32位数据占2个地址,所以下一个地址是--
功能码04:多种类型?
注意:?
数据类型 WORD:无符号双字节整型(字,16位)? ? ?取值:0~65535(非负)
五、案例
5.1 引入依赖
Modbus4j开源库:Serotonin Software用Java编写的Modbus协议的高性能且易于使用的实现。支持ASCII,RTU,TCP和UDP传输作为从站或主站,自动请求分区,响应数据类型解析和节点扫描。
Modbus的github地址
公共 Maven 存储库现已可用,最新构建将此添加到您的 pom 中.xml
<!-- 若想引用modbus4j需要引入下列repository id:ias-snapshots id:ias-releases 两个 ,使用默认仓库下载,不要使用阿里云仓库-->
<repositories>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>ias-snapshots</id>
<name>Infinite Automation Snapshot Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-snapshot/</url>
</repository>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>ias-releases</id>
<name>Infinite Automation Release Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-release/</url>
</repository>
</repositories>
<!-- 依赖信息是: -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
5.2 编写Modbus4j工具类
Modbus4jUtils.java
package com.ioufev;
import com.serotonin.modbus4j.BatchRead;
import com.serotonin.modbus4j.BatchResults;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.locator.BaseLocator;
/**
* modbus通讯工具类,采用modbus4j实现
*
*/
public class Modbus4jUtils {
/**
* 工厂。
*/
static ModbusFactory modbusFactory;
static {
if (modbusFactory == null) {
modbusFactory = new ModbusFactory();
}
}
/**
* 获取master
*
* @return
* @throws ModbusInitException
*/
public static ModbusMaster getMaster() throws ModbusInitException {
IpParameters params = new IpParameters();
params.setHost("127.0.0.1");
params.setPort(502);
//
// modbusFactory.createRtuMaster(wapper); //RTU 协议
// modbusFactory.createUdpMaster(params);//UDP 协议
// modbusFactory.createAsciiMaster(wrapper);//ASCII 协议
ModbusMaster master = modbusFactory.createTcpMaster(params, false);// TCP 协议
master.init();
return master;
}
/**
* 读取[01 Coil Status 0x]类型 开关数据
*
* @param slaveId
* slaveId
* @param offset
* 位置
* @return 读取值
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
* @throws ModbusInitException
* 异常
*/
public static Boolean readCoilStatus(int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 01 Coil Status
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, offset);
Boolean value = getMaster().getValue(loc);
return value;
}
/**
* 读取[02 Input Status 1x]类型 开关数据
*
* @param slaveId
* @param offset
* @return
* @throws ModbusTransportException
* @throws ErrorResponseException
* @throws ModbusInitException
*/
public static Boolean readInputStatus(int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 02 Input Status
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, offset);
Boolean value = getMaster().getValue(loc);
return value;
}
/**
* 读取[03 Holding Register类型 2x]模拟量数据
*
* @param slaveId
* slave Id
* @param offset
* 位置
* @param dataType
* 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
* @throws ModbusInitException
* 异常
*/
public static Number readHoldingRegister(int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 03 Holding Register类型数据读取
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
Number value = getMaster().getValue(loc);
return value;
}
/**
* 读取[04 Input Registers 3x]类型 模拟量数据
*
* @param slaveId
* slaveId
* @param offset
* 位置
* @param dataType
* 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return 返回结果
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
* @throws ModbusInitException
* 异常
*/
public static Number readInputRegisters(int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 04 Input Registers类型数据读取
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, offset, dataType);
Number value = getMaster().getValue(loc);
return value;
}
}
5.3 测试?
测试代码:
public static void main(String[] args) {
try {
// // 01测试
Boolean v011 = readCoilStatus(1, 0);
Boolean v012 = readCoilStatus(1, 1);
Boolean v013 = readCoilStatus(1, 6);
System.out.println("v011:" + v011);
System.out.println("v012:" + v012);
System.out.println("v013:" + v013);
// 02测试
Boolean v021 = readInputStatus(1, 0);
Boolean v022 = readInputStatus(1, 1);
Boolean v023 = readInputStatus(1, 2);
System.out.println("v021:" + v021);
System.out.println("v022:" + v022);
System.out.println("v023:" + v023);
//
// 03测试
Number v031 = readHoldingRegister(1, 0, DataType.TWO_BYTE_INT_SIGNED);// 注意,float
Number v032 = readHoldingRegister(1, 1, DataType.TWO_BYTE_INT_SIGNED);// 同上
System.out.println("v031:" + v031);
System.out.println("v032:" + v032);
// 04测试
Number v041 = readInputRegisters(1, 0, DataType.FOUR_BYTE_FLOAT);//
Number v042 = readInputRegisters(1, 2, DataType.FOUR_BYTE_FLOAT);//
System.out.println("v041:" + v041);
System.out.println("v042:" + v042);
} catch (Exception e) {
e.printStackTrace();
}
}
5.4 代码解释
5.5 仿真软件Modbus slave信息
?5.6 运行结果
补充:
?举例:
功能码 | 监测项 | 规约地址 | 数据类型 | 02 | 直流输入过高停机 | 1080H | BOOL |
功能码 | 描述 | PLC地址 | 寄存器地址 | 位/字操作 | 操作数量 |
---|
01H | 读线圈寄存器 | 00001-09999 | 0000H-FFFFH | 位操作 | 单个或多个 | 02H | 读离散输入寄存器 | 10001-19999 | 0000H-FFFFH | 位操作 | 单个或多个 | 03H | 读保持寄存器 | 40001-49999 | 0000H-FFFFH | 字操作 | 单个或多个 | 04H | 读输入寄存器 | 30001-39999 | 0000H-FFFFH | 字操作 | 单个或多个 | 05H | 写单个线圈寄存器 | 00001-09999 | 0000H-FFFFH | 位操作 | 单个 | 06H | 写单个保持寄存器 | 40001-49999 | 0000H-FFFFH | 字操作 | 单个 | 0FH | 写多个线圈寄存器 | 00001-09999 | 0000H-FFFFH | 位操作 | 多个 | 10H | 写多个保持寄存器 | 40001-49999 | 0000H-FFFFH | 字操作 | 多个 |
直流输入过高停机监测项的规约地址为1080H
? ? ? ? ? ? ? ? 转10进制:1080H? ————>? ?4224
? ? ? ? ? ? ? ? 02功能码的PLC起始地址为:10001 ~ 19999
? ? ? ? ? ? ? ??读离散输入寄存器值的地址为:PLC起始地址+规约地址? 10001 + 4224 = 14225
代码读离散输入寄存器的值:
public static void main(String[] args) {
try {
// 读离散输入寄存器
//参数1:slaveID 参数2:起始地址
Boolean v021 = readInputStatus(1, 14225);
System.out.println("v021:" + v021);
} catch (Exception e) {
e.printStackTrace();
}
}
|