DreamCll

博客园 首页 联系 订阅 管理

  对于一名芯片验证师而言,他可能面临的任务可能是模块级(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(计分板)。一般我们把 drivermonitorreference 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_sequencemy_driver 的联系纽带(连接),其实 my_sequencer 是有真正用途,它不是中间客,也不赚差价。这些将在后续 sequence 专题仔细叙述。仔细看图我们会发现,他们均继承与 uvm_* ,这种东西,这就是上面说的,前人总结的套路,其中已经帮我们定义了一些东西,我们可以直接用。哦,对了,下图还有一点应该解释一下,其中 sequence/sequencer/driver 均有一个小尾巴#( my_transaction ),这是一种叫模板类(也叫参数类)的玩意需要的参数,如果不写这东西,我们的sequence / sequencer / driver,里面发的“子弹”就会默认是 uvm_sequence_item 类型。我觉得作为一把“枪”,它用什么子弹,我们还是要清楚的,不然可能会导致一些不必要的编译麻烦。

  类似的 monitor 的代码样式,也是类似上图,继承自 uvm_montior

posted on 2019-11-21 19:48  DreamCll  阅读(7453)  评论(0编辑  收藏  举报