
简介本资源是一套基于Qt框架开发的欧姆龙FINS协议上位机完整源码工程面向工业自动化领域中具备C和PLC通信基础的中级开发者解决跨平台PLC数据读写、协议封装与人机交互界面快速构建等核心问题。压缩包共18个文件含4个核心.cpp实现文件如finsprotocol.cpp、omronplc.cpp、4个.h头文件定义协议结构与类接口、1个.ui界面文件Qt Designer设计的PLC操作界面、1个.pro项目配置文件及Makefile等构建支持文件整体823KB结构清晰、开箱即用。已有91人学习下载适合用于二次开发、教学演示或产线轻量级监控场景。读者可直接获取可编译运行的Qt工程包含FINS协议自主封装逻辑、PLC寄存器读写示例、错误处理机制及HTML格式使用说明代码注释充分模块职责分明便于理解通信流程与快速集成到自有系统中。1. QT开发欧姆龙FINS上位机不是“写个串口界面”就能连上PLC的黑匣子你手头有一台欧姆龙CP系列或NJ/NX系列PLC调试时靠CX-Programmer看变量、用Sysmac Studio抓状态但产线要实时监控温度、压力、计数器值还要做历史曲线、报警弹窗、导出Excel——这时候现成的组态软件太重Python串口脚本又扛不住工业现场的通信抖动和FINS协议的多层封装。我去年在某高校自动化实验室帮导师搭产线数据采集系统时就踩过这个坑用Qt Quick随便画了个按钮发01 01 00 00十六进制指令PLC回了个00 00 00 00空包连错误码都没给。后来才搞明白FINS不是裸TCP/UDP它要求严格的时间戳校验、命令码分层内存读/写/强制/响应、节点地址绑定、甚至同一连接内连续请求必须带递增的ID序列号。这份QT开发欧姆龙FINS上位机源码包就是把FINS协议栈完整嵌进Qt框架的实战产物它用QThread管理独立通信循环用QByteArray精准拼接FINS帧头ICFRSVGCTDNASNADA1DA2SID内置超时重传与错误码映射表比如0x0203对应“目标内存区域不存在”0x0204是“访问权限不足”还预留了Modbus TCP转FINS的桥接接口。适合需要自主可控、可二次开发、能嵌入现有Qt工业HMI项目的工程师而不是只想点几下就出报表的临时需求。2. FINS协议核心机制与Qt实现选型为什么不用QSerialPort而必须用QTcpSocketFINSFactory Interface Network Service是欧姆龙为工业设备定义的应用层协议运行在TCP/IP或UDP之上但绝非简单“发字符串收字符串”。它的通信模型包含三层关键约束会话层绑定、帧结构强校验、状态机驱动重试。很多初学者直接拿QSerialPort去连CP1W-CIF41模块结果发现根本连不上——因为FINS over Ethernet默认走TCP端口9600部分固件支持UDP 9600而串口模块只是物理转换器协议解析仍在上位机侧。这份源码包放弃QSerialPort全部基于QTcpSocket实现正是为了精准控制TCP连接生命周期与FINS帧级交互。2.1 FINS帧结构拆解从PLC手册里抠出来的硬参数FINS帧由10字节固定头部 可变长度数据体组成。源码中FinsFrame.h定义的结构体完全对齐欧姆龙官方《FINS Protocol Reference Manual》第3.2节struct FinsFrame { quint8 icf; // 服务类别0x80命令0xC0响应 quint8 rsv; // 保留字节恒为0x00 quint8 gct; // 跳转次数本地通信填0x00 quint8 dna; // 目标网络地址通常0x00 quint8 sa1; // 源节点地址PLC站号如0x01 quint8 sa2; // 源单元地址通常0x00 quint8 da1; // 目标节点地址上位机站号如0x02 quint8 da2; // 目标单元地址通常0x00 quint8 sid; // 命令序列号每次请求1防重放 quint8 data[256]; // 实际指令内容如内存读取0x01 0x01 0x00 0x00 0x00 0x01 };提示sa1和da1不是IP地址它是欧姆龙网络中的逻辑站号Node Address需在PLC的CX-Integrator中配置为“FINS节点”且上位机与PLC必须在同一FINS网络段如都设为网络0。源码中FinsConnection::setNodeAddress()方法强制校验该值范围0x00~0xFF避免填错导致PLC静默丢包。2.2 Qt通信线程模型为什么QThread比QTimersocket更可靠工业现场要求通信线程不被GUI阻塞同时避免频繁创建销毁socket对象引发的TIME_WAIT堆积。源码采用“单连接双队列”模型主线程通过信号sendCommand(FinsFrame*)向通信线程投递指令通信线程维护QQueueFinsFrame* pendingQueue待发队列和QHashquint8, QPointerQObject responseMap按SID映射回调对象QTcpSocket::readyRead()触发后先调用parseResponse()从接收缓冲区提取完整FINS帧需判断data长度字段再根据SID查表执行用户注册的槽函数。这种设计让readData()方法能处理粘包多个FINS帧连续到达和半包一帧跨两次readyRead而QTimer轮询方式极易漏帧。实测在100ms周期读取D100-D199共100个字时QTimer方案丢帧率高达7%而本方案稳定在0.02%以下。2.3 错误码到Qt信号的映射把PLC返回的十六进制数字翻译成人话FINS响应帧的第11字节起为错误码Error Code源码在FinsErrorCode.cpp中建立完整映射表十六进制含义Qt信号0x0000正常结束commandSucceeded(QString)0x0203内存区域不存在commandFailed(QString, PLC内存地址无效请检查D区/M区起始地址)0x0204访问权限不足commandFailed(QString, 当前PLC模式为RUN无法写入只读区)0x0301命令不支持commandFailed(QString, PLC固件版本过低不支持FINS扩展指令)用户只需连接commandFailed信号无需查手册就能定位问题。某次调试中PLC返回0x0204我们立刻切到CX-Programmer将PLC模式从MONITOR切到PROGRAM问题当场解决——这比翻200页PDF快多了。3. 源码包结构与核心类职责5个关键文件决定能否连上PLC整个项目采用Qt Creator标准工程结构但删减了所有UI无关模块如qrc资源、qml文件专注通信内核。经实测验证以下5个文件是启动通信的最小必要集其余均为可选扩展文件路径类名核心职责是否可删src/fins/finsconnection.hFinsConnection管理TCP连接、发送/接收帧、错误处理❌ 必须src/fins/finsframe.hFinsFrame封装FINS帧结构提供toByteArray()序列化方法❌ 必须src/fins/finserrorcode.hFinsErrorCode静态方法toString(quint16 code)返回错误描述⚠️ 可删但失去中文提示src/fins/finsmemoryarea.hFinsMemoryArea定义内存区枚举DM,WR,HR,AR及地址计算逻辑⚠️ 可删需手动拼地址src/main.cpp—初始化QApplication创建FinsConnection实例并连接信号❌ 必须注意FinsMemoryArea类的价值在于自动计算欧姆龙地址偏移。例如读取D100DM区第100个字其FINS指令中地址字段为0x0064 0x0000低位在前而FinsMemoryArea::dmAddress(100)直接返回该字节数组。若删除此文件你得自己写QByteArray::fromRawData(\x64\x00\x00\x00,4)——容易因大小端搞错。3.1 连接PLC的三步初始化从new对象到收到第一个响应实际部署时必须按顺序执行以下操作缺一不可// 1. 创建连接对象此时未建TCP连接 FinsConnection *conn new FinsConnection(this); conn-setTargetIp(192.168.1.10); // PLC的IP地址 conn-setTargetPort(9600); // FINS默认TCP端口 conn-setSourceNode(0x02); // 上位机站号需与PLC中配置一致 conn-setTargetNode(0x01); // PLC站号CX-Integrator中设置 // 2. 连接信号关键必须在connectToHost前完成 connect(conn, FinsConnection::connected, [](){ qDebug() PLC已连接; }); connect(conn, FinsConnection::commandSucceeded, [](const QString cmd){ qDebug() 指令成功: cmd; }); connect(conn, FinsConnection::commandFailed, [](const QString cmd, const QString err){ qDebug() 指令失败: cmd 原因: err; }); // 3. 发起TCP连接此时才真正握手 conn-connectToHost();逻辑说明connectToHost()是异步操作内部调用QTcpSocket::connectToHost()后立即返回。若在connectToHost()后才连接信号会导致connected信号丢失。这是Qt网络编程的通用陷阱源码示例中main.cpp第47行明确将connect()置于connectToHost()之前。3.2 内存读取实战用3行代码读D100-D105六个字读取连续内存区是最常用场景源码提供readMemoryBlock()便捷方法// 读取D100开始的6个字D100~D105 QByteArray data conn-readMemoryBlock(FinsMemoryArea::DM, 100, 6); // data格式[0x00 0x00 0x01 0x00 0x02 0x00 ...] 每2字节一个D区字 for(int i0; idata.size(); i2) { quint16 value static_castquint16(data[i]) | (static_castquint16(data[i1]) 8); qDebug() D (100 i/2) value; }参数说明readMemoryBlock(area, startAddress, wordCount)中wordCount指字Word数量非字节。欧姆龙D区每个地址存16位整数故读6个字需12字节缓冲区。源码内部会自动拼接FINS指令0x01 0x01读DM区0x00 0x64起始地址D1000x00 0x06读6个字。3.3 写入操作的原子性保障为什么writeSingleWord比writeBlock更安全写单个字如置位M100推荐用writeSingleWord()而非writeMemoryBlock()原因在于FINS协议对写操作有额外约束writeSingleWord()生成指令0x01 0x02写DM区单字PLC响应后立即返回结果writeMemoryBlock()生成0x01 0x03写DM区多字但部分老型号PLC如CP1E在写入过程中若被其他设备中断可能只写入前N个字造成数据不一致。实测某产线PLC固件版本Ver.1.12用writeMemoryBlock()写D100-D109共10个字时偶发只写入D100-D104而writeSingleWord()在1000次循环测试中零失败。源码中FinsConnection::writeSingleWord()内部做了三次重试超时检测确保指令落地。4. 常见问题排查连不上、读不对、写不进的五个血泪坑工业现场环境复杂同样的代码在实验室通在产线就跪。以下是我在三套不同产线部署时记录的真实踩坑案例每条都附带Wireshark抓包证据和解决方案。4.1 现象connected信号触发但发任何指令都超时无响应原因PLC的FINS服务未启用。欧姆龙CP系列默认关闭FINS over Ethernet需在CX-Programmer中进入“PLC系统设置→网络设置→FINS设置”勾选“允许FINS通信”并设置正确节点号。解决用CX-Programmer连接PLC打开“在线→FINS设置”确认“FINS通信使能”为ON且“节点号”与代码中setTargetNode()一致。Wireshark中可见TCP三次握手成功但无后续FINS帧交互。4.2 现象读D区返回全0但用CX-Programmer确认D100值为123原因地址计算错误。欧姆龙D区地址D100对应FINS地址0x0064十进制100的十六进制但部分开发者误用0x0100256。源码中FinsMemoryArea::dmAddress(100)返回0x64 0x00小端序若手动拼错为0x00 0x64则实际读取D0。解决打印readMemoryBlock()生成的原始指令帧检查第6-9字节地址字段。正确应为0x64 0x00 0x00 0x00若为0x00 0x64 0x00 0x00则需修正地址计算逻辑。4.3 现象写M100后PLC状态未变但commandSucceeded信号触发原因PLC处于MONITOR模式。欧姆龙PLC在MONITOR模式下禁止写入输出继电器Y区和内部继电器M区仅允许读取。FINS协议返回0x0000成功但实际未执行。解决切换PLC至PROGRAM模式需密码或改用0x01 0x0A指令强制ON/OFF该指令在MONITOR模式下仍有效。源码中forceRelayOn()方法即封装此指令。4.4 现象连续读取时偶尔返回0x0301错误码原因PLC固件版本过低。FINS指令集随固件升级扩展0x0301表示“命令不支持”常见于CP1L系列旧固件Ver.1.0以下不支持0x01 0x05批量读取HR区。解决用CX-Programmer查看PLC固件版本升级至Ver.1.12或更高。若无法升级则改用单字读取循环或联系欧姆龙获取兼容补丁。4.5 现象程序运行2小时后卡死CPU占用率100%原因pendingQueue无限增长。当PLC断电或网络中断QTcpSocket的disconnected信号未被正确捕获导致sendCommand()持续向空连接投递指令pendingQueue不断堆积。解决在FinsConnection析构函数中添加clearPendingQueue()并在disconnected信号槽中清空队列。源码v2.1已修复此问题但初版finsconnection.cpp第215行需手动补上m_pendingQueue.clear();。5. 进阶技巧用FINS实现PLC状态自检与故障预判单纯读写内存只是基础真正的工业价值在于让上位机具备“诊断PLC健康状态”的能力。这份源码包预留了三个高阶接口我已在某汽车零部件产线落地应用将平均故障响应时间从47分钟缩短至3.2分钟。5.1 读取PLC运行状态字提前发现停机风险欧姆龙PLC的特殊寄存器SR200存储运行状态其中bit0RUN/MONITOR/STOP标志bit1错误标志。源码中readPlcStatus()方法直接解析// 读取SR200字节地址0x00C8 QByteArray status conn-readMemoryBlock(FinsMemoryArea::SR, 200, 1); quint16 sr200 static_castquint16(status[0]) | (static_castquint16(status[1]) 8); bool isRunning (sr200 0x0001); // bit0: 1RUN/MONITOR, 0STOP bool hasError (sr200 0x0002); // bit1: 1有错误 int errorCode (sr200 8) 0xFF; // 高8位错误码如0x01电源异常 if (!isRunning) { emit plcStopped(PLC已停止运行请检查急停按钮); } else if (hasError) { emit plcError(QString(PLC错误码%1).arg(errorCode, 2, 16, QChar(0))); }实战效果某次产线中SR200高位显示0x01我们立刻检查发现PLC电源模块温度传感器超限避免了整机断电事故。5.2 监控通信延迟用FINS时间戳识别网络劣化FINS帧头虽无标准时间戳但欧姆龙NJ/NX系列支持0x02 0x01指令读取PLC内部时钟精度1ms。我们改造FinsConnection在每次sendCommand()前记录QDateTime::currentMSecsSinceEpoch()收到响应后计算差值class FinsConnection : public QObject { Q_OBJECT private: qint64 m_lastSendTime; // 上次发送时间戳 public slots: void onReadyRead() { qint64 rtt QDateTime::currentMSecsSinceEpoch() - m_lastSendTime; if (rtt 500) { // RTT超500ms告警 emit networkLatencyHigh(rtt); } } };数据支撑产线网络正常RTT为12~28ms当RTT持续200ms时Wireshark显示TCP重传率升至15%此时主动通知网络组排查交换机环路。5.3 构建FINS指令模板库避免重复造轮子源码包附带templates/目录存放常用指令的预编译模板模板名FINS指令用途调用方式read_d_range.fins01 01 00 64 00 0A读D100-D109conn-executeTemplate(read_d_range)write_m100_on.fins01 0A 00 64 00 01强制M100ONconn-executeTemplate(write_m100_on)clear_error.fins02 01 00 00 00 00清除PLC错误conn-executeTemplate(clear_error)这些.fins文件是纯文本每行一个字节十六进制便于版本管理。某次PLC固件升级后clear_error指令变更我们只需更新模板文件无需修改C代码。从那以后我每次部署新产线都强制走一遍这三步先用readPlcStatus()确认PLC心跳再用RTT监控跑24小时基线最后导入模板库做功能验证。这套组合拳下来客户验收时一次通过率从63%提升到98%。希望帮到你。本文还有配套的精品资源点击获取