Qt C++实战:Modbus RTU协议详解及主站实现

文章目录

一 Modbus RTU 协议1.1 Modbus 协议分类及通信过程1.2 Modbus-RTU 协议数据帧格式
二 Modbus RTU 主站上位机2.1 界面设计2.2 编码实现2.2.1 串口操作2.2.2 从站操作2.2.3 信息显示
2.3 测试
其他



Modbus
总线协议属于应用层协议,是一种常见的工业系统通讯协议,被广泛应用于工业领域。本篇将对
Modbus RTU
协议进行详细介绍,并实现一个基于Qt的
Modbus RTU
主站上位机。

一 Modbus RTU 协议

1.1 Modbus 协议分类及通信过程


Modbus
协议分为三类,包括:
Modbus-RTU

Modbus-ASCII

Modbus-TCP


Modbus
是主从方式通信,通信由主机发起,一问一答式,从机无法主动向主机发送数据。

《GB/T 19582.2》中规定:RTU 模式中每个字节为11位,格式为:8bit数据位(先发地位)、1bit起始位、1bit奇偶校验、1bit停止位。默认使用偶校验,也可以使用其他模式(奇校验、无校验)。使用无校验时要求2个停止位,以此来满足11bit的数据。

发送字符或字节的顺序为从左到右,如下图所示:

Qt C++实战:Modbus RTU协议详解及主站实现

通过配置,设备可以接受奇校验,偶校验或无校验,如果无校验,那么传送一个附加的停止位来填充字符帧使其成为完整的11位异步字符,如下图所示:

Qt C++实战:Modbus RTU协议详解及主站实现

1.2 Modbus-RTU 协议数据帧格式


Modbus-RTU
帧最大长度为256字节,帧描述如下:

Qt C++实战:Modbus RTU协议详解及主站实现

地址码:1个字节的从机地址码。0:广播地址;1-247:从机地址;248-255:保留;

功能码:常用的就是01、02、03、04、05、06、15和16,具体描述见下表;

数据区:数据区包括起始地址、数量和数据,这三项均是大端模式;

CRC校验:两个字节,小端模式,校验的数据范围为:地址码+功能码+数据区。

功能码的具体描述见下表:

功能码 名称 作用
01 读取线圈状态 取得一组逻辑线圈的当前状态(ON/OFF)
02 读取输入状态 取得一组开关输入的当前状态(ON/OFF)
03 读取保持寄存器 在一个或多个保持寄存器中取得当前的二进制值
04 读取输入寄存器 在一个或多个输入寄存器中取得当前的二进制值
05 强置单线圈 强置一个逻辑线圈的通断状态
06 预置单寄存器 把具体二进制装入一个保持寄存器
07 读取异常状态 取得8个内部线圈的通断状态,这8个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态
08 会送诊断校验 把诊断校验报文送从机,以对通信处理进行评鉴
09 编程(只用于484) 使主机模拟编程器作用,修改PC从机逻辑
10 控询(只用于484) 可使主机与一台正在执行长程序任务从机通信,探询该从机是否完成其操作任务,仅在含有功能码9的报文发送后,本功能码才能发送
11 读取事件计数 可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时。
12 读取通信事件记录 可使主机检索每台从机的Modbus事务处理通信事件记录。如果某项事务处理完成,记录会给出相关错误
13 编程(184/384 484 584) 可使主机模拟编程器功能修改PC从机逻辑
14 探询(184/384 484 584) 可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能13的报文发送后,本功能码才得发送
15 强置多线圈 强置一串连续逻辑线圈的通断
16 预置多寄存器 把具体的二进制值装入一串连续的保持寄存器
17 报告从机标识 可使主机判断编址从机的类型及该从机运行指示灯的状态
18 (884 和 MICRO 84) 可使主机模拟编程功能,修改PC状态逻辑
19 重置通信链路 发生非可修改错误后,使从机复位

QModbusDataUnit 是 Qt Modbus 模块中的一个核心类,用于表示Modbus协议中一次读写操作所涉及的数据单元。如下表所示:

QModbusDataUnit::RegisterType 枚举值 Modbus功能码 寄存器类型(通俗名称) 读写属性
Coils 01,05,15 线圈,位数据(1bit) 读写
DiscreteInputs 02 离散输入,位数据(1bit) 只读
InputRegisters 04 输入寄存器,字数据(16bits) 只读
HodingRegister 03,06,16 保持寄存器,字数据(16bits) 读写

二 Modbus RTU 主站上位机

2.1 界面设计

基于 Qt 设计上位机界面,包括通信方式选择,串口配置,读写配置,设备参数设置,发送/接收数据展示,发送数据和接收数据。具体界面设置如下图所示:

Qt C++实战:Modbus RTU协议详解及主站实现

2.2 编码实现

上位机软件主要功能包括串口操作、从站操作以及信息显示三个部分。

2.2.1 串口操作

首先将通信方式设置为
Serial Port
,然后配置串口参数,包括串口号、波特率、数据位、校验位、停止位、超时时间和重复次数。其中,串口号通过自动搜索获取当前可用的串口,实现方式如下:


foreach(const QSerialPortInfo& info, QSerialPortInfo::availablePorts())
{
  ui->comboBoxPort->addItem(info.portName());
}

串口操作主要是打开串口和关闭串口,具体实现如下:


void MainWindow::on_pushButton_clicked()
{
  if(m_stop){
    if (modbusDevice)
    {
      modbusDevice->disconnectDevice();
      delete modbusDevice;
      modbusDevice = nullptr;
    }

    int index = ui->cboxConnectType->currentIndex();
    if(0 == index){
      modbusDevice = new QModbusRtuSerialMaster(this);
      //配置串口参数
      modbusDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,ui->comboBoxPort->currentText());
      modbusDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,ui->comboBoxParity->currentIndex());
      modbusDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,ui->comboBoxBaud->currentText().toInt());
      modbusDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,ui->comboBoxData->currentText().toInt());
      modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,ui->comboBoxStop->currentText().toInt());
      modbusDevice->setTimeout(ui->leditOTime->text().toInt());          // 配置请求超时时间
      modbusDevice->setNumberOfRetries(ui->leditRTimes->text().toInt());     // 配置失败重试次数
    }else{
      modbusDevice = new QModbusTcpClient(this);
      //配置modbus tcp的连接参数 IP+Port modbus协议的端口号为502
      modbusDevice->setConnectionParameter(QModbusDevice::NetworkAddressParameter, ui->leditIP->text());
      modbusDevice->setConnectionParameter(QModbusDevice::NetworkPortParameter, ui->leditPort->text().toInt());
    }
    connect(modbusDevice, &QModbusClient::errorOccurred, [this](QModbusDevice::Error) {
      qDebug() << "modbus Error:" << modbusDevice->errorString();
    });

    if(!modbusDevice->connectDevice())
    {
      qDebug()<<tr("Connect failed: %1").arg(modbusDevice->errorString());
    }
    else
    {
      qDebug() << "Modbus open Success!";
      ui->comboBoxPort->setEnabled(false);
      ui->comboBoxParity->setEnabled(false);
      ui->comboBoxBaud->setEnabled(false);
      ui->comboBoxData->setEnabled(false);
      ui->comboBoxStop->setEnabled(false);
      ui->leditOTime->setEnabled(false);
      ui->leditRTimes->setEnabled(false);
      ui->cboxConnectType->setEnabled(false);
      ui->leditIP->setEnabled(false);
      ui->leditPort->setEnabled(false);
      m_stop = false;
      ui->pushButton->setText(QStringLiteral("关闭串口"));
    }
  }else{
    if (!modbusDevice)
      return;
    modbusDevice->disconnectDevice();
    delete modbusDevice;
    modbusDevice = nullptr;

    qDebug() << "Modbus close Success!";
    ui->comboBoxPort->setEnabled(true);
    ui->comboBoxParity->setEnabled(true);
    ui->comboBoxBaud->setEnabled(true);
    ui->comboBoxData->setEnabled(true);
    ui->comboBoxStop->setEnabled(true);
    ui->leditOTime->setEnabled(true);
    ui->leditRTimes->setEnabled(true);
    ui->cboxConnectType->setEnabled(true);
    ui->leditIP->setEnabled(true);
    ui->leditPort->setEnabled(true);
    m_stop = true;
    ui->pushButton->setText(QStringLiteral("打开串口"));
  }
}

2.2.2 从站操作

首先配置从站读写数据类型,从站设备ID和地址,读取数据是还需要配置读取数据的个数。然后对从站进行读取和写入数据操作,实现代码如下:


  //写入从站数据
  //获取要写入的寄存器数据
  QList<quint16> values;
  QStringList values_list = ui->leditSendInfo->text().split(" ");
  for(int i = 0 ; i < values_list.size(); i++)
  {
    values.append(values_list.at(i).toUInt());
  }
  int id = ui->leditID->text().toInt(); //设备地址
  int addr = ui->leditAddr->text().toInt(); //寄存器地址

  //组合写数据帧  table写入的数据类型 寄存器或线圈
  const auto table =
      static_cast<QModbusDataUnit::RegisterType> (ui->comboBoxValueType->currentData().toInt());
  QModbusDataUnit writeUnit = QModbusDataUnit(table,
                                              addr, values.size());
  for(int i=0; i<values.size(); i++)
  {
    writeUnit.setValue(i, values.at(i));
  }

  //id 发生给slave的ID
  if (auto *reply = modbusDevice->sendWriteRequest(writeUnit,id))
  {
    //对reply处理
    //......
  }

  //读取从站数据
  //获取设备信息
  int id = ui->leditID->text().toInt(); //设备地址
  int addr = ui->leditAddr->text().toInt(); //寄存器地址
  int readNum = ui->leditCount->text().toInt(); //读取寄存器个数

  //组合写数据帧  table写入的数据类型 寄存器或线圈
  const auto table =
      static_cast<QModbusDataUnit::RegisterType> (ui->comboBoxValueType->currentData().toInt());

  QModbusDataUnit readUint = QModbusDataUnit(table,
                                             addr, readNum);
  //读取数据
  if (auto *reply = modbusDevice->sendReadRequest(readUint, id))
  {
    if (!reply->isFinished())
      connect(reply, &QModbusReply::finished, this, &MainWindow::readReady);
    else
      delete reply;
  }
  else
  {
    qDebug() << "Read error: " << modbusDevice->errorString();
  }

2.2.3 信息显示

信息显示主要包括两方面,一是主站发送给从站的报文信息,二是读取回来的从站数据结果信息。实现代码如下:


    //发送报文信息
    QString timeStrLine="["+QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")+"][发送]: ";
    QString context = timeStrLine+ui->leditSendInfo->text()+"

";
    QString content = "<span style=" color:red;">"+context+"</span>";
    ui->textBrowser->append(content);

      // 结果数据信息
      QString send_buff;
      for (uint i = 0; i < unit.valueCount(); i++)
      {
        const QString entry = tr("Address: %1, Value: %2").arg(unit.startAddress() + i)
            .arg(QString::number(unit.value(i),
                                 unit.registerType() <= QModbusDataUnit::Coils ? 10 : 10));
        send_buff.append(QString::number(unit.value(i),
                                         unit.registerType() <= QModbusDataUnit::Coils ? 10 : 10) + " ");
      }
      //读取的数据
      ui->leditData->insert(send_buff);
      QString timeStrLine="["+QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")+"][接收]: ";
      QString context = timeStrLine+send_buff+"

";
      QString content = "<span style=" color:blue;">"+context+"</span>";
      ui->textBrowser_2->append(content);

2.3 测试

上位机软件编码调试后,需要对其进行一些测试。首先安装虚拟串口软件用于虚拟测试用的串口,然后准备一款
Modbus RTU
的从站模拟软件。虚拟串口软件的配置如下图所示:

Qt C++实战:Modbus RTU协议详解及主站实现


Modbus RTU
从站模拟软件使用
modbusslavep
。运行
Modbus RTU
主站上位机软件,测试读写从站数据操作,测试结果如下所示:

Qt C++实战:Modbus RTU协议详解及主站实现

其他

官方店:https://2td9n4muhvgusvzepfo0a3cyfue8cph.taobao.com/
微信公众号:
Qt C++实战:Modbus RTU协议详解及主站实现

© 版权声明

相关文章

暂无评论

none
暂无评论...