☰
MFC实现Modbus-TCP温湿度采集的工业级实践
2026/10/4 7:21:37 网站建设 项目流程

1. 项目概述:这不是一个“写个界面+连个设备”的简单活儿

MFC + Modbus-TCP 实现温湿度传感器采集,听起来像教科书里的一行课后习题——但真把它跑通、跑稳、跑进产线用,我前后踩了三轮坑,重写了四版通信模块,才敢把这套东西交到现场工程师手里。它不是单纯调个CWinThread开个线程、拖个CListCtrl显示两行数字就完事的事。核心在于:MFC 的消息循环与 Windows 网络 I/O 模型天然存在张力,而 Modbus-TCP 又是个对超时、重试、帧完整性极度敏感的工业协议。你用CAsyncSocket做异步收发?容易丢包;用WSAEventSelect?MFC 消息泵不认这个;直接send()/recv()阻塞在主线程?界面秒变灰色,用户点十次按钮只响应最后一次。更别说 DHT11 这类单总线传感器根本不能走 Modbus-TCP——标题里写的“温湿度传感器”,实际工程中几乎全是 SHT30、SHT85、BME280 这类带 I²C/SPI 接口、再通过 Modbus-TCP 网关(比如某国产 DTU 或西门子 SIMATIC IOT2000)桥接上来的以太网设备。所以第一步必须厘清:你面对的不是传感器本体,而是一个运行在 192.168.1.100:502 上、支持功能码 03(读保持寄存器)的 Modbus-TCP 从站设备,它内部已把原始 ADC 值换算成 0.01℃/0.01%RH 格式存入寄存器 40001~40002。MFC 在这里干的活,是当好一个“协议翻译官+界面调度员+异常守门人”——把二进制报文解析成 float,把用户点击“刷新”变成标准 ADU 帧,把网络断开、从站无响应、CRC 校验失败这些工业现场天天见的烂事,转化成弹窗提示或状态栏变色。适合谁?不是刚学完《VC++ 编程入门》的学生,而是手头有 FX5U PLC 要对接、正被客户催着交数采上位机的自动化工程师;是产线 QA 需要每小时导出 CSV 报表、但又不想装 LabVIEW 的产线技术员;是想用自己熟悉的 C++ 和 MFC 快速搭个监控看板、拒绝 Electron 那套 JS 堆栈的嵌入式老手。它解决的从来不是“能不能连”,而是“连得稳不稳、断了能不能自恢复、数据错不错、界面卡不卡”这四个直击生产痛点的问题。

2. 整体架构设计:为什么必须绕开 MFC 默认的消息泵做网络层?

2.1 传统思路的致命缺陷:主线程阻塞 vs 工业实时性

很多初学者会直接在OnBnClickedBtnRefresh()里写:

// ❌ 危险示范:绝对禁止! SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); connect(sock, (sockaddr*)&addr, sizeof(addr)); send(sock, modbus_frame, len, 0); recv(sock, buf, sizeof(buf), 0); // 主线程卡死在这里!

这在实验室点一次可能成功,但放到车间:PLC 网络偶尔抖动 200ms,你的界面就冻结 200ms;若从站掉电,recv()会等满系统默认超时(通常 60 秒),整个程序假死一分多钟。MFC 的CWinApp::Run()消息循环本质是GetMessage()→TranslateMessage()→DispatchMessage()的死循环,它只处理 Windows 消息(WM_PAINT、WM_COMMAND),对 SOCKET 事件完全无感。你用CAsyncSocket?它底层用WSAAsyncSelect()把网络事件转成WM_SOCKET消息,看似能进消息泵——但问题来了:CAsyncSocket的OnConnect()、OnReceive()回调是在非 UI 线程触发的,而 MFC 控件(CListCtrl、CEdit)只能由创建它的线程(即 UI 线程)安全访问。你直接在OnReceive()里m_listCtrl.InsertItem(),十次有八次崩在AfxAssertValidObject断言上。这是 MFC 与 Winsock 二十年来最经典的冲突点,不是 bug,是设计哲学差异:MFC 为 GUI 而生,Winsock 为网络而生。

2.2 我们采用的方案:Worker Thread + PostMessage 桥接

最终落地的架构是三层解耦:

  • 网络层(Worker Thread):独立CWinThread承载,使用select()+ 非阻塞 socket,严格控制超时(读超时设为 1500ms,写超时 800ms),实现“发帧→等响应→超时重试(最多2次)→解析→打包结果”闭环;
  • 通信层(PostMessage 桥梁):Worker Thread 解析出float temp = 25.3f, float humi = 62.7f后,调用PostMessage(m_hWnd, WM_MODBUS_DATA, (WPARAM)&data, 0),把数据地址发给主窗口;
  • UI 层(MFC 控件更新):主窗口在ON_MESSAGE(WM_MODBUS_DATA)处理函数中,安全地更新CStatic文本、CProgressCtrl进度条(湿度可视化)、CListCtrl历史记录。

提示:PostMessage是关键。它把数据传递行为变成“投递消息”,UI 线程在下一个消息循环中自然处理,彻底规避跨线程控件访问风险。别用SendMessage——它会同步等待,Worker Thread 卡在 UI 线程里,等于又回到阻塞原点。

2.3 为什么不用第三方库?比如 libmodbus 或 QModbus?

libmodbus 确实成熟,但引入它意味着:

  • 需要额外编译.lib,VS2010/2015/2017 工程配置易出错;
  • 它的modbus_tcp_new()返回modbus_t*,和 MFC 的CWnd生命周期管理不兼容;
  • 错误码ECONNREFUSED、ETIMEDOUT需要映射成 MFCAfxMessageBox可读的中文提示,中间层代码量不比手写少。

而手写 Modbus-TCP ADU 帧,其实就 12 字节固定结构:

[事务标识符 2B] [协议标识符 2B] [长度 2B] [单元标识符 1B] [功能码 1B] [起始地址 2B] [寄存器数量 2B]

例如读 40001~40002(2个寄存器):

00 01 00 00 00 06 01 03 00 00 00 02 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ TS PI Len UID FC SA QR

自己拼包,可控性强,调试时抓包 Wireshark 一眼比对,比查第三方库文档快得多。QModbus 更是 Qt 生态专属,硬塞进 MFC 工程等于给自己挖坑。

2.4 关键决策:选择 CWinThread 而非 _beginthreadex

_beginthreadex创建的纯 C 运行时线程,无法使用 MFC 的AfxGetMainWnd()、AfxMessageBox()等,错误提示只能写日志文件,用户根本看不到。CWinThread继承自CObject,自带m_bAutoDelete、m_pThreadParams,且AfxBeginThread()创建后,线程内可安全调用AfxGetApp()->m_pMainWnd获取主窗口句柄——这是实现PostMessage桥接的前提。我们定义线程参数结构体:

struct ModbusThreadParam { HWND hWnd; // 主窗口句柄,用于 PostMessage CString ip; // 192.168.1.100 UINT port; // 502 WORD startAddr; // 40001 → 0x0000 WORD regCount; // 2 };

启动线程:

ModbusThreadParam* pParam = new ModbusThreadParam{m_hWnd, _T("192.168.1.100"), 502, 0, 2}; AfxBeginThread(ModbusWorkerThread, pParam, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED, NULL);

3. 核心细节解析:Modbus-TCP 帧构造、解析与 MFC 控件联动

3.1 Modbus-TCP 请求帧的手工构造逻辑

Modbus-TCP 不需要校验码(CRC),但必须严格遵循 ADU(Application Data Unit)格式。重点在于:

  • 事务标识符(Transaction ID):必须每次请求递增,用于匹配响应帧。我们用static WORD s_transId = 0; s_transId++;全局静态变量维护,避免多线程竞争(Worker Thread 是单例,无需锁);
  • 协议标识符(Protocol ID):固定0x0000,表示 Modbus 协议;
  • 长度字段(Length):表示后续字节数(单元标识符 + 功能码 + 数据),此处为0x0006(1B UID + 1B FC + 2B SA + 2B QR = 6B);
  • 单元标识符(Unit ID):Modbus-RTU 中的 slave ID,TCP 中常设为0x01,部分网关要求0x00,需查设备手册;
  • 功能码(Function Code):0x03读保持寄存器;
  • 起始地址(Starting Address):寄存器地址减 1。40001 →0x0000,40002 →0x0001,所以读 40001~40002,SA =0x0000;
  • 寄存器数量(Quantity of Registers):0x0002。

构造代码(关键片段):

BYTE frame[12]; frame[0] = HIBYTE(s_transId); // 事务标识符高字节 frame[1] = LOBYTE(s_transId); // 事务标识符低字节 frame[2] = 0x00; frame[3] = 0x00; // 协议标识符 frame[4] = 0x00; frame[5] = 0x06; // 长度:6字节 frame[6] = 0x01; // 单元标识符 frame[7] = 0x03; // 功能码:读保持寄存器 frame[8] = HIBYTE(startAddr); // 起始地址高字节 frame[9] = LOBYTE(startAddr); // 起始地址低字节 frame[10] = HIBYTE(regCount); // 寄存器数量高字节 frame[11] = LOBYTE(regCount); // 寄存器数量低字节

注意:HIBYTE/LOBYTE是 Windows SDK 宏,#include <windows.h>即可。别用>>8 & 0xFF手动移位,易出错。

3.2 响应帧解析:如何从 15 字节中精准提取温度值?

正常响应帧结构:

[事务标识符 2B] [协议标识符 2B] [长度 2B] [单元标识符 1B] [功能码 1B] [字节数 1B] [寄存器值 N×2B]

例如返回 25.3℃(0x00FA = 250 → 25.0℃?不对!),这里有个大坑:Modbus 寄存器存的是整数倍,需按设备手册比例换算。SHT30 网关常见设置:

  • 温度寄存器 40001:值 × 0.01 = ℃,即 2530 → 25.30℃;
  • 湿度寄存器 40002:值 × 0.01 = %RH,即 6270 → 62.70%RH。

所以响应帧后 4 字节(2个寄存器 × 2字节):

... [02] [09 FA] [18 7E] ↑↑ ↑↑ T=2530 H=6270

解析代码:

// 响应帧 buf[0]~buf[14],共15字节 WORD tempRaw = MAKEWORD(buf[9], buf[8]); // buf[8]=0xFA, buf[9]=0x09 → 0x09FA = 2554 WORD humiRaw = MAKEWORD(buf[11], buf[10]); // buf[10]=0x7E, buf[11]=0x18 → 0x187E = 6270 float temperature = tempRaw * 0.01f; float humidity = humiRaw * 0.01f;

MAKEWORD(low, high)是关键,它把高低字节正确组合成WORD。千万别写成buf[8] | (buf[9]<<8)——字节序搞反,数据全错。

3.3 MFC 控件联动:让 CListCtrl 显示带时间戳的历史记录

CListCtrl是显示历史数据的首选,但默认不支持自动滚动到底部、不支持列宽自适应、不支持数值右对齐。我们封装一个CModbusListCtrl类继承CListCtrl,重写InsertItem:

int CModbusListCtrl::InsertData(float temp, float humi) { SYSTEMTIME st; GetLocalTime(&st); CString timeStr; timeStr.Format(_T("%04d-%02d-%02d %02d:%02d:%02d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); int index = InsertItem(GetItemCount(), timeStr); // 第一列:时间 SetItemText(index, 1, CString().Format(_T("%.2f"), temp)); // 第二列:温度 SetItemText(index, 2, CString().Format(_T("%.2f"), humi)); // 第三列:湿度 // 自动滚动到底部 if (index >= 0) { EnsureVisible(index, FALSE); } return index; }

在主窗口OnModbusData()中调用:

void CMainFrame::OnModbusData(WPARAM wParam, LPARAM lParam) { ModbusData* pData = (ModbusData*)wParam; m_listHistory.InsertData(pData->temperature, pData->humidity); m_staticTemp.SetWindowText(CString().Format(_T("%.1f℃"), pData->temperature)); m_staticHumi.SetWindowText(CString().Format(_T("%.1f%%RH"), pData->humidity)); }

实操心得:EnsureVisible(index, FALSE)的FALSE参数很重要,它表示“不滚动到顶部”,只确保可见即可,避免频繁刷新时列表疯狂跳动。另外,CListCtrl列宽必须手动设置,否则第一列时间戳会挤占所有空间。在OnInitDialog()或OnCreate()中加:

m_listHistory.SetColumnWidth(0, 150); // 时间列 m_listHistory.SetColumnWidth(1, 80); // 温度列 m_listHistory.SetColumnWidth(2, 80); // 湿度列

3.4 状态可视化:用 CProgressCtrl 直观显示湿度百分比

CProgressCtrl默认最大值 100,最小值 0,完美匹配湿度 0~100%RH。但要注意:

  • 必须在对话框资源中勾选 “Smooth” 属性,否则进度条是阶梯状;
  • 更新时用SetPos((int)humidity),而非SetRange(0, 100)——后者会重置当前值;
  • 若湿度超限(>100%),SetPos(100)即可,视觉上顶格显示,比弹窗警告更直观。

在OnModbusData()中追加:

m_progressHumi.SetPos((int)pData->humidity); // 颜色随湿度变化:干燥(<30%)蓝色,舒适(30~70%)绿色,潮湿(>70%)橙色 if (pData->humidity < 30.0f) { m_progressHumi.SendMessage(PBM_SETBARCOLOR, 0, RGB(64, 153, 255)); } else if (pData->humidity <= 70.0f) { m_progressHumi.SendMessage(PBM_SETBARCOLOR, 0, RGB(0, 192, 0)); } else { m_progressHumi.SendMessage(PBM_SETBARCOLOR, 0, RGB(255, 128, 0)); }

PBM_SETBARCOLOR是 Windows 7+ 支持的私有消息,需在OnInitDialog()中先m_progressHumi.ModifyStyle(0, PBS_SMOOTH);启用平滑模式。

4. 实操过程详解:从零搭建可运行工程的完整步骤

4.1 环境准备:VS2010/2015/2017 工程配置要点

我们以 VS2015 为例(兼容性最好),新建 MFC Application:

  • 应用程序类型:基于对话框(Dialog based),不要选“单文档”——对话框更轻量,适合监控类工具;
  • 高级功能:勾选“ActiveX 控件”(后续可能加 WebBrowser 查看趋势图)、“Windows Sockets”(关键!自动添加#include <winsock2.h>和ws2_32.lib);
  • 其他选项:取消“使用 Unicode 库”(若需兼容老旧设备,用多字节字符集更稳妥)。

注意:#include <winsock2.h>必须在#include <windows.h>之前,否则会因头文件顺序导致SOCKET重定义错误。这是 VS 里最经典的头文件陷阱。

4.2 添加网络线程:CWinThread 的完整实现

在MainFrm.cpp(或Dlg.cpp)中添加线程函数:

UINT __cdecl ModbusWorkerThread(LPVOID pParam) { ModbusThreadParam* p = (ModbusThreadParam*)pParam; if (!p) return 1; // 初始化 Winsock WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { PostMessage(p->hWnd, WM_MODBUS_ERROR, 0, (LPARAM)_T("Winsock初始化失败")); delete p; return 1; } SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock == INVALID_SOCKET) { PostMessage(p->hWnd, WM_MODBUS_ERROR, 0, (LPARAM)_T("Socket创建失败")); WSACleanup(); delete p; return 1; } // 设置非阻塞 u_long nonBlocking = 1; ioctlsocket(sock, FIONBIO, &nonBlocking); sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(p->port); addr.sin_addr.s_addr = inet_addr(CT2A(p->ip)); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { if (WSAGetLastError() != WSAEWOULDBLOCK) { PostMessage(p->hWnd, WM_MODBUS_ERROR, 0, (LPARAM)_T("连接服务器失败")); closesocket(sock); WSACleanup(); delete p; return 1; } } // 发送请求帧 BYTE frame[12]; // ... 构造 frame 代码(见 3.1 节)... send(sock, (char*)frame, sizeof(frame), 0); // select 等待响应 fd_set readfds; timeval timeout = {1, 500000}; // 1.5秒超时 FD_ZERO(&readfds); FD_SET(sock, &readfds); int ret = select(0, &readfds, NULL, NULL, &timeout); if (ret == 0) { PostMessage(p->hWnd, WM_MODBUS_TIMEOUT, 0, 0); closesocket(sock); WSACleanup(); delete p; return 1; } else if (ret == SOCKET_ERROR) { PostMessage(p->hWnd, WM_MODBUS_ERROR, 0, (LPARAM)_T("网络I/O错误")); closesocket(sock); WSACleanup(); delete p; return 1; } // recv 响应 BYTE buf[256]; int recvLen = recv(sock, (char*)buf, sizeof(buf)-1, 0); if (recvLen <= 0) { PostMessage(p->hWnd, WM_MODBUS_ERROR, 0, (LPARAM)_T("接收数据失败")); closesocket(sock); WSACleanup(); delete p; return 1; } buf[recvLen] = '\0'; // 解析数据(见 3.2 节) ModbusData data; data.temperature = ...; data.humidity = ...; PostMessage(p->hWnd, WM_MODBUS_DATA, (WPARAM)&data, 0); closesocket(sock); WSACleanup(); delete p; return 0; }

实操心得:select()的第一个参数在 Windows 下必须为 0,和 Linux 不同;timeval的tv_usec是微秒,500000= 0.5 秒,加上tv_sec=1,总超时 1.5 秒,这是工业现场经验值——太短(<1s)易误判,太长(>3s)影响刷新频率。

4.3 UI 布局与控件关联:对话框资源编辑技巧

在IDD_MAIN_DIALOG中拖入:

  • CStatic:IDIDC_STATIC_TEMP,Caption “--℃”,字体加粗,字号 16;
  • CStatic:IDIDC_STATIC_HUMI,Caption “--%RH”,同上;
  • CProgressCtrl:IDIDC_PROGRESS_HUMI,属性勾选 “Smooth”;
  • CListCtrl:IDIDC_LIST_HISTORY,View 属性设为 “Report”,插入三列(时间、温度、湿度);
  • CButton:IDIDC_BTN_REFRESH,Caption “手动刷新”;
  • CButton:IDIDC_BTN_AUTO,Caption “自动采集(5s)”,用SetCheck(BST_CHECKED)默认开启。

关联控件变量:

  • CStatic m_staticTemp;
  • CStatic m_staticHumi;
  • CProgressCtrl m_progressHumi;
  • CListCtrl m_listHistory;
  • CButton m_btnAuto;

注意:CListCtrl的InsertColumn()必须在OnInitDialog()中执行,否则列标题不显示:

m_listHistory.InsertColumn(0, _T("采集时间"), LVCFMT_LEFT, 150); m_listHistory.InsertColumn(1, _T("温度(℃)"), LVCFMT_RIGHT, 80); m_listHistory.InsertColumn(2, _T("湿度(%RH)"), LVCFMT_RIGHT, 80);

4.4 消息映射与事件处理:WM_MODBUS_DATA 的完整链路

在MainFrm.h(或Dlg.h)中定义消息:

#define WM_MODBUS_DATA (WM_USER + 101) #define WM_MODBUS_ERROR (WM_USER + 102) #define WM_MODBUS_TIMEOUT (WM_USER + 103)

在MainFrm.cpp的消息映射中添加:

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) // ... 其他映射 ON_MESSAGE(WM_MODBUS_DATA, &CMainFrame::OnModbusData) ON_MESSAGE(WM_MODBUS_ERROR, &CMainFrame::OnModbusError) ON_MESSAGE(WM_MODBUS_TIMEOUT, &CMainFrame::OnModbusTimeout) END_MESSAGE_MAP()

OnModbusData()实现见 3.3 节;OnModbusError()和OnModbusTimeout()用AfxMessageBox弹窗,并记录到CStatusBar:

void CMainFrame::OnModbusError(WPARAM wParam, LPARAM lParam) { CString msg = (LPCTSTR)lParam; AfxMessageBox(msg); m_wndStatusBar.SetPaneText(1, msg); // 状态栏第二格显示错误 }

实操心得:状态栏SetPaneText()比弹窗更友好——用户不会因频繁错误中断操作,但又能实时看到问题。我们把状态栏分三格:左格显示“就绪”,中格显示错误,右格显示 IP 地址,形成完整信息闭环。

4.5 自动采集定时器:用 SetTimer 实现 5 秒轮询

在OnInitDialog()中启动定时器:

SetTimer(1, 5000, NULL); // ID=1, 间隔=5000ms

在OnTimer()中触发采集:

void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1 && m_btnAuto.GetCheck()) { // 启动 Worker Thread ModbusThreadParam* pParam = new ModbusThreadParam{ m_hWnd, m_strIP, m_nPort, 0, 2 }; AfxBeginThread(ModbusWorkerThread, pParam, THREAD_PRIORITY_NORMAL, 0, 0, NULL); } CDialogEx::OnTimer(nIDEvent); }

注意:SetTimer的nIDEvent必须唯一,多个定时器需不同 ID;m_btnAuto.GetCheck()是复选框状态,用户可随时关闭自动采集。

5. 常见问题与排查技巧实录:那些让工程师熬夜的坑

5.1 网络层典型问题速查表

问题现象可能原因排查命令/工具解决方案
connect()返回WSAECONNREFUSED从站未运行、防火墙拦截、端口非 502telnet 192.168.1.100 502检查网关设备电源、网线、IP 配置;关闭 Windows 防火墙或添加入站规则
select()返回 0(超时)网络延迟高、从站响应慢、IP 地址错误ping 192.168.1.100,Wireshark抓包增大timeval超时值至 2.5 秒;确认设备手册中响应时间规格
recv()返回WSAEWOULDBLOCK非阻塞 socket 无数据可读Wireshark观察是否有响应帧发出此为正常现象,select()已保证有数据才recv,此错误不应出现,说明select()逻辑有误
recv()返回值 < 预期长度(如只收到 8 字节)TCP 粘包、从站发送不完整Wireshark查看 TCP Stream在recv()后加循环,直到收满expectedLen字节,或检查从站固件版本

提示:Wireshark 过滤 Modbus-TCP 流量用tcp.port==502,非常高效。看到请求帧发出但无响应帧,基本锁定网络或从站问题;看到响应帧但recv()收不全,就是粘包处理缺失。

5.2 MFC UI 层高频故障与修复

  • 问题:CListCtrl插入数据后不显示,或列标题空白
    原因:未调用InsertColumn(),或View属性未设为Report。
    修复:在OnInitDialog()中补全InsertColumn(),并在资源编辑器中右键CListCtrl→ Properties → View → Report。

  • 问题:PostMessage(WM_MODBUS_DATA)后OnModbusData()不触发
    原因:消息 ID 定义在.h文件中,但.cpp文件未#include该头文件;或ON_MESSAGE映射未添加。
    修复:检查#include "MainFrm.h"是否存在;用Class Wizard(VS2015 右键类名 → Add Message Handler)重新生成映射。

  • 问题:CProgressCtrl颜色不生效,始终蓝色
    原因:未启用PBS_SMOOTH样式,或PBM_SETBARCOLOR消息在 Windows XP 下不可用。
    修复:在OnInitDialog()中m_progressHumi.ModifyStyle(0, PBS_SMOOTH);若需 XP 兼容,改用CImageList绘制自定义进度条。

5.3 Modbus 协议层致命陷阱

  • 陷阱1:寄存器地址换算错误
    40001 是“逻辑地址”,Modbus 协议中叫“保持寄存器 0”,对应帧中0x0000。有人写成0x40001(0x9C41),导致send()发出乱码帧。
    修复:牢记公式帧中地址 = 逻辑地址 - 40001,40001→0x0000,40002→0x0001,40003→0x0002。

  • 陷阱2:字节序(Endianness)混淆
    SHT30 网关返回0x09FA表示 2554,但若误认为0xFA09,则得 64009,温度显示 640.09℃,明显荒谬。
    修复:用MAKEWORD(low, high),永远以buf[8]为低字节,buf[9]为高字节。

  • 陷阱3:功能码与寄存器类型不匹配
    对只读输入寄存器(3xxxx)误用功能码0x03(读保持寄存器),从站返回异常响应0x83(非法功能)。
    修复:查设备手册,确认寄存器类型:4xxxx 是保持寄存器(0x03),3xxxx 是输入寄存器(0x04)。

5.4 性能优化实战技巧

  • 技巧1:减少PostMessage频率
    自动采集设为 5 秒,但若用户快速点击 10 次“刷新”,会启动 10 个线程。在OnBnClickedBtnRefresh()中加锁:

    static BOOL s_isWorking = FALSE; if (s_isWorking) return; s_isWorking = TRUE; // ... 启动线程 ... // 在 OnModbusData() 结尾加 s_isWorking = FALSE;
  • 技巧2:CListCtrl大量数据卡顿
    历史记录超 1000 行时,InsertItem()变慢。启用虚拟模式:

    m_listHistory.ModifyStyle(LVS_TYPEMASK, LVS_REPORT | LVS_OWNERDATA);

    然后重写OnRetrieveVirtualItem(),只加载可视区域数据,内存占用直降 80%。

  • 技巧3:防止界面假死的终极方案
    在OnTimer()中不直接启动线程,而用PostMessage(WM_START_ACQUIRE),让 UI 线程在空闲时(OnIdle())再启动 Worker Thread,彻底释放 UI 线程压力。

6. 扩展可能性:从单点采集到小型 SCADA 系统

这套 MFC + Modbus-TCP 框架,绝不止于显示两个数字。我把它用在三个真实场景中:

  • 产线环境监控:接入 8 个 SHT30 网关(IP 192.168.1.101~108),每个网关挂 4 个传感器,CListCtrl改为CGridCtrl(开源 GridCtrl),行列冻结,支持按温度排序,导出 Excel 用libxl;
  • PLC 数据透传:FX5U 的 Modbus-TCP 从站功能开启后,MFC 上位机直接读取D1000(温度

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询