Verify This! · the course to read

Day 5The data

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 5 recap has to install the discomfort of the loose data, because that is what justifies a whole unit about objects that do nothing but carry data. The third bullet is literal: in yesterday's testbench, copying a command is three assignments, and comparing it is three ==. With two more fields it is five and five. Today that becomes copy() and compare(), written once.

see this slide in the deck ›

Agenda

Day 5 · ≈ 4 h 30 · unit 6 · the data

By the end of the day you can:

The second one is the novelty of the day and the one that pays off the most at work: measuring is half of it, closing is the other half. The last two exercises of the afternoon are two thirds of that cycle —measure the dist, close the bin— and day 7 closes it by accumulating seeds. Row 12 of the self-assessment says measure, not write, and that is on purpose: a dist with the weights right can give a histogram that lies, and whoever never measured it has no way of knowing.

see this slide in the deck ›

Copying an object that contains another

The handle is not the object

It is an OOP section in the middle of UVM, and it is worth saying why it is here and not on day 2: because only now is it needed. Up to reporting —the closing of day 3— the data was a struct and copied itself. The bug in the second bullet gets written by everybody once, and the symptom is horrible: you send a transaction to the scoreboard, you go on using the same handle for the next one, you modify it, and the scoreboard compares against data that changed after it received it. It does not fail every time, and when it does it looks like a DUT bug. The other half —the super at every level— has a different and more treacherous symptom: the day somebody puts a new class in the middle of the hierarchy, the convert2string() stops printing half the fields and nobody finds out, because it goes on printing something. The question to start with: if handle_a = handle_b, how many objects are there? One. It is the same question as in classes and extensions, now with consequences.

see this slide in the deck ›

Copying an object that contains another

Never duplicate code

"If we find ourselves copying code and making small modifications to it inside our program, we are doing things wrong. Code must never be duplicated."

In OOP, instead of copying code...

we extend classes

see this slide in the deck ›

Copying an object that contains another

convert2string(): the problem

The four levels of the hierarchy

code/u6/jerarquias/pre_convert2string.sv
// ILLUSTRATION -- not compiled. The WRONG way to write convert2string!
// The classes are drawn as comments: only the methods are on screen.
//class trago;
   virtual function string convert2string();
      return $sformatf("Hielos: %0d", hielos);
   endfunction : convert2string
//class fernet extends trago;
   function string convert2string();
      string coca_s;
      coca_s = (con_coca) ? "si" : "no";
      return $sformatf("Hielos: %0d  Coca: %s", hielos, coca_s);
   endfunction : convert2string

//class fernet_doble extends fernet;
   function string convert2string();
      return {$sformatf("hielos: %0d  coca: %0b  mesa: %s",
      	       hielos, con_coca, mesa) };
   endfunction : convert2string

This version works, and that is the problem: it looks fine and it is wrong. Every convert2string() prints every field by hand, including the ones it inherited. Go to the $sformatf of the bottom-most class and count how many times the name of a field of the class above is repeated. Every one of those repetitions is a line you have to remember to touch the day a field gets added. The question that sets up the next slide: what happens if tomorrow somebody puts a new class in the middle of the hierarchy? The ones below go on printing what they knew, the one in the middle does not show up, and there is no error. The method still returns a string.

see this slide in the deck ›

Copying an object that contains another

convert2string(): the deep version

A new level in the hierarchy breaks convert2string(): graduacion does not appear in the string

code/u6/jerarquias/pre_convert2string_ok.sv
// ILLUSTRATION -- not compiled. The same three methods written deep, with the
// classes drawn as comments. The version that does compile is deep.sv.
//virtual class trago;
   virtual function string convert2string();
      return $sformatf("Hielos: %0d", hielos);
   endfunction : convert2string

//class con_alcohol extends trago;

   function string convert2string();
      return {super.convert2string(),
              $sformatf("\ngraduacion: %0d",graduacion)};
   endfunction : convert2string

//class fernet extends con_alcohol;

   function string convert2string();
      string coca_s;
      coca_s = (con_coca) ? "si" : "no";
      return {super.convert2string(), "\nCoca: ",coca_s};
   endfunction : convert2string

The rule, short: every method of the hierarchy takes care only of its own fields and asks super for the rest. Nobody knows what is further up, and that is why inserting a class in the middle breaks nothing. The pattern repeats in the same way in the three methods of the section —convert2string(), do_copy(), do_compare()— and in the three of UVM too. If you get into the habit of writing the call to super first, before touching your own fields, you never forget it. It is worth pointing out the parallel with something they have already seen: it is exactly the same argument as the super.new() of classes and extensions. The top part of the object has to be built too — and it also has to be printed, copied and compared.

see this slide in the deck ›

Copying an object that contains another

The equals sign copies nothing

It copies the handle, not the object. It is the bug everybody writes once: you modify the transaction you already sent to the scoreboard and the scoreboard sees the change halfway through. This is where the clone() of the transactions comes from.

see this slide in the deck ›

Copying an object that contains another

Copying for real is two steps

Copying for real is two steps, and they have to be written: instantiate a new object and hand it the data field by field

code/u6/jerarquias/pre_copy.sv
// ILLUSTRATION -- not compiled. The initial block of deep.sv:110-118, on its own.
initial begin
   fernet_hielo1_h =
       new(.hielos(2), .con_coca(1), .graduacion(2), .mesa("mesa 4"), .medidas(2));
   $display("\n--- Fernet 1 ---\n", fernet_hielo1_h.convert2string());
   fernet_hielo2_h = new();
   $display("\n--- Fernet 2 before the copy ---\n", fernet_hielo2_h.convert2string());
   // do_copy is a method that copies member by member
   // it suffers from the same problem already seen with convert2string()
   // Each class can copy the variables defined in that class
   // super.do_copy() has to be used to copy the inherited ones
   fernet_hielo2_h.do_copy(fernet_hielo1_h);
   $display("\n--- Fernet 2 after the copy ---\n", fernet_hielo2_h.convert2string());
end

It is worth making the comparison with the assignment of the previous slide out loud: a = b is one line and copies nothing; copying for real is two steps and both have to be remembered. That the language does not bring a deep copy is not an oversight by the committee: there is no general answer. If the class has a handle to another object, do you copy the handle or the object? It depends, and that is why you have to decide it yourself in every do_copy(). It is exactly the same dilemma the transactions are going to have if some day they carry an array of objects inside. In this course it does not happen, but it is worth leaving the question on the table.

see this slide in the deck ›

Copying an object that contains another

do_copy(): each class touches what is its own

do_copy(): Every class in the hierarchy has to call its parent class, and therefore every do_copy() method needs the same type of argument. But we have different classes... We use polymorphism

code/u6/jerarquias/pre_copy2.sv · #fernet_con_hielo
class fernet_con_hielo extends fernet_doble;
   byte medidas;

   function new(int hielos = 0, bit con_coca = 0, int graduacion = 0,
                string mesa = "", byte medidas = 0);
      super.new(hielos, graduacion, con_coca, mesa);
      this.medidas = medidas;
   endfunction : new

   function string convert2string();
      return {super.convert2string(), "\n", $sformatf("medidas: %0d", medidas)};
   endfunction : convert2string

   //do_copy passes the argument up to its parent class
   function void do_copy(trago copia);
      fernet_con_hielo copia_fernet_con_hielo;
      super.do_copy(copia);
      // Then the copy gets cast to a variable of its own class type
      $cast(copia_fernet_con_hielo, copia);
      this.medidas = copia_fernet_con_hielo.medidas;
   endfunction : do_copy

endclass : fernet_con_hielo
el archivo entero, 48 líneas
// ILLUSTRATION -- not compiled. The bottom two classes of the hierarchy, with the
// comments that explain the do_copy() argument. The whole one is deep.sv:64-105.
class fernet_doble extends fernet;
   string mesa;

   function new(int hielos = 0, int graduacion = 0, bit con_coca = 0,
                string mesa = "");
      super.new(hielos, graduacion, con_coca);
      this.mesa = mesa;
   endfunction : new

   function string convert2string();
      return {super.convert2string(), "\nMesa: ", mesa};
   endfunction : convert2string

   // Polymorphism: do_copy takes a class derived from trago as its argument
   // so the trago class is what gets used as the argument type
   function void do_copy(trago copia);
      fernet_doble copia_fernet_doble;
      super.do_copy(copia);
      $cast(copia_fernet_doble, copia);
      this.mesa = copia_fernet_doble.mesa;
   endfunction : do_copy
endclass : fernet_doble

class fernet_con_hielo extends fernet_doble;
   byte medidas;

   function new(int hielos = 0, bit con_coca = 0, int graduacion = 0,
                string mesa = "", byte medidas = 0);
      super.new(hielos, graduacion, con_coca, mesa);
      this.medidas = medidas;
   endfunction : new

   function string convert2string();
      return {super.convert2string(), "\n", $sformatf("medidas: %0d", medidas)};
   endfunction : convert2string

   //do_copy passes the argument up to its parent class
   function void do_copy(trago copia);
      fernet_con_hielo copia_fernet_con_hielo;
      super.do_copy(copia);
      // Then the copy gets cast to a variable of its own class type
      $cast(copia_fernet_con_hielo, copia);
      this.medidas = copia_fernet_con_hielo.medidas;
   endfunction : do_copy

endclass : fernet_con_hielo

Here is the detail that looks bureaucratic and is not: every do_copy() of the hierarchy takes an argument of the base class, not of its own. If each one took its own type, the call to super.do_copy(rhs) would not compile — the types would not match. The price is that inside every method you have to $cast in order to read your own fields. And as we saw in the factory, that $cast checks at run time: if somebody tries to copy a mojito onto a fernet, it returns 0 and it has to be caught. It is the first time a signature convention shows up that exists only so that polymorphism can work. In UVM it is going to be the same and with more rules: the argument has to be a uvm_object —and by convention it is called rhs—, do_compare() also takes a uvm_comparer, and so on. When those signatures show up in the transactions, the answer to "why like this?" is this slide.

see this slide in the deck ›

Copying an object that contains another

Summary: the terrible solution

code/u6/jerarquias/wrong.sv · #fernet_con_hielo.convert2string
   function string convert2string();
      return {
         $sformatf(
             "hielos: %0d  coca: %0b  mesa: %s  medidas: %0d",
             hielos,
             con_coca,
             mesa,
             medidas
         )
      };
   endfunction : convert2string
el archivo entero, 108 líneas
virtual class trago;
   int hielos = -1;

   function new(int a = 0);
      hielos = a;
   endfunction : new

   virtual function string convert2string();
      return $sformatf("Hielos: %0d", hielos);
   endfunction : convert2string

   virtual function void do_copy(trago copia);
      hielos = copia.hielos;
   endfunction : do_copy

endclass : trago

class fernet extends trago;
   bit con_coca;

   function new(int hielos = 0, bit con_coca = 0);
      super.new(hielos);
      this.con_coca = con_coca;
   endfunction : new

   function string convert2string();
      string coca_s;
      coca_s = (con_coca) ? "si" : "no";
      return $sformatf("Hielos: %0d  Coca: %s", hielos, coca_s);
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet copia_fernet;
      super.do_copy(copia);
      $cast(copia_fernet, copia);
      this.con_coca = copia_fernet.con_coca;
   endfunction : do_copy

endclass : fernet

class fernet_doble extends fernet;
   string mesa;

   function new(int hielos = 0, bit con_coca = 0, string mesa = "");
      super.new(hielos, con_coca);
      this.mesa = mesa;
   endfunction : new

   function string convert2string();
      return {$sformatf("hielos: %0d  coca: %0b  mesa: %s", hielos, con_coca, mesa)};
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet_doble copia_fernet_doble;
      super.do_copy(copia);
      $cast(copia_fernet_doble, copia);
      this.mesa = copia_fernet_doble.mesa;
   endfunction : do_copy
endclass : fernet_doble

class fernet_con_hielo extends fernet_doble;
   byte medidas;

   function new(int hielos = 0, bit con_coca = 0, string mesa = "", byte medidas = 0);
      super.new(hielos, con_coca, mesa);
      this.medidas = medidas;
   endfunction : new

   function string convert2string();
      return {
         $sformatf(
             "hielos: %0d  coca: %0b  mesa: %s  medidas: %0d",
             hielos,
             con_coca,
             mesa,
             medidas
         )
      };
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet_con_hielo copia_fernet_con_hielo;
      super.do_copy(copia);
      $cast(copia_fernet_con_hielo, copia);
      this.medidas = copia_fernet_con_hielo.medidas;
   endfunction : do_copy

   function void bad_copy(fernet_con_hielo copia_fernet_con_hielo);
      medidas = copia_fernet_con_hielo.medidas;
      mesa = copia_fernet_con_hielo.mesa;
      con_coca = copia_fernet_con_hielo.con_coca;
      hielos = copia_fernet_con_hielo.hielos;
   endfunction

endclass : fernet_con_hielo

module top;
   fernet_con_hielo fernet_hielo1_h, fernet_hielo2_h;

   initial begin
      fernet_hielo1_h = new(2, 1, "Agnus", 2);
      $display("\n--- Fernet 1 ---\n", fernet_hielo1_h.convert2string());
      fernet_hielo2_h = new();
      $display("\n--- Fernet 2 before the copy ---\n", fernet_hielo2_h.convert2string());
      fernet_hielo2_h.bad_copy(fernet_hielo1_h);
      $display("\n--- Fernet 2 after the copy ---\n", fernet_hielo2_h.convert2string());
   end
endmodule : top

The rule to leave behind: a class only writes on its own fields. Everything else it asks super for. It is worth counting on screen: hielos shows up in four different convert2string() of this file and con_coca in three. Seven places to remember, and not one of them fails if you forget one. There is a bad_copy() a little further down the file worth opening if there is time to spare: it takes a fernet_con_hielo as an argument instead of a trago. It compiles and it works... until somebody calls it through a handle of the base class, and there no polymorphism can help.

see this slide in the deck ›

Copying an object that contains another

Summary: the deep solution

code/u6/jerarquias/deep.sv · #convert-and-copy
   function string convert2string();
      string coca_s;
      coca_s = (con_coca) ? "si" : "no";
      return {super.convert2string(), "\nCoca: ", coca_s};
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet copia_fernet;
      super.do_copy(copia);
      $cast(copia_fernet, copia);
      this.con_coca = copia_fernet.con_coca;
   endfunction : do_copy
el archivo entero, 117 líneas
virtual class trago;
   int hielos = -1;

   function new(int a = 0);
      hielos = a;
   endfunction : new

   virtual function string convert2string();
      return $sformatf("Hielos: %0d", hielos);
   endfunction : convert2string

   virtual function void do_copy(trago copia);
      hielos = copia.hielos;
   endfunction : do_copy

endclass : trago

class con_alcohol extends trago;
   int graduacion;
   function new(int a = 0, b = 0);
      super.new(a);
      graduacion = b;
   endfunction : new

   function string convert2string();
      return {
         super.convert2string(), $sformatf("\ngraduacion: %0d", graduacion)
      };
   endfunction : convert2string

   function void do_copy(trago copia);
      con_alcohol copia_con_alcohol;
      super.do_copy(copia);
      $cast(copia_con_alcohol, copia);
      graduacion = copia_con_alcohol.graduacion;
   endfunction : do_copy
endclass : con_alcohol

class fernet extends con_alcohol;
   bit con_coca;

   function new(int hielos = 0, int graduacion = 0, bit con_coca = 0);
      super.new(hielos, graduacion);
      this.con_coca = con_coca;
   endfunction : new

   function string convert2string();
      string coca_s;
      coca_s = (con_coca) ? "si" : "no";
      return {super.convert2string(), "\nCoca: ", coca_s};
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet copia_fernet;
      super.do_copy(copia);
      $cast(copia_fernet, copia);
      this.con_coca = copia_fernet.con_coca;
   endfunction : do_copy

endclass : fernet

class fernet_doble extends fernet;
   string mesa;

   function new(int hielos = 0, int graduacion = 0, bit con_coca = 0,
                string mesa = "");
      super.new(hielos, graduacion, con_coca);
      this.mesa = mesa;
   endfunction : new

   function string convert2string();
      return {super.convert2string(), "\nMesa: ", mesa};
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet_doble copia_fernet_doble;
      super.do_copy(copia);
      $cast(copia_fernet_doble, copia);
      this.mesa = copia_fernet_doble.mesa;
   endfunction : do_copy
endclass : fernet_doble

class fernet_con_hielo extends fernet_doble;
   byte medidas;

   function new(int hielos = 0, bit con_coca = 0, int graduacion = 0,
                string mesa = "", byte medidas = 0);
      super.new(hielos, graduacion, con_coca, mesa);
      this.medidas = medidas;
   endfunction : new

   function string convert2string();
      return {super.convert2string(), "\n", $sformatf("medidas: %0d", medidas)};
   endfunction : convert2string

   function void do_copy(trago copia);
      fernet_con_hielo copia_fernet_con_hielo;
      super.do_copy(copia);
      $cast(copia_fernet_con_hielo, copia);
      this.medidas = copia_fernet_con_hielo.medidas;
   endfunction : do_copy

endclass : fernet_con_hielo

module top;
   fernet_con_hielo fernet_hielo1_h, fernet_hielo2_h;

   initial begin
      fernet_hielo1_h = new
          (.hielos(2), .con_coca(1), .graduacion(2), .mesa("mesa 4"), .medidas(2));
      $display("\n--- Fernet 1 ---\n", fernet_hielo1_h.convert2string());
      fernet_hielo2_h = new();
      $display("\n--- Fernet 2 before the copy ---\n", fernet_hielo2_h.convert2string());
      fernet_hielo2_h.do_copy(fernet_hielo1_h);
      $display("\n--- Fernet 2 after the copy ---\n", fernet_hielo2_h.convert2string());
   end
endmodule : top

Closing of the section, and it is worth saying where all of this is going: the three methods they have just written by hand are exactly the three UVM asks for in a transaction. do_copy(), do_compare() and convert2string(), with the same rules about super and $cast. The difference is that in UVM they do not get called directly: you write do_copy() and the testbench calls copy(), which is the one that lives in uvm_object and takes care of the rest. Same idea as the phases — you write the bottom part, the library handles the protocol. And the question to walk out of the day with: if writing this properly costs three methods per class, why does nobody generate it? Somebody does: they are the `uvm_field_* macros. The course does not use them, and the reason is on the super.build_phase() slide of the components — they generate hundreds of lines nobody reads and that show up in the stack when something fails. Read them yes, write them no.

see this slide in the deck ›

Copying an object that contains another

Summary of the unit

The handle bug is the one everybody writes once: you modify the transaction you already sent to the scoreboard, and the scoreboard sees the change halfway through. It does not fail at the time; it fails later, and somewhere else. The short way to close: the = copies the slip of paper with the address, not the house. And that is why UVM brings copy(), clone() and compare() ready-made — which is exactly the unit that follows.

see this slide in the deck ›

Transactions

The testbench is well divided up, and the data is not

The honest question from the student is "and why not a struct?". The answer: the struct does not randomize itself, does not compare itself and does not print itself. Everything the testbench used to do by hand with command_s becomes a method of the class, and the rest of the testbench shrinks. The fourth bullet is the most concrete argument and it is worth leaning on it: up to here the analysis port of the result carries a shortint, that is to say a number. The VTALU has two outputs —result and ovf— and that is why the scoreboard of the previous units checks half the DUT and lets the other half through. With a result_transaction with two fields, the check of the ovf shows up on its own. It is the first time in the course that the class is not tidiness: it is the only way.

see this slide in the deck ›

Transactions

A transaction is data with the methods that operate on that data

The sentence on the slide is the definition to leave written on the board: data with the methods that operate on that data. The third bullet is the shift in the division of responsibilities of the section: until today the tester knew which values were legal —it had a get_data() with the bias written by hand—. From today that lives in the transaction, in a constraint. The question to throw out: if tomorrow the DUT accepts 16-bit operands, how many classes have to be touched? One.

see this slide in the deck ›

Transactions

Defining a transaction: which class it extends

code/u6/transactions/command_s.sv
// ILLUSTRATION -- not compiled. The struct as it is in code/u5/put-get/vtalu_pkg.sv,
// the one this section turns into a class.
typedef struct {
   byte unsigned A;
   byte unsigned B;
   operation_t   op;
} command_s;

Careful with the base class, because the old material says something else. The chain is uvm_sequence_item → uvm_transaction → uvm_object, and it is always the first one that gets extended. Two reasons. The small one: in IEEE 1800.2 uvm_transaction is a virtual class, it cannot be instantiated on its own. The big one: a uvm_sequence only knows how to send uvm_sequence_item. If the transaction extends plain uvm_transaction, everything from day 6 —start_item / finish_item— does not compile. Which means the base class we pick today is the one that lets us go on tomorrow.

see this slide in the deck ›

Transactions

The randomized fields: the "before"

code/u6/transactions/prev-random.sv
// ILLUSTRATION -- not compiled. The two functions as they are in
// code/u5/put-get/tb_classes/random_tester.svh, the section this one replaces.
//
// This function handles the fact that
// the operation bus has 8 possible values,
// but only 6 of them are valid operations
virtual function operation_t get_op();
  bit [2:0] op_choice;
  op_choice = $random;
  case (op_choice)
          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;
          3'b110 : return rst_op;
          3'b111 : return rst_op;
  endcase // case (op_choice)
endfunction : get_op

// This function was meant to spread out
// the probability of getting all zeros (prob = 1/3),
// biasing the randomness a little so the prob is not 1/256,
// since that would make our coverage goal hard to reach
virtual    function byte get_data();
  bit [1:0]   zero_ones;
  zero_ones = $random;
  if (zero_ones == 2'b00)
    return 8'h00;
  else if (zero_ones == 2'b11)
    return 8'hFF;
  else
    return $random;
endfunction : get_data

It is worth reading this code as the "before" and asking the question before giving the answer: which part of this belongs to the test and which part belongs to the data? The legal range of A and B, and the bias to the edges, are properties of the data — they have nothing to do with what you want to test. The hand-written $random is the symptom: every component that wants a valid command has to repeat it. And if the DUT changes, all of them have to be remembered. A detail that gets charged for in the seeds slide: $random is the Verilog-95 one and has a global seed, so two threads calling it step on each other and the run is not reproducible. $urandom has a per-thread seed. The randomize() of the next slide uses the second one. The punchline for the next slide: SystemVerilog already brings this ready-made, and there is no need to write a single $urandom.

see this slide in the deck ›

Transactions

The randomized fields: the "after"

code/u6/transactions/tb_classes/command_transaction.svh · #fields-and-constraints
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

Three things about this code, in the order in which they get forgotten. rand in front of every field: without that the field does not enter the draw and randomize() leaves it as it was, without warning. It is the first place to look when a value always comes out the same. The dist with :/ and not :=: it reproduces the bias to the edges that the get_data() of the conventional testbench did by hand. The difference between the two operators is a whole unit of the course, and it is not a style detail — with := the 00 bin comes up 1 time in 256 and the corner cases never get filled. And the op with no constraint: an enum randomizes over its values, so no_op and rst_op are going to come out too. That is on purpose — the verification plan asks for operations after a reset.

see this slide in the deck ›

Transactions

The constructor: a uvm_object has no parent

code/u6/transactions/tb_classes/command_transaction.svh · #new
   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

It is the distinction that has to be left clear: component is structure —it is in the tree, it has a parent, it has phases—; object is data —it gets created, it travels around the testbench and it gets thrown away—. Both are UVM classes, but only one of them builds the hierarchy. A trick to remember it: if you can draw it in the block diagram of the testbench, it is a component. If it travels along an arrow of the diagram, it is an object.

see this slide in the deck ›

Transactions

do_copy(): copy the object, not the handle

code/u6/transactions/tb_classes/command_transaction.svh · #do_copy
   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

The pattern is the one from the class hierarchies, with two UVM rules on top. The first one, the one that makes it compile: the argument has to be of type uvm_object —the base class of everything—, because what SystemVerilog demands for the virtual override to work is that the type and the direction match, not the name. rhs is the identifier used in the signature of uvm_object::do_copy and that is why everybody copies it, but you can call it whatever you like. The first thing to do is cast it. And since the $cast checks at run time, it goes with its uvm_fatal: if somebody tries to copy a result_transaction onto a command_transaction, you want to find out right there and not three components further on. The second one: the super.do_copy(rhs) goes before touching your own fields. It is the same discipline as in the class hierarchies and here it matters more, because uvm_transaction —two levels up— has fields of its own that you do not see: accept_time, begin_time, end_time and initiator. And the detail that confuses everybody: you write do_copy(), but you never call it. The testbench calls copy(), which lives in uvm_object, and that one takes care of invoking your do_copy(). It is the same division as the phases — you put in the bottom part, the library handles the protocol.

see this slide in the deck ›

Transactions

The MOOCOW rule: sharing yes, modifying no

The name is a joke from the UVM Primer, but the rule is the one that avoids the most expensive bug of the class hierarchies: you sent the transaction to the scoreboard, you still hold the handle, you modify it for the next one, and the scoreboard ends up comparing against data that changed after it received it. It does not fail every time — it fails under load, which is when it suits you worst. The practical version, without acronyms: whoever produces the data does not touch it again after publishing it. If you need to change it, clone. Look at how the tester of this section complies with it: it does not reuse the handle. Every turn of the repeat does a fresh create(). It is cheaper than cloning and it avoids the argument. And there is one documented exception they are going to see on day 6: the driver writes the result inside the item the sequence lent it. It is agreed between the two parties and it is the only place in the testbench where that gets done.

see this slide in the deck ›

Transactions

clone_me(): the $cast written a single time

code/u6/transactions/tb_classes/command_transaction.svh · #clone_me
   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

It is a three-line method and it exists for one reason only: clone() returns a uvm_object, so everybody who uses it has to cast. Writing the $cast once inside the class is better than writing it fifty times outside. The name is not in the standard: clone_me() is a UVM Primer convention. In somebody else's project it can be called something else or not exist at all — and there you are going to see the $cast repeated in every caller. And the trap that has to be named: clone() calls create() and then copy(), which ends up in your do_copy(). If do_copy() forgets a field, the clone comes out incomplete and nobody warns you. It is the same hole as in the class hierarchies, under another name.

see this slide in the deck ›

Transactions

do_compare(): comparing two objects without writing an if per field

code/u6/transactions/tb_classes/command_transaction.svh · #do_compare
   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

Two things that get copied wrong. The $cast here does not go with a uvm_fatal: if the type does not fit, the right answer is same = 0 —they are different, obviously, if they are not even of the same class—, not killing the simulation. It is the difference with do_copy(), where a wrong type really is a testbench bug. Worth showing them side by side. And the super.do_compare() goes chained with &&, not called and thrown away. The classic mistake is to write super.do_compare(rhs, comparer); on a line of its own and then same = (A == rhs.A) && ...: it compiles, it runs, and it silently stops comparing everything from the class above. The uvm_comparer of the second argument is the policy —how many differences to report, with what verbosity—. Almost nobody touches it, but it has to be declared because the signature asks for it.

see this slide in the deck ›

Transactions

convert2string(): the transaction prints itself

code/u6/transactions/tb_classes/command_transaction.svh · #convert2string
   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string
el archivo entero, 86 líneas
class command_transaction extends uvm_sequence_item;
   `uvm_object_utils(command_transaction)
   rand byte unsigned A;
   rand byte unsigned B;
   rand operation_t   op;

   // :/ and NOT :=  ---  this is the line that reproduces the bias towards the
   // corner cases that get_data() of the Functional coverage section did by hand (1/4 at 00, 1/4 at FF,
   // 1/2 in the middle). With ":=" the weight applies to EVERY value of the range, so the
   // middle takes 254 out of 256 and 00 comes up once every 256: the distribution
   // ends up almost uniform and the corner bins never fill.
   // The Constrained Random unit has the experiment with the numbers.
   constraint data {
      A dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
      B dist {
         8'h00 :/ 1,
         [8'h01 : 8'hFE] :/ 2,
         8'hFF :/ 1
      };
   }

   function void do_copy(uvm_object rhs);
      command_transaction copied_transaction_h;

      // Checks that make DEBUG easier
      if (rhs == null)
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy from a null pointer")

      if (!$cast(copied_transaction_h, rhs))
         `uvm_fatal("COMMAND TRANSACTION", "Tried to copy wrong type.")

      super.do_copy(rhs);  // copy all parent class data

      A = copied_transaction_h.A;
      B = copied_transaction_h.B;
      op = copied_transaction_h.op;

   endfunction : do_copy

   function command_transaction clone_me();
      command_transaction clone;
      uvm_object tmp;

      tmp = this.clone();
      $cast(clone, tmp);
      return clone;
   endfunction : clone_me

   function bit do_compare(uvm_object rhs, uvm_comparer comparer);
      command_transaction compared_transaction_h;
      bit same;

      if (rhs == null)
         `uvm_fatal("RANDOM TRANSACTION", "Tried to do comparison to a null pointer");

      // First check whether we are comparing the same type
      if (!$cast(compared_transaction_h, rhs)) same = 0;
      else
         // Deep comparison: what the parent compares, plus the three fields of
         // this class
         same = super.do_compare(
             rhs, comparer
         ) && (compared_transaction_h.A == A) && (compared_transaction_h.B == B) &&
             (compared_transaction_h.op == op);

      return same;
   endfunction : do_compare

   function string convert2string();
      string s;
      // op.name() can be add_op, no_op ...
      s = $sformatf("A: %2h  B: %2h op: %s", A, B, op.name());
      return s;
   endfunction : convert2string

   // Constructor of a uvm_object: dead simple, it only takes a name. A
   // transaction has no parent because it does not live in the component tree.
   function new(string name = "");
      super.new(name);
   endfunction : new

endclass : command_transaction

It is the most used of the three methods, and the one that gets the least attention: every message of the scoreboard and of the monitors of the rest of the course comes out of here. The .name() of the enum is the detail that changes the day: without it the log says op: 4 and you have to go looking for the typedef; with it, it says op: mul_op. The rule to take home: if a field is an enum, the log gets its name, never its value. Worth naming the relatives UVM brings and the course does not use: print() and sprint(), which print the transaction on their own if you registered the fields with the `uvm_field_* macros — or if you write do_print(), the manual equivalent of convert2string(), which does not show up in a single slide of the course. Careful with that: with no macros and no do_print(), a t.print() on the transactions here prints the header and nothing else. The format **can** be changed: the default is the uvm_table_printer —one row per field, with type and radix— and there is a uvm_line_printer that puts the whole object on one line. The reason to prefer convert2string() is simpler and should be said that way: written by hand it fits in one line that says what you care about, and in a file of a hundred thousand lines that matters.

see this slide in the deck ›

Transactions

The field macros: reading them even if you do not write them

class command_transaction extends uvm_sequence_item;
   `uvm_object_utils_begin(command_transaction)
      `uvm_field_int (A,  UVM_DEFAULT | UVM_HEX)   // an integral field
      `uvm_field_enum(operation_t, op, UVM_DEFAULT)
      `uvm_field_object(payload, UVM_DEFAULT | UVM_REFERENCE)  // handle, not a copy
   `uvm_object_utils_end
endclass

This is the "and when you open the VIP at work" slide. The course's decision —writing do_copy, do_compare and convert2string by hand— is taken and well taken, and it is not being reversed here: what gets added is the half that was missing, which is reading what somebody else wrote. The way to read it is always the same and is worth handing over as a recipe: the list of `uvm_field_* **is** the definition of what "equal" means and what "copy" means for that class. If a field is not on the list, compare() does not look at it — and that is the most expensive bug these macros produce: a scoreboard that goes green because the field that matters was left out of the declaration. The UVM_REFERENCE deserves its second, because it is the do_copy() trap again in another disguise: without it, copying the transaction also copies the object hanging off it, and a clone() in a monitor with heavy traffic gets expensive without anybody noticing. With it, the two transactions share the same payload — which is exactly what MOOCOW says must not be modified. The real cost, so the course's decision does not look like taste: the macros expand into hundreds of lines per class, and when something goes wrong the error points inside the expansion. A hand-written four-line do_compare reads on the spot.

see this slide in the deck ›

Transactions

The seven steps, and why there are seven

What it costs to change the data type of a TB that already exists. We look at 2, 3, 6 and 7; 1, 4 and 5 are a direct port and are in the repo:

# What Where
1 a result_transaction for the trip back result_transaction.svh
2 an add_transaction, additions only add_transaction.svh
3 the two testers merge into one tester.svh
4 the command_monitor publishes objects command_monitor.svh
5 the result_monitor, the same result_monitor.svh
6 the scoreboard uses compare() scoreboard.svh
7 add_test overrides the data add_test.svh

The seven steps are the most boring part of the section and also the most honest, so it is worth framing them instead of rushing through them: this is what it costs to change the data type of a testbench that already exists. The real advice is to do it from the start. The one to keep an eye on is number 3, because it is the only one that deletes a class: add_tester disappears. That is where the result of the section is, and it is worth announcing it — moving the decision into the data made a whole structural class redundant. If the group is going fast, steps 4 and 5 get opened in the editor and compared with the ones from the analysis ports: the difference is that instead of filling a struct there is a create() and fields get filled. Nothing else.

see this slide in the deck ›

Transactions

Step 2 · an add_transaction that only adds

code/u6/transactions/tb_classes/add_transaction.svh
class add_transaction extends command_transaction;
   `uvm_object_utils(add_transaction)

   constraint add_only {op == add_op;}

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

Eight lines, and out of those only one does anything: constraint add_only {op == add_op;}. It is worth putting it next to the add_tester of the env, which did the same thing by redefining a method. Two ways of saying "additions only", and today's does not touch behaviour code — it only describes which values are legal. The conceptual point, which is the one that orders the day: constraints are inherited and they accumulate. add_transaction does not replace data, it adds to it. The solver has to satisfy both at once, so there is still bias to the edges in A and B — and that is exactly what we want. From there comes the warning: two inherited constraints that contradict each other do not give a compilation error, they give a randomize() that returns 0. It is case 1 of the Constrained random experiment.

see this slide in the deck ›

Transactions

Step 3 · two testers become one

code/u6/transactions/tb_classes/tester.svh · #the-loop
   task run_phase(uvm_phase phase);
      command_transaction command;

      phase.raise_objection(this);

      // ALL transactions come out of the factory, the directed ones too.
      // A new() here would compile and work just the same, but it would skip
      // add_test's set_type_override: the factory never hears about what you
      // build by hand. See the slide "the new() that eats the override".
      command = command_transaction::type_id::create("command");
      command.op = rst_op;
      command_port.put(command);

      repeat (1000) begin : random_loop
         command = command_transaction::type_id::create("command");
         // randomize() returns 0 if the constraints have no solution, and aborts
         // nothing. A bare assert() lets an unrandomized transaction through in
         // silence: the TB carries on, sending garbage.
         if (!command.randomize()) `uvm_fatal("TESTER", "randomize() failed")
         command_port.put(command);
      end : random_loop

      // Directed: the multiplier overflow case, which 1000 random operations
      // might never touch. It comes out of the factory like the others, but
      // since it is not randomized, add_transaction's constraint does not run:
      // the factory picks the TYPE, constraints only act inside randomize().
      command = command_transaction::type_id::create("command");
      command.op = mul_op;
      command.A = 8'hFF;
      command.B = 8'hFF;
      command_port.put(command);

      #500;
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 49 líneas
class tester extends uvm_component;
   `uvm_component_utils(tester)

   uvm_put_port #(command_transaction) command_port;

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

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

   task run_phase(uvm_phase phase);
      command_transaction command;

      phase.raise_objection(this);

      // ALL transactions come out of the factory, the directed ones too.
      // A new() here would compile and work just the same, but it would skip
      // add_test's set_type_override: the factory never hears about what you
      // build by hand. See the slide "the new() that eats the override".
      command = command_transaction::type_id::create("command");
      command.op = rst_op;
      command_port.put(command);

      repeat (1000) begin : random_loop
         command = command_transaction::type_id::create("command");
         // randomize() returns 0 if the constraints have no solution, and aborts
         // nothing. A bare assert() lets an unrandomized transaction through in
         // silence: the TB carries on, sending garbage.
         if (!command.randomize()) `uvm_fatal("TESTER", "randomize() failed")
         command_port.put(command);
      end : random_loop

      // Directed: the multiplier overflow case, which 1000 random operations
      // might never touch. It comes out of the factory like the others, but
      // since it is not randomized, add_transaction's constraint does not run:
      // the factory picks the TYPE, constraints only act inside randomize().
      command = command_transaction::type_id::create("command");
      command.op = mul_op;
      command.A = 8'hFF;
      command.B = 8'hFF;
      command_port.put(command);

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

It is the result of the section, measured in files: add_tester.svh stops existing. It is worth saying out loud, because the env had spent a while justifying that hierarchy and now half of it is redundant. And it is not that the env was wrong: it was the right answer while the data was a mute struct. When the data knows how to randomize itself, that step of the hierarchy runs out of work. It is a good example of abstractions being justified against the problem of the moment, not forever. The if (!command.randomize()) uvm_fatal is the line to copy properly: randomize() returns 0 if the constraints have no solution and it aborts nothing. There is a whole slide on that further on.

see this slide in the deck ›

Transactions

Step 6 · the scoreboard compares objects, not numbers

code/u6/transactions/tb_classes/scoreboard.svh · #write
   function void write(result_transaction t);
      string data_str;
      command_transaction cmd;
      result_transaction predicted;

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

      predicted = predict_result(cmd);

      data_str = {
         cmd.convert2string(),
         " ==>  Actual ",
         t.convert2string(),
         "/Predicted ",
         predicted.convert2string()
      };

      if (!predicted.compare(t)) `uvm_error("SELF CHECKER", {"FAIL: ", data_str})
      else `uvm_info("SELF CHECKER", {"PASS: ", data_str}, UVM_HIGH)

   endfunction : write
el archivo entero, 59 líneas
class scoreboard extends uvm_subscriber #(result_transaction);
   `uvm_component_utils(scoreboard);

   uvm_tlm_analysis_fifo #(command_transaction) cmd_f;

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

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

   function result_transaction predict_result(command_transaction cmd);
      result_transaction predicted;

      predicted = result_transaction::type_id::create("predicted");

      case (cmd.op)
         add_op: predicted.result = cmd.A + cmd.B;
         sub_op: predicted.result = cmd.A - cmd.B;
         and_op: predicted.result = cmd.A & cmd.B;
         xor_op: predicted.result = cmd.A ^ cmd.B;
         mul_op: predicted.result = cmd.A * cmd.B;
      endcase  // case (op_set)

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

      return predicted;

   endfunction : predict_result

   function void write(result_transaction t);
      string data_str;
      command_transaction cmd;
      result_transaction predicted;

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

      predicted = predict_result(cmd);

      data_str = {
         cmd.convert2string(),
         " ==>  Actual ",
         t.convert2string(),
         "/Predicted ",
         predicted.convert2string()
      };

      if (!predicted.compare(t)) `uvm_error("SELF CHECKER", {"FAIL: ", data_str})
      else `uvm_info("SELF CHECKER", {"PASS: ", data_str}, UVM_HIGH)

   endfunction : write
endclass : scoreboard

Comparing this write() with the one from the analysis ports is the best way to close the section: over there was a case with the prediction written inside it and a hand-written comparison; here there is a predict_result() that returns an object and a predicted.compare(t). What was gained is concrete: the error message is now built with convert2string() of the three transactions, so it says what was sent, what came out and what was expected, all with the enums by name. And the day the transaction gains a field, the log shows it on its own. A detail that has to be pointed out because it is the rule of the section applied: the predicted comes from type_id::create(), not from new(). It is a transaction that travels —even if it only travels as far as the compare() on the next line—, so it comes out of the factory like all the others. And the do ... while that skips no_op and rst_op is still the same as in the analysis ports: those operations produce no result, and if they are not thrown away the comparison shifts by one and everything fails.

see this slide in the deck ›

Transactions

Step 7 · the override, now on the data

code/u6/transactions/tb_classes/add_test.svh
class add_test extends random_test;
   `uvm_component_utils(add_test);

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

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

endclass

It is worth putting this slide next to the add_test of the env and counting lines: the same idea, one level further down. Before, what generates was substituted; now, what is generated is. The order of the super is the trap of the slide, and it is a sibling of the one in the env: the factory decides what to build at the moment of the create(). If the override arrives afterwards, there is no error — the testbench runs with command_transaction and the "additions" test sends random operations. It does not break, it lies. And a question that always comes up: why does add_test call super.build_phase() here if the course says it is not needed? Because its base is random_test, a class of ours that has a build_phase with real work in it. The rule from the components was talking about uvm_component, which does not have one.

see this slide in the deck ›

Transactions

The new() that eats the override

command = new("command");                                   // NO
command.op = rst_op;

command = command_transaction::type_id::create("command");  // YES
command.op = rst_op;

This slide comes out of a bug this very course had: the tester created the two directed operations —the reset and the FF x FF multiplication— with new(), so under add_test those two were NOT add_transaction. Nobody noticed because the testbench works just the same. The question to throw at the group, which is the one that orders everything: if the reset now comes out of the factory and under add_test it is an add_transaction, why does the constraint op == add_op not turn it into an addition? Because we do not randomize it. The factory picks the type; constraints only act on randomize().

see this slide in the deck ›

Transactions

And randomize() never goes on its own

assert (command.randomize());                                  // NO
if (!command.randomize()) `uvm_fatal("TESTER", "randomize() failed")  // YES

It is one extra line that pays for itself the first time somebody adds a contradictory constraint. Without the else, the symptom is a scoreboard failing on transactions with absurd values and half an afternoon spent looking for it in the DUT. The `uvm_fatal is not an exaggeration: an unrandomized transaction is not bad stimulus, it is stimulus you did not choose. Better for the run to die with a message than to tell somebody that last night's regression meant nothing.

see this slide in the deck ›

Transactions

Summary of the unit

Closing of day 5, and it is worth saying what is missing: today the stimulus is generated by a tester you wrote yourself; in modern UVM that is a uvm_sequence running on a sequencer, and the set of driver + monitor + sequencer is packed into an agent. That is day 6.

see this slide in the deck ›

Constrained random

The other half of the pincer

It is the half of the pair that was missing: up to here the course measured coverage and built the transactions, but the stimulus still came out of a get_op() with a hand-written case. The sentence that orders the section: a constraint does not pick a value, it describes the set of legal values. The one that picks is the solver, and it picks differently from what you expect more often than you would like. The whole section is about measuring that instead of assuming it.

see this slide in the deck ›

Constrained random

rand and randomize(): what you have already seen

class command_transaction extends uvm_sequence_item;
   rand byte unsigned A;      // rand  -> it enters the draw
   rand byte unsigned B;
   rand operation_t   op;     // an enum randomizes over its values

   constraint data { ... }    // the rules, in a block of their own
endclass

The bullet to say slowly is the third one, because it is the wrong mental model everybody brings along: a constraint does not run. It is not a long if, it does not go from top to bottom, and the order they are written in makes no difference. It is a system of equations, and randomize() hands it to a solver —in this course, z3— so that it returns a random solution out of all the ones that satisfy it. From there comes the consequence of the last bullet, and it is the most expensive trap of the section: two incompatible constraints compile perfectly. The error shows up at simulation time, randomize() returns 0, and if nobody checks the return value the class is left with the values it had. The testbench goes on running and sends garbage. The rule to take away from here, and one the course keeps everywhere in code/: a randomize() always goes inside an if that checks the return value. Never on its own.

see this slide in the deck ›

Constrained random

The other three words: randc and the two hooks

class command_transaction extends uvm_sequence_item;
   rand  byte unsigned A;       // it can repeat: 3 3 6 1 3 4 6 2 ...
   randc bit [2:0]     indice;  // it walks the 8 and only then repeats

   function void pre_randomize();   // runs BEFORE the draw
      if (!primera) A = A_anterior; // …with the values still old
   endfunction

   function void post_randomize();  // runs AFTER, with the new values
      checksum = A ^ B;             // what is not drawn gets computed here
   endfunction
endclass

They are the three words of the language that are missing for the student to read any foreign transaction without stopping. None is hard; what is needed is knowing when each one is used. randc is the answer to "I want to go through every opcode", and the argument is efficiency: with rand over eight values it takes some twenty-two draws to see all eight —the coupon collector's problem—, and with randc eight are enough. And the width limit has a concrete reason: the solver has to carry the list of what has already come out, so a 32-bit randc would be a table of four billion entries. The two hooks are best explained by what they solve. post_randomize() is for the field that derives from the drawn ones: a CRC, a parity, the length of a payload that has already been drawn. Making it rand with a constraint tying it to the rest is asking the solver to solve a problem that is done with an XOR. pre_randomize() gets used less and has a trap worth naming: it runs before the draw, which means the fields still have the values of the previous transaction. That makes it useful for whatever depends on the past —keeping the previous address to ask for a contiguous access— and dangerous if you believe the new ones are already there. And the underlying warning, which is the same as the whole section's: a field computed in post_randomize() is outside the system of equations. The solver does not see it, the constraints do not restrict it, and randomize() will never return 0 because of it.

see this slide in the deck ›

Constrained random

dist: := is not the same as :/

A dist {8'h00 := 1, [8'h01 : 8'hFE] := 1, 8'hFF := 1};   // the weight goes to EACH value
A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};   // the weight gets SPLIT

The arithmetic has to be done on the board, because read out loud it does not sink in: with := the weights are 1, 1 and 1, but the middle one is applied to each one of its 254 values. A total of 256 equal parts, and A=00 comes up one time in 256. With :/ the parts are three —1, 2 and 1— and A=00 comes up one time in four. Sixty-four times more often, for two characters. And now the part that makes it dangerous: both versions work. Neither gives a warning, neither fails, the testbench runs the thousand transactions just the same. The only thing that changes is that with := the a_00 bin of the covergroup takes an eternity to fill, and whoever looks at the report concludes that a test is missing. It is exactly the kind of bug the course calls a silent trap, and that is why the next slide does not explain: it measures. The practical rule to hand down: if you write a dist, run a histogram before believing it.

see this slide in the deck ›

Constrained random

Do not assume it: measure it

code/u6/transactions/constraints/01_dist.sv · #both-spellings
   // The weight goes to EVERY value of the range: the middle takes 254 of 256.
   class por_valor;
      rand byte unsigned A;
      constraint data {A dist {8'h00 := 1, [8'h01 : 8'hFE] := 1, 8'hFF := 1};}
   endclass

   // The weight is SPLIT inside the range: 1/4 at 00, 1/2 in the middle, 1/4 at FF.
   // It is the same bias get_data() of the Functional coverage section did by hand.
   class por_rango;
      rand byte unsigned A;
      constraint data {A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};}
   endclass
el archivo entero, 49 líneas
// Constrained Random 1 -- dist: ":=" and ":/" do NOT hand out the same way.
//
// The verification plan of the course asks for "inputs all 0s and all 1s".
// Without the bias those bins never fill and the coverage stalls. This example
// measures the bias of both spellings instead of trusting intuition.
module top_dist;

   // The weight goes to EVERY value of the range: the middle takes 254 of 256.
   class por_valor;
      rand byte unsigned A;
      constraint data {A dist {8'h00 := 1, [8'h01 : 8'hFE] := 1, 8'hFF := 1};}
   endclass

   // The weight is SPLIT inside the range: 1/4 at 00, 1/2 in the middle, 1/4 at FF.
   // It is the same bias get_data() of the Functional coverage section did by hand.
   class por_rango;
      rand byte unsigned A;
      constraint data {A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};}
   endclass

   localparam int N = 400;

   initial begin
      por_valor v;
      por_rango r;
      int v00, vff, r00, rff;

      v = new();
      r = new();

      repeat (N) begin
         if (!v.randomize()) $fatal(1, "por_valor randomize() failed");
         if (v.A == 8'h00) v00 = v00 + 1;
         if (v.A == 8'hFF) vff = vff + 1;

         if (!r.randomize()) $fatal(1, "por_rango randomize() failed");
         if (r.A == 8'h00) r00 = r00 + 1;
         if (r.A == 8'hFF) rff = rff + 1;
      end

      $display("%0d randomizations of each version", N);
      $display("  :=   A=00 %5.1f%%   A=FF %5.1f%%   the weight goes to each value",
               100.0 * v00 / N, 100.0 * vff / N);
      $display("  :/   A=00 %5.1f%%   A=FF %5.1f%%   the weight gets split",
               100.0 * r00 / N, 100.0 * rff / N);
      $finish;
   end

endmodule : top_dist
$ cd code/u6/transactions/constraints && bash run.sh
400 randomizations of each version
  :=   A=00   0.2%   A=FF   0.2%   the weight goes to each value
  :/   A=00  23.2%   A=FF  27.0%   the weight gets split

Measured histogram: with := the edge bins come out 0.2 %; with :/ they come out 23 % and 27 %

This experiment came out of a bug in this very course: command_transaction had the dist written with :=, which means the bias to the corner cases the transactions claimed to have did not exist. The distribution was practically uniform and nobody found out, because the testbench passed just the same. That is the underlying point of the section, and it is worth saying it that bluntly: a badly written constraint does not fail, it lies. There is no warning, there is no compilation error, there is no test in red. The only thing that gives it away is the coverage that does not go up — and that is only noticed weeks later. A practical rule to take home: every time you write a dist, run a few thousand randomizations and print the histogram once. It costs ten minutes and it is the difference between believing and knowing.

see this slide in the deck ›

Constrained random

inside and randomize() with {}

code/u6/transactions/constraints/02_with.sv · #comando
   class comando;
      rand byte unsigned A;
      rand byte unsigned B;
      rand operation_t   op;

      constraint data {
         A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
         B dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
      }

      // inside is a set of legal values. Without it the random also asks for
      // no_op and rst_op, which compute nothing.
      constraint utiles {op inside {add_op, and_op, xor_op, mul_op};}
   endclass
el archivo entero, 74 líneas
// Constrained Random 2 -- inside, and randomize() with {}.
//
// The class constraint gives the bulk of the stimulus. The directed case is asked
// for at the point of use, without touching the class and without a new tester.
module top_with;

   typedef enum bit [2:0] {
      no_op  = 3'b000,
      add_op = 3'b001,
      sub_op = 3'b010,
      and_op = 3'b011,
      xor_op = 3'b100,
      mul_op = 3'b101,
      rst_op = 3'b111
   } operation_t;

   class comando;
      rand byte unsigned A;
      rand byte unsigned B;
      rand operation_t   op;

      constraint data {
         A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
         B dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
      }

      // inside is a set of legal values. Without it the random also asks for
      // no_op and rst_op, which compute nothing.
      constraint utiles {op inside {add_op, and_op, xor_op, mul_op};}
   endclass

   localparam int N = 400;

   initial begin
      comando c;
      int maximos;

      c = new();

      // The mul_max bin of the coverage in the Functional coverage section: a multiplication with
      // both legs at 0xFF. It is the multiplier overflow.
      repeat (N) begin
         if (!c.randomize()) $fatal(1, "randomize() failed");
         if (c.op == mul_op && c.A == 8'hFF && c.B == 8'hFF) maximos = maximos + 1;
      end
      $display("random:   %0d tries, mul_max filled %0d time(s)", N, maximos);

      // The same thing, asked for: with {} adds constraints ONLY for this call.
      // The request is satisfiable, but in 5.052 the dist is solved by picking a
      // value BEFORE checking the with: if the draw does not comply, it returns
      // 0 instead of retrying. With A and B on dist, both landing on 0xFF is
      // 1/4 x 1/4. The measured rates are in
      // code/verilator/repro-dist-with.sv; the detail in docs/verilator.md.
      if (!c.randomize() with {
            op == mul_op;
            A  == 8'hFF;
            B  == 8'hFF;
          })
         $display("with with: randomize() gave 0  <- a Verilator limitation, not yours");

      // The portable workaround meanwhile: turn off the split, which for a
      // directed case makes no sense anyway.
      c.data.constraint_mode(0);
      if (!c.randomize() with {
            op == mul_op;
            A  == 8'hFF;
            B  == 8'hFF;
          })
         $fatal(1, "randomize() with failed even with the constraint turned off");
      $display("with with: 1 try,      A=%2h %s B=%2h", c.A, c.op.name(), c.B);
      $finish;
   end

endmodule : top_with
code/u6/transactions/constraints/02_with.sv · #workaround
      // The portable workaround meanwhile: turn off the split, which for a
      // directed case makes no sense anyway.
      c.data.constraint_mode(0);
      if (!c.randomize() with {
            op == mul_op;
            A  == 8'hFF;
            B  == 8'hFF;
          })
         $fatal(1, "randomize() with failed even with the constraint turned off");
      $display("with with: 1 try,      A=%2h %s B=%2h", c.A, c.op.name(), c.B);
el archivo entero, 74 líneas
// Constrained Random 2 -- inside, and randomize() with {}.
//
// The class constraint gives the bulk of the stimulus. The directed case is asked
// for at the point of use, without touching the class and without a new tester.
module top_with;

   typedef enum bit [2:0] {
      no_op  = 3'b000,
      add_op = 3'b001,
      sub_op = 3'b010,
      and_op = 3'b011,
      xor_op = 3'b100,
      mul_op = 3'b101,
      rst_op = 3'b111
   } operation_t;

   class comando;
      rand byte unsigned A;
      rand byte unsigned B;
      rand operation_t   op;

      constraint data {
         A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
         B dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};
      }

      // inside is a set of legal values. Without it the random also asks for
      // no_op and rst_op, which compute nothing.
      constraint utiles {op inside {add_op, and_op, xor_op, mul_op};}
   endclass

   localparam int N = 400;

   initial begin
      comando c;
      int maximos;

      c = new();

      // The mul_max bin of the coverage in the Functional coverage section: a multiplication with
      // both legs at 0xFF. It is the multiplier overflow.
      repeat (N) begin
         if (!c.randomize()) $fatal(1, "randomize() failed");
         if (c.op == mul_op && c.A == 8'hFF && c.B == 8'hFF) maximos = maximos + 1;
      end
      $display("random:   %0d tries, mul_max filled %0d time(s)", N, maximos);

      // The same thing, asked for: with {} adds constraints ONLY for this call.
      // The request is satisfiable, but in 5.052 the dist is solved by picking a
      // value BEFORE checking the with: if the draw does not comply, it returns
      // 0 instead of retrying. With A and B on dist, both landing on 0xFF is
      // 1/4 x 1/4. The measured rates are in
      // code/verilator/repro-dist-with.sv; the detail in docs/verilator.md.
      if (!c.randomize() with {
            op == mul_op;
            A  == 8'hFF;
            B  == 8'hFF;
          })
         $display("with with: randomize() gave 0  <- a Verilator limitation, not yours");

      // The portable workaround meanwhile: turn off the split, which for a
      // directed case makes no sense anyway.
      c.data.constraint_mode(0);
      if (!c.randomize() with {
            op == mul_op;
            A  == 8'hFF;
            B  == 8'hFF;
          })
         $fatal(1, "randomize() with failed even with the constraint turned off");
      $display("with with: 1 try,      A=%2h %s B=%2h", c.A, c.op.name(), c.B);
      $finish;
   end

endmodule : top_with

The number that comes out of running 02_with.sv is the whole argument of the section: 400 random attempts fill mul_max a handful of times —6 with the default seed, between 2 and 8 depending on which one you use— because the dist biases towards the edges. The arithmetic is worth doing out loud: op comes out uniform among four and each leg lands on FF one time in four, that is 1/4 × 1/4 × 1/4 = 1 in 64, which over 400 attempts is six and a bit. With the badly written dist of the previous slide, the probability of both legs landing on FF is 1/65536 per operation: you never touch it. The mental sequence is always the same: I run random, I look at which bin was left empty, I write a three-line with {}, I run again. Never "I write 76 tests". The Verilator limitation the section states is there on purpose, and it is worth stating it properly because it is not a matter of "it works or it does not". Verilator resolves the dist by picking a concrete value first and only then checks the rest: if the one drawn does not satisfy the with, it returns 0 instead of looking for another. The success rate is therefore the probability of the bin. Measured: with {A == 8'hFF} resolves 25 % of the time —the weight of that bin—, with {A inside {[1:10]}} 2 %, and a field with no dist 100 %. Why it matters more than it looks: the symptom is an intermittent directed case, which works when you try it and fails in the overnight regression. The numbers are in code/verilator/repro-dist-with.sv, and the workaround —turning the constraint off— is the same constraint_mode() you see further on.

see this slide in the deck ›

Constrained random

soft: the default a with {} can override

class command_transaction extends uvm_sequence_item;
   rand byte unsigned A;
   constraint por_defecto { soft A == 8'h00; }   // "zero, unless somebody asks otherwise"
endclass

t.randomize();                          // A = 00
t.randomize() with { A inside {[1:10]}; };   // A between 1 and 10: the soft drops on its own

This is the piece that makes the previous slide scale to a real testbench. The randomize() with {} is for asking for a directed case, but if the class already has a hard constraint saying otherwise, the with does not override it: it contradicts it, and randomize() returns 0. The old way out was constraint_mode(0) — which forces whoever writes the test to know the name of a constraint in a class they did not write. The design rule to leave behind, because it is the one used in every modern VIP: what is a rule of the protocol goes hard; what is a preference of the stimulus goes soft. An address aligned to 4 is hard, because the DUT does not accept anything else. That burst_len defaults to 1 is soft, because the test that wants long bursts has the right to ask for them without permission. The tie-break between two soft ones is worth saying and not using: the last one declared wins, and if that matters to you for anything, the constraint was badly written. They came in with IEEE 1800-2012, so any simulator of the last decade has them — and Verilator too, measured right up there.

see this slide in the deck ›

Constrained random

The order of resolution biases without warning

rand bit           es_reset;
rand byte unsigned A;
constraint c {es_reset -> A == 8'h00;}   // "if I ask for a reset, the operands at zero"

One cell against a 16 by 16 block: the 257 solutions, and only one has the reset

The sentence that dismantles the intuition is the one in the first bullet, and it is worth repeating it word for word: the solver picks uniformly among the solutions, not among the values of each field. Nobody wrote that es_reset was going to come out 1 half the time; that came from assuming each field gets drawn separately, and it does not. It is worth counting the 257 solutions out loud, because the number convinces more than the argument: es_reset=1 forces A=00, that is to say a single combination; es_reset=0 leaves A free, that is to say 256. The draw is over the 257, and the reset takes one of them. The connection with the verification plan is what makes this slide worth it: the row "any operation after a reset" does not get filled, the coverage stays pinned down, and the report does not say why. A student without this slide adds random tests for an afternoon. And the warning about the cure, so that it does not get overused: solve ... before does not change which solutions are legal, only the order in which the solver picks. It is a distribution knob, not a correctness one — and it costs solver time, so it goes where it is needed and not out of habit.

see this slide in the deck ›

Constrained random

Measured, with the workaround that Verilator does honour

code/u6/transactions/constraints/03_solve.sv · #both-classes
   class sesgada;
      rand bit           es_reset;
      rand byte unsigned A;
      constraint c {es_reset -> A == 8'h00;}
   endclass

   // The language's answer is "solve es_reset before A": pick the control field
   // FIRST and then solve the rest. Verilator 5.052 accepts the syntax and does
   // NOT honour it -- it leaves es_reset stuck at 0, which is worse than
   // ignoring it. Repro in code/verilator/repro-solve-before.sv.
   //
   // The workaround that IS portable: ask explicitly for the split of the
   // control field. It says the same and does not rely on the solver ordering well.
   class pareja extends sesgada;
      constraint reparto {es_reset dist {1'b0 :/ 1, 1'b1 :/ 1};}
   endclass
el archivo entero, 52 líneas
// Constrained Random 3 -- the solve order biases without warning.
//
// The constraint below looks harmless and is the classic trap: the solver picks
// uniformly among the SOLUTIONS, not among the values of each field.
// With es_reset=1 there is a single solution (A=00); with es_reset=0 there are 256.
// Result: the reset gets asked for 1 in every 257 times, and the "any
// operation after a reset" bin of the Functional coverage section plan never fills.
module top_solve;

   class sesgada;
      rand bit           es_reset;
      rand byte unsigned A;
      constraint c {es_reset -> A == 8'h00;}
   endclass

   // The language's answer is "solve es_reset before A": pick the control field
   // FIRST and then solve the rest. Verilator 5.052 accepts the syntax and does
   // NOT honour it -- it leaves es_reset stuck at 0, which is worse than
   // ignoring it. Repro in code/verilator/repro-solve-before.sv.
   //
   // The workaround that IS portable: ask explicitly for the split of the
   // control field. It says the same and does not rely on the solver ordering well.
   class pareja extends sesgada;
      constraint reparto {es_reset dist {1'b0 :/ 1, 1'b1 :/ 1};}
   endclass

   localparam int N = 2000;

   initial begin
      sesgada s;
      pareja  p;
      int s_reset, p_reset;

      s = new();
      p = new();

      repeat (N) begin
         if (!s.randomize()) $fatal(1, "sesgada randomize() failed");
         if (s.es_reset) s_reset = s_reset + 1;

         if (!p.randomize()) $fatal(1, "pareja randomize() failed");
         if (p.es_reset) p_reset = p_reset + 1;
      end

      $display("%0d randomizations of each version", N);
      $display("  as written             es_reset=1 in %5.1f%%   (1 in 257)",
               100.0 * s_reset / N);
      $display("  with dist on es_reset  es_reset=1 in %5.1f%%", 100.0 * p_reset / N);
      $finish;
   end

endmodule : top_solve
2000 randomizations of each version
  as written             es_reset=1 in   0.3%   (1 in 257)
  with dist on es_reset  es_reset=1 in  51.7%

It is the second Verilator hole of the course, alongside the transition bins, and it gets treated the same way: it gets said head on, with a minimal repro and with a workaround that works. The concept belongs to the language, not to the simulator. What does have to be stressed is the asymmetry: the biased constraint gives 0.3 %, which is exactly what the theory predicts — which means the Verilator solver is fine; what is missing is the ordering directive. And the general moral, which holds for any tool: if the stimulus has a control field (a mode, a kind of operation, an "inject an error"), do not trust it to come out even. Measure it.

see this slide in the deck ›

Constrained random

When there is no solution

code/u6/transactions/constraints/04_falla.sv · #the-unsolvable
   class comando;
      rand byte unsigned A;
      constraint chico {A < 8'h10;}
      constraint grande {A > 8'hF0;}  // contradicts the previous one: no solution
   endclass

   initial begin
      comando c;
      c = new();

      // 1. Both constraints active: there is no A that satisfies the two.
      if (!c.randomize()) $display("1. randomize() returned 0: the constraints do not close");
      else $display("1. randomize() gave %2h  (should not get here)", c.A);

      // 2. constraint_mode(0) turns a constraint off at run time.
      c.grande.constraint_mode(0);
      if (!c.randomize()) $display("2. randomize() returned 0  (should not get here)");
      else $display("2. with 'grande' turned off: A=%2h, and it honours 'chico'", c.A);

      // 3. rand_mode(0) takes the field out of the draw: it keeps its current value.
      c.A.rand_mode(0);
      if (!c.randomize()) $display("3. randomize() returned 0  (should not get here)");
      else $display("3. with A out of the draw: A=%2h, the same as before", c.A);

      $finish;
el archivo entero, 35 líneas
// Constrained Random 4 -- when randomize() returns 0, and how to switch things off.
//
// randomize() neither aborts nor prints anything on its own: it returns 0 and
// carries on. If nobody looks at the return value, the testbench sends an
// unrandomized transaction and the bug shows up three components later.
module top_falla;

   class comando;
      rand byte unsigned A;
      constraint chico {A < 8'h10;}
      constraint grande {A > 8'hF0;}  // contradicts the previous one: no solution
   endclass

   initial begin
      comando c;
      c = new();

      // 1. Both constraints active: there is no A that satisfies the two.
      if (!c.randomize()) $display("1. randomize() returned 0: the constraints do not close");
      else $display("1. randomize() gave %2h  (should not get here)", c.A);

      // 2. constraint_mode(0) turns a constraint off at run time.
      c.grande.constraint_mode(0);
      if (!c.randomize()) $display("2. randomize() returned 0  (should not get here)");
      else $display("2. with 'grande' turned off: A=%2h, and it honours 'chico'", c.A);

      // 3. rand_mode(0) takes the field out of the draw: it keeps its current value.
      c.A.rand_mode(0);
      if (!c.randomize()) $display("3. randomize() returned 0  (should not get here)");
      else $display("3. with A out of the draw: A=%2h, the same as before", c.A);

      $finish;
   end

endmodule : top_falla
1. randomize() returned 0: the constraints do not close
2. with 'grande' turned off: A=06, and it honours 'chico'
3. with A out of the draw: A=06, the same as before

Here the circle closes with the assert(randomize()) slide: case 1 is precisely what happens when two constraints contradict each other, and without the else the testbench goes on sending the previous transaction. And the pairing with the soft slide, worth closing here: a soft constraint is the one the solver drops on its own when it gets in the way. constraint_mode(0) is the same thing asked for by hand and from the outside, and it is what is left when the constraint in the way is hard and the class is not yours.

see this slide in the deck ›

Constrained random

How a constraint that does not close gets debugged

// 1. Make the testbench say so. Without this there is no bug: there is silence
if (!t.randomize()) `uvm_fatal("RAND", "the constraint did not close")

// 2. Bisection: turn them off one at a time until it closes. The last one you turned off is it
foreach (nombres[i]) begin
   t.chico.constraint_mode(0);
   if (t.randomize()) `uvm_info("RAND", "it closes without 'chico'", UVM_LOW)
   t.chico.constraint_mode(1);
end

// 3. Take the field out of the draw and see which value was blocking it
t.A.rand_mode(0);

It is the half hour everybody loses once, and there is a way of losing it only once. It is worth saying the order out loud because it is the one that works. Step zero, and the one people skip: make it noisy. A randomize() without an if does not produce a visible bug, it produces stale stimulus. The course asks for it in all of code/ and the traps appendix has it catalogued; here is where it gets collected. Step one: bisection. A commercial simulator prints the set of conflicting constraints —Questa says Constraint solver failed and lists the blocks—, but Verilator returns 0 and says nothing, so bisection by hand is the tool there is. It is boring and takes two minutes, which is exactly why it is worth having the reflex instead of staring at the code. Step two, when bisection is not enough: the conflict is not between two constraints but between a constraint and a field that already has a value. There rand_mode(0) on the suspect freezes it and the message changes. And the design moral, which is the one that avoids the problem instead of solving it: if a base class defines hard constraints that a reasonable test is going to want to override, those constraints should have been soft. A hard constraint is a rule of the DUT, not a preference of whoever wrote the class first.

see this slide in the deck ›

Constrained random

This is how coverage gets closed

The closure cycle: randomize, read the report, fill the missing bin, another seed

Close by going back to the conventional testbench: the 86.8 % of u2/convencional comes out of 1000 random operations with the bias to the edges put in by hand. Now the student knows how to write that same bias in a constraint, measure it, and add the with {} for the cases that are missing. And leave the day 6 question hanging: if the stimulus is constraints and not code, what does a tester class with a run_phase go on existing for? It does not go on existing. It is called a sequence.

see this slide in the deck ›

Constrained random

"Another seed, and again": what it moves and what it does not

$ SEED=7 bash run.sh    # u6/transactions/constraints, 400 randomizations
    seed: 7
  :/   A=00  25.0%   A=FF  22.0%   # with SEED=8: 20.8% and 27.0%
$ SEED=7 bash run.sh    # u2/convencional, and again with SEED=8
  covergroup : 86.8% (66/76)       # both times. And the merge, the same

This slide exists to defuse a misunderstanding the previous cycle invites: "if I do not close, I run another seed". It was measured, and no: code/u2/convencional gives 66 out of 76 with the default seed, with 7 and with 8, and the merge of the three gives 66. The 10 that are missing are not missing out of luck — they are missing because Verilator generates the enum's automatic bins over the base type: 3'b110 does not exist in operation_t, so auto_5 stays at 0 forever, with its nine crosses. No seed is going to touch them. The seed is good for two other things, and both are matters of craft. First: reproducing. An intermittent bug does not get debugged, it gets repeated; and to repeat it you have to know which seed it came up with, which is exactly why run_sim prints it on every run. Second: accumulating, when the stimulus has not saturated yet. A real regression is the same test over a hundred seeds overnight, and the coverage gets merged — verilator_coverage --write does in Verilator what the ucdb merge does in Questa. Here it does not show because the example is small; on a chip it is half the work. And the +verilator+rand+reset+2 that shows up in the documentation is something else and it is worth not mixing them up: it does not touch randomize(), it decides what the uninitialized signals of the DUT start with. With 2 they start at random instead of at zero, which is the way to discover the register nobody reset and that in silicon does not start at zero.

see this slide in the deck ›

Constrained random

Summary of the unit

The last bullet is the one to leave burned in and the one that connects with the appendix of the traps. An unchecked randomize() does not break: it leaves the fields with the previous value and the testbench goes on sending stimulus that is not the one you think. The coverage is going to tell you three days later. And the tool notice that is needed from today: without z3 installed, Verilator resolves randomize() by returning 0 in silence. It is in docs/en/verilator.md, and it is the reason day 5 asks for the solver.

see this slide in the deck ›

Review · Day 5

1 of 8 · Deep copy

obj1_h = obj2_h. What did I copy?

Nothing — there is no copy, there is a second name for the same object. If you modify it through one handle, the other one sees it. Out of that comes the MOOCOW rule: if you are going to modify, copy first.

see this slide in the deck ›

Review · Day 5

2 of 8 · super.do_copy()

Why does every do_copy() in the hierarchy have to call super.do_copy()?

Otherwise the fields above get lost — it is the same problem as convert2string(): the day somebody puts a new level in the middle, the method stops seeing half the data and nobody warns you.

see this slide in the deck ›

Review · Day 5

3 of 8 · clone()

clone() returns a uvm_object. Why is it recommended to also write a clone_me()?

So as not to repeat the $cast — without it, every place that clones ends up writing its own cast. It is the same idea as always: if you are going to repeat it, wrap it.

see this slide in the deck ›

Review · Day 5

4 of 8 · dist: := against :/

A dist {8'h00 := 1, [8'h01:8'hFE] := 1, 8'hFF := 1}; — how often does A = 8'h00 come out?

1 in 256 — := gives weight 1 to each one of the 254 values in the middle, so the range weighs 254 against the 1 and 1 of the edges. To spread the weight inside the range you need :/. Both spellings compile and run: the difference only shows up in the coverage that does not go up.

see this slide in the deck ›

Review · Day 5

5 of 8 · randomize() with {}

cmd.randomize() with { A == 8'hFF; } — what happens to the constraints the class already had?

They add up — the with {} adds constraints for that call only, and erases nothing. That is why it is the tool for closing a bin without writing a new class: three lines at the point of use, and the rest of the stimulus stays what it always was. And that is also why it clashes with a dist that already biases the same field: both have to hold at once.

see this slide in the deck ›

Review · Day 5

6 of 8 · constraint_mode()

randomize() with { A == 8'hFF; } on a field the class spreads with a dist returns 0 three times out of four. What is the workaround?

Switch off the constraint that gets in the way — Verilator solves the dist by picking a value first and only then checking the rest, so the probability of success is the probability of the bin: measured, with {A == 8'hFF} solves 25 % of the time. constraint_mode(0) switches it off for that object alone and touches nobody else. It is exactly the line the d5c exercise asks for.

see this slide in the deck ›

Review · Day 5

7 of 8 · rand_mode()

What does cmd.A.rand_mode(0) do?

It stops being rand — they are the two run-time knobs and they get mixed up often: rand_mode(0) takes a field out of the draw, constraint_mode(0) switches off a constraint. One picks what gets drawn, the other which rules hold. It is what you use to pin one operand by hand and go on randomizing the rest.

see this slide in the deck ›

Review · Day 5

8 of 8 · The order of resolution

rand bit es_reset; rand byte unsigned A; with constraint c { es_reset -> A == 8'h00; }. How often does es_reset = 1 come out?

1 in 257 — the solver picks uniformly among the solutions, not among the values of each field: es_reset=1 leaves a single combination (A = 00) and es_reset=0 leaves 256. The "any operation after a reset" bin of the plan does not get filled, and the report does not say why. The answer in the language is solve es_reset before A; Verilator accepts it and does not honour it, so the portable workaround is to ask for the spread of the control field with a dist.

see this slide in the deck ›

Exercise · Day 5 · 1 of 3

The scoreboard screams and the DUT is healthy

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

It is the exercise that looks most like a day at work: the scoreboard screams and you have to decide who to believe. Let them start without the hint. Almost all of them are going to look at the RTL first — that is already half the lesson. When they get stuck, the hint is the verbosity: the uvm_info of the monitor are UVM_HIGH and they show what the monitor says it saw, which does not match what the scoreboard compares. The bug is a single line of command_monitor.svh: the monitor copies one of the two operands wrong.

see this slide in the deck ›

Exercise · Day 5 · 2 of 3

Measure your dist

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

It is the short exercise of the course and the one to leave for whoever ran out of time: it compiles and runs on its own, without UVM. The point is in the order: it gets run before touching anything. The file says 10, 80 and 10, the output says 0.1 % on the edges, and there the question of the section appears on its own — if the numbers are right, what is wrong? The operator is wrong, and there is no warning to say so. It is worth asking them to run it two or three times with different SEED= before calling it closed: with 4000 samples the 10 % bucket moves a good point between runs, between 9 and 11 — that is why the checker accepts ±2. A measured percentage is a sample, not the distribution — which is the other half of the rule of the section. An infrastructure detail worth mentioning if somebody asks why a program with no DUT takes twenty seconds: Verilator resolves every randomize() that has constraints by calling z3 from outside, so that is 4000 calls to an external process. And if they do not have it installed, randomize() returns 0 and every bucket gives zero — another one that does not break, it lies.

see this slide in the deck ›

Exercise · Day 5 · 3 of 3

Closing a bin

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

It is the exercise the course was missing, because coverage closure shows up twice as a noun and never as a verb. Here the student does the whole cycle: run, look at which bin was left empty, write three lines, run again. The trap is set on purpose and it is the one from constrained random: command_transaction has a dist on A and on B, and Verilator resolves the dist by choosing a value before looking at the with. With the distribution active, with {A == 8'hFF} resolves one out of every four times and the rest returns 0. Whoever does not read the hint is going to see an exercise that sometimes passes and sometimes does not — which is exactly the symptom the section describes. The workaround is constraint_mode(0), and it is worth defending it as a design decision and not as a patch for the simulator: a directed case does not want a distribution of probabilities, it wants a value. The seed of the run.sh is fixed. Without fixing it, some runs would fill the bin on their own with the 60 random ones — and that is the exercise day 7 starts with.

see this slide in the deck ›