賽道需要賽車
在第 5 級你搭建了 UVM 環境的靜態骨架:負責擺動接腳的 driver、負責觀察接腳的 monitor、負責核對結果的 scoreboard,全都包在一個 agent 裡。通電啟動後……什麼都沒發生。driver 在空轉,等著有人遞給它要送的東西。那個東西就是一筆 transaction(交易封包),而源源不絕產生 transaction 串流的,正是 序列。骨架是賽道,序列才是你真正開上去跑的賽車。
序列的職責是決定要丟什麼樣的「情境」給 DUT(待測設計)。最陽春的序列只送一筆隨機讀取。好的序列則捕捉驗證工程師真正擔心的事:「不斷對 FIFO 寫入直到滿溢,再把它讀到見底」,或「開始一段突發傳輸,然後在它腳下抽掉 reset」。每一個都是有名字、可重用的物件,你把它交給 sequencer,由它在彼此競爭的序列之間仲裁,再一次一筆地餵給 driver。
class write_read_seq extends uvm_sequence #(bus_txn);
`uvm_object_utils(write_read_seq)
rand int unsigned num;
constraint c_num { num inside {[8:16]}; }
task body();
bus_txn t;
// Phase 1: fill — back-to-back writes, no idle gaps
repeat (num) begin
t = bus_txn::type_id::create("t");
start_item(t);
assert(t.randomize() with { kind == WRITE; });
finish_item(t);
end
// Phase 2: drain — read everything back
repeat (num) begin
t = bus_txn::type_id::create("t");
start_item(t);
assert(t.randomize() with { kind == READ; });
finish_item(t);
end
endtask
endclass讓設計者冒汗的序列
臭蟲不會躲在輸入空間平易近人的中段,它們潛伏在邊緣。有三類序列值得精通,因為它們會刻意逼近那些邊緣。連續背靠背(back-to-back) 序列抽掉每個空閒週期,讓 transaction 互相碰撞、讓管線永遠保持滿載——這正是握手與停滯邏輯崩潰之處。錯誤注入(error-injection) 序列刻意送出畸形流量——同位元錯誤、未對齊位址、協定不合法的命令——以檢查 DUT 是否正確「回應」而非卡死。邊角案例(corner-case) 序列鎖定算術極值:reset 後的第一筆 transaction、剛好滿減一的 FIFO、計到最大值才回繞的計數器。
class b2b_error_seq extends uvm_sequence #(bus_txn);
`uvm_object_utils(b2b_error_seq)
task body();
bus_txn t;
repeat (200) begin
t = bus_txn::type_id::create("t");
start_item(t);
// 5% of traffic carries a bad-parity corner case,
// and inter_gap==0 forces back-to-back timing.
assert(t.randomize() with {
inter_gap == 0;
bad_parity dist { 0 := 95, 1 := 5 };
});
finish_item(t);
end
endtask
endclass序列也能「組合」。一個 virtual sequence(虛擬序列)可橫跨多個 agent 編排數個子序列——例如在一個 AXI 埠上跑流量的同時,由 config 序列在 APB 埠上重新編程暫存器——以重現真實 SoC 那種雜亂的並行性。這種分層正是第 5 級結構的回報:因為每個 agent 彼此獨立,你可以隨意混搭刺激,而不必重接測試平台的線路。
覆蓋率:你的測試計畫的計分板
約束隨機刺激很強大,卻是盲目的:它會欣然送出一百萬筆 transaction,卻可能一次都沒把 FIFO 完全填滿,而你毫不知情。你需要一把量尺。功能覆蓋率 就是那把量尺——它把你 驗證計畫 裡的文字敘述,轉成可執行、可計數的目標。計畫說「驗證每個通道上的每種突發長度」,一個 covergroup(覆蓋群組) 搭配 coverpoint(覆蓋點) 就會記錄模擬過程中,那些組合究竟有沒有真的發生過。
covergroup bus_cg with function sample(bus_txn t);
cp_kind : coverpoint t.kind { bins rd = {READ}; bins wr = {WRITE}; }
cp_len : coverpoint t.burst_len {
bins single = {1};
bins small = {[2:7]};
bins max = {8}; // the corner we care about
}
// Cross: did we see a MAX-length READ *and* a MAX-length WRITE?
x_kind_len : cross cp_kind, cp_len;
endgroup每個 bin 是一個桶子,當匹配的 transaction 被取樣時模擬器就遞增它;覆蓋率就是「至少被命中一次」的 bin 所佔的比例。cross 才是真正意圖所在——臭蟲偏愛的是組合(緊接 reset 之後的最大長度讀取),遠勝於孤立的單一值。請從 monitor 取樣你的 covergroup,而不是從 driver:你要量的是 DUT「實際在線上看到」的東西,包含背壓與重排,而非你「打算」送出的東西。
兩種覆蓋率,兩個問題
功能覆蓋率回答的是「我有沒有操練到所有我『打算』操練的東西?」——但它只能量到你記得去寫的目標。程式碼覆蓋率 回答的是互補的問題:「我有沒有操練到設計者『實際寫下』的每一行、每個分支、每個運算式與每個狀態?」模擬器會自動對 RTL 進行儀表化,因此能逮到你從沒想過要寫進計畫的死角:一個從未執行的 `else` 分支、一個從未進入的 FSM 狀態、一個從未被選中的 case 項目。
Coverage type What it measures Typical signoff --------------- ----------------------------- --------------- Line / statement every RTL line executed ~100% Branch both arms of every if/case ~100% Toggle every bit toggled 0->1 and 1->0 ~90-100% FSM every state + legal transition 100% states Expression/cond each sub-condition's true/false ~95-100%+ --------------- ----------------------------- --------------- Functional coverpoints/bins/crosses hit 100% of plan Assertion each cover-property triggered 100%
兩者以一種精確的方式互補。程式碼覆蓋率 100% 但功能覆蓋率偏低,意味你跑了大量程式碼卻從未建立真正重要的情境——你撥動了每一條線,卻從未填滿 FIFO。功能覆蓋率 100% 卻有程式碼覆蓋率破洞,意味存在一塊你的計畫從未想過的 RTL——往往是死碼、被遺忘的模式,或計畫本身漏列的項目。每個破洞都是你在簽核前必須回答的問題;覆蓋率不會告訴你設計正確,它告訴你的是「你還有哪裡沒看過」。
收斂覆蓋率:簽核迴圈
覆蓋率收斂 是一段反覆的苦工,帶你從「測試能跑」走到「我們有足夠把握可以下線(tape out)」。它鮮少是一條直線。你發動一輪數百種子的回歸測試,把它們的覆蓋率資料庫合併成單一全貌,研究破洞,再針對每個破洞決定:是補刺激、修模型,還是正式豁免它。然後再回歸一次。曲線起初上升飛快,接著攤平成一條漫長而頑強的尾巴,最後那幾個百分點的 bin,比前面九十個百分點還更費力。
- 回歸測試。 用許多隨機種子發動測試套件——不同種子會驅使 約束隨機 刺激走不同路徑,因此每次執行命中不同的 bin。
- 合併。 把每次執行的覆蓋率資料庫整併成一份統一報告。只要「任一」種子命中某個 bin,該 bin 就算命中。
- 分類破洞。 依重要性排序未覆蓋的 bin。把它們分群:這是一個破洞,還是某個缺失情境的五十個症狀?
- 逐一處理破洞——撰寫針對性序列、收緊或放寬某個約束、修正錯誤的 bin 定義,或為不可達的 bin 附上書面理由予以「豁免(waive)」。
- 重新回歸與重新合併。 確認破洞已補上,且沒有開出新的破洞。重複此循環,直到達成計畫的簽核目標。
有兩招能把你從那條頑強尾巴救出來。第一,針對性序列——當隨機刺激一直漏掉某個 bin,別再空等,寫一條直接逼出該條件的有向序列(或收緊約束,讓求解器偏向它)。第二,豁免(waiver)——有些 bin 是真正不可達的:一個冗餘卻不合法的狀態、一個正確設計永遠碰不到的防禦性 `default`。別永無止盡地追它們;附上書面理由排除掉,好讓下一位審查者信任那個綠色數字。至於那些可以「證明」而非「命中」的性質,形式性質驗證 能補上隨機模擬永遠達不到的覆蓋率。
把它們串起來
退一步看,整個流程其實是一個回饋迴圈。驗證計畫 宣告意圖;coverpoint 把意圖化為可量測的目標;經由 sequencer 驅動的 序列 產生刺激;monitor 同時為 scoreboard 的核對「以及」覆蓋率取樣。合併後的覆蓋率報告指出破洞,而破洞告訴你接下來該寫哪些序列。如此周而復始,直到計畫一片翠綠且每處都站得住腳。
這正是為何現代驗證能吞掉一顆晶片總工程量的一半以上。DUT 是有限的,但它的「行為」卻多到天文數字,而你是拿光罩成本在賭——賭你已探索過那些真正重要的行為。序列是你探索的方式;覆蓋率是你證明自己探索過的方式;收斂則是那份紀律,把一堆通過的測試,化為一個簽署過、站得住腳的主張:這個設計已準備好 下線。