詳解物聯網Modbus通訊協議

語言: CN / TW / HK

摘要:Modbus是當前非常流行的一種通訊協議。

本文分享自華為雲社群《一文搞懂物聯網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乙太網介面。

通訊示意圖如下:

一般來說,Modbus通訊協議原理具備如下的特徵:

  • 一次只有一個主機(Master)連線到網路
  • 只有主裝置(Master)可以啟動通訊並向從裝置(Slave)傳送請求
  • 主裝置(Master)可以使用其特定地址單獨定址每個從裝置(Slave),也可以使用地址0(廣播)同時定址所有從裝置(Slave)
  • 從裝置(Slave)只能向主裝置(Master)傳送回覆
  • 從裝置(Slave)無法啟動與主裝置(Master)或其他從裝置(Slave)的通訊

Modbus協議可使用2種通訊模式交換資訊:

  • 單播模式
  • 廣播模式

不管是請求報文還是答覆報文,資料結構如下:

即報文(幀資料)由4部分構成:地址(Slave Number)+功能碼(Function Codes)+資料(Data)+校驗(Check) 。其中的地址代表從裝置的ID地址,作為定址的資訊。功能碼錶示當前的請求執行具體什麼操作,比如讀還是寫。資料代表需要通訊的業務資料,可以根據實際情況來確定。最後一個校驗則是驗證資料是否有誤。其中的功能碼說明如下:

比如功能碼為03代表讀取當前暫存器內一個或多個二進位制值,而06代表將二進位制值寫入單一暫存器。為了模擬Modbus通訊協議過程,這裡可以藉助模擬軟體:

  • Modbus Poll(Master)
  • Modbus Slave

具體的安裝過程這裡不再贅述。首先這裡需要模擬一個物聯網感測器裝置,這裡用Modbus Slave來定義,首先開啟此軟體,並定義一個ID為1的裝置:

此功能碼為03。另外,設定連線引數,示例介面如下:

下面再用Modbus Poll軟體來模擬主機,來獲取從裝置的資料。首先定義一個讀寫報文。

然後再定義一個連線資訊:

注意:兩個COM口要使用不同的名稱。

成功建立通訊後,通訊的報文格式如下:

Tx代表請求報文,而Rx代表答覆報文。

3 ModBus Java實現

下面介紹一下如何用Java來實現一個Modbus TCP通訊。這裡Java框架採用Spring Boot,首先需要引入Modbus庫。Maven依賴庫的pom.xml定義如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!--Modbus Master -->
		<dependency>
			<groupId>com.digitalpetri.modbus</groupId>
			<artifactId>modbus-master-tcp</artifactId>
			<version>1.2.0</version>
		</dependency>
		<!--Modbus Slave -->
		<dependency>
			<groupId>com.digitalpetri.modbus</groupId>
			<artifactId>modbus-slave-tcp</artifactId>
			<version>1.2.0</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

其中關於Modbus庫的依賴項為com.digitalpetri.modbus,它分modbus-master-tcpmodbus-slave-tcp 。此示例用Java專案模擬了一個Modbus Master端,用Modbus Slave軟體模擬了Slave端,通訊連線方式選擇Modbus TCP/IP方式,IP地址和埠限定了Slave裝置。示意圖如下:

由於此處連線方式採用Modbus TCP方式,因此在Modbus Slave的連線配置的地方,需要調整連線方式,示意截圖如下:

Java核心程式碼如下:

package com.example.demo.modbus;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.digitalpetri.modbus.codec.Modbus;
import com.digitalpetri.modbus.master.ModbusTcpMaster;
import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
import com.digitalpetri.modbus.requests.ReadHoldingRegistersRequest;
import com.digitalpetri.modbus.responses.ReadHoldingRegistersResponse;
import io.netty.buffer.ByteBufUtil;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MBMaster {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    private final List<ModbusTcpMaster> masters = new CopyOnWriteArrayList<>();
    private volatile boolean started = false;

    private final int nMasters ;
    private final int nRequests ;

    public MBMaster(int nMasters, int nRequests) {
        if (nMasters < 1){
            nMasters = 1;
        }
        if (nRequests < 1){
            nMasters = 1;
        }
        this.nMasters = nMasters;
        this.nRequests = nRequests;
    }
   //啟動
    public void start() {
        started = true;

        ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder("127.0.0.1")
                .setPort(50201)
                .setInstanceId("S-001")
                .build();

        new Thread(() -> {
            while (started) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                double mean = 0.0;
                int mcounter = 0;

                for (ModbusTcpMaster master : masters) {
                    mean += master.getResponseTimer().getMeanRate();
                    mcounter += master.getResponseTimer().getCount();
                }

                logger.info("Mean Rate={}, counter={}", mean, mcounter);
            }
        }).start();

        for (int i = 0; i < nMasters; i++) {
            ModbusTcpMaster master = new ModbusTcpMaster(config);
            master.connect();
            masters.add(master);
            for (int j = 0; j < nRequests; j++) {
                sendAndReceive(master);
            }
        }
    }
   //傳送請求
    private void sendAndReceive(ModbusTcpMaster master) {
        if (!started) return;

        //10個暫存器
        CompletableFuture<ReadHoldingRegistersResponse> future =
                master.sendRequest(new ReadHoldingRegistersRequest(0, 10), 0);

       //響應處理
        future.whenCompleteAsync((response, ex) -> {
            if (response != null) {
                //System.out.println("Response: " + ByteBufUtil.hexDump(response.getRegisters()));
                System.out.println("Response: " + ByteBufUtil.prettyHexDump(response.getRegisters()));
                //[00 31 00 46 00 00 00 b3 00 00 00 00 00 00 00 00]
                byte[] bytes = ByteBufUtil.getBytes(response.getRegisters());
                System.out.println("Response Value = " + bytes[3]);//根據業務情況獲取暫存器數值
                ReferenceCountUtil.release(response);
            } else {
                logger.error("Error Msg ={}", ex.getMessage(), ex);
            }
            scheduler.schedule(() -> sendAndReceive(master), 1, TimeUnit.SECONDS);
        }, Modbus.sharedExecutor());
    }

    public void stop() {
        started = false;
        masters.forEach(ModbusTcpMaster::disconnect);
        masters.clear();
    }

    public static void main(String[] args) {
       //啟動Client進行資料互動
        new MBMaster(1, 1).start();
    }
}

首先,需要用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開啟連線後,設定介面如下所示:

執行Java程式。控制檯輸出示例如下所示:

Response Value = 16
Response:          +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 08 00 11 00 1b 00 00 00 00 00 00 00 00 00 00 |................|
|00000010| 00 00 00 00                                     |....            |
+--------+-------------------------------------------------+----------------+
Response Value = 17
Response:          +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 09 00 12 00 1c 00 00 00 00 00 00 00 00 00 00 |................|
|00000010| 00 00 00 00                                     |....            |
+--------+-------------------------------------------------+----------------+
Response Value = 18

由此,可以知曉,返回的報文中在0到f這15個位置中,有需要的業務資料,具體獲取哪個位置,取決於Slave裝置的設定。

 

點選關注,第一時間瞭解華為雲新鮮技術~