欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-28 10:23:54
...

前言

最近转战验证方向,想起了初学验证时候的心酸和迟迟不能跑通一个验证平台的苦恼,因此想写这个博客。不借助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本次功能正确。

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

 

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

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

 

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

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

写组件

确定仿真行为

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

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

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

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

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

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

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

 

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

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

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

暂结

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

 

 

 

 

 

 

 

 

 

 

相关标签: system verilog