在工业视觉检测、自动化设备开发中,Windows 端上位机与西门子 S7-1200 PLC 进行数据交互是非常常见的需求。本文基于 MFC + Snap7 开源库 ,从零实现一套稳定、可直接上线的 PLC 通信程序,包含自动连接、循环读取、线程安全写入、异常重连等完整功能,解决实际开发中最容易遇到的界面卡死、Job pending、读写冲突、断线不重连等痛点。
一、方案简介
- 开发环境:Windows + MFC + C++
- PLC 型号:西门子 S7‑1200
- 通信库:Snap7(开源、轻量、跨平台、无驱动依赖)
- 核心功能
- 后台线程循环读取 PLC 数据
- 50ms 稳定通信周期,适配相机联动场景
- 线程安全读写,避免多线程冲突
- 连接断开自动 1s 重连
- 界面实时显示连接状态与数据
二、关键结构设计
为保证界面不卡顿,PLC 通信必须放在独立子线程 中执行;为避免读写同时操作导致 Job pending 错误,使用 CCriticalSection 临界区 做线程互斥;界面更新不在线程内直接操作控件,而是通过 自定义消息 异步刷新。
整体流程:
启动线程 → 尝试连接 PLC → 连接成功循环读取 → 异常断开 → 1s 后自动重连
三、核心代码实现
1. PLC 通信线程(自动连接 + 循环读取)
cpp
// PLC 数据读取线程(独立线程,不卡界面)
UINT ReadPLCThreadFunc(LPVOID lparam)
{
Cs7client_testDlg *pDlg = (Cs7client_testDlg *)lparam;
// 线程主循环
while(pDlg->m_bRunThread)
{
// 连接 PLC:IP、机架0、槽号1
int res = pDlg->client.ConnectTo("192.168.11.10", 0, 1);
pDlg->m_nConnectStatus = res;
pDlg->PostMessage(WM_USER_UPDATE_UI);
// 连接成功后进入数据读取循环
while(res == 0 && pDlg->m_bRunThread)
{
Sleep(50); // 通信周期 50ms
// 加锁:与写操作互斥
pDlg->m_csPLC.Lock();
{
// 读取 DB1.DBB2288 4字节整数
byte buf1[4] = {0};
int ret1 = pDlg->client.ReadArea(S7AreaDB, 1, 2288, 4, S7WLByte, buf1);
if(ret1 != 0) { res = -1; break; }
pDlg->m_val1 = buf1[3] | (buf1[2] << 8) | (buf1[1] << 16) | (buf1[0] << 24);
Sleep(5); // 间隔避免连续读写报错
// 读取 DB1.DBB2632 4字节整数
byte buf2[4] = {0};
int ret2 = pDlg->client.ReadArea(S7AreaDB, 1, 2632, 4, S7WLByte, buf2);
if(ret2 != 0) { res = -1; break; }
pDlg->m_val2 = buf2[3] | (buf2[2] << 8) | (buf2[1] << 16) | (buf2[0] << 24);
}
pDlg->m_csPLC.Unlock();
pDlg->PostMessage(WM_USER_UPDATE_UI);
}
// 断开并重连
pDlg->m_nConnectStatus = -1;
pDlg->PostMessage(WM_USER_UPDATE_UI);
pDlg->client.Disconnect();
if(pDlg->m_bRunThread)
Sleep(1000);
}
pDlg->client.Disconnect();
return 0;
}
2. 线程安全写入(按钮下发数据)
cpp
void Cs7client_testDlg::OnBnClickedButton1()
{
CString sValue;
m_ctrlEdit1.GetWindowText(sValue);
int iValue = atoi(sValue);
if(!client.Connected())
{
AfxMessageBox("未连接,请先连接!");
return;
}
// 加锁保证读写不同时执行
m_csPLC.Lock();
{
byte writeBuffer[4] = {0};
// 大端字节序转换
writeBuffer[0] = (iValue >> 24) & 0xFF;
writeBuffer[1] = (iValue >> 16) & 0xFF;
writeBuffer[2] = (iValue >> 8) & 0xFF;
writeBuffer[3] = iValue & 0xFF;
int result = client.WriteArea(S7AreaDB, 1, 2632, 4, S7WLByte, writeBuffer);
if(result != 0)
{
CString sMsg;
sMsg.Format("Error writing data:%s ", CliErrorText(result));
AfxMessageBox(sMsg);
}
}
m_csPLC.Unlock();
}
3. 界面异步更新(避免线程崩溃)
cpp
LRESULT Cs7client_testDlg::OnUpdateUI(WPARAM wParam, LPARAM lParam)
{
// 更新连接状态
if(m_nConnectStatus == 0)
m_ctrlStaticNet.SetWindowText("连接成功");
else
m_ctrlStaticNet.SetWindowText("连接失败");
// 更新数值显示
CString s;
s.Format("%d", m_val1);
m_ctrlStatic1.SetWindowText(s);
s.Format("%d", m_val2);
m_ctrlStatic3.SetWindowText(s);
return 0;
}
4. 单次读写测试函数
用于调试、校验地址是否正确:
cpp
void Cs7client_testDlg::OnBnClickedButton2()
{
TS7Client client;
if(client.ConnectTo("192.168.11.10", 0, 1) == 0)
{
AfxMessageBox("Connected to PLC !");
byte buffer[4];
int result = client.ReadArea(S7AreaDB, 1, 2320, 4, S7WLByte, buffer);
if(result == 0)
{
int iValue = buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24;
CString sMsg;
sMsg.Format("Read data:%d,%d,%d,%d %d",buffer[0],buffer[1],buffer[2],buffer[3],iValue);
AfxMessageBox(sMsg);
}
// 写入测试
int iValue = 2352;
byte writeBuffer[4];
writeBuffer[0] = (iValue >> 24) & 0xFF;
writeBuffer[1] = (iValue >> 16) & 0xFF;
writeBuffer[2] = (iValue >> 8) & 0xFF;
writeBuffer[3] = iValue & 0xFF;
result = client.WriteArea(S7AreaDB, 1, 2352, 4, S7WLByte, writeBuffer);
client.Disconnect();
}
}
四、重点问题与解决方案
-
界面卡死将通信逻辑放到子线程,不阻塞主线程。
-
Job pending 错误连续读写之间加 5ms 间隔,并使用临界区锁保证串行执行。
-
断线后不重连读取失败标记断开,外层循环 1s 自动重试。
-
线程操作控件崩溃使用 PostMessage 异步更新 UI,不在线程内操作控件。
-
S7 大端字节序高低字节重新组合,保证 int 数据正确。
五、总结
本文基于 Snap7 库实现了一套工业级稳定 的 S7‑1200 通信程序,代码结构清晰、注释完整、可直接移植到项目中使用。整套方案支持自动重连、线程安全、实时读写,非常适合视觉相机、自动化产线、数据采集等场景。
相比于西门子官方 OPC 或 S7‑NET,Snap7 体积小、部署简单、无版权问题,是 C++ 上位机开发的首选通信方案。

