SystemVerilog Testbench Lab5: broad spectrum verification(上)

Lab4采用面向对象封装的方式来搭建验证平台,但是前面的几个Lab都只给16个端口的其中一个灌激励。Lab5将多个组件都封装成类,使得测试平台能够同时驱动所有的端口。

Driver Class

Driver类是由DriverBase基类扩展而来的,基类中(1)定义接口信号、sa/da/payload、Packet类对象等;(2)定义与Driver相关的函数,比如前面的Lab中涉及到的send()任务。因为每个端口都要发送数据,所以基类中定义了发送数据相关的property和method。

// DriverBase.sv
`ifndef INC_DRIVERBASE_SV
`define INC_DRIVERBASE_SV
class DriverBase;
	virtual	router_io.TB rtr_io;
	string	name;
	bit[3:0]	sa,da;		// global variables 
	bit[7:0]	payload[$];	
	Packet		pkt2send;

	extern function new(stirng name="DriverBase", virtual router_io.TB rtr_io);
	extern virtual task send();
	extern virtual task send_addrs();
	extern virtual task send_pad();
	extern virtual task send_payload();
endclass:DriverBase
// 相当于为DriverBase类创建空间,做一部分的初始化工作:初始化name和interface
function DriverBase::new(stirng name, virtual router_io.TB rtr_io);
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, name);
	this.name = name;
	this.rtr_io = rtr_io;
	//this.pkt2send = new(); // DriverBase中未用到pkt2send,个人认为应为pkt2send分配空间并初始化
endfunction
// send 调用类中的其他方法
task DriverBase::send();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.send_addrs();
	this.send_pad();
	this.send_payload();
endtask:send

task DriverBase::send_addrs();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.rtr_io.cb.frame_n[this.sa] <= 1'b0;
	for(int i=0; i<4; i++) begin
		this.rtr_io.cb.din[this.sa] <= this.da[i];
		@(this.rtr_io.cb);
	end
endtask:send_addr

task DriverBase::send_pad();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
	this.rtr_io.cb.din[this.sa] <= 1'b1;
	repeat(5) @(this.rtr_io.cb);
endtask:send_pad

task DriverBase::send_payload();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	foreach(this.payload[index]) begin
		for(int i=0; i<8; i++) begin
			this.rtr_io.cb.din[this.sa] <= this.payload[index][i];
			this.rtr_io.cb.valid_n[this.sa] <= 1'b0;
			this.rtr_io.cb.frame_n[this.sa] <= ( index == (this.payload.size()-1) ) && ( i==7 );
			@(this.rtr_io.cb);
		end
	end
	this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
endtask:send_payload

`endif
  • 注意:sa、da、payload定义为全局变量,对于该类中的method都是可见的,在类中使用时用句柄指向该变量即this.xxx而不是其本身!

Driver类要做的事情有2件,一是将来自Gen的数据包送到Dvr,由Dvr发送到DUT端(由DriverBase类中的send()实现);二是将Dvr刚发出去的数据包原样给Scb一份,用于和DUT输出端的数据包作比较。在Gen、Dvr、Scb之间通信的媒介是mailbox,数据被一个进程发送到另一个mailbox中,而另外一个进程可以从mailbox中获得该数据。

// router_test.h
typedef class Packet;
typedef mailbox #(Packet) pkt_mbox; // 参数化信箱定义:typedef mailbox #(type) mailbox_name;
  • mailbox——信箱:允许两个线程之间进行数据交换,实现进程之间的同步。参考link
    • 可以把它看成是一个先进先出的存储数组。 通常有一个/多个进程把数据写入一个 mailbox, 有一个/多个进程从 mailbox 读出数据。写数据方法是put();读数据方法是get()/peek(),前者读取数据并且将其从队列中删除,后者复制数据且不删除队列中的原数据。
    • mailbox可以创建成有界的队列,也可以是无界的队列。new的参数缺省即为无界:new()本例中使用的参数为16:new[16],即队列的边界为16,本Lab的gen()中创建了16个mailbox,每个都是无界的。当一个有界的mailbox所存储的数据达到其边界数目时,试图向已经满了的mailbox放置数据的进程会被阻塞,直到mailbox队列中有数据被读出,使得mailbox又有了足够的空间;而无界mailbox永远也不会阻塞一个发送操作进程。
    • 本例中使用的是参数化信箱,用typedef的方式定义了Packet类型的信箱,表明其只存取Packet类型的数据。建议使用参数化信箱,防止因数据类型不一致而出差错。
// Driver.sv
`ifndef INC_DRIVER_SV
`define INC_DRIVER_SV
`include "DriverBase.sv"
// Driver派生自基类DriverBase
class Driver extends DriverBase;
	pkt_mbox	in_box;		// pass Packets object from Gen to Dvr
	pkt_mbox	out_box;	// pass Packets object from Dvr to Scb
	semaphore	sem[];		// semaphore array

	extern function new(string name = "Driver", int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual rtouer_io.TB rtr_io);
	extern virtual task start();
endclass:Driver
`endif
// 将constructor的参数列表中的5个变量一一对应到类对象(包括继承自基类的对象)
function Driver::new(string name, int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual rtouer_io.TB rtr_io);
	super.new(name, rtr_io);
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.sa = port_id;
	this.sem = sem;
	this.in_box = in_box;
	this.out_nox = out_box;
endfunction
// 关键的一步的动作是从in_box中取出要发送的数据包,送到pkt2send中
// 再将pkt2send的属性传递到基类的全局变量中,最后通过send任务发送
task Driver::start();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	fork	// 个人理解此处fork并发执行的是各个dvr的start(结合test.sv来看)
		forever begin
			this.in_box.get(this.pkt2send);
			if(this.pkt2send.sa != this.sa) continue; // 跳过后面的语句,继续下一轮循环
			this.da = this.pkt2send.da;
			this.payload = this.pkt2send.payload;
			this.sem[this.da].get(1);
			this.send();
			this.out_box.put(this.pkt2send);
			this.sem[this.da].put(1);
		end
	join_none
endtask:start

`endif
  • new()super.new():SV中的class通过new()来创建实例,即创建类的对象;在创建对象的过程中,可以做一些初始化的工作。super.new()是父类的new()函数,子类通过super来索引父类的同名函数:

子类在定义new函数时,应该首先调用父类的new函数即super.new( )。如果父类的new函数没有参数,子类也可以缺省该调用,而系统会在编译时自动添加super.new( )函数。

子类中使用this.xxx是先访问子类有没有xxx的变量或者方法;而super.xxx直接访问父类的xxx变量和方法,不会访问子类。

  • semaphore——旗语:通过仲裁实现对共享资源的访问。
    • 如果多个进程要向同一个端口发送数据,每个进程必须等待权限到来后才能发送。本例中Gen产生的数据包是随机的,很可能出现多个输入想要驱动一个端口的情况。每当出现多个输入竞争同一个输出端口的时候,必须通过仲裁来决定哪个端口能够获得这个共享的资源。
    • 当为semaphore分配内存的时候,可以认为会创建一个带有一定数目key的存储桶(从test.sv中得知,本例中key=1)。使用semaphore的进程在继续执行之前必须先从存储桶中获得一个key,获得的方式是使用get方法。所有其他进程必须等待,直到其需要的足够数目的key被放回到存储桶中,放回的方式是使用put方法。
  • Driver的行为逻辑:不停地从Gen产生的in_box中获取数据包,放到pkt2send里;如果获取的数据包正好是要在此Dvr口this.sa发送的,则将pkt2send的属性传递到基类的全局变量this.da/this.payload中,否则重新获取数据包;如果这时该数据包的输出端口this.da没有被占用即获得了key,则调用基类的send()把数据发送给DUT,否则等待输出端口被释放;接着将发送的数据包pkt2send原样放一份到out_box里;最后释放输出端口,将key放回存储桶中。

Receiver Class

Receiver类派生自ReceiverBase基类。基类需要做的事情时定义与待接收的数据包相关的信号和接收数据包的具体任务。

// ReceiverBase.sv
`ifndef INC_RECEIVERBASE_SV
`define INC_RECEIVERBASE_SV

class ReceiverBase;
	virtual router_io.TB rtr_io;
	bit[3:0] da;
	bit[7:0] pkt2cmp_payload[$];
	Packet	pkt2cmp;

	extern function new(string name = "ReceiverBase", virtual router_io.TB rtr_io);
	extern virtual task recv();
	extern virtual task get_payload();
endclass:ReceiverBase
// new函数用于创建对象,在创建对象的过程中做一部分初始化工作
function ReceiverBase::new(string name, virtual router_io.TB rtr_io);
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, name);
	this.name = name;
	this.rtr_io = rtr_io;
	this.pkt2cmp = new();	// 调用new方法分配空间和初始化
endfunction

task ReceiverBase::recv();
	static int pkt_cnt = 0;
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.get_payload();
	this.pkt2cmp.da = this.da;
	this.pkt2cmp.payload = this.pkt2cmp_payload;
	this.pkt2cmp.name = $psprintf("rcvdPkt[%0d]", pkt_cnt++);
endtask:recv

task ReceiverBase::get_payload();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	this.pkt2cmp_payload.delete();
	fork	// 被称为guard fork,用于限制disable fork的作用范围
		begin:wd_timer_fork
			fork:frameo_wd
				@(negedge this.rtr_io.cb.frameo_n[this.da]);
				begin
					repeat(10000) @(this.rtr_io.cb);
					$finish;
				end
			join_any:frameo_wd
			disable fork;	// kill掉与之同一个层次的所有fork,在本例中就是wd_timer_fork
		end:wd_timer_fork
	join	
	forever begin
		logic[7:0] datum;
		for(int i=0; i<8; ) begin
			if(!this.rtr_io.cb.valido_n[this.da])
				datum[i++] = this.rtr_io.cb.dout[this.da];
			if(this.rtr_io.cb.frameo_n[this.da])
				if(i==8) begin
					this.pakt2cmp_payload.push_back(datum);
					return;	// 任务立即退出而不需要执行到末尾
				end
			else begin
					$display("\n%m\n[ERROR]%t Packet payload not byte aligned!\n", $realtime);
					$finish;
				end
			@(this.rtr_io.cb);
		end
		this.pkt2cmp_payload.push_back(datum);
	end
endtask:get_payload

`endif

Receiver做的事情是调用基类中的recv()任务,再拷贝一份基类中的数据包,放进out_box信箱中。

// Receiver.sv
`ifndef INC_RECEIVER_SV
`define INC_RECEIVER_SV
`include "ReceiverBase.sv"
// 派生自ReceiverBase,不需要再次定义name和interface
class Receiver extends ReceiverBase;
	pkt_mbox	out_box;	// Scoreboard mailbox
	
	extern function new(string name = "Receiver", int port_id, pkt_mbox outbox, virtual router_io.TB rtr_io);
	extern virtual task start();
endclass
// 将constructor的参数列表中的变量一一对应到类对象
function Receiver::new(string name, int port_id, pkt_mbox out_box, virtual router_io.TB rtr_io);
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	super.new(name, rtr_io);
	this.da = port_id;
	this.out_box = out_box;
endfunction

task Receiver::start();
	if(TRACE_ON) $display("[TRACE]%t %s: %m", $realtime, this.name);
	fork
		forever begin
			this.recv();	// 注意要用this.recv()而不是recv()!
			begin
				Packet pkt = new this.pkt2cmp;	// 浅拷贝一份pkt2cmp
				this.out_box.put(pkt);
			end
		end
	join_none	// 注意是join_none,不是join!
endtask

`endif
  • 通过new xxx来实现浅拷贝。浅拷贝又叫句柄拷贝,上述代码中的pkt只拷贝了pkt2cmp的数据变量,未拷贝pkt2cmp的数据操作(如类的对象和方法,此例中是compare()和display函数)以及其中定义的其他类的句柄,可采用“引用”的方式访问。
    • 浅拷贝与深拷贝的具体区别参考link。更多包括浅拷贝、深拷贝、类赋值在内的SV类知识参考link
    • 疑惑:为何浅拷贝一份存至out_mbox中,而不是像Driver那样直接放进去呢?
  • 特别注意最后应该使用fork…join_none,不要误写未fork…join!!!fork…join不能实现non-blocking并发。

more

后续见《SystemVerilog Testbench Lab5: broad spectrum verification(下)》

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值