為什麼手刻的測試平台無法規模化
想像你是個木工學徒,做出一張漂亮的椅子,接著被要求做一張桌子。你又得從原木開始——新的接榫、新的尺寸,沒有任何東西能重用。手寫測試平台的下場一模一樣:刺激產生器、匯流排驅動器、檢查器、覆蓋率收集器全部糾纏在同一個檔案裡,硬綁在這個區塊的接腳與協定上。椅子完美無缺,但沒有一塊料能轉用到桌子上。
再乘上現實。一顆現代 SoC 有數十個區塊,各自講著 AXI、APB、I2C、PCIe 或某種自訂協定。三個時區的三組團隊並行驗證它們,接著又在晶片層級重來一次。如果每位工程師都自創一套測試平台骨架——自己一套命名驅動器、提出 objection、印出錯誤的方式——什麼都拼不起來。一組團隊寫的 AXI 檢查器無法搬進另一組的環境。在大型晶片上,驗證工作已經吃掉 60–70% 的時程;在每個區塊上重造水電管線,正是這數字爆炸的原因。
UVM:一套共享的語彙,而非新語言
通用驗證方法學(UVM)*不是*一種新的程式語言,也*不是*你買來的工具。它是一套 SystemVerilog 類別程式庫——大約 350 個基底類別——由 Accellera 標準化、被整個 IEEE 1800.2 生態系採用,並由每家主流模擬器(Synopsys VCS、Cadence Xcelium、Siemens Questa)以相同方式支援。你寫的是普通的 SystemVerilog;你只是繼承 UVM 的基底類別,而非自創一套。
把它想成一套共享的語彙,讓全世界的驗證工程師看一眼就讀懂彼此的程式碼。當你讀某人的環境、看到一個繼承自 `uvm_driver` 的類別,你立刻知道它從 sequencer 取得交易並驅動接腳。`uvm_monitor` 觀察接腳並重建交易。`uvm_scoreboard` 比對預期與實際。無論區塊是個小小的 FIFO 還是一顆 64 位元的 CPU 核心,結構都一樣——只有協定相關的核心部分會變。
UVM environment (uvm_env) ┌──────────────────────────────────────────────┐ │ Agent (uvm_agent) ── reusable unit ────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ │ │ Sequencer │──>│ Driver │──pins──┐ │ │ │ └────────────┘ └────────────┘ │ │ │ │ ┌────────────┐ ▼ │ │ │ │ Monitor │<───── DUT │ │ │ └─────┬──────┘ (design │ │ │ │ under │ │ │ ┌────────────┐ analysis│ test) │ │ │ │ Scoreboard │<─────────┘ │ │ │ └────────────┘ │ │ │ ┌────────────┐ │ │ │ │ Coverage │<── analysis port │ │ │ └────────────┘ │ │ └──────────────────────────────────────────────┘ Test → starts a Sequence → on the Sequencer → feeds the Driver
Agent:重用的最小單位
如果 UVM 有個主角,那就是 UVM agent。一個 agent 是針對單一介面的自足封包——比如一個 AXI 埠。它捆綁了三個碰觸該協定的元件:排程交易的 sequencer、把每筆交易轉成接腳波動的 driver,以及觀察接腳、重建實際發生了什麼的 monitor。(之後的階段會逐一細建這些。)把你的 AXI agent 交給別人,他們就拿到一個完整、即插即用的 AXI 驗證元件——一個 VIP。
Agent 還有一個讓它可攜的把戲:一個模式開關。主動(active)agent 驅動匯流排——它擁有 driver 與 sequencer 並產生流量。被動(passive)agent 只負責監看——driver 與 sequencer 關閉,只留 monitor 聆聽。在區塊層級,你的 AXI agent 是主動的,用刺激猛擊設計。在晶片層級,當真正的 CPU 開始驅動同一條 AXI 匯流排時,你把*同一個* agent 切到被動:它停止驅動,單純觀察,一如既往地餵養 scoreboard 與覆蓋率。一個類別、兩種角色、零次重寫。
讓重用得以運作的三套機制:階段、工廠、組態
重用不會靠許願實現。UVM 在底層提供了三套機制協同運作,讓各自獨立寫成的元件能乾淨地拼合在一起。
- 階段(Phasing)——每個 UVM 元件都走過*同一套*有序的階段,由模擬器像指揮的拍點一樣呼叫。依序的關鍵幾個是:build(由上而下建構子元件)、connect(由下而上把埠接起來)、run(耗時的階段,刺激在此流動、檢查在此觸發),接著 report(統計結果)。因為大家都同意這條時間軸,你的 monitor 與別人的 scoreboard 會以保證的順序建構與連接——沒有脆弱的「誰先初始化」競態。
- 工廠(Factory)——元件不再為每個物件硬寫 `new()`,而是請*工廠*依型別製造它們。回報是:在你的 test 裡一行 override 就能告訴工廠「每當有人要一個正常封包,改為建構一個會注入錯誤的封包」——而所有現存的 sequence 照常運作,如今產生損毀的流量,卻不必更動環境裡的任何一行。
- 組態資料庫(Configuration database)——一個全域的鍵/值倉庫(`uvm_config_db`),最上層的 test 把設定丟進去,深處的元件再依名稱取出:「這個 agent 是被動的」、「用這個 virtual interface」、「位址寬度 = 64」。元件絕不伸手去抓鄰居;它們從 config DB 讀取自己的行軍命令。正是這份解耦,讓同一個 agent 能在兩個環境裡表現不同卻無須改碼。
把上一階的測試平台重塑成 UVM
UVM 在概念上對你毫無新意——它就是上一階*同一套*約束隨機流程,只是被拆解成可重用、由工廠建構、與階段對齊的類別。以下是一對一的對應。你手寫的刺激迴圈變成一連串交易物件組成的 sequence,在 sequencer 上執行。驅動匯流排的那個 `task` 變成 driver。那一團 `assert`/`if` 檢查變成由 monitor 餵養的 scoreboard。你的 `covergroup` 變成掛在 analysis port 上的覆蓋率訂閱者——就是你之前取樣的同一份功能覆蓋率,如今成了一個整潔的插件元件。
// Last rung — one tangled file: module tb; // random stimulus, bus driving, checking, coverage // all hardwired to THIS dut, reusable by no one. endmodule // This rung — same behaviour, UVM structure: class my_seq extends uvm_sequence #(my_txn); ... endclass // stimulus class my_driver extends uvm_driver #(my_txn); ... endclass // bus wiggles class my_mon extends uvm_monitor; ... endclass // observe pins class my_sb extends uvm_scoreboard; ... endclass // expected vs actual class my_agent extends uvm_agent; // bundles sequencer+driver+monitor class my_env extends uvm_env; // agent + scoreboard + coverage class my_test extends uvm_test; // picks the env, starts the sequence // reuse: my_agent now drops into ANY env that speaks this protocol.
多出來的這套儀式換來的是槓桿。Agent 能整塊抬進下一個區塊。工廠讓衍生的 test 注入錯誤而不碰環境。Config DB 在從區塊走向晶片的旅程上,把同一個 agent 從主動改為被動。而由於結構是業界標準,你的驗證計畫——那份把特性對應到覆蓋率目標的文件——如今能乾淨地對映到任何審查者都認得的、有名有姓的可重用元件上。測試平台不再是一張椅子,而成了一盒零件套件。