UVM實戰[二]


歡迎關注個人公眾號摸魚范式

本期將講解UVM環境構成和啟動方式。主要參考資料為

http://bbs.eetop.cn/thread-320165-1-1.html
http://rockeric.com/

環境構成

進行仿真驗證的基本流程是

  1. 例化DUT
  2. 產生並發送激勵
  3. 檢測響應
  4. 檢查響應是否正確

在驗證環境中,產生並發送激勵將會交給兩個不同的類完成,即uvm_driver和uvm_sequence,檢測響應通過uvm_monitor完成,而檢查響應是否正確通過uvm_scoreboard。除了保證某項功能正確,我們還需要能夠確保spec中的每一項功能都通過測試,而衡量驗證完備性的指標之一就是功能覆蓋率,在我們的驗證環境中收集功能覆蓋率的任務則交給了conv_coverage實現。

接下來將以數據從uvm_driver驅動到DUT,再從DUT到uvm_monitor,再到uvm_scoreboard的順序講解驗證環境的構成。

接口定義

當我們需要進行仿真驗證時,與DUT的交互是一個必要的內容,所我們首先分析DUT的接口,較為簡單,一共有四組接口,一組寄存器配置接口,三組數據接口用於輸入特征圖、權重和偏置數據的讀取,一組數據接口用於輸出特征圖的存儲接口。

interface的定義在頂層的tb.sv中,三組輸入數據幾口可以使用同一類型的接口實現,下列代碼中的具體內容省略了,詳情請自行查看。最后一組接口用於檢測寄存器的內容,當前版本沒有使用寄存器模型,所以這個接口是必要的。

interface cfg_intf (input clk , input rst_n);
  clocking drv_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
  clocking mon_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
endinterface

interface mem_in_intf (input clk , input rst_n);
  clocking drv_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
  clocking mon_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
endinterface

interface mem_out_intf (input clk , input rst_n);
  clocking mon_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
endinterface

interface conv_intf (input clk , input rst_n);
  clocking mon_ck @(posedge clk);
    default input #1ns output #1ns;
  endclocking
endinterface

注意在interface定義中,分別定義了兩個時鍾塊,一組驅動用的時鍾塊,一組檢測用的時鍾塊,目的就是為了模擬真實的建立保持時間,時鍾塊的具體用法可以參考綠皮書的第四章內容。

環境組件

有了接口定義以后,通過接口定義,我們便能夠與DUT交互,那么進行交互我們需要做什么呢?

首先看整體結構,如果看不清,后台回復UVM結構圖獲取VISIO文件。按照接口進行分類,可以分成兩大類,一類通過接口與DUT實現交互,另外一類構成了其他的組件,例如checker,通過其他組件收集到的數據進行數據比對,保證DUT的功能正確性。

驗證環境一共由四個PKG組成,通過在頂層import導入:

import cfg_pkg::*;
import mem_in_pkg::*;
import mem_out_pkg::*;
import conv_pkg::*;

與DUT直接聯系的組件

我們從DUT與驗證環境的接口處開始說起,cfg_pkg包含了對於寄存器進行驗證的組件。構建UVM環境基本的幾個組件包括uvm_driver,uvm_sequencer,uvm_monitor,uvm_agent。而uvm_sequence_item和uvm_sequence則不屬於環境的組件,他們是環境組件之間傳遞信息的載體。

package cfg_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  typedef enum {WR,RD,IDLE}cmd_t;

  class cfg_item extends uvm_sequence_item;
  endclass : cfg_item

  class cfg_driver extends uvm_driver #(cfg_item);
  endclass :cfg_driver

  class cfg_sequencer extends uvm_sequencer #(cfg_item);
  endclass: cfg_sequencer

  class cfg_base_sequence extends uvm_sequence #(cfg_item);
  endclass: cfg_conv_sequence

  class cfg_monitor extends uvm_monitor;
  endclass: cfg_monitor

  class cfg_agent extends uvm_agent;
    cfg_driver driver;
    cfg_monitor monitor;
    cfg_sequencer sequencer;
    local virtual cfg_intf vif;
  endclass:cfg_agent
    
endpackage

UVM的思想之一就是要降低組件之間的耦合度,讓組件的功能更加單純。

uvm_driver和uvm_monitor是距離DUT最近的兩個組件,可以直接與DUT的接口進行互動。

uvm_monitor通過檢測接口上的信號,轉化為數據包,如實地發送給checker,只實現這一單純的功能,而對於驅動DUT這一功能則交給uvm_driver。

uvm_driver通過從uvm_sequencer獲取到的uvm_sequence_item解析出驅動數據,如實的將uvm_sequence_item的內容驅動到DUT的接口上,也只實現這一單純的功能,至於具體的激勵內容,則通過uvm_sequencer暴露接口給頂層環境,讓驗證人員通過uvm_sequencer發送激勵。

而uvm_sequencer的功能就更加簡單了,只需要實現傳遞uvm_sequence_item即可,不需要關注其他的工作。

而uvm_agent中則通常會例化四個組件,uvm_driver,uvm_sequencer,uvm_monitor和對應的interface。uvm_agent的功能也非常單一,僅僅只是將對於一組接口的相關組件進行一個打包,把他們整合起來,這樣在頂層就只需要例化這一個組件即可。

mem_in_pkg和mem_out_pkg的內容整體上和cfg_pkg基本一致,不再贅述。

與DUT沒有直接聯系的組件

通過cfg_pkg的內容,我們實現了對DUT的驅動與檢測,那么驅動的內容從何而來,而檢測的數據包又要發送到哪里去呢?從前面圖中我們可以看到,除了五個agent以外,我們還有其他的組件,包括conv_checker,conv_coverage和conv_virtual_sequencer。

  class conv_checker extends uvm_scoreboard;
  endclass:conv_checker

  class conv_coverage extends uvm_component;
  endclass:conv_coverage

  class conv_virtual_sequencer extends uvm_sequencer;
    cfg_sequencer    cfg_sqr;
    mem_in_sequencer fmi_sqr;
    mem_in_sequencer wt_sqr;
    mem_in_sequencer bias_sqr;
  endclass:conv_virtual_sequencer

conv_checker繼承自uvm_scoreboard,他通過前面所述的五個agent中的monitor,獲取DUT的信息,進行數據對比檢查。在現在的DUT中,他所實現的功能是,在一次卷積運算結束后,使用軟件算法直接進行卷積運算,然后與DUT的計算結果進行對比,確保DUT功能正確。

UVM並沒有預置的類用於覆蓋率收集,所以conv_coverage繼承自uvm_component,成為最簡單的UVM組件。由於除了寄存器的接口以外,其他都是簡單得sram接口,所以只對寄存器進行覆蓋率收集。覆蓋率組件通過覆蓋率的收集,量化功能驗證的完備性,根據對應的功能點,設定對應的覆蓋率,而功能驗證的目的就是為了達到100%的功能覆蓋率。

conv_virtual_sequencer繼承自uvm_sequencer,其本身並沒有什么功能,所以他的名字中帶有virtual,他只是一個虛擬的sequencer。其內部包含了cfg_sqr,fmi_sqr,wt_sqr和bias_sqr,作用就是將他們整合在一起,sequencer就是一根數據線,uvm_sequence_item就是傳輸的數據,而virtual_sequencer就一個集線器或者說一個拓展塢,把很多條數據線綁在一起。

conv_env

數據的驅動由uvm_driver實現,檢測由uvm_monitor實現,激勵由uvm_sequencer傳遞,數據對比由conv_checker實現,覆蓋率收集由conv_coverage實現。那么接下來就需要把這些組件全部整合在一起,成為一個驗證環境,這便是conv_env。在這里,我們只需要完成各個組件的例化和他們之間的連接,不要關心其他工作。

  class conv_env extends uvm_env;
    cfg_agent cfg_agt;
    mem_in_agent fmi_agt;
    mem_in_agent wt_agt;
    mem_in_agent bias_agt;
    mem_out_agent fmo_agt;
    conv_checker chker;
    conv_coverage cvrg;
    conv_virtual_sequencer virt_sqr;
  endclass: conv_env

uvm_test

現在我們已經獲得了一個針對卷積模塊的DUT,那么如何開始仿真測試?回憶前面所提到的virtual_sequencer,我們只需要通過virtual_sequencer對每個DUT的接口進行驅動,就能讓DUT運轉起來。針對每一個測試,我們需要創建對應的uvm_test類,然后再uvm_test內通過virtual_sequencer進行激勵發送即可。

  class conv_base_test extends uvm_test;
    conv_env env;
      
    task run_phase(uvm_phase phase);
      phase.raise_objection(this);
      this.run_top_virtual_sequence();
      phase.drop_objection(this);
    endtask

    virtual task run_top_virtual_sequence();
    endtask

  endclass: conv_base_test

上述代碼中的run_phase的內容就是在構建環境后,整個仿真真正需要進行的測試內容。可以看到我們定義了一個run_top_virtual_sequence方法,用於運行virtual_sequence。

與virtual_sequencer對應的,virtual_sequence就是virtual_sequencer所需要傳輸的內容,它的內部會包括各式各樣的sequence,針對每一個agent發送不同的激勵。通過修改virtual_sequence的內容,我們就能夠完成不同的測試用例。

啟動方式

這里先不討論整個環境的樹狀結構、連接方式和運行機制,這些將在后續的推送中實現。

在構建完整個環境和測試用例以后,我們就需要在頂層啟動測試。

module tb ();
logic clk;
logic rst_n;
    conv i_conv ();//這里省略了端口連接,具體請參考實驗代碼
  // clock generation
  initial begin
    clk <= 0;
    forever begin
      #5 clk <= !clk;
    end
  end
  // reset trigger
  initial begin
    #10 rst_n <= 0;
    repeat(10) @(posedge clk);
    rst_n <= 1;
  end
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  import cfg_pkg::*;
  import mem_in_pkg::*;
  import mem_out_pkg::*;
  import conv_pkg::*;

cfg_intf cfg_if(.*);
mem_in_intf fmi_if(.*);
mem_in_intf wt_if(.*);
mem_in_intf bias_if(.*);
mem_out_intf fmo_if(.*);
conv_intf conv_if(.*);

assign conv_if.start              =i_conv.i_regfile.start              ;
assign conv_if.done               =i_conv.i_regfile.done               ;
assign conv_if.fmap_in_w          =i_conv.i_regfile.fmap_in_w          ;
assign conv_if.fmap_in_h          =i_conv.i_regfile.fmap_in_h          ;
assign conv_if.fmap_in_ch_div_32  =i_conv.i_regfile.fmap_in_ch_div_32  ;
assign conv_if.k_w                =i_conv.i_regfile.k_w                ;
assign conv_if.k_h                =i_conv.i_regfile.k_h                ;
assign conv_if.fmap_out_w         =i_conv.i_regfile.fmap_out_w         ;
assign conv_if.fmap_out_h         =i_conv.i_regfile.fmap_out_h         ;
assign conv_if.fmap_out_w_div_32  =i_conv.i_regfile.fmap_out_w_div_32  ;
assign conv_if.fmap_out_ch_div_32 =i_conv.i_regfile.fmap_out_ch_div_32 ;
assign conv_if.pooling_bypass     =i_conv.i_regfile.pooling_bypass     ;
assign conv_if.act_bypass         =i_conv.i_regfile.act_bypass         ;
assign conv_if.padding_cnt        =i_conv.i_regfile.padding_cnt        ;
assign conv_if.stripe             =i_conv.i_regfile.stripe             ;
assign conv_if.last_pixel         =i_conv.i_regfile.last_pixel         ;
assign conv_if.last_pixel_div_32  =i_conv.i_regfile.last_pixel_div_32  ;
assign conv_if.fmap_out_ch        =i_conv.i_regfile.fmap_out_ch        ;
assign bias_if.addr[15:8]='0;

  initial begin
    uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "fmi_in_vif", fmi_if);
    uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "wt_vif", wt_if);
    uvm_config_db#(virtual mem_in_intf)::set(uvm_root::get(), "uvm_test_top", "bias_vif", bias_if);
    uvm_config_db#(virtual cfg_intf)::set(uvm_root::get(), "uvm_test_top", "cfg_vif", cfg_if);
    uvm_config_db#(virtual mem_out_intf)::set(uvm_root::get(), "uvm_test_top", "fmo_vif", fmo_if);
    uvm_config_db#(virtual conv_intf)::set(uvm_root::get(), "uvm_test_top", "conv_vif", conv_if);
    // If no external configured via +UVM_TESTNAME=my_test, the default test is
    // std_test
    run_test("std_test");
  end

endmodule : tb

在tb的頂層模塊中,我們要做5件事:

  1. 定義時鍾與復位
  2. 例化dut
  3. 例化與連接各個interface
  4. 將每個interface句柄通過uvm_config_db傳遞到環境中去
  5. 通過run_test()方法啟動測試

uvm_config_db是UVM所提供用於傳遞數據的靜態方法,在后續的推送中將會展開講解。這里值得注意的是,一定要在run_test()之前實現uvm_config_db傳遞,否則在run_test()開始后,環境內部將無法獲取句柄,導致報錯。

run_test()是UVM提供的測試啟動方法,傳遞參數是一個字符串變量,該字符串將用於指定默認的testcase。如果在命令選項中,沒有通過+UVM_TESTNAME指定具體的TESTNAME,將會運行默認的testcase。

總結

本次講解了驗證環境的基本組件和構成,以及在頂層啟動的注意事項。

  1. 與DUT直接交互的組件為uvm_driver和uvm_monitor,傳遞激勵信息的組件為uvm_sequencer,uvm_agent將三者組合起來。
  2. uvm_scoreboard獲取各個uvm_monitor傳遞過來的數據,進行比對,保證DUT功能的正確性。
  3. conv_coverage用於收集覆蓋率
  4. conv_virtual_sequencer將每個uvm_agent中的uvm_sequencer集中起來進行管理,起到集線器或者說路由器的效果
  5. conv_env將上述所有組件容納起來,並且進行連接
  6. uvm_test通過編寫conv_virtual_sequence,經由conv_virtual_sequencer發送激勵實現不同的testcase
  7. 在頂層實現各項例化,並且在run_test()之前傳遞接口句柄
  8. 通過run_test()啟動測試,並且指定默認testcase

更多內容請期待后續推送,實驗代碼在后台回復“UVM實驗”即可獲得資源。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM