
简介本资源是面向工业自动化初学者与QT嵌入式开发者的三菱FX3U系列PLC编程口通信实战项目聚焦上位机与PLC的串行通信实现解决QT环境下FINS协议解析、串口参数配置、寄存器读写及通信稳定性保障等核心问题。压缩包共7个文件含2个核心cpp源码实现QSerialPort通信逻辑与FINS指令封装、2个h头文件定义协议结构与设备接口、1个ui界面文件提供串口配置与数据交互可视化控件另含pro工程配置及user用户设置文件整体仅12KB轻量易读结构清晰便于快速理解通信流程与模块职责。已有1006人学习下载适合希望掌握PLC底层通信原理、QT串口开发实践及工业协议对接能力的工程师与学生。读者可直接复用通信框架深入理解波特率/校验位配置、readyRead信号处理、FINS命令构造如读取D寄存器及基础错误重试机制为后续扩展HMI或数据采集系统打下扎实基础。1. 一个连不上PLC的串口调试器凭什么被某高校实验室当“通信底座”复用三年你手头有台老款三菱FX系列PLC——比如FX1S、FX2N或FX3U想用PC实时读写D寄存器、M继电器、定时器状态甚至下发梯形图下载指令。但Windows自带的“设备管理器超级终端”根本跑不通发一串HEX过去PLC没反应换几个波特率试COM口灯都不闪用GX Works2能连可它不开源、不提供API、没法嵌进你的上位机界面里。这时候有人甩给你一个叫FX_serial_asst.rar的压缩包解压后只有4个文件FXSerialAssist.exe、libserial.dll、config.ini和一份手写的README.txt。它没图标、没安装程序、双击就弹窗——但就是这个“土味工具”在某高校自动化实验室的三台教学实验台上稳定跑了三年成了学生做课程设计时默认的串口通信验证桩。它不碰以太网、不搞Modbus TCP只死磕RS-232物理层三菱编程口协议即所谓的“FX系列专用通信协议”非标准Modbus RTU把最底层的帧格式、校验算法、超时重试逻辑全写死了。如果你正卡在“PLC串口通信第一步”它不是万能解药但绝对是照着文档能跑通的第一块真实砖。2. 架构拆解从EXE到DLL看清楚它怎么把“编程口协议”焊进Windows串口驱动里这个项目本质是一个轻量级串口协议桥接器前端是Qt做的图形界面负责用户输入地址/数据/命令类型后端是C封装的串口通信模块负责组帧、发帧、收帧、校验、重试。它不依赖任何PLC厂商SDK所有协议细节来自三菱公开手册《FX系列编程口通信协议说明》文档号SH-080090核心逻辑全部硬编码在DLL中。整个结构极简没有网络层、没有数据库、不存历史数据纯粹做“请求-响应”透传。2.1 目录结构与文件职责4个文件各司其职解压后看到的4个文件不是随意堆放而是有明确分工文件名类型职责说明关键细节FXSerialAssist.exeQt5.15.2编译的GUI主程序提供串口选择、寄存器地址输入、数据编辑、发送按钮、接收区显示使用Qt SerialPort模块做UI层串口管理但不直接发数据所有通信调用DLL接口libserial.dllVC14.2VS2019编译的动态链接库实现完整FX编程口协议栈帧组装、CRC16校验、超时控制、重试机制、响应解析导出函数SendCommand()和ReceiveResponse()是真正干活的黑匣子config.ini文本配置文件存储上次使用的COM口、波特率、数据位、停止位、校验位格式为标准INI如[Serial] PortCOM3, BaudRate9600程序启动时自动加载README.txt纯文本说明列出支持的命令类型如READ_D,WRITE_M,READ_T、寄存器地址范围、典型错误码含义唯一提到“需将PLC设为‘编程口模式’且RUN/STOP开关置于RUN侧”提示libserial.dll是本项目真正的技术核心。它没用第三方串口库如QextSerialPort或libserial而是直接调用Windows APICreateFile(),SetCommTimeouts(),WriteFile(),ReadFile()对串口进行底层控制。这意味着它对系统兼容性要求低Win7~Win11均可用但也意味着——你改错一个超时参数整条链路就卡死。2.2 协议帧结构为什么发0x54 0x00 0x00 0x00 就能读D0三菱FX编程口协议是ASCIIHEX混合帧不是纯二进制。libserial.dll中最关键的组帧函数对应的是手册里定义的“读D寄存器”命令Function Code00// libserial.cpp 中关键片段伪代码还原 std::vectorBYTE BuildReadDFrame(int addr, int count) { std::vectorBYTE frame; // 起始符STX (0x02) frame.push_back(0x02); // 命令码READ_D 00 frame.insert(frame.end(), {0,0}); // 地址D寄存器地址左补零至4位ASCII如D0 → 0000, D100 → 0100 char addr_str[5]; sprintf_s(addr_str, %04d, addr); // 注意这里addr是十进制数值不是寄存器编号本身D100传入100 frame.insert(frame.end(), addr_str, addr_str 4); // 数据长度读取个数左补零至2位如读1个→01读10个→10 char len_str[3]; sprintf_s(len_str, %02d, count); frame.insert(frame.end(), len_str, len_str 2); // 结束符ETX (0x03) frame.push_back(0x03); // CRC16校验从命令码开始到ETX前一字节计算CRC16-IBM多项式0x8005 WORD crc CalcCRC16(frame[1], frame.size() - 2); // 跳过STX不包含ETX // CRC转为4位ASCII大写十六进制如0x1234 → 1234 char crc_str[5]; sprintf_s(crc_str, %04X, crc); frame.insert(frame.end(), crc_str, crc_str 4); return frame; }这段代码解释了三个玄学点为什么D100要传100而不是10000因为协议里D寄存器地址是十进制表示不是偏移地址。D00, D100100, D10001000。为什么地址要补零成4位协议强制规定地址字段固定4字符不足补0。发100会校验失败。CRC为什么从命令码开始算手册明确定义校验范围[Command][Addr][Len][ETX]不包括STX也不包括CRC自身。发出去的完整帧读D0是02 30 30 30 30 30 31 03 31 30 33 34ASCII码对应字符串\x02000001\x031034。如果用串口助手发这个HEX串PLC真能回数据——这证明协议实现是准确的。2.3 Qt GUI层如何与DLL交互不是信号槽是标准C接口FXSerialAssist.exe是Qt写的但和DLL通信不用Qt的QPlugin或QMetaObject而是最原始的LoadLibraryGetProcAddress。这是为了确保DLL能被其他语言如C#或Python直接调用也避免Qt版本冲突。// mainwindow.cpp 中调用DLL的关键代码 typedef int (__stdcall *SEND_CMD_FUNC)(LPCSTR port, DWORD baud, LPCSTR frame, BYTE* resp, DWORD* resp_len, DWORD timeout_ms); HMODULE hDll LoadLibrary(Llibserial.dll); if (hDll) { SEND_CMD_FUNC pSend (SEND_CMD_FUNC) GetProcAddress(hDll, SendCommand); if (pSend) { BYTE response[256]; DWORD resp_len sizeof(response); // 构建frame字符串如上面BuildReadDFrame返回的ASCII HEX串 std::string frame_str BuildReadDFrame(0, 1); int ret pSend(COM3, 9600, frame_str.c_str(), response, resp_len, 1000); if (ret 0) { // 成功response里是PLC返回的ASCII HEX字符串如020000000103A1B2 ui-textEditRecv-append(QString::fromLocal8Bit((char*)response)); } } }注意两点血泪经验DLL导出函数必须用__stdcall调用约定否则Qt调用时堆栈会乱response缓冲区必须由调用方EXE分配并传入DLL只往里填数据不负责内存管理——这是C风格DLL的铁律。3. 运行实操从零配置COM口到成功读出D0值的完整链路别急着双击EXE。这个工具对硬件连接和PLC状态极其敏感。我当年在某公司产线调试时光是连通就花了两天踩了至少7个坑。下面步骤按真实操作顺序写每一步都带验证动作。3.1 硬件准备一根线两个开关三个确认点线缆必须是原装FX编程电缆如SC-09或严格兼容的USB转RS232适配器芯片需为FTDI FT232RL或CP2102Prolific PL2303已普遍失效。普通CH340转接板大概率失败——因为FX协议对电平翻转时序要求苛刻CH340驱动在Win10/11下常丢字节。PLC侧设置以FX3U为例将PLC侧面的“RUN/STOP”拨动开关拨到RUN位置这是最容易忽略的STOP状态下PLC不响应编程口命令检查PLC前面板“POWER”灯亮“RUN”灯亮“ERR”灯不亮ERR亮程序错误或电池没电可选但推荐用GX Works2连接一次确认PLC固件版本≥2.00旧版可能不支持某些命令。PC侧确认设备管理器中找到对应COM口如COM4右键→属性→端口设置→勾选“RTS流控”和“DTR流控”FX协议需要DTR信号作为握手波特率必须设为9600FX系列编程口默认且唯一支持速率其他如19200会直接无响应数据位7停止位2校验位偶校验E——这是协议硬性规定错一个就全盘失败。验证动作打开设备管理器插拔编程电缆观察COM口号是否变化用万用表测SC-09电缆DB9端第4脚DTR和第7脚RTS对地电压插入瞬间应有12V跳变表明PC正确发出握手信号。3.2 软件配置config.ini不是摆设是救命配置首次运行FXSerialAssist.exe它会读取同目录下的config.ini。如果文件不存在它会创建一个默认的但默认值大概率不对。必须手动编辑[Serial] PortCOM4 BaudRate9600 DataBits7 StopBits2 ParityE [PLC] TimeoutMs1000 RetryCount3重点说明Port必须和设备管理器里看到的完全一致大小写敏感不能写Com4或com4ParityE必须是大写E小写e会被忽略导致校验失败TimeoutMs1000PLC响应通常在200ms内设1000ms足够太短如100ms易误判超时RetryCount3协议允许重试但超过3次仍失败基本可判定物理层有问题。编辑完保存再双击EXE。主界面右上角会显示“COM4:9600,E,7,2”说明配置加载成功。3.3 发送第一条命令读D0看返回是否含“0000”界面布局极简左上下拉框选COM口已由ini预设中上“寄存器类型”下拉选D数据寄存器中左“起始地址”填0D0中右“读取数量”填1右侧大按钮“READ”不是SEND下方大文本框接收区。点击“READ”等待1秒。如果一切正常接收区会刷出类似02 00 00 00 01 03 A1 B2或如果启用了ASCII显示模式\x02\x00\x00\x00\x01\x03\xa1\xb2关键验证点开头02是STX结尾03是ETX说明帧结构完整第2-3字节00是命令码READ_D正确第4-7字节0000是地址D0正确第8-9字节01是读1个正确最后4字节A1B2是CRC16校验值需用在线CRC计算器验证输入000001多项式0x8005初始值0x0000结果应为0xA1B2。如果收到02FF00000103XXXXFF开头说明PLC返回了错误码FF代表“命令不可识别”大概率是地址格式错或PLC未在RUN状态。4. 避坑指南那些让开发者凌晨三点还在抓头发的5个真实问题这个工具小而专但正因为专它的报错机制极其原始——没有日志、不弹详细错误框、失败就静默。以下是我在某跨平台系统集成项目中记录的真实踩坑清单每一条都附带Wireshark抓包验证过的根因。4.1 现象点击READ按钮接收区一直空10秒后才显示“Timeout”原因config.ini中TimeoutMs设为100百毫秒但PLC实际响应时间在150~300ms之间。Wireshark抓串口需用USB转TTL逻辑分析仪显示PC发帧后220ms PLC才回帧但程序100ms就放弃监听。解决将TimeoutMs改为1000并确认PLC RUN灯常亮。若仍超时检查USB转接板驱动是否为最新版尤其FTDI驱动旧版有100ms硬超时bug。4.2 现象接收区显示02FF00000103XXXX反复出现原因PLC处于STOP状态或内部程序有严重错误如除零、数组越界导致监控程序挂起无法处理编程口请求。FF是协议定义的“未定义命令”错误码但FX系列实际用它泛指“PLC忙/不可用”。解决先用GX Works2连接PLC看能否在线监控若GX Works2也连不上则重启PLC断电10秒再上电若GX Works2能连但本工具不行检查PLC参数设置→PLC系统参数→“编程口通信”是否启用默认是启用的但曾有客户误关。4.3 现象发送WRITE_M命令写M100PLC M100灯亮但再次READ_M100返回00未写入原因FX系列M继电器分“普通型”和“保持型”。M0~M499是断电不保持但WRITE_M命令写入的是当前扫描周期值PLC下一个扫描周期会根据梯形图逻辑覆盖该值。若梯形图中M100被其他触点复位它就会立刻变0。解决改用WRITE_MH写保持型M寄存器M500~M1023或在梯形图中确保M100有自锁逻辑更可靠的做法是——用READ_M确认写入后立即读若读回01则证明写入成功后续是否变0是逻辑问题非通信问题。4.4 现象同一台PC换了个USB口工具就报“Access Denied”原因Windows对不同USB控制器如Intel xHCI vs ASMedia ASM1083的串口权限管理不同。新USB口对应的COM号如COM5被系统标记为“受限设备”普通用户进程无法打开。解决以管理员身份运行FXSerialAssist.exe或进入设备管理器→对应COM口→属性→端口设置→高级→勾选“使用特殊的PCI中断”重启后重试。根本解法是统一使用主板原生USB2.0口非扩展坞。4.5 现象用Python调用libserial.dll的SendCommand总是返回-1失败原因Python的ctypes默认传递字符串为c_char_p但DLL期望的是LPCSTR即const char*。如果Python字符串含中文或特殊符号ctypes会尝试UTF-8编码而DLL内部按ANSI处理导致帧内容错乱。解决在Python中显式编码为GBKWindows默认ANSIframe_bytes frame_str.encode(gbk) # 不是utf-8 buf ctypes.create_string_buffer(frame_bytes) ret send_func(bCOM3, 9600, buf, response_buf, ctypes.byref(resp_len), 1000)5. 二次开发把libserial.dll嵌进你的Qt/C#上位机绕过EXE直接调用这才是本项目最大的价值——它不是一个“玩具工具”而是一套经过产线验证的、开箱即用的FX通信协议栈。你不需要重写CRC、不用研究超时重试逻辑只要把libserial.dll当做一个“通信原子”焊进你的工程里。下面以Qt和C#为例给出可直接粘贴的调用模板。5.1 Qt项目中集成比调用QSerialPort更简单假设你正在用Qt Creator开发一个设备监控系统需要在某个DeviceManager类里添加FX PLC读取功能// device_manager.h #include QLibrary #include QByteArray class DeviceManager { private: QLibrary m_libSerial; typedef int (__stdcall *SEND_FUNC)(LPCSTR, DWORD, LPCSTR, BYTE*, DWORD*, DWORD); SEND_FUNC m_sendCmd; public: bool initSerialLib(); int readDRegister(const QString port, int addr, int count, QByteArray data); }; // device_manager.cpp bool DeviceManager::initSerialLib() { m_libSerial.setFileName(libserial.dll); if (!m_libSerial.load()) { qWarning() Failed to load libserial.dll; return false; } m_sendCmd (SEND_FUNC)m_libSerial.resolve(SendCommand); return m_sendCmd ! nullptr; } int DeviceManager::readDRegister(const QString port, int addr, int count, QByteArray data) { // 复用前面2.2节的BuildReadDFrame逻辑生成frame_str std::string frame_str buildReadDFrame(addr, count); // 自己实现此函数 QByteArray response(256, 0); DWORD resp_len response.size(); int ret m_sendCmd( port.toLocal8Bit().constData(), // COM3 9600, frame_str.c_str(), (BYTE*)response.data(), resp_len, 1000 ); if (ret 0 resp_len 0) { data response.left(resp_len); return 0; // success } return ret; }关键点QLibrary自动处理DLL生命周期无需手动FreeLibrarytoLocal8Bit()确保字符串编码与DLL期望的ANSI一致response用QByteArray管理内存安全且Qt友好。5.2 C#项目中调用一行DllImport搞定在.NET Framework或.NET 6 WinForms/WPF项目中声明DLL导入极其简洁using System; using System.Runtime.InteropServices; using System.Text; public class FXCommunicator { [DllImport(libserial.dll, CallingConvention CallingConvention.StdCall, CharSet CharSet.Ansi)] private static extern int SendCommand( string port, uint baud, string frame, byte[] response, ref uint responseLength, uint timeoutMs); public static int ReadD(int addr, int count, out string response) { response ; byte[] buf new byte[256]; uint bufLen (uint)buf.Length; // 构建frame例如读D0\x0200000001\x031034 string frame BuildReadDFrame(addr, count); int ret SendCommand(COM3, 9600, frame, buf, ref bufLen, 1000); if (ret 0 bufLen 0) { response Encoding.ASCII.GetString(buf, 0, (int)bufLen); } return ret; } }注意CharSet CharSet.Ansi是关键它告诉CLR用系统默认ANSI编码即GBK传递字符串避免UTF-16乱码。5.3 改造libserial.dll增加新命令的实操路径项目原文没提扩展性但libserial.dll的源码结构是开放的。如果你需要新增“读取PLC型号”命令协议中为01只需三步在DLL头文件中声明新函数// libserial.h extern C __declspec(dllexport) int __stdcall ReadPLCModel(LPCSTR port, DWORD baud, char* model, DWORD* model_len, DWORD timeout_ms);在CPP中实现复用现有串口句柄和超时逻辑// libserial.cpp int __stdcall ReadPLCModel(LPCSTR port, DWORD baud, char* model, DWORD* model_len, DWORD timeout_ms) { // 组建命令帧02 01 0000 0003 XXXX 01读型号地址0000长度0003 std::string frame \x020100000003; WORD crc CalcCRC16((BYTE*)frame.c_str()1, frame.length()-2); char crc_str[5]; sprintf_s(crc_str, %04X, crc); frame crc_str; // 复用现有Send/Receive逻辑... return SendAndReceive(port, baud, frame.c_str(), (BYTE*)model, model_len, timeout_ms); }重新编译DLL用VS2019打开solution.sln假设存在配置为Release x64生成新DLL。旧EXE无需修改因为新增函数不影响原有导出表。从那以后我每次接手PLC通信需求都强制走一遍“用本工具先连通”的流程——哪怕客户说“我们有现成SDK”。因为FX编程口协议就像一个黑匣子只有看到02...03帧真实进出才能确信物理层、电气层、协议层全通。它不炫技但稳不花哨但准。希望帮到你。本文还有配套的精品资源点击获取