01為什麼需要 UVM
最小的 UVM testbench 只有三十行。它做的事情就是「印一行字」:
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 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 就是方向權限表。同一組線,三個人看到的方向不同:
modport dut:in1/in2是 input、out是 output —— 站在 DUT 的角度。modport drv:in1/in2變成 output —— Driver 是「寫的人」。modport mon:全部都是 input —— 這是刻意的。Monitor 只准看, 編譯器會擋住任何想寫訊號的程式碼,避免 monitor 不小心污染測試。
初學者最常卡的地方: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 裡把小孩生出來。
driver 與 monitor 各自握著一份 virtual interface 才能跨過去。
每一層負責什麼
| 層 | 職責 | 可不可以換掉 |
|---|---|---|
test | 決定「這次要跑什麼」:跑幾筆、目標覆蓋率、要不要 override | 每個測試情境一個 test,最常被換 |
env | 把 agent / scoreboard / coverage 兜起來並接線 | 整包重用,換 DUT 才動 |
agent | 一個協定介面的完整代理:產生 + 驅動 + 觀察 | 一種協定一個 agent(APB agent、AXI agent…) |
sequencer | sequence 與 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 裡加上這個判斷:
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 站。
逐站說明:資料此刻在哪、是什麼型態
| 站 | 發生什麼 | 資料此刻的身分 |
|---|---|---|
| ① 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:轉換的一半
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:轉換的另一半(也是最容易寫錯時序的地方)
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:資料的容器
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
三個設計決定值得注意:
out沒有rand:輸出是「觀察到的結果」不是「我們決定的刺激」。 加了rand會讓 randomize 亂改它,反而蓋掉 monitor 抓回來的值。- 位寬
[6:0] + [6:0] → [7:0]:兩個 7-bit 最大 127+127 = 254, 需要 8 bit 才裝得下。刻意留一個 bit 避免溢位,讓 scoreboard 的expected = in1 + in2跟 DUT 一致。如果out只給 7 bit, 兩邊都會截斷,測試「會過」但你就測不到進位了。 logic而不是bit:logic是四值(0/1/X/Z),bit是二值(0/1)。用logic才能看見「這條線根本沒被驅動」(X) 這種 bug;用bit的話未驅動會顯示成 0,bug 被藏起來。
`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 或 1 | scoreboard 比對、等 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 會在對的時間點自動叫它。
`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 收工。
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 一行都不用改。
uvm_analysis_port #(trans_c),
連線方向永遠是 port.connect(export/imp)。連線都寫在 connect_phase
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_imp | C: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/out | DUT 有深管線、亂序、或 in/out 相隔很多拍 |
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。
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
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 端 | 說明 |
|---|---|---|---|
| 1 | null | this |
起點 context。null = 從 uvm_root 起算(因為 module 不在元件樹上) |
| 2 | "uvm_test_top.m_env.m_agt.m_drv" | "" |
相對於 context 的路徑。get 端已經傳 this,所以路徑留空表示「就是我自己」 |
| 3 | "adder_vif" | "adder_vif" |
欄位名(字串 key),兩邊必須一字不差 |
| 4 | intf | adder_vif |
set 是「要存的值」,get 是「存進哪個變數」(傳參考) |
三種讓 get() 失敗的錯法,症狀都一樣是 UVM_FATAL:
- 型態不一致 —— set 用
virtual adder_intf、get 用virtual adder_intf.drv。這在uvm_config_db眼裡是兩個完全不同的字典, 存進 A 抽不出 B。這是最常見也最難看出來的一種。 - 路徑打錯 —— 例如
m_agt打成m_agent, 或忘了最前面的uvm_test_top.(run_test()建的 test 實體名固定叫這個)。 - key 打錯 ——
"adder_vif"打成"adder_intf"。
為什麼 set 兩次?因為 driver 和 monitor 要的 modport 不同
(.drv 可寫、.mon 唯讀),型態不同就得各存一份。
這一點很容易漏掉。
路徑可以用萬用字元。"*" 代表任意層,例如
set(null, "*", "adder_vif", intf) 讓所有元件都拿得到 ——
快速但危險,會讓 monitor 也拿到可寫的 handle。
09Factory 與 Override
Factory 是「用字串換出物件」的登記處。它存在的理由只有一個: 讓你不改任何既有程式碼,就能把系統裡某個 class 全面換成另一個。
三個步驟
- 註冊 ——
`uvm_object_utils(T)/`uvm_component_utils(T)。 沒註冊的 class,factory 根本不知道它存在。 - 用
create()生 —— 而不是new()。這是攔截點。 - 設 override —— 在任何
create()發生之前。
// (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 失效的兩個原因:
- 新 class 沒有
`uvm_object_utils—— factory 查不到這個型別。 - 某處用了
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 不代表驗完 —— 可能只是根本沒送出會出錯的那種輸入。
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 次」,而是「跑到覆蓋率達標為止」:
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 ——一行都沒動。 改的只有這四個地方:
| 要改 | adder | APB 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 成立那一拍才是一筆 |
// 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);
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: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 的inputskew 明確定義取樣點。
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 | ✘ 不夠 |
make | compile + run | ✔ 最保險 |
make random | compile 後用隨機 seed 跑 | ✔ |
Runtime 參數:改這三個不用重編
| 參數 | 範例 | 作用 |
|---|---|---|
seed | make run seed=2 |
換一組隨機測資。同一個 seed 結果可重現 —— debug 時務必固定 seed |
verbosity | make run verbosity=UVM_HIGH |
調整 log 詳細程度 |
TEST_NAME | make run TEST_NAME=test |
換要跑的 test class(等同 +UVM_TESTNAME=) |
Verbosity 五級
| 等級 | 數值 | 什麼時候用 |
|---|---|---|
UVM_NONE | 0 | 一定要印。最終統計、PASS/FAIL summary |
UVM_LOW | 100 | 重要里程碑,例如 phase marker、iteration 進度 |
UVM_MEDIUM | 200 | 預設門檻。每筆 transaction 的 DRV/MON/SCR 訊息 |
UVM_HIGH | 300 | debug 用的細節,例如每次 coverage 採樣 |
UVM_DEBUG | 500 | 最囉唆 |
規則:訊息等級 ≤ 當前門檻 才會印。所以預設 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] | test | iteration 進度、覆蓋率摘要 |
[PHASE] | test | phase 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重點回顧
每章濃縮成一句話,當索引用 —— 想不起來哪一段,從這裡跳回去:
| 章 | 一句話 |
|---|---|
| 01 | UVM 提供的是可重用、可隨機、自動比對、可量化這四件事 |
| 02 | class 碰不到訊號,interface + virtual interface 是唯一通道;modport 決定誰能寫 |
| 03 | 元件排成一棵樹,每層在自己的 build_phase 生小孩 |
| 04 | 資料流 8 站:物件 → 訊號 → 物件,轉換發生在 driver 和 monitor |
| 05 | uvm_object 是資料、uvm_component 是結構,建構子與註冊巨集都不同 |
| 06 | 只有 run / main 是 task,其他 phase 不能耗時;objection 決定何時收工 |
| 07 | 元件不互相呼叫,一律 port.connect(export);輸出有延遲就改用 FIFO |
| 08 | config_db 的型別、路徑、key 三者要一字不差 |
| 09 | 註冊 → create() → override,三者缺一就換不掉 |
| 10 | scoreboard 說「有沒有錯」,coverage 說「有沒有測到」 |
| 11 | 換協定只動 transaction / driver / monitor / interface,架構不動 |
| 12 | 15 條常見錯誤,卡住先翻這章 |
| 13 | 改了 .sv 一定要重編;沿 SEQ→DRV→MON→SCR 找斷點 |
如果只記得住五件事
- 資料流永遠是 sequence → sequencer → driver → interface → DUT → interface → monitor → analysis port → scoreboard / coverage。卡住的時候先問「斷在哪一站」。
- 兩大家族:transaction 系是
uvm_object(new(name)+`uvm_object_utils), 元件系是uvm_component(new(name, parent)+`uvm_component_utils)。 - 一律用
type_id::create(),不用new()。這是 factory override 的前提。 - 只有
run_phase/main_phase能消耗時間,其他 phase 都是 function。 - objection 決定模擬何時結束,忘了 raise 就是 0ns 收工。