C# 上位机开发 - Modbus RTU 主站实现(一篇搞定 建议收藏)
专栏导航 :上一篇:自定义通信协议设计 | 下一篇:ModbusTCP主站实现
前言
Modbus RTU 是工控最普及的串口协议之一。本文讲帧结构、常用功能码、手动组帧 的 ModbusRtuMaster,对比 NModbus4 用法,并以 温度传感器(读输入寄存器) 为例给出完整代码路径。
Modbus RTU 帧结构
| 段 | 内容 |
|---|---|
| 地址 | 1 字节,1~247 |
| PDU | 功能码 + 数据 |
| CRC16 | 2 字节,低字节在前 |
无 MBAP 头;定界靠 3.5 字符 silence + CRC。
常用功能码
| 码 | 名称 | 典型用途 |
|---|---|---|
| 01 | Read Coils | 读 DO 状态 |
| 03 | Read Holding Registers | 读可写寄存器 |
| 04 | Read Input Registers | 只读模拟量 |
| 06 | Write Single Register | 写单个 |
| 16 (0x10) | Write Multiple Registers | 写多个 |
手动组帧 ModbusRtuMaster
csharp
using System;
using System.IO.Ports;
using System.Threading;
using UpperComputer.Communication.Checksum;
namespace UpperComputer.Communication.Modbus
{
public class ModbusRtuMaster : IDisposable
{
private readonly SerialPort _port;
public ModbusRtuMaster(string com, int baud = 9600, Parity parity = Parity.Even)
{
_port = new SerialPort(com, baud, parity, 8, StopBits.One)
{
ReadTimeout = 2000,
WriteTimeout = 2000
};
_port.Open();
}
public byte[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count)
{
var pdu = new byte[6];
pdu[0] = slaveId;
pdu[1] = 0x03;
pdu[2] = (byte)(startAddress >> 8);
pdu[3] = (byte)(startAddress & 0xFF);
pdu[4] = (byte)(count >> 8);
pdu[5] = (byte)(count & 0xFF);
var frame = Crc16Modbus.Append(pdu);
_port.DiscardInBuffer();
_port.Write(frame, 0, frame.Length);
Thread.Sleep(InterFrameDelayMs(_port.BaudRate));
return ReadResponse(slaveId, 0x03);
}
private byte[] ReadResponse(byte slaveId, byte function)
{
var buf = new List<byte>();
var deadline = DateTime.UtcNow.AddMilliseconds(_port.ReadTimeout);
while (DateTime.UtcNow < deadline)
{
int n = _port.BytesToRead;
if (n > 0)
{
var tmp = new byte[n];
_port.Read(tmp, 0, n);
buf.AddRange(tmp);
if (buf.Count >= 5 && Crc16Modbus.Verify(buf.ToArray()))
return buf.ToArray();
}
Thread.Sleep(1);
}
throw new TimeoutException("Modbus 应答超时");
}
private static int InterFrameDelayMs(int baud) =>
baud >= 19200 ? 2 : (int)(3.5 * 11.0 / baud * 1000) + 1;
public void Dispose() => _port?.Dispose();
}
}
实际项目建议 NModbus4 处理异常码、分段、重试。
NModbus4 示例
NuGet:NModbus4 或 NModbus(注意版本与 .NET 目标框架)。
csharp
using Modbus.Device;
using System.IO.Ports;
public static class ModbusRtuClientFactory
{
public static IModbusMaster Create(string com, int baud = 9600)
{
var port = new SerialPort(com, baud, Parity.Even, 8, StopBits.One);
port.Open();
return ModbusSerialMaster.CreateRtu(port);
}
}
// 读温度:假设输入寄存器 0,值 = 寄存器 / 10.0 ℃
public double ReadTemperature(IModbusMaster master, byte slaveId)
{
ushort[] regs = master.ReadInputRegisters(slaveId, 0, 1);
return regs[0] / 10.0;
}
温度传感器完整流程
- 手册确认:功能码 04、寄存器地址、倍率、字节序;
- 串口 9600 8E1 或 8N1(以手册为准);
- 轮询周期 ≥ 设备最小间隔(通常 100~500 ms);
- CRC 失败记 通信质量计数,连续失败告警。
csharp
public class TemperaturePoller
{
private readonly IModbusMaster _master;
private readonly byte _slaveId;
public TemperaturePoller(IModbusMaster master, byte slaveId)
{
_master = master;
_slaveId = slaveId;
}
public double? Poll()
{
try
{
return _master.ReadInputRegisters(_slaveId, 0, 1)[0] / 10.0;
}
catch
{
return null;
}
}
}
RTU 主站注意事项
| 项 | 说明 |
|---|---|
| 485 拓扑 | 手拉手,终端 120Ω |
| 地址冲突 | 同一总线唯一 slaveId |
| 广播 0 | 无应答,慎用 |
| Float 寄存器 | 占 2 个寄存器,字序 CDAB/BADC 看手册 |
FAQ
Q1:Exception 0x83?
功能码 + 0x80,下一字节异常码 01/02/03/04 查 Modbus 规范。
Q2:读回来全 0?
接线、A/B 反、波特率、寄存器地址 从 0 还是从 1(文档差异)。
Q3:NModbus 与 SerialPort 冲突?
一个 port 只交给一个 Master;关闭时先 Dispose Master。
小结
手搓帧 懂原理,NModbus 扛生产。下一篇 Modbus TCP 与工厂模式统一 RTU/TCP。
专栏导航 :上一篇:自定义通信协议设计 | 下一篇:ModbusTCP主站实现
实战扩展:与日志、配置结合
工业现场调试串口/TCP 时,建议把 原始十六进制帧 与 解析结果 分开落日志(Serilog/NLog),并带上时间戳与 COM/IP。配置层用 appsettings.json 维护 PortName、BaudRate、PollIntervalMs,避免硬编码。联调阶段打开 Trace 级 日志,验收后降为 Warning,防止磁盘占满。
性能与内存建议
| 场景 | 建议 |
|---|---|
| 100ms 轮询 50 台设备 | 线程池 + 异步,避免 50 个 Thread |
| 大帧图像数据 | 不要用 List 反复 RemoveRange,用 RingBuffer 或 ArrayPool |
| UI 刷新 | 合并 200ms 内多条数据一次绑定,降低 Dispatcher 压力 |
延伸阅读
- 第三篇粘包与 RingBuffer 配合本篇传输层使用;
- 第四篇 CRC 是第五、六篇的共同基础;
- 第七篇工厂模式可扩展到 OPC UA Gateway 的多协议配置。
代码补注
ModbusRtuMaster.ReadResponse 使用 List<byte>,文件顶部需 using System.Collections.Generic;。若编译器提示 List<> 未找到,补上该命名空间即可。