為什麼驗證佔了大部分工作量
有個數字會讓剛入行的人吃驚:在一款正經設計上,驗證通常佔總工作量的 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定向測試 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覆蓋率:我們到底測了多少?
約束隨機帶出一個讓人不安的問題:既然工具是在亂扔隨機輸入,你怎麼知道它真的試到了那些要緊的情形? 沒準十萬個封包裡,長度恰好為 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);
瞥一眼形式驗證與 UVM
模擬哪怕跑上幾十億個隨機週期,測到的也只是你恰好生成出來的那些輸入——它是在對一片汪洋做取樣。形式驗證走的是另一條路:它把你的斷言交給一個數學引擎,讓引擎證明它們對*所有*可能的輸入都成立,或者遞給你一個具體的反例。沒有激勵,沒有覆蓋率盲區——而是一份證明。它撐不到整顆晶片的規模,但在一個棘手的模組上(一個仲裁器、一個 FIFO、一個協定的角落),它帶來的差別就是「我們沒看到它出錯」和「它不可能出錯」之間的差別。
隨著設計變大,臨時拼湊的測試平台會撐不住。UVM(通用驗證方法學)是業界用來搭建大型測試平台的標準框架——可重用的元件(driver、monitor、scoreboard)、一個用來生成約束隨機流量的 sequencer,以及一個覆蓋率模型,全都按照每個驗證團隊都認得的方式接在一起。把它想成那套鷹架,讓一個測試平台從一個工程師的腳本,長成一整個團隊可以維護多年的東西。