本文分享自华为云社区《一文搞懂物联网Modbus通讯协议丨【拜托了,物联网!】》,作者: jackwangcumt。

  1 概述

随着IT技术的快速发展,当前已经步入了智能化时代,其中的物联网技术将在未来占据越来越重要的地位。根据百度百科的定义,物联网(Internet of things,简称IOT )即“万物相连的互联网”,是互联网基础上的延伸和扩展的网络,物联网将各种信息有机的结合起来,实现任何时间、任何地点,人、机、物的互联互通。物联网从技术上来说,很重要的核心是通讯协议,即如何按约定的通讯协议,把机、物和人与互联网相连接,进行信息通信,以实现对人、机和物的智能化识别、定位、跟踪、监控和管理的一种网络。
一般来说,常见的物联网通讯协议众多,如蓝牙、Zigbee、WiFi、ModBus、PROFINET、EtherCAT、蜂窝等。而在众多的物联网通讯协议中,Modbus是当前非常流行的一种通讯协议。它一种串行通信协议,是Modicon公司于1979年为使用可编程逻辑控制器(PLC)通信而制定的,可以说,它已经成为工业领域通信协议的业界标准。其优势如下:

  • 免费无版税限制
  • 容易部署
  • 灵活限制少
  
2 ModBus协议概述
Modbus通讯协议使用请求-应答机制在主(Master)(客户端Client)和从(Slave)(服务器Server)之间交换信息。Client-Server原理是通信协议的模型,其中一个主设备控制多个从设备。这里需要注意的是:Modbus通讯协议当中的Master对应Client,而Slave对应Server。Modbus通讯协议的官网为http://www.modbus.org。目前官网组织已经建议将Master-Slave替换为Client-Server。从协议类型上可以分为:Modbus-RTU(ASCII)、Modbus-TCP和Modbus-Plus。本文主要介绍Modbus-RTU(ASCII)的通讯协议原理。标准的Modbus协议物理层接口有RS232、RS422、RS485以太网接口。
通讯示意图如下:
v2-6f2d84ad1b08fdc985d9f1ab0e62b55a_720w.jpg
一般来说,Modbus通信协议原理具备如下的特征:

  • 一次只有一个主机(Master)连接到网络
  • 只有主设备(Master)可以启动通信并向从设备(Slave)发送请求
  • 主设备(Master)可以使用其特定地址单独寻址每个从设备(Slave),也可以使用地址0(广播)同时寻址所有从设备(Slave)
  • 从设备(Slave)只能向主设备(Master)发送回复
  • 从设备(Slave)无法启动与主设备(Master)或其他从设备(Slave)的通信
Modbus协议可使用2种通信模式交换信息:

  • 单播模式
  • 广播模式
不管是请求报文还是答复报文,数据结构如下:
v2-a584f320fcdb8b9bdf27c251428fc0b9_720w.jpg
即报文(帧数据)由4部分构成:地址(Slave Number)+功能码(Function Codes)+数据(Data)+校验(Check) 。其中的地址代表从设备的ID地址,作为寻址的信息。功能码表示当前的请求执行具体什么操作,比如读还是写。数据代表需要通讯的业务数据,可以根据实际情况来确定。最后一个校验则是验证数据是否有误。其中的功能码说明如下:
v2-377eae2e5e97bf7af7a161751c8ff1eb_720w.jpg
比如功能码为03代表读取当前寄存器内一个或多个二进制值,而06代表将二进制值写入单一寄存器。为了模拟Modbus通讯协议过程,这里可以借助模拟软件:

  • Modbus Poll(Master)
  • Modbus Slave
具体的安装过程这里不再赘述。首先这里需要模拟一个物联网传感器设备,这里用Modbus Slave来定义,首先打开此软件,并定义一个ID为1的设备:
v2-c35eb2935e2127a45d7f1e2787072d42_720w.jpg
此功能码为03。另外,设置连接参数,示例界面如下:
v2-c23f248ba7705a0a8d0e27996ac1041e_720w.jpg
下面再用Modbus Poll软件来模拟主机,来获取从设备的数据。首先定义一个读写报文。
v2-00e474e116de3e362b99e51cf4cec020_720w.jpg
然后再定义一个连接信息:
v2-feea7f465d44e87e64c76419c289474c_720w.jpg
注意:两个COM口要使用不同的名称。
成功建立通讯后,通信的报文格式如下:
v2-ab83f3638dcc59882ee2749674147c67_720w.jpg
Tx代表请求报文,而Rx代表答复报文。

  3 ModBus Java实现
下面介绍一下如何用Java来实现一个Modbus TCP通信。这里Java框架采用Spring Boot,首先需要引入Modbus库。Maven依赖库的pom.xml定义如下:
  
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.         <modelVersion>4.0.0</modelVersion>
  5.         <parent>
  6.                 <groupId>org.springframework.boot</groupId>
  7.                 <artifactId>spring-boot-starter-parent</artifactId>
  8.                 <version>2.5.5</version>
  9.                 <relativePath/> <!-- lookup parent from repository -->
  10.         </parent>
  11.         <groupId>com.example</groupId>
  12.         <artifactId>demo</artifactId>
  13.         <version>0.0.1-SNAPSHOT</version>
  14.         <name>demo</name>
  15.         <description>Demo project for Spring Boot</description>
  16.         <properties>
  17.                 <java.version>1.8</java.version>
  18.         </properties>
  19.         <dependencies>
  20.                 <dependency>
  21.                         <groupId>org.springframework.boot</groupId>
  22.                         <artifactId>spring-boot-starter-web</artifactId>
  23.                 </dependency>
  24.                 <dependency>
  25.                         <groupId>mysql</groupId>
  26.                         <artifactId>mysql-connector-java</artifactId>
  27.                         <scope>runtime</scope>
  28.                 </dependency>
  29.                 <!--Modbus Master -->
  30.                 <dependency>
  31.                         <groupId>com.digitalpetri.modbus</groupId>
  32.                         <artifactId>modbus-master-tcp</artifactId>
  33.                         <version>1.2.0</version>
  34.                 </dependency>
  35.                 <!--Modbus Slave -->
  36.                 <dependency>
  37.                         <groupId>com.digitalpetri.modbus</groupId>
  38.                         <artifactId>modbus-slave-tcp</artifactId>
  39.                         <version>1.2.0</version>
  40.                 </dependency>
  41.                 <dependency>
  42.                         <groupId>org.springframework.boot</groupId>
  43.                         <artifactId>spring-boot-starter-test</artifactId>
  44.                         <scope>test</scope>
  45.                 </dependency>
  46.         </dependencies>
  47.         <build>
  48.                 <plugins>
  49.                         <plugin>
  50.                                 <groupId>org.springframework.boot</groupId>
  51.                                 <artifactId>spring-boot-maven-plugin</artifactId>
  52.                         </plugin>
  53.                 </plugins>
  54.         </build>
  55. </project>

其中关于Modbus库的依赖项为com.digitalpetri.modbus,它分modbus-master-tcpmodbus-slave-tcp 。此示例用Java项目模拟了一个Modbus Master端,用Modbus Slave软件模拟了Slave端,通信连接方式选择Modbus TCP/IP方式,IP地址和端口限定了Slave设备。示意图如下:
v2-458f307a745c9b427ada2723a7656e77_720w.jpg
由于此处连接方式采用Modbus TCP方式,因此在Modbus Slave的连接配置的地方,需要调整连接方式,示意截图如下:
v2-edff4bbba8bf6bcd51d22dd694e1a767_720w.jpg
Java核心代码如下:
  
  1. package com.example.demo.modbus;
  2. import java.util.List;
  3. import java.util.Random;
  4. import java.util.concurrent.CompletableFuture;
  5. import java.util.concurrent.CopyOnWriteArrayList;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.ScheduledExecutorService;
  8. import java.util.concurrent.TimeUnit;
  9. import com.digitalpetri.modbus.codec.Modbus;
  10. import com.digitalpetri.modbus.master.ModbusTcpMaster;
  11. import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
  12. import com.digitalpetri.modbus.requests.ReadHoldingRegistersRequest;
  13. import com.digitalpetri.modbus.responses.ReadHoldingRegistersResponse;
  14. import io.netty.buffer.ByteBufUtil;
  15. import io.netty.util.ReferenceCountUtil;
  16. import org.slf4j.Logger;
  17. import org.slf4j.LoggerFactory;
  18. public class MBMaster {
  19.     private final Logger logger = LoggerFactory.getLogger(getClass());
  20.     private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
  21.     private final List<ModbusTcpMaster> masters = new CopyOnWriteArrayList<>();
  22.     private volatile boolean started = false;
  23.     private final int nMasters ;
  24.     private final int nRequests ;
  25.     public MBMaster(int nMasters, int nRequests) {
  26.         if (nMasters < 1){
  27.             nMasters = 1;
  28.         }
  29.         if (nRequests < 1){
  30.             nMasters = 1;
  31.         }
  32.         this.nMasters = nMasters;
  33.         this.nRequests = nRequests;
  34.     }
  35.    //启动
  36.     public void start() {
  37.         started = true;
  38.         ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder("127.0.0.1")
  39.                 .setPort(50201)
  40.                 .setInstanceId("S-001")
  41.                 .build();
  42.         new Thread(() -> {
  43.             while (started) {
  44.                 try {
  45.                     Thread.sleep(3000);
  46.                 } catch (InterruptedException e) {
  47.                     e.printStackTrace();
  48.                 }
  49.                 double mean = 0.0;
  50.                 int mcounter = 0;
  51.                 for (ModbusTcpMaster master : masters) {
  52.                     mean += master.getResponseTimer().getMeanRate();
  53.                     mcounter += master.getResponseTimer().getCount();
  54.                 }
  55.                 logger.info("Mean Rate={}, counter={}", mean, mcounter);
  56.             }
  57.         }).start();
  58.         for (int i = 0; i < nMasters; i++) {
  59.             ModbusTcpMaster master = new ModbusTcpMaster(config);
  60.             master.connect();
  61.             masters.add(master);
  62.             for (int j = 0; j < nRequests; j++) {
  63.                 sendAndReceive(master);
  64.             }
  65.         }
  66.     }
  67.    //发送请求
  68.     private void sendAndReceive(ModbusTcpMaster master) {
  69.         if (!started) return;
  70.         //10个寄存器
  71.         CompletableFuture<ReadHoldingRegistersResponse> future =
  72.                 master.sendRequest(new ReadHoldingRegistersRequest(0, 10), 0);
  73.        //响应处理
  74.         future.whenCompleteAsync((response, ex) -> {
  75.             if (response != null) {
  76.                 //System.out.println("Response: " + ByteBufUtil.hexDump(response.getRegisters()));
  77.                 System.out.println("Response: " + ByteBufUtil.prettyHexDump(response.getRegisters()));
  78.                 //[00 31 00 46 00 00 00 b3 00 00 00 00 00 00 00 00]
  79.                 byte[] bytes = ByteBufUtil.getBytes(response.getRegisters());
  80.                 System.out.println("Response Value = " + bytes[3]);//根据业务情况获取寄存器数值
  81.                 ReferenceCountUtil.release(response);
  82.             } else {
  83.                 logger.error("Error Msg ={}", ex.getMessage(), ex);
  84.             }
  85.             scheduler.schedule(() -> sendAndReceive(master), 1, TimeUnit.SECONDS);
  86.         }, Modbus.sharedExecutor());
  87.     }
  88.     public void stop() {
  89.         started = false;
  90.         masters.forEach(ModbusTcpMaster::disconnect);
  91.         masters.clear();
  92.     }
  93.     public static void main(String[] args) {
  94.        //启动Client进行数据交互
  95.         new MBMaster(1, 1).start();
  96.     }
  97. }

首先,需要用ModbusTcpMasterConfig来初始化一个Modbus Tcp Master 主机的配置信息,比如IP地址(127.0.0.1)和端口号(50201),此需要和Slave一致。其次,将配置信息config作为参数传递到ModbusTcpMaster对象中,构建一个 master实例。最后,用master.sendRequest(new ReadHoldingRegistersRequest(0, 10), 0)对象来查询数据,此功能码为03,寄存器数据为10。在Modbus Slave开启连接后,设置界面如下所示:
v2-ef94eef04ef7c6f6196006c797374c41_720w.jpg
运行Java程序。控制台输出示例如下所示:
  
  1. Response Value = 16
  2. Response:          +-------------------------------------------------+
  3.          |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
  4. +--------+-------------------------------------------------+----------------+
  5. |00000000| 00 08 00 11 00 1b 00 00 00 00 00 00 00 00 00 00 |................|
  6. |00000010| 00 00 00 00                                     |....            |
  7. +--------+-------------------------------------------------+----------------+
  8. Response Value = 17
  9. Response:          +-------------------------------------------------+
  10.          |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
  11. +--------+-------------------------------------------------+----------------+
  12. |00000000| 00 09 00 12 00 1c 00 00 00 00 00 00 00 00 00 00 |................|
  13. |00000010| 00 00 00 00                                     |....            |
  14. +--------+-------------------------------------------------+----------------+
  15. Response Value = 18

由此,可以知晓,返回的报文中在0到f这15个位置中,有需要的业务数据,具体获取哪个位置,取决于Slave设备的设置。