ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于QT的串口、TCP、UDP通讯源代码与工程实践

基于QT的串口、TCP、UDP通讯源代码与工程实践 简介面向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 widgets2. 三大通讯模块的核心实现与关键细节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_castQSerialPort::DataBits(dataBits)); m_serial-setStopBits(static_castQSerialPort::StopBits(stopBits)); // parity处理字符串转枚举none/even/odd/mark/space m_serial-setParity(static_castQSerialPort::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 QVectordouble xData, const QVectordouble 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 QVectordouble timeDomainData) { int n timeDomainData.size(); kiss_fft_cfg cfg kiss_fft_alloc(n, 0, nullptr, nullptr); QVectorkiss_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下面解读出来全是乱码。另外部分设备使用的是8E18数据位偶校验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文件方便后期回溯问题。第二在现有协议解析基础上加入灵活的帧配置界面让用户不用改代码就能适配不同设备的协议格式。第三把界面改成模块化可停靠布局允许用户只显示自己关心通道的收发面板别让所有功能都堆在一个固定窗口里。第二点尤其推荐因为联调中最大的痛点就是设备协议各不相同硬编码方式每换一个设备就要改一次代码重新编译灵活配置协议帧之后这个工具就能真正成为日常开发的通用基础设施。本文还有配套的精品资源点击获取
返回列表