Modbus上位机访问形式详解及代码示例

文章目录

    • [1. Modbus协议概述](#1. Modbus协议概述)
    • [2. Modbus通信方式分类](#2. Modbus通信方式分类)
    • [3. Modbus RTU访问形式及代码示例](#3. Modbus RTU访问形式及代码示例)
      • [3.1 RTU通信特点](#3.1 RTU通信特点)
      • [3.2 C#实现Modbus RTU通信示例](#实现Modbus RTU通信示例)
      • [3.3 Python实现Modbus RTU通信示例](#3.3 Python实现Modbus RTU通信示例)
    • [4. Modbus ASCII访问形式及代码示例](#4. Modbus ASCII访问形式及代码示例)
      • [4.1 ASCII通信特点](#4.1 ASCII通信特点)
      • [4.2 Java实现Modbus ASCII通信示例](#4.2 Java实现Modbus ASCII通信示例)
    • [5. Modbus TCP/IP访问形式及代码示例](#5. Modbus TCP/IP访问形式及代码示例)
      • [5.1 TCP/IP通信特点](#5.1 TCP/IP通信特点)
      • [5.2 C#实现Modbus TCP通信示例](#实现Modbus TCP通信示例)
      • [5.3 Python实现Modbus TCP通信示例](#5.3 Python实现Modbus TCP通信示例)
    • [6. Modbus over TCP访问形式](#6. Modbus over TCP访问形式)
      • [6.1 Modbus over TCP特点](#6.1 Modbus over TCP特点)
      • [6.2 C++实现Modbus over TCP示例](#6.2 C++实现Modbus over TCP示例)

1. Modbus协议概述

Modbus是一种串行通信协议,最初由Modicon公司(现为施耐德电气)于1979年发布,用于其可编程逻辑控制器(PLC)。Modbus协议已经成为工业领域事实上的标准通信协议,并且现在是工业电子设备之间常用的连接方式。

Modbus协议具有以下特点:

  • 主从式架构(客户端/服务器)
  • 支持多种电气接口(RS-232、RS-485、TCP/IP等)
  • 简单、开放、易于实现
  • 支持多种数据类型的访问

2. Modbus通信方式分类

Modbus通信主要分为以下几种形式:

  1. Modbus RTU(基于串行通信)
  2. Modbus ASCII(基于串行通信)
  3. Modbus TCP/IP(基于以太网)
  4. Modbus over TCP(Modbus TCP的变种)
  5. Modbus Plus(专用网络协议)

3. Modbus RTU访问形式及代码示例

Modbus RTU是最常用的Modbus实现方式,使用二进制编码和紧凑的二进制协议表示数据,采用主从通信方式,通常通过RS-485或RS-232接口实现。

3.1 RTU通信特点

  • 使用二进制数据表示
  • 采用CRC-16校验
  • 通信效率高
  • 典型波特率:9600、19200、38400、115200等

3.2 C#实现Modbus RTU通信示例

csharp 复制代码
using System;
using System.IO.Ports;
using System.Threading;

public class ModbusRTU
{
    private SerialPort _serialPort;
    private byte _slaveAddress;

    public ModbusRTU(string portName, int baudRate, byte slaveAddress)
    {
        _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        _slavePort.Handshake = Handshake.None;
        _serialPort.ReadTimeout = 500;
        _serialPort.WriteTimeout = 500;
        _slaveAddress = slaveAddress;
    }

    public bool Connect()
    {
        try
        {
            if (!_serialPort.IsOpen)
            {
                _serialPort.Open();
                return true;
            }
            return false;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"连接失败: {ex.Message}");
            return false;
        }
    }

    public void Disconnect()
    {
        if (_serialPort.IsOpen)
        {
            _serialPort.Close();
        }
    }

    private byte[] CalculateCRC(byte[] data)
    {
        ushort crc = 0xFFFF;
        
        for (int pos = 0; pos < data.Length; pos++)
        {
            crc ^= data[pos];
            
            for (int i = 8; i != 0; i--)
            {
                if ((crc & 0x0001) != 0)
                {
                    crc >>= 1;
                    crc ^= 0xA001;
                }
                else
                {
                    crc >>= 1;
                }
            }
        }
        
        return new byte[] { (byte)(crc & 0xFF), (byte)((crc >> 8) & 0xFF) };
    }

    public bool[] ReadCoils(ushort startAddress, ushort numberOfPoints)
    {
        byte[] request = new byte[8];
        request[0] = _slaveAddress;  // 从站地址
        request[1] = 0x01;           // 功能码
        request[2] = (byte)(startAddress >> 8);  // 起始地址高字节
        request[3] = (byte)(startAddress & 0xFF); // 起始地址低字节
        request[4] = (byte)(numberOfPoints >> 8); // 数量高字节
        request[5] = (byte)(numberOfPoints & 0xFF); // 数量低字节
        
        byte[] crc = CalculateCRC(request, 0, 6);
        request[6] = crc[0];
        request[7] = crc[1];
        
        _serialPort.Write(request, 0, 8);
        
        Thread.Sleep(50); // 等待响应
        
        int bytesToRead = _serialPort.BytesToRead;
        byte[] response = new byte[bytesToRead];
        _serialPort.Read(response, 0, bytesToRead);
        
        // 验证响应
        if (response.Length < 5 || response[0] != _slaveAddress || response[1] != 0x01)
        {
            throw new Exception("无效的响应");
        }
        
        byte[] receivedCrc = new byte[] { response[response.Length - 2], response[response.Length - 1] };
        byte[] calculatedCrc = CalculateCRC(response, 0, response.Length - 2);
        
        if (receivedCrc[0] != calculatedCrc[0] || receivedCrc[1] != calculatedCrc[1])
        {
            throw new Exception("CRC校验失败");
        }
        
        int byteCount = response[2];
        bool[] coils = new bool[numberOfPoints];
        
        for (int i = 0; i < numberOfPoints; i++)
        {
            int byteIndex = 3 + (i / 8);
            int bitIndex = i % 8;
            coils[i] = (response[byteIndex] & (1 << bitIndex)) != 0;
        }
        
        return coils;
    }

    // 其他功能码实现类似...
}

3.3 Python实现Modbus RTU通信示例

python 复制代码
import serial
import time
from crcmod import mkCrcFun

class ModbusRTU:
    def __init__(self, port, baudrate=9600, slave_address=1, timeout=1):
        self.serial = serial.Serial(
            port=port,
            baudrate=baudrate,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS,
            timeout=timeout
        )
        self.slave_address = slave_address
        self.crc16 = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)

    def _calculate_crc(self, data):
        return self.crc16(data).to_bytes(2, 'little')

    def _send_receive(self, pdu):
        # 构建ADU: 从站地址 + PDU + CRC
        adu = bytes([self.slave_address]) + pdu
        crc = self._calculate_crc(adu)
        adu += crc
        
        # 发送请求
        self.serial.write(adu)
        
        # 等待响应
        time.sleep(0.1)  # 根据波特率调整
        
        # 读取响应
        response = self.serial.read(256)
        
        if len(response) < 5:
            raise Exception("响应数据过短")
            
        # 验证CRC
        received_crc = response[-2:]
        calculated_crc = self._calculate_crc(response[:-2])
        if received_crc != calculated_crc:
            raise Exception("CRC校验失败")
            
        # 验证从站地址
        if response[0] != self.slave_address:
            raise Exception("从站地址不匹配")
            
        return response[1:-2]  # 返回PDU部分

    def read_holding_registers(self, start_address, quantity):
        if quantity < 1 or quantity > 125:
            raise ValueError("数量必须在1-125之间")
            
        pdu = bytes([
            0x03,  # 功能码
            (start_address >> 8) & 0xFF,  # 起始地址高字节
            start_address & 0xFF,         # 起始地址低字节
            (quantity >> 8) & 0xFF,       # 数量高字节
            quantity & 0xFF               # 数量低字节
        ])
        
        response_pdu = self._send_receive(pdu)
        
        if response_pdu[0] != 0x03:
            raise Exception("无效的功能码响应")
            
        byte_count = response_pdu[1]
        if byte_count != quantity * 2:
            raise Exception("返回数据长度不匹配")
            
        registers = []
        for i in range(quantity):
            idx = 2 + i * 2
            value = (response_pdu[idx] << 8) + response_pdu[idx + 1]
            registers.append(value)
            
        return registers

    def write_single_register(self, address, value):
        pdu = bytes([
            0x06,  # 功能码
            (address >> 8) & 0xFF,  # 地址高字节
            address & 0xFF,          # 地址低字节
            (value >> 8) & 0xFF,     # 值高字节
            value & 0xFF             # 值低字节
        ])
        
        response_pdu = self._send_receive(pdu)
        
        if len(response_pdu) != 5 or response_pdu != pdu:
            raise Exception("写入失败")
            
        return True

    def close(self):
        if self.serial.is_open:
            self.serial.close()

4. Modbus ASCII访问形式及代码示例

Modbus ASCII是Modbus协议的另一种串行传输模式,使用ASCII字符表示十六进制数据,每个字节用两个ASCII字符表示。

4.1 ASCII通信特点

  • 使用ASCII字符表示数据
  • 采用LRC校验
  • 可读性较好
  • 通信效率低于RTU模式
  • 以冒号(:)开始,以CRLF结束

4.2 Java实现Modbus ASCII通信示例

java 复制代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.TooManyListenersException;
import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;

public class ModbusASCII {
    private SerialPort serialPort;
    private OutputStream out;
    private BufferedReader in;
    private int slaveAddress;
    private static final int TIMEOUT = 2000;
    
    public ModbusASCII(String portName, int baudRate, int slaveAddress) throws Exception {
        this.slaveAddress = slaveAddress;
        CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
        
        if (portIdentifier.isCurrentlyOwned()) {
            throw new Exception("端口已被占用");
        }
        
        CommPort commPort = portIdentifier.open(this.getClass().getName(), TIMEOUT);
        
        if (commPort instanceof SerialPort) {
            serialPort = (SerialPort) commPort;
            serialPort.setSerialPortParams(baudRate, 
                                        SerialPort.DATABITS_8,
                                        SerialPort.STOPBITS_1,
                                        SerialPort.PARITY_NONE);
            
            serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
            serialPort.enableReceiveTimeout(TIMEOUT);
            
            out = serialPort.getOutputStream();
            in = new BufferedReader(new InputStreamReader(serialPort.getInputStream()));
        } else {
            throw new Exception("不是串口");
        }
    }
    
    public void close() {
        if (serialPort != null) {
            serialPort.close();
        }
    }
    
    private byte calculateLRC(byte[] data) {
        byte lrc = 0;
        for (byte b : data) {
            lrc += b;
        }
        return (byte) (-lrc);
    }
    
    private String bytesToASCII(byte[] data) {
        StringBuilder sb = new StringBuilder();
        sb.append(":");
        for (byte b : data) {
            sb.append(String.format("%02X", b));
        }
        
        byte lrc = calculateLRC(data);
        sb.append(String.format("%02X", lrc));
        sb.append("\r\n");
        
        return sb.toString();
    }
    
    private byte[] ASCIIToBytes(String ascii) {
        if (!ascii.startsWith(":") || !ascii.endsWith("\r\n")) {
            throw new IllegalArgumentException("无效的ASCII格式");
        }
        
        String content = ascii.substring(1, ascii.length() - 2);
        if (content.length() % 2 != 0) {
            throw new IllegalArgumentException("无效的ASCII长度");
        }
        
        byte[] bytes = new byte[content.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            String hex = content.substring(i * 2, i * 2 + 2);
            bytes[i] = (byte) Integer.parseInt(hex, 16);
        }
        
        // 验证LRC
        byte calculatedLRC = calculateLRC(Arrays.copyOfRange(bytes, 0, bytes.length - 1));
        byte receivedLRC = bytes[bytes.length - 1];
        
        if (calculatedLRC != receivedLRC) {
            throw new IllegalArgumentException("LRC校验失败");
        }
        
        return Arrays.copyOfRange(bytes, 0, bytes.length - 1);
    }
    
    public int[] readHoldingRegisters(int startAddress, int quantity) throws Exception {
        if (quantity < 1 || quantity > 125) {
            throw new IllegalArgumentException("数量必须在1-125之间");
        }
        
        byte[] pdu = new byte[5];
        pdu[0] = (byte) slaveAddress;  // 从站地址
        pdu[1] = 0x03;                // 功能码
        pdu[2] = (byte) (startAddress >> 8);  // 起始地址高字节
        pdu[3] = (byte) (startAddress & 0xFF); // 起始地址低字节
        pdu[4] = (byte) quantity;      // 寄存器数量
        
        String request = bytesToASCII(pdu);
        out.write(request.getBytes());
        out.flush();
        
        String response = in.readLine();
        byte[] responseBytes = ASCIIToBytes(response);
        
        if (responseBytes.length < 3 || responseBytes[0] != pdu[0] || responseBytes[1] != pdu[1]) {
            throw new Exception("无效的响应");
        }
        
        int byteCount = responseBytes[2] & 0xFF;
        if (byteCount != quantity * 2) {
            throw new Exception("返回数据长度不匹配");
        }
        
        int[] registers = new int[quantity];
        for (int i = 0; i < quantity; i++) {
            int idx = 3 + i * 2;
            registers[i] = ((responseBytes[idx] & 0xFF) << 8) | (responseBytes[idx + 1] & 0xFF);
        }
        
        return registers;
    }
    
    public boolean writeSingleRegister(int address, int value) throws Exception {
        byte[] pdu = new byte[5];
        pdu[0] = (byte) slaveAddress;  // 从站地址
        pdu[1] = 0x06;                // 功能码
        pdu[2] = (byte) (address >> 8);  // 地址高字节
        pdu[3] = (byte) (address & 0xFF); // 地址低字节
        pdu[4] = (byte) (value >> 8);    // 值高字节
        pdu[5] = (byte) (value & 0xFF);   // 值低字节
        
        String request = bytesToASCII(pdu);
        out.write(request.getBytes());
        out.flush();
        
        String response = in.readLine();
        byte[] responseBytes = ASCIIToBytes(response);
        
        if (responseBytes.length != pdu.length || !Arrays.equals(responseBytes, pdu)) {
            throw new Exception("写入失败");
        }
        
        return true;
    }
}

5. Modbus TCP/IP访问形式及代码示例

Modbus TCP/IP是Modbus协议在TCP/IP网络上的实现,它去掉了RTU和ASCII模式中的校验码,使用TCP的错误检测机制。

5.1 TCP/IP通信特点

  • 基于标准TCP/IP协议
  • 使用端口502(默认)
  • 不需要校验码
  • 增加了MBAP头(Modbus Application Protocol header)
  • 支持多设备同时访问

5.2 C#实现Modbus TCP通信示例

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

public class ModbusTCP
{
    private string _ipAddress;
    private int _port;
    private ushort _transactionId = 0;

    public ModbusTCP(string ipAddress, int port = 502)
    {
        _ipAddress = ipAddress;
        _port = port;
    }

    private byte[] BuildReadHoldingRegistersRequest(byte unitId, ushort startAddress, ushort numberOfRegisters)
    {
        _transactionId++;
        byte[] packet = new byte[12];
        
        // MBAP头
        packet[0] = (byte)(_transactionId >> 8);  // 事务ID高字节
        packet[1] = (byte)(_transactionId & 0xFF); // 事务ID低字节
        packet[2] = 0x00;  // 协议ID高字节 (Modbus=0)
        packet[3] = 0x00;  // 协议ID低字节 (Modbus=0)
        packet[4] = 0x00;  // 长度高字节 (后面设置)
        packet[5] = 0x06;  // 长度低字节 (剩余6字节)
        
        // PDU
        packet[6] = unitId;  // 单元ID
        packet[7] = 0x03;   // 功能码
        packet[8] = (byte)(startAddress >> 8);  // 起始地址高字节
        packet[9] = (byte)(startAddress & 0xFF); // 起始地址低字节
        packet[10] = (byte)(numberOfRegisters >> 8); // 寄存器数量高字节
        packet[11] = (byte)(numberOfRegisters & 0xFF); // 寄存器数量低字节
        
        return packet;
    }

    private ushort[] ParseReadHoldingRegistersResponse(byte[] response)
    {
        if (response.Length < 9)
        {
            throw new Exception("响应数据过短");
        }
        
        // 检查事务ID
        ushort transId = (ushort)((response[0] << 8) + response[1]);
        if (transId != _transactionId)
        {
            throw new Exception("事务ID不匹配");
        }
        
        // 检查协议ID
        ushort protocolId = (ushort)((response[2] << 8) + response[3]);
        if (protocolId != 0)
        {
            throw new Exception("协议ID不正确");
        }
        
        // 检查单元ID
        byte unitId = response[6];
        
        // 检查功能码
        byte functionCode = response[7];
        if (functionCode != 0x03)
        {
            if (functionCode == 0x83)  // 错误响应
            {
                byte errorCode = response[8];
                throw new Exception($"Modbus错误: {GetModbusErrorDescription(errorCode)}");
            }
            throw new Exception("功能码不正确");
        }
        
        // 获取数据
        byte byteCount = response[8];
        if (byteCount % 2 != 0 || byteCount > response.Length - 9)
        {
            throw new Exception("数据长度不正确");
        }
        
        ushort[] registers = new ushort[byteCount / 2];
        for (int i = 0; i < registers.Length; i++)
        {
            int offset = 9 + i * 2;
            registers[i] = (ushort)((response[offset] << 8) + response[offset + 1]);
        }
        
        return registers;
    }

    public ushort[] ReadHoldingRegisters(byte unitId, ushort startAddress, ushort numberOfRegisters)
    {
        try
        {
            using (TcpClient client = new TcpClient(_ipAddress, _port))
            {
                client.SendTimeout = 1000;
                client.ReceiveTimeout = 1000;
                
                NetworkStream stream = client.GetStream();
                
                byte[] request = BuildReadHoldingRegistersRequest(unitId, startAddress, numberOfRegisters);
                stream.Write(request, 0, request.Length);
                
                byte[] buffer = new byte[256];
                int bytesRead = stream.Read(buffer, 0, buffer.Length);
                
                if (bytesRead == 0)
                {
                    throw new Exception("未收到响应");
                }
                
                byte[] response = new byte[bytesRead];
                Array.Copy(buffer, response, bytesRead);
                
                return ParseReadHoldingRegistersResponse(response);
            }
        }
        catch (Exception ex)
        {
            throw new Exception($"通信失败: {ex.Message}", ex);
        }
    }

    private string GetModbusErrorDescription(byte errorCode)
    {
        switch (errorCode)
        {
            case 0x01: return "非法功能";
            case 0x02: return "非法数据地址";
            case 0x03: return "非法数据值";
            case 0x04: return "从站设备故障";
            case 0x05: return "确认";
            case 0x06: return "从站设备忙";
            case 0x07: return "否定应答";
            case 0x08: return "内存奇偶错误";
            case 0x0A: return "网关路径不可用";
            case 0x0B: return "网关目标设备响应失败";
            default: return $"未知错误 ({errorCode:X2})";
        }
    }

    // 其他功能码实现...
}

5.3 Python实现Modbus TCP通信示例

python 复制代码
import socket
import struct
from typing import List

class ModbusTCP:
    def __init__(self, host: str, port: int = 502, timeout: float = 5.0):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.transaction_id = 0
        self.socket = None
        
    def connect(self):
        """建立TCP连接"""
        if self.socket is None:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(self.timeout)
            self.socket.connect((self.host, self.port))
            
    def close(self):
        """关闭TCP连接"""
        if self.socket is not None:
            self.socket.close()
            self.socket = None
            
    def _build_mbap_header(self, unit_id: int, pdu_length: int) -> bytes:
        """构建MBAP头"""
        self.transaction_id = (self.transaction_id + 1) % 65536
        return struct.pack(
            ">HHHBB",
            self.transaction_id,  # 事务ID
            0x0000,             # 协议ID (Modbus=0)
            pdu_length + 1,     # 长度 (PDU长度 + 单元ID)
            unit_id,            # 单元ID
        )
        
    def _send_receive(self, unit_id: int, pdu: bytes) -> bytes:
        """发送请求并接收响应"""
        if self.socket is None:
            self.connect()
            
        # 构建完整的请求帧
        request = self._build_mbap_header(unit_id, len(pdu)) + pdu
        
        # 发送请求
        self.socket.sendall(request)
        
        # 接收MBAP头 (7字节)
        mbap_header = self._receive_exact(7)
        if len(mbap_header) != 7:
            raise Exception("接收MBAP头失败")
            
        # 解析MBAP头
        trans_id, proto_id, length, unit_id_resp = struct.unpack(">HHHB", mbap_header)
        
        # 验证事务ID
        if trans_id != self.transaction_id:
            raise Exception("事务ID不匹配")
            
        # 验证协议ID
        if proto_id != 0:
            raise Exception("协议ID不正确")
            
        # 接收剩余数据 (length-1字节)
        pdu_length = length - 1
        pdu_data = self._receive_exact(pdu_length)
        
        return pdu_data
        
    def _receive_exact(self, length: int) -> bytes:
        """接收指定长度的数据"""
        data = bytearray()
        while len(data) < length:
            chunk = self.socket.recv(length - len(data))
            if not chunk:
                break
            data.extend(chunk)
        return bytes(data)
        
    def read_holding_registers(self, unit_id: int, start_address: int, quantity: int) -> List[int]:
        """读取保持寄存器 (功能码0x03)"""
        if quantity < 1 or quantity > 125:
            raise ValueError("数量必须在1-125之间")
            
        # 构建PDU
        pdu = struct.pack(">BHH", 0x03, start_address, quantity)
        
        # 发送并接收响应
        response_pdu = self._send_receive(unit_id, pdu)
        
        # 解析响应
        if len(response_pdu) < 2 or response_pdu[0] != 0x03:
            if len(response_pdu) >= 2 and response_pdu[0] == 0x83:
                error_code = response_pdu[1]
                raise Exception(f"Modbus错误: {self._get_error_description(error_code)}")
            raise Exception("无效的响应")
            
        byte_count = response_pdu[1]
        if byte_count != quantity * 2:
            raise Exception("返回数据长度不匹配")
            
        registers = []
        for i in range(quantity):
            idx = 2 + i * 2
            value = struct.unpack_from(">H", response_pdu, idx)[0]
            registers.append(value)
            
        return registers
        
    def write_single_register(self, unit_id: int, address: int, value: int) -> bool:
        """写入单个寄存器 (功能码0x06)"""
        # 构建PDU
        pdu = struct.pack(">BHH", 0x06, address, value)
        
        # 发送并接收响应
        response_pdu = self._send_receive(unit_id, pdu)
        
        # 验证响应
        if len(response_pdu) != 5 or response_pdu != pdu:
            raise Exception("写入失败")
            
        return True
        
    def write_multiple_registers(self, unit_id: int, start_address: int, values: List[int]) -> bool:
        """写入多个寄存器 (功能码0x10)"""
        quantity = len(values)
        if quantity < 1 or quantity > 123:
            raise ValueError("数量必须在1-123之间")
            
        # 构建PDU
        byte_count = quantity * 2
        pdu = bytearray()
        pdu.extend(struct.pack(">BHHB", 0x10, start_address, quantity, byte_count))
        
        # 添加寄存器值
        for value in values:
            pdu.extend(struct.pack(">H", value))
            
        # 发送并接收响应
        response_pdu = self._send_receive(unit_id, pdu)
        
        # 验证响应
        if len(response_pdu) != 5 or response_pdu[0] != 0x10:
            if len(response_pdu) >= 2 and response_pdu[0] == 0x90:
                error_code = response_pdu[1]
                raise Exception(f"Modbus错误: {self._get_error_description(error_code)}")
            raise Exception("写入失败")
            
        # 验证地址和数量
        resp_address, resp_quantity = struct.unpack_from(">HH", response_pdu, 1)
        if resp_address != start_address or resp_quantity != quantity:
            raise Exception("写入验证失败")
            
        return True
        
    def _get_error_description(self, error_code: int) -> str:
        """获取Modbus错误描述"""
        errors = {
            0x01: "非法功能",
            0x02: "非法数据地址",
            0x03: "非法数据值",
            0x04: "从站设备故障",
            0x05: "确认",
            0x06: "从站设备忙",
            0x07: "否定应答",
            0x08: "内存奇偶错误",
            0x0A: "网关路径不可用",
            0x0B: "网关目标设备响应失败",
        }
        return errors.get(error_code, f"未知错误 ({error_code:X2})")

6. Modbus over TCP访问形式

Modbus over TCP是Modbus TCP的一种变体,主要用于通过TCP隧道传输Modbus RTU或ASCII帧。

6.1 Modbus over TCP特点

  • 保留RTU或ASCII帧格式
  • 通过TCP传输
  • 不需要MBAP头
  • 常用于串口转以太网设备

6.2 C++实现Modbus over TCP示例

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

class ModbusOverTCP {
private:
    SOCKET sock;
    std::string host;
    int port;
    bool connected;
    
public:
    ModbusOverTCP(const std::string& host, int port = 502) 
        : host(host), port(port), connected(false), sock(INVALID_SOCKET) {
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
            throw std::runtime_error("WSAStartup failed");
        }
    }
    
    ~ModbusOverTCP() {
        disconnect();
        WSACleanup();
    }
    
    bool connect() {
        if (connected) return true;
        
        sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sock == INVALID_SOCKET) {
            return false;
        }
        
        sockaddr_in serverAddr;
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_port = htons(port);
        inet_pton(AF_INET, host.c_str(), &serverAddr.sin_addr);
        
        if (::connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
            closesocket(sock);
            sock = INVALID_SOCKET;
            return false;
        }
        
        connected = true;
        return true;
    }
    
    void disconnect() {
        if (connected) {
            closesocket(sock);
            sock = INVALID_SOCKET;
            connected = false;
        }
    }
    
    std::vector<uint8_t> readHoldingRegisters(uint8_t slaveId, uint16_t startAddr, uint16_t quantity) {
        if (!connected && !connect()) {
            throw std::runtime_error("连接失败");
        }
        
        // 构建RTU请求帧 (无CRC)
        std::vector<uint8_t> request = {
            slaveId,    // 从站地址
            0x03,       // 功能码
            static_cast<uint8_t>(startAddr >> 8),    // 起始地址高字节
            static_cast<uint8_t>(startAddr & 0xFF),  // 起始地址低字节
            static_cast<uint8_t>(quantity >> 8),     // 数量高字节
            static_cast<uint8_t>(quantity & 0xFF)    // 数量低字节
        };
        
        // 发送请求
        if (send(sock, reinterpret_cast<char*>(request.data()), request.size(), 0) == SOCKET_ERROR) {
            throw std::runtime_error("发送失败");
        }
        
        // 接收响应头 (至少5字节)
        std::vector<uint8_t> response(256);
        int bytesRead = recv(sock, reinterpret_cast<char*>(response.data()), response.size(), 0);
        if (bytesRead == SOCKET_ERROR || bytesRead < 5) {
            throw std::runtime_error("接收失败");
        }
        
        response.resize(bytesRead);
        
        // 验证响应
        if (response[0] != slaveId || response[1] != 0x03) {
            if (response.size() >= 2 && response[1] == 0x83) {
                throw std::runtime_error("Modbus错误: " + getErrorDescription(response[2]));
            }
            throw std::runtime_error("无效的响应");
        }
        
        uint8_t byteCount = response[2];
        if (byteCount != quantity * 2) {
            throw std::runtime_error("数据长度不匹配");
        }
        
        return std::vector<uint8_t>(response.begin() + 3, response.begin() + 3 + byteCount);
    }
    
    bool writeSingleRegister(uint8_t slaveId, uint16_t addr, uint16_t value) {
        if (!connected && !connect()) {
            throw std::runtime_error("连接失败");
        }
        
        // 构建RTU请求帧 (无CRC)
        std::vector<uint8_t> request = {
            slaveId,    // 从站地址
            0x06,       // 功能码
            static_cast<uint8_t>(addr >> 8),     // 地址高字节
            static_cast<uint8_t>(addr & 0xFF),   // 地址低字节
            static_cast<uint8_t>(value >> 8),    // 值高字节
            static_cast<uint8_t>(value & 0xFF)   // 值低字节
        };
        
        // 发送请求
        if (send(sock, reinterpret_cast<char*>(request.data()), request.size(), 0) == SOCKET_ERROR) {
            throw std::runtime_error("发送失败");
        }
        
        // 接收响应
        std::vector<uint8_t> response(8);
        int bytesRead = recv(sock, reinterpret_cast<char*>(response.data()), response.size(), 0);
        if (bytesRead == SOCKET_ERROR || bytesRead < request.size()) {
            throw std::runtime_error("接收失败");
        }
        
        response.resize(bytesRead);
        
        // 验证响应
        if (response != request) {
            if (response.size() >= 2 && response[1] == 0x86) {
                throw std::runtime_error("Modbus错误: " + getErrorDescription(response[2]));
            }
            throw std::runtime_error("写入失败");
        }
        
        return true;
    }
    
private:
    std::string getErrorDescription(uint8_t errorCode) {
        switch (errorCode) {
            case 0x01: return "非法功能";
            case 0x02: return "非法数据地址";
            case 0x03: return "非法数据值";
            case 0x04: return "从站设备故障";
            case 0x05: return "确认";
            case 0x06: return "从站设备忙";
            case 0x07: return "否定应答";
            case 0x08: return "内存奇偶错误";
            case 0x0A: return "网关路径不可用";
            case 0x0B: return "网关目标设备响应失败";
            default: return "未知错误";
        }
    }
};

在实际项目中,应根据具体需求选择合适的Modbus实现方式,并考虑性能、可靠性和安全性等因素。本文提供的代码示例可以作为开发起点,但实际应用中还需要根据具体场景进行调整和优化。

随着工业物联网(IIoT)的发展,Modbus协议仍然在工业自动化领域扮演着重要角色,理解并掌握其各种访问形式对于工业软件开发人员来说是一项基本而重要的技能。

相关推荐
半路_出家ren3 小时前
传输层协议TCP、UDP
网络协议·tcp/ip·udp·tcp
Fanmeang1 天前
OSPF路由过滤
运维·网络·华为·ip·路由·ospf·路由过滤
玥轩_5211 天前
BUUCTF [WUSTCTF2020]spaceclub 1
安全·网络安全·ctf·buuctf·ascii·spaceclub·wustctf2020
趙卋傑3 天前
TCP/UDP
udp·tcp·tcp核心机制
与火星的孩子对话3 天前
Unity进阶课程【六】Android、ios、Pad 终端设备打包局域网IP调试、USB调试、性能检测、控制台打印日志等、C#
android·unity·ios·c#·ip
liulilittle3 天前
深度剖析:OPENPPP2 libtcpip 实现原理与架构设计
开发语言·网络·c++·tcp/ip·智能路由器·tcp·通信
chirrupy_hamal4 天前
如何避免 SYN 攻击?
网络·tcp
Fanmeang5 天前
OSPF高级特性之FRR
运维·网络·华为·ip·ospf·spf·frr
清醒的兰5 天前
Qt 基于TCP套接字编程
网络·qt·tcp
Y多了个想法16 天前
RK3288 android7.1 将普通串口设置为调试串口
android·经验分享·串口·rk·rk3288·uart2