,,,,
(南京南瑞繼保電氣有限公司,南京 211102)
IEC61131是國際電工委員會(IEC)頒布的可編程控制器(PLC)國際標準[1-5],它可以規范工業控制系統平臺和應用程序開發,從而降低用戶的使用難度和維護成本[6]。IEC61131-3為軟件設計提供了標準化的編程概念和編程方法,定義了5種語言規范,國內外工控廠家和科研機構已經開始提供基于該標準的產品并進行了應用[7-10]。參考文獻[7]介紹了嵌入式軟PLC系統的架構,設計了將IEC61131-3語言轉換為C語言的開發系統。參考文獻[8]提出一種將ST語言轉換為IL指令的方法,解決了ST語言語法分解和優先級算法的相關問題。參考文獻[9]提出了基于指令向量表的軟PLC系統實現方法,實現了梯形圖指令的高效執行。參考文獻[10]是本課題組的前期研究成果,介紹了結構化文本的虛擬機指令架構和編譯優化方法。PLC系統編程指令的執行方式有編譯型和解釋型方式。編譯型效率高,需要針對運行的硬件環境開發專門的編譯器。解釋型執行方式具有靈活性高、易于跨平臺移植的特點,適用于在線無擾更新的應用場景,但在實時性方面存在不足。針對參考文獻[10]開發的編譯器形成的二進制虛擬機指令,設計了配套的上位機仿真用解釋器,本文介紹了一種高效解釋執行的方案。
本文在指令設計時參考了VCODE和CIL指令集的部分理念[10]:使執行頻繁的部分保持高效,使其他部分保持正確。根據ST語言的規范和特性,支持可變形參,可內置調用更豐富的庫函數接口。以算術運算為例,在存儲時指令類型占用1字節,指令存在二元運算如add(rd,rs1,rs2)、一元運算如not(rd,rs)、跳轉指令jmp(lab)、函數調用scall等模式。采用緊湊型存儲,根據指令類型動態輸出形參個數。考慮內存對齊、方便快速掃描定位的前提下,內存中三地址碼指令存儲格式定義如下:

指令碼(2 Bytes)參數1(2 Bytes)參數2可選(2 Bytes)參數3可選(2 Bytes)
存儲和讀取時根據指令類型動態計算參數偏移,例如add指令為3個形參,not指令為2個形參。

圖1 指令文件格式
在上位機通過工具處理將ST/FBD/LD/SFC等類型的POU編譯形成解釋器側所需的指令文件,文件采用小端方式存儲。文件結構如圖1所示,包括文件頭 、引用的外部變量信息區、POU變量區、常量區、臨時變量區、指令區、字符串池(變長存儲)、擴展信息段等。
圖中的變量區按照POU聲明的變量順序排列,即POU的輸入變量、輸出變量、輸入-輸出變量、中間變量。變量序號從0遞增,指令區的操作數記錄的是變量序號,對于復雜結構體采用深度優先遍歷平鋪展開為基本變量類型的成員變量。
單個IEC61131定義的基本變量用IecVar結構體表示,作為最小粒度的邏輯分區存儲單元,它有兩個子結構體成員,為VAR_FLAG和VAR_VALUE。邏輯變量定義如圖2所示。

圖2 邏輯變量定義
VAR_FLAG是個2字節的結構體,記錄變量下IEC61131定義的變量屬性:
struct VAR_FLAG{
ushort var_type: 5; //參照ST的標準定義
ushort bretain: 1; //是否掉電保留
ushort bnegate: 1; //是否取反,
ushort bin_out: 1; //是否為輸入-輸出類型
ushort bconst: 1; //是否為常量,寫保護
ushort bredge: 1; //是否上升沿檢測
ushort bfedge: 1; //是否下降沿檢測
ushort bwrite_back: 1; //是否需要回寫
ushort bupdate: 1; //更新位
ushort resv : 3;
};
VAR_VALUE用于表示變量的值、字符串類型信息、時間類型變量信息,為枚舉結構:
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字節的結構體表示,2字節表示長度,2字節表示在字符串池中的起始位置。
指令編碼定義如下:
struct IRCode {
OpType tp; //指令類型add/sub等
ushort arg1; //通常是目的地址序號
ushort arg2; //通常是源操作數序號1
ushort arg3; //通常是源操作數序號2
};
指令運行條目信息,存儲運行中變量地址和關聯的指令執行函數,是運行時調度的基本單位:
struct IRItem{
ushort idx; //指令序號
uint parg1; //形參1對應變量區變量地址
uint parg2; //形參2對應變量區變量地址
uint parg3; //形參地址或立即數
void (*irfunc)(IRItem* p); //指令執行函數
};
指令文件類定義如下:
class CMidFile {
public:
CMidFile(); ~CMidFile();
public:
MID_HEADER m_header; //文件頭
QList
QList
QList
QList
QList
QList
//存儲字符串列表
…};
解釋器在初始化時,讀取指令文件,形成變量鏈表和指令數組。
根據控制器內ST指令文件解析、執行的流程,可將解釋器的功能結構分為三個子模塊,如圖3所示。

圖3 解釋器運行模型
初始化加載:對指令文件進行初步解析,包括將緊湊排列的變量按照以IecVar為單位分配,方便數據信息的結構化封裝以及索引定位。
解釋執行任務:根據預處理后形成的內存文件,對指令區的指令逐條解釋執行,并讀取外部變量區和本地變量區的相關數據。
數據刷新任務:通過監視接口對內存文本中的數據進行設置、觀測,通過調試接口和調試符號表對指令區執行過程進行干預控制。
解釋器在初始化時讀取指令文件,先讀取文件頭,獲取各個變量區變量個數、指令個數等統計信息。之后依次讀取變量區、指令區、字符串池、調試表、擴展信息區。為了節省空間,變量值VAR_VALUE在文件中存儲時按照實際類型大小存儲,在讀取時,需要根據變量類型動態調整讀取的buf長度,單個變量的讀取示例如下:
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;
…}
指令區解析根據指令類型解析若干形參,形成指令編碼如下:
IRCode* pIR = new IRCode();
m_IRList.append(pIR);
pIR->tp = (OpType)getInt16(pbuf, &tmpoff);
switch(pIR->tp){
case e_add: //3個形參
case e_div:
…
pIR->arg1 = getInt16(pbuf, &tmpoff);
pIR->arg2 = getInt16(pbuf, &tmpoff);
pIR->arg3 = getInt16(pbuf, &tmpoff);
break;
case e_asgn: // 2個形參
case e_not:
…
pIR->arg1 = getInt16(pbuf, &tmpoff);
pIR->arg2 = getInt16(pbuf, &tmpoff);
break;
case e_jmp:// 1個形參
case e_lab:
pIR->arg1 = getInt16(pbuf, &tmpoff);
break;
}
在初始化過程中,通過如下處理為運行時提高效率創造條件:
① 在讀取完變量區和指令區后,根據指令形參中記錄的變量區序號,查找到動態分配的IecVar變量的地址,形成IRItem條目結構,并根據指令類型關聯對應的指令解析函數或系統庫函數指針,在初始化時完成數據形參、執行函數的關聯,實現初始化一次查找關聯,執行時直接使用轉換。圖4是add指令的形參和執行函數的關聯過程。

圖4 IRItem形成過程示例
② 創建lab標號和指令序號的hash表,即QHash
根據ST語言的基本指令碼索引其函數指針數組,獲得對應功能函數指針,傳入形參地址,進行解釋執行。對于系統庫函數指令碼的解釋執行,首先定位scall指令,再根據其第一個形參值(系統庫函數的指令碼值)索引定位系統庫函數指令碼函數指針數組,獲取對應功能函數指針,傳入在系統庫變量區的功能塊結構體首地址,進行解釋執行。當執行到JZ、JMP等跳轉指令時,將當前執行的指令數組下標修改為跳轉指令記錄的跳轉目的標號,之后順序執行從新下標起始對應的指令。由于初始化過程中已經根據指令類型設置了對應的執行函數指針,故運行過程中不需要進行指令類型判斷,順次執行即可:
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(); //將值更新到輸出 } 其中跳轉指令執行函數的作用是動態調整當前指令游標m_it、m_curpos,從而間接修改主函數while循環中下次運行的起始位置,實現指令數組順次執行可跳轉的功能: inline exec_jmp(IRItem* p){ ushort label = (ushort)p->arg1; QHash it = m_labelHash.find(label); if(it!=m_labelHash.end()){ //動態調整當前下標到跳轉的標簽處 int labpos = (int)it.value(); m_it += labpos-m_curpos; m_curpos = labpos; } }結 語
