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

Posted 饶先宏

tags:

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

12 SystemC

SystemC是一个C++的库,可以来做数字电路建模和仿真。由于使用C++环境,因此可以利用c/c++中的资源,仿真速度也比verilog RTL快。SystemC与Verilog之间可以相互翻译,用SystemC可以实现verilog中的绝大部分描述能力。SystemC已经发展到2.3.4,甚至已经成为了IEEE 的标准:IEEE_Std_1666,不过似乎已经停止了开发,github:SystemC上能够看到的版本是两年前更新的了。按说SystemC的描述能力与verilog相当,而且能够与verilog互换,用它来进行设计也是可以的,然而没有流行起来用SystemC来做系统设计,个人认为问题可能恰恰在于模拟verilog太像了,反而使它的地位比较尴尬,对软件人员要求太高,需要掌握比较深的数字电路知识,对数字电路的工程师,需要去学比较深的c++语言,可能感觉还不如直接写verilog RTL。后面讨论SystemC的仿真控制机制,基于事件仿真的仿真控制方法也限制了仿真系统的发挥,导致它的仿真过程跟verilog RTL直接仿真没有根本性的优势,可能也是一个原因之一。
本节的例子来自于“SystemC基础教程 A SystemC Primer J.Bhasker著,孙海平等译”一书。这本书作为SystemC入门书籍,值得一看。

12.1 SystemC建模

12.1.1 组合电路模块

SystemC实际是一个C++的库,在C++宏和模板功能的支持下,可以用来描述数字电路中的模块,端口,信号等概念,对组合电路和时序电路也能够较好的描述。我们先来看个组合电路的例子:

SC_MODULE(half_adder) 
	sc_in <bool>a, b;
	sc_out <bool> sum, carry;
	void prc_half_adder();

	SC_CTOR(half_adder) 
		SC_METHOD(prc_half_adder);
		sensitive << a << b;
	
;
void half_adder::prc_half_adder() 
	sum = a ^ b;
	carry = a & b;

这是个半加器的模型描述,输入a, b,输出sum和carry。描述的开始用SC_MODULE宏定义了一个c++类实现的数字电路模块,其中用sc_in和sc_out来定义模块的输入输出端口,使用模板可以选择端口的类型和宽度,然后定义了一个类的构成函数,在函数中用宏SC_METHOD,用来实现verilog中的持续性赋值和always 块,对always,可以描述信号变化事件,信号上下沿事件等。上述描述对应的verilog模型是

module half_adder(input a, b, output sum, carry);
reg sum, carry;
wire a, b;
always @(a or b) begin
	sum = a ^ b;
	carry = a & b;
end
endmodule

或者用持续性赋值

module half_adder(input a, b, output sum, carry);
assign sum = a ^ b;
assign carry = a& b;
endmodule

上述的SC_MODULE实际上定义了一个c++的子类,下面是宏展开的代码:

struct half_adder : ::sc_core::sc_module 
	sc_in <bool>a, b;
	sc_out <bool> sum, carry;
	void prc_half_adder();

	typedef half_adder SC_CURRENT_USER_MODULE;    
	half_adder(::sc_core::sc_module_name) 
        ::sc_core::sc_process_handle prc_half_adder_handle =               
	    sc_core::sc_get_curr_simcontext()->create_method_process(
		"prc_half_adder",  false, SC_MAKE_FUNC_PTR( SC_CURRENT_USER_MODULE, prc_half_adder ),
		this, 0 ); 
        sensitive << prc_half_adder_handle ; 
        sensitive_pos << prc_half_adder_handle ;                                
        sensitive_neg << prc_half_adder_handle ;    
		sensitive << a << b;
    
;
void half_adder :: prc_half_adder() 
	sum = a ^ b;
	carry = a & b;

比较有意思的是SC_METHOD宏,它在类的构成函数中调用全局的仿真控制上下文(sc_get_curr_simcontext()返回)生成了一个所谓的方法prc_half_adder_handle,其实就是一个回调函数登记项目,把模型中的成员函数prc_half_adder记录在其中。后面的代码:

		sensitive << prc_half_adder_handle ; 
        sensitive_pos << prc_half_adder_handle ;                                
        sensitive_neg << prc_half_adder_handle ;    
		sensitive << a << b;

巧妙利用了c++中运算符重载的功能,实现了端口或变量与回调函数登记项目之间的关联,上述代码等价于:

	sensitive.m_mode = SC_METHOD_;
    sensitive.m_handle = (sc_process_b*)prc_half_adder_handle ;
	sensitive_pos.m_mode = SC_METHOD_;
    sensitive_pos.m_handle = (sc_process_b*)prc_half_adder_handle ;
	sensitive_neg.m_mode = SC_METHOD_;
    sensitive_neg.m_handle = (sc_process_b*)prc_half_adder_handle ;
    a.make_sensitive( as_method_handle(prc_half_adder_handle ) );
    b.make_sensitive( as_method_handle(prc_half_adder_handle ) );

其中的make_sensitive是在a和b的一个回调表格中增加了一个prc_half_adder_handle的项目,在仿真过程中一旦a或者b被赋予一个与当前值不同的值,就会去设置调用回调表中的回调项目,让它处于需要执行的状态,由仿真控制系统稍后统一把这样的回调项目收集起来执行。这样就实现了在a或者b变化时,执行half_adder::prc_half_adder函数,更新sum和carry的值,这正好是组合逻辑所需要的功能。
如果要实现边沿触发,则使用sensitive_pos或者sensitive_neg的make_sensitive类似的功能,可以检测到端口或者变量在出现上下沿变化时进行回调。

12.1.2 层次化建模

SystemC还支持分层建模,在模型中可以实例化其他模型,比如我们用前面的半加器和一个或门来实现一个全加器:

SC_MODULE(full_adder) 
	sc_in <bool> a, b, carry_in;
	sc_out<bool> sum, carry_out;

	sc_signal<bool> c1, s1, c2;
	void prc_or();
	half_adder* ha1_ptr, * ha2_ptr;
	SC_CTOR(full_adder) 
		ha1_ptr = new half_adder("ha1");
		/*端口连接,命名方式连接*/
		ha1_ptr->a(a);
		ha1_ptr->b(b);
		ha1_ptr->sum(s1);
		ha1_ptr->carry(c1);

		ha2_ptr = new half_adder("ha2");
		/*端口连接,位置方式连接*/
		(*ha2_ptr)(s1, carry_in, sum, c2);
		SC_METHOD(prc_or);
		sensitive<<c1<<c2;
	

	~full_adder() 
		delete ha1_ptr;
		delete ha2_ptr;
	
;

void full_adder::prc_or() 
	carry_out = c1 | c2;

模块的实例化是通过c++的new生成一个c++对象实例来实现的,如果有实例化参数,则在声明模型时应该声明为c++的一个模板(template),然后实例化时带模板参数即可,虽然无法实现verilog中的默认参数之类的功能,但是至少能实现参数。
这里值得注意的是端口的连接,模块的端口只能连接到端口或者sc_signal等SystemC内部定义的数据类型变量上,这点不像verilog中可以连接到表达式上,当然也还是可以接受的,大不了声明一个变量,然后这个变量由一个SC_METHOD关联的项目实现表达式。端口的连接过程,其实类似于前面的SC_METHOD中实现的连接,也在相关的变量的变化事件触发表中生成一个项目。输入端口和输出端口实现的方法不一样,基本的思路时由驱动端来实现进行触发,一旦驱动源发生了变化,就设置被驱动信号的某个回调函数登记项,让仿真控制在恰当的时机进行回调函数调用,将驱动源的值赋予到被驱动端去。

12.1.3 时序电路建模

时序电路建模与组合电路建模方法基本一样,区别在于敏感表的处理,这点跟verilog中是一样的。由信号边沿触发的赋值,应该实现为非阻塞赋值,这样可以在信号边沿才实现输出的改变,比如D型触发器:

SC_MODULE(ff) 
	sc_in<bool> d, clk;
	sc_out<bool> q;
	void prc_ff();
	SC_CTOR(ff) 
		SC_METHOD(prc_ff);
		sensitive_pos << clk;
	
;

void ff::prc_ff() 
	q = d;

函数prc_ff在clk信号的上沿被调用,从而实现在clk的上沿输出的值变为输入的值,正好是时序电路对非阻塞赋值要求的。

12.1.4 行为级建模

SystemC还支持SC_THREAD等方式,来支持电路描述中的时间延迟,事件同步等机制,然而这些特征已经不是RTL描述了,因此我们不做更深入的介绍。这些描述能力使得SystemC与Verilog一样由行为级建模的能力。

12.2 SystemC仿真

建模完成后,可以编一个仿真程序来进行仿真运行。一般仿真程序有一个驱动模块,比如对前面的全加器,我们编制一个驱动模块,用SC_THREAD来实现每5ns输出一组激励信号。下面是驱动模块:

SC_MODULE(driver) 
	sc_out<bool> clk, d_a, d_b, d_cin;
	void prc_driver();
	SC_CTOR(driver) 
		SC_THREAD(prc_driver);
	
;
void driver::prc_driver() 
	sc_uint <3> pattern;
	pattern = 0;
	while (1) 
		d_a = pattern[0];
		d_b = pattern[1];
		d_cin = pattern[2];
		clk = 0;
		wait(5, SC_NS);
		clk = 1;
		wait(5, SC_NS);
		pattern++;
	

这个测试驱动模块是用SC_THREAD方法实现的,实际运行时,它启动了一个线程,然后在事件控制下进行多线程同步协同运行。其中的wait函数其实是生成了一个将来发生的事件,然后将线程暂停,等待仿真系统将时钟推进到事件发生时刻,再唤醒线程接着运行,这里模拟输出了一个10ns周期的时钟信号。
当然,仿真总得输出点什么,否则仿真就没有意义了。因此再编一个信号记录模块来记录关心的信号,更高级的用法比如生成VCD波形文件之类,这里不多介绍了,请参考SystemC的技术规范。

SC_MODULE(monitor) 
	sc_in<bool> clk, m_a, m_b, m_cin, m_sum, m_cout;
	void prc_monitor();
	SC_CTOR(monitor) 
		SC_METHOD(prc_monitor);
		sensitive_pos << clk;
	
;
void monitor::prc_monitor() 
	cout << "At time" << sc_time_stamp() << "::";
	cout << "(a, b, carry_in):";
	cout << m_a << m_b << m_cin;
	cout << "(sum, carry_out): " << m_sum << m_cout << endl;

这个记录模块只能说中规中矩,其实SystemC中有更加专业的信号记录方法,请参见相关的技术规范。
然后是主程序将几个模块连接在一起,并实现仿真运行:

int sc_main(int argc, char* argv[]) 
	sc_signal <bool> t_a, t_b, t_cin, t_sum, t_cout, clk;

	//加法器
	full_adder adderobj("FullAdder");
	adderobj(t_a, t_b, t_cin, t_sum, t_cout);
	//驱动器
	driver driverobj("drider");
	driverobj(clk, t_a, t_b, t_cin);
	//记录仪
	monitor monitorobj("monitor");
	monitorobj(clk, t_a, t_b, t_cin, t_sum, t_cout);
	//开始仿真,持续100ns
	sc_start(100, SC_NS);
	return (0);


下面是运行输出:

        SystemC 2.3.4_pub_rev_20191203-Accellera --- Jul  8 2021 19:56:05
        Copyright (c) 1996-2019 by all Contributors,
        ALL RIGHTS RESERVED

Info: (I804) /IEEE_Std_1666/deprecated: sc_sensitive_pos is deprecated use sc_sensitive << with pos() instead
At time0 s::(a, b, carry_in):000(sum, carry_out): 00
At time5 ns::(a, b, carry_in):000(sum, carry_out): 00
At time15 ns::(a, b, carry_in):100(sum, carry_out): 10
At time25 ns::(a, b, carry_in):010(sum, carry_out): 10
At time35 ns::(a, b, carry_in):110(sum, carry_out): 01
At time45 ns::(a, b, carry_in):001(sum, carry_out): 10
At time55 ns::(a, b, carry_in):101(sum, carry_out): 01
At time65 ns::(a, b, carry_in):011(sum, carry_out): 01
At time75 ns::(a, b, carry_in):111(sum, carry_out): 11
At time85 ns::(a, b, carry_in):000(sum, carry_out): 00
At time95 ns::(a, b, carry_in):100(sum, carry_out): 10

Info: (I804) /IEEE_Std_1666/deprecated: You can turn off warnings about
             IEEE 1666 deprecated features by placing this method call
             as the first statement in your sc_main() function:

  sc_core::sc_report_handler::set_actions( "/IEEE_Std_1666/deprecated",
                                           sc_core::SC_DO_NOTHING );

12.3 SystemC的仿真控制机制

从仿真的控制方面看,SystemC采用按事件驱动仿真的机制运行,它的仿真其实是基于变量赋值以及同步和时钟推进产生的新事件来推进仿真过程的。基本的过程如下:一开始将仿真事件设置为0,所有的回调处理按时间0都加入到事件列表中。然后开始以下的推进过程:

  1. 执行事件表中时间最小的事件(调用对应的回调函数),这个过程可能会执行新赋值动作,从而产生新的事件,将新的事件加入到事件表中,新的事件发生时间比当前时间晚,也可能发生时间就是当前时间。
  2. 重复1,直到事件表中最小的时间的事件的发生时间比当前事件晚,对wait等时间延迟的执行将唤醒对应的SC_THREAD的执行,从而产生新的事件加入到事件表中。此时把当前时间设置为这个时间(推进时钟),然后继续做1,直到仿真时间到达指定的结束时间,或者仿真过程由某个模型中调用sc_stop而停止。

这个机制其实是某种推的(PUSH)机制,某个计算过程产生新的值,用推的办法传播到sensitive表中影响的过程,将sensitive表中影响的过程加入到待运行事件表中去,然后执行新的事件。对于不带时间延迟描述的组合逻辑电路,由于组合电路不允许有圈,这个反复推的过程最后会稳定下来,不再产生新的当前时间事件,这样一个组合电路的计算网络的变化传播就完成了。然后是时钟步进,执行时钟信号的变化事件,wait(5, CS_NS)就是在事件表中加入一个当前时间加上5ns时刻发生的事件。在这个事件的计算过程中修改时钟信号变量,从而产生时钟沿事件和时钟信号变化事件,推进新的一轮组合电路变化的传播。
SystemC为了减少计算量,内部对同时发生的事件进行了排序处理,在一次组合电路的计算传播过程中,一个事件对应的回调函数只被调用一次,然而代价是必须按照依赖关系定义回调函数的调用顺序,这样处理计算量就能减少了。然而这种推送机制以及调用排序却带来一个问题,回调函数调用无法并发计算了。在数学上,变量之间的计算依赖性关系是一个偏序关系,偏序关系集合内部是存在并发性的,按理可以局部并发执行。但是事件驱动仿真机制以及事件产生的机制,使得这个偏序集合处于一个动态变化的状态中,集合的元素随着执行过程会出现增减,而且在集合中的元素执行也要求按一定的顺序,如果不按照特定顺序执行,就会导致一个回调函数在一个组合电路周期中被多次调用。计算项集合的动态变化以及要求按顺序执行,本质上是给这个集合赋予了一个线性序关系,此时偏序关系内在的并发关系被剔除了,也就剔除了多核并发计算甚至用GPU多核加速或者用分布式计算来加速的可能。这也是基于事件仿真控制的缺点,毕竟一个事件不执行,无法确定下一个事件是什么。无法用多核进行加速,对于目前有多核化分布式趋势的计算机体系而言是一个比较大的缺点了,再大再复杂的一个模型,只能运行在一个CPU核的一个线程上,其他的CPU核和硬件线程资源只能袖手旁观,做软件的应该感觉非常痛惜才是。
相比而言,HDL4SE的仿真控制基于周期推进模式,每个周期分为两个阶段,一个是组合逻辑计算阶段,一个是时序电路信号建立阶段,损失了部分灵活性,主要损失是多时钟支持,行为级描述中的时间延迟支持以及同步事件等方面,这些损失对RTL描述是可以接受的。HDL4SE的组合逻辑计算阶段采用的是拉的模式(PULL,对应SystemC的推PUSH)。每个模型为了更新输出,通过IHDL4SEUnit接口的GetValue函数得到输入端口的值,用于计算输出值。注意如果是组合逻辑,这种GetValue在依赖链上是连续调用的,因此能够确保得到最新的值。另外, GetValue实现的时候对于比较复杂的计算,可以采用整个组合逻辑计算过程中只计算一次,计算一次之后设置一个有效标志(该标志由时序阶段清除,确保每个周期开始时该有效标志被复位),后面的GetValue调用就直接返回前面的计算结果了,同时也阻止了GetValue的递归传播,减少了计算量。
HDL4SE在仿真开始时,将所有参与仿真的IHDL4SEUnit对象收集到一张表中。这张表格在整个仿真过程中不会变化,而且所谓仿真过程就是不断对表中的每个对象调用其ClkTick接口函数完成组合电路计算,再对每个对象调用Setup接口函数实现非持续赋值部分的输出信号建立。由于这个调用过程对顺序并无要求,因此用简单的OpenMP编程可以支持并发仿真。需要做的就是在GetValue实现时支持多线程,将实际的计算和结果缓冲的过程保护起来,只允许一个线程进行计算,其他线程被阻塞等待。等该线程计算完成后撤销保护,让等待的线程继续执行GetValue调用,直接返回前面线程的计算结果。后面会在HDL4SE中进行相关的研究,看看多核机制对HDL4SE仿真速度的影响。理论上甚至可以支持用GPU进行并行加速,充分利用计算机的计算资源。
另外,HDL4SE实现时没有考虑敏感表的支持。事实上,SystemC实现敏感表支持代价也不低,每个变量要比较当前值和赋予的值是否变化,如果发生变化才设置相应的回调标志,并将回调事件加入到事件表中去,很多情况下这个过程比重新计算一次的代价还要高。因此HDL4SE把这种功能交予程序员权衡,如果计算过程比较复杂,可以自行存储一份计算的输入值,在得到新的输入值后对比输入是否变化,然后决定是否重新计算输出值,如果计算过程比较简单,那就不妨重新计算一次好了。

12.4 HDL4SE中的宏定义辅助编程

不管如何,SystemC是一个非常优秀的建模工具,它充分利用了c++语言中的宏和模板,以及c++语言中的运算符重载功能,让程序的源代码看着简洁,并且更加接近verilog的描述方式,严格按照规范写的SystemC RTL描述俨然是另一种计算机语言,甚至可以编译为verilog。
为此,我们在HDL4SE中也定义了一系列宏,用来简化HDL4SE建模中的描述,并使之更加容易理解,增强可读性。特别是减少LCOM八股的代码量,并规范化其中的端口处理等方面。作为对比,我们贴出terris例子中terris_blockwrite.c的实现,来感受一下c语言中宏的威力,这是LCOM宏表示的带LCOM八股的实现:

/*
00, input           wClk,
01, input   [3:0]   bCtrlState,
02,	output          wCtrlStateComplete,
03,	output  [5:0]   bBWReadAddr,
04,	input   [63:0]  bBWReadData,
05,	output          wBWWrite,
06,	output  [5:0]   bBWWriteAddr,
07,	output  [63:0]  bBWWriteData,
08,	input   [63:0]  bCurBlock,
09,	input   [15:0]  bCurBlockPos
*/

/* wClk不算 */
#define INPUTPORTCOUNT 4
typedef struct _sTerrisBlockWrite 
	OBJECT_HEADER
	INTERFACE_DECLARE(IHDL4SEUnit)
	HDL4SEUNIT_VARDECLARE
	DLIST_VARDECLARE
	
	IHDL4SEModule** parent;
	char* name;

	IBigNumber**  inputdata;
	IHDL4SEUnit** input_unit[INPUTPORTCOUNT];
	int           input_index[INPUTPORTCOUNT];
	
	unsigned int index;
	unsigned int readindex; /* 模拟读地址寄存器,比index晚一拍 */
	unsigned int readindex_1;
sTerrisBlockWrite;

OBJECT_FUNCDECLARE(terris_blockwrite, CLSID_TERRIS_BLOCKWRITE);

HDL4SEUNIT_FUNCDECLARE(terris_blockwrite, CLSID_TERRIS_BLOCKWRITE, sTerrisBlockWrite);
DLIST_FUNCIMPL(terris_blockwrite, CLSID_TERRIS_BLOCKWRITE, sTerrisBlockWrite);

OBJECT_FUNCIMPL(terris_blockwrite, sTerrisBlockWrite, CLSID_TERRIS_BLOCKWRITE);


QUERYINTERFACE_BEGIN(terris_blockwrite, CLSID_TERRIS_BLOCKWRITE)
QUERYINTERFACE_ITEM(IID_HDL4SEUNIT, IHDL4SEUnit, sTerrisBlockWrite)
QUERYINTERFACE_ITEM(IID_DLIST, IDList, sTerrisBlockWrite)
QUERYINTERFACE_END

static const char* terris_blockwriteModuleInfo()

	return "0.3.0-20210622.1411 Terris BlockWrite module";


static int terris_blockwriteCreate(const PARAMITEM* pParams, int paramcount, HOBJECT* pObject)

	sTerrisBlockWrite* pobj;
	int i;
	pobj = (sTerrisBlockWrite*)malloc(sizeof(sTerrisBlockWrite));
	if (pobj == NULL)
		return -1;
	*pObject = 0;
	HDL4SEUNIT_VARINIT(pobj, CLSID_TERRIS_BLOCKWRITE);
	INTERFACE_INIT(IHDL4SEUnit, pobj, terris_blockwrite, hdl4se_unit);
	DLIST_VARINIT(pobj, terris_blockwrite);
	
	pobj->name = NULL;
	pobj->parent = NULL;
	for (i = 0;i< INPUTPORTCOUNT;i++)
		pobj->input_unit[i] = NULL;

	pobj->inputdata = bigintegerCreate(64);

	pobj->index = 0;

	for (i = 0; i < paramcount; i++) 
		if (pParams[i].name == PARAMID_HDL4SE_UNIT_NAME) 
			if (pobj->name != NULL)
				free(pobj->name);
			pobj->name = strdup((const char *)pParams[i].pvalue);
		 
		else if (pParams[i].name == PARAMID_HDL4SE_UNIT_PARENT) 
			pobj->parent = (IHDL4SEModule **)pParams[i].pvalue;
		
	

	/* 返回生成的对象 */
	OBJECT_RETURN_GEN(terris_blockwrite, pobj, pObject, CLSID_TERRIS_BLOCKWRITE);
	return EIID_OK;


static void terris_blockwriteDestroy(HOBJECT object)

	sTerrisBlockWrite* pobj;
	int i;
	pobj = (sTerrisBlockWrite*)objectThis(object);
	if (pobj->name != NULL)
		free(pobj->name);
	for (i = 0; i < INPUTPORTCOUNT; i++)
		objectRelease(pobj->input_unit[i]);
	objectRelease(pobj->inputdata);

	memset(pobj, 0, sizeof(sTerrisBlockWrite));
	free(pobj);


static int terris_blockwriteValid(HOBJECT object)

	sTerrisBlockWrite* pobj;
	pobj = (sTerrisBlockWrite*)objectThis(object);
	return 1;


static int 以上是关于HDL4SE:软件工程师学习Verilog语言的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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