用 Qt 做工业 SCADA 上位机整体架构怎么分层做工业上位机这些年见过不少项目栽在同一个地方一开始没想清楚分层写到后面数据流全是乱的。采集线程直接去改界面控件策略计算里顺手连数据库历史查询把主界面卡死几秒……这些问题单看都是小毛病根子却在架构上——没有把谁负责什么划清楚。这篇讲一套我们实际用过的分层思路基于 Qt C 实现行业上通用于各类 SCADA 场景。以讲分层与取舍为主关键处配少量示意代码——示意代码只表达设计形状不是可以直接编译的完整实现。一、先理清上位机到底要干几件事在动手分层之前先把职能列全。一套工业上位机通常要做四件事职能做什么采集通过 TCP 或串口连现场装置周期读取数据监控把实时数据画成画面给人看并支持下发命令存储把需要留痕的数据写进数据库支持历史回看转发把数据按映射关系转发给上层主站很多项目的问题在于把这四件事揉在一个模块里。结果是采集代码里混着界面刷新存储逻辑里夹着业务判断改一处牵动全身。分层的第一个动作就是让这四件事各归各位。二、数据模型装置—点分层的起点是数据结构。工业场景的数据模型基本都长这样装置一台现场设备编号唯一点装置上的一路数据点按用途分四类也就是电力行业说的「四遥」类别含义数据形态遥测连续变化的测量值浮点遥信两种状态的位置量整型 0/1遥控数字量输出命令命令遥调模拟量输出命令命令这个模型看起来简单但有两个坑必须一开始就定好坑一编号从几开始。点的编号往往允许空档配置里是人指定的业务编号装置编号则通常是连续的。两者混用后面做映射时会很痛苦。我们的做法是明确约定并写进注释点号从 0 开始装置号从 1 开始判断未选择不能用 0 当哨兵。坑二业务编号不能直接当数组下标。点号允许空档而内存里的点表数组是按配置行序填的。中间缺几个号用点号当下标就会整段读错数据——而且不报错。所以系统里必须统一走一层映射业务编号 → 数组行号。这个映射表是很多 bug 的源头值得单独封装。三、分层设计写值取数存库读值读值采集层设备接口 协议解析数据层内存实时库所有数据流的中转站存储层历史数据入库业务层策略与报警展示层画面引擎注意箭头方向采集层只往里写其余三层只从里读没有任何一条边是跨过数据层直连的。这就是下面要说的分层规矩。五层的职责依次展开如下。3.1 数据层内存实时库实时数据放内存不放数据库。这是工业上位机与普通信息系统的最大区别。界面刷新、策略求值、存库采样、转发取数全都要高频读实时值。走数据库根本扛不住。所以有一份常驻内存的实时库结构就是上面说的装置-点表。它有几个特征被多个模块共享界面、策略、采集、转发都读它采集线程、策略引擎、界面都会写它因此必须加锁而且要划清加锁的粒度我们的做法是每台装置对象自带一把锁而不是全局一把大锁。理由是不同装置之间互不干扰全局锁会让采集线程互相排队。代价是跨装置的操作比如统计全站数据需要多把锁实现时要小心死锁。这个取舍在后面第五节展开。对外暴露的接口大致长这样——只给读某个点“写某个点”不给内部数组理由见第四节规则一classRealtimeStore{public:staticRealtimeStoreinstance();// 按装置 点号读写。点号允许空档内部映射到数组行号boolwrite(constQStringdevice,constQStringpoint,doublevalue);doubleread(constQStringdevice,constQStringpoint,bool*oknullptr);// 批量读供画面刷新、策略求值使用内部按装置分批加锁voidreadBatch(constQVectorPointRefrefs,QVectordoubleout);};注意readBatch这一条批量接口必须提供否则调用方一定会自己去遍历。上层一旦写成循环调read就得反复进出锁——批量接口既是为了效率也是为了把调用方挡在锁的外面。3.2 采集层设备接口与协议解析采集层再拆成两半这是很多人容易忽略的一步子层职责设备接口只管收发字节、维护连接、断线重连协议解析只管组帧解帧、把值写进实时库分开的好处很直接换通讯方式不影响协议代码。串口改 TCP只动设备接口Modbus 改别的规约只动协议层。线程模型上我们用的是每个通道一个接口线程 每个协议一个解析线程接口线程阻塞在 socket 上收发天然一对一解析线程从通道缓冲区取数据组帧、解帧、写实时库为什么不用线程池因为采集是长连接 持续阻塞的场景线程池的复用优势体现不出来反而增加了哪个任务在哪个线程上的排查成本。通道数量由现场规模决定通常几十个以内线程数完全可控。这里有个必须注意的约束协议解析线程内部往往是死循环轮询因为要不停地读缓冲区不能用 Qt 的信号槽或 QTimer——那些依赖事件循环。正确做法是用一个运行标志位配合带超时的休眠退出时能及时响应。3.3 存储层实时与历史分离实时数据在内存历史数据进数据库两者物理隔离。存储层的设计要点第一写库不是全量轮询。每秒扫一遍所有点但每个点按自己配置的存储间隔决定这次要不要存。全站几百个点实际每秒落库的可能只有几个。第二用队列解耦。采样线程挑出该存的数据塞进队列写库线程消费。这样即使数据库偶发卡顿也不会阻塞采集。第三按时间分表。工业历史数据量随时间是线性增长的单表迟早扛不住。按天建表his_xxx_20260929这类命名是最常见也最好维护的方案清理过期数据只需要删表不用做代价极高的 DELETE。代价是跨天查询要拼多张表查询逻辑会复杂一些。3.4 业务层策略与报警策略层负责条件成立时执行动作报警层负责值异常时记录。这一层最容易犯的错是与数据层耦合过深——策略代码里直接操作点表数组、直接拼 SQL。结果就是策略改一个条件存储层要跟着动。我们的边界是策略只通过实时库的读写接口操作数据不碰底层结构。这样策略引擎可以独立测试也便于后续换成脚本引擎。报警同理越限判断发生在写值的那一刻在数据层内部但报警的记录与推送交给业务层。判断和执行分开后面做报警分级、推送渠道扩展都不用动数据层。3.5 展示层画面引擎展示层要做的事把配置文件里的画面在运行时重建出来并按数据源绑定实时值。关键在于画面与数据解耦画面文件里描述的是有哪些控件、什么位置、绑哪个点运行时解析后按绑定关系周期性去实时库取值这样改画面不用改代码甚至能做到画面文件热加载——编辑器里存盘运行态自动重新加载不用重启。调画面的时候这一点能省大量时间。四、分层的边界怎么划几条我们实际踩过之后总结的规则规则一数据层的接口要粗不要细。暴露读某个点写某个点就够了不要暴露内部数组。否则上层迟早会绕过接口直接操作数据。规则二跨层调用只允许相邻层。展示层不能直接调采集层采集层也不能直接刷界面。所有跨层的数据流都经过数据层中转。规则三加锁在数据层内部完成。上层调用写值接口时不应该关心锁——如果调用方需要自己加锁说明接口设计有问题早晚会漏加。规则四配置读取集中在一处。各个模块各读各的配置文件后期排查配置问题会很痛苦。统一由数据层在启动时加载其他模块从内存取。五、几个真实的取舍取舍一配置用文件还是数据库我们用 CSV 文件。理由是现场工程师能直接用 Excel 打开查看和批量编辑出问题能肉眼比对不依赖数据库也方便随包发布和版本管理。代价是编码、引号、字段内逗号这些都要自己处理跨文件的一致性校验要额外写代码。如果你的项目配置量极大上万点且频繁变更数据库可能更合适。取舍二单进程还是多进程我们用单进程多线程采集、存储、策略、界面全在一个进程内。好处是数据交换走内存没有进程间通信开销部署也简单一个可执行文件。代价是一个线程出问题会影响全局共享实时库的加锁面较大。如果现场对稳定性要求极高比如要求采集模块崩溃不影响界面多进程通过共享内存或消息队列通信会更稳但复杂度显著上升。取舍三实时库加锁粒度前面提过我们用每装置一把锁。这个选择在装置数量多、访问分散时优势明显但如果你的系统里存在大量遍历所有装置的操作多把锁会带来死锁风险与实现复杂度此时全局锁反而更简单可靠。没有普适的最优解只有与你访问模式匹配的解。小结工业上位机的分层说到底是为了回答一个问题当现场规模扩大、需求变更时改动会蔓延到哪里分层清晰的项目加一种协议只动协议层加一种报警只动业务层。分层混乱的项目改任何一处都要通读全部代码。几个关键决策回顾数据模型先定死装置—点、编号规则、映射层实时库放内存采集与存储解耦采集层拆成设备接口与协议解析两层加锁封在数据层内部上层无感画面与数据解耦配置驱动下一篇讲配置化设计为什么工业软件偏爱文件配置而不是数据库以及怎么把这条路走稳。
阅读完成 · 觉得有帮助?