工控软件控制系统设计

本文详细探讨了工控软件的特点,包括环境不可靠、多线程应用、业务流程管理与执行的模式化,以及如何处理实时数据交换与资源管理。重点介绍了各种流程控制如乱序、顺序、分支和循环流程,并强调了关键接口和数据类型转换的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

  工控软件一般指的是工业自动化软件、数据采集软件。工控软件更接近于游戏软件,与办公软件有着根本区别。

  工控软件的特点是:环境不可靠、无法简单地按MCV划分、总是需要根据硬件进行调整、不更新、不维护。

  工控软件与游戏软件的相同点为:原始命令简单但存在同时处理多个命令的情况、软件按设定的帧率循环运行(除定时器外几乎没有什么操作系统意义上的中断事件)、需要内置人工智能、几乎没有什么框架可选(甚至更少)、世界观是直观形象的。

  工控软件与办公软件的相同点为:简约的表现方式、重大的责任、几乎不需要配音、历史记录可能会很长、具有一定程度的定制性(类似于企业级办公系统)、长期运行。

  大多数软件开发框架都是基于办公软件的,其中的大多数是基于WEB环境的。这当然是因为对办公软件开发的需求。造成这种需求的原因是因为办公软件的门槛更低、业务逻辑无客观规律的约束、外观上的变化多、同一个企业出于各种不能说的目的可能会在几年内就更换另一款办公系统。

  所谓框架,就是找到一系列软件开发过程中的共同部分,并将其归纳为一个封闭的程序包,使一切符合此规律的软件开发过程都不再需要变更公共代码。

  工控软件之间的共性较少,这也使得共性部分与个性部分的区分更加明显。首先我们将画面部分排除在共性之外。即使是工控软件,画面也仍然有着多种风格。事实上画面控件的设计是无穷的,只有最基础的API才能作为其共性。本文所讨论的是控制系统部分,相信要比现有的工控软件设计思路更强大不少。

  控制系统的设计可以追述到PLC的设计。这种最原始的工控软件流程图(系统-采样-运算-驱动)体现了所有控制系统必需的四要素。这种原始的单一线程流程已经可以很完美地解决一般设备级别的控制。然而对于大型多任务控制系统,单片机的性能已经无法胜任,需要使用通用计算计算机的多线程功能才能实现。

多线程技术在工业控制系统中的应用模式化

业务块的划分

  一个模式化的软件总是由框架和业务两部分组成的。框架对业务进行管理、业务则实现所需要的功能。要设计出能对业务进行管理的框架,首先要了解业务有哪些。软件的功能是无限的,业务的种类也是无限的,没有一种框架可以兼容未知的业务类型。业务类型首先可以分为流程管理与流程执行。流程管理目前已有7种实现,按出现的时间顺序它们分别是:乱序流程、顺序流程、可编程分支流程、循环流程、定时流程、单次流程、二叉分支流程。流程执行目前有简单执行、三态离合器、简单状态机、三态离合状态机。

公共接口

  业务代码公共接口必须唯一,不论是流程管理还是流程执行,都实现了同一个共同接口,该接口只有一个方法,以供定时器调用。

流程管理

  流程管理是对下级流程执行顺序的管理程序。它将下级业务收集到一个可遍历容器中,每次调用时按照流程管理逻辑嵌套调用下级的公共接口方法。

乱序流程(parallel)

  乱序流程是基于多线程的,它应该具备线程数量设置的功能,使用指定数量的线程或自动创建线程以一个不可预测的顺序遍历所有下级业务。

顺序流程(serial)

  顺序流程是最简单的流程。它的流程管理逻辑是在当前线程中按顺序遍历所有的下级业务。

可编程分支流程(switch)

  可编程分支流程是最复杂的流程,它是顺序流程、乱序流程与一个解释器的结合体。它可以根据输入的顺序和线程数量,按指定的顺序以单线程或多线程嵌套调用或略过下级业务。它也可以在本体的一次执行时,重复调用某些下级业务多次。

循环流程(loop)

  循环流程在一次调用时,按顺序重复调用下级业务,直到得到停止信号时,待当前调用中的业务完成后退出。

定时流程(ticked)

  定时流程并不会在每一次本体被调用时遍历下级业务,它有一个内置的定时器,只有定时器给出信号后,待本体被调用时才分调用下级业务。

单次流程(once)

  单次流程只在SCADA循环启动后的第一个循环内执行,跳过之后的所有循环。

二叉分支流程(if|ifnot)(else|elseif|elseifnot)

  二叉分支流程是简单的选择性分支,相比于可编程分支流程,它的程序非常短小。另外它还有正、反逻辑,而可编程分支流程没有反逻辑的能力。但是二叉分支流程只能以当前线程执行,不可以改变执行的顺序。

  二叉分支流程为了实现二叉分化,需要实现副分支关键字、扩展分支关键字。流程的算法为如果使能变量为使能,则执行下一个子业务直到遇到一个分支关键字。如果使能变量为不使能,则查找下一个分支关键字,直到出现一次使能信号或达到流程尾部。

简单执行(leaf)

  一个简单执行器是公共接口的简单封装,它只需要实现公共接口的抽象方法,运行于当前线程中。

三态离合器(asynchronous leaf)

  三态离合器有串行、并行同步、并行异步三种状态,类比于法兰直连、离合器闭合、离合器分离的运行方式。其中串行方式与简单执行无异;并行同步方式的运行时序为每调用一次三态离合器就从交接点的位置继续运行,直到下一个交接点暂停切换到父线程,保证一次调用的过程中被唤醒的业务线程数量总为1个;并行异步方式的运行独立于父线程,业务线程数量为2,父线程不等待而直接退出。

  由于串行与并行的业务代码调用不在一个栈中,所以无法在串行与并行之间无缝切换。并行同步与并行异步是工作在同一个栈中的,所以可以无缝切换。其中同步方式下允许使用线程交接,这在一些需要阻塞等待信号的情况下非常好用;而异步方式不阻塞父线程,适合于重任务。

简单状态机(state machine leaf)

  简单状态机是在简单执行的基础上增加了一个状态变量和switch语句。

  简单状态机与三态离合器并行同步工作模式都可以实现状态法,但简单状态机是经典状态法的实现,它基于switch语句,通过状态变量决定走哪条分支或不执行。三态离合器是环式状态机,它的状态切换是在一个个小循环中切换的,没有一个专门的状态变量。

三态离合器状态机(asynchronous state machine leaf)

  三态离合器状态机是三态离合器的执行方法中调用经典状态法的实现方式,由于三态离合器会生成子线程,如果将状态机置于调用关系上层,三态离合器置于调用关系下层,则每一状态下都将产生不同的线程,无法实现线程交接。只能将三态离合器置于上层,状态机置于下层。

子系统(child leaf)

  子系统是一个完全复制了SCADA系统的三态离合器,因此它可以支持单线程调用和多线程调用。另外,子系统可以与父系统共享一个数据容器、也可以使用自己的局部容器并对其中部分字段按需要进行同步。

实时数据交换

  在各业务代码彼此独立设计的前提下,它们不可能拥有相同的成员变量,然而业务模块之间的数据如果无法传达,就和各自独立的小程序一样,框架也就没有意义了。关于这个问题,首先参考一下WEB是如何解决多页面数据传送的问题的。WEB程序可以采用以下几种数据缓存:Attribute,Property,Session,Cookie,Database,File System,Global Variants。其中Attribute为全局容器、Property为页面调用时的形参变量、Session为该会话的缓存容器、Cookie为长时页面历史记录、Database为可跨服务器的数据库、File System为本机访问的文件系统资源、Global Variants为程序中自定义全局变量。

  其中Property、Session、Cookie都限制于有限范围的业务,肯定无法用于各业务代码的数据交换。Database存在配置复杂、性能差、兼容性不佳、需要维护、消耗硬盘寿命的问题,不适合保存临时数据。File System和Global Variants则不易于代码管理,变量一多就很难知道哪些是在用的变量,哪些是垃圾数据。因此,Attribute是最好的选择。

  一个用于实时数据交换的Attribute应当具备必要的数据类型和数据类型转换的能力。它的操作类似于WEB程序中的Attribute,是以键值对的方式进行增删改查的。但是工控软件的数据主要是Number和Boolean类型,如果都以Object方式保存,则很不方便,但是Object类型是必须的,不能只做Number和Boolean接口。此外无数据和空数据也应当做一个区分以备不时之需。那么这个Attribute就需要具有四种值的类型:Boolean、Number、Object、Null,并有一个键不存在的编程语言所定义的关键字null(或nullptr、0、None)

Boolean类型

  Boolean,在不同编程语言中或有其它定义和拼写,在工控软件中表达开关量(亦称“数字量”)。该类型的合理值为true、false、null。但是由于合作的其它系统的程序员未必那么细心,甚至于一些合作的控制系统的设计理念不一致,有时候这些值会以字符串的形式或数字的形式传入本系统。如果对每一个Boolean都做一次类型判断,那将增加非常可观的验证代码。因此对其做隐式转换就非常有意义了。

  根据使用量非常大的C语言(事实上一些其它语言如Python、Matlab在这方面也是一致的)的设定,二进制数字的每一比特都是0时,它代表false,反之代表true。这一规则可用于数值向开关量的转换。通常不会遇到用小于1的小数或负数表示开关,如果需要,根据情况进行调整。比如逻辑运算有可能产生接近0的数值代表false,这时就应该选择一个阀值而不是直接转二进制用与逻辑判断了。但最好还是在逻辑运算程序中直接给定true或false。

  由于Boolean类型是表达开关状态的,当使用字符串表达时,就有多个单词可以表达同一状态。以字符串表达的数字应转换成数字后再处理。字符串"on","yes","true"都表示true,字符串"off","no","false"都表示false。其余为null。也可根据情况增加一些单词。

Number类型

  Number,一般用于表示模拟量和计数器,实际类型常常是long和double,有时那些从32位系统走过来的老师傅也会用short、int、float。水电计量和计数器之类的数据可能会需要使用 BigInteger、BigDecimal。同样存在一些不规范的合作软件会返回字符串来表达一个数值。另外BigDecimal没有一种规范的二进制表达方式,一般都是用字符串代替。因此Number类型也需要一个隐式转换。

Object类型

  Object类型常用于保存字符串,也用来保存容器,共享的工具对象、GUI控件。Object不需要隐式转换,所有数据都是Object对象。

  有时一个业务太复杂,需要设计成多个业务模块协同工作。这时可以用一个容器将它们联系起来。这种设计模式即称为“总线模式”。比如Modbus下位机程序,它有多种类型的接口,如果都做在一起就非常复杂,可以将不同类型的接口分别做成扩展模块,用总线跟通信模块连接,实现Modbus下位机的功能。多进程调用也存在输入管道、输出管道、进程管理、命令行编码和解码几个部分,将它们分开做会更简单。

Null类型

  Null类型是一个占位符,表示这个位置被占用,但没有定义一个有效的数据。Null类型和空指针具有相似的逻辑意义,但Null类型可以很方便地占用一个空间,而空指针的处理有时会变成删除这个位置。占用一个当前不需要的位置非常重要,程序每一次循环使用到的键都是一样的,没有必要删除掉这些位置。而假如每次一个位置不被需要时就删除它,将会改变Attribute的内部结构,导致无意义的重组。用C++编程时,我们通常使用红黑树实现Attribute,因为C++的HashMap不怎么好用。即使Java的HashMap性能较好(得益于Java中的String数据是常量,hashCode方法的执行非常快速),它的底层仍然有红黑树或链表结构。对链表中数据的增加和删除计算量较大。明知下一次这个位置很可能又被要用上的情况下,还去删除它是不明智的。

空指针

  空指针是程序中必然存在的一个值,它表示这个位置不存在。一些业务代码的输入端子可能被设计成支持无连接。无连接状态下这些输入端子将得到一个空指针。无连接的情况下,没有必要给这个键增加一个占位符,因为这个位置肯定是不会使用的。

资源共享

可回收工厂模式

  工控软件周期运行的特性使它需要频繁访问一些外部共享资源。主要是SQL数据库。SQL数据库一般用JDBC连接,建立连接时会创建一个JDBC对象,而JDBC对象是可以重复使用的。建立一个可回收工厂,当仓库中无JDBC时调用出货方法,它将生产一个JDBC。暂时不用的JDBC对象可以退还给仓库。如果仓库中有JDBC对象,则下一次出货将给出这个JDBC对象。

  实际应用中还需要加入自动销毁库存的功能。因此可回收工厂有三种:强引用可回收工厂、软引用可回收工厂、弱引用可回收工厂。

SOCKET唯一性

  同一个监听IP和端口的组合不宜产生两个不同的SOCKET连接。因为有些DTU设备对多路连接的处理有漏洞,会把一个会话的应答返回给另一个会话,甚至对UDP通信也是如此。需要对其进行存在性检查,防止产生多个指向同一目标的SOCKET。另外要注意添加关于SOCKET的线程锁。

后记:为什么写得这么草

  本文是一个在用工程的设计思路,目前已做了近十年的研究,但本文没有提到任何代码(一些旧版本的代码有缘人可能在网上找得到,但肯定没几个人看得懂)。这是因为该SCADA系统较复杂,写成一本书也没人看得懂,但只要有这个思路在,任何人都可以设计自己的一套SCADA或运动控制程序。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值