目录
- 概述
- [串口通讯 vs 网口通讯](#串口通讯 vs 网口通讯)
- [Modbus 协议基础](#Modbus 协议基础)
- [Modbus RTU vs Modbus TCP](#Modbus RTU vs Modbus TCP)
- 报文格式详解
- 实现架构
- 使用示例
概述
本项目实现了完整的串口通讯、网口通讯以及 Modbus RTU 和 Modbus TCP 协议调试工具。
串口通讯 vs 网口通讯
串口通讯(Serial Communication)
定义:
串口通讯是一种通过串行通信接口进行数据传输的方式,数据按位(bit)顺序,一位接一位地传输。
特点:
- 物理层: 使用物理电缆(如 RS-232、RS-485、RS-422)
- 传输方式: 串行传输,一位一位地发送
- 传输距离:
- RS-232:最长 15 米(约 50 英尺)
- RS-485:最长 1200 米(约 4000 英尺)
- 传输速率: 常用波特率:4800、9600、19200、38400、57600、115200 bps
- 连接方式: 点对点(RS-232)或总线型(RS-485,最多支持 32 个设备)
- 抗干扰能力: RS-485 具有较强的抗干扰能力,适合工业环境
应用场景:
- 工业现场设备通讯
- 传感器数据采集
- PLC 与上位机通讯
- 短距离设备控制
关键参数:
- 端口名称:COM1、COM2、COM3...
- 波特率:数据传输速度(如 9600 bps)
- 数据位:通常为 7 或 8 位
- 校验位:None、Odd、Even、Mark、Space
- 停止位:1、1.5、2 位
网口通讯(TCP/IP Communication)
定义:
网口通讯是基于 TCP/IP 协议栈的网络通讯方式,通过以太网进行数据传输。
特点:
- 物理层: 使用以太网线(双绞线、光纤)
- 传输方式: 并行传输,数据包形式
- 传输距离:
- 双绞线:100 米(单段)
- 光纤:数十公里
- 传输速率: 10/100/1000 Mbps,远高于串口
- 连接方式: 基于客户端-服务器模型
- 网络协议: TCP/IP 协议栈
应用场景:
- 远程设备监控
- 跨网络设备集成
- 高速数据采集
- 工业以太网环境
关键参数:
- IP 地址:如 192.168.1.100
- 端口号:如 502(Modbus TCP 默认端口)
- 通讯模式:客户端(Client)/服务器(Server)
- 连接类型:长连接/短连接
串口 vs 网口对比表
| 特性 | 串口通讯 | 网口通讯 |
|---|---|---|
| 物理介质 | 串行电缆 | 以太网线/光纤 |
| 传输距离 | 较短(15-1200米) | 较长(可达数十公里) |
| 传输速率 | 低(4800-115200 bps) | 高(10-1000 Mbps) |
| 抗干扰能力 | RS-485 较强 | 需要屏蔽和接地 |
| 布线复杂度 | 简单(总线型) | 需要交换机/路由器 |
| 设备数量 | 有限(RS-485: 32个) | 几乎无限 |
| 成本 | 低 | 中等 |
| 维护难度 | 简单 | 较复杂 |
| 适用环境 | 工业现场、短距离 | 远程监控、跨网络 |
Modbus 协议基础
Modbus 协议概述
Modbus 是一种工业通讯协议,由 Modicon 公司(现 Schneider Electric)于 1979 年发布,是工业领域中最流行的通讯协议之一。
特点:
- 开放、免费、无版权限制
- 简单、易于实现
- 支持多种物理层(RS-232、RS-485、TCP/IP)
- 主从架构(Master-Slave)
- 支持多种数据类型
Modbus 核心概念
1. 主从模式(Master-Slave)
Master(主站) Slave(从站)
↓ 发送请求
← 返回响应
- 只能由主站发起通讯
- 从站被动响应
- 同一时间只能有一个事务在进行
2. 功能码(Function Code)
| 功能码 | 名称 | 说明 | 数据类型 |
|---|---|---|---|
| 0x01 | 读取线圈 | 读取输出线圈的 ON/OFF 状态 | Bool[] |
| 0x02 | 读取离散输入 | 读取输入线圈的 ON/OFF 状态 | Bool[] |
| 0x03 | 读取保持寄存器 | 读取一个或多个保持寄存器的值 | Byte[] |
| 0x04 | 读取输入寄存器 | 读取一个或多个输入寄存器的值 | Byte[] |
| 0x05 | 写入单个线圈 | 写入单个线圈为 ON 或 OFF | - |
| 0x06 | 写入单个寄存器 | 写入单个保持寄存器的值 | - |
| 0x0F | 写入多个线圈 | 写入多个线圈为 ON 或 OFF | - |
| 0x10 | 写入多个寄存器 | 写入多个保持寄存器的值 | - |
3. 数据地址模型
线圈(Coil): 地址 0xxxx - 位读写,表示开关量
离散输入(Input):地址 1xxxx - 位只读,表示开关量输入
输入寄存器: 地址 3xxxx - 字只读,16位数据
保持寄存器: 地址 4xxxx - 字读写,16位数据
4. 字节序(Endianness)
- 大端序(Big-Endian): 高位字节在前,低位字节在后
- 小端序(Little-Endian): 低位字节在前,高位字节在后
- Modbus 标准: 使用大端序
Modbus RTU vs Modbus TCP
核心区别
| 特性 | Modbus RTU | Modbus TCP |
|---|---|---|
| 传输层 | 串口(RS-232/485) | TCP/IP |
| 编码方式 | 二进制 | 二进制 |
| 帧结构 | 从站地址 + 功能码 + 数据 + CRC | MBAP头 + 功能码 + 数据 |
| 错误校验 | CRC16(2字节) | TCP校验和 |
| 地址标识 | 从站地址(1字节) | 单元标识符(1字节) |
| 默认端口 | 串口参数配置 | 502 |
| 速度 | 较慢 | 较快 |
| 传输距离 | 短距离 | 长距离(跨网络) |
Modbus RTU 详细说明
报文结构:
┌──────────┬──────┬──────┬────────┬─────┐
│ 从站地址 │ 功能码 │ 数据 │ CRC16 │CRC │
│ 1 字节 │1 字节 │ N 字节│ 低位 │高位 │
└──────────┴──────┴──────┴────────┴─────┘
示例:读取保持寄存器(功能码 0x03)
请求报文:01 03 00 00 00 0A [CRC] [CRC]
- 01:从站地址
- 03:功能码(读保持寄存器)
- 00 00:起始地址(0)
- 00 0A:寄存器数量(10个)
- [CRC] [CRC]:CRC16校验码
响应报文:01 03 14 [20字节数据] [CRC] [CRC]
- 01:从站地址
- 03:功能码
- 14:返回字节数(20字节)
- [20字节数据]:实际数据
- [CRC] [CRC]:CRC16校验码
CRC16 校验算法:
csharp
// CRC16-MODBUS 算法
// 初始值:0xFFFF
// 多项式:0xA001
// 反转输入和输出
private byte[] CRC16(byte[] data, int length)
{
int i = 0;
byte[] res = new byte[2] { 0xFF, 0xFF };
ushort iIndex;
while (length-- > 0)
{
iIndex = (ushort)(res[0] ^ data[i++]);
res[0] = (byte)(res[1] ^ aucCRCHi[iIndex]);
res[1] = aucCRCLo[iIndex];
}
return res;
}
Modbus TCP 详细说明
报文结构:
┌───────────────────MBAP 头───────────────────┬──────┬────────┬─────┐
│ 事务ID │ 协议ID │ 长度 │ 单元ID │ 功能码 │ 数据 │
│ 2 字节 │ 2 字节 │ 2 字节│ 1 字节 │1 字节 │ N 字节│
└─────────┴────────┴───────┴────────┴──────┴────────┘
示例:读取保持寄存器(功能码 0x03)
请求报文:00 01 00 00 00 06 01 03 00 00 00 0A
- 00 01:事务标识符(Transaction ID)
- 00 00:协议标识符(Protocol ID,Modbus = 0)
- 00 06:长度(后续字节数:6字节)
- 01:单元标识符(Unit ID,对应从站地址)
- 03:功能码(读保持寄存器)
- 00 00:起始地址(0)
- 00 0A:寄存器数量(10个)
响应报文:00 01 00 00 00 15 01 03 14 [20字节数据]
- 00 01:事务标识符(与请求相同)
- 00 00:协议标识符
- 00 15:长度(21字节)
- 01:单元标识符
- 03:功能码
- 14:返回字节数(20字节)
- [20字节数据]:实际数据
MBAP 头(Modbus Application Protocol Header):
| 字段 | 长度 | 说明 |
|---|---|---|
| Transaction ID | 2 字节 | 事务标识符,用于匹配请求和响应。客户端每次递增,服务器原样返回 |
| Protocol ID | 2 字节 | 协议标识符,Modbus TCP 固定为 0 |
| Length | 2 字节 | 后续字节数(单元ID + 功能码 + 数据) |
| Unit ID | 1 字节 | 从站地址,对应 RTU 中的从站地址 |
事务ID处理:
csharp
private ushort transactionId = 0;
public ushort TransactionId
{
get
{
lock (lockobj)
{
return transactionId == ushort.MaxValue ? (ushort)1 : ++transactionId;
}
}
}
报文格式详解
读取操作报文
1. 读取线圈(功能码 0x01)
RTU 报文:
请求:[SlaveID][0x01][StartAddrHi][StartAddrLo][QtyHi][QtyLo][CRCL][CRCH]
响应:[SlaveID][0x01][ByteCount][CoilStatus...][CRCL][CRCH]
示例:读取从站1的线圈0-7(8个线圈)
请求:01 01 00 00 00 08 [CRC_L] [CRC_H]
响应:01 01 01 [ CoilData ] [CRC_L] [CRC_H]
└─1字节─┘
TCP 报文:
请求:[TransID][ProtoID][Len][UnitID][0x01][StartAddrHi][StartAddrLo][QtyHi][QtyLo]
响应:[TransID][ProtoID][Len][UnitID][0x01][ByteCount][CoilStatus...]
示例:读取从站1的线圈0-7(8个线圈)
请求:00 01 00 00 00 06 01 01 00 00 00 08
响应:00 01 00 00 00 04 01 01 01 [ CoilData ]
└─1字节─┘
2. 读取保持寄存器(功能码 0x03)
RTU 报文:
请求:[SlaveID][0x03][StartAddrHi][StartAddrLo][QtyHi][QtyLo][CRCL][CRCH]
响应:[SlaveID][0x03][ByteCount][DataHi][DataLo]...[CRCL][CRCH]
示例:读取从站1的保持寄存器地址0-9(10个寄存器)
请求:01 03 00 00 00 0A [CRC_L] [CRC_H]
响应:01 03 14 [20字节寄存器数据] [CRC_L] [CRC_H]
└─20字节─┘
TCP 报文:
请求:[TransID][ProtoID][Len][UnitID][0x03][StartAddrHi][StartAddrLo][QtyHi][QtyLo]
响应:[TransID][ProtoID][Len][UnitID][0x03][ByteCount][DataHi][DataLo]...
示例:读取从站1的保持寄存器地址0-9(10个寄存器)
请求:00 02 00 00 00 06 01 03 00 00 00 0A
响应:00 02 00 00 00 15 01 03 14 [20字节寄存器数据]
└─21字节─┘
写入操作报文
1. 写入单个寄存器(功能码 0x06)
RTU 报文:
请求:[SlaveID][0x06][RegAddrHi][RegAddrLo][RegValHi][RegValLo][CRCL][CRCH]
响应:[SlaveID][0x06][RegAddrHi][RegAddrLo][RegValHi][RegValLo][CRCL][CRCH]
示例:写入从站1的寄存器地址0,值为0x0001
请求:01 06 00 00 00 01 [CRC_L] [CRC_H]
响应:01 06 00 00 00 01 [CRC_L] [CRC_H]
└─回显请求报文(Echo)─┘
TCP 报文:
请求:[TransID][ProtoID][Len][UnitID][0x06][RegAddrHi][RegAddrLo][RegValHi][RegValLo]
响应:[TransID][ProtoID][Len][UnitID][0x06][RegAddrHi][RegAddrLo][RegValHi][RegValLo]
示例:写入从站1的寄存器地址0,值为0x0001
请求:00 03 00 00 00 06 01 06 00 00 00 01
响应:00 03 00 00 00 06 01 06 00 00 00 01
└─回显请求报文(Echo)─┘
2. 写入多个寄存器(功能码 0x10)
RTU 报文:
请求:[SlaveID][0x10][StartAddrHi][StartAddrLo][RegQtyHi][RegQtyLo][ByteCount]
[Data1_Hi][Data1_Lo]...[DataN_Hi][DataN_Lo][CRCL][CRCH]
响应:[SlaveID][0x10][StartAddrHi][StartAddrLo][RegQtyHi][RegQtyLo][CRCL][CRCH]
示例:写入从站1的寄存器地址0-1(2个寄存器,4字节数据)
请求:01 10 00 00 00 02 04 [Data] [CRC_L] [CRC_H]
└─4字节数据─┘
响应:01 10 00 00 00 02 [CRC_L] [CRC_H]
└─地址和数量确认─┘
TCP 报文:
请求:[TransID][ProtoID][Len][UnitID][0x10][StartAddrHi][StartAddrLo][RegQtyHi][RegQtyLo][ByteCount]
[Data1_Hi][Data1_Lo]...[DataN_Hi][DataN_Lo]
响应:[TransID][ProtoID][Len][UnitID][0x10][StartAddrHi][StartAddrLo][RegQtyHi][RegQtyLo]
示例:写入从站1的寄存器地址0-1(2个寄存器,4字节数据)
请求:00 04 00 00 00 0B 01 10 00 00 00 02 04 [Data]
└─11字节─┘ └─4字节数据─┘
响应:00 04 00 00 00 06 01 10 00 00 00 02
└─6字节(MBAP + 功能码 + 地址 + 数量)─┘
实现架构
整体架构
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ModbusRTUView│ │ ModbusTCPView│ │ SerialPort/TcpClient│ │
│ │ .xaml │ │ .xaml │ │ View │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ Binding
┌─────────────────────────────────────────────────────────┐
│ ViewModel Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ModbusRTUView │ │ModbusTCPView │ │ SerialPort/TcpClient │ │
│ │ Model │ │ Model │ │ ViewModel │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ModbusRTU │ │ ModbusTCP │ │ SerialBase │ │
│ │ (IModbusRW)│ │ (IModbusRW)│ │ │ │
│ │ │ │ │ │ TCPBase │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
↓ 通讯
┌─────────────────────────────────────────────────────────┐
│ Hardware/Transport Layer │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SerialPort │ │ TcpClient │ │
│ │ (System.IO │ │ (System.Net │ │
│ │ .Ports) │ │ .Sockets) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
核心类设计
1. ModbusRTU 类
csharp
public class ModbusRTU : SerialBase, IModbusRW
{
/// <summary>
/// 最后发送的完整报文
/// </summary>
public byte[] LastSendFrame { get; private set; }
/// <summary>
/// 最后接收的完整报文
/// </summary>
public byte[] LastReceiveFrame { get; private set; }
// 读取操作
public OperateResult<bool[]> ReadCoils(ushort start, ushort length, byte slaveId = 1)
public OperateResult<bool[]> ReadInputs(ushort start, ushort length, byte slaveId = 1)
public OperateResult<byte[]> ReadHoldingRegisters(ushort start, ushort length, byte slaveId = 1)
public OperateResult<byte[]> ReadInputRegisters(ushort start, ushort length, byte slaveId = 1)
// 写入操作
public OperateResult WriteSingleCoil(ushort start, bool value, byte slaveId = 1)
public OperateResult WriteSingleRegister(ushort start, byte[] value, byte slaveId = 1)
public OperateResult WriteMultipleCoils(ushort start, bool[] values, byte slaveId = 1)
public OperateResult WriteMultipleRegisters(ushort start, byte[] values, byte slaveId = 1)
}
关键实现要点:
- 继承自
SerialBase,复用串口通讯逻辑 - 实现
IModbusRW接口,提供统一的 Modbus 操作接口 - 使用
LastSendFrame和LastReceiveFrame保存完整报文 - CRC16 校验算法确保数据完整性
2. ModbusTCP 类
csharp
public class ModbusTCP : TCPBase, IModbusRW
{
/// <summary>
/// 默认的单元标识符
/// </summary>
public byte SlaveId { get; set; } = 0x01;
/// <summary>
/// 最后发送的完整报文
/// </summary>
public byte[] LastSendFrame { get; private set; }
/// <summary>
/// 最后接收的完整报文
/// </summary>
public byte[] LastReceiveFrame { get; private set; }
// 事务ID管理(线程安全)
private ushort transactionId = 0;
public ushort TransactionId { get { ... } }
// 读取和写入方法(与 RTU 相同接口)
}
关键实现要点:
- 继承自
TCPBase,复用 TCP 通讯逻辑 - MBAP 头构建(Transaction ID + Protocol ID + Length + Unit ID)
- 事务ID自动递增,支持并发请求
- 响应报文验证(长度、单元标识符)
3. SerialBase 和 TCPBase
SerialBase(串口基类):
csharp
public class SerialBase
{
private SerialPort serialPort = null;
public int ReadTimeOut { get; set; } = 500;
public int WriteTimeOut { get; set; } = 500;
public int SleepTime { get; set; } = 1;
public int ReceiveTimeOut { get; set; } = 100;
public OperateResult Open(string portName, int baudRate,
Parity parity, int dataBits, StopBits stopBits)
public void Close()
public OperateResult<byte[]> SendAndReceive(byte[] request)
}
TCPBase(网口基类):
csharp
public class TCPBase
{
private TcpClient tcpClient = null;
public int MaxWaitTime { get; set; } = 100;
public OperateResult Connect(string ipAddress, int port)
public void DisConnect()
public OperateResult<byte[]> SendAndReceive(byte[] request)
}
使用示例
示例 1:Modbus RTU 读取保持寄存器
csharp
// 1. 创建 ModbusRTU 实例
ModbusRTU modbus = new ModbusRTU();
// 2. 打开串口
var openResult = modbus.Open("COM1", 9600, Parity.None, 8, StopBits.One);
if (!openResult.IsSuccess)
{
Console.WriteLine($"串口打开失败:{openResult.Message}");
return;
}
// 3. 读取保持寄存器(起始地址0,读取10个寄存器,从站地址1)
var result = modbus.ReadHoldingRegisters(0, 10, 1);
if (result.IsSuccess)
{
// 4. 查看发送的完整报文
Console.WriteLine($"发送报文:{BitConverter.ToString(modbus.LastSendFrame)}");
// 5. 查看接收的完整报文
Console.WriteLine($"接收报文:{BitConverter.ToString(modbus.LastReceiveFrame)}");
// 6. 处理返回的数据(20字节)
byte[] data = result.Content;
// 7. 转换为具体数值(假设是 Int32,大端序)
int value1 = BitConverter.ToInt16(data, 0);
int value2 = BitConverter.ToInt16(data, 2);
// ...
}
else
{
Console.WriteLine($"读取失败:{result.Message}");
}
// 8. 关闭串口
modbus.Close();
示例 2:Modbus TCP 写入寄存器
csharp
// 1. 创建 ModbusTCP 实例
ModbusTCP modbus = new ModbusTCP();
// 2. 连接到服务器
var connectResult = modbus.Connect("192.168.1.100", 502);
if (!connectResult.IsSuccess)
{
Console.WriteLine($"连接失败:{connectResult.Message}");
return;
}
// 3. 写入单个寄存器(地址100,值为0x1234)
byte[] value = new byte[] { 0x12, 0x34 };
var result = modbus.WriteSingleRegister(100, value, 1);
if (result.IsSuccess)
{
// 4. 查看发送的完整报文
Console.WriteLine($"发送报文:{BitConverter.ToString(modbus.LastSendFrame)}");
// 5. 查看接收的完整报文
Console.WriteLine($"接收报文:{BitConverter.ToString(modbus.LastReceiveFrame)}");
Console.WriteLine("写入成功!");
}
else
{
Console.WriteLine($"写入失败:{result.Message}");
}
// 6. 断开连接
modbus.DisConnect();
示例 3:读取寄存器位操作
csharp
// Modbus RTU - 写入寄存器的某一位
string address = "100.5"; // 寄存器100,位5
bool bitValue = true;
var result = modbus.WriteRegisterBit(address, bitValue, isLittleEndian: false, slaveId: 1);
if (result.IsSuccess)
{
Console.WriteLine("位操作成功!");
}
附录
A. 串口参数配置
| 参数 | 可选值 | 说明 |
|---|---|---|
| 端口名称 | COM1-COM256 | 串口号 |
| 波特率 | 4800, 9600, 19200, 38400, 57600, 115200 | 数据传输速率 |
| 数据位 | 7, 8 | 数据字节长度 |
| 校验位 | None, Odd, Even, Mark, Space | 错误检测方式 |
| 停止位 | 1, 1.5, 2 | 数据包结束标志 |
B. Modbus 异常码
| 异常码 | 名称 | 说明 |
|---|---|---|
| 0x01 | 非法功能 | 收到的功能码不支持 |
| 0x02 | 非法数据地址 | 请求的地址超出允许范围 |
| 0x03 | 非法数据值 | 请求的值超出允许范围 |
| 0x04 | 从站设备故障 | 从站无法响应请求 |
C. 性能优化建议
-
批量读取
- 尽可能一次读取多个寄存器,而不是逐个读取
- 示例:使用
ReadHoldingRegisters(0, 10)一次读10个,而不是循环10次读取1个
-
超时设置
- RTU:根据设备响应速度调整
ReceiveTimeOut(默认100ms) - TCP:根据网络延迟调整
MaxWaitTime(默认100ms)
- RTU:根据设备响应速度调整
-
连接管理
- TCP:对于频繁操作,保持长连接
- RTU:操作完成后及时关闭串口
-
错误处理
- 所有操作都应检查
IsSuccess属性 - 查看
Message属性获取详细错误信息
- 所有操作都应检查
总结
本文档详细介绍了串口通讯、网口通讯、Modbus RTU 和 Modbus TCP 协议的技术特点、报文格式和实现方法。