13 HDL4SE建模与仿真
前面我們以LCOM为基础,引入了一套建模的方法,基本的意图是,设计一套LCOM接口,把数字电路模型设计为LCOM的类,实现数字电路描述和仿真的相关接口,然后用这些接口来支持建模与仿真。我们还用这个模式建立了一组基本单元,可以在建模过程中使用。这个过程在前面第8节中描述过。然而这种方法的问题是,建模工程师还是得跟比较复杂的LCOM八股文打交道,大部分精力用来维护满足LCOM规范上。使用LCOM提供了更多的灵活性,但是需要软件方面更多的工作,就像用专业相机拍照似的,需要更多的参数设置。事实上,在建模实践中,我们需要一个“傻瓜”的系统,大部分的模型其实都是服从一个模式的,不需要那么多的灵活性,此时需要象傻瓜相机一样的东西,简单几句描述,就能够建立模型,而不是精细地满足各种程序规范,设计数据结构,然后实现一个高效的模型出来。
因此,我们借鉴systemc的描述方法,设计了一套c语言下面的宏和库支持的描述方式,用来支持HDL4SE建模,编译器也随着改为生成这种描述方式的格式。下面我们分段说明这种描述方式。
13.1 模型组成
我们用半加器作为例子来说明,一个模型包括模型基本参数,变量名列表,模型声明,模型函数,模型初始化等几个部分:
/* 模型基本参数 */
#define M_ID(id) half_adder_##id
/* 变量名列表 */
IDLIST
VID(a),
VID(b),
VID(sum),
VID(carry),
END_IDLIST
/*模型声明*/
GEN_MODULE_DECLARE
END_GEN_MODULE_DECLARE
/* 模型函数 */
DEFINE_FUNC(do_Generate_Result, "a, b") {
unsigned int ia, ib;
ia = VREAD_U32(a);
ib = VREAD_U32(b);
VWRITE_U32(sum, ia ^ ib);
VWRITE_U32(carry, ia & ib);
} END_DEFINE_FUNC
/* 模型初始化 */
GEN_MODULE_INIT
PORT_IN(a, 1);
PORT_IN(b, 1);
PORT_OUT(sum, 1);
PORT_OUT(carry, 1);
GEN_FUNC("sum, carry", do_Generate_Result);
END_GEN_MODULE_INIT
#undef M_ID
一个模型以M_ID宏的定义开头,M_ID宏会被HDL4SE模型描述的宏引用,定义与模型相关的标识符。
#define M_ID(id) half_adder_##id
这个宏定义了后面直到#undef M_ID,中间的HDL4SE模型引用这个宏生成与half_adder相关的标识符。
我们内部用编号来表示每个模型中的数据,但是建模时显然还是更习惯用标识符,因此定义一个IDLIST的宏,用来将名称和编号关联在一起,所有在模型中出现的数据对象都应该在这个表中定义一项。
IDLIST
VID(a),
VID(b),
VID(sum),
VID(carry),
END_IDLIST
实际实现时,IDLIST被定义为c语言的enum,VID宏则直接使用前面的M_ID宏定义标识符:
#define VID(name) M_ID(name##_index)
#define IDLIST enum M_ID(id_list) {
#define END_IDLIST };
因此前面的IDLIST展开后相当于c语言代码:
enum half_adder_id_list {
half_adder_a_index,
half_adder_b_index,
half_adder_sum_index,
half_adder_carry_index,
};
这样处理,既照顾了标识符命名空间的问题,又能够让建模工程师比较容易理解和使用相应的标识符,在模型中,只要理解a这个标识符就行了,不用知道其实内部是通过half_adder_a_index这个符号来代表的编号来访问a这个数据。
后面的:
GEN_MODULE_DECLARE
END_GEN_MODULE_DECLARE
用来声明模型中的c语言局部数据结构,我们后面会详细描述。
再后面用DEFINE_FUNC 和END_DEFINE_FUNC括起来的代码定义一个所谓的模型函数,供建模时使用。一个模型中可能有多个模型函数,用于生成各种信号或寄存器的值,也作为时钟相应函数,模型的销毁函数等功能。后面也会进行详细的描述。
后面时模型的初始化部分:
GEN_MODULE_INIT
PORT_IN(a, 1);
PORT_IN(b, 1);
PORT_OUT(sum, 1);
PORT_OUT(carry, 1);
GEN_FUNC("sum, carry", do_Generate_Result);
END_GEN_MODULE_INIT
这部分定义模型的参数,端口,线网以及寄存器,还可以在这个部分进行模型实例化,实现层次化描述。关于模型初始化,我们在后面会进行详细描述。
13.2 模型基本参数
我们支持两种方式描述模型,一种是只能通过模型名字来实例化的模型,描述的模型实例化时直接引用模型的名称,我们称之为命名模型,一种是库模型,实现为标准的LCOM模块,模型实例化用CLSID引用,当然也可以通过名字访问,这类模型可以不提供源代码,直接用静态库或者动态库提供二进制码即可。在一个模型中,可以实例化两种不同的模型。
13.2.1 命名模型
命名模型就是仅仅用名称标识的模型,前面的半加器就是一个命名模型。此时模型基本参数就是一个命名说明:
/* 模型基本参数 */
#define M_ID(id) half_adder_##id
声明时用GEN_MODULE_DECLARE和END_GEN_MODULE_DECLARE括住,初始化时用GEN_MODULE_INIT和END_GEN_MODULE_INIT括住。实例化则用MODULE_INST宏完成,具体的用法后面会详细说明。
13.2.2 库模型
库模型除了给出名称标识外,还需要给出模型的CLSID和版本字符串:
#define M_ID(id) hdl4se_fifo##id
#define hdl4se_fifo_MODULE_VERSION_STRING "0.4.0-20210728.1558 HDL4SE fifo cell"
#define hdl4se_fifo_MODULE_CLSID CLSID_HDL4SE_FIFO
声明时用MODULE_DECLARE(hdl4se_fifo)和END_MODULE_DECLARE(hdl4se_fifo)括住,其中hdl4se_fifo就是模型的名称。
初始化时则用MODULE_INIT(hdl4se_fifo)和END_MODULE_INIT(hdl4se_fifo)括住,这四个宏提供了满足LCOM规范的函数定义和实现。
实例化则用CELL_INST来实现,表示这种模型作为一个基本单元使用。
13.3 模型中的数据标识
一个数字电路模型会跟下面几种数据打交道:参数,端口,线网,寄存器,临时变量,其中参数可能是整数,浮点数和字符串,端口可以是输入,输出和输入输出,这些数据都可以指定宽度,是否带符号等。参数在模型内是常数,不能赋值,端口中输入端口不能赋值,输出端口可以赋值,线网可以赋值,寄存器比较特殊,可以赋值,但是赋予的值在下一周期才会有效。
我们在实现时每个数据(参数除外)用一个ModuleVariable变量描述:
typedef struct sModuleVariable {
struct sModuleVariable * pNext, *pLast;
int type; /*类型:VTYPE_PORT, VTYPE_WIRE, VTYPE_REG, VTYPE_TEMP */
int width; /* 变量宽度 */
int portdirect; /* VTYPE_PORT类型变量对应的端口方向 */
int isunsigned; /* 是否是无符号数 */
char* name; /* 名称 */
sGeneralModule *moduledata; /* 所在的模块 */
IHDL4SEModuleVar* module; /* 连接在该变量上的模型实例,module为NULL时表示该变量上没有连接其他模型实例的端口 */
int moduleportindex; /* 连接在该变量上的模型端口编号 */
char* depend_list; /* 该变量依赖的变量表,表中的变量改变时,该变量需要重新计算 */
PointerArray variables; /* 依赖它的变量, 从依赖表以及连接关系中得到的影响表,表示该变量修改时,会影响哪些变量 */
int updatedisset; /* 仿真过程中的标记,在计算需要计算的变量时,如果该变量需要更新,则设置这个标记为1,
然后通知variables表中的各个变量也需要更新,用这个变量来防止递归循环设置,
该标记在变量重新计算后清零*/
int genfuncindex; /* 生成该数据的函数编号,变量的更新方式有两种,一种是指定模型函数进行更新,一种是指定模型实例的连接,
通过得到其他模型实例的端口值进行更新,genfuncindex就是模型函数的编号,这个编号为-1表示没有指定
对应的模型函数*/
#if USEBIGINT
IBigNumber** data;
IBigNumber** data_reg;
#else
union {
short int16;
unsigned short uint16;
int int32;
unsigned int uint32;
long long int64;
unsigned long long uint64;
unsigned long long data;
};
union {
short int16_reg;
unsigned short uint16_reg;
int int32_reg;
unsigned int uint32_reg;
long long int64_reg;
unsigned long long uint64_reg;
unsigned long long data_reg;
};
#endif
/* 上面的定义是变量中的数据,注意_reg为结尾的是寄存器类型变量特有的,对变量赋值时,如果对寄存器变量进行赋值,那就时对_reg结尾的成员进行赋值,对非寄存器变量进行赋值,则是对没有带_reg的成员赋值 */
THREADLOCK lock; /* 变量的线程锁,这个锁用来支持多线程仿真的基础,如果多个线程申请对同一个变量进行值更新,则用这个锁确保只有获得这个锁的第一个线程进行更新,其他线程则在获得锁时等待更新完成后直接获取值 */
int updatefunc; /* 这个标志用来控制变量是否需要更新,在每个周期的建立阶段,会检查每个寄存器是否修改(通过比较data和data_reg),如果寄存器被修改,则通过寄存器变量的variables表通知影响的每个变量需要更新,这个过程会扩散下去,直到所欲的依赖变量都扩散到(用前面的updatedisset来控制不会出现重复扩散),updatefunc在需要更新时设置为VUF_WAITUPDATE, 不需要更新的变量设置为VUF_UPDATED, 在更新过程中,如果该标志时VUF_UPDATED,则不需要重新计算该变量,如果时VUF_WAITUPDATE,则改为VUF_UPDATING,然后重新计算该变量,计算完成后修改为VUF_UPDATED,如果是VUF_UPDATING,则表示有其他线程正在更新这个变量,此时通过lock等待更新完成,我们通过对该成员的原子操作完成这个过程 */
}ModuleVariable;
所有的变量被存放在模型数据sGeneralModule结构的一个数组中,可以通过编号进行访问,变量的编号就是前面IDLIST中定义的编号。
我们要求在IDLIST中所有的端口按顺序占据IDLIST的前面几个编号,端口后面的线网和寄存器的顺序则无关紧要。临时变量不进入这个数组,也不通过编号访问。
13.4 模型声明
13.2中已经指出,模型有两类,需要两种方式声明。一个模型实例在C语言中是一个数据结构,这个数据结构包括两个部分,一个部分是通用的模型表示数据,一个部分是自定义的数据结构,模型声明时其实是声明一个自定义的数据结构,其中也包括通用的模型数据。
13.4.1 命名模型声明
命名模型声明用GEN_MODULE_DECLARE和END_GEN_MODULE_DECLARE括住的代码完成,中间可以声明模型需要使用的c语言数据结构。
GEN_MODULE_DECLARE
/* 声明模型需要使用的c语言数据结构 */
END_GEN_MODULE_DECLARE
这两个宏都引用了用户定义的M_ID宏,这个声明展开后的c语言代码是这样的(用在前面的half_adder定义中):
struct half_adder__sHDL4SE_data; \
typedef struct half_adder__sHDL4SE_data half_adder_sHDL4SE_data; \
struct half_adder__sHDL4SE_data {
sGeneralModule* pmodule;
/* 声明模型需要使用的c语言数据结构 */
};
可以看出,half_adder的数据结构中就只有一个pmodule的指针。事实上实现的时候,half_adder实现为HDL4SE_MODULE的一个实例,这个实例的数据结构就是sGeneralModule,这个结构定义如下:
typedef int (*MODULE_FUNC)(void* pobj, void * variable);
typedef struct _sGeneralModule {
IHDL4SEModuleVar * parent;
char* name;
PointerArray parameters;
PointerArray variables;
PointerArray modules;
PointerArray funcs;
MODULE_FUNC setup_func;
MODULE_FUNC clktick_func;
MODULE_FUNC init_func;
MODULE_FUNC deinit_func;
THREADLOCK lock;
void* pobj;
void* priv_data;
void* func_param;
int canruninthread;
}sGeneralModule;
命名模型类型实例化时,其实是实例化了HDL4SE_MODULE类的一个实例,在实例化时生成了一个half_adder_sHDL4SE_data的变量,将这个变量的指针存放在sGeneralModule结构中的priv_data中,同时也将sGeneralModule数据的指针存放在half_adder__sHDL4SE_data 的pmodule成员中,这样就可以相互进行引用访问。这个过程我们后面会更加详细地描述。
13.4.2 库模型的声明
库模型的声明,使用MODULE_DECLARE和END_MODULE_DECLARE宏完成,比如hdl4se_fifo的声明:
MODULE_DECLARE(hdl4se_fifo)
unsigned int width;
unsigned int depth;
unsigned int wordsize;
unsigned int* fifo_data;
END_MODULE_DECLARE(hdl4se_fifo)
展开后的c语言代码是这样的:
struct struct hdl4se_fifo_sHDL4SE_data;
typedef struct hdl4se_fifo_sHDL4SE_data hdl4se_fifosHDL4SE_data;
struct hdl4se_fifo_sHDL4SE_data {
OBJECT_HEADER
HDL4SEMODULE_VARDECLARE
DLIST_VARDECLARE
unsigned int width;
unsigned int depth;
unsigned int wordsize;
unsigned int* fifo_data;
};
OBJECT_FUNCDECLARE(hdl4se_fifo, hdl4se_fifo_MODULE_CLSID);
HDL4SEMODULE_FUNCIMPL(hdl4se_fifo, hdl4se_fifo_MODULE_CLSID, hdl4se_fifosHDL4SE_data);
DLIST_FUNCIMPL(hdl4se_fifo, hdl4se_fifo_MODULE_CLSID, hdl4se_fifosHDL4SE_data);
OBJECT_FUNCIMPL(hdl4se_fifo, hdl4se_fifosHDL4SE_data, hdl4se_fifo_MODULE_CLSID);
QUERYINTERFACE_BEGIN(hdl4se_fifo, hdl4se_fifo_MODULE_CLSID)
QUERYINTERFACE_ITEM(IID_HDL4SEMODULE, IHDL4SEModule, hdl4se_fifosHDL4SE_data)
QUERYINTERFACE_ITEM(IID_DLIST, IDList, hdl4se_fifosHDL4SE_data)
QUERYINTERFACE_END
static const char* hdl4se_fifoModuleInfo()
{
return hdl4se_fifo_MODULE_VERSION_STRING;
}
IHDL4SEModuleVar * hdl4se_fifo_module_create(HOBJECT parent, const char* instanceparam, const char* name, const char * connectlist)
{
IHDL4SEModuleVar * module;
IHDL4SEModuleVar* parentmodule = NULL;
A_u_t_o_registor_hdl4se_fifo();
module = hdl4seCreateModule(parent, hdl4se_fifo_MODULE_CLSID, instanceparam, name);
objectQueryInterface(parent, IID_HDL4SEMODULE, (const void**)&parentmodule);
if (parentmodule == NULL) {
return module;
}
hdl4se_module_Connect(parentmodule, module, connectlist);
objectRelease(parentmodule);
return module;
}
可以看到库模型声明展开后要复杂很多,毕竟要实现一个新的LCOM类,它定义了LCOM八股文的"起"的部分代码,它还定义了一个函数hdl4se_fifo_module_create,供实例化时使用。从这段代码中我们可以看出,建模工程师的注意力可以集中在模型相关的数据结构方面了,不需要过多地去关注LCOM八股文。
13.4.3 模型的跨文件使用
模型描述和模型使用不在一个文件中时,可以在使用的文件中声明模型的实例化生成函数,然后就可以用实例化宏正常使用命名实例化。实例化声明函数可以用下面的宏定义:
#define MODULE_CREATOR_DECLARE(module_name) IHDL4SEModuleVar * module_name##_module_create(HOBJECT parent, const char* instanceparam, const char* name, const char * connectlist)
当然建模时可以提供一个c语言的头文件,将模型生成函数和库模型类的CLSID放在其中,使用的文件只要包含这个文件即可。
如果由于各种考虑,比如基于命名空间的考虑,一个模型的实例化生成函数不想成为能够跨文件访问的,那就在模型定义的开始将这个实例化生成函数定义为static的就可以了。比如要将half_adder设置为局部可见的,可以在模型声明之前增加这样的声明:
static MODULE_CREATOR_DECLARE(half_adder);
当然,此时就不应该允许该模型跨文件使用了。
看上去,我们其实只要提供命名模型就可以了,然而我们还是保留了通过CLSID生成模型的库模型方式,这个主要是要支持用verilog语言调用c语言写的模型,实现C语言和verilog语言的模型级别比较方便互换的功能,只要在verilog代码中用HDL4SE单元的方式描述模型,编译器就会通过CLSID直接调用c语言写的模型,否则就使用verilog代码使用的模型,这在建模仿真过程中还是比较方便的。
13.5 模型初始化
模型声明只是给出了一个模型相关的数据结构,模型如何表达数字电路模型部分,比如参数,端口,线网,寄存器,模型实例化以及内部的计算逻辑部分,还需要更多的代码来描述,模型初始化就是来描述参数,端口,线网,寄存器和模型实例化,以及将他们跟内部计算逻辑部分连接在一起。内部计算逻辑则由模型函数完成。模型分为命名模型和库模型,初始化也不一样。
13.5.1 命名模型初始化
我们还是以半加器的初始化为例来说明命名模型初始化的方式。命名模型初始化用GEN_MODULE_INIT和END_GEN_MODULE_INIT两个宏括住,中间是初始化语句。
GEN_MODULE_INIT
PORT_IN(a, 1);
PORT_IN(b, 1);
PORT_OUT(sum, 1);
PORT_OUT(carry, 1);
GEN_FUNC("sum, carry", do_Generate_Result);
END_GEN_MODULE_INIT
GEN_MODULE_INIT和END_GEN_MODULE_INIT两个宏引用了用户定义的M_ID宏,如果不管中间的初始化语句,那么展开的c语言代码是:
static int half_adder_module_init(sGeneralModule* pdata) {
half_adder_sHDL4SE_data* pobj;
pobj = (half_adder_sHDL4SE_data*)mt_malloc(sizeof(half_adder_sHDL4SE_data));
if (pobj == NULL)
return -1;
pdata->priv_data = pobj;
pdata->func_param = pobj;
pobj->pmodule = pdata;
/* 初始化语句,后面详细描述其展开后的代码 */
PORT_IN(a, 1);
PORT_IN(b, 1);
PORT_OUT(sum, 1);
PORT_OUT(carry, 1);
GEN_FUNC("sum, carry", do_Generate_Result);
hdl4se_module_InitDepend(pdata);
return EIID_OK;
}
IHDL4SEModuleVar * half_adder_module_create(HOBJECT parent, const char* instanceparam, const char* name, const char * connectlist)
{
return hdl4se_module_Create(parent, instanceparam, name, connectlist, half_adder_module_init);
}
这里同样定义了一个_module_create函数,可以供实例化时使用。
13.5.2 库模型初始化
库模型的初始化我们用fifo的例子来说明:
IDLIST
VID(wClk),
VID(nwReset),
VID(wRead),
VID(wDataValid),
VID(bReadData),
VID(wWriteEnable),
VID(wWrite),
VID(bWriteData),
VID(readpos),
VID(writepos),
VID(wWriteEn),
VID(wReadEn),
VID(inputcount),
VID(outputcount),
VID(count),
VID(maxcount),
VID(reset),
END_IDLIST
MODULE_DECLARE(hdl4se_fifo)
unsigned int width;
unsigned int depth;
unsigned int wordsize;
unsigned int* fifo_data;
END_MODULE_DECLARE(hdl4se_fifo)
MODULE_INIT(hdl4se_fifo)
pobj->fifo_data = NULL;
pobj->width = (int)MODULE_PARAM(0);
pobj->depth = (int)MODULE_PARAM(1);
if (pobj->width <= 0)
return EIID_INVALIDPARAM;
if (pobj->depth <= 2)
return EIID_INVALIDPARAM;
pobj->wordsize = (pobj->width + 31) / 32;
pobj->fifo_data = (unsigned int*)mt_malloc(pobj->wordsize * pobj->depth * sizeof(unsigned int));
if (pobj->fifo_data == NULL)
return -1;
PORT_IN(wClk, 1);
PORT_IN(nwReset, 1);
PORT_IN(wRead, 1);
GPORT_OUT(wDataValid, 1, hdl4se_fifo_gen_wDataValid);
GPORT_OUT(bReadData, pobj->width, hdl4se_fifo_gen_bReadData);
GPORT_OUT(wWriteEnable, 1, hdl4se_fifo_gen_wWriteEnable);
PORT_IN(wWrite, 1);
PORT_IN(bWriteData, pobj->width);
REG(readpos, 32);
REG(writepos, 32);
REG(wReadEn, 1);
REG(wWriteEn, 1);
REG(inputcount, 32);
REG(outputcount, 32);
WIRE(count, 32);
REG(maxcount, 32);
REG(reset, 1);
CLKTICK_FUNC(hdl4se_fifo_ClkTick);
DEINIT_FUNC(hdl4se_fifo_deinit);
END_MODULE_INIT(hdl4se_fifo)
这个模型的IDLIST中,前面部分是模型的端口,包括wClk, nwReset, wRead, wDataValid, bReadData, wWriteEnable, wWrite和bWriteData,提供了一个FIFO的基本操作接口信号。
后面定义了readpos, writepos, wReadEn和wWriteEn等四个寄存器,用来控制FIFO的计算逻辑,再后面的寄存器和线网则是用来提供统计信息,可以用将它们在VCD文件中作为模型的调试信息使用。
库模型的初始化用MODULE_INIT和END_MODULE_INIT括住。可以看到模型的初始化包括几个部分:
- 参数解析:将模型的参数记录在内部缓冲区中,模型的实例化参数在实例化过程中通过一个用逗号隔开的字符串传入(字符串类型参数用双引号括住,因此内部可以有逗号),实例化过程中被分割开,存放在sGeneralModule的parameters数组中(字符串方式),通过编号可以访问。可以用MODULE_PARAM得到整数,用MODULE_PARAM_REAL得到浮点数,用MODULE_PARAM_STR得到字符串格式。一般参数解析后是存放在模型的c语言数据结构中。
- 内部数据结构初始化:初始化c语言内部的数据结构,比如FIFO,我们并不用一个RAM对象来实现FIFO的存储器,而是直接将数据存放在c语言的数据结构fifo_data 中。
- 端口声明:可以不按照顺序来对端口进行声明,声明包括名称,宽度,如果是输出端口,可以声明关联的生成函数。
- 寄存器及线网声明:包括名称,宽度,可以声明关联的生成函数。
- 时钟函数关联:可以关联一个模型在时钟到达阶段的处理函数,这个函数由仿真器直接调用。
- 模型销毁函数关联:模型销毁时调用的函数,一般用来释放初始化过程中申请的资源,比如释放内存。
如果不展开内部的初始化函数,上面的初始化代码展开后的c语言代码是这样的:
static int hdl4se_fifoCreate(const PARAMITEM* pParams, int paramcount, HOBJECT* pObject)
{
hdl4se_fifosHDL4SE_data* pobj;
pobj = (hdl4se_fifosHDL4SE_data*)mt_malloc(sizeof(hdl4se_fifosHDL4SE_data));
if (pobj == NULL)
return -1;
memset(pobj, 0, sizeof(hdl4se_fifosHDL4SE_data));
*pObject = 0;
HDL4SEMODULE_VARINIT(pobj, hdl4se_fifo);
DLIST_VARINIT(pobj, hdl4se_fifo);
OBJECT_RETURN_GEN(hdl4se_fifo, pobj, pObject, hdl4se_fifo_MODULE_CLSID);
pobj->fifo_data = NULL;
pobj->width = (int)MODULE_PARAM(0);
pobj->depth = (int)MODULE_PARAM(1);
if (pobj->width <= 0)
return EIID_INVALIDPARAM;
if (pobj->depth <= 2)
return EIID_INVALIDPARAM;
pobj->wordsize = (pobj->width + 31) / 32;
pobj->fifo_data = (unsigned int*)mt_malloc(pobj->wordsize * pobj->depth * sizeof(unsigned int));
if (pobj->fifo_data == NULL)
return -1;
PORT_IN(wClk, 1);
PORT_IN(nwReset, 1);
PORT_IN(wRead, 1);
GPORT_OUT(wDataValid, 1, hdl4se_fifo_gen_wDataValid);
GPORT_OUT(bReadData, pobj->width, hdl4se_fifo_gen_bReadData);
GPORT_OUT(wWriteEnable, 1, hdl4se_fifo_gen_wWriteEnable);
PORT_IN(wWrite, 1);
PORT_IN(bWriteData, pobj->width);
REG(readpos, 32);
REG(writepos, 32);
REG(wReadEn, 1);
REG(wWriteEn, 1);
REG(inputcount, 32);
REG(outputcount, 32);
WIRE(count, 32);
REG(maxcount, 32);
REG(reset, 1);
CLKTICK_FUNC(hdl4se_fifo_ClkTick);
DEINIT_FUNC(hdl4se_fifo_deinit);
hdl4se_module_InitDepend(pobj->pmodule);
return EIID_OK;
}
static void hdl4se_fifoDestroy(HOBJECT object)
{
hdl4se_fifosHDL4SE_data* pobj;
pobj = (hdl4se_fifosHDL4SE_data*)objectThis(object);
hdl4se_module_DeInit(pobj->pmodule);
memset(pobj, 0, sizeof(hdl4se_fifosHDL4SE_data));
mt_free(pobj);
}
static int hdl4se_fifoValid(HOBJECT object)
{
return 1;
}
13.5.3 模型初始化语句
前面给出了模型初始化的过程。模型初始化除了可以使用c语言代码来对模型内部数据结构进行初始化,HDL4SE还提供了几种宏,用来完成参数解析,端口定义,线网定义,寄存器定义,模型函数关联,时钟函数关联以及销毁函数关联,我们分类介绍如下:
13.5.3.1 参数解析
前面介绍过,模型的实例化参数在实例化过程中通过一个用逗号隔开的字符串传入(字符串类型参数用双引号括住,因此内部可以有逗号),实例化过程中被分割开,存放在sGeneralModule的parameters数组中(字符串方式),通过编号可以访问。可以用三个宏来得到指定参数的值,MODULE_PARAM得到整数,用MODULE_PARAM_REAL得到浮点数,用MODULE_PARAM_STR得到字符串格式。注意用MODULE_PARAM得到整数时,我们目前最多支持64位的整数,可以用合法的verilog整数表达的词法来表示整数。尽管参数可以在初始化函数和模型函数中使用,我们建议将参数在初始化函数中存放在模型声明的c语言变量中,这是因为参数的解析过程有一定的代价。
这里再举个例子说明字符串参数的使用方式:
MODULE_INIT(cnncell_coeffbuf)
int filelen;
char filename[256];
FILE * pCoeffFile;
pobj->header_loaded = 0;
sprintf(filename, DATADIR"/%s", MODULE_PARAM_STR(0));
pCoeffFile = fopen(filename, "rb");
if (pCoeffFile == NULL) {
printf("Can not open file %s\n", filename);
return -1;
}
fseek(pCoeffFile, 0, SEEK_END);
filelen = ftell(pCoeffFile);
fseek(pCoeffFile, 0, SEEK_SET);
pobj->databuf = (unsigned char *)malloc(filelen + 4);
if (pobj->databuf == NULL) {
fclose(pCoeffFile);
return -1;
}
filelen = fread(pobj->databuf, 1, filelen, pCoeffFile);
fclose(pCoeffFile);
这个例子通过字符串参数传入一个文件名,模型将文件读出来作为初始化数据(ROM?)使用。
13.5.3.2 端口定义
端口定义的宏包括:
PORT_IN(name, width)
PORT_OUT(name, width)
GPORT_OUT(name, width, func)
PORT_IN_SIGNED(name, width)
PORT_OUT_SIGNED(name, width)
GPORT_OUT_SIGNED(name, width, func)
其中基本的就是PORT_IN和PORT_OUT,其中包括名称和宽度,名称必须在IDLIST中出现过,内部实际上使用该名称对应的编号来生成sGeneralModule中variables数组中对应位置的ModuleVariable变量。对于输出端口,可以带G开始,表示后面直接关联一个生成函数。带SIGNED的表示这个端口是带符号的,默认是不带符号的。
这组宏中, 不带G的定义比较简单,就是在对应位置生成一个ModuleVariable变量,比如PORT_IN:
#define PORT_IN(name, width) hdl4se_module_SetVariable(pobj->pmodule, VID(name), VTYPE_PORT, width, PORT_INPUT, 1, #name)
int hdl4se_module_SetVariable(sGeneralModule* pobj, int index, int type, int width, int portdirect, int isunsigned, const char* name)
{
ModuleVariable* var;
var = variableCreate(type, width, portdirect, isunsigned, name);
if (var == NULL)
return -1;
if (pointerarraySetItem(&pobj->variables, index, var) != 0) {
variableDestroy(var);
return -1;
}
var->moduledata = pobj;
var->index = index;
return 0;
}
带G的定义要关联生成函数以及相关的依赖性变量,因此相对复杂点:
#define GPORT_OUT(name, width, func) \
do { \
ModuleVariable * v; \
hdl4se_module_SetVariable(pobj->pmodule, VID(name), VTYPE_PORT, width, PORT_OUTPUT, 1, #name); \
hdl4se_setgen_func_by_index(pobj->pmodule, VID(name), (void*)func); \
hdl4se_module_GetVariable(pobj->pmodule, VID(name), &v); \
hdl4se_set_depend_variables(v, __##func##_depend_list); \
} while (0)
其中包括生成变量,设置关联函数,然后设置关联函数的依赖变量,依赖变量用字符串表达,在模型函数定义时定义,在模型初始化完成后,由hdl4se_module_InitDepend来将该字符串解析为变量表中的变量,这样变量可以以任何顺序定义,也可以建立正确的依赖关系。
13.5.3.3 线网定义与寄存器定义
线网定义与寄存器定义用下面的宏来实现:
WIRE(name, width)
REG(name, width)
WIRE_S(name, width)
REG_S(name, width)
GWIRE(name, width, func)
GREG(name, width, func)
GWIRE_S(name, width, func)
GREG_S(name, width, func)
定义方式与端口定义类似,这里就不在赘述了。值得注意的时,线网和寄存器如果与内部模型实例连接在一起时,就不能再给出关联的生成函数。
13.5.3.4 模型函数关联
除了前面定义端口,线网时可以设置关联的模型函数,也可以用独立的模型函数关联宏来描述端口,线网或寄存器与模型函数的关联关系,我们用GEN_FUNC宏来实现,比如:
DEFINE_FUNC(do_Generate_Data, "") {
VWRITE_U32(d_a, (pobj->pattern & 1) ? 1 : 0);
VWRITE_U32(d_b, (pobj->pattern & 2) ? 1 : 0);
VWRITE_U32(d_cin, (pobj->pattern & 4) ? 1 : 0);
} END_DEFINE_FUNC
DEFINE_FUNC(test_driver_setup, "") {
pobj->pattern = pobj->pattern + 1;
} END_DEFINE_FUNC
DEFINE_FUNC(test_driver_init, "") {
pobj->pattern = 0;
} END_DEFINE_FUNC
GEN_MODULE_INIT
PORT_OUT(d_a, 1);
PORT_OUT(d_b, 1);
PORT_OUT(d_cin, 1);
GEN_FUNC("d_a, d_b, d_cin", do_Generate_Data);
INIT_FUNC(test_driver_init);
SETUP_FUNC(test_driver_setup);
END_GEN_MODULE_INIT
其中的GEN_FUNC(“d_a, d_b, d_cin”, do_Generate_Data);将d_a, d_b, d_cin与函数do_Generate_Data关联。
实际上,用G开头的端口,线网和寄存器定义宏来定义时,一个变量只能与一个模型函数关联,但是多个变量可以关联到同一个函数上。
13.5.3.5 模型内部函数关联
模型的通用数据结构sGeneralModule中(13.4.1),定义了四个内部函数,分别是:
MODULE_FUNC setup_func;
MODULE_FUNC clktick_func;
MODULE_FUNC init_func;
MODULE_FUNC deinit_func;
这四个函数分别在模型运行过程中的信号建立,时钟周期,初始化和销毁时调用。这几个函数默认为NULL,此时不进行调用,可以用下面的宏关联到用户自定义函数。后面的模型运行过程会讨论这几个函数的执行时机。
#define SETUP_FUNC(func) pobj->pmodule->setup_func = func
#define CLKTICK_FUNC(func) pobj->pmodule->clktick_func = func
#define DEINIT_FUNC(func) pobj->pmodule->deinit_func = func
#define INIT_FUNC(func) pobj->pmodule->init_func = func
13.5.3.6 模型实例化
模型实例化可以通过两种方式进行,命名模型由于自己不带CLSID,只能用MODULE_INST宏进行实例化,库模型则可以用两种方式,既可以用MODULE_INST来实例化,也可以用CELL_INST来实例化。
MODULE_INST实例化:
我们用两个半加器构成一个全加器:
#define M_ID(id) full_adder##id
IDLIST
VID(a),
VID(b),
VID(carry_in),
VID(sum),
VID(carry_out),
VID(c1),
VID(c2),
VID(s1),
END_IDLIST
GEN_MODULE_DECLARE
END_GEN_MODULE_DECLARE
DEFINE_FUNC(do_Generate_Or, "c1, c2") {
unsigned int c1, c2;
c1 = VREAD_U32(c1);
c2 = VREAD_U32(c2);
VWRITE_U32(carry_out, c1 | c2);
} END_DEFINE_FUNC
GEN_MODULE_INIT
PORT_IN(a, 1);
PORT_IN(b, 1);
PORT_IN(carry_in, 1);
PORT_OUT(sum, 1);
PORT_OUT(carry_out, 1);
WIRE(c1, 1);
WIRE(c2, 1);
WIRE(s1, 1);
MODULE_INST(half_adder, "ha1", "", "a, b, s1, c1");
MODULE_INST(half_adder, "ha2", "", "s1, carry_in, sum, c2");
GEN_FUNC("carry_out", do_Generate_Or);
END_GEN_MODULE_INIT
#undef M_ID
其中的MODULE_INST就是进行命名模型实例化的,它展开之后的c语言代码是这样的:
#define MODULE_INST(module_name, inst_name, instanceparam, connectlist) module_name##_module_create(pobj->pmodule->pobj, instanceparam, inst_name, connectlist)
也就是MODULE_INST(half_adder, "ha1", "", "a, b, s1, c1");等于调用:
half_adder_module_create(pobj->pmodule->pobj, "", "ha1", "a, b, s1, c1");
CELL_INST实例化的例子:
#define M_ID(id) top##id
IDLIST
VID(wClk),
VID(nwReset),
VID(bReadData),
VID(wWrite),
VID(bWriteAddr),
VID(bWriteData),
VID(bWriteMask),
VID(wRead),
VID(bReadAddr),
END_IDLIST
GEN_MODULE_DECLARE
END_GEN_MODULE_DECLARE
GEN_MODULE_INIT
PORT_IN (wClk, 1);
PORT_IN (nwReset, 1);
WIRE(bReadData, 32);
WIRE(wWrite, 1);
WIRE(bWriteAddr, 32);
WIRE(bWriteData, 32);
WIRE(bWriteMask, 4);
WIRE(wRead, 1);
WIRE(bReadAddr, 32);
CELL_INST("2925e2cf-dd49-4155-b31d-41d48f0f98dc", /* digitled */
"led",
"",
"wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData");
MODULE_INST(main,
"mainctrl",
"",
"wClk, nwReset, wWrite, bWriteAddr, bWriteData, bWriteMask, wRead, bReadAddr, bReadData");
END_GEN_MODULE_INIT
#undef M_ID
不同之处在于,CELL_INST通过给出库模型的CLSID即可进行实例化。当然这个库模型在使用之前必须注册过。
CELL_INST宏展开后是这样的:
#define CELL_INST(clsid, inst_name, instanceparam, connectlist) hdl4seCreateModule3(pobj->pmodule->pobj, clsid, instanceparam, inst_name, connectlist)
其中hdl4seCreateModule3实现如下:
IHDL4SEModuleVar* hdl4seCreateModule3(HOBJECT parent, const char* clsid, const char* instanceparam, const char* name, const char* connectlist)
{
IHDL4SEModuleVar* parentmodule = NULL;
IHDL4SEModuleVar* module = hdl4seCreateModule2(parent, clsid, instanceparam, name);
if (module == NULL) {
return NULL;
}
objectQueryInterface(parent, IID_HDL4SEMODULE, (const void**)&parentmodule);
if (parentmodule == NULL) {
return module;
}
hdl4se_module_Connect(parentmodule, module, connectlist);
objectRelease(parentmodule);
return module;
}
实例化时实例化参数通过一个字符串传入,每个参数用逗号分隔开,这里只支持按照参数顺序的参数实例化方式。端口的连接也是通过一个字符串connectlist描述的,字符串中是用逗号分隔开的变量名称,这里也只支持通过端口顺序连接,而且端口只能连接到变量上,不能连接到表达式上,如果要连接到表达式,必须先声明一个线网,然后将线网连接到模型实例化端口,再将表达式实现为一个模型函数,并将模型函数关联到这个线网变量上。比如counter例子中七段码解码器:
module dec2seg(input [3:0] dec, output [7:0] seg);
wire [7:0] wire_error = 8'b01111001;
hdl4se_mux16 #(8) mux_dec(dec,
8'b00111111,
8'b00000110,
8'b01011011,
8'b01001111,
8'b01100110,
8'b01101101,
8'b01111101,
8'b00000111,
8'b01111111,
8'b01101111,
wire_error,
wire_error,
wire_error,
wire_error,
wire_error,
wire_error,
seg);
endmodule ```
编译成HDL4SE模型是:
```c
/* Module dec2seg */
#define M_ID(id) dec2seg##id
IDLIST
VID(dec),
VID(seg),
VID(mux_dec_dot_in0), /* port:mux_dec(hdl4se_mux16).in0, 1 */
VID(mux_dec_dot_in1), /* port:mux_dec(hdl4se_mux16).in1, 2 */
VID(mux_dec_dot_in2), /* port:mux_dec(hdl4se_mux16).in2, 3 */
VID(mux_dec_dot_in3), /* port:mux_dec(hdl4se_mux16).in3, 4 */
VID(mux_dec_dot_in4), /* port:mux_dec(hdl4se_mux16).in4, 5 */
VID(mux_dec_dot_in5), /* port:mux_dec(hdl4se_mux16).in5, 6 */
VID(mux_dec_dot_in6), /* port:mux_dec(hdl4se_mux16).in6, 7 */
VID(mux_dec_dot_in7), /* port:mux_dec(hdl4se_mux16).in7, 8 */
VID(mux_dec_dot_in8), /* port:mux_dec(hdl4se_mux16).in8, 9 */
VID(mux_dec_dot_in9), /* port:mux_dec(hdl4se_mux16).in9, 10 */
VID(wire_error),
END_IDLIST
GEN_MODULE_DECLARE
END_GEN_MODULE_DECLARE
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in0, "") { /* port:mux_dec(hdl4se_mux16).in0, 1 */
VWRITE_S32(mux_dec_dot_in0, 63);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in1, "") { /* port:mux_dec(hdl4se_mux16).in1, 2 */
VWRITE_S32(mux_dec_dot_in1, 6);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in2, "") { /* port:mux_dec(hdl4se_mux16).in2, 3 */
VWRITE_S32(mux_dec_dot_in2, 91);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in3, "") { /* port:mux_dec(hdl4se_mux16).in3, 4 */
VWRITE_S32(mux_dec_dot_in3, 79);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in4, "") { /* port:mux_dec(hdl4se_mux16).in4, 5 */
VWRITE_S32(mux_dec_dot_in4, 102);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in5, "") { /* port:mux_dec(hdl4se_mux16).in5, 6 */
VWRITE_S32(mux_dec_dot_in5, 109);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in6, "") { /* port:mux_dec(hdl4se_mux16).in6, 7 */
VWRITE_S32(mux_dec_dot_in6, 125);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in7, "") { /* port:mux_dec(hdl4se_mux16).in7, 8 */
VWRITE_S32(mux_dec_dot_in7, 7);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in8, "") { /* port:mux_dec(hdl4se_mux16).in8, 9 */
VWRITE_S32(mux_dec_dot_in8, 127);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_mux_dec_dot_in9, "") { /* port:mux_dec(hdl4se_mux16).in9, 10 */
VWRITE_S32(mux_dec_dot_in9, 111);
} END_DEFINE_FUNC
DEFINE_FUNC(dec2seg_gen_wire_error, "") {
VWRITE_S32(wire_error, 121);
} END_DEFINE_FUNC
GEN_MODULE_INIT
PORT_IN (dec, 4);
PORT_OUT(seg, 8);
WIRE(wire_error, 8);
WIRE(mux_dec_dot_in0, 8);
WIRE(mux_dec_dot_in1, 8);
WIRE(mux_dec_dot_in2, 8);
WIRE(mux_dec_dot_in3, 8);
WIRE(mux_dec_dot_in4, 8);
WIRE(mux_dec_dot_in5, 8);
WIRE(mux_dec_dot_in6, 8);
WIRE(mux_dec_dot_in7, 8);
WIRE(mux_dec_dot_in8, 8);
WIRE(mux_dec_dot_in9, 8);
CELL_INST("69B4A095-0644-4B9E-9CF0-295474D7C243", /* hdl4se_mux16 */
"mux_dec",
"32'h8",
"dec, mux_dec_dot_in0, mux_dec_dot_in1, mux_dec_dot_in2, mux_dec_dot_in3, mux_dec_dot_in4"
", mux_dec_dot_in5, mux_dec_dot_in6, mux_dec_dot_in7, mux_dec_dot_in8, mux_dec_dot_in9"
", wire_error, wire_error, wire_error, wire_error, wire_error, wire_error, seg");
GEN_FUNC("mux_dec_dot_in0", dec2seg_gen_mux_dec_dot_in0);
GEN_FUNC("mux_dec_dot_in1", dec2seg_gen_mux_dec_dot_in1);
GEN_FUNC("mux_dec_dot_in2", dec2seg_gen_mux_dec_dot_in2);
GEN_FUNC("mux_dec_dot_in3", dec2seg_gen_mux_dec_dot_in3);
GEN_FUNC("mux_dec_dot_in4", dec2seg_gen_mux_dec_dot_in4);
GEN_FUNC("mux_dec_dot_in5", dec2seg_gen_mux_dec_dot_in5);
GEN_FUNC("mux_dec_dot_in6", dec2seg_gen_mux_dec_dot_in6);
GEN_FUNC("mux_dec_dot_in7", dec2seg_gen_mux_dec_dot_in7);
GEN_FUNC("mux_dec_dot_in8", dec2seg_gen_mux_dec_dot_in8);
GEN_FUNC("mux_dec_dot_in9", dec2seg_gen_mux_dec_dot_in9);
GEN_FUNC("wire_error", dec2seg_gen_wire_error);
END_GEN_MODULE_INIT
#undef M_ID
其中与mux_dec连接的常数表达式,就编译为对应的线网,然后连接到mux_dec上,并与对应的模型函数关联。这样相比起来,HDL4SE建模反而不如verilog简洁了。
上述的实例化都是在模型中实例化另外一个模型的例子,实际应用时还有一个特殊的实例化,就是顶层模型的实例化,此时应该用特别的宏来实例化顶层模型:
IHDL4SEModuleVar* hdl4seCreate_main(IHDL4SEModuleVar * parent, const char* instanceparam, const char* name)
{
return TOP_MODULE(top);
}
其中的宏TOP_MODULE就是用来生成顶层模型实例的,注意这里没有参数和连接表了。
其实HDL4SE的顶层模型还是有端口的,就是两个输入信号wClk和nwReset,这两个信号由仿真器给出,信号的连接在仿真器中来实现,在后面的模型运行过程会详细描述。
13.6 模型函数
前面已经看到模型函数的概念了,实际上数字电路模型中的所有逻辑运算是通过模型函数实现的,因此模型函数是建模的基础,建模工程师在完成模型接口的描述后,很大精力会花在模型函数的编程上。
模型函数通过DEFINE_FUNC和END_DEFINE_FUNC来定义,每个模型函数带了一个模型自定义数据结构的指针作为参数,参数名称固定为pobj,可以通过这个指针来访问自定义的c语言变量。也可以通过一组函数来读写模型变量。
13.6.1 模型函数定义
模型函数通过DEFINE_FUNC和END_DEFINE_FUNC来定义,比如半加器中:
DEFINE_FUNC(do_Generate_Result, "a, b") {
unsigned int ia, ib;
ia = vget(a);
ib = vget(b);
vput(sum, ia ^ ib);
vput(carry, ia & ib);
} END_DEFINE_FUNC
展开成c语言后是:
#define DEFINE_FUNC(funcname, dependlist) \
static const char * __do_Generate_Result_depend_list = "a, b";
static int do_Generate_Result(half_adder_sHDL4SE_data* pobj, ModuleVariable * variable) {
unsigned int ia, ib;
ia = vget(a);
ib = vget(b);
vput(sum, ia ^ ib);
vput(carry, ia & ib);
return 0;
}
其中定义了一个变量__do_Generate_Result_depend_list ,表示该函数依赖的变量表,这是一个用逗号隔开的变量名称表,在函数关联时会将关联的变量查找出来设置到变量的依赖表中。这种依赖关系在运行时用来优化运行过程。
一个模型函数可能关联到多个变量上,此时函数会记录在sGeneralModule中的funcs数组成员中, 用ModuleFunction结构表示,在ModuleVariable中只记录模型函数在该数组中的编号。一个模型函数在一个时钟周期中只需要调用一次,为了确保这一点,该函数对应的ModuleFunction数据结构中一个标志callit,用表示是否确实需要调用模型函数,模型函数调用一次后该标志清零,在每个周期结束时设置每个函数的这个标志。
13.6.2 内部变量读写
可以通过一系列宏来读写内部变量:
#define vget(v) hdl4se_module_GetVarUint32(pobj->pmodule, VID(v))
#define vput(v, value) hdl4se_module_SetVarUint32(pobj->pmodule, VID(v), value)
#define vgets(v) hdl4se_module_GetVarInt32(pobj->pmodule, VID(v))
#define vputs(v, value) hdl4se_module_SetVarInt32(pobj->pmodule, VID(v), value)
#define vget64(v) hdl4se_module_GetVarUint64(pobj->pmodule, VID(v))
#define vput64(v, value) hdl4se_module_SetVarUint64(pobj->pmodule, VID(v), value)
#define vget64s(v) hdl4se_module_GetVarInt64(pobj->pmodule, VID(v))
#define vput64s(v, value) hdl4se_module_SetVarInt64(pobj->pmodule, VID(v), value)
#define VAssignStr(vdst, str) hdl4se_module_AssignStr(pobj->pmodule, VID(vdst), str)
#define VAssignStr_idx(id, srt) hdl4se_module_AssignStr(pobj->pmodule, id, str)
#define VAssign(vdst, vsrc) hdl4se_module_Assign(pobj->pmodule, VID(vdst), VID(vsrc))
#define VAssign_idx(vdst, vsrc) hdl4se_module_Assign(pobj->pmodule, vdst, vsrc)
#define VAssignVar(vdst, vsrc) hdl4se_module_AssignVar(pobj->pmodule, VID(vdst), vsrc)
#define VAssignVar_idx(vdst, vsrc) hdl4se_module_AssignVar(pobj->pmodule, vdst, vsrc)
#define Var(vname) hdl4se_module_Var(pobj->pmodule, VID(vname))
#define Var_idx(idx) hdl4se_module_Var(pobj->pmodule, idx)
其中通过vget,vput系列可以得到或设置变量的值,这样可以用在c语言的表达式中,用VAssign系列可以直接操作变量,而不需要经过c语言表达式转手。
我们定义了typedef ModuleVariable* var;可以在模型函数中通过Var或Var_idx直接把模型变量取出来用,并用var开始的系列函数来操作模型变量,这些函数包括:
ModuleVariable * varTemp(int width, int isunsigned);
ModuleVariable * varConstStr(const char *str);
ModuleVariable * varConst(int width, unsigned long long v);
ModuleVariable * varConstSigned(int width, long long v);
ModuleVariable * varConcat(int multicount, ...);
ModuleVariable * varAdd(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varSub(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varMul(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varDiv(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varMod(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varPow(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varSHL(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varSHR(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varSAL(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varSAR(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varOr(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varXor(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varAnd(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varGT(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varGE(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varLT(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varLE(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varEQ(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varNE(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varAndL(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varOrL(ModuleVariable * a, ModuleVariable * b);
ModuleVariable * varNeg(ModuleVariable * a);
ModuleVariable * varNot(ModuleVariable * a);
ModuleVariable * varNotL(ModuleVariable * a);
ModuleVariable * varUAnd(ModuleVariable * a);
ModuleVariable * varUNAnd(ModuleVariable * a);
ModuleVariable * varUOr(ModuleVariable * a);
ModuleVariable * varUNOr(ModuleVariable * a);
ModuleVariable * varUXor(ModuleVariable * a);
ModuleVariable * varUNXor(ModuleVariable * a);
ModuleVariable * varBit(ModuleVariable * a, int bit);
ModuleVariable * varBits(ModuleVariable * a, int lsb, int width);
ModuleVariable * varSelect(ModuleVariable * a, ModuleVariable * b, ModuleVariable * c);
int varAssign(ModuleVariable * a, ModuleVariable * b);
int varCopy(ModuleVariable * a, ModuleVariable * b);
int varIsZero(ModuleVariable * a);
int varIsNotZero(ModuleVariable * a);
int varSetBits32(ModuleVariable* a, int index, unsigned int v);
unsigned int varGetBits32(ModuleVariable* a, int index);
int varAssign_S32(ModuleVariable * a, int v);
int varAssign_U32(ModuleVariable * a, unsigned int v);
int varAssign_S64(ModuleVariable * a, long long v);
int varAssign_U64(ModuleVariable * a, unsigned long long v);
int varGet_S32(ModuleVariable * a);
unsigned int varGet_U32(ModuleVariable * a);
long long varGet_S64(ModuleVariable * a);
unsigned long long varGet_U64(ModuleVariable * a);
int varGetStr(ModuleVariable* a, int base, char* str, int buflen);
其中甚至可以使用varTemp声明新的临时变量,系统能够自动回收这些临时变量。用这些函数操作模型变量的好处是可以操作任意宽度的变量(内部实现宽度不得超过2的30次方,这个宽度已经很大了,当然也受系统内存限制)。由于通过函数访问模型变量的代价比用c语言进行运算的代价高,我们建议在建模过程中尽量用c语言的表达式来完成计算,用c语言实在难以完成的才使用这些函数来完成。其中比较有用的可能是varSetBits32和varGetBits32两个函数,这两个函数可以以32位为单位读写模型变量的值。比如FIFO的例子,下面的模型函数生成FIFO的读数据信号:
DEFINE_FUNC(hdl4se_fifo_gen_bReadData, "readpos, wWrite") {
unsigned int readaddr = vget(readpos);
var readdata = Var(bReadData);
unsigned int j;
for (j = 0; j < pobj->wordsize; j++) {
varSetBits32(readdata, j, pobj->fifo_data[readaddr * pobj->wordsize + j]);
}
} END_DEFINE_FUNC
这个函数依赖读位置,读位置改变后读数据会需要重新生成,其实fifo_data变化后读数据也需要重新生成,然而这里没有精细地描述,只是简单地认为在FIFO每次写之后就需要更新读数据,因此这个函数的依赖变量表包括了wWrite变量。由于事先不知道FIFO的数据宽度(是通过实例化参数定义的),因此数据是按照32位对齐存放的,此时为了生成输出的bReadData信号,我们先定义一个变量readdata,通过Var函数将bReadData变量取出来(其实是它的指针),然后从fifo_data中得到每个32位段,然后通过varSetBits32设置到这个变量中。这样我们可以支持任意字宽的FIFO。
13.6.3 模型函数调用
模型函数的调用有四种可能:
- 关联到输出端口的模型函数,在外部需要读该端口数据时,此时更新这个变量就会调用关联的模型函数。
- 关联到线网的模型函数,在这个线网被访问时,如果需要更新时,则会调用关联的模型函数,调用一方面设置该线网不需要更新的标志,另一方面设置该函数不需要调用的标志(这样如果多个变量关联到一个函数时,这个函数只会调用一次。
- 关联到寄存器的模型函数,在时钟周期函数中会被调用,注意寄存器变量不会在被访问时进行更新,因为寄存器的值在一个周期中是不会变化的,只有在时钟周期结束后的信号建立过程才会被更新。
- 关联到模型内部函数的模型函数,在对应的时机被调用,后面的模型运行过程中会详细说明。
13.6.4 模型函数的设计原则
模型函数一般是一个线网寄存器变量或者输出端口对应一个(与端口连接的除外),也可以多个变量公用一个模型函数,这样可以增加可读性。然而原则是:
- 寄存器和线网不在一个函数中同时计算,
- 寄存器尽可能在模型的CLKTICK函数中计算,不要单独关联函数计算。
- 多个线网变量共享一个模型函数时,这些线网变量之间不要有相互依赖关系,如果确实有相互依赖关系,则应该先得到被依赖的变量,然后再计算依赖其他变量的变量。
- 为了确保正确的依赖关系,模型函数的依赖变量表必须正确提供,如果不能正确提供,可能导致计算结果不正确。
- 模型内部函数setup, init, clktick, deinit等模型函数可以不提供依赖关系表。
- clktick内只对寄存器变量进行计算,不要对线网或端口变量进行计算
模型函数中的可以访问的变量,可以分为如下几种: - 函数内部的临时变量,可以用c语言变量方式定义,也可以用varTemp方式生成,或者varXXX函数返回的临时变量,值的生存周期就在函数内,这部分按照普通的c语言变量使用。
- 模型内定义的IN_PORT型变量,在模型中不能赋值,只读型变量,读的时候内部会进行更新。
- 模型内定义的OUT_PORT型变量,只能绑定在其他模型端口或者是绑定在某个模型函数中,只有在绑定的模型函数中才能赋值,完成这类变量的更新,不应该在其他的地方进行赋值。
- 模型内定义的WIRE型变量 ,可能绑定到其他模型端口或者绑定到某个函数,在模型中可以读,读的时候可能引发变量更新(调用绑定函数或引发端口更新)。只有在绑定的模型函数中可以赋值,不应该在其他地方进行赋值。
- 模型内定义的REG型变量,内部有两个拷贝,一个拷贝是上一周期写入的值,一个拷贝是当前周期内写入的值,这类变量可以任意读写,读的值是该变量上一周期写入的。在仿真过程中的Setup阶段,会检查每个REG型变量,两个拷贝的值不一样就将当前周期内写入的值更新到上一周期写入的位置中,并因此更新下一周期需要重新计算的变量。
- 模型声明部分定义的c语言变量,这里分几种情况,a. 是类似于模型参数的变量,只在模型初始化过程中赋值,其他时间作为常数使用,这样的变量除了在初始化函数中之外,不应该进行赋值,属于模型的常数,b. 模型中定义的c语言变量,参与模型函数中的计算,与REG变量一样,在setup函数之外应该只写或者只读。只写的变量在setup函数中控制修改只读的变量的值,只读的变量在setup之外不能修改。在模型声明中用c语言声明的变量,在setup函数之外要么只读,要么只写,是为了确保在模型计算过程中的结果与计算顺序无关。
13.7 模型连接与运行
HDL4SE模型建立后,要进行仿真,需要先与仿真器连接在一起,然后再进行仿真,仿真过程中还可以选择其中的若干信号记录VCD文件中。
13.7.1 仿真程序的建立
建模完成后,我们得到一个顶层模型,它的接口定义如下:
module topmodule(intput wClk, input nwReset)
endmodule
用c语言描述,则是通过一个函数能够返回顶层模型的实例:
IHDL4SEModuleVar* hdl4seCreate_main(IHDL4SEModuleVar * parent, const char* instanceparam, const char* name)
{
return TOP_MODULE(top);
}
下面是GoogLeNet模型的仿真程序主程序:
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "time.h"
#include "object.h"
#include "bignumber.h"
#include "hdl4secell.h"
#include "hdl4sesim.h"
#include "hdl4sevcdfile.h"
IHDL4SESimulator** sim;
IHDL4SEModuleVar* topmodule;
unsigned long long clocks = 0;
#define VCDOUTPUT 0
int cnnInit();
IHDL4SEModuleVar* hdl4seCreate_main(IHDL4SEModuleVar * parent, const char* instanceparam, const char* name);
int main(int argc, char* argv[])
{
int i;
int width;
int count, unitcount;
time_t starttime;
time_t lasttime;
IHDL4SEWaveOutput** vcdfile;
cnnInit();
sim = hdl4sesimCreateSimulator();
topmodule = hdl4seCreate_main(NULL, "", "top");
objectCall1(sim, SetTopModule, topmodule);
objectCall1(sim, SetReset, 0);
unitcount = 0;
objectPrintInfo(stdout, fprintf);
#if VCDOUTPUT
vcdfile = hdl4sesimCreateVCDFile("googlenet.vcd");
objectCall2(vcdfile, AddSignal, "/top/srcbuf", "maxcount");
objectCall2(vcdfile, AddSignal, "/top/net/cnn_buf_167", "wRead");
objectCall2(vcdfile, AddSignal, "/top/net/cnn_buf_167", "wWrite");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "wCoeffRead");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "index");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "state");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "writeline");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "readline");
objectCall2(vcdfile, AddSignal, "/top/net/convolution_2", "lineindex");
objectCall1(vcdfile, SetTopModule, topmodule);
objectCall0(vcdfile, StartRecord);
#endif
clocks = 0;
lasttime = starttime = time(NULL);
do {
objectCall0(sim, ClkTick);
#if VCDOUTPUT
objectCall1(vcdfile, ClkTick, clocks);
#endif
objectCall0(sim, Setup);
if (clocks == 2)
objectCall1(sim, SetReset, 1);
clocks++;
if ((clocks & 0xFFFF) == 0) {
time_t thistime = time(NULL);
printf("clocks: %lld, TSPD=%lfcps, LSPD=%lfcps\n",
clocks,
(double)clocks / (double)(thistime - starttime),
4096.0 * 16 / (double)(thistime - lasttime));
lasttime = time(NULL);
}
} while (running);
#if VCDOUTPUT
objectCall0(vcdfile, StopRecord);
objectRelease(vcdfile);
#endif
return 0;
}
这个仿真主程序分如下几个部分:
- 仿真器和顶层模型生成与连接
- 仿真输出VCD文件初始化
- 仿真循环
仿真器是一个LCOM类,调用函数hdl4sesimCreateSimulator()可以生成一个仿真器。顶层模型通过调用hdl4seCreate_main(NULL, “”, “top”);生成,这里把顶层模型的实例名设置为 top。然后通过调用仿真器的SetTopModule,将顶层模型与仿真器连接在一起,这个函数内部实现流程如下:
static int hdl4sesim_hdl4se_simulator_SetTopModule(HOBJECT object, HOBJECT topmodule)
{
int i, j;
sHDL4SESim* pobj;
pobj = (sHDL4SESim*)objectThis(object);
objectQueryInterface(topmodule, IID_HDL4SEMODULE, (const void**)&pobj->topmodule);
hdl4semoduleTraversal(pobj->topmodule, hdl4sesim_collect_module, pobj);
/*连接topmodule到sim模块,0.wClk, 1.nwReset*/
hdl4se_module_ConnectInput(&pobj->topmodule->data, 0, object, 0);
hdl4se_module_ConnectInput(&pobj->topmodule->data, 1, object, 1);
return 0;
}
该函数首先得到顶层模块的HDL4SEMODULE接口,然后通过该接口调用hdl4semoduleTraversal将顶层模型中层次化结构的模型实例全部收集到仿真器中:
static int hdl4semoduleTraversal(HOBJECT object, hdl4se_module_TraversalFunc func, sHDL4SESim* param)
{
IHDL4SEModuleVar* module;
func(object, param);
if (0 == objectQueryInterface(object, IID_HDL4SEMODULE, (const void**)&module)) {
int count, i;
count = hdl4se_module_GetModuleCount(&module->data);
for (i = 0; i < count; i++) {
IHDL4SEModuleVar* unit;
hdl4se_module_GetModule(&module->data, i, &unit);
if (hdl4semoduleTraversal(unit, func, param) != 0)
return -1;
}
objectRelease(module);
}
return 0;
}
static int hdl4sesim_collect_module(HOBJECT module, sHDL4SESim* pobj)
{
IHDL4SEModuleVar* m;
int canruninthread = 0;
if (EIID_OK == objectQueryInterface(module, IID_HDL4SEMODULE, (const void**)&m)) {
canruninthread = m->data.canruninthread;
if (canruninthread) {
pointerarrayAddItem(&pobj->multithreadmodules, m);
}
else {
pointerarrayAddItem(&pobj->singlethreadmodules, m);
}
}
return 0;
}
其实就是遍历整个层次化结构,从顶层开始,每个模型下遍历其实例化的子模型实例,收集到的模型实例指针按照建模时指定的能否在多线程中运行分别放在仿真器的multithreadmodules或singlethreadmodules数组中。
然后通过如下调用:
/*连接topmodule到sim模块,0.wClk, 1.nwReset*/
hdl4se_module_ConnectInput(&pobj->topmodule->data, 0, object, 0);
hdl4se_module_ConnectInput(&pobj->topmodule->data, 1, object, 1);
把顶层模型的wClk和nwReset连接到仿真器的对应端口上(仿真器也实现了一个Module接口)。此时仿真环境就建立起来了。
如果仿真过程中需要输出信号,可以通过VCD文件输出,基本的过程如下:
- 调用vcdfile = hdl4sesimCreateVCDFile(“googlenet.vcd”); 生成VCD文件对象
- 重复调用AddSignal接口函数:objectCall2(vcdfile, AddSignal, “/top/srcbuf”, “maxcount”);把需要记录的信号增加到VCD文件中,这个函数有两个参数,一个时模块实例名,这是要给全路径的层次化名字,层之间用"/"分隔开,另一个参数是信号的名称,这个名称必须是建模时出现在IDLIST中的名称。
- 调用vcd文件对象的SetTopModule将顶层模型与VCD文件连接在一起objectCall1(vcdfile, SetTopModule, topmodule);,这个过程与顶层模型与仿真器连接类似,遍历顶层模型的整个层次结构,找到对应的模型以及变量,将变量记录在VCD中,在每个周期中仿真主程序调用VCD文件的周期函数ClkTick,该函数从每个需要记录的变量得到值并记录在VCD文件中。
- 调用objectCall0(vcdfile, StartRecord);来开始记录信号,其实就是生成VCD文件的文件头,为记录每个周期的信号做好准备。
这样,我们就准备好仿真环境了。
13.7.2 仿真循环
HDL4SE的仿真,是基于时钟周期的仿真。我们把一个时钟周期分为两个阶段,一个阶段是计算所有需要计算的变量,计算的输入就是顶层的nwReset输入和每个模型的寄存器的值,在这个阶段中我们调用每个模型实例的ClkTick接口,这个是通过调用仿真器的ClkTick接口实现的,第二个阶段更新每个模型的寄存器的值,根据寄存器的值是否变化来得到后面需要计算的变量。这个阶段将调用每个模型实例的Setup接口具体是通过调用仿真器的Setup接口实现的,两个阶段之间插入VCD记录所需要的调用,后面再增加一些时间统计的代码,就构成了仿真循环代码:
objectCall1(sim, SetReset, 0);
clocks = 0;
lasttime = starttime = time(NULL);
do {
objectCall0(sim, ClkTick);
#if VCDOUTPUT
objectCall1(vcdfile, ClkTick, clocks);
#endif
objectCall0(sim, Setup);
if (clocks == 2)
objectCall1(sim, SetReset, 1);
clocks++;
if ((clocks & 0xFFFF) == 0) {
time_t thistime = time(NULL);
printf("clocks: %lld, TSPD=%lfcps, LSPD=%lfcps\n",
clocks,
(double)clocks / (double)(thistime - starttime),
4096.0 * 16 / (double)(thistime - lasttime));
lasttime = time(NULL);
}
} while (running);
开始时我们通过调用仿真器的SetReset将整个系统的nwReset信号设置为0,进入复位状态,仿真循环中到第二个周期时调用SetReset将复位置为1,结束复位状态。
每隔64K个时钟周期计算一次仿真速度,其中TSPD表示全局的仿真速度,用总的时钟周期除以总的时间,LSPD表示局部仿真速度,表示过去的64K个时钟周期中的仿真速度。仿真速度用每秒周期数表示。这样我们对仿真速度就有个定量的概念。
13.7.3 仿真计算阶段
RTL的仿真,是由寄存器变化(或者外部信号变化,HDL4SE的顶层模型中,只有一个外部信号nwReset,wClk实际上不参与仿真计算),来驱动的,仿真计算的目的就是根据当前寄存器的值计算出新的寄存器的值(RT),因此在仿真计算阶段调用每个模型的ClkTick函数,这个函数就是用来计算每个寄存器的新的值。我们在模型连接时已经将顶层模型下的所有模型实例收集在两个数组中,因此仿真器的ClkTick实现如下:
#define THREADCOUNT 3
static int hdl4sesim_hdl4se_simulator_ClkTick(HOBJECT object)
{
sHDL4SESim* pobj;
int i;
pobj = (sHDL4SESim*)objectThis(object);
pobj->clk = 0;
hdl4se_module_ClkTick(&pobj->data);
for (i = 0; i < pobj->singlethreadmodules.itemcount; i++) {
hdl4se_module_ClkTick(&((IHDL4SEModuleVar*)(pobj->singlethreadmodules.array[i]))->data);
}
#pragma omp parallel for num_threads(THREADCOUNT)
for (i = 0; i < pobj->multithreadmodules.itemcount; i++) {
hdl4se_module_ClkTick(&((IHDL4SEModuleVar*)(pobj->multithreadmodules.array[i]))->data);
}
return 0;
}
首先调用仿真器自身模型的ClkTick,这个函数检查内部reset寄存器的变化,是否被外部调用过SetReset,该函数修改reset寄存器的值。这个寄存器的输出用来仿真器生成的nwReset信号。然后调用每个不能在多线程环境下运行的模型实例的ClkTick。这样的模型是很少的,一般可能跟UI相关。再后面建立一个openmp循环,启动多线程调用每个模型的ClkTick接口,让每个模型实例计算模型下面每个寄存器的新值。此时每个模型实例的ClkTick实现如下:
int hdl4se_module_ClkTick(sGeneralModule* pobj)
{
int i;
if (pobj->clktick_func != NULL) {
pobj->clktick_func(pobj->func_param, NULL);
}
for (i = 0; i < pobj->variables.itemcount; i++) {
ModuleVariable* var = (ModuleVariable*)pobj->variables.array[i];
if (var == NULL)
continue;
var->updatedisset = 0;
if (var->type == VTYPE_REG) {
int updatefunc;
updatefunc = var->updatefunc;
if (updatefunc != VUF_UPDATED)
{
if (var->genfuncindex >= 0) {
threadlockLock(var->lock);
functionCallAndSetNone(var->moduledata->funcs.array[var->genfuncindex]);
threadlockUnlock(var->lock);
}
else if (var->module != NULL) {
threadlockLock(var->lock);
hdl4se_module_GetValue(&var->module->data, var->moduleportindex, var->width, var);
threadlockUnlock(var->lock);
}
var->updatefunc = VUF_UPDATED;
}
}
}
return 0;
}
这个过程中,如果模型中设置了自定义的clktick内部函数,则首先调用这个自定义函数,这个自定义函数可以生成寄存器的值,不见得要将寄存器与某个模型函数关联在一起。然后检查模型实例下的所有变量,将变量的updatedisset标志清零,以便在信号建立阶段中计算哪些变量需要重新计算。然后如果是寄存器类型变量,则计算它的值。计算过程是:
- 判断是否需要计算,
- 如果需要计算,则如果genfuncindex大于等于0,表示有关联的模型函数,则优先调用模型函数,调用时对变量进行加锁,以防止多线程同时进行一个模型函数的计算。如果没有关联函数,则判断是否有连接的端口(module成员是否有效),如果有连接模型信号,则通过hdl4se_module_GetValue得到连接的端口的值。由于得到值也可能引发计算,因此同样加锁处理。
- 最后将该变量的计算标志设置为VUF_UPDATED。
在计算过程中,关联的模型函数访问一个模型实例的变量之前,如果不是寄存器变量,就会尝试去更新它的值,更新值的可能引发新的函数计算或者端口计算,这些计算就会引发新的更新,这样计算请求就会递归发送到整个模型相关的实例中,每次引用新的实例的计算时都会进行锁定,确保计算的正确性,每次计算完成后,就将相关的变量更新标志设置为无需更新状态,后面的更新请求就会自动跳过,减少计算量。模型函数调用一次后,其callit标志就会被清零,这样这个模型函数将来被调用时就会跳过,也可以减少计算量。我们来看看funcCallAndSetNone的实现:
#define functionCallAndSetNone(obj) \
do { \
ModuleFunction * func = (ModuleFunction *)obj; \
int callit = LOCKED_SWAP(func->callit, 0); \
if (callit) { \
func->func(func->pobj, func->var); \
} \
} while (0)
用宏实现,最大限度减少了调用的开销。调用开始时用LOCKED_SWAP原子操作把callit标志设置为0,并返回原来的值,如果原来的标志有效,则实际调用该模型函数。
我们再来看看取端口值的实现:
inline int hdl4se_module_GetValue(sGeneralModule* pobj, int index, int width, ModuleVariable * value)
{
ModuleVariable* var;
if (index < 0 || index >= pobj->variables.itemcount)
return -1;
var = (ModuleVariable*)pobj->variables.array[index];
UpdateVariable(var);
#if USEBIGINT
if (width <= 0) {
objectCall1(value, SetUnsigned, var->isunsigned);
objectCall1(value, SetWidth, var->width);
}
objectCall1(value, Assign, var->data);
#else
if (width <= 0) {
value->isunsigned = var->isunsigned;
value->width = var->width;
}
if (value->type == VTYPE_REG) {
value->data_reg = var->data;
}
else {
value->data = var->data;
}
#endif
return 0;
}
首先调用UpdateVariable,尝试更新该连接的端口变量的值,然后将端口变量的值取到变量中,注意如果是寄存器类型,则存放在data_reg中,表示寄存器变量在一个周期中不改变值。UpdateVariable的实现如下:
#define UpdateVariable(v) \
do { \
if (v->type != VTYPE_REG) { \
if (LOCKED_COMP_SWAP(v->updatefunc, VUF_UPDATING, VUF_WAITUPDATE) != VUF_UPDATED) {\
threadlockLock(v->lock); \
if (v->updatefunc != VUF_UPDATED) { \
if (v->genfuncindex >= 0) { \
functionCallAndSetNone(v->moduledata->funcs.array[v->genfuncindex]); \
} \
else if (v->module != NULL) { \
hdl4se_module_GetValue(&v->module->data, v->moduleportindex, v->width, v); \
} \
v->updatefunc = VUF_UPDATED; \
} \
threadlockUnlock(v->lock); \
} \
} \
} while (0)
可以看到更新值的行为只对非寄存器变量进行,这段代码通过LOCKED_COMP_SWAP原子操作,将变量的成员updatefunc为VUF_WAITUPDATE的改变为VUF_UPDATING(如果不是VUF_WAITUPDATE的则不改变),并且返回原来的值,如果原来的值不是VUF_UPDATED,则将变量加锁,得到锁的线程判断当前的updatefunc不是VUF_UPDATED时进行计算,然后将updatefunc设置为VUF_UPDATED以阻止后面等待计算的进程进行计算。
13.7.4 信号建立阶段
仿真计算阶段完成后,每个模型已经将需要计算的寄存器的值计算出来放在变量的data_reg中。此时仿真器进入信号建立阶段,通过调用仿真器的Setup接口完成,下面是仿真器Setup接口的实现:
static int hdl4sesim_hdl4se_simulator_Setup(HOBJECT object)
{
sHDL4SESim* pobj;
int i;
pobj = (sHDL4SESim*)objectThis(object);
pobj->clk = 1;
pobj->clocks++;
vartempClean();
hdl4se_module_Setup(&pobj->data);
for (i = 0; i < pobj->singlethreadmodules.itemcount; i++) {
hdl4se_module_Setup(&((IHDL4SEModuleVar*)(pobj->singlethreadmodules.array[i]))->data);
}
#pragma omp parallel for num_threads(THREADCOUNT)
for (i = 0; i < pobj->multithreadmodules.itemcount; i++) {
hdl4se_module_Setup(&((IHDL4SEModuleVar*)(pobj->multithreadmodules.array[i]))->data);
}
return 0;
}
信号建立阶段首先清理回收所有的临时变量,将它们收集到一个空闲变量链表中供下一个周期使用。然后调用仿真器模型的Setup函数,这一步主要是检查reset信号的变化,如果发生变化,则通知依赖这个信号变化的变量需要更新。再往后对每个不能在多线程环境下运行的模型调用Setup接口,然后使用openmp启动多线程实现对能够运行在多线程环境下的模型调用Setup接口。
每个模型实例的Setup接口实现如下:
static int hdl4se_var_set_update(ModuleVariable* v)
{
int i;
if (v == NULL)
return 0;
if (v->updatedisset) {
return 0;
}
v->updatedisset = 1;
if (v->genfuncindex >= 0 || v->module != NULL)
v->updatefunc = VUF_WAITUPDATE;
for (i = 0; i < v->variables.itemcount; i++) {
hdl4se_var_set_update((ModuleVariable*)v->variables.array[i]);
}
return 0;
}
int hdl4se_module_Setup(sGeneralModule* pobj)
{
int i;
if (pobj->setup_func != 0) {
pobj->setup_func(pobj->func_param, NULL);
}
for (i = 0; i < pobj->variables.itemcount; i++) {
ModuleVariable* var = (ModuleVariable*)pobj->variables.array[i];
if (var == NULL)
continue;
if (var->type == VTYPE_REG) {
#if USEBIGINT
objectCall1(var->data, Clone, var->data_reg);
#else
if (var->data != var->data_reg) {
var->data = var->data_reg;
#endif
hdl4se_var_set_update(var);
}
}
}
for (i = 0; i < pobj->funcs.itemcount; i++) {
ModuleFunction* function = (ModuleFunction *)pobj->funcs.array[i];
function->callit = 1;
}
return 0;
}
其中的工作包括:
- 如果设置了模型的setup函数,则调用模型自定义的setup函数。
- 对每个寄存器变量,如果变量的计算结果值与当前寄存器中的值不一样,就进行更新,然后调用hdl4se_var_set_update通知依赖这个变量的变量需要更新(在下一个周期的计算阶段中),其中hdl4se_var_set_update对变量进行更新设置时,如果已经设置过,则直接返回,避免重复的循环设置,否则设置已经设置的标志updatedisset(该标志在计算阶段清零),然后设置updatefunc 为VUF_WAITUPDATE(有待更新),再对每个依赖该变量的变量递归调用hdl4se_var_set_update,将更新请求传播出去,直到所有需要更新的变量传播到为止。这一步根据寄存器的变化情况计算出下一个周期中需要更新的变量,可以有效减少下一周期中需要完成的计算工作量,加快仿真的运行。虽然这个判断有点粗,很多实际上不需要更新的变量也通知到了,但是维持了一个静态的计算变量表,可以支持在多线程下进行仿真。
- 设置每个函数的需要调用标志,该标志在计算阶段计算后即被清除,这里需要恢复。
相比SystemC在每个变量实际发生变化时才决定下一步需要计算的变量,这种做法可能导致实际的计算工作量有增加,但是能得到多线程的支持,同样可以提高仿真速度,孰优孰劣,只能在后面的工作中凭统计数据说话了。
【请参考】
01.HDL4SE:软件工程师学习Verilog语言(十二)
02.HDL4SE:软件工程师学习Verilog语言(十一)
03.HDL4SE:软件工程师学习Verilog语言(十)
04.HDL4SE:软件工程师学习Verilog语言(九)
05.HDL4SE:软件工程师学习Verilog语言(八)
06.HDL4SE:软件工程师学习Verilog语言(七)
07.HDL4SE:软件工程师学习Verilog语言(六)
08.HDL4SE:软件工程师学习Verilog语言(五)
09.HDL4SE:软件工程师学习Verilog语言(四)
10.HDL4SE:软件工程师学习Verilog语言(三)
11.HDL4SE:软件工程师学习Verilog语言(二)
12.HDL4SE:软件工程师学习Verilog语言(一)
13.LCOM:轻量级组件对象模型
14.LCOM:带数据的接口
15.工具下载:在64位windows下的bison 3.7和flex 2.6.4
16.git: verilog-parser开源项目
17.git: HDL4SE项目
18.git: LCOM项目
19.git: GLFW项目
20.git: SystemC项目