拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 Qt 做工业 SCADA 上位机:整体架构怎么分层

用 Qt 做工业 SCADA 上位机:整体架构怎么分层

用 Qt 做工业 SCADA 上位机:整体架构怎么分层

做工业上位机这些年,见过不少项目栽在同一个地方:一开始没想清楚分层,写到后面数据流全是乱的。

采集线程直接去改界面控件,策略计算里顺手连数据库,历史查询把主界面卡死几秒……这些问题单看都是小毛病,根子却在架构上——没有把"谁负责什么"划清楚。

这篇讲一套我们实际用过的分层思路,基于 Qt + C++ 实现,行业上通用于各类 SCADA 场景。以讲分层与取舍为主,关键处配少量示意代码——示意代码只表达设计形状,不是可以直接编译的完整实现。

一、先理清:上位机到底要干几件事

在动手分层之前,先把职能列全。一套工业上位机通常要做四件事:

职能做什么
采集通过 TCP 或串口连现场装置,周期读取数据
监控把实时数据画成画面给人看,并支持下发命令
存储把需要留痕的数据写进数据库,支持历史回看
转发把数据按映射关系转发给上层主站

很多项目的问题在于:把这四件事揉在一个模块里。结果是采集代码里混着界面刷新,存储逻辑里夹着业务判断,改一处牵动全身。

分层的第一个动作,就是让这四件事各归各位。

二、数据模型:装置—点

分层的起点是数据结构。工业场景的数据模型基本都长这样:

  • 装置:一台现场设备,编号唯一
  • 点:装置上的一路数据

点按用途分四类,也就是电力行业说的「四遥」:

类别含义数据形态
遥测连续变化的测量值浮点
遥信两种状态的位置量整型 0/1
遥控数字量输出命令命令
遥调模拟量输出命令命令

这个模型看起来简单,但有两个坑必须一开始就定好:

坑一:编号从几开始。点的编号往往允许空档(配置里是人指定的业务编号),装置编号则通常是连续的。两者混用,后面做映射时会很痛苦。我们的做法是明确约定并写进注释:点号从 0 开始,装置号从 1 开始,判断"未选择"不能用 0 当哨兵。

坑二:业务编号不能直接当数组下标。点号允许空档,而内存里的点表数组是按配置行序填的。中间缺几个号,用点号当下标就会整段读错数据——而且不报错。所以系统里必须统一走一层映射:业务编号 → 数组行号。

这个映射表是很多 bug 的源头,值得单独封装。

三、分层设计

写值

取数存库

读值

读值

采集层
设备接口 + 协议解析

数据层
内存实时库
所有数据流的中转站

存储层
历史数据入库

业务层
策略与报警

展示层
画面引擎

注意箭头方向:采集层只往里写,其余三层只从里读,没有任何一条边是跨过数据层直连的。这就是下面要说的分层规矩。

五层的职责依次展开如下。

3.1 数据层:内存实时库

实时数据放内存,不放数据库。这是工业上位机与普通信息系统的最大区别。

界面刷新、策略求值、存库采样、转发取数,全都要高频读实时值。走数据库根本扛不住。

所以有一份常驻内存的实时库,结构就是上面说的装置-点表。它有几个特征:

  • 被多个模块共享(界面、策略、采集、转发都读它)
  • 采集线程、策略引擎、界面都会写它
  • 因此必须加锁,而且要划清加锁的粒度

我们的做法是每台装置对象自带一把锁,而不是全局一把大锁。理由是:不同装置之间互不干扰,全局锁会让采集线程互相排队。

代价是跨装置的操作(比如统计全站数据)需要多把锁,实现时要小心死锁。这个取舍在后面第五节展开。

对外暴露的接口大致长这样——只给"读某个点"“写某个点”,不给内部数组(理由见第四节规则一):

classRealtimeStore{public:staticRealtimeStore&instance();// 按装置 + 点号读写。点号允许空档,内部映射到数组行号boolwrite(constQString&device,constQString&point,doublevalue);doubleread(constQString&device,constQString&point,bool*ok=nullptr);// 批量读:供画面刷新、策略求值使用,内部按装置分批加锁voidreadBatch(constQVector<PointRef>&refs,QVector<double>&out);};

注意readBatch这一条:批量接口必须提供,否则调用方一定会自己去遍历。上层一旦写成"循环调read",就得反复进出锁——批量接口既是为了效率,也是为了把调用方挡在锁的外面。

3.2 采集层:设备接口与协议解析

采集层再拆成两半,这是很多人容易忽略的一步:

子层职责
设备接口只管收发字节、维护连接、断线重连
协议解析只管组帧解帧、把值写进实时库

分开的好处很直接:换通讯方式不影响协议代码。串口改 TCP,只动设备接口;Modbus 改别的规约,只动协议层。

线程模型上,我们用的是每个通道一个接口线程 + 每个协议一个解析线程:

  • 接口线程阻塞在 socket 上收发,天然一对一
  • 解析线程从通道缓冲区取数据,组帧、解帧、写实时库

为什么不用线程池?因为采集是长连接 + 持续阻塞的场景,线程池的复用优势体现不出来,反而增加了"哪个任务在哪个线程上"的排查成本。通道数量由现场规模决定,通常几十个以内,线程数完全可控。

这里有个必须注意的约束:协议解析线程内部往往是死循环轮询(因为要不停地读缓冲区),不能用 Qt 的信号槽或 QTimer——那些依赖事件循环。正确做法是用一个运行标志位配合带超时的休眠,退出时能及时响应。

3.3 存储层:实时与历史分离

实时数据在内存,历史数据进数据库,两者物理隔离。

存储层的设计要点:

第一,写库不是全量轮询。每秒扫一遍所有点,但每个点按自己配置的存储间隔决定这次要不要存。全站几百个点,实际每秒落库的可能只有几个。

第二,用队列解耦。采样线程挑出该存的数据塞进队列,写库线程消费。这样即使数据库偶发卡顿,也不会阻塞采集。

第三,按时间分表。工业历史数据量随时间是线性增长的,单表迟早扛不住。按天建表(his_xxx_20260929这类命名)是最常见也最好维护的方案,清理过期数据只需要删表,不用做代价极高的 DELETE。

代价是跨天查询要拼多张表,查询逻辑会复杂一些。

3.4 业务层:策略与报警

策略层负责"条件成立时执行动作",报警层负责"值异常时记录"。

这一层最容易犯的错是与数据层耦合过深——策略代码里直接操作点表数组、直接拼 SQL。结果就是策略改一个条件,存储层要跟着动。

我们的边界是:策略只通过实时库的读写接口操作数据,不碰底层结构。这样策略引擎可以独立测试,也便于后续换成脚本引擎。

报警同理:越限判断发生在写值的那一刻(在数据层内部),但报警的记录与推送交给业务层。判断和执行分开,后面做报警分级、推送渠道扩展都不用动数据层。

3.5 展示层:画面引擎

展示层要做的事:把配置文件里的画面在运行时重建出来,并按数据源绑定实时值。

关键在于画面与数据解耦:

  • 画面文件里描述的是"有哪些控件、什么位置、绑哪个点"
  • 运行时解析后,按绑定关系周期性去实时库取值

这样改画面不用改代码,甚至能做到画面文件热加载——编辑器里存盘,运行态自动重新加载,不用重启。调画面的时候这一点能省大量时间。

四、分层的边界怎么划

几条我们实际踩过之后总结的规则:

规则一:数据层的接口要粗,不要细。暴露"读某个点""写某个点"就够了,不要暴露内部数组。否则上层迟早会绕过接口直接操作数据。

规则二:跨层调用只允许相邻层。展示层不能直接调采集层,采集层也不能直接刷界面。所有跨层的数据流都经过数据层中转。

规则三:加锁在数据层内部完成。上层调用写值接口时不应该关心锁——如果调用方需要自己加锁,说明接口设计有问题,早晚会漏加。

规则四:配置读取集中在一处。各个模块各读各的配置文件,后期排查配置问题会很痛苦。统一由数据层在启动时加载,其他模块从内存取。

五、几个真实的取舍

取舍一:配置用文件还是数据库?

我们用 CSV 文件。理由是现场工程师能直接用 Excel 打开查看和批量编辑,出问题能肉眼比对,不依赖数据库,也方便随包发布和版本管理。

代价是:编码、引号、字段内逗号这些都要自己处理;跨文件的一致性校验要额外写代码。如果你的项目配置量极大(上万点且频繁变更),数据库可能更合适。

取舍二:单进程还是多进程?

我们用单进程多线程:采集、存储、策略、界面全在一个进程内。

好处是数据交换走内存,没有进程间通信开销;部署也简单,一个可执行文件。

代价是一个线程出问题会影响全局,共享实时库的加锁面较大。如果现场对稳定性要求极高(比如要求采集模块崩溃不影响界面),多进程通过共享内存或消息队列通信会更稳,但复杂度显著上升。

取舍三:实时库加锁粒度

前面提过,我们用每装置一把锁。这个选择在装置数量多、访问分散时优势明显;但如果你的系统里存在大量"遍历所有装置"的操作,多把锁会带来死锁风险与实现复杂度,此时全局锁反而更简单可靠。

没有普适的最优解,只有与你访问模式匹配的解。

小结

工业上位机的分层,说到底是为了回答一个问题:当现场规模扩大、需求变更时,改动会蔓延到哪里?

分层清晰的项目,加一种协议只动协议层,加一种报警只动业务层。分层混乱的项目,改任何一处都要通读全部代码。

几个关键决策回顾:

  1. 数据模型先定死(装置—点、编号规则、映射层)
  2. 实时库放内存,采集与存储解耦
  3. 采集层拆成设备接口与协议解析两层
  4. 加锁封在数据层内部,上层无感
  5. 画面与数据解耦,配置驱动

下一篇讲配置化设计:为什么工业软件偏爱文件配置而不是数据库,以及怎么把这条路走稳。


返回列表