Subscriber built reg_coverage, which only ever watches and counts - it never judges whether a transaction was correct. This article builds the component that actually checks correctness: a scoreboard that predicts what reg_block should do, and flags it when reality disagrees.

9 min read | Intermediate Level

What You'll Learn

  • What a scoreboard does, and how that's different from what reg_coverage does
  • How to declare a uvm_analysis_imp by hand, without uvm_subscriber's shortcut
  • How to build a simple reference model that predicts reg_block's expected contents
  • How to connect reg_monitor to a scoreboard

What Is a Scoreboard?

A scoreboard is a verification component that checks whether a design behaved correctly, usually by comparing what actually happened against what should have happened. For a register block like reg_block, the question a scoreboard answers is simple to state: if a write put a certain byte at a certain address, does a later read from that same address return that same byte back?

Answering that question means the scoreboard needs to remember every write it's seen, so it has something to compare a later read against. That remembered, predicted state is sometimes called a reference model, or a predictor - a simplified model of what the DUT's internal state should be, built and maintained entirely in the testbench, with no dependence on reg_block's own implementation.

Building reg_scoreboard

1. Declare the Class


class reg_scoreboard extends uvm_scoreboard;

  `uvm_component_utils(reg_scoreboard)

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // Code for rest of the steps come here
endclass

reg_scoreboard extends uvm_scoreboard rather than plain uvm_component, for the same reason reg_monitor extends uvm_monitor instead: it's recommended practice, and it makes a scoreboard immediately recognizable as one by its class, even though uvm_scoreboard doesn't add any methods on top of uvm_component itself.

2. Declare an Analysis Implementation Port

Subscriber gave reg_coverage its own analysis_export automatically, just by extending uvm_subscriber. uvm_scoreboard doesn't include that convenience, so reg_scoreboard has to declare the same underlying mechanism, uvm_analysis_imp, by hand:


uvm_analysis_imp #(reg_transaction, reg_scoreboard) ap_imp;

function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  ap_imp = new("ap_imp", this);
endfunction

ap_imp is a plain class property, so it needs constructing with new() just like reg_coverage's analysis_export did underneath uvm_subscriber's own constructor - the difference here is that reg_scoreboard does that construction itself, inside its own build_phase, rather than getting it for free.

3. Build the Reference Model

reg_scoreboard needs somewhere to keep track of what it expects reg_block's memory to contain, built up purely from the writes it's observed:


bit [7:0] expected_mem [16];
bit       written [16];

virtual function void write(reg_transaction tr);
  if (tr.wr_en) begin
    expected_mem[tr.addr] = tr.wdata;
    written[tr.addr]      = 1;
  end
  else if (tr.rd_en) begin
    if (!written[tr.addr]) begin
      `uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
    end
    else if (tr.rdata != expected_mem[tr.addr]) begin
      `uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
    end
    else begin
      `uvm_info(get_type_name(), $sformatf("addr=%0d matched: 0x%0h", tr.addr, tr.rdata), UVM_HIGH)
    end
  end
endfunction

write() is the method ap_imp calls automatically every time reg_monitor broadcasts a captured transaction - the same role reg_coverage's own write() plays, just with checking instead of sampling inside it. On a write, reg_scoreboard simply records what it expects that address to hold from now on. On a read, it compares reg_monitor's captured rdata against that prediction.

written exists to handle a real edge case honestly: a read from an address that's never been written has no known expected value to compare against - reg_block's own memory contents are undefined at that point, not necessarily zero. Comparing against an assumed default there would risk flagging a meaningless mismatch, so reg_scoreboard skips the check entirely until it has actually seen a write to that address.

Quick Check

Q: Why does write() check tr.wr_en and tr.rd_en instead of just checking whether tr.rdata is populated?

Show Answer

reg_transaction's wr_xor_rd_c constraint already guarantees every transaction is clearly one or the other, so checking the flag directly is the more reliable signal - rdata is simply never populated by reg_monitor on a write, but relying on that as an implicit signal would be more fragile than checking the field that actually says what kind of transaction this is.

Connecting reg_monitor to reg_scoreboard

Same as reg_coverage, reg_scoreboard does nothing until something is actually connected to ap_imp. That connection is made wherever both reg_monitor (inside reg_agent) and reg_scoreboard are owned by the same parent - typically the environment, covered in UVM Environment:


virtual function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  m_agent.m_monitor.mon_analysis_port.connect(m_scoreboard.ap_imp);
endfunction

This snippet names m_agent and m_scoreboard as if they already exist side by side in some parent component, since that's exactly the situation UVM Environment builds. The connection itself is no different from the monitor-to-coverage wiring already shown inside reg_agent - one analysis port, connected to one analysis implementation, with reg_monitor never needing to know who's on the other end.

Common Mistakes

Mistake #1: Performing the comparison inside the monitor instead of the scoreboard

It can be tempting to have reg_monitor do the comparison itself, since it already sees every transaction. Keeping that logic in a separate scoreboard instead means the same monitor can feed multiple different checkers, and means changing the checking logic never requires touching the monitor at all.

Mistake #2: Comparing against an assumed default instead of tracking what's actually been written

Skipping the written array and assuming every address starts at a known value - usually zero - produces a scoreboard that occasionally flags failures that aren't real bugs, simply because nothing was ever written to that address yet. A reference model should only ever check what it actually has a basis to predict.

Practice Exercise

Challenge: Count Mismatches Instead of Just Reporting Them

Add a property to reg_scoreboard that counts how many mismatches have occurred across the whole test, and print the total in report_phase.

Hint

Increment a counter inside the same branch that calls `uvm_error, then add a report_phase function that prints it - this phase runs once, near the very end of the simulation, after every transaction has already been processed.

Solution
int unsigned mismatch_count = 0;

virtual function void write(reg_transaction tr);
  if (tr.wr_en) begin
    expected_mem[tr.addr] = tr.wdata;
    written[tr.addr]      = 1;
  end
  else if (tr.rd_en) begin
    if (!written[tr.addr]) begin
      `uvm_info(get_type_name(), $sformatf("addr=%0d read before any write, skipping check", tr.addr), UVM_LOW)
    end
    else if (tr.rdata != expected_mem[tr.addr]) begin
      mismatch_count++;
      `uvm_error(get_type_name(), $sformatf("Mismatch at addr=%0d: expected=0x%0h actual=0x%0h", tr.addr, expected_mem[tr.addr], tr.rdata))
    end
  end
endfunction

virtual function void report_phase(uvm_phase phase);
  `uvm_info(get_type_name(), $sformatf("Total mismatches: %0d", mismatch_count), UVM_LOW)
endfunction
  

report_phase is a good fit here specifically because it runs after everything else is done - printing the count any earlier could miss mismatches that hadn't happened yet.

Summary

In this article, you learned:

  • A scoreboard predicts a DUT's expected behavior and compares it against what actually happened
  • uvm_scoreboard doesn't provide a built-in analysis export the way uvm_subscriber does, so a scoreboard declares its own uvm_analysis_imp and constructs it in build_phase
  • A reference model should only check addresses it has actually seen written, not assume an unwritten address starts at a known value
  • Connecting a monitor's analysis port to a scoreboard's implementation port uses the exact same mechanism already used to connect a monitor to coverage

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5