Modbus协议

Modbus协议

1. Modbus基础概念

1.1 什么是Modbus?

Modbus是由Modicon公司(现为施耐德电气)在1979年开发的工业通信协议,是世界上最早用于工业电子设备之间通信的协议之一。它是一个**主从式(Master-Slave)**通信协议,广泛应用于工业自动化领域。

1.2 Modbus的特点

  • 开放性:协议规范公开,任何厂商都可以使用
  • 简单性:协议结构简单,易于实现和理解
  • 可靠性:内置错误检测机制
  • 灵活性:支持多种物理层和数据链路层

1.3 主从架构

主站(Master)  ←→  从站1(Slave 1)
     ↓
从站2(Slave 2)
     ↓
从站3(Slave 3)
  • 主站:发起通信,发送请求
  • 从站:响应主站请求,不能主动发起通信
  • 网络中只能有一个主站,最多可以有247个从站

2. Modbus数据模型

Modbus定义了四种数据类型,每种都有独立的地址空间:

2.1 四种数据区域

数据类型 地址范围 访问权限 数据大小 功能码
线圈(Coils) 00001-09999 读/写 1位 01,05,15
离散输入(Discrete Inputs) 10001-19999 只读 1位 02
输入寄存器(Input Registers) 30001-39999 只读 16位 04
保持寄存器(Holding Registers) 40001-49999 读/写 16位 03,06,16

2.2 地址映射

用户地址 → 协议地址
40001   →   0000
40002   →   0001
40010   →   0009

注意:Modbus协议中的实际地址比用户地址少1

3. Modbus变体

3.1 Modbus RTU(远程终端单元)

特点

  • 使用二进制数据传输
  • 数据紧凑,传输效率高
  • 使用CRC校验
  • 常用于RS-485/RS-232串口通信

帧格式

[从站地址][功能码][数据][CRC校验]
   1字节    1字节   N字节   2字节

3.2 Modbus ASCII

特点

  • 使用ASCII字符传输
  • 数据可读性好,易于调试
  • 使用LRC校验
  • 传输效率相对较低

帧格式

[起始符][地址][功能码][数据][LRC校验][结束符]
   :     2字节   2字节   N字节   2字节   CR LF

3.3 Modbus TCP/IP

特点

  • 基于以太网传输
  • 使用TCP协议,无需额外校验
  • 支持并发连接
  • 传输距离远,速度快

帧格式

[MBAP头部][功能码][数据]
  7字节     1字节   N字节

MBAP头部结构:

  • 事务处理标识符(2字节)
  • 协议标识符(2字节,固定为0000)
  • 长度字段(2字节)
  • 单元标识符(1字节)

4. 主要功能码详解

4.1 读取功能码

01 - 读取线圈状态
// C# 示例:构造读取线圈请求
public byte[] BuildReadCoilsRequest(byte slaveId, ushort startAddress, ushort quantity)
{
    byte[] request = new byte[6];
    request[0] = slaveId;           // 从站地址
    request[1] = 0x01;              // 功能码
    request[2] = (byte)(startAddress >> 8);   // 起始地址高字节
    request[3] = (byte)(startAddress & 0xFF); // 起始地址低字节
    request[4] = (byte)(quantity >> 8);       // 数量高字节
    request[5] = (byte)(quantity & 0xFF);     // 数量低字节
    
    // 实际应用中还需要添加CRC校验
    return request;
}
03 - 读取保持寄存器
public class ModbusHelper
{
    // 读取保持寄存器
    public byte[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort quantity)
    {
        List<byte> request = new List<byte>
        {
            slaveId,                           // 从站地址
            0x03,                              // 功能码
            (byte)(startAddress >> 8),         // 起始地址高字节
            (byte)(startAddress & 0xFF),       // 起始地址低字节
            (byte)(quantity >> 8),             // 数量高字节
            (byte)(quantity & 0xFF)            // 数量低字节
        };
        
        // 添加CRC校验
        ushort crc = CalculateCRC(request.ToArray());
        request.Add((byte)(crc & 0xFF));
        request.Add((byte)(crc >> 8));
        
        return request.ToArray();
    }
    
    // CRC校验计算
    private ushort CalculateCRC(byte[] data)
    {
        ushort crc = 0xFFFF;
        
        foreach (byte b in data)
        {
            crc ^= b;
            for (int i = 0; i < 8; i++)
            {
                if ((crc & 0x0001) == 1)
                {
                    crc = (ushort)((crc >> 1) ^ 0xA001);
                }
                else
                {
                    crc = (ushort)(crc >> 1);
                }
            }
        }
        
        return crc;
    }
}

4.2 写入功能码

05 - 写入单个线圈
public byte[] WriteSingleCoil(byte slaveId, ushort address, bool value)
{
    List<byte> request = new List<byte>
    {
        slaveId,                           // 从站地址
        0x05,                              // 功能码
        (byte)(address >> 8),              // 地址高字节
        (byte)(address & 0xFF),            // 地址低字节
        (byte)(value ? 0xFF : 0x00),       // 值高字节(FF00=ON, 0000=OFF)
        0x00                               // 值低字节
    };
    
    // 添加CRC校验
    ushort crc = CalculateCRC(request.ToArray());
    request.Add((byte)(crc & 0xFF));
    request.Add((byte)(crc >> 8));
    
    return request.ToArray();
}
06 - 写入单个寄存器
public byte[] WriteSingleRegister(byte slaveId, ushort address, ushort value)
{
    List<byte> request = new List<byte>
    {
        slaveId,                           // 从站地址
        0x06,                              // 功能码
        (byte)(address >> 8),              // 地址高字节
        (byte)(address & 0xFF),            // 地址低字节
        (byte)(value >> 8),                // 值高字节
        (byte)(value & 0xFF)               // 值低字节
    };
    
    // 添加CRC校验
    ushort crc = CalculateCRC(request.ToArray());
    request.Add((byte)(crc & 0xFF));
    request.Add((byte)(crc >> 8));
    
    return request.ToArray();
}

5. 完整的C# Modbus客户端示例

using System;
using System.IO.Ports;
using System.Threading;

public class ModbusRTUClient
{
    private SerialPort serialPort;
    private object lockObject = new object();
    
    public ModbusRTUClient(string portName, int baudRate = 9600)
    {
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort.ReadTimeout = 1000;
        serialPort.WriteTimeout = 1000;
    }
    
    public bool Connect()
    {
        try
        {
            if (!serialPort.IsOpen)
            {
                serialPort.Open();
            }
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"连接失败: {ex.Message}");
            return false;
        }
    }
    
    public void Disconnect()
    {
        if (serialPort.IsOpen)
        {
            serialPort.Close();
        }
    }
    
    // 读取保持寄存器
    public ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort quantity)
    {
        lock (lockObject)
        {
            // 构造请求
            byte[] request = BuildReadHoldingRegistersRequest(slaveId, startAddress, quantity);
            
            // 发送请求
            serialPort.DiscardInBuffer();
            serialPort.Write(request, 0, request.Length);
            
            // 等待响应
            Thread.Sleep(50);
            
            // 读取响应
            byte[] response = new byte[256];
            int bytesRead = serialPort.Read(response, 0, response.Length);
            
            // 验证响应
            if (bytesRead < 5)
            {
                throw new Exception("响应数据不完整");
            }
            
            if (response[0] != slaveId)
            {
                throw new Exception("从站地址不匹配");
            }
            
            if (response[1] != 0x03)
            {
                throw new Exception($"功能码错误: {response[1]:X2}");
            }
            
            // 检查是否为异常响应
            if ((response[1] & 0x80) != 0)
            {
                throw new Exception($"设备返回异常: {response[2]:X2}");
            }
            
            // 验证CRC
            if (!VerifyCRC(response, bytesRead))
            {
                throw new Exception("CRC校验失败");
            }
            
            // 解析数据
            byte dataLength = response[2];
            ushort[] registers = new ushort[dataLength / 2];
            
            for (int i = 0; i < registers.Length; i++)
            {
                registers[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
            }
            
            return registers;
        }
    }
    
    // 写入单个寄存器
    public bool WriteSingleRegister(byte slaveId, ushort address, ushort value)
    {
        lock (lockObject)
        {
            try
            {
                // 构造请求
                byte[] request = BuildWriteSingleRegisterRequest(slaveId, address, value);
                
                // 发送请求
                serialPort.DiscardInBuffer();
                serialPort.Write(request, 0, request.Length);
                
                // 等待响应
                Thread.Sleep(50);
                
                // 读取响应
                byte[] response = new byte[8];
                int bytesRead = serialPort.Read(response, 0, response.Length);
                
                // 验证响应(写入成功时,响应应该与请求相同)
                if (bytesRead == 8 && ArraysEqual(request, response))
                {
                    return true;
                }
                
                return false;
            }
            catch
            {
                return false;
            }
        }
    }
    
    private byte[] BuildReadHoldingRegistersRequest(byte slaveId, ushort startAddress, ushort quantity)
    {
        List<byte> request = new List<byte>
        {
            slaveId,
            0x03,
            (byte)(startAddress >> 8),
            (byte)(startAddress & 0xFF),
            (byte)(quantity >> 8),
            (byte)(quantity & 0xFF)
        };
        
        ushort crc = CalculateCRC(request.ToArray());
        request.Add((byte)(crc & 0xFF));
        request.Add((byte)(crc >> 8));
        
        return request.ToArray();
    }
    
    private byte[] BuildWriteSingleRegisterRequest(byte slaveId, ushort address, ushort value)
    {
        List<byte> request = new List<byte>
        {
            slaveId,
            0x06,
            (byte)(address >> 8),
            (byte)(address & 0xFF),
            (byte)(value >> 8),
            (byte)(value & 0xFF)
        };
        
        ushort crc = CalculateCRC(request.ToArray());
        request.Add((byte)(crc & 0xFF));
        request.Add((byte)(crc >> 8));
        
        return request.ToArray();
    }
    
    private ushort CalculateCRC(byte[] data)
    {
        ushort crc = 0xFFFF;
        
        foreach (byte b in data)
        {
            crc ^= b;
            for (int i = 0; i < 8; i++)
            {
                if ((crc & 0x0001) == 1)
                {
                    crc = (ushort)((crc >> 1) ^ 0xA001);
                }
                else
                {
                    crc = (ushort)(crc >> 1);
                }
            }
        }
        
        return crc;
    }
    
    private bool VerifyCRC(byte[] data, int length)
    {
        if (length < 2) return false;
        
        byte[] dataWithoutCRC = new byte[length - 2];
        Array.Copy(data, 0, dataWithoutCRC, 0, length - 2);
        
        ushort calculatedCRC = CalculateCRC(dataWithoutCRC);
        ushort receivedCRC = (ushort)(data[length - 2] | (data[length - 1] << 8));
        
        return calculatedCRC == receivedCRC;
    }
    
    private bool ArraysEqual(byte[] array1, byte[] array2)
    {
        if (array1.Length != array2.Length) return false;
        
        for (int i = 0; i < array1.Length; i++)
        {
            if (array1[i] != array2[i]) return false;
        }
        
        return true;
    }
}

6. 使用示例

// 使用示例
class Program
{
    static void Main(string[] args)
    {
        ModbusRTUClient client = new ModbusRTUClient("COM3", 9600);
        
        try
        {
            // 连接
            if (client.Connect())
            {
                Console.WriteLine("连接成功");
                
                // 读取从站1的地址0开始的5个保持寄存器
                ushort[] registers = client.ReadHoldingRegisters(1, 0, 5);
                
                Console.WriteLine("读取的寄存器值:");
                for (int i = 0; i < registers.Length; i++)
                {
                    Console.WriteLine($"寄存器 {i}: {registers[i]}");
                }
                
                // 写入单个寄存器
                bool writeSuccess = client.WriteSingleRegister(1, 0, 1234);
                Console.WriteLine($"写入结果: {writeSuccess}");
            }
            else
            {
                Console.WriteLine("连接失败");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"操作失败: {ex.Message}");
        }
        finally
        {
            client.Disconnect();
        }
        
        Console.ReadKey();
    }
}

7. 错误处理和异常码

7.1 常见异常码

异常码 名称 描述
01 非法功能码 不支持的功能码
02 非法数据地址 地址超出范围
03 非法数据值 数据值超出范围
04 从站设备故障 设备内部错误

7.2 异常响应格式

[从站地址][功能码+0x80][异常码][CRC]

8. 实际应用注意事项

8.1 通信参数配置

  • 波特率:常用9600、19200、38400
  • 数据位:8位
  • 停止位:1位
  • 校验位:无校验或偶校验

8.2 性能优化

  1. 批量操作:使用功能码15、16进行批量读写
  2. 轮询间隔:避免过于频繁的通信
  3. 超时设置:合理设置读写超时时间
  4. 重试机制:通信失败时进行重试

8.3 网络拓扑

主站 ─── RS485转换器 ─┬─ 从站1 (地址1)
                     ├─ 从站2 (地址2)
                     ├─ 从站3 (地址3)
                     └─ 从站N (地址N)

8.4 常见问题

  1. 地址混淆:注意用户地址与协议地址的差异
  2. 字节序:Modbus使用大端序(高字节在前)
  3. 响应超时:检查通信参数和网络连接
  4. CRC错误:检查数据传输质量和计算方法

9. 高级功能

9.1 Modbus TCP客户端

using System.Net.Sockets;

public class ModbusTCPClient
{
    private TcpClient tcpClient;
    private NetworkStream stream;
    private ushort transactionId = 0;
    
    public bool Connect(string ipAddress, int port = 502)
    {
        try
        {
            tcpClient = new TcpClient();
            tcpClient.Connect(ipAddress, port);
            stream = tcpClient.GetStream();
            return true;
        }
        catch
        {
            return false;
        }
    }
    
    public ushort[] ReadHoldingRegisters(byte unitId, ushort startAddress, ushort quantity)
    {
        // 构造MBAP头部
        byte[] mbapHeader = new byte[7];
        mbapHeader[0] = (byte)(transactionId >> 8);    // 事务ID高字节
        mbapHeader[1] = (byte)(transactionId & 0xFF);  // 事务ID低字节
        mbapHeader[2] = 0x00;                          // 协议ID高字节
        mbapHeader[3] = 0x00;                          // 协议ID低字节
        mbapHeader[4] = 0x00;                          // 长度高字节
        mbapHeader[5] = 0x06;                          // 长度低字节
        mbapHeader[6] = unitId;                        // 单元ID
        
        // 构造PDU
        byte[] pdu = new byte[5];
        pdu[0] = 0x03;                                 // 功能码
        pdu[1] = (byte)(startAddress >> 8);           // 起始地址高字节
        pdu[2] = (byte)(startAddress & 0xFF);         // 起始地址低字节
        pdu[3] = (byte)(quantity >> 8);               // 数量高字节
        pdu[4] = (byte)(quantity & 0xFF);             // 数量低字节
        
        // 发送请求
        byte[] request = new byte[12];
        Array.Copy(mbapHeader, 0, request, 0, 7);
        Array.Copy(pdu, 0, request, 7, 5);
        
        stream.Write(request, 0, request.Length);
        
        // 接收响应
        byte[] response = new byte[256];
        int bytesRead = stream.Read(response, 0, response.Length);
        
        // 解析响应(省略验证步骤)
        byte dataLength = response[8];
        ushort[] registers = new ushort[dataLength / 2];
        
        for (int i = 0; i < registers.Length; i++)
        {
            registers[i] = (ushort)((response[9 + i * 2] << 8) | response[10 + i * 2]);
        }
        
        transactionId++;
        return registers;
    }
}

10. 总结

Modbus协议因其简单性和可靠性,在工业自动化领域得到了广泛应用。掌握Modbus协议的关键点包括:

  1. 理解主从架构:明确通信流程和角色
  2. 掌握数据模型:四种数据类型的特点和用途
  3. 熟悉功能码:常用功能码的使用场景
  4. 注意细节:地址映射、字节序、校验等
  5. 实践应用:结合具体项目需求选择合适的实现方式

你可能感兴趣的:(网络,Modbus)