用 TouchSocket.Modbus 从 0 到 1:把 Modbus 主站、从站、对象映射和多设备桥接一次讲透

如果你正在做 PLC 通信、设备采集、边缘网关、工业上位机,甚至只是想把"读寄存器、写线圈"这件事尽快跑起来,那 Modbus 几乎绕不开。

但真正让人头疼的,通常不是协议名词本身,而是这些更现实的问题:

  1. 主站怎么建,TCP、UDP、RTU、RTU Over TCP 到底怎么选?
  2. 从站怎么搭,数据区怎么配,多站点怎么做?
  3. 读出来的是字节,怎么稳定地还原成 shortintlong、字符串?
  4. 项目一大,寄存器越来越多,代码是不是注定会变成一堆地址和函数码?

这篇文章不讲空洞概念,直接基于 TouchSocket 仓库里的真实文档和示例代码,把一条完整的 Modbus 使用路线串起来:

  • 先把 ModbusTcpSlaveModbusTcpMaster 跑通
  • 再看常见读写方式和类型转换
  • 然后补上插件、多站点、本地内存读写
  • 最后讲 ModbusObjectPlcBridge Modbus 这两条"规模化开发"路线

整篇内容主要整理自以下资料:

  • handbook/docs/modbusdescription.mdx
  • handbook/docs/modbusmaster.mdx
  • handbook/docs/modbusslave.mdx
  • handbook/docs/plcbridgemodbus.mdx
  • examples/Modbus/ModbusMasterConsoleApp/Program.cs
  • examples/Modbus/ModbusSlaveConsoleApp/Program.cs
  • examples/Modbus/ModbusObjectConsoleApp/Program.cs
  • examples/PlcBridges/ModbusPlcBridgeConsoleApp/Program.cs

官方链接

示例直达:

先记住两件事:Modbus 的角色和 TouchSocket 的分工

Modbus 本质上是一个请求/应答协议,而且是典型的主从架构:

  • Master 主动发请求
  • Slave 被动响应请求
  • 数据主要分成四个区:CoilsDiscreteInputsHoldingRegistersInputRegisters

对应到 TouchSocket,最重要的几个组件如下:

  • TouchSocket.Modbus

    提供主站能力,包含 ModbusTcpMasterModbusUdpMasterModbusRtuMasterModbusRtuOverTcpMasterModbusRtuOverUdpMaster

  • TouchSocketPro.Modbus

    提供从站能力,包含 ModbusTcpSlaveModbusUdpSlaveModbusRtuSlaveModbusRtuOverTcpSlaveModbusRtuOverUdpSlave

  • TouchSocketPro.PlcBridges

    面向更复杂项目,把多个 Modbus 设备桥接成一个统一地址空间,再配合 PlcObject 做类型化访问

安装也很直接:

bash 复制代码
dotnet add package TouchSocket.Modbus
dotnet add package TouchSocketPro.Modbus

从当前仓库里的 Src/TouchSocket.Modbus/TouchSocket.Modbus.csprojSrc/TouchSocketPro.Modbus/TouchSocketPro.Modbus.csproj 可以看到,这两个包都支持:

  • net462
  • netstandard2.0
  • netstandard2.1
  • net6.0
  • net8.0
  • net10.0

这点非常实用,意味着它既适合新项目,也照顾到了不少工控现场常见的历史项目。

为什么 TouchSocket 的 Modbus 值得直接上手

从文档和示例代码来看,这套 Modbus 能力有几个非常明显的优点:

  • 协议类型覆盖完整,TCP、UDP、RTU、RTU Over TCP、RTU Over UDP 都有
  • 读写基础类型的体验很好,不只是停留在"发字节、收字节"
  • 端序处理很完整,支持 ABCDDCBABADCCDAB
  • 从站支持插件拦截,适合做权限、审计、日志和控制逻辑
  • 从站的数据区支持本地直接访问,不一定每次都要走网络
  • 更大的项目还能进阶到 PlcBridge,把多设备、多协议做统一封装

简单说,它不是只给你一个"能发 Modbus 包"的低层能力,而是从"能通信"一直考虑到了"能维护"。

一、先跑通再说:5 分钟搭一个 Modbus TCP 从站

如果你是第一次接 TouchSocket.Modbus,最推荐的入门方式不是先研究 RTU,而是先用 TCP 把流程跑通,因为它最容易验证。

下面这段代码来自从站示例的核心思路,创建一个 ModbusTcpSlave,监听 7808 端口,并挂一个站点 SlaveId = 1

csharp 复制代码
using TouchSocket.Core;
using TouchSocket.Modbus;

var slave = new ModbusTcpSlave();

await slave.SetupAsync(new TouchSocketConfig()
    .SetListenIPHosts(7808)
    .ConfigurePlugins(a =>
    {
        a.AddModbusSlavePoint(options =>
        {
            options.SlaveId = 1;
            options.IgnoreSlaveId = false;
            options.DataLocater = new ModbusDataLocater(10, 10, 20, 20);
        });
    }));

await slave.StartAsync();

Console.WriteLine("ModbusTcpSlave 已启动,监听 127.0.0.1:7808");

这段代码里,最关键的是 AddModbusSlavePoint。你可以把它理解成"往这个从站里挂一个可被访问的数据站点"。

这里的 ModbusDataLocater(10, 10, 20, 20) 表示四个数据区的长度:

  • 线圈 Coils:10
  • 离散输入 DiscreteInputs:10
  • 保持寄存器 HoldingRegisters:20
  • 输入寄存器 InputRegisters:20

这一步完成后,从站已经能接收主站请求了。

二、主站连接上去,马上开始读写

主站的体验和 TouchSocket 其他网络客户端保持一致,先建对象,再配置,再连接。

csharp 复制代码
using TouchSocket.Core;
using TouchSocket.Modbus;

var master = new ModbusTcpMaster();

await master.SetupAsync(new TouchSocketConfig()
    .SetRemoteIPHost("127.0.0.1:7808"));

await master.ConnectAsync();

Console.WriteLine("ModbusTcpMaster 已连接");

连接成功之后,最常见的就是写保持寄存器、再读回来验证:

csharp 复制代码
await master.WriteSingleRegisterAsync(1, 0, 123);
await master.WriteSingleRegisterAsync(1, 1, 456);

var response = await master.ReadHoldingRegistersAsync(1, 0, 2);

if (response.IsSuccess)
{
    var span = response.Data.Span;
    Console.WriteLine(span.ReadValue<ushort>(EndianType.Big));
    Console.WriteLine(span.ReadValue<ushort>(EndianType.Big));
}
else
{
    Console.WriteLine($"读取失败,错误码:{response.ErrorCode}");
}

这里的几个参数别混:

  • 第一个 1 是站号 SlaveId
  • 第二个 01 是起始地址
  • 最后的 2 是读取的寄存器数量

如果你能顺利把 123456 读出来,那整条链路就已经通了。

三、别只会读 03 和写 06,TouchSocket 已经把常见功能码封装好了

很多人刚接触 Modbus 时,会把自己困在"怎么拼报文"上。TouchSocket 在这方面做得很好,常用功能码已经封装成了直观的扩展方法。

常见读写方法基本可以这么记:

  • ReadCoilsAsync 对应 FC1
  • ReadDiscreteInputsAsync 对应 FC2
  • ReadHoldingRegistersAsync 对应 FC3
  • ReadInputRegistersAsync 对应 FC4
  • WriteSingleCoilAsync 对应 FC5
  • WriteSingleRegisterAsync 对应 FC6
  • WriteMultipleCoilsAsync 对应 FC15
  • WriteMultipleRegistersAsync 对应 FC16

比如线圈操作:

csharp 复制代码
await master.WriteSingleCoilAsync(1, 0, true);
await master.WriteMultipleCoilsAsync(1, 2, new bool[] { true, false, true });

var coils = await master.ReadCoilsAsync(1, 0, 5);
foreach (var value in coils.Span)
{
    Console.WriteLine(value);
}

读取离散输入:

csharp 复制代码
var values = await master.ReadDiscreteInputsAsync(1, 0, 5);
foreach (var value in values.Span)
{
    Console.WriteLine(value);
}

读取输入寄存器:

csharp 复制代码
var response = await master.ReadInputRegistersAsync(1, 0, 5);
var span = response.Data.Span;
var value = span.ReadValue<ushort>(EndianType.Big);
Console.WriteLine(value);

如果你只想尽快把业务打通,优先用这些快捷方法就够了。

四、真正实用的地方:同一批寄存器里写多种类型

工控项目里最常见的麻烦,不是"不会写一个寄存器",而是"一段寄存器里塞了不同类型的数据,怎么稳定读写"。

TouchSocket 示例里给了一种很实用的写法:先用 ValueByteBlock 构造一段连续内存,再一次性写到寄存器区。

csharp 复制代码
using var valueByteBlock = new ValueByteBlock(1024);

WriterExtension.WriteValue(ref valueByteBlock, (ushort)2, EndianType.Big);
WriterExtension.WriteValue(ref valueByteBlock, (ushort)2000, EndianType.Little);
WriterExtension.WriteValue(ref valueByteBlock, int.MaxValue, EndianType.BigSwap);
WriterExtension.WriteValue(ref valueByteBlock, long.MaxValue, EndianType.LittleSwap);
WriterExtension.WriteString(ref valueByteBlock, "Hello1");

await master.WriteMultipleRegistersAsync(1, 2, valueByteBlock.Memory);

然后你就可以按同样顺序读回来:

csharp 复制代码
var response = await master.ReadHoldingRegistersAsync(1, 0, 30);
var span = response.Data.Span;

Console.WriteLine(span.ReadValue<ushort>(EndianType.Big));
Console.WriteLine(span.ReadValue<ushort>(EndianType.Big));
Console.WriteLine(span.ReadValue<ushort>(EndianType.Big));
Console.WriteLine(span.ReadValue<ushort>(EndianType.Little));
Console.WriteLine(span.ReadValue<int>(EndianType.BigSwap));
Console.WriteLine(span.ReadValue<long>(EndianType.LittleSwap));
Console.WriteLine(span.ReadString());

这套方式非常适合下面这些场景:

  • 一批寄存器里混着 ushortintlong
  • 不同厂商设备端序不一致
  • 想自己控制布局,而不是被对象映射束缚

这里有一个很重要的提醒,官方文档也明确提到了:

写入字符串时,写入后的字节总数必须是双数,否则会报错。

原因很简单,寄存器本身是 16 位,也就是 2 字节对齐。

五、如果你想更底层一点,也可以直接发原生 ModbusRequest

虽然大部分场景用快捷方法已经够了,但有时候你就是想完全控制请求参数,比如统一做一层自己的协议网关,或者封装一套更通用的工业通信接口。

这个时候就可以直接使用 ModbusRequest

csharp 复制代码
var modbusRequest = new ModbusRequest(FunctionCode.ReadCoils);
modbusRequest.SlaveId = 1;
modbusRequest.StartingAddress = 0;
modbusRequest.Quantity = 1;

using var cts = new CancellationTokenSource(1000);
var response = await master.SendModbusRequestAsync(modbusRequest, cts.Token);

if (response.IsSuccess)
{
    var bools = TouchSocketBitConverter.ConvertValues<byte, bool>(response.Data.Span);
    Console.WriteLine(bools[0]);
}

这条路线更像是"保留 Modbus 原始味道"的写法,适合你自己再向上抽象一层。

六、协议类型怎么选:TCP、UDP、RTU、RTU Over TCP 到底差在哪

TouchSocket 的主站不只支持 TCP,还覆盖了多个常见变体。

1. ModbusTcpMaster

最适合做开发调试、上位机、工业网关、局域网设备通信。上手成本最低,也是本文最推荐的入门路线。

csharp 复制代码
var client = new ModbusTcpMaster();
await client.SetupAsync(new TouchSocketConfig()
    .SetRemoteIPHost("127.0.0.1:502"));
await client.ConnectAsync();

2. ModbusUdpMaster

适合 UDP 版的 Modbus 设备,创建方式也很直接:

csharp 复制代码
var client = new ModbusUdpMaster();
await client.SetupAsync(new TouchSocketConfig()
    .UseUdpReceive()
    .SetRemoteIPHost("127.0.0.1:502"));
await client.StartAsync();

3. ModbusRtuMaster

这是串口场景最常见的用法,适用于 RS-232RS-485

csharp 复制代码
var client = new ModbusRtuMaster();
await client.SetupAsync(new TouchSocketConfig()
    .SetSerialPortOption(options =>
    {
        options.BaudRate = 9600;
        options.DataBits = 8;
        options.Parity = System.IO.Ports.Parity.Even;
        options.PortName = "COM2";
        options.StopBits = System.IO.Ports.StopBits.One;
    }));
await client.ConnectAsync();

4. ModbusRtuOverTcpMaster

底层走 TCP,但数据帧还是 RTU 风格:

csharp 复制代码
var client = new ModbusRtuOverTcpMaster();
await client.ConnectAsync("127.0.0.1:502");

5. ModbusRtuOverUdpMaster

同理,底层走 UDP,但数据仍按 RTU 打包:

csharp 复制代码
var client = new ModbusRtuOverUdpMaster();
await client.SetupAsync(new TouchSocketConfig()
    .UseUdpReceive()
    .SetRemoteIPHost("127.0.0.1:502"));
await client.StartAsync();

一个简单的选择建议:

  • 新手调试:优先 ModbusTcpMaster
  • 串口现场:用 ModbusRtuMaster
  • 设备本身协议已定:直接跟设备类型走
  • 需要兼容一些特殊网关:再考虑 RtuOverTcpRtuOverUdp

七、从站不只是"能回包",更重要的是数据区和控制点

如果主站解决的是"怎么发请求",那从站解决的就是"我维护哪些数据,别人怎么来读写"。

TouchSocket 从站最核心的概念有两个:

  • ModbusSlavePoint
  • ModbusDataLocater

你可以把它们理解成:

  • ModbusSlavePoint:一个逻辑站点
  • ModbusDataLocater:这个站点背后的四块数据区

比如下面这个多站点示例,来自官方从站 Demo 的设计思路:

csharp 复制代码
await service.SetupAsync(new TouchSocketConfig()
    .SetListenIPHosts(7808)
    .ConfigurePlugins(a =>
    {
        a.AddModbusSlavePoint(options =>
        {
            options.SlaveId = 1;
            options.IgnoreSlaveId = false;
            options.DataLocater = new ModbusDataLocater(10, 10, 10, 10);
        });

        a.AddModbusSlavePoint(options =>
        {
            options.SlaveId = 2;
            options.IgnoreSlaveId = false;
            options.DataLocater = new ModbusDataLocater()
            {
                Coils = new BooleanDataPartition(1000, 10),
                DiscreteInputs = new BooleanDataPartition(1000, 10),
                HoldingRegisters = new ShortDataPartition(1000, 10),
                InputRegisters = new ShortDataPartition(1000, 10)
            };
        });
    }));

这个例子很有代表性,因为它说明了两件事:

  1. 一个从站服务实例里,可以挂多个站点
  2. 不同站点的数据区甚至可以定义不同的起始地址

这里一定要记住一个容易踩坑的点:

  • 如果你要做多站点,请把 IgnoreSlaveId 设为 false

否则你明明配了多个站号,但框架层面又忽略了站号校验,最后行为会和你的预期不一致。

八、插件是从站的真正加分项:日志、权限、限流都能做

官方文档里提到,从站支持两个非常关键的插件接口:

  • IModbusSlaveExecutingPlugin
  • IModbusSlaveExecutedPlugin

也就是"执行前"和"执行后"两个切面。

csharp 复制代码
internal class MyModbusSlavePlugin : PluginBase,
    IModbusSlaveExecutingPlugin,
    IModbusSlaveExecutedPlugin
{
    public async Task OnModbusSlaveExecuting(IModbusSlavePoint sender, ModbusSlaveExecutingEventArgs e)
    {
        // 如果要拒绝这次请求:
        // e.IsPermitOperation = false;
        // e.ErrorCode = ModbusErrorCode.ExecuteError;

        await Console.Out.WriteLineAsync("slave 操作数据");
        await e.InvokeNext();
    }

    public async Task OnModbusSlaveExecuted(IModbusSlavePoint sender, ModbusSlaveExecutedEventArgs e)
    {
        await Console.Out.WriteLineAsync("slave 操作数据完成");
        await e.InvokeNext();
    }
}

为什么这很重要?因为工控项目往往不是"通信通了就结束",而是还要加很多业务控制:

  • 哪些设备允许写,哪些只允许读
  • 哪些地址范围禁止外部修改
  • 哪些写操作需要审计日志
  • 某些功能码在特定时段不允许执行

如果没有插件机制,这些逻辑往往会散落在各处;有了插件,就能相对优雅地收束起来。

九、一个很容易被忽略的高价值能力:从站本地直接读写

很多人写从站时,会下意识再开一个主站走网络去回读数据。实际上,在 TouchSocket 里没这个必要。

你可以直接通过从站的数据区创建一个本地 Master:

csharp 复制代码
var modbusSlavePoint = service.GetSlavePointBySlaveId(1);
var localMaster = modbusSlavePoint.DataLocater.CreateDataLocaterMaster();

var coils = await localMaster.ReadCoilsAsync(0, 1);
Console.WriteLine(coils.Span[0]);

这个能力非常适合做:

  • 从站内部业务逻辑处理
  • 本地状态同步
  • 与界面或上层服务共享数据
  • 单元测试或仿真测试

而且它不是"走本机回环网络",而是直接读内存,所以速度会非常快。

如果你的项目里既有"对外提供 Modbus 服务"的需求,又有"本地逻辑实时处理"的需求,这个点非常值钱。

十、地址越来越多以后,ModbusObject 会让代码好看很多

当项目刚开始时,手写:

  • ReadHoldingRegistersAsync
  • WriteSingleRegisterAsync
  • ReadCoilsAsync

当然没问题。

但一旦寄存器规模上来,代码就会很快变成这样:

  • 大量魔法数字
  • 大量重复地址
  • 一堆"这个 1000 到底是压力还是温度"的维护噩梦

所以 TouchSocket 提供过一条比较直观的路线:ModbusObject

你可以把 Modbus 地址映射成一个类:

csharp 复制代码
class MyModbusObject : ModbusObject
{
    [ModbusProperty(SlaveId = 1, Partition = Partition.Coils, StartAddress = 1000, Timeout = 1000)]
    public bool Running
    {
        get => this.GetValue<bool>();
        set => this.SetValue(value);
    }

    [ModbusProperty(
        SlaveId = 1,
        Partition = Partition.HoldingRegisters,
        StartAddress = 1000,
        Timeout = 1000,
        EndianType = EndianType.Big)]
    public short Temperature
    {
        get => this.GetValue<short>();
        set => this.SetValue(value);
    }
}

然后像访问普通对象一样访问设备:

csharp 复制代码
var myModbusObject = master.CreateModbusObject<MyModbusObject>();

myModbusObject.Running = true;
Console.WriteLine(myModbusObject.Temperature);

从可读性上说,这个思路真的很舒服,尤其适合中小型项目、演示项目和快速验证。

但这里必须说一个很重要的结论

官方文档已经明确提醒:

  • ModbusObject 因为性能和设计缺陷问题,后续版本不再维护,也可能会废弃
  • 新项目更推荐 PlcBridge Modbus + PlcObject

所以我的建议是:

  • 小型演示、一次性工具、快速验证:ModbusObject 依然很好用
  • 正式项目、长期维护、设备较多:优先走 PlcBridge

十一、如果你的系统里不止一台设备,直接看 PlcBridge

真正的工业项目,往往不是"连一台 Modbus 设备就结束了",而是:

  • 一台 Modbus TCP 设备
  • 一台 Modbus UDP 设备
  • 一台串口 RTU 设备
  • 每台设备还只暴露了一段寄存器

这个时候,如果你还在业务代码里到处写:

  • 设备 A 读 0-20
  • 设备 B 读 10-30
  • 设备 C 读 20-40

维护成本会非常高。

TouchSocket 的 PlcBridgeService 就是专门解决这个问题的。它的核心思路是:

  1. 先连接多个真实设备
  2. 再把它们的地址段映射到一个统一的虚拟地址空间
  3. 最后只对统一地址做读写

下面是根据官方 ModbusPlcBridgeConsoleApp 提炼出来的核心结构:

csharp 复制代码
var plcBridge = new PlcBridgeService();
await plcBridge.SetupAsync(new TouchSocketConfig());

var modbusTcpMaster = new ModbusTcpMaster();
await modbusTcpMaster.ConnectAsync("127.0.0.1:502");

await plcBridge.AddDriveAsync(new ModbusHoldingRegistersDrive(
    modbusTcpMaster,
    new ModbusDriveOption
    {
        Start = 0,
        Count = 20,
        Name = "TcpDevice1",
        SlaveId = 1,
        ModbusStart = 0
    }));

var modbusUdpMaster = new ModbusUdpMaster();
await modbusUdpMaster.SetupAsync(new TouchSocketConfig()
    .UseUdpReceive()
    .SetRemoteIPHost("127.0.0.1:503"));
await modbusUdpMaster.StartAsync();

await plcBridge.AddDriveAsync(new ModbusHoldingRegistersDrive(
    modbusUdpMaster,
    new ModbusDriveOption
    {
        Start = 40,
        Count = 20,
        Name = "UdpDevice1",
        SlaveId = 1,
        ModbusStart = 10
    }));

await plcBridge.StartAsync();

var plcOperator = plcBridge.CreateOperator<short>();
var result = await plcOperator.ReadAsync(0, 60);

这段代码背后的意义非常大:

  • 业务层只看统一地址
  • 底层到底来自 TCP、UDP 还是串口,都被桥接层吃掉了
  • 后续你要调整设备映射,不需要满项目找寄存器地址

这就是为什么我会说,PlcBridge 更像是面向"真实工业项目"的做法。

十二、PlcObject 才是更现代的对象映射方式

PlcBridge 之上,你还可以继续把统一地址映射成对象字段,这就是 PlcObject 的价值。

官方示例里有这样的设计:

csharp 复制代码
internal partial class MyPlcObject : PlcObject
{
    public MyPlcObject(IPlcBridgeService bridgeService) : base(bridgeService)
    {
    }

    [PlcField<short>(Start = 0, Quantity = 80)]
    private ReadOnlyMemory<short> m_allInt16Data;

    [PlcField<short>(Start = 0, Quantity = 20)]
    private ReadOnlyMemory<long> m_allInt64Data;

    [PlcField<short>(Start = 59)]
    private long m_int64Data;
}

然后你可以直接做强类型读写:

csharp 复制代码
var myPlcObject = new MyPlcObject(plcBridge);

var setInt64Result = await myPlcObject.SetInt64DataAsync(1000);
var readInt64Result = await myPlcObject.GetInt64DataAsync();

这件事在多设备系统里非常舒服,因为你面对的已经不是"散乱的设备地址",而是"领域对象"。

十三、主站和从站之外,协议本身这些知识最好也别丢

这篇文章重点是使用 TouchSocket,但如果你想把问题排查得更稳,还是建议把官方 modbusdescription.mdx 里的几个基础点牢牢记住。

1. 四种核心数据模型

  • Coils:1 位,可读写
  • Discrete Inputs:1 位,只读
  • Holding Registers:16 位,可读写
  • Input Registers:16 位,只读

2. 最常见功能码

  • 0x01 读线圈
  • 0x02 读离散输入
  • 0x03 读保持寄存器
  • 0x04 读输入寄存器
  • 0x05 写单个线圈
  • 0x06 写单个寄存器
  • 0x0F 写多个线圈
  • 0x10 写多个寄存器

3. TCP 和 RTU 最大的差异

  • Modbus TCPMBAP
  • Modbus RTU 末尾有 CRC

4. 错误响应不是"超时"这么简单

官方文档里也提到了一些标准异常码,例如:

  • 0x01 非法功能码
  • 0x02 非法数据地址
  • 0x03 非法数据值
  • 0x04 从站设备故障

另外,TouchSocket 在 CRC 校验失败时,还会给出自己的错误处理逻辑,而不只是简单抛异常。这对现场环境并不稳定的场景,非常实用。

十四、几个特别容易踩的坑,我建议你上线前先看一遍

1. 地址表写的是 40001,不代表代码里就该写 40001

很多设备手册会把保持寄存器写成 4000140002 这种展示形式,但代码里的实际起始地址通常还是偏移地址。你要分清:

  • 文档展示地址
  • 实际协议地址
  • 你代码里定义的数据区起点

这一点如果没统一,最容易出现"能通信,但读出来全不对"的问题。

2. 端序不一致,是数值异常的头号元凶

同样一个 int,设备可能按:

  • ABCD
  • DCBA
  • BADC
  • CDAB

来存。

TouchSocket 已经把端序支持做得比较完整了,但前提是你自己知道设备是哪一种。

3. 字符串写入必须注意双字节对齐

这一点上面提过一次,但值得再强调一次。寄存器是 2 字节单位,如果字符串编码后的总字节数是奇数,写入就会出问题。

4. 多站点时不要忽略 SlaveId

只要不是单站点玩具项目,我都建议你把这个意识建立起来:

  • 单站点仿真时可以灵活一点
  • 多站点时必须严谨处理 SlaveId

5. 新项目尽量别把 ModbusObject 当长期方案

它适合快速上手,但不适合承担长期架构。正式项目更建议上 PlcBridge + PlcObject

6. 从站本地逻辑优先直接访问内存,不要自己绕网络

CreateDataLocaterMaster() 这种现成功能时,就别再为了"统一接口"强行让程序自己连自己了,性能和复杂度都不划算。

十五、一个更实用的选择建议:不同阶段到底该怎么用

如果你现在要开始一个 TouchSocket 的 Modbus 项目,我会这样建议:

场景一:先把一台设备跑通

  • ModbusTcpMaster
  • 先读写 HoldingRegisters
  • 用模拟器验证值和端序

场景二:我要做一个对外提供寄存器的模拟设备

  • ModbusTcpSlave
  • 把四个数据区先定义清楚
  • 有审计和控制需求时,加插件

场景三:项目里地址很多,想少写样板代码

  • 临时方案可以用 ModbusObject
  • 长期方案优先考虑 PlcBridge + PlcObject

场景四:我要同时接好几台不同协议的设备

  • 直接上 PlcBridgeService
  • 提前做好统一虚拟地址规划
  • 让业务层只面对桥接地址和对象字段

十六、本文涉及的本地源码位置

如果你打算继续往下看源码,建议直接从这几个文件开始:

  • examples/Modbus/ModbusMasterConsoleApp/Program.cs
  • examples/Modbus/ModbusSlaveConsoleApp/Program.cs
  • examples/Modbus/ModbusObjectConsoleApp/Program.cs
  • examples/PlcBridges/ModbusPlcBridgeConsoleApp/Program.cs
  • handbook/docs/modbusdescription.mdx
  • handbook/docs/modbusmaster.mdx
  • handbook/docs/modbusslave.mdx
  • handbook/docs/plcbridgemodbus.mdx

它们之间的关系也很清晰:

  • modbusdescription.mdx 负责协议基础
  • modbusmaster.mdx 负责主站能力
  • modbusslave.mdx 负责从站能力
  • plcbridgemodbus.mdx 负责复杂项目的桥接路线

总结

如果只用一句话概括 TouchSocket 的 Modbus 体验,我会这么说:

它不是只帮你"把 Modbus 连上",而是在"主站通信、从站建模、类型读写、插件扩展、多设备桥接"这整条链路上,都给了你比较成熟的落地方案。

入门时,你完全可以先记住最简单的一条线:

  1. ModbusTcpSlave 先搭一个从站
  2. ModbusTcpMaster 连上去
  3. HoldingRegisters 开始读写
  4. 把端序、地址、站号这三件事先跑顺

等你把这条线跑通,再往上加:

  • 插件
  • 多站点
  • 本地内存读写
  • ModbusObject
  • PlcBridge + PlcObject

这样学,既不容易乱,也最接近真实项目的成长路径。

如果你准备把项目做大,我的结论也很明确:

  • 主站/从站层面,TouchSocket 已经足够好用
  • 长期维护层面,优先考虑 PlcBridge
  • 对象映射层面,优先考虑 PlcObject

这会比把整个项目写成"函数码 + 地址 + 字节偏移"的组合拳,轻松太多。

相关推荐
MC皮蛋侠客3 天前
Modbus Poll 使用文档
物联网·modbus·调试
疆鸿智能研发小助手4 天前
注塑机协议互通改造:Ethernet IP转Modbus RTU的实战应用
modbus·工业自动化·ethernet ip·工业通讯·modbus rtu·协议转换网关
疆鸿智能研发小助手5 天前
压延机“语言不通”?ETHERNET IP转MODBUS RTU来解决
modbus·工业自动化·ethernet ip·工业通讯·modbus rtu·协议转换网关
星野云联AIoT技术洞察11 天前
OPC UA、MQTT、Modbus 应该如何分层:工业 IoT 接入架构新思路
mqtt·modbus·opc ua·边缘网关·设备接入·协议分层·工业iot
czhc114007566312 天前
modbus 49 线程 modbusmaster
modbus
fundoit12 天前
Modbus调试软件实战指南:从基础连接到高级脚本的全方位调试方案
modbus·mthings·调试工具
爱凤的小光14 天前
Modbus协议指南---个人学习笔记
modbus
麦德泽特15 天前
基于 Go 语言的 Modbus 项目实战:构建高性能、可扩展的工业通信服务器
服务器·开发语言·golang·modbus·rtu
李庆政37022 天前
modbus协议四 rtu Over tcp & mbslave & CRC校验码计算方法
网络协议·tcp/ip·modbus·rtu over tcp