我在前面学习将“运动控制功能块”移植到OOP中示例时,对于OO中如何实现不循环调用而实现运动控制功能的执行是迷糊的。故而只能再读PLCopen的官方文档,在浩瀚的文档中,终于找到了《 面向对象程序设计-使用指南V1.0》,给我豁然开朗的指导。该文档对OOP设计思想有很好的指导作用,值得读了再读,以领会其精要。当然,我也还没真正全面领会这篇文章,故对于“组合”的翻译是不当的,待日后领会深刻了再修正。
面向对象程序设计-使用指南V1.0
1、本文件简介
第3版的IEC61131-3标准使面向对象编程(OOP)成为可能。与此同时,PLCopen也定义了相应的功能块库的概念,包括OOP和PLCopen通用行为模型(请参阅PLCopen网站2017年5月4日发布的“创建PLCopen兼容库V1_0”),以及运动控制、安全和通信功能块集。
从一开始OOP就需要做出几个选择:是否所有功能块都在一个类中?以PLCopen运动控制功能块为例,我们是否需要AxisRef作为轴的引用,甚至MC_作为FB名称的一部分?它们如何包含方法?我们是只使用方法,还是也直接访问变量?轴的状态机是否由方法控制?是否所有轴对象都有方法,我们只能通过这些方法访问它们?接口如何?比起继承,我们更喜欢组合吗?
所有这些选择都会给不同系统的用户带来不同的外观和感觉、不同的培训指导和维护差异。而这正是PLCopen希望提供帮助和指导的地方,以创建更加统一的编程方法。
1.1 本工作组的目标
总体而言,关于如何将OOP用于工业控制或操作技术 (OT) 的信息很少。本小组的目标是提出以下建议:
- 在“经典”方法之外,指导如何使用OOP。
- 在不同的平台和实现中使用OOP时,提供相同的外观和感觉。
- 为工业控制编程创建通用设计模式。
- 在使用OOP的同时,也应使用传统的编程方式(例如,这可能意味着我们必须使用接口、方法、属性以及输入和输出来扩展传统的FB)。
例如,将使用锅炉例程来展示不同形式的编程。
2、第3版IEC 61131-3标准的OOP功能介绍
2.1 IEC61131-3第3版
现代编程环境在许多情况下都采用OOP,包括Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby和PHP。
IEC61131-3标准第三版的重要方面是集成了面向对象特性。这包括类(包括方法和接口)、功能块的 OOP 功能和命名空间。
IEC61131-3 编程语言标准第3版被批准为国际标准。这意味着该版本现已正式成为国际标准,可在www.IEC.ch网站上查阅。该标准与IEC 61131-3,2003(第2版)完全向上兼容。
加入这些扩展后,PLC代码就可以采用与几十年来使用高级语言开发(面向对象)软件相同的成熟概念和最佳实践来实现。这些概念使代码更易读、更模块化、更易于维护。此外,由于PLC变得更加现代化,并与上述高度分布式语言相似,因此可以吸引一大批软件工程师加入PLC开发领域。因此,PLC程序员也可以成为企业软件集中的一等公民。
2.2 IEC61131-3标准中的定义
与第2版相比,这些OOP扩展有一些变化或新定义。例如,程序组织单元最初定义为函数、功能块或程序,现在扩展了“class”类别。
功能块和类都可以包含方法。此外,它们还具有可继承性,这就分别产生了基础类型、派生功能块和派生类的概念。
这里列出了最重要的新定义:
名称 | 描述 |
---|---|
base type | 数据类型、功能块类型或类,并可从中继承/派生出其他类型。 |
call | 执行功能、功能块或方法的语言结构。 |
class | 一种程序组织单元包括:1. 数据定义 2.在数据结构上执行的一组方法(如子程序)。 类是一种实现–一种具体的数据结构和子程序集合–而类型则是一个接口。 |
derived class | 通过继承另一个类而创建的类。注1:派生类也叫扩展类或子类。注2:这当然与IEC61131-3-2003中定义的派生数据类型和派生功能块类型高度一致。 |
dynamic binding | 在运行时根据实例或接口的实际类型检索方法调用实例的情况。 |
inheritance | 根据现有的类、功能块类型或接口创建新的类、功能块类型或接口。 |
input variable | 变量,用于为除类以外的程序组织单元提供值。 |
instance | 与功能块类型、类或程序类型相关的数据结构的单个命名实体,在调用相关操作后仍保留其值。 |
interface | 语言元素,其中包含一组方法原型。 |
method | 类似于函数的语言元素,只能在功能块或类的范围内定义,并可隐式访问功能块实例或类实例的实例变量。 |
override | 关键字与派生类或功能块类型中的方法一起使用,用于使用新方法体的与基类或功能块类型中的方法具有相同签名的方法。 |
output variable | 变量,用于从程序组织单元返回值,类除外。 |
program organization unit | 功能、功能块、类或程序。 |
signature | 由METHOD名称及其所有参数(即输入、输出、输入输出变量和结果类型)的名称、类型和顺序组成的一组信息,明确定义了METHOD参数接口的身份。 |
2.3 OOP语言的差异
对比其他OOP语言,下表简要介绍了它们的共同点和不同点。
语言属性 | IEC61131-3 第2版 | IEC61131-3 第3版 | C++ | Java | C# |
---|---|---|---|---|---|
Multi languages | + | + | - | - | - |
OOP/procedural mixed | - | + | + | - | - |
Classes | ~(FB) | + | + | + | + |
Methods | ~(Actions) | + | + | + | + |
Interfaces | - | + | - | + | + |
Polymorphism多态 | - | + | +/- | + | + |
Semantic Reference | - | +(Interfaces) | - | + | + |
Constructor/Destructor | -/+ | -/+ | + | + | + |
Properties | - | - | - | - | + |
Dyn. Memory (“new”) | - | - | + | + | + |
Access control | ~(Variables) | ~(Variables) | + | + | + |
3、一般方法
3.1 通用的
PLCopen文件“面向对象使用指南”在IEC61131-3第3版的背景下,定义了一套目前普遍接受的良好OOP设计规则。
建议阅读文件《PLCopen软件创建指南:创建符合PLCopen标准的库》,2017年5月4日1.0版,可从PLCopen网站下载。作为其中的一部分,我们将在自动化技术环境中使用SOLID 原则。SOLID 代表:
- SRP:单一功能原则
- OCP:开放/封闭原则
- LSP:Liskov替代原则
- IPS:接口分离原则
- DIP:依赖反转原则
3.2 对象的模块和命令
在以OOP方式设计应用程序和程序库时,我们要区分模块和命令。
- 模块代表自动化应用的一部分,包括其软件功能,也可根据需要分层嵌套。例如,代码中可以用“模块”来表示执行器、传感器或机器组件。
- 命令令代表模块的具体操作。它被分配给一个模块或一组实现相同接口的模块。
图1:IModules和ICommand的表示法
每个模块都在实现一个通用的IModule接口。有些模块还实现了更专业的接口。例如,提供轴功能的模块实现的是IAxis接口。
每条命令都将执行通用的ICommand 接口,并遵循符合PLCopen标准的功能块设计准则。与轴相关的命令提供IAxis类型的输入,以获取它们将执行的相关轴实例的引用。
有了这种结构,就有可能将两个世界调和在一起。例如,经典的PLCopen运动功能块可以以传统形式重新使用。同时,这些功能块还可以通过现代接口进行控制。这为创新解决方案提供了新的可能性,例如在运行时重新配置应用程序。
4、基本示例:锅炉示例
4.1 概述
原始示例用于与OPC基金会合作在汉诺威博览会上进行演示。
原始代码被转换成符合PLCopen指导方针的格式,并以统一的功能块接口为基础。
下一步是将其转换为面向对象,然后添加错误处理程序和警报管理。
程序采用结构化文本编写,代码作为Codesys项目在PLCopen网站上可以找到。
4.2 应用说明
锅炉示例应用程序包括一个模拟锅炉,如图2所示:
图2:锅炉示例
4.3 经典程序风格的示例
该应用程序模拟了一个锅炉过程,该过程带有一个由流量控制(FC)和液位控制(LC)支持的基本PID控制器,包括一个用于干扰给料的随机发生器。
共有两个程序、4个用户派生函数和 17个用户派生功能块。
图3:POU布局
4.4 将程序转换为符合PLCopen标准的版本
根据SOLID第一条规则“单一功能原则”,设计结构已经非常合理。 每个硬件元件和每个新任务都有一个单独的FB。
遗憾的是,这个应用程序还没有按照OOP方法设计,因此没有遵守其他规则。没有接口,也没有抽象。所有FB都是直接在细节层面上编程和使用的。
有两种方法来调整这种应用:
- 根据 OOP 方法和规则重新设计应用程序。通过这种方式,可以实现特别好的架构。遗憾的是,这种解决方案非常耗时且昂贵,因此机器制造商很难获得管理层对全新一代软件的认可。
- 因此,我们尝试提出第二种部分优化应用程序的可能性。由于这一步起点较高,可以考虑。对于已经发展多年、嵌套过多的应用程序,最好还是依靠新一代程序。
最初的模块是从最底层进行编程的。第一步是实现构建模块的标准化和抽象化。PLCopen功能块库指南为此提供了清晰的界面和标准化的行为。该指南的精髓就是所谓的“行为模型”。因此,第一步是将原始程序(包括已定义的功能和功能块)转换为适用的行为模型,包括状态机和错误行为。PLCopen通用行为模型的OOP结构为我们提供了绝佳的机会,只需几个步骤和改动即可实现。
此外,我们还尝试使用 “开放/封闭原则 ”和“依赖反转原则”。此外,我们还定义了第一个接口,可用于未来的扩展。
由于模块可以通过已定义的行为模型进行扩展(在这种情况下使用的是LConC),因此它们可以获得一个抽象的更高层次,而不依赖于低层次的细节。LConC 还定义了一个接口,旧的FB必须实现这个接口。不过,在第一步中,只需将旧FB主体中的代码复制到循环动作方法中即可。
这样做的结果是,“开放/封闭原则”得到了遵守,程序块的功能得以保留,现有的软件代码也无需更改。这也让传统风格的软件开发人员有机会慢慢适应这种转换。由于FB仍然是1对1的,实现方式也没有重大改变。但模块现在可以扩展。此外,现在还可以在StartAction中监控不正确的用户输入和参数设置(如 PLCopen库文件中定义的边缘触发功能块中使用的那样),以提高稳定性。此外,模块现在有了一个抽象统一的接口,可以根据“接口隔离原则”进行进一步扩展。
图4:LConC的功能块接口和状态图
图5:不同设备的相同接口
通用行为模型的一致性设计使我们能够创建一个与所选状态机无关的抽象概念。与所有状态机一样,它也是从一个具有通用定义接口的抽象类派生出来的。
这就为在更高层次上以统一的方式管理FB提供了可能。例如,要实现一个能管理所有FB的错误处理程序,而不管它们的实现方式如何(接口隔离原则)
图6:仿真状态概览
请注意,这里的主程序不能用于实际应用,因为它缺少启动序列、关闭序列和错误处理。这部分是为每个应用程序定制的。这些补充内容将进一步显示OOP为重复使用而带来的好处。
4.5 添加错误行为
例如,错误行为被添加为一个中央错误处理功能块,可跟踪所有使用过的功能块。这有两种方式:通过继承和通过组合。
我们不会直接使用不同的具体FB,而是使用我们的抽象接口IBehaviourModel(见图5)。该接口允许访问功能块的输出,包括xDone、xBusy、xError、xAborted 和 iErrorID。这就是 “接口隔离原则”的理念,有了它,现在就可以连接通用模型库中的所有FB。这并不是只对应一种FB类型,也不是只对应一种行为模型。所有已描述的行为和类型都可以连接起来,而无需对ErrorHandler进行任何调整。
我们的想法是将所有功能模块映射到一个数组中,这个数组包含在错误处理程序中,以便对所有功能模块进行迭代。在这个数组中,我们将没有指向不同FB的详细指针,而只有基本接口IBehaviour Model。通过GetModelState方法,我们可以监控所有FB的所需信息。请参见图8:带有设备(FB)列表的错误处理 FB。
错误处理程序有两种方法将其与设备(FB)连接起来:AttachDevice和DetachDevice。错误处理程序有这两个用于attach和detach的方法,反映在设备列表中。错误处理程序引用该列表(见图 10:错误处理程序的结构)。
现在,我们可以使用相同的GetModelState方法监控所有FB,并建立一个包含状态信息的结构,以便查看所有功能块的状态。
METHOD AttachDevice
(* Register a Device by the BehaiourModel Interface*)
VAR_INPUT
(*Device which will be monitor from Error Handler*)
IDevice:CBML.IBehaviourModel;
END_VAR
VAR
uiLoop:UINT; //Help Variable for the For Loog
END_VAR
//go to the next free input in the array and save the Interface
FOR uiLoop := 0 TO GVL_Const.MAX_Observer DO
IF IDevice <> 0 AND THIS^.aritfObserver[uiLoop] = 0 THNE
THIS^.aritfObserver[uiLoop] := IDevice;
END_IF
END_FOR
图7:错误处理程序的AttachDevice方法
// Error Hander which will be monitor all FBs whice has an behaiour
FUNCTION_BLOCK FB_ErrorHander IMPLEMENTS I_ErrorHandlerSubject
VAR
//Help Variable for Attaching and read Information from Devices
_stInfoState :ARRAY[1..GVL_Const.MAX_Observer] OF tsCBMLInfo; //State from all existing FB
aritfObserver :ARRAY[1..GVL_Const.MAX_Observer] OF CBML.IBehaviourModel; //Interface list from all FB which will be monitor
uiLoop :UINT; //Help Variable to get access to the Interface during a For Loop
END_VAR
//monitor all register Device by the IBehaviourModel
FOR uiLoop := 0 TO GVL_Const.MAX_Observer DO
IF aritfObserver[uiLoop] <> 0 THEN
aritfObserver[uiLoop].GetModelState(xCommit := _stInfoState[uiLoop].xCommit,
xDone =>_stInfoState[uiLoop].xDone,
xBusy => _stInfoState[uiLoop].xBusy,
xError => _stInfoState[uiLoop].xError,
xAborted => _stInfoState[uiLoop].xAborted,
iErrorID => _stInfoState[uiLoop].iErrorID,
eState => _stInfoState[uiLoop].eState);
END_FOR
图8:带有设备(FB)列表的错误处理FB
Attach和Detach方法的定义在一个接口中完成,该接口由错误处理程序实现。现在,Boiler可以使用该接口将所有实例附加到错误处理程序。
FUNCTION_BLOCK FB_Boiler
VAR_INPUT
(*Adjustable value*)
rSetFillLevel :REAL;
rSteamDemand :REAL;
itfErrorHandler : I_ErrorHandlerSubject;
END_VAR
IF NOT bInitDone THEN
bInitDone := TRUE;
Pipe1001.xEnable := TRUE;
Drum1001.xEnable := TRUE;
Pipe1002.xEnable := TRUE;
LC1001.xEnable := TRUE;
CC1001.xEnable := TRUE;
FC1001.xEnable := TRUE;
IF itfErrorHandler <> 0 THEN
itfErrorHandler.AttachDevice(IDevie := Pipe1001);
itfErrorHandler.AttachDevice(IDevie := Drum1001);
itfErrorHandler.AttachDevice(IDevie := FC1001);
itfErrorHandler.AttachDevice(IDevie := LC1001);
itfErrorHandler.AttachDevice(IDevie := Pipe1002);
itfErrorHandler.AttachDevice(IDevie := CC1001);
END_IF
END_IF
图9:在FB_Boiler中的错误处理器初始化
TYPE tsXBMInfo
STRUCT
xCommint :BOOL;
xDone :BOOL;
xBusy :BOOL;
xError :BOOL;
xAborted :BOOL;
iErrorID :INT;
eState :CBML.STATE;
strName :STRING;
END_STRUCT
END_TYPE
图10:错误处理程序的结构类型
现在,我们可以在主程序中根据错误处理程序创建一个实例,并将错误处理程序连接到锅炉实例:
PROGRAM MAIN
VAR
fbBoiler1 :FB_Boiler;
fbInput1 :FB_InputParameter;
fbErrorHandler :FB_ErrorHandler;
bInitDone :BOOL := FALSE;
test :FB_DeviceBasic;
END_VAR
IF NOT bInitDone THEN
P_Initialisation();
bInitDone := TRUE;
END_IF
counter := counter + 1;
(*Get Input*)
fbInput1();
(*Simulater Boiler*)
fbBoiler1(
rSetFillLevel := fbInput1.rFillLevel,
rSteamDemand := fbInput1,rSteamDemand,
itfErrorHandler := fbErrorHandler);
(*Monitor all Instance by IBehaviourModell*)
fbErrorHandler();
图11:带有错误处理功能的主程序
图12:一个FB的错误信息示例
错误处理程序现已成为所有FB的标准组件。它可以集中处理所有设备,而无需单独调用所用FB的所有实例,同时还能集中读取FB的所有相关信息。
这一概念使得仅用一个列表控制所有功能块变得更加容易。如果采用其他方法,则必须连接所有FB并指向所有不同的可用功能块。现在只需一个接口和一个列表,就可以控制所有列出的功能块。
图13:架构概览
图13:架构概览中的模型显示,LConC(及其接口)继承自BehaviourModelIBase,因此也继承自接口IBehaviourModel。
I_ErrorHandler接口与演示中使用的所有FB以及FB_ErrorHandler相连。这样,ErrorHandler就可以连接到所有适用的FB。
使用继承作为FB的基础设计提供了一个很好的抽象控制选项,但也显示出一些局限性。我们无法再在抽象层上扩展详细的设备FB。造成这种限制的原因是,我们无法在抽象层BehaviourModelBase上使用继承,因为它已经使用了继承。
这就是我们要检查的一点,即我们是否可以通过对行为模型使用组合而不是继承来优化设计(参见5.1《面向对象编程指南》的基本原理–注意事项)。为此,我们必须说明如何将行为模型定义为FB的一个成员,但以与以前相同的方式定义不同状态的方法。(开放/封闭原则)。
通用行为模型的一个功能可以帮助我们实现这一设计。库中的FB支持动作控制器的连接(见图16中的红线),它允许实现IActionControl的FB进行方法调用,而不是使用行为模型中的方法。
现在的想法是创建一个新的抽象设备基类,它的新功能是将FB的“名称”作为一个完整的路径。为了支持行为模型的合成,我们的基类实现了IActionControl。为了遵循“开放/封闭原则”,建议同时实现IActionProvider 接口。这样,详细的设备FB就能以与以前相同的方式与行为实现一起工作。
在“ActionController”的“方法”中,可以调用“ActionProvider”的“方法”。
下面的示例展示了ControlCyclicAction 方法的情况:
图14:与IActionControl关联的循环操作示例
基类还将获得一个用于“get”和 “set” IBehaviourModel的属性。在设置器中,IActionControl将直接链接到已创建的行为模型实例。
图15:执行行为模型的 “Set”操作
详细的FB现在可以决定使用哪种行为模型。唯一适用的更改如下:
- 将LConC的继承改为我们自己的基类。
- 根据您决定的行为,为xEnable 或 xExecute创建一个输入变量。
- 根据要使用的行为创建一个实例。
- 更改主体中模型的执行,并将成员行为实例赋予属性。
请看下面的示例:
FUNCTION_BLOCK FB_OutputPipe EXTENDS FB_DeviceBasic
VAR_INPUT
rSteamDemand :REAL;
xEnable :BOOL;
END_VAR
VAR_OUTPUT
eErrorID :ERROR; //Error code describing error condition
rSteamFlow :REAL;
END_VAR
VAR
FT1002 :FB_SteamFlow;
StateBehaviour :CBML.LConC;
END_VAR
THIS^.IBehaviourModel := StateBehaviour;
StateBehaviour(xEnable := xEnable,xBusy => xBusy, xError => xError);
图16:设备FB所需的更改
为了将我们的FB附加到错误处理程序,我们使用了我们的属性,这样错误处理程序的接口仍然可以保持不变。
IF NOT bInitDone THEN
bInitDone := TRUE;
Pipe1001.xEnable := TRUE;
Drum1001.xEnable := TRUE;
Pipe1002.xEnable := TRUE;
LC1001.xEnable := TRUE;
CC1001.xEnable := TRUE;
FC1001.xEnable := TRUE;
IF itfErrorHandler <> 0 THEN
itfErrorHandler.AttachDevice(IDevie := Pipe1001.IBehaviourModel);
itfErrorHandler.AttachDevice(IDevie := Drum1001.IBehaviourModel);
itfErrorHandler.AttachDevice(IDevie := FC1001.IBehaviourModel);
itfErrorHandler.AttachDevice(IDevie := LC1001.IBehaviourModel);
itfErrorHandler.AttachDevice(IDevie := Pipe1002.IBehaviourModel);
itfErrorHandler.AttachDevice(IDevie := CC1001.IBehaviourModel);
END_IF
END_IF
图17:对所连接设备的相应更改
该架构如图18所示:新设计的UML图。它现在的优点是,我们可以使用新的抽象FB以一致的方式添加与所有FB相关的新功能。同时也避免了每个FB中的代码冗余。此外,它还提供了通过属性BehaviourModel从外部注入行为模型的可能性。
图18:新设计的UML图
在此基础上,很容易创建其他功能,如警报列表,如4.6添加系统警报列表。
4.6 添加系统警报列表
需要区分过程警报和系统警报,以便为不同用户(如油罐操作员和系统管理员)过滤警报。本示例处理的是系统报警,但过程报警也可以以类似方式创建。甚至还可以用ProcessAlarm 这样的输出扩展FB,并单独处理这些警报。
要创建错误列表,最好能知道错误发生在哪个FB中,甚至是完整的实例路径。
为此,我们在抽象基类中实现了名称和访问名称的方法。名称由Codesys开发系统的一项功能生成,但也可以通过属性进行设置,例如在BoilerFB的初始例程中。使用直接描述方法的接口符合接口隔离原则。
要访问该方法,我们需要为错误处理程序创建一个新的或扩展的接口。改变接口通常不是最好的方式,但在这种情况下,我们可以直接实现接口,使其只包含必要的信息。在本示例中,这就是GetModuleState和GetName方法。因此,调整Attach/Detach方法和接口阵列中的接口,使其不使用IBehaviourModel,而使用新的接口IBasicElement,是非常有意义的。方法GetModelState将把请求委托给内部设置的状态机,如下所示(注:图4:LConC的功能块接口和状态图中定义了 ERROR 类型):
METHOD GetModelState :ERROR
VAR_INPUT
xCommit :BOOL;
END_VAR
VAR_OUTPUT
xDone :BOOL;
xBusy :BOOL;
xError :BOOL;
xAborted:BOOL;
iErrorID:INT;
eState :CBML.STATE;
END_VAR
IF _iBehaviourModell <> 0 THEN
_iBehaviourModell.GetModelState(xCommint := xCommit, xDone=>xDone,xBusy=>xBusy,xError=>xError,xAborted=>xAborted,iErrorID=>iErrorID,eState=>eState);
END_IF
图19:GetModelState的委托执行
其实现方式是,所有发生的错误都以字符串数组的形式呈现,用户可以读取。
图20:错误处理程序的字符串数组
图21:错误字符串示例
图22:在错误信息中创建字符串的示例,显示了作为错误处理一部分的一个小函数,该函数仅在xError输出的上升沿和下降沿将错误转换为清晰字符串。
IF NOT _stErrorState[uiLoop] AND _stInfoSte[uiLoop].xError THEN
_stErrorState[uiLoop] := _stInfoSte[uiLoop].xError;
//Build Error String
FOR uiHelp := 10 TO 1 BY -1 DO
IF uiHelp > 1 THEN
astrActive[uiHelp] := astrActive[uiHelp -1];
ELSIF uiHelp = 1 THEN
astrActive[uiHelp] := CONCAT(CONCAT(CONCAT('In FB',_stInfoState[uiLoop],strName),'the following error is active:'),FC_ErrorCode(_stInfoState[uiLoop].iErrorID));
END_IF
END_FOR
ELSIF _stErrorState[uiLoop] AND NOT _strInfoState[uiLoop].xError THEN
_stErrorState[uiLoop] := _strInfoState[uiLoop].xError;
//Build Error String
FOR uiHelp := 10 TO 1 BY -1 DO
IF uiHelp > 1 THEN
astrActive[uiHelp] := astrActive[uiHelp -1];
ELSIF uiHelp = 1 THEN
astrActive[uiHelp] := CONCAT(CONCAT(CONCAT('In FB',_stInfoState[uiLoop],strName),'the following error is active:'),FC_ErrorCode(_stInfoState[uiLoop].iErrorID));
END_IF
END_FOR
END_IF
图22:在错误信息中创建字符串的示例
结合函数FC_ErrorCode,可将iErrorID转换为字符串(见图23:函数FC_ErrorCode)。
FUNCTION FC_ErrorCode:STRING
VAR_INPUT
iErrorID :INT;
END_VAR
VAR
END_VAR
CASE iErrorID OF
GVL_ErrorDef.Error_CC :FC_ErrorCode := 'Error Control Computation';
GVL_ErrorDef.Error_Drum :FC_ErrorCode := 'Error Drum';
GVL_ErrorDef.Error_FC :FC_ErrorCode := 'Error Flow Controller';
GVL_ErrorDef.Error_LC :FC_ErrorCode := 'Error Level Controller';
GVL_ErrorDef.Error_Motor:FC_ErrorCode := 'Error Motor';
GVL_ErrorDef.Error_Pipe :FC_ErrorCode := 'Error Pipe';
GVL_ErrorDef.Error_Valve:FC_ErrorCode := 'Error Valve';
END_CASE
图23:FC_ErrorCode函数
改进设计的一个可行方法是改变ErrorHandler循环检查所有FB的方式。一种方法是在这里使用观察者模式(Observer Pattern),即ErrorHandler作为观察者连接到设备,一旦状态发生变化,设备就会发出通知。
5、补充参考资料
5.1 OOP的基本原则–考虑因素
在本文件中,应考虑到一些关键方面:
- 设计好但性能差
为了不对性能造成太大影响,应考虑哪些细节? - 继承和组成 – 继承如何削弱封装性
许多德高望重的 OO 设计师都曾说过,只要有可能,就应该优先考虑组合而不是继承,只有在必要时才使用继承。 然而,这种说法有些言过其实。只要有可能就应使用组合的说法掩盖了实际问题。经验表明,在大多数情况下,组合比继承更合适。在大多数情况下,组合似乎更合适,但这并不意味着要避免使用继承。继承和组合这两种方法都有其合理性,但要在适当的情况下使用。 - 避免依赖性
在运行时引入依赖关系(依赖注入)在讨论组合是否应优先于继承时起着核心作用。需要注意的是,这只是一个讨论。讨论的目的并不一定是找到设计类的 “最佳”方法,而是就如何充分处理继承或组合的决定提供一些思考材料。
5.2 性能注意事项
随着体系结构的变化,重要的是不要忽视PLC与PC/服务器系统相比的特殊情况。
总的来说,有三个角色还可以在OOP的应用方面做得更多一些:
- 硬件设计
目前,我们可以看到运行时间的测量结果存在很大差异。根据硬件的不同,与普通的子程序调用相比,虚拟方法调用所需的时间要长得多,或者几乎不重要。当然,在循环执行的PLC操作中,运行时间的少量延长往往非常重要。这里的决定性因素是数据/代码存储器与地址和数据总线的连接,以及适当的缓存机制的可能性。尤其是高速缓冲存储器可以帮助我们在循环操作中节省时间。如今,在设计控制器时,问题不再仅仅是1024条IL指令需要多少时间! - 编译器设计
PLC的代码生成器必须确保应用程序的代码部分与附近的高速缓存结构相关。这样,循环使用的代码和数据就能得到快速处理,而无需从主存储器重新传输。接口、方法和属性以及引用的实现必须适应PLC的需要。因此,必须尽早充分考虑在线更改和调试等用例。在使用方法时,堆栈(局部变量)上的内存分配不得造成问题。大量数据结构的初始化可能会造成问题(例如,在循环调用时),因此应由用户进行相应的控制。 - 软件设计
必须有意识地使用ABSTRACT、FINAL、PRIVATE…等关键字。这样,开发人员就可以在早期阶段影响软件运行时的重要方面。 开发人员必须注意是否以及何时必须对局部变量进行初始化。PLC开发环境应提供防止初始化一组特定变量的方法,这样可以节省 “大量”时间。
5.3 进一步调查说明
- 同步与非同步通信/运行时环境
- 我们是否应该在经典控制领域引入动态内存分配?
- 对已有的实现方式和编程风格不作任何偏好。
- 编程范式的转变可能是不可避免的。