美文网首页程序员
基于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