JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

約束隨機激勵與覆蓋率

一顆現代 CPU 的合法輸入序列數量,比整個銀河系的原子還多——你不可能為每一種情況親手寫一個測試。本篇將說明[[constrained-random|約束隨機]]激勵如何讓模擬器替你產生數百萬個合法情境,以及[[functional-coverage|功能覆蓋率]]如何告訴你究竟摸到了哪些角落,讓你知道何時可以收手。

為什麼指向式測試會走到死路

在第 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
一個可隨機化的匯流排交易。`c_align`/`c_range`/`c_len` 強制**合法性**;`c_mix`/`c_hotcold` 則用加權的 `dist` 塑形**分布**。

加權分布:把你的隨機性花在哪裡

均勻隨機很少是你真正想要的。如果 `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.
為何兩者都需要。100% 程式碼覆蓋率,可以和一個沒測到的功能角落並存;功能覆蓋率才是抓出那道缺口的東西。

把迴圈接起來:用覆蓋率引導種子

讓整套做法能收斂的引擎在此。你跑一輪含許多種子的迴歸測試,把每次執行的覆蓋率資料庫合併(merge)成一張全景,再讀出漏洞——那些隨機激勵從未填上的 bin。這些漏洞就是一份待辦清單。也許突發長度 8 從沒和被反壓的匯流排同時出現過;於是你調一個分布權重、加一條約束,或寫一段針對性序列去逼出那個角落。重跑、重新合併、看著曲線往上爬。這個迴授迴圈——隨機向外發散、覆蓋率量測、你朝缺口操舵——正是約束隨機驗證的日常節奏。

  1. 計畫——把驗證計畫翻譯成具體的 covergroup 與 coverpoint;這就定義了「做完」。
  2. 產生——用許多隨機種子跑迴歸;每個種子都在模擬中探索一條新的合法路徑。
  3. 量測——取樣功能覆蓋率並自動收集程式碼覆蓋率;把所有種子合併成一個資料庫。
  4. 分析——依重要性替漏洞排序;一個怎麼都填不上的空 bin,通常意味著約束太緊,或是隨機靠運氣到不了的情境。
  5. 操舵——調整權重、放鬆/新增約束,或寫一段指向式隨機序列去打那個漏洞;接著回到「產生」再繞一圈。
  6. 收斂——當每個必中 bin 都填滿、迴歸全綠時,你就達成了覆蓋率收斂(coverage closure)——這才是「做完了嗎?」站得住腳的答案。

用隨機方式重建第 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());
同一個測試平台,換了產生器。三個手寫向量變成 100,000 筆合法隨機交易,並由 covergroup 記錄各個角落——包括「寫入 × FIFO 已滿」的交叉。

這就是整個心態的轉變。你不再描述*例子*,而開始描述*合法行為的空間,外加值得量測的情境*。模擬器負責不知疲倦地漫遊;覆蓋率負責記分;而你則把稀缺的人類注意力,從敲打向量轉移到那個價值高得多的問題上:對這個設計而言,「有趣」究竟是什麼意思。