HDL4SE:软件工程师学习Verilog语言(十三)

Posted 饶先宏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言(十三)相关的知识,希望对你有一定的参考价值。

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括住。可以看到模型的初始化包括几个部分:

  1. 参数解析:将模型的参数记录在内部缓冲区中,模型的实例化参数在实例化过程中通过一个用逗号隔开的字符串传入(字符串类型参数用双引号括住,因此内部可以有逗号),实例化过程中被分割开,存放在sGeneralModule的parameters数组中(字符串方式),通过编号可以访问。可以用MODULE_PARAM得到整数,用MODULE_PARAM_REAL得到浮点数,用MODULE_PARAM_STR得到字符串格式。一般参数解析后是存放在模型的c语言数据结构中。
  2. 内部数据结构初始化:初始化c语言内部的数据结构,比如FIFO,我们并不用一个RAM对象来实现FIFO的存储器,而是直接将数据存放在c语言的数据结构fifo_data 中。
  3. 端口声明:可以不按照顺序来对端口进行声明,声明包括名称,宽度,如果是输出端口,可以声明关联的生成函数。
  4. 寄存器及线网声明:包括名称,宽度,可以声明关联的生成函数。
  5. 时钟函数关联:可以关联一个模型在时钟到达阶段的处理函数,这个函数由仿真器直接调用。
  6. 模型销毁函数关联:模型销毁时调用的函数,一般用来释放初始化过程中申请的资源,比如释放内存。
    如果不展开内部的初始化函数,上面的初始化代码展开后的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以上是关于HDL4SE:软件工程师学习Verilog语言(十三)的主要内容,如果未能解决你的问题,请参考以下文章

HDL4SE:软件工程师学习Verilog语言(十四)

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言