IEC上位机modbus协议通信流程

Modbus RTU 通信流程文档

一、读取流程(上位机 → 读 → 下位机)

1:读取

on_Btn_Read_clicked()

1
2
3
4
5
6
7
8
9
10
11
12
void MainWindow::on_Btn_Read_clicked()
{
QList<int> plist = requestRowId(); // 获取勾选的行号列表
for (int i = 0; i < plist.size(); i++)
{
QVector<int> addrs, nums, value;
QVector<QString> sta_id_no_name;
int fun = 0x03; // 功能码 0x03: 读保持寄存器
paraOfRequestDataInit(plist[i], ..., addrs, nums, fun, value, sta_id_no_name);
emit requestData(addrs, nums, fun, value, sta_id_no_name); // 发送信号
}
}

2:参数初始化

paraOfRequestDataInit()

从数据库/视图模型拼装三个核心参数:

参数 来源 代码
寄存器地址 Code 列(索引10) (gID << 8) | adress
写入数据 CurValue ÷ Resolution(读操作时用不到) CurValue(5) / Resolution(9)
追踪标识 sta_id_no_name 格式 "1_{ID}_{序号}_{Ascii}"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MainWindow::paraOfRequestDataInit(int i, ..., QVector<int> &addrs, ...,
int &fun, QVector<QString> &sta_id_no_name)
{
// 1. 从 Code 列解析寄存器地址
QString str = m_viewModel->data(m_viewModel->index(i, 10)).toString(); // 第10列=Code
sscanf(str.toStdString().c_str() + str.length() - 2, "%2hhx", &adress); // 低2位
sscanf(str.toStdString().c_str() + str.length() - 4, "%2hhx", &gID); // 高2位
addrs << int((gID << 8) | adress); // 完整地址

// 2. 计算原始值(写入时使用)
value << qRound(m_viewModel->data(m_viewModel->index(i, 5)).toDouble() // CurValue
m_viewModel->data(m_viewModel->index(i, 9)).toDouble()); // Resolution

// 3. 构造追踪标识符
sta_id_no_name << "1_" + strID + "_" + strNo + "_" + strName; // 如 "1_001_000_MotorType"
}

3:信号传递到工作线程

1
connect(this, &MainWindow::requestData, m_ModbusWorker, &ModbusWorker::doSendDataWork);

4:Modbus 工作线程执行读写

doSendDataWork()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void ModbusWorker::doSendDataWork(const QVector<int> addrs, const QVector<int> nums,
const int fun, QVector<int> value,
const QVector<QString> sta_id_no_name)
{
modbus_set_slave(modbus, slave); // 设置从机地址(slave=1)

// 根据功能码执行不同操作
switch (fun) {
case MODBUS_FC_READ_HOLDING_REGISTERS: // 0x03 读保持寄存器
ret = modbus_read_registers(modbus, addr, num, dest32);
break;
case MODBUS_FC_WRITE_SINGLE_REGISTER: // 0x06 写单个寄存器
ret = modbus_write_register(modbus, addr, value[i]);
break;
// ... 其他功能码
}

// 成功后发出结果信号
if (ret == num) {
for (int ii = 0; ii < num; ++ii) {
emit sendResultToGui(data, sta_id_no_name.at(...));
// data = dest32[ii] 原始整数
// sta_id = "0_001_000_MotorType"(读) 或 "2_001_000_MotorType"(写)
}
}
}

三个参数的最终去向:

Modbus 参数 实际值 设置位置
从机地址 1(硬编码) modbus_set_slave(modbus, 1)
寄存器地址 addrs[i] → 如 0x1303 作为第2个参数传入 modbus_read_registers(modbus, addr, ...)
数据 value[i] → 原始整数 作为第2个参数传入 modbus_write_register(modbus, addr, value)

二、写入流程(上位机 → 写 → 下位机)

1:批量写入选中行

on_Btn_Write_clicked() (mainwindow.cpp:3133)

流程与读取基本一致,区别:

  • 功能码 fun = 0x06(写单个寄存器)
  • paraOfRequestDataInit() 中的 value 会被使用:CurValue ÷ Resolution
1
2
3
4
5
6
7
8
9
10
void MainWindow::on_Btn_Write_clicked()
{
for (int i = 0; i < nsel; i++) {
int fun = 0x06;
paraOfRequestDataInit(plist[i], ..., addrs, nums, fun, value, sta_id_no_name);
emit requestData(addrs, nums, fun, value, sta_id_no_name);
// value 已在 paraOfRequestDataInit 中计算好
// value = CurValue(列5) / Resolution(列9)
}
}

2:控件联动写入

SendDataToD()

UI 控件值变化时自动触发写入,通过 paraOfAddrNameInit() 查数据库获取寄存器地址:

1
2
3
4
5
6
7
8
9
void MainWindow::SendDataToD(QString ascii, QString data)
{
int fun = 0x06;
sta_id_no_name << ascii; // 传入参数名,如 "MotorType"
value << data.toInt(); // 传入原始整数值

paraOfAddrNameInit(addrs, nums, fun, sta_id_no_name); // 根据 Ascii 查Code列获取地址
emit requestData(addrs, nums, fun, value, sta_id_no_name);
}

paraOfAddrNameInit()

根据 Ascii 参数名查数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::paraOfAddrNameInit(QVector<int> &addrs, ..., QVector<QString> &sta_id_no_name)
{
// 拼 SQL: select * from NewParameters where Ascii = "MotorType" OR Ascii = "..."
QString sql = "select * from NewParameters where Ascii = \"" + sta_id_no_name.at(0) + "\"";

query.exec();
while (query.next()) {
QString str = query.value(10).toString(); // 取Code列
// 解析 gID 和 adress
addrs[index] = int((gID << 8) | adress); // 拼寄存器地址

// 构造 sta_id_no_name,读响应用 "0_",写入确认用 "2_"
sta_id_no_name[index] = QString("2_%1_000_%2").arg(strID).arg(strName);
}
}

三、下位机响应解析

入口函数

handleResult()

1
void MainWindow::handleResult(int data, QString sta_id_no_name)

通过信号连接接收:

1
connect(m_ModbusWorker, &ModbusWorker::sendResultToGui, this, &MainWindow::handleResult); 

解析过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
收到信号: handleResult(data=123, sta_id_no_name="0_001_000_MotorType")

┌───────────────┼───────────────┐
│ │ │
mid(0,1) mid(2,3) mid(10)
= "0" = "001" = "MotorType"
│ │ │
▼ ▼ ▼
读回显(0) 数据库 ID=1 更新 lineDDSC
仅刷新控件 查 NewParameters 等对应控件
不写库 取 Resolution


原始值 × Resolution = 工程值
123 × 0.1 = 12.3


写入数据库 CurValue
刷新 UI 控件显示

sta_id_no_name

格式:X_YYY_ZZZ_AsciiName

位置 内容 示例 说明
字符0 X 0/1/2 类型:0=读回显, 1=读请求, 2=写入
字符1 _ _ 分隔符
字符2-4 YYY 001 数据库主键 ID
字符5 _ _ 分隔符
字符6-8 ZZZ 000 行号(3位数字,最后一行=999)
字符9 _ _ 分隔符
字符10+ AsciiName MotorType 参数名

三种类型的构造:

1
2
3
4
5
6
7
// 请求时 (paraOfRequestDataInit)
sta_id_no_name = "1_001_000_MotorType"; // 读请求(fun=0x03)
sta_id_no_name = "2_001_000_MotorType"; // 写请求(fun=0x06)

// 响应时
sta_id_no_name = "0_001_000_MotorType"; // 读取结果回显 → 刷新控件
sta_id_no_name = "2_001_000_MotorType"; // 写入确认回显 → 更新数据库

分辨率换算(Resolution)

handleResult() 中的换算逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
double fdata = data;                                // Modbus原始整数
QString floatString = query.value(9).toString(); // 取Resolution列(第9列)
double unit = floatString.toDouble(); // 如 0.1

fdata *= unit; // 123 × 0.1 = 12.3 (工程值)

// 计算小数位数用于格式化
double resolution = unit;
int count = 0;
while (resolution - std::floor(resolution) > 0) {
resolution *= 10;
count++;
}
QString fdataString = QString::number(fdata, 'f', count); // "12.3"

setOneText2sql(id, 5, fdataString); // 写入数据库 CurValue(列5)

四、Resolution 的作用

Modbus 寄存器是 16 位整数(0~65535),无法直接传输小数。

Resolution(换算系数) 充当 Modbus 原始整数与用户界面工程值之间的桥梁:

场景 操作 公式
读取 下位机→上位机 工程值 = Modbus原始值 × Resolution
写入 上位机→下位机 发送值 = CurValue ÷ Resolution

五、通信线程

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::modbusThreadInit()
{
m_ModbusWorker = new ModbusWorker(m_modbus, 1); // 从机地址=1
m_ModbusWorker->moveToThread(m_ModbusThread);

// 请求信号 (主线程 → 工作线程)
connect(this, &MainWindow::requestData,
m_ModbusWorker, &ModbusWorker::doSendDataWork);

// 结果信号 (工作线程 → 主线程)
connect(m_ModbusWorker, &ModbusWorker::sendResultToGui,
this, &MainWindow::handleResult);

m_ModbusThread->start();
}