目录
一、Modbus RTU协议介绍
Modbus RTU(Remote Terminal Unit)是一种广泛使用的串行通信协议,主要用于工业自动化领域。它基于Modbus协议,但使用串行接口(如RS-232或RS-485)进行通信。Modbus RTU协议定义了数据帧的格式和通信规则,确保主设备(Master)和从设备(Slave)之间的可靠通信。
Modbus RTU协议支持几种不同类型的寄存器,这些寄存器用于存储不同类型的数据。以下是Modbus RTU中最常见的四种寄存器类型及其用途:
1. 线圈(Coils): 地址范围:0x0000 - 0xFFFF 数据类型:1位(布尔值)
功能码:
0x01:读线圈状态(Read Coils)
0x05:写单个线圈(Write Single Coil)
0x0F:写多个线圈(Write Multiple Coils)
用途:用于控制数字输出,例如继电器、开关等。
2.离散输入(Discrete Inputs):地址范围:0x0000 - 0xFFFF 数据类型:1位(布尔值)
功能码:0x02:
读离散输入状态(Read Discrete Inputs)
用途:用于读取数字输入,例如传感器状态、按钮等
3.保持寄存器(Holding Registers):地址范围:0x0000 - 0xFFFF 数据类型:16位(无符号整数)
功能码:
0x03:读保持寄存器(Read Holding Registers)
0x06:写单个寄存器(Write Single Register)
0x10:写多个寄存器(Write Multiple Registers)
用途:用于读写可编程控制器中的数据,例如温度、压力、计数值等
4.输入寄存器(Input Registers):地址范围:0x0000 - 0xFFFF 数据类型:16位(无符号整数)
功能码:
0x04:读输入寄存器(Read Input Registers)
用途:用于读取模拟输入数据,例如传感器读数等
5.举例说明
发送:01 03 00 00 00 02 C4 0B
01:表示从机的485地址
03:功能码0x03表示读保持寄存器(Read Holding Registers)
00 00 00 02:起始地址高字节 起始地址低字节 寄存器数量高字节 寄存器数量低字节
C4 0B:CRC校验码的低字节 CRC校验码的高字节
|地址域|功能码|起始地址高字节|起始地址低字节|寄存器数量高字节|寄存器数量低字节|CRC低字节|CRC高字节|
|------|------|-------------|-------------|---------------|---------------|--------|----------|
| 0x01 | 0x03 | 0x00 | 0x00 | 0x00 | 0x02 | 0xC4 | 0x0B |
返回:01 03 04 40 26 14 7B C6 3F
01:表示从机的 485 地址
03:功能码0x03表示读保持寄存器(Read Holding Registers)
04:表示返回的寄存器数据有 4 个字节
40 26:表示返回的第一个寄存器数据
14 7B:表示返回的第二个寄存器数据
|地址域 |功能码| 字节总数 | 寄存器数据1 | 寄存器数据2 |CRC低字节|CRC高字节|
|------|------|-------------|-------------|---------------|--------|--------|
| 0x01 | 0x03 | 0x04 | 0x40 0x26 | 0x14 0x7B | 0xC6 | 0x3F |
二、Modbus RTU上位机代码实现
环境与工具:VS2022、QT6.7.2、Modbus Slave(从机)、Virtual Serial port Driber Pro(虚拟串口)
实现功能如下:
1、编写主站程序,向从站读取和写入保持寄存器,写入的数据为16进制
2、自动获取可用串口功能,消息窗口自动滚屏效果,波特率选项设置
UI界面如下:
具体代码步骤实现如下:
mainwindow.cpp 源文件
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "qstring.h"
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//1、自动滚屏
QTextCursor cursor = ui->textEdit_5->textCursor();
cursor.movePosition(QTextCursor::End);
ui->textEdit_5->setTextCursor(cursor);
//2、添加串口选项
QStringList comport = get_avail_sp_();
ui->comboBox_serialport->addItems(comport);
//3、添加波特率选项
ui->baudRateComboBox->addItem("9600", QVariant::fromValue(QSerialPort::Baud9600));
ui->baudRateComboBox->addItem("19200", QVariant::fromValue(QSerialPort::Baud19200));
ui->baudRateComboBox->addItem("38400", QVariant::fromValue(QSerialPort::Baud38400));
ui->baudRateComboBox->addItem("57600", QVariant::fromValue(QSerialPort::Baud57600));
ui->baudRateComboBox->addItem("115200", QVariant::fromValue(QSerialPort::Baud115200));
//4、连接打开按钮
connect(ui->on_btn_open, &QPushButton::clicked, this, [=]() {
//获取选中的波特率
int baudRate = ui->baudRateComboBox->currentData().toInt();
//获取串口号
QString port = ui->comboBox_serialport->currentText();
//定义串口对象
modbusMaster = new QModbusRtuSerialMaster(this);
modbusMaster->setConnectionParameter(QModbusDevice::SerialPortNameParameter, port); //串口号
modbusMaster->setConnectionParameter(QModbusDevice::SerialBaudRateParameter, baudRate); //波特率
modbusMaster->setConnectionParameter(QModbusDevice::SerialParityParameter, QSerialPort::NoParity); //无校验
modbusMaster->setConnectionParameter(QModbusDevice::SerialDataBitsParameter, QSerialPort::Data8); //数据位
modbusMaster->setConnectionParameter(QModbusDevice::SerialStopBitsParameter, QSerialPort::OneStop); //停止位
//打开串口按钮
if (!modbusMaster->connectDevice())
{
ui->statusbar->showMessage("连接失败", 2000);
}
else
{
ui->statusbar->showMessage("连接成功", 2000); value = true;
}
});
//5、连接关闭按钮
connect(ui->on_btn_close, &QPushButton::clicked, this, [=]() {
if (value)
{
modbusMaster->disconnectDevice();
delete modbusMaster;
ui->statusbar->showMessage("串口关闭", 1000);
value = false;
}
});
//6、读寄存器按钮
connect(ui->read_Registers, &QPushButton::clicked, this, [=]() {
//检测串口是否打开
if (TestPort() != true) return;
//获取从机地址
QString url = ui->url->toPlainText();
//获取线圈起始地址
QString coiladdress_start = ui->coil_start->toPlainText();
QString coiladdress_end = ui->coil_finish->toPlainText();
//定义读写单元
QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters, coiladdress_start.toInt(), coiladdress_end.toInt()); //0是所要读取的寄存器起始地址,10是所要读取的寄存器个数
//发送请求
auto* reply = modbusMaster->sendReadRequest(readUnit, url.toInt());
if (reply)
{
connect(reply, &QModbusReply::finished, this, &MainWindow::readRegisters);
}
else
{
ui->statusbar->showMessage("无效请求", 1000);
}
});
//7.写入寄存器按钮,获取水位值
connect(ui->write_Registers, &QPushButton::clicked, this, [=]() {
//检测串口是否打开
if (TestPort()!=true) return;
//获取从机地址
QString url = ui->url->toPlainText();
//分割字符串
QString str = ui->textEdit_write->toPlainText();
//按空格分割文本
QStringList parts = str.split(' ', Qt::SkipEmptyParts);
//定义读写单元
QModbusDataUnit writeUnit(QModbusDataUnit::HoldingRegisters, 0, parts.size());
for (int i = 0; i < parts.size(); i++)
{
bool ok;
quint16 value = parts[i].toUInt(&ok, 16); //16进制转换为十进制
if (ok)
{
ui->statusbar->showMessage("写入成功 获取水位值", 1000);
}
else
{
ui->statusbar->showMessage("写入失败", 1000);
return;
}
writeUnit.setValue(i, value);
}
ui->textEdit_5->append(str); //向消息窗口行尾添加数据
//发送写入请求
modbusMaster->sendWriteRequest(writeUnit, 1);
});
}
//6.1读保持寄存器函数
void MainWindow::readRegisters()
{
//接收信号
auto reply = qobject_cast<QModbusReply*>(sender());
if (!reply)
{
ui->statusbar->showMessage("无效回复", 1000);
return;
}
//处理响应
const QModbusDataUnit unit = reply->result();
// 将数据值转换为字符串
QString dataStr;
for (int i = 0; i < unit.valueCount(); ++i) {
dataStr += QString::number(unit.value(i)) + " ";
}
if(dataStr.size()!=0)
{
ui->textEdit_5->append(dataStr);
ui->statusbar->showMessage("读取成功", 1000);
}
else
{
ui->textEdit_5->append("无数据");
}
}
//检测串口是否打开
bool MainWindow::TestPort()
{
if (value)
{
ui->statusbar->showMessage("正常连接", 1000);
return true;
}
else
{
ui->statusbar->showMessage("串口已关闭", 1000);
value = false;
return false;
}
}
MainWindow::~MainWindow()
{
delete ui;
}
manwindow.h 头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QtSerialBus>
#include <QModbusDataUnit>
#include <QModbusClient>
#include <QModbusRtuSerialMaster>
#include <QModbusReply>
#include <QStringList>
#include <QSettings>
#include <QDebug>
#include <QTextStream>
#include <QString>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QModbusRtuSerialMaster* modbusMaster; //创建modbus对象
public slots:
void readRegisters(); //读寄存器函数
public:
bool value=false; //标志位
MainWindow(QWidget *parent = nullptr);
~MainWindow();
public:
//检测串口是否打开
bool TestPort();
//获取可用串口
QStringList get_avail_sp_() noexcept {
QStringList list_avail_sp;
foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts()) {
QSerialPort serial;
serial.setPort(info);
try {
if (serial.open(QIODevice::ReadWrite)) {
list_avail_sp.append(serial.portName());
serial.close();
}
}
catch (const std::exception& e) {
qWarning() << "Error opening serial port" << info.portName() << ":" << e.what();
}
}
return list_avail_sp;
}
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
三、实现效果
1.运行Virtual Serial Port Driver Pro 创建虚拟串口 COM1-COM2,创建完成后设备管理器中可以观察到,如图1所示
(图1)
2.运行Modbus Slave从机模拟软件,如图2、图3所示
(图2) 设置从机地址、寄存器模式、线圈地址开始-结束
(图3) 选择COM2->COM1 波特率等如图所示 其他默认
3.从机连接COM2->COM1,上位机连接COM1->COM2,如图4所示
(图4 )上位机做主站
4.从机模拟软件设置线圈地址为0-10,上位机点击“读保持寄存器线圈”按钮,效果如图5所示;
(图5)
5.写指令文本框中输入要写入的指令,用空格分开,以16进制传输,从机接收数据后转为10进制,效果如图6、图7所示;
图6
(图7)
四、工具与源码下载
通过百度网盘分享的文件:Modbus_RTU_QT通信源码与工具.zip
链接:https://pan.baidu.com/s/1AdwZrDn5-AoZo3q5NAoIpA
提取码:ym5a