基于SNL的状态机
snl是面向状态的语言,作为一门语言,当然有自己的词法语法解析,这不是本文的范畴。同时,基于snl语言和epics-base开发的sequencer,集成到epics support中,给epics应用的流程控制提供支持。sequencer在ca框架下,属于client的部分,能独立运行,需要创建上下文、信道等惯例操作。
基本概念
-
program
一个流程控制的完整程序,也可能是按功能模块划分后的独立完整程序。 -
state set(ss)
通常一个program由一个或多个ss状态集组成,这是一种逻辑上的划分。在seq启动的时候,一个ss会分配一个线程,分别独立的运行下去。 -
state
ss内部包含一个或多个state,所有状态组合形成了一个ss,在任何时候,一个ss内只会停留在一个state状态,和普通状态机相同。
基本语法
本文只针对sequencer结构,详尽的语法可以参考网站https://www-csr.bessy.de/control/SoftDist/sequencer/index.html。
嵌入式代码
sequencer是基于snl语法的,是一种c-like的代码,而且可以调用嵌入的c代码,形式上是包含在百分号+括号内,如下所示。
{
#action代码
%{
printError(ErrId, ErrLevel, FILE_AND_LINE_STRING, ": LayerSwitch\n");
}%
}
同时也可以先声明后调用:
%{
epicsInt32 transINT32(double value, double norminal)
{
return (epicsInt32)(((pow(2, 30) - 1) * value) / norminal);
}
}%
{
#action代码
val = transINT32(6, 2.0);
}
需要特别注意的是,涉及pv的操作,要传入参数ssid,表示这个操作是在哪个状态集中完成的。ssid可以通过pvIndex
获取,该调用不能嵌入到c代码中。
官方宣传是可以任何c/c++代码的,但本人在实际运用过程中,发现部分高级的是不支持的,索性直接用native的c代码好了,欢迎批评。
状态机
作为状态机,基本的要素包括:状态、事件、行为,简单的描述就是在某种状态,遇到某件事情,触发某个动作,并转移到另一状态。
下面的代码简单描述了snl状态机。
ss state_set_name
{
state state1
{
entry
{
//action
}
when (event)
{
//action
} state state2
when (event)
{
//action
} state state3
exit
{
//action
}
}
state state2
{
...
}
}
- state 括号内包含完成状态描述,在该状态下需要监听的事件,以及需要完成的动作;
- entry 进入状态时的行为,一般作为条件的初始化;
- exit 退出状态之前的行为,对应的完成资源的释放等;
- when 条件判定,所有的when会顺序执行,直到某个条件满足跳转到对应state;
- event 需要判定的条件,可以使某个变量,某个pv值,某个evtflag;
- action 具体的行为,可调用sequencer提供的各种接口;
事件绑定
通常声明变量、pv关联、事件关联等操作如下:
int reqpowersupply; //声明变量
assign reqpowersupply to "G-ACS-PS:REQ-MRW"; //绑定pv
monitor reqpowersupply; //监视pv值,及时改变变量值
evflag powersupplyevt; //声明事件
sync reqpowersupply powersupplyevt; //绑定事件
这样一个关联pv的事件就定义好了,pv变量值得改变会直接改变事件变量,从而用efTest
判定事件达成与否。相应地,efSet
可以直接改变事件变量的值,efClear
清除变量的值。
以上采用sync
的方式绑定事件和变量,如果对于变化较快的pv值,可能无法及时处理事件的变化。因此使用syncQ
将每次改变暂存在队列,需要的时候pvGetQ
从队列头依次取出。
连接管理
通常在执行证实逻辑前,需要判定pv连接情况,包括分配数量、连接数量等。例如:
when (pvConnectCount() == pvAssignCount())
{
...
}
when (pvConnectCount() == pvChannelCount())
{
...
}
基本流程
编译过程
预编译
snc编译器读取.st或.stt文件,进行词法、语法解析,编译生成.c代码。本文不对编译过程和原理进行剖析,仅对生成结果进行解析。
- program,整个sequencer的主体结构,一切要素的入口。
/* Program table (global) */
seqProgram psctrl = {
/* magic number */ 2002005,
/* program name */ "psctrl",
/* channels */ seqg_chans,
/* num. channels */ 5829,
/* state sets */ seqg_statesets,
/* num. state sets */ 2,
/* user var size */ 0,
/* param */ "",
/* num. event flags */ 0,
/* encoded options */ (0 | OPT_CONN | OPT_NEWEF),
/* init func */ seqg_init,
/* entry func */ 0,
/* exit func */ 0,
/* num. queues */ 0
};
- 变量声明
/* Variable declarations */
# 全局变量
# line 29 "../psctrl.st"
static char tempstr[100];
# ss局部变量
struct seqg_vars_pscheck {
# line 1418 "../psctrl.st"
int i;
} seqg_vars_pscheck;
# state局部变量
struct seqg_vars_psctrl {
# line 266 "../psctrl.st"
struct {
# line 603 "../psctrl.st"
int initall_dcon;
# line 604 "../psctrl.st"
int initall_start;
# line 605 "../psctrl.st"
int initall_done;
} seqg_vars_st_InitAll;
} seqg_vars_psctrl;
- channel表(绑定到pv的变量)
/* Channel table */
static seqChan seqg_chans[] = {
/* chName, offset, varName, varType, count, eventNum, efId, monitored, queueSize, queueIndex */
{"", (size_t)&temppointlist[0], "temppointlist[0]", P_INT, 1, 1, 0, 0, 0, 0},
{"G-ACS-SNS:RNUM-MRW", (size_t)&room_number, "room_number", P_INT, 1, 5002, 0, 1, 0, 0},
};
- ss表,包含名字、状态数量,以及指向状态表的指针。
/* State set table */
static seqSS seqg_statesets[] = {
{
/* state set name */ "psctrl",
/* states */ seqg_states_psctrl,
/* number of states */ 16
},
{
/* state set name */ "pscheck",
/* states */ seqg_states_pscheck,
/* number of states */ 2
},
};
- 状态表,包含状态名、以及经典的状态机三要素。
/* State table for state set "pscheck" */
static seqState seqg_states_pscheck[] = {
{
/* state name */ "st_Init",
/* action function */ seqg_action_pscheck_1_st_Init,
/* event function */ seqg_event_pscheck_1_st_Init,
/* entry function */ seqg_entry_pscheck_1_st_Init,
/* exit function */ 0,
/* event mask array */ seqg_mask_pscheck_1_st_Init,
/* state options */ (0)
},
{
/* state name */ "st_Monitor",
/* action function */ seqg_action_pscheck_1_st_Monitor,
/* event function */ seqg_event_pscheck_1_st_Monitor,
/* entry function */ seqg_entry_pscheck_1_st_Monitor,
/* exit function */ 0,
/* event mask array */ seqg_mask_pscheck_1_st_Monitor,
/* state options */ (0)
},
};
- 状态处理函数
seqg_entry_
进入到某个状态时执行,对应语法为entry
。其中SS_ID
参数为当前所在ss的id,程序内部自动获取,用户不应关心。对应的有exit
的处理函数,本文未使用。
/* Entry function for state "st_Init" in state set "pscheck" */
static void seqg_entry_pscheck_1_st_Init(SS_ID seqg_env)
{
# line 1421 "../psctrl.st"
printf("start ps check sequence\n");
}
seqg_event_
执行条件检查时需要,对应语法为when
。其中seqg_ptrn
表示将要执行action的第几个case,seqg_pnst
表示将要跳转到第几个状态。均为按照出现先后,从0计数。每个if
为每个when
的条件判定,从语法上也可以推断出各个条件之间有先后顺序。
/* Event function for state "st_Init" in state set "pscheck" */
static seqBool seqg_event_pscheck_1_st_Init(SS_ID seqg_env, int *seqg_ptrn, int *seqg_pnst)
{
# line 1424 "../psctrl.st"
if (seq_pvConnectCount(seqg_env) == seq_pvAssignCount(seqg_env))
{
*seqg_pnst = 1;
*seqg_ptrn = 0;
return TRUE;
}
# line 1427 "../psctrl.st"
if (seq_delay(seqg_env, 1.0))
{
*seqg_pnst = 0;
*seqg_ptrn = 1;
return TRUE;
}
return FALSE;
}
seqg_action
为各个条件下所需要执行的行为,各个case对应上文event不同的if
分支。
/* Action function for state "st_Init" in state set "pscheck" */
static void seqg_action_pscheck_1_st_Init(SS_ID seqg_env, int seqg_trn, int *seqg_pnst)
{
switch(seqg_trn)
{
case 0:
{
}
return;
case 1:
{
}
return;
}
}
编译
标准的gcc编译流程,不在赘述。
初始化流程
-
iocsh解析
sequencer通过ioc命令行启动,执行seq programName即可,可直接在ioc启动时执行。程序启动时,调用注册到iocsh的
seqCallFunc
,解析第一个参数为program/threadID
,并且正式调用seq函数,传入包含该名字的seqProgram
结构。 -
注册Program
首先会向sequencerProgram
结构体注册当前ProgramseqProgram
,并创建Program的实例program_instance
。program_instance
可以看做seqProgram
的具象,不仅包含前者的静态数据,还包含运行时分配的动态数据。例如动态assign数量、连接数量、monitor数量、读写请求队列等。 -
初始化
首先是给evFlags
分配空间,bitMask类型的数组,用于事件达成的判定。然后给绑定的变量通道syncedChans
分配空间,对于通过syncQ
方式绑定的事件,还会创建相应数量的队列。紧接着,给状态集数组分配空间,有多少个ss就分配几倍的
state_set
空间。分配好之后,就需要对每个状态集进行初始化。主要是对每个channel读/写请求的结构创建,以及对应的数据空间pv_meta_data
(包括时间戳、状态、严重程度以及错误消息)的创建。当然,channel本身的空间CHAN
也是需要分配和初始化的。在初始化过程中,有三个事件id非常重要。一个是用于时间同步的信号量,一个是所有通道连接已建立的标志,一个是
ss
退出的标记。 -
主线程
初始化工作完成以后,会激发一个线程启动主工作线程,其入口函数是sequencer
。首先,将program添加到program列表(一般而言,一个足以)。然后创建ca上下文,这点类似于epics框架下的client。
接着,会触发当前这个program的
initFunc
,该函数在snc编译时自动生成。如下:
/* Program init func */
static void seqg_init(PROG_ID seqg_env)
{
}
接着,会对ss状态集中每个变量进行初始化。其中snc生成的变量按各个state区分开的,如下:
struct seqg_vars_psctrl {
# line 266 "../psctrl.st"
int i;
# line 266 "../psctrl.st"
int j;
# line 266 "../psctrl.st"
int x;
struct {
# line 603 "../psctrl.st"
int initall_dcon;
# line 604 "../psctrl.st"
int initall_start;
# line 605 "../psctrl.st"
int initall_done;
} seqg_vars_st_InitAll;
}seqg_vars_psctrl;
其中i
,j
,x
声明在ss,initall_dcon
,initall_start
,initall_done
声明在状态st_InitAll里面,作用域会有所不同。
接着,对所有的pv进行connect,这步操作过程可参考《CA工作机制》。而线程会循环等待,直到所有结果返回。如果状态错误,那么会进入退出sequencer流程,断掉连接,释放资源等。
接着,如果program有entryFunc
就会执行,不过一般entryFunc
和exitFunc
均为空。当然也可以利用这一时机,处理很多逻辑上的资源分配、释放问题。
最后,为每个ss激发线程,使其在独立线程中运行,而主线程会直接接管第一个ss,并跳转到入口ss_entry
。这是整个状态机的主循环,处理ss所有的事件信息。
主循环
主循环是游戏里面的概念,逻辑上是个死循环,在循环体内每次都要更新信息、处理事件等。在sequencer中,也是在while大循环中更新pv数据、处理事件请求,并执行相应的状态跳转。
- 将
ss
的状态切换成当前state
,主要是状态挂载的eventMask
信息,这是个多比特位数据,用于事件触发判定; - 判定是否有状态切换,并进入状态的
entry
,执行入口函数; - 对所有未处理完成的pv事件进行一次flush,即强制发送所有缓存的pv请求。这样保证在实际进入状态前,所有pv已处理完毕;
- 触发同步事件信号,唤醒所有需要同步的变量操作;
- 进入小循环,等待
evt
事件或者超时。一般包括pv的get/put/monitor、连接改变,以及evtFlag
的set和clear等,当然ss
的退出等也会触发信号量的而改变。
① 根据脏标记,将所有变化的pv,从ca通道拷贝到sequencer变量;
② 重新设置超时时间;
③ 检查当前状态的when
条件是否满足,打上触发标记,等待执行;
④ 重置绑定pv事件的标记,等待新的事件;
⑤ 如果有触发标记,那么跳出小循环,执行后续操作;否则继续小循环; - 执行触发标记对应的
action
,执行后跳转到对应的state
;发生状态切换前,会执行exit
方法,完成可能需要的清理; - 如果有
dead
标记发生,则会退出主循环;
总结下来一句话,基于snl的sequencer,依赖MainLoop处理各种事件和变量的更新,状态机通过状态表和指针的切换完成。整体结构和实现细节,还是十分值得深入学习的。