news 2026/11/5 7:24:29

Qt与三菱PLC通讯实战:从协议解析到上位机开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt与三菱PLC通讯实战:从协议解析到上位机开发

简介:这份资源面向具备一定 C++ 与 Qt 基础的工控开发者,聚焦 Qt 与三菱 PLC 之间的通讯实现,借助 MX Component 完成配置后,可在 Qt 5.12.9 与 VS2019 环境下建立连接,并提示通讯是否成功、显示所连 PLC 型号,适合设备上位机开发、产线数据采集等场景的入门与参考。压缩包共 70 个文件,约 47.52MB,以 cpp、h 源码与 vcxproj、sln 工程文件为核心,辅以 ui 界面、qrc 资源、pro 工程配置及编译中间产物,完整保留了可复现的工程结构。目前已有 2094 人学习下载。读者可从中获得一套可直接编译运行的通讯示例工程,理解 MX Component 类型库的调用方式、连接状态判断与型号读取逻辑,并参考其工程组织与调试配置,减少环境搭建和联调中的试错成本。

1. 工控上位机绕不开的一道坎:Qt 与三菱 PLC 的通讯

车间里一台设备要接上位机,PLC 用的是三菱 FX 系列,工程师丢过来一句“你做个界面能读能写就行”。听起来简单,真动手才发现坑不少:协议是厂商私有的,串口参数错一位就全是乱码,读写地址的进制换算能把人绕晕。这个项目就是拿 Qt 配 VS2019,把上位机和三菱 PLC 之间的通讯链路完整跑通——读 D 寄存器、写 M 线圈、定时轮询、异常重连,一套能直接塞进产线软件的骨架。适合正在做工控上位机、MES 数据采集、设备监控的 C++ 开发者,尤其是习惯 Qt Widgets 那一套、又想用 Visual Studio 调试的人。下面按“能干什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改”的顺序拆。

2. 技术选型与工程骨架:为什么是 Qt + VS2019 + 三菱协议

2.1 这套组合解决的是什么问题

三菱 PLC 的通讯方式主要有几种:串口(RS-422/485)走 MC 协议或专用协议、以太网走 MC 协议(3E/4E 帧)、以及部分老机型用的编程口协议。上位机侧要做的无非三件事:建立连接、按地址读写软元件、处理超时和断线。Qt 负责界面和事件循环,串口用QSerialPort,网络用QTcpSocket,协议帧的拼装和解析自己写——因为三菱的帧格式没有现成的官方 C++ SDK,第三方库要么年久失修,要么只支持特定型号。

选 VS2019 而不是纯 Qt Creator,主要是调试体验:工控现场经常要单步跟协议帧,VS 的断点、内存窗口、串口数据监视配合起来更顺手。Qt 版本建议 5.12 以上(QSerialPort成熟稳定),用 Qt VS Tools 插件挂到 VS2019 里,.pro和.vcxproj双轨维护,团队里谁用哪个 IDE 都不打架。

2.2 工程目录怎么分层

一个能长期维护的工控上位机,最忌讳把协议代码和界面代码搅在一起。这个项目的目录大致是这样分的:

PlcComm/ ├── core/ │ ├── PlcProtocol.h/.cpp # 帧结构定义、校验、编解码 │ ├── PlcClient.h/.cpp # 连接管理、读写接口、超时重试 │ └── PlcAddress.h/.cpp # 软元件地址解析(D/M/X/Y) ├── transport/ │ ├── SerialTransport.h/.cpp # QSerialPort 封装 │ └── TcpTransport.h/.cpp # QTcpSocket 封装 ├── ui/ │ ├── MainWindow.h/.cpp/.ui # 主界面 │ └── MonitorWidget.h/.cpp # 实时数据展示 ├── utils/ │ └── Logger.h/.cpp # 通讯日志落盘 └── main.cpp

core层不依赖任何 UI 头文件,纯 C++ 加 QtCore,这样协议逻辑可以单独写单元测试,也能被命令行工具复用。transport层做接口抽象,串口和网口实现同一套ITransport,上层PlcClient不关心底层是线还是网。这个分层是后面所有改造的基础,别省。

2.3 开发环境搭建的具体步骤

先把工具链装齐,顺序别乱:

  1. 安装 VS2019,勾选“使用 C++ 的桌面开发”工作负载。
  2. 安装 Qt 5.15(或 5.12 LTS),组件里勾 MSVC 2019 64-bit 和 Qt Serial Port。
  3. VS2019 里装扩展 Qt Visual Studio Tools,在“扩展 → Qt VS Tools → Qt Versions”里把 qmake.exe 路径指过去。
  4. 新建 Qt Widgets Application,或者直接打开现成的.vcxproj。

.pro文件里串口模块必须显式声明,否则链接阶段报QSerialPort未定义:

QT += core gui serialport CONFIG += c++11 TARGET = PlcComm TEMPLATE = app SOURCES += main.cpp \ core/PlcClient.cpp \ core/PlcProtocol.cpp \ transport/SerialTransport.cpp HEADERS += core/PlcClient.h \ core/PlcProtocol.h \ transport/SerialTransport.h

QT += serialport这一行是新手最容易漏的,漏了之后编译能过、链接报一堆 undefined reference,血泪经验。如果用 VS 的.vcxproj而不是.pro,对应要在“Qt Project Settings → Qt Modules”里勾上 SerialPort。

2.4 串口参数与连接初始化

三菱 FX 系列走编程口或 485 时,常见参数是 9600 波特率、7 位数据位、偶校验、1 位停止位(7E1),部分机型用 8E1 或 8N1,具体看 PLC 侧 D8120 寄存器的设置。代码里把这些做成可配置项,别写死:

// transport/SerialTransport.cpp bool SerialTransport::open(const SerialConfig &cfg) { m_port.setPortName(cfg.portName); // 如 "COM3" m_port.setBaudRate(cfg.baudRate); // 9600 / 19200 / 38400 m_port.setDataBits(cfg.dataBits); // QSerialPort::Data7 m_port.setParity(cfg.parity); // QSerialPort::EvenParity m_port.setStopBits(cfg.stopBits); // QSerialPort::OneStop m_port.setFlowControl(QSerialPort::NoFlowControl); if (!m_port.open(QIODevice::ReadWrite)) { qWarning() << "open failed:" << m_port.errorString(); return false; } // 清空残留缓冲,避免上次断线留下的半帧干扰 m_port.clear(QSerialPort::AllDirections); return true; }

参数说明:portName在 Windows 下是COMx,Linux 下是/dev/ttySx;dataBits用QSerialPort::Data7对应 7 位;parity设EvenParity就是偶校验。clear()那一步很关键——串口断线重连时缓冲区里可能还躺着上一帧的后半截,不清掉的话第一帧解析必错,现象是“刚连上读出来全是 0 或者乱跳”。

3. 协议帧拆解:一帧读写指令从拼装到解析的完整链路

3.1 三菱 MC 协议/编程口帧的基本结构

以常见的编程口协议(FX 系列)为例,一帧读 D 寄存器的请求大致是:起始符 + 命令字 + 首地址 + 字节数 + 校验 + 结束符。不同帧格式(1E/3E)字段长度不同,但套路一致:命令码定操作类型,地址字段定软元件和偏移,长度字段定读多少,末尾校验保证完整性。写操作则把要写的数据追加在长度字段之后。

地址解析是第一个容易翻车的地方。三菱的软元件地址是“字母 + 十进制数字”,比如D100、M50、X10,但协议帧里传的是软元件编号加偏移,且 X/Y 是八进制编号。PlcAddress这个类专门干这件事:

// core/PlcAddress.cpp bool PlcAddress::parse(const QString &addr, DeviceType &type, int &number) { QRegularExpression re("^([DMXYTCR])(\\d+)$"); auto m = re.match(addr.trimmed().toUpper()); if (!m.hasMatch()) return false; QChar dev = m.captured(1).at(0); bool ok = false; int raw = m.captured(2).toInt(&ok); if (!ok) return false; switch (dev.toLatin1()) { case 'D': type = DeviceType::D; number = raw; break; // 数据寄存器,十进制 case 'M': type = DeviceType::M; number = raw; break; // 辅助继电器,十进制 case 'X': type = DeviceType::X; number = toOctal(raw); break; // 输入,八进制 case 'Y': type = DeviceType::Y; number = toOctal(raw); break; // 输出,八进制 default: return false; } return true; }

逻辑说明:正则先卡住“一个字母 + 数字”的格式,toUpper()兼容小写输入。D 和 M 直接取十进制值,X/Y 要转八进制——因为 PLC 侧 X/Y 的编号本身是八进制,用户写X10指的是八进制 10(十进制 8),如果直接按十进制传,读出来的点位全错位。toOctal就是把用户输入的“看起来像十进制”的字符串按八进制解释,这个转换不做,调试时你会怀疑人生。

3.2 请求帧的拼装与校验

拼帧的核心是把命令、地址、长度按协议顺序塞进QByteArray,最后算校验和。下面是一个读 D 寄存器的简化实现:

// core/PlcProtocol.cpp QByteArray PlcProtocol::buildReadFrame(DeviceType type, int number, int count) { QByteArray frame; frame.append(char(0x02)); // STX 起始符 frame.append(cmdForRead(type)); // 命令字,如 '0' 读 D frame.append(toAsciiHex(number, 4)); // 首地址,4 位 ASCII frame.append(toAsciiHex(count, 2)); // 读取点数,2 位 ASCII frame.append(calcChecksum(frame)); // 累加和校验 frame.append(char(0x03)); // ETX 结束符 return frame; } QByteArray PlcProtocol::calcChecksum(const QByteArray &body) { quint8 sum = 0; for (char c : body) sum += static_cast<quint8>(c); return toAsciiHex(sum, 2); // 两位十六进制 ASCII }

参数说明:number是软元件起始编号,count是连续读取的点数(D 寄存器一次最多读几十个,具体上限看机型)。toAsciiHex把数值转成固定位宽的十六进制 ASCII 字符串,位宽不够要补零——协议对字段长度是严格的,少一位整帧就废。校验和用累加和取低字节,再转两位 ASCII,这是三菱编程口协议的常见做法;如果走 3E 帧,校验算法和字段布局会变,但buildReadFrame这个函数签名不用动,换实现即可。

3.3 响应帧解析与超时处理

发出去之后不能傻等,串口读是异步的,QSerialPort::readyRead信号触发后把数据追加到接收缓冲,再判断是否凑够一帧。这里有两个坑:一是半包,数据分两次到达;二是粘包,两次响应挤在一起。处理方式是维护一个缓冲区,按起始符和结束符切帧:

// core/PlcClient.cpp void PlcClient::onReadyRead() { m_rxBuf.append(m_transport->readAll()); while (true) { int stx = m_rxBuf.indexOf(char(0x02)); int etx = m_rxBuf.indexOf(char(0x03), stx + 1); if (stx < 0 || etx < 0) break; // 还没凑够一帧 QByteArray frame = m_rxBuf.mid(stx, etx - stx + 1); m_rxBuf.remove(0, etx + 1); // 消费掉已解析部分 if (!verifyChecksum(frame)) { emit errorOccurred("checksum mismatch"); continue; // 丢弃坏帧,继续找下一帧 } emit frameReceived(parseResponse(frame)); } }

逻辑说明:indexOf找 STX 和 ETX,找不到就退出等待下次数据;找到后切出完整帧,从缓冲区移除,再校验。校验失败只丢当前帧不中断连接,因为现场干扰导致的偶发坏帧很常见,直接断连反而让上层频繁重连。超时用QTimer单独管:发请求时启动一个 500ms 定时器,收到匹配响应就停,超时则重发或上报错误,重试次数设上限(比如 3 次),避免死循环把串口堵死。

4. 避坑与排查:现场调试最容易翻车的五个点

4.1 现象:能连上但读出来全是 0

原因通常是地址进制搞错,或者读的软元件类型和 PLC 侧实际不符。X/Y 按十进制传是最常见的错误,另外 D 寄存器编号超出 PLC 实际范围也会返回 0 而不是报错。解决:先用 PLC 编程软件确认目标地址真实存在,再在代码里打印实际发出的帧,逐字节对照协议手册。日志里把buildReadFrame的返回值以十六进制打出来,比猜快得多。

4.2 现象:偶发校验错误,重发就好

现场电磁干扰、串口线过长、屏蔽没做好都会导致坏帧。代码层面已经做了坏帧丢弃,但根子上要查硬件:485 线用双绞屏蔽线、终端电阻按需加、波特率别贪高(9600 比 38400 稳)。软件侧把重试次数和超时做成可配置,别硬编码,不同现场差别很大。

4.3 现象:界面卡死

把串口读写放在 UI 线程里同步等待,是新手最常犯的错。QSerialPort本身是异步的,但如果你在按钮槽函数里waitForReadyRead(1000),界面就冻住了。解决:所有通讯走信号槽,请求发出后立即返回,响应到了再更新界面;或者把PlcClient整个挪到独立QThread,UI 只发命令收结果。

4.4 现象:断线重连后第一帧必错

前面提过,串口缓冲区残留半帧。除了open()时clear(),重连逻辑里也要先清缓冲再发请求。另外重连不要立刻狂发,加个 200ms 延迟让 PLC 侧端口稳定,否则会出现“连上就断”的循环。

4.5 现象:VS 里编译过、运行报缺少 Qt5SerialPort.dll

部署时忘了带串口模块的动态库。用windeployqt自动拷依赖,但注意它默认可能不包含 serialport,需要手动确认Qt5SerialPort.dll在输出目录。发布前在一台干净机器上跑一遍,比在开发机上反复试靠谱。

5. 二次开发改造:从能通到好用,我一般这么动

拿到这套骨架之后,真正落到项目里通常要改三块:接口抽象、数据模型、异常策略。先说接口抽象——PlcClient现在直接依赖SerialTransport,如果现场换成网口 MC 协议,得改上层。做法是抽一个ITransport纯虚接口,SerialTransport和TcpTransport都实现它,PlcClient只持有ITransport*,构造时注入。这样换通讯介质不动业务代码,测试时还能塞个 Mock 进去。

数据模型方面,原始代码读写的是裸int和QByteArray,业务层用起来很别扭。我一般加一层PlcTag结构,把“地址字符串、数据类型(bool/int16/float)、缩放系数、工程单位”绑在一起,读写接口收PlcTag而不是散参数。批量轮询时按地址连续性分组,一次读多个寄存器再拆,比一个地址发一帧效率高一个数量级——现场几十个点位,逐点读能把轮询周期拖到秒级。

异常策略上,原始实现是超时重试三次后上报错误,但没区分“通讯故障”和“地址非法”。地址非法重试多少次都没用,应该直接标记该点位为坏点并跳过,别拖累整轮采集。改造时给错误码分个类,Timeout走重试,BadAddress走隔离,Checksum走丢弃重发。

验证方法很简单:写个模拟从站的测试程序,用虚拟串口对(Windows 下可用 com0com 这类工具)把请求帧原样回环,先验证拼帧解析的正确性,再上真机。协议层单测覆盖地址解析的边界(X7 之后是 X10 不是 X8)、校验和、半包粘包,这几块测过,现场问题能少一大半。

最后说个习惯:从那以后我每次接新 PLC 型号,都先把“发出去的帧”和“收到的帧”以十六进制完整落盘,跑通一个读一个写再动界面。协议这东西,黑匣子式调试最费时间,把帧摊开看,玄学就变成了算术。希望帮到你。

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

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

Redis布隆过滤器原理与实战应用

1. Redis布隆过滤器初探第一次接触布隆过滤器是在处理一个千万级用户系统的缓存穿透问题时。当时我们的Redis集群频繁被不存在的用户ID查询打满CPU&#xff0c;常规的缓存空对象方案又面临内存爆炸的风险。技术总监拍了拍我肩膀说&#xff1a;"试试Bloom Filter吧&#xf…

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

混合云大模型开发环境搭建:阿里云百炼与Ollama实践

1. 项目概述&#xff1a;混合云大模型开发环境搭建在当前的AI开发领域&#xff0c;大模型开发环境配置一直是让开发者头疼的问题。纯云端方案虽然方便但成本高昂且存在数据安全顾虑&#xff0c;纯本地方案又受限于硬件性能。最近我在一个金融风控项目中&#xff0c;成功实现了阿…

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

DeepSORT多目标跟踪实战:数据关联、卡尔曼滤波与ID跳变调参

干这行时间长了&#xff0c;总会被问到一个问题&#xff1a;为什么视频里那段轨迹线每隔几秒就断一次&#xff0c;颜色还乱跳&#xff1f;前阵子帮一个团队看他们的视频分析模块&#xff0c;前端画面上三个行人交错走过&#xff0c;原本三条不同颜色的轨迹&#xff0c;两秒之内…

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

基于mmap与哈希索引的高性能本地KV存储实现

1. 从零打造超快本地 KV 存储&#xff1a;mmap 哈希索引完胜 Redis 的极致优化之旅1.1 为什么需要本地 KV 存储&#xff1f;在实时推荐系统的开发过程中&#xff0c;我发现网络延迟成为了性能瓶颈。当用户请求到达时&#xff0c;系统需要在极短时间内完成用户画像查询、商品特…

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

AI智能体多步骤工具调用评估框架设计与实践

1. 项目背景与核心价值这个项目瞄准的是当前AI领域最前沿也最具挑战性的方向之一——多步骤Agent与工具调用的系统性评估。过去两年里&#xff0c;我们看到大模型驱动的Agent系统开始从简单的单轮问答&#xff0c;进化到能够自主规划任务、调用工具、处理复杂工作流的智能体。但…

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

计算机中断机制原理与实战解析

1. 中断机制基础概念解析中断作为计算机系统中处理器与外部设备交互的核心机制&#xff0c;其本质是一种硬件触发的服务请求。当我在调试嵌入式系统时&#xff0c;常常需要深入理解中断的执行细节。中断与普通的函数调用最大的区别在于它的异步特性——中断请求可能在任何指令执…

作者头像 李华