Modbus,Socket,串口集锦
之所以必须把 Int32 (32位整数) 拆分成 2 个 ushort (16位短整数),是因为 Modbus 协议的“先天基因限制”。
我们可以通过一个“快递装箱”的比喻和底层的原理来彻底弄懂它:
1. 根本原因:Modbus 协议的“柜子”太小了
Modbus 协议诞生于 1979 年,那个时候计算机和 PLC 的主流还是 16 位架构。 在 Modbus 的协议规定中,用来存储数据的基础单元叫 “保持寄存器 (Holding Register)”。 1 个寄存器,死规定了只能装 16 位(2个字节)的数据。 无论你用什么语言、什么类库,发给 PLC 的物理电信号里,每个格子的容量就是 16 位(即 C# 里的 ushort)。
2. “大件货物拆包”的比喻
-
C# 里的
Int32(你要下发的值): 它是一个 32 位(4个字节)的大件货物。比如你想下发一个产量数据100,000。 -
Modbus 的运输车(寄存器): 它的每个车厢(寄存器)只能装 16 位(2个字节)。
-
现实冲突: 一个 32 位的大件货物,无论如何也塞不进一个 16 位的车厢里。
唯一的解决办法就是“暴力拆解”: 把你这个 32 位的大数据,从中间一刀切开,分成两半(前 16 位和后 16 位)。
-
把一半装进
寄存器 A(比如地址 40012)。 -
把另一半装进紧挨着的
寄存器 B(比如地址 40013)。
当这两个车厢开到 PLC 端时,PLC 端也按照约定的规则,把这两个车厢里的半成品拿出来,用胶水重新“拼”成一个 32 位的数字。这就完成了数据的传输!
3. 代码逐行拆解:这把“刀”是怎么切的?
我们再回头看这段代码,它就是一个标准的“切割 -> 装箱”流水线:
一、socket
1.socket是什么?
socket是网络通信的基石(相当于打电话的电话听筒),它由(ip+端口+传输协议tcp/udp)组成,后台为我们封装成接口api(如连接、发送、接收、关闭)
2.socket原理
承上启下,它在应用程序和复杂的网络操作之间提供了一个简单的“文件式”抽象接口。应用程序只管向这个“文件”读写数据,而所有繁重的工作——如数据打包、寻址、传输、丢包重传、数据排序等——都交由操作系统底层的网络协议栈去处理。
3.socket在项目中应用场景(什么环境下用)
当通信双方使用非HTTP标准协议时。
典型场景:
在你的ModbusTCP代码中(MTH-Project项目),正是因为工业设备需要实时监控和高频控制,所以才使用Socket而不是HTTP。
总结:什么时候用Socket?什么时候用HTTP?
| 方面 | 使用 Socket | 使用 HTTP |
|---|---|---|
| 通信模式 | 实时、双向、长连接 | 请求-响应、短连接 |
| 数据频率 | 高频、持续数据流 | 低频、间歇性请求 |
| 协议 | 自定义协议、专用协议 | 标准HTTP/HTTPS协议 |
| 性能要求 | 低延迟、高实时性 | 可接受一定延迟 |
| 典型场景 | 聊天、游戏、物联网、工业控制 | 网页浏览、REST API、文件下载 |
简单判断标准:
-
如果需要服务器主动推数据给你 → 用Socket
-
如果通信非常频繁 → 用Socket
-
如果使用的是特殊设备协议 → 用Socket
-
如果只是偶尔请求数据 → 用HTTP
-
如果主要做网页开发 → 用HTTP
4.socket怎么使用?
封装modbus通信类调用soket方法进行发送与接收
5.socket有什么优点与缺点?
缺点:
| 特性/方面 | 使用原始 Socket | 使用高级框架 (如 gRPC, HTTP REST) |
|---|---|---|
| 开发效率 | 极低,需要造大量轮子 | 高,开箱即用 |
| 可维护性 | 差,业务与通信耦合 | 好,关注点分离 |
| 功能特性 | 需自行实现所有高级功能 | 内置服务发现、负载均衡、监控等 |
| 性能 | 潜力高,但实现难度大 | 优秀,且稳定可靠 |
| 适用场景 | 底层基础设施、自定义协议、极致性能要求 | 绝大多数业务系统、微服务 |
6.socket你们接收数据是一次性还是分段循环接收?
使用分段循环接收
-
分片接收:Modbus TCP报文可能被分片传输,循环接收确保读取完整报文
-
避免阻塞:通过SleepTime入口睡眠时间和MaxWaitTimes最大控制等待时间,避免无限期阻塞
7.如何保证socket性能高,不堵塞,安全?
在封装类中使用对象锁(lock)或SimpleHybirdLock混合锁
(1)什么是SimpleHybirdLock混合锁
-
SimpleHybirdLock是一个手动实现的、轻量级的线程同步锁。它的设计目标是:在无竞争(没有多个线程同时争抢锁)的情况下,提供非常快的性能(用户模式);而在有竞争时(内核模式),又能保证正确地让线程等待,不会白白消耗CPU资源。
(2)SimpleHybirdLock混合锁 原理了解过吗
线程A持有锁,m_waiters = 1
↓
线程B调用 Enter()
↓
m_waiters: 1 -> 2
↓
判断 m_waiters != 1 -> 是 (这里是用户模式极快)
↓
线程B在 m_waiterLock.WaitOne() 上休眠 (这里是内核模式,线程等待)
---------------------------------------
线程A工作完成,调用 Leave()
↓
m_waiters: 2 -> 1
↓
判断 m_waiters != 0 -> 是
↓
调用 m_waiterLock.Set() 唤醒线程B
↓
线程B被唤醒,成功获取锁,继续执行
总结与类比
你可以把 SimpleHybirdLock 想象成一个智能门卫:
-
平时没人(无竞争):你直接推门就进,门卫都不抬眼看你。(用户模式,极快)
-
里面有人了(有竞争):门卫让你在旁边的等候室(内核对象) 坐下休息(线程休眠),等里面的人出来时,门卫再叫你进去。(内核模式,保证公平且不耗CPU)
(3)在这个ModbusTCP类库中的作用
在这个代码中,SimpleHybirdLock 用于保护 SendAndReceive 方法。这确保了:
-
线程安全:即使多个线程同时向同一个Modbus设备发送请求,它们的请求报文也不会在网络上交织在一起,导致数据混乱。
-
请求/响应完整性:一个请求必须对应一个响应,锁保证了在收到上一个请求的完整响应之前,下一个请求不会开始发送。
这种混合锁的设计,在常见的客户端应用场景中(通常竞争不激烈),
8. 这个ModbusTCP类为什么需要用到Socket?能不能用HttpClient代替?
考察点:Socket vs HTTP 的应用场景理解
通俗解释:
就像打电话和发邮件的区别:
-
Socket(打电话):实时双向通信,建立连接后可以随时收发数据,适合Modbus这种设备控制协议
-
HttpClient(发邮件):每次请求都要建立连接、发送、断开,开销大,不适合高频的设备通信
答案:不能代替。ModbusTCP是专门的工业协议,需要保持长连接和实时通信,而HTTP是短连接、无状态的,不适合这种场景。
9.对Socket超时这一块有没有做处理?
设置了发送和接收超时时间,防止网络故障时程序永远卡住
10. 同步Socket和异步Socket有什么区别?各有什么优缺点?
考察点:Socket编程的两种模式理解
对比:
| 方面 | 同步Socket | 异步Socket |
|---|---|---|
| 编程难度 | 简单直观 | 复杂,需要回调 |
| 线程使用 | 一个连接一个线程 | 少量线程处理大量连接 |
| 性能 | 连接数少时OK | 支持高并发 |
| 资源占用 | 线程资源消耗大 | 资源利用率高 |
// 线程会在这里阻塞,直到数据到达
int bytesRead = socket.Receive(buffer);
// 不会阻塞,有数据时会自动回调
socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None,
new AsyncCallback(ReceiveCallback), socket);
private void ReceiveCallback(IAsyncResult ar)
{
// 数据到达后自动执行这里
int bytesRead = socket.EndReceive(ar);
// 处理数据...
}
11. 什么是Socket的Keep-Alive?它有什么作用?
考察点:长连接维护机制
通俗解释:
就像朋友之间定期发个"在吗?"确认对方还在:
-
没有Keep-Alive:不知道对方什么时候掉线,可能白等
-
有Keep-Alive:定期检查,发现掉线就及时重连
-
// 设置Keep-Alive public void SetKeepAlive(Socket socket, bool on, int interval, int timeout) { byte[] inValue = new byte[12]; BitConverter.GetBytes(on ? 1 : 0).CopyTo(inValue, 0); BitConverter.GetBytes(interval).CopyTo(inValue, 4); // 检查间隔(ms) BitConverter.GetBytes(timeout).CopyTo(inValue, 8); // 超时时间(ms) socket.IOControl(IOControlCode.KeepAliveValues, inValue, null); }
12. 如果Socket的Send方法成功返回,是否意味着对方已经收到数据?
考察点:TCP协议的理解
正确答案:不是
通俗解释:
就像寄信:
-
Send成功 = 你把信投进了邮筒 ✅
-
对方收到 = 收信人实际拿到了信 ❌(可能中途丢失)
实际情况:
-
Send成功只表示数据被本地系统的网络栈接受
-
数据进入发送缓冲区,由操作系统负责发送
-
中间可能经过多个网络节点
-
对方可能因为各种原因没收到(网络故障、对方程序崩溃)
13.TCP三次握手 粘包如何解决:
粘包发生原因:当A发送端向B发送数据时,是划分成数据包的形式,但接收端则是无序的接收,可能接收了一部分,或接收到本包外的数据.
解决办法:1.规定发送协议包字节大小 2.指定字符串符号为包结尾,接收端读取时根据符号判断是否接受完毕 3.定义包头+包体格式 包头是固定大小,并说明包体有多大
二、Modbus协议
1.modbus是什么?
Modbus 是一种用于工业设备的“通用语言”,它让不同的工业设备(比如PLC、传感器、仪表)能够相互理解和交换数据。分为Modbus RTU,ModbusAscll,ModbusTcp,ModbusUdp
核心特点(记住这3点)
-
简单通用
-
协议格式简单,易于开发和实现
-
几乎所有的工业设备都支持,成为事实标准
-
-
主从模式
-
一个主站(如电脑、触摸屏)发问
-
多个从站(如传感器、执行器)回答
-
主站不同,从站不主动说话
-
-
功能码明确
-
问题:列举常用的功能码及其作用?
功能码 名称 作用 数据类型 01 读线圈 读取开关量输出 位(Bit) 02 读输入线圈 读取开关量输入 位(Bit) 03 读保持寄存器 读取可读写寄存器 字(Word) 04 读输入寄存器 读取只读寄存器 字(Word) 05 写单个线圈 设置单个开关量 位(Bit) 06 写单个寄存器 设置单个寄存器 字(Word) 15 写多个线圈 设置多个开关量 位(Bit) 16 写多个寄存器 设置多个寄存器 字(Word)
-
2. 如何使这个ModbusTCP程序同时连接多个设备?
// 方案1:创建多个ModbusTCP实例
var device1 = new ModbusTCP();
var device2 = new ModbusTCP();
device1.Connect("192.168.1.10", 502);
device2.Connect("192.168.1.11", 502);
// 方案2:使用连接池管理多个Socket连接
public class ModbusTCPManager
{
private Dictionary<string, ModbusTCP> _connections = new Dictionary<string, ModbusTCP>();
public ModbusTCP GetConnection(string ip, int port)
{
// 管理多个设备连接
}
}
-
创建多个ModbusTCP实例 ≠ 自动获得多线程(实际还是一个线程)
-
每个实例的Socket操作在调用它的线程中执行
-
要实现真正的并行通信,需要手动创建多个线程/Task
3.串口与Modbus TCP核心区别
| 方面 | 串口通信 | Modbus TCP |
|---|---|---|
| 物理层 | RS-232、RS-485、RS-422等硬件接口 | 以太网(网线、WiFi) |
| 协议层次 | 物理层+数据链路层 | 应用层协议,运行在TCP/IP之上 |
| 连接方式 | 点对点直接连接 | 通过网络IP地址和端口连接 |
| 通信距离 | 较短(通常几十米) | 理论上无限(通过路由器) |
| 典型应用 | 工业设备、传感器、PLC本地连接 | 工业以太网、远程监控、SCADA系统 |
常见的Modbus变种
-
Modbus RTU - 运行在串口上(RS-485最常见)
-
Modbus ASCII - 运行在串口上
-
Modbus TCP - 运行在以太网上
-
Modbus UDP - 运行在以太网上(较少使用)
使用串口通信(Modbus RTU)时:
// 连接本地PLC,通过COM口
SerialPort port = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
port.Open();
// 发送Modbus RTU帧...
使用Modbus TCP时:
// 连接远程设备,通过网络
ModbusTCP client = new ModbusTCP();
client.Connect("192.168.1.100", 502); // 使用Socket
// 发送Modbus TCP帧...
4.modbusTcp用的什么方法通信?
1. 创建对象: soket s1=new soket(AddressFamily.InterNetwork(ip地址),SocketType.Stream(soket传输类型 双向传输stream流),ProtocolType.Tcp(soket支持的协议类型))
2.连接: s1.connect(ip,port)
2.发送: s1.send(array[](发送数据数组),length[](数组长度),SocketFlags.None(发送接收行为))
4. 接收:s1.receive(array[](接收数据数组),SocketFlags.None(发送接收为))
5. Modbus TCP 与 Modbus RTU 报文结构对比
问题:Modbus TCP 和 Modbus RTU 的报文结构有什么不同?
参考答案:
Modbus RTU 报文讲解:

Modbus TCP 报文:
[事务标识][协议标识][长度][单元标识][功能码][数据]
-
RTU 使用CRC校验,TCP 依赖TCP的可靠性
-
TCP 增加了MBAP头(事务标识、协议标识等)
-
TCP 单元标识相当于RTU的从站地址
6.Modbus 异常响应处理
问题:Modbus 设备返回异常时如何处理?
参考答案:
在Modbus协议中:
-
正常响应:功能码保持不变
-
比如请求
0x03(读寄存器),正常响应也是0x03
-
-
异常响应:功能码 = 原始功能码 +
0x80-
比如请求
0x03,异常响应就是0x83
-
-
01 - 非法功能码(不支持的功能)
-
02 - 非法数据地址(地址不存在)
-
03 - 非法数据值(数据范围错误)
-
04 - 从站设备故障
public bool ProcessModbusResponse(byte[] response, byte expectedFunctionCode)
{
//检查Modbus设备是否返回了异常响应。
if (response[1] == (byte)(expectedFunctionCode | 0x80))
{
byte errorCode = response[2];
switch (errorCode)
{
case 0x01: Console.WriteLine("非法功能码"); break;
case 0x02: Console.WriteLine("非法数据地址"); break;
case 0x03: Console.WriteLine("非法数据值"); break;
default: Console.WriteLine($"设备异常: {errorCode}"); break;
}
return false;
}
return true;
}
7. 线圈与寄存器的区别
问题:Modbus 中的线圈和寄存器有什么区别?
参考答案:
| 方面 | 线圈 | 寄存器 |
|---|---|---|
| 数据类型 | 位(Bit) | 字(Word) |
| 大小 | 1位 | 16位 |
| 取值范围 | 0/1 | 0-65535 |
| 功能码 | 01,02,05,15 | 03,04,06,16 |
| 典型应用 | 开关、继电器 | 温度、压力、计数器 |
8. 当需要读取大量 Modbus 数据时,如何优化性能?
1.批量读取:合并小请求为大请求,减少通信次数
// 低效做法:读取10个寄存器要发10次请求
ushort value1 = ReadHoldingRegisters(0, 1); // 第1次请求
ushort value2 = ReadHoldingRegisters(1, 1); // 第2次请求
ushort value3 = ReadHoldingRegisters(2, 1); // 第3次请求
// ... 总共10次请求
//优化后:
public Dictionary<ushort, ushort> ReadMultipleRegisters(ushort start, ushort count)
{
// 高效做法:1次请求读取多个寄存器
byte[] data = ReadHoldingRegisters(start, count);
var result = new Dictionary<ushort, ushort>();
for (ushort i = 0; i < count; i++)
{
// 假设每个寄存器2个字节,解析数据
ushort value = (ushort)((data[i * 2] << 8) | data[i * 2 + 1]);
result[(ushort)(start + i)] = value;
}
return result;
}
// 使用示例:1次请求读取10个寄存器
var registers = ReadMultipleRegisters(0, 10);
ushort value1 = registers[0]; // 地址0的值
ushort value2 = registers[1]; // 地址1的值
// ... 从字典中获取所有值
2.分块读取:大请求拆分为小请求,避免超时
// 危险做法:一次性读取500个寄存器
byte[] data = ReadHoldingRegisters(0, 500); // 可能超时!
//优化后
public List<byte[]> ReadLargeData(ushort start, ushort totalCount, ushort chunkSize = 100)
{
var results = new List<byte[]>();
// 分段读取:500个寄存器分成5次读取,每次100个
for (ushort i = 0; i < totalCount; i += chunkSize)
{
// 计算当前块要读多少个寄存器
ushort currentCount = (ushort)Math.Min(chunkSize, totalCount - i);
// 读取当前块
var chunk = ReadHoldingRegisters((ushort)(start + i), currentCount);
results.Add(chunk);
}
return results;
}
// 使用示例:分块读取500个寄存器
var allData = ReadLargeData(0, 500, 100);
// 返回5个数据块,每个块包含100个寄存器的数据
3.缓存机制:避免重复读取相同数据,提高响应速度
// 重复读取:程序多次读取同一个地址
ushort temperature = ReadHoldingRegisters(100, 1); // 第1次读取
// ... 一些其他操作
temperature = ReadHoldingRegisters(100, 1); // 第2次读取(重复!)
// 缓存项类
public class CacheItem
{
public ushort Value { get; set; }
public DateTime Timestamp { get; set; }
public CacheItem(ushort value, DateTime timestamp)
{
Value = value;
Timestamp = timestamp;
}
}
private Dictionary<ushort, CacheItem> registerCache = new Dictionary<ushort, CacheItem>();
public ushort ReadCachedRegister(ushort address)
{
// 检查缓存中是否有这个地址的数据
if (registerCache.TryGetValue(address, out var item) &&
DateTime.Now - item.Timestamp < TimeSpan.FromSeconds(5))
{
// 缓存命中:数据在5秒内读取过,直接返回缓存值
return item.Value;
}
// 缓存未命中:从设备实际读取
var value = ReadHoldingRegisters(address, 1);
// 更新缓存
registerCache[address] = new CacheItem(value, DateTime.Now);
return value;
}
9.modbus协议与串口的区别?(串口属于modbus的一种吗)
不,这个说法完全反了。串口不属于Modbus,恰恰相反,Modbus可以使用串口作为其物理传输介质之一。
这是一个非常常见的概念混淆点。让我们来清晰地分解一下:
核心关系:载体 vs. 协议
您可以这样理解:
-
串口:像一条“公路”
-
它只定义了车辆(数据)如何行驶的基础规则:比如公路有多宽(数据位)、靠左还是靠右行驶(停止位)、车速限制(波特率)。
-
它不关心车上装的是什么货物(具体的数据含义),也不关心货物要运给谁(设备地址)。
-
-
Modbus:像一套“交通规则和货物编码标准”
-
它定义了:如何识别收货人(设备地址)、要执行什么操作(功能码,如读、写)、操作哪个位置(寄存器地址)、以及具体的数据(寄存器值)。
-
这套规则既可以通过公路(串口)来运输,也可以通过铁路(TCP/IP网络)来运输。
-
详细对比
| 特性 | 串口 | Modbus |
|---|---|---|
| 本质 | 物理层/数据链路层协议 | 应用层通信协议 |
| 角色 | 传输介质/载体 | 通信规则/语言 |
| 定义内容 | 电气特性、波特率、数据位、停止位、流控 | 报文结构、功能码、寄存器映射、错误校验 |
| 关心的问题 | 比特流如何准确地在线上传输? | 如何请求线圈状态?如何读取保持寄存器的值? |
| 类比 | 公路系统 | 货运规则和货物清单格式 |
Modbus如何工作在串口上?(Modbus RTU)
当Modbus使用串口(通常是RS-232或RS-485)时,我们称之为 Modbus RTU 模式。
-
物理连接:设备通过RS-485串口线连接起来。
-
配置参数:所有设备必须设置相同的波特率、数据位、停止位等。
-
通信过程:
-
主站发送一个报文帧,这个帧包含了遵循Modbus规则的信息:
[从站地址] [功能码] [寄存器地址] [数据] [CRC校验]。 -
这个帧被转换成二进制的比特流,通过串口线物理地发送出去。
-
所有从站都通过串口线收到这个比特流。
-
只有地址匹配的从站会处理这个Modbus请求,并按照Modbus协议规则回复一个响应帧。
-
所以,串口是Modbus RTU的“腿”,负责跑腿送信,而Modbus是信的“内容格式”,规定了信里写的是什么、该怎么理解。
总结与类比
| 组合方式 | 类比 |
|---|---|
| 串口 + Modbus协议 | 公路 + 标准货运规则 = Modbus RTU |
| TCP/IP网络 + Modbus协议 | 互联网 + 标准货运规则 = Modbus TCP |
| 串口 + 自定义协议 | 公路 + 你自己发明的暗号(其他设备听不懂) |
结论:
-
串口是一种基础的、底层的通信载体。
-
Modbus是一种构建在底层载体之上的、高级的通信语言。
因此,正确的说法是:Modbus RTU 是一种基于串口通信的协议,而不是“串口属于Modbus”。
10.Modbus在c#中数据顺序性问题
(1).ushort类型转float类型

(2).Bool数组转byte类型

(3).为什么32位寄存器,需要转成2个ushort数组再写入(mes监控项目重点回顾)
// 核心难点:32位整数需要拆分成 2个 16位的 ushort
byte[] bytes = BitConverter.GetBytes(value);
ushort[] registers = new ushort[2];
// 【注意】:这里涉及字节序 (Endianness),目前使用最常见的 C D A B (低字在前,高字在后)
registers[0] = BitConverter.ToUInt16(bytes, 0); // 低 16 位
registers[1] = BitConverter.ToUInt16(bytes, 2); // 高 16 位 我想知道为什么要这么做分成16位short
之所以必须把 Int32 (32位整数) 拆分成 2 个 ushort (16位短整数),是因为 Modbus 协议的“先天基因限制”。
我们可以通过一个“快递装箱”的比喻和底层的原理来彻底弄懂它:
1. 根本原因:Modbus 协议的“柜子”太小了
Modbus 协议诞生于 1979 年,那个时候计算机和 PLC 的主流还是 16 位架构。 在 Modbus 的协议规定中,用来存储数据的基础单元叫 “保持寄存器 (Holding Register)”。 1 个寄存器,死规定了只能装 16 位(2个字节)的数据。 无论你用什么语言、什么类库,发给 PLC 的物理电信号里,每个格子的容量就是 16 位(即 C# 里的 ushort)。
2. “大件货物拆包”的比喻
-
C# 里的
Int32(你要下发的值): 它是一个 32 位(4个字节)的大件货物。比如你想下发一个产量数据100,000。 -
Modbus 的运输车(寄存器): 它的每个车厢(寄存器)只能装 16 位(2个字节)。
-
现实冲突: 一个 32 位的大件货物,无论如何也塞不进一个 16 位的车厢里。
唯一的解决办法就是“暴力拆解”: 把你这个 32 位的大数据,从中间一刀切开,分成两半(前 16 位和后 16 位)。
-
把一半装进
寄存器 A(比如地址 40012)。 -
把另一半装进紧挨着的
寄存器 B(比如地址 40013)。
当这两个车厢开到 PLC 端时,PLC 端也按照约定的规则,把这两个车厢里的半成品拿出来,用胶水重新“拼”成一个 32 位的数字。这就完成了数据的传输!
3. 代码逐行拆解:这把“刀”是怎么切的?
我们再回头看这段代码,它就是一个标准的“切割 -> 装箱”流水线:
// 【第一步:化整为零】
// 这一步把 32位的 int,打碎成了 4个独立的 byte (字节)。
// 假设你的数字对应字节是 [字节0, 字节1, 字节2, 字节3]
byte[] bytes = BitConverter.GetBytes(value);
// 【第二步:准备车厢】
// 准备两个 Modbus 寄存器车厢 (1个 ushort 能装 2 个字节)
ushort[] registers = new ushort[2];
// 【第三步:装车厢 A (低 16 位)】
// 把刚才的 字节0 和 字节1 捏在一起,变成一个 16 位的数字,装进第 1 个车厢
registers[0] = BitConverter.ToUInt16(bytes, 0);
// 【第四步:装车厢 B (高 16 位)】
// 把剩下的 字节2 和 字节3 捏在一起,变成另一个 16 位的数字,装进第 2 个车厢
registers[1] = BitConverter.ToUInt16(bytes, 2);
// 最后,通过 WriteMultipleRegisters 方法,把这两个车厢连在一起发给 PLC
4. 为什么注释里要强调“字节序 (C D A B)”?
既然是把大件拆成了两半,就涉及到一个“先发哪一半”的问题。
-
如果在 C# 端,你先把“头部”装进了车厢 A,把“尾巴”装进了车厢 B。
-
但 PLC 那个厂家比较奇葩,它非要先拿“尾巴”再拿“头部”。
-
结果就是:拼出来的数字完全面目全非(比如把
1000拼成了12582912)。这就是工业界经常听到的“高低字反转”。
这段代码中的写法(先装第0位,再装第2位),对应的是底层的 CDAB 格式(Little-Endian 的一种变体),这是目前大部分国产 PLC 和仪表(如信捷、台达)最常用的拼接顺序。如果拼错乱码了,只需要把 registers[0] 和 registers[1] 的取值顺序对调一下,就解决了。
11.Modbus写读时注意事项?
(1)在读写32位寄存器时注意入参数值

12.主从概念及储存器代号(龙马Modbus通讯协议视频)
modbusTcp:客户端(主)->服务器(从)
modbusRtu:PC/上位机(主)->传感器/IO信号
储存区(代号演变)
只读布尔-输入布尔-输入状态(0x01)
只读数字-输入寄存器(0x03)
读写布尔-输出布尔-线圈状态(0x00)
读写数字-输出寄存器--保持寄存器(0x04)
数据一般存在slave(从)中
13.大小端知识
如返回错误码会将高位置为1 例:1000 0000
大端小端:
c#默认小端储存:
数组[2C 01 00 00]
数据高位放在高位地址是小端:2C 01 00 00
数据高位放在低位地址是大端:00 00 2C 01
三、串口
1.串口通信完整流程
-
第一阶段:准备工作
1. 确定通信参数(双方约定好)
// 就像打电话前要先约定用什么语言、语速
string portName = "COM1"; // 串口号
int baudRate = 9600; // 波特率(通信速度)
Parity parity = Parity.None; // 校验位(检错)
int dataBits = 8; // 数据位(每个字符的位数)
StopBits stopBits = StopBits.One; // 停止位(帧结束标志)
2. 初始化串口对象
SerialPort serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
第二阶段:建立连接
3. 打开串口连接
try
{
serialPort.Open(); // 建立物理连接
// 此时通信链路就绪
}
catch (Exception ex)
{
// 处理打开失败(端口被占用、不存在等)
}
第三阶段:数据通信
4. 发送数据
// 方法1:发送字符串
serialPort.Write("Hello Device!");
// 方法2:发送字节数组
byte[] data = new byte[] { 0x01, 0x02, 0x03 };
serialPort.Write(data, 0, data.Length);
5. 接收数据
csharp
// 方式1:事件驱动(推荐)
serialPort.DataReceived += (sender, e) =>
{
SerialPort sp = (SerialPort)sender;
int bytesToRead = sp.BytesToRead;
byte[] buffer = new byte[bytesToRead];
sp.Read(buffer, 0, bytesToRead);
// 处理接收到的数据
};
// 方式2:轮询方式
while (true)
{
if (serialPort.BytesToRead > 0)
{
byte[] buffer = new byte[serialPort.BytesToRead];
serialPort.Read(buffer, 0, buffer.Length);
// 处理数据
}
Thread.Sleep(10);
}
第四阶段:关闭连接
6. 关闭串口
serialPort.Close(); // 释放资源,断开连接
2. 同步和异步接收串口数据有什么区别?(MTH项目中遇到,算是难点)如何选择?
考察点:串口数据接收模式
对比回答:
| 方式 | 实现方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 同步 | Read()、ReadByte() |
简单直观 | 阻塞线程 | 简单应用,数据量小 |
| 异步 | DataReceived 事件 |
不阻塞,实时性好 | 编程复杂 | 工业控制,实时监控 |
// 异步方式(推荐)
//new byte[serialPort.BytesToRead]目的:创建一个大小正好合适的字节数组,用来存放当前接收缓冲区中的所有数据。
serialPort.BytesToRead 是一个属性,它返回当前在串口接收缓冲区中等待读取的字节数。
serialPort.DataReceived += (s, e) => {
byte[] data = new byte[serialPort.BytesToRead];
byte[] data = new byte[serialPort.BytesToRead];
serialPort.Read(data, 0, data.Length);
// 处理数据...
};
// 同步方式
while (true)
{
if (serialPort.BytesToRead > 0)
{
byte[] data = new byte[serialPort.BytesToRead];
serialPort.Read(data, 0, data.Length);
}
Thread.Sleep(100);
}
-
你的项目中用的异步吗,有什么要注意的?
是的,要注意线程安全问题,在后台线程中不能直接修改UI控件,否则程序会崩溃。所以我们调用ui线程需跨线程调用
注意:DataReceived 事件在后台线程中触发,不是UI线程。
错误做法:
serialPort.DataReceived += (s, e) => {
string data = serialPort.ReadExisting();
textBox1.Text = data; // ❌ 跨线程访问UI控件,会崩溃!
};
正确做法:
serialPort.DataReceived += (s, e) => {
string data = serialPort.ReadExisting();
// 方法1:使用Invoke这段代码检查当前是否在UI线程,如果不是,就通过 Invoke 方法"把修改操作交给UI线程去执行"。(WinForms)
if (textBox1.InvokeRequired)
{
//对UI线程说"拜托你帮我把这个文本框的内容改一下"
textBox1.Invoke(new Action(() => textBox1.Text = data));
}
else
{
textBox1.Text = data;
}
// 方法2:使用Dispatcher(WPF)
// Dispatcher.BeginInvoke(new Action(() => textBox1.Text = data));
};
2. 实际项目中的故障排查(串口)
问题:ModbusRTU 通信失败时,如何进行故障排查?
参考答案:
排查步骤:
-
检查物理连接
-
线缆是否完好
-
接口是否松动
-
终端电阻是否正确
-
-
检查参数配置
// 确认所有参数一致 string portName = "COM1"; // 串口号 int baudRate = 9600; // 波特率 Parity parity = Parity.Even; // 校验位 int dataBits = 8; // 数据位 StopBits stopBits = StopBits.One; // 停止位 byte slaveId = 1; // 从站地址
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)