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

沒驗證過,就是壞的

晶片專案的大部分工作並不是設計晶片,而是證明晶片是對的。一旦設計走到了流片,一個錯誤就要賠上幾個月和幾百萬,而且事後沒有修補檔可推。所以團隊會把大部分精力花在驗證上:搭一個能驅動設計運行的測試平台,從手寫測試走向約束隨機激勵,用覆蓋率來判斷驗證是否做完,用斷言在錯誤發生的瞬間抓住它,到了高階再上形式驗證和 UVM。本指南會用一些小而可執行的例子帶你走完這條路線。

為什麼驗證佔了大部分工作量

有個數字會讓剛入行的人吃驚:在一款正經設計上,驗證通常佔總工作量的 60–70%——投入的工程師、算力和日曆時間都比設計本身還多。為什麼這麼懸殊?因為硬體不留情面。一個網頁應用出了 bug,你當天下午就能推個修復上去。一顆晶片出了 bug,修復的代價是一套新光罩、一次新流片,外加你人生裡的一個季度。

所以這份活兒不是「讓它跑通一次」,而是要用證據說服你自己:對矽晶片這輩子會遇到的每一種輸入,它都是對的。你不可能把輸入全試一遍——哪怕只是一個 32 位元加法器,也有兩個 32 位元運算元,也就是 2^64 種輸入組合——所以驗證其實是一門不靠窮舉卻令人信服地周全的藝術:選對激勵,盯對該看的東西,再衡量你究竟驅動了設計的多少部分。

測試平台

測試平台是硬體的測試夾具——但它本身不是晶片的一部分。把它想成電子實驗室裡的一張試驗台:一套夾住你電路、給它餵訊號、再盯著回來什麼的裝置。在 Verilog 裡,它是一個沒有埠的模組——它坐在設計外面,把待測對象(DUT)實體化進來,驅動它的輸入,再檢查它的輸出。

比起「光看波形」,關鍵的升級在於讓測試平台自己會查:它事先就知道正確答案,一旦現實跟它不符就當場喊出來。那一句 `$error` 就是全部的關鍵——它把「我用肉眼掃了一遍,看著還行」變成了「機器把每一個結果都和黃金期望值比對過,沒發現一個錯的」。

module tb;
  logic [3:0] a, b;
  logic [4:0] sum;
  adder dut (.a(a), .b(b), .sum(sum));   // DUT: the design under test

  initial begin
    a = 4'd7; b = 4'd9;                   // drive inputs
    #1;                                   // let combinational logic settle
    if (sum !== a + b)                    // compare against expected
      $error("bad sum: %0d + %0d gave %0d", a, b, sum);
    $finish;
  end
endmodule
一個最小的自檢測試平台:將 DUT 實體化,驅動輸入,如果輸出跟期望的和不一致就報 $error。

定向測試 vs 約束隨機

上面那個測試平台是一個定向測試:是你這個工程師親手挑了 7 + 9,因為你直覺它要緊。定向測試精確、可讀,對那些你叫得出名字的情形再合適不過——重置行為、溢位邊界、那個你心裡有數會咬人的角落。它的弱點也正是它的本性:你只能找到那些你早就想到要去找的 bug。

約束隨機把這件事反了過來。你不再一個個去點名情形,而是描述輸入的合法空間,讓工具朝設計扔出成千上萬條「隨機但合法」的激勵。約束讓隨機性保持理智——一個合法的運算碼、一個在範圍內的位址——而隨機性則能觸及你這輩子也不會手挑出來的角落。讓它跑一整夜,它會探索出沒有哪個人會去敲出來的組合。

class packet;
  rand bit [7:0] addr;
  rand bit [3:0] len;
  constraint legal { addr < 8'd200; len inside {[1:8]}; }  // stay in the legal space
endclass
// call randomize() in a loop -> thousands of valid, varied stimuli
一段約束隨機激勵:addr 和 len 都是隨機生成的,但約束保證每一個生成出來的封包都合法。

覆蓋率:我們到底測了多少?

約束隨機帶出一個讓人不安的問題:既然工具是在亂扔隨機輸入,你怎麼知道它真的試到了那些要緊的情形? 沒準十萬個封包裡,長度恰好為 8 的一次都沒碰上。覆蓋率就是答案——它是一塊記分牌,告訴你設計被你實際驅動了多少,把「我們跑了好多測試」變成「我們命中了自己聲明在意的場景裡的 94%」。

它有兩種口味。程式碼覆蓋率是自動的,問的是「每一行、每個分支、每次翻轉都被驅動到了嗎?」——必要,但淺。[[functional-coverage|功能覆蓋率]]才是真正撐得起價值的那個:是*你*來寫下覆蓋點,給那些要緊的情形點名——每個運算碼都見過了、FIFO 既碰到過空也碰到過滿、每個合法的長度值——再由工具統計究竟哪些真的發生了。它回答的是那個真問題:我們做完了嗎?

covergroup cg @(posedge clk);
  coverpoint len    { bins all[] = {[1:8]}; }   // did we see every length 1..8?
  coverpoint state;                              // did we visit every FSM state?
endgroup
一個功能覆蓋群組:覆蓋點記錄這一輪運行裡實際到達了哪些長度、哪些狀態。

斷言:當場抓住 bug

自檢測試平台是在輸出端抓 bug——而且是等壞值傳播出來之後才抓到。斷言則是在案發現場抓它:它是一條直接嵌進設計裡的規則,說「這件事必須永遠為真」,一旦被違反就在那一瞬間觸發,徑直指向出問題的訊號和週期。

它們在跨越時間的規則上最出彩。普通的 `if` 只檢查某一個瞬間;一條 SVA(SystemVerilog 斷言)檢查的是跨越多個週期的關係——「一個請求之後,三個時脈之內必須有應答」「這個獨熱訊號永遠不會有兩位同時為 1」。那個 `|->` 讀作蘊含:左邊一旦發生,右邊就必須跟著發生。

// after every request, an ack must arrive within 1 to 3 cycles
property req_gets_ack;
  @(posedge clk) req |-> ##[1:3] ack;
endproperty
assert property (req_gets_ack);
一條 SVA 斷言:只要 req 為高,ack 就必須在 1 到 3 個時脈週期內跟上,否則斷言失敗,並指出出錯的確切週期。

瞥一眼形式驗證與 UVM

模擬哪怕跑上幾十億個隨機週期,測到的也只是你恰好生成出來的那些輸入——它是在對一片汪洋做取樣形式驗證走的是另一條路:它把你的斷言交給一個數學引擎,讓引擎證明它們對*所有*可能的輸入都成立,或者遞給你一個具體的反例。沒有激勵,沒有覆蓋率盲區——而是一份證明。它撐不到整顆晶片的規模,但在一個棘手的模組上(一個仲裁器、一個 FIFO、一個協定的角落),它帶來的差別就是「我們沒看到它出錯」和「它不可能出錯」之間的差別。

隨著設計變大,臨時拼湊的測試平台會撐不住。UVM(通用驗證方法學)是業界用來搭建大型測試平台的標準框架——可重用的元件(driver、monitor、scoreboard)、一個用來生成約束隨機流量的 sequencer,以及一個覆蓋率模型,全都按照每個驗證團隊都認得的方式接在一起。把它想成那套鷹架,讓一個測試平台從一個工程師的腳本,長成一整個團隊可以維護多年的東西。