资讯动态

自建SCADA系统:VB数据采集与VC监控界面开发详解

发布时间:2026/10/22 19:48:39 来源:尧图企业网站定制
简介一套基于VBVisual Basic与VCVisual C混合编程实现的集群设备SCADA监控系统源码包面向工业监控软件开发者和自动化专业学习者解决现场数据采集、界面展示与控制交互问题可用于电力、石油、化工等场景的虚拟监控实训或小规模项目参考。压缩包共84个文件大小约1019KB包含Visual Basic工程vbp、frm、bas、VC工程dsp、cpp、h、控件与动态库ocx、dll、lib、数据库mdb、db、配置与图像文件ini、txt、gif、jpg等结构清晰便于按模块研读。目前已有180人学习下载可作为SCADA课程设计、毕业设计及二次开发的基础蓝本。源码完整呈现VB设计友好界面、VC处理底层逻辑的混合编程架构涵盖实时数据采集、设备管理、报警显示等典型功能并附带MDB数据库与设备配置INI文件可帮助读者快速还原运行环境理解工业监控系统的工程组织方式具备较高的参考与复用价值。1. scada.rar 解包之后是什么先看清这套 SCADA 编程软件的地基结构拿到 scada.rar 这种工程压缩包很多人第一反应是找安装程序结果翻半天发现里面是源码、点表和界面工程。这个标题真正指向的对象是一套用 VB 做采集数据、VC 做监控界面的 SCADA 编程软件工程包——上位机开发里最常见也最容易被低估的结构VB 负责把设备数据读上来VC 负责把数据画成画面。反直觉的地方在于这套组合看着“老”却是理解 SCADA 数据链路最干净的入门样本。它解决的问题很具体不想为十几台仪表买整套商用组态授权或者设备协议太特殊只能用自定义报文时你需要自己写采集端和界面端。适合的读者是想自建中小型监控系统的工程师以及打算从零理解 SCADA 数据流转的开发者。规模上几千个点以内完全够用再往上就该换架构了。2. 先把数据链路讲透SCADA data 在 VB 采集端的两条路与数据模型2.1 从传感器到内存数组VB 采集程序的结构与数据模型做 SCADA data 采集最忌讳一上来就写串口代码。先把程序分成三层设备层负责跟仪表说话采集层负责把字节变成数值存储层负责把数值交给界面或数据库。VB 这边常见做法的采集程序拆成三个模块一个模块专门管理串口或 Socket一个模块维护点表Tag Table一个模块负责把解析结果写进共享内存区。点表是整套程序的地基。每个点至少要包含设备地址、功能码、寄存器起始地址、寄存器数量、数据类型、缩放系数、采集周期。我一般用自定义 Type 来定义点表结构体再用数组把它实例化这样循环读点、统一解析都方便示例结构如下Private Type TagItem DeviceAddr As Byte 从站地址 FuncCode As Byte 功能码3 表示读保持寄存器 StartReg As Integer 起始寄存器地址 RegCount As Integer 寄存器数量 DataType As Integer 1整型 2浮点 3开关量 Scale As Single 缩放系数 Value As Single 解析后的工程值 Quality As Byte 质量戳0坏 1好 End Type Global TagTable(1 To 500) As TagItem这里有几个参数值得注意DataType 决定解析字节时怎么拼Scale 用于把原始值换算成工程量Quality 是给界面用的坏值和停用值不能画进曲线。把数据从设备读上来先不落数据库而是放在内存数组里原因很简单——界面刷新和数据库写入都跟不上设备采集速度内存数组起到缓冲作用这也是大型 SCADA 系统里实时库概念的简化版。有了点表之后采集主循环就一行逻辑每条点记录对应一帧请求放进发送队列等待响应解析更新内存然后进入下一条。关键是不要把串口操作和界面刷新混在同一个循环里VB 里的 DoEvents 看着省事采集周期一短界面就会变成“慢放”。2.2 串口与网口两条主流链路Modbus RTU 和 Modbus TCP 怎么选SCADA data 采集的通信链路九成以上落在 Modbus 协议族里。串口走 Modbus RTU网口走 Modbus TCP。选型依据主要看设备端接口和现场布线不是哪个高级用哪个。下面这张表是我实际选型时常用的对比基准对比项Modbus RTU串口Modbus TCP网口物理层RS-232 / RS-485以太网最大从站数32 个485 总线理论上受 IP 限制传输距离485 可达 1200 米取决于网络环境实时性轮询周期稳定受网络负载影响调试难度需要看波形/报文用抓包工具直接看我一般优先 Modbus 而不是自定义协议原因很实际调试工具多、资料多、仪表厂商基本都支持。自定义协议看着能“定制”实际上每台设备都得单独写解析后期维护成本是成倍往上走的。除非设备协议没得选否则用 Modbus 是性价比最高的路。RTU 报文格式是设备地址加功能码加数据加 CRC 校验TCP 报文则多了 MBAP 头但功能码和数据结构完全一致。代码层面我通常把两种链路抽象成同一个函数传入点表记录返回一段请求帧收到响应后走同一套解析逻辑。这样无论是接串口仪表还是网口网关采集主程序完全不用改。这里有一个容易忽略的坑RTU 是字节流没有帧起始和帧结束标记靠时间间隔来断帧。所以串口接收必须按“静默时间超过 3.5 个字符周期”来判断一帧数据结束处理不严谨就容易把两帧粘在一起。TCP 链路没有这个问题因为 MBAP 头里带了字节长度。3. 用 VB 把采集数据跑起来MSComm 串口读取、帧解析与断线重连3.1 最小可运行的 MSComm 串口采集代码VB 里做串口采集最常见的控件是 MSComm。先配置串口参数再通过 OnComm 事件接收数据。关键配置项包括波特率、数据位、停止位、校验位还有接收阈值 RThreshold——设为 1 表示每收到一个字节触发一次 OnComm。但在高波特率下每字节触发一次事件效率太低我一般按报文长度设置阈值比如响应最长 256 字节就设为 256 的约数。下面是一个最小可运行的采集代码骨架轮询一台从站的保持寄存器Private Sub Form_Load() MSComm1.CommPort 1 使用 COM1 MSComm1.Settings 9600,N,8,1 波特率9600 无校验 8数据位 1停止位 MSComm1.InputMode comInputModeBinary 二进制模式否则字节会被当文本处理 MSComm1.RThreshold 8 接收缓冲达到8字节触发OnComm MSComm1.SThreshold 0 MSComm1.PortOpen True End Sub Private Sub MSComm1_OnComm() Dim buffer() As Byte Dim frame() As Byte Dim i As Integer If MSComm1.CommEvent comEvReceive Then buffer MSComm1.Input 把收到的字节追加到全局接收缓冲 然后检查帧完整性完整时拷贝到 frame 并清空缓冲 End If End Sub逻辑说明Form_Load 里完成串口初始化和打开OnComm 事件只在有数据到达时被触发Input 属性一次性取出缓冲里的字节。注意 InputMode 必须设成 comInputModeBinary这是新手最容易踩的坑——默认文本模式下字节值 0x0A、0x0D 会被当成换行符截断报文永远解析不对。参数说明RThreshold 设为 8 是因为假设最小响应帧是 8 字节设备地址、功能码、字节数、2字节数据、2字节CRC但现场最好设成 1 配合接收缓冲自己拼帧更稳妥Settings 字符串格式固定是“波特率,校验,数据位,停止位”顺序不能写错。3.2 数据校验与异常帧过滤CRC16 与时间戳Modbus RTU 每一帧都带 CRC16 校验码低字节在前。采集端收到一帧后先算 CRC跟报文里的校验字节比对不一致直接丢弃。不要抱着侥幸心理跳过这一步——485 现场电磁干扰多帧错位、字节损坏是常态不校验的话错误数据会直接进入监控画面。VB 里算 CRC16 用查表法或按位计算。按位计算虽然慢一点但代码短、依赖少小点数轮询完全够用Public Function Crc16(data() As Byte) As Integer Dim crc As Integer Dim i As Integer Dim j As Integer crc HFFFF For i 0 To UBound(data) crc crc Xor data(i) For j 0 To 7 If (crc And 1) Then crc (crc \ 2) Xor HA001 Else crc crc \ 2 End If Next j Next i Crc16 crc End Function逻辑说明CRC 初始值为 0xFFFF每个字节进来先异或再循环右移 8 次按最低位决定是否异或多项式 0xA001。计算完后要与接收帧最后两字节比对注意帧里是低字节在前比对时要反着拼。异常帧过滤还有一个容易被忽视的点Modbus 异常响应帧的功能码最高位置 1例如读保持寄存器功能码 0x03异常回复是 0x83后面带一个异常码。解析时如果发现功能码最高位是 1这帧就是异常帧报文已经带 CRC不能当普通数据处理。我一般遇到异常帧会把该点质量戳置为 0界面显示为坏值而不是把异常码当成数据。时间戳的处理在采集端就要做。每一条点记录更新时把本地时间打上去格式统一用 yyyy-mm-dd hh:mm:ss 字符串还是 Long 型时间戳取决于后续要不要进数据库。我习惯存 Long 型显示交给界面格式化这样在 VC 端画时间轴时可直接换算。3.3 掉线检测与自动重连看门狗一样的存在串口 SCADA 最容易出问题的场景不是解析而是设备掉线后的恢复。很多采集程序一开始跑得好好的现场某台仪表断电重启后数据就再也不刷新了原因是程序只做了“读不到就跳过”没有做“读不到就重新建立链路”的处理。掉线检测的思路是给每个从站维护一个失败计数器。连续失败达到阈值就判定该站离线此时停止向该站发请求间隔一段时间后重新尝试连接。串口链路下“重连”就是重新打开端口、清空缓冲、重新同步网口链路下则需要断开 Socket 再重建连接。Private Sub CheckLink() Dim i As Integer For i 1 To SlaveCount If FailCount(i) 5 Then FailCount(i) 0 If Not MSComm1.PortOpen Then MSComm1.PortOpen True End If 清空接收缓冲丢弃残留字节 On Error Resume Next MSComm1.Input On Error GoTo 0 SyncFrame() 向设备发一帧广播或对时请求重新同步 End If Next i End Sub逻辑说明这个函数在采集主循环的空闲时段调用FlailCount 数组记录每个从站连续失败次数超过 5 次判定离线触发重连动作。重连动作包括重新打开端口、清空接收缓冲、重新同步帧。注意要加 On Error 保护串口没插设备时打开端口会直接抛异常。参数说明连续失败阈值 5 是我常用的起步值现场如果设备响应慢阈值要调大否则会造成反复重连的抖动如果设备响应快但链路不稳定阈值可以调小尽早发现问题。心跳广播不一定要用 Modbus 标准功能码有些设备不支持广播这时可以用对时命令代替。4. 用 VCMFC把监控界面立起来线程分工、曲线绘制与数据落库4.1 MFC 界面线程与采集线程的分工VC 监控界面最核心的设计决策是线程模型。常见的做法是界面主线程管窗口消息和绘制采集线程独立读共享内存避免界面卡死。MFC 里创建采集线程用 AfxBeginThread线程函数里做循环读取通过 PostMessage 把“有新数据”的通知发给主线程。这样分工的原因有二一是界面绘制和消息处理不能阻塞否则窗口拖动、缩放都会卡二是采集数据是持续不断的流必须在独立线程里以固定周期轮询共享内存跟 UI 的刷新频率解耦。两个线程之间共享内存的读写要加保护最轻量的方式是事件对象加互斥锁。UINT CollectThreadProc(LPVOID pParam) { // 采集线程入口pParam 传入共享内存指针 while (bRunning) { WaitForSingleObject(hDataMutex, 50); // 加锁防止界面读取时数据被写 ReadTagsToSharedMemory(); // 从共享内存读取新点表数据 ReleaseMutex(hDataMutex); PostMessage(hMainWnd, WM_DATA_READY, 0, 0); // 通知界面刷新 Sleep(100); // 采集周期100ms } return 0; }逻辑说明bRunning 是全局运行标志置 FALSE 时线程退出循环WaitForSingleObject 和 ReleaseMutex 成对出现保证同一时刻只有一边在访问共享内存PostMessage 是异步的通知窗口有新数据可刷新但不等待处理完成避免采集线程被界面拖慢。参数说明Sleep(100) 对应 10Hz 的刷新频率滚动曲线够用如果点数多或刷新要求高可以调到 50ms 以下但要注意 CPU 占用和 UI 刷新压力。互斥锁等待 50ms 是防止界面读取耗时过长而无限阻塞超过 50ms 就跳过本次写操作保证线程不进死锁。4.2 实时曲线与报警列表的刷新逻辑曲线绘制是 VC 监控界面里最容易出效果也最容易翻车的部分。常见做法是 CDC 的 Polyline把共享内存里的点表数据按时间顺序连成曲线。曲线窗口的 OnTimer 消息里定时重画每次从缓冲数组里取最新的一段数据即可。void CMonitorDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) // 定时器1负责刷新曲线 { // 加锁读取共享内存 WaitForSingleObject(hDataMutex, 30); memcpy(tempBuffer, sharedData, dataCount * sizeof(float)); ReleaseMutex(hDataMutex); // 计算坐标并绘制 RefreshCurve(tempBuffer, dataCount); } CDialog::OnTimer(nIDEvent); }逻辑说明OnTimer 里先加锁读取数据因为采集线程随时可能写这块内存memcpy 拷贝到局部缓冲后再绘制避免绘制过程中数据被改变。RefreshCurve 函数内部会把数据按时间轴映射成屏幕坐标再用 Polyline 一次画完一整条曲线而不是逐点画否则闪烁严重。报警列表的刷新逻辑类似但不需要重画全部历史只处理新增报警。MFC 里用 CListCtrl 管理报警列表主线程每次收到 WM_DATA_READY 后扫描点表质量戳发现从好变坏就插入一条报警记录插入时用 InsertItem 加时间戳和描述。这里有一个经验参数曲线数据缓冲不要无限增长我一般保留最近 5 分钟的数据比如 10Hz 采集下就是 3000 个点再久的数据交给数据库查询界面只画实时窗口。缓冲太大不仅占内存Polyline 一次画几千个点也会明显掉帧。4.3 采集数据如何落库从内存数组到历史查询实时界面看的是当下历史分析必须落库。VC 这边最轻量的方案是直接用 Access 数据库也可以通过 ADO 连 SQL Server数据量不大时前者足够。落库的频率不要跟采集频率一致10Hz 采集如果 10Hz 写库数据库压力大而且数据冗余严重常见做法是每秒钟写入一次即把 10 个瞬时值聚合成一条记录存平均值、最大值、最小值。-- 一分钟聚合表的建表语句按设备ID和时间索引 CREATE TABLE tag_history ( id INT IDENTITY(1,1) PRIMARY KEY, device_id INT NOT NULL, tag_name VARCHAR(32) NOT NULL, sample_time DATETIME NOT NULL, avg_value FLOAT NOT NULL, max_value FLOAT NOT NULL, min_value FLOAT NOT NULL ); CREATE INDEX idx_history_time ON tag_history(device_id, sample_time);逻辑说明这张表存的是聚合数据不是原始采样值。sample_time 精确到分钟或秒级avg_value、max_value、min_value 是这一秒内的聚合结果。加索引的目的是让历史查询走索引扫描避免全表扫。落库线程我一般放在采集线程结束时触发而不是和采集循环同步执行。采集线程每攒够 10 个数据点就把这 10 点的聚合结果放进一个写入队列由数据库线程异步写入。这样做的好处是采集线程不会被数据库 IO 卡住缺点是断电时最后几秒数据可能丢失取舍看现场要求。5. 联调自测与排障SCADA 开发里最容易翻车的 4 类问题5.1 串口能收到字节但解析出来的数值全乱现象串口调试助手能看到设备正常回帧但 VB 采集程序解析出来的数据忽大忽小甚至出现负数。原因最常见的是 MSComm 控件的 InputMode 没有设置为二进制模式。默认文本模式下字节值 0x0D、0x0A 会被当作回车换行处理Input 属性返回的是字符串二进制帧被截断或转码解析自然全乱。其次是接收缓冲没有清空旧帧残留跟新帧粘在一起解析起点错位。解决InputMode 必须设置成 comInputModeBinary收到数据后一次性取出全部字节按全局接收缓冲拼接。每次判定完整帧并解析成功后立即清空缓冲。Modbus RTU 协议本身没有帧头帧尾断帧靠时间间隔接收逻辑里必须处理“收到半帧”的情况不能收到多少解析多少。5.2 界面像慢放拖动窗口卡顿明显现象VC 监控界面平时显示正常一拖动窗口或弹出对话框就特别卡曲线刷新明显迟滞。原因监控界面线程和采集逻辑混在同一个线程里。最常见的是用 MFC 的 OnTimer 里直接做串口读取或数据库写入这些阻塞操作会卡住消息循环窗口拖动自然就不流畅。解决把串口读取和下位机通信全部移到独立线程UI 线程只通过 PostMessage 接收通知。界面刷新用定时器控制频率不能跟采集频率一致。如果界面是 OnTimer 100ms 刷新一次曲线采集线程保持 50ms 采集一次数据量大的时候还可以把刷新降为 200ms曲线仍能保持连续。5.3 从站重连之后曲线时间轴对不上现象设备断电重启后自动重连成功数据能继续显示但曲线时间轴上有一段明显断开的数据而且断开前后的数据出现了重叠。原因重连时没有清空旧接收缓冲和帧同步状态。旧帧残留在缓冲里重连后第一次读到的可能是上一条残留帧时间戳还用的是旧时间于是界面把历史数据画进了新时间段。解决重连动作里必须强制清空缓冲、重置帧状态机、重新同步。我一般还会在重连完成后给该站所有点打一个“数据无效”质量戳等到收到第一帧完整有效响应后再恢复质量戳。这样曲线在断开区间会显示断线而不是错误连接时间戳也不会重叠。5.4 VC 和 VB 之间传递结构体数据偶尔取值错位现象内存共享数据大多数正常但偶尔某个点的值明显不对而且数值是固定偏移了一个字节。原因VC 和 VB 的结构体成员对齐方式不一致。MFC 默认按 4 字节对齐结构体VB 里 Type 默认按 2 字节对齐也可以手动指定。两边结构体内存布局不同直接拷贝或共享映射时字段就会错位。解决共享内存或文件映射里的数据结构不要直接用结构体拷贝改为显式字节流拼接和解析。拼接顺序固定先设备地址再功能码再数据字节最后 CRC两边按相同顺序解析。如果必须要用结构体VC 侧用 #pragma pack(1) 强制 1 字节对齐VB 侧对应设置然后两边各写一个结构体转字节流的测试函数互相对拍验证。6. 进阶把解包来的老套路升级成可维护的采集方案6.1 把点表和设备参数外置成配置文件采集端最直接的升级是把点表和通信参数从代码里拆出来放到外部配置文件中。这样新增一台仪表、改一个站地址不用重新编译整个工程现场维护的人只要会改文本就行。配置文件格式用 INI 即可可读性好Windows 下也不用引额外解析库。INI 里应该有设备段和点表段设备段放串口号、波特率、从站地址列表点表段放寄存器地址、点数、数据类型、缩放系数。VB 读取 INI 用 GetPrivateProfileString 和 GetPrivateProfileInt 两个 API采集程序启动时读取一次运行期间不重读。VC 界面端也可以共用同一份点表文件保证两边对点的定义一致。6.2 用模拟从站脚本给采集端做回归验证没有真实设备时用一个模拟从站脚本是最快的验证手段。下面的 Python 脚本模拟一个 Modbus TCP 从站监听本地端口收到请求后回一帧固定数据。采集端连这个端口就能验证报文组帧、定时轮询、掉线重连这些核心逻辑。import socket import struct def build_response(req): # 只处理读保持寄存器请求返回两个寄存器值 100 和 200 output struct.pack(HH, 100, 200) return struct.pack(BBH, 0x01, 0x04, len(output)) output srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind((127.0.0.1, 1502)) srv.listen(1) print(模拟从站启动监听 1502 端口) while True: conn, addr srv.accept() data conn.recv(256) if data: print(收到请求帧:, data.hex()) conn.send(build_response(data)) conn.close()逻辑说明脚本创建一个 TCP 服务端监听 1502 端口Modbus TCP 的标准端口。收到请求后先取出请求中的功能码再构造一个最小响应帧返回。隧道地址和 CRC 校验的部分省略了实际使用时要把 MBAP 头和事务 ID 原样返回否则严格的采集端校验会拒绝。这个脚本的价值在于可控可以人为制造延时、丢响应、改返回数据用来验证采集端的重连逻辑和坏值处理。日常开发中我把这个脚本和采集端放在同一台机器上改一次点表配置就跑一轮回归比反复跑现场省事得多。做这套方案给我最深的教训是SCADA 开发里“能通”和“能稳定跑”是两码事。协议通了只是万里长征第一步掉线重连、断帧处理、对齐问题这些才是现场真实的战场。把自己的点表、脚本、测试记录都留存好下一次接新设备时半小时就能验证一套新方案。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑