Modbus是由Modicon公司(现为施耐德电气)在1979年开发的工业通信协议,是世界上最早用于工业电子设备之间通信的协议之一。它是一个**主从式(Master-Slave)**通信协议,广泛应用于工业自动化领域。
主站(Master) ←→ 从站1(Slave 1)
↓
从站2(Slave 2)
↓
从站3(Slave 3)
Modbus定义了四种数据类型,每种都有独立的地址空间:
数据类型 | 地址范围 | 访问权限 | 数据大小 | 功能码 |
---|---|---|---|---|
线圈(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 |
用户地址 → 协议地址
40001 → 0000
40002 → 0001
40010 → 0009
注意:Modbus协议中的实际地址比用户地址少1
特点:
帧格式:
[从站地址][功能码][数据][CRC校验]
1字节 1字节 N字节 2字节
特点:
帧格式:
[起始符][地址][功能码][数据][LRC校验][结束符]
: 2字节 2字节 N字节 2字节 CR LF
特点:
帧格式:
[MBAP头部][功能码][数据]
7字节 1字节 N字节
MBAP头部结构:
// 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;
}
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;
}
}
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();
}
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();
}
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;
}
}
// 使用示例
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();
}
}
异常码 | 名称 | 描述 |
---|---|---|
01 | 非法功能码 | 不支持的功能码 |
02 | 非法数据地址 | 地址超出范围 |
03 | 非法数据值 | 数据值超出范围 |
04 | 从站设备故障 | 设备内部错误 |
[从站地址][功能码+0x80][异常码][CRC]
主站 ─── RS485转换器 ─┬─ 从站1 (地址1)
├─ 从站2 (地址2)
├─ 从站3 (地址3)
└─ 从站N (地址N)
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;
}
}
Modbus协议因其简单性和可靠性,在工业自动化领域得到了广泛应用。掌握Modbus协议的关键点包括: