
簡介這是一款基于Qt開發的串口與網口調試助手面向嵌入式開發、上位機編程及網絡通信調試場景適合需要快速驗證串口收發或UDP數據交互的工程師與學習者。程序實現了串口數據收發和UDP網口數據收發支持ASCII與十六進制兩種顯示與發送格式串口參數、網絡地址等均可在界面上靈活配置實用性強。資源包共33個文件以C源文件、頭文件、Qt界面文件和工程文件為主輔以配置文件、makefile及編譯產物壓縮包僅2.34MB結構完整、輕量易用。目前已有1506人瀏覽學習程序內包含詳盡注釋從界面布局到通信邏輯均有清晰說明代碼層次分明關鍵函數均有注釋便于快速掌握QSerialPort和QUdpSocket的典型用法。作者還提供了release可執行文件下載后可直接運行體驗非常適合作為Qt通信編程的入門參考和工具模板。1. Qt 調試助手為什么值得自己寫串口與 UDP 同框的工程選型串口調試助手和網口調試助手通常是兩個獨立的工具調一塊同時帶串口和以太網的板卡時經常要在兩個窗口之間來回切換波特率、目標 IP、端口這些參數還要分別在兩個工具里維護。這個工程把串口和 UDP 收進同一個 Qt 界面底層用 QSerialPort 和 QUdpSocket支持 ASCII 與 16 進制收發參數統一保存到 uartConfig.ini。源碼按 uart.cpp、udp.cpp、iniconfigrw.cpp 拆成獨立模塊注釋密度很高適合從零研究 Qt 串口通信和 UDP 網口數據收發的完整流程。如果你正在做自己的調試工具或者想找一個能二次改造的底座這套代碼的主線很清楚可以邊拆邊改。2. 工程結構與數據流從 qmake 工程到串口/UDP 雙通道驅動拿到工程先不要急著編譯把文件之間的關系看明白。根目錄的drive.pro是 qmake 工程文件drive.cpp負責主界面和控件聯動uart.cpp封裝串口通道udp.cpp封裝 UDP 通道iniconfigrw.cpp統一讀寫配置。像Makefile.Debug、Makefile.Release、.qmake.stash都是 Qt Creator 影子構建生成的產物刪掉后重新構建會自動生成。數據流可以概括為UI 把控件參數交給驅動類驅動類操作 QSerialPort / QUdpSocket收到數據后通過信號回傳給主窗口。先把這條鏈路理清后面改功能才不會把邏輯堆在按鈕槽函數里。2.1 drive.pro 組織MinGW32 下的模塊劃分drive.pro里最關鍵的是 Qt 模塊引入少了serialport或network編譯階段直接找不到頭文件QT core gui serialport network greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET drive TEMPLATE app SOURCES main.cpp \ drive.cpp \ uart.cpp \ udp.cpp \ iniconfigrw.cpp HEADERS drive.h \ uart.h \ udp.h \ iniconfigrw.h FORMS drive.ui其中QT serialport network是串口和 UDP 兩個通道的基礎FORMS drive.ui會在編譯時由 uic 工具生成ui_drive.h這個文件不要手工修改。項目里實際出現了ui_drive.h就是因為drive.ui存在。命令行編譯時我一般直接走 qmake 加 makeqmake drive.pro mingw32-make -j4 release/drive.exe第一行生成 Makefile第二行并行編譯第三行直接運行 release 目錄下的可執行程序。機器上同時裝多套 Qt 時要注意 PATH 里排前面的 qmake 和 mingw32-make 必須是同一套工具鏈否則會出現missing separator或找不到 Qt 頭文件的報錯。這個工程從構建目錄build-drive-Desktop_Qt_5_12_11_MinGW_32_bit-Debug來看用的是 Qt 5.12.11 加 32 位 MinGW部署時也要用同一架構的 windeployqt不能用 64 位版本替代。2.2 串口通道 uart.cpp 的狀態機和配置讀取串口通道本質上是一個帶打開、關閉、發送三種狀態的封裝類。打開串口的常見做法是先把可能處于打開狀態的句柄關掉再用 8N1 參數重新打開bool Uart::openPort(const QString name, int baud) { if (m_serial-isOpen()) { m_serial-close(); } m_serial-setPortName(name); m_serial-setBaudRate(baud); 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)) { m_lastError m_serial-errorString(); // 保留錯誤信息給 UI 層顯示 return false; } connect(m_serial, QSerialPort::readyRead, this, Uart::onDataReady); return true; }參數說明setBaudRate接收 int可以直接傳115200這樣的整形setDataBits設置為 8 位數據、無校驗、1 位停止位即最常見的 8N1 格式絕大多數設備手冊默認都是這個組合。open(QIODevice::ReadWrite)表示串口要支持雙向收發如果設備只收不發可以改成 WriteOnly但調試助手場景下基本都用 ReadWrite。把connect放在 open 成功之后是為了避免未打開時就收到 readyRead 信號。errorString()返回的是驅動層可讀信息串口被占用時通常能直接看到AccessError比 UI 層自己猜原因可靠得多。接收側要把字節流轉成 UI 能顯示的字符串同時保留原始數據用于 hex 模式void Uart::onDataReady() { const QByteArray bytes m_serial-readAll(); m_rxBytes bytes.size(); if (m_hexMode) { emit dataReceived(bytes.toHex( ).toUpper()); } else { emit dataReceived(QString::fromLatin1(bytes)); } }這里toHex( )會在每個字節之間插入空格方便肉眼對照設備手冊上的報文。QString::fromLatin1是讓每個字節按 Latin1 映射成字符不解析多字節編碼真正的中文編碼問題放到 UI 層解決否則串口驅動類會被具體的字符集綁死。高頻數據到來時readyRead可能一次攜帶幾百字節readAll可以全部取走不會出現只讀一半的情況。2.3 UDP 通道 udp.cpp 的 QUdpSocket 收發與端口管理UDP 和串口的最大差異是串口是字節流沒有消息邊界UDP 是數據報一次 send 對應一次 recv。所以 UDP 通道的第一步是綁定本地端口而不是連接目標bool Udp::bind(int localPort) { if (localPort 0 || localPort 65535) { return false; } if (m_socket-isBound()) { m_socket-close(); } m_socket-abort(); // 清空歷史緩沖避免舊包干擾 bool ok m_socket-bind(QHostAddress::AnyIPv4, localPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint); if (ok) { connect(m_socket, QUdpSocket::readyRead, this, Udp::onReadyRead); } return ok; }端口范圍限制在 1 到 65535小于 1024 的端口在 Windows 上一般也能綁定但容易被系統服務占用所以調試工具默認建議用 9000 以上的端口。ShareAddress配合ReuseAddressHint可以讓調試人員在另一個進程里打開同一個端口觀測流量如果只是普通工具不加這兩個 flag 也能用但遇到網卡較多或程序二次啟動時會麻煩一些。綁定AnyIPv4表示所有 IPv4 網卡上的這個端口都會收到數據適合開發板通過有線網口連接、PC 同時又連著 WiFi 的場景。接收數據報時要注意 UDP 的readyRead只表示“至少有一個數據報到達”必須用 while 循環取完void Udp::onReadyRead() { while (m_socket-hasPendingDatagrams()) { QByteArray datagram; datagram.resize(int(m_socket-pendingDatagramSize())); QHostAddress sender; quint16 senderPort 0; m_socket-readDatagram(datagram.data(), datagram.size(), sender, senderPort); emit datagramReceived(datagram, sender.toString(), senderPort); } }pendingDatagramSize()返回當前緩沖區里第一個待取數據報的大小resize 到該大小后 readDatagram 才能完整取出。如果把 QByteArray 聲明在 while 外復用可能因為上一次 resize 的大小不同造成截斷或越界。發送側用writeDatagram(datagram, QHostAddress(ip), targetPort)它是異步的返回值只表示是否進入系統發送緩沖不能代表對端已經收到所以需要聯調的場合要主動做應用層應答。下面把兩條通道的關鍵差異放在一起對比通道底層類打開方式數據邊界參數保存串口QSerialPortopen(ReadWrite)字節流無邊界uartConfig.ini 的 serial 段UDP 網口QUdpSocketbind(localPort)數據報有邊界uartConfig.ini 的 udp 段3. 收發鏈路實現ASCII 與 16 進制模式切換及 UI 聯動驅動層把數據通道打開后UI 層的核心工作就集中在兩件事上把輸入框里的字符串按用戶選擇的模式轉換成真正的字節以及把收到的字節按模式顯示出來。很多初學者會直接在發送按鈕里寫serial-write(ui-sendEdit-toPlainText().toUtf8())這在設備要求 ASCII 或 hex 報文時都會出問題。下面按串口參數面板、hex 轉換、定時發送三個環節拆開講。3.1 串口參數面板與 QSerialPort 的開啟/關閉時序串口面板上的參數在打開之后就不應該再允許修改否則用戶改了波特率但底層句柄沒有重建會產生“看起來改了、實際沒生效”的誤解。打開按鈕的槽函數我是這么組織的void Drive::onOpenSerialButton() { if (m_uart-isOpen()) { m_uart-closePort(); ui-serialGroup-setEnabled(true); ui-openSerialButton-setText(打開串口); return; } const QString portName ui-portCombo-currentText(); if (portName.isEmpty()) { QMessageBox::warning(this, 提示, 請先選擇串口號); return; } int baud ui-baudCombo-currentText().toInt(); if (baud 0) { QMessageBox::warning(this, 提示, 波特率不合法); return; } if (m_uart-openPort(portName, baud)) { ui-serialGroup-setEnabled(false); ui-openSerialButton-setText(關閉串口); } else { QMessageBox::critical(this, 打開失敗, m_uart-lastError()); } }打開成功后禁用serialGroup這個細節能避免調試過程中誤改參數。portCombo里的串口列表應該在窗口構造函數里刷新一次同時在打開失敗時刷新一次因為 USB 轉串口設備經常是后插上的。刷新列表用QSerialPortInfo::availablePorts()最簡單void Drive::refreshPortList() { ui-portCombo-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { ui-portCombo-addItem(info.portName()); } }注意 Windows 上返回的是COM3這樣的名字Linux 上返回的是/dev/ttyUSB0不要對字符串做平臺相關的過濾直接交給 QSerialPort 即可。3.2 Hex 編碼轉換QByteArray 與 QString 的互轉細節發送區轉換成字節建議統一封裝成一個入口不要在按鈕槽里散落兩套邏輯QByteArray Drive::buildSendBytes(const QString input, bool hexMode) { if (!hexMode) { return input.toLatin1(); } QByteArray bytes; const QStringList parts input.split(QRegularExpression(\\s), Qt::SkipEmptyParts); for (const QString part : parts) { QString hexPart part; if (hexPart.startsWith(0x, Qt::CaseInsensitive)) { hexPart hexPart.mid(2); // 去掉 0x 前綴toInt 不識別 } bool ok false; int value hexPart.toInt(ok, 16); if (!ok || value 0 || value 255) { continue; // 非法字節不參與組包 } bytes.append(static_castchar(value)); } return bytes; }ASCII 模式優先用toLatin1()很多串口設備的協議棧只按單字節處理中文用 UTF-8 發出去會變成多字節設備端很難判斷幀邊界。如果你確認設備支持 UTF-8再換成toUtf8()。Hex 模式允許輸入AA BB或0xAA 0xBB兩種風格切分后先去掉0x前綴。QString::toInt在 base 為 16 時并不會自動處理0x所以必須手動移除。非法字節直接跳過比起把半個錯誤幀發出去更安全。接收側正好反向mid。我們已經在uart.cpp里生成顯示用的字符串。這里有一個容易踩的坑如果設備返回中文且使用 GB2312 編碼QString::fromLatin1會顯示成亂碼。排查方法很簡單先切到 hex 模式看原始字節如果看到C4 E3 BA C3再換成QString::fromLocal8Bit(bytes)就能正確顯示“你好”。下面這張表是我平時選擇編碼時的判斷依據場景推薦轉換原因通用 ASCII 字符設備QString::toLatin1()一個字符一個字節與設備端 ASCII 表一致含中文的私有協議QString::toUtf8()只有雙方明確約定 UTF-8 時才使用16 進制幀發送toInt(ok, 16)后拼 QByteArray輸入直觀便于寫固定報文接收顯示 hexQByteArray::toHex( )空格分隔便于比對手冊報文3.3 定時發送與計數器的實現姿勢定時發送用 QTimer 就能實現但要注意 start 之前先設置好間隔并且超時槽里不要創建新對象m_txTimer new QTimer(this); m_txTimer-setInterval(ui-intervalSpin-value()); connect(m_txTimer, QTimer::timeout, this, Drive::sendFromEdit);開關定時發送時if (ui-timerCheck-isChecked()) { m_txTimer-start(); } else { m_txTimer-stop(); }定時周期小于 50 ms 時QTimer 的精度會受到 Windows 消息循環粒度的影響實際觸發間隔可能漂移。需要精確到毫秒級就要把發送放到獨立線程或者改用QElapsedTimer做更精細的控制但 UI 線程不能阻塞。計數器統計要放在驅動層不要放在界面刷新邏輯里。例如在onDataReady里累加m_rxBytes再用每秒定時器讀取差值ui-rxRateLabel-setText(QString(RX %1 B/s).arg(m_uart-rxBytes() - m_lastRxBytes));這樣即使接收區刷新頻率不高速率統計也不會漏數據。4. 配置持久化與常見故障uartConfig.ini 的讀寫和排錯清單程序里保留uartConfig.ini是因為每個項目的串口號、目標 IP 都不同寫死在代碼里每次都要重新編譯。QSettings讀寫 ini 很直接但如果 key 規劃不清晰配置項會越加越亂。下面結合iniconfigrw.cpp講一下我常用的封裝方式再列一份實打實的排錯清單。4.1 iniconfigrw 的 QSettings 封裝與 key 規劃封裝的最小單元是讀寫函數把 section 和 key 作為參數傳進去調用方不需要關心 QSettings 的細節QVariant IniConfigRW::read(const QString section, const QString key, const QVariant defaultValue) const { QSettings settings(m_iniPath, QSettings::IniFormat); settings.beginGroup(section); QVariant value settings.value(key, defaultValue); settings.endGroup(); return value; } void IniConfigRW::write(const QString section, const QString key, const QVariant value) { QSettings settings(m_iniPath, QSettings::IniFormat); settings.beginGroup(section); settings.setValue(key, value); settings.endGroup(); }m_iniPath建議用QCoreApplication::applicationDirPath() /uartConfig.ini拼接絕對路徑。如果只寫文件名程序運行目錄變了就會生成一份新 ini看起來像是“配置丟失”。beginGroup對應 ini 文件中的[section]段落配置結構規劃如下[serial] portCOM3 baud115200 dataBits8 parityN stopBits1 [udp] localPort9000 targetIp192.168.1.10 targetPort9001 [display] hexReceivefalse hexSendfalse showTimestamptrueparity用N/E/O表示無校驗、偶校驗、奇校驗字符串里存中文“無校驗”在跨平臺讀取時容易遇到編碼問題。display段保存界面狀態比如 hex 開關和時間戳開關否則每次啟動程序都要重新設一遍調試條件。保存時機最好放在主窗口的closeEvent或者程序退出前統一執行而不是每次控件的值變化都寫磁盤。4.2 常見故障打開失敗、亂碼、端口被占用、UI 卡頓網口調試助手和串口調試助手名聲在外但很多問題不是代碼邏輯不行而是外部環境導致。下面是我實際調試中遇到最多的四類現象最常見原因排查方向串口打開后立刻返回失敗被另一款調試工具或設備驅動占用關閉其它串口工具查看 errorString發送后設備無反應波特率或校驗位不匹配對照設備手冊檢查 8N1檢查 TX/RX 是否接反接收區中文亂碼設備用 GB2312程序按 UTF-8 解碼切 hex 模式看原始字節再換 fromLocal8BitUDP 收不到數據防火墻攔截或端口被占綁定高端口先 ping 對端網卡確認監聽地址打開失敗時errorString()比任何自定義提示都有用。在Uart::openPort中先保存m_serial-errorString()UI 層再彈窗顯示能直接看出是AccessError還是DeviceNotFound。很多 USB 轉串口芯片在 Windows 上被系統識別成兩個 COM 口比如 CH340 是COM3和COM4其中一個可能是不可用的列表刷新后要試第二個。亂碼問題的本質是字節序列和字符集不匹配。先用 hex 模式觀察例如收到C4 E3 BA C3按 GB2312 解碼是“你好”按 UTF-8 解碼就是亂碼這時在上位機里改用QString::fromLocal8Bit解碼即可。對于不需要顯示中文的協議fromLatin1是最穩定的保底方案因為每個字節都能找到對應的 Latin1 字符不會像 UTF-8 那樣遇到非法序列就變成\uFFFD。UI 卡頓是高頻數據下最常見的問題。不要在QTextEdit里每收到一包就append一次高頻時很可能一秒鐘收到上千個包界面線程根本刷不過來。我一般用緩沖加定時刷新的方式void Drive::onDataReceived(const QByteArray data) { m_recvBuffer.append(data); if (!m_recvTimer-isActive()) { m_recvTimer-start(100); // 100ms 刷新一次界面 } } void Drive::flushRecvBuffer() { if (m_recvBuffer.isEmpty()) return; ui-recvEdit-moveCursor(QTextCursor::End); ui-recvEdit-insertPlainText(QString::fromLatin1(m_recvBuffer)); ui-recvEdit-ensureCursorVisible(); m_recvBuffer.clear(); }QTextEdit::append會在末尾自動加換行高頻數據下每一包占一行QTextEdit 的內容越積越多滾動和重繪都會變慢insertPlainText按緩沖批量插入刷新率控制在 10 Hz 左右人眼觀察足夠程序流暢度能明顯改善。5. 讓這個助手更適合日常調試回環測試與性能觀察技巧代碼跑通之后先別急著連設備用回環測試把整條鏈路驗證一遍能省下大量排查時間。5.1 串口回環短接 TX 與 RX拿出 USB 轉串口模塊用杜邦線把 TX 和 RX 短接。打開調試助手設好波特率后發送AA 55接收區如果顯示AA 55說明程序、驅動、線材、串口芯片這條鏈路是通的。如果收不到先看設備管理器里能不能看到 COM 口再看短接線有沒有接觸不良換一個模塊再試。這個測試還能驗證波特率是否真實生效波特率不匹配時回環數據會變成亂碼或直接丟字節。配合串口助手里的定時發送把間隔設成 100 ms觀察接收計數是否穩定增長就能快速判斷整條鏈路是否可靠。5.2 UDP 回環本機地址自測UDP 通道可以把目標 IP 設為127.0.0.1目標端口和本地端口填同一個值發送一幀任意數據能收到說明 QUdpSocket 的收發鏈路正常。連開發板之前先用ping -n 3 目標IP確認網口物理鏈路可通。Windows 防火墻第一次運行 Qt 程序時會彈窗必須允許訪問否則程序里 bind 成功但收不到外部網口數據。這個測試是網口調試助手里“本地正常、現場不通”的典型分界點。5.3 在接收區加入時間戳與吞吐量回環通過后可以給接收顯示加上時間戳用來觀察設備響應延遲。時間戳要在 flush 緩沖時打不要在onDataReady里打否則每個小包都會產生一次時間字符串性能開銷反而掩蓋了真實時序QString timestamp QDateTime::currentDateTime().toString(HH:mm:ss.zzz); ui-recvEdit-append(timestamp QString::fromLatin1(m_recvBuffer)); m_recvBuffer.clear();吞吐量統計用 1 秒窗口內的字節差放到獨立 QTimer 槽函數里int rxSpeed m_uart-rxBytes() - m_lastRxBytes; m_lastRxBytes m_uart-rxBytes(); ui-statusBar-showMessage(QString(RX %1 B/s).arg(rxSpeed));把速度值配合回環測試一起看如果速度一跳一停說明設備發送不是勻速可能需要在應用層加一個緩存隊列來平滑處理。本文還有配套的精品資源點擊獲取