Part 8 · Checking & Coverage · Intermediate

Dual Analysis Imps: write_exp and write_act

uvm_analysis_imp_decl macro, implementing write_exp/write_act, and clone/copy before queueing.

Why one write() is not enough

A standard UVM subscriber has a single write() function invoked when ap.write(txn) fires. That works for coverage collectors and loggers — one stream in, one action out. A scoreboard has two independent streams arriving asynchronously: expected transactions from the ref model or golden monitor, and actual transactions from the DUT monitor. Both need handlers on the same component so they share the match queue or ID map.

You cannot connect two analysis exports to one vanilla analysis imp — the second connection overwrites the first, or both streams invoke the same write() with no way to distinguish expected from actual. The scoreboard would mix streams and produce nonsense compares.

UVM solves this with `uvm_analysis_imp_decl(_suffix), a macro that generates a uniquely named analysis imp class and matching write_suffix() function for each stream. Declare once per stream, instantiate in the constructor, connect in connect_phase.

diagram
[CHECK] why one write() fails

  SINGLE imp (broken):
    exp_port.connect(scb.imp)   // write() gets expected
    act_port.connect(scb.imp)   // OVERWRITES — only act arrives

  DUAL imps (correct):
    exp_port.connect(scb.exp_imp)  // write_exp() gets expected
    act_port.connect(scb.act_imp)  // write_act() gets actual
    both coexist on same scoreboard object

Macro expansion — what uvm_analysis_imp_decl creates

Each `uvm_analysis_imp_decl(_suffix) invocation expands into two things: a new imp class uvm_analysis_imp_suffix #(T, IMP_TYPE) and a virtual function write_suffix(T t) on your scoreboard that the imp calls. You implement the body of each write_suffix.

diagram
[UVM] uvm_analysis_imp_decl macro diagram

  `uvm_analysis_imp_decl(_exp)     `uvm_analysis_imp_decl(_act)
           │                                  │
           ▼                                  ▼
  class uvm_analysis_imp_exp       class uvm_analysis_imp_act
           │                                  │
           ▼                                  ▼
  function write_exp(T t)          function write_act(T t)
     your scoreboard code            your scoreboard code

  connect_phase:
    ref_model.ap.connect(scb.exp_imp)
    dut_mon.ap.connect(scb.act_imp)
systemverilog
`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)

class bus_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(bus_scoreboard)

  // One imp instance per declared suffix
  uvm_analysis_imp_exp #(bus_txn, bus_scoreboard) exp_imp;
  uvm_analysis_imp_act #(bus_txn, bus_scoreboard) act_imp;

  bus_txn exp_q[$];
  int match_count = 0, mismatch_count = 0, unexpected_count = 0;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_imp = new("exp_imp", this);
    act_imp = new("act_imp", this);
  endfunction

  function void write_exp(bus_txn t);
    bus_txn snap = bus_txn::type_id::create("snap");
    snap.copy(t);
    exp_q.push_back(snap);
    `uvm_info("SCB", $sformatf("queued exp id=%0h addr=%0h", snap.id, snap.addr), UVM_HIGH)
  endfunction

  function void write_act(bus_txn t);
    if (t.kind != RESPONSE) return;  // filter requests
    compare(t);
  endfunction

  // compare(), check_phase, report_phase — see FIFO / ID lessons
endclass
  • Macro must appear OUTSIDE the class, before the class definition.

  • Suffix is arbitrary — _exp/_act, _expected/_actual, _gold/_dut all work.

  • Instantiate imp in new() — one new() per declared suffix.

  • write_exp queues; write_act triggers compare (typical pattern).


Walkthrough — two writes on one APB response

Follow one APB response through both imps. The ref model predicts first; the DUT monitor samples second. Both land on the same scoreboard object but different write functions.

diagram
[CHECK] dual-imp walkthrough — APB response id=0x05

  Step 1: ref_model.ap.write(exp_txn)
           exp_imp  write_exp(exp_txn)
           clone, push exp_q = [exp(id=05)]

  Step 2: dut_mon.ap.write(act_txn)
           act_imp  write_act(act_txn)
           kind==RESPONSE  compare(act_txn)
           pop_front exp_q  exp(id=05)
           act.compare(exp)  MATCH, match_count++

Third imp for request/response split

Some architectures feed requests to the ref model and responses to the scoreboard on separate connections. Declare a third suffix:

systemverilog
`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)

// connect_phase:
dut_agt.mon_req_ap.connect(ref_model.req_imp);
dut_agt.mon_req_ap.connect(scb.req_imp);     // optional: log requests
dut_agt.mon_rsp_ap.connect(scb.rsp_imp);      // actual responses
ref_model.ap.connect(scb.exp_imp);            // expected responses
  • req_imp receives observed requests — can trigger predict or coverage.

  • rsp_imp (or act_imp) receives responses — triggers compare.

  • Split ports avoid kind-filtering inside write_act.


Clone before queue — the silent bug

Monitors reuse a single transaction object across samples for performance. The monitor fills fields on object t, calls ap.write(t), then on the next clock edge overwrites t's fields for the next sample. If you queue the handle instead of a copy, every queued entry points to the same object — and that object's fields always reflect the last sample, not the one you intended to store.

This bug is silent and devastating. Your scoreboard appears to work in simple single-transaction tests. In regression with back-to-back traffic, every compare uses stale or wrong data. Mismatches appear random. UVM_ERROR messages show identical-looking transactions that fail compare() because the queued handle mutated after push.

diagram
[CHECK] clone bug — what goes wrong

  Monitor reuses txn object M:

  T1: M.addr=0x1000  ap.write(M)   exp_q.push_back(M)  // pushed HANDLE
  T2: M.addr=0x2000  ap.write(M)   exp_q.push_back(M)  // same handle!
  T3: compare act(addr=0x1000)
      pop_front  M.addr is now 0x2000 (mutated at T2)
      compare fails — looks like DUT bug, is scoreboard bug
systemverilog
function void write_exp(bus_txn t);
  // WRONG — queues handle, mutates on next monitor sample
  exp_q.push_back(t);

  // RIGHT — independent snapshot
  bus_txn snap = bus_txn::type_id::create("snap");
  snap.copy(t);           // deep copy all fields via field macros
  exp_q.push_back(snap);
endfunction

function void write_act(bus_txn t);
  bus_txn snap = bus_txn::type_id::create("act_snap");
  snap.copy(t);
  compare(snap);          // clone act too — monitor may reuse before compare finishes
endfunction
  • copy() requires uvm_field_* macros on the transaction class.

  • clone() works without field macros but you must cast: bus_txn'(t.clone()).

  • Clone in BOTH write_exp and write_act — monitor reuse affects both paths.

  • Clone before associative-array insert too: exp_by_id[id] = snap.

Key takeaways

  • Declare imps with uvm_analysis_imp_decl — unique write_suffix per stream.

  • Instantiate each imp in new(); connect each to its own analysis port.

  • Clone transactions before storing in queues or ID maps — always.

  • write_exp queues expected; write_act filters and triggers compare.

Common pitfalls

  • Queuing handles — silent compare failures from mutated data.

  • Connecting both streams to same imp — cannot distinguish exp vs act.

  • Macro inside class body — must be outside, before class definition.

  • Forgetting to clone act in write_act — race with next monitor sample.