如果你正在做 PLC 通信、设备采集、边缘网关、工业上位机,甚至只是想把"读寄存器、写线圈"这件事尽快跑起来,那 Modbus 几乎绕不开。
但真正让人头疼的,通常不是协议名词本身,而是这些更现实的问题:
- 主站怎么建,TCP、UDP、RTU、RTU Over TCP 到底怎么选?
- 从站怎么搭,数据区怎么配,多站点怎么做?
- 读出来的是字节,怎么稳定地还原成
short、int、long、字符串? - 项目一大,寄存器越来越多,代码是不是注定会变成一堆地址和函数码?
这篇文章不讲空洞概念,直接基于 TouchSocket 仓库里的真实文档和示例代码,把一条完整的 Modbus 使用路线串起来:
- 先把
ModbusTcpSlave和ModbusTcpMaster跑通 - 再看常见读写方式和类型转换
- 然后补上插件、多站点、本地内存读写
- 最后讲
ModbusObject和PlcBridge Modbus这两条"规模化开发"路线
整篇内容主要整理自以下资料:
handbook/docs/modbusdescription.mdxhandbook/docs/modbusmaster.mdxhandbook/docs/modbusslave.mdxhandbook/docs/plcbridgemodbus.mdxexamples/Modbus/ModbusMasterConsoleApp/Program.csexamples/Modbus/ModbusSlaveConsoleApp/Program.csexamples/Modbus/ModbusObjectConsoleApp/Program.csexamples/PlcBridges/ModbusPlcBridgeConsoleApp/Program.cs
官方链接
- 官网与文档首页:https://touchsocket.net/
- Modbus 协议介绍:https://touchsocket.net/docs/current/modbusdescription
- Modbus 主站文档:https://touchsocket.net/docs/current/modbusmaster
- Modbus 从站文档:https://touchsocket.net/docs/current/modbusslave
- PlcBridge Modbus 文档:https://touchsocket.net/docs/current/plcbridgemodbus
- GitHub 仓库:https://github.com/RRQM/TouchSocket
- Gitee 仓库:https://gitee.com/RRQM_Home/TouchSocket
示例直达:
- GitHub 主站 Demo:https://github.com/RRQM/TouchSocket/tree/master/examples/Modbus/ModbusMasterConsoleApp
- GitHub 从站 Demo:https://github.com/RRQM/TouchSocket/tree/master/examples/Modbus/ModbusSlaveConsoleApp
- GitHub ModbusObject Demo:https://github.com/RRQM/TouchSocket/tree/master/examples/Modbus/ModbusObjectConsoleApp
- GitHub PlcBridge Demo:https://github.com/RRQM/TouchSocket/tree/master/examples/PlcBridges/ModbusPlcBridgeConsoleApp
先记住两件事:Modbus 的角色和 TouchSocket 的分工
Modbus 本质上是一个请求/应答协议,而且是典型的主从架构:
Master主动发请求Slave被动响应请求- 数据主要分成四个区:
Coils、DiscreteInputs、HoldingRegisters、InputRegisters
对应到 TouchSocket,最重要的几个组件如下:
-
TouchSocket.Modbus提供主站能力,包含
ModbusTcpMaster、ModbusUdpMaster、ModbusRtuMaster、ModbusRtuOverTcpMaster、ModbusRtuOverUdpMaster -
TouchSocketPro.Modbus提供从站能力,包含
ModbusTcpSlave、ModbusUdpSlave、ModbusRtuSlave、ModbusRtuOverTcpSlave、ModbusRtuOverUdpSlave -
TouchSocketPro.PlcBridges面向更复杂项目,把多个 Modbus 设备桥接成一个统一地址空间,再配合
PlcObject做类型化访问
安装也很直接:
bash
dotnet add package TouchSocket.Modbus
dotnet add package TouchSocketPro.Modbus
从当前仓库里的 Src/TouchSocket.Modbus/TouchSocket.Modbus.csproj 和 Src/TouchSocketPro.Modbus/TouchSocketPro.Modbus.csproj 可以看到,这两个包都支持:
net462netstandard2.0netstandard2.1net6.0net8.0net10.0
这点非常实用,意味着它既适合新项目,也照顾到了不少工控现场常见的历史项目。
为什么 TouchSocket 的 Modbus 值得直接上手
从文档和示例代码来看,这套 Modbus 能力有几个非常明显的优点:
- 协议类型覆盖完整,TCP、UDP、RTU、RTU Over TCP、RTU Over UDP 都有
- 读写基础类型的体验很好,不只是停留在"发字节、收字节"
- 端序处理很完整,支持
ABCD、DCBA、BADC、CDAB - 从站支持插件拦截,适合做权限、审计、日志和控制逻辑
- 从站的数据区支持本地直接访问,不一定每次都要走网络
- 更大的项目还能进阶到
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 - 第二个
0或1是起始地址 - 最后的
2是读取的寄存器数量
如果你能顺利把 123 和 456 读出来,那整条链路就已经通了。
三、别只会读 03 和写 06,TouchSocket 已经把常见功能码封装好了
很多人刚接触 Modbus 时,会把自己困在"怎么拼报文"上。TouchSocket 在这方面做得很好,常用功能码已经封装成了直观的扩展方法。
常见读写方法基本可以这么记:
ReadCoilsAsync对应 FC1ReadDiscreteInputsAsync对应 FC2ReadHoldingRegistersAsync对应 FC3ReadInputRegistersAsync对应 FC4WriteSingleCoilAsync对应 FC5WriteSingleRegisterAsync对应 FC6WriteMultipleCoilsAsync对应 FC15WriteMultipleRegistersAsync对应 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());
这套方式非常适合下面这些场景:
- 一批寄存器里混着
ushort、int、long - 不同厂商设备端序不一致
- 想自己控制布局,而不是被对象映射束缚
这里有一个很重要的提醒,官方文档也明确提到了:
写入字符串时,写入后的字节总数必须是双数,否则会报错。
原因很简单,寄存器本身是 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-232、RS-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 - 设备本身协议已定:直接跟设备类型走
- 需要兼容一些特殊网关:再考虑
RtuOverTcp或RtuOverUdp
七、从站不只是"能回包",更重要的是数据区和控制点
如果主站解决的是"怎么发请求",那从站解决的就是"我维护哪些数据,别人怎么来读写"。
TouchSocket 从站最核心的概念有两个:
ModbusSlavePointModbusDataLocater
你可以把它们理解成:
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)
};
});
}));
这个例子很有代表性,因为它说明了两件事:
- 一个从站服务实例里,可以挂多个站点
- 不同站点的数据区甚至可以定义不同的起始地址
这里一定要记住一个容易踩坑的点:
- 如果你要做多站点,请把
IgnoreSlaveId设为false
否则你明明配了多个站号,但框架层面又忽略了站号校验,最后行为会和你的预期不一致。
八、插件是从站的真正加分项:日志、权限、限流都能做
官方文档里提到,从站支持两个非常关键的插件接口:
IModbusSlaveExecutingPluginIModbusSlaveExecutedPlugin
也就是"执行前"和"执行后"两个切面。
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 会让代码好看很多
当项目刚开始时,手写:
ReadHoldingRegistersAsyncWriteSingleRegisterAsyncReadCoilsAsync
当然没问题。
但一旦寄存器规模上来,代码就会很快变成这样:
- 大量魔法数字
- 大量重复地址
- 一堆"这个 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 就是专门解决这个问题的。它的核心思路是:
- 先连接多个真实设备
- 再把它们的地址段映射到一个统一的虚拟地址空间
- 最后只对统一地址做读写
下面是根据官方 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 TCP有MBAP头Modbus RTU末尾有CRC
4. 错误响应不是"超时"这么简单
官方文档里也提到了一些标准异常码,例如:
0x01非法功能码0x02非法数据地址0x03非法数据值0x04从站设备故障
另外,TouchSocket 在 CRC 校验失败时,还会给出自己的错误处理逻辑,而不只是简单抛异常。这对现场环境并不稳定的场景,非常实用。
十四、几个特别容易踩的坑,我建议你上线前先看一遍
1. 地址表写的是 40001,不代表代码里就该写 40001
很多设备手册会把保持寄存器写成 40001、40002 这种展示形式,但代码里的实际起始地址通常还是偏移地址。你要分清:
- 文档展示地址
- 实际协议地址
- 你代码里定义的数据区起点
这一点如果没统一,最容易出现"能通信,但读出来全不对"的问题。
2. 端序不一致,是数值异常的头号元凶
同样一个 int,设备可能按:
ABCDDCBABADCCDAB
来存。
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.csexamples/Modbus/ModbusSlaveConsoleApp/Program.csexamples/Modbus/ModbusObjectConsoleApp/Program.csexamples/PlcBridges/ModbusPlcBridgeConsoleApp/Program.cshandbook/docs/modbusdescription.mdxhandbook/docs/modbusmaster.mdxhandbook/docs/modbusslave.mdxhandbook/docs/plcbridgemodbus.mdx
它们之间的关系也很清晰:
modbusdescription.mdx负责协议基础modbusmaster.mdx负责主站能力modbusslave.mdx负责从站能力plcbridgemodbus.mdx负责复杂项目的桥接路线
总结
如果只用一句话概括 TouchSocket 的 Modbus 体验,我会这么说:
它不是只帮你"把 Modbus 连上",而是在"主站通信、从站建模、类型读写、插件扩展、多设备桥接"这整条链路上,都给了你比较成熟的落地方案。
入门时,你完全可以先记住最简单的一条线:
- 用
ModbusTcpSlave先搭一个从站 - 用
ModbusTcpMaster连上去 - 从
HoldingRegisters开始读写 - 把端序、地址、站号这三件事先跑顺
等你把这条线跑通,再往上加:
- 插件
- 多站点
- 本地内存读写
ModbusObjectPlcBridge + PlcObject
这样学,既不容易乱,也最接近真实项目的成长路径。
如果你准备把项目做大,我的结论也很明确:
- 主站/从站层面,TouchSocket 已经足够好用
- 长期维护层面,优先考虑
PlcBridge - 对象映射层面,优先考虑
PlcObject
这会比把整个项目写成"函数码 + 地址 + 字节偏移"的组合拳,轻松太多。