本篇目标
对嵌入式工程师来说,串口调试助手是出厂率最高的自研上位机。本篇从零实现一个功能完整的版本:自动枚举可用串口、下拉选择波特率/数据位/停止位/校验位、一键开关串口、接收区支持 HEX/文本双模式显示、发送区支持 文本/HEX 双模式发送 和 定时发送,状态栏实时显示连接状态与收发字节数。做完它能学到 QSerialPort 的完整用法、信号槽处理异步数据的思路,以及和单片机联调的实战经验。
一、项目目标与界面设计
先明确功能清单:
- 参数配置:串口号(自动枚举)、波特率(9600~115200 常用档)、数据位(7/8)、停止位(1/2)、校验位(无/偶/奇);
- 连接管理:打开/关闭串口一键切换,异常掉线(拔掉 USB 转串口)自动复位界面;
- 接收区:
readyRead信号驱动,HEX 与文本两种显示模式可切换,可选时间戳,显示累计接收字节数; - 发送区:文本或 HEX 两种编码发送,支持定时循环发送(周期可调);
- 状态栏:连接状态、当前串口参数、收发字节计数。
界面布局规划如下:
界面分为四块:顶部参数配置区、中部接收区、下部发送区、底部状态栏。控件不多,全部用代码 + 布局器搭建,不依赖 .ui 文件。
二、工程搭建
串口功能在 Qt Serial Port 模块里,需要显式引入。qmake 的 .pro:
# SerialAssistant.pro
QT += core gui serialport # 串口模块,Qt5/Qt6 写法相同
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
TEMPLATE = app
TARGET = SerialAssistant
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.hCMake 写法:
cmake_minimum_required(VERSION 3.16)
project(SerialAssistant LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets SerialPort)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets SerialPort)
add_executable(SerialAssistant
main.cpp
mainwindow.cpp
mainwindow.h
)
target_link_libraries(SerialAssistant PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::SerialPort)找不到 QSerialPort 头文件
Qt6 需要安装 6.2 及以上版本才带 Serial Port 模块(在线安装器的 Additional Libraries 里勾选);Linux 发行版则需要另装开发包,例如 Ubuntu 的
libqt5serialport5-dev。报错QSerialPort: No such file时先检查模块是否装上,再检查.pro/CMake 是否引入。
三、头文件设计
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QTimer>
// 前置声明,减少头文件依赖
class QComboBox;
class QPushButton;
class QCheckBox;
class QSpinBox;
class QPlainTextEdit;
class QLabel;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
private slots:
void refreshPorts(); // 重新枚举可用串口
void toggleSerial(); // 打开/关闭串口(同一个按钮复用)
void onReadyRead(); // 收到数据(readyRead 信号触发)
void sendData(); // 发送数据
void toggleTimerSend(bool checked); // 定时发送开关
private:
// 界面搭建(每个区域一个函数,避免构造函数臃肿)
QWidget *buildSerialConfig(); // 参数配置区
QWidget *buildRecvArea(); // 接收区
QWidget *buildSendArea(); // 发送区
void buildStatusBar(); // 状态栏
// 工具函数
void appendRecv(const QString &text); // 向接收区追加一行
void updateStatusBar(); // 刷新状态栏与按钮文字
static QString toHexString(const QByteArray &data); // 字节 -> "48 65 ..."
static QByteArray fromHexString(const QString &hex); // "48 65 ..." -> 字节
// 串口与定时器
QSerialPort *serial_ = nullptr;
QTimer *sendTimer_ = nullptr;
// 参数配置区
QComboBox *portBox_ = nullptr;
QComboBox *baudBox_ = nullptr;
QComboBox *dataBox_ = nullptr;
QComboBox *stopBox_ = nullptr;
QComboBox *parityBox_ = nullptr;
QPushButton *openBtn_ = nullptr;
// 接收区
QPlainTextEdit *recvEdit_ = nullptr;
QCheckBox *hexShowCheck_ = nullptr;
QCheckBox *timeStampCheck_ = nullptr;
QPushButton *clearRecvBtn_ = nullptr;
// 发送区
QPlainTextEdit *sendEdit_ = nullptr;
QCheckBox *hexSendCheck_ = nullptr;
QCheckBox *timerCheck_ = nullptr;
QSpinBox *intervalSpin_ = nullptr;
QPushButton *sendBtn_ = nullptr;
// 状态栏
QLabel *statusLabel_ = nullptr;
QLabel *rxLabel_ = nullptr;
QLabel *txLabel_ = nullptr;
qint64 rxCount_ = 0; // 累计接收字节数
qint64 txCount_ = 0; // 累计发送字节数
};
#endif // MAINWINDOW_H设计要点:
- 枚举值直接当
currentData存进 QComboBox(如QSerialPort::EvenParity),发送配置时取出来转一下类型即可,避免一长串 if-else 文本翻译; sendTimer_超时信号直接连sendData(),定时发送就是”每 N 毫秒帮你点一次发送按钮”,不引入第二套发送逻辑;- 收发计数用
qint64,长时间挂机也不会溢出。
四、构造函数与参数配置区
#include "mainwindow.h"
#include <QCheckBox>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSerialPortInfo>
#include <QSpinBox>
#include <QStatusBar>
#include <QTextCursor>
#include <QTime>
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle(tr("串口调试助手"));
resize(860, 560);
// 串口对象与定时发送定时器
serial_ = new QSerialPort(this);
sendTimer_ = new QTimer(this);
connect(sendTimer_, &QTimer::timeout, this, &MainWindow::sendData);
// 组装三个区域 + 状态栏
auto *central = new QWidget(this);
auto *layout = new QVBoxLayout(central);
layout->addWidget(buildSerialConfig());
layout->addWidget(buildRecvArea(), 1); // stretch=1,接收区可拉伸
layout->addWidget(buildSendArea(), 1);
setCentralWidget(central);
buildStatusBar();
// 信号槽:串口数据到达 / 串口异常 / 界面交互
connect(serial_, &QSerialPort::readyRead, this, &MainWindow::onReadyRead);
connect(serial_, &QSerialPort::errorOccurred, this, &MainWindow::onSerialError);
connect(openBtn_, &QPushButton::clicked, this, &MainWindow::toggleSerial);
connect(sendBtn_, &QPushButton::clicked, this, &MainWindow::sendData);
connect(clearRecvBtn_, &QPushButton::clicked, recvEdit_, &QPlainTextEdit::clear);
connect(timerCheck_, &QCheckBox::toggled, this, &MainWindow::toggleTimerSend);
refreshPorts(); // 启动时枚举一次串口
updateStatusBar();
}
MainWindow::~MainWindow()
{
if (serial_->isOpen())
serial_->close(); // 退出前关闭串口,把资源还给系统
}
QWidget *MainWindow::buildSerialConfig()
{
auto *group = new QWidget(this);
auto *layout = new QHBoxLayout(group);
layout->setContentsMargins(8, 8, 8, 8);
// 各参数下拉框:把 QSerialPort 枚举值直接作为 itemData 存起来
portBox_ = new QComboBox(group);
baudBox_ = new QComboBox(group);
const QList<qint32> bauds { 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600 };
for (qint32 b : bauds)
baudBox_->addItem(QString::number(b), b);
baudBox_->setCurrentText(QStringLiteral("115200")); // 默认最常用档位
dataBox_ = new QComboBox(group);
dataBox_->addItem(QStringLiteral("8"), QSerialPort::Data8);
dataBox_->addItem(QStringLiteral("7"), QSerialPort::Data7);
dataBox_->setCurrentIndex(0);
stopBox_ = new QComboBox(group);
stopBox_->addItem(QStringLiteral("1"), QSerialPort::OneStop);
stopBox_->addItem(QStringLiteral("2"), QSerialPort::TwoStop);
stopBox_->setCurrentIndex(0);
parityBox_ = new QComboBox(group);
parityBox_->addItem(tr("无"), QSerialPort::NoParity);
parityBox_->addItem(tr("偶"), QSerialPort::EvenParity);
parityBox_->addItem(tr("奇"), QSerialPort::OddParity);
parityBox_->setCurrentIndex(0);
openBtn_ = new QPushButton(tr("打开串口"), group);
// 一行排开:标签 + 下拉框 + 打开按钮
auto addPair = [&layout](const QString &text, QWidget *w) {
layout->addWidget(new QLabel(text, w->parentWidget()));
layout->addWidget(w);
};
addPair(tr("串口号"), portBox_);
addPair(tr("波特率"), baudBox_);
addPair(tr("数据位"), dataBox_);
addPair(tr("停止位"), stopBox_);
addPair(tr("校验位"), parityBox_);
layout->addWidget(openBtn_);
layout->addStretch(1); // 剩余空白推到右侧
return group;
}说明:
addItem(文字, 数据)的第二个参数通过currentData()取回,参数配置到串口时一行代码完成映射,这是本篇最推荐的小技巧;- 波特率默认 115200,这是现代单片机(尤其带 DMA 的 HAL 库工程)最常用的档位,减少每次打开都要改一遍的麻烦;
- 界面拆成
buildXxx()三个小函数,构造函数只剩”组装 + 连信号”,可读性远比几百行塞在一起好。
五、接收区与发送区
QWidget *MainWindow::buildRecvArea()
{
auto *group = new QWidget(this);
auto *layout = new QVBoxLayout(group);
// 标题行:接收区 + HEX 显示 + 时间戳 + 计数 + 清空按钮
auto *topRow = new QHBoxLayout;
hexShowCheck_ = new QCheckBox(tr("HEX 显示"), group);
timeStampCheck_ = new QCheckBox(tr("显示时间戳"), group);
clearRecvBtn_ = new QPushButton(tr("清空"), group);
topRow->addWidget(new QLabel(tr("接收区"), group));
topRow->addWidget(hexShowCheck_);
topRow->addWidget(timeStampCheck_);
topRow->addStretch(1);
topRow->addWidget(clearRecvBtn_);
layout->addLayout(topRow);
// 接收文本框:只读,随便复制
recvEdit_ = new QPlainTextEdit(group);
recvEdit_->setReadOnly(true);
layout->addWidget(recvEdit_, 1);
return group;
}
QWidget *MainWindow::buildSendArea()
{
auto *group = new QWidget(this);
auto *layout = new QVBoxLayout(group);
auto *topRow = new QHBoxLayout;
hexSendCheck_ = new QCheckBox(tr("HEX 发送"), group);
timerCheck_ = new QCheckBox(tr("定时发送"), group);
intervalSpin_ = new QSpinBox(group);
intervalSpin_->setRange(10, 60000); // 10ms ~ 60s
intervalSpin_->setValue(1000);
intervalSpin_->setSuffix(tr(" ms"));
topRow->addWidget(new QLabel(tr("发送区"), group));
topRow->addWidget(hexSendCheck_);
topRow->addWidget(timerCheck_);
topRow->addWidget(intervalSpin_);
topRow->addStretch(1);
layout->addLayout(topRow);
sendEdit_ = new QPlainTextEdit(group);
sendEdit_->setPlaceholderText(tr("输入要发送的内容,HEX 模式下请写 \"01 A0 FF\" 这样的十六进制串"));
layout->addWidget(sendEdit_, 1);
sendBtn_ = new QPushButton(tr("发送"), group);
layout->addWidget(sendBtn_);
return group;
}
void MainWindow::buildStatusBar()
{
statusLabel_ = new QLabel(this);
rxLabel_ = new QLabel(this);
txLabel_ = new QLabel(this);
statusBar()->addWidget(statusLabel_, 1); // stretch=1,占满剩余宽度
statusBar()->addPermanentWidget(rxLabel_);
statusBar()->addPermanentWidget(txLabel_);
}两个细节:
- 接收框
setReadOnly(true)但不禁止选择复制——调试时经常要把报文贴进文档,只读编辑器正好满足”能看能抄不能改”; - 状态栏分两组:
addWidget放的消息会被showMessage()暂时顶掉,addPermanentWidget放的收发计数永远可见,各司其职。
六、枚举可用串口
void MainWindow::refreshPorts()
{
const QString lastPort = portBox_->currentText(); // 记住当前选择
portBox_->clear();
// QSerialPortInfo 扫描系统里所有可用串口
const auto ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : ports) {
// 显示 "COM3(CH340)" 这样带硬件描述的条目,数据仍存纯端口名
const QString desc = info.description();
const QString text = desc.isEmpty()
? info.portName()
: QStringLiteral("%1(%2)").arg(info.portName(), desc);
portBox_->addItem(text, info.portName());
}
// 尽量恢复之前的选择
const int idx = portBox_->findData(lastPort);
if (idx >= 0)
portBox_->setCurrentIndex(idx);
}说明:
QSerialPortInfo::availablePorts()返回系统当前所有串口(Windows 上是 COMx,Linux 上是 ttyUSB0/ttyACM0 等);- 显示描述、存储端口名:CH340、CP2102 这类 USB 转串口芯片的描述能帮你一眼认出哪根线是单片机;
- 界面上可以再加一个”刷新”按钮连接到
refreshPorts();更优雅的做法是定时器每秒枚举一次,让拔插串口自动反映到列表里。
串口被占用
如果打开串口报
PermissionError(Linux)或打开后收发无反应,第一反应应该是:是不是被别的程序占了? 串口是独占资源,Arduino IDE 的串口监视器、另一个调试助手、甚至还没关干净的上一次运行实例,都会把口占住。Windows 上可以用设备管理器确认端口号,Linux 上用lsof /dev/ttyUSB0查占用进程。
七、打开与关闭串口
void MainWindow::toggleSerial()
{
if (serial_->isOpen()) {
// ---- 关闭分支 ----
sendTimer_->stop();
timerCheck_->setChecked(false);
serial_->close();
updateStatusBar();
return;
}
// ---- 打开分支:把界面上的参数逐项写入 QSerialPort ----
serial_->setPortName(portBox_->currentData().toString());
bool ok = false;
serial_->setBaudRate(baudBox_->currentData().toInt(&ok));
serial_->setDataBits(static_cast<QSerialPort::DataBits>(dataBox_->currentData().toInt()));
serial_->setStopBits(static_cast<QSerialPort::StopBits>(stopBox_->currentData().toInt()));
serial_->setParity(static_cast<QSerialPort::Parity>(parityBox_->currentData().toInt()));
serial_->setFlowControl(QSerialPort::NoFlowControl); // 普通调试线不接流控线
if (!serial_->open(QIODevice::ReadWrite)) {
QMessageBox::critical(this, tr("错误"),
tr("打开串口失败:%1").arg(serial_->errorString()));
return;
}
// 打开成功:锁定参数区,防止运行中误改
for (QComboBox *box : { portBox_, baudBox_, dataBox_, stopBox_, parityBox_ })
box->setEnabled(false);
updateStatusBar();
}
void MainWindow::updateStatusBar()
{
const bool open = serial_->isOpen();
openBtn_->setText(open ? tr("关闭串口") : tr("打开串口"));
if (open) {
statusLabel_->setText(tr("串口已打开:%1")
.arg(serial_->portName()));
} else {
statusLabel_->setText(tr("串口已关闭"));
for (QComboBox *box : { portBox_, baudBox_, dataBox_, stopBox_, parityBox_ })
box->setEnabled(true);
}
rxLabel_->setText(tr("接收:%1 字节").arg(rxCount_));
txLabel_->setText(tr("发送:%1 字节").arg(txCount_));
}要点:
- 一个按钮复用两个语义:根据
isOpen()决定本次是开还是关,界面始终只有一个”打开/关闭串口”按钮,不会出现两个按钮状态不同步的问题; - 打开成功后锁定参数下拉框。运行中改参数要么被忽略要么行为怪异,锁掉是最省心的方案(需要热改波特率的高级场景另说);
open()失败必须弹窗并展示errorString(),“拒绝访问”四个字能帮你 10 秒定位到占用问题。
八、接收数据:readyRead 与双模式显示
void MainWindow::onReadyRead()
{
const QByteArray data = serial_->readAll(); // 一次性取走缓冲区全部数据
if (data.isEmpty())
return;
rxCount_ += data.size();
QString line;
if (timeStampCheck_->isChecked())
line += QStringLiteral("[%1] ").arg(QTime::currentTime().toString("HH:mm:ss.zzz"));
if (hexShowCheck_->isChecked()) {
// HEX 模式:"He" -> "48 65",toUpper 规范成大写
line += toHexString(data);
} else {
// 文本模式:按 UTF-8 解码(对方发 GBK 需要改用 QStringDecoder 转码)
line += QString::fromUtf8(data);
}
appendRecv(line);
rxLabel_->setText(tr("接收:%1 字节").arg(rxCount_));
}
void MainWindow::appendRecv(const QString &text)
{
// 光标移到末尾再插入,保证新数据追加在最后并自动滚动
QTextCursor cursor = recvEdit_->textCursor();
cursor.movePosition(QTextCursor::End);
cursor.insertText(text);
}
QString MainWindow::toHexString(const QByteArray &data)
{
return QString::fromLatin1(data.toHex(' ').toUpper());
}
QByteArray MainWindow::fromHexString(const QString &hex)
{
QString cleaned = hex;
cleaned.remove(' '); // 允许带空格
cleaned.remove('\n'); // 允许带换行
cleaned.remove('\r');
if (cleaned.isEmpty())
return QByteArray();
if (cleaned.size() % 2 != 0) // 奇数个字符:补前导 0,"5A 1" -> "05A 1"
cleaned.prepend('0');
return QByteArray::fromHex(cleaned.toLatin1());
}理解这一节的关键是 readyRead 的语义:
readyRead只保证”缓冲区里有新数据”,不保证”一次是一整帧”。单片机连发 100 字节,可能触发 1 次,也可能触发 5 次回调,这取决于操作系统和 USB 转串口芯片的打包时机。这就是所谓的粘包/分包问题:HEX 显示无所谓,但按”行”解析协议时必须自己在数据里找帧头帧尾,或者积累到固定长度再解析;- 处理方式永远先
readAll()清空缓冲区,不读完的话下一次 readyRead 立刻又来,白耗 CPU; - HEX 与文本只是”渲染方式”不同,底层收到的永远是 QByteArray 原始字节——这个分离设计让显示模式可以随时热切换,互不干扰。
HEX 显示下"中文乱码"
用 HEX 模式看到
E4 BD A0 E5 A5 BD是正常现象——那是”你好”的 UTF-8 字节,不是乱码;切回文本模式就能看到中文。反过来,文本模式下出现浣犲ソ,说明对方发的是 GBK 编码字节,需要用 QStringDecoder(Qt6)或 QTextCodec(Qt5)指定 GBK 解码。先搞清楚字节层面发的是什么,再谈显示对不对。
九、发送数据与定时发送
void MainWindow::sendData()
{
if (!serial_->isOpen()) {
QMessageBox::information(this, tr("提示"), tr("请先打开串口"));
timerCheck_->setChecked(false); // 顺带关掉定时
return;
}
QByteArray payload;
if (hexSendCheck_->isChecked())
payload = fromHexString(sendEdit_->toPlainText()); // "01 A0 FF" -> 原始字节
else
payload = sendEdit_->toPlainText().toUtf8(); // 文本按 UTF-8 编码
if (payload.isEmpty())
return;
const qint64 written = serial_->write(payload);
if (written < 0) {
QMessageBox::warning(this, tr("错误"), tr("发送失败:%1").arg(serial_->errorString()));
return;
}
txCount_ += written;
txLabel_->setText(tr("发送:%1 字节").arg(txCount_));
}
void MainWindow::toggleTimerSend(bool checked)
{
if (checked) {
sendTimer_->start(intervalSpin_->value()); // 按 SpinBox 设定的周期开跑
} else {
sendTimer_->stop();
}
}说明:
- HEX 发送对协议调试极其重要:很多单片机协议是二进制的,用文本框发
01 A0 FF这种字符串是发不出去的,必须转成真正的字节流,fromHexString()干的就是这个活; - 定时发送本质是 QTimer 周期触发
sendData(),周期从 SpinBox 读,运行中也能改——QTimer 的start(ms)每次都会重置间隔; - 发送失败(串口中途被拔)时记得停掉定时器,否则会每秒弹一个错误框。
十、异常处理与收尾
// 头文件 private slots 中补一个声明:void onSerialError(QSerialPort::SerialPortError error);
void MainWindow::onSerialError(QSerialPort::SerialPortError error)
{
// NoError 只是例行通知,忽略
if (error == QSerialPort::NoError)
return;
// 设备被拔掉、资源突然失效:复位整个界面
if (serial_->isOpen() &&
(error == QSerialPort::ResourceError ||
error == QSerialPort::DeviceNotFoundError ||
error == QSerialPort::PermissionError)) {
sendTimer_->stop();
serial_->close();
QMessageBox::warning(this, tr("串口异常"),
tr("串口已断开:%1").arg(serial_->errorString()));
updateStatusBar();
}
}入口文件照例很短:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow win;
win.show();
return app.exec();
}完整文件清单:
| 文件 | 职责 |
|---|---|
main.cpp | 程序入口 |
mainwindow.h | 界面成员、槽函数、工具函数声明 |
mainwindow.cpp | 界面搭建、串口配置、收发逻辑、异常复位 |
运行程序,选好串口和 115200,点”打开串口”,状态栏变成”串口已打开”,拔掉 USB 线试试——界面应自动弹窗复位,这就是 errorOccurred 在起作用。
十一、与单片机联调的实用建议
自研上位机的终点是接上真板子。下面是几条踩出来的经验:
第一步永远做 echo 测试。 给单片机烧一段”收到什么就回什么”的固件(以 STM32 HAL 为例,在 HAL_UART_RxCpltCallback 里 HAL_UART_Transmit 原样回发,再重启接收;或用带透传固件的 ESP8266/蓝牙模块)。echo 通了,说明整条链路(上位机发送 → 线缆 → 单片机 → 回发 → 上位机接收)全部正确,之后协议调不通就只可能是代码逻辑问题,排查范围一下子缩小一半。
收不到数据时,按这个顺序排查:
- TX/RX 是否交叉:单片机 TX 接 USB 转串口的 RX,交叉相连,这是最高频的接错点;
- 是否共地:USB 转串口与单片机板之间必须连 GND,只接两根信号线十有八九收不到;
- 波特率是否一致:注意单片机时钟配置错了会导致”标称 115200 实际 9600”,用示波器量一下字节起始位的脉宽(115200 对应约 8.68 µs)最快定位;
- 串口是否被占用:烧录器、Arduino IDE 串口监视器、别的调试助手都会独占端口;
- 电平是否匹配:3.3V 单片机接 RS232 电平(±12V)会烧片,USB 转串口模块要确认是 TTL 电平,且跳线在 3.3V 档;
- 驱动是否装好:CH340/CH341、CP2102、FT232 各需要对应驱动,Windows 设备管理器里有黄色感叹号就是它;
- 流控是否关了:单片机一般不接 RTS/CTS,上位机务必设 NoFlowControl。
协议调试技巧:先开 HEX 显示和 HEX 发送,确认字节层面的收发完全一致后,再写协议解析。带帧头帧尾的协议在 readyRead 里做状态机解析,不要假设”一次回调 = 一帧”。
最佳实践
- 接收处理里绝不调用
waitForReadyRead()之类的阻塞函数——信号槽本身就是为异步设计的,阻塞会冻结界面;- 收发都走原始
QByteArray,显示/编辑层才做文本转换,层次分清后乱码问题自然消失;errorOccurred里处理ResourceError是”拔线不掉死”的关键,正式工具必备。
十二、扩展练习
- 接收区自动清屏:数据超过 10 万行自动清空,防止内存无限膨胀;
- 保存接收日志:加”记录到文件”开关,用 QFile 把原始字节落盘,事后离线分析;
- 协议帧解析:定义一个”帧头 + 长度 + 数据 + CRC”的小协议,在接收端实现状态机拆帧并校验;
- 多串口同时收发:用多个 QSerialPort 实例做 A/B 口对通测试;
- 数据波形显示:解析收到的 ADC 数值,用 QtCharts 实时画曲线,一步到位做成简易示波器。
自测:三个问题检验掌握程度
readyRead信号触发一次,能保证读到”一整帧”数据吗?不能的话该如何设计解析?QComboBox::addItem("偶", QSerialPort::EvenParity)里第二个参数有什么用?比 if-else 翻译文本好在哪?- 单片机端 echo 固件回发正常,但上位机收不到数据,列举至少三个可能的硬件原因。 (答案都在上文)