Qt/C++ 多通信协议代码解析(TCP/串口/VXI-11/XDMA)
Qt/C++ 多通信协议代码解析(TCP/串口/VXI-11/XDMA)
本文基于实际项目代码,详细拆解 TCP、串口、VXI-11、XDMA 四种通信方式的实现逻辑、核心细节及注意事项,适合 Qt/C++ 开发人员参考学习,重点梳理线程调度、数据组帧、异常处理等关键模块。
一、TCP:从抽象接口到「队列线程 + 同步读写」
1. 三层结构(先建立心智模型)
核心设计:通过抽象接口解耦,让同一套线程/队列逻辑支持 TCP 与 HiSLIP 两种协议,业务层不直接依赖具体实现。
| 层次 | 类/文件 | 作用 |
|---|---|---|
| 抽象 | ProtocolBase | 定义「能Open / send / sendAndRead / connectState / reconnect」仪器连接接口,并声明 connectStateChanged、SignalConnectStatusMessage 等信号(TCPClient/ProtocolBase.h)。 |
| 实现 A | TCPSocket | 用 QTcpSocket 做 RAW TCP + SCPI 风格文本(终止符 \n,见 TCPSocket.h 里 _terminateStr)。 |
| 实现 B | HiSlip | 地址匹配 regHislip 时创建,同一套 ProtocolBase 接口,走 HiSLIP 协议(tcpclient.h 85–86 行正则)。 |
| 调度 | TCPClient | 继承 QThread,run() 里循环:维护连接、从 cmdQueue 取命令、调用 m_pClientSocket(tcpclient.h 18–87 行)。 |
2. TCPSocket:连接、发送、收包组帧
(1)构造阶段:信号都连上,但「收包主路径」不在 readyRead
TCPSocket.cpp Lines 17-24 核心信号绑定:
connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(dataReceived()));
connect(&tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(connectChange(QAbstractSocket::SocketState)));
// ... error lambda ...
connect(&tcpSocket, &QTcpSocket::connected, this, &TCPSocket::SlotConnect);
connect(&tcpSocket, &QTcpSocket::disconnected, this, &TCPSocket::SlotDisConnect);
-
connectToHost:在 init() 里,仅当 UnconnectedState 时调用(约 179–184 行)。
-
Open(address):用正则从字符串里取出 ip 和 port,再 close() + init()(约 27–40 行)。
(2)项目细节
sendAndRead 流程(约 437–466 行),核心是保证读写同步、避免粘包:
-
_mutex.lock()—— 保证同一时刻只有一条「写请求 + 等完整应答」在执行,避免多线程把两条 SCPI 应答粘在一起解读。 -
给命令补终止符
_terminateStr(默认 \n)。 -
DataBufferClear():不仅dataBuffer.clear(),还把 socket 里已到达但未读的字节读掉丢弃(约 424–434 行),避免上一次会话的残留字节拼进本次应答。 -
write(Command.toLatin1())—— 这里用的是类里重载的write(const QByteArray&)(约 342–345 行),内部用QMetaObject::invokeMethod调QTcpSocket::write,目的是把写操作投递到 tcpSocket 所在线程事件循环。 -
Read(timeout):在超时时间内循环waitForReadyRead(100),有数据就读入并 append 到 dataBuffer,直到 dataBuffer 中出现_terminateStr,取**从开头到该终止符(含)**的一段返回,并dataBuffer.clear()(约 367–402 行)。
原理:TCP 是字节流,一次 read 可能只有半条指令,也可能多条指令粘在一起;这里用接收缓冲 dataBuffer + 终止符界定「一条完整文本消息」,是典型的 SCPI over TCP 写法。
(3)sendAndReadBytes 与主路径不一致
sendAndReadBytes(约 548–592 行)在 dataBuffer.clear() 和 write 之后,只靠 Sleep 轮询 dataBuffer.length() 是否变化来判定收完,但该函数内没有任何代码从 tcpSocket 读字节填入 dataBuffer。而 Read() 才会读 socket。因此这条路径若仍被调用,很容易出现永远等不到 dataBuffer 增长或行为与 sendAndRead 不一致。
3. 重连
-
reconnect():若 _connectState 为真则先 close()(等待断开),再 init()(约 224–230 行)。
-
init() 里 ConnectingState:超过约 1 秒会 send(“*IDN?”),用于在正在连接时也能往对端打一点流量(约 196–202 行)。*IDN? 是常见仪器标识查询,这里兼作连接过程探活。
-
connectState():在 UnconnectedState 或 ConnectingState 时,若距离 toTime 超过 10 秒会走一大段分支(其中 if(true)//2026 使 _connectState 恒为 true 等,逻辑较绕);在「未超时」分支里,若仍处于 ConnectingState 会再 send(“*IDN?”)(约 233–267 行)。
理解要点:这不是标准意义的固定周期心跳包,而是状态查询 + 定时探活 + 用户提示的组合。 -
waitForDone:用 *OPC? 做操作完成查询,失败时会 close + Sleep + init(),带失败重连语义(约 109–137 行)。
4. TCPClient:命令队列与两种「sendAndRead」
(1)工作线程 run() 在干什么
-
首次根据 _iviAddress 匹配正则:匹配 ip:port 则 new TCPSocket(),匹配 HiSLIP 则 new HiSlip()(tcpclient.cpp 约 30–42 行)。
-
若 !connectState() 则 reconnect()(约 54–56 行)。
-
队列空时自动入队 SAN + Upload(约 64–72 行):保证线程里总有事做,相当于周期性向上位/对端发一条轻量命令。
-
取出命令后:Upload 只 send;否则 sendAndRead,并把结果交给 changeQuery(约 81–90 行)。
-
若 sendAndRead 连续多次得到空字符串,累计超过 10 次会 reconnect()(约 92–99 行)——这是应答超时/失败驱动的重连。
(2)UI/其它线程用的 TCPClient::sendAndRead(约 173–201 行)
与 TCPSocket::sendAndRead 不是同一个函数,核心是跨线程通信:
-
它把命令封装成 _CMD_INFO,类型为 Query,insertCMD 入队。
-
然后(msleep(1))直到 getQuery() 非空或超时。
-
工作线程在 run() 里执行真正的 m_pClientSocket->sendAndRead,结果通过 changeQuery 写回 QueryStr。
关键:跨线程的「查询」是靠队列 + 共享字符串 QueryStr + 两把锁 mutex / mutex2 协作完成的;学习时要分清「TCPClient 层异步队列」和「TCPSocket 层同步读写」两层。
(3)单例
GetInstance() 双重检查加锁创建唯一 TCPClient(tcpclient.h 49–58 行)。
(4)换 IP 时
5. 项目用途
setIpAddress 会 close + delete 旧的 m_pClientSocket 并置空,再根据新地址拼 _iviAddress(tcpclient.cpp 209–234 行),下次 run() 会重新 new 协议对象。
结合项目代码的队列调度、同步读写及重连机制,TCPClient 核心用途围绕「仪器远程控制与数据交互」展开,适配工业测试场景,具体包括:
-
远程仪器控制:通过 TCP 协议向测试仪器(如示波器、信号源)发送 SCPI 指令(如 *IDN? 查询设备信息、参数配置指令等),实现仪器的远程操作,替代本地手动操作,提升测试自动化程度。
-
数据交互与反馈:接收仪器返回的测试数据、状态信息,通过 sendAndRead 同步获取应答,确保指令下发与数据接收的一致性,为上层业务逻辑(如数据解析、报表生成)提供可靠数据支撑。
-
多协议适配兼容:基于 ProtocolBase 抽象接口,同时支持 RAW TCP 和 HiSLIP 两种协议,可根据仪器支持的通信协议自动适配,适配不同型号、不同通信标准的测试仪器,提升项目通用性。
-
高可靠通信保障:通过命令队列、互斥锁、重连机制及探活逻辑,避免指令乱序、通信中断等问题,适用于长时间、高稳定要求的工业测试场景(如连续数据采集、长时间仪器监控)。
二、串口通信
1. 为什么「创建后再 moveToThread」
CSerialTransManager 在 GetInstance() 里的核心操作(CSerialTransManager.cpp Lines 195-206):
instance = new CSerialTransManager();
instance->moveToThread(&m_qThread);
m_qThread.start();
-
CSerialTransManager 和静态成员 m_qSerialPort 在主线程(首次调用 GetInstance 的线程)里完成静态初始化语义上的「存在」,但 moveToThread 把 instance 这个 QObject 的事件循环挂到 m_qThread。
-
之后发往该对象的信号槽、QMetaObject::invokeMethod 到槽,会在工作线程里执行(取决于连接类型;AutoConnection 对跨线程会排队到接收方线程)。
原理:QSerialPort 是 QObject,应在固定线程内 open/读写;先创建再整体移到工作线程,比「在工作线程里 new 串口」更符合常见 Qt 实践,也避免主线程里直接轮询串口。
2. 谁在工作线程里打开串口
LoadDevice() / CloseDevice() 使用 invokeMethod(…, Qt::AutoConnection)(约 46–54 行),槽 SlotLoadDevice / SlotCloseDevice 在对象所在线程执行,即工作线程里 open/close(约 296–335 行)。
3. 粘包 / 拆包:m_qDataBuffer + strTerminate
-
头文件约定行结束为 “\n”(CSerialTransManager.h 第 13 行 strTerminate)。
-
SlotReadForReadDatas:readAll() 追加到 m_qDataBuffer(约 289–294 行)。
-
Read():lastIndexOf(strTerminate) 找到最后一条完整行(若一次收到多行,这里取的是包含最后一个 \n 的那一段),截出后 remove(0, len) 从缓冲头部删掉(约 272–286 行)。
与 TCP 的异同:同样是流式数据 + 分隔符;注意这里用 lastIndexOf,若缓冲里有多条 \n,行为是取最后一段完整前缀(具体是否满足你的 SCPI 协议要看是否一次只应处理一行)。
4. SendAndRead 的互斥与清缓冲
-
发送前 m_qDataBuffer.clear()(约 127–128 行注释「防止数据错位」),避免旧数据混进本次应答。
-
用 m_qMutex.tryLock 在超时时间内抢锁(约 97–120 行),避免多线程同时 SendAndRead 交错。
5. 关闭顺序
Destroy():先 quit + wait 线程结束,再 instance->deleteLater()(约 208–218 行)。这样避免工作线程仍在执行 SlotReadForReadDatas 或持有串口时,对象已被销毁。
6. 项目用途
- 本地硬件调试与控制:与本地连接的小型测试模块、嵌入式板卡(如单片机、FPGA 开发板)通信,发送控制指令(如启动采集、参数配置),接收硬件返回的状态数据或采集数据,用于硬件调试和功能验证。
- 低速数据传输:适用于数据量不大、传输速率要求不高的场景(如传感器数据采集、小型仪器本地通信),通过 SCPI 指令交互,实现本地测试数据的实时传输与处理。
三、VXI-11:Device_WriteParms、RPC 与 LxiText 生命周期
1. 协议数据结构(与标准 VXI-11 一致)
Vxi11/Vxi11Protocol.h 里 Device_WriteParms 含 lid、io_timeout、lock_timeout、flags、以及 data_len / data_val(约 40–51 行)。远端「写仪器」时,C 层会带上这一段。
2. C 回调 → C++ 静态函数指针
Vxi11.cpp 里的核心回调机制:
-
OnVxi11WriteCmd → 调 Vxi11::NewCmdCallBack(用户通过 RegisterCallBack 设置)。
-
OnVxi11GetStbData → 调 Vxi11::GetStbData(RegisterGetDataCallBack)。
即:OncRPC 生成的服务端代码在收到 RPC 时调用这些 C 函数,再转到你在 Vxi11 上注册的回调。
3. LxiText 的典型启动顺序(LxiText.cpp)
-
vxi = Vxi11::GetInstance() —— 单例。
-
std::thread(&Vxi11::StartVxi11Server) —— 独立线程跑 StartVxi11()(内部是 RPC 服务循环,阻塞型)。
-
初始化 Device_WriteParms Vxi11RXData,data_val 指向静态大数组 Vxi11RXDataBuffer(避免在栈上放巨大缓冲)。
-
RegisterCallBack / RegisterGetDataCallBack。
-
析构时 StopVxi11Server() 再 join 线程(约 51–55 行)—— 与串口「先停服务线程再清资源」思想一致。
4. OnNewVxiCmd 里 flags & 0x08 的含义(学习角度)
协议里 flags 常用来表示是否本次 write 为「结束块 / 带结束标志」(具体位定义需对照你们的 VXI-11 实现与 vxi11_svc.c);本项目中:
-
带 0x08:认为本段 SCPI 拼完,拷贝到 CmdArg,调用 OnNewCmd,再清空。
-
否则:只 append 到 CmdArg.DataVal,等待后续片段。
这与 TCP 里「多包拼一条指令」是同一类问题。
5. ScpiDataHandler(约 82–91 行)
while(true) 里每 10ms 调 GetData / OnNewCmd / ClearDataLength。若在实际工程中启用,需要退出条件和与 UI 线程的交互方式(例如只应在独立线程跑,或通过 QMetaObject::invokeMethod 把结果投递到主线程)。
6. 项目用途
工业仪器通信协议(常用于程控仪器、示波器、信号源等设备的远程控制),说明项目可能面向测试测量仪器的 TCP 远程控制;
四、XDMA:全局 XDMA_device 与 DLL 导出面
XDMA(eXtensible Direct Memory Access,可扩展直接内存访问)技术展开,是面向硬件设备(如 FPGA/PCIe 设备)的 DMA 通信相关的开发项目,以下是详细拆解:
1. XDMA 硬件通信
-
XDMA 是赛灵思(Xilinx)等厂商推出的 PCIe DMA 解决方案,用于 FPGA/ASIC 等硬件与主机(x86/x64)之间的高速数据传输;
-
子文件夹 XDMAdevice/ 直接指向 “XDMA 设备” 相关逻辑,设备的驱动 / 通信层开发;
-
hds2102a/ 是具体硬件型号的适配模块;在安装时要打开电脑测试模式;
-
public/(公共接口)、userdef/(用户自定义配置)是分层设计的典型结构,用于隔离公共 API 和用户定制化逻辑。
2. 关键文件 / 文件夹说明
| 文件 / 文件夹 | 作用 |
|---|---|
| XDMA.cpp/.h | 项目核心业务逻辑(XDMA 通信、硬件交互)的实现与头文件 |
| dllmain.cpp | DLL 入口函数,处理 DLL 的加载 / 卸载、线程附加 / 分离等生命周期事件 |
| XDMA.def | 定义 DLL 导出的函数列表,让外部程序(如 EXE)能调用 DLL 中的功能 |
| XDMA.vcxproj | Visual Studio 项目配置文件(编译选项、依赖、输出路径等) |
| stdafx.h/.cpp | 预编译头文件,减少重复编译时间(Windows C/C++ 项目标配) |
| XDMAdevice/ | XDMA 设备底层操作模块(如设备枚举、数据读写、中断处理等) |
| hds2102a/ | 特定硬件(HDS2102A)的适配层(寄存器配置、协议解析、硬件初始化等) |
| public/ | 公共接口层,对外暴露统一的 API,隔离底层实现细节 |
| userdef/ | 用户自定义配置(如参数模板、硬件配置文件、日志规则等) |
3. 项目用途
结合 XDMA 技术背景和文件结构,该项目主要用于:
-
PCIe 高速数据传输:实现 Windows 主机与 FPGA/PCIe 硬件设备之间的高带宽、低延迟数据交互(如工业采集、数据卡、嵌入式计算);
-
硬件设备驱动封装:将底层硬件操作(如寄存器读写、DMA 通道配置)封装为 DLL,供上层应用程序(如测试工具、业务软件)调用;
-
特定硬件适配:针对 hds2102a 型号的硬件做定制化适配,满足该设备的通信和控制需求。
五、总结(核心对比)
| 通信方式 | 组帧方式 | 线程/并发 | 关闭/停服务 |
|---|---|---|---|
| TCP TCPSocket | dataBuffer + \n,sendAndRead 用互斥 | Read() 内阻塞等数据 | close 等 disconnect |
| TCP TCPClient | 业务上靠队列里的命令字符串 | 独立 QThread::run | stopThread |
| 串口 | m_qDataBuffer + \n | moveToThread + 槽在工作线程 | Destroy 先停线程 |
| VXI-11 | 分段 write + flags 拼命令 | std::thread 跑 RPC | Stop + join |
| XDMA | 寄存器+块长度由协议/驱动定义 | 由调用方保证 | xdma_CloseDevice 等 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)