1. Modbus协议基础知识
1.1 为什么要学习Modbus
- 协议简单,对初学者友好
- 工控领域应用广泛,学会后容易上手其他协议
- 1979年诞生,由Modicon(莫迪康,后被施耐德收购)公司制定
- 诞生原因:解决PLC控制器之间通信的问题
- Mod + Bus = Modbus
1.2 Modbus协议特点
|----------|---------------------------------------------------------|
| 特点 | 说明 |
| 免费 | 开放协议,免费使用才能获取最大使用量 |
| 简单 | 帧格式简单紧凑,用户易理解,厂商易集成 |
| 接口灵活 | 应用层协议,不绑定物理层------可在串口(232/485/422)、以太网、光纤、蓝牙、无线等多种介质传输 |
关键认知:站在协议制定者的角度思考------协议的目的是数据传输,存储区是数据的载体,功能码是行为的代号。
1.3 Modbus调试软件
|-----------------|-----------------------------------|
| 软件 | 用途 |
| ModbusPoll | Modbus主站/客户端 |
| ModbusSlave | Modbus从站/服务器 |
| VSPD | Virtual Serial Port Driver,虚拟一对串口 |
1.4 Modbus存储区说明
存储区类型分为布尔(线圈) 和**数据(寄存器)**两大类:
|--------|-------|----|----|-----------------|
| 区号 | 名称 | 类型 | 读写 | 说明 |
| 0区 | 输出线圈 | 布尔 | 读写 | Output Coil |
| 1区 | 输入线圈 | 布尔 | 只读 | Input Coil |
| 3区 | 输入寄存器 | 数据 | 只读 | Input Register |
| 4区 | 输出寄存器 | 数据 | 读写 | Output Register |
- Modbus规定每个存储区最大范围 65536
- 对比各PLC地址表示:西门子
MW100/DB1.DBD0,三菱D0/X0/Y0,欧姆龙D0/W0
地址模型
- 绝对地址 = 区号 + 相对地址
- Modbus绝对地址 = 区号 + (相对地址+1)
-
- 例:输出寄存器第一个绝对地址 = 40001
- 长地址模型 vs 短地址模型:数据量小用短地址,否则用长地址
人为交流/说明文档用绝对地址 ;协议报文用相对地址(功能码已隐含区号信息)
1.5 Modbus协议功能码
功能码 = 行为的代号:
|----------|---------|-----|
| 功能码 | 名称 | 说明 |
| 0x01 | 读取输出线圈 | 读0区 |
| 0x02 | 读取输入线圈 | 读1区 |
| 0x03 | 读取输出寄存器 | 读4区 |
| 0x04 | 读取输入寄存器 | 读3区 |
| 0x05 | 写入单个线圈 | 写0区 |
| 0x06 | 写入单个寄存器 | 写4区 |
| 0x0F | 写入多个线圈 | 写0区 |
| 0x10 | 写入多个寄存器 | 写4区 |
此外还有异常功能码和自定义功能码
1.6 Modbus协议分类
|-----------------|----------------|---------------|
| 分类 | 通信介质 | 说明 |
| ModbusRTU | 串口 232/485/422 | 二进制帧,紧凑高效 |
| ModbusASCII | 串口 232/485/422 | ASCII编码帧,可读性好 |
| ModbusTCP | 以太网 TCP/IP | 基于TCP,无CRC校验 |
| ModbusUDP | 以太网 UDP/IP | 基于UDP |
扩展组合:
- ModbusRTU Over TCP / ModbusRTU Over UDP
- ModbusASCII Over TCP / ModbusASCII Over UDP
2. ModbusRTU通信报文分析
2.1 通用报文帧格式
| 从站地址(1B) | 功能码(1B) | 数据部分(NB) | CRC16(2B) |
- 从站地址:和哪个设备通信
- 功能码:要做什么
- CRC16校验:保证数据准确,本质是一种算法(与串口奇/偶/无校验无关)
各操作的报文结构
|----------|----------------------------------------------|--------------------------------|
| 操作 | 发送报文 | 接收报文 |
| 读取 | 从站地址 + 功能码 + 开始地址 + 读取数量 + CRC | 从站地址 + 功能码 + 字节计数 + 数据 + CRC |
| 写入单个 | 从站地址 + 功能码 + 具体地址 + 写入数据 + CRC | 从站地址 + 功能码 + 具体地址 + 写入数据 + CRC |
| 写入多个 | 从站地址 + 功能码 + 开始地址 + 写入数量 + 字节计数 + 写入数据 + CRC | 从站地址 + 功能码 + 开始地址 + 写入数量 + CRC |
2.2 01H 读取输出线圈
读取1号站点从10开始的20个线圈
- 发送:
01 01 00 0A 00 14 1C 07 - 接收:
01 01 03 03 00 00 CC 4E
报文解析:
01从站地址 |01功能码 |00 0A起始地址10 |00 14数量20 |1C 07CRC- 返回:
03字节计数(3字节=24bit≥20) |03 00 00线圈数据
2.3 02H 读取输入线圈
读取5号站点从20开始的10个线圈
- 发送:
05 02 00 14 00 0A B9 8D - 接收:
05 02 02 03 00 48 88
2.4 03H 读取输出寄存器
读取2号站点从10开始的4个寄存器
- 发送:
02 03 00 0A 00 04 64 38 - 接收:
02 03 08 00 01 00 02 00 03 00 04 02 50
报文解析:
08字节计数(4个寄存器×2字节=8字节)00 01 00 02 00 03 00 044个寄存器值
2.5 04H 读取输入寄存器
读取2号站点从10开始的4个寄存器
- 发送:
02 04 00 0A 00 04 D1 F8 - 接收:
02 04 08 00 01 00 02 00 03 00 04 B3 8A
2.6 05H 预置单线圈
将2号站点地址05置位
- 发送:
02 05 00 05 FF 00 9C 08 - 接收:
02 05 00 05 FF 00 9C 08(原样回显)
FF 00 = 置位(ON),00 00 = 复位(OFF)
2.7 06H 预置单寄存器
将5号站点地址10写入123(0x7B)
- 发送:
05 06 00 0A 00 7B E8 6F - 接收:
05 06 00 0A 00 7B E8 6F(原样回显)
2.8 0FH 预置多线圈
将1号站点从1开始的5个线圈写入 True True False True False
- 发送:
01 0F 00 01 00 05 01 16 D3 58 - 接收:
01 0F 00 01 00 05 C4 08
5个线圈 = 1 1 0 1 0 → 位排列 00010110 = 0x16
2.9 10H 预置多寄存器
将1号站点从10开始的5个寄存器写入 01 02 03 04 05
- 发送:
01 10 00 0A 00 05 0A 00 01 00 02 00 03 00 04 00 05 E0 60 - 接收:
01 10 00 0A 00 05 E0 60
0A = 字节计数(5个寄存器×2字节=10字节)
3. ModbusRTU通信库开发
|------|------------------------------|
| 章节 | 内容 |
| 3.1 | 串口连接与断开 |
| 3.2 | 读取输入输出线圈 |
| 3.3 | 读取输入输出寄存器 |
| 3.4 | 预置单线圈与寄存器 |
| 3.5 | 预置多线圈与寄存器 |
| 3.6 | 测试平台UI界面设计 |
| 3.7 | 参数初始化及串口连接 |
| 3.8 | 输入输出线圈读取测试 |
| 3.9 | 输入输出寄存器读取测试 |
| 3.10 | 预置单线圈多线圈测试 |
| 3.11 | 预置单寄存器多寄存器测试 |
| 3.12 | Modbus通信库加锁处理(重要:防止并发冲突) |
4. 多路温湿度采集项目案例
|-----|-------------------------|
| 章节 | 内容 |
| 4.1 | 温湿度硬件接线及设置 |
| 4.2 | UI界面设计及搭建 |
| 4.3 | 实现单温湿度模块采集 |
| 4.4 | 实现多温湿度模块采集 |
| 4.5 | 使用自定义控件优化 |
| 4.6 | 基于OOP实现多路采集(面向对象封装) |
需求:后续需要自行开发
5. ModbusTCP通信报文分析
5.1 通信格式说明
MBAP报文头(7字节)
| 事务标识(2B) | 协议标识(2B) | 长度(2B) | 单元标识(1B) | 功能码(1B) | 数据(NB) |
|---------|----|----------------------|
| 字段 | 长度 | 说明 |
| 事务处理标识符 | 2B | 报文ID,不参与运算,用于匹配请求/响应 |
| 协议标识符 | 2B | 固定 00 00 |
| 长度 | 2B | 长度字段之后所有字节数(N+2) |
| 单元标识符 | 1B | 相当于RTU中的从站地址 |
ModbusTCP vs RTU 对比
|---------|---------|--------------------------------|
| 对比项 | RTU | TCP |
| 校验 | CRC16校验 | 无应用层校验(TCP传输层已有校验) |
| 站地址 | 从站地址 | 单元标识符(弱化,IP已区分设备;可用可不用,默认1或忽略) |
| 报文头 | 无 | MBAP头7字节 |
5.2 读取输入输出线圈
读取1号站点从10开始的20个线圈
读取输入线圈(02H):
- 发送:
00 51 00 00 00 06 01 02 00 0A 00 14 - 接收:
00 51 00 00 00 06 01 02 03 03 00 00
读取输出线圈(01H):
- 发送:
00 51 00 00 00 06 01 01 00 0A 00 14 - 接收:
00 51 00 00 00 06 01 01 03 03 00 00
5.3 读取输入输出寄存器
读取1号站点从4开始的2个寄存器
读取输入寄存器(04H):
- 发送:
01 08 00 00 00 06 01 04 00 04 00 02 - 接收:
01 08 00 00 00 07 01 04 04 00 7B 01 59
读取输出寄存器(03H):
- 发送:
01 08 00 00 00 06 01 03 00 04 00 02 - 接收:
01 08 00 00 00 07 01 03 04 00 7B 01 59
5.4 预置单线圈与寄存器
预置单线圈(05H):将1号站点08线圈置位
- 发送:
00 00 00 00 00 06 01 05 00 08 FF 00 - 接收:
00 00 00 00 00 06 01 05 00 08 FF 00
预置单寄存器(06H):将1号站点08地址写入123(0x7B)
- 发送:
00 00 00 00 00 06 01 06 00 08 00 7B - 接收:
00 00 00 00 00 06 01 06 00 08 00 7B
5.5 预置多线圈与寄存器
预置多线圈(0FH):将1号站点从0开始的6个线圈写入 1 1 0 0 1 1
- 发送:
07 90 00 00 00 08 01 0F 00 00 00 06 01 33 - 接收:
07 90 00 00 00 06 01 0F 00 00 00 06
预置多寄存器(10H):将1号站点从0开始的3个寄存器写入 12 34 56
- 发送:
08 B4 00 00 00 0D 01 10 00 00 00 03 06 00 0C 00 22 00 38 - 接收:
08 B4 00 00 00 06 01 10 00 00 00 03
6. ModbusTCP通信库编写
|-----|----------------|
| 章节 | 内容 |
| 6.1 | 以太网连接与断开 |
| 6.2 | ByteArray通用工具类 |
| 6.3 | 读取输入输出线圈 |
| 6.4 | 读取输入输出寄存器 |
| 6.5 | 预置单线圈与寄存器 |
| 6.6 | 预置多线圈与寄存器 |
| 6.7 | 测试平台UI界面及连接 |
| 6.8 | 线圈寄存器读取测试 |
| 6.9 | 预置线圈寄存器测试 |
7. ModbusTCP与西门子PLC通信
|-----|-------------------------------|
| 章节 | 内容 |
| 7.1 | 西门子PLC仿真环境搭建(PLCSIM-Advanced) |
| 7.2 | 编写ModbusServer程序 |
| 7.3 | 西门子PLC通信界面设计 |
| 7.4 | 常用数据类型及解析思路 |
| 7.5 | 多线程实现PLC数据解析 |
| 7.6 | 多线程实现解析并更新 |
| 7.7 | ModbusTCP变量写入 |
7.1 PLC仿真环境搭建要点
- 搭建PLCSIM-Advanced,设置IP地址(与PLCSIM一致)
- 根据情况设置允许PutGet
- 创建PLC程序,勾选"块编译时支持仿真"
- DB需要取消优化访问(Optimized Block Access)
7.4 Tag变量解析格式
Tag由三部分组成,用分号分割:
VarType;Start;OffsetOrLength
例:INT;0;2 表示从地址0开始的INT类型,长度2
速查表
RTU报文快速对照
|-----|--------|--------------------------|-------------------|
| 功能码 | 操作 | 发送报文结构 | 接收报文结构 |
| 01 | 读输出线圈 | 站址+01+起始地址+数量+CRC | 站址+01+字节数+数据+CRC |
| 02 | 读输入线圈 | 站址+02+起始地址+数量+CRC | 站址+02+字节数+数据+CRC |
| 03 | 读输出寄存器 | 站址+03+起始地址+数量+CRC | 站址+03+字节数+数据+CRC |
| 04 | 读输入寄存器 | 站址+04+起始地址+数量+CRC | 站址+04+字节数+数据+CRC |
| 05 | 写单线圈 | 站址+05+地址+FF00/0000+CRC | 原样回显 |
| 06 | 写单寄存器 | 站址+06+地址+值+CRC | 原样回显 |
| 0F | 写多线圈 | 站址+0F+起始地址+数量+字节数+数据+CRC | 站址+0F+起始地址+数量+CRC |
| 10 | 写多寄存器 | 站址+10+起始地址+数量+字节数+数据+CRC | 站址+10+起始地址+数量+CRC |
TCP vs RTU 快速对比
|------|-----------|--------------|
| 对比项 | RTU | TCP |
| 报文头 | 无 | MBAP头(7B) |
| 校验 | CRC16(2B) | 无 |
| 站地址 | 从站地址(1B) | 单元标识(1B,可忽略) |
| 典型场景 | 串口通信 | 以太网通信 |
8. NModbus4 --- C# Modbus通信库
8.1 为什么选NModbus4
|-----------|--------------------------------------------------------------------------------------------------------------------------------------------|
| 优势 | 说明 |
| 开源免费 | MIT协议,GitHub: https://github.com/frede-bundy/nmodbus4 |
| 协议全覆盖 | 支持 RTU / TCP / ASCII / UDP |
| 跨平台 | .NET Framework 4.5+、.NET Core / .NET 5+,Windows/Linux/macOS |
| 异步支持 | 完整async/await接口,不阻塞UI线程 |
| 社区活跃 | 目前最稳定的社区维护分支,修复了老版本内存泄漏和并发问题 |
核心价值:手写CRC/帧拼接/解析极易出错(字节序、位操作、异常帧处理),NModbus4封装了所有底层细节,只留干净的API。
8.2 安装
dotnet add package NModbus4
# 或 NuGet包管理器搜索 NModbus4
8.3 核心命名空间
using Modbus.Device; // 核心设备类(Master/Slave)
using Modbus.Data; // 数据存储(寄存器、线圈等)
using System.IO.Ports; // RTU/ASCII模式需要(串口操作)
using System.Net.Sockets; // TCP模式需要
8.4 核心概念速查
|----------|--------------------------|---------------|--------|----|
| 存储区 | NModbus4 API名称 | 地址范围 | 类型 | 读写 |
| 0区 输出线圈 | Coils | 0x0000-0xFFFF | bool | 读写 |
| 1区 输入线圈 | Discrete Inputs / Inputs | 0x0000-0xFFFF | bool | 只读 |
| 3区 输入寄存器 | Input Registers | 0x0000-0xFFFF | ushort | 只读 |
| 4区 输出寄存器 | Holding Registers | 0x0000-0xFFFF | ushort | 读写 |
⚠️ 地址注意 :NModbus4默认使用0起始地址,与多数工业设备一致。例如40001对应代码中地址0。开发时需避免地址偏移错误。
8.5 Modbus TCP模式
适用场景:远程PLC、物联网网关、跨网络设备,需知道设备IP和Modbus端口(默认502)。
// 1. 创建TCP连接
using (TcpClient tcpClient = new TcpClient())
{
tcpClient.Connect("192.168.1.100", 502); // 设备IP + Modbus端口
// 2. 创建Master
IModbusMaster master = ModbusIpMaster.CreateIp(tcpClient);
master.Transport.ReadTimeout = 1000; // 读取超时1秒
master.Transport.WriteTimeout = 1000; // 写入超时1秒
byte slaveId = 1; // 从站地址,多数设备默认1
// 3. 读取输入寄存器(如温度,地址0,读1个)
ushort[] inputRegs = master.ReadInputRegisters(slaveId, 0, 1);
float temperature = inputRegs[0] / 10.0f; // 多数设备16位整型存小数
Console.WriteLine($"当前温度:{temperature}℃");
// 4. 写入保持寄存器(如变频器频率,地址1,写50Hz)
master.WriteSingleRegister(slaveId, 1, 50);
// 5. 读取线圈(如设备运行状态,地址0,读1个)
bool[] coils = master.ReadCoils(slaveId, 0, 1);
Console.WriteLine($"设备状态:{(coils[0] ? "运行" : "停止")}");
// 6. 写入线圈(控制启动,地址0,置true)
master.WriteSingleCoil(slaveId, 0, true);
}
8.6 Modbus RTU模式
适用场景:车间PLC、传感器、变频器等短距离设备,RS232/RS485串口连接。
// 1. 配置串口参数(必须与设备一致,否则通信失败)
using (SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
serialPort.Open();
// 2. 创建RTU Master
IModbusMaster master = ModbusSerialMaster.CreateRtu(serialPort);
master.Transport.ReadTimeout = 1000;
master.Transport.WriteTimeout = 1000;
byte slaveId = 1;
// 3. 读写操作(API与TCP几乎一致)
ushort[] holdingRegs = master.ReadHoldingRegisters(slaveId, 0, 10);
master.WriteSingleRegister(slaveId, 0, 1234);
// ⚠️ RTU操作间加延迟,让总线稳定,避免通信冲突
Thread.Sleep(10);
}
关键点 :RTU是主从轮询模式,总线上同一时间只能有一个主站发指令,操作之间加 Task.Delay(10) 避免冲突。
8.7 异步API(推荐)
// TCP异步连接与读写
var tcpClient = new TcpClient();
await tcpClient.ConnectAsync("192.168.1.100", 502);
var master = ModbusIpMaster.CreateIp(tcpClient);
// 异步读取
ushort[] regs = await master.ReadHoldingRegistersAsync(slaveId: 1, startAddress: 0, numberOfRegisters: 10);
// 异步写入
await master.WriteSingleRegisterAsync(slaveId: 1, registerAddress: 0, value: 1234);
8.8 常用方法速查表
// ===== 读 =====
ReadCoilsAsync(slaveId, start, count) → bool[]
ReadInputsAsync(slaveId, start, count) → bool[]
ReadHoldingRegistersAsync(slaveId, start, count) → ushort[]
ReadInputRegistersAsync(slaveId, start, count) → ushort[]
// ===== 写单个 =====
WriteSingleCoilAsync(slaveId, address, value) → void
WriteSingleRegisterAsync(slaveId, address, value) → void
// ===== 写多个 =====
WriteMultipleCoilsAsync(slaveId, start, values) → void
WriteMultipleRegistersAsync(slaveId, start, values) → void
// ===== 高级(很少用)=====
ReadWriteMultipleRegistersAsync(...) // 读写组合
MaskWriteRegisterAsync(...) // 位掩码写
同步版本去掉 Async 后缀即可,如 ReadHoldingRegisters() → ushort[]
8.9 工业级封装(RTU + TCP 统一客户端)
using Modbus.Device;
using System.Net.Sockets;
using System.IO.Ports;
public class IndustrialModbusClient : IDisposable
{
private ModbusMaster _master;
private SerialPort _serialPort;
private TcpClient _tcpClient;
private readonly string _target;
private readonly bool _isTcp;
public IndustrialModbusClient(string target, bool isTcp = false,
int baudRate = 9600, Parity parity = Parity.None)
{
_target = target;
_isTcp = isTcp;
if (!_isTcp)
{
_serialPort = new SerialPort(target, baudRate, parity, 8, StopBits.One)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
DtrEnable = true,
RtsEnable = true
};
}
}
public async Task<bool> ConnectAsync()
{
try
{
if (_isTcp)
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_target, 502);
_master = ModbusIpMaster.CreateIp(_tcpClient);
}
else
{
if (!_serialPort.IsOpen)
{
_serialPort.Open();
await Task.Delay(100); // 等待串口稳定
}
_master = ModbusSerialMaster.CreateRtu(_serialPort);
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"连接失败: {ex.Message}");
return false;
}
}
// 统一读写接口
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveId, ushort start, ushort count)
{
await EnsureConnectedAsync();
return await _master.ReadHoldingRegistersAsync(slaveId, start, count);
}
public async Task WriteSingleRegisterAsync(byte slaveId, ushort address, ushort value)
{
await EnsureConnectedAsync();
await _master.WriteSingleRegisterAsync(slaveId, address, value);
}
private async Task EnsureConnectedAsync()
{
if ((_isTcp && (_tcpClient == null || !_tcpClient.Connected)) ||
(!_isTcp && (_serialPort == null || !_serialPort.IsOpen)))
{
await ConnectAsync();
}
}
public void Dispose()
{
_master?.Dispose();
_serialPort?.Close();
_serialPort?.Dispose();
_tcpClient?.Close();
_tcpClient?.Dispose();
}
}
使用示例:
// RTU模式
var client = new IndustrialModbusClient("COM3", isTcp: false, baudRate: 19200);
await client.ConnectAsync();
ushort[] values = await client.ReadHoldingRegistersAsync(1, 0, 5);
// TCP模式
var client = new IndustrialModbusClient("192.168.1.100", isTcp: true);
await client.ConnectAsync();
ushort[] values = await client.ReadHoldingRegistersAsync(1, 0, 5);
8.10 定时轮询(数据采集核心模式)
using System.Timers;
// 每秒轮询一次
var pollTimer = new Timer(1000);
pollTimer.AutoReset = true;
pollTimer.Elapsed += async (sender, e) => await PollRegistersAsync();
pollTimer.Start();
async Task PollRegistersAsync()
{
try
{
ushort[] data = await master.ReadHoldingRegistersAsync(1, 0, 4);
// 更新UI(WinForms需用Invoke)
Console.WriteLine($"寄存器值: {string.Join(", ", data)}");
}
catch (Exception ex)
{
Console.WriteLine($"轮询异常: {ex.Message}");
}
}
轮询防坑:
- 用
System.Timers.Timer(线程池线程),不要用WinForms Timer(UI线程会卡) - 加异常兜底,工业现场网络和硬件不稳定是常态
- 防止重入:上次还没读完新的请求又来 → 用
lock或_isPolling标志
8.11 多从站组网(RS485典型场景)
public class MultiSlaveModbusManager
{
private readonly IndustrialModbusClient _client;
private readonly byte[] _slaveIds = { 1, 2, 3, 4 };
public MultiSlaveModbusManager(IndustrialModbusClient client) => _client = client;
public async Task<Dictionary<byte, ushort[]>> ReadMultiSlavesAsync(ushort start, ushort count)
{
var results = new Dictionary<byte, ushort[]>();
foreach (byte slave in _slaveIds)
{
try
{
var data = await _client.ReadHoldingRegistersAsync(slave, start, count);
results[slave] = data;
}
catch (Exception ex)
{
Console.WriteLine($"从站{slave}读取失败: {ex.Message}");
// 跳过失败的从站,继续读下一个
}
}
return results;
}
}
8.12 Modbus TCP从站(Slave/Server)
// 创建从站,地址1,监听502端口
var slave = ModbusTcpSlave.CreateTcp(1, IPAddress.Any, 502);
// 初始化数据存储
slave.DataStore = DataStoreFactory.CreateDefaultDataStore();
// 注册写入事件(主站写寄存器时触发)
slave.DataStore.DataStoreWrittenTo += (sender, e) =>
{
ushort val = e.Data.Registers[0];
Console.WriteLine($"主站写入地址{e.StartAddress},值={val}");
};
// 后台线程监听(Listen是阻塞方法,必须放Task.Run)
Task.Run(() => slave.Listen());
// 手动设置寄存器值
slave.DataStore.HoldingRegisters[1] = 100; // 地址0=值100
8.13 常见踩坑与解决方案
|------------|---------------------|--------------------------------------------|
| 问题 | 原因 | 解决 |
| RTU通信乱码 | 串口参数(波特率/校验位)与设备不一致 | 严格对照设备手册配置 |
| TCP连接慢/超时 | 目标IP不存在时等几十秒 | 先Ping或用CancellationToken设超时 |
| 数据大小端错误 | Modbus大端 vs Intel小端 | NModbus已处理;自己转float/int时用BitConverter注意字节序 |
| 跨线程UI更新 | 后台线程直接操作控件抛异常 | WinForms用Invoke,WPF用Dispatcher.Invoke |
| Linux串口打不开 | 权限不足 | sudo usermod -a -G dialout $USER,重新登录 |
| 多任务并发冲突 | 同时发多个Modbus请求 | 加锁(lock或SemaphoreSlim),RTU总线同一时间只能一个请求 |
| 端口耗尽 | 频繁创建TcpClient未释放 | 用using确保释放,或复用连接 |