Modbus RTU 主站实现

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:NModbus4NModbus(注意版本与 .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;
}

温度传感器完整流程

  1. 手册确认:功能码 04、寄存器地址、倍率、字节序;
  2. 串口 9600 8E18N1(以手册为准);
  3. 轮询周期 ≥ 设备最小间隔(通常 100~500 ms);
  4. 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 维护 PortNameBaudRatePollIntervalMs,避免硬编码。联调阶段打开 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<> 未找到,补上该命名空间即可。