
简介本资源是一套基于Visual C 6.0开发的三菱PLC上位机编程软件完整源码工程面向工业自动化领域初/中级C开发者、工控软件学习者及MFC实践者解决PLC通信协议解析、上位机界面设计与调试功能集成等核心问题。压缩包共60个文件含14个头文件.h定义通信结构与类接口、13个源文件.cpp实现串口通信、LAD逻辑视图、IO点管理及文档视图架构辅以图标.ico、光标.cur、位图.bmp等资源文件支撑MFC界面另有可执行程序.exe、工程配置.dsp/.dsw及调试辅助文件.ncb/.opt整体仅731KB轻量但结构完整。已有1282人学习下载是难得的VC6.0MFC三菱PLC三者融合的实操范例。读者可直接编译运行深入理解RS-232/485通信协议封装、梯形图.lad与PLC程序.plc解析逻辑、多文档界面MDI下LADView/MULDoc等模块协同机制并基于源码快速定制监控、下载或仿真功能。1. 这不是PLC仿真器而是一套能真正发指令、读状态、存历史的上位机通信系统用VC6.0原生C直连三菱FX/Q系列PLC无中间件、无封装库、纯Win32 API串口/以太网底层协议栈你手头刚拿到一台FX3U PLC产线急着要加个本地监控面板——没有现成HMI不想买昂贵的组态软件授权更不打算等外包排期。这时候打开这个VC6.0源码包你会看到一个带工程树、实时数据表格、梯形图状态映射、报警日志导出的完整Windows桌面程序它不依赖OPC Server不调用任何第三方DLL比如GX Developer的COM接口而是直接用CreateFile打开COM口或socket连接PLC的MC协议端口按QnA兼容帧格式拼包、校验、超时重传。项目面向的是现场工程师和嵌入式开发者你需要改通信参数、加新寄存器地址、对接自有数据库或者把界面逻辑移植到新项目里。它不是教学Demo而是某自动化集成商在三个食品包装产线上实际跑过两年的稳定版本——所有串口缓冲区溢出处理、以太网心跳保活、断线自动重连逻辑都已实装连PLC掉电后上位机弹窗提示的“PLC未响应”文案都按产线要求本地化成了中文简体。如果你正卡在“怎么让VC6写的程序真正和PLC对话”这一步这份代码就是你该拆的第一份真实工程。2. 环境复现与编译配置VC6.0 Windows XP SP3虚拟机是唯一可靠组合现代系统需手动补全ATL/WTL头文件路径2.1 开发环境硬性约束为什么必须用VC6.0而非VS2022这个项目诞生于2005年前后其核心通信模块大量使用了VC6.0特有的#import msxml3.dll生成的智能指针、_variant_t类型隐式转换、以及ATL 3.0的CComPtr模板特化。最致命的是串口操作部分它直接调用SetupComm()设置缓冲区大小并用WaitCommEvent()配合OVERLAPPED结构实现异步等待——这些API在VC6.0的winbase.h中定义的结构体成员偏移与现代SDK存在ABI级差异。我曾尝试用VS2019加载.dsp工程并强制指定/Zc:wchar_t-和/MTd结果在CSerialPort::Open()调用CreateFile()返回INVALID_HANDLE_VALUE后GetLastError()却始终返回0玄学错误。最终验证只有在Windows XP SP3虚拟机中安装原版VC6.0 SP6补丁 Platform SDK 2003才能100%编译通过且运行稳定。现代Windows 10/11用户若坚持本地开发建议用VMware Workstation创建XP SP3虚拟机分配2GB内存、40GB磁盘安装后先打全SP3补丁再装VC6.0。2.2 工程目录结构与关键文件定位解压后的目录结构极简符合工业现场项目特征VC6_PLC_UpperPC_v1.0/ ├── PLCUpperPC.dsw # 工作区文件 ├── PLCUpperPC.dsp # 主工程文件MFC EXE ├── Source/ │ ├── MainFrm.cpp # 主框架窗口含菜单栏/工具栏初始化 │ ├── PLCUpperPCView.cpp # 核心视图类负责刷新数据显示、响应按钮事件 │ ├── CommManager.cpp # 通信管理器串口/以太网双模切换、协议帧组装/解析 │ ├── MCProtocol.cpp # 三菱MC协议实现QnA兼容模式含读D寄存器、写Y输出、批量读X输入 │ ├── DataLogger.cpp # 历史数据记录器CSV格式写入支持按小时分卷 │ └── AlarmHandler.cpp # 报警处理阈值比较、声光提示、日志归档 ├── Resource/ │ ├── PLCUpperPC.rc # 资源脚本含对话框、图标、字符串表 │ └── res/ │ ├── mainfrm.bmp # 工具栏位图 │ └── icon1.ico # 程序图标 └── Docs/ └── MC_Protocol_Ver2.pdf # 三菱官方MC协议文档QnA帧格式说明提示MCProtocol.cpp是整个项目的协议心脏所有与PLC交互的二进制帧都在此生成。不要试图用Wireshark抓包后反推——它已内置完整的CRC16校验Modbus风格、帧头0x54 0x00、命令码编码规则甚至处理了FX系列特有的“块读取地址偏移修正”。2.3 编译前必改的三处配置项串口参数硬编码打开CommManager.cpp定位CCommManager::InitSerialPort()函数修改以下行m_strPortName _T(COM3); // 改为你的PLC实际连接串口号 m_dwBaudRate 9600; // FX3U默认9600Q系列常用115200 m_nParity NOPARITY; // 校验位NOPARITY/ODDPARITY/EVENPARITY注意PLC侧需在参数设置中同步配置相同波特率、数据位7或8、停止位1或2、校验方式否则ReadFile()会持续返回0字节。以太网PLC IP地址在MCProtocol.cpp中搜索m_strIPAddr修改为PLC的IPm_strIPAddr _T(192.168.1.10); // FX3U需配以太网模块Q系列直接支持 m_nPort 6000; // 三菱默认MC协议端口资源路径修正DataLogger.cpp中日志路径写死为C:\\PLCLog\\若目标机器无此目录需提前创建或修改CDataLogger::OpenLogFile()中的路径字符串。3. 核心通信模块拆解MC协议帧构造与应答解析从发送读D100指令到获取4字节整数的完整链路3.1 MC协议基础为什么不用ModbusQnA帧结构详解三菱PLC原生支持两种上位机协议Modbus ASCII/RTU通用但效率低和专有的MC协议高速、支持批量读写、可访问内部继电器。本项目采用MC协议的QnA兼容模式因其帧结构简洁、错误率低且被FX/Q系列全面支持。一个典型读D寄存器请求帧如下十六进制54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 固定帧头16字节 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0......实际有效部分在帧尾省略了填充10 00 00 00 // 命令码0x0000 批量读取 00 00 00 00 // 子命令0x0000 无 00 00 00 00 // 网络号/PLC号/目标模块号FX系列填0 00 00 // 请求数据长度字节数 00 00 // 响应数据长度字节数请求时填0 00 00 // CPU监视定时器ms填0表示不监视 00 00 // 设备代码D寄存器0x90 00 00 00 64 // 起始地址D100 0x00000064小端序 00 00 00 01 // 读取点数1个D寄存器4字节关键点设备代码0x90对应D寄存器0x92对应W寄存器0x9C对应Y输出继电器。地址必须转为32位小端序D100 →0x00000064→ 字节流64 00 00 00。3.2 发送读D100指令的完整C实现打开MCProtocol.cpp查看CMCProtocol::ReadDRegister()函数BOOL CMCProtocol::ReadDRegister(WORD wStartAddr, WORD wCount, DWORD* pdwData) { // 步骤1构造QnA请求帧简化版实际含完整64字节头 BYTE pSendBuf[256] {0}; int nPos 0; // 帧头固定16字节0x54 0x00 14字节0 pSendBuf[nPos] 0x54; pSendBuf[nPos] 0x00; memset(pSendBuf nPos, 0, 14); nPos 14; // 命令码0x0000批量读 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 子命令0x0000 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 网络号/PLC号/目标模块号全0 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 请求数据长度此处为0由PLC计算 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 响应数据长度请求时填0 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // CPU监视定时器0禁用 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 设备代码D寄存器0x90 pSendBuf[nPos] 0x90; pSendBuf[nPos] 0x00; // 起始地址D100 0x00000064小端序 pSendBuf[nPos] 0x64; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 读取点数1个D寄存器4字节 pSendBuf[nPos] 0x01; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 步骤2计算CRC16校验从第2字节开始到末尾 WORD wCRC CalcCRC16(pSendBuf 1, nPos - 1); pSendBuf[nPos] LOBYTE(wCRC); // CRC低字节 pSendBuf[nPos] HIBYTE(wCRC); // CRC高字节 // 步骤3发送帧 DWORD dwWritten; if (!WriteFile(m_hComm, pSendBuf, nPos, dwWritten, NULL) || dwWritten ! nPos) { return FALSE; } // 步骤4等待响应超时1000ms BYTE pRecvBuf[256] {0}; DWORD dwRead; if (!ReadFile(m_hComm, pRecvBuf, sizeof(pRecvBuf), dwRead, NULL) || dwRead 20) { return FALSE; } // 步骤5校验响应帧CRCpRecvBuf[1]到倒数第2字节 WORD wRecvCRC MAKEWORD(pRecvBuf[dwRead-2], pRecvBuf[dwRead-1]); WORD wCalcCRC CalcCRC16(pRecvBuf 1, dwRead - 3); if (wRecvCRC ! wCalcCRC) { return FALSE; // CRC错误 } // 步骤6解析数据D寄存器数据从pRecvBuf[18]开始4字节小端序 *pdwData MAKELONG( MAKEWORD(pRecvBuf[18], pRecvBuf[19]), MAKEWORD(pRecvBuf[20], pRecvBuf[21]) ); return TRUE; }参数说明wStartAddr起始D地址如D100传入100函数内自动转0x00000064wCount读取点数本例为1pdwData输出缓冲区接收4字节整数值逻辑说明帧头严格遵循QnA格式不可增减字节CRC16使用标准多项式0x8005从第2字节索引1开始计算不包含帧头首字节0x54响应帧中数据区起始位置固定为第18字节索引17因前17字节为协议头状态码MAKELONG宏确保跨平台字节序正确避免直接memcpy导致大小端错乱。3.3 写Y0输出的控制逻辑实现写操作比读更需谨慎因涉及实际控制。CMCProtocol::WriteYOutput()函数核心如下BOOL CMCProtocol::WriteYOutput(BYTE bYAddr, BOOL bOn) { BYTE pSendBuf[256] {0}; int nPos 0; // 帧头同上 pSendBuf[nPos] 0x54; pSendBuf[nPos] 0x00; memset(pSendBuf nPos, 0, 14); nPos 14; // 命令码0x0001 批量写入 pSendBuf[nPos] 0x01; pSendBuf[nPos] 0x00; // 子命令0x0000 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 网络号等全0 for(int i0; i6; i) pSendBuf[nPos] 0x00; // 请求/响应长度写1点Y数据区2字节 pSendBuf[nPos] 0x02; pSendBuf[nPos] 0x00; // 请求2字节 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 响应0字节 // 监视定时器 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 设备代码Y继电器0x9C pSendBuf[nPos] 0x9C; pSendBuf[nPos] 0x00; // Y地址Y0 0x00000000 pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 写入点数1 pSendBuf[nPos] 0x01; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; pSendBuf[nPos] 0x00; // 数据0x0001ON0x0000OFF注意Y是16位寄存器单点占1位但协议要求写16位值 pSendBuf[nPos] bOn ? 0x01 : 0x00; pSendBuf[nPos] 0x00; // 高字节恒为0 // 计算并附加CRC WORD wCRC CalcCRC16(pSendBuf 1, nPos - 1); pSendBuf[nPos] LOBYTE(wCRC); pSendBuf[nPos] HIBYTE(wCRC); // 发送... return WriteFile(m_hComm, pSendBuf, nPos, dwWritten, NULL) (dwWritten nPos); }注意Y输出是位操作但MC协议要求以16位字为单位写入。bOn ? 0x01 : 0x00表示将Y0置位/复位其余Y1-Y15保持原状实际PLC会忽略高位。若需同时控制多个Y点需构造16位掩码值。4. 常见问题排查串口无响应、数据错乱、连接闪断——来自三个产线现场的血泪经验4.1 现象串口通信完全无响应ReadFile()始终返回0字节原因PLC侧未启用编程口通信或参数不匹配。FX系列PLC需在GX Works2中设置“PLC参数→PLC系统参数→I/O分配设置→编程口通信设置”勾选“允许上位机通信”并确认波特率、数据位、停止位、校验位与上位机代码完全一致。常见错误是PLC设为8N1而上位机代码写成7E2。解决用串口调试助手如XCOM发送十六进制54 00最小合法帧头若PLC返回54 00 00 00...则证明物理层连通否则检查PLC参数。4.2 现象读取D寄存器数据恒为0或随机大数原因地址转换错误或字节序处理失误。D100在代码中需转为0x00000064若误写为0x00000100D256则读取错误区域更隐蔽的是MAKELONG调用时参数顺序颠倒导致高低字节错位。解决在ReadDRegister()中添加日志打印pRecvBuf[18]到pRecvBuf[21]四个字节的十六进制值对照PLC当前D100值如D1001234则期望收到D2 04 00 00小端序验证是否为字节序问题。4.3 现象以太网连接成功但频繁断开WSAGetLastError()返回10054原因PLC侧MC协议端口被防火墙拦截或PLC网络参数中“允许外部访问”未启用。Q系列PLC需在GX Works2中设置“网络参数→以太网接口→MC协议设置→允许外部访问”。解决在PLC侧用GX Works2的“在线→诊断→网络诊断”查看MC协议连接状态在上位机用telnet 192.168.1.10 6000测试端口连通性若失败则检查PLC网络配置。4.4 现象报警日志CSV文件中文乱码Excel打开显示方块原因DataLogger.cpp中文件写入使用CStdioFile默认ANSI编码而Windows记事本/Excel默认用UTF-8或GBK解析。解决修改CDataLogger::WriteLogLine()在文件开头写入UTF-8 BOM0xEF 0xBB 0xBF并确保字符串用CT2A转换为UTF-8// 写BOM BYTE bom[3] {0xEF, 0xBB, 0xBF}; file.Write(bom, 3); // 写内容m_strLogLine为CString CT2A pszUTF8(m_strLogLine, CP_UTF8); file.WriteString(pszUTF8);4.5 现象界面刷新卡顿实时数据显示延迟超过2秒原因PLCUpperPCView.cpp中OnTimer()函数默认每500ms轮询一次但ReadDRegister()单次调用耗时约150ms串口超时叠加多寄存器读取导致阻塞。解决将轮询改为异步线程消息队列。新建CDataPollThread类在InitInstance()中启动用PostMessage()将读取结果发回主线程更新UI避免OnTimer()中直接调用阻塞IO。5. 二次开发实战新增Modbus RTU支持、对接MySQL历史库、定制化报警弹窗5.1 扩展Modbus RTU协议支持替代MC协议虽然项目主打MC协议但某些老旧设备仅支持Modbus。我们可在CommManager.cpp中新增CModbusRTUManager类复用现有串口句柄// 新增函数读取Modbus保持寄存器功能码0x03 BOOL CCommManager::ReadModbusHoldingRegister(BYTE nSlaveID, WORD wStartAddr, WORD wCount, WORD* pwData) { BYTE pSendBuf[256] {0}; int nPos 0; // Modbus RTU帧[slave][func][start_hi][start_lo][count_hi][count_lo][crc_lo][crc_hi] pSendBuf[nPos] nSlaveID; // 从站地址 pSendBuf[nPos] 0x03; // 功能码读保持寄存器 pSendBuf[nPos] HIBYTE(wStartAddr); pSendBuf[nPos] LOBYTE(wStartAddr); // 起始地址 pSendBuf[nPos] HIBYTE(wCount); pSendBuf[nPos] LOBYTE(wCount); // 寄存器数 // 计算CRC16Modbus专用多项式0xA001 WORD wCRC CalcModbusCRC16(pSendBuf, nPos); pSendBuf[nPos] LOBYTE(wCRC); pSendBuf[nPos] HIBYTE(wCRC); // 发送... if (!WriteFile(m_hComm, pSendBuf, nPos, dwWritten, NULL)) return FALSE; // 接收响应[slave][func][byte_count][data...][crc] BYTE pRecvBuf[256] {0}; if (!ReadFile(m_hComm, pRecvBuf, sizeof(pRecvBuf), dwRead, NULL) || dwRead 5) return FALSE; // 校验CRC if (CalcModbusCRC16(pRecvBuf, dwRead-2) ! MAKEWORD(pRecvBuf[dwRead-1], pRecvBuf[dwRead-2])) return FALSE; // 解析数据每个寄存器2字节小端序 for(int i0; iwCount; i) { pwData[i] MAKEWORD(pRecvBuf[32*i], pRecvBuf[42*i]); } return TRUE; }关键差异Modbus CRC使用多项式0xA001反向与MC协议的0x8005不同地址从0开始Modbus规范而MC协议D100对应地址100响应帧中数据区从第3字节开始pRecvBuf[3]为第一个字节。5.2 对接MySQL存储历史数据DataLogger.cpp原为本地CSV现需写入MySQL。引入MySQL C APImysql.h在CDataLogger::OpenLogFile()中初始化连接// 全局MySQL连接句柄 MYSQL* g_pMySql NULL; BOOL CDataLogger::InitMySQL() { g_pMySql mysql_init(NULL); if (!mysql_real_connect(g_pMySql, 127.0.0.1, plc_user, plc_pass, plc_db, 3306, NULL, 0)) { AfxMessageBox(_T(MySQL连接失败) CString(mysql_error(g_pMySql))); return FALSE; } mysql_query(g_pMySql, SET NAMES utf8mb4); // 防止中文乱码 return TRUE; } void CDataLogger::WriteToMySQL(DWORD dwD100, DWORD dwD101, time_t tTime) { CString strSQL; strSQL.Format(_T(INSERT INTO plc_history (d100, d101, collect_time) VALUES (%lu, %lu, %s)), dwD100, dwD101, _tcsftime(_T(%Y-%m-%d %H:%M:%S), tTime)); if (mysql_query(g_pMySql, CT2A(strSQL, CP_UTF8)) ! 0) { // 写入失败时降级到本地CSV WriteToCSV(dwD100, dwD101, tTime); } }提示编译时需链接libmysql.lib并将libmysql.dll放入程序目录。生产环境建议用连接池避免频繁创建销毁连接。5.3 定制化报警弹窗与声光提示原AlarmHandler.cpp仅弹出AfxMessageBox现需增强。在CAlarmHandler::TriggerAlarm()中void CAlarmHandler::TriggerAlarm(LPCTSTR lpszAlarmMsg, DWORD dwLevel) { // 步骤1播放WAV警报音资源ID为IDR_WAVE_ALARM PlaySound(MAKEINTRESOURCE(IDR_WAVE_ALARM), AfxGetResourceHandle(), SND_RESOURCE | SND_ASYNC); // 步骤2创建非模态报警窗口避免阻塞主界面 CAlarmDlg* pDlg new CAlarmDlg(); pDlg-Create(IDD_ALARM_DIALOG, this); pDlg-SetAlarmText(lpszAlarmMsg, dwLevel); // 根据dwLevel设置红/黄背景 pDlg-ShowWindow(SW_SHOW); // 步骤3触发硬件声光假设有USB IO模块 if (m_hIODevice ! INVALID_HANDLE_VALUE) { DWORD dwIOData (dwLevel ALARM_CRITICAL) ? 0xFF : 0x0F; // 高电平触发蜂鸣器红灯 WriteFile(m_hIODevice, dwIOData, 1, dwWritten, NULL); } }界面增强点IDD_ALARM_DIALOG对话框添加“确认”按钮和“静音1小时”按钮使用SetTimer()实现倒计时静音期间不再触发声音将报警记录写入alarm_log.dbSQLite数据库便于追溯。6. 从那以后我每次接手PLC上位机项目都强制走一遍“三查一测”流程查PLC通信参数文档、查上位机源码协议实现、查物理接线拓扑、测最小帧收发闭环。这份VC6.0源码最珍贵的不是它能跑起来而是它把工业协议落地的所有毛刺都摊开给你看——串口缓冲区溢出怎么清、以太网心跳包怎么设计、CRC校验怎么跨平台兼容、甚至PLC掉电时上位机如何优雅降级。它不教你高大上的架构模式只告诉你WriteFile()返回FALSE时该看GetLastError()还是重试三次。我把它当作工业软件的“汇编语言”来读没有封装没有抽象每一个字节都在你眼皮底下流动。后来做基于Qt的跨平台上位机时MC协议解析模块直接移植了这里的CalcCRC16()和帧结构体定义连注释都没改。希望帮到你。本文还有配套的精品资源点击获取