返回博客
Qt 串口上位机开发:Modbus RTU 调试工具实战

Qt 串口上位机开发:Modbus RTU 调试工具实战

2025年5月15日Qt, 上位机, Modbus RTU

Qt 工程创建与 QSerialPort 模块

Qt 提供跨平台串口通讯模块 QSerialPort,支持 Windows、Linux、macOS。

创建 Qt 工程

使用 Qt Creator 创建 Qt Widgets Application:

# ModbusTool.pro
QT       += core gui serialport

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = ModbusTool
TEMPLATE = app

SOURCES += main.cpp \
           mainwindow.cpp

HEADERS += mainwindow.h

FORMS   += mainwindow.ui

添加 QSerialPort 模块

在 .pro 文件中添加 serialport 模块:

QT += serialport

头文件引用

#include <QSerialPort>
#include <QSerialPortInfo>

串口搜索与配置

搜索可用串口

#include <QSerialPortInfo>

void MainWindow::Search_SerialPorts(void) {
    ui->comboBox_port->clear();
    
    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
        ui->comboBox_port->addItem(info.portName());
    }
}

串口配置

#include <QSerialPort>

QSerialPort *serial;

void MainWindow::Config_SerialPort(void) {
    serial = new QSerialPort(this);
    
    // 设置端口名
    serial->setPortName(ui->comboBox_port->currentText());
    
    // 设置波特率
    serial->setBaudRate(ui->comboBox_baudrate->currentText().toInt());
    
    // 设置数据位
    serial->setDataBits(QSerialPort::Data8);
    
    // 设置校验位
    serial->setParity(QSerialPort::NoParity);
    
    // 设置停止位
    serial->setStopBits(QSerialPort::OneStop);
    
    // 设置流控制
    serial->setFlowControl(QSerialPort::NoFlowControl);
}

打开串口

void MainWindow::on_pushButton_open_clicked() {
    if (serial->isOpen()) {
        serial->close();
        ui->pushButton_open->setText("打开串口");
        return;
    }
    
    Config_SerialPort();
    
    if (serial->open(QIODevice::ReadWrite)) {
        ui->pushButton_open->setText("关闭串口");
        
        // 连接接收信号
        connect(serial, &QSerialPort::readyRead, this, &MainWindow::Read_SerialData);
    } else {
        QMessageBox::warning(this, "错误", "串口打开失败");
    }
}

关闭串口

void MainWindow::Close_SerialPort(void) {
    if (serial->isOpen()) {
        serial->close();
    }
}

Modbus RTU 帧构造与发送

CRC-16 计算

uint16_t Calculate_CRC16(const uint8_t *data, uint16_t length) {
    uint16_t crc = 0xFFFF;
    
    for (uint16_t i = 0; i < length; i++) {
        crc ^= data[i];
        
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    
    return crc;
}

构造读取请求(功能码 03)

QByteArray Build_Modbus_Read_Request(uint8_t slave_addr, 
                                     uint16_t start_addr, 
                                     uint16_t reg_count) {
    QByteArray frame;
    
    frame.append(slave_addr);          // 从站地址
    frame.append(0x03);                // 功能码
    frame.append((start_addr >> 8) & 0xFF);  // 起始地址高字节
    frame.append(start_addr & 0xFF);         // 起始地址低字节
    frame.append((reg_count >> 8) & 0xFF);   // 寄存器数量高字节
    frame.append(reg_count & 0xFF);          // 寄存器数量低字节
    
    // 添加 CRC
    uint16_t crc = Calculate_CRC16((uint8_t *)frame.data(), frame.size());
    frame.append(crc & 0xFF);          // CRC 低字节
    frame.append((crc >> 8) & 0xFF);   // CRC 高字节
    
    return frame;
}

构造写入请求(功能码 06)

QByteArray Build_Modbus_Write_Request(uint8_t slave_addr, 
                                      uint16_t reg_addr, 
                                      uint16_t reg_value) {
    QByteArray frame;
    
    frame.append(slave_addr);          // 从站地址
    frame.append(0x06);                // 功能码
    frame.append((reg_addr >> 8) & 0xFF);    // 寄存器地址高字节
    frame.append(reg_addr & 0xFF);           // 寄存器地址低字节
    frame.append((reg_value >> 8) & 0xFF);   // 寄存器值高字节
    frame.append(reg_value & 0xFF);          // 寄存器值低字节
    
    // 添加 CRC
    uint16_t crc = Calculate_CRC16((uint8_t *)frame.data(), frame.size());
    frame.append(crc & 0xFF);
    frame.append((crc >> 8) & 0xFF);
    
    return frame;
}

发送数据

void MainWindow::Send_Modbus_Request(const QByteArray &frame) {
    if (!serial->isOpen()) {
        QMessageBox::warning(this, "错误", "请先打开串口");
        return;
    }
    
    serial->write(frame);
    serial->flush();
    
    // 显示发送数据
    ui->textEdit_tx->append(ByteArray_To_Hex(frame));
}

接收数据解析与 CRC 校验

接收数据

void MainWindow::Read_SerialData(void) {
    QByteArray data = serial->readAll();
    
    rx_buffer.append(data);
    
    // 显示接收数据
    ui->textEdit_rx->append(ByteArray_To_Hex(data));
    
    // 解析 Modbus 帧
    Parse_Modbus_Response();
}

CRC 校验

bool Check_CRC16(const QByteArray &frame) {
    if (frame.size() < 4) return false;
    
    uint16_t crc_calc = Calculate_CRC16((uint8_t *)frame.data(), frame.size() - 2);
    uint16_t crc_recv = (uint8_t)frame[frame.size() - 1] << 8 | 
                        (uint8_t)frame[frame.size() - 2];
    
    return crc_calc == crc_recv;
}

解析响应帧

void MainWindow::Parse_Modbus_Response(void) {
    if (rx_buffer.size() < 4) return;
    
    // 检查 CRC
    if (!Check_CRC16(rx_buffer)) {
        ui->statusBar->showMessage("CRC 错误");
        return;
    }
    
    uint8_t slave_addr = rx_buffer[0];
    uint8_t function_code = rx_buffer[1];
    
    // 异常响应
    if (function_code & 0x80) {
        uint8_t exception_code = rx_buffer[2];
        ui->statusBar->showMessage(QString("异常响应: 0x%1").arg(exception_code, 2, 16, QChar('0')));
        rx_buffer.clear();
        return;
    }
    
    // 功能码 03 响应解析
    if (function_code == 0x03) {
        uint8_t byte_count = rx_buffer[2];
        if (rx_buffer.size() < 3 + byte_count + 2) return;
        
        // 解析寄存器值
        for (int i = 0; i < byte_count / 2; i++) {
            uint16_t reg_value = (uint8_t)rx_buffer[3 + i * 2] << 8 | 
                                 (uint8_t)rx_buffer[4 + i * 2];
            
            // 更新界面显示
            Update_Register_Display(i, reg_value);
        }
        
        rx_buffer.clear();
    }
    
    // 功能码 06 响应解析
    if (function_code == 0x06) {
        uint16_t reg_addr = (uint8_t)rx_buffer[2] << 8 | (uint8_t)rx_buffer[3];
        uint16_t reg_value = (uint8_t)rx_buffer[4] << 8 | (uint8_t)rx_buffer[5];
        
        Update_Register_Display(reg_addr, reg_value);
        rx_buffer.clear();
    }
}

功能码 03/06/10 交互实现

读取保持寄存器(03)

void MainWindow::on_pushButton_read_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = ui->spinBox_start_addr->value();
    uint16_t reg_count = ui->spinBox_reg_count->value();
    
    QByteArray frame = Build_Modbus_Read_Request(slave_addr, start_addr, reg_count);
    Send_Modbus_Request(frame);
}

写入单个寄存器(06)

void MainWindow::on_pushButton_write_single_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t reg_addr = ui->spinBox_reg_addr->value();
    uint16_t reg_value = ui->spinBox_reg_value->value();
    
    QByteArray frame = Build_Modbus_Write_Request(slave_addr, reg_addr, reg_value);
    Send_Modbus_Request(frame);
}

写入多个寄存器(10)

QByteArray Build_Modbus_Write_Multiple_Request(uint8_t slave_addr, 
                                               uint16_t start_addr, 
                                               const QVector<uint16_t> &values) {
    QByteArray frame;
    
    frame.append(slave_addr);          // 从站地址
    frame.append(0x10);                // 功能码
    frame.append((start_addr >> 8) & 0xFF);    // 起始地址高字节
    frame.append(start_addr & 0xFF);           // 起始地址低字节
    frame.append((values.size() >> 8) & 0xFF); // 寄存器数量高字节
    frame.append(values.size() & 0xFF);        // 寄存器数量低字节
    frame.append(values.size() * 2);           // 字节数
    
    // 添加寄存器值
    for (int i = 0; i < values.size(); i++) {
        frame.append((values[i] >> 8) & 0xFF);
        frame.append(values[i] & 0xFF);
    }
    
    // 添加 CRC
    uint16_t crc = Calculate_CRC16((uint8_t *)frame.data(), frame.size());
    frame.append(crc & 0xFF);
    frame.append((crc >> 8) & 0xFF);
    
    return frame;
}

void MainWindow::on_pushButton_write_multiple_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = ui->spinBox_start_addr->value();
    
    QVector<uint16_t> values;
    values.append(ui->spinBox_value1->value());
    values.append(ui->spinBox_value2->value());
    values.append(ui->spinBox_value3->value());
    
    QByteArray frame = Build_Modbus_Write_Multiple_Request(slave_addr, start_addr, values);
    Send_Modbus_Request(frame);
}

十六进制收发显示

字节数组转十六进制字符串

QString MainWindow::ByteArray_To_Hex(const QByteArray &data) {
    QString hex;
    
    for (int i = 0; i < data.size(); i++) {
        hex += QString("%1 ").arg((uint8_t)data[i], 2, 16, QChar('0'));
    }
    
    return hex.toUpper();
}

十六进制字符串转字节数组

QByteArray MainWindow::Hex_To_ByteArray(const QString &hex) {
    QByteArray data;
    
    QStringList hex_list = hex.split(' ', Qt::SkipEmptyParts);
    
    for (const QString &str : hex_list) {
        bool ok;
        uint8_t byte = str.toUInt(&ok, 16);
        if (ok) {
            data.append(byte);
        }
    }
    
    return data;
}

发送十六进制数据

void MainWindow::on_pushButton_send_hex_clicked() {
    QString hex_str = ui->lineEdit_hex_send->text();
    QByteArray data = Hex_To_ByteArray(hex_str);
    
    if (data.isEmpty()) {
        QMessageBox::warning(this, "错误", "十六进制格式错误");
        return;
    }
    
    serial->write(data);
    serial->flush();
    
    ui->textEdit_tx->append(hex_str.toUpper());
}

寄存器批量读写界面设计

寄存器表格显示

void MainWindow::Init_Register_Table(void) {
    ui->tableWidget_registers->setColumnCount(3);
    ui->tableWidget_registers->setHorizontalHeaderLabels({"地址", "值", "描述"});
    
    // 设置行数
    ui->tableWidget_registers->setRowCount(10);
    
    // 填充地址
    for (int i = 0; i < 10; i++) {
        ui->tableWidget_registers->setItem(i, 0, 
            new QTableWidgetItem(QString::number(i)));
        ui->tableWidget_registers->setItem(i, 1, 
            new QTableWidgetItem("0"));
        ui->tableWidget_registers->setItem(i, 2, 
            new QTableWidgetItem(QString("寄存器 %1").arg(i)));
    }
}

批量读取

void MainWindow::on_pushButton_read_all_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = 0;
    uint16_t reg_count = 10;
    
    QByteArray frame = Build_Modbus_Read_Request(slave_addr, start_addr, reg_count);
    Send_Modbus_Request(frame);
}

void MainWindow::Update_Register_Display(int index, uint16_t value) {
    if (index < ui->tableWidget_registers->rowCount()) {
        ui->tableWidget_registers->setItem(index, 1, 
            new QTableWidgetItem(QString::number(value)));
    }
}

批量写入

void MainWindow::on_pushButton_write_all_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = 0;
    
    QVector<uint16_t> values;
    
    for (int i = 0; i < ui->tableWidget_registers->rowCount(); i++) {
        QString value_str = ui->tableWidget_registers->item(i, 1)->text();
        values.append(value_str.toUInt());
    }
    
    QByteArray frame = Build_Modbus_Write_Multiple_Request(slave_addr, start_addr, values);
    Send_Modbus_Request(frame);
}

QTimer 实现周期轮询

定时器初始化

#include <QTimer>

QTimer *poll_timer;

void MainWindow::Init_Poll_Timer(void) {
    poll_timer = new QTimer(this);
    connect(poll_timer, &QTimer::timeout, this, &MainWindow::Poll_Task);
}

启动轮询

void MainWindow::on_pushButton_poll_start_clicked() {
    uint32_t interval = ui->spinBox_poll_interval->value() * 1000;
    
    poll_timer->start(interval);
    ui->pushButton_poll_start->setEnabled(false);
    ui->pushButton_poll_stop->setEnabled(true);
}

停止轮询

void MainWindow::on_pushButton_poll_stop_clicked() {
    poll_timer->stop();
    ui->pushButton_poll_start->setEnabled(true);
    ui->pushButton_poll_stop->setEnabled(false);
}

轮询任务

void MainWindow::Poll_Task(void) {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = 0;
    uint16_t reg_count = 10;
    
    QByteArray frame = Build_Modbus_Read_Request(slave_addr, start_addr, reg_count);
    Send_Modbus_Request(frame);
}

日志记录功能

日志文件初始化

#include <QFile>
#include <QTextStream>
#include <QDateTime>

QFile *log_file;
QTextStream *log_stream;

void MainWindow::Init_Log_File(void) {
    QString filename = QString("modbus_log_%1.txt")
                       .arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));
    
    log_file = new QFile(filename);
    
    if (log_file->open(QIODevice::WriteOnly | QIODevice::Append)) {
        log_stream = new QTextStream(log_file);
        *log_stream << "========== Modbus 调试日志 ==========\n";
        log_stream->flush();
    }
}

记录日志

void MainWindow::Write_Log(const QString &message) {
    QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    QString log_entry = QString("[%1] %2\n").arg(timestamp, message);
    
    *log_stream << log_entry;
    log_stream->flush();
    
    ui->textEdit_log->append(log_entry);
}

记录收发数据

void MainWindow::Send_Modbus_Request(const QByteArray &frame) {
    // ... 发送代码
    
    Write_Log(QString("发送: %1").arg(ByteArray_To_Hex(frame)));
}

void MainWindow::Read_SerialData(void) {
    QByteArray data = serial->readAll();
    
    // ... 接收代码
    
    Write_Log(QString("接收: %1").arg(ByteArray_To_Hex(data)));
}

打包发布

Windows 发布

使用 windeployqt 工具打包依赖:

# 编译 Release 版本
qmake ModbusTool.pro
nmake release

# 打包依赖
windeployqt.exe release/ModbusTool.exe

Linux 发布

使用 linuxdeployqt 工具:

# 编译 Release 版本
qmake ModbusTool.pro
make release

# 打包依赖
linuxdeployqt release/ModbusTool -appimage

macOS 发布

使用 macdeployqt 工具:

# 编译 Release 版本
qmake ModbusTool.pro
make release

# 打包依赖
macdeployqt release/ModbusTool.app -dmg

完整代码示例

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSerialPort>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow {
    Q_OBJECT

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

private slots:
    void on_pushButton_open_clicked();
    void on_pushButton_read_clicked();
    void on_pushButton_write_single_clicked();
    void Read_SerialData();

private:
    Ui::MainWindow *ui;
    QSerialPort *serial;
    QByteArray rx_buffer;
    
    void Search_SerialPorts(void);
    void Config_SerialPort(void);
    uint16_t Calculate_CRC16(const uint8_t *data, uint16_t length);
    QByteArray Build_Modbus_Read_Request(uint8_t slave_addr, uint16_t start_addr, uint16_t reg_count);
    QByteArray Build_Modbus_Write_Request(uint8_t slave_addr, uint16_t reg_addr, uint16_t reg_value);
    bool Check_CRC16(const QByteArray &frame);
    void Parse_Modbus_Response(void);
    QString ByteArray_To_Hex(const QByteArray &data);
    void Send_Modbus_Request(const QByteArray &frame);
};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
    
    serial = new QSerialPort(this);
    Search_SerialPorts();
    
    connect(serial, &QSerialPort::readyRead, this, &MainWindow::Read_SerialData);
}

MainWindow::~MainWindow() {
    if (serial->isOpen()) {
        serial->close();
    }
    delete ui;
}

void MainWindow::Search_SerialPorts(void) {
    ui->comboBox_port->clear();
    
    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
        ui->comboBox_port->addItem(info.portName());
    }
}

void MainWindow::Config_SerialPort(void) {
    serial->setPortName(ui->comboBox_port->currentText());
    serial->setBaudRate(ui->comboBox_baudrate->currentText().toInt());
    serial->setDataBits(QSerialPort::Data8);
    serial->setParity(QSerialPort::NoParity);
    serial->setStopBits(QSerialPort::OneStop);
    serial->setFlowControl(QSerialPort::NoFlowControl);
}

void MainWindow::on_pushButton_open_clicked() {
    if (serial->isOpen()) {
        serial->close();
        ui->pushButton_open->setText("打开串口");
        return;
    }
    
    Config_SerialPort();
    
    if (serial->open(QIODevice::ReadWrite)) {
        ui->pushButton_open->setText("关闭串口");
    } else {
        QMessageBox::warning(this, "错误", "串口打开失败");
    }
}

uint16_t MainWindow::Calculate_CRC16(const uint8_t *data, uint16_t length) {
    uint16_t crc = 0xFFFF;
    
    for (uint16_t i = 0; i < length; i++) {
        crc ^= data[i];
        
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    
    return crc;
}

QByteArray MainWindow::Build_Modbus_Read_Request(uint8_t slave_addr, uint16_t start_addr, uint16_t reg_count) {
    QByteArray frame;
    
    frame.append(slave_addr);
    frame.append(0x03);
    frame.append((start_addr >> 8) & 0xFF);
    frame.append(start_addr & 0xFF);
    frame.append((reg_count >> 8) & 0xFF);
    frame.append(reg_count & 0xFF);
    
    uint16_t crc = Calculate_CRC16((uint8_t *)frame.data(), frame.size());
    frame.append(crc & 0xFF);
    frame.append((crc >> 8) & 0xFF);
    
    return frame;
}

QString MainWindow::ByteArray_To_Hex(const QByteArray &data) {
    QString hex;
    
    for (int i = 0; i < data.size(); i++) {
        hex += QString("%1 ").arg((uint8_t)data[i], 2, 16, QChar('0'));
    }
    
    return hex.toUpper();
}

void MainWindow::Send_Modbus_Request(const QByteArray &frame) {
    if (!serial->isOpen()) {
        QMessageBox::warning(this, "错误", "请先打开串口");
        return;
    }
    
    serial->write(frame);
    serial->flush();
    
    ui->textEdit_tx->append(ByteArray_To_Hex(frame));
}

void MainWindow::on_pushButton_read_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t start_addr = ui->spinBox_start_addr->value();
    uint16_t reg_count = ui->spinBox_reg_count->value();
    
    QByteArray frame = Build_Modbus_Read_Request(slave_addr, start_addr, reg_count);
    Send_Modbus_Request(frame);
}

void MainWindow::Read_SerialData(void) {
    QByteArray data = serial->readAll();
    
    rx_buffer.append(data);
    ui->textEdit_rx->append(ByteArray_To_Hex(data));
    
    // 解析响应帧
    if (rx_buffer.size() >= 4) {
        uint16_t crc_calc = Calculate_CRC16((uint8_t *)rx_buffer.data(), rx_buffer.size() - 2);
        uint16_t crc_recv = (uint8_t)rx_buffer[rx_buffer.size() - 1] << 8 | 
                            (uint8_t)rx_buffer[rx_buffer.size() - 2];
        
        if (crc_calc == crc_recv) {
            ui->statusBar->showMessage("接收成功");
            rx_buffer.clear();
        }
    }
}

void MainWindow::on_pushButton_write_single_clicked() {
    uint8_t slave_addr = ui->spinBox_slave_addr->value();
    uint16_t reg_addr = ui->spinBox_reg_addr->value();
    uint16_t reg_value = ui->spinBox_reg_value->value();
    
    QByteArray frame;
    frame.append(slave_addr);
    frame.append(0x06);
    frame.append((reg_addr >> 8) & 0xFF);
    frame.append(reg_addr & 0xFF);
    frame.append((reg_value >> 8) & 0xFF);
    frame.append(reg_value & 0xFF);
    
    uint16_t crc = Calculate_CRC16((uint8_t *)frame.data(), frame.size());
    frame.append(crc & 0xFF);
    frame.append((crc >> 8) & 0xFF);
    
    Send_Modbus_Request(frame);
}

总结

Qt 串口上位机开发关键点:

  • 使用 QSerialPort 模块实现串口通讯
  • 实现 Modbus RTU 帧构造和 CRC 校验
  • 支持功能码 03/06/10 的读写操作
  • 十六进制收发显示便于调试
  • 寄存器表格实现批量读写
  • QTimer 实现周期轮询
  • 日志记录功能便于问题排查
  • 使用 windeployqt/linuxdeployqt/macdeployqt 打包发布