1. 背景
- 在PLC一直以来的工程经验里面。如果想要做一个顺序控制(业务逻辑):
- 用Graph实现
- 用Csae..Of分支跳转语句
- 用int数的变化切换不同的执行语句
- 下面一种一种的来看,先看看使用int数切换实现一个顺序控制的案例:
- 在这个案例里面,程序扫描currrent_step的值的不同,从而选择执行不同的程序段的内容,根据条件是否满足则可以做到在一个程序段里实现往下顺序执行,或者往上循环回跳直到满足跳出循环的条件。或者定点跳到指定步。这种方法的缺点是:1.可读性差;2.新增顺序控制的时候难以直接在原程序之间差入程序而不引入老程序逻辑的修改;3.在当前步执行的过程中,不知道前序步是从哪儿过来的,因为也许有好几个前序步在同时指向当前步;4. 本质还是IF判断,意味着每个循环内所有条件都被判断了一遍。好处就是简单好写,顺序编写,只管满足条件就跳转,不用过多考虑逻辑判断的冲突。
- 如果用Case..Of来写,情况又会好一点:
- Step_int用在了Switch语句里面,指定哪个分支就跳到那个分支,效率提高。
- 但是缺点依旧,复杂程序步序很多,Step_int在分支语句中被重新赋值,但是不知道前序是哪里来的,也很难在不去改变Step_int值的情况下插入新的逻辑代码
//step
CASE #step_int OF
//init start
20:
IF "enable_cycle" THEN
#step_int := 21;
END_IF;
//cycle buffer
21:
"recordsData".buffer := "random_data".buffer[#random_cycle].buffer;
#random_cycle := #random_cycle + 1;
#step_int := 22;
//cycle count
22:
IF #random_cycle < 4 THEN
#step_int := 30;
ELSIF #random_cycle = 4 THEN
#random_cycle := 0;
#step_int := 30;
END_IF;
//way of ascii send to plc300
30:
"AsciiSendBuffer".buffer := "recordsData".buffer;
"_cm1241data".P_SEND.Req := true;
IF "_cm1241data".P_SEND.Done THEN
#step_int := 40;
END_IF;
//tcp inform to 300plc-->inform ascii send finish
40:
"_cm1241data".P_SEND.Req := false;
#step_int := 50;
//tcp inform to 1200plc-->inform ascii rcv finish
50:
IF "_cm1241data".P_RCV.Ndr THEN
#step_int := 60;
END_IF;
//wait for the feedback/compare
60:
#my_time := T#200ms;
#timeline.enable := true;
IF #timeline.time_down AND NOT #impulse_array[1] THEN
"recordsData".records.total_count := "recordsData".records.total_count + 1;
IF "AsciiRcvBuffer".buffer <> "recordsData".buffer THEN
"recordsData".records.result := 'N';
#TmpInt := RD_LOC_T("recordsData".records.timestamp);
IF "recordsData".records.error_count < 1024 THEN
"recordsData".records.table["recordsData".records.error_count].total_count := "recordsData".records.total_count;
"recordsData".records.table["recordsData".records.error_count].errror_count := "recordsData".records.error_count;
"recordsData".records.table["recordsData".records.error_count].time_stamp := "recordsData".records.timestamp;
"recordsData".records.error_count := "recordsData".records.error_count + 1;
#step_int := 70;
ELSE
"recordsData".records.error_carry := "recordsData".records.error_carry + 1;
"recordsData".records.error_count := "recordsData".records.error_count := 0;
"recordsData".records.table["recordsData".records.error_count].total_count := "recordsData".records.total_count;
"recordsData".records.table["recordsData".records.error_count].errror_count := "recordsData".records.error_count;
"recordsData".records.table["recordsData".records.error_count].time_stamp := "recordsData".records.timestamp;
"recordsData".records.error_count := "recordsData".records.error_count + 1;
#step_int := 70;
END_IF;
ELSE
"recordsData".records.result := 'Y';
#TmpInt := RD_LOC_T("recordsData".records.timestamp);
#step_int := 70;
END_IF;
END_IF;
#impulse_array[1] := #timeline.time_down;
//data clean
70:
#timeline.enable := FALSE;
#step_int := 71;
//rcv rst
71:
"_cm1241data".P_RST.REQ := true;
#step_int := 72;
//rst ok
72:
#rst_ton(IN:=(#step_int=72),
PT:=T#500ms);
IF "_cm1241data".P_RST.DONE OR #rst_ton.Q THEN
"_cm1241data".P_RST.REQ := false;
#step_int := 80;
END_IF;
//next turn
80:
RESET_TIMER(#rst_ton);
#step_int := 21;
END_CASE;
//execute
#Random_Instance(udintORchar:=true,
Out_char_random=>#generate_char);
#alternate_time_Instance(In_switchtime:=#my_time,
In_startAlternate:=#timeline.enable,
Out_switchtime_up=>#timeline.time_up,
Out_switchtime_Down=>#timeline.time_down);
- 在这种Cse..Of的情况下,有方法可以做到记录跳转前序:
//1. 把Step_Int改成一个Current_Step,一个nextStep
//2. case依靠nextStep转换,步序转换赋值给currentStep
//3. 新建一个方法,方法在每个扫描周期都会被调用,方法作用是当发现currentStep和nextStep值不同时,把新的currentStep的值重新赋值给nextStep
//4. 这种方法还可以灵活的在转换间隔加上延时以控制延时时间,或者加入一些暂停,取消,终止,手动等等很多步序控制的新功能。
- 西门子PLC其实提供了更好的做顺序控制的方法,如图:
- 这是Graph,他有
Step
,也有Trans
- 用户不用再去考虑分支转换的问题,只要在当前步,满足条件就可去到下一步或者指定步。
- 有很多状态点可以知道程序的现在位置,和上一个位置,以及下一个位置
- 状态图形化,直观。
- 这是Graph,他有
2. 什么是状态机
- 上面提到的都是关于顺序控制的,也就是说上面的控制一定是逐过程的。当新写一段Graph程序的时候,设计者通常考虑的都是:初始化状态是什么, 第一步要干什么,满足第一步条件之后要跳到哪一步去,下一步又要干嘛,结束和恢复状态条件是什么。这种更是一种边做边写的过程。步序从头到位以满足使用需要。
- 状态机和顺控有一些简单的相似点,但是他们更多的是不同点。比如西门子的Graph就可以理解为一种状态机,这是一种“从进到出的”顺序,它一定有前序,每一步的执行或多或少是和一些前序步和前置条件绑定的。
- 但是状态机的思想是:它更少的去关注过程(条件),更多的关注状态,每一种独立的行为或者动作就是一个状态。这个时候可以尽量的把有限的状态给统计起来,一个状态做一个方法。当写程序之前,首先画一画状态图,考虑当前状态要进入的条件,或者当前状态要出去的条件,注意的是,这种情况下我们其实仅仅在关注当前状态的前后条件。然后我们用Switch..Case把所有状态枚举出来。通过一个闲时状态统一管理状态的切换,每一个状态执行完毕之后都回到闲时状态,由它来决定下一步怎么走。
- 和顺控不一样的地方出来了,顺口大部分是一步一步走,状态机无所谓上一步,它只会在Idle中去调用分支。对于一些事件触发(按钮事件),按下则触发响应的状态。
- 如果状态机要做一个需要前后判断的状态动作,则可以在Idle状态里面去判断当前状态的标志位,下一个状态没有进行的标志位,如果标志位满足逻辑则进入。
- 状态机最重要的是状态图,程序的修改要及时修改状态图。
- 状态机的扩展仅仅只需要扩展一个新的状态,按照新的状态图改变即可
- 有一些复杂的场景,状态机的状态表示甚至可以用一个二维数组的表示
- 一个状态机的全周期,其实就可以理解为一个产品的全部功能。