為什麼指向式測試會走到死路
在第 2 級你建好了測試平台,並餵給它指向式向量——手挑的輸入,例如「3 加 5,預期得 8」。指向式測試很棒:它好讀、針對特定功能,而且光是動手寫它就逼你去思考。但它們有一個致命弱點:只能測到你想得到的情況。最後出貨才爆的臭蟲,幾乎總是沒人想到的那個——剛好撞上更新週期的連續寫入、在 reset 解除後一個週期才抵達的封包、在 FIFO 只剩一格時恰好進位漣漪的那一刻。
更深層的問題是算術。一個有兩個 32 位元資料輸入的模組,光是單一週期就有 2⁶⁴ ≈ 1.8×10¹⁹ 種輸入組合。再加上內部狀態與多週期序列,合法激勵空間會膨脹到宇宙熱寂之前都列舉不完。如果每個指向式測試要一位工程師花一小時撰寫與覆核,那你等於拿一根茶匙去舀乾整片海洋。
讓機器自己發明激勵
約束隨機驗證的核心想法,是把工作的歸屬翻轉過來。你不再指定*某個值*,而是指定*一個合法值必須遵守的規則*,再由模擬器內的約束求解器在每一筆交易上隨機挑出一個實際值。用一百萬個不同的隨機種子跑同一個模擬一百萬次,你就掃過了一百萬個合法情境,而完全不必親手把它們敲出來。
在 SystemVerilog 中,你把激勵打包成一個交易(transaction)類別:一堆 `rand` 欄位加上 `constraint` 區塊。呼叫 `randomize()` 就是請求求解器替每個 `rand` 欄位指派一個能同時滿足所有作用中約束的值。純粹的 `$random` 會樂呵呵地產生非法操作碼或未對齊位址——這些都是 DUT 本來就不打算處理的;約束則讓每一筆隨機交易都落在合法範圍內,所以一旦失敗就代表真臭蟲,而不是你自己挖的坑。
class BusTxn;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit [3:0] len; // burst length
rand bit is_write;
// --- legality constraints ---
constraint c_align { addr[1:0] == 2'b00; } // word-aligned
constraint c_range { addr inside {[32'h0000_0000 : 32'h0000_FFFF]}; }
constraint c_len { len inside {1, 2, 4, 8}; } // legal burst sizes only
// --- shaping the DISTRIBUTION (not legality) ---
constraint c_mix { is_write dist { 1 := 70, 0 := 30 }; } // 70% writes
constraint c_hotcold{ addr dist { [0:'hFF] := 50, // hammer page 0
['h100:'hFFFF] := 50 }; }
endclass
// In the driver / sequence body:
BusTxn t = new();
assert ( t.randomize() ) // solver fills legal values
else $fatal("constraints unsatisfiable");
drive(t); // send it to the DUT加權分布:把你的隨機性花在哪裡
均勻隨機很少是你真正想要的。如果 `addr` 在 64 K 個位址上均勻分布,那麼短短一段測試裡任何一個位元組被寫兩次的機率微乎其微——然而對同一個位置的連續寫入,正是一致性與資料前遞臭蟲藏身之處。`dist` 運算子讓你能對骰子動手腳:`:=` 給範圍內每個值都套上那個固定權重,而 `:/` 則把一個權重*攤分*到整個範圍。把火力集中在一小塊熱頁上,你就能讓碰撞變得很可能發生,而不是天文數字般罕見。
分布也是你重現真實流量的手段。一台網路交換器看到的大多是小封包,偶爾才來一個巨型訊框;一個記憶體控制器看到的則是時間上聚集成串的突發。用權重把那個形狀編碼進去,你的隨機測試平台就能像真實世界那樣去壓榨設計——卻又會漫遊進入手寫測試永遠到不了的罕見組合。
覆蓋率:隨機究竟有沒有打到東西?
光是無止盡地擲骰子,本身證明不了什麼。一百萬個種子也許只是反覆敲打同樣三個情境,而把一個關鍵模式完全晾在一旁。所以約束隨機唯有在你把它和覆蓋率配對之後,才真正成為一套*方法*——覆蓋率是對「模擬實際操練到了什麼」的量測。它回答每位專案經理遲早會問的問題:*我們做完了嗎?又憑什麼這麼說?* 答案就寫在你的驗證計畫裡,那份清單列出所有在投片前必須被命中的行為。
這裡有兩把不同的尺,把它們搞混是個經典陷阱。[[functional-coverage|功能覆蓋率]]問的是*我們測對了東西嗎?*——這是手寫的意圖。你宣告 `covergroup` 與 `coverpoint`,描述重要的情境:每一種突發長度、每一種讀寫比例、以及「FIFO 已滿」與「進來的寫入」這兩件事的交叉(cross)。[[ic-code-coverage|程式碼覆蓋率]]問的則是*我們操練到那段程式碼了嗎?*——這是自動的,由模擬器擷取:哪些行跑過、哪些分支兩個方向都走過、有限狀態機的哪些狀態與弧線被造訪、哪個表達式翻轉過。
100% code coverage, 60% functional coverage
----------------------------------------------
Every LINE of RTL ran... but you never drove
a write that lands while the FIFO is FULL.
The line for that branch executed (read path),
so code coverage is happy --- yet the bug in
the full+write CORNER is still in there, unseen.
Lesson: code coverage = necessary, not sufficient.
functional coverage encodes the SCENARIOS
your verification plan actually cares about.把迴圈接起來:用覆蓋率引導種子
讓整套做法能收斂的引擎在此。你跑一輪含許多種子的迴歸測試,把每次執行的覆蓋率資料庫合併(merge)成一張全景,再讀出漏洞——那些隨機激勵從未填上的 bin。這些漏洞就是一份待辦清單。也許突發長度 8 從沒和被反壓的匯流排同時出現過;於是你調一個分布權重、加一條約束,或寫一段針對性序列去逼出那個角落。重跑、重新合併、看著曲線往上爬。這個迴授迴圈——隨機向外發散、覆蓋率量測、你朝缺口操舵——正是約束隨機驗證的日常節奏。
用隨機方式重建第 2 級的測試平台
回到第 2 級的測試平台。它的骨架——產生器、驅動器、監視器、記分板——不會變。會變的是*產生器*:以前它讀的是一個固定的指向式向量陣列,現在它改成建構一筆交易、呼叫 `randomize()`、然後送出。記分板依舊預測預期結果並比對,和先前一模一樣——輸入端隨機、檢查端確定。再加上一個由監視器取樣的 covergroup,這個你早已熟悉的測試平台,就搖身一變成了一台自動駕駛的探索機器。
// rung 2: directed --------------------------------
bit [31:0] vecs[] = '{32'h0003, 32'h0005, 32'h00FF};
foreach (vecs[i]) drive(vecs[i]); // 3 hand-picked cases
// rung 3: constrained-random ----------------------
covergroup cg @(posedge clk);
cp_len : coverpoint t.len { bins b[] = {1,2,4,8}; }
cp_dir : coverpoint t.is_write;
x_full : cross cp_dir, fifo_full; // the dangerous corner
endgroup
cg cov = new();
repeat (100_000) begin // 100k random cases
BusTxn t = new();
assert ( t.randomize() ); // legal by construction
drive(t);
cov.sample(); // record what we hit
end
$display("functional coverage = %0.1f%%", cov.get_coverage());這就是整個心態的轉變。你不再描述*例子*,而開始描述*合法行為的空間,外加值得量測的情境*。模擬器負責不知疲倦地漫遊;覆蓋率負責記分;而你則把稀缺的人類注意力,從敲打向量轉移到那個價值高得多的問題上:對這個設計而言,「有趣」究竟是什麼意思。