SystemVerilog / UVM 1.2 · 初學者筆記

UVM 基礎觀念
架構、資料流與資料型態

這份筆記不逐條解釋語法,而是回答四個初學者最需要先搞懂的問題: UVM 的架構長什麼樣、 一筆資料從產生到被檢查中間走過哪些站、沿路上它是什麼型態、 以及哪些細節不注意就會卡住。

主範例 DUT:adder(7-bit + 7-bit → 8-bit,1 cycle latency) 擴充範例:apb_slave 模擬器:VCS + -ntb_opts uvm-1.2

01為什麼需要 UVM

最小的 UVM testbench 只有三十行。它做的事情就是「印一行字」:

最小 UVM testbenchhello_world_uvm.sv
program automatic test;

  import uvm_pkg::*;
  `include "uvm_macros.svh"        // 沒有這行,`uvm_info / `uvm_component_utils 都不能用

  class hello_world extends uvm_test;
    `uvm_component_utils(hello_world)          // 註冊到 factory

    function new(string name = "hello_world", uvm_component parent = null);
      super.new(name, parent);
    endfunction

    virtual task run_phase(uvm_phase phase);
      phase.raise_objection(this);             // 舉手:我還沒做完,別結束模擬
      #50;
      `uvm_info("TEST", "Hello World!", UVM_MEDIUM)
      phase.drop_objection(this);              // 放手:可以結束了
    endtask
  endclass

  initial begin
    run_test("hello_world");                   // 啟動 UVM,指定要跑哪個 test
  end

endprogram

同一個資料夾裡,測一顆加法器的 testbench 卻有四百多行、八個 class。 多出來的四百行不是為了加法本身 —— 加法只有一行 expected = in1 + in2;。 那四百行提供的是這四件事:

① 分工

「產生刺激」「打到腳位上」「觀察波形」「判對錯」「算覆蓋率」拆成五個獨立 class, 改其中一個不用動其他四個。

② 隨機

測資是 rand 出來的,不是手寫的。換一個 seed 就是一組全新測資,不用改程式。

③ 自動比對

Scoreboard 自己算預期值跟 DUT 比,你不用盯波形。錯了自己噴 UVM_ERROR。

④ 可量化

Coverage 告訴你「測夠了沒」。可以寫成「跑到覆蓋率 50% 才停」,而不是「跑 100 次應該夠吧」。

一句話總結:UVM 不是幫你寫測試,是幫你把測試環境拆成可以各自替換的零件, 並約定好這些零件之間怎麼溝通(TLM)、什麼時候啟動(Phase)、怎麼被建立(Factory)、怎麼拿到設定(config_db)。 後面五章講的就是這四套約定。

02Interface:class 與 module 的交界

這是最容易讓初學者混亂的一點:一個 UVM testbench 裡其實有兩個完全不同的世界, 它們的生命週期、記憶體模型都不一樣。

 Module 世界(靜態)Class 世界(動態)
成員 module top、adder dut、adder_intf intf、clk/rst 產生器 test、env_c、agent_c、driver_c、trans_c …
何時存在 編譯期就決定,模擬全程都在,數量固定 執行期用 create() 生出來,可以生一千個,也可以中途丟掉
能碰訊號嗎 可以,直接接線 不行,class 沒有「腳位」這種東西
能被 randomize 嗎 不行 可以,rand + constraint

Class 世界要驅動 DUT,唯一的通道就是 interface。 interface 本身住在 module 世界(top 裡實體化一份), class 這邊拿到的是一個指向它的參照,型態叫 virtual interface。

interface 與三個 modportadder_intf.sv
interface adder_intf (input logic clk, input logic rst_n);
  logic [6:0] in1;
  logic [6:0] in2;
  logic [7:0] out;

  modport dut (input in1, input in2, output out);              // DUT:吃輸入、吐輸出

  modport drv (output in1, output in2, input out,              // Driver:只能寫 in1/in2
               input clk, input rst_n);

  modport mon (input in1, input in2, input out,                // Monitor:純觀察,全 input
               input clk, input rst_n);
endinterface

modport 就是方向權限表。同一組線,三個人看到的方向不同:

初學者最常卡的地方:Driver 宣告的型態不是 virtual adder_intf, 而是 virtual adder_intf.drv(帶 modport)。這個型態必須跟 uvm_config_db#(...) 傳遞時用的型態一字不差, 否則 get() 會回 false 然後 UVM_FATAL。詳見第 8 章。

03架構全景圖

UVM 元件排成一棵樹。樹根是 uvm_test_top(run_test("test") 自動幫你建), 往下每一層都在自己的 build_phase 裡把小孩生出來。

test uvm_test → uvm_test_top env_c uvm_env agent_c uvm_agent is_active = UVM_ACTIVE / UVM_PASSIVE scoreboard_c uvm_scoreboard cov_coll_c uvm_subscriber #(trans_c) driver_c uvm_driver sequencer uvm_sequencer monitor_c uvm_monitor ↑ class 世界  ↓ module 世界 adder_intf intf (.clk, .rst_n) adder dut module top vif.drv vif.mon
元件樹:這是 UVM testbench 最典型的形狀。虛線以上是 class、以下是 module; driver 與 monitor 各自握著一份 virtual interface 才能跨過去。

每一層負責什麼

層職責可不可以換掉
test決定「這次要跑什麼」:跑幾筆、目標覆蓋率、要不要 override每個測試情境一個 test,最常被換
env把 agent / scoreboard / coverage 兜起來並接線整包重用,換 DUT 才動
agent一個協定介面的完整代理:產生 + 驅動 + 觀察一種協定一個 agent(APB agent、AXI agent…)
sequencersequence 與 driver 之間的仲裁與排隊幾乎不用自己寫,直接用 uvm_sequencer#(T)
driver把 transaction 翻譯成訊號時序(協定細節都在這)換協定必改
monitor把訊號時序翻回 transaction 並廣播出去換協定必改
scoreboard算預期值、和 DUT 實際輸出比對、統計 PASS/FAIL換 DUT 功能必改(reference model)
coverage採樣、統計「測到哪些情境」換驗證目標就改

Active vs Passive:uvm_agent 內建一個 is_active 欄位。 Active 時建 driver + sequencer + monitor;Passive 時只建 monitor(純觀察,不出刺激)。 做法是在 agent 的 build / connect 裡加上這個判斷:

Active / Passive 判斷agent_c
virtual function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  if (is_active == UVM_ACTIVE) begin              // 只有 Active 才需要出刺激
    m_drv = driver_c::type_id::create("m_drv", this);
    m_sqr = uvm_sequencer#(trans_c)::type_id::create("m_sqr", this);
  end
  m_mon = monitor_c::type_id::create("m_mon", this);   // Monitor 永遠要建
endfunction

virtual function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  if (is_active == UVM_ACTIVE)
    m_drv.seq_item_port.connect(m_sqr.seq_item_export);   // 沒建就別連,否則 null handle
endfunction

04Datapath:一筆資料走一遍

這是整份筆記的核心。下面追蹤一筆 trans_c, 從被隨機生出來、被打到腳位上、DUT 算完、被觀察回來、到被比對,中間經過 8 站。

class 世界(物件在記憶體裡) sequence_c body() 1 sequencer 仲裁/排隊 2 driver_c run_phase 3 monitor_c run_phase 6 scoreboard_c write() 比對 8 cov_coll_c covgrp.sample() finish_item get_next_item m_mon_port.write() 7 adder_intf  in1[6:0] in2[6:0] out[7:0] clk rst_n virtual interface 邊界 vif.in1 <= tr.in1; vif.in2 <= tr.in2; 4 取樣 in1/in2/out adder (DUT) out <= in1 + in2 (下一拍) 5 in out module 世界(實際的線)
一筆 transaction 的完整路徑。①②③ 是「物件在傳遞」,④⑤⑥ 是「訊號在跑」,⑦⑧ 又回到物件。

逐站說明:資料此刻在哪、是什麼型態

站發生什麼資料此刻的身分
① sequence create() 生一個 trans_c,randomize() 灌入 in1/in2 trans_c 物件,只有輸入欄位有效,out 還是無意義
② sequencer finish_item() 把物件交給 sequencer,若 driver 還在忙就擋在這裡等 同一個物件的 handle 被傳遞(沒有複製)
③ driver seq_item_port.get_next_item(m_trans) 拿到 handle 同一個物件,driver 現在可以讀它的欄位
④ 跨界 @(posedge clk) 後 vif.in1 <= m_trans.in1; 物件 → 訊號。從這裡開始資料變成 logic 電平
⑤ DUT always_ff 在下一個 posedge 把 in1+in2 寫進 out 純訊號,有 1 個 cycle 的延遲
⑥ monitor 先記住這拍的 in1/in2,等下一拍才讀 out,湊成完整一筆 訊號 → 物件。create() 出一個全新的 trans_c
⑦ 廣播 m_mon_port.write(m_trans) 一次送給所有訂閱者 同一個物件 handle 被送給 N 個接收端
⑧ 檢查 scoreboard 算 expected = in1+in2 比對;coverage 採樣 物件被讀完就沒人再用,交給 GC

Driver:轉換的一半

從 sequencer 拿 item、驅動腳位、回報完成driver_c
virtual task run_phase(uvm_phase phase);
  trans_c m_trans;

  adder_vif.in1 <= '0;                    // reset 期間先把訊號歸零
  adder_vif.in2 <= '0;
  wait (adder_vif.rst_n === 1'b1);        // 等 reset 放開才開始工作

  forever begin
    seq_item_port.get_next_item(m_trans);       // ← 阻塞:沒東西就在這等
    @(posedge adder_vif.clk);                   //   對齊時脈
    adder_vif.in1 <= m_trans.in1;               //   非阻塞賦值,避免 race
    adder_vif.in2 <= m_trans.in2;
    `uvm_info("DRV", $sformatf("Drive in1=%0d, in2=%0d",
              m_trans.in1, m_trans.in2), UVM_LOW)
    seq_item_port.item_done();                  // ← 告訴 sequencer:這筆做完了
  end
endtask

get_next_item() 和 item_done() 必須成對。 少了 item_done(),sequencer 會認為 driver 永遠沒做完, 第二筆 transaction 就再也送不出來,模擬看起來像卡死。

Monitor:轉換的另一半(也是最容易寫錯時序的地方)

對付 1-cycle latency 與 NBA racemonitor_c
virtual task run_phase(uvm_phase phase);
  trans_c m_trans;
  logic [6:0] in1_q, in2_q;      // 暫存這一拍的輸入
  int idx = 0;

  wait (adder_vif.rst_n === 1'b1);
  @(posedge adder_vif.clk);
    #1;                          // ★ 讓 NBA 區更新完再讀,避開取樣 race

  forever begin
    in1_q = adder_vif.in1;       // cycle N:記住輸入
    in2_q = adder_vif.in2;
    @(posedge adder_vif.clk);
    #1;
    m_trans = trans_c::type_id::create($sformatf("mon_trans_%0d", idx++), this);
    m_trans.in1 = in1_q;         // cycle N 的輸入
    m_trans.in2 = in2_q;
    m_trans.out = adder_vif.out; // cycle N+1 的輸出 ← 這才是同一筆交易
    m_mon_port.write(m_trans);   // 廣播
  end
endtask

為什麼要 #1? DUT 用 always_ff 加非阻塞賦值 <= 更新 out, 這個更新發生在同一個時間點的 NBA 區。如果 monitor 在 @(posedge clk) 醒來就立刻讀,可能讀到「更新前」的舊值,而且每次執行順序不保證一樣(race)。 補一個 #1(或改用 clocking block)就能確保讀到穩定值。

為什麼要暫存 in1_q/in2_q? 因為 DUT 有 1 cycle latency:in1/in2 在 cycle N,對應的 out 在 cycle N+1。 如果同一拍把 in 跟 out 一起抓,就是把「這一筆的輸入」配上「上一筆的輸出」,scoreboard 會全紅。

05資料型態總覽

兩大家族:uvm_object vs uvm_component

UVM 所有 class 都從 uvm_object 長出來,但很早就分成兩支,行為完全不同:

 uvm_object 家族資料uvm_component 家族結構
代表 uvm_sequence_item(transaction)
uvm_sequence
uvm_driver、uvm_monitor、uvm_scoreboard、uvm_subscriber、uvm_agent、uvm_env、uvm_test、uvm_sequencer
有沒有位置 沒有。像一封信,可以到處傳、複製、丟掉 有。在元件樹上有固定路徑,例如 uvm_test_top.m_env.m_agt.m_drv
生命週期短。一筆交易一個,用完就回收長。build_phase 建好後模擬全程存在
建構子 function new(string name = "");
只有一個參數
function new(string name, uvm_component parent);
多一個 parent
註冊巨集 `uvm_object_utils(T)
或 `uvm_object_utils_begin/end
`uvm_component_utils(T)
有 phase 嗎沒有有:build / connect / run / report / final …
能被 randomize可以(rand + constraint)不會這樣用

用錯巨集的症狀:在 transaction 上寫 `uvm_component_utils, 或在 component 上寫 `uvm_object_utils,編譯就會炸在建構子簽章對不上 (因為巨集會展開出對應家族的 create() 實作)。看到一堆奇怪的 new() 相關錯誤,先回頭確認這兩個巨集有沒有配對。

Transaction:資料的容器

典型 transactiontrans_c
class trans_c extends uvm_sequence_item;
  rand logic [6:0] in1;        // rand:randomize() 時會被灌值
  rand logic [6:0] in2;
       logic [7:0] out;        // 沒有 rand:這是 DUT 給的,不是我們產生的

  function new(string name = "");
    super.new(name);
  endfunction

  `uvm_object_utils_begin(trans_c)      // begin/end 版本才能塞 field macro
    `uvm_field_int(in1, UVM_DEFAULT)
    `uvm_field_int(in2, UVM_DEFAULT)
    `uvm_field_int(out, UVM_DEFAULT)
  `uvm_object_utils_end
endclass

三個設計決定值得注意:

`uvm_field_int 換到什麼

登記欄位之後,UVM 自動幫你生出這些方法,不用手寫:

方法用途
print()印出整筆 transaction 的表格(debug 時很好用)
copy() / clone()深拷貝一筆交易(要存進 queue 比對時必用)
compare()兩筆逐欄位比對
sprint() / convert2string()轉字串塞進 log
pack() / unpack()攤平成 bit stream

參數化型別:角括號裡放的是什麼

寫法參數意義
uvm_sequence #(trans_c)這條 sequence 產生哪種 item
uvm_sequencer #(trans_c)這台 sequencer 搬運哪種 item
uvm_driver #(trans_c)這個 driver 收哪種 item(決定 seq_item_port 型態)
uvm_analysis_port #(trans_c)這個發送埠廣播哪種 item
uvm_analysis_imp #(trans_c, scoreboard_c)兩個參數:收哪種 item + 誰來實作 write()
uvm_subscriber #(trans_c)內建一個 analysis_export,你只要 override write()
uvm_tlm_analysis_fifo #(trans_c)可暫存的接收端,用 get() 取
virtual adder_intf.drv指向 interface 的參照,帶 modport

new() vs type_id::create()

兩個都能生物件,但意義差很多:

obj = new("name")

直接呼叫建構子。生出來的一定是你寫的那個 class,factory 攔不到。

只在 TLM port/export/FIFO 這種「不需要被 override」的東西上用,例如 m_mon_port = new("m_mon_port", this);

obj = T::type_id::create("name", this)

透過 factory 生。factory 會先查有沒有 override,有的話換成別的 class。

所有 transaction、sequence、component 都該用這個。這是「不改架構就能換行為」的前提(見第 9 章)。

參數差異:create() 給 component 用時要傳 this 當 parent (create("m_drv", this));給 object 用時通常不用 (create("m_trans_0"))—— 因為 object 沒有樹上的位置。

SystemVerilog 面:幾個一定要分清楚的

寫法意義在 UVM 裡什麼時候用
=(阻塞)立刻生效class 內部運算、monitor 抓回來的值存進 transaction
<=(非阻塞)排到 NBA 區才生效driver 驅動訊號一律用這個,避免和 DUT 的 always_ff 打架
==邏輯比較,遇到 X/Z 回傳 X盡量避開
===四值全等,X 跟 X 算相等,結果一定是 0 或 1scoreboard 比對、等 reset 一律用這個
wait (expr)條件成立才往下(已成立就不等)wait (vif.rst_n === 1'b1)
@(posedge clk)一定要等到下一個上升緣時序對齊
function不能消耗時間build/connect/report/write() 都是 function
task可以消耗時間run_phase、main_phase、sequence 的 body()

06Phase 機制:誰先跑、誰後跑

UVM 幫你排好了一條固定的流程。你不用自己呼叫,只要覆寫(override)你想插手的那一段, UVM 會在對的時間點自動叫它。

build_phase function · top-down · 用 create() 生小孩、config_db::get() connect_phase function · bottom-up · 接 TLM、接 seq_item_port end_of_elaboration_phase function · 樹已定型,最後檢查的機會 start_of_simulation_phase function · 常在這裡 uvm_root::get().print_topology() run_phase / main_phase ★ 唯一會消耗模擬時間的階段 task · 所有 component 的 run_phase 同時 fork 執行 objection 全部 drop 光了,這個階段才結束 extract / check / report_phase function · bottom-up · 印 PASS/FAIL 統計、coverage 數字 final_phase 時間 →
Phase 順序。想親眼確認的話,在每個 phase 裡加一行 `uvm_info marker,log 就會照這個次序印出來。

兩條要背起來的規則

規則一:function phase 不能延遲

build_phase、connect_phase、report_phase… 全是 function void。裡面放 #10 或 @(posedge clk) 會直接報 Illegal time-consuming statement。

要等時間,只能寫在 run_phase / main_phase(task)。

規則二:build 由上往下、connect 由下往上

build_phase 是 top-down:先建 test,test 建 env,env 建 agent… 因為要先有 parent 才能建 child。

connect_phase 反過來 bottom-up: 要先確定小孩都存在了,才連得起來。

Objection:決定模擬什麼時候結束

UVM 不知道你的測試要跑多久。Objection 就是一個計數器: 只要還有人「舉手反對結束」,run phase 就不會停;歸零的瞬間,UVM 收工。

用 objection 圈出「還在做事」的區間test::main_phase
virtual task main_phase(uvm_phase phase);
  sequence_c m_seq;
  int iter = 0;

  phase.raise_objection(this);            // ← 舉手:別結束

  // 一直送 sequence,直到覆蓋率達標或跑滿次數
  while (m_env.m_cov.covgrp.get_coverage() < cov_target && iter < max_iter) begin
    m_seq = sequence_c::type_id::create($sformatf("seq_%0d", iter), this);
    m_seq.start(m_env.m_agt.m_sqr);       // ← 把 sequence 掛到 sequencer 上跑
    iter++;
  end

  phase.phase_done.set_drain_time(this, 20ns);   // 多留 20ns 給管線裡的殘料流完

  phase.drop_objection(this);             // ← 放手:可以收工了
endtask

忘了 raise_objection 的症狀:模擬在 0ns 就結束, log 幾乎是空的,只有 UVM 的 header 跟 report summary。看起來像「編譯壞了」, 其實是「沒人反對,UVM 就直接收工了」。

set_drain_time 在幹嘛:最後一筆刺激打進 DUT 後, DUT 還要幾拍才吐得出結果。如果 objection 一 drop 就立刻停, 最後幾筆的 out 就永遠等不到,monitor 少抓、scoreboard 少比。 留一段 drain time 讓管線排空。

run_phase 和 main_phase 有什麼不同? run_phase 是總開關;UVM 另外把它切成 12 個 sub-phase: pre_reset → reset → post_reset → pre_configure → configure → post_configure → pre_main → main → post_main → pre_shutdown → shutdown → post_shutdown, 它們跟 run_phase 並行。常見的慣例是: driver / monitor 這種「一直跑的」寫在 run_phase, test 裡「有開始有結束的測試流程」寫在 main_phase。

07TLM:元件之間怎麼連

UVM 元件之間不直接呼叫對方的方法,而是透過 TLM port 連接。 好處是 monitor 不需要知道有誰在聽 —— 你今天多掛一個 coverage collector,monitor 一行都不用改。

A · analysis_imp monitor scoreboard m_mon_port.connect(m_scr_imp) scoreboard 自己實作 write() monitor 一 write(),立刻同步執行 適合:輸出立即可比對 uvm_analysis_imp #(trans_c, scoreboard_c) B · uvm_subscriber monitor cov_coll m_mon_port.connect(m_cov.analysis_export) 內建 analysis_export,不用自己 new 只要 override write() 適合:coverage、log recorder uvm_subscriber #(trans_c) C · analysis_fifo driver/mon FIFO scoreboard connect(m_scr.drv_fifo.analysis_export) write() 先存進 FIFO(不會擋住) scoreboard 在 run_phase 用 get() 取 適合:pipeline / 有 latency 的 DUT uvm_tlm_analysis_fifo #(trans_c)
三種接收端寫法。共同點:發送端都是 uvm_analysis_port #(trans_c), 連線方向永遠是 port.connect(export/imp)。

連線都寫在 connect_phase

一個 port 廣播給兩個接收端env_c
virtual function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  m_agt.m_mon.m_mon_port.connect(m_scr.m_scr_imp);          // → scoreboard
  m_agt.m_mon.m_mon_port.connect(m_cov.analysis_export);    // → coverage
endfunction

uvm_analysis_port 是一對多的: connect 幾次就有幾個訂閱者,write() 呼叫一次,所有訂閱者的 write() 都會被依序執行。而且 write() 是 function, 不消耗時間,所以廣播是「瞬間完成」的。

架構 A(imp)vs 架構 C(FIFO)差在哪

 A:analysis_impC:analysis_fifo
資料來源只有 monitor 一路,同一筆裡就有 in 和 out兩路:driver 送「輸入」、monitor 送「輸出」
比對時機write() 被呼叫的當下立刻比run_phase 裡 get() 到兩邊資料才比
寫在哪virtual function void write(trans_c t)virtual task run_phase(...)(要能 blocking)
要多做的事無driver 要多開一個 uvm_analysis_port,在 item_done() 前 write()
什麼時候該用DUT 簡單、monitor 能一次抓齊 in/outDUT 有深管線、亂序、或 in/out 相隔很多拍
FIFO 版 scoreboard 的核心scoreboard_c(FIFO 版)
uvm_tlm_analysis_fifo #(trans_c) drv_fifo;   // 存 driver 送來的「輸入」
uvm_tlm_analysis_fifo #(trans_c) mon_fifo;   // 存 monitor 送來的「輸出」

virtual task run_phase(uvm_phase phase);
  trans_c drv_tr, mon_tr;
  logic [7:0] expected;

  forever begin
    drv_fifo.get(drv_tr);                 // blocking:沒資料就在這等
    mon_fifo.get(mon_tr);                 // 兩邊各取一筆,自動配對
    expected = reference_model(drv_tr);   // expected = drv_tr.in1 + drv_tr.in2

    if (mon_tr.out === expected) pass_cnt++;
    else `uvm_error("SCR", $sformatf("FAIL: expected %0d, got %0d", expected, mon_tr.out))
  end
endtask

注意 get() 是 task 不是 function。 它會 blocking 等到 FIFO 裡有東西,所以只能寫在 run_phase 裡。 這就是為什麼 FIFO 版的 scoreboard 沒有 write() —— 換成 run_phase 了。

08config_db:把東西從外面遞進來

Driver 需要 virtual interface,但 interface 住在 module top 裡。 Driver 是被 factory 生出來的,沒辦法在建構子裡把 interface 傳進去。 uvm_config_db 就是解這個問題的全域字典:一邊 set,另一邊 get。

set 端:在 top 裡,run_test() 之前module top
initial begin
  uvm_config_db#(virtual adder_intf.drv)::set(null,
      "uvm_test_top.m_env.m_agt.m_drv", "adder_vif", intf);

  uvm_config_db#(virtual adder_intf.mon)::set(null,
      "uvm_test_top.m_env.m_agt.m_mon", "adder_vif", intf);

  run_test("test");        // set 一定要在 run_test 之前
end
get 端:在 build_phase 裡driver_c
virtual adder_intf.drv adder_vif;          // 型態要跟 set 端一模一樣

virtual function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  if (!uvm_config_db#(virtual adder_intf.drv)::get(this, "", "adder_vif", adder_vif))
    `uvm_fatal("DRV", "Unable to get interface adder_vif (drv)")
endfunction

四個參數逐一拆解

位置set 端get 端說明
1nullthis 起點 context。null = 從 uvm_root 起算(因為 module 不在元件樹上)
2"uvm_test_top.m_env.m_agt.m_drv""" 相對於 context 的路徑。get 端已經傳 this,所以路徑留空表示「就是我自己」
3"adder_vif""adder_vif" 欄位名(字串 key),兩邊必須一字不差
4intfadder_vif set 是「要存的值」,get 是「存進哪個變數」(傳參考)

三種讓 get() 失敗的錯法,症狀都一樣是 UVM_FATAL:

  1. 型態不一致 —— set 用 virtual adder_intf、get 用 virtual adder_intf.drv。這在 uvm_config_db 眼裡是兩個完全不同的字典, 存進 A 抽不出 B。這是最常見也最難看出來的一種。
  2. 路徑打錯 —— 例如 m_agt 打成 m_agent, 或忘了最前面的 uvm_test_top.(run_test() 建的 test 實體名固定叫這個)。
  3. key 打錯 —— "adder_vif" 打成 "adder_intf"。

為什麼 set 兩次?因為 driver 和 monitor 要的 modport 不同 (.drv 可寫、.mon 唯讀),型態不同就得各存一份。 這一點很容易漏掉。

路徑可以用萬用字元。"*" 代表任意層,例如 set(null, "*", "adder_vif", intf) 讓所有元件都拿得到 —— 快速但危險,會讓 monitor 也拿到可寫的 handle。

09Factory 與 Override

Factory 是「用字串換出物件」的登記處。它存在的理由只有一個: 讓你不改任何既有程式碼,就能把系統裡某個 class 全面換成另一個。

三個步驟

  1. 註冊 —— `uvm_object_utils(T) / `uvm_component_utils(T)。 沒註冊的 class,factory 根本不知道它存在。
  2. 用 create() 生 —— 而不是 new()。這是攔截點。
  3. 設 override —— 在任何 create() 發生之前。
不改架構,把全系統的 trans_c 換成受限版test::build_phase
// (1) 繼承出一個新的 transaction,加一條約束
class trans_in1_3_c extends trans_c;
  `uvm_object_utils(trans_in1_3_c)          // ★ 一定要註冊,否則 override 不生效

  function new(string name = "");
    super.new(name);
  endfunction

  constraint in1_eq_3 { in1 == 3; }         // 把 in1 釘死成 3
endclass

// (2) 在 test 的 build_phase 最前面設 override
virtual function void build_phase(uvm_phase phase);
  set_type_override_by_type(
    trans_c::get_type(),                    // 原本要生的
    trans_in1_3_c::get_type()               // 實際生出來的
  );

  super.build_phase(phase);                 // ← override 必須在這行之前
  m_env = env_c::type_id::create("m_env", this);

  uvm_factory::get().print();               // (3) 印出 Type Overrides 表驗證
endfunction

設完之後,sequence_c 裡那行 trans_c::type_id::create(...) 一個字都沒改,但生出來的已經是 trans_in1_3_c。 Log 裡每筆 in1 都變成 3,scoreboard 依然全 PASS (功能沒壞),但 cp1 的覆蓋率掉到只剩一個 bin。

Override 失效的兩個原因:

  1. 新 class 沒有 `uvm_object_utils —— factory 查不到這個型別。
  2. 某處用了 new() 而不是 create() —— 那一處會偷偷生出舊型別,你會看到「有些 transaction 被換了、有些沒有」的詭異行為。

兩種 override 範圍:

  • set_type_override_by_type(A, B) —— 全系統所有 A 都換成 B。
  • set_inst_override_by_type(A, B, "路徑") —— 只換指定路徑下的那一個實體。 例如只把某一個 agent 的 driver 換掉,其他保持原樣。

10Functional Coverage:測夠了沒

Scoreboard 回答「有沒有錯」,Coverage 回答「有沒有測到」。 兩者都 PASS 不代表驗完 —— 可能只是根本沒送出會出錯的那種輸入。

covergroup 掛在 subscriber 裡cov_coll_c
class cov_coll_c extends uvm_subscriber #(trans_c);
  `uvm_component_utils(cov_coll_c)

  trans_c m_trans;                    // ★ covergroup 取樣的來源是這個成員變數

  covergroup covgrp;
    option.per_instance = 1;          // 每個實體各自統計

    cp1: coverpoint m_trans.in1 {
      bins in1_min    = {0};          // 單一值
      bins in1_max    = {127};
      bins in1_low[]  = {[1:63]};     // [] = 自動展開成 63 個 bin
      bins in1_high[] = {[64:126]};
    }
    cp2: coverpoint m_trans.in2 { /* 同上 */ }
    cp3: coverpoint m_trans.out {
      bins low  = {[0:127]};
      bins high = {[128:254]};        // 有沒有測到「會進位」的情況
    }
    cp1xcp2: cross cp1, cp2;          // 交叉:兩邊的組合有沒有走遍
  endgroup

  function new(string name = "", uvm_component parent);
    super.new(name, parent);
    covgrp = new();                   // ★ covergroup 必須在 new() 裡實體化
  endfunction

  virtual function void write(trans_c t);   // monitor 每廣播一筆就進來一次
    m_trans = t;                            // 先更新成員變數
    covgrp.sample();                        // 再取樣
  endfunction
endclass

順序不能顛倒。covgrp.sample() 抓的是 m_trans 這個成員變數當下的值。如果先 sample() 再 m_trans = t;, 每次採樣到的都是上一筆資料。

covgrp = new() 要寫在建構子。寫在 build_phase 也可以,但寫漏了就是 null handle,一 sample() 就炸。

用覆蓋率當停止條件

比較好的做法不是「跑固定 N 次」,而是「跑到覆蓋率達標為止」:

覆蓋率驅動的迴圈test::main_phase
while (m_env.m_cov.covgrp.get_coverage() < cov_target && iter < max_iter) begin
  m_seq = sequence_c::type_id::create($sformatf("seq_%0d", iter), this);
  m_seq.start(m_env.m_agt.m_sqr);
  iter++;
end

if (iter >= max_iter)
  `uvm_warning("TEST", "Hit max_iter before reaching coverage target")
API回傳
covgrp.get_coverage()這個型別的整體覆蓋率(所有實體合併)
covgrp.get_inst_coverage(covered, total)這個實體的覆蓋率,並用 output 參數回填「中了幾個 / 共幾個 bin」
covgrp.cp1.get_coverage()單一 coverpoint 的覆蓋率(找出是哪一項拖累)

max_iter 這道保險為什麼一定要有: 如果 constraint 把輸入限制得太死(例如把 in1 釘死成單一值), 覆蓋率永遠到不了目標,沒有 max_iter 就是無窮迴圈。 看到 Hit max_iter 這個 warning,通常代表「約束跟覆蓋率目標互相矛盾」。

11換一個協定,會變什麼

把 DUT 從加法器換成 APB slave 之後,整個 UVM 架構——test / env / agent / sequencer / TLM 連線 / phase / factory ——一行都沒動。 改的只有這四個地方:

要改adderAPB slave
interface 訊號 in1 / in2 / out psel penable pwrite paddr pwdata prdata pready pslverr
transaction 欄位 rand in1, in2;out rand pwrite, paddr, pwdata;prdata, pslverr,再加位址 constraint
driver 時序 一拍打上去就好 兩階段:Setup → Access,還要等 pready
monitor 取樣條件 每一拍都是一筆 只有 psel && penable && pready 成立那一拍才是一筆
clk psel penable pready SETUP ACCESS IDLE IDLE wait state ▲ psel && penable && pready 同時成立的邊緣=transfer 完成,monitor 在此取樣
APB transfer:PSEL 先拉高進 Setup,下一拍 PENABLE 拉高進 Access, 再等 PREADY = 1 才算完成。中間 PADDR / PWRITE / PWDATA 都不能變。
Driver:兩階段時序 + wait statedriver_c(APB)
// Setup phase:PSEL=1、PENABLE=0,放上位址/方向/寫資料
@(posedge apb_vif.clk);
apb_vif.psel    <= 1'b1;
apb_vif.penable <= 1'b0;
apb_vif.pwrite  <= m_trans.pwrite;
apb_vif.paddr   <= m_trans.paddr;
apb_vif.pwdata  <= m_trans.pwdata;

// Access phase:下一拍才把 PENABLE 拉高
@(posedge apb_vif.clk);
apb_vif.penable <= 1'b1;

// Wait state:PREADY=0 就繼續等,期間不能改 PADDR/PWRITE/PWDATA
while (!apb_vif.pready)
  @(posedge apb_vif.clk);
Monitor:只在 transfer 真的完成時取樣monitor_c(APB)
if (apb_vif.psel && apb_vif.penable && apb_vif.pready) begin
  s_pwrite = apb_vif.pwrite;      // (a) 先鎖存刺激欄位
  s_paddr  = apb_vif.paddr;
  s_pwdata = apb_vif.pwdata;

  @(posedge apb_vif.clk);
  #1;                             // (b) 等 DUT 用 NBA 寫的 prdata/pslverr 穩定

  m_trans = trans_c::type_id::create($sformatf("mon_trans_%0d", idx++), this);
  m_trans.pwrite  = s_pwrite;
  m_trans.paddr   = s_paddr;
  m_trans.pwdata  = s_pwdata;
  m_trans.prdata  = apb_vif.prdata;   // (c) 再補上 DUT response
  m_trans.pslverr = apb_vif.pslverr;
end

Assertion 與 Cover Property:UVM 之外的另一層

協定規則除了寫在 driver 裡,還可以用 SVA 直接釘在訊號上, 寫在 module top(不在 class 世界)。任何人違反就立刻報錯 —— 包括你自己的 driver 寫錯的時候。

協定 assertion + cover propertymodule top(APB)
// assertion:PENABLE 只能在「前一拍已經是 Setup phase」之後才拉高
property apb_penable_after_psel;
  @(posedge clk) disable iff (!rst_n)
    $rose(intf.penable) |->
      (intf.psel && $past(intf.psel && !intf.penable));
endproperty

apb_penable_after_psel_a : assert property (apb_penable_after_psel)
  else `uvm_error("APB_ASSERT", "PENABLE asserted without previous APB setup phase")

// cover property:確認測試中真的走過 Setup → Access 這條路
property apb_setup_to_access;
  @(posedge clk) disable iff (!rst_n)
    (intf.psel && !intf.penable) ##1 (intf.psel && intf.penable);
endproperty

apb_setup_to_access_cp : cover property (apb_setup_to_access);

assert 和 cover 是一體兩面: assert property 說「這件事不准發生」, cover property 說「這件事要確認有發生過」。 只寫 assert 的話,一個從來沒被觸發的 assertion 會永遠 PASS —— 看起來很安全,其實你根本沒測到。disable iff (!rst_n) 則是避免 reset 期間的無效狀態被誤判成違規。

12初學者地雷清單

下面每一條都是實務上真的會撞到的狀況。卡住的時候從這裡開始翻。

01改了 .sv 卻只跑 make run,行為完全沒變

症狀
你明明改了程式,log 卻跟上次一模一樣。
原因
make run 只是執行既有的 simv,不會重新編譯。
正解
make(= compile + run)或 make compile 後再 make run。 只有 seed / verbosity / TEST_NAME 這三個是 runtime 參數,改它們才不用重編。

02模擬在 0ns 就結束,log 幾乎空的

症狀
沒有任何 DRV / MON / SCR 訊息,直接跳到 report summary。
原因
忘了 phase.raise_objection(this)。沒人反對,UVM 立刻收工。
正解
耗時的 phase 一開頭 raise、結尾 drop,兩者必須成對。

03Illegal time-consuming statement

症狀
編譯或執行時報這個錯。
原因
在 build_phase / connect_phase / write() 這種 function 裡放了 #10 或 @(posedge clk)。
正解
要等時間,搬到 run_phase / main_phase(task)。

04UVM_FATAL: Unable to get interface

症狀
一開跑就 fatal,元件根本沒建起來。
原因
config_db 的三要素之一不符:型態(virtual adder_intf ≠ virtual adder_intf.drv)、路徑、key 字串。
正解
把 set 端和 get 端兩行並排比對,逐字檢查。八成是 modport 那截漏了。

05Scoreboard 全紅,但 DUT 明明是對的

症狀
每一筆都 FAIL,而且錯得很有規律(像是差一拍)。
原因
Monitor 取樣時序錯了。DUT 有 1 cycle latency, 同一拍抓 in1/in2 和 out,等於把這一筆的輸入配上上一筆的輸出。
正解
先暫存輸入,@(posedge clk) 之後才讀輸出(見第 4 章 monitor 範例)。

06結果每次跑都不一樣,或偶爾抓到舊值

症狀
同一個 seed 換個模擬器版本結果就變了。
原因
取樣 race。DUT 用 <= 在 NBA 區更新, monitor 在 @(posedge clk) 醒來就讀,兩者順序不保證。
正解
取樣前補 #1,或改用 clocking block 的 input skew 明確定義取樣點。

07Driver 寫訊號用了 =

症狀
波形看起來對,但結果時好時壞。
原因
阻塞賦值和 DUT 的 always_ff 在同一個時間點搶更新順序。
正解
驅動 DUT 輸入一律用非阻塞 <=。 class 內部的變數運算才用 =。

08比對用 == 而不是 ===

症狀
訊號是 X 的時候,比對結果既不是 PASS 也不是 FAIL,if 判斷行為詭異。
原因
== 遇到 X/Z 會回傳 X(不是 0 也不是 1)。
正解
四值訊號的比對一律用 ===,它保證回 0 或 1,X 也能正確比出來。

09每圈迴圈跑出兩筆 transaction

症狀
PASS 數是預期的兩倍。
原因
在 sequence 的 body() 裡同時用了 `uvm_do(m_trans) 和手寫的 start_item/finish_item。 `uvm_do 本身就已經包含 create + start_item + randomize + finish_item。
正解
兩種寫法擇一。想加自訂 constraint 或印 log 就用手寫版,圖方便就用 `uvm_do。

10模擬跑很久然後 Hit max_iter

症狀
迭代跑滿上限,覆蓋率還是很低。
原因
constraint 把輸入空間限制死了(例如 in1 < 10 或 in1 == 3), 高位的 bin 永遠打不到。
正解
這其實是正確且有價值的訊號:它告訴你「約束和覆蓋率目標互相矛盾」。 用 cp1.get_coverage() 找出是哪個 coverpoint 卡住。

11Factory override 設了卻沒生效

症狀
uvm_factory::get().print() 看得到 override 表,行為卻沒變。
原因
(a) 新 class 忘了 `uvm_object_utils; (b) 某處用 new() 而不是 type_id::create(); (c) override 設在 create() 之後才生效不了。
正解
override 放在 build_phase 的第一行, 在 super.build_phase() 之前。

12+UVM_TESTNAME=... not found

症狀
UVM_FATAL:Requested test from command line not found。
原因
命令列給的名字,跟 `uvm_component_utils(...) 註冊的 class 名字不一樣(打錯字、或 Makefile 的 TEST_NAME 沒改)。
正解
factory 是用字串查的,名字必須完全一致(大小寫也算)。

13訊息沒印出來

症狀
明明寫了 `uvm_info,log 上找不到。
原因
訊息等級低於當前 verbosity 門檻。寫 UVM_HIGH 的訊息在預設 UVM_MEDIUM 下不會顯示。
正解
make run verbosity=UVM_HIGH(runtime 參數,不用重編)。 重要的統計訊息用 UVM_NONE,保證一定印。

14漏了 `include "uvm_macros.svh"

症狀
一整排 `uvm_info / `uvm_component_utils 都說「未定義」。
原因
import uvm_pkg::*; 只帶進 class library, 巨集要靠 `include 才有。
正解
檔案開頭兩行一起寫,並用 `ifndef UVM_MACROS_SVH 包起來避免重複 include。

15第二筆 transaction 送不出去,像卡死

症狀
只看到第一筆 DRV 訊息,之後就沒動靜了。
原因
driver 的 get_next_item() 沒有對應的 item_done()。 Sequencer 認為上一筆還沒做完,不會給下一筆。
正解
兩者成對出現,中間不能有 return 或 break 跳過。

13執行速查

Make target

指令做什麼改了 .sv 之後夠不夠
make clean砍掉 simv、log、fsdb 等產物—
make compile只編譯,產生 simv✔ 要
make run只執行既有的 simv✘ 不夠
makecompile + run✔ 最保險
make randomcompile 後用隨機 seed 跑✔

Runtime 參數:改這三個不用重編

參數範例作用
seedmake run seed=2 換一組隨機測資。同一個 seed 結果可重現 —— debug 時務必固定 seed
verbositymake run verbosity=UVM_HIGH 調整 log 詳細程度
TEST_NAMEmake run TEST_NAME=test 換要跑的 test class(等同 +UVM_TESTNAME=)

Verbosity 五級

等級數值什麼時候用
UVM_NONE0一定要印。最終統計、PASS/FAIL summary
UVM_LOW100重要里程碑,例如 phase marker、iteration 進度
UVM_MEDIUM200預設門檻。每筆 transaction 的 DRV/MON/SCR 訊息
UVM_HIGH300debug 用的細節,例如每次 coverage 採樣
UVM_DEBUG500最囉唆

規則:訊息等級 ≤ 當前門檻 才會印。所以預設 UVM_MEDIUM 下, NONE/LOW/MEDIUM 會出現,HIGH/DEBUG 被過濾掉。

從 log 的 ID 反查是誰印的

ID來源元件看到它代表
[SEQ]sequence_c產生了一筆隨機刺激
[DRV]driver_c刺激已經打到腳位上
[MON]monitor_c從腳位觀察回一筆完整交易
[SCR]scoreboard_c比對結果 PASS / FAIL
[COV]cov_coll_c採樣了一筆(UVM_HIGH 才看得到)
[TEST]testiteration 進度、覆蓋率摘要
[PHASE]testphase marker(自己加的)

Debug 建議順序:照著 SEQ → DRV → MON → SCR 這條線看。 在哪一步斷掉,問題就在那一段:
沒有 SEQ → sequence 沒 start 或 objection 沒 raise;
有 SEQ 沒 DRV → driver 的 get_next_item 卡住或 vif 沒拿到;
有 DRV 沒 MON → monitor 時序寫錯或卡在 wait();
有 MON 沒 SCR → TLM 沒 connect。

14重點回顧

每章濃縮成一句話,當索引用 —— 想不起來哪一段,從這裡跳回去:

章一句話
01UVM 提供的是可重用、可隨機、自動比對、可量化這四件事
02class 碰不到訊號,interface + virtual interface 是唯一通道;modport 決定誰能寫
03元件排成一棵樹,每層在自己的 build_phase 生小孩
04資料流 8 站:物件 → 訊號 → 物件,轉換發生在 driver 和 monitor
05uvm_object 是資料、uvm_component 是結構,建構子與註冊巨集都不同
06只有 run / main 是 task,其他 phase 不能耗時;objection 決定何時收工
07元件不互相呼叫,一律 port.connect(export);輸出有延遲就改用 FIFO
08config_db 的型別、路徑、key 三者要一字不差
09註冊 → create() → override,三者缺一就換不掉
10scoreboard 說「有沒有錯」,coverage 說「有沒有測到」
11換協定只動 transaction / driver / monitor / interface,架構不動
1215 條常見錯誤,卡住先翻這章
13改了 .sv 一定要重編;沿 SEQ→DRV→MON→SCR 找斷點

如果只記得住五件事

  1. 資料流永遠是 sequence → sequencer → driver → interface → DUT → interface → monitor → analysis port → scoreboard / coverage。卡住的時候先問「斷在哪一站」。
  2. 兩大家族:transaction 系是 uvm_object(new(name) + `uvm_object_utils), 元件系是 uvm_component(new(name, parent) + `uvm_component_utils)。
  3. 一律用 type_id::create(),不用 new()。這是 factory override 的前提。
  4. 只有 run_phase / main_phase 能消耗時間,其他 phase 都是 function。
  5. objection 決定模擬何時結束,忘了 raise 就是 0ns 收工。