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

為什麼要驗證:在矽晶圓之前抓出錯誤

現代晶片沒有地方藏自己的錯誤。一旦被蝕刻進矽晶圓,單一個邏輯錯誤就可能要花上數個月的時程與數百萬美元才能修正。本篇說明為什麼**驗證**——在[[tapeout|流片]]之前很久,就在模擬裡證明設計正確——會佔掉打造一顆晶片整整一半的工作量,以及一座[[testbench|測試平台]]與一份[[ic-verification-plan|驗證計畫]]如何把「看起來會動」變成「我們檢查過了」。

那個你無法熱修的錯誤

軟體有一張令人安心的安全網:出了 bug,推一個修補檔。你的手機半夜下載新版本,錯誤就消失了。晶片沒有這種網。當一個設計終於流片——送到晶圓代工廠去製造——邏輯就被一層金屬、一層矽地逐層凍結成實體,事後沒有任何辦法可以編輯。電晶體在哪就在哪。導線往哪走就往哪走。如果第 4,000,000 號閘本該是個 AND,你卻把它接成 OR,那麼晶圓上百萬顆晶片,每一顆都以完全相同的方式出錯。

修正它意味著一次重做(respin):改設計、製作一整套全新的光罩、再讓晶圓跑一次代工廠。在最先進的製程節點,光是一套光罩就可能要花上數千萬美元,而在你拿到新矽晶圓之前,往返晶圓廠就要兩到三個月。現在再乘上現實——錯誤總是成群結隊,而一個在上市後才被發現的嚴重錯誤可能引發召回。1994 年 Intel Pentium 的 FDIV 錯誤,是除法查表中的一個瑕疵,會對極小部分的輸入算出錯誤答案,光是更換已出貨的零件就讓公司付出約 4.75 億美元。那就是一個逃過驗證的錯誤的代價。

兩種工作,兩種心態

在一支晶片團隊裡,工作分成兩個並肩而坐的角色。設計工程師撰寫 RTL——暫存器傳輸級的描述,通常用 Verilog 或 SystemVerilog——說明硬體*應該做什麼*:這個計數器會遞增、那個緩衝區會存八個字、這個狀態機在等待一個請求。驗證工程師的工作正好與「建造」相反:他們的工作是把它*弄壞*。他們寫的程式碼,整個目的就是要證明設計是錯的,而只有在付出巨大努力之後仍然弄不壞時,他們才會鬆一口氣。

這是真正不同的兩種心態。設計者想的是*「這是我為它而打造的情況。」*驗證者想的是*「這是你忘掉的情況。」*這種對抗式的分工是刻意的,也是為什麼這兩個角色通常由不同的人擔任:要在你才剛剛深情寫下的程式碼裡獵捕瑕疵,在心理上很難。在一顆複雜的 SoC 上,驗證也絕不是個小小的事後補充——它通常會吃掉總工程量的 60% 到 70%。一顆晶片上的驗證工程師人數,往往比設計者還多。

測試平台到底是什麼

你沒辦法拿邏輯探棒去戳一顆還不存在的晶片。取而代之,你在軟體裡搭起一張虛擬的實驗工作台,並在邏輯模擬器上運行它——這個程式假裝自己是硬體,計算每一條訊號在每一個時脈邊緣上的行為。那張虛擬工作台就是測試平台,它有三個部分,會在你日後打造的每一個驗證環境裡反覆出現。

  1. 激勵(驅動器):產生輸入並餵進設計的程式碼——時脈邊緣、重置、資料、控制訊號。這就是你伸手進去按下按鈕。
  2. 待測設計(DUT):你正在驗證的真實 RTL,像把晶片插進插座一樣放進工作台中央。測試平台包在它外面,但不會改動它。
  3. 檢查(監視器 + 計分板):監看輸出、並自動判定它們是否正確的程式碼。這是初學者會忘掉的部分——也是最重要的部分。
          +----------------------------------------------+
          |                  TESTBENCH                   |
          |                                              |
  random/ |   +-----------+      +-------------+          |
  directed|   |  STIMULUS |----->|     DUT     |---+      |
  vectors --->|  (driver) | clk  |  (your RTL) |   |      |
          |   +-----------+ rst  +-------------+   |      |
          |                                       v      |
          |   +-----------------+          +-----------+ |
          |   | REFERENCE MODEL |--expected| CHECKER / | |
          |   | (golden, in C)  |--------->| SCOREBOARD|-+--> PASS / FAIL
          |   +-----------------+   actual  +-----------+ |
          |                                              |
          +----------------------------------------------+
測試平台的通用形狀:把輸入驅動進去,讓 DUT 計算,再把它的輸出和一個獨立的「黃金」參考相比對。只要這兩者有任何一次意見不合,你就抓到了一個錯誤——自動發生,不需要人盯著波形看。

「編譯過了」不等於「會動」

當你的 RTL 乾淨地編譯成功時,工具只確認了一件事:你的程式碼在文法上是合法的 Verilog——每一條線都有宣告、每一個括號都成對。它對「設計是否做了正確的事」隻字未提。一個完美編譯的 FIFO,可能會丟資料、重複計數、或永遠卡死。編譯證明的是*語法*;驗證證明的是*行為*。我們用一個微型的範例把這件事講具體,這個範例我們在之後每一個階段都會重複使用:一個小型的同步 FIFO——一個先進先出的佇列,深度八格,在一個快的寫入者與一個較慢的讀取者之間緩衝資料。

// A FIFO that COMPILES PERFECTLY but is BROKEN.
// The 'full' flag forgets one corner case.

module fifo8 (
  input        clk, rst,
  input        wr, rd,        // write / read requests
  input  [7:0] din,
  output [7:0] dout,
  output       full, empty
);
  reg [7:0] mem [0:7];
  reg [3:0] count;            // 0..8 items held

  assign full  = (count == 4'd8);
  assign empty = (count == 4'd0);

  always @(posedge clk)
    if (rst) count <= 0;
    else begin
      if (wr)            count <= count + 1;   // BUG: writes even when full!
      if (rd && !empty)  count <= count - 1;
    end
  // ...mem read/write omitted...
endmodule
找出錯誤:寫入路徑即使在 FIFO 已經滿了的時候,仍然會把 count 遞增,悄悄地把緩衝區寫爆並破壞資料。編譯器毫無怨言。只有一座會*檢查*的測試平台——一座往八格深的 FIFO 寫入九筆資料、並驗證第九筆有被拒絕的測試平台——才抓得到它。

那個錯誤對編譯來說是隱形的,用眼睛也很容易漏掉,因為它只在一種情況下才重要:往一個已經滿了的 FIFO 寫入。那是一個邊角情況(corner case)——一組罕見的條件組合,讓設計悄悄地做了錯誤的事。真實的晶片在邊角情況失敗的次數,遠多於在常見路徑上失敗,因為常見路徑正是設計者想得最透徹的地方。整門驗證的藝術,就是有系統地去獵捕那些設計者從未想像過的邊角。

驗證計畫:你的合約

如果驗證的意思是「檢查每一個功能與每一個邊角情況」,那麼顯而易見的問題是:*你怎麼知道自己做完了?*你不能只是一直測到自己有信心為止——對一個一億閘的設計來說,那種感覺一文不值。答案是驗證計畫(vplan):一份事先談妥、白紙黑字的文件,逐項列出設計必須具備的每一個功能、以及每一個必須被檢查的情境。它是定義「完成」的合約。在計畫說某件事已驗證之前,它就尚未被驗證;而一個從計畫裡漏掉的功能,就是一個沒有人會去測試的功能。

  FIFO8 VERIFICATION PLAN  (excerpt)
  ID    Feature / scenario                       Method     Status
  ----  --------------------------------------    --------   -------
  F-01  Reset clears count -> empty asserted      directed   PASS
  F-02  Write then read returns same data (FIFO)   directed   PASS
  F-03  full asserts at exactly 8 entries          directed   PASS
  C-01  Write to a FULL fifo is ignored            directed   FAIL  <-- our bug
  C-02  Read from an EMPTY fifo is ignored         directed   PASS
  C-03  Simultaneous wr & rd holds count steady    random     ----
  C-04  Random wr/rd mix, 1M cycles, never corrupt random     ----
  X-01  All 8 fill levels exercised (coverage)     coverage   72%
驗證計畫把模糊的「測一測 FIFO」變成一張帶有可量測狀態的檢查清單。注意它在 C-01 那一列抓到了我們的溢位錯誤——因為有人把那個邊角情況白紙黑字寫下來,當成一件*必須*被檢查的事,而不是寄望於恰巧撞見它。

看看方法那一欄——它分成兩種哲學,而本軌道接下來的內容,本質上就是從第一種走到第二種的旅程。指向式測試(directed tests)是手寫的情境:身為工程師的你決定「寫入九筆,然後檢查第九筆被拒絕」,然後你把那件事精確地寫成程式碼。它們精準、容易除錯,但每一個只測試你想到的那一個情況。對我們這個小小的 FIFO 來說,那也許就夠了。但對一顆有著數十億種可能指令序列的 CPU 而言,你窮盡一生也手寫不出足夠的指向式測試——你的想像力,永遠會比你的錯誤先用完。

另一種選擇是約束隨機、覆蓋率驅動(constrained-random, coverage-driven)的驗證——本軌道接下來的全部內容都建立在這個方法上。你不再寫一個情境,而是描述輸入的*合法*空間(任意的寫入與讀取組合,但絕不同時送進兩筆垃圾),然後讓測試平台產生數以千計的隨機化序列,並自動拿去和一個黃金模型比對。隨機性會觸及那些你根本想不到要手寫的邊角。但光有隨機性是盲目的,所以你把它和功能覆蓋率配成一對:一張記錄「哪些有趣情況真的發生過」的計分板——FIFO 曾經剛好滿過嗎?有寫入和讀取落在同一個週期嗎?覆蓋率回答了那個真正的問題:「我們是否操練過計畫所要求的一切?」,並告訴你什麼時候可以停下來。