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.cppcore层不依赖任何 UI 头文件纯 C 加 QtCore这样协议逻辑可以单独写单元测试也能被命令行工具复用。transport层做接口抽象串口和网口实现同一套ITransport上层PlcClient不关心底层是线还是网。这个分层是后面所有改造的基础别省。2.3 开发环境搭建的具体步骤先把工具链装齐顺序别乱安装 VS2019勾选“使用 C 的桌面开发”工作负载。安装 Qt 5.15或 5.12 LTS组件里勾 MSVC 2019 64-bit 和 Qt Serial Port。VS2019 里装扩展 Qt Visual Studio Tools在“扩展 → Qt VS Tools → Qt Versions”里把 qmake.exe 路径指过去。新建 Qt Widgets Application或者直接打开现成的.vcxproj。.pro文件里串口模块必须显式声明否则链接阶段报QSerialPort未定义QT core gui serialport CONFIG c11 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.hQT 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 下是COMxLinux 下是/dev/ttySxdataBits用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_castquint8(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整个挪到独立QThreadUI 只发命令收结果。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 型号都先把“发出去的帧”和“收到的帧”以十六进制完整落盘跑通一个读一个写再动界面。协议这东西黑匣子式调试最费时间把帧摊开看玄学就变成了算术。希望帮到你。本文还有配套的精品资源点击获取