
簡介一份基于Qt框架的串口調試助手完整工程源碼面向Qt初學者與需要快速搭建串口通信工具的開發者解決從零創建圖形界面、配置串口參數以及收發數據的常見需求。壓縮包共31個文件包含9個C源文件、9個頭文件、3個UI界面文件以及圖標、背景圖片、資源文件和工程配置文件整體大小約234KB結構清晰便于直接編譯運行。目前已有1164人學習/下載。源碼涵蓋波特率、數據位等參數設置串口打開關閉與數據收發邏輯并封裝了QextSerialPort串口通信類界面部分配有UI設計文件和樣式表適合對照學習串口通信、槽函數事件處理及界面布局技巧也可作為功能擴展的起點。 很多做嵌入式、硬件調試或者上位機開發的朋友第一個想動手寫的桌面工具就是串口調試助手。Qt 生態里現成的例子不少但真要把功能做成自己順手的樣子還是得從零開始搭一遍。這篇文章分享我用 Qt Widgets 從零實現串口調試助手 V1.0 的完整過程包含源碼級別的實現思路和踩坑記錄適合想練手 Qt 串口編程、或者想定制自己調試工具的朋友參考。1. 動手之前串口調試助手的功能邊界1.1 一個串口工具需要哪些功能串口調試助手的核心就一句話把電腦的串口數據收進來、發出去并且讓人看得懂。圍繞這個核心V1.0 我列了一個功能清單串口配置端口號、波特率、數據位、校驗位、停止位開關串口打開成功后配置區鎖死防止誤改數據接收支持 ASCII 和 Hex 兩種顯示模式數據發送支持 ASCII 和 Hex 兩種發送模式支持發送新行\n 和 \r\n清空接收區、清空發送區收發字節計數狀態欄實時顯示串口開關狀態這個清單看著簡單但每一項都對應著實際調試場景。比如 Hex 收發幾乎每個搞單片機的人都要用因為很多下位機協議就是按字節定義的直接用 ASCII 看會亂套。再比如計數功能調試大數據量傳輸時用來判斷有沒有丟字節非常實用。1.2 V1.0 版本故意不做的事我見過不少人在第一個版本就想加各種復雜功能波形顯示、Modbus 解析、自動應答、腳本執行……我的建議是第一版別碰這些。原因很簡單這些功能全部依賴底層的串口收發邏輯底子沒打穩上面堆再多都是空中樓閣。所以 V1.0 我刻意砍掉了這些東西不做 QThread 收發線程第一版在 UI 線程里直接讀寫串口串口本身有緩沖區只要不一次性讀上千字節完全夠用。后面發現問題再加線程也不遲。不做波形繪制那是 V2.0 的事。不做分包粘包智能分析V1.0 只做最樸素的讀一段、顯示一段。說白了V1.0 的目標不是炫技而是把一個串口調試助手該有的骨架搭出來跑通了、用順了后續才有迭代的底氣。2. 環境準備Qt 版本、編譯器與工程文件2.1 我的開發環境選型我用的組合是 Qt 5.15.2 MinGW 64-bit。選 5.15 LTS 而不是 Qt 6主要原因有三個Qt 5.15 的 QSerialPort 模塊非常穩定網上資料最多遇到問題搜一下遍地都是答案。很多嵌入式開發板廠商提供的交叉編譯工具鏈、示例工程還是基于 Qt 5直接對齊省心。如果你還想把代碼移植到 ARM 板子上跑Qt 5.15 的支持面更廣。編譯器我選了 MinGW。因為它是開源工具鏈不依賴 Visual Studio下載安裝 Qt 時自帶配置零成本。MSVC 版本我也試過但 Qt 對 MinGW 的適配更順手Debug 和 Release 都不容易出幺蛾子。2.2 創建工程與 pro 文件配置打開 Qt Creator新建項目選擇Application → Qt Widgets Application類名我起了MainWindow基類保持QMainWindow不變。建好工程后第一件事是改.pro文件加上串口模塊QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets QT serialport TARGET SerialAssistant TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp HEADERS \ mainwindow.h FORMS \ mainwindow.ui這里有個細節QT serialport必須加否則#include QSerialPort編譯直接報找不到頭文件。很多新手卡在這一步其實根本不是代碼問題是模塊沒引入。2.3 確認編譯器與運行庫用 MinGW 64-bit 編譯運行后Release 包需要帶上Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、Qt5SerialPort.dll和platforms/qwindows.dll這幾個關鍵文件才能在其他電腦上跑。V1.0 階段不用折騰打包開發機上直接運行就行但心里要有個數。3. 界面設計不是簡單擺控件3.1 串口配置區的交互邏輯界面我用QDesigner拖出來的但布局邏輯不是隨手放的。整個窗口分成三個區域頂部配置區、中間收發區、底部狀態欄。配置區我用了一個QGroupBox標題叫串口設置里面放了兩行第一行端口號下拉框、波特率下拉框、數據位下拉框第二行校驗位下拉框、停止位下拉框、打開/關閉按鈕這幾個下拉框的值要提前填好。波特率我填了這些常用值波特率典型場景9600老式設備、工業儀表默認值115200嵌入式開發最常用460800GPS/高速模塊調試921600串口屏、快速固件升級注意QSerialPort有現成的QSerialPort::BaudRate枚舉但我直接用QComboBox的文本再轉int這樣以后要加 250000、1000000 這種非標準波特率只要在界面里加一行就行不用改代碼。3.2 收發區與按鈕布局中間收發區我用了QSplitter分成左右兩個子區域左邊是接收區右邊是發送區。用QSplitter的好處是用戶可以自己拖分隔條接收區大了看數據舒服發送區大了編輯多行協議方便。接收區是一個只讀的QTextEdit上方一行復選框Hex 顯示、自動滾動。發送區是一個可編輯的QTextEdit上方一行復選框Hex 發送、追加回車換行下面還有一行按鈕發送、清空發送區、清空接收區、定時發送。定時發送我加了一個QSpinBox輸入框單位是毫秒旁邊放一個啟動/停止按鈕。這個功能調試周期性的心跳包非常管用。3.3 控件禁用/啟用管理的信號槽設計界面做好之后最重要的是狀態管理邏輯。核心原則是配置控件只在串口關閉時可編輯打開后一律禁用發送相關只在串口打開時可操作。這一步用代碼寫在打開和關閉函數里void MainWindow::setSerialEnabled(bool enabled) { ui-comboBoxPort-setEnabled(!enabled); ui-comboBoxBaud-setEnabled(!enabled); ui-comboBoxDataBit-setEnabled(!enabled); ui-comboBoxParity-setEnabled(!enabled); ui-comboBoxStopBit-setEnabled(!enabled); ui-btnSend-setEnabled(enabled); ui-btnTimerSend-setEnabled(enabled); ui-spinBoxTimerInterval-setEnabled(enabled); }別小看這個集中管理的函數。一開始我是在打開串口成功后一行行去 setEnabled在關閉函數里再一行行設回來代碼又長又容易漏。抽成函數之后無論從哪個入口切換串口狀態界面狀態都不會錯亂。4. 核心實現串口打開、接收、發送與 Hex 顯示4.1 打開串口前參數如何生效打開串口的代碼是重點直接決定后續收發是否正常。我的實現思路是先從界面上把所有參數讀出來再依次設置到QSerialPort對象上最后調用open()。void MainWindow::onBtnOpenClicked() { if (m_serial-isOpen()) { m_serial-close(); ui-btnOpen-setText(打開串口); setSerialEnabled(false); return; } m_serial-setPortName(ui-comboBoxPort-currentText().split( ).first()); m_serial-setBaudRate(ui-comboBoxBaud-currentText().toInt()); 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)) { ui-btnOpen-setText(關閉串口); setSerialEnabled(true); statusBar()-showMessage(tr(串口 %1 已打開) .arg(ui-comboBoxPort-currentText().split( ).first())); } else { QMessageBox::warning(this, 錯誤, tr(打開串口失敗%1).arg(m_serial-errorString())); } }這里有個經驗端口下拉框的顯示文本我用了comName description的格式比如COM3 USB-SERIAL CH340所以在設置端口名時一定要.split( ).first()把純凈的COM3取出來否則串口打不開。還有一個細節串口打開之后如果參數設置不對很多設備不會立刻報錯而是收發數據亂碼。所以數據位、校驗位、停止位在 V1.0 里我先固定給了最常用的 8N1 組合界面上雖然留了下拉框但默認值就是 8 位、無校驗、1 位停止位避免新手亂選導致調試時出詭異問題。4.2 readyRead 信號與 readAll 的配合接收數據的核心是QSerialPort::readyRead信號。這個信號只要串口緩沖區里來了新數據就會觸發一次我在構造函數里把它連到了自定義槽函數onReadyRead。connect(m_serial, QSerialPort::readyRead, this, MainWindow::onReadyRead);void MainWindow::onReadyRead() { QByteArray data m_serial-readAll(); if (data.isEmpty()) return; m_recvBytes data.size(); ui-labelRecvBytes-setText(tr(接收: %1 字節).arg(m_recvBytes)); if (ui-checkBoxHexRecv-isChecked()) { QString hex QString(data.toHex( )).toUpper(); ui-textEditRecv-insertPlainText(hex ); } else { ui-textEditRecv-insertPlainText(QString::fromUtf8(data)); } if (ui-checkBoxAutoScroll-isChecked()) { QTextCursor cursor ui-textEditRecv-textCursor(); cursor.movePosition(QTextCursor::End); ui-textEditRecv-setTextCursor(cursor); } }這里有一個非常重要的概念readyRead信號不保證一次收到完整的一幀數據。串口數據是一個字節一個字節進來的操作系統攢一批就會通知應用去讀這個一批可能是半個協議幀、也可能是三五個協議幀。V1.0 我只是把收到的數據原樣追加到文本框里不做幀解析所以直接readAll()沒問題。如果你后面要解析協議一定要自己維護一個接收緩沖區做粘包處理這是后話。4.3 Hex 顯示與編碼處理Hex 顯示我用了QByteArray::toHex( )這個重載函數會用空格分隔每個字節的 Hex 值顯示出來像01 03 00 21肉眼很容易對齊校驗。再調用.toUpper()把字母轉大寫視覺上更清爽。實測發現小寫的de ad be ef和大寫的DE AD BE EF在協議對比場景下大寫明顯更好核對。ASCII 顯示這里有個容易忽視的坑串口下位機返回的字節不一定都是 UTF-8 編碼。很多老設備用的是 GBK 或者 GB2312直接QString::fromUtf8(data)會出現亂碼。V1.0 我先統一按 UTF-8 處理因為手頭調試的設備都是自己寫的代碼控制端也用 UTF-8 發送夠用了。如果哪天要接老設備再根據情況選擇QString::fromLocal8Bit或者QTextCodec。你要做自己的版本務必搞清楚下位機的編碼格式。4.4 發送邏輯與定時發送發送數據相對簡單但要分清楚 Hex 和 ASCII 兩條路徑Hex 模式下用戶輸入的是01 03 00 00 00 01這種字符串必須用QByteArray::fromHex轉成真正的字節數組再發送ASCII 模式下直接把字符串轉 UTF-8 字節數組發送。void MainWindow::onBtnSendClicked() { if (!m_serial-isOpen()) { QMessageBox::warning(this, 提示, 請先打開串口); return; } QByteArray data; if (ui-checkBoxHexSend-isChecked()) { data QByteArray::fromHex(ui-textEditSend-toPlainText().toUtf8()); } else { data ui-textEditSend-toPlainText().toUtf8(); if (ui-checkBoxAppendNewline-isChecked()) { data.append(\r\n); } } if (data.isEmpty()) return; qint64 written m_serial-write(data); m_sendBytes written; ui-labelSendBytes-setText(tr(發送: %1 字節).arg(m_sendBytes)); }追加回車換行我默認用\r\n不用單獨的\n。因為很多 MCU 端的串口中斷處理是按行掃描的\n之前如果需要\r才能正確解析少了就出問題。\r\n是兼容性最好的選擇。定時發送我用了QTimer在構造函數里創建m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, MainWindow::onBtnSendClicked);啟動按鈕的槽函數里void MainWindow::onBtnTimerToggled(bool checked) { if (checked) { int interval ui-spinBoxTimerInterval-value(); m_timer-start(interval); ui-btnTimerSend-setText(停止定時發送); } else { m_timer-stop(); ui-btnTimerSend-setText(啟動定時發送); } }注意QTimer的最小精度受操作系統影響Windows 下如果定時間隔設成 1 毫秒實際誤差可能到 15 毫秒左右。所以定時發送我建議最小間隔設 10ms實際測試大多數設備 10ms 發一包也夠快了沒必要追求 1ms 極值。5. 踩坑與優化V1.0 留下的經驗5.1 Hex 輸入校驗不嚴謹V1.0 里有個 bug 一直沒修如果用戶在 Hex 發送模式下輸入了非法字符比如01 03 ZZQByteArray::fromHex會把非法字符當成 0 來處理結果發出去的數據完全不是用戶想要的還不好排查。我的建議是如果你要繼續開發一定要在發送前校驗輸入合法性。簡單做法是檢查字符串中每個字符是否在0-9A-Fa-f和空白字符范圍內不合法就彈窗提示別讓數據稀里糊涂發出去了。5.2 大流量接收時界面卡頓V1.0 在接收高頻數據時比如下位機每 10ms 發 100 字節QTextEdit每觸發一次readyRead就插入一段文本界面刷新壓力很大極端情況下 CPU 能跑到 40% 以上。這個問題最簡單的緩解方案是限制接收緩沖區長度。比如只保留最近 200KB 的顯示數據超過就丟棄前面的舊內容if (ui-textEditRecv-document()-characterCount() 200000) { QTextCursor cursor ui-textEditRecv-textCursor(); cursor.setPosition(0); cursor.setPosition(10000, QTextCursor::KeepAnchor); cursor.removeSelectedText(); }但即使這樣V1.0 在 115200 波特率滿速接收時依然會有點卡。真要長期跑高速數據還是得把接收數據處理放到QThread里UI 線程只負責更新顯示。這是 V2.0 的重點優化方向。5.3 端口下拉框的刷新問題程序啟動時QSerialPortInfo::availablePorts()只會獲取一次端口列表。如果用戶先把設備插好再打開程序一切正常但如果程序開著中途拔了 USB 轉串口再插一個不同號的端口下拉框不會自動更新。最簡單的人工刷新方案是加一個刷新端口按鈕void MainWindow::refreshPorts() { ui-comboBoxPort-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { QString desc info.description(); if (desc.isEmpty()) desc info.manufacturer(); ui-comboBoxPort-addItem(info.portName() desc); } }注意處理description為空的情況很多免驅芯片在部分系統上拿不到描述信息直接拼字符串會多一個空位看起來怪怪的。5.4 串口被占用時打開失敗的提示調試時最容易遇到的一個場景串口助手已經打開了某個端口你再開一個串口終端去連同一端口此時open()會失敗。V1.0 的代碼里已經彈了QMessageBox::warning但在實際使用中我還加了一行日志輸出把errorString()打印到控制臺方便對照。這里想特別提醒遇到端口打開失敗的時候先檢查是不是被別的工具占用了不要盲目懷疑代碼。6. 一些使用層面的心得V1.0 寫完之后我日常調試 STM32 和 ESP32 的串口日志、調傳感器模塊、看 GPS 報文用的都是這個小工具。雖然功能上完全沒法跟 SSCOM、XCOM 這種老牌工具比但自己寫的工具最大的優勢就是聽話——想加什么功能直接改。比如我后來在 V1.0 基礎上加了一個定時發送遞增序號的功能用來測試一個設備長時間跑穩定性。這個需求放在現成工具上很難實現但在自己的代碼里只是加一個計數器拼進數據包的事情。另外一個小技巧調試時把接收區設置成自動滾動數據會一直往下刷但如果想回看之前的內容記得把自動滾動關掉。這個看似不起眼的交互細節在真實調試中能省很多事。用 Qt 做串口調試助手的這條路走到這里算是真正入門了。從打開串口到了解readyRead的觸發機制、從toHex顯示到QTimer定時發送每一個功能點背后都有值得深挖的原理。你把 V1.0 跑通之后再去研究 V2.0 的線程優化、協議解析、波形顯示會順暢得多。畢竟代碼骨架是你的想怎么長都是你說了算。本文還有配套的精品資源點擊獲取