对于一名芯片验证师而言,他可能面临的任务可能是模块级(module level
)、子系统级(subsystem level
)或者系统级(chip level
)的验证。但是俗话说"条条大路通罗马",它们用得方式是一样的,当前业界通常采用 systemverilog
和 UVM 来验证 DUT。
UVM 是以 systemverilog
为基础,同时吸收了 C++ 的一些思想发展起来的一套验证方法学(函数库),简单来说就是前人在自己工作基础上总结的一套验证的套路,我们在使用时,只需在这套机制的不同部分填充不同的内容,即可完成DUT的验证工作。
世上最简单验证平台组成对于芯片验证,我们所要做的事情是,验证 DUT 的输出是否满足设计的正常功能,对于一个裸露的 DUT,我们需要先对其输入一定的数据,满足它需要的输入时序要求,它才会正常的输出数据。玩过 FPGA 的同学应该都有过目测时序是否满足设计要求的经历。但是对于上千万个信号的时序,需要检查其输出是否正确。
在验证的世界中,我们把给 DUT 灌数据的东西,专门用 driver 来完成,而接受 DUT 输出的东西,用 montior
来完成;为了完成与 DUT 的输出数据比对的任务,我们需要一个能实现与 DUT 相同功能的东西,这东西有个专门的名称要参考模型( reference model ),把 monitor
接受到的数据和 reference model
的输出数据进行比对,如果比对成功就表示 DUT 能完成设计的功能,反之你觉得呢?而完成比对的任务的东西,也有个专门的名称叫 scoreboard
(计分板)。一般我们把 driver
、monitor
、reference model
合计起来叫做环境( environment
),当然这是一个最简单的环境,后面随着介绍的越来越多,环境中的东西会越来越多。用一个图来表示就是下面这个模样(UVM 实战)。
假设 DUT
代码如下:
run_phase 运行机制
module dut(clk, rst_n, rxd, rx_dv, txd, tx_en); input clk; input rst_n; input[7:0] rxd; input rx_dv; output [7:0] txd; output tx_en;
reg[7:0] txd;
reg tx_en;
always @(posedge clk) begin
if(!rst_n) begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else begin
txd <= rxd;
tx_en <= rx_dv;
end
end
endmodule
现在需要实现对上述 DUT
的验证,一个小白最能想到的 driver
可能是下面这个样子:
run_phase 运行机制
module top_tb;
reg clk;
reg rst_n;
reg[7:0] rxd;
reg rx_dv;
wire[7:0] txd;
wire tx_en;
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rxd(rxd),
.rx_dv(rx_dv),
.txd(txd),
.tx_en(tx_en)
);
initial begin
rxd <= 8'b0;
rx_dv <= 1'b0;
while(!rst_n)
@(posedge clk);
for(int i = 0; i < 256; i++)begin
@(posedge clk);
rxd <= $urandom_range(0, 255);
rx_dv <= 1'b1;
end
@(posedge top_tb.clk);
rx_dv <= 1'b0;
//finish
$finish();
end
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
end
initial begin
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end
endmodule
driver 在验证中的作用,可以类似一把手枪,验证的 DUT
的功能点就是我们靶子,如果把信号的产生,也驱动时序(驱动协议)均放在 driver
中实现,会带来很多麻烦,如
driver代码部分显得过于繁杂,缺乏层次感对后续维护和重用代码麻烦;
信号的驱动协议可能是一致的,只是不同的功能点验证,需要的信号或者信号间的组合
为此 UVM 中验证 DUT一般采用类来实现,通过把 driver 的部分拆分成不同的功能组件,有实现信号产生的类( sequence
)、信号传递的类( sequencer
)和信号驱动的类( driver
);这些东西对于初学者不是那么友好,可以暂且理解为一个完成特定功能的类,只是大家给它们取了一个特定的名字而已。现在脑子里应该是这样的,sequence
是是弹夹,sequencer
是弹簧,driver
是一把手枪。貌似忘记了一个很重要的东西,一把枪怎么可能没有子弹呢,这在 UVM 中用一个叫 seqence_item
的东西完成,这四个部分实现一个 DUT 验证驱动时序的产生和驱动过程。
我们再对上述的枪的比喻进行进一步扩展理解,一把手枪装一个短弹夹,可以一次发射七颗子弹(貌似),但是如果我牛逼一点搞了一个长弹夹,一次性装五十颗子弹,只要弹夹能插上手枪也能完成射击任务。这就可以类比理解为我们验证不同的功能点,但是他们遵循的驱动协议一致,只是配置不同而已,那我们只需要重新扩展或者重新定义 sequence
即可,因为子弹是一样的,我们不需要换枪管( sequencer
)和手枪( driver
);有时候待验证的功能点比较讨厌,对于同一个 DUT,它需要满足不同的协议,这个时候就需要我们把手枪换来福或者发射筒,即需要把四个组件重新依据协议定义实现。一般来说 montior
检测 DUT 的输出是和 driver 具有强相关的,俗话说有应必有果。
到现在为止,我们的验证代码框架应该是长成如下图这样的,其中 my_transaction
类中定义了一些数据变量,my_sequence
则实现 transaction
的组织和打包,my_driver
是实现 my_transaction
中的数据安装一定的协议驱动至 DUT,而最后 my_sequencer
则是 my_sequence
和 my_driver
的联系纽带(连接),其实 my_sequencer
是有真正用途,它不是中间客,也不赚差价。这些将在后续 sequence
专题仔细叙述。仔细看图我们会发现,他们均继承与 uvm_*
,这种东西,这就是上面说的,前人总结的套路,其中已经帮我们定义了一些东西,我们可以直接用。哦,对了,下图还有一点应该解释一下,其中 sequence/sequencer/driver
均有一个小尾巴#( my_transaction )
,这是一种叫模板类(也叫参数类)的玩意需要的参数,如果不写这东西,我们的sequence / sequencer / driver
,里面发的“子弹”就会默认是 uvm_sequence_item
类型。我觉得作为一把“枪”,它用什么子弹,我们还是要清楚的,不然可能会导致一些不必要的编译麻烦。
类似的 monitor
的代码样式,也是类似上图,继承自 uvm_montior
。