詳解物聯網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通訊協議的官網為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、ModBusJava實現

下面介紹一下如何用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 http://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(newReadHoldingRegistersRequest(0, 10), 0)對象來查詢數據,此功能碼為03,寄存器數據為10。在Modbus Slave開啟連接後,設置界面如下所示:

運行Java程序。控制枱輸出示例如下所示:

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

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