☰
Qt串口通讯入门:QSerialPort实现串口调试助手全流程解析
2026/10/19 3:15:25 网站建设 项目流程

简介:面向QT初学者的串口通讯入门例程,整合了虚拟串口与串口调试助手,帮助零基础用户在无物理串口设备的环境下快速上手QSerialPort库的参数配置、数据收发与设备枚举。资源共11个文件,压缩包大小约5.51MB,以Qt完整工程源码为主,既有.cpp、.h、.pro、.ui等工程文件,也附带XCOM串口调试助手和vspd虚拟串口程序,无需额外准备工具即可对照学习。例程演示了打开串口、设置波特率、数据位、校验位、停止位以及发送和接收数据的核心流程,并给出可独立运行的exe程序,方便通过调试助手实时查看收发内容,从现象反向理解代码逻辑。已有380人浏览学习,适合正在学习Qt开发、嵌入式通信或物联网毕业设计的初学者,也可作为快速搭建串口调试环境、验证通信流程的参考模板。

1. 项目定位:一个让串口调试不靠玄学的 Qt 入门例程

嵌入式新手第一周最容易翻车的不是烧录,而是串口。焊好板子、写好下位机,上位机却怎么都收不到数据,最后发现是波特率配错或者串口被占。这里要拆的这套 Qt 串口通讯小白入门例程,就是一套开箱即用的串口调试工具源码:用 Qt 的 QSerialPort 模块实现了从端口扫描、参数配置、数据收发到虚拟串口环回联调的完整链路,主界面功能对标常见串口调试助手。拿到源码可以直接编译跑通,边看代码边改参数,适合刚接触 Qt 和嵌入式上位机开发的读者。不需要自己造轮子去碰 Windows API,一个模块就能解决跨平台串口读写。

2. 项目结构与技术选型:QSerialPort 与信号槽驱动的通信骨架

2.1 目录结构:从 main.cpp 到 mainwindow 的头一回分离

这套例程的目录结构非常克制,没有引入复杂的工程分层,但已经为二次开发留好了位置。我拆开源码包之后,核心文件一眼就能看清职责:

qt-serial-demo/ ├── CMakeLists.txt # 构建脚本,Qt6 默认使用 CMake ├── main.cpp # 应用入口,启动事件循环 ├── mainwindow.h # 主窗口头文件,定义了 UI 控件的槽函数 ├── mainwindow.cpp # 主窗口实现,串口逻辑全在这里 ├── mainwindow.ui # Qt Designer 设计的界面布局 └── resources/ # 图标、qss 样式文件

注意这里用 CMake 而不是 qmake,是 Qt6 社区的默认趋势。如果你还在用 Qt5,CMakeLists 里只需要把find_package(QT NAMES Qt6 COMPONENTS SerialPort)改成Qt5即可,其余逻辑完全兼容。

mainwindow.cpp 这一个文件承担了大部分工作,它把串口对象QSerialPort作为成员变量持有,界面上的按钮、下拉框通过信号槽和串口动作绑定。这种写法对入门者来说最好理解:界面是前台,串口是后台,一个类里管全部。

2.2 为什么选 QSerialPort 而不自己封装 C++ 串口

有些老开发习惯用第三方的serial库或者直接调用open()、ioctl()操作文件描述符,跨平台代码要写两套。QSerialPort 的聪明之处在于它把 Windows COM 口、Linux ttyS 和 macOS cu 设备统一成了setPortName()接口,配置参数也是一套结构体搞定。

另一个关键点是事件驱动。传统串口程序往往要开一个线程循环读数据,而 QSerialPort 拿着readyRead()信号通知你“有数据到了”,配合 Qt 的主事件循环,天然适合 UI 程序。你不用自己管理读写锁,信号槽队列会把跨线程问题挡在处理之外。

虚拟串口联调也是这个例程的亮点。项目源码里没有带虚拟串口软件,但文档指明用类似“Configure Virtual Serial Port Driver”的工具成对创建 COM1 和 COM2,然后把例程绑到 COM1,用另一个串口助手绑 COM2,数据就从 COM1 进去虚拟串口、从 COM2 出来,实现环回。这样即使没有硬件板子,也能把整个收发链路跑通例程。

3. 核心模块拆解:一个字节从设备到文本框的完整链路

3.1 串口配置界面与参数绑定

界面里那几个下拉框不是摆设,它们直接决定底层串口打开的成败。先看 mainwindow.cpp 里的初始化函数片段:

void MainWindow::initUi() { // 扫描本机可用串口端口,填入下拉框 foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { ui->comboBoxPort->addItem(info.portName()); } // 常见波特率预设,实际使用时可以手动输入 QStringList baudList = {"9600", "19200", "38400", "115200", "460800"}; ui->comboBoxBaud->addItems(baudList); ui->comboBoxBaud->setCurrentText("115200"); // 数据位、校验位、停止位按最常用的组合设置 ui->comboBoxDataBits->addItems({"8", "7"}); // 数据位 ui->comboBoxStopBits->addItems({"1", "2"}); // 停止位 ui->comboBoxParity->addItems({"None", "Even", "Odd"}); }

这段代码做了三件事:枚举可用串口、预设波特率、列出常见帧格式参数。QSerialPortInfo::availablePorts()是静态方法,返回当前系统挂载的所有串口设备名字,比写死在配置里强得多。波特率里 115200 是嵌入式调试最常用的速率,和大多数单片机默认配置一致。

配置参数不是选完就生效的,必须等到点“打开串口”按钮后,这些下拉框的值才会被读取并应用到serialPort对象上:

void MainWindow::onOpenButtonClicked() { if (serialPort->isOpen()) { serialPort->close(); ui->btnOpen->setText("打开串口"); return; } serialPort->setPortName(ui->comboBoxPort->currentText()); serialPort->setBaudRate(ui->comboBoxBaud->currentText().toInt()); serialPort->setDataBits(QSerialPort::Data8); // 和下拉框8对应 serialPort->setStopBits(QSerialPort::OneStop); serialPort->setParity(QSerialPort::NoParity); if (!serialPort->open(QIODevice::ReadWrite)) { QMessageBox::critical(this, "错误", "串口打开失败,请检查占用情况"); return; } ui->btnOpen->setText("关闭串口"); }

注意打开串口的调用open(QIODevice::ReadWrite),这个参数表示读写同时打开,比只读或只写更符合调试助手需求。如果返回 false,多半是串口被别的软件占用,或者端口名不存在。这里的错误弹窗对小白很友好,不用去看 stderr 输出。

3.2 数据接收:readyRead 信号与丢包分析

接收数据是本例程最有料的部分。QSerialPort 只要收到数据,就会发readyRead()信号,你要在构造函数里把这个信号和一个槽函数连起来:

connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::onReceiveData);

槽函数内部怎么读?最常见的做法是:

void MainWindow::onReceiveData() { QByteArray data = serialPort->readAll(); if (ui->checkBoxHexReceive->isChecked()) { // 十六进制显示模式 QString hexStr = data.toHex(' '); ui->textEditReceive->append(hexStr.toUpper()); } else { // 直接按文本追加,常见设备输出ASCII可以这样读 ui->textEditReceive->append(QString::fromUtf8(data)); } }

这里有个坑:readAll()返回的是当前缓冲区里所有字节,但一次readyRead()可能只有几十字节,尤其串口速率高时,一帧数据可能被拆成多次信号触发。如果你直接把这些片段各自 append 到文本框,会出现“一帧数据断成几行”的现象。更稳的做法是维护一个成员变量QByteArray m_recvBuffer,每次收到数据先追加进去,再用自定义帧头帧尾解析出完整报文。

参数说明:toHex(' ')会把0x01转成01,每个字节之间加空格,方便人工对照协议文档。QString::fromUtf8(data)假设设备发送的是 UTF-8 文本,如果下位机发的是 GBK 编码,这里就要换成QString::fromLocal8Bit(data),不然中文注释会乱码。

3.3 数据发送:手动发送与定时轮询

发送逻辑相对简单,核心是write()方法:

void MainWindow::onSendButtonClicked() { QByteArray sendData; if (ui->checkBoxHexSend->isChecked()) { QString text = ui->lineEditSend->text().trimmed(); // 把 "01 02 A0 FF" 这种十六进制字符串解析成字节数组 QStringList hexList = text.split(' ', Qt::SkipEmptyParts); for (const QString &hex : hexList) { sendData.append(static_cast<char>(hex.toInt(nullptr, 16))); } } else { sendData = ui->lineEditSend->text().toUtf8(); } qint64 wrote = serialPort->write(sendData); if (wrote < 0) { statusBar()->showMessage("发送失败,串口可能已关闭"); } }

手动发送时要注意编码:勾选十六进制模式下,用户在输入框里写AA BB,代码会按空格拆分并转成字节;没勾选时直接按 UTF-8 文本发送。很多下位机协议是十六进制帧格式,比如帧头0xAA、帧尾0x0D 0x0A,直接用这个模式就好,不用再接一个在线编码转换工具。

定时发送功能也不复杂,例程里用了一个QTimer,每隔固定毫秒调用一次以上发送逻辑。定时发送的间隔不能太短,建议至少设置 50ms,否则底层驱动缓冲区和虚拟串口转发都可能丢数据。这部分代码在例程里是注释掉的,因为不同设备的稳定时间差异太大,需要你自己按实际设备调。

4. 常见问题与避坑指南:从编译到虚拟串口调试的硬经验

4.1 串口总是打开失败,返回 “Permission denied”

现象:在 Linux 下运行例程,点“打开串口”后弹出错误框,串口列表明明能看到/dev/ttyUSB0。

原因:当前用户没有dialout组权限,Linux 对串口设备的访问受组权限控制。

解决:不要用 sudo 运行,执行sudo usermod -aG dialout $USER,然后重新登录一次。Windows 下类似的问题通常是由串口驱动被其他程序抢占导致,先把设备管理器里那个 COM 口的占用进程找出来关掉。

4.2 收到数据显示成乱码,中文全变问号

现象:下位机发的是 GBK 编码的中文字符串,但界面显示乱码。

原因:上一节提过,默认QString::fromUtf8只能解 UTF-8。很多国产单片机固件用 GBK 编码。

解决:把接收显示那句改成QString::fromLocal8Bit(data),或者界面增加一个编码下拉框,运行时切换。发送方向同理,如果下位机只认 ASCII 或者 GBK,就不该用toUtf8(),改为QString::toLatin1()或指定编码。

4.3 虚拟串口配好了,但数据发不出去

现象:用了虚拟串口工具创建 COM1 和 COM2,例程绑定 COM1,另一个串口助手绑定 COM2,点击发送,两边都看不到数据。

原因:虚拟串口工具没有勾选“启用环回”或者两个端口没有连接成 pair。少数免费工具只创建单个虚拟串口,需要手动把两个口用虚拟线缆连起来。

解决:打开虚拟串口工具的连接设置,确认 COM1 和 COM2 已经关联到同一条虚拟线缆。连接完成后,用最简单的“自发自收”验证:例程绑定 COM1,发送数据,然后让例程同时监听 COM2,如果 COM2 能收到,说明链路通。

4.4 界面卡死,接收大数据量时窗口无响应

现象:设备每秒发送几百字节波形数据,读取一段时间后界面拖动很卡。

原因:textEditReceive->append()在数据量增加时会触发全量重绘,Qt 的富文本控件在长文本下性能极差。

解决:限制显示行数,比如保留最多 1000 行,超出后删除前面的部分。更专业的方式是接收数据不直接进 UI,而是先缓存到自定义环形缓冲区,用定时器每 100ms 刷新一次界面。

4.5qmake报错找不到 SerialPort 模块

现象:随手从网上下载的 Qt 5.9 环境里,编译时提示Project ERROR: Unknown module(s) in QT: serialport。

原因:安装 Qt 时没有勾选 SerialPort 模块,这个模块不是默认组件。

解决:重跑 Qt 安装器,在“组件”列表里勾选对应版本的 “Qt SerialPort”,或者用包管理器安装。Ubuntu 下sudo apt install qt6-serialport-dev,Windows 下需要重装 Qt 组件。

5. 二次开发:把例程改成你的专属调试工具

5.1 增加十六进制报文日志存储

调试下位机时,光看屏幕上的数据不够,得把每一帧存下来分析。在现有代码基础上加一个日志类是顺手的事:

void MainWindow::appendLog(const QByteArray &rawData) { QFile logFile("data.log"); if (logFile.open(QIODevice::Append)) { logFile.write(rawData.toHex(' ')); logFile.write("\n"); } }

在接收槽函数里调用它,就能把每次收到的原始数据按十六进制追加到日志文件。注意写文件是磁盘 I/O,不能在串口数据回调里做太频密,一般每帧数据写上一条就可以。想更高级就按日期生成文件名,配合QDateTime自动起名。

5.2 按帧解析协议并绘制实时波形

很多设备会连续发送传感器数据,例程里的文本显示只能看到“乱数字”。改造思路是先按帧头帧尾切帧,再把负载字段转成整数,用 Qt Charts 绘制曲线。核心解析代码大概长这样:

QByteArray frameData; if (data.startsWith(0xAA) && data.endsWith(0x0D)) { frameData = data.mid(1, data.size() - 2); // 去掉帧头和帧尾 }

拿到 frameData 后按协议位偏移解析出温度、压力等,然后丢给 QLineSeries 更新图表。这种改造不涉及串口底层,完全是在现有接收链路里加一道解析,和例程的耦合度很低。

5.3 将串口对象独立成后台线程

如果串口数据量大到主线程 UI 卡顿,可以考虑把QSerialPort移到单独的QThread里,主线程用信号槽接收解析好的数据帧。注意不要再跨线程调用serialPort->write外的 UI 方法,Qt 信号槽队列连接会自动处理线程切换。

我自己的习惯是拿到这套例程后,先不改代码,用虚拟串口把收发链路完整跑一遍,确认环境没问题,再开始动 UI 和协议解析。当年第一次碰串口时,因为不知道端口被设备管理器里一个隐藏进程占了,硬是折腾了两天,后来按“设备管理器—端口—查看占用”的排查顺序才解决。从那以后,每次拆新例程都强制自己先编译、再环回、最后看代码。希望这篇文章帮你省下那两天用来踩坑的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询