简介:本资源是一套基于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.0+MFC+三菱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默认9600,Q系列常用115200 m_nParity = NOPARITY; // 校验位:NOPARITY/ODDPARITY/EVENPARITY注意:PLC侧需在参数设置中同步配置相同波特率、数据位(7或8)、停止位(1或2)、校验方式,否则
ReadFile()会持续返回0字节。以太网PLC IP地址:在
MCProtocol.cpp中搜索m_strIPAddr,修改为PLC的IP:m_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协议基础:为什么不用Modbus?QnA帧结构详解
三菱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:校验响应帧CRC(pRecvBuf[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,函数内自动转0x00000064)wCount:读取点数(本例为1)pdwData:输出缓冲区,接收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 i=0; i<6; 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; // 数据:0x0001=ON,0x0000=OFF(注意: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,若误写为0x00000100(D256)则读取错误区域;更隐蔽的是MAKELONG调用时参数顺序颠倒,导致高低字节错位。
解决:在ReadDRegister()中添加日志,打印pRecvBuf[18]到pRecvBuf[21]四个字节的十六进制值,对照PLC当前D100值(如D100=1234,则期望收到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 BOM(0xEF 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); // 寄存器数 // 计算CRC16(Modbus专用多项式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 i=0; i<wCount; i++) { pwData[i] = MAKEWORD(pRecvBuf[3+2*i], pRecvBuf[4+2*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 API(mysql.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()和帧结构体定义,连注释都没改。希望帮到你。
本文还有配套的精品资源,点击获取