,,,,
(南京南瑞繼保電氣有限公司,南京 211102)
IEC61131是國(guó)際電工委員會(huì)(IEC)頒布的可編程控制器(PLC)國(guó)際標(biāo)準(zhǔn)[1-5],它可以規(guī)范工業(yè)控制系統(tǒng)平臺(tái)和應(yīng)用程序開(kāi)發(fā),從而降低用戶(hù)的使用難度和維護(hù)成本[6]。IEC61131-3為軟件設(shè)計(jì)提供了標(biāo)準(zhǔn)化的編程概念和編程方法,定義了5種語(yǔ)言規(guī)范,國(guó)內(nèi)外工控廠(chǎng)家和科研機(jī)構(gòu)已經(jīng)開(kāi)始提供基于該標(biāo)準(zhǔn)的產(chǎn)品并進(jìn)行了應(yīng)用[7-10]。參考文獻(xiàn)[7]介紹了嵌入式軟PLC系統(tǒng)的架構(gòu),設(shè)計(jì)了將IEC61131-3語(yǔ)言轉(zhuǎn)換為C語(yǔ)言的開(kāi)發(fā)系統(tǒng)。參考文獻(xiàn)[8]提出一種將ST語(yǔ)言轉(zhuǎn)換為IL指令的方法,解決了ST語(yǔ)言語(yǔ)法分解和優(yōu)先級(jí)算法的相關(guān)問(wèn)題。參考文獻(xiàn)[9]提出了基于指令向量表的軟PLC系統(tǒng)實(shí)現(xiàn)方法,實(shí)現(xiàn)了梯形圖指令的高效執(zhí)行。參考文獻(xiàn)[10]是本課題組的前期研究成果,介紹了結(jié)構(gòu)化文本的虛擬機(jī)指令架構(gòu)和編譯優(yōu)化方法。PLC系統(tǒng)編程指令的執(zhí)行方式有編譯型和解釋型方式。編譯型效率高,需要針對(duì)運(yùn)行的硬件環(huán)境開(kāi)發(fā)專(zhuān)門(mén)的編譯器。解釋型執(zhí)行方式具有靈活性高、易于跨平臺(tái)移植的特點(diǎn),適用于在線(xiàn)無(wú)擾更新的應(yīng)用場(chǎng)景,但在實(shí)時(shí)性方面存在不足。針對(duì)參考文獻(xiàn)[10]開(kāi)發(fā)的編譯器形成的二進(jìn)制虛擬機(jī)指令,設(shè)計(jì)了配套的上位機(jī)仿真用解釋器,本文介紹了一種高效解釋執(zhí)行的方案。
本文在指令設(shè)計(jì)時(shí)參考了VCODE和CIL指令集的部分理念[10]:使執(zhí)行頻繁的部分保持高效,使其他部分保持正確。根據(jù)ST語(yǔ)言的規(guī)范和特性,支持可變形參,可內(nèi)置調(diào)用更豐富的庫(kù)函數(shù)接口。以算術(shù)運(yùn)算為例,在存儲(chǔ)時(shí)指令類(lèi)型占用1字節(jié),指令存在二元運(yùn)算如add(rd,rs1,rs2)、一元運(yùn)算如not(rd,rs)、跳轉(zhuǎn)指令jmp(lab)、函數(shù)調(diào)用scall等模式。采用緊湊型存儲(chǔ),根據(jù)指令類(lèi)型動(dòng)態(tài)輸出形參個(gè)數(shù)??紤]內(nèi)存對(duì)齊、方便快速掃描定位的前提下,內(nèi)存中三地址碼指令存儲(chǔ)格式定義如下:
指令碼(2 Bytes)參數(shù)1(2 Bytes)參數(shù)2可選(2 Bytes)參數(shù)3可選(2 Bytes)
存儲(chǔ)和讀取時(shí)根據(jù)指令類(lèi)型動(dòng)態(tài)計(jì)算參數(shù)偏移,例如add指令為3個(gè)形參,not指令為2個(gè)形參。
圖1 指令文件格式
在上位機(jī)通過(guò)工具處理將ST/FBD/LD/SFC等類(lèi)型的POU編譯形成解釋器側(cè)所需的指令文件,文件采用小端方式存儲(chǔ)。文件結(jié)構(gòu)如圖1所示,包括文件頭 、引用的外部變量信息區(qū)、POU變量區(qū)、常量區(qū)、臨時(shí)變量區(qū)、指令區(qū)、字符串池(變長(zhǎng)存儲(chǔ))、擴(kuò)展信息段等。
圖中的變量區(qū)按照POU聲明的變量順序排列,即POU的輸入變量、輸出變量、輸入-輸出變量、中間變量。變量序號(hào)從0遞增,指令區(qū)的操作數(shù)記錄的是變量序號(hào),對(duì)于復(fù)雜結(jié)構(gòu)體采用深度優(yōu)先遍歷平鋪展開(kāi)為基本變量類(lèi)型的成員變量。
單個(gè)IEC61131定義的基本變量用IecVar結(jié)構(gòu)體表示,作為最小粒度的邏輯分區(qū)存儲(chǔ)單元,它有兩個(gè)子結(jié)構(gòu)體成員,為VAR_FLAG和VAR_VALUE。邏輯變量定義如圖2所示。
圖2 邏輯變量定義
VAR_FLAG是個(gè)2字節(jié)的結(jié)構(gòu)體,記錄變量下IEC61131定義的變量屬性:
struct VAR_FLAG{
ushort var_type: 5; //參照ST的標(biāo)準(zhǔn)定義
ushort bretain: 1; //是否掉電保留
ushort bnegate: 1; //是否取反,
ushort bin_out: 1; //是否為輸入-輸出類(lèi)型
ushort bconst: 1; //是否為常量,寫(xiě)保護(hù)
ushort bredge: 1; //是否上升沿檢測(cè)
ushort bfedge: 1; //是否下降沿檢測(cè)
ushort bwrite_back: 1; //是否需要回寫(xiě)
ushort bupdate: 1; //更新位
ushort resv : 3;
};
VAR_VALUE用于表示變量的值、字符串類(lèi)型信息、時(shí)間類(lèi)型變量信息,為枚舉結(jié)構(gòu):
union LOGIC_VAR_VALUE{
uint m_uint;
int m_int;
float m_float;
USTR m_str; //字符串
…
uint64 m_uint64;
double m_double;
IEC_TIME m_t;
IEC_DATE m_date;
…};
其中字符串用4字節(jié)的結(jié)構(gòu)體表示,2字節(jié)表示長(zhǎng)度,2字節(jié)表示在字符串池中的起始位置。
指令編碼定義如下:
struct IRCode {
OpType tp; //指令類(lèi)型add/sub等
ushort arg1; //通常是目的地址序號(hào)
ushort arg2; //通常是源操作數(shù)序號(hào)1
ushort arg3; //通常是源操作數(shù)序號(hào)2
};
指令運(yùn)行條目信息,存儲(chǔ)運(yùn)行中變量地址和關(guān)聯(lián)的指令執(zhí)行函數(shù),是運(yùn)行時(shí)調(diào)度的基本單位:
struct IRItem{
ushort idx; //指令序號(hào)
uint parg1; //形參1對(duì)應(yīng)變量區(qū)變量地址
uint parg2; //形參2對(duì)應(yīng)變量區(qū)變量地址
uint parg3; //形參地址或立即數(shù)
void (*irfunc)(IRItem* p); //指令執(zhí)行函數(shù)
};
指令文件類(lèi)定義如下:
class CMidFile {
public:
CMidFile(); ~CMidFile();
public:
MID_HEADER m_header; //文件頭
QList
QList
QList
QList
QList
QList
//存儲(chǔ)字符串列表
…};
解釋器在初始化時(shí),讀取指令文件,形成變量鏈表和指令數(shù)組。
根據(jù)控制器內(nèi)ST指令文件解析、執(zhí)行的流程,可將解釋器的功能結(jié)構(gòu)分為三個(gè)子模塊,如圖3所示。
圖3 解釋器運(yùn)行模型
初始化加載:對(duì)指令文件進(jìn)行初步解析,包括將緊湊排列的變量按照以IecVar為單位分配,方便數(shù)據(jù)信息的結(jié)構(gòu)化封裝以及索引定位。
解釋執(zhí)行任務(wù):根據(jù)預(yù)處理后形成的內(nèi)存文件,對(duì)指令區(qū)的指令逐條解釋執(zhí)行,并讀取外部變量區(qū)和本地變量區(qū)的相關(guān)數(shù)據(jù)。
數(shù)據(jù)刷新任務(wù):通過(guò)監(jiān)視接口對(duì)內(nèi)存文本中的數(shù)據(jù)進(jìn)行設(shè)置、觀(guān)測(cè),通過(guò)調(diào)試接口和調(diào)試符號(hào)表對(duì)指令區(qū)執(zhí)行過(guò)程進(jìn)行干預(yù)控制。
解釋器在初始化時(shí)讀取指令文件,先讀取文件頭,獲取各個(gè)變量區(qū)變量個(gè)數(shù)、指令個(gè)數(shù)等統(tǒng)計(jì)信息。之后依次讀取變量區(qū)、指令區(qū)、字符串池、調(diào)試表、擴(kuò)展信息區(qū)。為了節(jié)省空間,變量值VAR_VALUE在文件中存儲(chǔ)時(shí)按照實(shí)際類(lèi)型大小存儲(chǔ),在讀取時(shí),需要根據(jù)變量類(lèi)型動(dòng)態(tài)調(diào)整讀取的buf長(zhǎng)度,單個(gè)變量的讀取示例如下:
CIecVar* pvar = new CIecVar(varIdx++);
varList.append(pvar);
getMemory(buf, &tmpoff, (uchar*)&pvar->flag, len1);
switch( pvar->m_flag.var_type){
case e_BOOL:
case e_SINT:
pvar->m_val.m_char = (char)getChar(buf, &tmpoff);
break;
case e_USINT:
case e_BYTE:
pvar->m_val.m_uchar = getChar(buf, &tmpoff);
break;
…}
指令區(qū)解析根據(jù)指令類(lèi)型解析若干形參,形成指令編碼如下:
IRCode* pIR = new IRCode();
m_IRList.append(pIR);
pIR->tp = (OpType)getInt16(pbuf, &tmpoff);
switch(pIR->tp){
case e_add: //3個(gè)形參
case e_div:
…
pIR->arg1 = getInt16(pbuf, &tmpoff);
pIR->arg2 = getInt16(pbuf, &tmpoff);
pIR->arg3 = getInt16(pbuf, &tmpoff);
break;
case e_asgn: // 2個(gè)形參
case e_not:
…
pIR->arg1 = getInt16(pbuf, &tmpoff);
pIR->arg2 = getInt16(pbuf, &tmpoff);
break;
case e_jmp:// 1個(gè)形參
case e_lab:
pIR->arg1 = getInt16(pbuf, &tmpoff);
break;
}
在初始化過(guò)程中,通過(guò)如下處理為運(yùn)行時(shí)提高效率創(chuàng)造條件:
① 在讀取完變量區(qū)和指令區(qū)后,根據(jù)指令形參中記錄的變量區(qū)序號(hào),查找到動(dòng)態(tài)分配的IecVar變量的地址,形成IRItem條目結(jié)構(gòu),并根據(jù)指令類(lèi)型關(guān)聯(lián)對(duì)應(yīng)的指令解析函數(shù)或系統(tǒng)庫(kù)函數(shù)指針,在初始化時(shí)完成數(shù)據(jù)形參、執(zhí)行函數(shù)的關(guān)聯(lián),實(shí)現(xiàn)初始化一次查找關(guān)聯(lián),執(zhí)行時(shí)直接使用轉(zhuǎn)換。圖4是add指令的形參和執(zhí)行函數(shù)的關(guān)聯(lián)過(guò)程。
圖4 IRItem形成過(guò)程示例
② 創(chuàng)建lab標(biāo)號(hào)和指令序號(hào)的hash表,即QHash
根據(jù)ST語(yǔ)言的基本指令碼索引其函數(shù)指針數(shù)組,獲得對(duì)應(yīng)功能函數(shù)指針,傳入形參地址,進(jìn)行解釋執(zhí)行。對(duì)于系統(tǒng)庫(kù)函數(shù)指令碼的解釋執(zhí)行,首先定位scall指令,再根據(jù)其第一個(gè)形參值(系統(tǒng)庫(kù)函數(shù)的指令碼值)索引定位系統(tǒng)庫(kù)函數(shù)指令碼函數(shù)指針數(shù)組,獲取對(duì)應(yīng)功能函數(shù)指針,傳入在系統(tǒng)庫(kù)變量區(qū)的功能塊結(jié)構(gòu)體首地址,進(jìn)行解釋執(zhí)行。當(dāng)執(zhí)行到JZ、JMP等跳轉(zhuǎn)指令時(shí),將當(dāng)前執(zhí)行的指令數(shù)組下標(biāo)修改為跳轉(zhuǎn)指令記錄的跳轉(zhuǎn)目的標(biāo)號(hào),之后順序執(zhí)行從新下標(biāo)起始對(duì)應(yīng)的指令。由于初始化過(guò)程中已經(jīng)根據(jù)指令類(lèi)型設(shè)置了對(duì)應(yīng)的執(zhí)行函數(shù)指針,故運(yùn)行過(guò)程中不需要進(jìn)行指令類(lèi)型判斷,順次執(zhí)行即可:
void CInterpreter:::execIRList(){
readInput(); //刷新外部輸入
m_curpos = 0;
m_it =m_IRList.begin();
while(m_curpos IRItem* pitem =*m_it; if(pitem->irfunc ) pitem->irfunc(pitem); ++m_it; ++m_curpos; } writeOutput(); //將值更新到輸出 } 其中跳轉(zhuǎn)指令執(zhí)行函數(shù)的作用是動(dòng)態(tài)調(diào)整當(dāng)前指令游標(biāo)m_it、m_curpos,從而間接修改主函數(shù)while循環(huán)中下次運(yùn)行的起始位置,實(shí)現(xiàn)指令數(shù)組順次執(zhí)行可跳轉(zhuǎn)的功能: inline exec_jmp(IRItem* p){ ushort label = (ushort)p->arg1; QHash it = m_labelHash.find(label); if(it!=m_labelHash.end()){ //動(dòng)態(tài)調(diào)整當(dāng)前下標(biāo)到跳轉(zhuǎn)的標(biāo)簽處 int labpos = (int)it.value(); m_it += labpos-m_curpos; m_curpos = labpos; } }結(jié) 語(yǔ)