Verify This! · the course to read

Day 3UVM walks in

The same slides as the deck, with the instructor notes inside the text and the complete code. If you are taking the course on your own, start here and keep the deck alongside for the reviews.

Yesterday we left…

Where the testbench stands

Day 3 is where UVM walks in, so the recap has an extra job: making it clear that what is coming replaces pieces the student already wrote, and is not a pile of new pieces to learn from scratch. The whole unit can be read as "this thing you did by hand, the library brings it done". The third bullet is the hook: the case of 060 is exactly what `uvm_component_utils does on its own, and saying it here stops the macro from being a magic formula.

see this slide in the deck ›

Agenda

Day 3 · ≈ 4 h 30 · unit 4 · UVM walks in

By the end of the day you can:

The third one is the one that pays off the most in the long run and the one most underestimated: it is row 10 of the closing self-assessment and the one aiming at the 47 % of day 1. A uvm_error that prints "FAIL" leaves whoever is debugging where they were; one that prints the transaction, the prediction and the time saves them half a morning. The first two are library mechanics and they are the ones most often answered with an "it rings a bell". The second exercise of the day exists so that they stop ringing and start having been suffered.

see this slide in the deck ›

Tests

Compile once, pick the test from the command line

$ ./obj_dir/top/sim +UVM_TESTNAME=random_test
$ ./obj_dir/top/sim +UVM_TESTNAME=add_test     # the same binary

The arithmetic is UVM's commercial argument, and it is worth saying it with the numbers in hand: it is not that the modular testbench is wrong, it is that it does not scale. If possible, live: compile once and run the two lines above. Let them see that the second one compiles nothing. What gets collected from the object-based testbench: the testbench is already split into tester, coverage and scoreboard, and that is why you can change the tester alone. What gets paid up front: in the components those three classes stop being loose objects that the test new()s and become components of the tree.

see this slide in the deck ›

Tests

The top: instantiate, publish the interface, start

code/u4/tests/top.sv · #run-test
   initial begin
      // null and "*": from the root and visible to the whole tree. See the
      // uvm_config_db slide.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

      // UVM reads +UVM_TESTNAME, and builds THAT test with the factory.
      run_test();
el archivo entero, 31 líneas
module top;
   import uvm_pkg::*;
   `include "uvm_macros.svh"

   // Our classes and macros, same as in the modular version.
   import vtalu_pkg::*;
   `include "vtalu_macros.svh"

   vtalu_bfm bfm ();
   vtalu DUT (
       .A(bfm.A),
       .B(bfm.B),
       .op(bfm.op_set),
       .clk(bfm.clk),
       .reset_n(bfm.reset_n),
       .start(bfm.start),
       .done(bfm.done),
       .ovf(bfm.ovf),
       .result(bfm.result)
   );

   initial begin
      // null and "*": from the root and visible to the whole tree. See the
      // uvm_config_db slide.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

      // UVM reads +UVM_TESTNAME, and builds THAT test with the factory.
      run_test();
   end

endmodule : top

The virtual interface gets passed through the config_db because classes do not see the signals of the module: bfm lives in top, and random_test is an object that gets created at simulation time. There is no way to hand it over through the constructor — uvm_component's constructor only accepts name and parent. It is worth pointing at the order: first the set, then run_test(). The other way round the test gets built before the data is in the database and the get fails with the fatal. It is a mistake you rarely see because almost nobody writes the top the wrong way round, but it explains why the set cannot go in a separate initial. The slide that follows is the one to explain well: it is mistake number one of whoever starts with UVM.

see this slide in the deck ›

Tests

uvm_config_db: the testbench's mailbox

code/u4/tests/config_db.svh
// ILLUSTRATION -- not compiled. The two halves of the config_db, side by side:
// the set() lives in top.sv and the get() in tb_classes/driver.svh.
// -- IN THE TOP -- top is a module, not a uvm_component ---------------------
//   cntxt = null : the scope starts at the UVM root
//   inst  = "*"  : visible to the whole component tree
uvm_config_db #(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

// -- IN A COMPONENT -- always inside build_phase ----------------------------
//   cntxt = this : the scope is the hierarchical path of THIS component
//   inst  = ""   : with no suffix, the path is exactly the component's
if (!uvm_config_db #(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
  `uvm_fatal("DRIVER", "Failed to get BFM")

// The 3rd argument ("bfm") is the NAME of the datum inside the database.
// If the string in the set and the one in the get are not identical, this still
// compiles and blows up at run time with a null pointer.

The first two arguments are where, the third is what. In the set of the top it is null, "*" because top is a module, it is not in the UVM tree: the translation is "from the root, visible to everybody". In the get it is this, "", that is, "me". It is worth writing on the board the path that comes out: uvm_test_top.env_h.driver_h. And the shortcut to warn against from the start: get(null, "*", ...) also compiles and also works — it searches the whole tree and keeps the first thing it finds. With a single interface you never notice. The day the testbench has two, the driver of bus A can end up driving bus B, and it does not fail: it lies. It is one of those mistakes that get paid for three months later.

see this slide in the deck ›

Tests

A test in three parts: 1 · register it

code/u4/tests/tb_classes/random_test.svh · #class-head
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

It is the first time the factory shows up for real, and you still cannot see what it is for: here it only acts as a directory of names. The second half —creating by type and being able to substitute it from outside— arrives in the env with the overrides. The semicolon after the macro is redundant and UVM tolerates it. It is in the code of the book and we left it the same so that whoever compares does not get dizzy, but if somebody asks: it is not needed.

see this slide in the deck ›

Tests

2 · the constructor and the build_phase

code/u4/tests/tb_classes/random_test.svh · #constructor-and-build
   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

The fixed signature is the first thing everybody breaks: a constructor with one argument too many and the factory cannot create it, because it always calls with (name, parent). build_phase is a phase, not a function you call yourself: UVM walks it top-down over the already assembled tree. In this test you cannot tell yet, because the tree is a single node; in the components, when the env builds its children, the top-down order is what makes the parent exist before the child. The fatal is not paranoia: without it, a typo in the name of the data —"bfm" against "BFM"— gives a simulation that runs, sends not one stimulus and finishes with 0 errors.

see this slide in the deck ›

Tests

3 · the run_phase: this is where the simulation happens

code/u4/tests/tb_classes/random_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

It is worth showing it next to the tb.execute() of the object-based testbench: the stimulus did not change by one line. The only thing we did was move it inside a class that UVM knows how to create and call. That is the whole job of this unit. join_none and not join: coverage and scoreboard are infinite loops that watch the signals. If we waited for them to finish, it never finishes. And the one people miss: the simulation does not last as long as the run_phase lasts, it lasts as long as the objections last. That is what comes now.

see this slide in the deck ›

Tests

Objections: who decides when it ends

task run_phase(uvm_phase phase);
   phase.raise_objection(this);    // "I still have work"
   ...                             // the stimulus
   phase.drop_objection(this);     // "done, as far as I am concerned it can end"
endtask

It is the mechanism that decides how long the simulation lasts, and it is the one that produces the two classic hangs of the first week. Worth writing the two failures on the board, because the student is going to see them before any DUT bug. The rule of thumb: the objection gets raised by whoever has work, and dropped by the same one. In this course it is always the test. Never raise it in a component and drop it in another one — that is how a real testbench hangs. And a third participant is missing, which is the next slide: the objection knows when the test finished sending, not when the scoreboard finished comparing. That is the difference the drain_time pays for. And the day 6 hook: set_automatic_phase_objection(1) is this same thing, done by the sequencer instead of by the test.

see this slide in the deck ›

Tests

The end that is not the end: drain_time

task run_phase(uvm_phase phase);
   phase.get_objection().set_drain_time(this, 200ns);  // a cushion after the last drop
   phase.raise_objection(this);
   ...                                                 // the stimulus
   phase.drop_objection(this);
endtask

It is the flip side of the previous slide and it is worth saying it in those words: the objection measures the stimulus, not the analysis. The test knows when it finished sending; the scoreboard is the one that knows when it finished comparing, and nobody asked it. The drain_time is the cheap answer and it is enough for 90 % of the cases: a fixed cushion after the last drop_objection, in the order of what the DUT takes to answer. In this course it would be a couple of cycles; on a bus with latency, whatever the spec says. phase_ready_to_end() is the expensive answer and the one used in a serious testbench, because it is not a number: the scoreboard looks at its own FIFO, and if it has items left it raises one more objection. The shape is always the same —check phase.get_name() == "run", and raise only if something really is missing—, and the trap has to be said: if whoever implements it never drops it, they hung the phase. The third piece, for whoever wants to hook in at exactly that instant, is all_dropped: a callback of the objection object that fires when the count reaches zero. And the hook forward: the scoreboard with a FIFO of the analysis ports is exactly the case of this slide, and the capstone steps on it again.

see this slide in the deck ›

Tests

The timeline, end to end

Timeline of an objection: raise, stimulus, drop, drain_time and the end of the phase, with the transactions still in flight

The figure is there to be paused on, asking where the simulation ends. The answer almost everybody gives the first time —*"when the test finished sending"*— is exactly the one that produces the bug: that is where the last objection drops, and that is where the phase cuts if nobody asked for a cushion. It is worth pointing at the three empty boxes: they are transactions the DUT has not answered yet. They are not lost through anybody's mistake, they are in flight, which is the normal state of any pipeline. What decides whether they get compared or not is how much longer the phase lives. The detail worth saying out loud: the drain_time is measured from the last drop, not from the beginning. If the test sends a thousand operations and the DUT takes two cycles to answer, the cushion is two cycles and not two thousand. And the version with a criterion —phase_ready_to_end()— is this same figure with one difference: the length of the cushion gets decided by the scoreboard looking at its FIFO, instead of being decided up front by whoever wrote the test.

see this slide in the deck ›

Tests

The second test: the same thing, with another tester

code/u4/tests/tb_classes/add_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      add_tester add_tester_h;
      coverage   coverage_h;
      scoreboard scoreboard_h;

      phase.raise_objection(this);

      add_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      add_tester_h.execute();
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 37 líneas
class add_test extends uvm_test;
   `uvm_component_utils(add_test);

   virtual vtalu_bfm bfm;

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

   // Same build_phase as random_test: the virtual interface is read from the
   // config_db, with this and "" as the scope. See random_test.svh.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("ADD TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      add_tester add_tester_h;
      coverage   coverage_h;
      scoreboard scoreboard_h;

      phase.raise_objection(this);

      add_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      add_tester_h.execute();
      phase.drop_objection(this);
   endtask : run_phase

endclass

The duplication should not be apologized for, it should be pointed at. The student has to finish this slide with the feeling of "this cannot be right", because that feeling is exactly the reason for the components and the env. If somebody jumps ahead and proposes a base class with the common run_phase and a virtual method to pick the tester: that is fine, and it is more or less what base_test does in the sequences. Tell them so and move on.

see this slide in the deck ›

Tests

How it gets run, and what it prints

code/u4/tests/run.sh · #the-run
set -e
. "$(dirname "${BASH_SOURCE[0]}")/../../verilator/common.sh"
vlt_uvm top --coverage-user -Wno-fatal -f dut.f -f tb.f
run_sim +UVM_TESTNAME=random_test
run_sim +UVM_TESTNAME=add_test
cov_report
el archivo entero, 14 líneas
#!/bin/bash
# Runs the example with Verilator. See docs/verilator.md.
#
# --coverage-user: measures the covergroups and leaves out line/toggle/branch,
# which are not the topic. The number comes out at the end, with cov_report.
#
# -Wno-fatal: the examples have sloppy widths on purpose (WIDTHEXPAND/WIDTHTRUNC)
# and by default any warning stops the build. They still get printed.
set -e
. "$(dirname "${BASH_SOURCE[0]}")/../../verilator/common.sh"
vlt_uvm top --coverage-user -Wno-fatal -f dut.f -f tb.f
run_sim +UVM_TESTNAME=random_test
run_sim +UVM_TESTNAME=add_test
cov_report
code/u4/tests/output.txt · líneas 1-13 de 27
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 46340: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 46ns
el archivo entero, 27 líneas
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 46340: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 46ns
    seed: la de Verilator por defecto, que tambien es fija
UVM_INFO @ 0: reporter [RNTST] Running test add_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 40540: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 41ns

Worth running it live and timing it: the compilation takes minutes, the two run_sim take seconds. That is the number that justifies all the ceremony of the unit. The $finish at 46ns at the bottom is from the first test; add_test ends at 41 ns. Both send the same number of operations, but the multiplication takes more cycles than the addition, so the time depends on what came out of the random. And a useful warning: UVM_ERROR : 0 means "nobody called uvm_error", not "the DUT is fine". Here the scoreboard does report with `uvm_error, so the count is worth something — but a scoreboard that never receives anything gives exactly the same zero. Reporting shows how to tell them apart.

see this slide in the deck ›

Tests

Summary of the unit

Closing of the first real UVM unit. It is worth acknowledging the discomfort out loud: today they put in a lot of ceremony to do the same thing they were already doing. What they bought is the first bullet, and it does not pay out until there are two tests — which is the env. The mistake they are going to walk into and it is worth anticipating: the get() that forgets to check the return value. It does not fail, it lies: it leaves the virtual interface at null and the testbench falls over three layers further down. That is why every get of the course goes inside an if with its uvm_fatal.

see this slide in the deck ›

Components and phases

A component is what is in the tree, and the tree is walked by UVM

The sentence on the slide is the one that orders the unit: a component is what is in the tree. Everything else —the name, the parent, the phases, the print_topology of the agents— comes out of there. The practical consequence that has to be said out loud: if something is not a component, UVM does not see it. It does not build it, it does not call phases on it and it does not show up in the topology. Later on we are going to have objects that on purpose are not components —the transactions— and that is where the distinction buys you something. The three axes —structure, sequences, data— work as a map of the rest of the course: components and env are structure, transactions are data, sequences are sequences.

see this slide in the deck ›

Components and phases

Intro UVM phases

Three things that have to be said no matter what: build_phase is top-down, connect_phase is bottom-up, and every run_phase runs in parallel, each one in its thread. And the classic mistake: creating components after build has ended. UVM does warn you, and well: ILLCRT tells you which component and under which parent.

see this slide in the deck ›

Components and phases

The tree, twice: one walk goes down and the other goes up

The tree walked twice: build_phase goes down and connect_phase goes up, and the real order in which a child gets built

The drawing answers two questions that in the table read like an arbitrary convention. The first one: why build_phase is top-down. Because a parent has to exist in order to create its children, and there is no other way to assemble a tree; the ones in the middle need the opposite, that the children are already there. The second one is the one asked under one's breath: if the parent creates the child inside its own build_phase, when does the build_phase of the child run? Later, and that is the part that has to be said slowly: create() calls the constructor, not the phase. UVM walks the tree calling the phase on the components that showed up along the way, so the child gets built in two steps. The practical consequence is the one in the last bullet, and it is the one that buys you something: a uvm_config_db::set() written after the create() of the child arrives late, the child already read, and the field stays at its default without anybody warning. It is the same asymmetric failure mode as the next slide. If somebody asks about the code: uvm_topdown_phase::traverse runs the phase on the component and only then walks its children —get_first_child()—, which by then already exist.

see this slide in the deck ›

Components and phases

And what about super.build_phase()?

// uvm_component.svh:2385-2393 -- uvm-core 2020.3.1, with the return; elided
function void uvm_component::build_phase(uvm_phase phase);
   build();            // -> apply_config_settings(): the automatic config
endfunction

function void uvm_component::connect_phase(uvm_phase phase);
   connect();          // -> return;  it is empty, like all the others
endfunction

This slide exists because the student is going to see super.build_phase(phase) in every piece of material on the internet and in most of the course's build_phase it is not there. The honest answer is not "they forgot": it is that without the field macros the call to uvm_component's does nothing. The numbers, so that nobody has to take our word for it: grep -rn 'super\.build_phase' code/ --include='*.sv' --include='*.svh' --exclude-dir=.uvm gives 15 real calls —plus 6 comments that talk about them— over 306 build_phase. Those 15 call the super of a base_test or a random_test of ours, which builds the env. None of them calls uvm_component's, which is what the slide is about. The other underlying difference: there are two ways of configuring a component. The automatic one (`uvm_field_* + config_db, magic by macros) and the manual one (uvm_config_db::get() in the build_phase, which is the one the course uses). The manual one is more code and vastly easier to debug — the field macros generate hundreds of lines you are never going to read. Why the recommendation for out there is the opposite of what the course does: the failure mode is asymmetric. Putting it in for nothing has zero effect. Leaving it out when it was needed is silence: the field stays at its default and the simulation runs. This course can name why in its case it is a no-op —there is not a single `uvm_field_* macro in code/ outside the vendored library —there are 0, and the build counts them—; whoever walks into somebody else's testbench cannot. uvm_agent is the counterexample we have at hand: besides uvm_component, it is implemented by uvm_agent —which reads is_active there— and by the two sequencer bases (seq/uvm_sequencer_base.svh:122, which reads wait_for_sequences_count from the config_db, and seq/uvm_sequencer_param_base.svh:236). It comes back on day 6. The same goes for the other phases, cheaper still: in uvm_component they are return;, but uvm_driver::end_of_elaboration_phase checks that the seq_item_port is connected, and that one we do extend.

see this slide in the deck ›

Components and phases

The full picture: there are nine, not five

Phase What for Order
build_phase instantiate the components top-down
connect_phase connect the ports bottom-up
end_of_elaboration_phase hierarchy ready, before simulating bottom-up
start_of_simulation_phase last call before time 0 bottom-up
run_phase task: this is where the simulation happens one thread each
extract_phase gather the data from the run bottom-up
check_phase decide whether it passed bottom-up
report_phase print the verdict bottom-up
final_phase close files and exit top-down

The table is there so that nobody leaves believing UVM has five phases: it has nine common ones and twelve runtime ones. The course uses five because with a single agent that is enough, and that has to be said — it is not that the others do not matter. The three you are most likely to run into outside: start_of_simulation_phase is where people print the configuration banner of the test; check_phase is where a tidy scoreboard decides the verdict, instead of counting errors along the way; and report_phase is the one they have already seen. That extract / check / report are separate has a concrete reason: they are bottom-up, so a child scoreboard finishes extracting before the parent env decides. If you do the three things in report_phase, you lose that guarantee. The question that orders the table: why is build_phase top-down? Because a parent has to exist in order to create its children. The ones in the middle need the opposite — that the children are already there. The other top-down one is final_phase, which closes the tree in the same order it was built.

see this slide in the deck ›

Components and phases

And inside the run_phase, a schedule

The twelve runtime phases running in parallel with run_phase, and where the default_sequence hooks in

This slide exists so that main_phase does not show up for the first time on day 6. The exact distinction, which confuses everybody: run_phase and the schedule reset → configure → main → shutdown run at the same time, not one inside the other. A component can implement either of the two paths; what is not advisable is mixing them in the same testbench, because then nobody knows what runs when. The honest question is "so which one do I use?". For a testbench of a single block, run_phase, which is what the course does. The schedule earns its keep in integration: the agent of the configuration bus finishes its configure and only then does the data one start its main, without either of the two teams having had to write a uvm_event or a shared flag.

see this slide in the deck ›

Components and phases

The scoreboard, now as a component

code/u4/components/tb_classes/scoreboard.svh · #class-and-build
// The scoreboard of the TB in objects, now as a uvm_component. The four steps
// --extend, register, constructor, phases-- are in the slides of the components section.
class scoreboard extends uvm_component;
   `uvm_component_utils(scoreboard);

   virtual vtalu_bfm bfm;

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

   // Self-sufficient: the component asks the config_db for the BFM. Before, the
   // test passed it in through the constructor, and the test had to receive it
   // only to hand it out.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("SCOREBOARD", "Failed to get BFM")
   endfunction : build_phase
el archivo entero, 51 líneas
// The scoreboard of the TB in objects, now as a uvm_component. The four steps
// --extend, register, constructor, phases-- are in the slides of the components section.
class scoreboard extends uvm_component;
   `uvm_component_utils(scoreboard);

   virtual vtalu_bfm bfm;

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

   // Self-sufficient: the component asks the config_db for the BFM. Before, the
   // test passed it in through the constructor, and the test had to receive it
   // only to hand it out.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("SCOREBOARD", "Failed to get BFM")
   endfunction : build_phase

   // run_phase is the only phase that is a task: the only one that consumes
   // time. UVM launches it in its own thread.
   task run_phase(uvm_phase phase);
      shortint predicted_result;
      bit      predicted_ovf;
      forever begin : self_checker
         @(posedge bfm.done) #1;
         case (bfm.op_set)
            add_op: predicted_result = bfm.A + bfm.B;
            sub_op: predicted_result = bfm.A - bfm.B;
            and_op: predicted_result = bfm.A & bfm.B;
            xor_op: predicted_result = bfm.A ^ bfm.B;
            mul_op: predicted_result = bfm.A * bfm.B;
         endcase  // case (op_set)

         // ovf belongs to sub and to nobody else: with 8-bit inputs and a 16-bit
         // output, neither the addition nor the multiplication can overflow.
         predicted_ovf = (bfm.op_set == sub_op) && (bfm.A < bfm.B);

         if ((bfm.op_set != no_op) && (bfm.op_set != rst_op))
            if (predicted_result != bfm.result || predicted_ovf != bfm.ovf)
               `uvm_error("SCOREBOARD", $sformatf(
                          "FAILED: A: %0h  B: %0h  op: %s result: %0h ovf: %0b",
                          bfm.A,
                          bfm.B,
                          bfm.op_set.name(),
                          bfm.result,
                          bfm.ovf
                          ))
      end : self_checker
   endtask : run_phase
endclass : scoreboard

The point of the slide is self-sufficiency. A component that needs somebody to hand it its dependencies forces that somebody to know them, and the test ends up being a delivery service for handles it does not use. With the config_db, the test does not know that the scoreboard needs a BFM. That is exactly what makes the env able to exist: if every component configures itself, the parent only has to build it. The price has to be said too: the dependency stopped being in the constructor, where it could be seen, and moved into a string. A misspelt "bfm" compiles. That is why the if (!get(...)) uvm_fatal is not optional.

see this slide in the deck ›

Components and phases

The test builds the tree and steps aside

code/u4/components/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   random_tester tester_h;
   coverage      coverage_h;
   scoreboard    scoreboard_h;

   // Components get instantiated in build_phase, never in the constructor.
   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction : build_phase

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

   // No run_phase: the test's job ends once the tree is built.

endclass

It is the change of mindset of the unit, and it is worth saying it in these words: the test stopped doing the test. Now it assembles it and steps aside. The objection also moved: it is no longer raised by the test, it is raised by the components that have work — or, in this example, by the tester. Worth showing random_tester.svh for a second so that it can be seen. And the trap that is going to show up on its own: new() works here because the type is written in the declaration. In the env that gets replaced by type_id::create(), and that is the change that enables the overrides. Not yet, but it is worth planting.

see this slide in the deck ›

Components and phases

The second test: it inherits, and it copies anyway

code/u4/components/tb_classes/add_test.svh
// add_test extends random_test, redeclares tester_h with another type and
// REWRITES the whole build_phase. That duplication is the problem the env section solves
// with the factory override.

class add_test extends random_test;
   `uvm_component_utils(add_test);

   add_tester tester_h;

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

   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction

endclass

This is the slide that has to be left uncomfortable. If the student walks out thinking "this is still wrong", the env explains itself. The shadowing of the handle deserves a minute: in the base class there is a random_tester tester_h and here an add_tester tester_h. Any method inherited from random_test that uses tester_h is going to see the base one, which stayed at null. Here it does no harm because there is none, but it is a time bomb and it is exactly the kind of silent mistake the course goes after. The hook: in the env there are not two tests, there is one; and add_test becomes a line of set_type_override_by_type.

see this slide in the deck ›

Components and phases

Summary of the unit

The unit that turns the testbench into something UVM can walk, and with that the tools that did not exist before show up: print_topology() shows the tree, and the uvm_config_db can use paths because now there are paths. The super.build_phase() deserves the pass we gave it because it is the most repeated question in the forums. What has to be said out loud is the asymmetry: not calling it switches off the automatic assignment of the fields registered with `uvm_field_*, and that does not give an error, it gives a default. This course does not use those macros and that is why it can skip it; the student who lands in somebody else's testbench does not know whether they can, so they put it in. What is not valid is doing half of each.

see this slide in the deck ›

The env: structure and stimulus

Two things that change for different reasons

Hinge section of the course, and the sentence to leave behind is this one: what changes together goes together, what changes separately goes separately. Everything that comes afterwards —agents, sequences— is this same operation applied to something else. If the group is going slowly, this is the unit where it is worth spending whatever time is left over, even if it has to be taken from somewhere else. A student who does not understand why the env and the test are two classes is not going to be able to read any production testbench, because they are all built like this. The question to start with, before showing one line of code: if tomorrow the design adds a second interface, how many of your ten tests have to be touched? None, if the structure lives in a single place. All ten, if each test assembles it.

see this slide in the deck ›

The env: structure and stimulus

Intractable vs. adaptable

"Playing football is very simple, but playing simple football is the hardest thing there is." — Johan Cruyff

The same move: tangled and simple

Swap football for testbench and Cruyff's line is the whole unit. The three rules sound like an office poster until you measure them, so it is worth landing them on the testbench in front of them: One class, one thing — the random_test of the components does two: it assembles the structure and it picks the stimulus. That is why it has to be copied to make add_test. Nothing hardcoded — tester_h = new(...) nails the type into that line. The type_id::create() does not. Against the interface — the env is going to ask for a base_tester, which is an abstract class: it neither knows nor cares which one arrives. And an honest warning, because at work this gets abused: adaptable code does not mean code prepared for everything. Every point of flexibility is paid for in indirection, and the indirection is paid for at three in the morning. What gets separated is what has already been shown to change — and here it has been shown: there are two tests and there is already copy and paste.

see this slide in the deck ›

The env: structure and stimulus

The problem: two tests, the same build_phase() twice

random_test and add_test write the same build_phase()

This is the problem slide, and it is worth not jumping ahead to the solution: let the duplication be seen first, and let it be seen where it is. It is not in the testers —those already inherit properly— but in the tests, which is where nobody looks for it. The question that orders the next two slides: which part of a tester is the test, and which part is infrastructure? The get_op() is the test —four lines—; all the rest is infrastructure that has nothing to do with what you want to prove. And the general criterion, which holds for any language: when two sibling classes share an identical method, the method is in the wrong place. It goes to the parent.

see this slide in the deck ›

The env: structure and stimulus

The way out: an abstract class, and the stimulus in the daughters

abstract base_tester with a shared run_phase() and pure virtual methods

This is where polymorphism gets collected: base_tester is an abstract class with pure virtual methods, that is, the run_phase() is written a single time and the daughters are forced to define get_op() and get_data(). If one forgets, it does not compile — which is the whole argument of that unit. And along the way the trap of the day 2 exercise gets fixed: here get_op() is virtual, because it is pure virtual. Worth going back two days and showing it. The bullet that matters is the last one, and it is the one that pushes into the next slide: the two testers still randomize the operands the same way. Two sisters sharing code is a sign that a step is missing in the hierarchy — and the step is that add_tester should not inherit from base_tester but from random_tester.

see this slide in the deck ›

The env: structure and stimulus

The result: the three-storey hierarchy

random_tester as the base and add_tester inheriting from it

The jump to point out is the third arrow: add_tester does not inherit from the abstract one, it inherits from the concrete one. It is counter-intuitive the first time and it is the decision that makes a new test cost four lines. The rule, said in a way that sticks: you inherit from whoever has what you do not want to write again. Not from the one that "conceptually corresponds". And the hook backwards: this is polymorphism used for real. pure virtual in the parent is what guarantees that no tester forgets to define get_op() — and if it forgets, it does not compile.

see this slide in the deck ›

The env: structure and stimulus

add_tester

code/u4/env/tb_classes/add_tester.svh
class add_tester extends random_tester;
   `uvm_component_utils(add_tester)

   function operation_t get_op();
      return add_op;
   endfunction : get_op

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

endclass : add_tester

This is the slide of the section worth leaving on screen for a while: a whole test of the VTALU is four useful lines. Everything else —sending a thousand operations, randomizing operands, talking to the BFM— is already written and does not get touched. The rule you take away: if adding a test costs four lines, you are going to write many. If it costs copying a class of sixty, you are going to write three and then invent excuses. The coverage of the project is decided here, not in the covergroup. And the question that orders the hierarchy: why does add_tester inherit from random_tester and not from base_tester? Because it wants the random operands and only changes the operation. Inheritance is not chosen by conceptual likeness, it is chosen by what you do not want to write again.

see this slide in the deck ›

The env: structure and stimulus

The other half: who instantiates whom

code/u4/components/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   random_tester tester_h;
   coverage      coverage_h;
   scoreboard    scoreboard_h;

   // Components get instantiated in build_phase, never in the constructor.
   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction : build_phase

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

   // No run_phase: the test's job ends once the tree is built.

endclass

It is worth doing the arithmetic out loud, because the problem is easier to see with numbers than with the design argument: if every test instantiates its three components, with ten tests there are ten build_phase saying the same thing. The day a fourth component is added to the testbench, that is ten edits — and the one you forget does not fail at compile time, it simply runs without that component. uvm_env is not magic and it does not bring new behaviour: it is a uvm_component with another name. What it contributes is convention: when somebody opens a testbench that is not theirs, they know the structure is in the env. That is half the value of UVM and it is worth saying it that bluntly — it is not the library, it is that everybody puts the things in the same place. Good question: why does the env almost never have a run_phase? Because it neither sends nor watches anything; it only builds and connects. If your env has a run_phase, it is a sign that test logic got inside the structure.

see this slide in the deck ›

The env: structure and stimulus

The map after splitting

The class diagram before and after splitting structure from stimulus with uvm_env

Worth leaving the diagram on screen and asking them to place where each class of the components ended up. The exercise makes it obvious what disappeared: there is no longer a build_phase per test instantiating three components. The control question: if tomorrow the testbench adds a fourth component, how many files get touched? One, the env. With the structure of the components it was as many as there were tests.

see this slide in the deck ›

The env: structure and stimulus

The env class: instantiate and connect, nothing else

code/u4/env/tb_classes/env.svh
// The structure of the TB, and nothing else: it instantiates the components and
// connects them. Which tester arrives is the test's call, with a factory override.
class env extends uvm_env;
   `uvm_component_utils(env);

   base_tester tester_h;
   coverage    coverage_h;
   scoreboard  scoreboard_h;

   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase

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

endclass

Hinge section of the course: separating WHAT structure the testbench has (env) from WHAT stimulus gets sent (test). Everything that comes afterwards —agents, sequences— is this same idea taken further. If the group is going slowly, this is the slide where it is worth spending whatever time is left over.

see this slide in the deck ›

The env: structure and stimulus

UVM's factory: the class signs itself up

The macro is what replaces the case of the hand-written factory: each class signs itself up in the dictionary, and adding a type does not force you to edit the factory. The difference between the two macros is the one that gets collected in the transactions, and it is worth planting now: uvm_component_utils is for what lives in the tree and receives phases; uvm_object_utils is for what travels —a piece of data— and receives none. Picking the wrong one compiles and gives strange errors later. Not having to cast is not cosmetic: the $cast of the factory could be forgotten, and forgetting it was a null pointer in simulation.

see this slide in the deck ›

The env: structure and stimulus

The formula, read from left to right

Anatomy of the line type::type_id::create(name, this)

Worth reading the formula out loud once, slowly, because it is going to be written fifty times in what is left of the course: "class, colon colon, type_id, colon colon, create, name, this". The last bullet is the one that is not reassuring, and it should be said that way. If base_tester was not registered and nothing above it was either, base_tester::type_id does not exist and it does not compile: that is the good case. But in this very section's hierarchy —the three of them registered— forgetting the macro in add_tester breaks nothing: the type_id and the get_type() are inherited from random_tester, the testbench compiles clean, and the set_type_override of add_test ends up registering random_tester. Measured: add_tester::get_type().get_type_name() returns random_tester, the env builds a random_tester, and the test runs with UVM_ERROR : 0. It belongs to the "does not break, lies" family of the silent-traps appendix: the only case where it does fail to compile is if the handle is declared of the derived type, and here the handle is base_tester tester_h;. The name of the first argument is not decorative: it is the path of the component, the one the config_db uses and the one print_topology prints. Giving it a name other than the handle's is a cheap way of losing half an hour in the agents.

see this slide in the deck ›

The env: structure and stimulus

The objection that comes on its own: base_tester is abstract

code/u4/env/tb_classes/env.svh · #build_phase
   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase
el archivo entero, 22 líneas
// The structure of the TB, and nothing else: it instantiates the components and
// connects them. Which tester arrives is the test's call, with a factory override.
class env extends uvm_env;
   `uvm_component_utils(env);

   base_tester tester_h;
   coverage    coverage_h;
   scoreboard  scoreboard_h;

   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase

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

endclass

The objection on the slide is correct and it has to be taken seriously, because otherwise it looks like a dirty trick: base_tester is abstract and that code does compile and run. The explanation in one line: type_id::create() does not build a base_tester; it asks the factory what to build when somebody asks for a base_tester, and returns that. The analogy that works: base_tester here is not a mould, it is an order form. Nobody instantiates the form. The corollary, which is the one paid for later: if no test did the override, the factory really does try to build the abstract class and there it really does explode — and the message does not say "you were missing the override", it says something about an abstract class. Worth showing it once by commenting out the set_type_override of random_test.

see this slide in the deck ›

The env: structure and stimulus

The override: changing the part without touching the plan

The sentence that orders the slide: the override does not change the code, it changes what the factory returns. The env still says exactly the same thing. It is worth naming the two forms that exist, because both show up in code that is not yours: set_type_override changes every request for that type in the whole testbench; set_inst_override changes only the ones on one path — "uvm_test_top.env_h.tester_h", absolute—. The course uses the first because there is a single tester; with two agents, the second is the one that helps, and it has its slide two further on. And the hook forward: it is exactly the mechanism that in the transactions is going to replace a command_transaction with a maxmult_transaction. Same call, another type.

see this slide in the deck ›

The env: structure and stimulus

The factory override

Anatomy of the factory override with set_type_override()

The override is what makes the env never change: the test asks for a base_tester and the factory returns whichever one that test wants. When somebody asks whether this is not black magic: it is a dictionary of types, and that is why everything has to be registered with `uvm_component_utils. If you forget it, the factory does not find the class and the error message does not help at all.

see this slide in the deck ›

The env: structure and stimulus

The factory override, in the test

code/u4/env/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   env env_h;

   function void build_phase(uvm_phase phase);
      base_tester::type_id::set_type_override(random_tester::get_type());
      env_h = env::type_id::create("env_h", this);
   endfunction : build_phase

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

endclass

The whole test is two lines: the override and the create() of the env. Worth showing it next to the random_test of the components, which instantiated three components by hand. That is the result of the section, measured. And there is a detail of order that is paid for dearly and that is worth saying now, because it comes back as question 5 of the review: the set_type_override() goes before the create() of the env. The factory decides what to build at the moment of the create(), not before and not after. If the override arrives late, there is no error — the env has already built the base class and the test runs with the wrong stimulus. The same thing again: it does not break, it lies. With that the arc is said: the env never changes, the test always changes, and the only thing that binds them is a type string in the factory.

see this slide in the deck ›

The env: structure and stimulus

By instance, and the knob for seeing what got set

base_tester::type_id::set_type_override(add_tester::get_type());          // ALL of them
base_tester::type_id::set_inst_override(mult_tester::get_type(),
                                        "uvm_test_top.env_h.t2_h");      // just one

uvm_coreservice_t::get().get_factory().print(0);   // which overrides are in place
Instance Overrides:
  Requested Type  Override Path            Override Type
  base_tester     uvm_test_top.env_h.t2_h  mult_tester
  base_tester     env_h.t3_h               mult_tester   <- registered and did NOT apply
Type Overrides:
  base_tester     add_tester

It is the second half of the override and the one that shows up as soon as there are two agents of the same type: you do not want to change both, you want to change one. The path trap has to be said with the measurement's number, because it is exactly the way this mechanism fails: the override with the badly written path gets registered all the same. factory.print(0) shows it in the table, tidy, as if it were working. There is no warning, there is no error, and the component comes out of the base type. It is the same family as the late set_type_override of the traps appendix: the factory does not validate paths because it cannot — when the override is registered, the component does not exist yet. The practical rule: the path is the one uvm_top.print_topology() prints, without the <unnamed> at the top. If the tree says uvm_test_top.env_h.t2_h, that is what goes between quotes. And if you do not want to depend on where they instantiate you, *.env_h.t2_h, which is also measured and also applies. The pair of tools is what to take home: factory.print(0) says what the factory has written down, print_topology() says what actually got built. An override that shows up in the first and is not visible in the second is a badly written path or a create() that arrived first.

see this slide in the deck ›

The env: structure and stimulus

Summary of the unit

It is the unit where all of day 2 gets collected. It is worth making it explicit by pointing at the add_test: it is two lines —the override and nothing else—, and it changes the whole stimulus without touching the env. That could not be done in the object-based testbench. The intractable / adaptable distinction is the one to leave as vocabulary, because it comes back with the agent and with the sequences. Intractable code is the one that comes out fast and afterwards will not let itself be touched: almost always it is duplicated structure.

see this slide in the deck ›

Reporting

47 % of the time goes here

It is worth going back to the day 1 chart before starting, because this section looks like plumbing and it is the one that returns the most hours. The argument is not "nice messages": it is that a message that brings along the hierarchical path saves you the question "who said it?", which is half of any debug. The second idea, which is the one that costs: debug messages do not get deleted. A `uvm_info at UVM_HIGH costs nothing when the ceiling is at UVM_MEDIUM, and the day you need it, it is there. That the day 5 exercise gets solved by raising the verbosity is no coincidence — it is set up so that they live it. And the distinction on the last line is worth writing on the board from the start, because it is question 6 of the review: verbosity and info on one side, actions and error/warning/fatal on the other. They never cross.

see this slide in the deck ›

Reporting

What $display cannot do

The question that orders the slide, and it is worth throwing it at the group before the answer: what is wrong with $display? The answer that always comes is "it is ugly", and it is not that one. $display prints or does not exist: the decision of whether a message comes out is taken at the moment of writing it, by editing the code. What UVM adds is not format, it is that the decision moves to the moment of running, and it is taken from outside with a plusarg. That change of moment is the whole section: that is why the messages can be left in place, and that is why the log of a regression and the log of a debug can come out of the same binary. The piece of context, for whoever comes from VHDL or plain Verilog: none of this is simulator magic. `uvm_info is a macro that assembles a string and hands it to an object —the report server— that decides what to do. Everything that follows in the section is configuring that object.

see this slide in the deck ›

Reporting

The reporting macros

code/u4/reporting/reporting.sv
// ILLUSTRATION -- not compiled. The signature of each macro, not a call.
`uvm_info   (Message ID String, Message String, Verbosity)
`uvm_warning(Message ID String, Message String)
`uvm_error  (Message ID String, Message String)
`uvm_fatal  (Message ID String, Message String)

The usual confusion: verbosity filters `uvm_info, and nothing else. The warnings, errors and fatals have no verbosity — they are controlled with actions, which is the last part of the section.

see this slide in the deck ›

Reporting

The reporting macros: what each one does

code/u4/reporting/tb_classes/scoreboard.svh · #the-report
      data_str = $sformatf(
          " %2h %0s %2h = %4h (%4h predicted)",
          cmd.A,
          cmd.op.name(),
          cmd.B,
          t,
          predicted_result
      );

      if (predicted_result != t) `uvm_error("SCOREBOARD", {"FAIL: ", data_str})
      else `uvm_info("SCOREBOARD", {"PASS: ", data_str}, UVM_HIGH)
el archivo entero, 45 líneas
class scoreboard extends uvm_subscriber #(shortint);
   `uvm_component_utils(scoreboard);

   uvm_tlm_analysis_fifo #(command_s) cmd_f;

   function void build_phase(uvm_phase phase);
      cmd_f = new("cmd_f", this);
   endfunction : build_phase

   function void write(shortint t);
      shortint predicted_result;
      string data_str;
      command_s cmd;
      cmd.A = 0;
      cmd.B = 0;
      cmd.op = no_op;

      do
      if (!cmd_f.try_get(cmd)) `uvm_fatal("SCOREBOARD", "Missing command in self checker")
      while ((cmd.op == no_op) || (cmd.op == rst_op));

      case (cmd.op)
         add_op: predicted_result = cmd.A + cmd.B + 1;  //force an error on adds
         sub_op: predicted_result = cmd.A - cmd.B;
         and_op: predicted_result = cmd.A & cmd.B;
         xor_op: predicted_result = cmd.A ^ cmd.B;
         mul_op: predicted_result = cmd.A * cmd.B;
      endcase  // case (op_set)
      data_str = $sformatf(
          " %2h %0s %2h = %4h (%4h predicted)",
          cmd.A,
          cmd.op.name(),
          cmd.B,
          t,
          predicted_result
      );

      if (predicted_result != t) `uvm_error("SCOREBOARD", {"FAIL: ", data_str})
      else `uvm_info("SCOREBOARD", {"PASS: ", data_str}, UVM_HIGH)
   endfunction : write

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new
endclass : scoreboard
code/u4/reporting/tb_classes/scoreboard_error.txt
UVM_ERROR tb_classes/scoreboard.svh(39) @ 290: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] FAIL:  5b add_op ff = 015a (015b predicted)

Worth stopping on the log line and reading it out loud from left to right, field by field, because it is the one the student is going to look at a thousand times and nobody ever explains it: severity, time, file and line of the `uvm_error, and then uvm_test_top.env_h.scoreboard_h — the instance path. Only then comes the ID and the message. The pedagogical point is what is **not** in the code: the scoreboard did not print the time or its own name. The macro put them in. That is the whole argument in favour of never writing a $display in a testbench again. And the honest comparison with the $error of day 2: that scoreboard also made the test fail, but the context had to be written by hand — and there nobody writes the hierarchical path, because they do not know it.

see this slide in the deck ›

Reporting

The six levels, and why the message stays

code/u4/reporting/tb_classes/verbosidad.svh
// ILLUSTRATION -- not compiled. uvm_object_globals.svh:382-390, uvm-core 2020.3.1,
// copied verbatim: code/.uvm/ is downloaded and gitignored, so the slide cannot
// pull the enum straight out of the library.
typedef enum {
   UVM_NONE   = 0,
   UVM_LOW    = 100,
   UVM_MEDIUM = 200,  // the default: with nothing set, UVM_MEDIUM is what you get
   UVM_HIGH   = 300,
   UVM_FULL   = 400,
   UVM_DEBUG  = 500
} uvm_verbosity;

// Any message above the verbosity that was set does not get printed

The six levels, from least to most noisy: UVM_NONE, UVM_LOW, UVM_MEDIUM —the default one—, UVM_HIGH, UVM_FULL and UVM_DEBUG. UVM_FULL is the one nobody uses and the one that shows up in the enum on the slide, so it is worth naming. A message gets printed if its verbosity is less than or equal to the ceiling, so UVM_NONE always comes out. The rule of thumb worth handing down, because otherwise everybody invents their own: UVM_LOW for what you want to see in a regression —which test ran, how many transactions—; UVM_MEDIUM for the summary of a component; UVM_HIGH for every transaction that goes past. The monitors of the course are UVM_HIGH precisely for that: in a normal run they are not seen, and when something fails they get switched on with a plusarg. The formal detail that gets copied wrong: the verbosity is the third argument of the `uvm_info, and if you forget it, it does not compile. The ID —the first one— is a free string, but do not pick it at random: it is the key you are going to filter with afterwards. Make it the name of the component, in capitals, and always the same one.

see this slide in the deck ›

Reporting

The global ceiling: a plusarg, without recompiling

code/u4/reporting/tb_classes/verbosity_ceiling.txt
./obj_dir/top/sim +UVM_TESTNAME=random_test +UVM_VERBOSITY=UVM_HIGH

Worth running it live, because it is the cheapest demonstration of the section: the same line above —./obj_dir/top/sim +UVM_TESTNAME=random_test— with and without +UVM_VERBOSITY=UVM_HIGH, and the log goes from 7 uvm_info to 22. With make they are 6 and 21, because run_sim adds +UVM_NO_RELNOTES and that takes the banner away. Without recompiling anything — it is the same binary. They look like few because this example sends ten operations on purpose, so the transcript fits on the screen; with the thousand of the other sections the difference is three orders of magnitude. The detail that has to be said so that it does not surprise anybody later: the plusarg sets the ceiling of the whole tree, from uvm_top down. There is no way of asking for "high, but only the monitor" from the command line with this flag; for that there is +uvm_set_verbosity, which is longer to type and almost nobody remembers. The honest way out, and the one the next slide teaches, is setting it from the code in the component you care about. And the underlying argument, in case the group underrates it: this is the mechanism that makes a regression log of a thousand tests readable. It is not classroom cosmetics.

see this slide in the deck ›

Reporting

uvm_cmdline_processor: the command line, whole

uvm_cmdline_processor clp = uvm_cmdline_processor::get_inst();
string valor;

if (clp.get_arg_value("+COUNT=", valor)) count = valor.atoi();

The course uses $value$plusargs and $test$plusargs in the base_test because they are one line and they read themselves. Worth saying why in a production testbench this class always shows up: $value$plusargs is a system task, so it cannot be inherited, it cannot be substituted by the factory and it cannot be tested. The uvm_cmdline_processor is an object, and that is enough for the three things. The argument that pays off most is the last bullet and it is not the one you would expect: get_args() printing the whole line in the start_of_simulation_phase is what turns an old log into something reproducible. Without that, the log says what happened but not what it was run with, and a regression from three months ago cannot be repeated. The difference in the second bullet is worth an anecdote: two +COUNT= on the same line —because the script adds them and the user does too— is a real case, and $value$plusargs takes one without saying which. The uvm_cmdline_processor returns 2, so at least you can notice. It is the kind of thing you pay for once and remember.

see this slide in the deck ›

Reporting

The ceiling per branch: two methods and a tree

Instance hierarchy UVM builds in build_phase

This diagram is worth more than it looks, because it explains where that very long prefix each message brings comes from: uvm_test_top.env_h.scoreboard_h. It is not decoration — it is the instance path of the component that spoke. And it is the same path the uvm_config_db uses for the scope. Worth saying it explicitly: when in the tests we said "the scope is a hierarchical path", it was literally this path. A single tree serves both purposes. The clarification in the bullet is not a detail: this hierarchy is not the DUT's. A scoreboard is not inside any module. It is the tree UVM assembles by calling build_phase from the top down, and it exists only in the testbench. Classroom trick: uvm_top.print_topology() prints this tree for real, written by the library. It is the best way of showing that it is not a drawing. +UVM_CONFIG_DB_TRACE is a different thing and it is better not to mix them: it does not draw the tree, it traces every set() and every get() of the config_db with the scope that was used — which is the knob for the other problem, the scope that does not match.

see this slide in the deck ›

Reporting

The ceiling per branch: where the call goes

code/u4/reporting/tb_classes/env.svh · #end_of_elaboration_phase
   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase
el archivo entero, 46 líneas
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester    random_tester_h;
   coverage  coverage_h;
   scoreboard scoreboard_h;
   driver    driver_h;
   command_monitor command_monitor_h;
   result_monitor result_monitor_h;
   uvm_tlm_fifo #(command_s) command_f;

   function void build_phase(uvm_phase phase);
      command_f = new("command_f", this);
      random_tester_h = random_tester::type_id::create("random_tester_h", this);
      driver_h = driver::type_id::create("driver_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);

   endfunction : build_phase

   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase

   function void connect_phase(uvm_phase phase);
      driver_h.command_port.connect(command_f.get_export);
      random_tester_h.command_port.connect(command_f.put_export);

      result_monitor_h.ap.connect(scoreboard_h.analysis_export);

      command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export);
      command_monitor_h.ap.connect(coverage_h.analysis_export);

   endfunction : connect_phase

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

endclass

The window is what has to be left burned in, because the mistake gets made in both directions. If the call goes in the build_phase, the component whose ceiling you want to raise does not exist yet —its parent builds it further down— and the call finds nobody. If it goes in the run_phase, the simulation has already started and you missed the messages of the earlier phases. end_of_elaboration_phase is exactly the gap between the two things: the whole tree, and the time still at zero. It is the first time in the course that it gets used for anything, and it is worth saying it that way — in the table of the nine phases of day 3 it was not decoration. The _hier suffix is the one that gets forgotten, and it fails silently: you set the ceiling of the env without _hier, the env prints nothing anyway, and the monitor you wanted to hear stays quiet. Rule of thumb: if you are aiming at a branch, _hier; if you are aiming at a class, without.

see this slide in the deck ›

Reporting

Switching off warnings, errors and fatal messages

code/u4/reporting/scoreboard1.txt
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO tb_classes/scoreboard.svh(40) @ 150: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] PASS:  43 and_op ff = 0043 (0043 predicted)
UVM_ERROR tb_classes/scoreboard.svh(39) @ 290: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] FAIL:  5b add_op ff = 015a (015b predicted)
...
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    4
UVM_WARNING :    0
UVM_ERROR :    2
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
[SCOREBOARD]     5
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 820ps

The log on the slide is that of a scoreboard that adds wrong on purpose, and it is worth looking at the Report Summary at the end before the error: UVM_ERROR : 2, one for each add_op that came up in the ten operations of the example. That is the sum to do out loud: with the thousand operations of the other sections it is a hundred-odd errors, one per addition. That is the real problem the section comes to solve — not "the message is annoying", but that the log stops being any use for looking for something else. The question to throw at the group, because the wrong answer is the intuitive one: "I lower the verbosity ceiling and that is it?". No. A `uvm_error has no verbosity; the ceiling does not touch it. It is the same distinction as the first slide, and here it gets collected for the first time. And the honest clarification before teaching how to switch off errors, because otherwise it sounds odd: this is not so that the regression comes out green. It is to be able to work half an afternoon while somebody else fixes their class, and it comes out the same day.

see this slide in the deck ›

Reporting

Switching off messages: where it gets done

code/u4/reporting/tb_classes/UVM_report_actions.sv
// ILLUSTRATION -- not compiled. uvm_object_globals.svh:354-364, uvm-core 2020.3.1,
// copied verbatim: code/.uvm/ is downloaded and gitignored, so the slide cannot
// pull the enum straight out of the library.
typedef enum {
   UVM_NO_ACTION = 'b0000000,
   UVM_DISPLAY   = 'b0000001,
   UVM_LOG       = 'b0000010,
   UVM_COUNT     = 'b0000100,
   UVM_EXIT      = 'b0001000,
   UVM_CALL_HOOK = 'b0010000,
   UVM_STOP      = 'b0100000,
   UVM_RM_RECORD = 'b1000000
} uvm_action_type;

The seven actions are a bitwise OR, not a list of mutually exclusive options: the same message can be printed and written to a file and counted towards the summary. That is why they combine with |. UVM_COUNT is the one most often explained wrong, and it is worth saying it in full: it is already switched on by default on every `uvm_error, and what it increments is **a global counter** for the run, not one per ID. On its own it kills nothing, because the default maximum is 0, which means "no limit". The one that cuts is +UVM_MAX_QUIT_COUNT=N (uvm_root.svh:1187) —or set_report_max_quit_count(N), uvm_report_object.svh:578—, and that one really is the answer to the four-gigabyte log when a scoreboard fails on every transaction: you find out anyway, and in thirty seconds instead of in twenty minutes. UVM_NO_ACTION is the one the next slide uses to switch off the errors, and it has to be said out loud what it is **not** for: it is not so that the regression comes out green. It is to carry on working while somebody else fixes their class, and it comes out the same day. A UVM_NO_ACTION that survives a commit is a bug waiting to happen. And the pair worth naming: set_report_severity_action is by severity, set_report_id_action is by that ID string you picked when writing the message. That is where the discipline of having always used the same one gets paid back.

see this slide in the deck ›

Reporting

Switching off messages: the complete example, and the log you are left with

code/u4/reporting/tb_classes/env.svh · #end_of_elaboration_phase
   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase
el archivo entero, 46 líneas
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester    random_tester_h;
   coverage  coverage_h;
   scoreboard scoreboard_h;
   driver    driver_h;
   command_monitor command_monitor_h;
   result_monitor result_monitor_h;
   uvm_tlm_fifo #(command_s) command_f;

   function void build_phase(uvm_phase phase);
      command_f = new("command_f", this);
      random_tester_h = random_tester::type_id::create("random_tester_h", this);
      driver_h = driver::type_id::create("driver_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);

   endfunction : build_phase

   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase

   function void connect_phase(uvm_phase phase);
      driver_h.command_port.connect(command_f.get_export);
      random_tester_h.command_port.connect(command_f.put_export);

      result_monitor_h.ap.connect(scoreboard_h.analysis_export);

      command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export);
      command_monitor_h.ap.connect(coverage_h.analysis_export);

   endfunction : connect_phase

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

endclass
code/u4/reporting/scoreboard2.txt
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 820ps

Say it out loud: switching off errors is for carrying on working while somebody else fixes their class, not so that the regression comes out green. And showing the Report Summary of the second log next to the first one is the best warning the section gives: the testbench says 0 UVM_ERROR and the DUT is still exactly as broken as it was when we broke it. That is the reason a UVM_NO_ACTION cannot survive a commit — it is a silent trap, and it is in the appendix. The scoreboard of this section adds too much ON PURPOSE, it is the bug we are showing. That is why code/u4/reporting/run.sh exports UVM_ERRORS_OK=1: by default a uvm_error makes the run fail, and here the error is the example.

see this slide in the deck ›

Reporting

uvm_report_catcher: demoting the error you were expecting

class demotar_pslverr extends uvm_report_catcher;
   virtual function action_e catch();
      if (get_severity() == UVM_ERROR && get_id() == "PSLVERR")
         set_severity(UVM_INFO);      // stops counting as an error
      return THROW;                   // and carries on, already downgraded
   endfunction
endclass

demotar_pslverr c = new();
uvm_report_cb::add(null, c);          // null = the whole testbench; or a component

It is the right answer to the previous slide, and the difference is one of honesty, not of syntax. UVM_NO_ACTION on the scoreboard switches off the error and the log ends up identical to that of a healthy testbench: nobody reading that log finds out. The catcher downgrades that message, lets all the others through, and on top of that counts it on a line of its own in the UVM Report catcher Summary. A reviewer who opens the log sees that there was an error and that somebody decided it was fine. When it really gets used: in the negative tests, which are half of a serious verification plan. Writing to a read-only register has to give PSLVERR; the test that proves it expects the error, and without a catcher that test can never come out green. It is exactly the case of the capstone. The implementation detail that surprises: a catcher is a uvm_callback, that is, the same piece as the callbacks of day 6. That is why it gets registered with uvm_report_cb::add and not with a set_ of the component. And the trap: if the catcher is too wide —by severity and nothing else— you have switched off every error of the testbench with more ceremony than UVM_NO_ACTION. The get_id() of the if is not optional.

see this slide in the deck ›

Reporting

Summary of the unit

Close the day by going back to the 47 %: of everything seen today —tests, components, phases, env— this is the section used every day, and the only one you notice when it is missing. A testbench without reporting works just the same; debugging it costs double. The control question, which is also number 6 of the review: *"I lowered the verbosity ceiling and the `uvm_error still shows up, why?"*. If the group answers it without hesitating, the section closed. And the warning for the day 5 exercise, worth giving now and not when they are stuck: the bug is not visible in the default log. It is printed, at UVM_HIGH. Whoever does not remember this section is going to read the code for half an hour before raising the verbosity.

see this slide in the deck ›

Review · Day 3

1 of 7 · uvm_test

What does +UVM_TESTNAME=add_test let you do that you could not do before?

Pick the test without recompiling — UVM reads that plusarg and asks the factory for the test by name. It is the difference between 1000 tests × 5 minutes of compilation and a single compilation.

see this slide in the deck ›

Review · Day 3

2 of 7 · UVM Phases

In what order does UVM walk the hierarchy in build_phase and in connect_phase?

Build top-down, connect bottom-up — and it makes sense: you cannot connect a component that does not exist yet, so first the whole hierarchy gets built and only then does it get connected.

see this slide in the deck ›

Review · Day 3

3 of 7 · Objections

What are raise_objection() / drop_objection() for in the run_phase?

So that the phase does not end early — every run_phase runs in parallel, each one in its thread, and the phase ends when the last objection drops. Without raising it, the simulation ends on you at time 0.

see this slide in the deck ›

Review · Day 3

4 of 7 · The env

What does each one get: uvm_env and uvm_test?

Structure vs. stimulus — each class does one single thing well. That is why the env almost always has only build_phase and connect_phase, and the test almost always has only a factory override.

see this slide in the deck ›

Review · Day 3

5 of 7 · Factory override

set_type_override() replaces base_tester with add_tester. When does it have to be called?

Before the build_phase — the factory decides what to build at the moment of the create(). If the override arrives late, the env has already instantiated the base class and it does nothing.

see this slide in the deck ›

Review · Day 3

6 of 7 · Verbosity

The verbosity ceiling (+UVM_VERBOSITY=UVM_HIGH), which macros does it act on?

Only on `uvm_info — warnings, errors and fatals are immune to the verbosity ceiling, and it is right that they should be: nobody wants to switch off an error by accident. For those you need the actions mechanism.

see this slide in the deck ›

Review · Day 3

7 of 7 · Report actions

You want to silence the `uvm_error of a scoreboard somebody else is fixing. Where does the set_report_severity_action_hier() go?

In end_of_elaboration_phase — it has to be after the hierarchy is built (otherwise the component does not exist yet) and before the simulation starts. That phase is exactly that window.

see this slide in the deck ›

Exercise · Day 3 · 1 of 2

A new test without touching the structure

cd code/ejercicios/d3 && bash run.sh

Before turning them loose on the exercise, go back to the slide "The map of day 3: what replaces what" of the object-based testbench and read the right-hand column straight through: the build_phase of the env, the config_db, the run_phase, the objections and the set_type_override and the $display that became uvm_info. The six rows are crossed out, and the day closes there. It is the day 2 exercise again, but with the factory. Worth having them compare the two: on day 2 the type was picked in the code of the testbench, here the factory picks it and the testbench does not change one line. And have them look at the coverage at the end: with nothing but multiplications it drops to 26 %. A focused test covers less, and that is the reason many are needed — and the reason adding one has to be cheap.

see this slide in the deck ›

Exercise · Day 3 · 2 of 2

The uvm_error that says nothing

cd code/ejercicios/d3b && bash run.sh

It is the short exercise of day 3 and it closes the arc the day-1 waves exercise opened: there the student was the one reading the FAILED, here they are the one writing it. Ten minutes of typing, and the point is not the $sformatf. The point is the second request, which is the one that surprises: the PASS also gets written. The typical reaction is "what do I want a thousand PASS lines for?", and the answer is that you do not want them today: you want them the day operation 700 fails and you need to know what happened in 699. That is why it gets written and hidden behind UVM_HIGH. That is the difference between severity and verbosity, told with your hands. The checker runs the same testbench three times, and it is worth telling because it is how a log gets tested: one with the DUT broken —+VTALU_BUG— so the error fires and can be read, one with the usual verbosity to demand that the PASS is not there, and one with +UVM_VERBOSITY=UVM_HIGH to demand that it is there once per comparison. A message that was not tested all three ways is a message that is going to lie in the nightly regression.

see this slide in the deck ›