news 2026/11/5 7:24:29

QT与VS2019实现三菱PLC串口通讯:从组包到解析的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QT与VS2019实现三菱PLC串口通讯:从组包到解析的完整指南

简介:这份资源面向需要在 Windows 平台用 Qt 与三菱 PLC 建立通讯的 C++ 开发者,尤其适合已具备一定 Qt 基础、希望打通上位机与 PLC 数据交互的中级学习者。资源基于 Qt 5.12.9 与 VS2019 环境,配合 MX Component S4.19 完成配置,实现通讯状态提示与 PLC 型号显示,可作为工业自动化上位机项目的入门参考。压缩包共 70 个文件,约 47.52MB,包含 cpp、h 源码与工程配置,以及 obj、pdb、tlog 等编译中间产物和 exe 可执行文件,另有 pro、sln、ui、qrc 等 Qt 与 VS 工程文件,便于直接打开对照。目前已有 2094 人学习下载。读者可从中获取 MX Component 调用方式、通讯成功与否的判断逻辑及型号读取思路,并借助完整工程结构快速复现与二次开发。

1. 工控上位机绕不开的一道坎:用 QT 和 VS2019 把三菱 PLC 通讯跑通

车间里一台设备要接上位机,PLC 是三菱的 FX 系列,现场工程师丢过来一句「你写个界面能读能写就行」,然后就没有然后了。真正动手才发现,通讯协议、串口参数、地址映射、字节序,每一项都能让人卡半天。这个项目就是干这件事的:在 Visual Studio 2019 里用 QT 搭一个上位机界面,通过串口和三菱 PLC 建立通讯,实现寄存器读写、状态监控这类基础功能。适合正在做工控上位机、需要把 C++ 界面和 PLC 数据打通的人,也适合刚接触 QT 串口编程、想找一个能跑起来的完整例子照着改的开发者。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。

2. 技术选型与工程结构:为什么是 QT + VS2019 这套组合

2.1 选型逻辑:界面归 QT,编译归 VS

工控上位机这个场景,界面要稳、要能长期维护,还要方便和硬件层代码混编。QT 的信号槽机制天然适合「串口收到数据 → 界面刷新」这种事件驱动模型,而 VS2019 的调试器在排查串口时序、断点看缓冲区时比纯 QT Creator 顺手得多。所以这个项目走的是「QT 做 UI 和串口封装,VS2019 做工程管理和调试」的路线,本质上是 QT 的 MSVC 编译套件在 VS 里的集成用法。

常见做法是装好 QT 后,在 VS 里装 QT 的扩展插件,让 VS 能识别.pro或直接建 QT 工程。这样写出来的代码既可以用 QT 的QSerialPort管串口,又能用 VS 的断点和监视窗口看变量,调试体验比来回切工具好很多。

需要提前准备的环境大致是这几样:

组件作用注意点
Visual Studio 2019工程管理、编译、调试装「使用 C++ 的桌面开发」工作负载
QT(MSVC 版本)UI 框架、串口模块版本要和 VS 的编译器位数一致
QT VS Tools 扩展让 VS 识别 QT 工程装完要在 VS 里配置 QT 版本路径
三菱 PLC 及通讯线实际通讯对象确认是串口还是以太网,本项目走串口

这里有个血泪经验:QT 一定要选 MSVC 编译的版本,不要选 MinGW 版本。MinGW 编出来的库和 VS 的编译器不兼容,链接阶段会报一堆找不到符号的错误,新手很容易在这里翻车。

2.2 工程目录结构:把通讯层和界面层分开

一个能长期改的项目,目录不能全堆在一起。这个项目的结构我一般会拆成三层:界面层、通讯层、协议层。界面层只管显示和按钮响应,通讯层管串口的打开关闭和收发,协议层管三菱报文的组包和解包。这样以后换 PLC 型号,只动协议层就行。

PlcComm/ ├── PlcComm.sln # VS 解决方案 ├── PlcComm/ │ ├── main.cpp # 程序入口 │ ├── MainWindow.h/.cpp # 主界面 │ ├── MainWindow.ui # QT Designer 界面文件 │ ├── PlcProtocol.h/.cpp # 三菱报文组包解包 │ ├── PlcClient.h/.cpp # 串口封装、连接管理 │ └── PlcComm.pro # QT 工程文件

PlcClient是对QSerialPort的一层包装,对外只暴露connectPlc()、readRegister()、writeRegister()这类语义化接口,界面层不需要知道底下是串口还是网口。PlcProtocol则是纯逻辑,输入地址和值,输出字节数组,不依赖任何 QT 类,方便单独测试。这种分层在后期加功能时省事很多,比如要加一个批量读取,只改协议层和客户端,界面加个按钮就行。

2.3 串口参数配置:三菱 FX 的默认通讯设置

三菱 FX 系列走串口时,默认参数一般是 9600 波特率、7 位数据位、偶校验、1 位停止位。这个 7 位数据位是个坑,很多人习惯性写 8 位,结果通讯一直超时。配置代码大概是这样:

// PlcClient.cpp 串口初始化 bool PlcClient::connectPlc(const QString &portName) { m_serial.setPortName(portName); m_serial.setBaudRate(QSerialPort::Baud9600); // 三菱 FX 默认 9600 m_serial.setDataBits(QSerialPort::Data7); // 注意是 7 位,不是 8 位 m_serial.setParity(QSerialPort::EvenParity); // 偶校验 m_serial.setStopBits(QSerialPort::OneStop); m_serial.setFlowControl(QSerialPort::NoFlowControl); if (!m_serial.open(QIODevice::ReadWrite)) { return false; // 打开失败,交给上层提示 } return true; }

参数说明:setDataBits(QSerialPort::Data7)是这里最关键的一行,三菱 FX 的编程口协议默认就是 7 位数据位加偶校验,写成 8 位会导致每个字节的高位被截断,PLC 收到的报文全是错的。setFlowControl设成无流控,因为工控现场一般不用硬件流控,除非线缆很长需要防丢包。打开失败时不要直接弹窗,返回布尔值让界面层统一处理,方便以后加自动重连。

3. 核心通讯链路拆解:从组包到解析的完整过程

3.1 三菱报文格式:读懂帧头和校验

三菱的串口通讯协议(编程口协议)报文结构相对固定,一帧通常由帧头、站号、命令、数据区、校验和帧尾组成。以读寄存器为例,报文大致是STX + 站号 + 命令 + 起始地址 + 点数 + ETX + 校验和。校验和是把前面所有字节累加后取低字节。这个协议不算复杂,但字节序和地址编码容易搞错。

地址编码这块,三菱的 D 寄存器地址是十进制直接转十六进制,比如 D100 就是 0x0064,但有些型号要加偏移。我一般会在协议层写一个地址转换函数,把用户输入的「D100」这种字符串解析成协议需要的字节,避免界面层直接拼字节。

// PlcProtocol.cpp 地址解析:把 "D100" 转成协议地址字节 QByteArray PlcProtocol::parseAddress(const QString &addr) { QByteArray result; if (addr.startsWith("D", Qt::CaseInsensitive)) { // D 寄存器:去掉前缀,十进制转十六进制 bool ok = false; int num = addr.mid(1).toInt(&ok); if (!ok) return result; result.append(static_cast<char>((num >> 8) & 0xFF)); // 高字节 result.append(static_cast<char>(num & 0xFF)); // 低字节 } // 其他寄存器类型(M、X、Y)按需扩展 return result; }

逻辑说明:三菱的地址在报文里是高低字节顺序,先高后低。num >> 8取高字节,num & 0xFF取低字节。这里用QByteArray而不是裸指针,是为了后面拼报文时方便追加。参数上,addr是界面传进来的字符串,函数内部做格式校验,解析失败返回空数组,调用方据此判断地址是否合法。

3.2 组包与校验:一帧读命令怎么拼出来

组包的核心是把命令、地址、点数按协议顺序拼起来,最后算校验和。下面是一个读 D 寄存器的组包函数:

// PlcProtocol.cpp 组读寄存器命令帧 QByteArray PlcProtocol::buildReadFrame(const QString &address, int count) { QByteArray frame; frame.append(0x02); // STX 帧头 frame.append(0x30); // 站号 '0' frame.append(0x30); // 命令高字节 frame.append(0x30); // 命令低字节(读命令) frame.append(parseAddress(address)); // 起始地址 frame.append(static_cast<char>(count & 0xFF)); // 读取点数 // 计算校验和:前面所有字节累加取低字节 quint8 sum = 0; for (char c : frame) { sum += static_cast<quint8>(c); } frame.append(static_cast<char>(sum)); // 校验和 frame.append(0x03); // ETX 帧尾 return frame; }

逻辑说明:帧头0x02和帧尾0x03是固定的,站号和命令按协议填。校验和只累加帧头到点数这一段,不含帧尾。参数count是读取的寄存器个数,超过 255 要拆包,这里简化处理只支持单字节。实际项目里如果点数多,需要循环组包,每次读一批。

提示:校验和算错是通讯失败最常见的原因。调试时可以把组好的帧打印成十六进制,和 PLC 手册里的示例帧逐字节对比,一眼就能看出哪里不对。

3.3 接收解析:粘包和半包怎么处理

串口收数据不会按帧给你,可能一次收到半帧,也可能两帧粘在一起。所以接收端不能假设「一次 readyRead 就是一帧」,要自己维护一个缓冲区,按帧头帧尾切分。常见做法是在PlcClient里挂一个QByteArray m_buffer,每次收到数据追加进去,然后循环找0x02开头、0x03结尾的完整帧。

// PlcClient.cpp 串口数据到达槽函数 void PlcClient::onReadyRead() { m_buffer.append(m_serial.readAll()); while (true) { int start = m_buffer.indexOf(0x02); // 找帧头 int end = m_buffer.indexOf(0x03); // 找帧尾 if (start < 0 || end < 0 || end < start) break; QByteArray frame = m_buffer.mid(start, end - start + 1); m_buffer.remove(0, end + 1); // 移除已处理部分 parseFrame(frame); // 交给协议层解析 } }

逻辑说明:indexOf找帧头和帧尾的位置,mid截出完整帧,remove把已处理的数据从缓冲区删掉,避免重复解析。这个循环能处理粘包(一次多个帧)和半包(帧不完整时end < start或找不到帧尾,直接 break 等下次数据)。参数上,m_buffer要设上限,防止异常数据把内存撑爆,比如超过 4KB 就清空重来。

3.4 数据解析与界面刷新:把字节变成可读数值

收到完整帧后,要跳过帧头、站号、命令这些固定字段,取出数据区,再按寄存器类型转成数值。D 寄存器是 16 位有符号数,两个字节拼起来。解析完通过信号槽通知界面刷新。

// PlcProtocol.cpp 解析读响应帧,返回寄存器值列表 QVector<int> PlcProtocol::parseReadResponse(const QByteArray &frame) { QVector<int> values; // 帧结构:STX + 站号 + 命令 + 数据区 + 校验 + ETX int dataStart = 4; // 跳过 STX、站号、命令两字节 int dataEnd = frame.size() - 2; // 去掉校验和 ETX for (int i = dataStart; i + 1 < dataEnd; i += 2) { quint8 high = static_cast<quint8>(frame[i]); quint8 low = static_cast<quint8>(frame[i + 1]); values.append((high << 8) | low); // 高字节在前 } return values; }

逻辑说明:dataStart = 4是协议固定的偏移,dataEnd去掉末尾校验和帧尾。循环每次取两个字节拼成一个 16 位值,高字节在前。返回QVector<int>方便界面直接遍历显示。如果寄存器是有符号的,这里还要做符号位判断,负数要转成补码对应的负值,具体看 PLC 侧的数据定义。

4. 避坑与排查:串口通讯里最容易翻车的几个点

4.1 现象:串口能打开但读不到数据

原因通常是参数不匹配。三菱 FX 默认 7 位数据位、偶校验,如果代码里写成 8 位无校验,串口能打开,但 PLC 收到的报文解析失败,不会回数据。解决方法是先确认 PLC 侧的通讯参数(在 PLC 参数设置里看),然后逐项对齐代码里的setDataBits、setParity、setBaudRate。排查时可以先用串口助手手动发一帧,看 PLC 有没有回,确认硬件和参数没问题再查代码。

4.2 现象:偶尔收到数据,但解析出来全是乱码

多半是粘包处理没做好,或者缓冲区没清干净。如果onReadyRead里直接readAll()就当一帧解析,遇到半包就会把半截数据当完整帧,解析结果自然错。解决方法是按 3.3 节的缓冲区切分逻辑,严格按帧头帧尾取帧。另外每次重新连接前要m_buffer.clear(),否则上次残留的数据会污染新连接。

4.3 现象:校验和总是对不上

原因可能是累加范围搞错,把帧尾0x03也算进去了,或者用了有符号char累加导致溢出。解决方法是校验和只累加帧头到数据区,用quint8做累加变量,最后取低字节。调试时把发送帧和手册示例逐字节对比,重点看校验和那一位。

4.4 现象:VS 里编译报一堆 QT 符号找不到

这是 QT 版本和 VS 编译器不匹配的典型表现,常见于装了 MinGW 版 QT 却在 VS 里用。解决方法是换成 MSVC 编译的 QT 版本,并在 QT VS Tools 里把 QT 版本路径指向 MSVC 目录。改完记得清理解决方案重新生成,有时候旧的中间文件会干扰。

4.5 现象:界面卡死,点按钮没反应

串口读写如果放在主线程里同步等待,界面就会卡。这个项目里所有串口操作都是异步的,write之后靠readyRead信号回调,不在主线程里waitForReadyRead。如果确实需要同步语义,用状态机加超时定时器,而不是阻塞等待。这一点在工控上位机里尤其重要,界面卡死在现场是要被投诉的。

5. 二次开发改造:把这套通讯框架用到自己的设备上

5.1 换 PLC 型号时改哪里

这套代码的分层设计就是为了换型号时少动刀。如果换成三菱 Q 系列走以太网,界面层完全不用动,只需要把PlcClient里的QSerialPort换成QTcpSocket,connectPlc改成连 IP 和端口,onReadyRead的槽函数名不变,协议层PlcProtocol里的帧格式按新协议重写。如果换成其他品牌的 PLC,协议层基本要重写,但客户端和界面的接口保持不变,改动范围可控。

改造时我一般会先写一个协议接口基类,把buildReadFrame、parseReadResponse这些方法声明成虚函数,不同品牌各写一个子类。这样以后加新设备就是加一个子类,不用改现有代码。下面是接口的骨架:

// IPlcProtocol.h 协议接口,方便扩展多品牌 class IPlcProtocol { public: virtual ~IPlcProtocol() = default; virtual QByteArray buildReadFrame(const QString &addr, int count) = 0; virtual QVector<int> parseReadResponse(const QByteArray &frame) = 0; virtual QByteArray buildWriteFrame(const QString &addr, int value) = 0; };

参数说明:三个纯虚函数覆盖了读、解析、写三个核心动作,子类实现各自品牌的报文格式。PlcClient持有一个IPlcProtocol*指针,构造时注入具体实现,运行时可切换。这种写法在需要同时对接多台不同品牌 PLC 的项目里很实用。

5.2 加一个定时轮询和断线重连

实际产线上位机不能只靠手动点按钮读数据,一般要定时轮询关键寄存器。做法是用QTimer定时触发读命令,间隔根据 PLC 响应速度定,常见 200ms 到 1s。断线重连则是在QSerialPort的errorOccurred信号里判断错误类型,如果是设备拔出或通讯超时,启动一个重连定时器,隔几秒重试connectPlc。

// MainWindow.cpp 定时轮询 m_pollTimer = new QTimer(this); connect(m_pollTimer, &QTimer::timeout, this, [this]() { if (m_client->isConnected()) { m_client->readRegister("D100", 10); // 每 500ms 读 10 个寄存器 } }); m_pollTimer->start(500);

逻辑说明:轮询前先判断连接状态,避免对已断开的串口发命令。读回来的数据通过信号槽更新界面,不要在定时器里直接操作 UI 控件。间隔别设太短,三菱 FX 的串口响应有延迟,太快会导致缓冲区堆积,反而丢帧。

5.3 验证改造是否成功的笨办法

改完通讯层,别急着上产线。我习惯先用一个虚拟串口工具把发送和接收两端对接起来,自己发自己收,确认组包和解析逻辑闭环没问题,再接真实 PLC。接上 PLC 后,先读一个已知值的寄存器(比如 PLC 程序里固定写死的 D0),对比界面显示和 PLC 编程软件里的监控值,一致了再测写操作。写操作一定要先在测试寄存器上试,别直接写运行中的控制寄存器,这是拿设备安全换来的教训。

从那以后我每次改通讯代码,都强制先走一遍「虚拟串口自环 → 单寄存器读 → 单寄存器写 → 批量轮询」这个顺序,一步不跳。希望这套拆解能帮到你,把手上那台 PLC 顺利接进界面里。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 7:22:59

物联网技术演进:从IoT到AIoT的架构与应用

1. 物联网技术全景解析&#xff1a;从IoT到AIoT的演进之路当我们在2023年谈论物联网时&#xff0c;已经不能简单地用"联网的冰箱"或"远程控制的灯泡"来定义这个领域。过去十年间&#xff0c;物联网技术经历了三次重大范式升级&#xff1a;从最初设备联网的…

作者头像 李华
网站建设 2026/11/5 7:22:26

影视聚合搜索接口设计:从参数定义到多源合并的工程实践

影视资源聚合类项目的搜索接口设计&#xff0c;是个看起来简单、做起来处处是细节的活儿。标题里提到的"libvio之影视搜索接口获取"&#xff0c;本质上要解决的是&#xff1a;一个影视内容聚合前端&#xff0c;如何稳定、高效地从数据源拿到搜索结果并呈现给用户。这…

作者头像 李华
网站建设 2026/11/5 7:22:26

Python Cookbook 2024:300+菜谱式代码,解决你的编程难题

简介&#xff1a;《Python Cookbook&#xff1a;Everyone can cook delicious recipes 300》面向希望系统提升 Python 实战能力的中级开发者与自学者&#xff0c;以“食谱”式案例组织内容&#xff0c;帮助读者把抽象类、继承、描述符、算法与新版语法特性真正落到代码里。全书…

作者头像 李华
网站建设 2026/11/5 7:20:06

C8051F340 USB全速设备开发:枚举排错与端点FIFO配置

简介&#xff1a;面向嵌入式初学者及需要为C8051F340扩展USB通信功能的开发者&#xff0c;这份实例围绕Silicon Labs C8051F340给出从设备枚举、端点配置到数据收发的可参考实现&#xff0c;适用于数据采集、嵌入式设备与PC互联等场景。压缩包共28个文件&#xff0c;约181KB&am…

作者头像 李华
网站建设 2026/11/5 7:18:26

Java大数据技术在智慧旅游客流分析中的应用实践

1. 项目背景与核心价值旅游景点客流量数据分析是当前智慧旅游建设的核心环节。去年在某5A级景区实施类似项目时&#xff0c;我们发现传统人工统计方式误差率高达30%&#xff0c;而基于大数据的实时分析系统能将准确率提升至98%以上。这个Java技术栈实现的分析系统&#xff0c;主…

作者头像 李华
网站建设 2026/11/5 7:18:26

医疗AI存储架构优化:从元数据性能到三级存储设计

1. 医疗数据存储的三大挑战与元数据性能危机在医疗AI和基因组学研究领域&#xff0c;数据存储系统正面临前所未有的压力。我们团队在搭建PB级医疗超算平台时&#xff0c;深刻体会到元数据性能对整个系统效率的决定性影响。当价值千万的GPU集群因为存储元数据响应缓慢而闲置时&a…

作者头像 李华