【验证小白】就用SV+modelsim学验证(1)——把平台搭起来

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/moon9999/article/details/81436828

前言

最近转战验证方向,想起了初学验证时候的心酸和迟迟不能跑通一个验证平台的苦恼,因此想写这个博客。不借助UVM、VMM等验证方法学,凭着system verilog和modelsim尝试着搭一个能够跑通、能够明白原理、能够直观看到波形的验证平台,或许对于我这样的验证初学者也是有好处的。

最简单的验证平台

常见的验证平台如下图所示,这几个模块可以说是最基础的元素了:

gen负责生成某一class类型的数据A并发送给driver,driver解析该数据为时序信号,打到总线上;

数据信号进入DUT,DUT对数据进行处理打出处理结果;

mon1采集入口数据总线信号打包为该类型数据A,传输给reference model;

RM模拟DUT行为,产生相同的结果,传送出处理结果B至checker;

mon2采集DUT出口,打包为结果C传输给checker;

checker对比B与C是否一致,如果一致证明DUT本次功能正确。

这个平台由于包含DUT和RM,其实还可以进一步简化。如果就想先学下验证方法,我们可以搭建一个更简单的平台,仅仅要探究各个组件的功能以及看看各个组件的工作是否正确,如下图。

就要这4个组件就够了,RM和DUT这两个先不要。这样的一个自检平台可以检测每个组件功能是否正确,无论driver还是monitor出了问题都无法在checker比对通过。

那么要写一个验证平台,你要有一个仿真工具,简单起见本人使用了modelsim 10.04,之后也会尽量追求简单操作。

写组件

确定仿真行为

在写平台之前当然要预先确定DUT的接口需要什么行为的信号,之后由test来发出符合该要求的信号(当然之后也会进行注错来检查异常处理,这阶段暂且不提)。既然我们没有dut,那就自己构想一个行为吧。我构想的数据行为如下图:

clk:时钟;

rst_n:复位,低有效;

data:DUT入口接收的信号,为8bit一定长度的报文数据流(即包,packet);

vld:data有效标志;

sop:标记当拍的data为数据流的头(start);

eop:标记当拍的data为数据流的尾(end);

此外,包与包之间还必须要有一定的间隔(interval)。以上即为DUT入口的预期行为,当然此时我们没有DUT不过没关系把数据打到总线上就好了。

interface

确定了行为后,我们就可以来写interface文件了,interface详见SV绿皮书第四章。我的interface.sv文件代码如下:

`ifndef PKT_IF_SV
`define PKT_IF_SV

interface pkt_if(input clk, rst_n);
		
	logic [7:0] data;
	logic 	    sop;
	logic	    eop;
	logic	    vld;
	
	clocking drv @(posedge clk);
		output 	data;
		output	sop, eop, vld;
	endclocking : drv
	modport pkt_drv (clocking drv);
	
	clocking mon @(posedge clk);
		input 	data;
		input	sop, eop, vld;
	endclocking : mon
	modport pkt_mon (clocking mon);
	
endinterface

typedef virtual pkt_if.drv vdrv;
typedef virtual pkt_if.mon vmon;

`endif

代码的`ifndef-`define-`endif是为了防止重复编译,与代码主体功能无关,写上即可。modelsim的编译找不到和重复编译困扰了我很久,最后采用了这样的方式。接口与wire总线对上,在内部划分出信号对于drv和mon是输出还是输入即可,不过赘述。此外,关于virtual interface说明详见SV绿皮书285页虚接口。

pkt_data

有了接口之后,就要开始写pkt_data.sv了,这是最难也是最关键的一个模块。在class pkt_data中,我们需要对发到总线上的数据的属性与行为进行描述,之后由driver对pkt_data进行解析,得到时序信号。那么就刚刚的看到的波形而言,有哪些需要提炼的数据属性呢?

1.payload:即每一拍的8bit数据,在测试时候我们希望每一拍都是随机数;

2.pkt_len:每一个包(packet)的长度;

3.interval:每两个包之间的间隔,测试时可以模拟为每个包之前需要打几个空拍;

OK这就是我们packet的属性,对这些属性进行约束并且进行相关的操作后,就可以得到pkt_data。

约束有哪些呢?包长要有一个范围,那么一上来我们就约束包长为10拍吧;两个包之间空拍要有范围,不妨约束为3~6拍。

操作有哪些呢?

1.new():构造函数不多说了,相当于“新建”,不过我们这个pkt_data没什么可new()的,里面空着就行。

2.pack():为什么需要一个task pack()呢?主要是方便之后driver把包解析为时序信号。在类中我申明了一个11bit的队列data[$],队列中每一个值第11bit为vld标志,第10bit为sop标志,第9bit为eop标志,低8bit为数据。那么当打空拍时将第11bit置为0,打payload时第11bit置为1数据总线上的vld就会为H标志数据有效,sop标志和eop标志是同样的效果。把payload和interval依据属性打入data[$]中,之后driver直接根据这个队列来发送信号。

3.copy():两个pkt_data之间复制函数,主要为了之后的工厂模式服务(或称蓝图模式,SV220页),不过暂时不用细究;

4.psprintrf():预置打印函数;

`ifndef PKT_DATA_SV
`define PKT_DATA_SV

class pkt_data;

	rand bit [7:0]   payload_q[$];
	rand int         interval;
	rand int	 pkt_len;
	
	bit		 send_over;	
	bit [10:0]       data[$];
	
	constraint data_size_cons{
		payload_q.size() == pkt_len;
	};

	constraint pkt_len_cons{
		pkt_len == 10;
	};
	
	constraint interval_cons{
		interval inside {[3:6]};
	};
	
	extern function new();
	extern virtual function void psprintf();
	extern virtual function void pack();
//	extern virtual function unpack();
	extern virtual function pkt_data copy(input pkt_data to=null);
	
endclass

function pkt_data::new();

endfunction

function void pkt_data::pack();
	foreach (this.payload_q[i]) begin
		if (i==0)
			this.data.push_back({1'b1, 1'b1, 1'b0, payload_q[i]});		
		else if (i==pkt_len-1)
			this.data.push_back({1'b1, 1'b0, 1'b1, payload_q[i]});
		else
			this.data.push_back({1'b1, 1'b0, 1'b0, payload_q[i]});		
	end
	for(int i=0; i<interval; i++)begin
	    this.data.push_front({'0});
	end
endfunction

function void pkt_data::psprintf();
	$display("pkt_data.pkt_len=%0d\n", this.pkt_len);
	foreach(this.payload_q[i])begin
		$display("pkt_data.payload_q[%0d]=%0h\n", i, this.payload_q[i]);
	end
endfunction

function pkt_data pkt_data::copy(input pkt_data to=null);
	pkt_data tmp;
	if (to == null)
		tmp = new();
	else
		$cast(tmp, to);
	tmp.interval = this.interval;
	tmp.pkt_len  = this.pkt_len;
	tmp.send_over= this.send_over;
	foreach(this.payload_q[i])begin
		tmp.payload_q.push_back(this.payload_q[i]);
	end
	return tmp;
endfunction

`endif

driver

有了数据类型后,就可以根据这个数据类写pkt_drv.sv了。其实generator和driver最好在一起说,不过我们先来看driver可以。

首先看class pkt_drv中的属性,drv需要从gen接收pkt_data类的数据,怎么接收呢?需要一个信箱(mailbox),因此我们需要声明一个mailbox gen2drv,信箱详细请见Svetlana199页。之后,drv需要把信号打到数据总线上,因此需要一个虚接口vdrv dif(注意,在pkt_if.sv中已经做出相应typedef )。有这两个入口和出口就足够了,不过由于我们使用pkt_data和pkt_if,我们需要把这两个文件`include一下,这里不用怕重复编译,因为每一个文件都有`ifndef来保护免遭重复编译。

接下来看下行为。

1.new():新建一下,drv的new主要是把自己的信箱和接口与传进来的信箱和接口连接在一起(之后可认为二者等同);

2.run():主函数,看里面行为就好;

3.rst_sig:在不发包的时候你得把总线的信号收拾一下吧,要么随机要么全1全1啥的,这个task3.就是干这个的;

4.pkt_send():每次drv由gen2drv收到一个数据,就可以调用这个task一次把数据打到总线上,里面的行为看一下应该就明白了,注意@posedge top.clk的写法是不规范的,一般要@posedge dif.clk才好。

对了,drv怎么判断该发的包已经发完了呢?peek会查看信箱gen2drv中还有没有数据,如果没有就会一直等在这(阻塞),如果有数据就会"复制"出一份(详见SV204页)。那么倘若gen已经发完了,drv就会一直在这等,这个函数没法结束。因此我设计让gen在所有的包发完之后再额外发一个“pkt.send_over==1”的包,drv接收到包之后首先查看下send_over是否为1,不为1的话把这个包发出去,一旦为1那么就从while(1)中break出来,结束这个函数。

`ifndef PKT_DRV_SV
`define PKT_DRV_SV

`include "pkt_data.sv"
`include "pkt_if.sv"
class pkt_drv;
	mailbox gen2drv;
	vdrv dif;
	
	int get_num;

	
	extern function new(input vdrv dif,
			    input mailbox gen2drv
		            );
	extern virtual task run();
	extern virtual task rst_sig();
	extern virtual task pkt_send(input pkt_data pkt);

endclass

function pkt_drv::new(	input vdrv dif,
			input mailbox gen2drv
			);
	this.dif = dif;
	this.gen2drv = gen2drv;
	this.get_num = 0;
endfunction

task pkt_drv::run();
	pkt_data send_pkt;
	
	$display("pkt_drv run()!");
	rst_sig();
	$display("after rst_n at %t", $time);
	while(1) begin
		//$display("drv while(1)!");
		gen2drv.peek(send_pkt);
		if(send_pkt.send_over == 1) begin
			$display("get over pkt");
			break;
		end
		$display("drv get no.%0d pkt from gen", this.get_num++);		
		send_pkt.pack();
		pkt_send(send_pkt);
		gen2drv.get(send_pkt);
		rst_sig();
	end
endtask

task pkt_drv::rst_sig();
	wait(top.rst_n == 1'b1);
	@(posedge top.clk);
	this.dif.vld <= '0;
	this.dif.sop <= '0;
	this.dif.eop <= '0;
	this.dif.data<= '0;
//	$display("vld rst over");
endtask

task pkt_drv::pkt_send(input pkt_data pkt);
	$display("drv pkt_send begin!");
	foreach(pkt.data[i]) begin
		@(posedge top.clk);
		this.dif.vld <= pkt.data[i][10];
		this.dif.sop <= pkt.data[i][9];
		this.dif.eop <= pkt.data[i][8];
		this.dif.data<= pkt.data[i][7:0];
	end	
endtask

`endif

generator

pkt_gen.sv负责产生随机的pkt_data类报文(进行随机化),之后丢给mailbox gen2drv传送给pkt_drv。因此其中一个关键属性就是send_num即要发送几个包。代码其实不用说太多,比较简单。注意$cast的使用(检查两个数据是否可以句柄复制)和工厂模式的使用。

`ifndef PKT_GEN_SV
`define PKT_GEN_SV
`include "pkt_data.sv"

class pkt_gen;
	mailbox gen2drv;
	pkt_data pkt;
	int send_num;
	
	extern function new(input mailbox gen2drv);
	extern virtual task run();
	
endclass

function pkt_gen::new(input mailbox gen2drv);
	this.gen2drv  = gen2drv;
	this.pkt      = new();
endfunction

task pkt_gen::run();
	pkt_data send_pkt;
	
	$display("send_num = %0d", send_num);
	repeat(send_num) begin
		assert(pkt.randomize());
		$cast(send_pkt, pkt.copy());
		gen2drv.put(send_pkt);
		$display("gen send a pkt to drv");
	end

	assert(pkt.randomize());
	pkt.send_over = 1;
	$cast(send_pkt, pkt.copy());
	gen2drv.put(send_pkt);	
	$display("gen over pkt");
endtask

`endif

environment

至此所有的组件就写完了,需要用env把所有的组件连接起来。在env中我们申明下各个组件和interface,并且建立一个容量为1的信箱供gen与drv之间传递数据。在new()中把接口和外面的接口连接起来,在build()中新建gen和drv,在run()中以fork-join的形式把gen和drv跑起来,如果两者都结束了才会从这个task跳出来。最后预留report()以备之后使用。

`ifndef ENV_SV
`define ENV_SV

`include "pkt_gen.sv"
`include "pkt_drv.sv"
`include "pkt_if.sv"

class environment;
	pkt_gen gen;
	pkt_drv drv;
	mailbox gen2drv;
	event drv2gen;
	event gen2drv_over;
	vdrv dif;	
	vmon mif;
	int send_pkt_num;

	extern function new(input vdrv dif,
			    input vmon mif
			   );
	extern virtual task build();
	extern virtual task run();
	extern virtual task report();	
endclass

function environment::new(input vdrv dif,
			  input vmon mif
			  );
	this.dif = dif;
	this.mif = mif;

endfunction

task environment::build();
	$display("environment::build() start!");
	gen2drv = new(1);
	gen = new(gen2drv);
	drv = new(dif, gen2drv);
	$display("environment::build() over!");
endtask
	
task environment::run();
	
	fork
		drv.run();
		gen.run();
	join
	
	$display("send pkt over");	
endtask

task environment::report();	
	repeat(100) @top.clk;
endtask
`endif

test

test是验证的顶层,在其中实例化env并执行env的操作。

`include "pkt_if.sv"
`include "environment.sv"
program automatic test(
	pkt_if.pkt_drv dif,
	pkt_if.pkt_mon mif,
	input clk, rst_n
	);
	
	environment env;
	
	initial begin
		env = new(dif, mif);
		env.build();
		env.gen.send_num = 5;
		env.run();
		$display("env run over at %d!", $time);
		env.report();
	end	
endprogram

top

最后把最顶层写一下,一般来说top中写四种东西:

1.产生时钟复位信号;

2.例化interface;

3.例化dut;

4.例化test;

根据我们这个简单平台,如下写法就可以了。

module top();
	
	logic clk;
	logic rst_n;
	
	initial begin
       		#0ns clk = 0;
		forever #5ns clk = ~clk;
	end
		
	initial begin
		#0ns rst_n = 0;
		#225ns rst_n = 1;
		$display("%0d, let's go", $time);	
	end

	pkt_if u_if(clk, rst_n);
	test u_test(u_if, u_if, clk, rst_n);
	
endmodule

编译运行

把所有 文件准备好后,新建modelsim工程,添加文件,编译,可以注意下编译顺序:compile-compile order

编译通过后就可以仿真了。

注意,仿真时请点击simulate键,修改一下,否则非端口信号都不会在波形中出现我记得(这个对应的命令行看了下貌似是vsim -gui -voptargs=+acc work.top,之前真的没敲过)。OK后,在左侧界面选择work->top,OK进入仿真界面。

5.到仿真界面,把transcript放到显眼的地方后(如果刚刚那步这东西不见了,去view里点出来再说)。在左侧top-u_if处右键add to wave。直接点击run或在在transcript键入run 1000n仿真1000ns,会在界面中看到如下打印结果和波形。

暂结

OK到这一步波形出来,log信息打出来,最简单的平台总算是搭起来了。下次继续探讨研究!!

猜你喜欢

转载自blog.csdn.net/moon9999/article/details/81436828