当前位置: 首页 > 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.xdnf.cn/news/1077085.html

相关文章:

  • Python OrderedDict 用法详解
  • Day 3:Python模块化、异常处理与包管理实战案例
  • A模块 系统与网络安全 第三门课 网络通信原理-3
  • 【C++】inline的作用
  • 若依中复制到剪贴板指令的使用v-clipboard
  • js严格模式和非严格模式
  • 【Python基础】13 知识拓展:CPU、GPU与NPU的区别和联系
  • 【科研绘图系列】基于R语言的复杂热图绘制教程:环境因素与染色体效应的可视化
  • SeaTunnel 社区月报(5-6 月):全新功能上线、Bug 大扫除、Merge 之星是谁?
  • 基于Spring Cloud微服务架构的API网关方案对比分析
  • 3.1.1.9 安全基线检查项目九:检查是否设置限制su命令用户组
  • [C#] WPF - 自定义样式(Slider篇)
  • 位运算经典题解
  • ELK日志分析系统(filebeat+logstash+elasticsearch+kibana)
  • Python 库 包 nltk (Natural Language Toolkit)
  • 视频断点续播全栈实现:基于HTML5前端与Spring Boot后端
  • 141.在 Vue 3 中使用 OpenLayers Link 交互:把地图中心点 / 缩放级别 / 旋转角度实时写进 URL,并同步解析显示
  • 【Maven 】 <resources> 配置中排除 fonts/** 目录无效,可能是由于以下原因及解决方案:
  • 计算机网络(二)应用层HTTP协议
  • (LangChain)RAG系统链路向量存储之Milvus(四)
  • 【1.4 漫画PostgreSQL高级数据库及国产数据库对比】
  • 【MyBatis保姆级教程下】万字XML进阶实战:配置指南与深度解析
  • 2025年6月28和29日复习和预习(C++)
  • JVM调优实战 Day 15:云原生环境下的JVM配置
  • SQLite与MySQL:嵌入式与客户端-服务器数据库的权衡
  • sqlmap学习ing(2.[第一章 web入门]SQL注入-2(报错,时间,布尔))
  • C++ 第四阶段 STL 容器 - 第九讲:详解 std::map 与 std::unordered_map —— 关联容器的深度解析
  • 解决安装UBUNTU20.04 提示尝试将SCSI(0,0,0),第一分区(sda)设备的一个vfat文件系统挂载到/boot/efi失败...问题
  • poi java设置字体样式
  • 数据结构day4——栈