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

序列、覆蓋率模型與覆蓋率收斂

一個布滿 agent、driver 與 scoreboard 的測試平台,就像一條精心鋪設卻沒有賽車的賽道。序列就是那些賽車——它們編寫出連續突發、錯誤注入與邊角案例,真正去操練你的設計。覆蓋率則是圈數計數器,告訴你哪些彎道你從未開過。本篇教你如何撰寫豐富的序列,再把覆蓋率一路收斂到簽核。

賽道需要賽車

在第 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
先填滿再排空的序列。`start_item`/`finish_item` 把每筆 transaction 交給 sequencer→driver;`with {}` 子句在 transaction 自身的約束之上,再疊一層情境意圖。

讓設計者冒汗的序列

臭蟲不會躲在輸入空間平易近人的中段,它們潛伏在邊緣。有三類序列值得精通,因為它們會刻意逼近那些邊緣。連續背靠背(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
一條序列同時施加兩種壓力:零間隔的時序,加上權重 5% 的錯誤注入。`dist` 運算子讓隨機抽樣偏斜,卻不抹去那條罕見但合法的順利路徑。

序列也能「組合」。一個 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
一個含兩個 coverpoint 與一個 cross 的 covergroup。cross 才是威力所在:2 種類型 × 3 個長度 bin = 6 種回歸測試必須命中的組合,而不只是 2+3 個邊際情況。

每個 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%
程式碼覆蓋率由工具從 RTL 自動產生;功能覆蓋率由你依計畫親手撰寫。兩者都需要——任何一方都無法取代另一方。

兩者以一種精確的方式互補。程式碼覆蓋率 100% 但功能覆蓋率偏低,意味你跑了大量程式碼卻從未建立真正重要的情境——你撥動了每一條線,卻從未填滿 FIFO。功能覆蓋率 100% 卻有程式碼覆蓋率破洞,意味存在一塊你的計畫從未想過的 RTL——往往是死碼、被遺忘的模式,或計畫本身漏列的項目。每個破洞都是你在簽核前必須回答的問題;覆蓋率不會告訴你設計正確,它告訴你的是「你還有哪裡沒看過」。

收斂覆蓋率:簽核迴圈

覆蓋率收斂 是一段反覆的苦工,帶你從「測試能跑」走到「我們有足夠把握可以下線(tape out)」。它鮮少是一條直線。你發動一輪數百種子的回歸測試,把它們的覆蓋率資料庫合併成單一全貌,研究破洞,再針對每個破洞決定:是補刺激、修模型,還是正式豁免它。然後再回歸一次。曲線起初上升飛快,接著攤平成一條漫長而頑強的尾巴,最後那幾個百分點的 bin,比前面九十個百分點還更費力。

  1. 回歸測試。 用許多隨機種子發動測試套件——不同種子會驅使 約束隨機 刺激走不同路徑,因此每次執行命中不同的 bin。
  2. 合併。 把每次執行的覆蓋率資料庫整併成一份統一報告。只要「任一」種子命中某個 bin,該 bin 就算命中。
  3. 分類破洞。 依重要性排序未覆蓋的 bin。把它們分群:這是一個破洞,還是某個缺失情境的五十個症狀?
  4. 逐一處理破洞——撰寫針對性序列、收緊或放寬某個約束、修正錯誤的 bin 定義,或為不可達的 bin 附上書面理由予以「豁免(waive)」。
  5. 重新回歸與重新合併。 確認破洞已補上,且沒有開出新的破洞。重複此循環,直到達成計畫的簽核目標。

有兩招能把你從那條頑強尾巴救出來。第一,針對性序列——當隨機刺激一直漏掉某個 bin,別再空等,寫一條直接逼出該條件的有向序列(或收緊約束,讓求解器偏向它)。第二,豁免(waiver)——有些 bin 是真正不可達的:一個冗餘卻不合法的狀態、一個正確設計永遠碰不到的防禦性 `default`。別永無止盡地追它們;附上書面理由排除掉,好讓下一位審查者信任那個綠色數字。至於那些可以「證明」而非「命中」的性質,形式性質驗證 能補上隨機模擬永遠達不到的覆蓋率。

把它們串起來

退一步看,整個流程其實是一個回饋迴圈。驗證計畫 宣告意圖;coverpoint 把意圖化為可量測的目標;經由 sequencer 驅動的 序列 產生刺激;monitor 同時為 scoreboard 的核對「以及」覆蓋率取樣。合併後的覆蓋率報告指出破洞,而破洞告訴你接下來該寫哪些序列。如此周而復始,直到計畫一片翠綠且每處都站得住腳。

這正是為何現代驗證能吞掉一顆晶片總工程量的一半以上。DUT 是有限的,但它的「行為」卻多到天文數字,而你是拿光罩成本在賭——賭你已探索過那些真正重要的行為。序列是你探索的方式;覆蓋率是你證明自己探索過的方式;收斂則是那份紀律,把一堆通過的測試,化為一個簽署過、站得住腳的主張:這個設計已準備好 下線