UVM Basics

Chapter 9 of 10

Scoreboard and Analysis Ports

The receiving side of an analysis_port -- uvm_analysis_imp and write() -- as a second, software-based checking strategy alongside SV ch15's assertions, plus packaging an agent and scoreboard into a uvm_env.

Chapter 8's monitor has been broadcasting into the void — mon.ap.write(tr) calls with nothing connected to receive them. This chapter connects something: a scoreboard, UVM's standardized version of the "detect a mismatch" goal SV ch15 (assertions) already taught with `uvm_error/assertions. Assertions check timing- and protocol-level properties close to the RTL; a scoreboard checks end-to-end correctness in software, by comparing what actually happened against what should have — a second, complementary strategy, not a replacement.

The receiving side: uvm_analysis_imp and write()

Chapter 8 only covered the sending half of an analysis connection — uvm_analysis_port#(T)::write(t), called by a monitor. Something has to be on the other end, actually implementing write(T t) so there's something for a connected port to call:

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)
 
  uvm_analysis_imp #(mux_transaction, my_scoreboard) analysis_export;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    analysis_export = new("analysis_export", this);
  endfunction
 
  function void write(mux_transaction tr);
    bit expected_y;
    expected_y = tr.sel ? tr.b : tr.a;
    if (tr.y !== expected_y)
      `uvm_error("SB", $sformatf("mismatch: %s expected_y=%0b", tr.convert2string(), expected_y))
    else
      `uvm_info("SB", $sformatf("match: %s", tr.convert2string()), UVM_MEDIUM)
  endfunction
endclass

uvm_analysis_imp#(T, IMP) is the receiving counterpart to uvm_analysis_port#(T) — its second parameter, IMP, names the class that actually implements write(T t) (here, my_scoreboard itself), since the analysis_imp just forwards every incoming write() call to that class's own method. Everything a monitor broadcasts to a connected analysis_imp ends up as a call to write() right here.

mux2 being purely combinational and stateless is what makes this scoreboard this simple: because chapter 8's monitor already packages stimulus (sel/a/b) and observed output (y) into the same transaction, write() can compute the expected result directly from the fields it was just handed, with nothing to remember between calls. A stateful DUT (anything with a register, a FIFO, a pipeline) would need the scoreboard to track history across multiple write() calls instead — out of scope here, but worth knowing this simplicity is specific to a combinational DUT, not a general property of scoreboards.

Connecting monitor to scoreboard, and packaging it all: uvm_env

The connection itself is one line, but it needs both the agent and the scoreboard to already exist — which means it belongs in whatever component owns both. That's a uvm_env:

class my_env extends uvm_env;
  `uvm_component_utils(my_env)
 
  my_agent      agent;
  my_scoreboard sb;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = my_agent::type_id::create("agent", this);
    sb    = my_scoreboard::type_id::create("sb", this);
  endfunction
 
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    agent.mon.ap.connect(sb.analysis_export);
  endfunction
endclass

agent.mon.ap.connect(sb.analysis_export) reaches through my_agent to its mon member (public, same as any other field without local/protected — SV ch10) and connects that monitor's analysis port to the scoreboard's analysis_imp. From here on, every transaction the monitor observes reaches the scoreboard's write() automatically.

The test, one more time

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
 
  my_env env;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction
 
  task run_phase(uvm_phase phase);
    my_sequence seq;
    phase.raise_objection(this);
    seq = my_sequence::type_id::create("seq");
    seq.start(env.agent.sqr);
    phase.drop_objection(this);
  endtask
endclass

Each chapter has simplified my_test further — it went from owning a driver directly (chapter 3), to owning a driver and sequencer (chapter 7), to owning one agent (chapter 8), and now just one env. That's not an accident: as more structure gets pushed down into reusable components, the test's own job shrinks to "build the environment, then decide what stimulus to run" — exactly what a test should do.

A pattern worth naming: most of these base classes add nothing

uvm_test (chapter 3), uvm_monitor (chapter 8), uvm_scoreboard, and uvm_env (both this chapter) all extend uvm_component and add no new members or behavior at all — every one of them is a pure naming convention, marking a component's role for anyone reading the hierarchy later. The two real exceptions are uvm_driver#(REQ, RSP) (chapter 7, adds seq_item_port) and uvm_agent (chapter 8, adds is_active and the active/passive convention). Recognizing which base classes are "just labels" versus which ones actually add something is worth carrying forward — it's the difference between a class hierarchy that documents itself and one where every class needs to be checked to know what it does.

Summary

  • A scoreboard is a second, software-based checking strategy alongside SV ch15's assertions — same "detect a mismatch" goal, checked in software against what a monitor observed rather than at the RTL/protocol level.
  • uvm_analysis_imp#(T, IMP) is the receiving side of an analysis connection; IMP names the class whose write(T t) method actually receives what gets broadcast.
  • Because mux2 is combinational and stateless, the scoreboard can compute the expected result directly from each transaction's own fields — a stateful DUT would need to track history across calls instead.
  • agent.mon.ap.connect(sb.analysis_export) belongs in whatever component owns both the agent and the scoreboard — a uvm_env.
  • uvm_test, uvm_monitor, uvm_scoreboard, and uvm_env all extend uvm_component with nothing added — pure naming conventions; uvm_driver and uvm_agent are the two base classes in this track that actually add something.

What does the second type parameter in uvm_analysis_imp #(mux_transaction, my_scoreboard) actually specify?

Why can this scoreboard's write() compute the expected result directly from tr's own fields, with nothing remembered between calls?

Why does agent.mon.ap.connect(sb.analysis_export) belong in my_env's connect_phase rather than, say, my_agent's?

Which two base classes covered in this track actually add members/behavior on top of uvm_component, unlike uvm_test/uvm_monitor/uvm_scoreboard/uvm_env?