当前位置: 首页 > news >正文

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 性能优化

  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// 构造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协议的关键点包括:

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

相关文章:

  • 数字图像处理学习笔记
  • Spring IOC容器核心阶段解密:★Bean实例化全流程深度剖析★
  • 菜谱大全——字符串处理艺术:从文本解析到高效搜索 [特殊字符][特殊字符]
  • 城市灯光夜景人像街拍摄影后期Lr调色教程,手机滤镜PS+Lightroom预设下载!
  • 自由学习记录(66)
  • RESTful API 设计原则深度解析
  • 转录组分析流程(六):列线图
  • 笨方法学python-习题12
  • JavaScript 安装使用教程
  • 解码知识整理,使您的研究更高效!
  • 分区表设计:历史数据归档与查询加速
  • [论文阅读] 人工智能 + 软件工程 | 从软件工程视角看大语言模型:挑战与未来之路
  • python训练day46 通道注意力
  • 2025-0701学习记录19——“问题-方法-洞见”框架做汇报
  • 半导体和PN结
  • socket编程
  • Android11 添加自定义物理按键事件监听回调
  • Vite 7.0 与 Vue 3.5:前端开发的性能革命与功能升级
  • 【Linux】进程
  • NLP——RNN变体LSTM和GRU
  • Android布局管理器实战指南:从LinearLayout到ConstraintLayout的优化之旅
  • Redis——常用指令汇总指南(一)
  • 【Python】断言(assert)
  • 监听器模式
  • [Python] -基础篇8-Python中的注释与代码风格PEP8指南
  • 【C++】inline的作用
  • InnoDB数据页
  • 61、【OS】【Nuttx】【构建】向量表
  • OpenCv基础(C++)
  • 6.Docker部署ES+kibana