☰
基于QT的串口、TCP、UDP通讯源代码与工程实践
2026/10/18 3:45:45 网站建设 项目流程

简介:面向QT开发者的TCP/UDP与串口通信源码包,覆盖网络通信和本地串行通信两大场景,适合嵌入式、物联网及设备控制等项目参考学习。压缩包共67个文件,以cpp/h源码为主,包含TCP客户端/服务器、UDP收发、串口读写等核心模块,另有ui界面文件、pro工程文件和说明文档,整体仅2.11MB,便于快速导入QT工程。到目前为止已有434人学习下载。借助这份代码,开发者可以直接查看QTcpSocket、QUdpSocket、QSerialPort等类的实际用法,理解连接管理、数据收发、串口参数配置及异常处理流程;源码中完整的界面和工程结构也能帮助快速搭建跨平台通信原型,通过pro工程即可在Windows、Linux等系统上构建运行,是学习QT通信编程的实用示例。

基于QT的TCP UDP 串口 通讯源代码

做上位机开发的朋友应该都有体会,日常调试硬件设备、对接传感器、下发控制指令,几乎绕不开三种通讯方式:串口、TCP、UDP。以前我电脑上装了三个软件——串口调试助手、网络调试助手、UDP测试工具,桌面乱成一团,而且不同软件的操作逻辑还不一样,每次换工具都要重新适应一遍。后来干脆用QT写了一个集成了三类通讯方式的上位机工具,实测下来效率提升明显,代码也一直在迭代完善。今天就把这套源代码的架构思路、关键实现和踩坑记录整理出来,给同样需要这个方向参考的朋友一个完整的交代。

这套源码解决的核心问题很直接:一套界面、一套数据收发框架,同时搞定串口、TCP Server/Client、UDP收发,并且预留了波形显示和时域转频域的扩展接口。适合正在做工控上位机、仪器仪表数据采集、嵌入式联调的朋友参考,也适合刚入门QT通讯开发、想看正经工程代码的人拿来学习。

1. 项目定位与整体架构设计

1.1 为什么把三类通讯塞进同一个工具

用同一个工具管理所有通讯方式,远远不只是界面好看的问题,关键在于“数据链条统一了”。做联调的时候经常遇到测试场景:先用串口接设备看原始报文,然后把报文通过TCP转发给远程服务端,同时用UDP组播给其他节点。如果每个环节都换一个软件,数据拷来拷去,不仅效率低,还容易出错。

我开发时给这套工具定了一个原则:底层通讯分开管,上层逻辑统一调。所有接收到的数据都统一格式化成十六进制字符串和一帧带时间戳的记录,发给同一个数据转发模块。这样一来,无论是串口收到FE 01 02 A8,还是TCP收到相同内容,后续的数据处理逻辑完全一致,不用为每个通道单独写一套协议解析。

QT在这个场景下的优势很明显。QSerialPort、QTcpSocket、QTcpServer、QUdpSocket这几个类都是Qt Network和Qt SerialPort模块里现成的跨平台封装,信号槽机制天然适合异步收发。相比调用Windows API或第三方串口库,QT的封装程度高,代码量能省一半以上,而且Linux下重新编译一遍就能跑,不用改业务逻辑。

1.2 模块划分与代码骨架

整个项目按功能拆成四个核心模块,各自独立成类,通过信号槽通信:

模块类名职责
串口通讯SerialWorker串口参数配置、数据收发、错误处理
TCP通讯TcpWorkerTCP客户端连接、服务端监听、数据收发
UDP通讯UdpWorkerUDP单播/组播收发、端口绑定
界面交互MainWindow参数配置区、数据显示区、协议处理入口

这种划分方式有个直接好处:任何一个通讯模块出问题,只需要改对应的Worker类,完全不影响其他模块。比如我后来发现UDP接收缓冲溢出,只需要在UdpWorker里调整socket的接收缓冲区大小,串口和TCP代码一行都不用动。

项目使用QT5.12以上的版本开发,pro文件里需要包含serialport和network模块:

QT += core gui network serialport greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

2. 三大通讯模块的核心实现与关键细节

2.1 串口通讯:QSerialPort的正确用法

串口通讯的核心流程不复杂:配置参数 -> 打开端口 -> 连接信号 -> 收发数据。真正复杂的是参数配置的正确姿势和错误处理。常见的坑是新手直接把BaudRate写成9600这样的裸数字,看起来能跑,但遇到波特率超过115200的高频通讯时,输出结果经常不对,因为程序里走的是int到SerialPort枚举的隐式转换路径。

我实现的串口配置区是这样处理的:

void SerialWorker::openPort(const QString &portName, int baud, int dataBits, int stopBits, const QString &parity) { if (m_serial->isOpen()) { m_serial->close(); } m_serial->setPortName(portName); m_serial->setBaudRate(baud); m_serial->setDataBits(static_cast<QSerialPort::DataBits>(dataBits)); m_serial->setStopBits(static_cast<QSerialPort::StopBits>(stopBits)); // parity处理:字符串转枚举,none/even/odd/mark/space m_serial->setParity(static_cast<QSerialPort::Parity>(parityIndex)); m_serial->setFlowControl(QSerialPort::NoFlowControl); if (!m_serial->open(QIODevice::ReadWrite)) { emit errorOccurred(m_serial->errorString()); return; } connect(m_serial, &QSerialPort::readyRead, this, &SerialWorker::handleReadyRead); }

串口数据接收用的是readyRead信号。这里要特别提醒:readyRead信号不代表一帧完整的报文。串口底层是按字节流传输的,一帧数据可能分几次到达,也可能一次到达好几帧。实测下来非常常见的现象是:设备按100ms间隔发送一帧12字节数据,有时候readyRead一次性触发带12字节,有时候分两次,第一次8字节第二次4字节。

所以接数据的地方必须做粘包、半包的缓存处理。我采用的是最实用的协议帧方案:在协议设计层面约定帧头+长度+校验,接收时按照状态机逐字节解析。对于没有协议帧的裸数据流场景,则提供一个“按时间间隔分包”的选项,用QTimer做超时处理,间隔时间内没有新数据就把缓存里已有的数据当作一帧抛出来。

void SerialWorker::handleReadyRead() { m_buffer.append(m_serial->readAll()); // 如果没有启用协议解析,就持续缓存等待 while (m_buffer.size() > 0) { // 按协议帧解析:查找帧头、长度、校验,完整则取出一帧 if (tryParseFrame(m_buffer)) { // 解析成功,m_buffer已移除已处理的数据 } else { break; } } }

提示:如果设备手册里明确给了帧格式,按协议解析是第一选择。如果只是把串口当作透传通道(比如外接4G模块、蓝牙模块),用定时器分包会更省心。

2.2 TCP通讯:Server端与Client端的分工协作

TCP模块我同时实现了客户端和服务端两个方向。客户端模式用于主动连接远端设备或云端服务器,服务端模式用于让设备主动连接到本机,比如摄像头、工控屏、数据采集器通常作为TCP客户端主动上报数据,上位机只需要监听一个固定端口等待连接即可。

先看TCP客户端的关键代码:

void TcpWorker::connectToServer(const QString &ip, quint16 port) { m_socket->abort(); m_socket->connectToHost(ip, port); // 等待连接完成,带超时处理 if (!m_socket->waitForConnected(3000)) { emit errorOccurred(QString("TCP连接失败: %1").arg(m_socket->errorString())); return; } }

这里我加了一个3秒超时,因为TCP连接如果目标主机不可达,默认要等很久才报错,用户体验非常差。waitForConnected的返回值直接判断是否连接成功,比监听stateChanged信号再判断状态要直观得多。但是注意,连接之后的数据收发必须走信号槽,不能用waitForReadyRead之类的阻塞函数,不然界面会卡死。

TCP服务端使用QTcpServer监听,有新的客户端连接时通过nextPendingConnection把建立好的socket接管过来:

void TcpWorker::startServer(quint16 port) { m_server = new QTcpServer(this); connect(m_server, &QTcpServer::newConnection, this, [=]() { QTcpSocket *client = m_server->nextPendingConnection(); m_clients.append(client); connect(client, &QTcpSocket::readyRead, this, &TcpWorker::handleClientData); connect(client, &QTcpSocket::disconnected, this, [=]() { m_clients.removeAll(client); client->deleteLater(); }); }); m_server->listen(QHostAddress::Any, port); // 监听所有网卡 }

注意listen的第一个参数,我写的是QHostAddress::Any,这意味着本机所有IP地址(包括无线网卡和虚拟网卡)都能收到连接。如果你只需要局域网内某个IP访问,改成QHostAddress(m_ip)可以过滤掉其他网卡的连接请求。但实测开发阶段用Any最省事,避免多个网卡时找不到该绑定哪个IP。

TCP通讯里最常见的是“粘包”问题,尤其是收发频繁时。解决方案和串口类似:必须以报文形式解析数据,而不能期望每次readyRead正好收到一条完整消息。另外TCP是流式传输,存在半包问题,需要自行维护缓冲区。

2.3 UDP通讯:无连接模式下的收发实现

UDP和TCP最大的区别在于:无连接、不可靠、只管发。QT里QUdpSocket的用法比TCP简单太多,但有几个细节需要特别注意。

UDP绑定端口时最常见的报错就是:

error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address

这个错误在Windows下非常典型,意思是端口已经被占用了。处理方式分两步:先修改socket的端口复用属性。

m_udpSocket->setSocketOption(QAbstractSocket::LowDelayOption, 1); m_udpSocket->bind(port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);

这里的ShareAddress允许其他socket绑定到同一个端口,ReuseAddressHint则显式告诉系统复用地址。不过要留心:即便加了这两个选项,如果上一个进程异常退出后端口还处于TIME_WAIT状态,绑定也可能失败。常用的补救办法是,开发调试时端口被占用就直接换一个测试端口,不用死磕同一个端口;需要稳定复用端口时,可以在程序退出时主动调用close,确保socket完全释放。

UDP数据接收要在界面初始化时先绑定好端口,再连接readyRead信号:

void UdpWorker::initSocket(quint16 bindPort) { m_udpSocket->bind(QHostAddress::Any, bindPort, QUdpSocket::ShareAddress); connect(m_udpSocket, &QUdpSocket::readyRead, this, &UdpWorker::handlePendingDatagrams); } void UdpWorker::handlePendingDatagrams() { while (m_udpSocket->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(m_udpSocket->pendingDatagramSize()); m_udpSocket->readDatagram(datagram.data(), datagram.size()); emit dataReceived(datagram); } }

UDP发送时有个容易被新手忽略的坑:如果对端IP不在同一网段,UDP发送不会报错,但数据根本到不了对端。我用iperf3做局域网UDP打流测试时发现,只要两台机器IP不在同一个子网,UDP数据包发送端毫无异常,接收端一个包都收不到。所以用UDP之前最好先ping一下对端IP,确认三层连通性正常。

2.4 串口、TCP、UDP三种通讯方式的选型对比

很多做项目规划的朋友问过我:通讯方式到底怎么选?下面是我的经验总结:

对比维度串口TCPUDP
连接方式物理线缆点对点三次握手建立连接无连接,直接发数据
可靠性受线缆和干扰影响可靠传输,自动重传不可靠,可能丢包
适用距离15米以内(RS232)局域网/广域网均可局域网/组播场景
典型场景单片机调试、传感器采集设备对接云平台、远程控制视频流、游戏同步、实时监测
QT模块QSerialPortQTcpSocket / QTcpServerQUdpSocket

如果你是在做多台设备之间的数据同步或实时控制,延迟敏感但丢几个包无所谓的场景,优先选UDP。如果传输的是设备配置、关键指令,一个字节都不能错,必须选TCP。串口则适合那些只提供串口接口的老旧设备,比如很多带RS232接口的称重仪表、PLC、扫描枪。

3. 进阶功能扩展:时域转频域与波形显示

3.1 QtCustomPlot集成与波形绘制

原始的通讯工具只能看十六进制报文,不够直观。后来根据实际需求,引入了QtCustomPlot做波形显示,并且通过kissfft实现了时域到频域的转换。比如需要对比收到的振动传感器数据和理论频率成分时,直接在界面上选中一段数据,点击“FFT转换”,就能看到频谱图,方便快速判断有没有异常频率成分。

QtCustomPlot是一套纯QT的绘图库,不需要OpenGL依赖,集成很简单:把qcustomplot.h和qcustomplot.cpp直接加入工程,然后在UI界面里放一个QWidget控件,提升为QCustomPlot类即可。绘图发数时只需设置好x轴和y轴数据:

void WavePlotWidget::plotWave(const QVector<double> &xData, const QVector<double> &yData) { ui->customPlot->graph(0)->setData(xData, yData); ui->customPlot->rescaleAxes(); ui->customPlot->replot(); }

这里的核心逻辑是把收到的报文按照“数值型协议”解析成double数组,再交给绘图控件。

3.2 FFT转换的实现原理与代码思路

时域转频域在QT里有很多实现方式,我选用kissfft是因为它是轻量级开源FFT库,不像FFTW那样庞大,而且接口简单,适合嵌入式风格的C++工程。集成方式就是把kiss_fft.h、kiss_fft.c等文件加入工程中。

FFT的核心计算过程是:采集N点时域数据,经过窗函数处理,再做FFT变换,最后计算幅值谱。

void FreqAnalyzer::fftTransform(const QVector<double> &timeDomainData) { int n = timeDomainData.size(); kiss_fft_cfg cfg = kiss_fft_alloc(n, 0, nullptr, nullptr); QVector<kiss_fft_cpx> fin(n), fout(n); for (int i = 0; i < n; ++i) { fin[i].r = timeDomainData[i]; fin[i].i = 0; } kiss_fft(cfg, fin.data(), fout.data()); free(cfg); // 计算幅值,前半部分即为有效频谱 for (int i = 0; i < n / 2; ++i) { double re = fout[i].r, im = fout[i].i; m_freqData.push_back(sqrt(re * re + im * im) / (n / 2)); } }

这里有三个比较容易踩的坑:一是采样点数N必须是2的整数次幂,比如256、512、1024,用非2的幂次做FFT,结果会在尾部出现大量伪谱线;二是FFT前最好加汉宁窗或海明窗,否则频谱泄漏很严重,看起来像是信号里有很多不存在的频率成分;三是只取N/2的有效部分,另一半是镜像,不处理的话图形会多出一截对称的噪声尾巴。

做FFT时所用的数据量大小直接影响频率分辨率,计算公式是:频率分辨率 = 采样率 / N。比如采样率是1000Hz,采集1024个点,分辨率为约0.98Hz,这意味着频率间隔接近1Hz。想要区分频率接近的多个成分,就得多采集数据点。

4. 常见问题排查与工程落地经验

4.1 端口占用、本机多IP与连接被重置

开发期问遇到最多的问题就是端口相关。除了上文提到的bind冲突,还有两种情况:

第一,本机存在多个IP导致服务端收不到数据。比如笔记本同时连着无线网和有线网,如果服务端绑定了特定IP,而客户端恰好连到了另一个IP,连接会直接失败。解决办法是服务端统一用QHostAddress::Any监听,或者动态获取当前网卡的IP供用户选择。

第二,TCP连接被对端重置。外部设备连接软件后频繁报"tcp connection reset by peer",这种多半是设备侧或者对端服务端做了连接空闲超时,双方有一段时间没数据交互就被强制踢掉了。解决办法是应用层做心跳包,每隔一段时间发送一次握手数据,维持连接活跃。

Windows下做TCP客户端调试还有一个隐蔽的坑:如果客户端程序崩溃或者异常退出,连接没有正常关闭,服务器端socket会一直保持ESTABLISHED状态,导致端口资源被耗尽。代码里要在析构函数中主动断开连接,最好加上socket状态检测和自动重连机制。

4.2 串口占用、驱动不识别与数据乱码

串口调试中“端口识别不到”是最常见的问题。CH340、FTDI这些常见芯片的驱动都装好的情况下,设备管理器里还是看不到COM口,大概率是USB线的问题——有些USB线只供电不能传输数据。换线后基本都能解决。

串口被其他软件占用时,QSerialPort打开会直接报PermissionDenied错误。这种情况我一般用两种方式处理:一是提示用户先关闭串口调试助手等其它软件,二是提供端口热插拔检测,壳代码里监听QSerialPortInfo的availablePorts变化,设备拔插后自动刷新端口列表,避免用户反复手动打开设置界面。

数据乱码问题多半是波特率不对或者校验位设置错了。9600的报文拿到115200下面解,读出来全是乱码。另外,部分设备使用的是8E1(8数据位+偶校验+1停止位),默认参数8N1去读这种情况也会乱码,所以我界面上把数据位、停止位、校验位都做成了下拉选择,而不是只暴露一个波特率配置。

4.3 中文乱码与编码转换问题

工控设备发送的中文分两种:GBK编码和UTF-8编码。QT5默认处理字符串是UTF-8,如果直接用QString::fromUtf8去解析收到的GBK数据就会出现“锟斤拷”这类经典乱码。收发时做个可选择的编码转换就解决了:

QString decodeByEncoding(const QByteArray &raw, const QString &encoding) { QTextCodec *codec = QTextCodec::codecForName(encoding.toUtf8()); return codec ? codec->toUnicode(raw) : QString::fromUtf8(raw); }

但是这里我要提醒一点:如果收发的是十六进制报文,千万不要做编码转换。十六进制字符串FE 01 02直接按ASCII码处理即可,一旦走QTextCodec转换,就会变成不可预知的乱码。所以我的数据处理流程分了两条路:十六进制模式走原样转发,文本模式才走编码转换。

4.4 打包发布:windeployqt与运行环境

开发完的QT程序不能直接发给别人用,目标机器上没有QT运行库是跑不起来的。我在Windows下发布用的是官方提供的windeployqt工具,直接在命令行执行:

cd /d D:\build\MyTool\release C:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe MyTool.exe

执行完这条命令后,所有QT依赖的dll和插件会自动拷贝到exe同目录。这里最常见的报错是“windows no qt platform plugin could be initialized”,本质就是platforms目录缺失或没有正确加载。解决方法是确认windeployqt执行成功,并且发布目录里必须存在platforms/qwindows.dll。

发布另一个重点是:不要只拷贝exe文件过去执行,一定要连同整个发布目录一起打包。很多朋友图省事就发一个exe,在开发机器上运行正常,换一台纯净机器就报错,基本都是漏了插件或者编译器运行时库(比如libgcc_s_seh-1.dll、libwinpthread-1.dll)。

5. 实操体验与后续扩展方向

这套工具从最初只有串口收发功能的雏形,到逐步加入TCP、UDP、波形显示和FFT分析,前后迭代了几个版本。我个人最大的体会是:通讯工具的价值不在于代码本身多复杂,而在于能不能稳定应对真实联调环境里的各种脏活累活。比如设备主动断开重连、缓冲区溢出、编码不一致、端口被占用,这些在教科书里通常不会写,但工程现场几乎每天都可能遇到。

如果这个项目方向对你有参考价值,建议拿源码后不要停留在跑通的阶段,可以继续做这几个扩展:

第一,把数据收发加上日志记录功能,用QFile按天写入CSV文件,方便后期回溯问题。第二,在现有协议解析基础上加入灵活的帧配置界面,让用户不用改代码就能适配不同设备的协议格式。第三,把界面改成模块化可停靠布局,允许用户只显示自己关心通道的收发面板,别让所有功能都堆在一个固定窗口里。

第二点尤其推荐,因为联调中最大的痛点就是设备协议各不相同,硬编码方式每换一个设备就要改一次代码重新编译,灵活配置协议帧之后,这个工具就能真正成为日常开发的通用基础设施。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询