简介:这款Qt串口助手以可执行程序形式发布,专为嵌入式开发者、硬件工程师和电子爱好者打造,满足日常串口调试、参数配置、数据收发与通信测试需求,也适合有一定编程基础的读者直接使用或二次扩展。压缩包内共五十一个文件,整体大小约二十三点五三兆,其中包含二十四个动态链接库、二十二个界面翻译文件、两个可执行程序,以及配套的源码与编译中间文件。动态链接库与主程序配套完整,解压后即可运行;翻译文件覆盖多语言界面;源码与目标文件则有助于了解Qt资源加载与编译过程。目前已有一百三十人次学习下载,既可作为即用型调试助手,也可以当作学习Qt串口通信和程序发布的参考案例。通过该程序,读者能快速上手串口通信模块的调用方式,理清依赖库的组织结构与发布目录的常见分工,对后续构建自己的串口工具或入门学习都有帮助。 做串口调试这一行的朋友,电脑里应该都躺过好几个串口调试助手。但每次遇到新设备、新协议,或者要复现一个偶发的通信bug,总会觉得现成工具差那么一点意思——界面固定死板、没法定制收发协议,更别说把收到的数据直接画成频谱图了。所以我花了两个晚上,用Qt从零写了一个串口助手exe程序,把串口通信、数据解析、时域波形显示甚至FFT频域分析都塞了进去,再用windeployqt打包成绿色版的exe发给同事,彻底告别“借别人的工具还得将就”的日子。
这篇文章就是把我实际开发、打包、踩坑的过程完整记录下来。我不会把每个函数都贴出来,那是给生成器干的事,但核心设计思路、QSerialPort的配置细节、QCustomPlot怎么做时域转频域、打包exe时遇到的问题排查方法,这些都会展开讲。如果你是刚接触Qt串口编程的嵌入式工程师,或者想把自己的小工具打包分发出去又老碰壁,这篇应该能帮你省下不少时间。
1. 为什么要自己写串口助手:工具选型与整体设计
1.1 现成工具够用,为什么还要自己开发
市面上现成的串口助手其实不少,SSCOM、XCOM、SecureCRT这些我都用过,日常收发个数据、调个AT指令完全没问题。但真到自己搞设备调试或者做产测工具的时候,痛点就很明显了:协议定制功能太弱,比如要按Modbus RTU格式组帧、要自动附加CRC校验,很多工具要么不支持,要么脚本功能极其难用;数据处理和可视化能力约等于零,收到的波形数据想直接看曲线、看频谱,还得拷贝到别的软件里;界面和交互逻辑是人家定死的,没法根据自己习惯调整,也没法集成到自动化测试流程里。
更重要的是,大部分现成工具都不是绿色免安装、可配置的,部署到产线或客户现场时很麻烦。所以自己开发一个串口助手exe,图的不是“能用”,而是“顺手”。
1.2 功能需求梳理与技术路线
做之前先盘了一下需求。核心功能必须有:串口参数配置(波特率、数据位、停止位、校验位)、收发数据显示(ASCII/HEX切换)、定时发送、文件发送、清空、统计收发字节数。这些是串口助手的标配,一个都不能少。扩展功能我加了两块:一是接收数据的时域波形滚动显示,二是对采集到的波形做FFT变换,直接切到频域看频谱。这个在调试传感器、音频模块、电机驱动器时特别好用。
技术路线用的是Qt Widgets加QSerialPort模块,编译环境是Qt 5.15.2加MSVC2019 64位,绘图用的是QCustomPlot(配合KissFFT做时域转频域)。这里有个非常重要的选型考量:编译器尽量选MSVC而不是MinGW,后面打包分发会省很多事,具体原因我在第4章详细说。
2. 串口通信核心功能实现:协议、收发与界面
2.1 QSerialPort使用要点:从枚举到参数配置
QSerialPort这个类封装得相当干净,但有几个细节新手容易踩坑。第一个是串口枚举,直接用QSerialPortInfo::availablePorts()遍历就行,但要注意它返回的是QList<QSerialPortInfo>,每个元素里有portName()(COM口号)、description()(设备描述)、manufacturer()(厂商)和serialNumber()。生产环境里最好把description和manufacturer也一起显示出来,不要只显示COM号。
我遇到过这种情况:设备管理器里能看到CH340或FTDI的USB转串口,但Qt程序枚举不到。这通常不是Qt的问题,而是驱动没装好或者设备被其他程序占用。所以我做下拉框时,除了刷新按钮,还在状态栏里把枚举结果和setPortName()后的错误信息都打出来了。打开串口的参数配置直接上代码:
serialPort->setPortName(ui->comCom->currentText()); serialPort->setBaudRate(ui->comBaud->currentText().toInt()); serialPort->setDataBits(QSerialPort::Data8); serialPort->setStopBits(QSerialPort::OneStop); serialPort->setParity(QSerialPort::NoParity); serialPort->setFlowControl(QSerialPort::NoFlowControl); if (!serialPort->open(QIODevice::ReadWrite)) { QMessageBox::critical(this, "错误", "串口打开失败:" + serialPort->errorString()); return; }需要重点提醒的是关闭串口前务必确认读写操作已经结束。我之前就遇到过一个用户反馈:程序退出后,串口在系统里还被占用了几秒钟,别的工具打不开。后来定位到是析构函数里没有显式调用close(),而是依赖QSerialPort析构自动关闭,这在极端情况下会延迟释放。现在我的代码里无论是退出还是切换串口,都会先disconnect()信号,再close()。
2.2 收发数据完整性的处理:粘包、半包和缓冲
用QSerialPort接收数据时,最直观的方式是监听readyRead()信号,然后调用readAll()。但直接这样做会有一个经典问题:粘包和半包。串口底层是按字节流到达的,驱动和操作系统会做缓冲,应用层每次readyRead()拿到的并不一定是一帧完整的数据,可能是半帧,也可能攒了好几帧。
具体的处理思路要分情况。如果只是做一个简单的调试助手,数据直接追加显示没问题,但要按帧处理就得引入帧同步机制。我在工程里加了一个接收缓冲QByteArray m_rxBuffer,每次readyRead后先append进去,然后再从缓冲里按帧格式解析。帧格式一般是固定的,比如Modbus RTU的帧头0x3A、地址码、功能码这些,解析完一帧就从缓冲里移除对应的字节。
对于高速串口,还有个容易忽略的坑:UI卡顿。如果波特率是921600,一秒能收将近90KB数据,如果每收到一次readyRead都去更新一次TextEdit,界面绝对卡死。我这里的方案是定时刷新:用QTimer每隔100毫秒把缓冲中的数据统一追加到显示区,同时把“接收字节数”的统计值在状态栏更新。这样即便数据量大,界面也不会明显掉帧。
发送端同样有讲究。比如定时发送,我用的是一个独立的QTimer,最小间隔10毫秒,不能低于这个值,否则Windows系统定时器精度不够,时间误差会很大。发送HEX格式时,需要把“AA BB CC”这种带空格的字符串转成字节数组,转换要严格校验非法字符,防止用户输了个“GG”导致程序解析崩溃。
2.3 界面设计里那些容易被忽略的细节
界面布局不用太花哨,但要符合调试习惯。左上角是串口参数区,波特率下拉框加一个“自定义”选项,方便输入非标波特率(比如7.3728M这种少见值)。接收区支持HEX和ASCII切换,这个用QPlainTextEdit的只读模式就够了。接收显示支持暂停滚动,用户正在翻历史数据时可以勾选暂停,否则源源不断的输出会让人抓狂。
状态栏放收发字节计数器和最近一条错误信息。这个“最近一条错误信息”很重要,很多人调串口时遇到Resource error这种提示一脸懵,其实在代码里加上connect(serialPort, &QSerialPort::errorOccurred, ...)并在状态栏显示,什么问题都一目了然。
同时建议给波特率、数据位、停止位这些参数加上配置持久化,也就是程序退出前写到QSettings里,下次启动自动加载。这个功能看似不起眼,实际工作中真的能省不少操作时间。
3. 数据可视化扩展:用QCustomPlot把时域信号转成频域波形
3.1 时域波形窗口的实时滚动显示
只显示串口收来的ASCII码和HEX,终究不够直观。调试音频模块、振动传感器这类设备时时域波形非常关键。我引入了QCustomPlot做波形显示,把串口收到的数据按“每一帧取一个数值点”的方式,追加到一个环形缓冲区中,然后实时重绘。
QCustomPlot的集成很直接:把qcustomplot.h和qcustomplot.cpp两个文件拷到工程里,pro文件里加一行SOURCES += qcustomplot.cpp。然后初始化一个QCustomPlot对象,设置x轴为时间(或点数),y轴为幅值。实时刷新时,不要直接清空全部数据再重绘,那样CPU占用会飙升。正确的做法是用graph(0)->setData()传入最新的整段数据,然后调用replot()。这里我踩过一个坑:如果每秒刷新三十次以上,replot()太重了,CPU占用接近满核。后来改成只在采集到新数据时才触发重绘,且重绘间隔限制在30fps以内,CPU占用瞬间降到10%以下。
3.2 FFT与KissFFT的集成:从时域到频域的关键转换
频域分析是另一个让我觉得“这工具没白写”的功能。嵌入式行业的大部分数据都是时域采集的,比如振动波形、电流波形,但很多故障特征(比如50Hz工频干扰、某个频率的谐振点)在时域里几乎看不出来,必须在频域里才明显。
Qt本身没有现成的FFT接口,网上有人推FFTW,功能强大但体积太大了,打包出来的exe会多出好几兆。我最后选的是KissFFT,它非常轻量,只由几个.c文件组成,直接加到工程里编译就行。核心调用流程如下:
- 初始化:
kiss_fft_alloc(nfft, 0, NULL, NULL),第一个参数是FFT点数,一般取采样点数的2次幂,比如1024、2048。 - 填充输入:把时域数据按实部、虚部排列成
kiss_fft_cpx结构体数组,虚部填0。 - 执行变换:
kiss_fft(cfg, fin, fout),得到频域复数结果。 - 计算幅度谱:对每个频率点的实部和虚部求模,即
sqrt(re*re + im*im)。 - 把频率轴映射出来:频率分辨率 = 采样率 / FFT点数。比如采样率是2000Hz,做了1024点FFT,那频率分辨率就是2000/1024≈1.95Hz,也就是说频谱图上相邻每个点代表的频率间隔约1.95Hz。
这里必须提醒一个重要的前提:采样率必须满足奈奎斯特定理,也就是采样率至少是信号最高频率的两倍,否则频谱图会出现混叠。串口助手里没有硬件做带限滤波,所以只能靠用户自己理解当前信号的频率成分,否则看到的高频分量可能是假的。
3.3 QCustomPlot在Release模式下崩溃的排查
用QCustomPlot做FFT频谱图时,我遇到一个极其诡异的问题:Debug模式下一切正常,一编译成Release版,点击“频域显示”按钮,程序直接崩溃,甚至没有报错信息。排查了很久,最后锁定在两个地方:
一个是KissFFT的缓冲区分配。我原来在堆上new了一个kiss_fft_cpx数组,但Release模式下优化开了之后,可能因为某些未初始化内存的问题导致读取越界。改成用std::vector管理内存后问题消失。另一个是QCustomPlot的刷新线程冲突。我在一个后台线程里调用了customPlot->graph(0)->setData(),而Qt的UI操作是线程不安全的。Debug运气好没炸,Release一开优化就露馅。把所有QCustomPlot的重绘都挪回主线程,就稳定了。
4. exe打包发布与部署:windeployqt、驱动与图标
4.1 用windeployqt把依赖项一网打尽
写好了串口助手,发给自己用很简单,但发给同事、客户就得打包成干净利落的exe。Qt的发布最有名的一个坑就是:在自己的开发机上双击运行好好的exe,拷到同是Windows的别的电脑上,双击毫无反应,或者弹窗报错“无法定位程序输入点”,再或者就是那句大名鼎鼎的“no qt platform plugin could be initialized”。
其实原因就是依赖的动态链接库没带全。Qt项目编译完的exe只会在开发机上借助系统环境变量找到Qt的DLL,别人电脑上可没有这些环境变量。解决办法是使用官方自带的windeployqt工具。我通常是这样操作的:
cd build-YourProject-Desktop_Qt_5_15_2_MSVC2019_64bit-Release windeployqt --release --no-system-d3d-compiler YourProject.exe跑完之后,exe所在的目录会多出一堆东西,包括platforms文件夹(里面最重要的是qwindows.dll)、styles文件夹、一堆Qt5*.dll,还有一个translations文件夹。这些缺一不可。特别是platforms/qwindows.dll,如果你只拷exe和Qt5Core.dll、Qt5Gui.dll,少了qwindows.dll,程序启动的时候就会报“could not be initialized”这个错。
很多新手图省事,想手动一股脑把Qt安装目录的bin文件夹全拷过去,这绝对不可取。正确的做法是只用windeployqt生成的最小文件集,然后整个目录一起分发。
这里顺便说说为什么前面强调用MSVC而不是MinGW。如果你用MinGW编译,windeployqt也能用,但打包出来的目录里会带着libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll这一堆MinGW运行时库,目标机器没装这些库也跑不起来。MSVC方案就好在Windows几乎所有机器都自带VC运行库(或通过系统更新,很常见),分发时的运行环境复杂度和潜在故障率都会小很多。
4.2 “no qt platform plugin could be initialized”问题解析
这个报错我应该已经收到过不下十次了:“我打包好的exe发给我同事,他打开直接弹窗说Windows no qt platform plugin could be initialized reinstalling the application。”
产生这个问题的原因基本排除了代码逻辑,就是运行时找不到平台插件。具体情况有两种:一种是你用windeployqt部署后,不小心把platforms文件夹删了;另一种是程序在加载插件时,因为有其他Qt版本的环境变量干扰,加载了不兼容的插件。解决办法除了确认platforms/qwindows.dll存在外,还有个高级技巧:在程序入口的main函数里,把QLibraryInfo::location(QLibraryInfo::PluginsPath)输出的路径打印出来,对比一下和exe目录是否一致。如果不一致,可以在main函数最开头调用QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath() + "/plugins"),把插件路径强制指定到本地。
对于“exe改了图标但显示还是默认图标”的问题,方法有两种:一种是在代码里调用setWindowIcon,这只改了运行时的窗口图标;另外生成的exe文件图标可以用RC文件的方式在pro文件里加入,或借助资源文件进行编译。我更喜欢RC文件方式,简单直接。
4.3 串口驱动的分发细节:CH340、CH341、FTDI
自己开发串口助手exe的用户,多半是在和USB转串口设备打交道。市面上最常用的USB转串口芯片是CH340、CH341和FTDI系列。如果目标客户电脑没装对应驱动,打开串口一定会失败。
我处理的方案是:在程序点击“打开串口”失败时,会先检查枚举出来的串口设备描述,如果是CH340芯片则提示“检测到CH340设备但可能缺少驱动,请先安装CH340驱动”,并把官方驱动下载地址写到界面上。不要试图在安装包里捆绑驱动并静默安装,驱动安装涉及系统底层,经常被杀毒软件拦截,把官方下载指引做进界面是风险最小、成功率最高的方式。
另外一提,如果用到的是USB转串口设备,拔插USB后COM口号往往变了,这在自动化工装测试里特别头疼。程序侧可以记录当前USB设备的VID/PID,拔插后重新枚举,自动匹配设备描述进入对应的新COM口号。这个功能不复杂,但对产测人员来说非常省心。
5. 实际排查经验与实用技巧
5.1 串口关闭失败、数据丢失、端口占用
开发和使用过程中遇到过几个典型问题,我直接整理成一个速查表,方便你遇到对应情况能快速定位:
| 现象 | 原因 | 排查方向与解决 |
|---|---|---|
| 打开串口提示“Access denied”或“占用” | 串口被其他程序占用 | 用任务管理器关闭上一次异常退出的调试助手,或查一下是否有监控软件占用串口 |
| 串口能打开,但收不到数据 | 波特率/校验位配错,或接线问题 | 先用串口助手自发自收(短接TX/RX)排除外部硬件问题,再用示波器量TXD波形 |
| 接收数据出现乱码 | 波特率不匹配,或USB转串口芯片质量问题 | 检查设备管理器串口参数,更换高品质USB转串口线(FTDI芯片的可靠性显著高于某些寨版CH340) |
| 高波特率下丢数 | 接收缓冲区溢出或UI刷新不及时 | 用定时刷新UI、加大QSerialPort缓冲、必要时使用线程接收 |
| exe在其他电脑打不开/闪退 | 缺失Qt运行库或VC运行库 | 用windeployqt重新部署,MSVC编译时注意VC运行库;如为静态编译则考虑版权与体积问题 |
| Linux下串口权限不足 | 用户无串口设备读写权限 | 将用户加入dialout组,或使用udev规则分配权限 |
5.2 防丢数的底层机制:接收线程与事件循环
丢数这个问题值得多写几句。之前接手过一个项目,客户反馈用串口助手接收蓝牙模块发来的大量数据,丢包率接近30%。刚开始我以为是波特率太高驱动扛不住,查了半天才发现问题出在代码在主线程里用readyRead直接处理数据,而UI刷新占用了大量事件循环时间,缓冲区里的数据被新数据覆盖。
解决办法是标准的“生产者-消费者”模式:用一个QThread专门接收串口数据,readyRead信号连接到一个槽函数,里面只把数据append进线程安全队列,不做任何UI操作。主线程用QTimer定时从队列取数据去刷新显示区。队列本身可以用QMutex保护住,或者直接上QQueue加锁。这样实测921600波特率、数据量最大时,丢包率直接归零。如果你只是自己调试用,不搞这么复杂也行,但只要是要做产测、做长时间可靠性测试,这个线程模型一定要尽早设计进去。
还有一个容易忽略的点:串口关闭的时序。程序退出时如果还有数据在飞,close()后被挂起的数据会丢失。我习惯在关闭串口前主动waitForBytesWritten并延时一小段时间(比如10ms),给底层驱动一点时间把数据送完。
5.3 串口助手还能怎么扩展
当然,写到这里肯定有朋友会问:这篇文章里提到的核心技术是不是就只能用在串口助手?其实不是。QSerialPort加QCustomPlot的组合完全可以用于各类数据采集上位机,比如CAN转串口调试、GPS/NMEA协议解析显示轨迹、传感器标定工具等。FFT频谱分析功能也完全可以复用在音频采集、振动分析、电网谐波分析等场景中。
以我个人的实际体会来说,自己动手做一个Qt串口助手exe的过程,收获最大的其实不是那几百行代码,而是对整个工具链的理解——从串口通信的时序到底层驱动,再到UI刷新与线程模型,最后到exe打包与部署。这一整套链条下来,你再回去用现成的串口调试工具,哪怕人家界面再简陋,你也能猜到它背后大概经历了哪些步骤、哪些地方可能出问题。
最后再分享一个小技巧:发布exe前,记得在开发机上用打包出的exe目录整体拷贝一份到虚拟机(Win10/Win11都装一个)里做一次“洁净环境测试”。因为开发机上Qt环境变量、VC运行库都齐,很难暴露问题;虚拟机里啥都没有,一跑就能看出哪些DLL漏了、哪些驱动缺了。用这个办法前后帮我堵住了至少一半的发布问题,强烈推荐。
本文还有配套的精品资源,点击获取