Verify This! · the course to read

Day 6The reusable testbench

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

The day 6 recap has to name the two separations the unit makes, because there are two and they get confused: the agent separates the structure and the sequence separates the stimulus. Both are the same operation —taking something out of the component tree— applied to different things. The third bullet is the one to leave said in the student's own words: today, to test three stimuli and their combinations, six test classes are needed. That grows factorially, and it is the reason the unit exists.

see this slide in the deck ›

Agenda

Day 6 · ≈ 4 h 45 · unit 7 · the reusable testbench

By the end of the day you can:

The first two are rows 13 and 14 of the closing self-assessment. The 14th is the question thrown on day 1, when the UVM class diagram showed up, and it gets answered today: uvm_object is the data and uvm_component is the structure. The third one is what closes the unit and the day: when that line goes, no line of the test names a component from inside the env, and only then is the testbench reusable for one agent. Coordinating two is day 7.

see this slide in the deck ›

Agents

The problem: a testbench that cannot be copied

The env of transactions against the env with an agent

The question that orders the whole section: what is the natural unit of reuse in a testbench? It is not the component, and it is not the env. It is the interface. Everything that knows how to speak one protocol gets packed together, and that box gets instantiated once for every interface of the design. That is the agent, and it is nothing more. If the group comes from RTL, the analogy works on its own: the agent is to the testbench what a parameterizable module is to RTL. Nobody instantiates a FIFO by copying its registers one at a time.

see this slide in the deck ›

Agents

What is inside an agent

Internal structure of the agent

There is a decision here that is worth saying out loud because the UVM Primer takes it the other way round: it puts the scoreboard and the coverage inside the agent. We do not. The reason is practical: the day you put a passive agent on an interface that somebody else drives, you do not want a second scoreboard thrown in for free. The agent is the protocol layer; the analysis belongs to the env, which is the one that knows what is being verified. The other reason is that if the scoreboard lives inside, the two analysis ports that the agent exposes have nobody to serve, and half the design ends up decorative. About the last bullet, because somebody is going to open code/u7/agents and see it: up to the transactions the monitors extended uvm_component; from here on they extend uvm_monitor. And uvm_monitor does not add a single line of behaviour — it is uvm_component under another name. What it adds is intent: anybody who opens the testbench knows what that class does without reading it, and print_topology() shows it as a monitor. It is the same economy as uvm_env: half the value of UVM is not in the code of the library, it is in everybody using the same names for the same things. uvm_driver and uvm_sequencer do bring things —the seq_item_port and the arbitration—; uvm_monitor, uvm_env and uvm_agent are almost pure vocabulary.

see this slide in the deck ›

Agents

The jump: from the tester to the sequence

From the put/get to the seq_item_port

This is the hardest jump of the course, and the reason is that there are two changes on the same line. It is worth separating them. Change one, structural: the FIFO is called a sequencer and comes with the driver. Change two, conceptual: the stimulus generator stops being a component. A component is built once in build_phase and lives the whole simulation; a sequence is created, runs, finishes and gets thrown away — and you can run another one right after on the same sequencer. That is what lets a test combine stimuli without touching the structure, and that is what the sequences section is all about. The question to throw at the group: why does the driver of the passive agent not exist, but the monitor does? Because driving is optional, watching is not.

see this slide in the deck ›

Agents

The handshake: get_next_item() / item_done()

code/u7/agents/tb_classes/driver.svh · #run_phase
   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase
el archivo entero, 27 líneas
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)

   virtual vtalu_bfm bfm;

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

   function void build_phase(uvm_phase phase);
      vtalu_agent_config cfg;
      // The BFM is not asked for on its own: ask for the agent config, and it comes from there.
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("DRIVER", "Failed to get agent config")
      bfm = cfg.bfm;
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase

endclass : driver

Forgetting item_done() is mistake number one of the first week, and the symptom is misleading: no error, no warning, the simulation simply stops advancing in time and the objection never gets dropped. Verilator has no timeout by default: it keeps spinning until you kill it. The trick to remember it: get_next_item / item_done is a loan, not a copy. The sequence lends you the item and waits for you to give it back. put/get was a delivery: the tester dropped the command and moved on. And out of that comes the other useful difference: since the sequence still holds the handle, the driver can write the result inside it and the sequence reads it when it comes back from finish_item(). The sequences section uses that.

see this slide in the deck ›

Agents

The driver, now a uvm_driver

code/u7/agents/tb_classes/driver.svh
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)

   virtual vtalu_bfm bfm;

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

   function void build_phase(uvm_phase phase);
      vtalu_agent_config cfg;
      // The BFM is not asked for on its own: ask for the agent config, and it comes from there.
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("DRIVER", "Failed to get agent config")
      bfm = cfg.bfm;
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase

endclass : driver

It is worth opening the driver of the put and get ports alongside and counting lines: they are almost the same. The only thing that changed is where the item comes from —before a uvm_get_port, now the seq_item_port— and that there is an item_done() at the end. The question worth asking: why is the driver so short? Because the protocol is not here. It is in the BFM since interfaces and BFM, and this class only translates transaction into call. That is the division of labour that makes an AXI agent have the same twenty-line driver. And the corollary, which is the one from the bus appendix: when the protocol changes, the BFM and the monitor change. The driver, the agent, the env and the test never find out.

see this slide in the deck ›

Agents

The sequencer gets declared with a typedef

typedef uvm_sequencer #(command_transaction) sequencer;

This is the moment when the decision from the transactions gets cashed in. If that day the transaction had extended uvm_transaction, today this typedef would not compile: uvm_sequencer #(T) requires that T derive from uvm_sequence_item. It is worth saying explicitly: it is the first time in the course that a decision from an earlier section enables —or breaks— the next one. That is exactly what "adaptable code" means and it is not a phrase out of a manual. And in case somebody tries it: extending uvm_sequencer to add things to it is almost always a sign that it belonged in the sequence. The sequencer is an arbiter, not a place to put logic.

see this slide in the deck ›

Agents

The minimal sequence

code/u7/agents/tb_classes/command_sequence.svh · #class-and-body
class command_sequence extends uvm_sequence #(command_transaction);
   `uvm_object_utils(command_sequence)

   function new(string name = "command_sequence");
      super.new(name);
   endfunction : new

   task body();
      command_transaction command;

      // All transactions come out of the factory, the directed ones too:
      // a new() would skip add_test's set_type_override.
      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = rst_op;
      finish_item(command);

      repeat (1000) begin : random_loop
         command = command_transaction::type_id::create("command");
         start_item(command);
         // The randomize() goes BETWEEN start_item and finish_item: that way the
         // constraints are solved when the item is about to be handed over, and
         // not before. Today it makes no difference; the day a constraint
         // depends on the DUT state, it will.
         if (!command.randomize()) `uvm_fatal("COMMAND SEQ", "randomize() failed")
         finish_item(command);
      end : random_loop

      // Directed: the multiplier overflow, which 1000 random operations might
      // never touch. It comes out of the factory like the others, but since it
      // is not randomized, add_transaction's constraint does not run.
      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = mul_op;
      command.A  = 8'hFF;
      command.B  = 8'hFF;
      finish_item(command);
   endtask : body
el archivo entero, 45 líneas
// The run_phase of the u6/transactions tester, turned into the body() of a sequence.
// Same stimulus, same count, same directed cases.
//
// What changed: it is no longer a uvm_component, it is not in the tree, it
// connects to nothing and it does not raise the objection -- the test that starts it does.
class command_sequence extends uvm_sequence #(command_transaction);
   `uvm_object_utils(command_sequence)

   function new(string name = "command_sequence");
      super.new(name);
   endfunction : new

   task body();
      command_transaction command;

      // All transactions come out of the factory, the directed ones too:
      // a new() would skip add_test's set_type_override.
      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = rst_op;
      finish_item(command);

      repeat (1000) begin : random_loop
         command = command_transaction::type_id::create("command");
         start_item(command);
         // The randomize() goes BETWEEN start_item and finish_item: that way the
         // constraints are solved when the item is about to be handed over, and
         // not before. Today it makes no difference; the day a constraint
         // depends on the DUT state, it will.
         if (!command.randomize()) `uvm_fatal("COMMAND SEQ", "randomize() failed")
         finish_item(command);
      end : random_loop

      // Directed: the multiplier overflow, which 1000 random operations might
      // never touch. It comes out of the factory like the others, but since it
      // is not randomized, add_transaction's constraint does not run.
      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = mul_op;
      command.A  = 8'hFF;
      command.B  = 8'hFF;
      finish_item(command);
   endtask : body

endclass : command_sequence

Two things worth marking in the code, because they are the ones that get copied wrong. First: the transaction comes out of type_id::create(), the reset one and the directed multiplication one too. It is the same rule as the transactions —the factory only finds out about what goes through create()— and here it matters again because the add_test is still around. Second: randomize() between start_item and finish_item is not a whim of style. Randomizing before works just the same today, but it breaks the day a constraint depends on the state of the DUT or on the previous response: by then the value had already been chosen. Randomizing late is the correct habit and it costs nothing to adopt it from day one.

see this slide in the deck ›

Agents

is_active: active and passive

Active against passive

The honest question is "and what do I want an agent that drives nothing for?". Three answers from the field, in order of frequency: One, output interfaces. A port the DUT drives and you only verify does not need a driver, and the passive agent gives you the monitor and the coverage for free. Two, integration. When the block you verified becomes part of a chip, the interface stops being yours: the block next door drives it. The same agent, with one different line in the config, still works. That is the real return on having built an agent. Three, the one from the example: living with stimulus that is not yours — a legacy module, a reference model, a generator from another team.

see this slide in the deck ›

Agents

And it can be seen

      clase_agent_h          vtalu_agent
        command_ap           uvm_analysis_port
        command_monitor_h    command_monitor
        driver_h             driver
          seq_item_port      uvm_seq_item_pull_port
        result_ap            uvm_analysis_port
        result_monitor_h     result_monitor
        sequencer_h          uvm_sequencer
          seq_item_export    uvm_seq_item_pull_imp
      modulo_agent_h         vtalu_agent
        command_ap           uvm_analysis_port
        command_monitor_h    command_monitor
        result_ap            uvm_analysis_port
        result_monitor_h     result_monitor

This tree is printed by the library, not by the course, and that is the whole point: up to here the hierarchy was a diagram on a slide, and now it is an output you can grep. It is worth running it live — it is the print_topology() of the reporting section, now with something worth looking at. The two things to make them see, side by side: clase_agent_h has driver_h and sequencer_h, and modulo_agent_h does not. That is is_active in practice — the same class type, instantiated twice, and one built two objects fewer. Both monitors are in both, because watching is never optional. And the one that is not there: there is no sequence in this tree. A sequence is a uvm_object, not a uvm_component, so it does not live in the topology. It is the question thrown out on day 1 with the class diagram, and here it can be answered pointing at the screen.

see this slide in the deck ›

Agents

The config object of the agent

code/u7/agents/tb_classes/vtalu_agent_config.svh
// The agent config is NOT a uvm_object: it is a plain class. That is why the
// constructor can DEMAND both values, and whoever forgets one does not compile.
// A uvm_object is built with type_id::create(), which only takes a name, and
// that guarantee is lost.
class vtalu_agent_config;

   virtual vtalu_bfm bfm;

   // protected + getter: nobody changes it after the agent was built.
   protected uvm_active_passive_enum is_active;

   function new(virtual vtalu_bfm bfm, uvm_active_passive_enum is_active);
      this.bfm = bfm;
      this.is_active = is_active;
   endfunction : new

   function uvm_active_passive_enum get_is_active();
      return is_active;
   endfunction : get_is_active

endclass : vtalu_agent_config

Here the course departs from the custom of the industry on purpose, and it is worth saying so: most projects do class agent_config extends uvm_object and register it in the factory, so it can be overridden. It is a legitimate decision and it has its reason. The downside is the one in the bullet: you lose the constructor that forces you. What is not negotiable is the underlying idea, and it is the one to leave behind: every level of hierarchy receives its configuration through an object. If a component needs five pieces of data, that is not five uvm_config_db::get() scattered around: it is a config. The day you add the sixth piece of data, you touch one class and not six. Notice that the driver and the two monitors also ask for the config, not the BFM: the agent does not hand out handles by hand.

see this slide in the deck ›

Agents

The agent class: build_phase

code/u7/agents/tb_classes/vtalu_agent.svh · #class-and-build
class vtalu_agent extends uvm_agent;
   `uvm_component_utils(vtalu_agent)

   vtalu_agent_config cfg;

   sequencer       sequencer_h;
   driver          driver_h;
   command_monitor command_monitor_h;
   result_monitor  result_monitor_h;

   uvm_analysis_port #(command_transaction) command_ap;
   uvm_analysis_port #(result_transaction)  result_ap;

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

   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("AGENT", "Failed to get agent config")
      is_active = cfg.get_is_active();

      if (get_is_active() == UVM_ACTIVE) begin : estimulo
         sequencer_h = sequencer::type_id::create("sequencer_h", this);
         driver_h    = driver::type_id::create("driver_h", this);
      end : estimulo

      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h  = result_monitor::type_id::create("result_monitor_h", this);

      command_ap = new("command_ap", this);
      result_ap  = new("result_ap", this);
   endfunction : build_phase
el archivo entero, 43 líneas
class vtalu_agent extends uvm_agent;
   `uvm_component_utils(vtalu_agent)

   vtalu_agent_config cfg;

   sequencer       sequencer_h;
   driver          driver_h;
   command_monitor command_monitor_h;
   result_monitor  result_monitor_h;

   uvm_analysis_port #(command_transaction) command_ap;
   uvm_analysis_port #(result_transaction)  result_ap;

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

   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("AGENT", "Failed to get agent config")
      is_active = cfg.get_is_active();

      if (get_is_active() == UVM_ACTIVE) begin : estimulo
         sequencer_h = sequencer::type_id::create("sequencer_h", this);
         driver_h    = driver::type_id::create("driver_h", this);
      end : estimulo

      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h  = result_monitor::type_id::create("result_monitor_h", this);

      command_ap = new("command_ap", this);
      result_ap  = new("result_ap", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      if (get_is_active() == UVM_ACTIVE)
         driver_h.seq_item_port.connect(sequencer_h.seq_item_export);

      command_monitor_h.ap.connect(command_ap);
      result_monitor_h.ap.connect(result_ap);
   endfunction : connect_phase

endclass : vtalu_agent

A detail worth gold that nobody tells: the build_phase of uvm_agent that the library brings only reads is_active from the resource pool. It is in code/.uvm/src/comps/uvm_agent.svh, it can be opened in class. Which means that in a testbench that calls super.build_phase(), a uvm_config_db#(int)::set(this, "mi_agent", "is_active", UVM_PASSIVE) works on its own, without a config object. Since this vtalu_agent does not call it, that mechanism is switched off, and that is why we assign it by hand. And here is where day 3 gets paid: uvm_agent and uvm_sequencer are the two classes in the library that do real work in their build_phase —uvm_agent reads is_active, and uvm_sequencer_param_base hooks up the response fifo (seq/uvm_sequencer_param_base.svh:288)—. It is exactly the case the rule for out there covers: if you extend either of the two and you write a build_phase, the super is mandatory. You inherited from an intermediate class that does real work, and skipping the super switches it off without a word. We can skip it because we wrote the line that replaces it; whoever does not write that line, cannot. Both ways are valid. What is not valid is half of each one: putting is_active in the config_db, not calling super.build_phase(), and then wondering why the agent starts up active. It happens often.

see this slide in the deck ›

Agents

The agent class: connect_phase

Port forwarding is what confuses, and it is worth saying why it exists before how it is written. The agent wants to offer outwards a piece of data produced by a child of its own. It could expose the child —the env looking at agent.command_monitor_h.ap— and that is where the encapsulation ends: the day the monitor changes name, the env breaks. The way out is for the agent to have its own analysis port and plug it into the monitor's. From outside a single port is seen, command_ap, and inside there can be whatever. It is exactly what a module does when it connects a port of its own to that of a submodule — the same idea, with classes. The detail that looks like it breaks the mantra: here a port connects to another port, not to an export. It is not a capricious exception — the direction is still the same, from the one who calls to the one who implements; the only thing that happens is that the agent is in the middle and implements nothing, it only forwards.

see this slide in the deck ›

Agents

How the env ends up

code/u7/agents/env_un_agent.svh · #class-and-build
class env extends uvm_env;
   `uvm_component_utils(env)

   vtalu_agent        agent_h;
   vtalu_agent_config agent_cfg_h;

   scoreboard scoreboard_h;
   coverage   coverage_h;

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

   function void build_phase(uvm_phase phase);
      virtual vtalu_bfm bfm;

      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("ENV", "Failed to get BFM")

      agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
      uvm_config_db#(vtalu_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);

      agent_h      = vtalu_agent::type_id::create("agent_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      coverage_h   = coverage::type_id::create("coverage_h", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      agent_h.command_ap.connect(scoreboard_h.cmd_f.analysis_export);
      agent_h.command_ap.connect(coverage_h.analysis_export);
      agent_h.result_ap.connect(scoreboard_h.analysis_export);
   endfunction : connect_phase
el archivo entero, 36 líneas
// ILLUSTRATION -- not compiled. The env of a SINGLE VTALU, to compare against
// the env.svh of the Transactions section.
class env extends uvm_env;
   `uvm_component_utils(env)

   vtalu_agent        agent_h;
   vtalu_agent_config agent_cfg_h;

   scoreboard scoreboard_h;
   coverage   coverage_h;

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

   function void build_phase(uvm_phase phase);
      virtual vtalu_bfm bfm;

      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("ENV", "Failed to get BFM")

      agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
      uvm_config_db#(vtalu_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);

      agent_h      = vtalu_agent::type_id::create("agent_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      coverage_h   = coverage::type_id::create("coverage_h", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      agent_h.command_ap.connect(scoreboard_h.cmd_f.analysis_export);
      agent_h.command_ap.connect(coverage_h.analysis_export);
      agent_h.result_ap.connect(scoreboard_h.analysis_export);
   endfunction : connect_phase

endclass : env

This is the slide for making the comparison live: open the env.svh of transactions alongside. The difference is not that it is shorter —which it also is—, it is what disappeared: the env of transactions named command_f, which means it knew the internal mechanism of how the stimulus reached the driver. This one does not name it. That is encapsulation measured objectively: count the names that the class above has to know. If the group is doing well on time, the question is worth it: what would have to change in this env to go from a FIFO to a sequencer? Nothing. It is already inside the agent.

see this slide in the deck ›

Agents

Two agents, one single string

The hierarchical scope of the config_db

Here is the classic trap of the section, and it is one of the ones that never fail. With a single agent, everybody writes "*" in the scope and it works. The day the second one shows up, the second set() overwrites the first and both agents get the same config: the same BFM and the same is_active. With the order in env.svh —the class first and the module after— the one left standing is the passive one, so both start up passive, with no driver and no sequencer, and the test's seq.start() lands on a null handle. With the order reversed they would be two drivers, but driving the same interface. What is invariant, and what to take away: the last set() wins, and both agents end up identical. The error does not show up in build_phase. The asterisk matters: "clase_agent_h*" with an asterisk reaches the children; "clase_agent_h" without an asterisk, only the agent. If you write it without an asterisk, the agent finds its config and the driver does not — uvm_fatal in build_phase, which at least is an honest error. Short rule to take away: the scope of the set() is a path, not a label.

see this slide in the deck ›

Agents

When the config_db does not find: +UVM_CONFIG_DB_TRACE

$ bash run.sh +UVM_CONFIG_DB_TRACE
UVM_INFO ... Configuration 'config' (type vtalu_agent_config) set by env_h
UVM_INFO ... Configuration 'config' read by
             accessor=uvm_test_top.env_h.clase_agent_h.driver_h
UVM_INFO ... Configuration 'config' read by
             accessor=uvm_test_top.env_h.modulo_agent_h.command_monitor_h

This is the tool that tests was missing, when we said that get(null, "*", ...) "does not fail: it lies". With the trace on, lying becomes difficult: you see who put what and who read it. The use almost nobody discovers on their own is the second one: since every get() prints the path of the component that called it, the output is a census of the tree written by the library. If a component does not show up, it was not built. The exercise of the day uses exactly that to grade itself, and that is why it cannot be cheated: the student does not write those lines. A close relative worth naming: +UVM_OBJECTION_TRACE, for the other classic hang —the simulation that does not finish because somebody did not drop their objection—.

see this slide in the deck ›

Agents

The example: two VTALU and the legacy module

code/u7/agents/top.sv · #top
module top;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "vtalu_macros.svh"
   `include "uvm_macros.svh"

   // The VTALU driven by the class testbench
   vtalu_bfm clase_bfm ();
   vtalu clase_dut (
       .A(clase_bfm.A), .B(clase_bfm.B), .op(clase_bfm.op_set),
       .clk(clase_bfm.clk), .reset_n(clase_bfm.reset_n), .start(clase_bfm.start),
       .done(clase_bfm.done),
       .ovf(clase_bfm.ovf), .result(clase_bfm.result)
   );

   // The VTALU driven by the legacy module
   vtalu_bfm modulo_bfm ();
   vtalu modulo_dut (
       .A(modulo_bfm.A), .B(modulo_bfm.B), .op(modulo_bfm.op_set),
       .clk(modulo_bfm.clk), .reset_n(modulo_bfm.reset_n), .start(modulo_bfm.start),
       .done(modulo_bfm.done),
       .ovf(modulo_bfm.ovf), .result(modulo_bfm.result)
   );

   vtalu_tester_module stim_module (modulo_bfm);

   initial begin
      // The top knows nothing about agents: it leaves the two interfaces with a
      // name and walks away. Handing them out is the test's job.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "clase_bfm", clase_bfm);
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "modulo_bfm", modulo_bfm);
      run_test();
   end

endmodule : top
el archivo entero, 35 líneas
module top;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "vtalu_macros.svh"
   `include "uvm_macros.svh"

   // The VTALU driven by the class testbench
   vtalu_bfm clase_bfm ();
   vtalu clase_dut (
       .A(clase_bfm.A), .B(clase_bfm.B), .op(clase_bfm.op_set),
       .clk(clase_bfm.clk), .reset_n(clase_bfm.reset_n), .start(clase_bfm.start),
       .done(clase_bfm.done),
       .ovf(clase_bfm.ovf), .result(clase_bfm.result)
   );

   // The VTALU driven by the legacy module
   vtalu_bfm modulo_bfm ();
   vtalu modulo_dut (
       .A(modulo_bfm.A), .B(modulo_bfm.B), .op(modulo_bfm.op_set),
       .clk(modulo_bfm.clk), .reset_n(modulo_bfm.reset_n), .start(modulo_bfm.start),
       .done(modulo_bfm.done),
       .ovf(modulo_bfm.ovf), .result(modulo_bfm.result)
   );

   vtalu_tester_module stim_module (modulo_bfm);

   initial begin
      // The top knows nothing about agents: it leaves the two interfaces with a
      // name and walks away. Handing them out is the test's job.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "clase_bfm", clase_bfm);
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "modulo_bfm", modulo_bfm);
      run_test();
   end

endmodule : top

This top.sv is the one that closes the argument of the section, and it is worth reading looking for what did not change: there are two VTALU, two interfaces, two different config_db, and the vtalu_tester_module is still the same old Verilog module as always. Nobody touched it so it could live with UVM. That is the real scenario the section comes to solve, and it is worth naming it like this: you arrive at a project where there is already a stimulus written in Verilog that works and nobody is going to rewrite. With a passive agent you can watch it —scoreboard, coverage, assertions— without asking anybody's permission or touching a line of it. The technical detail that makes the comparison fair, and that has to be pointed at with a finger: the module calls the same bfm.send_op() as the driver. It receives the interface through a port instead of through a virtual interface, but the protocol is the same code. If each one drove the bus its own way, comparing coverages would not mean anything.

see this slide in the deck ›

Agents

The test, and what is still missing

seq = command_sequence::type_id::create("seq");
seq.start(env_h.clase_agent_h.sequencer_h);
code/u7/agents/tb_classes/dual_test.svh · #build-and-run
   function void build_phase(uvm_phase phase);
      virtual vtalu_bfm clase_bfm, modulo_bfm;
      env_config env_config_h;

      // The top left the two interfaces loose; the test packs them up.
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "clase_bfm", clase_bfm))
         `uvm_fatal("DUAL TEST", "Failed to get clase_bfm")
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "modulo_bfm", modulo_bfm))
         `uvm_fatal("DUAL TEST", "Failed to get modulo_bfm")

      env_config_h = new(.clase_bfm(clase_bfm), .modulo_bfm(modulo_bfm));
      uvm_config_db#(env_config)::set(this, "env_h*", "config", env_config_h);

      env_h = env::type_id::create("env_h", this);
   endfunction : build_phase

   // +TOPOLOGY prints the component tree. It is how to SEE that the passive
   // agent built neither a sequencer nor a driver.
   function void end_of_elaboration_phase(uvm_phase phase);
      if ($test$plusargs("TOPOLOGY")) uvm_root::get().print_topology();
   endfunction : end_of_elaboration_phase

   task run_phase(uvm_phase phase);
      command_sequence seq;

      phase.raise_objection(this);

      // The only thing left wired by hand: the test reaches across the
      // hierarchy to get to the sequencer. The sequences section takes it out of here.
      seq = command_sequence::type_id::create("seq");
      seq.start(env_h.clase_agent_h.sequencer_h);

      #500;
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 46 líneas
class dual_test extends uvm_test;
   `uvm_component_utils(dual_test);

   env env_h;

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

   function void build_phase(uvm_phase phase);
      virtual vtalu_bfm clase_bfm, modulo_bfm;
      env_config env_config_h;

      // The top left the two interfaces loose; the test packs them up.
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "clase_bfm", clase_bfm))
         `uvm_fatal("DUAL TEST", "Failed to get clase_bfm")
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "modulo_bfm", modulo_bfm))
         `uvm_fatal("DUAL TEST", "Failed to get modulo_bfm")

      env_config_h = new(.clase_bfm(clase_bfm), .modulo_bfm(modulo_bfm));
      uvm_config_db#(env_config)::set(this, "env_h*", "config", env_config_h);

      env_h = env::type_id::create("env_h", this);
   endfunction : build_phase

   // +TOPOLOGY prints the component tree. It is how to SEE that the passive
   // agent built neither a sequencer nor a driver.
   function void end_of_elaboration_phase(uvm_phase phase);
      if ($test$plusargs("TOPOLOGY")) uvm_root::get().print_topology();
   endfunction : end_of_elaboration_phase

   task run_phase(uvm_phase phase);
      command_sequence seq;

      phase.raise_objection(this);

      // The only thing left wired by hand: the test reaches across the
      // hierarchy to get to the sequencer. The sequences section takes it out of here.
      seq = command_sequence::type_id::create("seq");
      seq.start(env_h.clase_agent_h.sequencer_h);

      #500;
      phase.drop_objection(this);
   endtask : run_phase

endclass : dual_test

This is the slide to close the day with if there is no time for the summary: it leaves an open question and a concrete promise. And it is honest: seq.start(env_h.clase_agent_h.sequencer_h) is real code that works and that is seen in many testbenches. It is not "wrong". It is simply the last thing in the testbench that is still hard-wired, and it is the one the section that follows knocks over. If somebody asks why we do not do it right from the start: because to understand the default_sequence you first have to have suffered writing the start() by hand.

see this slide in the deck ›

Agents

What this section does differently

That the material says where it departs from its sources is not a detail of tidiness: it is what lets the student read the UVM Primer afterwards without believing that one of the two is broken. And along the way it leaves the lesson most used at work: the structure of a UVM testbench is not fixed by the standard, it is fixed by custom. IEEE 1800.2 does not say anywhere that the scoreboard goes in the env. It says what a uvm_agent is and nothing else. Everything else is convention — good, but convention.

see this slide in the deck ›

Agents

Summary of the unit

Close of the section and of the biggest jump of the course. It is worth saying where they ended up standing: with today's material, the student can read the testbench of any UVM project and recognise the structure. An agent per interface, a config per level, analysis in the env. It is literally 80 % of what they will see on their first day. What is missing —sequences— is what they will write on their first day. That is why they go together on the same day and in this order: first the house, then what happens inside.

see this slide in the deck ›

Callbacks

The third hook

This half section exists because unit 1 promises three hooks, and without it the course delivers two. Fifteen minutes are enough: the mechanics are small and what has to be left behind is the criterion for when it gets used. The criterion, said in one line: override when the what changes, callback when the when changes. If the new class differs from the old one in several methods, it is an override. If it is the same class with an addition at one point, it is a callback — and above all if the original class is not yours and you cannot touch it. The key word in all of this is VIP. In a real project half of the agents come from outside, with their licence and their support, and editing them is not an option: the next version wipes your change. The callback is the extension point the one who wrote the VIP left on purpose. And that they stack is what makes it possible for two engineers to hook different things into the same driver without stepping on each other. With overrides, the second one wins.

see this slide in the deck ›

Callbacks

The hook: one class, one empty virtual method

code/u7/callbacks/tb_classes/driver_callback.svh · #driver_callback
class driver_callback extends uvm_callback;
   `uvm_object_utils(driver_callback)

   function new(string name = "driver_callback");
      super.new(name);
   endfunction : new

   // A task, not a function: a callback that only edits the transaction could
   // be a function, but one that adds delay needs to consume time. Making the
   // hook a task costs nothing and leaves both open.
   virtual task pre_send(driver drv, command_transaction cmd);
   endtask : pre_send

endclass : driver_callback
el archivo entero, 68 líneas
// The hook itself. A callback is NOT a component: it does not live in the tree
// and it has no phases. It is an object that hangs off a component, and the
// component decides where -- and whether -- it gets called.
//
// The base class is empty on purpose: a testbench with no callbacks registered
// runs exactly as it did before, at the cost of one empty virtual call.
typedef class driver;

class driver_callback extends uvm_callback;
   `uvm_object_utils(driver_callback)

   function new(string name = "driver_callback");
      super.new(name);
   endfunction : new

   // A task, not a function: a callback that only edits the transaction could
   // be a function, but one that adds delay needs to consume time. Making the
   // hook a task costs nothing and leaves both open.
   virtual task pre_send(driver drv, command_transaction cmd);
   endtask : pre_send

endclass : driver_callback

// --------------------------------------------------------------------------
// Error injection: flip one bit of A, once every `cada` transactions.
//
// This is the classic use -- and the reason it is worth showing is what it does
// NOT do to the scoreboard. See run.sh.
class flip_bit_cb extends driver_callback;
   `uvm_object_utils(flip_bit_cb)

   int unsigned cada = 8;   // one out of every N
   int unsigned vistas;

   function new(string name = "flip_bit_cb");
      super.new(name);
   endfunction : new

   virtual task pre_send(driver drv, command_transaction cmd);
      vistas++;
      if (vistas % cada != 0) return;
      `uvm_info("FLIP_BIT_CB", $sformatf("A: %02h -> %02h", cmd.A, cmd.A ^ 8'h01),
                UVM_MEDIUM)
      cmd.A ^= 8'h01;
   endtask : pre_send

endclass : flip_bit_cb

// --------------------------------------------------------------------------
// Timing injection: hold the bus idle for a couple of clocks before driving.
//
// It is here to make one point that the factory cannot make: callbacks STACK.
// Both of these are registered on the same driver, and both run, in the order
// they were added. A factory override picks one class and only one.
class jitter_cb extends driver_callback;
   `uvm_object_utils(jitter_cb)

   int unsigned ciclos = 2;

   function new(string name = "jitter_cb");
      super.new(name);
   endfunction : new

   virtual task pre_send(driver drv, command_transaction cmd);
      repeat (ciclos) @(posedge drv.bfm.clk);
   endtask : pre_send

endclass : jitter_cb

It is worth stopping on the fact that it is a normal class, extending uvm_callback, which extends uvm_object. There is no magic: the only thing uvm_callback does is give it a place in the pool UVM keeps per component instance. The typedef class driver; at the top of the file is the boring and necessary part: the callback receives the driver, and the driver registers the callback, so one of the two has to be forward-declared. In a real testbench this lives in its own file for that very reason. If somebody asks why the base method is empty instead of abstract: because uvm_do_callbacks walks the whole queue, and an empty queue has to be legal. The empty base class is what makes "no callbacks" the normal case and not an exception.

see this slide in the deck ›

Callbacks

Two lines in the driver, and not one more

code/u7/callbacks/tb_classes/driver.svh · #register-cb
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)
   // Declares "this component accepts callbacks of this type". Without it the
   // add() below still hooks up and the callback still runs, but with a CBUNREG
   // warning and without the type check: outside the contract, and nearly silent.
   `uvm_register_cb(driver, driver_callback)

   virtual vtalu_bfm bfm;
el archivo entero, 35 líneas
// The Agents driver with the hook added. Diff against ../agents/tb_classes:
// two lines -- `uvm_register_cb and `uvm_do_callbacks. Nothing else moves.
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)
   // Declares "this component accepts callbacks of this type". Without it the
   // add() below still hooks up and the callback still runs, but with a CBUNREG
   // warning and without the type check: outside the contract, and nearly silent.
   `uvm_register_cb(driver, driver_callback)

   virtual vtalu_bfm bfm;

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

   function void build_phase(uvm_phase phase);
      vtalu_agent_config cfg;
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("DRIVER", "Failed to get agent config")
      bfm = cfg.bfm;
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         // Every callback registered on this driver, in the order they were
         // added. With none registered this is a loop over an empty queue.
         `uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase

endclass : driver
code/u7/callbacks/tb_classes/driver.svh · #run_phase
   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         // Every callback registered on this driver, in the order they were
         // added. With none registered this is a loop over an empty queue.
         `uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase
el archivo entero, 35 líneas
// The Agents driver with the hook added. Diff against ../agents/tb_classes:
// two lines -- `uvm_register_cb and `uvm_do_callbacks. Nothing else moves.
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)
   // Declares "this component accepts callbacks of this type". Without it the
   // add() below still hooks up and the callback still runs, but with a CBUNREG
   // warning and without the type check: outside the contract, and nearly silent.
   `uvm_register_cb(driver, driver_callback)

   virtual vtalu_bfm bfm;

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

   function void build_phase(uvm_phase phase);
      vtalu_agent_config cfg;
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("DRIVER", "Failed to get agent config")
      bfm = cfg.bfm;
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_transaction command;
      forever begin : command_loop
         seq_item_port.get_next_item(command);
         // Every callback registered on this driver, in the order they were
         // added. With none registered this is a loop over an empty queue.
         `uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
         bfm.send_op(command.A, command.B, command.op);
         seq_item_port.item_done();
      end : command_loop
   endtask : run_phase

endclass : driver

The diff against the driver of the Agents section is exactly two lines, and it is worth showing it with a live diff: diff code/u7/agents/tb_classes/driver.svh code/u7/callbacks/tb_classes/driver.svh. Everything else —the get_next_item, the send_op, the item_done— is the same. The failure mode of the missing uvm_register_cb is not the one you expect, and it is worth telling it with the source in hand because half the tutorials get it backwards: uvm_callback.svh:745 reports a UVM_WARNING CBUNREG and carries on — the add() puts the callback into m_base_inst.m_pool all the same (:781-785), and `uvm_do_callbacks reads that pool without consulting the registry (:964-1006). Which means: without the macro the callback runs, and what you lose is the type check and add_by_name by derived type. That is worse than an error: it is a warning among a thousand lines, and the testbench ends up outside the contract without anyone noticing. That is why the run.sh of the example does not check the macro but the effect: it counts how many times the callback ran and fails if it is zero. An example that "passes" without having injected anything proves nothing, whatever the reason it came unhooked. Where to put the `uvm_do_callbacks is the real design decision, and it is the same one taken by whoever writes a VIP: every hook point is a promise of forward compatibility. That is why VIPs have three or four, not thirty.

see this slide in the deck ›

Callbacks

Putting them in: the test, and nothing but the test

code/u7/callbacks/tb_classes/inject_test.svh · #hooking-the-cb
   // end_of_elaboration and not build: the driver is a grandchild of the env,
   // and build_phase runs top-down -- when this test's build_phase returns, the
   // driver does not exist yet.
   function void end_of_elaboration_phase(uvm_phase phase);
      super.end_of_elaboration_phase(phase);

      flip_h   = flip_bit_cb::type_id::create("flip_h");
      jitter_h = jitter_cb::type_id::create("jitter_h");

      // Both on the same driver, and both run: the queue is per component
      // instance, in insertion order. The passive agent has no driver, so there
      // is nothing to hook there.
      uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, jitter_h);
      uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, flip_h);

      if ($test$plusargs("CALLBACK_TRACE"))
         uvm_callbacks #(driver, driver_callback)::display(env_h.clase_agent_h.driver_h);
   endfunction : end_of_elaboration_phase
el archivo entero, 36 líneas
// The same test as ../agents, with two callbacks hung on the driver.
//
// Note what this class does NOT touch: not the env, not the agent, not the
// driver, not the sequence. That is the whole promise of the third hook -- and
// it is the same promise as the factory override, one level lower: the override
// swaps a class, the callback changes what one class does at one point.
class inject_test extends dual_test;
   `uvm_component_utils(inject_test)

   flip_bit_cb flip_h;
   jitter_cb   jitter_h;

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

   // end_of_elaboration and not build: the driver is a grandchild of the env,
   // and build_phase runs top-down -- when this test's build_phase returns, the
   // driver does not exist yet.
   function void end_of_elaboration_phase(uvm_phase phase);
      super.end_of_elaboration_phase(phase);

      flip_h   = flip_bit_cb::type_id::create("flip_h");
      jitter_h = jitter_cb::type_id::create("jitter_h");

      // Both on the same driver, and both run: the queue is per component
      // instance, in insertion order. The passive agent has no driver, so there
      // is nothing to hook there.
      uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, jitter_h);
      uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, flip_h);

      if ($test$plusargs("CALLBACK_TRACE"))
         uvm_callbacks #(driver, driver_callback)::display(env_h.clase_agent_h.driver_h);
   endfunction : end_of_elaboration_phase

endclass : inject_test

bash run.sh +CALLBACK_TRACE prints the queue of the driver

The phase trap is the one that takes somebody's whole afternoon: uvm_callbacks::add needs the handle of the component, and the tree is built top down. In the build_phase of the test, env_h exists —it has just created it— but env_h.clase_agent_h is null. end_of_elaboration_phase runs with the whole tree assembled, which is exactly what is needed. The alternative that is sometimes seen is add_by_name("*driver_h", cb, this), with a pattern instead of a handle. It is useful when you do not want to go across the hierarchy, and it has the same phase problem. And the passive agent does not show up in either of the two lines for a reason that is obvious once said: it has no driver. Callbacks hang off instances, not off types, so "the driver of the active agent" is a concrete address in the tree.

see this slide in the deck ›

Callbacks

The flipped bit the scoreboard does not see

cd code/u7/callbacks && bash run.sh

The hook Changes How many at a time
Constraint the values as many as you like
Factory override the whole class one
Callback what a class does at one point as many as you like

This is the slide of the half section, and the exercise of thinking is worth more than the code: "if I inject an error and the scoreboard does not scream, is the scoreboard wrong?" Let the group argue about it for a minute before answering. The answer is no, and it is one of the most important things in the course: the scoreboard predicts from what the monitor saw on the bus. If the driver sends A^1, the DUT computes with A^1, the monitor sees A^1 and the prediction gives A^1. All consistent. The only testbench this callback would break is one with a monitor that does not watch the signals — and that testbench was wrong before the callback ever existed. Out of that comes the rule we already saw in Agents and that now has a demonstration: the monitor watches the wire, always. An injection callback is, besides a tool, the cheapest test there is for finding out whether your monitor does it. To inject an error the scoreboard does have to catch, you have to break the DUT, not the stimulus — which is exactly what the +BUG=1 of the capstone does.

see this slide in the deck ›

Sequences

The only thing left hard-wired

seq.start(env_h.clase_agent_h.sequencer_h);

The sentence that orders the whole section: stimulus is not structure. A component is built once, before the simulation starts, and it stays there until the end. Stimulus changes from test to test, and sometimes it changes inside the same test. Transactions, agents and sequences are the same operation repeated on three different things: separating the data (transaction), separating the structure (agent), separating the stimulus (sequence). The question to throw at the group before going on: with the command_sequence of the agents, how many classes are needed to try three stimuli and their combinations? Six, and it grows factorially. In the UVM Primer that is called explosion of tester classes.

see this slide in the deck ›

Sequences

An object, not a component

The sequence lives outside the component tree; the sequencer, inside

There is a detail here that confuses and is worth getting ahead of, because they are going to see it in the log: even though the sequence is not in the tree, UVM reports it hanging off the sequencer, with an @@ in the middle: uvm_test_top.env_h.clase_agent_h.sequencer_h@@full_seq.random_seq That does NOT mean it is a child of the sequencer. The @@ is precisely the mark that what follows is a sequence and not a component: to the left, the path of the sequencer it runs on; to the right, the path of nested sequences. If somebody looks for sequencer_h.full_seq with uvm_root::get().find() they are never going to find it. The other consequence, the one used every day: since it is not a component, a sequence can have configurable fields that get changed between the create() and the start(). That is what full_seq.count = 200 does at the end of the unit, and with a component it could not be done: by the time you want to change it, the build_phase has already gone by.

see this slide in the deck ›

Sequences

The smallest sequence there is

code/u7/sequences/tb_classes/reset_sequence.svh
// The smallest sequence there is: a single item.
//
// It is a uvm_object, not a uvm_component: it is not in the tree, it has no
// parent, it has no phases. It gets created, it runs, and it gets thrown away.
class reset_sequence extends uvm_sequence #(command_transaction);
   // `uvm_object_utils, NOT `uvm_component_utils: the constructor takes a single
   // argument and there is no parent to pass.
   `uvm_object_utils(reset_sequence)

   function new(string name = "reset_sequence");
      super.new(name);
   endfunction : new

   // body() is a TASK, not a function: it blocks inside. Nobody calls it by
   // hand; UVM calls it when somebody starts the sequence.
   task body();
      command_transaction command;
      command = command_transaction::type_id::create("command");
      start_item(command);    // blocks until the sequencer gives us our turn
      command.op = rst_op;    // only now do we decide what to send
      finish_item(command);   // blocks until the driver calls item_done()
   endtask : body

endclass : reset_sequence

The two typing mistakes everybody makes the first time, and which give messages that do not help:

  1. `uvm_component_utils instead of `uvm_object_utils. The error talks about a constructor with two arguments and nobody relates it to the macro.
  2. function body() instead of task body(). The declaration compiles, and then the start_item() inside does not, because a function cannot block. And a design one worth marking: the command is declared inside body(), not as a field of the class. In the UVM Primer it is a field. If it is a field, two runs of the same sequence share the handle, and the day somebody starts the same sequence twice in parallel you have two threads writing the same object. And a protocol one, which is what makes this sequence exist: the first item of every test is a reset. The VTALU starts up with reset_n at 0 and never raises done; the driver stays in its while (bfm.done == 0) and the sequence, in the first finish_item(). There is no error: the simulation stops advancing. It is the same symptom as the forgotten item_done(), with another cause, and it is worth showing it once in class by deleting the reset_sequence from full_sequence.

see this slide in the deck ›

Sequences

The handshake, now from the sequence side

Timeline of start_item, get_next_item, item_done and finish_item

Confusion number one of the section: believing that finish_item() comes back when the item was delivered. No: it comes back when the driver called item_done(), that is when the operation finished in the DUT. It is the difference between "I sent it" and "it is done", and it is what makes the Fibonacci sequence possible. Out of that comes as well why the #500 that the tester of the transactions had at the end disappeared: it was not stimulus, it was a patch so the objection would not drop before its time. With finish_item() the stimulus no longer gets cut in half. Careful, honesty: the last result can still end up uncompared, because the objection gets dropped as soon as start() comes back and the result_monitor publishes one edge later. It can be seen in the log of the add_test run with +UVM_VERBOSITY=UVM_HIGH: 1001 commands, 1000 comparisons. If somebody asks about the non-blocking cousin: there is try_next_item(), which comes back right away with null if the sequencer has nothing. It is the same get() / try_get() pair from the communication between threads and it is useful when the protocol forces you to drive the bus even when there is no stimulus — idle cycles, refresh of a DRAM. The VTALU does not need it: between one operation and the next the signals can stay still.

see this slide in the deck ›

Sequences

What happens between start_item() and finish_item()

code/u7/sequences/tb_classes/random_sequence.svh · #body
   task body();
      command_transaction command;

      repeat (count) begin : random_loop
         // Through the factory, same as in the Transactions section: that is what lets
         // add_test change the TYPE without touching a line of this sequence.
         command = command_transaction::type_id::create("command");

         start_item(command);

         // Late randomization: the values are picked AFTER getting the
         // sequencer's turn, not when the object was created.
         if (!command.randomize())
            `uvm_fatal("RANDOM SEQUENCE", "randomize() failed")

         finish_item(command);
      end : random_loop
   endtask : body
el archivo entero, 31 líneas
class random_sequence extends uvm_sequence #(command_transaction);
   `uvm_object_utils(random_sequence)

   // The length is a field, not a new class. Since the sequence is an object and
   // not a component, whoever starts it can change it before start().
   int unsigned count = 1000;

   function new(string name = "random_sequence");
      super.new(name);
   endfunction : new

   task body();
      command_transaction command;

      repeat (count) begin : random_loop
         // Through the factory, same as in the Transactions section: that is what lets
         // add_test change the TYPE without touching a line of this sequence.
         command = command_transaction::type_id::create("command");

         start_item(command);

         // Late randomization: the values are picked AFTER getting the
         // sequencer's turn, not when the object was created.
         if (!command.randomize())
            `uvm_fatal("RANDOM SEQUENCE", "randomize() failed")

         finish_item(command);
      end : random_loop
   endtask : body

endclass : random_sequence

The honest question: "and what if I randomize before start_item()?". It works. On a small testbench you do not notice the difference. You notice it when the sequence has a thousand items queued up and a constraint depends on something that changes during the run —an address that has already been written, a buffer that has filled up, the previous result—. If you randomized everything at the beginning, you decided with old information. The other reason, more practical: between start_item() and finish_item() is where UVM calls pre_do() and mid_do(), which are the hooks people use to inject errors without touching the original sequence. If you fill the item before, those hooks have nothing to do. And the hook back to day 5: this is where the randomize() with {} of the coverage closure goes. The directed case gets asked for at the point of use, without writing a new class. The slide that follows is exactly that, with the Verilator trap included.

see this slide in the deck ›

Sequences

Directed late randomization, and the Verilator hole

start_item(command);
command.data.constraint_mode(0);                       // 1. I switch off the dist
if (!command.randomize() with {A inside {[1:10]};})    // 2. I ask for the directed one
   `uvm_fatal("SEQ", "randomize() failed")
finish_item(command);
with {op == mul_op}                       100 of 100   op has no dist
with {A inside {[1:10]}}                    1 of 100   A does have one
constraint_mode(0) + with {A inside ...}  100 of 100   the workaround

It is the same limitation they already saw in the Constrained random unit, but here it shows up in the place where they are really going to use it, so it is worth measuring it again against the real transaction. What the measurement adds on top of what docs/en/verilator.md says: the trigger is not "there is a dist in the class". It is the with {} restricting a field that has a dist. Asking for op == mul_op works perfectly, even though A and B have dist in the same resolution. That is why the sequences of the section do not run into this: none of them uses with {}. And the workaround is not a concession: for a directed case, switching off the distribution constraint is the right thing anyway. The dist is there so the random hits the edges; if you already know which value you want, it has nothing to contribute. constraint_mode(0) is per object, not per class, and the object gets thrown away after finish_item(): there is no need to remember to switch it back on.

see this slide in the deck ›

Sequences

The item comes back with the result inside

code/u7/sequences/tb_classes/driver.svh · #run_phase
   task run_phase(uvm_phase phase);
      command_transaction command;
      shortint unsigned   alu_result;
      forever begin : command_loop
         seq_item_port.get_next_item(command);  // blocks until there is an item
         bfm.send_op(command.A, command.B, command.op, alu_result);
         command.result = alu_result;  // the way back
         seq_item_port.item_done();  // unblocks the finish_item()
      end : command_loop
   endtask : run_phase
el archivo entero, 29 líneas
class driver extends uvm_driver #(command_transaction);
   `uvm_component_utils(driver)

   virtual vtalu_bfm bfm;

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

   function void build_phase(uvm_phase phase);
      vtalu_agent_config cfg;
      // The BFM is not asked for on its own: ask for the agent config, and it comes from there.
      if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
         `uvm_fatal("DRIVER", "Failed to get agent config")
      bfm = cfg.bfm;
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_transaction command;
      shortint unsigned   alu_result;
      forever begin : command_loop
         seq_item_port.get_next_item(command);  // blocks until there is an item
         bfm.send_op(command.A, command.B, command.op, alu_result);
         command.result = alu_result;  // the way back
         seq_item_port.item_done();  // unblocks the finish_item()
      end : command_loop
   endtask : run_phase

endclass : driver

This is the subtlest point of the section and it is worth saying it slowly: there is no return channel at all. There is no port, there is no FIFO. There is a shared handle, and the two parties agreed that the driver writes and the sequence reads, and that the safe moment to read is after finish_item(). If somebody asks "wasn't the rule to clone?": yes, that is the rule of the transactions, and this is the documented exception. UVM has besides a formal mechanism for this —the REQ/RSP pair of uvm_sequence #(REQ, RSP) with put_response() on the driver side and get_response() on the sequence side— that almost nobody uses, because writing the result into the request is enough in the vast majority of cases. It is worth naming so they recognise it if they see it. An implementation detail that does matter: the send_op reads bfm.result on the same edge on which it sees done high. One edge later the DUT has already started the next operation.

see this slide in the deck ›

Sequences

The formal way back: get_response()

// The sequence declares TWO types, and the response is no longer the request
class fib_sequence extends uvm_sequence #(command_transaction, result_transaction);
   start_item(cmd);
   finish_item(cmd);
   get_response(rsp);              // blocks until the driver answers

// And the driver, on the other side:
   seq_item_port.get_next_item(req);
   rsp = result_transaction::type_id::create("rsp");
   rsp.set_id_info(req);           // without this the response does not find its sequence
   seq_item_port.item_done(rsp);   // or item_done() now and put_response(rsp) later

This is the half slide that avoids a poor answer in an interview. The question sounds like "how do you give the data back to the sequence?", and answering "I write the result field into the item" is right for this driver and fails on any real bus. It is worth saying both halves together. The reasoning to leave behind: the shared handle trick is not a technique, it is a consequence of the driver being blocking. The condition is written in the get_next_item()/item_done() of the previous slide — as long as the item_done() is after having the response, there is a single item in flight and the handle is enough. The day somebody moves that item_done() up to gain throughput, the testbench keeps compiling and starts lying. The other failure mode, more treacherous: a VIP that clones the item between the sequence and the driver. There the driver writes into its copy, the sequence reads the original, and the result that arrives is always the previous one. There is no error, there is no warning: there is a scoreboard shifted by one. And the set_id_info() detail, which is the one that makes this fail silently the first time: if it is missing, the response is sent anyway and get_response() waits forever. A test that "hangs after the first transaction" and uses REQ/RSP is this bug until proven otherwise.

see this slide in the deck ›

Sequences

Fibonacci: when the stimulus depends on the result

code/u7/sequences/tb_classes/fibonacci_sequence.svh · #body
   task body();
      byte unsigned n_menos_2 = 0;
      byte unsigned n_menos_1 = 1;
      command_transaction command;

      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = rst_op;
      finish_item(command);

      `uvm_info("FIBONACCI", "Fib(01) = 000", UVM_MEDIUM)
      `uvm_info("FIBONACCI", "Fib(02) = 001", UVM_MEDIUM)

      // Up to 14 and no further: Fib(14) = 233 and Fib(15) = 377, which does not
      // fit in the 8 bits of A and B.
      for (int ff = 3; ff <= 14; ff++) begin : fib_loop
         command = command_transaction::type_id::create("command");
         start_item(command);
         command.A  = n_menos_2;
         command.B  = n_menos_1;
         command.op = add_op;
         finish_item(command);        // returns when the driver called item_done()

         n_menos_2 = n_menos_1;
         n_menos_1 = command.result;  // the driver wrote it inside the item
         `uvm_info("FIBONACCI", $sformatf("Fib(%02d) = %03d", ff, n_menos_1),
                   UVM_MEDIUM)
      end : fib_loop
   endtask : body
el archivo entero, 41 líneas
// Fibonacci with the VTALU adder. Every addition needs the RESULT of the previous
// one: it is the case that forces reading the item back, and that with the tester
// of the Transactions section could not be written without giving the monitor a handle.
class fibonacci_sequence extends uvm_sequence #(command_transaction);
   `uvm_object_utils(fibonacci_sequence)

   function new(string name = "fibonacci_sequence");
      super.new(name);
   endfunction : new

   task body();
      byte unsigned n_menos_2 = 0;
      byte unsigned n_menos_1 = 1;
      command_transaction command;

      command = command_transaction::type_id::create("command");
      start_item(command);
      command.op = rst_op;
      finish_item(command);

      `uvm_info("FIBONACCI", "Fib(01) = 000", UVM_MEDIUM)
      `uvm_info("FIBONACCI", "Fib(02) = 001", UVM_MEDIUM)

      // Up to 14 and no further: Fib(14) = 233 and Fib(15) = 377, which does not
      // fit in the 8 bits of A and B.
      for (int ff = 3; ff <= 14; ff++) begin : fib_loop
         command = command_transaction::type_id::create("command");
         start_item(command);
         command.A  = n_menos_2;
         command.B  = n_menos_1;
         command.op = add_op;
         finish_item(command);        // returns when the driver called item_done()

         n_menos_2 = n_menos_1;
         n_menos_1 = command.result;  // the driver wrote it inside the item
         `uvm_info("FIBONACCI", $sformatf("Fib(%02d) = %03d", ff, n_menos_1),
                   UVM_MEDIUM)
      end : fib_loop
   endtask : body

endclass : fibonacci_sequence

Fibonacci is not there to teach Fibonacci: it is there to force the case where the stimulus N+1 depends on the result N. A test like that with the tester of the transactions is impossible without giving the tester a handle to the monitor, that is without breaking the separation we had been building. Numbers for the blackboard: 0 1 1 2 3 5 8 13 21 34 55 89 144 233. It stops at 233 because the next one is 377 and A and B are 8 bits. Having the student see why the for goes up to 14 and not up to 20 is worth more than the whole sequence. And one that is seen in the real run: the whole test takes 500 units of time, against the 43,000 of the random test. Thirteen operations. A well-written directed test is cheap; what is expensive is the random, and that is why it runs at night.

see this slide in the deck ›

Sequences

Sequences that call sequences

full_sequence starting three sub-sequences on the same sequencer

The idea of the slide is that a sequence is not a special layer: it is an object with a body(), and inside a body() another sequence can be started just the way the first one is started. There is no nesting limit nor a different class for "sequence that calls sequences". The practical consequence, which is the one that counts: stimulus gets composed like functions. A small sequence —"write the four registers", "send a maximum multiplication"— gets written once and afterwards goes into any scenario. It is exactly what in the conventional testbench was done by copying and pasting blocks of the tester. The last bullet is the one that avoids the most common misunderstanding: that three sequences are composed does not mean that their items get mixed on the bus. The sequencer hands over one at a time, and if two sequences compete on the same sequencer there is an arbitration policy deciding. With sequences nested on a single sequencer, the order is the one the body() says.

see this slide in the deck ›

Sequences

full_sequence: three pieces, one single sequencer

code/u7/sequences/tb_classes/full_sequence.svh · #body
   task body();
      reset_sequence   reset_seq;
      random_sequence  random_seq;
      maxmult_sequence maxmult_seq;

      reset_seq   = reset_sequence::type_id::create("reset_seq");
      random_seq  = random_sequence::type_id::create("random_seq");
      maxmult_seq = maxmult_sequence::type_id::create("maxmult_seq");

      random_seq.count = count;

      // get_sequencer() returns the sequencer start() handed us: the children run
      // on the same one. The second argument declares them CHILDREN -- without it
      // they compete with us in the arbitration instead of inheriting our turn.
      reset_seq.start(get_sequencer(), this);
      random_seq.start(get_sequencer(), this);
      maxmult_seq.start(get_sequencer(), this);
   endtask : body
el archivo entero, 32 líneas
// The command_sequence of the agents section, split into three pieces that can now be
// combined at will. This sequence sends no item of its own: it only
// starts the other ones.
class full_sequence extends uvm_sequence #(command_transaction);
   `uvm_object_utils(full_sequence)

   int unsigned count = 1000;

   function new(string name = "full_sequence");
      super.new(name);
   endfunction : new

   task body();
      reset_sequence   reset_seq;
      random_sequence  random_seq;
      maxmult_sequence maxmult_seq;

      reset_seq   = reset_sequence::type_id::create("reset_seq");
      random_seq  = random_sequence::type_id::create("random_seq");
      maxmult_seq = maxmult_sequence::type_id::create("maxmult_seq");

      random_seq.count = count;

      // get_sequencer() returns the sequencer start() handed us: the children run
      // on the same one. The second argument declares them CHILDREN -- without it
      // they compete with us in the arbitration instead of inheriting our turn.
      reset_seq.start(get_sequencer(), this);
      random_seq.start(get_sequencer(), this);
      maxmult_seq.start(get_sequencer(), this);
   endtask : body

endclass : full_sequence

get_sequencer() is the accessor of IEEE 1800.2; the field is called m_sequencer and the UVM Primer uses it directly. Both forms work — the get_ one is the one worth teaching, because m_ is the UVM convention for "this is internal". And if somebody has already read production code they are going to ask about p_sequencer: it is a typed handle to the sequencer, which shows up when you use `uvm_declare_p_sequencer(mi_sequencer). It is only useful if you extended uvm_sequencer to put something of your own in it, because m_sequencer is of type uvm_sequencer_base and does not see those fields. Since in this course the sequencer is a bare typedef, p_sequencer contributes nothing. The second argument of the start() is the one almost nobody puts in and that almost always matters: without a parent, if mother and daughter ask for the turn at the same time the sequencer treats them as two independent sequences. With a parent, the daughter inherits priority and context. If they ask about the order with fork/join: the default arbitration is UVM_SEQ_ARB_FIFO, that is order of arrival. It gets changed with set_arbitration() of the sequencer, and there are six modes, one of them with weights.

see this slide in the deck ›

Sequences

Starting a sequence, form 1: start()

code/u7/sequences/tb_classes/full_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      full_sequence full_seq;
      full_seq = full_sequence::type_id::create("full_seq");

      // THE TEST raises the objection, not the sequence: the sequence knows
      // nothing about phases, and has to be able to run inside another sequence.
      phase.raise_objection(this);
      full_seq.start(sequencer_h);   // does not return until body() finished
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 20 líneas
// Same stimulus as the dual_test of the agents section, now in three sequences.
class full_test extends base_test;
   `uvm_component_utils(full_test)

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

   task run_phase(uvm_phase phase);
      full_sequence full_seq;
      full_seq = full_sequence::type_id::create("full_seq");

      // THE TEST raises the objection, not the sequence: the sequence knows
      // nothing about phases, and has to be able to run inside another sequence.
      phase.raise_objection(this);
      full_seq.start(sequencer_h);   // does not return until body() finished
      phase.drop_objection(this);
   endtask : run_phase

endclass : full_test

start() does three things, in this order: it saves the sequencer (it is what get_sequencer() returns afterwards), it calls pre_body() / body() / post_body(), and it comes back. The detail to underline: the objection is raised by the test, not by the sequence. The sequence knows nothing about phases. That is on purpose — the same sequence has to be able to run inside another sequence, where raising an objection would make no sense, and in fact that is what full_sequence does with its three daughters. The sequencer_h is resolved by base_test in end_of_elaboration_phase, not in build_phase: during the build_phase of the test the agent has not built its sequencer yet. It is the same reason why the connections go in connect_phase.

see this slide in the deck ›

Sequences

Form 2: default_sequence through uvm_config_db

code/u7/sequences/tb_classes/default_seq_test.svh · #build_phase
   function void build_phase(uvm_phase phase);
      full_sequence full_seq;

      // super.build_phase() of base_test, which is a class of OURS: it builds the
      // env. What the course never calls is uvm_component's build_phase.
      super.build_phase(phase);

      full_seq = full_sequence::type_id::create("full_seq");
      full_seq.count = 200;

      // WITHOUT this line main_phase ends at t=0: nobody raises the objection,
      // and a phase without an objection lasts zero. The test PASSES with no stimulus.
      full_seq.set_automatic_phase_objection(1);

      // The instance name carries the "_phase" suffix: that is the phase the
      // sequencer will start it in. Notice the test no longer names the sequencer
      // as a handle: it names it as a PATH, and that is a config, not code.
      uvm_config_db#(uvm_sequence_base)::set(
          this, "env_h.clase_agent_h.sequencer_h.main_phase", "default_sequence",
          full_seq);
   endfunction : build_phase
el archivo entero, 32 líneas
// The same stimulus as full_test, WITHOUT run_phase: the sequencer itself starts
// the sequence when the phase begins.
class default_seq_test extends base_test;
   `uvm_component_utils(default_seq_test)

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

   function void build_phase(uvm_phase phase);
      full_sequence full_seq;

      // super.build_phase() of base_test, which is a class of OURS: it builds the
      // env. What the course never calls is uvm_component's build_phase.
      super.build_phase(phase);

      full_seq = full_sequence::type_id::create("full_seq");
      full_seq.count = 200;

      // WITHOUT this line main_phase ends at t=0: nobody raises the objection,
      // and a phase without an objection lasts zero. The test PASSES with no stimulus.
      full_seq.set_automatic_phase_objection(1);

      // The instance name carries the "_phase" suffix: that is the phase the
      // sequencer will start it in. Notice the test no longer names the sequencer
      // as a handle: it names it as a PATH, and that is a config, not code.
      uvm_config_db#(uvm_sequence_base)::set(
          this, "env_h.clase_agent_h.sequencer_h.main_phase", "default_sequence",
          full_seq);
   endfunction : build_phase

endclass : default_seq_test

The trap, and it is one of the ones that give no warning. Without the line set_automatic_phase_objection(1), nobody raises the objection of the main_phase. And a phase without an objection finishes as soon as it starts. What was tried with Verilator 5.052: the run finishes at t=0, prints the Report Summary with 0 UVM_ERROR / 0 UVM_FATAL, and exits with code 0. That is: the test PASSES without having sent a single piece of stimulus. It is in code/u7/sequences as no_objection_test, so it can be run in class alongside the good one. It is exactly the same kind of bug as the new() that eats the override of the transactions: it does not break, it lies. And in a regression of a thousand tests, a test that passes in zero seconds is one nobody looks at. Fine detail: main_phase is the one to use, not run_phase, but not because run_phase does not work — uvm_task_phase::traverse calls start_phase_sequence() on every task phase (base/uvm_task_phase.svh:121), so "…sequencer_h.run_phase" works just as well. The reason is one of coexistence: the run_phase runs in parallel with the schedule reset → configure → main → shutdown, and hooking onto main_phase puts the sequence inside that schedule instead of alongside it.

see this slide in the deck ›

Sequences

When to use which

seq.start(sqr) default_sequence
Where it is written run_phase of the test build_phase of the test, or the top
Objection the test handles it set_automatic_phase_objection(1)
It reads top to bottom you have to go looking for it
Several sequences in order trivial one per phase
Changing it without recompiling no yes, it is a config
Knows the inside of the env yes, through the handle no, only the path
Reusable third-party agent not always possible it is the intended mechanism

In the industry both live side by side and the choice is almost always political, not technical: start() when the one writing the test is the owner of the testbench, default_sequence when the testbench belongs to somebody else. A third way worth naming without developing: the same uvm_config_db from the top module, with null as the context. There the stimulus gets chosen from the command line and the testbench does not even get recompiled. That is how the big regressions work. And the row that matters for the arc of the section is the second to last: default_sequence is what finishes closing the env that the agents encapsulated.

see this slide in the deck ›

Sequences

The `uvm_do macro and why the course does not use it

`uvm_do(command)                         // one line

command = command_transaction::type_id::create("command");   // four
start_item(command);
if (!command.randomize()) `uvm_fatal("SEQ", "randomize() failed")
finish_item(command);

It is the same discussion as the `uvm_field_* macros of the transactions, and the same conclusion: they save typing and charge you in debug. It is worth being fair: `uvm_do_with is genuinely convenient for a one-line inline constraint, and you are going to see it in every testbench in the world. You have to know how to read it. What the course does not do is teach it first: whoever learns with the macro does not know what happens between start_item() and finish_item(), and that is the whole section. A rule of thumb for work: read them all, write the explicit ones.

see this slide in the deck ›

Sequences

The override still works, and no sequence found out

code/u7/sequences/tb_classes/add_test.svh
// The same add_test as the Transactions section, word for word. No sequence finds
// out: random_sequence still asks the factory for a command_transaction,
// and the factory hands it back an add_transaction.
class add_test extends full_test;
   `uvm_component_utils(add_test)

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

   function void build_phase(uvm_phase phase);
      command_transaction::type_id::set_type_override(add_transaction::get_type());
      super.build_phase(phase);
   endfunction : build_phase

endclass : add_test

It is the best proof that the separation worked: changing the data type did not require touching the stimulus, and changing the stimulus does not require touching the data type. Two independent axes, which in the transactions were stuck together and in the agents still shared a class. And the last bullet is the review of the trap of the transactions, now in a place where it can be seen running: the factory picks the TYPE, the constraints only act on randomize().

see this slide in the deck ›

Sequences

Two sequences on the same sequencer: the arbitration

fork
   fondo_seq.start(sqr);                 // priority 100, the default one
   urgente_seq.start(sqr, null, 500);    // the third argument is the priority
join
The mode, in sqr.set_arbitration(...) How it hands out
UVM_SEQ_ARB_FIFO order of arrival, and the priority is not looked at (default)
UVM_SEQ_ARB_STRICT_FIFO the priority rules; at equal priority, order of arrival
UVM_SEQ_ARB_WEIGHTED at random, with the priority as the weight
UVM_SEQ_ARB_RANDOM at random, all equal
UVM_SEQ_ARB_STRICT_RANDOM at random, but only among the highest priority ones
UVM_SEQ_ARB_USER user_priority_arbitration(), you write it

The question that orders the slide is "who decides which item goes into the driver?", and until today the answer was "there is nobody to decide about". The sequencer arbitrates per item, not per sequence: two sequences in a fork do not take turns in blocks, they interleave item by item. It is worth saying because intuition says the opposite. The mistake to leave engraved is in the last bullet and it is out of a manual: the priority gets written, it gets run, and nothing happens — because UVM_SEQ_ARB_FIFO does not look at it. Priority without set_arbitration(UVM_SEQ_ARB_STRICT_FIFO) is an expensive comment. UVM_SEQ_ARB_STRICT_FIFO is the one used 90 % of the time, and the name confuses: strict is about the priority, not about the FIFO. It first orders by priority and only within each level does it respect the order of arrival. And the one almost nobody uses but is worth naming, because it explains what UVM_SEQ_ARB_USER exists for: there are protocols where the turn depends on the state of the DUT —do not send a write if the FIFO of the DUT is full—. That is not a priority, it is a function, and that is what the user mode is for. The cousin of that idea is is_relevant(), which keeps a sequence out of the arbitration until it says itself that it is ready.

see this slide in the deck ›

Sequences

lock() and grab(): when the arbitration is not enough

task body();
   grab();                  // cuts ahead of everyone and closes the door
   ... the items of the scenario, with nobody in between ...
   unlock();                // ungrab() is the same method, under another name
endtask

The difference between the two is one single word and it is worth saying it like this: lock is polite, grab is rude. Both end up with the sequencer all to yourself; what changes is whether you wait your turn or take it. When it is really needed, because abusing this is a classic: only when the scenario loses its meaning if something gets interleaved. A read-modify-write on a register is the canonical example —if another transaction goes by between the read and the write, the value you write is old—. Sending ten items in a row does not need a lock: a sequence already guarantees that. The two endings of the missing unlock() have to be told together, because one warns you and the other does not. If the sequence holding the lock finishes its body(), uvm_sequencer_base::remove_sequence_from_queues (seq/uvm_sequencer_base.svh:1258-1267) takes the lock away from it and reports a UVM_ERROR SEQFINERR — "should not finish before locks … are removed": annoying, but with a name on it. The silent case is the other one: the sequence is still alive and blocked on something else, and there the sequencer stays closed without anybody saying a word. That is the one that sends you looking on the wrong side: the simulation does not advance and the +UVM_OBJECTION_TRACE says nothing odd, because the objection is fine — the one that is stuck is the driver, waiting for an item that is never going to arrive. The tool that shows it is +UVM_TIMEOUT and then reading who has the lock. And the detail that saves an afternoon: grab() does not interrupt the item that is in flight. It cuts in at the next arbitration, not in the middle of a handshake — which is exactly what one wants.

see this slide in the deck ›

Sequences

The testbench, complete

The final testbench: sequences, agent, sequencer, driver, monitors and the analysis layer

The diagram shows one agent, which is the unit of reuse; in code/u7/sequences there are two, the active one and the passive one of the agents, and the bottom one is identical except that it has neither sequencer nor driver. It is worth doing the count out loud: the conventional testbench was an initial of a hundred lines that drove signals. This one has test, env, agent, sequencer, driver, two monitors, coverage and scoreboard, and every piece can be replaced without touching the others. And the three layers at the top —sequences, agent, analysis— change for different reasons: the stimulus changes per test, the structure per design, the analysis per verification plan.

see this slide in the deck ›

Sequences

Summary of the unit · the sequence and the item

The first half of the summary is the mechanism: what a sequence is, where the randomize() lives, and which way the result comes back. Anyone who got lost in the unit catches up here.

see this slide in the deck ›

Sequences

Summary of the unit · how they compose

Close of day 6. None of the pieces we did see is magic: we built them all by hand —the observer of talking to several objects, the FIFO of put and get, the tester of transactions— before UVM handed them to us done. That is the reason the course goes in this order and does not start with the uvm_agent. And the best place to go on reading is code/.uvm/src/: by this point uvm_sequence_base.svh can be opened and understood. What is left for tomorrow is the virtual sequence, and it is left out of today on purpose: it is only understood with this unit's testbench closed, and the morning of day 7 is where the student looks at a new DUT and has to decide which structure it deserves.

see this slide in the deck ›

Review · Day 6

1 of 10 · is_active

An agent in UVM_PASSIVE, what does its build_phase build?

The monitors, always; the sequencer and the driver are left at null — watching is never optional: a passive agent goes on feeding scoreboard and coverage. What gets skipped is what drives the interface, because there somebody else is already driving it.

see this slide in the deck ›

Review · Day 6

2 of 10 · The handshake of the driver

The driver calls get_next_item(), drives the signals and forgets the item_done(). What happens?

It hangs inside finish_item(), and without saying anything — it is the classic mistake of the first week, and the symptom misleads: there is no error and no warning, time stops advancing and the objection never gets dropped. get_next_item() is a loan; item_done() is giving it back.

see this slide in the deck ›

Review · Day 6

3 of 10 · Scope of the config_db

The env instantiates two agents and does both set() with the scope "*". What does each one receive?

Both receive the same thing — the uvm_config_db does not match by order nor by type: it matches by path. With "*" both entries describe the same components, so the last one wins. The scope is a path in the tree, not a label.

see this slide in the deck ›

Review · Day 6

4 of 10 · The sequencer

typedef uvm_sequencer #(command_transaction) sequencer; compiles without touching the transaction of the transactions. Why?

Because of the base class we chose in transactions — uvm_sequencer #(T) requires that T derive from uvm_sequence_item. If that day the transaction had extended a plain uvm_transaction, this typedef would not compile today. A decision of one section enabling the next.

see this slide in the deck ›

Review · Day 6

5 of 10 · super.build_phase()

Instead of the config object, you put is_active straight into the uvm_config_db. In this course the agent starts up active all the same. Why?

That mechanism is switched off — uvm_agent::build_phase looks for is_active in the resource pool (it is in code/.uvm/src/comps/uvm_agent.svh, it can be opened). Without super.build_phase() that line never runs, and nobody warns you. Both ways are valid; what does not work is half of each one.

see this slide in the deck ›

Review · Day 6

6 of 10 · Object, not component

What is the practical difference of a uvm_sequence being a uvm_object and not a uvm_component?

It gets created, runs and is thrown away — you can start several, one after the other, on the same sequencer. A component is built once in build_phase and lives to the end. That is why the stimulus cannot be a component: it changes from test to test, and sometimes within the same test. Out of that comes as well that it can be configured between the create() and the start(), the way full_seq.count = 200 does.

see this slide in the deck ›

Review · Day 6

7 of 10 · start_item()

start_item(command) has just come back. What is it that this guarantees?

You have the turn —nobody else is going to beat you to the driver— and you have not handed anything over yet — and that is why the randomize() goes after: it is the last possible moment to choose the values, when you already know what state the DUT is in. That is late randomization, and it is where pre_do() and mid_do() hook on.

see this slide in the deck ›

Review · Day 6

8 of 10 · The way back

At what moment does command.result have a value that can be read?

After finish_item() — there is no return channel at all: there is a shared handle and an agreement between the two parties. The REQ/RSP pair exists and is the formal mechanism, but almost nobody uses it: writing the result into the request is enough. This is what makes Fibonacci possible.

see this slide in the deck ›

Review · Day 6

9 of 10 · default_sequence

You configure a default_sequence through uvm_config_db and forget the set_automatic_phase_objection(1). What happens?

It passes in zero seconds without sending a single piece of stimulus, and it lies — a phase without an objection finishes as soon as it starts. It is the same kind of bug as the new() that eats the override: it does not break, it deceives. And in a regression of a thousand tests, the one that passes in zero seconds is one nobody looks at.

see this slide in the deck ›

Review · Day 6

10 of 10 · Sub-sequences

full_sequence starts its daughters with reset_seq.start(get_sequencer(), this). What is the second argument for?

It declares the mother-daughter relationship — without it, the sequencer treats the two as independent sequences and they compete for the turn. With it, the daughter inherits the mother's turn and her priority in the arbitration. And for the parallel that is not enough: a fork / join around the start() is needed.

see this slide in the deck ›

Exercise · Day 6 · 1 of 3

The agent that only watches

cd code/ejercicios/d6-agents && bash run.sh

The exercise puts together the three things of the section that can be done wrong without the compiler saying anything: is_active, the hierarchical scope of the set(), and the connect_phase of the analysis ports. The trap almost everybody is going to find is the scope: with "*" in both set(), the second overwrites the first and both agents end up with the same config — same BFM and same is_active. The checker catches it by crossing the counts — the module sends 200 operations and the sequence more than a thousand, so if both agents see similar numbers it means they are watching the same interface. And the shortcut of putting both in UVM_PASSIVE so it "passes" does not work either: the checker demands that clase_agent_h still have a driver.

see this slide in the deck ›

Exercise · Day 6 · 2 of 3

A sequence that only multiplies

cd code/ejercicios/d6-sequences && bash run.sh

It is the same exercise as day 2 and day 3, for the third time and with the final tool: "I want it to only multiply". On day 2 it was solved by adding virtual, on day 3 with a factory override, and today by writing twenty lines of body() without touching a single class of the testbench. It is worth having them compare the three: that is the whole story of the course in one exercise. The trap they are going to find is the first one of the section: if the sequence does not send a rst_op as the first item, the VTALU starts up with reset_n at 0, done never rises and the driver stays in the first finish_item(). The run.sh has a +UVM_TIMEOUT set so that finishes with a message instead of hanging.

see this slide in the deck ›

Exercise · Day 6 · 3 of 3

Three planted bugs, and none of them looks like the others

cd code/ejercicios/d6-debug && bash run.sh

File How it shows up
1 driver.svh it hangs: it dies on [PH_TIMEOUT]
2 default_seq_test.svh it ends at t=0 and says PASS
3 random_sequence.svh it lies in green: add_test passes with the wrong stimulus

It is the only exercise of the course where there is nothing to write: there is something to find. And it is the one that looks most like your first month on the job, where nobody hands you a file with a hole — they hand you a testbench that does not work. The idea that orders the three rows, and worth saying before letting them go: the failure modes of a UVM testbench do not look like each other. The one that hangs shows up at once and costs nothing. The other two pass in green, and those are the ones that cost weeks. All three are in the debug appendix table, and reading it is allowed: the list exists precisely so that the second time it takes them five minutes. The third one deserves its own comment because it breaks the reflex the course has been training: +TOPOLOGY is the right tool for *"the tree is not the one you drew"*… and here it is not enough, because what got built wrong is a uvm_object and the tree only shows uvm_component. The only way to see it is to look at what went out on the bus.

see this slide in the deck ›