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 打包发布