,,,,
(南京南瑞继保电气有限公司,南京 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; } }结 语