简介:本资源是一套基于Visual C++ 6.0开发的三菱PLC上位机编程软件完整源码工程,面向工业自动化领域初/中级C++开发者、工控软件学习者及MFC实践者,解决PLC通信协议解析、上位机界面设计与调试功能集成等核心问题。压缩包共60个文件,含14个头文件(.h)定义通信与UI结构、13个源码文件(.cpp)实现串口通信、LAD逻辑视图、IO点管理等核心模块,辅以图标(.ico)、光标(.cur)、位图(.bmp)等资源文件支撑MFC界面,另有可执行程序(.exe)、工程配置(.dsp/.dsw)及调试日志(.plg),整体仅731KB,轻量易读。已有1282人学习下载,适合通过真实工业场景代码反向掌握VC6.0+MFC工控软件开发范式。读者可直接编译运行Debug PLCEDIT.exe进行PLC指令读写与逻辑调试,深入理解LAD文件解析(.lad/.plc)、多文档界面(MDI)架构及三菱串口协议封装逻辑,为定制化监控系统开发提供可复用的工程骨架与通信模板。
1. 这不是个“老古董演示项目”:它真能连上正在运行的FX系列PLC,读写D寄存器、M点、定时器,还能自定义报文收发——适合需要快速验证通讯逻辑、不想从零啃三菱MC协议文档的现场工程师和自动化课设学生
你手头有一台FX3U PLC,产线停机半小时,产线主管催着你改一个配方参数;或者你正带学生做《工业控制网络》课程设计,要求实现PC端对PLC的实时监控界面。这时候打开VC6.0新建一个MFC工程,从头写串口初始化、CRC16校验、MC协议帧拼装、超时重试、多线程数据刷新……三天过去,界面还没画完。而这个源码包,解压即见完整可编译的MFC对话框工程,主界面左侧是树状设备节点(CPU、扩展模块),中间是寄存器地址表(D0–D999、M0–M1023、T0–T255),右侧是实时值编辑区+强制写入按钮。它不依赖任何第三方控件,所有通讯逻辑封装在CMcProtocol.cpp里,用的是最朴素的Windows API串口操作(CreateFile/SetCommTimeouts/WriteFile/ReadFile),没有COM组件、没有.NET互操作、没有Qt信号槽——就是纯C++ + Win32,编译出来一个不到800KB的EXE,插上USB转485线就能连。它不是教学Demo,而是某高校实验室为本地一家包装机械厂做的调试工具迭代版,已实际用于三台FX2N/FX3U设备的参数下发与故障诊断。如果你要的不是“能跑”,而是“能立刻拿来改、改完马上上线”,那这份VC6.0源码就是你今天该下载的资源。
2. 工程结构与核心模块拆解:从MFC对话框框架到MC协议解析器的完整链路
2.1 工程目录与关键文件职责说明
整个VC6.0工程名为PLC_UpperPC,采用标准MFC AppWizard生成的基于对话框的应用程序结构。解压后共17个文件,不含资源图片和说明文档,核心源码文件如下:
| 文件名 | 类型 | 关键职责 |
|---|---|---|
PLC_UpperPC.h/.cpp | MFC应用类 | 程序入口、全局变量声明(如g_hComPort串口句柄)、主对话框创建 |
PLC_UpperPCDlg.h/.cpp | 主对话框类 | UI控件管理(树控件CTreeCtrl m_TreeDev、列表控件CListCtrl m_ListReg)、按钮消息响应(OnBnClickedBtnConnect等) |
McProtocol.h/.cpp | 独立协议模块 | 核心:MC协议帧构造(BuildReadCommand())、CRC16计算(CalcCRC16())、响应解析(ParseResponse())、超时控制(WaitForResponse()) |
SerialPort.h/.cpp | 独立串口模块 | 封装CreateFile/SetupComm/SetCommTimeouts,提供OpenPort()/ClosePort()/SendData()/RecvData()接口 |
RegAddress.h | 头文件 | 定义寄存器地址常量(ADDR_D_REG = 0x80)、数据类型枚举(REG_TYPE_D,REG_TYPE_M)、最大读取长度(MAX_READ_COUNT = 128) |
提示:
McProtocol.cpp中所有函数均未使用MFC类(如CString),全部采用unsigned char*和int参数,确保协议层完全脱离UI框架,可直接移植到嵌入式Linux或FreeRTOS环境。
2.2 主对话框初始化与设备树构建逻辑
主对话框CPLC_UpperPCDlg在OnInitDialog()中完成两件关键事:初始化串口控件下拉列表、构建PLC设备树。设备树并非硬编码,而是通过LoadDeviceTree()函数动态加载:
// CPLC_UpperPCDlg.cpp void CPLC_UpperPCDlg::LoadDeviceTree() { HTREEITEM hRoot = m_TreeDev.InsertItem(_T("FX Series PLC"), TVI_ROOT); HTREEITEM hCPU = m_TreeDev.InsertItem(_T("CPU Module"), hRoot); m_TreeDev.InsertItem(_T("D Register (D0-D999)"), hCPU, (HTREEITEM)REG_TYPE_D); m_TreeDev.InsertItem(_T("M Bit (M0-M1023)"), hCPU, (HTREEITEM)REG_TYPE_M); m_TreeDev.InsertItem(_T("Timer (T0-T255)"), hCPU, (HTREEITEM)REG_TYPE_T); HTREEITEM hExt = m_TreeDev.InsertItem(_T("Extension Modules"), hRoot); // 实际项目中此处会根据配置文件或扫描结果插入AD/DA模块节点 m_TreeDev.InsertItem(_T("AD Module #1 (CH1-CH4)"), hExt, (HTREEITEM)REG_TYPE_AD); }关键点在于每个树节点的lParam参数被赋值为对应寄存器类型枚举值(REG_TYPE_D等)。当用户点击“D Register”节点时,OnTvnSelchangedTreeDev()捕获选中事件,直接取出lParam即可知道要读取哪类寄存器,无需字符串匹配或配置表查找——这是MFC树控件最实用但常被忽略的技巧。
2.3 MC协议帧构造与CRC16校验实现细节
三菱MC协议(QnA兼容模式)采用固定帧头0x54 0x00,后接命令码、站号、等待时间、数据长度等字段。CMcProtocol::BuildReadCommand()函数严格按协议手册生成字节流:
// McProtocol.cpp bool CMcProtocol::BuildReadCommand(unsigned char* pBuf, int nBufLen, REG_TYPE type, int nStartAddr, int nCount) { if (nBufLen < 16) return false; // 帧头:0x54 0x00 pBuf[0] = 0x54; pBuf[1] = 0x00; // 命令码:D寄存器读=0x0000, M位读=0x0100, T定时器读=0x0200 unsigned short cmd = 0; switch(type) { case REG_TYPE_D: cmd = 0x0000; break; case REG_TYPE_M: cmd = 0x0100; break; case REG_TYPE_T: cmd = 0x0200; break; default: return false; } pBuf[2] = (cmd >> 8) & 0xFF; // 高字节 pBuf[3] = cmd & 0xFF; // 低字节 // 站号:默认0x00(本机) pBuf[4] = 0x00; // 等待时间:0x0014 (20ms) pBuf[5] = 0x00; pBuf[6] = 0x14; // 数据长度:2字节,表示后续地址+数据长度字段总字节数 // 此处为固定地址格式:2字节类型 + 2字节起始地址 + 2字节读取数量 pBuf[7] = 0x00; pBuf[8] = 0x06; // 地址类型字段(D=0x0000, M=0x0100, T=0x0200) pBuf[9] = (cmd >> 8) & 0xFF; pBuf[10] = cmd & 0xFF; // 起始地址(BCD码,非十进制!) unsigned short bcdAddr = DecimalToBCD(nStartAddr); pBuf[11] = (bcdAddr >> 8) & 0xFF; pBuf[12] = bcdAddr & 0xFF; // 读取数量(BCD码) unsigned short bcdCount = DecimalToBCD(nCount); pBuf[13] = (bcdCount >> 8) & 0xFF; pBuf[14] = bcdCount & 0xFF; // CRC16校验(从帧头开始,含所有已写入字节) unsigned short crc = CalcCRC16(pBuf, 15); pBuf[15] = (crc >> 8) & 0xFF; pBuf[16] = crc & 0xFF; return true; }注意三个易错点:
- 地址必须转BCD码:
DecimalToBCD(123)返回0x0123,而非0x007B。MC协议所有地址字段均用BCD,否则PLC直接丢弃帧; - CRC范围包含帧头:从
pBuf[0]开始计算至倒数第三个字节(即校验前所有有效数据),不包括最后两个CRC字节本身; - 命令码与地址类型字段一致:
REG_TYPE_D对应命令码0x0000,也对应地址类型字段0x0000,二者必须严格匹配,否则PLC返回0x0001错误码(命令不支持)。
CalcCRC16()采用标准Modbus CRC16算法(多项式0xA001),代码简洁无依赖:
// McProtocol.cpp unsigned short CMcProtocol::CalcCRC16(const unsigned char* pData, int nLength) { unsigned short crc = 0xFFFF; for (int i = 0; i < nLength; i++) { crc ^= pData[i]; for (int j = 0; j < 8; j++) { if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; }该CRC实现经实测与三菱官方调试软件发出帧完全一致,可放心用于生产环境。
3. 编译与运行全流程:从VC6.0环境搭建到真实PLC联调
3.1 VC6.0开发环境准备与工程配置
虽然VC6.0早已停止支持,但其对Win32 API的原生支持仍是工业领域不可替代的优势。安装步骤极简:
下载VC6.0安装包(常见于高校软件镜像站),运行
setup.exe,仅勾选“Visual C++ 6.0”组件,取消所有SDK、Java、数据库选项,全程约15分钟;安装完成后,进入
Tools → Options → Directories,在“Include files”路径末尾添加:$(VCInstallDir)PlatformSDK\Include在“Library files”路径末尾添加:
$(VCInstallDir)PlatformSDK\Lib注意:无需单独安装Platform SDK,VC6.0自带精简版,足够支撑串口API调用。
打开
PLC_UpperPC.dsw工作区,右键PLC_UpperPC工程 →Settings→C/C++页签 → 将Category设为General,Preprocessor definitions中添加_CRT_SECURE_NO_DEPRECATE(屏蔽strcpy等安全警告);Link页签 →Input→Object/library modules中确认已包含comctl32.lib(用于树控件)和winmm.lib(虽未用但部分旧工程依赖)。
提示:若编译报错
error C2065: 'LPCTSTR' : undeclared identifier,说明stdafx.h未正确包含windows.h,在PLC_UpperPCDlg.h顶部添加#include <windows.h>即可解决。
3.2 串口连接与PLC参数设置
硬件连接采用标准RS485方式:PC端USB转485适配器(如FTDI芯片方案),PLC端接FX系列485-BD模块(型号FX2N-485-BD或FX3U-485-BD)。关键参数必须严格匹配:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 波特率 | 19200 | FX2N/FX3U默认最高支持38400,但19200稳定性最佳 |
| 数据位 | 8 | 固定值,MC协议无7位模式 |
| 停止位 | 1 | 固定值 |
| 校验位 | None | MC协议不使用奇偶校验 |
| 站号 | 0x00 | PLC侧需在参数设置中将“站号”设为0(十六进制) |
PLC侧设置方法(以GX Works2为例):
- 进入
PLC参数→PLC系统参数→串行通信设置; - “通信方式”选
RS-485,“站号”填0(注意是十进制0,对应十六进制0x00); - “通信速度”选
19200bps,“数据长度”8bit,“停止位”1bit,“校验”无; - 务必勾选“启用MC协议”(此项默认关闭,是联调失败最常见原因)。
3.3 主界面操作与数据读写验证
启动编译后的PLC_UpperPC.exe,按顺序执行:
- 端口选择:点击“串口设置”按钮,在弹出对话框中选择对应COM口(如
COM3),点击“确定”; - 连接PLC:点击“连接”按钮,状态栏显示
Connected to COM3,树控件根节点图标变为绿色; - 读取寄存器:展开树控件 → 右键“D Register (D0-D999)” → 选择“读取D0-D99”;此时程序向PLC发送读D0~D99共100个字的命令帧;
- 验证响应:若PLC在线且参数正确,列表控件
m_ListReg将在2秒内填充100行数据,每行显示D0=0000、D1=0005等十六进制值; - 写入测试:在列表中双击
D0单元格,输入1234(十六进制),回车后程序自动发送写D0=0x1234命令,PLC对应寄存器立即更新。
注意:写入操作有防误触机制——双击编辑后必须按回车或点击其他单元格才触发写入,避免光标移动误触发。
4. 常见问题排查:那些让现场工程师抓狂的“玄学”连接失败
4.1 现象:点击“连接”后状态栏显示Failed to open COMx
原因:
- COM端口号被其他程序占用(如GX Developer、串口调试助手未关闭);
- USB转485驱动未正确安装(设备管理器中显示黄色感叹号);
- Windows 10/11系统启用了“快速启动”,导致USB设备热插拔识别异常。
解决:
- 任务管理器结束所有
GX、Serial相关进程; - 设备管理器中卸载USB Serial Port驱动,勾选“删除此设备的驱动程序软件”,重启后重新插拔适配器;
- 关闭“快速启动”:
控制面板 → 电源选项 → 选择电源按钮的功能 → 更改当前不可用的设置 → 取消勾选“启用快速启动”。
4.2 现象:连接成功但读取超时,状态栏显示Timeout waiting for response
原因:
- PLC站号设置为1(十进制)而非0(十进制),导致PLC忽略帧头中的
0x00站号; - 485线路A/B线接反(表现为能发不能收,或收发都失败);
- PLC未处于RUN状态(STOP状态下MC协议服务默认关闭)。
解决:
- 用GX Works2在线连接PLC,确认
PLC参数 → 串行通信设置 → 站号为0; - 用万用表测量485适配器A/B端子与PLC 485-BD模块A/B端子电压,正常应有±1.5V差分电压,若为0V则线路接反;
- 按PLC面板RUN/STOP按钮切换至RUN状态,或通过GX Works2在线写入RUN指令。
4.3 现象:读取数据全为0000,但PLC实际D0值为1234
原因:
- 地址未转BCD码:程序中
BuildReadCommand()传入nStartAddr=0,但BCD转换函数DecimalToBCD(0)返回0x0000正确,若传入100却返回0x0064(十进制100的十六进制)则错误; - PLC侧寄存器类型不匹配:D寄存器读命令发给了M点地址(如D0地址是0x0000,M0地址是0x0100),但程序未校验节点类型。
解决:
- 在
BuildReadCommand()开头添加日志:TRACE(_T("Reading %d registers from address %d (BCD: %04X)\n"), nCount, nStartAddr, DecimalToBCD(nStartAddr));,用Debug版本运行查看输出; - 检查树节点
lParam值:在OnTvnSelchangedTreeDev()中加断点,观察pNMTreeView->itemNew.lParam是否等于预期REG_TYPE_D(0)或REG_TYPE_M(1)。
4.4 现象:写入后PLC值不变,但读取显示已更新
原因:
- 写入命令使用了错误的命令码:D寄存器写命令码为
0x0001,但程序可能误用读命令码0x0000; - PLC程序中对该D寄存器有循环覆盖逻辑(如每周期执行
MOV K123 D0),导致写入瞬间被覆盖。
解决:
- 对照
McProtocol.h中定义的命令码宏:#define CMD_WRITE_D 0x0001,确认BuildWriteCommand()中pBuf[2]/pBuf[3]赋值正确; - 在GX Works2中搜索PLC程序,查找对目标D地址的所有引用,临时注释掉可能的覆盖指令。
5. 二次开发实战:从“能连上”到“能定制”的三步改造法
5.1 第一步:扩展寄存器类型支持(增加C计数器与R文件寄存器)
原工程仅支持D/M/T三类寄存器,但实际项目常需访问C计数器(C0-C255)和R文件寄存器(R0-R32767)。改造只需两处:
1. 新增寄存器类型枚举与地址映射
在RegAddress.h中追加:
typedef enum { REG_TYPE_D, REG_TYPE_M, REG_TYPE_T, REG_TYPE_C, // 新增:计数器 REG_TYPE_R // 新增:文件寄存器 } REG_TYPE; // 地址类型字段定义(MC协议规范) #define ADDR_TYPE_C 0x0300 // 计数器地址类型 #define ADDR_TYPE_R 0x0400 // 文件寄存器地址类型2. 修改设备树构建与命令构造逻辑
在CPLC_UpperPCDlg::LoadDeviceTree()中添加:
m_TreeDev.InsertItem(_T("Counter (C0-C255)"), hCPU, (HTREEITEM)REG_TYPE_C); m_TreeDev.InsertItem(_T("R File Reg (R0-R32767)"), hCPU, (HTREEITEM)REG_TYPE_R);在CMcProtocol::BuildReadCommand()的switch(type)中补充:
case REG_TYPE_C: cmd = 0x0300; break; // 计数器读命令码 case REG_TYPE_R: cmd = 0x0400; break; // R寄存器读命令码并同步修改地址类型字段赋值(pBuf[9]/pBuf[10])为对应ADDR_TYPE_C或ADDR_TYPE_R。
血泪经验:R寄存器地址范围大(0–32767),BCD转换需支持5位数字,
DecimalToBCD()函数需升级为支持unsigned int参数,原unsigned short会溢出。
5.2 第二步:增加批量写入功能(一次发送多地址写命令)
原工程仅支持单寄存器双击写入,效率低下。批量写入需构造MC协议“块写”命令(命令码0x0002),其数据格式为:[地址类型][起始地址][数据长度][数据1][数据2]...。关键改造点:
1. UI层新增“批量写入”对话框
新建对话框资源IDD_BATCH_WRITE,含CEdit m_editAddr(起始地址)、CEdit m_editCount(数量)、CListCtrl m_listData(数据列表),在主对话框右键菜单中添加“批量写入”项。
2. 协议层实现块写命令构造
在McProtocol.cpp中新增函数:
bool CMcProtocol::BuildBlockWriteCommand(unsigned char* pBuf, int nBufLen, REG_TYPE type, int nStartAddr, int nCount, const WORD* pData) { if (nBufLen < 16 + nCount * 2) return false; // 帧头+命令码(0x0002)+站号+等待时间+数据长度(固定10字节头+2*nCount数据) pBuf[0] = 0x54; pBuf[1] = 0x00; pBuf[2] = 0x00; pBuf[3] = 0x02; // 块写命令码 pBuf[4] = 0x00; // 站号 pBuf[5] = 0x00; pBuf[6] = 0x14; // 等待时间 int nHeaderLen = 10; // 地址类型+起始地址+数据长度字段共10字节 int nDataLen = nCount * 2; pBuf[7] = (nHeaderLen + nDataLen) >> 8; pBuf[8] = (nHeaderLen + nDataLen) & 0xFF; // 地址类型字段(复用REG_TYPE到MC地址类型的映射) unsigned short addrType = GetMcAddrType(type); pBuf[9] = (addrType >> 8) & 0xFF; pBuf[10] = addrType & 0xFF; // 起始地址(BCD) unsigned short bcdAddr = DecimalToBCD(nStartAddr); pBuf[11] = (bcdAddr >> 8) & 0xFF; pBuf[12] = bcdAddr & 0xFF; // 数据长度(BCD) unsigned short bcdCount = DecimalToBCD(nCount); pBuf[13] = (bcdCount >> 8) & 0xFF; pBuf[14] = bcdCount & 0xFF; // 写入数据(WORD数组,高位在前) for (int i = 0; i < nCount; i++) { pBuf[15 + i*2] = (pData[i] >> 8) & 0xFF; pBuf[15 + i*2 + 1] = pData[i] & 0xFF; } // CRC校验(从pBuf[0]到pBuf[14+nDataLen]) unsigned short crc = CalcCRC16(pBuf, 15 + nDataLen); pBuf[15 + nDataLen] = (crc >> 8) & 0xFF; pBuf[15 + nDataLen + 1] = crc & 0xFF; return true; }3. 调用链打通
在批量写入对话框“确定”按钮响应函数中,收集用户输入的数据,调用BuildBlockWriteCommand()生成帧,再通过CSeriaPort::SendData()发出。实测单次可写入128个D寄存器,耗时<150ms,比逐个写快8倍以上。
5.3 第三步:集成简单脚本引擎(支持.ini配置的自动化流程)
现场调试常需重复执行“读D0→判断是否=0→写D1=1→延时100ms→读T0状态”等流程。原工程无脚本能力,我们用最轻量方式实现:
1. 定义INI配置格式
新建auto_flow.ini,内容示例:
[Step1] Action=Read RegType=D StartAddr=0 Count=1 [Step2] Action=Compare Value=0 JumpIfTrue=Step4 [Step3] Action=Write RegType=D StartAddr=1 Value=1 [Step4] Action=Delay Ms=1002. 解析引擎核心逻辑
在CPLC_UpperPCApp.cpp中添加RunAutoFlow()函数,使用GetPrivateProfileString()逐行读取INI,用std::map<CString, int>缓存寄存器值,Sleep()实现延时。关键点在于Compare动作的跳转逻辑——用goto语句实现标签跳转(VC6.0支持),避免复杂状态机。
3. 启动入口
在主对话框添加“自动流程”按钮,点击后弹出文件选择对话框定位auto_flow.ini,调用RunAutoFlow()。此方案无需引入Lua或Python解释器,编译后体积不变,却让工具具备产线一键自检能力。
从那以后我每次接到新PLC调试需求,第一件事就是把这份VC6.0源码拉出来,在McProtocol.cpp里补上客户指定的特殊寄存器类型,再用INI脚本把标准检查流程固化下来。它不炫技,但稳定得像螺丝刀——拧得紧,不打滑,十年如一日。希望帮到你。
本文还有配套的精品资源,点击获取