1. 背景
在PLC里一直以来的工程经验里面。如果想要做一个顺序控制(业务逻辑):
用Graph实现
用Csae..Of分支跳转语句
用int数的变化切换不同的执行语句
下面一种一种的来看,先看看使用int数切换实现一个顺序控制的案例:
在这个案例(代码片段)里面,程序扫描currrent_step的值的不同,从而选择执行不同的程序段的内容,根据条件是否满足则可以做到在一个程序段里实现往下顺序执行,或者往上循环回跳直到满足跳出循环的条件。或者定点跳到指定步。这种方法的缺点是:1.可读性差;2.新增顺序控制的时候难以直接在原程序之间差入程序而不引入老程序逻辑的修改;3.在当前步执行的过程中,不知道前序步是从哪儿过来的,因为也许有好几个前序步在同时指向当前步;4. 本质还是IF判断,意味着每个循环内所有条件都被判断了一遍。 好处就是简单好写,顺序编写,只管满足条件就跳转,不用过多考虑逻辑判断的冲突。
如果用Case..Of来写,情况又会好一点:
Step_int用在了Switch语句里面,指定哪个分支就跳到那个分支,效率提高。
但是缺点依旧,复杂程序步序很多,Step_int在分支语句中被重新赋值,但是不知道前序是哪里来的,也很难在不去改变Step_int值的情况下插入新的逻辑代码
CASE #step_int OF
20 :
IF "enable_cycle" THEN
;
#step_int := 21 ;
END_IF;
21 :
;
#step_int := 22 ;
22 :
IF condition1 THEN
#step_int := 30 ;
ELSIF condition2 THEN
;
#step_int := 30 ;
END_IF;
30 :
;
IF conidtion THEN
#step_int := 40 ;
END_IF;
END_CASE;
在这种Case..Of的情况下,有方法可以做到记录跳转前序:
西门子PLC其实提供了更好的做顺序控制的方法,如图:
这是Graph,他有Step
,也有Trans
用户不用再去考虑分支转换的问题,只要在当前步,满足条件就可去到下一步或者指定步。
有很多状态点可以知道程序的现在位置,和上一个位置,以及下一个位置
状态图形化,直观。
2. 什么是状态机
网上有很多关于状态机的介绍和描述,有有限状态机
,有层次状态机
,也有Mealy和Moore状态机
。分类具体的细节我作为刚接触状态机的人也解释不清楚。所以下面所有关于状态机的陈述都是基于我自己的理解和使用总结,有很多我没用到的,或者没见过的当然就不会提及,也不会有太多抽象的理论。仅仅只是站在个人浅薄认知的角度,从顺控和状态机的差异之间做一个简单的比较和理解,力求一个印象加深。
上面提到的都是关于顺序控制的,也就是说上面的控制一定是逐过程 的。当新写一段Graph程序的时候,设计者通常考虑的都是:初始化状态是什么, 第一步要干什么,满足第一步条件之后要跳到哪一步去,下一步又要干嘛,结束和恢复状态条件是什么 。这种更是一种边做边写的过程。步序从头到尾以满足使用需要。
状态机和顺控有类似,但是他们出发点其实不一样。比如西门子的Graph可以理解为一种状态机,这是一种“从进到出的”顺序,它一定有前序,每一步的执行或多或少是和一些前序步和前置条件绑定的。但是Graph在使用的时候我们通常也没有把系统状态 抽象出来,并且给出详尽的状态图,再通过状态图来设计。
个人理解状态机的思想是:更少的关注过程变化,更多的状态对象,每一种可以被模块化 的行为或者动作就是一个状态。 这个时候可以尽量的把有限的状态给汇总起来。当写程序之前,首先画状态图,考虑当前状态要进入的条件,或者当前状态要出去的条件,注意的是,这种情况下我们其实仅仅在关注当前状态的前后条件。 然后我们用Switch..Case把所有状态枚举出来。通过管理状态的切换来决定下一步怎么走。
一个状态机的全周期,其实就可以理解为一个产品的全部功能。
3. 尝试自己完成一个状态机
先统计系统存在多少种状态,(那种一进一出没有分支的状态,其实就可以合并在一起写,形成一个统一的状态。),以及状态关系:
这是自己随便画的,正式使用时状态图应该有专门的工具来做,而且状态图需要随着状态机一起保存在代码里,更新状态机的时候就要更新状态图,不然维护代码的时候不方便。
创建一个状态机的简单框架:
1.枚举所有状态
2.把该状态种的所有动作和行为囊括在该状态命名下的方法中
3.写一个Process()方法,新建一个线程,在循环中调用各个方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace new_test
{
enum State
{
Init,
get_recipe,
pre_write_ds,
calc_value,
calc_time,
write_ds,
read_back_ds_display,
catch_exception,
cancel,
terminal,
reduce_step,
display_write_confirm,
fill_value,
wait_idle
}
internal class StateMachine
{
State nextState;
public bool Init_ok;
public bool get_recipe_ok;
public bool pre_write_ds_ok;
public bool calc_value_ok;
public bool calc_time_ok;
public bool write_ds_ok;
public bool read_back_ds_ok;
public bool catch_exception_ok;
public bool cancel_ok;
public bool terminal_ok;
public bool reduce_step_ok;
public bool display_write_confirm_ok;
public bool fill_value_ok;
public bool wait_idle_ok;
public StateMachine ()
{
nextState = State.wait_idle;
}
public void Process ()
{
while (true )
{
Task.Run(() =>
{
switch (nextState)
{
case State.Init:
Init(out Init_ok);
break ;
case State.get_recipe:
GetRecipe(out get_recipe_ok);
break ;
case State.pre_write_ds:
PreWriteDs(out pre_write_ds_ok);
break ;
case State.calc_value:
CalcValue(out calc_value_ok);
break ;
case State.calc_time:
CalcTime(out calc_time_ok);
break ;
case State.write_ds:
WriteDs(out write_ds_ok);
break ;
case State.read_back_ds_display:
ReadBackandDisplay(out read_back_ds_ok);
break ;
case State.catch_exception:
CatchException(out catch_exception_ok);
break ;
case State.cancel:
Cancel(out cancel_ok);
break ;
case State.terminal:
Terminal(out terminal_ok);
break ;
case State.reduce_step:
ReduceStep(out reduce_step_ok);
break ;
case State.display_write_confirm:
DisplayandWriteConfirm(out display_write_confirm_ok);
break ;
case State.fill_value:
FillValue(out fill_value_ok);
break ;
case State.wait_idle:
WaitIdle(out wait_idle_ok);
break ;
}
});
Thread.Sleep(500 );
}
}
public void TransferTo (State currentState )
{
nextState = currentState;
}
public void Init (out bool status )
{
status = true ;
}
public void GetRecipe (out bool status )
{
status = true ;
}
public void PreWriteDs (out bool status )
{
status = true ;
}
public void CalcValue (out bool status )
{
status = true ;
}
public void CalcTime (out bool status )
{
status = true ;
}
public void WriteDs (out bool status )
{
status = true ;
}
public void ReadBackandDisplay (out bool status )
{
status = true ;
}
public void CatchException (out bool status )
{
status = true ;
}
public void Cancel (out bool status )
{
status = true ;
}
public void Terminal (out bool status )
{
status = true ;
}
public void ReduceStep (out bool status )
{
status = true ;
}
public void DisplayandWriteConfirm (out bool status )
{
status = true ;
}
public void FillValue (out bool status )
{
status = true ;
}
public void WaitIdle (out bool status )
{
status = true ;
}
}
}
框架建立好了,现在关键的内容是如何实现各个状态的转换(转换条件如状态图所示):
1.把转换条件写在各个方法的具体实现里面(这种情况不用管状态怎么进来的,只用管怎么出去的 ),比如当正在PreWriteDs状态下的时候,要跳出该状态的条件:
或者放这儿,效果一样:
2.把转换条件写在Idle(闲时状态)里,思路是每次当前状态执行一个循环后,总会回到闲时状态中,由闲时状态管理和分配下一个状态应该怎么去。这种思路需要利用到状态的标志位。=1表示在当前状态中。以PreWriteDs状态下为例,思路如下:
这种写法需要在每次循环结束的时候都先回到Idle状态下去:
3.比较两种方法:
4. 顺控和状态机之间的差别
状态机的框架已经在上面展示了,下面用顺控的思想把这个案例随便写个代码片段,主要表达编程逻辑,以做对比:
顺控的步序靠Switch来转换,这和状态机一样
顺控从一步到下一步会比较清晰,有些时候甚至不需要太多判断逻辑,这一步做完就直接往下一步跳转。
顺控在写的时候其实可以不用先画状态图,从开始到结束,做好步序衔接就行。
简单逻辑用顺序控制其实会更方便。
在PLC的应用中,顺控的思想随处可见,这也和PLC的应用场景有关,但是状态机的原理如果清晰了,其实也可以在PLC上实现状态机。
两种方法都尝试过以后,其实也是各有好处,状态机写起来不够直接,但好在一个容易拓展。结构清晰。实际使用上,也不是非选哪个才行,根据实际情况实现自己想要的逻辑就可以了。