上位机开发-MODBUS面试常见问题

最近在指导同学面试C++上位机岗位时,经常被问到modbus协议相关的问题,特整理如下。准备做C++上位机开发的同学,可以详细学习一下。

一、RS232 和 RS485 的本质区别

1.RS232:适合"一台上位机对一台设备"

RS232 可以理解成:

两个人打电话,一根线主要负责你说,一根线主要负责我说。

典型连接方式:

bash 复制代码
上位机 RS232口                    下位机 RS232口
┌─────────────┐                ┌─────────────┐
│   TXD 发送   │──────────────▶│   RXD 接收   │
│   RXD 接收   │◀──────────────│   TXD 发送   │·
│   GND 地线   │───────────────│   GND 地线   │
└─────────────┘                └─────────────┘

注意:

RS232 的 TX 要接对方的 RX,RX 要接对方的 TX,也就是交叉连接

它的特点是:

|----------|----------------|
| 特点 | RS232 |
| 通信方式 | 点对点 |
| 常见连接数量 | 1 个上位机连 1 个下位机 |
| 线缆 | TX、RX、GND |
| 抗干扰能力 | 一般 |
| 通信距离 | 较短,工程上通常几米到十几米 |
| 是否适合工业现场 | 一般 |
| 是否适合多设备 | 不适合 |

所以 RS232 比较适合:

bash 复制代码
电脑 / 上位机  ─────  单台仪表 / 单片机 / PLC

比如:

bash 复制代码
上位机 ──RS232── 温控器
上位机 ──RS232── 电源模块
上位机 ──RS232── 单片机板子

2.RS485:适合"一台上位机对多台设备"

RS485 的特点是:

|----------|-------------------------|
| 特点 | RS485 |
| 通信方式 | 总线式 |
| 常见连接数量 | 1 个上位机连多个下位机 |
| 线缆 | A、B,最好再接 GND |
| 抗干扰能力 | 强 |
| 通信距离 | 较长,工程上常用于几十米到上百米,低速时可更远 |
| 是否适合工业现场 | 很适合 |
| 是否适合多设备 | 适合 |

RS485 特别适合工业现场,比如:

bash 复制代码
上位机 ──RS485── PLC1
          ├────── 温控器
          ├────── 电表
          ├────── 变频器
          └────── IO模块

二、最核心的区别:RS232 是点对点,RS485 是总线

可以用这张图理解:

cpp 复制代码
RS232:

上位机 ───────── 下位机
        一对一通信


RS485:

              ┌── 下位机1 地址=1
              │
上位机 ────────┼── 下位机2 地址=2
              │
              ├── 下位机3 地址=3
              │
              └── 下位机4 地址=4
        一对多通信

RS232 更像:

bash 复制代码
我只和你一个人聊天

RS485 更像:

bash 复制代码
我在微信群里点名发消息
谁被点到,谁回复
其他人保持沉默

三、Modbus 使用 RS232 和 RS485 时有没有区别?

结论先说

Modbus 协议本身基本没有区别。

比如你要读保持寄存器:

bash 复制代码
读 1号设备 的 40001 寄存器,读 2 个

无论底层是 RS232 还是 RS485,Modbus 的核心含义都是一样的:

bash 复制代码
设备地址 + 功能码 + 寄存器地址 + 数量 + CRC

典型 Modbus RTU 帧:

bash 复制代码
┌────────┬────────┬────────────┬────────┬──────┐
│ 设备地址 │ 功能码  │ 数据内容      │ 数据长度 │ CRC  │
└────────┴────────┴────────────┴────────┴──────┘

例如:

bash 复制代码
01 03 00 00 00 02 C4 0B

大概意思是:

bash 复制代码
01      设备地址:1号设备
03      功能码:读保持寄存器
0000    起始寄存器地址
0002    读取2个寄存器
C40B    CRC校验

但是,使用习惯上有区别

1. RS232 通常只连一个设备

bash 复制代码
上位机 ──RS232── 下位机

虽然 Modbus RTU 帧里面仍然有"设备地址",但是因为只有一台设备,所以工程上通常就固定为:

复制代码
设备地址 = 1

上位机代码里可能经常这样写:

复制代码
modbus_set_slave(ctx, 1);
modbus_read_registers(ctx, 0, 10, regs);

2. RS485 通常会挂多个设备

复制代码
上位机 ──RS485总线── 1号设备
                   ├─ 2号设备
                   ├─ 3号设备
                   └─ 4号设备

这时候设备地址就非常重要。

复制代码
读1号设备:
地址=1,功能码=03

读2号设备:
地址=2,功能码=03

读3号设备:
地址=3,功能码=03

上位机轮询时一般是:

复制代码
读设备1
等设备1回复

读设备2
等设备2回复

读设备3
等设备3回复

读设备4
等设备4回复

也就是:

复制代码
上位机:1号,把温度发给我
1号设备:当前温度 25.3℃

上位机:2号,把压力发给我
2号设备:当前压力 0.6MPa

上位机:3号,把转速发给我
3号设备:当前转速 1200rpm

四、RS232 和 RS485 使用 Modbus 时,代码层面有什么区别?

从上位机代码看,主要区别不是 Modbus 命令,而是串口参数和设备数量。

1. 相同点

都要配置这些串口参数:

复制代码
串口号:COM1 / COM3 / ttyS0 / ttyUSB0
波特率:9600 / 19200 / 115200
数据位:8
校验位:N / E / O
停止位:1 / 2
协议格式:Modbus RTU

常见配置:

复制代码
9600, 8, N, 1
19200, 8, E, 1
115200, 8, N, 1

也就是:

复制代码
波特率:9600
数据位:8
校验位:无校验
停止位:1

2. 不同点

|------------|------------------------|------------------------|
| 项目 | RS232 + Modbus | RS485 + Modbus |
| 设备数量 | 通常 1 台 | 可挂多台 |
| 设备地址 | 通常固定 1 | 每台设备必须不同 |
| 接线 | TX/RX/GND | A/B/GND |
| 通信模式 | 全双工常见 | 两线半双工常见 |
| 轮询方式 | 简单 | 需要按设备地址轮询 |
| 现场问题 | TX/RX接反、波特率不对 | A/B接反、终端电阻、地址冲突、总线干扰 |
| 软件复杂度 | 低 | 更高 |


五、RS485 为什么容易出现"冲突"?

RS485 常见是半双工通信

半双工的意思是:

复制代码
同一时间,要么上位机说话,要么下位机说话。
不能两边同时说。

像对讲机:

复制代码
你说话的时候,我不能说;
我说话的时候,你不能说。

RS485 半双工通信过程:

复制代码
① 上位机发送请求
   上位机 ─────────▶ 下位机

② 上位机停止发送,切换到接收
   上位机 ◀───────── 下位机

③ 下位机回复数据

如果两个设备同时说话:

复制代码
上位机:我要读寄存器!
下位机:我正在回复!
另一个设备:我也回复!

结果:总线数据混乱,CRC错误,超时,读不到数据。

这就是 RS485 上常见的通信冲突。


六、多个线程能不能同时使用 Modbus?

结论:同一个串口、同一条 RS485 总线,不要让多个线程同时发送 Modbus 请求。

否则很容易出现问题。

比如有三个线程:

复制代码
线程A:读取温度
线程B:读取压力
线程C:写入控制命令

如果它们同时操作同一个串口:

复制代码
线程A发送:01 03 00 00 00 02 ...
线程B发送:02 03 00 10 00 02 ...
线程C发送:03 06 00 20 00 01 ...

最坏的情况是数据被交叉写入串口:

复制代码
01 03 02 03 00 00 03 06 00 10 ...

下位机收到之后根本不知道这是哪条命令。


七、为什么多个线程同时 Modbus 会出问题?

1. 串口是"字节流",不是消息队列

串口不像 HTTP 请求那样天然一条一条分开。

串口看到的是连续字节:

复制代码
01 03 00 00 00 02 C4 0B 02 03 00 10 00 02 ...

如果多个线程同时写,就可能变成:

复制代码
01 03 02 03 00 00 00 10 00 02 ...

这样一条 Modbus 帧就被破坏了。


2. Modbus RTU 一般是"一问一答"

典型流程:

复制代码
发送请求
   ↓
等待回复
   ↓
收到回复后,校验CRC
   ↓
解析数据
   ↓
再发送下一条请求

也就是说,正常应该是:

复制代码
请求1 ──▶ 回复1
请求2 ──▶ 回复2
请求3 ──▶ 回复3

而不是:

复制代码
请求1 ──▶
请求2 ──▶
请求3 ──▶
回复1 ◀──
回复2 ◀──
回复3 ◀──

Modbus RTU 没有像 Modbus TCP 那样的事务 ID,无法很好地区分"这个回复到底属于哪个线程的请求"。


八、正确的软件设计方式

推荐两种方式。


方案一:简单项目,用 mutex 加锁

如果项目比较简单,可以用一把锁保护 Modbus。

复制代码
线程A ─┐
线程B ─┼── mutex ── Modbus串口 ── 下位机
线程C ─┘

意思是:

复制代码
谁拿到锁,谁使用 Modbus;
没拿到锁的线程,等待。

伪代码:

cpp 复制代码
std::mutex modbusMutex;

bool readTemperature()
{
    std::lock_guard<std::mutex> lock(modbusMutex);

    // 1. 设置从站地址
    modbus_set_slave(ctx, 1);

    // 2. 发送Modbus请求,并等待回复
    uint16_t regs[2];
    int rc = modbus_read_registers(ctx, 0, 2, regs);

    // 3. 解析结果
    return rc == 2;
}

bool writeSpeed(int speed)
{
    std::lock_guard<std::mutex> lock(modbusMutex);

    modbus_set_slave(ctx, 2);

    int rc = modbus_write_register(ctx, 10, speed);

    return rc == 1;
}

关键点是:

复制代码
加锁范围必须包含:
设置设备地址
发送请求
等待回复
解析结果

不能只锁发送,不锁接收。

错误做法:

复制代码
// 错误示意
lock();
sendRequest();
unlock();

readResponse();

为什么错?

因为你刚发完请求,锁就释放了,另一个线程马上又发送了第二条请求。然后两个线程都在等回复,回复就可能乱套。

正确做法:

复制代码
lock();

sendRequest();
readResponse();
parseResponse();

unlock();

方案一的优点

复制代码
简单
改造成本低
适合小项目

方案一的缺点

复制代码
如果某个线程等待超时,会占用锁
如果在UI线程里调用,界面可能卡死
复杂项目不好管理优先级、重试、超时

所以工业上位机项目里,更推荐第二种方式。


方案二:推荐做法,单独建立 Modbus 通信线程

比较规范的架构是:

复制代码
┌──────────────┐
│   UI界面线程   │
└──────┬───────┘
       │ 操作命令
       ▼
┌──────────────┐
│   业务逻辑层   │
└──────┬───────┘
       │ 读温度 / 写参数 / 读状态
       ▼
┌─────────────────────────┐
│     Modbus请求队列        │
│  read dev1 reg0          │
│  read dev2 reg10         │
│  write dev3 reg20        │
└──────────┬──────────────┘
           ▼
┌─────────────────────────┐
│   ModbusWorker通信线程    │
│   一次只处理一个请求       │
└──────────┬──────────────┘
           ▼
┌─────────────────────────┐
│      RS232 / RS485        │
└──────────┬──────────────┘
           ▼
┌─────────────────────────┐
│       下位机 / PLC / 仪表  │
└─────────────────────────┘

这个架构的核心思想是:

整个系统只有一个地方真正操作串口。
其他线程不能直接碰串口,只能提交任务。


九、Qt 上位机中推荐的 Modbus 架构

如果你使用 Qt,可以这样设计:

图示:

复制代码
┌────────────────────┐
│ MainWindow UI线程    │
│ 按钮、表格、曲线显示   │
└─────────┬──────────┘
          │ 信号
          ▼
┌────────────────────┐
│ ModbusManager       │
│ 封装读写接口          │
└─────────┬──────────┘
          │ 请求入队
          ▼
┌────────────────────┐
│ ModbusWorker线程     │
│ open/read/write      │
│ timeout/retry/CRC    │
└─────────┬──────────┘
          │ 串口
          ▼
┌────────────────────┐
│ RS485设备1/2/3/4     │
└────────────────────┘

这样做有几个好处:

复制代码
1. UI线程不会卡死
2. 串口不会被多个线程同时写
3. 所有超时、重试、日志都集中管理
4. 方便做轮询
5. 方便处理优先级

十一、RS485 多设备轮询应该怎么做?

假设你有 4 个设备:

复制代码
1号:温度模块
2号:压力模块
3号:流量模块
4号:变频器

推荐轮询方式:

复制代码
while (running)
{
    读1号设备温度
    等回复

    读2号设备压力
    等回复

    读3号设备流量
    等回复

    读4号设备状态
    等回复

    休眠一小段时间
}

图示:

复制代码
时间轴:

上位机: 读1号 ─ 等1号 ─ 读2号 ─ 等2号 ─ 读3号 ─ 等3号 ─ 读4号 ─ 等4号
设备1:        回复
设备2:                      回复
设备3:                                    回复
设备4:                                                  回复

不要这样:

复制代码
线程A一直读1号
线程B一直读2号
线程C一直读3号
线程D一直读4号

如果它们都直接操作同一个串口,就容易冲突。


十二、控制命令和轮询采集同时存在,怎么办?

这是上位机项目里非常常见的问题。

比如:

复制代码
后台轮询:一直读取温度、压力、状态
用户操作:突然点击"启动电机"
报警处理:突然需要写入急停命令

这时候不能让三个线程同时发 Modbus。

正确做法是设计优先级队列

复制代码
高优先级:急停、停止、关键控制命令
中优先级:用户手动写参数
低优先级:普通周期轮询

图示:

复制代码
┌────────────────────────┐
│ 高优先级队列             │  急停 / 停止 / 启动
└──────────┬─────────────┘
           │
┌──────────▼─────────────┐
│ 中优先级队列             │  写参数 / 手动读取
└──────────┬─────────────┘
           │
┌──────────▼─────────────┐
│ 低优先级队列             │  周期轮询
└──────────┬─────────────┘
           ▼
┌────────────────────────┐
│ ModbusWorker 一次处理一个 │
└────────────────────────┘

处理原则:

复制代码
1. 正在发送的一条Modbus请求不能被打断
2. 当前请求完成后,优先处理高优先级命令
3. 普通轮询可以延后
4. 控制命令要有超时和结果反馈

十三、实际工程中容易踩的坑

1. RS485 的 A/B 接反

现象:

复制代码
完全收不到数据
一直超时
CRC错误

解决:

复制代码
交换 A/B 线试一下

因为不同厂家的 A/B 标注有时不完全统一,有的叫 A/B,有的叫 D+/D-。


2. 多个 RS485 设备地址重复

比如:

复制代码
设备1 地址=1
设备2 地址=1

上位机问:

复制代码
1号设备,你的数据是多少?

结果两个设备同时回复:

复制代码
设备1:我回复
设备2:我也回复

结果就是:

复制代码
总线冲突,数据乱掉

所以 RS485 多设备时,必须保证:

复制代码
每个设备地址唯一

3. 没有终端电阻

RS485 长距离通信时,通常需要在总线两端加终端电阻。

典型示意:

复制代码
       120Ω                             120Ω
A ────/\/\/\────┬────────┬────────┬────/\/\/\────
B ────/\/\/\────┴────────┴────────┴────/\/\/\────
             设备1      设备2      设备3

更准确地说,终端电阻是接在 A 和 B 之间,通常在总线两端各一个:

复制代码
       120Ω                         120Ω
A ─────┬────────设备1────设备2────设备3────┬────
       │                                  │
B ─────┴──────────────────────────────────┴────

短距离、低速时可能不明显;距离长、干扰强、波特率高时,不加终端电阻容易出问题。


4.星形接法导致通信不稳定

RS485 推荐总线型,也就是一根主干线串过去:

不推荐星形:

星形接法在短距离可能能用,但距离变长、波特率提高后,容易出现反射和干扰。


5. UI线程直接读 Modbus,导致界面卡死

错误方式:

复制代码
void MainWindow::on_readButton_clicked()
{
    modbus_read_registers(ctx, 0, 10, regs); // 可能阻塞
}

如果下位机不回复,界面就可能卡住几百毫秒甚至几秒。

推荐:

复制代码
UI线程只发请求
Modbus线程负责通信
通信完成后用信号通知UI刷新

十五、最终结论

可以记住这几句话:

第一,RS232 和 RS485 是物理通信接口,Modbus 是通信协议。

复制代码
RS232 / RS485 解决的是:数据怎么通过线传过去
Modbus 解决的是:数据内容怎么组织、怎么解释

第二,Modbus 用 RS232 和 RS485 时,协议命令基本一样。

复制代码
功能码03还是读保持寄存器
功能码06还是写单个寄存器
功能码16还是写多个寄存器

区别主要在:

复制代码
RS232 通常一对一
RS485 通常一对多
RS485 更依赖设备地址
RS485 更要注意总线冲突、终端电阻、A/B接线

第三,同一个串口、同一条 RS485 总线,不要多个线程同时发 Modbus。

正确方式是:

复制代码
要么用 mutex 把完整的一问一答锁住
要么更推荐使用单独的 ModbusWorker 通信线程 + 请求队列

最推荐的工业上位机架构是:

复制代码
UI线程 / 业务线程
        │
        ▼
Modbus请求队列
        │
        ▼
Modbus通信线程
        │
        ▼
RS232 / RS485
        │
        ▼
下位机设备

基于 TCP 的 Modbus,比串口 Modbus 更适合多线程/并发,但不等于可以让多个线程随便同时操作同一个连接。

一句话结论:

Modbus TCP 可以支持多个线程发请求,但同一个 socket / 同一个 Modbus 客户端对象(主站)最好仍然串行化;多个 TCP 连接可以并发,但要看下位机是否支持,也要避免业务层面的读写冲突。


十六、Modbus RTU 和 Modbus TCP 的核心区别

之前讲 RS232/RS485 时,Modbus RTU 是这样:

复制代码
Modbus RTU:
设备地址 + 功能码 + 数据 + CRC

比如:

复制代码
01 03 00 00 00 02 C4 0B

而 Modbus TCP 是这样:

复制代码
Modbus TCP:
MBAP头 + 功能码 + 数据

大概结构:

复制代码
┌──────────────┬────────┬────────────┐
│ MBAP头        │ 功能码  │ 数据内容     │
└──────────────┴────────┴────────────┘

MBAP 头里面有一个非常重要的字段:

复制代码
Transaction ID,事务ID

它的作用类似:

复制代码
请求1:编号 1001
请求2:编号 1002
请求3:编号 1003

服务器回复时也带着这个编号:

复制代码
回复1001:对应请求1001
回复1002:对应请求1002
回复1003:对应请求1003

所以理论上,Modbus TCP 比 Modbus RTU 更容易做并发。


十七、多个线程是否可以同时使用基于TCP的Modbus?

分 3 种情况看。


情况一:多个线程共享同一个 TCP 连接

比如:

复制代码
线程A ─┐
线程B ─┼── 同一个 Modbus TCP Client ── TCP连接 ── PLC
线程C ─┘

这种情况下,不建议多个线程直接同时调用同一个 Modbus 客户端对象

原因有三个。


1. 同一个 socket 本质上还是一条字节流

TCP 也是字节流。

如果多个线程同时写同一个 socket:

复制代码
线程A:发送请求A
线程B:发送请求B
线程C:发送请求C

如果底层库没有做好保护,就可能出现:

复制代码
请求A、请求B、请求C 的数据交叉写入

虽然 TCP 不会丢字节,但你的应用层数据包可能会乱。


2. Modbus 客户端库不一定是线程安全的

很多 Modbus 库内部会有:

复制代码
当前请求缓冲区
当前响应缓冲区
当前事务ID
socket句柄
超时时间
连接状态

如果多个线程同时调用:

复制代码
modbus_read_registers(...)
modbus_write_register(...)
modbus_read_input_registers(...)

就可能把内部状态搞乱。

所以即使 Modbus TCP 协议本身有 Transaction ID,也不代表你用的库就一定支持多线程并发调用。


3. 很多下位机并不真正支持"并发处理"

有些 PLC、网关、控制器虽然是 TCP,但内部处理逻辑仍然是:

复制代码
收到一个请求
处理
回复
再处理下一个请求

如果你并发发很多请求,可能出现:

复制代码
响应变慢
超时
返回异常码
连接被断开

所以共享同一个 TCP 连接时,推荐仍然这样设计:

复制代码
多个线程请求
    │
    ▼
加锁 / 请求队列
    │
    ▼
一个 Modbus TCP Client 顺序处理
    │
    ▼
PLC / 下位机

4、同一个 TCP 连接的推荐做法

方案一:加 mutex

简单项目可以这样:

cpp 复制代码
std::mutex modbusMutex;

bool readTemperature()
{
    std::lock_guard<std::mutex> lock(modbusMutex);

    // 这一整段必须锁住
    // 包括发送请求、等待回复、解析数据
    int rc = modbus_read_registers(ctx, 0, 10, regs);

    return rc == 10;
}

bool writeSpeed(int speed)
{
    std::lock_guard<std::mutex> lock(modbusMutex);

    int rc = modbus_write_register(ctx, 20, speed);

    return rc == 1;
}

注意,加锁范围必须是完整的一次通信:

复制代码
加锁
  ↓
发送请求
  ↓
等待回复
  ↓
解析结果
  ↓
解锁

不要只锁发送,不锁接收。


方案二:更推荐,请求队列 + Modbus 通信线程

复制代码
线程A:读温度 ─┐
线程B:读压力 ─┼── 请求队列 ── ModbusWorker线程 ── TCP ── PLC
线程C:写参数 ─┘

这是工业上位机里更稳的方式。


情况二:多个线程使用多个 TCP 连接

例如:

复制代码
线程A ── TCP连接1 ── PLC
线程B ── TCP连接2 ── PLC
线程C ── TCP连接3 ── PLC

这种方式理论上是可以的。

因为每个线程有自己的 socket:

复制代码
线程A 不会破坏线程B的发送缓冲区
线程B 不会破坏线程C的响应缓冲区

但是也要注意几个问题。


1. PLC 是否允许多个 TCP 客户端连接?

不是所有设备都支持很多连接。

有些 PLC 可能只支持:

复制代码
1 个连接
2 个连接
4 个连接
8 个连接

超过后可能连接失败。


2. 设备内部处理能力有限

即使允许多个 TCP 连接,也不代表它能高并发处理。

很多下位机内部还是单任务处理:

复制代码
连接1请求来了
连接2请求来了
连接3请求来了

PLC内部还是排队处理

如果并发太高,可能出现:

复制代码
响应超时
通信延迟增大
CPU占用升高
设备拒绝连接

3. 业务层面可能产生读写冲突

比如:3个线程对同一个modbus从站设备(server)的同一个寄存器进行读写。

复制代码
线程A:写寄存器100 = 启动
线程B:写寄存器100 = 停止
线程C:读寄存器100

即使 TCP 通信没有冲突,业务逻辑也会冲突。

最后寄存器到底是什么状态?

复制代码
取决于谁最后写进去

这就是典型的业务竞争。

所以多个 TCP 连接可以并发,但仍然要设计好:

复制代码
同一个设备的控制命令顺序
同一个寄存器的读写保护
启动、停止、急停等命令优先级

情况三:Modbus TCP 转 RS485 网关

这个非常关键。

很多项目表面上是:

复制代码
上位机 ── Ethernet ── Modbus TCP网关

但网关后面其实是:

复制代码
Modbus TCP网关 ── RS485总线 ── 多个仪表

完整结构:

复制代码
┌────────────┐
│ 上位机      │
└─────┬──────┘
      │ Modbus TCP
      ▼
┌────────────┐
│ TCP转485网关 │
└─────┬──────┘
      │ Modbus RTU / RS485
      ▼
┌────────────────────────┐
│ 设备1  设备2  设备3  设备4 │
└────────────────────────┘

这种情况下,千万不要以为 TCP 可以并发,后面的 RS485 也可以并发。

因为后面的 RS485 仍然是:

复制代码
半双工
一问一答
同一时间只能一个设备回复

所以如果多个线程同时通过 TCP 网关发请求:

复制代码
线程A:读1号设备
线程B:读2号设备
线程C:读3号设备

网关可能内部帮你排队,也可能处理不好,导致:

复制代码
超时
CRC错误
回复错乱
设备无响应

所以对于 TCP 转 RS485 网关,最好仍然按串口思路处理:

复制代码
一个网关
一个请求队列
一次只发一条
收到回复后再发下一条

图示:

复制代码
线程A ─┐
线程B ─┼── 请求队列 ── TCP网关 ── RS485总线 ── 设备1/2/3
线程C ─┘

最终建议:怎么设计Modebus TCP上位机最稳?

如果是普通 Modbus TCP 设备

比如:

复制代码
上位机 ── TCP ── PLC

推荐:

复制代码
每个 PLC 一个 ModbusManager
每个 ModbusManager 管理一个连接
内部用请求队列串行处理

图示:

复制代码
┌────────────┐
│ 业务线程A   │
└─────┬──────┘
      │
┌─────▼──────┐
│ 业务线程B   │
└─────┬──────┘
      │
      ▼
┌────────────────────┐
│ PLC1_ModbusManager  │
│ 请求队列             │
│ TCP连接              │
└─────────┬──────────┘
          ▼
┌────────────────────┐
│ PLC1                │
└────────────────────┘

如果有多个 PLC

可以按设备分开:

复制代码
PLC1_ModbusManager ── TCP连接1 ── PLC1
PLC2_ModbusManager ── TCP连接2 ── PLC2
PLC3_ModbusManager ── TCP连接3 ── PLC3

这种方式是可以并发的:

复制代码
PLC1 和 PLC2 可以同时通信
PLC2 和 PLC3 可以同时通信

但是同一个 PLC 内部,仍然建议按队列串行。


如果是 TCP 转 RS485 网关

推荐:

复制代码
每个网关一个 ModbusManager
网关后面的所有设备共用一个请求队列

图示:

复制代码
┌──────────────┐
│ 上位机业务层   │
└──────┬───────┘
       ▼
┌────────────────────┐
│ 网关_ModbusManager  │
│ 一个请求队列          │
└─────────┬──────────┘
          ▼
┌────────────────────┐
│ TCP转RS485网关       │
└─────────┬──────────┘
          ▼
┌────────────────────┐
│ 设备1 / 设备2 / 设备3 │
└────────────────────┘

不要给网关后面的每个 RS485 设备都开一个线程疯狂发请求。


十八、实际工程推荐规则

可以直接记这个表:

|-------------------|----------------------|-------------------|
| 场景 | 是否可以多线程同时 Modbus | 推荐做法 |
| 同一个串口 Modbus RTU | 不建议 | 单线程队列 / mutex |
| 同一个 RS485 总线 | 不建议 | 必须串行一问一答 |
| 同一个 Modbus TCP 连接 | 不建议直接并发 | 客户端对象加锁或队列 |
| 同一个 PLC,多个 TCP 连接 | 可能可以 | 看 PLC 支持,业务层要控并发 |
| 多个 PLC,各自 TCP 连接 | 可以 | 每个 PLC 一个 Manager |
| TCP 转 RS485 网关 | 不建议并发 | 按 RS485 思路串行 |
| 读不同设备状态 | 可以适度并发 | 按设备/连接分组 |
| 写同一批控制寄存器 | 不建议并发 | 必须统一调度 |


十九、总结

Modbus TCP 比 Modbus RTU 更适合并发,因为它有 TCP 连接和 Transaction ID;但是在实际上,同一个 Modbus 客户端对象、同一个 TCP 连接、同一个 PLC 或 TCP 转 RS485 网关,仍然建议统一排队、串行处理。

最稳的工程原则是:

复制代码
不同设备、不同连接:可以并发
同一设备、同一连接:最好串行
同一网关、同一RS485总线:必须串行
控制命令:必须统一调度,不能多个线程乱写

二十、补充

刚才提到的 Modbus TCP Client,基本就可以理解为 Modbus 的主站

不过更准确地说:

复制代码
Modbus RTU / 串口通信里,常说:
主站 Master  ←→  从站 Slave

Modbus TCP / 网络通信里,常说:
客户端 Client  ←→  服务器 Server

它们对应关系大概是:

|-----------|-------------------|---------------------|
| 传统叫法 | Modbus TCP 叫法 | 在上位机项目里的角色 |
| 主站 Master | Client 客户端 | 上位机、工控机、SCADA、Qt软件 |
| 从站 Slave | Server 服务器 | PLC、仪表、IO模块、变频器、控制器 |

所以可以这么理解:

复制代码
Modbus RTU:
上位机主站  ──RS485/RS232──  下位机从站

Modbus TCP:
上位机Client  ──Ethernet/TCP──  下位机Server

如果本文对你学习 C++ 上位机、Modbus 通信有所帮助,欢迎点赞、转发、关注。

如果你需要C/C++ 学习路线、上位机开发就业路线,或是面试备考指导,都可以联系我交流。

后续持续分享工业上位机、Qt、Modbus 实战干货!

相关推荐
从零开始的代码生活_2 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法
云小逸2 小时前
【C++ 第七阶段:模板、泛型编程与工程综合详解】
开发语言·c++
GIS阵地2 小时前
QgsSingleBandPseudoColorRenderer 完整详解(QGIS 3.40.13 C++)
开发语言·前端·c++·qt·qgis
王维同学3 小时前
[原创][Windows C++]LSA 认证、安全与通知包的注册表枚举
c++·windows·安全
_wyt0014 小时前
完全背包问题详解
c++·背包dp
皓月斯语5 小时前
B3842 [GESP202306 三级] 春游 题解
数据结构·c++·算法·题解
CedarQR6 小时前
万字长文:从零在 RK3588 上部署 PaddleSpeech 中文 TTS 全流程(FastSpeech2 + HiFiGAN)
开发语言·c++·嵌入式硬件·ubuntu·json
萌动的小火苗7 小时前
嵌入式开发中的栈与队列:任务调度为什么依赖数据结构
数据结构·c++·单片机·嵌入式硬件
PBitW7 小时前
git 中容易遗忘的点 (二) ⚡⚡⚡
前端·git·面试