摘要: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-tcp和modbus-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設備的設置。