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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容