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}");}// 验证CRCif (!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 性能优化
- 批量操作:使用功能码15、16进行批量读写
- 轮询间隔:避免过于频繁的通信
- 超时设置:合理设置读写超时时间
- 重试机制:通信失败时进行重试
8.3 网络拓扑
主站 ─── RS485转换器 ─┬─ 从站1 (地址1)├─ 从站2 (地址2)├─ 从站3 (地址3)└─ 从站N (地址N)
8.4 常见问题
- 地址混淆:注意用户地址与协议地址的差异
- 字节序:Modbus使用大端序(高字节在前)
- 响应超时:检查通信参数和网络连接
- 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// 构造PDUbyte[] 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协议的关键点包括:
- 理解主从架构:明确通信流程和角色
- 掌握数据模型:四种数据类型的特点和用途
- 熟悉功能码:常用功能码的使用场景
- 注意细节:地址映射、字节序、校验等
- 实践应用:结合具体项目需求选择合适的实现方式