尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QT 串口助手从零开发:QSerialPort 收发、HEX 与打包发布

QT 串口助手从零开发:QSerialPort 收发、HEX 与打包发布 搞嵌入式和工控的电脑里谁没躺着三四个串口助手。sscom、xcom、友善串口助手、commix、hc-t 这些我都装过用起来确实方便点两下就能收发数据。但真要把它当成产品的一部分交付给客户问题就来了界面不能改、协议不能自定义、数据没法直接对接自己的业务逻辑客户那边还得额外装一个第三方工具售后成本一下就上去了。所以这一篇我就把之前用 QT 从零撸一个串口助手的完整过程拆开讲一遍从 QT 安装、串口模块配置、界面布局到收发线程模型、HEX 显示、定时发送再到打包发布和实机联调踩的坑全都摊开说。不管你是刚接触 QT 的新手还是写过几年上位机想补全细节的老手这篇内容都能直接拿去抄作业改成自己的协议工具、老化测试工具、产线烧写工具。1. 需求拆解与整体架构从能用到好用1.1 现成工具到底能覆盖多少场景先说清楚为什么要自己写。日常调个模块、看一眼 AT 指令回显sscom 或者 xcom 完全够用甚至比我自己写的还顺手。但下面这几种情况通用工具就开始吃力了。第一种是产线老化测试需要按固定周期发指令、收数据、判 PASS/FAIL、自动存日志通用工具没有这些逻辑只能靠人眼盯。第二种是私有协议解析比如帧头 0xAA 0x55、带长度域、带 CRC16 校验通用工具只会给你一堆十六进制数还得自己拿计算器验。第三种是交付形态客户买的是整套设备上位机必须和你的软件长一个样不能弹出来一个第三方工具的界面。第四种是自动化需要脚本驱动串口、批量烧写、自动重连这时候就得有能调用的接口。我自己遇到的最典型场景是一台带 COG12864 屏的控制板主控通过串口把屏的显示内容透传出去调试阶段要一边看屏、一边看串口原始数据、一边还要模拟按键下发。用通用助手得开两个窗口来回切最后干脆自己写了一个三栏布局的工具左边配置、中间收发、右边一个十六进制监视器效率立刻不一样。1.2 功能清单与模块划分动手之前我把功能列了一遍分成必须有的和锦上添花的。必须有的串口枚举与刷新、波特率/数据位/停止位/校验位/流控配置、打开关闭、ASCII/HEX 收发、发送历史、接收区自动滚动和暂停、清空、保存日志。锦上添花的定时发送、循环发送、自定义帧尾、CRC 校验、多串口同时打开、数据曲线、国际化多语言、命令行参数启动。按模块划分大概是这样的模块职责关键技术点设备枚举扫描可用串口、显示描述信息QSerialPortInfo参数配置波特率等参数组装QSerialPort 属性接口收发核心打开、读、写、关QSerialPort readyRead数据转换ASCII/HEX 互转、校验QByteArray、QString显示层接收/发送区渲染QPlainTextEdit辅助功能定时器、日志、配置持久化QTimer、QSettings打包发布生成可分发目录windeployqt这张表看着简单实际每一格背后都有坑下面一节一节抠。1.3 技术选型QSerialPort 还是自己封装QT 从 5.1 开始就把串口做成了独立模块 Qt Serial Port类名 QSerialPort。它底层在 Windows 走的是文件句柄加重叠 IO在 Linux 走的是 termios在 macOS 走的是 IOKit跨平台封得比较干净。我对比过三种方案直接用 QSerialPort、自己包一层 termios、用第三方库。自己包 termios 的好处是可控能直接调 ioctl 拿 modem 状态线坏处是 Windows 那边要重写一遍跨平台成本高。第三方库多见于老项目维护状态参差不齐。QSerialPort 的缺点是抽象掉了一些底层细节比如想读 DSR/CTS 需要走 pinoutSignals()灵活性略差但对绝大多数上位机够用了。所以我的选择很明确新项目一律 QSerialPort需要极限性能或者特殊 ioctl 的场景再单独开洞。2. 环境搭建QT 安装、SerialPort 模块与驱动那点事2.1 QT 版本怎么选安装包从哪来新手最容易卡在版本上。我的建议是做传统桌面上位机优先 QT 5.14 或 5.15这两个版本是长期支持线资料多、第三方库兼容好、离线安装包也齐全。QT 6 系列 API 有不少变化比如 QString::split 的行为、QRegExp 被 QRegularExpression 取代老教程照抄会编译不过。如果团队没有强制要求先用 5.15 把项目跑通再考虑升 6。编译器这块Windows 上 MinGW 和 MSVC 都能用。MinGW 的好处是自带、不用装 Visual Studio缺点是调试器是 gdb遇到 QT 内部崩溃时栈信息不如 MSVC 清晰。MSVC 需要装对应版本的 VS 生成工具然后 QT 安装器里勾选 MSVC 的 kit。我一般教学演示用 MinGW正式项目用 MSVC。安装时有个关键点QT 在线安装器默认只勾了基础组件Serial Port 模块是单独一项叫 Qt Serial Port一定要手动勾上。离线包比如 qt-opensource-windows-x86-5.14.2.exe里通常默认包含但如果装的时候取消了后面就会出现下面这个问题。2.2 Unknown module in QT: serialport 的三种成因这个报错几乎每个写串口助手的人都会遇到一次报错原文一般是Project ERROR: Unknown module in QT: serialport。它出现的原因就三类按概率从高到低排第一类QT 安装里根本没装 Serial Port 组件。去 QT 安装目录下找mkspecs/modules/qt_lib_serialport.pri这个文件如果没有就是没装。用 MaintenanceTool 补装即可。第二类.pro 文件里没加模块。这是最蠢但最常见的一种光QT core gui是不够的QT core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET SerialAssistant TEMPLATE app CONFIG c11第三类切了 kit 或者改过 QT 安装路径后构建目录里缓存了旧的 qmake 信息。这种最隐蔽表现是明明装了也说找不到。解决办法是删掉整个构建目录build-xxx-Debug 文件夹重新执行 qmake、重新构建。我一般直接在 QT Creator 里右键项目选 Clean再执行 Run qmake。如果用的是 CMake 工程而不是 qmake写法完全不同find_package(Qt5 COMPONENTS Widgets SerialPort REQUIRED) target_link_libraries(SerialAssistant PRIVATE Qt5::Widgets Qt5::SerialPort)漏了SerialPort这一项同样会在链接阶段报一堆 undefined reference。这两个构建系统混着用是新手出错的另一个重灾区别在一个工程里既写 .pro 又写 CMakeLists.txt。2.3 Windows 下的 CH340 驱动与 Linux 权限代码写得再对驱动没装也是白搭。市面上常见的 USB 转串口芯片主要有三类CH340/CH341、CP2102、PL2303。CH340 的驱动在 Windows 10 之后有时会自动装但自动装的版本经常有兼容问题表现是设备管理器里能看到 COM 口但打开就报错或者一收大数据就掉线。这时候去设备管理器右键更新驱动手动指向厂商提供的最新驱动问题基本就解决了。PL2303 有个老坑旺玖的驱动分新旧两版老芯片用新驱动会报错代码 10得回退到旧版。Linux 这边比如 Ubuntu 20.04驱动是内核自带的插上一般直接出/dev/ttyUSB0。用dmesg | grep ch34能看到识别日志。真正的坑在权限上普通用户默认没有读写/dev/ttyUSB0的权限一打开就 permission denied。两个办法临时用sudo chmod 666 /dev/ttyUSB0永久的话把自己加进 dialout 组sudo usermod -aG dialout $USER加完必须注销重登才生效很多人加完发现还是不行就是这个原因。验证用groups命令看有没有 dialout。还有一个交叉编译的场景在 Ubuntu 上搭 QT 交叉编译环境给 ARM 板子用。这时候要注意 host 端的 QT 和 target 端的 QT 版本必须匹配sysroot 要对qmake 要指定 target 的那一个。这块展开能写一整篇先记着这个原则就行。3. 界面设计布局、绘图与大数据量显示3.1 控件选型与布局策略界面我推荐用 QSplitter 做三栏左窄中宽右窄用户可以拖拽调整。左侧放配置串口下拉框、波特率下拉框、数据位、停止位、校验位、流控全用 QComboBox 或者 QSpinBox。中间上方是接收区下方是发送区用一个垂直 QSplitter 分隔。右侧可以放统计数据比如收发字节数、帧计数、错误计数。接收区的控件选择很关键这是新人最容易踩的性能坑。QTextEdit 支持富文本每次 append 都要走一遍 HTML 解析数据量一上来界面直接卡死。接收区一定要用 QPlainTextEdit它只处理纯文本性能高一个数量级。发送区用 QLineEdit 或者 QPlainTextEdit 都行看要不要多行。串口下拉框的刷新按钮别忘加因为热插拔是常态。有人图省事只在启动时枚举一次结果插拔后列表不更新只能重启软件。3.2 接收区刷新策略与 qt 绘图效率真正要命的是刷新频率。假设波特率 921600一秒能来 90KB 数据如果你在 readyRead 里直接 append 到 QPlainTextEdit主线程会被拖垮表现是界面卡住、按钮点不动严重时还会反过来影响串口接收导致丢包。我试过三种方案。第一种是限流攒够一定字节数或者过了 N 毫秒再刷新一次界面中间数据先存 QByteArray。第二种是设置setMaximumBlockCount(5000)限制 QPlainTextEdit 的段落数超过就自动丢弃最老的这个方法简单有效代价是历史数据没了需要保存就得同时写文件。第三种是接收区和显示区分离接收线程只管往一个环形缓冲区塞界面用 QTimer 每 50ms 取一次增量渲染。实测下来方案二加方案三的组合最省心环形缓冲保底界面上给个暂停显示的勾选框用户需要细看的时候勾上数据照收不误只是不刷新界面。这个功能和串口监听工具的暂停是一个思路。如果还要画曲线比如调 PID 的时候看响应曲线QCustomPlot 比 QtCharts 轻量、刷新快几百个点每秒毫无压力。QtCharts 的优势是官方维护、和 QT 版本同步缺点是自定义样式比较绕。数据量上千点以后记得开降采样不然绘图会成为新的瓶颈。3.3 编码与显示细节显示这块有两个容易忽略的点。一个是编码中文乱码基本都是因为设备发的是 GBK而 QT 默认按 UTF-8 解析。要么在转换时用 QTextCodec 指定 GBK要么干脆默认按 HEX 或者 Latin1 显示把解码交给用户。另一个是行尾处理很多设备发的数据没有换行符你如果按行读会一直读不完整正确做法是按字节流读自己判断帧尾再决定要不要换行显示。发送区我习惯加一个追加换行的勾选因为很多 AT 指令必须以 \r\n 结尾手敲太麻烦。发送历史用下拉框或者列表保存最近 20 条配合上下键切换效率提升很明显。4. 核心通信实现打开、收发与线程模型4.1 QSerialPort 配置与打开流程枚举串口用 QSerialPortInfovoid MainWindow::refreshPorts() { ui-cbPort-clear(); const auto ports QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : ports) { QString label info.portName(); if (!info.description().isEmpty()) label ( info.description() ); ui-cbPort-addItem(label, info.portName()); } }注意 addItem 的第二个参数存的是纯端口名比如 COM3 或者 ttyUSB0显示给人看的描述放在文本里。这样用户看到的是COM3 (USB-SERIAL CH340)程序拿到的是COM3两不耽误。打开串口的顺序很重要先 setPortName再 setBaudRate 等一系列参数最后 openm_serial new QSerialPort(this); m_serial-setPortName(portName); m_serial-setBaudRate(baudRate); m_serial-setDataBits(QSerialPort::Data8); m_serial-setParity(QSerialPort::NoParity); m_serial-setStopBits(QSerialPort::OneStop); m_serial-setFlowControl(QSerialPort::NoFlowControl); if (!m_serial-open(QIODevice::ReadWrite)) { QMessageBox::warning(this, 打开失败, m_serial-errorString()); return; } connect(m_serial, QSerialPort::readyRead, this, MainWindow::onReadyRead);这里有个细节参数必须在 open 之前设置open 之后再改波特率某些平台上不生效得先 close 再重开。所以界面上参数控件在串口打开后应该禁用避免用户中途改参数造成状态不一致。4.2 数据接收粘包、缓存与 Linux 丢数据readyRead 信号触发时数据可能来了一半也可能一次来了好几帧这就是粘包。串口是字节流没有消息边界你必须自己维护一个接收缓冲区按协议切分。最简单的是按帧尾切比如以 \n 结尾复杂的就是帧头长度校验。void MainWindow::onReadyRead() { m_rxBuffer.append(m_serial-readAll()); // 按帧尾切分 int idx; while ((idx m_rxBuffer.indexOf(\n)) 0) { QByteArray frame m_rxBuffer.left(idx); m_rxBuffer.remove(0, idx 1); handleFrame(frame); } }重点来了Linux 下丢数据是个高频问题。现象是短数据没问题一跑大数据量就少几包。原因通常是 readyRead 槽函数里干了耗时操作比如大量界面刷新、写文件、同步等待主线程被占住期间内核串口缓冲区满了没人读新数据就被覆盖丢弃。解决办法有两个一是槽函数里只做搬运把数据丢进队列立刻返回耗时处理交给别的线程或者 QTimer二是显式设置读缓冲区并且用 readData 主动读。如果数据量特别大可以把 QSerialPort 对象 moveToThread 挪到工作线程用 QueuedConnection 接收信号。顺带说一个概念性的坑readAll() 返回的是当前缓冲区所有可读数据read(n) 是读指定字节数readLine() 是按行读。新手容易在 readyRead 里调 readLine 而设备又没发换行结果缓冲区越积越多最后溢出。记住一条读数操作只用一个别混着来。4.3 发送、关闭与信号槽细节发送用 write返回的是实际写入的字节数不保证已经发出去。要确保发出去了可以配合 waitForBytesWritten但注意这个函数会阻塞事件循环GUI 程序里慎用非要用就设个短超时。关闭串口不是简单调 close 就完事void MainWindow::closeSerial() { if (m_serial m_serial-isOpen()) { disconnect(m_serial, nullptr, this, nullptr); m_serial-clear(QSerialPort::AllDirections); m_serial-close(); } m_rxBuffer.clear(); }先断开信号防止关闭过程中还有 readyRead 进来访问已释放的对象再清缓冲最后 close。不做这几步偶发的崩溃就是这么来的。信号槽的返回值问题也顺带说一句。connect 返回 bool成功为 true失败为 false失败一般是信号或槽不存在、参数不匹配。槽函数本身可以有返回值但在信号槽机制里返回值会被忽略所以别指望 connect 传参回来要拿结果只能用引用参数或者异步回调。跨线程连接记得显式指定 Qt::QueuedConnection不然默认是 AutoConnection行为依赖对象所属线程容易出玄学问题。5. 实用功能加料定时发送、HEX 模式与国际化5.1 定时发送与循环发送定时发送用 QTimer间隔由 QSpinBox 控制单位毫秒。要注意的是定时发送的频率不能超过串口物理能力的上限否则要么堵塞要么丢。921600 波特率理论上一秒约 92KB你发一帧 20 字节间隔 1ms 就是 20KB/s没问题但如果间隔设成 0 或者 1ms 去发大帧就会堆积。我一般限制最小间隔 10ms并在界面上给个提示。循环发送加一个计数器支持发送 N 次后停止产线测试很实用。发送前把数据准备好放一个变量QTimer 到点直接 write别在槽里做字符串解析。5.2 HEX 与 ASCII 双模式HEX 输入解析是个小坑。用户可能输入 AA 55 01 也可能输入 AA5501还可能混着中文逗号。我的处理是统一去掉分隔符空格、逗号、换行再按两位切QByteArray MainWindow::hexStringToBytes(const QString text) { QString clean text; clean.remove(QRegularExpression([^0-9A-Fa-f])); QByteArray out; if (clean.size() % 2 ! 0) return out; // 奇数个非法 for (int i 0; i clean.size(); i 2) { bool ok false; out.append(static_castchar( clean.mid(i, 2).toInt(ok, 16))); } return out; }逆向转换接收区 HEX 显示QString toHexDisplay(const QByteArray data) { return data.toHex( ).toUpper(); }toHex 传一个分隔符参数就能自动加空格比手写循环清爽。显示大段数据时注意每帧后面加换行会让界面更易读但会拖慢渲染建议加一个自动换行开关。校验这块常用的有累加和、异或、CRC16。校验值和帧一起发的时候记得明确大小端很多协议文档写得含糊实际联调时对不上只能靠开发工具抓包反推。5.3 简易协议解析与数据落盘有了上面的框架加协议解析就是往 handleFrame 里填逻辑。举个例子假设协议是 0xAA 0x55 长度 数据 CRC16先判断帧头再读长度长度够了取整帧算 CRC 对比。校验通过就交给业务处理不通过就丢进错误统计。数据落盘用 QFile 加 QTextStream或者直接 QFile 加写脏数据。文件路径可以用 QFileInfo 拿到目录、后缀、创建时间做日志按日期命名很方便。日志格式我建议用 CSV 或者带时间戳的纯文本方便后面用脚本分析。写文件同样要避免在 readyRead 里同步写量大时用缓冲区加定时刷盘。5.4 国际化和命令行参数如果工具要发给海外客户tr() 和国际化就得安排上。所有面向用户的字符串用 tr(Open) 包起来然后在 .pro 里加TRANSLATIONS zh_CN.ts en_US.ts用命令行跑lupdate SerialAssistant.pro lrelease SerialAssistant.pro生成的 .qm 文件在程序启动时用 QTranslator 加载即可。注意别把拼接的字符串放进 tr() 里tr(COM) QString::number(i)这种是翻译噩梦正确做法是用带占位符的tr(COM%1).arg(i)。命令行参数用 QCommandLineParser支持--port COM3 --baud 115200 --script test.txt这种启动方式配合自动化脚本批量测试非常香。有一个热词提到 QT 模拟鼠标点击事件这块主要用在自动化测试里用 QTest::mouseClick 或者直接构造 QMouseEvent 发送给控件能省掉大量手工点击。6. 联调实战从 STM32 到打包交付6.1 实机联调烧写失败与 PID 调试上位机写好了接下来是拿真实设备验证。最典型的是 STM32 或 51 单片机的串口升级。串口烧写失败的排查逻辑其实和普通串口通信排查是同一套现象可能原因排查方法打开串口失败驱动异常/被占用/权限不足看设备管理器关掉其他助手Linux 查 dialout能打开但无数据接线错/波特率不匹配交换 TX/RX逐个试波特率数据乱码波特率或校验位不符核对双方配置检查晶振误差下载中途失败时序太紧/流控未开降低波特率开 RTS/CTS加延时大数据量丢包缓冲区溢出/处理耗时见 4.2 节分离处理线程调试 PID 参数的时候串口助手配合曲线显示实时看设定值和实际值两条曲线比盯数字直观得多。这时候帧率可能到几百 Hz接收处理必须够快不然曲线会跳。还有一种场景是通过串口去配置 STM32CubeMX 生成的工程参数比如运行中改 SDIO 的时钟分频本质上是设备端支持了一条配置指令上位机负责下发。这类需求对发送帧的格式要求很严格建议在发送区做一个模板功能把常用指令存成下拉项一键调用。6.2 Windows 打包发布开发机上跑得好好的换台电脑就报 missing dll这是没做依赖收集。QT 提供了 windeployqt 工具# 先构建 Release 版本假设 exe 在 release 目录 windeployqt SerialAssistant.exe --release --no-translations执行完会把 Qt5Core.dll、Qt5Gui.dll、Qt5SerialPort.dll、platforms/qwindows.dll 等依赖拷到 exe 旁边。注意三点一是必须在 Release 构建产物上跑Debug 版本会带上调试 dll体积大还依赖调试库二是 MinGW 和 MSVC 的 windeployqt 不能混用要用对应 kit 目录下的三是插件目录 platforms 必须存在且里面是对应的平台插件少了它会报 This application failed to start because no Qt platform plugin could be initialized。如果要发给没装 VC 运行库的机器MSVC 版本还要带上 vcredist或者干脆静态编译。MinGW 版本相对省事但首次启动会检查一些运行库。6.3 Ubuntu 下的打包Linux 打包用 linuxdeployqt原理类似但要注意可执行文件的依赖路径和 RPATH。如果你在 Ubuntu 20.04 上构建产出的包在老一点的发行版上可能因为 glibc 版本过高跑不起来。跨发行版兼容是个大坑稳妥做法是在目标系统对应的容器或者虚拟机上构建。7. 我踩过的坑和几点经验7.1 几个反复出现的老问题数据量一大界面就卡十有八九是接收区控件选错了或用 QTextEdit 渲染富文本。换成 QPlainTextEdit 加限流立竿见影。Linux 下丢数据八成是槽函数里同步干了重活把读取和显示拆开就好。串口打开后切换参数不生效是因为参数必须在 open 之前设置open 之后要改只能关了重开。程序退出时偶发崩溃检查串口是否在析构前正确 close信号是否断开QSerialPort 对象是否挂在 this 上自动释放。对方用 CH340 模块我这边收到的数据总多几个字节后来发现是流控没关设备那边把流控线当数据了统一设成 NoFlowControl 就正常。7.2 一点个人体会做这类工具别一上来就追求功能全。我建议先把能打开、能收发、能看清这三件事做扎实跑通一个真实设备然后再逐个加定时、加 HEX、加校验、加曲线。每加一个功能就回归测试一次避免改着改着把基础功能改坏。另外协议解析这部分一定要留扩展点。我现在写的这个串口助手核心收发是一个类协议解析抽成了独立的接口换一套设备协议只改一个文件其他代码一行不动。这个习惯是从第三个项目开始养成的前期多花半天设计后期省下的时间远不止半天。最后再分享一个小技巧工具的配置上次选的串口、波特率、发送历史、窗口布局全部用 QSettings 存起来下次启动自动恢复。看着不起眼但每次省下的那几十秒一年下来也是不少时间用户对工具的好感度也是从这些细节上来的。
返回列表