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
endclassuvm_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
endclassagent.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
endclassEach 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;IMPnames the class whosewrite(T t)method actually receives what gets broadcast.- Because
mux2is 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 — auvm_env.uvm_test,uvm_monitor,uvm_scoreboard, anduvm_envall extenduvm_componentwith nothing added — pure naming conventions;uvm_driveranduvm_agentare 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?