【验证小白】就用SV+modelsim学验证(1)——把平台搭起来
前言
最近转战验证方向,想起了初学验证时候的心酸和迟迟不能跑通一个验证平台的苦恼,因此想写这个博客。不借助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信息打出来,最简单的平台总算是搭起来了。下次继续探讨研究!!