Windows端,libmodbus + Qt 上位机测试+报文解析

Windows端,libmodbus + Qt 上位机测试+报文解析

本文是在上一篇VS上实现modbus通讯的基础上,在Qt上做了一些简单的修改,以实现Qt上modbus的通讯。(也算是曲线救国了)

1 理清思路

一直以来,我对于Modbus-Master(主机端)和Modbus-Slave(从机端)通讯中,关于到底是在哪一端对寄存器赋值有点纠结。
主要原因:

  • 1.是自己对通讯概念的不清晰,主机端和从机端都可以传递数据,都可对寄存器进行赋值;
  • 2.参考网上的资料,大都是在Qt端建立Master与Slave的连接,而赋值是在工具Modbus Slave上完成的,所以造成误解

嵌入式STM32学习笔记(8)——libmodbus+Qt上位机测试
文中所展示的功能即是在Modbus Slave工具中对寄存器进行赋值,连接成功后,在命令窗口可看到值的传递情况。
但是对于我的实际情况中,Qt作为上位机,要通过对Slave端传递数值,以达到发送指令的效果。所以需要,对其中的代码做一些修改才能为我所用。

2 验证思路

在写代码前,需要对思路进行验证,看是否合理,首先借助Modbus辅助工具。

2.1 Modbus 辅助工具测试

借用辅助工具,建立Modbus Poll和Modbus Slave连接,然后分别以Modbus Poll端传递数据给Modbus Slave端,接着以Modbus Slave端传递数据给Modbus Poll端。
1.从Modbus Poll端分别传递1值给Modbus Slave端,如下图1所示。
在这里插入图片描述
相应的报文为:
(1)Modbus Poll:
Tx: 01 03 00 00 00 01 84 0A
Rx: 01 03 02 00 01 79 84
(2)Modbus Slave:
Rx: 01 03 00 00 00 01 84 0A
Tx: 01 03 02 00 01 79 84
其中,Tx表示发送,Rx表示接收。
报文格式:01 03 00 00 00 01 84 0A
01——从机地址
03——功能码(读取数据)
00——高位地址
00——低位地址
00——寄存器个数高位
01——寄存器个数低位
84——CRC高位
0A——CRC低位

报文格式:01 03 02 00 01 79 84
01——从机地址
03——功能码(读取数据)
02——数据长度
00——数据高位字节
01——数据低位字节
79——CRC高位
84——CRC低位

这个过程这样描述:
Modbus Poll端,手动输入数值后,主机端读取该地址处输入的数据值,发送给从机端,从机端响应主机端(我收到了)。

2.从Modbus Slave端传递2值给Modbus Poll端,如下图2所示。
在这里插入图片描述
相应的报文为:
(1)Modbus Poll:
Tx: 01 03 00 00 00 00 01 84 0A
Rx: 01 03 02 00 02 39 85
(2)Modbus Slave:
Rx: 01 03 00 00 00 00 01 84 0A
Tx: 01 03 02 00 02 39 85
其中,Tx表示发送,Rx表示接收。
相应的报文格式同上。
与上一样,说明寄存器赋值的地方不影响通讯方式。

3 Qt上位机测试

以上位机Qt作Modbus-Master(主机端),并在主机端,对寄存器进行赋值,采用了随机采样赋值方法。
先贴上代码:
1.mywidget.h

#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>
#include <libmodbus/modbus.h>

#define ADDRESS_START 0
#define ADDRESS_END 9
#define LOOP  1

QT_BEGIN_NAMESPACE
namespace Ui { class myWidget; }
QT_END_NAMESPACE

class myWidget : public QWidget
{
    Q_OBJECT

public:
    myWidget(QWidget *parent = nullptr);
    ~myWidget();

private slots:
    void on_pushButton_clicked();

private:
    Ui::myWidget *ui;
    modbus_t *ctx;
    uint16_t *tab_reg;
    uint16_t *send_tab_reg;
    int nb;
    int addr;
    modbus_mapping_t *mb_mapping;
    int nb_fail;
    int nb_loop;
};
#endif // MYWIDGET_H

2.mywidget.cpp

#include "mywidget.h"
#include "ui_mywidget.h"

#include "libmodbus/modbus.h"
#include "QThread"
#include <QtDebug>
#include <errno.h>
#include <stdlib.h>
#include <malloc.h>


myWidget::myWidget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::myWidget)
{
    ui->setupUi(this);
}

myWidget::~myWidget()
{
    delete ui;
}


void myWidget::on_pushButton_clicked()
{
    int rc;
    int i;

    ctx = modbus_new_rtu("COM3", 115200, 'N', 8, 1);
    modbus_set_slave(ctx,1);

    modbus_set_debug(ctx,TRUE);

    if (modbus_connect(ctx)==-1)
        {
            fprintf(stderr, "Connection failed: %s\n",modbus_strerror(errno));
            modbus_free(ctx);
        }


        /*
        uint32_t old_response_to_sec;//秒
        uint32_t old_response_to_usec;//微秒,1秒=1,000,000微秒

        modbus_get_response_timeout(ctx, &old_response_to_sec, &old_response_to_usec);//获取当前设置的超时

        modbus_set_response_timeout(ctx,0,50000);//设定超时,默认50ms(50,000us)
        qDebug()<<"get timeout sec:"<<old_response_to_sec;
        qDebug()<<"get timeout usec:"<<old_response_to_usec;



        printf("\n----------------\n");*/
        nb=ADDRESS_END-ADDRESS_START;

        tab_reg=(uint16_t *)malloc(nb * sizeof(uint16_t));
        memset(tab_reg,0,nb * sizeof(uint16_t));

        /*写入保持寄存器数值*/
        nb_loop=nb_fail=0;
        while(nb_loop++<LOOP)
        {
            for(addr=ADDRESS_START;addr<ADDRESS_END;addr++)
            {
                int i;
                for(i=0;i<nb;i++)
                {
                    tab_reg[i]=(uint16_t)(65535*rand()/(RAND_MAX+1.0));
                }
                nb=ADDRESS_END-addr;

                rc=modbus_write_register(ctx,addr,tab_reg[0]);
                if(rc !=1)
                {
                    printf("ERROR modbus_write_register(%d)\n",rc);
                    nb_fail++;
                }
                else
                {
                    rc = modbus_read_registers(ctx,addr,1, tab_reg); //03#读取保持寄存器的值,(对象,起始地址,读取数量,存储读取到的值)
                    if (rc != 1)
                    {
                        fprintf(stderr,"%s\n", modbus_strerror(errno));
                        qDebug()<<"rc错误"<<modbus_strerror(errno);
                        nb_fail++;
                    }
                    else
                    {
                        qDebug()<<"链接成功";
                        for (i=0; i<10; i++)
                        {
                            printf("reg[%d] = %d(0x%x)\n", i, tab_reg[i], tab_reg[i]);
                            qDebug()<<"rc收到:"<<tab_reg[i];
                        }
                     }

                 }

               }
        }
        modbus_close(ctx);  //关闭modbus连接
        modbus_free(ctx);   //释放modbus资源,使用完libmodbus需要释放掉
}




执行后的效果,如下图所示:
图3
可以看到,寄存器赋的值已经传递进Modbus Slave中,说明此代码在Modbus Master端赋值寄存器的值有效。
另外,在做这一测试时,需要参考我的上两篇文章的内容,即libmodbus库的编辑以及modbus通讯模拟。

4 Modbus通讯数据包解析

实现通讯连接之后,就需要对报文进行解析。如下图所示,单寄存器10位的数值在左侧栏可清楚看到,报文信息在右侧可详细看到,那么,现在就来分析报文与数值是怎么对应的。
报文解析

(1)首地址的数值对应的报文解析:
首地址处的数值为-23
接收到的报文信息是:
01 06 00 00 FF E9 09 B4
其中RTU报文的格式是这样定义的:
报文格式
01——从机地址
06——功能码
00——寄存器高位地址
00——寄存器低位地址
FF——数据高位
E9——数据低位
09——CRC高位
B4——CRC低位
现在主要来计算数据位的计算:
-23,首先23对应的16进制数为0017,那么-23=0-0017=FFFF -0017+1=FFE9
至于CRC的计算,因为有CRC函数就不需要考虑了。
(2)首地址的数值传递的报文解析:
在这里插入图片描述
Rx表示接收,Tx表示传递
过程是这样的:

先是从机端接收到这样一条报文信息,然后将收到的数值以报文的信息再发送回给主机端,(告诉我收到了),主机端读取报文信息,从机端再应答一下。
为了验证一下思路是否正确,再做一个
(3)地址为1的数值传递
地址1处的数值为-21157
接收到的报文信息是:
01 06 00 01 AD 5B E5 61
按照上面报文格式的介绍,只对其中数据高低位进行计算:
21157对应的16进制数为52A5,
-21157=0-52A5=FFFF-52A5+1=AD5B
数据高位 AD 数据低位 5B
(4)地址1处的数值传递报文解析:
在这里插入图片描述
如图所示,数值传递的过程是这样的:
先是从机端接收到这样一条报文,然后对主机端进行应答,发送相应的报文,然后主机端对报文进行读取,即读取01地址处的值,从机端响应主机端,将地址处的信息发回。
接下来要做的是对数据包的处理。
参考1:libmodbus在Windows端Qt 5上的使用注意事项
参考2:Visual Studio 上基于libmodbus库的modbus RTU模式的通讯模拟
参考3:嵌入式STM32学习笔记(8)——libmodbus+Qt上位机测试
参考4:Modbus RTU报文格式

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是一个简单的示例代码,演示了如何在Qt上位机中实时解析并显示收到的数据: ```c++ // 定义一个串口通信类 class SerialPort : public QObject { Q_OBJECT public: // 构造函数 SerialPort(QObject *parent = nullptr); // 打开串口 bool open(const QString &portName, int baudRate); // 关闭串口 void close(); // 发送数据 void send(const QByteArray &data); signals: // 接收到数据信号 void dataReceived(const QByteArray &data); private slots: // 读取串口数据 void readData(); private: // 串口对象 QSerialPort *m_serialPort; // 数据缓存区 QByteArray m_dataBuffer; }; SerialPort::SerialPort(QObject *parent) : QObject(parent) , m_serialPort(nullptr) { } bool SerialPort::open(const QString &portName, int baudRate) { // 创建串口对象 m_serialPort = new QSerialPort(portName, this); // 配置串口参数 m_serialPort->setBaudRate(baudRate); m_serialPort->setDataBits(QSerialPort::Data8); m_serialPort->setParity(QSerialPort::NoParity); m_serialPort->setStopBits(QSerialPort::OneStop); // 打开串口 if (!m_serialPort->open(QIODevice::ReadWrite)) { return false; } // 连接信号槽 connect(m_serialPort, &QSerialPort::readyRead, this, &SerialPort::readData); return true; } void SerialPort::close() { if (m_serialPort) { m_serialPort->close(); m_serialPort = nullptr; } } void SerialPort::send(const QByteArray &data) { if (m_serialPort) { m_serialPort->write(data); } } void SerialPort::readData() { // 读取串口数据 QByteArray data = m_serialPort->readAll(); // 将数据添加到缓存区 m_dataBuffer.append(data); // 如果缓存区的数据长度超过某个阈值,就开始解析数据 if (m_dataBuffer.length() >= 10) { // 解析数据 // ... // 发射数据接收信号 emit dataReceived(m_dataBuffer); // 清空缓存区 m_dataBuffer.clear(); } } // 主界面类 class MainWindow : public QMainWindow { Q_OBJECT public: // 构造函数 MainWindow(QWidget *parent = nullptr); private slots: // 数据接收槽函数 void onDataReceived(const QByteArray &data); private: // 串口通信类对象 SerialPort *m_serialPort; // 文本框控件对象 QTextEdit *m_textEdit; }; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , m_serialPort(nullptr) { // 创建串口通信类对象 m_serialPort = new SerialPort(this); // 打开串口 m_serialPort->open("COM1", 9600); // 连接数据接收信号槽函数 connect(m_serialPort, &SerialPort::dataReceived, this, &MainWindow::onDataReceived); // 创建文本框控件对象 m_textEdit = new QTextEdit(this); m_textEdit->setReadOnly(true); setCentralWidget(m_textEdit); } void MainWindow::onDataReceived(const QByteArray &data) { // 解析数据 // ... // 显示数据 m_textEdit->append(QString::fromUtf8(data)); } ``` 在上面的示例代码中,SerialPort 类封装了串口通信的相关操作,包括打开串口、关闭串口、发送数据和接收数据等。在接收数据的槽函数 readData() 中,我们将接收到的数据添加到数据缓存区 m_dataBuffer 中,并在缓存区的长度达到某个阈值时开始解析数据,并发射数据接收信号 dataReceived()。 MainWindow 类是一个主界面类,它创建了一个串口通信类对象 m_serialPort,并连接了其数据接收信号槽函数 onDataReceived()。在 onDataReceived() 槽函数中,我们可以解析数据并将其显示在界面上的文本框控件中。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值