前面几个例子,虽然抽象的味道越来越浓,但功能都比较单纯,因此接口也容易构建。本节我们面对一个稍微复杂一点的例程:信号复归。
在微机保护装置中,发生保护动作后必须进行人工干预,只有排除故障后才可以重新投入运行。为了确保这一过程,发生保护动作后,相关led状态或类似信号等都会处于自保持状态,为了清除这些状态信息,需要执行一个复归命令,称之为信号复归。
早期,信号复归主要用于清除动作led灯的状态,如我们会发现很多国外产品中信号复归写作ledRst。目前随着微机保护功能越来越多,也越来越智能化,信号复归功能已经参与到诸多保护逻辑中,为了保持逻辑上的清晰,我们团队内部习惯将信号复归直译作SignalRst。
信号复归本身功能很简单,但麻烦在于同很多模块关联耦合在一起。信号复归有多种来源,最简单的就是继保设备前面板信号复归按键,通过按键触发信号复归。但如果一面屏柜上有很多台继保,逐一按键操作也比较麻烦,此时惯例在整个屏柜上统一放置信号复归总按钮。因为该信息一般是通过开入信号传入继保设备的,因此常称之为开入信号复归。
目前很多电力系统变电站都是无人值守的,故障后还需要到现场去执行信号复归显然不可取,必须远传可控,因此又诞生了第三种信号复归入口:通讯信号复归。
汇总后,我们发现至少有三种类型入口:设备本身信号复归、开入信号复归和通讯信号复归。
信号复归不仅输入源头多,发生信号复归命令后,需要执行的功能更多,如需清除保护LED状态,收回一些自保持出口状态、清除通讯远传保护状态、关闭液晶报告弹屏界面,当然更多的是参与到各种复杂的保护逻辑中,如备自投逻辑中为下一次动作做好准备等。
◇◇◇
分析信号复归的需求,我们发现信号复归功能本身并不复杂,但麻烦在关联模块比较多。此时,会出现一个尴尬的局面,每增加一个模块,都需在信号复归模块内部做一点修改,如下图示意:
如增加新模块同信号复归输出存在关联,增加该模块后,需要在信号复归模块中修改多处位置。类似这样的情况持续积累,每增加一个模块,可能需要在多个类信号复归模块中做相应修改,各模块之间耦合开始增加。
记得早期我维护过一款产品,每增加一个规约模块后,必须同步修改的地方竟有几十处。为了让大家少犯错误,项目经理写了一份详细文档用于描述规约修改过程,甚至作为新人年底考核内容。但即使如此,依然挡不住大家持续不停的犯错误。
以前和大家提及,软件本身有强烈的耦合特性,如果不加控制,即使一开始模块分割的比较清晰,也容易在迭代中慢慢的耦合成一团乱麻,然后让新人上不了手,老人脱不开身,进而引发管理灾难。希望通过这个例子能让大家体会一二。
为了去耦合,我们期望每增加一个模块后,仅新增模块代码处发生变化,其他地方(如信号复归模块)不需要改动。如下图示意:
为了做到这一点,策略其实很简单,有两种简单且常见的策略:
1. 回调函数机制
2. 消息机制
回调函数机制常用于前后台系统中。采用回调函数机制时,需要信号复归模块提供一个注册函数接口,并在内部维护注册函数管理数组或列表,在发生信号复归命令后,依次调用所有注册接口函数,新增模块仅需在初始化时调用信号复归注册接口函数。通过简单的回调函数机制,我们可以将所有的改动集中在新增模块内部。
采用回调函数机制存在一个缺点,新增模块的接口函数是在信号复归环境中执行的,如果是前后台系统不存在问题,但基于os调度机制的执行环境中,会引入同步互斥问题。因此基于os,一般选择采用消息机制,相当于异步的回调函数调用。此时,回调函数注册变成了消息注册,回调函数调用变成了发送消息,消息函数在各任务内部执行,回避了同步互斥问题。
◇◇◇
完成信号复归的输出部分抽象后,我们再来分析信号复归的输入源抽象。
信号复归有三种输入源:设备按键、开入和远方通讯。这三种输入源中最简单的是远方通信,因为其功能固定,因此可简单的构建一接口函数让通讯规约调用即可。接口函数如下:
void apiSignalRstTrip(void);
通过按键和开入为何不能调用该接口函数触发信号复归命令呢?关键在于按键和开入是可变项。
继保设备一般在面板上有单独的信号复归按键,通过该按键即可触发信号复归命令。在一个地铁现场,用户认为这样的操作不严谨,要求增加限制条件:需按下复归和enter组合按键且持续一秒以上的时间,才可以触发信号复归命令。
碰到这个需求就比较尴尬了,做特殊工程版本吧,会导致程序版本混乱,增加额外配置选项,代码被分割,程序比较混乱。有没有更好的策略呢?实际上最佳的策略就是按键模块和信号复归模块解耦,分别提供外部可控制的接口,并用脚本连接。
微机保护行业中,最早采用这种设计理念的是北美的sel保护公司。sel保护为每个按键(包含常用组合按键)提供一输出布尔量,如signalRstKey等,为信号复归提供一输入布尔量signalRstIn,然后通过维护软件设置如下表达式:
signalRstIn = time(signalRstKey, 1000, 0); //按键持续1000ms后才置位signalRstIn
此时,大家有没有惊奇的发现按键模块和信号复归模块完全解耦了,按键模块仅需处理signalRstKey,而信号复归模块仅需在signalRstIn置位时调用apiSignalRstTrip()即可。
类同按键,开入作为信号复归源最大的问题在于难以确定各现场使用哪路开入作为输入源。解耦后就比较简单了,建设某现场将开入5作为输入源,需编辑如下表达式:
signalRstIn = time(DI5, 300, 50); //300ms上升沿防抖,50ms下降沿防抖,提升按键可靠性
不仅输入源可以借助signalRstIn解耦,一些信号复归输出模块(大多是保护模块)也可以借助脚本解耦,此时信号复归需要额外增加一输出布尔量signalRstOut,某保护逻辑脚本示例如下:
A=!signalRstOut*A+DI2; //开入2置位时,A信号保持置位,直到触发信号复归操作。
该处提及的关于重定义信号复归按键需求并非杜撰,是一真实现场用户需求。我们在一开始做产品时,很难预判到这样的需求,此时按键和信号复归模块大概率是强耦合在一起的(按键模块直接调用apiSignalRstTrip()函数),被认为是不可变部分。但随着需求的迭代,我们顺势完成了按键和信号复归模块的解耦。工业产品很少是静态的,都是在需求中不断迭代的,希望通过这个例子让大家理解到工业产品这个特点,以及我们应该采取的态度(绝不是hack大法)。
早期的sel保护并不支持上述time函数表达式,也没对按键进行抽象,上述表达式都是持续迭代优化后的产物。实际上,早期的sel保护仅支持布尔量和逻辑表达式,并将这些布尔量称之为继电器字(relayWord),用bit表示,将布尔表达式称之为逻辑方程。
随着需求持续迭代,我们目前不仅支持布尔量而且支持模拟量,惯性使然,我们团队内部依然使用继电器字(relayWord,缩写为RW)这个术语。原有逻辑方程优化比较大,不仅支持函数,而且支持类C语法,改动过大,借助计算机术语,将逻辑方程优化为脚本(script)了。
◇◇◇
至此,我们已经完成了整个信号复归模块的解耦,接口函数汇总如下:
BOOL apiSignalRstInit(void);
void apiSignalRstTrip(void);
BOOL apiSignalRstRegister(HANDLE hReac, DWORD dwMsg);
输入继电器字:signalRstIn
输出继电器字:signalRstOut
◇◇◇
为了解耦按键、开入和信号复归模块,我们被迫引入了一个新的模块:脚本模块,而且还需要外部维护软件支持。此时,简单的接口抽象已经很难胜任了,需要引入一些程序设计,慢慢的开始需要架构支撑了。脚本是架构设计中很关键的一环,下一章,让我们一起携手迈入精彩纷呈的架构世界。
——————————————
返回目录
我是小马儿,一个渴望良知与灵魂的嵌入式软件工程师,欢迎您的陪伴与同行,如感兴趣可加个人微信号nzn_xiaomaer交流,需备注“异维”二字。