snl是面向狀態(tài)的語言,作為一門語言雾鬼,當(dāng)然有自己的詞法語法解析萌朱,這不是本文的范疇。同時策菜,基于snl語言和epics-base開發(fā)的sequencer晶疼,集成到epics support中,給epics應(yīng)用的流程控制提供支持又憨。sequencer在ca框架下翠霍,屬于client的部分,能獨立運行蠢莺,需要創(chuàng)建上下文寒匙、信道等慣例操作。
基本概念
program
一個流程控制的完整程序躏将,也可能是按功能模塊劃分后的獨立完整程序锄弱。state set(ss)
通常一個program由一個或多個ss狀態(tài)集組成考蕾,這是一種邏輯上的劃分。在seq啟動的時候会宪,一個ss會分配一個線程肖卧,分別獨立的運行下去。state
ss內(nèi)部包含一個或多個state掸鹅,所有狀態(tài)組合形成了一個ss塞帐,在任何時候,一個ss內(nèi)只會停留在一個state狀態(tài)河劝,和普通狀態(tài)機相同。
基本語法
本文只針對sequencer結(jié)構(gòu)矛紫,詳盡的語法可以參考網(wǎng)站https://www-csr.bessy.de/control/SoftDist/sequencer/index.html赎瞎。
嵌入式代碼
sequencer是基于snl語法的,是一種c-like的代碼颊咬,而且可以調(diào)用嵌入的c代碼务甥,形式上是包含在百分號+括號內(nèi),如下所示喳篇。
{
#action代碼
%{
printError(ErrId, ErrLevel, FILE_AND_LINE_STRING, ": LayerSwitch\n");
}%
}
同時也可以先聲明后調(diào)用:
%{
epicsInt32 transINT32(double value, double norminal)
{
return (epicsInt32)(((pow(2, 30) - 1) * value) / norminal);
}
}%
{
#action代碼
val = transINT32(6, 2.0);
}
需要特別注意的是敞临,涉及pv的操作,要傳入?yún)?shù)ssid麸澜,表示這個操作是在哪個狀態(tài)集中完成的挺尿。ssid可以通過pvIndex
獲取,該調(diào)用不能嵌入到c代碼中炊邦。
官方宣傳是可以任何c/c++代碼的编矾,但本人在實際運用過程中,發(fā)現(xiàn)部分高級的是不支持的馁害,索性直接用native的c代碼好了窄俏,歡迎批評。
狀態(tài)機
作為狀態(tài)機碘菜,基本的要素包括:狀態(tài)凹蜈、事件、行為忍啸,簡單的描述就是在某種狀態(tài)仰坦,遇到某件事情,觸發(fā)某個動作计雌,并轉(zhuǎn)移到另一狀態(tài)缎岗。
下面的代碼簡單描述了snl狀態(tài)機。
ss state_set_name
{
state state1
{
entry
{
//action
}
when (event)
{
//action
} state state2
when (event)
{
//action
} state state3
exit
{
//action
}
}
state state2
{
...
}
}
- state 括號內(nèi)包含完成狀態(tài)描述白粉,在該狀態(tài)下需要監(jiān)聽的事件传泊,以及需要完成的動作鼠渺;
- entry 進入狀態(tài)時的行為,一般作為條件的初始化眷细;
- exit 退出狀態(tài)之前的行為拦盹,對應(yīng)的完成資源的釋放等;
- when 條件判定溪椎,所有的when會順序執(zhí)行普舆,直到某個條件滿足跳轉(zhuǎn)到對應(yīng)state;
- event 需要判定的條件校读,可以使某個變量沼侣,某個pv值,某個evtflag歉秫;
- action 具體的行為蛾洛,可調(diào)用sequencer提供的各種接口;
事件綁定
通常聲明變量雁芙、pv關(guān)聯(lián)轧膘、事件關(guān)聯(lián)等操作如下:
int reqpowersupply; //聲明變量
assign reqpowersupply to "G-ACS-PS:REQ-MRW"; //綁定pv
monitor reqpowersupply; //監(jiān)視pv值,及時改變變量值
evflag powersupplyevt; //聲明事件
sync reqpowersupply powersupplyevt; //綁定事件
這樣一個關(guān)聯(lián)pv的事件就定義好了兔甘,pv變量值得改變會直接改變事件變量谎碍,從而用efTest
判定事件達成與否。相應(yīng)地洞焙,efSet
可以直接改變事件變量的值蟆淀,efClear
清除變量的值。
以上采用sync
的方式綁定事件和變量澡匪,如果對于變化較快的pv值扳碍,可能無法及時處理事件的變化。因此使用syncQ
將每次改變暫存在隊列仙蛉,需要的時候pvGetQ
從隊列頭依次取出笋敞。
連接管理
通常在執(zhí)行證實邏輯前,需要判定pv連接情況荠瘪,包括分配數(shù)量夯巷、連接數(shù)量等。例如:
when (pvConnectCount() == pvAssignCount())
{
...
}
when (pvConnectCount() == pvChannelCount())
{
...
}
基本流程
編譯過程
預(yù)編譯
snc編譯器讀取.st或.stt文件哀墓,進行詞法趁餐、語法解析,編譯生成.c代碼篮绰。本文不對編譯過程和原理進行剖析后雷,僅對生成結(jié)果進行解析。
- program,整個sequencer的主體結(jié)構(gòu)臀突,一切要素的入口勉抓。
/* 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表,包含名字候学、狀態(tài)數(shù)量藕筋,以及指向狀態(tài)表的指針。
/* 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
},
};
- 狀態(tài)表梳码,包含狀態(tài)名隐圾、以及經(jīng)典的狀態(tài)機三要素。
/* 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)
},
};
- 狀態(tài)處理函數(shù)
seqg_entry_
進入到某個狀態(tài)時執(zhí)行掰茶,對應(yīng)語法為entry
暇藏。其中SS_ID
參數(shù)為當(dāng)前所在ss的id,程序內(nèi)部自動獲取濒蒋,用戶不應(yīng)關(guān)心盐碱。對應(yīng)的有exit
的處理函數(shù),本文未使用啊胶。
/* 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_
執(zhí)行條件檢查時需要甸各,對應(yīng)語法為when
垛贤。其中seqg_ptrn
表示將要執(zhí)行action的第幾個case焰坪,seqg_pnst
表示將要跳轉(zhuǎn)到第幾個狀態(tài)。均為按照出現(xiàn)先后聘惦,從0計數(shù)某饰。每個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
為各個條件下所需要執(zhí)行的行為黔漂,各個case對應(yīng)上文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命令行啟動,執(zhí)行seq programName即可剂跟,可直接在ioc啟動時執(zhí)行减途。程序啟動時,調(diào)用注冊到iocsh的
seqCallFunc
曹洽,解析第一個參數(shù)為program/threadID
鳍置,并且正式調(diào)用seq函數(shù),傳入包含該名字的seqProgram
結(jié)構(gòu)送淆。 注冊Program
首先會向sequencerProgram
結(jié)構(gòu)體注冊當(dāng)前ProgramseqProgram
,并創(chuàng)建Program的實例program_instance
。program_instance
可以看做seqProgram
的具象肄满,不僅包含前者的靜態(tài)數(shù)據(jù),還包含運行時分配的動態(tài)數(shù)據(jù)撞羽。例如動態(tài)assign數(shù)量、連接數(shù)量梧兼、monitor數(shù)量放吩、讀寫請求隊列等。-
初始化
首先是給evFlags
分配空間羽杰,bitMask類型的數(shù)組渡紫,用于事件達成的判定。然后給綁定的變量通道syncedChans
分配空間考赛,對于通過syncQ
方式綁定的事件惕澎,還會創(chuàng)建相應(yīng)數(shù)量的隊列。緊接著颜骤,給狀態(tài)集數(shù)組分配空間唧喉,有多少個ss就分配幾倍的
state_set
空間。分配好之后忍抽,就需要對每個狀態(tài)集進行初始化八孝。主要是對每個channel讀/寫請求的結(jié)構(gòu)創(chuàng)建,以及對應(yīng)的數(shù)據(jù)空間pv_meta_data
(包括時間戳鸠项、狀態(tài)干跛、嚴重程度以及錯誤消息)的創(chuàng)建。當(dāng)然祟绊,channel本身的空間CHAN
也是需要分配和初始化的楼入。在初始化過程中,有三個事件id非常重要牧抽。一個是用于時間同步的信號量嘉熊,一個是所有通道連接已建立的標志,一個是
ss
退出的標記扬舒。 -
主線程
初始化工作完成以后阐肤,會激發(fā)一個線程啟動主工作線程,其入口函數(shù)是sequencer
讲坎。首先孕惜,將program添加到program列表(一般而言,一個足以)衣赶。然后創(chuàng)建ca上下文诊赊,這點類似于epics框架下的client。
接著府瞄,會觸發(fā)當(dāng)前這個program的
initFunc
碧磅,該函數(shù)在snc編譯時自動生成碘箍。如下:
/* Program init func */
static void seqg_init(PROG_ID seqg_env)
{
}
接著,會對ss狀態(tài)集中每個變量進行初始化鲸郊。其中snc生成的變量按各個state區(qū)分開的丰榴,如下:
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
聲明在狀態(tài)st_InitAll里面盗蟆,作用域會有所不同。
接著舒裤,對所有的pv進行connect喳资,這步操作過程可參考《CA工作機制》。而線程會循環(huán)等待腾供,直到所有結(jié)果返回仆邓。如果狀態(tài)錯誤,那么會進入退出sequencer流程伴鳖,斷掉連接节值,釋放資源等。
接著榜聂,如果program有entryFunc
就會執(zhí)行搞疗,不過一般entryFunc
和exitFunc
均為空。當(dāng)然也可以利用這一時機峻汉,處理很多邏輯上的資源分配贴汪、釋放問題脐往。
最后休吠,為每個ss激發(fā)線程,使其在獨立線程中運行业簿,而主線程會直接接管第一個ss瘤礁,并跳轉(zhuǎn)到入口ss_entry
。這是整個狀態(tài)機的主循環(huán)梅尤,處理ss所有的事件信息柜思。
主循環(huán)
主循環(huán)是游戲里面的概念,邏輯上是個死循環(huán)巷燥,在循環(huán)體內(nèi)每次都要更新信息赡盘、處理事件等。在sequencer中缰揪,也是在while大循環(huán)中更新pv數(shù)據(jù)陨享、處理事件請求,并執(zhí)行相應(yīng)的狀態(tài)跳轉(zhuǎn)。
- 將
ss
的狀態(tài)切換成當(dāng)前state
抛姑,主要是狀態(tài)掛載的eventMask
信息赞厕,這是個多比特位數(shù)據(jù),用于事件觸發(fā)判定定硝; - 判定是否有狀態(tài)切換皿桑,并進入狀態(tài)的
entry
,執(zhí)行入口函數(shù)蔬啡; - 對所有未處理完成的pv事件進行一次flush诲侮,即強制發(fā)送所有緩存的pv請求。這樣保證在實際進入狀態(tài)前箱蟆,所有pv已處理完畢浆西;
- 觸發(fā)同步事件信號,喚醒所有需要同步的變量操作顽腾;
- 進入小循環(huán)近零,等待
evt
事件或者超時。一般包括pv的get/put/monitor抄肖、連接改變久信,以及evtFlag
的set和clear等,當(dāng)然ss
的退出等也會觸發(fā)信號量的而改變漓摩。
① 根據(jù)臟標記裙士,將所有變化的pv,從ca通道拷貝到sequencer變量管毙;
② 重新設(shè)置超時時間腿椎;
③ 檢查當(dāng)前狀態(tài)的when
條件是否滿足,打上觸發(fā)標記夭咬,等待執(zhí)行啃炸;
④ 重置綁定pv事件的標記,等待新的事件卓舵;
⑤ 如果有觸發(fā)標記南用,那么跳出小循環(huán),執(zhí)行后續(xù)操作掏湾;否則繼續(xù)小循環(huán)裹虫; - 執(zhí)行觸發(fā)標記對應(yīng)的
action
,執(zhí)行后跳轉(zhuǎn)到對應(yīng)的state
融击;發(fā)生狀態(tài)切換前筑公,會執(zhí)行exit
方法,完成可能需要的清理尊浪; - 如果有
dead
標記發(fā)生匣屡,則會退出主循環(huán)涩拙;
總結(jié)下來一句話,基于snl的sequencer耸采,依賴MainLoop處理各種事件和變量的更新兴泥,狀態(tài)機通過狀態(tài)表和指針的切換完成。整體結(jié)構(gòu)和實現(xiàn)細節(jié)虾宇,還是十分值得深入學(xué)習(xí)的搓彻。