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

UVM 元件:驅動器、監測器、序列器與計分板

一個 [[uvm|UVM]] 測試平台就像一座小工廠:序列器發派工單,驅動器把工單鎖到真實的接腳上,監測器透過單向鏡觀察產線,計分板則檢驗每一個成品。本文帶你認識這四個元件——它們被包進一個代理器、再由分析埠串接起來——看它們如何把抽象的交易翻成接腳訊號、再翻回來,並完整追蹤一筆讀取交易從頭到尾的流程。

工廠的產線:為什麼要四種角色,而不是一坨大程式

想像你經營一間工坊,由同一個人接單、加工零件、盯著機器、再驗收成品——全部一手包辦。一旦出錯,你根本不知道出事的當下他正在做哪一道工序。早期的 Verilog 測試平台正是如此:一個巨大的 `initial` 區塊裡,同時產生激勵、驅動接腳、取樣輸出、檢查結果,全部纏在一起。要重複使用幾乎不可能;只要協定一改,整套就垮了。

UVM 把這坨程式拆成四個職責分明的角色,每個只做一件事。序列器決定「要送什麼」;驅動器決定「怎麼把它放到線上」;監測器觀察訊號線、重建「實際發生了什麼」;計分板判斷「這樣對不對」。每個元件對其他元件的內部一無所知——它們只透過定義良好的交握與埠彼此溝通。正是這份切割,讓一個 UVM 測試平台能跨專案重用、自由組合,並擴展到一個由 200 位工程師打造的 SoC。

序列器與驅動器:把意圖變成接腳訊號

UVM 的激勵由序列產生——像是「隨機寫十個位址,再把它們讀回來」這種腳本化的配方。但序列本身不碰受測物(DUT),它把交易交給序列器。序列器扮演交通指揮:它把請求排隊、在彼此競爭的序列間仲裁,並在驅動器表示準備好時,一次釋出一筆交易。可以把序列器想成計程車派遣站的調度員——它握著叫車需求,把每一單配給一輛空車。

驅動器是唯一知道接腳層級協定的元件——建立時間、`valid`/`ready` 的先後順序、一個叢發要花幾個時脈。它跑一個永久迴圈:向序列器要下一筆交易(`get_next_item`)、嚴格依協定要求擺動匯流排、再告訴序列器已完成(`item_done`)。這組 `get_next_item`/`item_done` 交握是整個流程的心跳——它提供天然的反壓,讓序列器永遠不會以快過真實硬體吸收速度的步調淹沒驅動器。

task run_phase(uvm_phase phase);
  forever begin
    seq_item_port.get_next_item(req);   // pull one transaction from sequencer
    drive_transfer(req);                // wiggle pins per protocol
    seq_item_port.item_done();          // release sequencer for the next
  end
endtask

task drive_transfer(bus_txn req);
  @(posedge vif.clk);
  vif.addr  <= req.addr;                 // present address
  vif.wdata <= req.data;
  vif.valid <= 1'b1;
  vif.we    <= (req.kind == WRITE);
  do @(posedge vif.clk); while (!vif.ready);  // wait for slave handshake
  vif.valid <= 1'b0;                     // de-assert; transfer complete
endtask
一個最精簡的 UVM 驅動器。`vif` 是虛擬介面——驅動器看見實際 DUT 接腳的唯一窗口。注意它從不自行捏造資料;它只忠實地重播序列器交給它的那筆交易。

監測器:通往匯流排的單向鏡

如果說驅動器是在匯流排上書寫的手,那麼監測器就是閱讀它的眼睛。它嚴格地「被動」:它從不驅動任何一條訊號,只負責取樣。它的工作正是驅動器的鏡像——驅動器拿一筆交易產生接腳訊號,監測器則拿接腳訊號重建出交易。它觀察 `valid`、`ready`、`addr`、`data`,辨認出一筆完整的傳輸,把它包裝成一個全新的交易物件,再發布出去。

監測器透過一個分析埠(寫作 `uvm_analysis_port`)通往外界。不同於序列器↔驅動器那種點對點且會阻塞的交握,分析埠是一種發了就不管的廣播:監測器呼叫 `write(txn)`,這筆交易便會被送到連接該埠的「每一個」訂閱者,且沒有反壓。一個監測器可以同時餵給一個計分板、一個功能覆蓋率收集器與一個記錄器,三者都不會拖慢監測器。這就是發布/訂閱模式在矽智財驗證裡的化身。

class bus_monitor extends uvm_monitor;
  uvm_analysis_port #(bus_txn) ap;     // broadcast outlet

  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk iff vif.valid && vif.ready);  // sample on a real beat
      bus_txn t = bus_txn::type_id::create("t");
      t.addr = vif.addr;
      t.data = vif.we ? vif.wdata : vif.rdata;
      t.kind = vif.we ? WRITE : READ;
      ap.write(t);                       // fan out to ALL subscribers
    end
  endtask
endclass
一個被動監測器。它只在 `valid && ready` 標示出真正的傳輸時取樣,組出一筆交易,再廣播出去。一次 `write()` 呼叫能同時抵達計分板與覆蓋率收集器。

計分板:最後的驗收員

到目前為止的一切都只是在「搬動」資料。計分板才是真正決定通過或失敗的地方。它訂閱監測器的分析埠(透過 `uvm_analysis_imp`),於是每一筆重建出來的交易都落進它的 `write()` 方法。在裡頭,它把 DUT 做了什麼,拿來和「應該」發生什麼比對——而這個期望來自一個參考模型(有時叫黃金模型或預測器):一小段獨立的程式碼,描述正確的行為。

針對一個記憶體映射區塊,常見的計分板會保有一個影子模型——一個簡單的關聯陣列,鏡映 DUT 暫存器應有的內容。遇到 WRITE,它更新自己的影子;遇到 READ,它查出期望值,並與監測器觀察到的資料比對。不符就觸發 `uvm_error`;相符則安靜地把通過計數加一。這個參考模型刻意「不是」RTL 的副本——若它是,它就會把 RTL 的臭蟲一起複製。它是對「意圖」的獨立描述。

class bus_scoreboard extends uvm_scoreboard;
  `uvm_analysis_imp_decl(_obs)
  uvm_analysis_imp_obs #(bus_txn, bus_scoreboard) obs_imp;
  bit [31:0] shadow [bit [31:0]];        // golden reference model

  function void write_obs(bus_txn t);    // called per observed transaction
    if (t.kind == WRITE) begin
      shadow[t.addr] = t.data;           // predict
    end else begin                       // READ: check
      bit [31:0] exp = shadow.exists(t.addr) ? shadow[t.addr] : 32'h0;
      if (t.data !== exp)
        `uvm_error("SCB", $sformatf("addr %0h: got %0h exp %0h",
                                     t.addr, t.data, exp))
      else
        `uvm_info("SCB", "read match", UVM_HIGH)
    end
  endfunction
endclass
一個自我檢查的計分板。WRITE 更新黃金影子;READ 則拿來與之比對。DUT 與模型彼此獨立,因此真正的臭蟲會以「分歧」的形式現形。

代理器:把一切打包,以及線怎麼接

對一個介面而言,序列器、驅動器、監測器幾乎總是結伴而行——所以 UVM 把它們綁進一個可重用的容器,叫做代理器。把代理器想成專屬於「一個」協定埠的全裝備維修小組:給它一個虛擬介面,它就能同時激勵與觀察那個埠。一顆有 AXI 主控、APB 從屬與 UART 的晶片,會配三個代理器,各自獨立完整。

代理器有兩種風味,由一個旗標 `is_active` 控制。主動代理器會建立全部三個子元件——它既驅動也觀察;被動代理器「只」建立監測器——它觀察一個由別人驅動的埠(真實 DUT,或另一個區塊)。正是這個開關讓 UVM 環境能擴展:在區塊級測試裡代理器是主動的、會驅動 DUT;把同一個代理器重用到 SoC 級、此時該埠由內部驅動,把它翻成被動,它就變成純觀察者,餵給同一個計分板。

接線發生在 `connect_phase`。驅動器的 `seq_item_port` 連到序列器的 `seq_item_export`(激勵的交握通道);監測器的分析埠連到計分板的分析輸入埠,並同時連到一個覆蓋率訂閱者。關鍵在於:「代理器」會把監測器的分析埠向外曝露,讓環境能把觀察到的交易導向任何地方——使計分板留在代理器之外,那才是它該待的位置。

  agent  (active)                      env
  +-------------------------+      +------------------+
  | sequencer --seq_item--> |      |                  |
  |     |        driver --->|=pins=| DUT              |
  |     v                   |      |   |              |
  | (stimulus)     monitor--|--ap--+-->| scoreboard   |
  +-------------------------+      |   +--> coverage  |
                                   +------------------+
  seq_item_port  <-> seq_item_export   (blocking handshake)
  analysis_port  --> analysis_imp(s)   (non-blocking broadcast)
環境中的一個主動代理器。兩種連接:用於激勵、會阻塞的序列器↔驅動器交握;以及從監測器到計分板與覆蓋率、不會阻塞的分析廣播。

一筆交易,從頭到尾

讓我們完整追蹤一筆 `READ addr=0x40`,看每個元件依序碰它一次。這正是每個 UVM 測試平台每晚跑上千萬次的迴圈;看過一次,整套架構就豁然開朗。

  1. 序列 → 序列器。 序列隨機化一筆交易(`kind=READ, addr=0x40`),呼叫 `start_item`/`finish_item`。該物件停在序列器中,等待空閒的驅動器。
  2. 序列器 → 驅動器。 驅動器的迴圈呼叫 `get_next_item`;序列器把我們這筆 READ 交出去。此刻交易歸驅動器所有。
  3. 驅動器 → 接腳。 驅動器呈現 `addr=0x40`、拉高 `valid`、清掉 `we`(這是讀取),等待 DUT 的 `ready`。交握完成後它呼叫 `item_done`——它的工作就此結束。它從不去看回傳的資料;檢查不是它的職責。
  4. DUT → 接腳。 DUT 解碼位址,從暫存器檔取出 `0xDEAD`,在回應週期把它驅動到 `rdata` 上、並拉高 `ready`。
  5. 接腳 → 監測器。 監測器獨立地在那個週期看到 `valid && ready`,取樣 `addr=0x40` 與 `rdata=0xDEAD`,組出一筆全新交易 `{READ, 0x40, 0xDEAD}`,並呼叫 `ap.write(t)`。
  6. 分析埠 → 計分板+覆蓋率。 那一次 `write` 扇出。計分板的 `write_obs` 查它的影子模型(先前某筆 WRITE 已設為 `0xDEAD`),比對後相符——通過。與此並行,功能覆蓋率收集器取樣該位址的籃子,讓驗證計畫日後能證明 `0x40` 確實被操練過。