
简介一套基于VB与VC混合编程的SCADA集群设备监控系统工程源码面向自动化、工控上位机及监控界面开发人员适合课程设计、毕业设计及实际项目二次开发参考。该工程完整演示了从现场数据采集、串口/网络通信、设备管理到可视化监控界面的实现流程并展示了如何用VB快速搭建友好界面、用VC处理底层数据与控制逻辑。压缩包共84个文件大小约1019KB主要包含VB窗体文件(frm)、VC源文件(cpp/h)、通信动态库(dll/ocx)、数据库(mdb)以及界面图片(gif/jpg)等类型覆盖前端界面、后台逻辑、通信封装和资源文件项目结构清晰便于归类学习。已有180人学习下载。通过分析源码和窗体设计可掌握多语言混合编程的工程组织方式理解实时数据刷新、报警提示、设备状态监测等关键功能的编码技巧为独立开发大型集群监控系统打下扎实基础。1. 从 scada.rar 说起一套 VB 采数据、VC 出画面的 SCADA 骨架如果你在工控圈子里混过一阵大概率见过这类叫 scada.rar 的压缩包里面躺着一份 VB 写的采集程序源代码、一份 VC 写的监控界面工程再加一堆乱码说明文档。SCADA 这个词听起来高大上说破了就是数据采集SCADA 里的 SCADA data加监控画面这两件事。标题里同时出现 VB 采集数据和 VC 监控界面恰好还原了十年前工控组态软件还没普及、大家喜欢自己攒一套小系统的时代。适合谁很适合刚接手老旧项目、要读别人源码的从业者也适合想把采集和界面分离设计的人。这套方案的核心价值不是代码量而是把「底层设备怎么读」和「屏幕怎么画」拆成两个进程中间用一块共享内存做接力——这个架构今天看依然不过时。2. VB 采集端怎么做串口协议、轮询周期与数据帧组织2.1 为什么用 VB 采集而不是 VC拿到这种老架构第一步别急着改代码先理解 VB 在里面的分工。常见做法是 VB 负责跟 PLC、仪表、采集模块打交道VC 负责画面。VB 的优势不在性能而在 MSComm 控件和 WinSock 控件让串口和网络通信的开发成本极低。你不需要懂 IOCP不需要管线程池拖一个控件、配一下波特率和校验位就能收发字节。这对 SCADA 数据采集这种低频次轮询场景来说完全够用。VC 虽然也能做串口但写起来要面对 CreateFile、ReadFile 和重叠 IO调试周期长不少。在「快速上线、稳定运行」的老项目里VB 是更务实的选择。2.2 与设备通信的最小可靠代码假设你面对的是一台支持 Modbus RTU 的温控表地址是 01要读保持寄存器 40001 到 40004。VB 里用 MSComm 控件通常的写法是Private Sub btnPoll_Click() Dim frame() As Byte ReDim frame(7) frame(0) H1 设备地址 frame(1) H3 功能码读保持寄存器 frame(2) H0 起始地址高字节 frame(3) H0 起始地址低字节 frame(4) H0 寄存器数量高字节 frame(5) H4 寄存器数量低字节 Dim crc As Long crc CalculateCRC16(frame, 6) 对前6字节算CRC frame(6) crc And HFF frame(7) (crc \ H100) And HFF MSComm1.Output frame End Sub这段代码的核心是拼一个标准 Modbus RTU 请求帧。很多人会直接把 CRC 算错位导致设备不回应所以我会把 CRC 计算单独放一个函数用广播地址 00 或者功能码 03 去验证。注意 frame 数组的长度必须和实际发送字节数一致VB 里如果 ReDim 了 8 字节最后两个填充 CRC顺序是先低字节后高字节。MSComm1.Output 赋值字节数组时控件会自动按设置的数据位和停止位打包发送。接收端要放在 MSComm1_OnComm 事件里并且设置 RThreshold 为期望的返回长度。Modbus RTU 返回帧长度是 3 数据字节数 2 个 CRC。读 4 个寄存器就是 38213 字节。代码思路Private Sub MSComm1_OnComm() Dim buf() As Byte Dim i As Integer Dim recvLen As Integer recvLen MSComm1.InBufferCount If recvLen 13 Then ReDim buf(recvLen - 1) buf MSComm1.Input 校验地址、功能码、字节数、CRC然后解析数据 If ValidateFrame(buf) Then UpdateTagValues(buf) 更新带时间戳的点表 End If End If End Sub一个关键参数是 RThreshold。如果设为 13缓冲区不满就不会触发事件但实际串口是流式到达的一次 OnComm 未必凑齐 13 字节所以通常我会把它设成 1然后在事件里判断 InBufferCount 是否大于等于期望长度。这里容易翻车如果设备响应超时VB 的 Input 会读到不完整帧需要加超时保护否则残留的数据会影响下一轮解析。我的习惯是在 OnComm 里先用 DoEvents 让控件收几十毫秒再一次性取走数据或者用辅助标志位把解析逻辑和数据接收拆开。2.3 轮询逻辑怎么组织才能不丢数据SCADA 采集端最忌讳的是「一竿子轮到底」。常见的错误是写一个死循环挨个发指令、挨个等回复遇到某个设备响应慢了后面所有点全部堵住。我的做法是把点位表拆成多个轮询队列每个队列对应一个串口或者一个设备种类用 Timer 控件做定时触发。比如温控表组每 500ms 轮询一次电表组每 2s 轮询一次这样既不会把一个串口的带宽占满也不会因为单一设备卡顿影响全局。轮询状态机可以用一个简单索引实现Private Sub tmrScan_Timer() pollIndex 指向当前正在轮询的点 curPoint pollTable(pollIndex) SendReadRequest curPoint.CmdFrame 记录发出时间用于超时判断 requestTime(pollIndex) GetTickCount() pollIndex (pollIndex 1) Mod UBound(pollTable) End Sub这里要盯住的参数是超时时间。串口通信的响应时间通常跟波特率有关9600 波特率下一个 8 字节请求帧传输需要约 10ms设备处理再加 20ms所以 200ms 超时是底线。如果设备是 19200 或更高可以缩到 100ms。但这个超时是「发出请求到收到首字节」的时间不是整个接收完成的时间容易搞混。我一般会在发送前清空接收缓冲区再在 OnComm 接收中单独判断帧是否完整避免超时误判。数据采集上来之后SCADA data 还要带质量戳。老项目中很多只存数值不存是否超时、是否通信失败导致上位机画面上出现一个冻结的假数据现场背锅。我会在点表里给每个采集点加一个布尔量 valid通信成功置 True超时置 FalseVC 端的监控界面直接读这个标志来决定是否显示灰色或闪烁这样仪表坏了你能第一时间看见而不是看一个永远不变的 25.0 度。3. VC 监控界面怎么搭图元、数据映射与双缓冲刷新3.1 画面组态别硬画先建一个点表映射VC 这边要做的不是把一个 Bitmap 贴上去而是把画面上的每个图元阀门、管道、温度表和 VB 采上来的数据点绑定。这个思想跟组态软件是一致的画面上没有数据点只有图元 ID。你拖一个矩形表示水箱它的填充高度由 tag_level 决定颜色由 tag_alarm 决定。VC 程序启动时先读一个配置文件把图元 ID 到数据点的对应关系装进内存。命名要讲究否则现场换表你会疯掉。我见过最乱的项目图元在代码里写死叫 Rect_Liquid数据点叫 fData12两者之间没有任何文档。下一次改点位VC 程序就得改代码重新编译。正确做法是给每个数据点起一个稳定的逻辑名比如 LI-101 代表 101 号液位这个逻辑名在 VB、VC、数据库、报表里统一使用。这样即使设备地址变了也只是 VB 那边改配置VC 这边的映射表不用动。3.2 实时刷新用定时器还是线程VC 监控界面最容易翻车的地方是多线程刷新 UI。新手喜欢在工作线程里直接调用 Invalidate结果界面闪烁、崩溃甚至死锁。MFC 里的控件和窗口类都不是线程安全的所有绘图操作必须回到主线程。常见做法是开一个 100ms 的定时器在 OnTimer 里从共享内存取最新数据更新图元状态后再 Invalidate。100ms 对大多数 PLC 监控场景足够对人眼来说也是平滑的。代码结构大致是这样void CMonitorView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_REFRESH) { RefreshTagData(); // 从共享内存读最新数据存到 m_tagArray UpdateShapeStates(m_tagArray); // 根据数据点值更新图元属性 Invalidate(FALSE); // 不擦背景直接重绘减少闪烁 } CView::OnTimer(nIDEvent); }注意 Invalidate(FALSE) 里的 FALSE 是关键参数。默认的 TRUE 会先擦除背景再重绘产生明显闪烁。整个画面重绘本来就不便宜擦白再画白视觉上像打闪。用 FALSE 让系统直接用新画面覆盖旧画面辅以双缓冲位图几乎感觉不到刷新痕迹。代价是如果图元之间有重叠旧的残影可能残留所以要用双缓冲把所有图元画到内存位图再一次 BitBlt 贴上屏幕。3.3 实时曲线和报警列表的最小实现曲线不一定非要引入第三方图表库。VC 里自己画曲线很简单维护一个环形缓冲区每个点存 1000 个历史值OnTimer 里把整条曲线画到一个 CBitmap 上然后 BitBlt。横坐标是时间刻度纵坐标按量程归一化。一个容易忽略的细节是曲线坐标系的零点在屏幕左上角但工程上习惯把最小值放在底部所以绘制时要翻转 Y 轴用 rect.height - value / range * height 换算。报警列表更直接用一个 CListCtrl当质量戳从有效变无效或者模拟量超过高低限插一行记录颜色区分红色是紧急报警黄色是提醒。判断报警变化的逻辑要放在数据更新时而不是绘制时否则画面卡顿的瞬间报警会被漏掉。4. VB 和 VC 之间怎么通信共享内存、命名管道与数据帧格式4.1 三种通信方式的取舍VB 采完数据要交给 VC 显示这个数据通道是整套 SCADA 的主动脉。老项目里最常见的方案是三种共享内存、命名管道、UDP 套接字。共享内存的速度最快数据不用拷贝两份但需要处理同步互斥和进程生命周期问题。命名管道可靠缺点是阻塞模式下容易互相等。UDP 简单方便但会丢包而且本机通信回环也是要走协议栈的实时性不如共享内存。一个搞过多年的老工程师跟我说过一句话本机两个进程传数据只要不是跨机器共享内存永远是第一选择UDP 只有在要同时往远程转发数据时才加一个镜像。4.2 共享内存的核心实现VC 端创建共享内存HANDLE hMap CreateFileMapping( INVALID_HANDLE_VALUE, // 用系统页文件 NULL, PAGE_READWRITE, 0, sizeof(TagBuffer), LGlobal\\ScadaTagMemory); if (hMap NULL) { /* 错误处理 */ } LPVOID pData MapViewOfFile( hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);这里有个非常容易被坑的参数内存映射文件的名字。名字前加 Global 前缀可以让不同会话的进程共享。如果 VB 和 VC 跑在同一用户会话下不加 Global 也行但现场如果存在多用户快速切换的场景加 Global 更保险。另一个参数是内存大小建议预留 64KB 甚至更大的缓冲区不要只够当前点数。因为现场加点位是常事尺寸写死到刚好能容纳后来要扩展就得改三个地方VB 定义、VC 定义、共享内存大小少了任何一个都会导致访问越界。VB 端读这个共享内存用的是 VB6 的 API 声明Private Declare Function OpenFileMapping Lib kernel32 Alias OpenFileMappingA _ (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, _ ByVal lpName As String) As Long Private Declare Function MapViewOfFile Lib kernel32 _ (ByVal hFileMappingObject As Long, ByVal dwDesiredAccess As Long, _ ByVal dwFileOffsetHigh As Long, ByVal dwFileOffsetLow As Long, _ ByVal dwNumberOfBytesToMap As Long) As Long Dim hMap As Long hMap OpenFileMapping(HF0003, False, Global\\ScadaTagMemory) Dim pBuf As Long pBuf MapViewOfFile(hMap, HF0003, 0, 0, 0)注意 VB 里 Long 是 32 位句柄能装得下。MapViewOfFile 返回的地址也是 32 位。拿到 pBuf 后你用 CopyMemory 从该地址拷贝数据到 VB 的结构体里。这里的一个玄学问题是如果 VC 端在 VB 还没 OpenFileMapping 时就写入数据而 VB 此时没有映射视图这没问题如果 VC 端崩溃退出共享内存映射文件对象引用计数减一VB 的映射视图依然存在但没人再往里面写新数据导致 VB 读到的是死数据。所以 VB 端必须自己判断数据帧里的序列号是否递增不递增就标记通信故障。4.3 数据帧格式设计时间戳、质量戳和序列号进程间共享内存里的数据组织我不建议每一个点单独放一个变量。因为那样做多个点的数据一致性无法保证——VC 先读了电压然后 VB 更新了电流VC 再读电流这两次读到的值就不是同一时刻的快照。所以共享内存里应该放一个整体结构体称之为「数据快照」。结构体里放一个全局序列号每采集一轮就加 1然后是每个点的值、时间戳、质量戳。typedef struct _TagItem { float value; // 工程值比如温度 25.6 DWORD timestamp; // 采集时刻的 GetTickCount BYTE quality; // 0无效, 1有效, 2报警 BYTE reserved; } TagItem; typedef struct _TagBuffer { DWORD seqNo; // 全局序列号每轮递增 DWORD tagCount; // 点位数量 TagItem tags[256]; DWORD crc32; // 整个缓冲区的校验值 } TagBuffer;加 CRC32 是很多人忽视的一笔。共享内存是进程间直接读写的理论上不会像网络那样出错但内存覆盖、写越界、缓冲别名导致的脏数据在真实工程中出现过不止一次。一旦 VC 发现 CRC32 校验失败就不该用这一帧数据同时把界面上的通信灯置红。别信玄学这就是一个防错习惯。CRC 的计算放在 VB 写入之后、VC 读取之前不做全量校验的代价就是画面突然冒出一个 999999.99现场去排查那一次数据是什么时候进的库比算一次 CRC 痛苦十倍。5. 这套 SCADA 编程软件里最容易踩的坑现象、原因与解决5.1 采集端跑一会儿就丢数据重启又好了现象VB 采集程序连续跑几个小时某几个点的数据偶尔变成 0 或者完全不更新重启程序后恢复正常再过几小时又犯病。原因串口缓冲区溢出。VB 的 MSComm 控件默认 InputMode 是文本模式如果设备返回的字节流里包含特殊字符会被当作字符编码处理导致缓冲帧错位。另一个更常见的原因是 OnComm 事件里处理事务太重比如在事件里直接写数据库下一帧数据到来时事件排队处理缓冲区被后来的数据撑满旧数据还没取出来就被新数据覆盖。解决把 MSComm 的 InputMode 设为 comInputModeBinary0确保字节流原样进出。把所有耗时操作写文件、写数据库、刷新列表移出 OnComm事件里只做接收和校验其他逻辑放到另一个 Timer 的队列里慢慢消化。最后把 RThreshold 设为 1每次事件进来先把 InBufferCount 和期望帧长度比对不足则直接退出不残留读操作。5.2 监控界面点按钮没反应像死机但 CPU 没占满现象VC 监控界面打开后点击任何按钮都要过 2 秒才有反应鼠标移动流畅但窗口不刷新多次点击后窗口标题栏显示「未响应」。原因主线程被某个阻塞调用卡住了。最常见的是 OnTimer 里直接访问命名管道或串口而那块设备响应超时ReadFile 迟迟不返回。另一个原因是共享内存读取时没注意跨进程访问的同步问题在拷贝数据时正好遇到写入方连续写入读到了一个中间状态但我见过更笨的是有人在 OnTimer 里发了 MessageBox弹窗一出来整个 UI 就停在那。解决凡是涉及任何外部设备读写的操作通通放到一个专门的 worker 线程。主线程只负责 UI 和定时器。数据处理线程和 UI 线程之间用 PostMessage 传递一个数据副本绝对不让 UI 直接去访问共享内存。排查时打开任务管理器看线程数如果发现某个线程在等待设备句柄基本就是阻塞调用。用超时小于 200ms 的 WaitForSingleObject 包裹所有等待超时就放弃这一轮哪怕读不到数据也别卡死画面。5.3 VB 和 VC 里的时间戳对不上差了几秒现象历史曲线里 VB 的采集时间戳和 VC 的显示时间差了 5 秒而且误差不是固定的时大时小导致曲线对不齐报警记录。原因VB 的 Timer 控件默认精度是 55ms而且一旦消息循环繁忙定时器事件会跳事件触发时刻并不等于实际采集时刻。如果采集程序里写的是「收到数据后取当前系统时间」那么这个时间戳反映的是通信收到时刻不是设备内部采样时刻。VC 端如果也另起一套时间逻辑两边就会各自漂移。解决统一以 VB 采集端收到完整帧的时刻作为时间戳VC 端不再二次打时间。关键是精确打点VB 里用 GetTickCount 而不是 Now 函数因为 Now 的分辨率是秒级GetTickCount 能拿到毫秒级的相对时间再通过启动时同步系统时间换算成绝对时间。如果设备本身就返回寄存器里的时间字则优先用设备时间。另一点是让 VC 每 10 秒向 VB 对一次时同步偏移量保证报警列表里的事件时间和曲线 x 轴用的是同一个时钟基准。5.4 断电重启后数据恢复到一半界面上的点全部变灰现象现场突然断电PLC 和上位机同时掉电。重新上电后VB 采集程序启动了VC 也启动了但界面上所有数据点都是灰色等了好久也不变绿。检查 VB 采集端发现通信正常数据帧序列号在涨可 VC 就是显示无效。原因共享内存映射文件在断电后内容丢失但 VC 端读取到的第一帧数据可能是一个中间状态。更常见的是 VB 在启动后先往共享内存里写了一个空快照所有 quality 都是 0然后才开始通信。VC 启动得比 VB 早它读到了这个空快照并且把 quality0 缓存到图元状态里。之后 VB 正常写入有效数据但 VC 的更新逻辑仅在「值变化」时重绘当值恰好不变时就永远保持着灰色。解决VC 端增加一个启动检测逻辑启动后前 3 秒不要直接取共享内存先检查 seqNo 是否从 0 变成非 0 且持续增加再让图元状态从「初始化」切到「跟随」模式。数据点更新时不管值变没变只要质量戳由无效变有效就必须强制刷新图元。同时 VB 端启动后先写入一个 seqNo0 的清零帧等第一轮采集完成后再写 seqNo1这样任何时刻读共享内存的人一眼就能判断数据是否就绪。5.5 模拟量跳变像心跳画面上的温度 25.6 和 26.1 来回跳现象一个温度点采集回来的值在 25.6 和 26.1 之间不停跳现场仪表正常PLC 测出来也正常但 SCADA 画面上数值来回蹦报警也时有时无。原因数据没有做工程值转换和滤波。仪表输出的工程量可能是 256 对应 100 度而 VB 只取了原始整数除以一个错误的系数导致分辨率不够。另一个原因是模拟量通道的电磁干扰SCADA 采集端没有做数字滤波直接把瞬时值摆到画面上干扰信号就会放大显示。解决在 VB 采集端加入一阶低通滤波对每个模拟量点做平滑公式很简单smooth_value alpha * new_value (1 - alpha) * old_valuealpha 建议取 0.2 到 0.5。alpha 越大跟随越快但抗干扰越差alpha 越小越平滑但滞后越大。温度、液位这类慢变量建议 alpha0.2压力流量这类快变量 alpha0.4。另外要在点表里配好量程转换参数原始值 0~1000 对应工程值 0~100。有个易错点量程转换应该在滤波之前还是之后先转换再滤波否则两个不同量纲的量直接平均会导致数值失真。最后别忘了给报警判断加一个死区当偏差在 0.5% 量程之内时不触发报警翻转这样既能滤掉抖动又不影响灵敏度。6. 把这套 SCADA 方案做成现场可用配置化、看门狗和调优习惯6.1 用配置文件代替硬编码设备地址我接手过最难受的项目是换一块仪表要重新编译两个程序。VB 里设备地址写在代码里VC 里点表结构写在头文件里现场调试时靠改源码来适配一个引入 20 个点位光编译就浪费半小时。后来我把所有点位信息、串口参数、量程设定、报警上下限全部移到一个文本配置文件中VB 启动时读一次配置文件初始化点表数组。[SerialPort] ComPortCOM3 BaudRate9600 DataBits8 ParityN StopBits1 [Point1] NameLI-101 DeviceID1 Register40001 DataTypeFLOAT Scale100 Offset0 AlarmHigh80 AlarmLow20VC 端不直接读这个文件而是通过共享内存里的 tagCount 和点描述符来初始化图元映射。这样做的好处是改地址、改量程、改报警值都只需要改文本文件两边程序重启后自动生效。配置文件必须放在程序同目录下的一个固定位置别放在系统目录现场同事杀了后台服务可能就把配置一起清理了。另外配置文件的编码统一用 UTF-8 可能在某些老系统里打不开我一般用 GBK 编码配合注释避免中文乱码导致解析失败。6.2 看门狗与自动重启工控现场最怕的不是程序报错而是程序静默死掉。VB 采集端因为操作简单很少有崩溃但它可能在长时间运行后出现串口句柄泄漏表现为不再采集但进程还在。VC 界面端也可能因为某种位置的内存越界进入假死。我的习惯是给两个程序各加一个看门狗一个独立的小程序每 5 秒检查两个进程的窗口标题和进程计数发现响应超时就回杀死并用 CreateProcess 重新拉起。同时让采集端周期性地把 seqNo 写入一个心跳文件看门狗读这个文件的最后修改时间超过 3 次间隔未更新就判定异常。别看这个设计简陋它比任何调试器都实用。现场断电恢复后看门狗能保证程序自上而下按顺序启动先启动看门狗看门狗再拉起 VB 采集等共享内存出现有效数据后再拉起 VC 界面。顺序错了会导致 VC 先读到一个初始化帧出现 5.4 节那种全体置灰的情况。我一般会在看门狗里加一个简单的状态跳转进程不存在启动、进程存在但不响应则重启、页面初始化完成前不让 VC 进入主界面。这个机制帮我省了不少半夜去现场重启系统的路费。6.3 现场调优的三个实用技巧第一个技巧是调试模式下把数据打印窗口打开。VC 监控界面上留一个隐藏窗口用快捷键呼出实时显示每个数据点的原始值、转换值、质量戳和 Scan 周期。现场问题绝大多数是数据链路问题你看到原始值一直跳就能判断是干扰而不是程序 bug。第二个技巧是给采集周期做一个动态调整在配置里加一个 ScanRateFactor当连续 5 轮没有数据变化时自动把轮询频率降为原来的 1/2检测到变化后再恢复。这样对仪表寿命和串口带宽都是保护现场还觉得你系统很智能。第三个技巧是日志。别把所有日志都写到一个文件按天拆分成 log_20250601.txt每条日志带时间戳和功能模块名。排查问题时先查日志里最后一次通信失败发生的时间再对照报警列表能快很多。我曾经在半夜三更被叫去现场结果打开日志一看是门卫大爷把仪表电源线碰掉了十分钟就定位没日志的话那晚注定要拆机箱。这套 VB 采集 VC 监控的方案说穿了没有高深技术但每一行代码背后都是设备通信的细节和现场运行的血泪。我后来无论用什么语言写 SCADA都还保留着共享内存快照、序列号加质量戳、配置与代码分离这三个习惯已经成了一种本能。希望这篇拆解对正要啃这类 scada.rar 老项目的人有点用别再重复我当年踩过的坑。本文还有配套的精品资源点击获取