简介FX_serial_asst.rar 是一份面向工业自动化上位机开发初学者与 PLC 通信实践者的 Qt 工程源码围绕三菱 FX3U 编程口串行通信展开可用于搭建上位机连接、数据采集与寄存器读写等练习场景。压缩包共 7 个文件约 12KB包含 2 个 cpp、2 个 h、1 个 ui、1 个 user 与 1 个 pro 文件分别承载通信逻辑、类声明、界面布局、Qt Creator 用户配置及工程构建信息结构紧凑。已有 1004 人学习下载。源码中可见 QSerialPort 串口参数配置、数据收发处理以及面向 FX3U 指令集的解析与读写请求实现并涉及通信失败后的重试与错误处理思路适合对照理解编程口通信流程、FINS 指令组织方式和 Qt 串口工程搭建方法也可作为自行开发 PLC 通信工具的基础样例。1. 三菱FX编程口通信一个串口助手小工具能顶多少事车间里一台老 FX 系列 PLC 摆在电控柜里编程口是圆头 8 针笔记本上没有串口手边只有一根 USB 转串口线和一堆对不上号的通信线。现场想确认 PLC 到底有没有在回数据最土的办法是拿一个串口助手发一串十六进制报文看返回的字节对不对。这个叫 FX_serial_asst 的小工具干的就是这件事把三菱 FX 编程口通信里那些固定帧格式、校验位、读写命令封装成能点的界面同时把 Qt 的串口收发、定时器、报文拼接这一套骨架完整摆出来。它适合三类人现场调试 PLC 的电气工程师、要写上位机采集 FX 数据的开发、以及想学 Qt 串口编程但不知道从哪下手的初学者。后面拆的是它怎么组织代码、怎么发一帧、怎么把协议层和界面层切开最后讲二次开发时怎么把固定协议换成可配置的。2. 工程结构与技术选型Qt串口助手怎么组织代码先看骨架。这类基于 Qt 的串口上位机常见的做法是 qmake 或 CMake 二选一按模块拆成「界面」「串口封装」「协议编解码」三层。项目名里的 FX 指的是三菱 FX 系列 PLCserial 指串口asst 是 assistant 的缩写合起来就是一个针对 FX 编程口协议的串口调试助手。它不是通用串口工具报文格式、站号、校验都按 FX 的规则来这是它和市面通用串口助手最大的区别。2.1 分层结构与目录约定我一般把这类项目拆成下面几个目录源码包里即使叫法略有差异职责也是这一套FX_serial_asst/ ├── FX_serial_asst.pro # qmake 工程文件声明 QT serialport widgets ├── main.cpp # 入口QApplication 主窗口 ├── mainwindow.h/.cpp/.ui # 界面层控件、信号槽、日志显示 ├── serialworker.h/.cpp # 串口层打开/关闭/读写/错误处理 └── fxprotocol.h/.cpp # 协议层帧组装、校验、解析、软元件地址换算界面层只管控件和显示串口层只管字节流的进出协议层只做纯函数式的组帧和拆帧不碰 QSerialPort。这样分层的好处很实在协议层可以脱离硬件做单元测试界面层换皮肤不动逻辑串口层换 USB 转串口芯片也不用改协议。很多人写串口工具把三者揉在一个 cpp 里第一版能跑等要加第二种 PLC 协议时就只能复制粘贴血泪经验。工程文件里最关键的一行是串口模块的引入QT core gui serialport CONFIG c11 TARGET FX_serial_asst TEMPLATE app SOURCES main.cpp mainwindow.cpp serialworker.cpp fxprotocol.cpp HEADERS mainwindow.h serialworker.h fxprotocol.h FORMS mainwindow.ui这里serialport是 Qt5.1 之后才有的模块Qt4 时代用的是第三方 QextSerialPort如果拿到的代码里出现qextserialport.h说明是老工程得按新模块改写。CONFIG c11影响QByteArray的初始化列表写法Qt5.12 以上不写也能过但显式声明更稳。2.2 串口层的封装QSerialPort 怎么用才不出错串口层要解决的是一件事把「打开端口、设参数、收数据、报错误」这四步做扎实。FX 编程口默认参数是 9600 波特率、7 位数据位、偶校验、1 位停止位这个 7E1 组合是最容易翻车的地方因为大多数 USB 转串口默认是 8N1参数不对时收到的全是乱码或者干脆没反应。// serialworker.cpp 核心片段 bool SerialWorker::openPort(const QString name) { m_port.setPortName(name); m_port.setBaudRate(QSerialPort::Baud9600); // FX 编程口默认 9600 m_port.setDataBits(QSerialPort::Data7); // 关键7 位数据位 m_port.setParity(QSerialPort::EvenParity); // 偶校验 m_port.setStopBits(QSerialPort::OneStop); // 1 位停止位 m_port.setFlowControl(QSerialPort::NoFlowControl); // 编程口不用流控 if (!m_port.open(QIODevice::ReadWrite)) { emit errorOccurred(m_port.errorString()); // 把错误抛给界面显示 return false; } connect(m_port, QSerialPort::readyRead, this, SerialWorker::onReadyRead); return true; }setDataBits(QSerialPort::Data7)和EvenParity这两行是 FX 编程口的命门缺一个都不通。FlowControl必须关掉编程口是两线半双工风格的时序开流控会把帧卡住。错误不吞掉而是用信号抛给界面是因为现场最常见的问题就是端口被别的软件占用返回 PermissionError把errorString()原样显示出来一眼就知道是被占用了还是线没插好。读数据的槽函数要处理粘包和半包串口不是一次 readyRead 就给你一整帧void SerialWorker::onReadyRead() { m_buffer.append(m_port.readAll()); // 先全部收进缓冲 // FX 应答帧以 0x02(STX) 开头、0x03(ETX) 结尾按这个边界切帧 int start m_buffer.indexOf(0x02); int end m_buffer.indexOf(0x03, start); while (start 0 end start) { QByteArray frame m_buffer.mid(start, end - start 1); emit frameReceived(frame); // 完整帧交给上层解析 m_buffer.remove(0, end 1); start m_buffer.indexOf(0x02); end m_buffer.indexOf(0x03, start); } if (m_buffer.size() 1024) m_buffer.clear(); // 防御异常数据不无限涨 }这里用0x02/0x03做帧边界是 FX 协议本身的约定比按长度切稳得多因为应答长度随命令变化。缓冲上限 1024 是个保险防止波特率设错时收到一堆乱码把内存撑爆这个防呆在实际现场救过不少次。2.3 界面层与信号槽的接线方式界面层典型布局是顶部串口选择下拉框 打开/关闭按钮中部是十六进制发送框底部是收发日志。Qt Designer 画好 .ui 后用connect把按钮信号接到槽上串口层的frameReceived信号接到日志显示槽。数据统一用十六进制字符串渲染因为 FX 报文里大量不可打印字节直接当文本显示会变成方框。界面上还有个容易被忽略的细节发送按钮在串口没打开时必须禁用否则用户点了没反应会以为程序死了。这个状态管理放到openPort成功/失败的分支里同步更新ui-btnSend-setEnabled(bool)比在槽函数里再判断一次端口状态更直观。日志框用QPlainTextEdit而不是QTextEdit几千行报文刷下来前者性能明显更好这也是我踩过卡顿之后才改的习惯。3. FX协议报文编解码一帧读命令怎么拼出来协议层是整个项目含金量最高的部分。三菱 FX 编程口的通信格式是 ASCII 报文帧结构固定为 STX 站号 命令 数据 校验 ETX校验是把从站号到数据末尾的 ASCII 码逐字节相加取低两位十六进制。搞懂这一帧读写软元件就是换命令字的事情。3.1 帧结构与校验算法以读 D 寄存器为例命令字是0读软元件代码 D 是D0十六进制 0xA0 加地址应答里带同样的校验。组帧函数不该依赖任何 Qt 控件纯输入输出// fxprotocol.cpp QByteArray FxProtocol::buildReadFrame(int station, const QString device, int count) { QString body QString(%1%2%3%4) .arg(station, 2, 16, QChar(0)) // 站号默认 00 .arg(0) // 命令读 .arg(device) // 软元件地址如 0A00 表示 D0 .arg(count, 2, 16, QChar(0)); // 读取点数十六进制 int sum 0; for (QChar c : body) sum c.toLatin1(); // ASCII 累加和 QString frame QString(\x02%1%2\x03) // STX body 校验 ETX .arg(body) .arg(sum 0xFF, 2, 16, QChar(0)); // 取低字节两位十六进制 return frame.toLatin1(); }几个参数要盯住station是站号单机默认00多机时每台不同device的编码规则是软元件类型码加 4 位地址D 区类型码0A、M 区08、X 区80、Y 区8C地址用十进制转十六进制补四位count是一次读的点数十六进制表示读 10 个点要写0A而不是10。校验算法sum 0xFF后转两位十六进制位数不够要补零少补一个零对方直接判帧错误。提示不同 FX 子型号FX1N/FX2N/FX3U的软元件地址范围和类型码有细微差别改协议前先对一遍对应型号的通信手册别拿一个型号的地址表套所有机器。3.2 应答解析与异常码处理应答帧里最需要处理的是异常。FX 的否定应答是NAK(0x15)开头或者返回带错误码的报文错误码常见有02软元件地址越界、03数据长度超限、06帧格式错误。解析时先看首字节再分流void FxProtocol::parseResponse(const QByteArray frame, QVectorquint16 out) { out.clear(); if (frame.isEmpty()) return; if (frame.at(0) 0x15) { // NAKPLC 拒绝了这帧 emit protocolError(QString(PLC 返回 NAK检查地址与长度)); return; } QByteArray body frame.mid(1, frame.size() - 4); // 去掉 STX、校验、ETX // 校验比对重算 body 的 ASCII 和 int sum 0; for (char c : body) sum static_castquint8(c); QString calc QString(%1).arg(sum 0xFF, 2, 16, QChar(0)).toUpper(); QString recv QString::fromLatin1(frame.right(3).left(2)).toUpper(); if (calc ! recv) { emit protocolError(QString(校验不匹配算出 %1收到 %2).arg(calc, recv)); return; } // 数据区每 4 个 ASCII 字符是一个 16 位寄存器值 for (int i 0; i 4 body.size(); i 4) { bool ok false; out.append(body.mid(i, 4).toUShort(ok, 16)); if (!ok) emit protocolError(数据区出现非十六进制字符); } }frame.mid(1, frame.size() - 4)这个偏移量要算清楚首字节 STX 占 1尾部校验 2 位加 ETX 1 位共 3所以正文长度是总长减 4。数据区按 4 个 ASCII 字符一组解析因为一个 16 位寄存器用 4 位十六进制表示。这里把校验不匹配单独报出来而不是静默丢弃是因为现场接触不良时校验失败频率会明显上升能当成线路质量的指示灯用。3.3 软元件地址换算的一个坑地址换算是最容易写错的地方。D 寄存器地址是十进制的报文里要转成十六进制 4 位D100 对应0064但 X/Y 区是八进制编号X10 不是十六进制 0x10要按八进制换算再补位。常见做法是做一个换算函数输入软元件名和数字输出 4 位十六进制地址串QString FxProtocol::deviceAddress(const QString dev, int num) { if (dev.startsWith(X) || dev.startsWith(Y)) return QString(%1).arg(num, 4, 8, QChar(0)); // 八进制编号 return QString(%1).arg(num, 4, 16, QChar(0)).toUpper(); // 其余十六进制 }八进制和十六进制混用这件事我第一次写的时候直接把 X10 当十六进制发出去PLC 返回 NAK查了半天才反应过来。这个函数看着只有几行但是整个协议层最值得单独测的部分输入输出明确适合写一组对照用例。4. 现场调试避坑串口打不开、收到乱码、帧不回怎么办下面几条都是这类工具跑现场时高频出现的问题按「现象 → 原因 → 解决」记下来比查手册快。4.1 串口打开失败提示权限或占用现象点打开按钮没反应日志里出现PermissionError或Device or resource busy。原因端口被别的调试软件占着或者驱动没装好设备管理器里显示黄色感叹号。解决先关掉其他串口工具确认设备管理器里端口号正常程序里把errorString()原样显示出来别自己编一句「打开失败」否则你永远不知道是被占用还是线不对。USB 转串口线插拔后端口号会变下拉框刷新逻辑要跟着做。4.2 通信参数设成 8N1收回来全是乱码现象能打开端口发出去有回应但日志里一堆乱码STX/ETX 都找不到。原因FX 编程口是 7 位数据位偶校验用了默认 8N1 导致高位截断。解决把Data7、EvenParity、OneStop三个参数一起改只改一个照样不通。改完还乱就换一根质量好点的 USB 转串口线劣质芯片在 9600 下也会丢位。4.3 发了帧但一直没有应答现象发送日志有记录接收区空白。原因常见有三种——接线把 TX/RX 接反了、站号写错、软元件地址越界但 PLC 返回的 NAK 被程序当垃圾数据丢了。解决先用一根线短接 TX/RX 做回环测试确认收发通路正常再把站号固定成00试最后检查parseResponse里对0x15的处理有没有被提前 return 掉。4.4 偶发校验失败重发就好现象十次里有两次校验报错重发一次又正常。原因多为接地不良或线太长引起的位错误也可能是程序没做半包拼接把两帧的碎片当成一帧处理。解决先确认onReadyRead里的缓冲区切帧逻辑是按 STX/ETX 边界切的不是按固定长度切再从硬件上缩短通信线、给 PLC 和电脑共地。软件层面加一次重发机制比什么都实在。4.5 界面卡死日志框越来越慢现象通信跑几分钟后界面明显变卡日志窗口滚动迟滞。原因每收到一个字节就 append 一次信号槽高频触发加上用了QTextEdit而非QPlainTextEdit。解决收发日志做批量刷新用QTimer每 100ms 把缓冲的报文一次性刷进界面日志超过一定行数自动截断。这个改动前后差别很大现场长时间挂着调试的时候体会最深。5. 二次开发把固定协议改成可配置的采集上位机这份代码最值得学的不是界面是它把协议层独立出来的结构。想把它变成自己的采集工具改造点集中在三个地方我按动手顺序说。第一处是协议参数外置。现在站号、软元件、点数是写在代码里的改成从配置文件或界面表格读。做一个简单的设备配置结构体用QSettings或 JSON 存界面加一个表格控件列出「站号 / 软元件 / 地址 / 点数 / 周期」采集线程按表格逐行轮询。这样加一台 PLC 只是加一行配置不用重新编译。第二处是采集调度从主线程挪出去。现在大多是在界面槽里发帧采集周期一短就卡。用QThread把SerialWorker挪到子线程主线程只负责 UI 更新和配置下发跨线程用信号槽传数据注意QSerialPort对象必须在它被创建的线程里使用不要在主线程 new 了再 moveToThread 后又直接调它的方法。轮询用QTimer加一个待发队列发一帧等应答超时再做下一帧避免把 PLC 的通信缓冲打满。// 采集线程里的轮询骨架 void Poller::tick() { if (m_waiting) return; // 上一帧还没回跳过本轮 if (m_queue.isEmpty()) buildQueue(); // 一轮读完重新生成任务 auto task m_queue.dequeue(); m_waiting true; m_serial-write(FxProtocol::buildReadFrame( task.station, task.device, task.count)); m_timeout.start(500); // 500ms 无应答判超时 }m_waiting这个标志位是防止命令叠发的关键串口是半双工的上一帧没回就发下一帧回来的是哪一帧的应答就分不清了。超时时间按现场线长和 PLC 扫描周期定500ms 对 9600 波特率读十几个点足够。第三处是解析结果落库。读数解析成QVectorquint16之后按软元件类型和地址做快照带时间戳写进本地数据库或者推给 MQTT做成历史曲线就完成了从调试助手到采集上位机的转变。这里建议把「原始报文」和「解析值」都留一条日志参数出问题时能回放对账比事后猜强得多。验证改造是否正确的办法很直接拿一个已知值的 D 寄存器从界面上写一个数进去再用 PLC 编程软件在线监视读回来两边对得上说明组帧、校验、地址换算整条链是通的。我习惯先用最小的帧——单站号、读 1 个点——跑通再扩到批量轮询一上来就配几十个点出错了根本定位不到是地址错了还是切帧错了。从那以后我每次改协议层都强制先跑这一条最小用例成了固定动作。希望这套拆法帮到你源码拿到手先照第 2 章的结构对着看一遍再动第 5 章的三个改造点。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
FX PLC编程口调试助手:Qt串口组帧校验与批量轮询 简介:面向Qt上位机与三菱FX3U PLC编程口通信学习的示例工程,适合工业自动化方向的上位机开发者、PLC初学者及需要理解串行通信流程的工程人员参考。压缩包共7个文件,约12KB,以cpp、h源码为主,另有ui界面与pro工程配置&… · 2026/11/5 19:00:04
视频翻译实战:产品演示视频多语言本地化完整指南 接这个任务之前,我对“视频翻译”的理解一直停留在字幕层面。直到一个5分钟的产品演示视频需要翻成英文、日文、西班牙文三个版本,交付周期只有十天,我才发现这个活儿根本不是换字幕那么简单。把一段演示视频做成多语言版本,要动的… · 2026/11/5 5:46:05
C++ OPC DA客户端封装:COM/DCOM连接与异步订阅避坑 简介:面向工业自动化、上位机及数据采集方向的 C/C 开发者,OPC DA 客户端静态库源码包聚焦本地与远程 OPC 服务器连接这一常见需求,适合具备一定编程基础、需要在项目中集成 OPC 通信能力的中高级人员参考。压缩包共 32 个文件,以… · 2026/11/5 12:17:00
EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析 1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被… · 2026/11/5 16:49:35
Rust原来很简单二:所有权系统 Part 2 核心篇 —— 所有权系统
本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理
6… · 2026/11/5 16:26:08
基于Hadoop+Spark的民宿智能推荐系统设计与实现 1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享… · 2026/11/5 14:59:25
ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配RS485,解决远距离温度监控难题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 14:10:41
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07