Verify This! · the course to read

Day 7The other half, and the wrap-up

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 7 recap works as a hinge, and it is worth saying the two things it is made of: this morning UVM gets closed —the virtual sequence is the last piece, and with it the testbench of the course is whole— and this afternoon opens what UVM does not solve, which is the other half of the plan. The third bullet is the arc planted on day 1 with the verification plan and left open on purpose: the scoreboard checks what, the assertion checks how. Today it closes.

see this slide in the deck ›

Agenda

Day 7 · ≈ 5 h 15 · unit 8 · the other half

And afterwards, day 8: optional, and for whoever has already handed in the capstone

By the end of the day you can:

The morning has three pieces and the order matters: the seed warm-up is ten minutes without SystemVerilog and it hooks into the make regresion the capstone is going to ask for; the virtual sequences close UVM; and SVA opens the other half of the plan. The afternoon is the capstone. The third objective is the one the course is worth, and it is honest to put it as an objective of the day even if the hand-in comes later: in a company format the capstone is handed in the next day, and in a term it goes to the exam period. What gets done this afternoon is starting it with the instructor next to you, which is when it pays off the most.

see this slide in the deck ›

Exercise · Day 7 · 1 of 2

The same sequence, another seed

cd code/ejercicios/d7-semillas && bash run.sh

It is the last row of the coverage closure table, done, and it is the warm-up of day 7: ten minutes, zero SystemVerilog, and it leaves in place the make regresion this afternoon's capstone is going to ask for. It also closes the arc day 5 opened with the directed case: one fills the bin random cannot reach, the other accumulates what random does reach, and they do not replace each other. The detail to underline is why the test sends 25 operations and not a thousand: with a thousand this does not work. It is measured and it is on the slide "another seed, and again" — u2/convencional and u7/sequences give 66 out of 76 with any seed, and the merge too. With 25 the stimulus has not saturated yet and there each seed does cover a different piece: 41 to 47 bins each, 62 for the five merged. The rule they take away, in one line: another seed accumulates while the stimulus has not saturated, and reproduces an intermittent failure. It does not fill a bin the stimulus cannot reach. And the merge command is worth naming for what it is in the industry: verilator_coverage --write is here what the ucdb merge is in Questa. A real regression is a hundred seeds overnight and one single number in the morning.

see this slide in the deck ›

Virtual sequences

The problem: two interfaces and a single start()

First hour of day 7, and that is on purpose. It is the last piece of the reusable testbench, and it arrives on capstone day because it is the day the student looks at a new DUT and decides which structure it deserves. The capstone has two APB slaves, each with its own interface —one driven by the testbench, the other by a module— and that is the shape of the agents section: one active and one passive, a single sequencer, and no virtual sequence. This hour is what gives the student the criterion to decide that instead of guessing it, and what leaves them the answer written down for the day the second slave is theirs too. It is worth starting from the problem and not from the syntax, because the syntax is half a screen: what is hard to understand is why a new class is needed when full_sequence already composes sequences. The answer is the one in the last bullet, and it is geometric: a normal sequence knows one sequencer, the one start() handed it. Anything that wants to talk to two interfaces needs two handles, and those handles have to come from somewhere. The example runs and it is in code/u7/sequences/virtual/: it is the testbench of this section with two lines changed in the env, and everything else by reference. It is worth saying before showing the code, because it is half of the argument: a virtual sequence does not change the structure of the testbench.

see this slide in the deck ›

Virtual sequences

The virtual sequencer: handles, not items

code/u7/sequences/virtual/tb_classes/virtual_sequencer.svh · #the-handles
class virtual_sequencer extends uvm_sequencer;
   `uvm_component_utils(virtual_sequencer)

   sequencer clase_sequencer_h;
   sequencer modulo_sequencer_h;

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

endclass : virtual_sequencer
el archivo entero, 20 líneas
// The virtual sequencer: it has no queue, it arbitrates no items, it talks to no
// driver. It is a component that exists for two things:
//
//   1. to hold the HANDLES to the real sequencers, somewhere the sequence can
//      reach without looking up by string;
//   2. to be a uvm_component, that is, to live in the tree and have a name.
//
// That is why it extends uvm_sequencer WITHOUT parameters: the default item is
// uvm_sequence_item, and none is ever sent.
class virtual_sequencer extends uvm_sequencer;
   `uvm_component_utils(virtual_sequencer)

   sequencer clase_sequencer_h;
   sequencer modulo_sequencer_h;

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

endclass : virtual_sequencer
code/u7/sequences/virtual/tb_classes/env.svh · #wiring-the-sequencers
      // The handles are passed HERE and not looked up by string: in connect_phase
      // both agents already exist, and if somebody renames a component this does
      // not compile instead of returning null at simulation time.
      virtual_sequencer_h.clase_sequencer_h  = clase_agent_h.sequencer_h;
      virtual_sequencer_h.modulo_sequencer_h = modulo_agent_h.sequencer_h;
el archivo entero, 65 líneas
// The env of the Sequences section with TWO changes, and no more:
//
//   1. the second agent is UVM_ACTIVE instead of UVM_PASSIVE;
//   2. there is a virtual_sequencer, and connect_phase hands it the handles of
//      the two real sequencers.
//
// The rest --agents, scoreboards, coverage, the config_db scope-- is
// identical. That is the point: a virtual sequence does not change the structure
// of the testbench, it only adds who coordinates it.
class env extends uvm_env;
   `uvm_component_utils(env);

   vtalu_agent        clase_agent_h, modulo_agent_h;
   vtalu_agent_config clase_cfg_h, modulo_cfg_h;

   scoreboard clase_scoreboard_h, modulo_scoreboard_h;
   coverage   clase_coverage_h, modulo_coverage_h;

   virtual_sequencer virtual_sequencer_h;

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

   function void build_phase(uvm_phase phase);
      env_config env_config_h;

      if (!uvm_config_db#(env_config)::get(this, "", "config", env_config_h))
         `uvm_fatal("ENV", "Failed to get env config")

      // Both active: each with its own sequencer and driver.
      clase_cfg_h  = new(.bfm(env_config_h.clase_bfm),  .is_active(UVM_ACTIVE));
      modulo_cfg_h = new(.bfm(env_config_h.modulo_bfm), .is_active(UVM_ACTIVE));

      uvm_config_db#(vtalu_agent_config)::set(this, "clase_agent_h*", "config", clase_cfg_h);
      uvm_config_db#(vtalu_agent_config)::set(this, "modulo_agent_h*", "config", modulo_cfg_h);

      clase_agent_h  = vtalu_agent::type_id::create("clase_agent_h", this);
      modulo_agent_h = vtalu_agent::type_id::create("modulo_agent_h", this);

      virtual_sequencer_h = virtual_sequencer::type_id::create("virtual_sequencer_h", this);

      clase_scoreboard_h  = scoreboard::type_id::create("clase_scoreboard_h", this);
      modulo_scoreboard_h = scoreboard::type_id::create("modulo_scoreboard_h", this);
      clase_coverage_h    = coverage::type_id::create("clase_coverage_h", this);
      modulo_coverage_h   = coverage::type_id::create("modulo_coverage_h", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      // The handles are passed HERE and not looked up by string: in connect_phase
      // both agents already exist, and if somebody renames a component this does
      // not compile instead of returning null at simulation time.
      virtual_sequencer_h.clase_sequencer_h  = clase_agent_h.sequencer_h;
      virtual_sequencer_h.modulo_sequencer_h = modulo_agent_h.sequencer_h;

      clase_agent_h.command_ap.connect(clase_scoreboard_h.cmd_f.analysis_export);
      clase_agent_h.command_ap.connect(clase_coverage_h.analysis_export);
      clase_agent_h.result_ap.connect(clase_scoreboard_h.analysis_export);

      modulo_agent_h.command_ap.connect(modulo_scoreboard_h.cmd_f.analysis_export);
      modulo_agent_h.command_ap.connect(modulo_coverage_h.analysis_export);
      modulo_agent_h.result_ap.connect(modulo_scoreboard_h.analysis_export);
   endfunction : connect_phase

endclass : env

The question that comes up on its own: and why a component, if it is only two pointers? For two reasons. One, so the sequence reaches it with p_sequencer instead of looking it up by string. Two, because it has to be in the tree so the test can start a sequence on top of it: seq.start(env_h.virtual_sequencer_h). The book resolves the handles with uvm_top.find("*.env_h.sequencer_h"). It still works —uvm_top is there for compatibility, the current form is uvm_root::get()— but looking components up by string is fragile: the day somebody renames the env_h, the find() returns null and the fatal shows up far from the change. Assigning them in the connect_phase does not compile if the name changed, which is exactly what one wants.

see this slide in the deck ›

Virtual sequences

The virtual sequence: p_sequencer and two branches

code/u7/sequences/virtual/tb_classes/coordinada_sequence.svh · #p-sequencer
// `uvm_declare_p_sequencer declares `p_sequencer` with the type above and casts
// it on its own inside m_set_p_sequencer. Without it, get_sequencer() returns a
// uvm_sequencer_base and every use would need a cast by hand.
class coordinada_sequence extends uvm_sequence;
   `uvm_object_utils(coordinada_sequence)
   `uvm_declare_p_sequencer(virtual_sequencer)
el archivo entero, 76 líneas
// THE virtual sequence.
//
// Two things set it apart from every sequence in the Sequences section:
//
//   1. It has NO items of its own. It extends uvm_sequence without parameters
//      and never calls start_item(): all it does is start OTHER sequences.
//      Hence "virtual" -- it is tied to no item type and no sequencer.
//   2. It runs on more than one sequencer, and that is where the one thing no
//      single sequence can do comes from: coordinating two interfaces.

// `uvm_declare_p_sequencer declares `p_sequencer` with the type above and casts
// it on its own inside m_set_p_sequencer. Without it, get_sequencer() returns a
// uvm_sequencer_base and every use would need a cast by hand.
class coordinada_sequence extends uvm_sequence;
   `uvm_object_utils(coordinada_sequence)
   `uvm_declare_p_sequencer(virtual_sequencer)

   int unsigned count = 1000;

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

   task body();
      reset_sequence   reset_a, reset_b;
      maxmult_sequence maxmult;
      random_sequence  random_b;
      un_op_sequence   primera, segunda;

      reset_a  = reset_sequence::type_id::create("reset_a");
      reset_b  = reset_sequence::type_id::create("reset_b");
      maxmult  = maxmult_sequence::type_id::create("maxmult");
      random_b = random_sequence::type_id::create("random_b");
      primera  = un_op_sequence::type_id::create("primera");
      segunda  = un_op_sequence::type_id::create("segunda");

      random_b.count = count;

      // --- 1. Both ALUs get reset at the same time -----------------------------
      // fork/join over TWO sequencers. Each branch blocks on its own driver, and
      // the join waits for both: that cannot be written inside a normal
      // sequence, which only knows the sequencer that started it.
      fork
         reset_a.start(p_sequencer.clase_sequencer_h);
         reset_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 2. Parallel traffic, different on each interface -------------------
      // A does the directed overflow case while B sends at random.
      fork
         maxmult.start(p_sequencer.clase_sequencer_h);
         random_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 3. What no single sequence can do ----------------------------------
      // The result of VTALU A is the operand of B. It is a dependency BETWEEN
      // interfaces: the second one cannot even start being built until the
      // first one answered.
      primera.A  = 8'h0F;
      primera.B  = 8'h07;
      primera.op = mul_op;
      primera.start(p_sequencer.clase_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "A returned %0d: it goes as an operand into B", primera.result), UVM_LOW)

      segunda.A  = primera.result[7:0];
      segunda.B  = 8'h01;
      segunda.op = add_op;
      segunda.start(p_sequencer.modulo_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "B returned %0d", segunda.result), UVM_LOW)
   endtask : body

endclass : coordinada_sequence
code/u7/sequences/virtual/tb_classes/coordinada_sequence.svh · #the-fork
      // --- 1. Both ALUs get reset at the same time -----------------------------
      // fork/join over TWO sequencers. Each branch blocks on its own driver, and
      // the join waits for both: that cannot be written inside a normal
      // sequence, which only knows the sequencer that started it.
      fork
         reset_a.start(p_sequencer.clase_sequencer_h);
         reset_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 2. Parallel traffic, different on each interface -------------------
      // A does the directed overflow case while B sends at random.
      fork
         maxmult.start(p_sequencer.clase_sequencer_h);
         random_b.start(p_sequencer.modulo_sequencer_h);
      join
el archivo entero, 76 líneas
// THE virtual sequence.
//
// Two things set it apart from every sequence in the Sequences section:
//
//   1. It has NO items of its own. It extends uvm_sequence without parameters
//      and never calls start_item(): all it does is start OTHER sequences.
//      Hence "virtual" -- it is tied to no item type and no sequencer.
//   2. It runs on more than one sequencer, and that is where the one thing no
//      single sequence can do comes from: coordinating two interfaces.

// `uvm_declare_p_sequencer declares `p_sequencer` with the type above and casts
// it on its own inside m_set_p_sequencer. Without it, get_sequencer() returns a
// uvm_sequencer_base and every use would need a cast by hand.
class coordinada_sequence extends uvm_sequence;
   `uvm_object_utils(coordinada_sequence)
   `uvm_declare_p_sequencer(virtual_sequencer)

   int unsigned count = 1000;

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

   task body();
      reset_sequence   reset_a, reset_b;
      maxmult_sequence maxmult;
      random_sequence  random_b;
      un_op_sequence   primera, segunda;

      reset_a  = reset_sequence::type_id::create("reset_a");
      reset_b  = reset_sequence::type_id::create("reset_b");
      maxmult  = maxmult_sequence::type_id::create("maxmult");
      random_b = random_sequence::type_id::create("random_b");
      primera  = un_op_sequence::type_id::create("primera");
      segunda  = un_op_sequence::type_id::create("segunda");

      random_b.count = count;

      // --- 1. Both ALUs get reset at the same time -----------------------------
      // fork/join over TWO sequencers. Each branch blocks on its own driver, and
      // the join waits for both: that cannot be written inside a normal
      // sequence, which only knows the sequencer that started it.
      fork
         reset_a.start(p_sequencer.clase_sequencer_h);
         reset_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 2. Parallel traffic, different on each interface -------------------
      // A does the directed overflow case while B sends at random.
      fork
         maxmult.start(p_sequencer.clase_sequencer_h);
         random_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 3. What no single sequence can do ----------------------------------
      // The result of VTALU A is the operand of B. It is a dependency BETWEEN
      // interfaces: the second one cannot even start being built until the
      // first one answered.
      primera.A  = 8'h0F;
      primera.B  = 8'h07;
      primera.op = mul_op;
      primera.start(p_sequencer.clase_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "A returned %0d: it goes as an operand into B", primera.result), UVM_LOW)

      segunda.A  = primera.result[7:0];
      segunda.B  = 8'h01;
      segunda.op = add_op;
      segunda.start(p_sequencer.modulo_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "B returned %0d", segunda.result), UVM_LOW)
   endtask : body

endclass : coordinada_sequence

The uvm_declare_p_sequencer is sugar, and it is worth saying so: the only thing it does is declare the variable and redefine m_set_p_sequencer() with the $cast. If the sequencer it gets started on is not of that type, the fatal fires there and not twenty lines later. The detail that gets overlooked: the two branches of the fork start different sequences on different sequencers. If they were two sequences on the same sequencer, they would compete for the arbitration and the items would come out interleaved — which is a legitimate scenario, but it is another one. And a warning from the field: a virtual sequence with a single sequencer is a normal sequence with more ceremony. The class is justified when there are two.

see this slide in the deck ›

Virtual sequences

What no sequence on its own can do

code/u7/sequences/virtual/tb_classes/coordinada_sequence.svh · #the-ordered-pair
      // --- 3. What no single sequence can do ----------------------------------
      // The result of VTALU A is the operand of B. It is a dependency BETWEEN
      // interfaces: the second one cannot even start being built until the
      // first one answered.
      primera.A  = 8'h0F;
      primera.B  = 8'h07;
      primera.op = mul_op;
      primera.start(p_sequencer.clase_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "A returned %0d: it goes as an operand into B", primera.result), UVM_LOW)

      segunda.A  = primera.result[7:0];
      segunda.B  = 8'h01;
      segunda.op = add_op;
      segunda.start(p_sequencer.modulo_sequencer_h);
el archivo entero, 76 líneas
// THE virtual sequence.
//
// Two things set it apart from every sequence in the Sequences section:
//
//   1. It has NO items of its own. It extends uvm_sequence without parameters
//      and never calls start_item(): all it does is start OTHER sequences.
//      Hence "virtual" -- it is tied to no item type and no sequencer.
//   2. It runs on more than one sequencer, and that is where the one thing no
//      single sequence can do comes from: coordinating two interfaces.

// `uvm_declare_p_sequencer declares `p_sequencer` with the type above and casts
// it on its own inside m_set_p_sequencer. Without it, get_sequencer() returns a
// uvm_sequencer_base and every use would need a cast by hand.
class coordinada_sequence extends uvm_sequence;
   `uvm_object_utils(coordinada_sequence)
   `uvm_declare_p_sequencer(virtual_sequencer)

   int unsigned count = 1000;

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

   task body();
      reset_sequence   reset_a, reset_b;
      maxmult_sequence maxmult;
      random_sequence  random_b;
      un_op_sequence   primera, segunda;

      reset_a  = reset_sequence::type_id::create("reset_a");
      reset_b  = reset_sequence::type_id::create("reset_b");
      maxmult  = maxmult_sequence::type_id::create("maxmult");
      random_b = random_sequence::type_id::create("random_b");
      primera  = un_op_sequence::type_id::create("primera");
      segunda  = un_op_sequence::type_id::create("segunda");

      random_b.count = count;

      // --- 1. Both ALUs get reset at the same time -----------------------------
      // fork/join over TWO sequencers. Each branch blocks on its own driver, and
      // the join waits for both: that cannot be written inside a normal
      // sequence, which only knows the sequencer that started it.
      fork
         reset_a.start(p_sequencer.clase_sequencer_h);
         reset_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 2. Parallel traffic, different on each interface -------------------
      // A does the directed overflow case while B sends at random.
      fork
         maxmult.start(p_sequencer.clase_sequencer_h);
         random_b.start(p_sequencer.modulo_sequencer_h);
      join

      // --- 3. What no single sequence can do ----------------------------------
      // The result of VTALU A is the operand of B. It is a dependency BETWEEN
      // interfaces: the second one cannot even start being built until the
      // first one answered.
      primera.A  = 8'h0F;
      primera.B  = 8'h07;
      primera.op = mul_op;
      primera.start(p_sequencer.clase_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "A returned %0d: it goes as an operand into B", primera.result), UVM_LOW)

      segunda.A  = primera.result[7:0];
      segunda.B  = 8'h01;
      segunda.op = add_op;
      segunda.start(p_sequencer.modulo_sequencer_h);

      `uvm_info("COORDINADA", $sformatf(
                "B returned %0d", segunda.result), UVM_LOW)
   endtask : body

endclass : coordinada_sequence

This is the slide that justifies the class. The two fork of the previous slide could be imitated with two tests running in parallel; this one cannot, because there is a piece of data that crosses from one interface to the other in the middle of the scenario. It is exactly the shape of the real case the bus appendix promises: configure over APB, read the status, and only then send traffic over AXI with what the configuration returned. The protocol changes, not the structure. And the criterion for the afternoon: in the capstone the second slave is driven by a module, so there is one sequencer and this is not needed. The virtual sequence comes in the day both interfaces belong to the testbench and one depends on the other — and then it is written just like here. And the honest close: the example runs on two VTALU because it is the DUT we have. With two different interfaces —two transactions, two drivers— the virtual sequence is written the same: the handles would be of two types of sequencer, and nothing more.

see this slide in the deck ›

Virtual sequences

And if branch B has to wait for A?

// uvm_event: a notice, from one to many. It comes from the global pool, nobody builds it
uvm_event listo = uvm_event_pool::get_global("dut_configurado");

listo.trigger();          // the branch that configured gives notice
listo.wait_trigger();     // the one waiting blocks here
listo.wait_ptrigger();    // the same, but if it already happened, it walks on

// uvm_barrier: a meeting point between N branches
uvm_barrier arranque = uvm_barrier_pool::get_global("arranque");
arranque.set_threshold(2);
arranque.wait_for();      // neither of the two goes on until both have arrived

This is the question that shows up on its own as soon as the fork of the previous slide is drawn, and it is worth answering right there: "and if branch B cannot start until A configured the DUT?". The distinction to make clear is one to many versus symmetric. The uvm_event has a side that gives notice and another that waits, and the roles do not change: it is for "the reset finished", "the DUT is configured", "the interrupt arrived". The uvm_barrier has no sides: the N branches execute the same line and none goes on until all are there. It is for "start together" and for "everybody wait until the configuration phase ends". The wait_ptrigger() deserves its own second, because it is the classic race: if branch A triggers before B reaches the wait_trigger(), B waits forever. The p is for persistent: it asks whether the event already happened at some point. The practical rule: if you cannot guarantee the order, wait_ptrigger(). And why not a shared bit, which is the first thing anybody tries: a bit does not keep history, does not carry attached data, and cannot be reset between phases without somebody swallowing a pulse. The two UVM objects exist precisely because that homemade version fails once every hundred runs. Its own failure mode: a barrier with the threshold set wrong does not give an error — it hangs. The symptom is a test that ends on +UVM_TIMEOUT without a single UVM_ERROR, and +UVM_OBJECTION_TRACE shows the objection up and nobody moving on.

see this slide in the deck ›

Assertions (SVA)

The hole the scoreboard left

It is worth opening the section with the question and leaving it hanging for half a minute: which part of the testbench would notice if the DUT raised done two cycles early? The honest answer is none, and it comes as a surprise, because by this point the testbench already has a scoreboard, coverage, two agents and sequences. The reason is structural, not an oversight: the scoreboard receives transactions, which is precisely what is left after erasing the protocol. The monitor erases it in the analysis ports and it does well: that is what makes the scoreboard not know that a clk exists. The price is that the layer that sees the wires —the interface— is the only one that can check them, and until today it only moved them. This is also the answer to "why is SVA the other half of verification?": it is not another technique for doing the same thing, it is the half this testbench does not cover.

see this slide in the deck ›

Assertions (SVA)

Immediate and concurrent: the one you know and the new one

// IMMEDIATE: it is a STATEMENT. It runs when the thread goes past it,
// once, and that is all. The one from the transactions is of this family.
assert (cmd.op inside {add_op, sub_op, and_op, xor_op, mul_op})
   else `uvm_error("SEQ", "invalid operation");

// CONCURRENT -- the new thing. It is a DECLARATION with a clock: it gets
// plugged in when the simulation starts and evaluated on every edge, forever.
a_done : assert property (@(posedge clk) start |=> ##[0:4] done);

The distinction is hard at first and it is worth anchoring it with the RTL analogy: an immediate one is to an if what a concurrent one is to an always_ff. The first executes; the second gets instantiated. The practical consequence is the one that matters: the immediate one can only look at the present —the value of a variable at that instant—, while the concurrent one has a time axis of its own and can say "if this happens, three cycles later that has to happen". No amount of if writes that sentence without inventing state machines by hand. And the hook back to the transactions: assert(x.randomize()) is an immediate one, and there is the silent trap of that section —assert is a simulation directive, and with asserts switched off the argument does not get executed. Here you see why: they are the same keyword for two different things.

see this slide in the deck ›

Assertions (SVA)

Anatomy of a property

a_done_llega :                      // <- the label: NOT optional
assert property (
   @(posedge clk)                   // <- the clock: when it gets sampled
   disable iff (!reset_n)           // <- when NOT to check
   start && (op_set != no_op)       // <- the antecedent
   |->                              // <- the implication
   ##[1:5] done                     // <- the consequent
) else `uvm_error("SVA", "...");    // <- what to do if it fails

It is worth writing this slide on the blackboard as a template and going back to it every time a new property comes up: the student who gets lost in SVA almost always gets lost because they do not tell the antecedent from the consequent. The label deserves a paragraph of its own. Without it, the simulator invents one (__unnamed$$_0), and that name is the one that is going to show up in the log of the nightly regression and in the assertion coverage report. It is exactly the same argument as the begin : name the course has been using since interfaces and BFM. The disable iff is looked at in detail further on, but it is worth getting ahead of why it is so high up in the template: it is the part that gets forgotten most and the one that generates the most false positives.

see this slide in the deck ›

Assertions (SVA)

|-> against |=>, mistake number one

// |->  overlapping: the consequent starts on THE SAME edge
a : assert property (@(posedge clk) start |-> !done);

// |=>  non-overlapping: the consequent starts on the NEXT edge
b : assert property (@(posedge clk) start |=> !done);

This is the first question of every SVA interview and it is worth practising it with the DUT in hand: done_1c <= start && (op != 3'b000) is an NBA, so what gets written on edge n is only read on n+1. With |-> the property would compare start against the old done. The operational rule, which is more useful than the theory: |=> is |-> ##1, so the question is not which of the two operators goes in but how many edges later the spec promises it. An assign answers on the same edge and a <= on the next one, but that is the easy case: the star property of this very section —start && op_set != no_op |-> ##[1:5] done— uses |-> for a done that comes out of an always_ff, because the spec promises a window of one to five edges and not a single one. And the trap to name out loud, without overstating it: getting this wrong sometimes shouts —|-> against a registered signal fails on every transaction— and sometimes stays quiet, when the antecedent you wrote never occurs and the property is vacuously satisfied. That second case is the one the final report takes as good, and it is the reason why the cover property slide is not an extra: it is the only check of the check.

see this slide in the deck ›

Assertions (SVA)

Looking at the past: $rose, $fell, $stable, $past

$rose(start)      // 0 -> 1 between the previous edge and this one
$fell(done)       // 1 -> 0
$stable(A)        // the value is the same as on the previous edge
$past(result, 3)  // the value it had 3 edges ago

Careful with $rose on a signal that moves on the same edge the property samples: it is not going to see it go up on that edge, it is going to see it go up on the next one. That is exactly the topic of the slide that follows, and it is worth planting it here. $past with the second argument is the one that gets underestimated the most: checking "the result of now corresponds to the operands of four edges ago" is one line, and by hand it is a shift register and three bugs. It is worth saying as well where they can be used, because there is a half-truth going around: they can be called from procedural code —an always @(posedge clk) if ($rose(req)) … is legal (1800-2017 §16.9.3) and Verilator 5.052 accepts it—, because that is where they infer the clock from. What they always need is a clock: in an initial with no clock event they mean nothing. And the practical limit of this flow: $past with the explicit clock argument is not supported by Verilator (Unsupported: $past expr2 and/or clock arguments).

see this slide in the deck ›

Assertions (SVA)

The property that is worth the section

code/u8/assertions/vtalu_bfm.sv · #stable-operands
   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))
el archivo entero, 217 líneas
interface vtalu_bfm;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "uvm_macros.svh"

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;

   command_monitor        command_monitor_h;

   function operation_t op2enum();
      case (op_set)
         3'b000:  return no_op;
         3'b001:  return add_op;
         3'b010:  return sub_op;
         3'b011:  return and_op;
         3'b100:  return xor_op;
         3'b101:  return mul_op;
         default: $fatal(1, "Illegal operation on op bus");
      endcase  // case (op_set)
   endfunction : op2enum

   always @(posedge clk) begin : op_monitor
      static bit in_command = 0;
      // The null guard rst_monitor already had, now here too. With two BFMs in
      // the top, one can be left without a monitor while the testbench is being
      // built -- or forever, if someone forgets to instantiate the passive
      // agent. Without the guard that is a simulator crash instead of a
      // testbench that sees nothing.
      if (command_monitor_h != null) begin : con_monitor
         if (start) begin : start_high
            if (!in_command) begin : new_command
               command_monitor_h.write_to_monitor(A, B, op2enum());
               in_command = (op2enum() != no_op);
            end : new_command
         end : start_high
         else  // start low
            in_command = 0;
      end : con_monitor
   end : op_monitor

   always @(negedge reset_n) begin : rst_monitor
      if (command_monitor_h != null)  //guard against VCS time 0 negedge
         command_monitor_h.write_to_monitor(A, B, rst_op);
   end : rst_monitor

   result_monitor result_monitor_h;

   initial begin : result_monitor_thread
      forever begin : result_monitor
         @(posedge clk);
         // Same guard as op_monitor and rst_monitor: with two BFMs in the top,
         // one can be left without a monitor. Without this, forgetting the
         // passive agent is a simulator "Null pointer dereferenced" instead of
         // the checker's message -- which is exactly where exercise d6 starts.
         if (done && result_monitor_h != null) result_monitor_h.write_to_monitor(result, ovf);
      end : result_monitor
   end : result_monitor_thread

   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end


   // --- the protocol, in the BFM ---
   // Everything that knows HOW the DUT is talked to lives here, in one place:
   // the rest of the testbench asks for an operation and never touches a wire.
   // That is the idea of the interfaces-and-BFM section, and it holds to the end of the course.

   task reset_alu();
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start   = 1'b0;
   endtask : reset_alu

   // NEW with the sequences: the fourth argument. The protocol did not change; the only
   // thing added is reading result on the edge where done is already up.
   bit bug_operandos;
   initial bug_operandos = $test$plusargs("BUG");

   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint unsigned oresult);
      oresult = 0;
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start   = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)
   endtask : send_op


   // ==========================================================================
   //  The protocol, checked where it happens
   // ==========================================================================
   //
   // The properties live HERE, with the signals, and not in the testbench: they
   // plug themselves in, nobody connects them, and the passive agent of the Agents
   // section gets them for free. All they need in order to run is --assert.

   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);

   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))

   // --- The DUT's answer ---

   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))

   // --- Every assertion comes with its cover property ---
   //
   // An assertion whose antecedent never happens PASSES, and checks nothing.
   // The cover is the antidote. And it also debunks mental models: the
   // "three-cycle" multiplication takes FOUR edges (done3 -> done2 ->
   // done1 -> done_mult), so c_mult_3ciclos stays at 0 forever.
   // --- VTALU rev2: the output the scoreboard only glances at ---

   // ovf belongs to sub and to nobody else. An assertion, because it is a rule of
   // the spec and not arithmetic: the scoreboard checks WHAT it computes, this
   // checks that it does not invent a flag where none belongs.
   property p_ovf_solo_en_sub;
      @(posedge clk) done && (op_set != sub_op) |-> !ovf;
   endproperty : p_ovf_solo_en_sub

   a_ovf_solo_en_sub :
   assert property (p_ovf_solo_en_sub)
   else `uvm_error("SVA", $sformatf("%m: ovf arriba con op=%s, que no puede desbordar", op2enum().name()))

   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);

endinterface : vtalu_bfm

This is the moment to go back to the spec slide and read it word for word. The distance between "start has to stay at 1 and the operands stable until the operation finishes" and the line of SVA on screen is almost zero — and that is the whole argument for why assertions get written early and not at the end: the specification is already written, it only has to be translated. The $stable(op_set) at the end is the one that tends to be missing and it is the one that catches the ugliest bug: changing the operation halfway through a transaction is legal for the compiler, illegal for the DUT and transparent for the scoreboard. It is worth showing the @(negedge clk) and not explaining it yet: leave it as a curiosity that gets cleared up on the next slide. Somebody is going to ask first.

see this slide in the deck ›

Assertions (SVA)

⚠ Two clocks: an assertion is worth what its sampling is worth

code/u8/assertions/vtalu_bfm.sv · #two-clocks
   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);
el archivo entero, 217 líneas
interface vtalu_bfm;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "uvm_macros.svh"

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;

   command_monitor        command_monitor_h;

   function operation_t op2enum();
      case (op_set)
         3'b000:  return no_op;
         3'b001:  return add_op;
         3'b010:  return sub_op;
         3'b011:  return and_op;
         3'b100:  return xor_op;
         3'b101:  return mul_op;
         default: $fatal(1, "Illegal operation on op bus");
      endcase  // case (op_set)
   endfunction : op2enum

   always @(posedge clk) begin : op_monitor
      static bit in_command = 0;
      // The null guard rst_monitor already had, now here too. With two BFMs in
      // the top, one can be left without a monitor while the testbench is being
      // built -- or forever, if someone forgets to instantiate the passive
      // agent. Without the guard that is a simulator crash instead of a
      // testbench that sees nothing.
      if (command_monitor_h != null) begin : con_monitor
         if (start) begin : start_high
            if (!in_command) begin : new_command
               command_monitor_h.write_to_monitor(A, B, op2enum());
               in_command = (op2enum() != no_op);
            end : new_command
         end : start_high
         else  // start low
            in_command = 0;
      end : con_monitor
   end : op_monitor

   always @(negedge reset_n) begin : rst_monitor
      if (command_monitor_h != null)  //guard against VCS time 0 negedge
         command_monitor_h.write_to_monitor(A, B, rst_op);
   end : rst_monitor

   result_monitor result_monitor_h;

   initial begin : result_monitor_thread
      forever begin : result_monitor
         @(posedge clk);
         // Same guard as op_monitor and rst_monitor: with two BFMs in the top,
         // one can be left without a monitor. Without this, forgetting the
         // passive agent is a simulator "Null pointer dereferenced" instead of
         // the checker's message -- which is exactly where exercise d6 starts.
         if (done && result_monitor_h != null) result_monitor_h.write_to_monitor(result, ovf);
      end : result_monitor
   end : result_monitor_thread

   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end


   // --- the protocol, in the BFM ---
   // Everything that knows HOW the DUT is talked to lives here, in one place:
   // the rest of the testbench asks for an operation and never touches a wire.
   // That is the idea of the interfaces-and-BFM section, and it holds to the end of the course.

   task reset_alu();
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start   = 1'b0;
   endtask : reset_alu

   // NEW with the sequences: the fourth argument. The protocol did not change; the only
   // thing added is reading result on the edge where done is already up.
   bit bug_operandos;
   initial bug_operandos = $test$plusargs("BUG");

   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint unsigned oresult);
      oresult = 0;
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start   = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)
   endtask : send_op


   // ==========================================================================
   //  The protocol, checked where it happens
   // ==========================================================================
   //
   // The properties live HERE, with the signals, and not in the testbench: they
   // plug themselves in, nobody connects them, and the passive agent of the Agents
   // section gets them for free. All they need in order to run is --assert.

   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);

   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))

   // --- The DUT's answer ---

   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))

   // --- Every assertion comes with its cover property ---
   //
   // An assertion whose antecedent never happens PASSES, and checks nothing.
   // The cover is the antidote. And it also debunks mental models: the
   // "three-cycle" multiplication takes FOUR edges (done3 -> done2 ->
   // done1 -> done_mult), so c_mult_3ciclos stays at 0 forever.
   // --- VTALU rev2: the output the scoreboard only glances at ---

   // ovf belongs to sub and to nobody else. An assertion, because it is a rule of
   // the spec and not arithmetic: the scoreboard checks WHAT it computes, this
   // checks that it does not invent a flag where none belongs.
   property p_ovf_solo_en_sub;
      @(posedge clk) done && (op_set != sub_op) |-> !ovf;
   endproperty : p_ovf_solo_en_sub

   a_ovf_solo_en_sub :
   assert property (p_ovf_solo_en_sub)
   else `uvm_error("SVA", $sformatf("%m: ovf arriba con op=%s, que no puede desbordar", op2enum().name()))

   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);

endinterface : vtalu_bfm
With a single clock Over 1000 operations
everything on posedge 145 false positives on the stability of operands
everything on negedge false positives on the done properties
stimulus on negedge, response on posedge 0 errors ✅

This is the most valuable lesson of the section and it is not in the tutorials, so it is worth telling it with the trace in hand: on two consecutive no_op, start goes down at t=111 and comes back up at t=120 — between two posedge. The sampling does not see it go down. The two transactions read as a single one, with the operands changing in the middle, and the property screams 145 times about something that never happened. The short way of saying it: an assertion does not check what happened, it checks what it saw. And what it sees depends on a single character in the @(). It is another silent trap, and one of the worst: it compiles, it runs, and it throws 145 errors at you that do not exist. The beginner's reflex is to loosen the property until it shuts up —and there they are left without a check. The correct reflex is to look at which edge the one driving the stimulus writes on. The underlying industrial way out is the clocking block, which declares the sampling once for the whole interface instead of repeating it property by property. It is in day 1 —030-interfaces-bfm.md and docs/clocking-blocks.md (in Spanish)— and it is worth naming again here, because only now is the full problem it solves in view.

see this slide in the deck ›

Assertions (SVA)

The pulse the sampling does not see

Two no_op in a row: start goes down at 111 and up at 120, between two posedge

This is the figure to pause on and walk through with a finger, because the whole argument is in two edges. It is worth asking before showing it: how many transactions does an assertion sampled on posedge see? The answer read off the drawing is one, and there are two. The amber band is what the section has been saying in words: between t=111 and t=120 the signal changes twice and no posedge sampling lands in there. It is not a bug of the simulator nor of the property — it is that sampling happens in preponed, and at t=130 what it sees is start high, same as at t=110. And the second row is the cure: the same drawing with the other edge. At t=120 the negedge sampling sees start low, the antecedent is false, and the property does not even start. One character. The detail almost nobody looks at: the values of A, B and op_set change exactly at t=120, that is, inside the transaction the posedge sampling believes it is watching. That is why the 145 errors talk about operand stability and not about something else.

see this slide in the deck ›

Assertions (SVA)

Variable latency, in one line

code/u8/assertions/vtalu_bfm.sv · #done-arrives
   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))
el archivo entero, 217 líneas
interface vtalu_bfm;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "uvm_macros.svh"

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;

   command_monitor        command_monitor_h;

   function operation_t op2enum();
      case (op_set)
         3'b000:  return no_op;
         3'b001:  return add_op;
         3'b010:  return sub_op;
         3'b011:  return and_op;
         3'b100:  return xor_op;
         3'b101:  return mul_op;
         default: $fatal(1, "Illegal operation on op bus");
      endcase  // case (op_set)
   endfunction : op2enum

   always @(posedge clk) begin : op_monitor
      static bit in_command = 0;
      // The null guard rst_monitor already had, now here too. With two BFMs in
      // the top, one can be left without a monitor while the testbench is being
      // built -- or forever, if someone forgets to instantiate the passive
      // agent. Without the guard that is a simulator crash instead of a
      // testbench that sees nothing.
      if (command_monitor_h != null) begin : con_monitor
         if (start) begin : start_high
            if (!in_command) begin : new_command
               command_monitor_h.write_to_monitor(A, B, op2enum());
               in_command = (op2enum() != no_op);
            end : new_command
         end : start_high
         else  // start low
            in_command = 0;
      end : con_monitor
   end : op_monitor

   always @(negedge reset_n) begin : rst_monitor
      if (command_monitor_h != null)  //guard against VCS time 0 negedge
         command_monitor_h.write_to_monitor(A, B, rst_op);
   end : rst_monitor

   result_monitor result_monitor_h;

   initial begin : result_monitor_thread
      forever begin : result_monitor
         @(posedge clk);
         // Same guard as op_monitor and rst_monitor: with two BFMs in the top,
         // one can be left without a monitor. Without this, forgetting the
         // passive agent is a simulator "Null pointer dereferenced" instead of
         // the checker's message -- which is exactly where exercise d6 starts.
         if (done && result_monitor_h != null) result_monitor_h.write_to_monitor(result, ovf);
      end : result_monitor
   end : result_monitor_thread

   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end


   // --- the protocol, in the BFM ---
   // Everything that knows HOW the DUT is talked to lives here, in one place:
   // the rest of the testbench asks for an operation and never touches a wire.
   // That is the idea of the interfaces-and-BFM section, and it holds to the end of the course.

   task reset_alu();
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start   = 1'b0;
   endtask : reset_alu

   // NEW with the sequences: the fourth argument. The protocol did not change; the only
   // thing added is reading result on the edge where done is already up.
   bit bug_operandos;
   initial bug_operandos = $test$plusargs("BUG");

   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint unsigned oresult);
      oresult = 0;
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start   = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)
   endtask : send_op


   // ==========================================================================
   //  The protocol, checked where it happens
   // ==========================================================================
   //
   // The properties live HERE, with the signals, and not in the testbench: they
   // plug themselves in, nobody connects them, and the passive agent of the Agents
   // section gets them for free. All they need in order to run is --assert.

   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);

   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))

   // --- The DUT's answer ---

   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))

   // --- Every assertion comes with its cover property ---
   //
   // An assertion whose antecedent never happens PASSES, and checks nothing.
   // The cover is the antidote. And it also debunks mental models: the
   // "three-cycle" multiplication takes FOUR edges (done3 -> done2 ->
   // done1 -> done_mult), so c_mult_3ciclos stays at 0 forever.
   // --- VTALU rev2: the output the scoreboard only glances at ---

   // ovf belongs to sub and to nobody else. An assertion, because it is a rule of
   // the spec and not arithmetic: the scoreboard checks WHAT it computes, this
   // checks that it does not invent a flag where none belongs.
   property p_ovf_solo_en_sub;
      @(posedge clk) done && (op_set != sub_op) |-> !ovf;
   endproperty : p_ovf_solo_en_sub

   a_ovf_solo_en_sub :
   assert property (p_ovf_solo_en_sub)
   else `uvm_error("SVA", $sformatf("%m: ovf arriba con op=%s, que no puede desbordar", op2enum().name()))

   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);

endinterface : vtalu_bfm

The upper bound is what separates a useful assertion from a decorative one. ##[1:$] done is also true for a DUT that answers next Tuesday: it compiles, it passes, and it checks nothing. The number that goes there comes out of the specification, not out of what the DUT does today — and if it is not in the specification, that is the finding of the section. The 5 in this example has a deliberate margin over the 4 real edges of the multiplication. It is worth asking the group whether they would prefer ##[1:4], and why. The honest answer is that it depends on whether the specification says "four" or says "up to five": an assertion is a contract, and tightening it more than the contract turns it into a source of false positives when somebody changes the pipeline. The p_no_op_sin_done below is the complement and it uses |=> precisely because of what the previous slide said: done comes out of an always_ff.

see this slide in the deck ›

Assertions (SVA)

disable iff and the reset

default disable iff (!reset_n);   // once, for the whole interface

property p;
   @(posedge clk) disable iff (!reset_n)   // or property by property
   start |=> done;
endproperty

The second classic mistake, after the |->/|=>. And the symptom misleads: the regression fills up with errors at the beginning of every test, which is precisely when one looks least, because "it is still initialising". It is worth naming the inverse mistake, which is more dangerous: a disable iff with the condition the wrong way round —disable iff (reset_n)— switches the property off during normal operation and leaves it active only during the reset. It always passes, it never checks, and there is no way of realising it from the log. It is the second silent trap of the section and it is also headed off with cover property. The difference between aborting and failing matters when the reset arrives halfway through a long transaction. In this DUT it is one cycle; on a real bus, with transactions of dozens, it is the difference between a usable property and one that has to be switched off.

see this slide in the deck ›

Assertions (SVA)

Where they live: inside the interface

This is the slide that connects the section with the whole architecture of the course. The interface had been the place where the testbench moves wires; from here on it is also where it watches over them, and both for the same reason: it is the only layer that sees them. The reuse argument is the one that convinces a team: the properties travel with the interface. Whoever instantiates this BFM in another project gets the protocol check already fitted, without reading a line of UVM. It is exactly what does not happen with a scoreboard, which has to be built, connected and configured. And the twist from the agents is worth saying slowly: the legacy module —the "boss's tester", without one line of UVM— is also being checked. Nobody asked its permission. That is the exercise of the day.

see this slide in the deck ›

Assertions (SVA)

bind: when the interface is not yours

// The checker lives outside. The RTL is not touched -- often it cannot be.
module apb_checks (input bit PCLK, PSEL, PENABLE, PREADY);
   a_setup : assert property (@(posedge PCLK) PSEL && !PENABLE |=> PENABLE);
   c_setup : cover  property (@(posedge PCLK) PSEL && !PENABLE);
endmodule

// And in the top, one line per module you want to watch over:
bind apb_regs apb_checks chk (.*);

This is the interview question that follows the previous slide, and it is worth putting it like this: "very nice to put the properties in the interface — and when the interface is not yours?". The answer is bind, and it is the way assertions are used in the industry: one checker file per block, all bound from the top, and the RTL without a line of verification inside. The underlying reason is not aesthetic: in a synthesis flow the RTL that is handed over is the one that gets synthesized, and putting assert property inside it forces everybody to carry the verification along. With bind, whoever synthesizes does not compile the checker file and that is that. The concrete advantage, and the one that makes it worth it: the bound checker sees the DUT's internal signals — the FSM state, the wait state counter —, which is exactly what the interface does not see. That is where assertions stop checking the protocol and start checking the implementation. And the warning: a badly written bind does not fail, connects nothing and the property is left not running. As always, the antidote is the cover property — if the cover is at zero, the bind did not arrive.

see this slide in the deck ›

Assertions (SVA)

The integration with UVM: the else that makes it count

// The default: $error, and in Verilator also $stop — it cuts off on the first failure
a : assert property (p);

// What you want in UVM: the failure counts and the simulation goes on
a : assert property (p)
    else `uvm_error("SVA", $sformatf("%m: operand changed"));

It is the bridge between the two halves of the course and it has to be made explicit: the assertion is not a world apart with its own report. It is one more uvm_error, and that is why uvm_summary_ok of common.sh detects it without anything having had to be touched. The reason the default is no good is practical: a nightly regression that gets cut off at the first failure reports one failure. The same run with uvm_error reports all 154, and that is the difference between "something is wrong" and "it is wrong on every multiplication". The %m looks like a detail and it is not: with two instances of the same interface, a message without %m does not say which of the two ALUs broke. It is the same problem get_full_name() solved in reporting, with the tool of the language instead of the one of the library.

see this slide in the deck ›

Assertions (SVA)

Every assertion goes with its cover property

code/u8/assertions/vtalu_bfm.sv · #the-covers
   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);
el archivo entero, 217 líneas
interface vtalu_bfm;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "uvm_macros.svh"

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;

   command_monitor        command_monitor_h;

   function operation_t op2enum();
      case (op_set)
         3'b000:  return no_op;
         3'b001:  return add_op;
         3'b010:  return sub_op;
         3'b011:  return and_op;
         3'b100:  return xor_op;
         3'b101:  return mul_op;
         default: $fatal(1, "Illegal operation on op bus");
      endcase  // case (op_set)
   endfunction : op2enum

   always @(posedge clk) begin : op_monitor
      static bit in_command = 0;
      // The null guard rst_monitor already had, now here too. With two BFMs in
      // the top, one can be left without a monitor while the testbench is being
      // built -- or forever, if someone forgets to instantiate the passive
      // agent. Without the guard that is a simulator crash instead of a
      // testbench that sees nothing.
      if (command_monitor_h != null) begin : con_monitor
         if (start) begin : start_high
            if (!in_command) begin : new_command
               command_monitor_h.write_to_monitor(A, B, op2enum());
               in_command = (op2enum() != no_op);
            end : new_command
         end : start_high
         else  // start low
            in_command = 0;
      end : con_monitor
   end : op_monitor

   always @(negedge reset_n) begin : rst_monitor
      if (command_monitor_h != null)  //guard against VCS time 0 negedge
         command_monitor_h.write_to_monitor(A, B, rst_op);
   end : rst_monitor

   result_monitor result_monitor_h;

   initial begin : result_monitor_thread
      forever begin : result_monitor
         @(posedge clk);
         // Same guard as op_monitor and rst_monitor: with two BFMs in the top,
         // one can be left without a monitor. Without this, forgetting the
         // passive agent is a simulator "Null pointer dereferenced" instead of
         // the checker's message -- which is exactly where exercise d6 starts.
         if (done && result_monitor_h != null) result_monitor_h.write_to_monitor(result, ovf);
      end : result_monitor
   end : result_monitor_thread

   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end


   // --- the protocol, in the BFM ---
   // Everything that knows HOW the DUT is talked to lives here, in one place:
   // the rest of the testbench asks for an operation and never touches a wire.
   // That is the idea of the interfaces-and-BFM section, and it holds to the end of the course.

   task reset_alu();
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start   = 1'b0;
   endtask : reset_alu

   // NEW with the sequences: the fourth argument. The protocol did not change; the only
   // thing added is reading result on the edge where done is already up.
   bit bug_operandos;
   initial bug_operandos = $test$plusargs("BUG");

   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint unsigned oresult);
      oresult = 0;
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start   = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)
   endtask : send_op


   // ==========================================================================
   //  The protocol, checked where it happens
   // ==========================================================================
   //
   // The properties live HERE, with the signals, and not in the testbench: they
   // plug themselves in, nobody connects them, and the passive agent of the Agents
   // section gets them for free. All they need in order to run is --assert.

   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);

   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))

   // --- The DUT's answer ---

   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))

   // --- Every assertion comes with its cover property ---
   //
   // An assertion whose antecedent never happens PASSES, and checks nothing.
   // The cover is the antidote. And it also debunks mental models: the
   // "three-cycle" multiplication takes FOUR edges (done3 -> done2 ->
   // done1 -> done_mult), so c_mult_3ciclos stays at 0 forever.
   // --- VTALU rev2: the output the scoreboard only glances at ---

   // ovf belongs to sub and to nobody else. An assertion, because it is a rule of
   // the spec and not arithmetic: the scoreboard checks WHAT it computes, this
   // checks that it does not invent a flag where none belongs.
   property p_ovf_solo_en_sub;
      @(posedge clk) done && (op_set != sub_op) |-> !ovf;
   endproperty : p_ovf_solo_en_sub

   a_ovf_solo_en_sub :
   assert property (p_ovf_solo_en_sub)
   else `uvm_error("SVA", $sformatf("%m: ovf arriba con op=%s, que no puede desbordar", op2enum().name()))

   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);

endinterface : vtalu_bfm
code/u8/assertions/cover.txt
  covergroup : 86.8% (66/76)
  user       : 80.0% ( 4/ 5)

This slide closes the circle the conventional testbench opened and it is worth saying so: the question "how do I know I measured?" had three answers in the course, and this is the third one. Functional coverage says what stimulus was generated; assertion coverage says which checks actually got evaluated. The case of c_mult_3ciclos is a pedagogical gift and it came out of writing the section, not out of inventing it: the intuitive count was three —the module is called vtalu_mult— and the hardware takes four, because the done travels through a pipeline of its own. The property with ##3 would have passed just the same if it had been a badly written assert, and the cover at zero is the only thing that gives it away. The rule to take away: for every assert property you write, write the cover property of the antecedent. It is two lines and it is the difference between a testbench that checks and one that says it checks.

see this slide in the deck ›

Assertions (SVA)

Assertion or scoreboard: the same rule, two places

Assertion Scoreboard
What it checks the protocol: who moves, and when the transformation: A op B
Where it lives in the interface, with the wires in the testbench, with the transactions
When it screams on the exact edge on which it breaks when the result arrives
What it needs nothing: it plugs itself in monitor, analysis port, connect_phase

The question that always comes up: why not check everything in a single place? The answer is that the two layers see different things and neither can see the other's without undoing the monitor's work. The advantage of the "it screams on the exact edge" is the one that saves the most time in real life, and it is worth putting in numbers: a scoreboard mismatch gives you a wrong result and you have to track backwards until you find the cycle where it started. An assertion gives you the cycle. On a bus with overlapping transactions, that is the difference between half an hour and two days. The other half of the rule matters too: SVA does not replace the scoreboard. Nobody wants to read a reference model written in properties, and whoever tried it tells it as an anecdote.

see this slide in the deck ›

Assertions (SVA)

The example of the section: the bug the scoreboard does not see

code/u8/assertions/vtalu_bfm.sv · #the-planted-bug
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
el archivo entero, 217 líneas
interface vtalu_bfm;
   import uvm_pkg::*;
   import vtalu_pkg::*;
   `include "uvm_macros.svh"

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;

   command_monitor        command_monitor_h;

   function operation_t op2enum();
      case (op_set)
         3'b000:  return no_op;
         3'b001:  return add_op;
         3'b010:  return sub_op;
         3'b011:  return and_op;
         3'b100:  return xor_op;
         3'b101:  return mul_op;
         default: $fatal(1, "Illegal operation on op bus");
      endcase  // case (op_set)
   endfunction : op2enum

   always @(posedge clk) begin : op_monitor
      static bit in_command = 0;
      // The null guard rst_monitor already had, now here too. With two BFMs in
      // the top, one can be left without a monitor while the testbench is being
      // built -- or forever, if someone forgets to instantiate the passive
      // agent. Without the guard that is a simulator crash instead of a
      // testbench that sees nothing.
      if (command_monitor_h != null) begin : con_monitor
         if (start) begin : start_high
            if (!in_command) begin : new_command
               command_monitor_h.write_to_monitor(A, B, op2enum());
               in_command = (op2enum() != no_op);
            end : new_command
         end : start_high
         else  // start low
            in_command = 0;
      end : con_monitor
   end : op_monitor

   always @(negedge reset_n) begin : rst_monitor
      if (command_monitor_h != null)  //guard against VCS time 0 negedge
         command_monitor_h.write_to_monitor(A, B, rst_op);
   end : rst_monitor

   result_monitor result_monitor_h;

   initial begin : result_monitor_thread
      forever begin : result_monitor
         @(posedge clk);
         // Same guard as op_monitor and rst_monitor: with two BFMs in the top,
         // one can be left without a monitor. Without this, forgetting the
         // passive agent is a simulator "Null pointer dereferenced" instead of
         // the checker's message -- which is exactly where exercise d6 starts.
         if (done && result_monitor_h != null) result_monitor_h.write_to_monitor(result, ovf);
      end : result_monitor
   end : result_monitor_thread

   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end


   // --- the protocol, in the BFM ---
   // Everything that knows HOW the DUT is talked to lives here, in one place:
   // the rest of the testbench asks for an operation and never touches a wire.
   // That is the idea of the interfaces-and-BFM section, and it holds to the end of the course.

   task reset_alu();
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start   = 1'b0;
   endtask : reset_alu

   // NEW with the sequences: the fourth argument. The protocol did not change; the only
   // thing added is reading result on the edge where done is already up.
   bit bug_operandos;
   initial bug_operandos = $test$plusargs("BUG");

   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint unsigned oresult);
      oresult = 0;
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start   = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            // +BUG=1: changes B halfway through the multiplication. The
            // multiplier already latched A and B on the first edge, so the
            // RESULT DOES NOT CHANGE: the scoreboard stays green. The only
            // thing that sees it is the assertion. That is the whole section.
            if (bug_operandos && iop == mul_op) begin
               @(negedge clk);
               @(negedge clk);
               B = ~iB;
            end
            do @(negedge clk); while (done == 0);
            oresult   = result;
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)
   endtask : send_op


   // ==========================================================================
   //  The protocol, checked where it happens
   // ==========================================================================
   //
   // The properties live HERE, with the signals, and not in the testbench: they
   // plug themselves in, nobody connects them, and the passive agent of the Agents
   // section gets them for free. All they need in order to run is --assert.

   // TWO CLOCKS, and it is not an implementation detail: the BFM drives on
   // negedge and the DUT registers on posedge. An assertion samples in the
   // preponed region of the edge it is given, so a signal written ON the negedge
   // is not seen on that negedge: it is seen on the next one. With everything on
   // posedge, two consecutive no_op -- start goes down at t=111 and back up at
   // t=120, between two posedges -- read as ONE transaction with the operands
   // changing: 145 false positives every 1000 operations. With everything on
   // negedge, the false positives move to the done properties.
   //
   //   stimulus (start, A, B, op_set)  -> negedge, where the BFM writes
   //   response (done, result)         -> posedge, where the DUT registers

   default disable iff (!reset_n);

   // --- The stimulus ---

   // The rule from slide 1 of day 1, executable at last: while start is up, the
   // operands and the operation are not touched.
   property p_operandos_estables;
      @(negedge clk) start |=> $stable(A) && $stable(B) && $stable(op_set);
   endproperty : p_operandos_estables

   a_operandos_estables :
   assert property (p_operandos_estables)
   else
      `uvm_error("SVA", $sformatf(
                 "%m: operand changed with start up: A=%0d B=%0d op=%s", A, B, op2enum().name()))

   // --- The DUT's answer ---

   // The variable latency from the VTALU spec section, in one line: one cycle for the
   // one-cycle ops, four edges for the multiplication.
   property p_done_llega;
      @(posedge clk) start && (op_set != no_op) |-> ##[1:5] done;
   endproperty : p_done_llega

   a_done_llega :
   assert property (p_done_llega)
   else `uvm_error("SVA", $sformatf("%m: start con op=%s y done no llego en 5 ciclos", op2enum().name()))

   // The fine print: no_op is the only operation that does not answer.
   property p_no_op_sin_done;
      @(posedge clk) start && (op_set == no_op) |=> !done;
   endproperty : p_no_op_sin_done

   a_no_op_sin_done :
   assert property (p_no_op_sin_done)
   else `uvm_error("SVA", $sformatf("%m: done subio para una no_op"))

   // --- Every assertion comes with its cover property ---
   //
   // An assertion whose antecedent never happens PASSES, and checks nothing.
   // The cover is the antidote. And it also debunks mental models: the
   // "three-cycle" multiplication takes FOUR edges (done3 -> done2 ->
   // done1 -> done_mult), so c_mult_3ciclos stays at 0 forever.
   // --- VTALU rev2: the output the scoreboard only glances at ---

   // ovf belongs to sub and to nobody else. An assertion, because it is a rule of
   // the spec and not arithmetic: the scoreboard checks WHAT it computes, this
   // checks that it does not invent a flag where none belongs.
   property p_ovf_solo_en_sub;
      @(posedge clk) done && (op_set != sub_op) |-> !ovf;
   endproperty : p_ovf_solo_en_sub

   a_ovf_solo_en_sub :
   assert property (p_ovf_solo_en_sub)
   else `uvm_error("SVA", $sformatf("%m: ovf arriba con op=%s, que no puede desbordar", op2enum().name()))

   // And the cover that goes with it: without this, a regression where the random
   // never subtracted too much leaves the assertion green having checked nothing.
   c_ovf : cover property (@(posedge clk) done && ovf);
   c_sub_sin_borrow : cover property (@(posedge clk) done && (op_set == sub_op) && !ovf);

   c_mult_3ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##3 done);
   c_mult_4ciclos : cover property (@(posedge clk) $rose(start) && (op_set == mul_op) ##4 done);
   c_un_ciclo : cover property (@(posedge clk) $rose(start) && (op_set inside {add_op, sub_op, and_op, xor_op}) ##1 done);

endinterface : vtalu_bfm
code/u8/assertions/sva.txt
** Report counts by severity
UVM_INFO :    2
UVM_WARNING :    0
UVM_ERROR :  154
UVM_FATAL :    0
** Report counts by id
[MAXMULT]     1
[RNTST]     1
[SVA]   154

Run both runs live if there is time: bash code/u8/assertions/run.sh does the clean one and the buggy one, one after the other, and the comparison of the two Report Summary is the slide. It is worth pointing out why the bug is invisible, because it is not obvious: the pipeline does a_int <= A on the first edge and mult1 <= a_int * b_int on the second. Everything that happens to A and B after the first edge gets discarded. A designer would say the DUT is robust; a verification engineer would say the testbench is lying — the stimulus violated the contract and nobody found out. And the punchline: this bug is not hypothetical. It is exactly what the legacy module of the exercise does, and it is exactly the kind of thing that survives for years in a block that "works".

see this slide in the deck ›

Assertions (SVA)

The traps of this section

The symptom The cause How to head it off
The property always passes the antecedent never occurs one cover property per assertion
False positives on every transaction you sample it with the edge it is written on stimulus and response go on different edges
False positives at the start of every test the disable iff (!reset) is missing default disable iff once, at the top
The log says PASS and the properties did not run --assert is missing at compile time the cover at 0 is the one that warns you

The fourth row deserves a minute because it is specific to the flow of this course and comes as a surprise: --assert is not a "more warnings" flag, it is the switch that turns a declaration into a check. Without it, the whole block of the interface is expensive documentation. And the antidote is the same for all four, which is the elegant part: the cover property. If the cover is at zero, either the property does not run, or its antecedent does not occur. In both cases you have to go and look, and in both cases the assert on its own would have said that everything is fine. These four are added to the appendix of the silent traps, which from this section on are twenty-one.

see this slide in the deck ›

Assertions (SVA)

Summary of the unit

If the group takes away a single sentence, let it be the last one. Everything else in this section is syntax and can be looked up; the sampling is judgement, and it is what separates a property that checks from one that makes noise or from one that stays quiet. The second one to take away is the one about the cover property, for a cultural reason: it is the only defence against the testbench that feels safe. A team with three hundred assertions and no assertion coverage does not know how many it is running. And it is worth closing with the location on the map: this is the half that was missing. The scoreboard checks what the DUT computes; the assertions, how you talk to it. A serious verification plan has both columns.

see this slide in the deck ›

Assertions (SVA)

What this section does differently

It is worth being explicit with the group about where each thing comes from, because it is part of what the course teaches: the difference between reading about SVA and writing SVA is exactly the two-clocks slide. No tutorial has it because the tutorials use a DUT where stimulus and response share an edge. It is also the answer to "why one more UVM course?". This one runs. And when something does not add up, the section tells why it did not add up instead of changing the example. What is left out and is worth naming so nobody discovers it late: formal assertions, expect, and the prefabricated bus properties that come with commercial VIP. With what is in this section they can be read without help.

see this slide in the deck ›

Exercise · Day 7 · 2 of 2

The legacy module violates the protocol and nobody knew

cd code/ejercicios/d7-sva && bash run.sh

The exercise gets solved with a three-line property, but the second condition of the checker is the one that teaches: whoever samples it on posedge is going to see false positives on the interface that does respect the protocol, and the message is going to send them to look at the edge. It is the two-clocks slide, cashed in. It is worth insisting on the cover property hint for whoever gets no firings: the natural reflex is to loosen the property until it does something, and it is the wrong reflex. The cover at 0 tells "the property is badly written" apart from "the antecedent does not occur", and they are two different problems. And the moral that goes beyond the exercise: nobody wrote the property thinking about the boss's module. It lives in the interface, and that is why it checks everybody who uses it — including code that was written five years before this testbench existed.

see this slide in the deck ›

Review · Day 7

1 of 5 · Immediate and concurrent

What is the underlying difference between assert(x.randomize()) and assert property (@(posedge clk) …)?

Statement against declaration — the immediate one runs when the thread goes past it; the concurrent one gets evaluated on its own, on every edge of its clock. It is to an if what the concurrent one is to an always_ff: one executes, the other gets instantiated. That is why only the concurrent one can describe something that lasts several cycles.

see this slide in the deck ›

Review · Day 7

2 of 5 · |-> against |=>

The done of the VTALU comes out of an always_ff. Which implication goes in start |?? done?

|=> when the consequent comes out of a <= — because |=> is |-> ##1, and the question to ask is how many edges later the spec promises it. With |-> against a registered signal the property does not pass vacuously: it fails on every transaction, because it compares against the old done. The one that passes quietly is the one whose antecedent never occurs — which is why it always goes with its cover property.

see this slide in the deck ›

Review · Day 7

3 of 5 · The sampling edge

All the properties of the VTALU sampled on @(posedge clk) give 145 errors over 1000 operations, and the DUT is healthy. Why?

An assertion is worth what its sampling is worth — the BFM writes the stimulus on the negedge, and on two consecutive no_op start goes down and comes back up between two posedge: the sampling does not see it go down. The stimulus gets sampled where the stimulus gets written. Stimulus on negedge, response of the DUT on posedge: zero errors. With a single clock there is no way.

see this slide in the deck ›

Review · Day 7

4 of 5 · The assertion that checks nothing

An assert property reports 0 failures during the whole regression. What do you know?

Zero failures and zero evaluations look the same — its antecedent may never have occurred, or --assert may be missing and it is not even being evaluated. That is why every assertion goes with its cover property: it is the only check of the check. In the section, c_mult_3ciclos stays at 0 and gives away that the real latency is four edges, not three.

see this slide in the deck ›

Review · Day 7

5 of 5 · Assertion or scoreboard

The DUT gives back the right result but drops done one cycle earlier than the specification says. Who catches it?

Protocol → assertion. Data → scoreboard — and it is not a preference: by the time the transaction reaches the scoreboard, the protocol is no longer there. Writing the check there would be rebuilding by hand the time the monitor has just erased.

see this slide in the deck ›

Capstone · Day 7

The whole testbench, from a blank sheet

cd code/ejercicios/d7-final && cat spec.en.md

It is the afternoon of day 7 and it is the exercise that justifies the whole course. It is worth saying it that bluntly: up to here the student has completed somebody else's structure, which is exactly what they are going to be asked for on the first day of a real project — but they have never built one. The difference between "I did the course" and "I know how to do it" is this afternoon. If the group is short of time, stage 1 —the monitor— on its own already leaves something: it is the deliverable the bus appendix recommends as the first one, and it gets done in half an hour. The other use, for whoever is studying alone: it is the exercise that can be shown in an interview. A UVM testbench on an APB, written from scratch, with its coverage plan, is exactly the example they ask for.

see this slide in the deck ›

Capstone · Day 7

The DUT: four registers and the small print

Addr Name Access Contents
0x00 CTRL RW bit 0 = EN · bit 1 = CLR, autoclear
0x04 SCRATCH RW 32 bits, and adds to ACC if EN
0x08 ACC RO the accumulator
0x0C STATUS RO bit 0 = EN · bit 1 = OVF, sticky
code/ejercicios/d7-final/rtl/apb_regs.sv · #handshake
   // The handshake: the write does not wait, the read inserts ONE wait state.
   // rd_wait re-arms as soon as PENABLE drops, that is between one transfer and
   // the next, with no need to count cycles.
   logic rd_wait;
   always_ff @(posedge PCLK or negedge PRESETn) begin
      if (!PRESETn) rd_wait <= 1'b1;
      else if (!PENABLE) rd_wait <= 1'b1;
      else if (access && !PWRITE) rd_wait <= 1'b0;
   end

   assign PREADY  = !PSEL ? 1'b1 : (PWRITE ? 1'b1 : !rd_wait);
   assign PSLVERR = access && !mapped;
el archivo entero, 95 líneas
// The capstone DUT: an APB3 slave with four registers.
//
// The spec is in spec.md, and it is the only thing to read in order to verify it.
// This file is NOT touched: if your testbench needs to change the RTL to pass,
// the one that is wrong is the testbench.
//
// The only liberty taken is bug_en, which would not exist in a real DUT:
// with +BUG=1 the accumulator stops looking at CTRL.EN. It is there so you can
// check that your scoreboard checks something -- a scoreboard that never saw an
// error is not tested.
module apb_regs (
    input  logic        PCLK,
    input  logic        PRESETn,
    input  logic        PSEL,
    input  logic        PENABLE,
    input  logic        PWRITE,
    input  logic [ 7:0] PADDR,
    input  logic [31:0] PWDATA,
    input  logic        bug_en,
    output logic [31:0] PRDATA,
    output logic        PREADY,
    output logic        PSLVERR
);

   logic        en;       // CTRL[0]
   logic        ovf;      // STATUS[1], pegajoso
   logic [31:0] scratch;  // SCRATCH
   logic [31:0] acc;      // ACC

   // The map fits in 16 bytes: four 32-bit registers. PADDR[1:0] is
   // ignored; from 0x10 up there is nobody and the answer is PSLVERR.
   wire mapped = (PADDR < 8'h10);
   wire access = PSEL && PENABLE;

   // The handshake: the write does not wait, the read inserts ONE wait state.
   // rd_wait re-arms as soon as PENABLE drops, that is between one transfer and
   // the next, with no need to count cycles.
   logic rd_wait;
   always_ff @(posedge PCLK or negedge PRESETn) begin
      if (!PRESETn) rd_wait <= 1'b1;
      else if (!PENABLE) rd_wait <= 1'b1;
      else if (access && !PWRITE) rd_wait <= 1'b0;
   end

   assign PREADY  = !PSEL ? 1'b1 : (PWRITE ? 1'b1 : !rd_wait);
   assign PSLVERR = access && !mapped;

   // The cycle where the transfer actually happens: it is the only edge that
   // matters to the monitor, and the only one where the DUT changes state.
   wire xfer = access && PREADY;

   always_comb begin
      PRDATA = 32'h0;
      if (mapped)
         case (PADDR[3:2])
            2'd0: PRDATA = {31'h0, en};        // CTRL: CLR es autoclear, lee 0
            2'd1: PRDATA = scratch;
            2'd2: PRDATA = acc;
            default: PRDATA = {30'h0, ovf, en};  // STATUS
         endcase
   end

   logic [32:0] suma;
   assign suma = {1'b0, acc} + {1'b0, PWDATA};

   always_ff @(posedge PCLK or negedge PRESETn) begin
      if (!PRESETn) begin
         en      <= 1'b0;
         ovf     <= 1'b0;
         scratch <= 32'h0;
         acc     <= 32'h0;
      end else if (xfer && PWRITE && mapped) begin
         case (PADDR[3:2])
            2'd0: begin
               en <= PWDATA[0];
               if (PWDATA[1]) begin  // CLR
                  acc <= 32'h0;
                  ovf <= 1'b0;
               end
            end
            2'd1: begin
               scratch <= PWDATA;
               // The accumulator only runs with EN=1. It is the only line
               // +BUG=1 breaks.
               if (en || bug_en) begin
                  acc <= suma[31:0];
                  if (suma[32]) ovf <= 1'b1;  // sticky until the next CLR
               end
            end
            default: ;  // ACC and STATUS are read-only: the write is ignored
         endcase
      end
   end

endmodule : apb_regs

The four traps of the spec are put there on purpose and all four are about a spec badly read, not about UVM: the wait state of the read, the CLR that never gets read at 1, the read-only register that gets written without giving an error, and the accumulator that only runs with EN=1. They are the four that hang the first testbench, and they are together in a section of spec.en.md called The small print — in a real spec they are not going to be together nor in a section with that name. The assign of the PREADY is the whole slide if one has to be picked: it says that the write does not wait and the read does, and out of that comes the do @(posedge PCLK); while (!PREADY); of the driver. A driver that takes for granted that ACCESS lasts one cycle reads PRDATA one cycle too early — and it does not fail every time, which is the worst that can happen.

see this slide in the deck ›

Capstone · Day 7

The order: monitor, driver, scoreboard, coverage

The order is not a whim of the checker: it is the field advice of the appendix From the VTALU to a real bus. The first thing that gets written is the monitor, because whoever starts with the driver writes stimulus nobody is watching and discovers a week later that their monitor does not reconstruct. That is why the top.sv comes with the second slave: so the monitor has something to watch before the driver exists. Stage 3 is the one that gets discussed the most and the one that pays off the most: a scoreboard that has never seen an error is not tested. +BUG=1 takes the CTRL.EN gate off the DUT, so the accumulator always adds. A model that has not modelled EN passes both runs and the checker catches it — which is exactly what a regression with a mutation test would do. And a hint about shape for stage 3, which saves a rewrite: the APB answers in order and one at a time, so a queue is enough and the scoreboard can compare on the spot. It is worth writing that assumption down in the plan like any other, because it is the first one that falls over when the DUT stops being a simple slave: there the pattern is the table indexed by ID of the analysis ports. And stage 4 closes the circle of day 6: the covergroup does not get invented, it gets copied from the table of the verification plan. If a bin is left at zero, there is a row of the plan that was not verified — no matter how green the scoreboard is.

see this slide in the deck ›

Capstone · Day 7

Stage 5: the bug the scoreboard cannot see

// In apb_if.sv, with the signals. The payload does not move until it ends
property p_payload_estable;
   @(posedge PCLK) PSEL && !(PENABLE && PREADY) |=> $stable({PADDR, PWRITE, PWDATA});
endproperty

a_payload_estable : assert property (p_payload_estable)
   else `uvm_error("SVA", $sformatf("%m: the payload moved before PREADY"))

This is the stage that makes the capstone integrate the whole day and not half of it. The argument "the scoreboard checks what, the assertion checks how" came all the way from day 1 and got demonstrated on the VTALU; here it gets demonstrated on a bus with addresses, two phases and a wait state, which is where the student is going to need it. It is worth doing the arithmetic out loud, because it is what convinces: with +BUG=2 the module changes the address after SETUP. The DUT is combinational in its decoding, so it serves the new address. The monitor samples on the handshake edge, so it reconstructs the transfer with the new address. Monitor and DUT agree — there is nothing the scoreboard can compare wrongly. The only one that knows the address moved is the one that was watching the wire on every edge. The two empty rows are the other half of the exercise and they are deliberately uncomfortable: the two things they measure are promised in the spec, each on a loose line, and none of the seven rows above measures them. Row 9 also trips up the scoreboard that decodes with the whole address — PADDR[1:0] is ignored, so 0x06 is the SCRATCH. And a trap worth telling because the course itself fell for it: the obvious reading of "PSLVERR is valid together with PREADY" is PSLVERR |-> PREADY, and it is false on this DUT. PSLVERR is combinational, so on an unmapped read it is already high during the wait state. Written as an assertion it fires on the healthy DUT. That rule is a cover, not an assert.

see this slide in the deck ›

Appendix · From the VTALU to a real bus

What does not change

This appendix exists because everybody asks the question when they finish, and the short answer is good news: the architecture does not change at all. What changes is a single class, the driver, and one more single class, the monitor. It is worth saying it with numbers so it does not sound like consolation: of the nine classes of code/u7/sequences, in an APB agent two get rewritten —driver and monitor—, the transaction gains fields, and the rest —test, env, agent, config, scoreboard, coverage, sequences— gets copied and adapted. And the underlying reason is the one the course has been repeating since the env: what knows about the protocol is shut away in one place. Changing protocol touches that place and nothing else. If the student had to rewrite the env, it would be a sign that the agent was badly built.

see this slide in the deck ›

Appendix · From the VTALU to a real bus

What does change

The first bullet is the one to underline, because it is where the intuition the course built breaks. In the VTALU, "an operation" and "an exchange on the bus" are the same thing. In AXI they are not: a write transaction is three channels that can go in any order, and item_done() gets called when the response arrived, not when the address was put out. A practical consequence worth getting ahead of: the moment the driver has several transactions in flight, the get_next_item / item_done pair stops being enough and a uvm_tlm_fifo shows up inside the driver, or the pipelined mode. It is not a new topic: they are the same pieces as threads and as put and get. And about the second bullet, to bring the anxiety down: the monitor of a known protocol almost never gets written. It gets bought or downloaded.

see this slide in the deck ›

Appendix · From the VTALU to a real bus

Vertical reuse: the block env inside the system env

class soc_env extends uvm_env;
   apb_env apb_env_h;     // the block env, without touching a line of it
   axi_env axi_env_h;
   soc_scoreboard sb_h;   // the system one watches both ends, not the transfers

   function void build_phase(uvm_phase phase);
      // What changes is not the env's code: it is its CONFIGURATION
      apb_cfg.is_active = UVM_PASSIVE;   // here the APB is driven by the SoC's CPU
      uvm_config_db #(apb_env_config)::set(this, "apb_env_h*", "cfg", apb_cfg);
      apb_env_h = apb_env::type_id::create("apb_env_h", this);
   endfunction
endclass

This is the answer to "and what are all those classes for, if my DUT has a bus?", and the answer is that the block testbench is not written for the block: it is written so that in six months it goes whole inside the chip's testbench. The concrete rule that decides whether an env is reusable, worth handing over as a three-point checklist: it does not create its own interface —it receives it through config_db—, it does not read from the config_db with absolute paths —no uvm_test_top.env_h.*, because at system level that path does not exist—, and it does not raise objections of its own unless it is the one in charge. An env that breaks any of the three compiles just the same and cannot be instantiated twice. The change from active to passive is the typical case and is worth explaining slowly: at block level the testbench drives the APB because there is nobody else. At SoC level that bus is driven by the real processor, so the same agent has to watch without driving — and that is already solved, it is the is_active of the agents section. Not one new line. And the fact that closes the capstone: the env they wrote in d7-final already is reusable, because its config comes from outside and so does its interface. It was not a coincidence; it was the apb_env_config.

see this slide in the deck ›

Appendix · From the VTALU to a real bus

Where to start

The order of this slide is field advice and it is worth defending: whoever starts with the driver writes stimulus nobody is watching, and discovers a week later that their monitor does not reconstruct. Whoever starts with the monitor can plug it in passively on somebody else's stimulus —or on a test from the designer— and is already contributing on the first day. If somebody asks where the learning path goes on after this course, the short list, in order: TLM2 —uvm_tlm_generic_payload and the sockets, which is how bus VIPs get connected to each other—, the RAL backdoor —docs/en/verilator.md explains why it is not here—, the uvm_sequence_library for the stress test you do not write, and reading the dv/ of a real open project: the agents of OpenTitan and the AXI VIP of pulp-platform/axi are production code and they read for free. And one that is not UVM and is worth the career: formal with SymbiYosys, which proves the same properties they wrote today without any stimulus. What is not on that list is what they have already seen: RAL is day 8, the regression with seeds is the day 7 warm-up and make regresion, and the clocking block is half a section of unit 2. And a last one, which is not technical: in a real project the testbench almost always already exists. What gets asked for on the first day is not building one, it is adding a test to it — which is exactly what they practised in the exercises of days 3 and 6.

see this slide in the deck ›

Appendix · The debug toolbox

The tools we saw, all together

Tool What for Where it came up
VLT_TRACE=1 + GTKWave see the real signals The VTALU spec
+UVM_VERBOSITY=UVM_HIGH see the monitors’ messages Reporting
+UVM_CONFIG_DB_TRACE who put what in the config_db, and who read it Agents
+TOPOLOGY → print_topology() the tree UVM actually built Agents
+UVM_OBJECTION_TRACE who raised and who dropped the objection Tests
+UVM_TIMEOUT=N,NO a ceiling for the hang, instead of waiting Tests
+UVM_MAX_QUIT_COUNT=N kill the run at the Nth error Reporting

This appendix exists because the tools are spread over four sections and they are all needed together, at the worst moment: when something does not work and the clock is running. It is the slide worth printing — and in fact it is already printed: docs/en/uvm-cheatsheet.pdf is this table, the class hierarchy, the nine phases and the handshake of the driver on a single A4 side. The source is res/en/machete.html, it opens with a double click and it prints with Ctrl+P. The point to say out loud is the one about the plusargs. A $display added by hand costs a recompilation of UVM —minutes— and on top of that you have to remember to take it out. Six of these seven are already fitted in the binary you compiled: they get switched on from the command line and they leave no trace. The seventh, VLT_TRACE, also needs the $dumpfile/$dumpvars block in the top — today u2/convencional and ejercicios/d1b have it. And one detail of +UVM_TIMEOUT that bites, because it does not error out: the value is an integer in time units, not an expression with a unit. UVM reads it with $sscanf(…, "%d,%s") (uvm_root.svh:916), so +UVM_TIMEOUT=5ms does not complain: it takes the 5 and cuts the simulation off at 5 ns. You write +UVM_TIMEOUT=5000000,NO, and the NO is so that a set_timeout() written in the testbench does not overwrite what you asked for on the command line. +TOPOLOGY is not from UVM: it is from the base_test of the course, which reads the plusarg and calls uvm_root::get().print_topology(). It is worth clarifying so nobody looks for it in the LRM. The same for VLT_TRACE=1, which is from the run.sh. And one that is not in the table because it is not a tool but a habit: running with SEED=N. A bug that shows up once every ten runs does not get debugged until you can repeat it at will.

see this slide in the deck ›

Appendix · The debug toolbox

Which one to use, according to the symptom

The symptom The first suspect What to look at it with
It ends at t=0 and says PASS nobody raised the objection +UVM_OBJECTION_TRACE
It never ends an item_done() that was not called, or an objection that does not get dropped +UVM_TIMEOUT=5000000,NO and then the trace
The scoreboard screams on every one the monitor samples badly — the DUT hardly ever is the problem +UVM_VERBOSITY=UVM_HIGH
The config_db does not find the scope of the set(), not the get() +UVM_CONFIG_DB_TRACE
The coverage gives 0 % the new() or the sample() of the covergroup is missing read the .dat with verilator_coverage
The tree is not the one you drew a create() without the factory, or a late override +TOPOLOGY

This slide is the index of the previous one, and the order of the rows is not accidental: they are the six things that actually happen, ordered by how many times you are going to see them. The first and the second are two sides of the same coin: the objection. If nobody raises it, the sim ends at zero; if nobody drops it, it never ends. That is why the objection trace is the first tool to switch on when the simulation time makes no sense. The third is the one from the day 5 exercise, and the lesson it leaves is a matter of craft, not of syntax: a monitor that samples on the wrong edge invents failures that do not exist and makes you lose days. Before opening the RTL, look at what the monitor says it saw. The fourth has a trap inside the trap: get(null, "*", ...) does not fail —it gives back the first thing it finds— so the symptom is not "it does not find", it is "it found somebody else's". The trace shows it; the get does not. And to close: the last column is all there is. If the symptom is not here, the next step is the waveforms, which is the tool that does 47 % of the work.

see this slide in the deck ›

Appendix · The 21 silent traps

It compiles, it runs, and it lies

The symptom The cause How to head it off Section
The coverage gives 0 % the new() or the sample() of the covergroup is missing one sample() per transaction, in the subscriber Functional coverage
The daughter runs the method of the mother without virtual, SV resolves by the type of the variable, at compile time virtual on every method somebody is going to extend Polymorphism
The component reads somebody else's config get(null, "*", …) gives back the first thing that matches get(this, "", …), and +UVM_CONFIG_DB_TRACE Tests
The test passes with the wrong stimulus the set_type_override() arrived after the create() of the env the override first, always. +TOPOLOGY gives it away The env
The register model goes green and the field was never tested the access starts with WO: bit_bash skips it and do_check drops it from the mask if the field is readable, WC or W1C. WO* only if it really is not RAL · d8

This appendix is the one to print out and stick next to the monitor. They are spread over seven sections because each one shows up when the concept shows up, but they are needed together — and they are needed at the worst moment. The thesis of the appendix is in the subtitle, and it is worth stating it as a general rule of the craft: in verification, the expensive mistake is not the one that breaks, it is the one that lies. A compilation error costs two minutes. A testbench that passes measuring the wrong thing costs a tape-out. The fourth row is the one that hurts the most in a big team, because whoever commits it is not the one who suffers it: somebody adds an override in their test, somebody else moves the create(), and the stimulus changes without anybody touching the stimulus file. That is why the +TOPOLOGY of the agents is so cheap: it is ten seconds of reading the tree UVM actually built, against the one you drew in your head.

see this slide in the deck ›

Appendix · The 21 silent traps

The one publishing and the one listening

The symptom The cause How to head it off Section
The subscriber counts 0 and the monitor prints 1000 the connect() in the connect_phase is missing cross-check it: against what the one that already worked says Analysis ports · d4
The monitor never publishes, and the scoreboard screams bfm.command_monitor_h = this is missing in the build_phase the uvm_fatal shows up in the class that is not to blame Analysis ports
The copy loses the fields of the mother a do_copy() that does not call super.do_copy(rhs) every do_copy() calls the one above first Hierarchies
Two different transactions compare equal super.do_compare() called on a loose line and thrown away chained with &&, never loose Transactions

The bfm.command_monitor_h = this row is the one that teaches the pattern best, and it is worth telling it as a story: whoever forgets that line sees a uvm_fatal from the scoreboard, opens the scoreboard, and the scoreboard is perfect. The monitor —which is the one that got it wrong— says absolutely nothing, because its always in the interface never called it. The rule that comes out of there, and that holds for all four: when something screams, go up a layer. The one that reports a mismatch is the last of the chain; the bug is almost always in the one feeding it. The last two are the ones from the deep operations of the class hierarchies and it is worth pointing out the asymmetry: do_copy() without super loses data in silence; do_compare() without && compares too few of them, and gives back equal for two objects that are not. The second is worse, because it switches off the whole scoreboard without switching it off.

see this slide in the deck ›

Appendix · The 21 silent traps

Stimulus, scope and objections

The symptom The cause How to head it off Section
The edge bins never get filled dist with :=: the weight goes to each value of the range measure the histogram once, with :/ Constrained random
The randomize() does not run with the asserts switched off assert(x.randomize()) — assert is a simulation directive if (!x.randomize()) uvm_fatal(…) Constrained random
Under the override, some transactions are of the old type a new() where a type_id::create() belonged what goes through the factory is what gets created with create() Transactions
Both agents start up the same two set() with scope "*": the second overwrites the first the scope is the path of the one that reads, with * at the end Agents · d6
The agent starts up active even though you set is_active in the config_db the missing super.build_phase(): the one that reads it is uvm_agent either a config object, or super — never half of each Agents
The simulation never ends an item_done() that was not called +UVM_TIMEOUT=5000000,NO first, the trace after Agents
It ends at t=0 and says PASS nobody raised the objection around the sequence +UVM_OBJECTION_TRACE Sequences

The last two are the only ones on the list that do make noise — one hangs and the other ends oddly — and they are here because the noise they make does not point at the culprit. A hang does not say what was left waiting; a t=0 says PASS, which is worse than an error. The one about assert(randomize()) deserves being told in full because it is the one most badly copied from the internet: assert is not a function, it is a directive, and a simulator with asserts disabled does not execute the argument. Which means the randomize simply does not happen, the transaction gets sent with the previous values, and the testbench carries on as if nothing had happened. The one about the new() that eats the override is the subtlest of day 5, and it is worth showing the number: in add_test, two of the transactions of the testbench were built with new() and therefore were not add_transaction. Nobody found out, because the scoreboard compared fine all the same. And a closing recommendation for whoever is studying alone: read this appendix now and again on the day something does not work. The first time you understand none of them; the second you understand exactly the one that is happening to you.

see this slide in the deck ›

Appendix · The 21 silent traps

The four from the assertions

The symptom The cause How to head it off Section
The log says PASS and the properties did not run --assert is missing at compile time the cover property at 0 is the only one that warns you Assertions
The property always passes the antecedent never occurs, or the implication is the one that is wrong one cover property per assert property Assertions
145 false positives and the DUT is healthy you sample it with the edge it is written on stimulus on negedge, response of the DUT on posedge Assertions · d7
False positives at the start of every test the disable iff (!reset_n) is missing default disable iff, once, at the very top Assertions

These four reached the appendix with the assertions and they are the only ones on the list where the check itself is what fails — the rest are bugs of the testbench; these are bugs of the one checking the testbench. That is why the antidote is always the same and that is why it is worth repeating it until you are sick of it: one cover property per assert property. The first row is the cheapest to commit in this flow and it is worth showing it live: taking --assert out of the run.sh of code/u8/assertions leaves the run with +BUG=1 at 0 UVM_ERROR. The 154 failures disappear without anything warning you. The third is the lesson of the section and the only one on the list that is not in any tutorial. The wrong reflex, when the false positives show up, is to loosen the property until it shuts up: there you are left without a check and with the feeling of having fixed it. The right reflex is to ask on which edge the one driving the stimulus writes.

see this slide in the deck ›

Appendix · The 21 silent traps

And the one the clocking block brings

The symptom The cause How to head it off Section
The scoreboard fails once every twenty and the waveform looks fine half the signals get read through cb.sig and the other half through sig if it went into the clocking block, the whole protocol reads it through there Interfaces and BFM

This trap closes the appendix and it is the one that best sums up its thesis, because the symptom is the worst of all the ones on the list: intermittent. The rest fail always or never fail; this one fails when the data changes, which is one run in however many and precisely the one you are not watching. And it has a twist worth marking out loud: it is the only one on the list that shows up because you used the tool that avoids another problem. The clocking block takes the sampling race out and puts the possibility of this mixture in. It is not an argument for not using it — it is the argument for using it whole, or not using it. The rule in one line, which is Dave Rich's: if a signal belongs to the timing contract of a clocking block, it is always accessed through that contract. The long material is in docs/clocking-blocks.md (in Spanish).

see this slide in the deck ›

Glossary · the words of the trade

What it is called on the job, and not in the textbook

What a textbook says What gets said at work
coverage of the functionality / of the code functional / code coverage
the test plan verification plan, coverage plan
reaching full coverage coverage closure
edge cases corner cases
hand-written / random stimulus directed / constrained-random stimulus
signal plots waveforms
the random number seed seed
the nightly test run regression
the first chips back · redoing the masks first silicon · respin
a check written for the simulator assertion — and assert property never gets translated
reading a signal at a given moment sampling — and the edge it happens on, the sampling edge
test bed, test harness testbench, always — nobody says anything else

This slide exists because between knowing a concept and being able to look it up there is a vocabulary, and it is the one nobody teaches: the student has just learned the concepts and from tomorrow every search, every question on a forum and every interview goes through these exact words. The last row is not a joke. "Test bed" and "test harness" turn up in translated books and in software, and nobody says them here: in an interview or in a mail to the team you say testbench. The same with coverage, seed and tape-out. A rule of thumb for work: the technical nouns are fixed and do not get paraphrased. "Randomize the transaction", "run the regression", "this does not close coverage". It sounds like jargon and it is how the trade talks.

see this slide in the deck ›

Glossary · The pieces

One line each, to have them together

Word What it is
handle the name of an object — it is not the object
factory the dictionary of types: you ask for the base class and you get the derived one
override changing the type the factory hands out, without touching whoever asks for it
transaction a piece of data that travels through the testbench: a uvm_sequence_item
agent everything that knows how to speak one interface, in a box
sequence / sequencer the stimulus (object) and the arbiter that hands it out (component)
virtual sequence the one that sends no items of its own: it coordinates several sequencers
driver turns a transaction into signals
monitor watches the bus and publishes transactions
scoreboard compares what came out against what should have come out
subscriber anybody listening to an analysis port
objection the "I still have work to do" that keeps the phase alive
assertion a rule of the protocol written so the simulator checks it
property the rule itself: clock, antecedent, implication, consequent
antecedent / consequent the "if" and the "then" of a property
cover property counts how many times a property actually held. The check of the check

It is the slide to keep open while reading somebody else's code, and also the one that serves as a quick review before an interview. The two rows that get confused the most are sequence and sequencer, and the way not to get them wrong is the one from day 6: the one ending in -er is the component —it is in the tree, it has phases—; the other is the object that gets created, runs and is thrown away. The same holds for driver and monitor. The rule is about the pieces of the tree and not about the suffix: the UVM policies —uvm_printer, uvm_comparer, uvm_packer, uvm_recorder— and the uvm_report_catcher end the same way and are objects. And the first row is the hardest one and the one that costs the most: handle. If the student takes a single word away from this glossary, let it be that a handle is not an object. The virtual sequence row brings two more words along that are going to show up in any production testbench: the virtual sequencer, which is a component without a queue whose only contents are the handles to the real sequencers, and p_sequencer, the handle already cast to that type that `uvm_declare_p_sequencer declares for you. A warning about sequence*, which now shows up twice in the course with two meanings: the UVM one is an object that generates stimulus; the SVA one (sequence ... endsequence, the assertions) is a temporal expression —"this, and three edges later that"*— and has nothing to do with it. The context always tells them apart, but the first time it confuses.

see this slide in the deck ›

Glossary · Three words that mislead

The three are here because each one has already caused a confusion in the course, and it is worth closing by naming them. virtual was flagged in polymorphism and it comes back in the testbench in objects with the virtual interface. The question that sorts it out: virtual what? If it is a method, it looks at the object. If it is a class, it does not get instantiated. If it is an interface, it is a handle. The assert is the trap of the transactions and it is worth repeating why: it reports outside UVM, it does not go into the Report Summary, and with the assertions switched off there are simulators that do not even evaluate the expression — which means randomize() does not get called. And the last row looks obvious until the first project meeting, where "how many tests do you have" and "how many tests did you run last night" are two different questions with two very different numbers.

see this slide in the deck ›

References

The list is ordered by when you are going to need it, and it is worth saying so. The LRM and the User Guide are for consulting: nobody reads them straight through, you go and look things up when there is a discussion about syntax or semantics. The Verification Academy is the place where almost everything this course left out lives, and it is free with registration. About Salemi's book it is worth repeating what docs/en-que-se-diferencia.md (in Spanish) says: this course does not replace it and does not follow it. If somebody wants a second pass over the same concepts written by another person, it is the best one there is to start with. And the point about the charts, because it is the part a student may want to verify: the percentages of unit 1 are from the Wilson Research Group study; the charts are our own and get regenerated with make figs from a JSON that is in the repo. Nothing in unit 1 is an image downloaded from the internet.

see this slide in the deck ›

If you want to keep reading

The book I learned with The UVM Primer Ray Salemi · Boston Light Press, 2013 · ISBN 978-0974164939

This course is not that book: it has another structure, another DUT, another simulator and four units of material the book does not cover. What it does share is credited in NOTICE, and the complete list of differences is in docs/en-que-se-diferencia.md (in Spanish).

It is worth saying out loud when you get here: the book is short, it is well written and it is still the best way to start in English. If the course was of any use to somebody, let them buy it.

see this slide in the deck ›

Credits

This slide is not legal formalism, and it is worth saying why it is here: the course asks to be cited, so it has to cite first. What was borrowed is named, with its licence and with the list of changes in the NOTICE. The practical part for whoever wants to reuse the material: CC BY 4.0 the slides and the docs, Apache-2.0 the code, MIT the tools. It can be taught, adapted, translated and charged for. The only condition is citing the source, and there is no hidden "non-commercial" clause. If somebody asks why three licences instead of one: because they are three different things. The code derives from Apache-2.0 code and has to go on being that; the slides are original work and CC BY is the one an academic setting understands; the tools are software and MIT is what anybody copying them expects.

see this slide in the deck ›

This is as far as we got

What you take with you

It is worth reading the list out loud and slowly, because after seven days nobody has a sense of how much they saw. It is seven bullets and each one was a whole course a week ago. The one to underline is the last, and not out of activism: all of this runs on the student's machine. There is no part of the course left in demo mode because a licence is missing, and that means that the day they want to repeat an example, try out an idea or show something to somebody, they can. Most of the UVM material they are going to find from here on does not have that property. And the honest clarification before the slide about what was left out, so nobody leaves with the wrong idea: this is an introductory course, and what follows is a list of what was not covered. Finishing it is not knowing UVM — it is being able to read somebody else's testbench, write one of your own, and understand the next thing you learn without it sounding like gibberish.

see this slide in the deck ›

What was left out

and where to go on

Topic Why you are going to run into it
uvm_event, uvm_barrier The synchronization that is left when the objection is not enough
uvm_heartbeat The one that warns you a component stopped showing signs of life in a regression of hours
uvm_sequence_library A bag of sequences the sequencer picks from at random: the stress test without writing it
uvm_pool, uvm_queue The collections of UVM. An associative array is enough until it has to be shared
TLM2, phase jumps The limit of the course, and it is explicit
UPF, gate-level, PSS Three worlds apart: low power, the netlist with delays, and generating the tests from a model

RAL came out of this table and is now unit 9, optional, with the example that runs in code/u9/ral/ and the exercise d8-ral. It is on day 8, after this slide and not in the middle, because it is only understood once the student has already written the scoreboard RAL replaces — and because as far as we know it is the first course that shows it running on a free simulator. What was left out of RAL, and is said in the unit: the backdoor and generating the model from IP-XACT. The clocking block also came out of this table: it is half a section of unit 2, with its example that runs in code/u2/clocking/. The callbacks came out for the same reason: they are half a section of unit 7, with the code/u7/callbacks/ that injects the error in the driver — the third hook unit 1 promises. And regression and seeds as well: the exercise d7-semillas is the whole cycle, and make regresion left it as a tool, with a report of the bins that were left open. SVA came out of this table and is now a whole unit, and it is one of the most original things the course has: no introductory UVM book brings it. The virtual sequences came out too: they are the first hour of today, with an example that runs in code/u7/sequences/virtual/. The last three rows are worth reading out loud with this sentence in front: an introductory course that lists what it does not cover is worth more than one that pretends to cover everything. uvm_heartbeat, the sequence library and the collections get understood in an afternoon when they are needed; UPF, gate-level and PSS are another career. The last line is not a joke. After this course the student can open uvm_component.svh and understand what build_phase does, because they saw every piece the library hands them built by hand. If there is time, open it live and look together for the m_set_full_name() that makes the config_db find things. It is the best way to close the course: the library stops being magic.

see this slide in the deck ›

Self-assessment

Fifteen things you should be able to do by now · 1 of 3

Can I…? Where it is
1 · Explain why 47 % of a verification engineer's time goes on debug, and what this course does about it Trends
2 · Write a five-column verification plan for a DUT I have not seen before The verification plan
3 · Say why 100 % code coverage does not mean the DUT is verified Functional coverage
4 · Say what to look at first when a covergroup reports 0 % Functional coverage
5 · Explain which race a clocking block avoids — and why they are optional anyway Interfaces and BFM

This rubric is for the student, not for the instructor, and it is worth saying so: nobody grades it. It is good for two different moments —before the exam, as a study guide; and right now, as a diagnosis— and the instruction is the same in both: read the statement and ask yourself "can I explain it to somebody?", not "does it ring a bell?". The difference between the two answers is the whole course. Every row links to the section of the book, which is the version to read straight through, with the instructor note inside the text. A "no" is not bad news: it is a link. If it is being taught in class, project the three slides and ask for hands per row. The three that get the most hands are the ones to review, and that measurement is worth more than the complete review.

see this slide in the deck ›

Self-assessment

Fifteen things you should be able to do by now · 2 of 3

Can I…? Where it is
6 · Extend a class and change its behaviour without copying it, and say what super does Classes and extensions
7 · Change the class a testbench uses without touching the env, with a set_type_override The factory pattern
8 · Explain why a UVM simulation ends at t=0 if nobody raises an objection Tests
9 · Say why build_phase runs top down and connect_phase the other way round Components and phases
10 · Make a scoreboard that fails say something more than "it failed" Reporting

The 8 and the 9 are the two most often answered with a "it rings a bell": they are mechanics of the library and they are understood by looking, until the day you have to debug a testbench that finishes without having sent any stimulus. If the student hesitates on either of the two, the Tests section is half an hour and closes them. The 10 is the one that pays off most in the long run and the one most underestimated. The whole course points at the 47 % of row 1: a uvm_error that prints "FAIL" leaves whoever is debugging exactly where they were, and one that prints the transaction, the prediction and the time saves them half a morning. It is the difference between a testbench that works and one you can work with.

see this slide in the deck ›

Self-assessment

Fifteen things you should be able to do by now · 3 of 3

Can I…? Where it is
11 · Hang a new subscriber off an analysis port without touching the one publishing A single place that watches the wire
12 · Write a constraint that reaches a corner case, and measure whether the dist does what it says Constrained random
13 · Say what an agent with is_active = UVM_PASSIVE builds and what it does not Agents
14 · Explain why a uvm_sequence does not show up in print_topology() Sequences
15 · Write a property that sees a protocol violation, and say why --assert is not optional Assertions (SVA)

The 14 is the question thrown out on day 1, when the UVM class diagram shows up, and it gets answered on day 6. If the student can answer it, they understood the division that explains half the library: uvm_object is the data and uvm_component is the structure. An object gets created and thrown away; a component gets built once and lasts the whole simulation. The 12 says measure, not write, and it is on purpose: the exercise d5b exists because a dist with the weights properly set can give a histogram that lies, and whoever never measured it has no way of knowing. And the last line is the only honest thing that can be said about the fifteen: they are a necessary condition, not a sufficient one. What proves somebody knows how to do this is a testbench of their own working, and that is the capstone. A student who ticks all fifteen and does not finish it did not finish the course.

see this slide in the deck ›

On Monday

What to do with this once the screen goes off

Thanks for getting this far.

The last slide of the course, and it is worth it being short and in the imperative: after seven days nobody takes away one more new idea, but they do take away one instruction. The instruction is the first line, and it is worth saying it looking at the camera or at the group: the only thing that proves somebody can do this is a testbench of their own, running. Not the capstone marked, not the fifteen rows of the self-assessment in green: one of their own, on a DUT nobody prepared to be easy. The second bullet is the most practical advice on the slide and the one most thanked six months later. Of everything that got printed this week, what gets opened again is two pages: the cheat sheet —docs/en/uvm-cheatsheet.pdf, one page, A4— and the silent-traps table, which is written to be searched by symptom and not to be read. And the invitation in the fourth is not courtesy. The course improves where somebody trips: half the presenter notes exist because somebody asked the same thing twice. If it is taught in class, close by explicitly asking for the three things that were not understood — and writing them down before the group leaves.

see this slide in the deck ›