美文网首页程序员
基于SNL的状态机

基于SNL的状态机

作者: 七彩布谷 | 来源:发表于2020-12-09 22:01 被阅读0次

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_instanceprogram_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;

其中ijx声明在ss,initall_dconinitall_startinitall_done声明在状态st_InitAll里面,作用域会有所不同。

接着,对所有的pv进行connect,这步操作过程可参考《CA工作机制》。而线程会循环等待,直到所有结果返回。如果状态错误,那么会进入退出sequencer流程,断掉连接,释放资源等。

接着,如果program有entryFunc就会执行,不过一般entryFuncexitFunc均为空。当然也可以利用这一时机,处理很多逻辑上的资源分配、释放问题。

最后,为每个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处理各种事件和变量的更新,状态机通过状态表和指针的切换完成。整体结构和实现细节,还是十分值得深入学习的。

相关文章

网友评论

    本文标题:基于SNL的状态机

    本文链接:https://www.haomeiwen.com/subject/sdhtgktx.html