顺序控制系统安全编程与状态链模式
顺序控制系统安全编程与状态链模式
杨敬东
(广东佛山菜鸟控制实验室)
摘要:本文阐述工业自动控制领域中,编写顺序控制程序的一种思维模式,便于识别程序中隐含的缺陷,尽量避免因程序缺陷引起的工业安全事故。
引言:一直以来,顺序控制系统的编程似乎被认为是简单的、枯燥的、没有挑战性的,不知是否这样的原因,对于顺序控制系统编程的安全性问题,往往不被设计人员所重视。而随着系统复杂度的增加,由软件缺陷引起的工业安全事故相信并不陌生,因此,各种安全壮健的软件编写模式,成为控制系统工作者应当重视的问题。受【美】Miro Samek博士所发明的“量子框架”的启发,本文提出一种适合于顺序控制系统安全设计的状态链模式,并在最后列出实现状态链的适合于单片机的C代码和适合于PLC的梯形图。值得一提的是,即使不专门设计状态链而用经典的嵌套Switch Case语句,或是其他的编程方法,在设计前绘制状态链,对理清思路、跟踪潜在的缺陷也有很大的帮助。
一个事例:
某冲压设备设计制造厂家生产的某型冲压机,由电动机驱动液压系统,完成产品的冲压过程,该机控制方式分为:运行方式1、运行方式2、运行方式3;每种运行方式下的工序又分为:送料、定位、夹紧、快进、冲压、吹风、出料、清除废料等多个行程,一个行程接一个行程,由此完成一个工作循环,且这些行程在不同的方式下有不同的次序和运行参数。某日,机器油路发生异响、冲压区内的定位机构又发生故障需要抢修,工人在冲压行程正在进行时按下了马达停止按钮,使全机停运。为节省时间,安排了2名工人分别对上述部位进行检修,工人为检修的方便,拆卸了冲压区的安全防护装置,在未断电的情况下,其中一人在冲压区重新调整定位机构。按照操作人员对这台机器存在的思维习惯:“开始工作时,先按启动马达按钮启动马达,然后按开始按钮,机器才会运行,开始按钮是两个串联的,安全性高,而且马达未启动时,即时按下开始按钮机器也不会开始运行”。按照这个习惯,另一名工人按下了马达启动按钮,试图观察油路情况,这时机器突然接着停止时的行程启动冲压,造成1人手部受伤。
事故原因
在日常工作中,人们往往对设置一些标志、一组数值或启动一个开关等事件怀有明确的目的,而对于何时恢复这些标志、数值或开关就往往糊涂了。例如2005年8月14日,一架太阳神航空522班机在起飞前的检查维修过程中,地面工程师将飞机的增压开关设置在手动模式进行测试,而在维修完毕没有将增压开关恢复为自动模式,当飞机升至万米高空,机内因欠压发生警报,鬼使神差,正副机长都将警报误判为空调系统故障,忙乱检查空调系统,不知不觉缺氧昏迷,多重疏忽的叠加造成飞机坠毁。
在上述液压机事故中,除了违反停机操作规程、未使用具备专业知识的维修人员,违反操作规程进行维修外,潜在的原因是控制器软件中,未充分考虑到安全性,以下是这台机器部分伪代码:
if(MON==true) MoToOut=1; // 马达按钮按下,启动马达
if(MOFF==true) MoToOut=0; // 马达停止按钮按下,马达停
………………
if(MoToOut==1) //如果马达已经开启
{
if(FangShi==1) //如果是方式1,则按如下序列转换
{
switch(Cur_Runing){ //执行状态转换和运行
case 0: ………………;
if(Cur_Runing == 0 && Star==1) //如果开始按钮按下
{
Cur_Runing == 1; //置当前状态为“送料”
………………
}
break;
case 1: ………………;
if(Cur_Runing == 1 &&
{
……………… //置位某些并行状态
Cur_Runing == 2; // 置当前状态为“定位”
}
break;
………………
}
}
If(FangShi==2) //如果是方式2,则按如下规则转换
………………;
}
………………;
程序以Cur_Runing变量标识当前机器所处状态,0表示初始状态,1表示送料状态,2表示定位状态……,Star表示开始按钮,K1、K2等表示行程开关、传感器等。
以上第4行虽然设置了一个“马达启动后才能进入送料行程”的制约条件,但是当进入各个行程后,如果停止了马达,则Cur_Runing变量仍然停留在停止马达前的值,这时马达重新启动,则机器会在该行程的断点重新运行,于是就发生上述事故。
其实这台机器的状态不算复杂,要避免该缺陷,其实只需在
If(MoToOut==1)
{
}
else //加上
{
Cur_Runing=0; //复位当前状态
……………… //复位其他并行的状态
}
但是,可能程序员已经被上述代码的深度嵌套扰乱了思维,没有发现该潜在的缺陷,而且不知为什么,很多单片机程序员似乎不太喜欢else语句,更多用if() return;
如果有更多的判断条件,代码的嵌套更深,则与机器各状态间相互耦合关系的代码就可能散布在程序各处,不容易发现各种存在的缺陷。
状态链模式及规则简介
以上述冲压机为例,以下是状态链的结构图
状态之间的直线,表示控制和被控制关系,带箭头的曲线表示某状态响应某输入所发生的转移,转移曲线下面的字母表示某个输入。
每条状态链有且仅有1个根状态S0,对应第一层,根状态下有多个下游状态S11(方式1)、S12(单步运行)、S13(连续运行),没有下游状态的状态或并行状态链的起点(并行状态稍后说明)称为梢状态,上游状态与下游状态之间表示控制和被控制关系,例如,只有S0被激活,其下游状态S11、S12、S13才能被激活,而根状态S0在上电时就被激活。
每个状态被激活后,需要激活其下游的至少1个状态(如果该状态激活2个以上的状态,该状态就成为并行状态链的起点),这个过程称为激活初始状态。例如,上电后S0即被激活,随即激活S0选定的初始激活状态S11,S11又相继激活S21,这个过程一直到达某个梢状态被激活为止,初始状态通常处于就绪状态,用于启动整个状态序列的转换,如马达启动后,即进入1个就绪状态S21,该状态响应“开始”按钮,向“送料”转换。
整个状态链结构,始终保持有1条从根状态至梢状态的链被激活(如果1个梢状态为并行链的起点,那么该并行链就作为子链按上述规则激活),由此表征一台机器某个时刻所处的特定状态,例如当S0、S1、S12、S22被激活,表示机器正处于运行方式1的送料状态。
每个激活的状态,响应某些条件(如输入输出数据的刷新、某变量的刷新或定时器溢出等)发生相应的转换,例如机器正处于运行方式1的送料状态,这时S1、S12、S22是激活的,S2休眠,这时如果按下方式转换开关,S1响应外部开关Switch的闭合,调用系统中的状态转移函数将自己及其控制的下游状态逐一退出激活状态并逐一调用这些状态的退出函数,顺序是从梢节点开始向上游节点蔓延,即先退出S22,接着退出S12,最后退出S1,上述过程结束后,激活S2及其控制的初始激活状态S21,并逐一调用这些状态的初始化函数。退出函数和初始化函数很有意义,例如,可以在“马达开”向“马达关”的转换过程中,通过在“马达开”的退出函数中复位马达输出端,以后凡是发生“马达开”的退出都自动复位马达输出端,就不必担心在代码中哪处需要复位马达输出端;在“马达开”向“马达关”的转换过程中,又自动完成了“马达开”状态的下游状态,如“冲压”、“快进”等的退出动作,而不必担心在代码中哪处需要复位阀门输出端。
以上所述的转换,只发生在同层状态之间,不支持越层状态之间的转换,虽然可以设计成越层状态转换,但不建议这样,因为越层状态转换可能隐含不安全因素,例如,如果在方式1的冲压状态下,响应一个条件,向方式2的冲压转换,则由于两种“冲压”可能有不同的动作或参数要求而发生令操作者预知不到的结果。而较为安全的做法是,方式1向方式2转换,继而激活方式2下的“就绪”状态,从而遵循操作者的思维定势。
自转换:1个活动的状态可以响应某个输入,先退出自己继而又重新激活自己。这种转换很有意思,例如,当按下停车按钮RET,无论机器是运转在送料、冲压等状态序列,都一律返回就绪状态,等待下一次开始按钮Star的按下。传统的编程方法是用switch case语句,在各个分支都要对RET的按下事件设置返回就绪状态的代码。
并行状态:某些情况下,可能要求2个以上相对独立的同层状态为活动的,以完成其各自的任务,如图(2)。例如,在方式1下,除完成送料、冲压等序列,还可以启动照明、冷却等相对于前者序列无直接关系的转换序列,那么此时的方式1,就属于这几个并行状态序列的启动节点,而此时的方式1,就成了主状态链的其中1个梢节点,当方式1退出,将自动引起两个并行序列的退出。并行状态序列的某个状态又可以触发某些并行状态序列而引发嵌套关系。并行状态之间,可以使用标志位实现同步。并行状态实际上将整个状态链分为若干子链,图(2)中虚线框所示,双边框为并行节点,增加了2个虚虚拟的根节点,用以同时启动各自的子链(这两个虚拟的根节点也可以做一些事情,如设置成定时器等,但不用于进行转换)。
状态链模式的好处
(1)将设计人员从程序中多重嵌套和杂乱的互锁条件中解脱出来,只需将精力集中在定义状态链的构造、状态之间如何转换和各状态被激活或退出时完成的任务。
(2)从状态链中删除或增加节点非常容易,如果其他节点的功能在增删某个节点后不变的话,不会影响其他节点,这样就无需在繁杂的代码之间考虑增删后的变化。
(3)避免如太阳神522班机那样的疏忽,每个状态的退出函数,可以将设计人员的注意力引导到该状态退出时应当恢复的那些设置,上游状态的退出自动的引起下游状态的退出,恢复它们各自所关心的那些变量,无需担心马达停车后,在那里还有未复位的变量。
(4)机器因各类异常引起停机时,状态链的退出动作和重新初始化动作会使机器停留在确定的、统一的某个就绪运行点,使机器重新启动时的动作符合操作者的思维定势,减少误操作引发的事故。
状态链的单片机实现
用结构表示状态链中每一个状态
Struct STA{
Bool h; //状态激活标志
Bool z; //是否为并行状态链的起点
Struct STA * ps; //指向该状态的上级状态
Struct STA * px; //指向需激活的下游初始状态
//并行状态的Px=NULL,表征该状态是一条子链的稍节点
Void (* fpin)(void); //初始化函数指针
Void (* fpdel)(void); //退出函数指针
Void (* fpRun)(void);//运行函数,符合条件时转换
Void (* fpdo)(void); //完成既定任务的函数指针
}
用1个指针数组Struct STA * pG[MAXZJ]贮存状态链的根状态,其中pG[0]存整个状态链主链的唯一一个的根状态S0,其他单元依次存放并行状态下游的各个虚拟启动状态,这样就把具有并行状态的下游各状态链分成若干子链,该数组实际上是贮存了每个子链的虚拟根节点。为了缩短状态链路径的搜索时间(整个状态链的搜索时间取决于并行状态的数量和状态链的最大长度即纵向复杂程度,横向复杂度即分支的数量不影响搜索时间),各虚拟根节点遵循如下贮存规则:按并行状态所处的嵌套层深度由浅至深依次贮存于数组pG[1]至pG[MAXZJ-1],相同嵌套深度的不分先后,如图(3),SA为没有嵌套的并行状态,存放于pG[1];SB也为没有嵌套的并行状态,存放于pG[2];SY嵌套于SA属第1层,存放在pG[3];SZ嵌套于SY,属第2层,存放在pG[4],可以写一个函数,对上述规则进行排序。
用1个指针数组Struct STA * pW[MAXZJ]贮存各子链的被激活的梢状态,由于每条子链只允许1个梢状态被激活,因此该数组与pG数组共同描述了每条子链的活动状态。
有5个函数管理整个状态链的运行:
Void ST_Init(struct STA * pin);
该函数从指定参数的状态开始,激活该状态及其指定的初始化下游节点或其控制的子链。
Void ST_Del(struct STA * pdel);
该函数从指定参数的状态开始,退出该状态极其所控制的下游活动状态。
Void ST_ChgTo(struct STA * ps, struct STA * pd);
该函数先调用Void ST_Del(struct STA * pdel);退出源状态,然后调用Void ST_Init(struct STA * pin);进入目的状态,完成状态之间的转换。
Void ST_Run(void);该函数逐条子链由梢向根调用各活动状态的Run函数。
Void ST_Do(void);该函数逐条子链由梢向根调用各活动状态的Do函数。
以图(4)状态链为例,参考C代码如下,在Borland C++ 6.0编译运行,为节省篇幅,写于同一C文件内,未设计链头虚拟状态的排序函数,ST_Do与ST_Run的原理是一样的,只不过调度的时间粒度可以不同,这里作了省略,另外一些应用函数也作了部分省略,实
际使用中,应模仿PLC扫描周期的方式,在每一个扫描周期内采样输入,运行状态链;在多任务环境中使用,应将状态链的运行至于最高优先级。
//----------------------------------------------------
#pragma hdrstop
//----------------------------------------------------
#pragma argsused
#include
#define MAXZJ 6 //最大数量的并行状态
struct STA
{
int h;
int z;
struct STA * ps;
struct STA * px;
void (*fpin) (void);
void (*fpde) (void);
void (*fpRun) (void);
void (*fpDo)(void);
};
// 状态链系统的函数和全局变量
struct STA * pG[MAXZJ];
struct STA * pW[MAXZJ];
void ST_Init(struct STA * pin);
void ST_Del(struct STA * pdel);
void ST_ChgTo(struct STA * sp, struct STA * dp);
void ST_Run(void); //运行函数
void ST_Do(void);
// 与应用有关的函数和变量
void StBuild (void); //状态链构造
void S1_Do(void);
//以下是各状态的进入函数
void S0_init (void){ puts("S0_init");};
void S1_init (void){ puts("S1_init");};
void S2_init (void){ puts("S2_init");};
void S3_init (void){ puts("S3_init");};
void S4_init (void){ puts("S4_init");};
void S5_init (void){ puts("S5_init");};
void S6_init (void){ puts("S6_init");};
void S7_init (void){ puts("S7_init");};
void S8_init (void){ puts("S8_init");};
void SA_init (void){ puts("SA_init");};
void SB_init (void){ puts("SB_init");};
void SY_init (void){ puts("SY_init");};
void SZ_init (void){ puts("SZ_init");};
//以下是退出函数
void S0_del (void){ puts("S0_del");};
void S1_del (void){ puts("S1_del");};
void S2_del (void){ puts("S2_del");};
void S3_del (void){ puts("S3_del");};
void S4_del (void){ puts("S4_del");};
void S5_del (void){ puts("S5_del");};
void S6_del (void){ puts("S6_del");};
void S7_del (void){ puts("S7_del");};
void S8_del (void){ puts("S8_del");};
void SA_del (void){ puts("SA_del");};
void SB_del (void){ puts("SB_del");};
void SY_del (void){ puts("SY_del");};
void SZ_del (void){ puts("SZ_del");};
/*以下是2个状态的运行函数,设置了2个转换,其中1个是自传换*/
void S3_Run(void);
void S1_Run(void);
void S1_Do(void){ puts("HELLO S1");};
struct STA S0,S1,S2,S3,S4,S5,S6,S7,S8,SA,SB,SY,SZ;
int main(int argc, char* argv[])
{
StBuild(); //构造状态链
ST_Init(&S0); //初始化状态链
ST_Run(); //运行一次状态链
while(1);
return 0;
}
//---------------------------------------------------------------------------
//系统函数
void ST_Init(struct STA * pin) //从pin开始初始化
{
int i;
int zb;
struct STA * sptmp;
sptmp=pin;
Loop1: if(sptmp->ps!=NULL) //查找pin所属组别
if(sptmp->ps->z!=1)
{
sptmp=sptmp->ps;
goto Loop1;
};
for(zb=0;zb
if(sptmp==pG[zb]) break;
sptmp=pin;
while(sptmp->px!=NULL) //如果不属于稍
{
sptmp->h=1;
if(sptmp->fpin != NULL) (*sptmp->fpin)();
sptmp=sptmp->px;
}
pW[zb]=sptmp;
sptmp->h=1; //激活稍并存于链尾数组
if(sptmp->fpin != NULL) (*sptmp->fpin)();
if(sptmp->z == 0) return; //如不属并行退出
for(i=1;(zb+i)
{
if(pG[zb+i]==NULL) goto next;
if(pG[zb+i]->ps->h==1&&pG[zb+i]->h==0)
{
sptmp=pG[zb+i]; //指向链头并激活该子链
while(sptmp->px != NULL)
{
sptmp->h=1;
if(sptmp->fpin != NULL) (*sptmp->fpin)();
sptmp=sptmp->px;
};
sptmp->h=1;
if(sptmp->fpin != NULL) (*sptmp->fpin)();
pW[zb+i]=sptmp;
}
next: ;
}
}
void ST_Del(struct STA * pdel) //退出所有活动状态至pdel
{
int i;
int zb;
struct STA * sptmp;
sptmp=pdel;
Loop1: if(sptmp->ps!=NULL) //查找pdel所属组别
if(sptmp->ps->z!=1)
{
sptmp=sptmp->ps;
goto Loop1;
};
for(zb=0;zb
if(sptmp==pG[zb]) break;
if(pW[zb]==NULL) return; /*将与处于同一活动子链的尾节点休眠*/
pW[zb]->h=0;
for(i=1;(zb+i)
{ //查询上述步骤对其他子链的影响
if(pG[zb+i]!=NULL)
{ //如果有影响则休眠尾节点,为退出准备
if(pG[zb+i]->ps->h==0 && pW[zb+i]!=NULL)
pW[zb+i]->h=0 ;
}
}
for(i=MAXZJ-1;i>=zb;i--) //按顺序退出所有子链直至pdel
{
if(pW[i]==NULL) goto next;
if(pW[i]->h==0)
{
sptmp=pW[i];
while(sptmp!=pG[i] && sptmp!=pdel)
{
sptmp->h=0;
if(sptmp->fpde!=NULL) (*sptmp->fpde)();
sptmp=sptmp->ps;
}
sptmp->h=0;
if(sptmp->fpde!=NULL) (*sptmp->fpde)();
pW[i]=NULL;
}
next: ;
}
}
//状态链的转换函数
void ST_ChgTo(struct STA * sp, struct STA * dp)
{
if(sp==NULL||dp==NULL) {puts("Error1");return;};
if(sp->ps != dp->ps) {puts("Error2");return;}; /*越层转换报错*/
if(dp->h==1 && sp!=dp) {puts("Error3");return;};/*向活动节点转换报错,自传换除外*/
ST_Del(sp);
ST_Init(dp);
}
//状态链运行函数
void ST_Run(void)
{
int i;
struct STA * sptmp;
for(i=MAXZJ-1;i>=0;i--)
{
if(pW[i]==NULL) goto next;
sptmp=pW[i];
while(sptmp != pG[i])
{
if(sptmp->fpRun != NULL) (*sptmp->fpRun)();
sptmp=sptmp->ps;
}
if(sptmp->fpRun != NULL) (*sptmp->fpRun)();
next: ;
}
}
//应用函数
void StBuild (void)
{
S0.ps=NULL;
S0.px=&S1;
S0.fpin=S0_init;
S0.fpde=S0_del;
S1.ps=&S0;
S1.z=1;
S1.fpin=S1_init;
S1.fpde=S1_del;
S1.fpRun=S1_Run;
S2.ps=&S0;
S2.fpin=S2_init;
S2.fpde=S2_del;
SA.ps=&S1;
SA.px=&S3;
SA.fpin=SA_init;
SA.fpde=SA_del;
SB.ps=&S1;
SB.px=&S5;
SB.fpin=SB_init;
SB.fpde=SB_del;
S3.ps=&SA;
S3.fpin=S3_init;
S3.fpde=S3_del;
S3.fpRun=S3_Run;
S4.ps=&SA;
S4.z=1;
S4.fpin=S4_init;
S4.fpde=S4_del;
S5.ps=&SB;
S5.fpin=S5_init;
S5.fpde=S5_del;
S6.ps=&SB;
S6.fpin=S6_init;
S6.fpde=S6_del;
SY.ps=&S4;
SY.px=&S7;
SY.fpin=SY_init;
SY.fpde=SY_del;
SZ.ps=&S4;
SZ.px=&S8;
SZ.fpin=SZ_init;
SZ.fpde=SZ_del;
S7.ps=&SY;
S7.fpin=S7_init;
S7.fpde=S7_del;
S8.ps=&SZ;
S8.fpin=S8_init;
S8.fpde=S8_del;
pG[0]=&S0;
pG[1]=&SA;
pG[2]=&SB;
pG[3]=&SY;
pG[4]=&SZ;
}
void S3_Run(void)
{
if(1) ST_ChgTo(&S3,&S4); //假设转换条件成立
}
void S1_Run(void)
{
if(1) ST_ChgTo(&S1,&S1);//S1自传换
}
状态链的PLC实现
PLC的梯形图似乎注定就是适合于这种状态链设计的,它与状态链在形状上基本上可以说一一对应,相比起C编程更加直观。
不足之处是增删节点不像C编程那样方便、受梯形图设计区域宽度的限制(可以使用触点主控指令,但层级关系就不那么直观了)、转换的条件设置比较麻烦,需要用转换条件的常闭触点断开源状态,然后以转换条件的常开触点进入目的状态、进入和退出函数执行顺序需要手工排列等
以三菱Fx系列PLC为例,用M元件代表状态,如图(5)。图中M8002和一些脉冲上升沿触点用以进入初始化状态,用功能指令配合脉冲上升下降沿触点完成状态的进入函数和退出函数。
不要用STL指令,STL指令不能管理状态链的层级关系,而且存在2个并行序列之间可以相互转换的缺陷,如果所设计的机器不需要这种状态链式的层级关系,那么STL指令当然是首选的。
结语
以图(1)的液压机为例,状态链可以设计成多种方式,例如还可以设计成在马达关闭的时候才可以进行运行方式的撤换,只不过在机器开始运行的时候要在各个序列之中设立条件转换而已,关键是要遵循状态之间安全的逻辑层级关系,这当中设计者所考虑到的才是最重要的。

提交
巧用中断——PLC扩展AB相高速计数的方法