Verify This! · the course to read

Day 2The OOP UVM takes for granted

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

Thirty seconds, and they are not filler: day 2 is the most abstract of the course and it starts without touching the VTALU all morning. The student has to walk in knowing this is the answer to something that already happened to them yesterday. The bullet to underline is the third one, because it is the problem the whole unit solves. If the group comes from a previous class, it is enough to ask "how did you make a tester that only multiplies yesterday?" and wait for the answer —copy and delete— before moving on to the agenda. If it is taught on video, this is the slide that replaces the week of distance between one chapter and the next.

see this slide in the deck ›

Agenda

Day 2 · ≈ 4 h · unit 3 · the OOP UVM takes for granted

By the end of the day you can:

Day 2 is the only one that does not touch the VTALU all morning, and that is why it is worth putting the three verbs up front: they are the answer to "what is this for?", which is the question that shows up half an hour in. The third one is the one that closes the day and the one practised in the exercise. The first two are rows 6 and 7 of the closing self-assessment.

see this slide in the deck ›

Classes and extensions

Why OOP in a testbench?

Here is the real wall of the course, and it is not UVM: it is OOP. Whoever arrives from RTL has never written a class, and from the tests on everything is classes. If the group comes from software, this unit goes fast and you gain time for day 3. If it comes from RTL, it is the unit where you spend whatever time is left over, even if you have to take it from somewhere else. The question that orders everything: does this exist all the time or does it show up and leave? The first is a module; the second, an object.

see this slide in the deck ›

Classes and extensions

What a struct cannot do

code/u3/clases/structs.sv
typedef struct {
   int length;
   int width;
} rectangle_struct;

typedef struct {int side;} square_struct;

module top_struct;
   rectangle_struct rectangle_s;
   square_struct    square_s;
   initial begin
      rectangle_s.length = 50;
      rectangle_s.width = 20;
      $display("rectangle area: %0d", rectangle_s.length * rectangle_s.width);
      square_s.side = 50;
      $display("square area: %0d", square_s.side ** 2);
   end
endmodule

It is worth starting by asking what is wrong with the struct, because the spontaneous answer is "nothing" — and they are right: for holding three fields it works perfectly. The course is not here to say the struct is wrong, it is here to say where it falls short. The three bullets are three different things and it is worth separating them. The first one is cohesion: the data is here and the operation on the data is in another file, so nothing forces them to agree. The second is inheritance: a square is a rectangle and the language has no way of saying so, so it gets copied and pasted. The third is the one you pay for on day 5: a command_s does not know how to randomize itself or compare itself against itself, and all of that has to be written outside, once for every place that uses it. The honest punchline, so that nobody leaves with a religious idea: the command_s of the conventional testbench is a struct, it works, and it was fine while the testbench was a module. What changes is the scale.

see this slide in the deck ›

Classes and extensions

The first class: data, methods and constructor

code/u3/clases/rectangle_only.sv
class rectangle;
   int length;
   int width;

   function new(int l, int w);
      length = l;
      width = w;
   endfunction

   function int area;
      return length * width;
   endfunction
endclass : rectangle

module top_rectangle;

   rectangle rectangle_h;

   initial begin

      rectangle_h = new(.l(50), .w(20));

      $display("rectangle area: %0d", rectangle_h.area());

   end
endmodule : top_rectangle

The comparison with the previous slide is the whole class: the same two fields, plus a function that used to live loose. Worth pointing at area() and saying that that is the entire difference — the data and what gets done with the data travel together. new() is worth presenting without mystery: it is an ordinary function, with a reserved name, that the simulator calls on its own when the object gets created. It returns the handle. It does nothing magic and there is no destructor on the other side. Passing by name looks cosmetic and it is not: when the constructor has five arguments —and uvm_component's has two that are always the same— .l(50) is what makes the call readable without going to look up the signature. The whole of code/ in the course is written that way. And the question to leave hanging, the one the next slide answers: if I declare rectangle r; and I never call new(), how many objects are there?

see this slide in the deck ›

Classes and extensions

A handle and an object are not the same thing

rectangle rectangle_h;               // 1. a HANDLE. No object yet: it is null
rectangle_h = new(.l(50), .w(20));   // 2. new() creates the OBJECT and returns its handle

The null pointer is THE mistake of the first week, and Verilator's message does not help: it tells you that you dereferenced null, not where the new() was missing. It is worth writing the two lines on the board and asking how many objects there are after each one. The answer is zero and one. This comes back twice more in the course: in the class hierarchies, when obj1_h = obj2_h copies the handle and not the object; and in the tests, when the virtual interface that was not read from the config_db is left at null.

see this slide in the deck ›

Classes and extensions

Extending: extends and super

code/u3/clases/classes.sv · #rectangle-and-square
class rectangle;
   // Data members
   int length;
   int width;

   function new(int l, int w);
      length = l;
      width = w;
   endfunction

   function int area();
      return length * width;
   endfunction
endclass

// Extending classes!
class square extends rectangle;
   // Override Constructor
   function new(int side);
      // super: the call to the parent method
      super.new(.l(side), .w(side));
   endfunction

endclass
el archivo entero, 48 líneas
// Memory Management: creating an object and passing it around the TB means
//                    allocating memory and sharing it between threads
//                    The simulator treats a class differently from a struct
//                    It allocates the memory of a struct as soon as it sees it,
//                    while for classes that has to be done with new()
class rectangle;
   // Data members
   int length;
   int width;

   function new(int l, int w);
      length = l;
      width = w;
   endfunction

   function int area();
      return length * width;
   endfunction
endclass

// Extending classes!
class square extends rectangle;
   // Override Constructor
   function new(int side);
      // super: the call to the parent method
      super.new(.l(side), .w(side));
   endfunction

endclass

module top_class;
   //Handle: like a pointer, but arithmetic on it is not allowed
   rectangle rectangle_h;
   square    square_h;

   initial begin

      rectangle_h = new(.l(50), .w(20));
      $display("rectangle area: %0d", rectangle_h.area());

      square_h = new(.side(50));
      $display("square area: %0d", square_h.area());

      // Since a square is a particular case of a rectangle:
      // can rectangle_h point to a square? Yes, polymorphism

   end
endmodule

The constructor order is the first thing to nail down, because it is mandatory and it fails badly: super.new() goes before anything else of the child. The reason is physical — the object is a single one, and the part the base class contributes has to be built before touching anything. If you forget it, SystemVerilog calls it on its own when the base constructor takes no arguments, and it does not compile when it does take them. On day 3, uvm_component's takes them. The last bullet is the whole argument of the unit and it is worth saying with an example: if area() changes tomorrow, in the struct version you have to remember both places; here it changes in one and the other finds out on its own. That "finds out on its own" is what inheritance buys you, and it is the same promise the env makes to the tests on day 3. And the warning about where this is heading, so that the word does not turn up cold on the next slide: inheriting lets the child use what belongs to the parent. What still cannot be done is for the parent to call the child's version — that is polymorphism, and it is the section that follows.

see this slide in the deck ›

Classes and extensions

Where all of this comes back

What you have just seen What it is called in UVM
class with data and methods uvm_object — the transactions of day 5
class that also lives in the TB tree uvm_component — driver, monitor, scoreboard
new() the constructor, with name (and parent if it is a component)
extends every test, every tester, every agent of the course
super.new() the mandatory first line of every UVM constructor

Close with the table and not with the example: the student has to leave knowing that this was not a programming detour, it was the vocabulary of the rest of the course. The last bullet leaves the door open to polymorphism, which is the only thing missing before a factory can be read. And one single thing more before moving on: declaring a handle creates nothing. Until the new() it is null, and using it there is the mistake that repeats the most in the whole week. It comes back three times —here, in the tests with the virtual interface that was not read from the config_db, and in the hierarchies when obj1_h = obj2_h copies the handle and not the object— so it is worth naming it all three.

see this slide in the deck ›

Polymorphism

A trago variable, a fernet object: which servir() runs?

trago, fernet and mojito: which servir() lives in which class

It is the concept that holds up everything that comes: the factory, the overrides, the transactions. Whoever gets lost here loses the rest of the course, so it is not worth moving on until everybody sees why the variable can be of the base class. It is worth taking a vote before showing the code: "what does it print?". Half the room says "Fernet", and seeing that they are wrong is what fixes the concept. And warn about it before they ask: in SystemVerilog virtual is overloaded — virtual method, virtual class and virtual interface are three different things. Here it is the first one.

see this slide in the deck ›

Polymorphism

The three classes: a trago that does not know how to serve itself

code/u3/polimorfismo/01-sin-virtual/not_virtual.sv · #three-classes
class trago;
   int hielos = -1;

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

   function void servir();
      $fatal(1, "A generic trago cannot be served: order a real one.");
   endfunction : servir

endclass : trago

class fernet extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Fernet: 70/30, and the coke last");
   endfunction : servir

endclass : fernet
el archivo entero, 66 líneas
// Basic polymorphism, WITHOUT virtual: the case that fails. See the polymorphism section.
class trago;
   int hielos = -1;

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

   function void servir();
      $fatal(1, "A generic trago cannot be served: order a real one.");
   endfunction : servir

endclass : trago

class fernet extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Fernet: 70/30, and the coke last");
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Mojito: mint, lime and crushed ice");
   endfunction : servir

endclass : mojito

module top;

   initial begin

      fernet fernet_h;
      mojito mojito_h;
      trago  trago_h;

      fernet_h = new(15);
      fernet_h.servir();
      $display("The fernet has %0d ice cubes", fernet_h.hielos);

      mojito_h = new(1);
      mojito_h.servir();
      $display("The mojito has %0d ice cubes", mojito_h.hielos);

      // The variable is trago, the object is fernet.
      trago_h = fernet_h;
      trago_h.servir();  // ** Fatal: A generic trago cannot be served
      $display("The trago has %0d ice cubes", trago_h.hielos);

      trago_h = mojito_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

   end  // initial begin

endmodule : top

The $fatal in the base class is a pattern you see a lot and that in this same section we are going to replace with something better: if the method cannot be implemented here, the right thing is not to explode in simulation, it is not to let it compile. That the three classes have a method with the same name and the same signature is what makes everything that follows possible. Now it remains to decide which one runs, and that is not yet said anywhere in the code. The whole file is in code/u3/polimorfismo/01-sin-virtual/not_virtual.sv.

see this slide in the deck ›

Polymorphism

Without virtual the variable rules, and that breaks

code/u3/polimorfismo/01-sin-virtual/not_virtual.sv · #the-calls
      fernet_h = new(15);
      fernet_h.servir();
      $display("The fernet has %0d ice cubes", fernet_h.hielos);

      mojito_h = new(1);
      mojito_h.servir();
      $display("The mojito has %0d ice cubes", mojito_h.hielos);

      // The variable is trago, the object is fernet.
      trago_h = fernet_h;
      trago_h.servir();  // ** Fatal: A generic trago cannot be served
      $display("The trago has %0d ice cubes", trago_h.hielos);

      trago_h = mojito_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);
el archivo entero, 66 líneas
// Basic polymorphism, WITHOUT virtual: the case that fails. See the polymorphism section.
class trago;
   int hielos = -1;

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

   function void servir();
      $fatal(1, "A generic trago cannot be served: order a real one.");
   endfunction : servir

endclass : trago

class fernet extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Fernet: 70/30, and the coke last");
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Mojito: mint, lime and crushed ice");
   endfunction : servir

endclass : mojito

module top;

   initial begin

      fernet fernet_h;
      mojito mojito_h;
      trago  trago_h;

      fernet_h = new(15);
      fernet_h.servir();
      $display("The fernet has %0d ice cubes", fernet_h.hielos);

      mojito_h = new(1);
      mojito_h.servir();
      $display("The mojito has %0d ice cubes", mojito_h.hielos);

      // The variable is trago, the object is fernet.
      trago_h = fernet_h;
      trago_h.servir();  // ** Fatal: A generic trago cannot be served
      $display("The trago has %0d ice cubes", trago_h.hielos);

      trago_h = mojito_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

   end  // initial begin

endmodule : top

This is worth running: bash code/u3/polimorfismo/01-sin-virtual/run.sh. The example ends in $fatal on purpose, and the run.sh gives PASS precisely when that message shows up. The question that always comes: "and why does hielos come out right?". Because fields are never virtual: the variable sees the fields of its own type, and hielos is in trago. Methods are the only thing that can be resolved by object, and only if you ask for it.

see this slide in the deck ›

Polymorphism

Virtual methods: one word, and now the object rules

code/u3/polimorfismo/02-virtual/virtual.sv · #trago.servir
   virtual function void servir();
      $fatal(1, "A generic trago cannot be served: order a real one.");
   endfunction : servir
el archivo entero, 67 líneas
// Identical to 01-sin-virtual except for ONE word: trago's servir() is
// now virtual. See the polymorphism section.

class trago;
   int hielos = -1;

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

   virtual function void servir();
      $fatal(1, "A generic trago cannot be served: order a real one.");
   endfunction : servir

endclass : trago

class fernet extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Fernet: 70/30, and the coke last");
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Mojito: mint, lime and crushed ice");
   endfunction : servir

endclass : mojito

module top;

   initial begin

      fernet fernet_h;
      mojito mojito_h;
      trago trago_h;

      fernet_h = new(15);
      fernet_h.servir();
      $display("The fernet has %0d ice cubes", fernet_h.hielos);

      mojito_h = new(1);
      mojito_h.servir();
      $display("The mojito has %0d ice cubes", mojito_h.hielos);

      trago_h = fernet_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

      trago_h = mojito_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

   end  // initial begin

endmodule : top

Worth running the two examples one after the other and showing the diff: one word. A detail that always gets asked: fernet and mojito do not write virtual on their servir(), and they are virtual all the same. Once the method is virtual in the base, it is virtual for all of its descendants. Writing it anyway does no harm and plenty of people do it out of tidiness. The rule of thumb for the rest of the course: in verification, every method you might want to redefine goes virtual. UVM does it that way — build_phase, run_phase, do_copy, do_compare are all virtual, and that is why the override of the env works. The cost exists and it is real —a method table and an indirect jump— but in a testbench you are never going to measure it. In synthesizable RTL there are no classes, so the discussion does not apply.

see this slide in the deck ›

Polymorphism

Abstract classes: moving the error to compile time

This is the slide to leave clear, because the criterion repeats through the whole course: the sooner it fails, the better. Compile time better than simulation, simulation better than silicon. In verification a compile error is good news: the overnight regression does not get lost, and whoever broke something finds out in a minute. Where it is going to show up again: uvm_object and uvm_component are declared virtual class in the library, so they are abstract by right. What `uvm_component_utils does **not** do is force you to implement anything: the other way round, it *provides* get_type_name() and the registry's type_id. The real pure virtual in UVM is uvm_subscriber::write(), which you are going to have to write no matter what on day 4. And in the day 3 exercise, the virtual class base_tester.

see this slide in the deck ›

Polymorphism

pure virtual: the same example, with no $fatal possible

code/u3/polimorfismo/03-virtual-pura/pure_virtual.sv · #trago
// Abstract class: it only works as a base, it cannot be instantiated.
virtual class trago;
   int hielos = -1;

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

   // pure virtual: no body. Extending it forces you to redefine it.
   pure virtual function void servir();

endclass : trago
el archivo entero, 67 líneas
// Abstract class: it only works as a base, it cannot be instantiated.
virtual class trago;
   int hielos = -1;

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

   // pure virtual: no body. Extending it forces you to redefine it.
   pure virtual function void servir();

endclass : trago

class fernet extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   function void servir();
      $display("Fernet: 70/30, and the coke last");
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos);
      super.new(hielos);
   endfunction : new

   // Deleting this override does NOT compile -- which is exactly the point.
   function void servir();
      $display("Mojito: mint, lime and crushed ice");
   endfunction : servir

endclass : mojito

module top;

   initial begin

      fernet fernet_h;
      mojito mojito_h;
      trago trago_h;

      // trago_h = new(3);  <- does not compile: trago is abstract

      fernet_h = new(15);
      fernet_h.servir();
      $display("The fernet has %0d ice cubes", fernet_h.hielos);

      mojito_h = new(1);
      mojito_h.servir();
      $display("The mojito has %0d ice cubes", mojito_h.hielos);

      trago_h = fernet_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

      trago_h = mojito_h;
      trago_h.servir();
      $display("The trago has %0d ice cubes", trago_h.hielos);

   end  // initial begin

endmodule : top

The point is not the $fatal, it is when you find out: without an abstract class it explodes in the middle of the simulation, with pure virtual it does not compile. Worth doing live: comment out mojito's servir() and compile. The compiler message is the one that teaches. And the hook forward: the factory uses this to create, and the env for the overrides. What today is a fernet and a mojito, tomorrow is going to be a base_tester and the testers that extend it — same mechanics, another vocabulary.

see this slide in the deck ›

Polymorphism

Summary of the unit

It is the unit that unlocks the next half of the course, and it is worth saying so: without polymorphism there is no factory, there is no uvm_component that can serve as a base, and there is no override. Everything else is plumbing. Forgetting virtual is also the trap of the day 2 exercise —get_op() is declared without virtual on purpose—, so if somebody asks about it here, do not give it away: it is understood much better by suffering it.

see this slide in the deck ›

Static variables

The global variable you can actually defend

The question that orders the unit: what is there only one of in a testbench? The factory, the configuration database, the error counter. All of that in UVM is static, and that is why you write uvm_config_db#(T)::set(...) with a double colon and without instantiating anything. And the second half, which is the one you take to work: static solves the "where it lives", not the "who touches it". That one is solved by protected plus access methods, which is the third slide. A public static is a global variable with a longer prefix.

see this slide in the deck ›

Static variables

code/u3/estaticas/01-variables/static_variables.sv · #tray-and-top
class bandeja_de_fernet;
   // Static queue
   static fernet vasos[$];

endclass : bandeja_de_fernet

module top;
   initial begin
      fernet fernet_h;
      fernet_h = new(2, "the one at table 7");
      // :: operator
      //----------------
      //   The variable in the bandeja_de_fernet class is reached by
      //   naming the class and putting the :: operator before the name
      //   That tells the compiler we want a static variable inside
      //   the namespace of the class
      bandeja_de_fernet::vasos.push_back(fernet_h);
      fernet_h = new(3, "the one at table 4");
      bandeja_de_fernet::vasos.push_back(fernet_h);
      fernet_h = new(15, "the one at the bar");

      // class::static_variable. You know right away where it is declared
      bandeja_de_fernet::vasos.push_back(fernet_h);

      $display("Fernets on the tray:");
      foreach (bandeja_de_fernet::vasos[i]) $display(bandeja_de_fernet::vasos[i].get_name());
   end
endmodule : top
el archivo entero, 72 líneas
// Static variables
virtual class trago;
   protected int hielos = -1;

   function new(int hielos);
      set_hielos(hielos);
   endfunction : new

   function void set_hielos(int a);
      hielos = a;
   endfunction : set_hielos

   function int get_hielos();
      if (hielos == -1) $fatal(1, "You didn't set the hielos.");
      else return hielos;
   endfunction : get_hielos

   pure virtual function void servir();

endclass : trago

class fernet extends trago;
   protected string name;

   function new(int hielos, string n);
      super.new(hielos);
      name = n;
   endfunction : new

   function void servir();
      $display("%s: 70/30, and the coke last", get_name());
   endfunction : servir

   function string get_name();
      return name;
   endfunction : get_name

endclass : fernet

// Static Variable to handle global var
// There is only one copy of a static variable in memory,
// no matter how many instances of the class exist
// They are global variables created in a controlled way

class bandeja_de_fernet;
   // Static queue
   static fernet vasos[$];

endclass : bandeja_de_fernet

module top;
   initial begin
      fernet fernet_h;
      fernet_h = new(2, "the one at table 7");
      // :: operator
      //----------------
      //   The variable in the bandeja_de_fernet class is reached by
      //   naming the class and putting the :: operator before the name
      //   That tells the compiler we want a static variable inside
      //   the namespace of the class
      bandeja_de_fernet::vasos.push_back(fernet_h);
      fernet_h = new(3, "the one at table 4");
      bandeja_de_fernet::vasos.push_back(fernet_h);
      fernet_h = new(15, "the one at the bar");

      // class::static_variable. You know right away where it is declared
      bandeja_de_fernet::vasos.push_back(fernet_h);

      $display("Fernets on the tray:");
      foreach (bandeja_de_fernet::vasos[i]) $display(bandeja_de_fernet::vasos[i].get_name());
   end
endmodule : top

Put plainly: static is the global variable you can actually defend in a code review. It lives in the class, not in the object. The second slide shows why it has to be encapsulated with static methods: if you leave it public, the day you swap the queue for another structure you have to touch the whole testbench.

see this slide in the deck ›

Static variables

code/u3/estaticas/01-variables/ejemplo2.sv
class pepe;
   static    int    cant;
   int    val = -1;

   function new(int a);
      // Static Variable
      cant++;
      val = a;
   endfunction : new

endclass : pepe

//----------------------------------------
module top_pepes;
   initial begin
      pepe pepe1_h, pepe2_h;

      pepe1_h = new(10);
      pepe2_h = new(254654);

      $display("PEPE1 VAL = %0d", pepe1_h.val);
      $display("PEPE2 VAL = %0d", pepe2_h.val);

      // There is only one copy of a static variable in memory,
      // no matter how many instances of the class exist
      $display("NUMBER OF PEPES = %0d", pepe::cant);

   end
endmodule : top_pepes

And what about encapsulation?

pepe::cant counts how many objects got created, and the detail to point out is that the counter exists even if there is not one object. A static does not wait for the new(): it is there from the moment the simulation starts. That is why it works for counting instances, and that is why it cannot be initialized with anything that depends on an object. A good question to throw out: if cant gets incremented in the constructor, what happens if somebody extends pepe and forgets the super.new()? It does not count. It is the same topic as classes and extensions seen from another side. The question on the slide is on purpose: cant is public, and anybody in the testbench can write it. Leaving that discomfort hanging in the air for a while is what makes the next slide make sense.

see this slide in the deck ›

Static variables

Static methods

code/u3/estaticas/02-metodos/static_methods.sv · #tray-and-top
class bandeja_de_fernet;

   protected static fernet vasos[$];

   static function void bandeja_fernet(fernet l);
      vasos.push_back(l);
   endfunction : bandeja_fernet

   static function void lista_fernets();
      $display("Fernets on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_fernets

endclass : bandeja_de_fernet

module top;

   initial begin
      fernet fernet_h;
      fernet_h = new(2, "the one at table 7");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      fernet_h = new(3, "the one at table 4");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      fernet_h = new(15, "the one at the bar");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      bandeja_de_fernet::lista_fernets();
   end

endmodule : top
el archivo entero, 69 líneas
// Static methods
virtual class trago;
   protected int hielos = -1;

   function new(int hielos);
      set_hielos(hielos);
   endfunction : new

   function void set_hielos(int a);
      hielos = a;
   endfunction : set_hielos

   function int get_hielos();
      if (hielos == -1) $fatal(1, "You didn't set the hielos.");
      else return hielos;
   endfunction : get_hielos

   pure virtual function void servir();

endclass : trago

class fernet extends trago;

   protected string name;

   function new(int hielos, string n);
      super.new(hielos);
      name = n;
   endfunction : new

   function void servir();
      $display("%s: 70/30, and the coke last", get_name());
   endfunction : servir

   function string get_name();
      return name;
   endfunction : get_name

endclass : fernet

class bandeja_de_fernet;

   protected static fernet vasos[$];

   static function void bandeja_fernet(fernet l);
      vasos.push_back(l);
   endfunction : bandeja_fernet

   static function void lista_fernets();
      $display("Fernets on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_fernets

endclass : bandeja_de_fernet

module top;

   initial begin
      fernet fernet_h;
      fernet_h = new(2, "the one at table 7");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      fernet_h = new(3, "the one at table 4");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      fernet_h = new(15, "the one at the bar");
      bandeja_de_fernet::bandeja_fernet(fernet_h);
      bandeja_de_fernet::lista_fernets();
   end

endmodule : top

The difference between the two versions is two words —protected in front of the queue and static function on the accessors— and it changes who can break what. In the first one, any line of the testbench can push_back onto the queue; in this one, only bandeja_fernet(). The argument is not purism: it is that now vasos can be swapped for an associative array, for a uvm_tlm_fifo or for whatever, without going out to fix the whole testbench. Encapsulating is being able to change your mind later. A syntax detail that confuses: a static method cannot touch instance variables, only static ones. It makes sense —it gets called without an object, so there is no this—, but the simulator's error message does not put it that way. And the hook: UVM's type_id::create(), which they are going to write a hundred times from the env on, is exactly this — a static access method to a static structure.

see this slide in the deck ›

Static variables

Summary of the unit

The hook worth leaving served: uvm_config_db#(T)::set(...) gets written with a double colon and without instantiating anything, and now they know why. Same for type_id::create(), which they are going to write a hundred times from the env on — it is exactly a static access method to a static structure. The question that orders the unit and works as a review: what is there only one of in a testbench? The factory, the configuration database, the error counter. All of that is static, and not by accident.

see this slide in the deck ›

Parameterized classes

It is not a class: it is a mould

Starting from Verilog's parameter is deliberate: the RTL student already knows how to parameterize, only with widths. Here the parameter is a type, and the rest is the same. What has to be left crystal clear before moving on is the third bullet, because it is question 6 of the review and because it is the base of all of UVM: bandeja#(fernet) and bandeja#(mojito) are not the same class with a different field. SystemVerilog generates a whole class for every combination of parameters, at compile time. That is why each one has its own static queue. The short way of saying it: the parameterized class is not a class, it is the recipe for manufacturing classes. The class appears when you write the #(...). And a limit worth naming now so that it does not surprise them: since it gets resolved at compile time, the type has to be known there. You cannot pick the parameter at runtime. Picking at runtime is what the factory solves, which is the unit that follows.

see this slide in the deck ›

Parameterized classes

code/u3/parametricas/01-memoria/pres-ch8.sv
module RAM #(
    awidth,
    dwidth
) (
    input wire [awidth-1:0] address,
    inout wire [dwidth-1:0] data,
    input we
);

   initial $display("awidth: %0d  dwidth %0d", awidth, dwidth);
   // code to implement RAM

   //...

endmodule  // RAM

module top;
   wire [ 7:0] address;
   wire [15:0] data;
   wire        write_enable;
   RAM #(
       .awidth(8),
       .dwidth(16)
   ) my_ram (
       address,
       data,
       write_enable
   );
endmodule  // top

It is worth saying what we are going to use them for, otherwise it looks like a loose OOP topic: uvm_analysis_port#(T), uvm_subscriber#(T), uvm_tlm_fifo#(T). Every #(...) that shows up in the rest of the course is this.

see this slide in the deck ›

Parameterized classes

code/u3/parametricas/02-estatica/bandejas.sv · #tray-and-top
// Parameterized class
// the T (type) parameter is what says which
// kind of queue this is.
class bandeja #(
    type T
);

   protected static T vasos[$];

   static function void bandeja_trago(T l);
      vasos.push_back(l);
   endfunction : bandeja_trago

   static function void lista_tragos();
      $display("Drinks on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_tragos

endclass : bandeja

module top;

   initial begin
      fernet fernet_h;
      mojito mojito_h;
      fernet_h = new(15, "the one at the bar");
      bandeja#(fernet)::bandeja_trago(fernet_h);
      fernet_h = new(15, "the one at table 4");
      bandeja#(fernet)::bandeja_trago(fernet_h);

      mojito_h = new(1, "the one at table 7");
      bandeja#(mojito)::bandeja_trago(mojito_h);
      mojito_h = new(1, "the one at the counter");
      bandeja#(mojito)::bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja#(fernet)::lista_tragos();
      $display("-- Mojitos --");
      bandeja#(mojito)::lista_tragos();
   end

endmodule : top
el archivo entero, 89 líneas
// Static Class version

virtual class trago;
   protected int hielos = -1;
   protected string name;

   function new(int a, string n);
      hielos = a;
      name = n;
   endfunction : new

   function int get_hielos();
      return hielos;
   endfunction : get_hielos

   function string get_name();
      return name;
   endfunction : get_name

   pure virtual function void servir();

endclass : trago

class fernet extends trago;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The fernet %s: 70/30, and the coke last", get_name());
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The mojito %s: mint, lime and crushed ice", get_name());
   endfunction : servir

endclass : mojito

// Parameterized class
// the T (type) parameter is what says which
// kind of queue this is.
class bandeja #(
    type T
);

   protected static T vasos[$];

   static function void bandeja_trago(T l);
      vasos.push_back(l);
   endfunction : bandeja_trago

   static function void lista_tragos();
      $display("Drinks on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_tragos

endclass : bandeja

module top;

   initial begin
      fernet fernet_h;
      mojito mojito_h;
      fernet_h = new(15, "the one at the bar");
      bandeja#(fernet)::bandeja_trago(fernet_h);
      fernet_h = new(15, "the one at table 4");
      bandeja#(fernet)::bandeja_trago(fernet_h);

      mojito_h = new(1, "the one at table 7");
      bandeja#(mojito)::bandeja_trago(mojito_h);
      mojito_h = new(1, "the one at the counter");
      bandeja#(mojito)::bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja#(fernet)::lista_tragos();
      $display("-- Mojitos --");
      bandeja#(mojito)::lista_tragos();
   end

endmodule : top

This is the slide where the mould thing gets demonstrated, and it is worth doing it by running the example: two fernets and two mojitos go in, and each lista_tragos() prints only its own. The queue is static and even so there are two. The question for the board: how many queues are there in memory? Two. And how many would there be with three drinks? Three. Every bandeja#(X) the compiler sees in the code generates its class, with its own static. The ones nobody uses do not exist. A detail that always gets asked: T is not declared anywhere as "trago or derivatives". SystemVerilog has no such constraint — the check is by use. If you put in a type that has no get_name(), the error shows up when compiling the specialization, not when declaring the class, and the message points inside bandeja. It is confusing the first time.

see this slide in the deck ›

Parameterized classes

Variables with parameters

code/u3/parametricas/03-instanciada/bandejas.sv · #top
module top;

   // Since bandeja is not static, it has to be instantiated
   // the parameter is a variable type
   // Using a type that does not have the get_name()
   // method is a syntax error
   fernet fernet_h;
   mojito mojito_h;

   bandeja #(fernet) bandeja_de_fernet;
   bandeja #(mojito) bandeja_de_mojito;

   initial begin
      bandeja_de_fernet = new();
      fernet_h = new(15, "the one at the bar");
      bandeja_de_fernet.bandeja_trago(fernet_h);
      fernet_h = new(15, "the one at table 4");
      bandeja_de_fernet.bandeja_trago(fernet_h);

      bandeja_de_mojito = new();
      mojito_h = new(1, "the one at table 7");
      bandeja_de_mojito.bandeja_trago(mojito_h);

      mojito_h = new(1, "the one on the sidewalk");
      bandeja_de_mojito.bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja_de_fernet.lista_tragos();
      $display("-- Mojitos --");
      bandeja_de_mojito.lista_tragos();
   end

endmodule : top
el archivo entero, 96 líneas
virtual class trago;
   protected int hielos = -1;
   protected string name;

   function new(int a, string n);
      hielos = a;
      name = n;
   endfunction : new

   function int get_hielos();
      return hielos;
   endfunction : get_hielos

   function string get_name();
      return name;
   endfunction : get_name

   pure virtual function void servir();

endclass : trago

class fernet extends trago;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The fernet %s: 70/30, and the coke last", get_name());
   endfunction : servir

endclass : fernet

class mojito extends trago;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The mojito %s: mint, lime and crushed ice", get_name());
   endfunction : servir

endclass : mojito

// bandeja is not static any more
class bandeja #(
    type T
);

   protected T vasos[$];

   function void bandeja_trago(T l);
      vasos.push_back(l);
   endfunction : bandeja_trago

   function void lista_tragos();
      $display("Drinks on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_tragos

endclass : bandeja

module top;

   // Since bandeja is not static, it has to be instantiated
   // the parameter is a variable type
   // Using a type that does not have the get_name()
   // method is a syntax error
   fernet fernet_h;
   mojito mojito_h;

   bandeja #(fernet) bandeja_de_fernet;
   bandeja #(mojito) bandeja_de_mojito;

   initial begin
      bandeja_de_fernet = new();
      fernet_h = new(15, "the one at the bar");
      bandeja_de_fernet.bandeja_trago(fernet_h);
      fernet_h = new(15, "the one at table 4");
      bandeja_de_fernet.bandeja_trago(fernet_h);

      bandeja_de_mojito = new();
      mojito_h = new(1, "the one at table 7");
      bandeja_de_mojito.bandeja_trago(mojito_h);

      mojito_h = new(1, "the one on the sidewalk");
      bandeja_de_mojito.bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja_de_fernet.lista_tragos();
      $display("-- Mojitos --");
      bandeja_de_mojito.lista_tragos();
   end

endmodule : top

The two versions do exactly the same thing and the difference is one of design, not of syntax: in the previous one the tray is global —anybody in the testbench sees it—; in this one you have to have the handle to be able to use it. The rule you take to work: static when there really is only one in the whole testbench, instantiated when there can be more than one. And when in doubt, instantiated: it is easier to add a second object than to take the static off something half the testbench already uses. With that in hand, place UVM: the uvm_config_db is static because there is only one. A uvm_analysis_port#(command_transaction) is instantiated because each monitor has its own. The student already has the criterion to read both.

see this slide in the deck ›

Parameterized classes

Summary of the unit

The bullet about the different classes is the one to leave nailed down, because it is the review question and because it is the base of all of UVM. bandeja#(fernet) and bandeja#(mojito) are not the same class with a different field: they are two whole classes, each one with its own static. And the last bullet places the section: the uvm_config_db is static because there is only one; a uvm_analysis_port#(command_transaction) is instantiated because each monitor has its own. With that they can already read both.

see this slide in the deck ›

The factory pattern

Who decides the type?

The sentence that orders the unit: whoever uses the object stops being the one who picks the type. Everything else is implementation detail. It is worth saying up front where this ends, because otherwise it looks like a programming detour: the +UVM_TESTNAME=add_test of the tests, tomorrow, is going to be a factory — UVM reads a string from the command line and builds the class that corresponds. Here you see the mechanism from the inside. And for whoever comes from software: yes, it is the GoF Factory Method, and we invented nothing. The Python version that closes the section is there precisely for that.

see this slide in the deck ›

The factory pattern

code/u3/factory/without-factory.sv
// ILLUSTRATION -- not compiled. In the previous example, new() gets called every
// time an object is needed: building a new set of drinks means editing the source.
bandeja_de_fernet = new();
fernet_h = new(15, "the one at the bar");
bandeja_de_fernet.bandeja_trago(fernet_h);
fernet_h = new(15, "the one at table 4");
bandeja_de_fernet.bandeja_trago(fernet_h);

bandeja_de_mojito = new();
mojito_h = new(1, "the one at table 7");
bandeja_de_mojito.bandeja_trago(mojito_h);

mojito_h = new(1, "the one on the sidewalk");
bandeja_de_mojito.bandeja_trago(mojito_h);

// Consider the case where a set of random drinks has to be read from a file
// dynamically.
//    --> Doing that with NON hardcoded code is impossible
//        You would need a script that generates the source, recompile it...

The idea is to create data of random types without changing the code.

The problem first, the pattern after: I want to change the type of object that gets created without touching the code that creates it. With that in your head, UVM's factory —the env— is the same idea wrapped in macros. The Python version is next to it on purpose: it shows whoever comes from software that we invented nothing.

see this slide in the deck ›

The factory pattern

cantina builds fernet and mojito, and returns a trago handle


The factory does not work without polymorphism, and it is worth saying it that way: if hacer_trago() could not return a fernet stored in a trago variable, there would be nothing to manufacture. They are the two halves of the same thing. The useful question about the diagram: what type is the thing hacer_trago() returns? trago, always. What changes is which object comes inside. The caller never finds out, and that is exactly the point. And the limit you see right away: if the caller needs something only fernet has —the sin_hielo of the example— the comfort is over and you have to cast. That is the next slide, and it is the reason UVM's factory is better than this one.

see this slide in the deck ›

The factory pattern

code/u3/factory/factory.sv · #cantina
//*********************************************************************
// Part of the factory Class!
//*********************************************************************
class cantina;

   // Static method that builds the drinks
   static function trago hacer_trago(string pedido, int hielos, string name);
      mojito nuevo_mojito;
      fernet nuevo_fernet;
      case (pedido)
         "fernet": begin
            nuevo_fernet = new(hielos, name);
            return nuevo_fernet;
         end

         "mojito": begin
            nuevo_mojito = new(hielos, name);
            return nuevo_mojito;
         end

         default: begin
            $fatal(1, {"No such drink: ", pedido});
            return null;
         end

      endcase  // case (pedido)

   endfunction : hacer_trago

endclass : cantina
el archivo entero, 134 líneas
// Example: the factory pattern
virtual class trago;
   protected int hielos = -1;
   protected string name;

   function new(int a, string n);
      hielos = a;
      name = n;
   endfunction : new

   function int get_hielos();
      return hielos;
   endfunction : get_hielos

   function string get_name();
      return name;
   endfunction : get_name

   pure virtual function void servir();
endclass : trago

class fernet extends trago;

   // This variable belongs to fernet, but does not exist in trago
   bit sin_hielo = 0;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The fernet %s: 70/30, and the coke last", get_name());
   endfunction : servir

endclass : fernet

class mojito extends trago;
   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The mojito %s: mint, lime and crushed ice", get_name());
   endfunction : servir
endclass : mojito

//*********************************************************************
// Part of the factory Class!
//*********************************************************************
class cantina;

   // Static method that builds the drinks
   static function trago hacer_trago(string pedido, int hielos, string name);
      mojito nuevo_mojito;
      fernet nuevo_fernet;
      case (pedido)
         "fernet": begin
            nuevo_fernet = new(hielos, name);
            return nuevo_fernet;
         end

         "mojito": begin
            nuevo_mojito = new(hielos, name);
            return nuevo_mojito;
         end

         default: begin
            $fatal(1, {"No such drink: ", pedido});
            return null;
         end

      endcase  // case (pedido)

   endfunction : hacer_trago

endclass : cantina

class bandeja #(
    type T = trago
);

   protected static T vasos[$];

   static function void bandeja_trago(T l);
      vasos.push_back(l);
   endfunction : bandeja_trago

   static function void lista_tragos();
      $display("Drinks on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_tragos

endclass : bandeja

module top;

   initial begin
      trago trago_h;
      fernet fernet_h;
      mojito mojito_h;

      // Using the factory!
      trago_h = cantina::hacer_trago("fernet", 15, "the one at the bar");
      trago_h.servir();

      // Reaching a member of fernet (derived from trago) through a trago variable
      // requires casting it to fernet.
      if (!$cast(fernet_h, trago_h)) $fatal(1, "Could not cast trago_h to fernet_h");

      if (fernet_h.sin_hielo) $display("And on top of that it comes warm!");
      bandeja#(fernet)::bandeja_trago(fernet_h);

      if (!$cast(fernet_h, cantina::hacer_trago("fernet", 2, "the one at table 4")))
         $fatal(1, "Could not cast the cantina drink to fernet_h");

      bandeja#(fernet)::bandeja_trago(fernet_h);

      if (!$cast(mojito_h, cantina::hacer_trago("mojito", 1, "the one at table 7")))
         $fatal(1, "Could not cast the cantina result to mojito_h");

      bandeja#(mojito)::bandeja_trago(mojito_h);

      if (!$cast(mojito_h, cantina::hacer_trago("mojito", 1, "the one on the sidewalk")))
         $fatal(1, "Could not cast the cantina result to mojito_h");

      bandeja#(mojito)::bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja#(fernet)::lista_tragos();
      $display("-- Mojitos --");
      bandeja#(mojito)::lista_tragos();
   end

endmodule : top
code/u3/factory/factory.sv · #casting
      // Using the factory!
      trago_h = cantina::hacer_trago("fernet", 15, "the one at the bar");
      trago_h.servir();

      // Reaching a member of fernet (derived from trago) through a trago variable
      // requires casting it to fernet.
      if (!$cast(fernet_h, trago_h)) $fatal(1, "Could not cast trago_h to fernet_h");

      if (fernet_h.sin_hielo) $display("And on top of that it comes warm!");
      bandeja#(fernet)::bandeja_trago(fernet_h);

      if (!$cast(fernet_h, cantina::hacer_trago("fernet", 2, "the one at table 4")))
         $fatal(1, "Could not cast the cantina drink to fernet_h");
el archivo entero, 134 líneas
// Example: the factory pattern
virtual class trago;
   protected int hielos = -1;
   protected string name;

   function new(int a, string n);
      hielos = a;
      name = n;
   endfunction : new

   function int get_hielos();
      return hielos;
   endfunction : get_hielos

   function string get_name();
      return name;
   endfunction : get_name

   pure virtual function void servir();
endclass : trago

class fernet extends trago;

   // This variable belongs to fernet, but does not exist in trago
   bit sin_hielo = 0;

   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The fernet %s: 70/30, and the coke last", get_name());
   endfunction : servir

endclass : fernet

class mojito extends trago;
   function new(int hielos, string n);
      super.new(hielos, n);
   endfunction : new

   function void servir();
      $display("The mojito %s: mint, lime and crushed ice", get_name());
   endfunction : servir
endclass : mojito

//*********************************************************************
// Part of the factory Class!
//*********************************************************************
class cantina;

   // Static method that builds the drinks
   static function trago hacer_trago(string pedido, int hielos, string name);
      mojito nuevo_mojito;
      fernet nuevo_fernet;
      case (pedido)
         "fernet": begin
            nuevo_fernet = new(hielos, name);
            return nuevo_fernet;
         end

         "mojito": begin
            nuevo_mojito = new(hielos, name);
            return nuevo_mojito;
         end

         default: begin
            $fatal(1, {"No such drink: ", pedido});
            return null;
         end

      endcase  // case (pedido)

   endfunction : hacer_trago

endclass : cantina

class bandeja #(
    type T = trago
);

   protected static T vasos[$];

   static function void bandeja_trago(T l);
      vasos.push_back(l);
   endfunction : bandeja_trago

   static function void lista_tragos();
      $display("Drinks on the tray:");
      foreach (vasos[i]) $display(vasos[i].get_name());
   endfunction : lista_tragos

endclass : bandeja

module top;

   initial begin
      trago trago_h;
      fernet fernet_h;
      mojito mojito_h;

      // Using the factory!
      trago_h = cantina::hacer_trago("fernet", 15, "the one at the bar");
      trago_h.servir();

      // Reaching a member of fernet (derived from trago) through a trago variable
      // requires casting it to fernet.
      if (!$cast(fernet_h, trago_h)) $fatal(1, "Could not cast trago_h to fernet_h");

      if (fernet_h.sin_hielo) $display("And on top of that it comes warm!");
      bandeja#(fernet)::bandeja_trago(fernet_h);

      if (!$cast(fernet_h, cantina::hacer_trago("fernet", 2, "the one at table 4")))
         $fatal(1, "Could not cast the cantina drink to fernet_h");

      bandeja#(fernet)::bandeja_trago(fernet_h);

      if (!$cast(mojito_h, cantina::hacer_trago("mojito", 1, "the one at table 7")))
         $fatal(1, "Could not cast the cantina result to mojito_h");

      bandeja#(mojito)::bandeja_trago(mojito_h);

      if (!$cast(mojito_h, cantina::hacer_trago("mojito", 1, "the one on the sidewalk")))
         $fatal(1, "Could not cast the cantina result to mojito_h");

      bandeja#(mojito)::bandeja_trago(mojito_h);

      $display("-- Fernets --");
      bandeja#(fernet)::lista_tragos();
      $display("-- Mojitos --");
      bandeja#(mojito)::lista_tragos();
   end

endmodule : top

Two things about this code, and both come back in UVM. The first: $cast checks at runtime and returns 0, it does not abort. That is why in the example every call goes inside an if (!$cast(...)) $fatal(...). A $cast without a check is the assert(randomize()) of the transactions in another disguise: if it fails, you carry on with a handle at null and the error shows up three lines later, in another place. The second: look at the case (pedido) of hacer_trago(). To add a new drink you have to edit the factory. In other words we took the hardcoding out of whoever uses it and put it in a single place — better, but it is still there. There is what UVM solves, and it is worth saying so: `uvm_component_utils registers the class **on its own**, so UVM's factory has no case to maintain, and type_id::create() returns the right type without anybody casting. Both annoyances of this slide disappear in the env.

see this slide in the deck ›

The factory pattern

Python Style

code/u3/factory/factory.py · #factory-class
class Trago:
    """Factory class that builds a Trago."""

    @staticmethod
    def factory(kind):
        """Factory Method."""
        if kind == "CubaLibre":
            return CubaLibre()
        if kind == "Fernet":
            return Fernet()
        if kind == "Mojito":
            return Mojito()
        if kind == "Whiscola":
            return Whiscola()
        raise ValueError("No such trago: " + kind)
el archivo entero, 86 líneas
"""Factory pattern example, in Python."""
import random


class Trago:
    """Factory class that builds a Trago."""

    @staticmethod
    def factory(kind):
        """Factory Method."""
        if kind == "CubaLibre":
            return CubaLibre()
        if kind == "Fernet":
            return Fernet()
        if kind == "Mojito":
            return Mojito()
        if kind == "Whiscola":
            return Whiscola()
        raise ValueError("No such trago: " + kind)


class CubaLibre(Trago):
    """Rum and coke."""

    def hielo(self):
        """Ice cubes."""
        print("CubaLibre.hielo: 2")

    def graduacion(self):
        """Strength."""
        print("CubaLibre.graduacion: medium")


class Whiscola(Trago):
    """Whisky and coke."""

    def hielo(self):
        """Ice cubes."""
        print("Whiscola.hielo: 2")

    def graduacion(self):
        """Strength."""
        print("Whiscola.graduacion: strong")


class Fernet(Trago):
    """Fernet and coke."""

    def hielo(self):
        """Ice cubes."""
        print("Fernet.hielo: 3")

    def graduacion(self):
        """Strength."""
        print("Fernet.graduacion: strong")


class Mojito(Trago):
    """Mint, lime and soda."""

    def hielo(self):
        """Ice cubes."""
        print("Mojito.hielo: 5")

    def graduacion(self):
        """Strength."""
        print("Mojito.graduacion: light")


# Generate Tragos: at random
def trago_generator(n):
    """Factory Method."""
    # Ask the language for every subclass that inherits from Trago
    types = Trago.__subclasses__()
    # Build a generator
    for _ in range(n):
        yield random.choice(types).__name__


tragos = [Trago.factory(kind) for kind in trago_generator(20)]


# It does not matter which trago comes out: they all get served the same
for trago in tragos:
    trago.hielo()
    trago.graduacion()

It is there on purpose and the moral is short: this is not a verification oddity, it is a design pattern from 1994 that in Python takes ten lines. The bottom part of the file is worth opening if there is time left: Trago.__subclasses__() asks the language which classes inherit from Trago, and with that the generator no longer needs a hand-written list. It is exactly what UVM's factory registration does with `uvm_component_utils, but from the top down instead of each class signing itself up. The punchline: the final loop treats the twenty drinks the same, without asking what type they are. That is polymorphism, and it is what makes a factory worth anything. The slide works for two opposite audiences. For whoever comes from software it lowers the suspicion —"ah, it is the same old factory"—. For whoever comes from RTL it shows that what they are learning is not "weird SystemVerilog stuff": it is programming, and you can go and read about it outside the EDA world.

see this slide in the deck ›

The factory pattern

Summary of the unit

Closing of the most abstract section of day 2, and it is worth landing it with what is coming: the +UVM_TESTNAME=add_test of the tests is a factory — UVM reads a string from the command line and builds the class that corresponds. Here they saw the mechanism from the inside, a day early. The sentence that sums up the whole section: whoever uses the object stops being the one who picks the type. Everything else is implementation detail. And for whoever comes from software: it is the GoF Factory Method, from 1994, and in Python it takes ten lines. We invented nothing, and that is good news.

see this slide in the deck ›

A testbench without a single module

The same testbench, without a single module

The real goal of the section is not to teach anything new, it is to leave a point of comparison. If the student can look at testbench.svh and say "this is the env, this is the build_phase, this is the run_phase", day 3 starts downhill. It is worth writing the table on the board before showing the code, and coming back to it at the end of day 3:

| Here, by hand | From the tests on | | testbench.execute() that does new() on the three | build_phase of the env | | passing the BFM through the constructor | uvm_config_db | | fork ... join_none | UVM runs one run_phase per component | | $finish inside the tester | objections |

And say out loud what can be seen on its own: there is not a single module in the classes of the testbench. The modules were left for the DUT, the clock and the interface — which is precisely what gets synthesized.

see this slide in the deck ›

A testbench without a single module

The VTALU TB, piece by piece

The object tree of the testbench without modules: top elaborates, and inside live the testbench class and its three pieces

It is the same testbench as interfaces and BFM, but in objects: what changes is the shape, not what it does. If possible, show them side by side. The drawing is the one to be able to redraw from memory at the end of the day, and the line that splits it in half is the one that matters: above, what gets elaborated —the module, the DUT, the interface—; below, what somebody has to create. An object nobody creates is a null handle, and that is the mistake of the first afternoon. It is the last version without UVM. From the tests on the same thing gets assembled by UVM, and the student has to be able to say which part replaces which.

see this slide in the deck ›

A testbench without a single module

The top: the only thing that is still a module

code/u3/tb-en-objetos/top.sv
module top;

   // Class definitions and shared resources are kept in a package
   import vtalu_pkg::*;
   // The macros are defined in common too. UVM does this as well
   `include "vtalu_macros.svh"

   // The DUT and the BFM get instantiated
   vtalu DUT (
       .A(bfm.A),
       .B(bfm.B),
       .op(bfm.op_set),
       .clk(bfm.clk),
       .reset_n(bfm.reset_n),
       .start(bfm.start),
       .done(bfm.done),
       .ovf(bfm.ovf),
       .result(bfm.result)
   );

   vtalu_bfm bfm ();

   // A variable to hold our TB
   testbench testbench_h;

   initial begin
      // The testbench object gets built and launched
      testbench_h = new(bfm);
      testbench_h.execute();
   end

endmodule : top

The top is still a module and it is going to stay that way until the end of the course, even with UVM. The reason is that classes do not get elaborated: somebody has to exist before time 0 to instantiate the DUT, the interface and the clock. The detail to point a finger at is how the BFM gets passed: here it gets handed to the constructor, new(bfm), and it works because the top knows the class. In UVM that line is not going to exist — the test gets created by the factory and nobody can pass it arguments. That is why the uvm_config_db shows up in the tests. A good question to leave hanging: why is the package import before everything else? Because the classes live in a package and the module has to see them. It is the first time the compilation order matters, and it will not be the last.

see this slide in the deck ›

A testbench without a single module

The testbench class: the one that assembles and starts

code/u3/tb-en-objetos/tb_classes/testbench.svh
class testbench;

   // virtual: the object-world equivalent of a module's port list. It tells
   // the compiler that this variable is going to receive a handle to an
   // interface at some point in the future. See the slide in the TB-in-objects section.
   virtual vtalu_bfm bfm;

   tester    tester_h;
   coverage  coverage_h;
   scoreboard scoreboard_h;

   function new(virtual vtalu_bfm b);
      bfm = b;
   endfunction : new

   task execute();
      tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      // One thread per object. join_none: execute() returns and the three go on.
      fork
         tester_h.execute();
         coverage_h.execute();
         scoreboard_h.execute();
      join_none
   endtask : execute
endclass : testbench

This is the class UVM is going to replace, so it is worth reading it line by line: new() on the three objects, fork ... join_none to start them, and that is it. That is an env written by hand. Two things to flag. The first is the virtual vtalu_bfm bfm: the word virtual here has nothing to do with the virtual methods of polymorphism. A virtual interface is a handle to an interface that is going to be assigned later on — it is the equivalent, in the world of objects, of the port list of a module. It is the third meaning of virtual in SystemVerilog and that is why it is worth naming it out loud. The second is the join_none: the three objects run in separate threads and execute() returns right away. If it were join, the testbench would wait for the three to finish and the scoreboard never finishes. It is the same decision UVM takes on its own when it runs a run_phase per component. And one that always gets asked: who ends the simulation? The tester's $finish, inside execute(). It is ugly and it shows: one object decides for everybody. In the tests that becomes an objection, which is the tidy form of the same thing.

see this slide in the deck ›

A testbench without a single module

The tester class: the stimulus, with no wires

code/u3/tb-en-objetos/tb_classes/tester.svh · #execute
   // The initial block is replaced by an execute() task
   task execute();
      byte unsigned     iA;
      byte unsigned     iB;
      shortint unsigned result;
      operation_t       op_set;
      bfm.reset_alu();
      op_set = rst_op;
      iA = get_data();
      iB = get_data();
      bfm.send_op(iA, iB, op_set, result);
      op_set = mul_op;
      bfm.send_op(iA, iB, op_set, result);
      bfm.send_op(iA, iB, op_set, result);
      op_set = rst_op;
      bfm.send_op(iA, iB, op_set, result);
      repeat (1000) begin : random_loop
         op_set = get_op();
         iA = get_data();
         iB = get_data();
         bfm.send_op(iA, iB, op_set, result);
         $display("%2h %6s %2h = %4h", iA, op_set.name(), iB, result);
      end : random_loop
      $finish;
   endtask : execute
el archivo entero, 58 líneas
class tester;

   virtual vtalu_bfm bfm;

   function new(virtual vtalu_bfm b);
      bfm = b;
   endfunction : new

   protected 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

   protected 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

   // The initial block is replaced by an execute() task
   task execute();
      byte unsigned     iA;
      byte unsigned     iB;
      shortint unsigned result;
      operation_t       op_set;
      bfm.reset_alu();
      op_set = rst_op;
      iA = get_data();
      iB = get_data();
      bfm.send_op(iA, iB, op_set, result);
      op_set = mul_op;
      bfm.send_op(iA, iB, op_set, result);
      bfm.send_op(iA, iB, op_set, result);
      op_set = rst_op;
      bfm.send_op(iA, iB, op_set, result);
      repeat (1000) begin : random_loop
         op_set = get_op();
         iA = get_data();
         iB = get_data();
         bfm.send_op(iA, iB, op_set, result);
         $display("%2h %6s %2h = %4h", iA, op_set.name(), iB, result);
      end : random_loop
      $finish;
   endtask : execute

endclass : tester

There is a trap here put in on purpose and it is worth not giving it away now: look at the declaration of get_op(). It says protected function, not protected virtual function. Without virtual, execute() is always going to call this class's one, even if the object is of a derived class — which is exactly what they saw in polymorphism with servir(). That is the day 2 exercise, and it is good that they walk into it: inheritance does not show until somebody writes virtual, and that is understood much better by suffering it than by reading it. The other thing to point out is what is not in this class: the protocol. reset_alu() and send_op() still live in the BFM, the same as in interfaces and BFM, and the tester calls them with bfm.send_op(...). That is the whole point of the BFM and that is why it holds without changes until the end of the course: the class above asks for an operation, and it does not even know a done exists. And the detail you see at the start of execute(): the four directed operations before the repeat (1000) are not decoration. They are the reset, the multiplication after the reset and the repeated mult — that is, three rows of the verification plan of the conventional testbench that the random should not be left to wait for.

see this slide in the deck ›

A testbench without a single module

The scoreboard: the always became a task

code/u3/tb-en-objetos/tb_classes/scoreboard.svh · #execute
   // The always @(posedge done) of the modular version, as a forever loop: an
   // object has no sensitivity list, it has a task that blocks.
   task execute();
      shortint predicted_result;
      bit      predicted_ovf;
      forever begin : self_checker
         @(posedge bfm.done) #1;
         case (bfm.op_set)
            add_op: predicted_result = bfm.A + bfm.B;
            sub_op: predicted_result = bfm.A - bfm.B;
            and_op: predicted_result = bfm.A & bfm.B;
            xor_op: predicted_result = bfm.A ^ bfm.B;
            mul_op: predicted_result = bfm.A * bfm.B;
         endcase  // case (op_set)

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

         if ((bfm.op_set != no_op) && (bfm.op_set != rst_op))
            if (predicted_result != bfm.result || predicted_ovf != bfm.ovf)
               $error(
                   "FAILED: A: %0h  B: %0h  op: %s result: %0h ovf: %0b",
                   bfm.A,
                   bfm.B,
                   bfm.op_set.name(),
                   bfm.result,
                   bfm.ovf
               );

      end : self_checker
   endtask : execute
el archivo entero, 41 líneas
class scoreboard;
   virtual vtalu_bfm bfm;

   function new(virtual vtalu_bfm b);
      bfm = b;
   endfunction : new

   // The always @(posedge done) of the modular version, as a forever loop: an
   // object has no sensitivity list, it has a task that blocks.
   task execute();
      shortint predicted_result;
      bit      predicted_ovf;
      forever begin : self_checker
         @(posedge bfm.done) #1;
         case (bfm.op_set)
            add_op: predicted_result = bfm.A + bfm.B;
            sub_op: predicted_result = bfm.A - bfm.B;
            and_op: predicted_result = bfm.A & bfm.B;
            xor_op: predicted_result = bfm.A ^ bfm.B;
            mul_op: predicted_result = bfm.A * bfm.B;
         endcase  // case (op_set)

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

         if ((bfm.op_set != no_op) && (bfm.op_set != rst_op))
            if (predicted_result != bfm.result || predicted_ovf != bfm.ovf)
               $error(
                   "FAILED: A: %0h  B: %0h  op: %s result: %0h ovf: %0b",
                   bfm.A,
                   bfm.B,
                   bfm.op_set.name(),
                   bfm.result,
                   bfm.ovf
               );

      end : self_checker
   endtask : execute

endclass : scoreboard

The translation to point out: the always @(posedge done) of the modular testbench is a forever begin @(posedge bfm.done) ... end here. It is the same wait, written as sequential code inside a method. An object has no sensitivity list; it has a task that blocks. The #1 of the first line is one of those details that get paid for dearly: without it, the scoreboard reads the signals at the very instant done goes up and can eat a delta race. With it, it reads when everything has settled. It is the reason verification samples on the edge the design does not use. And what this scoreboard still does wrong, to leave it hanging: it reads the operation off the wire (bfm.op_set) at the moment of the result. It works because the protocol forces the operands to stay stable, but it is fragile — the day the DUT has several operations in flight, it breaks. In the analysis ports the scoreboard is going to compare against a queue of commands the monitor sends it, and that does scale.

see this slide in the deck ›

A testbench without a single module

The coverage class: the covergroup inside an object

code/u3/tb-en-objetos/tb_classes/coverage.svh · #sampling
  // In a class, we don't need to declare variables to hold the covergroups, but we do need
  // to call a constructor to create them.
   function new (virtual vtalu_bfm b);
     op_cov = new();
     zeros_or_ones_on_ops = new();
     bfm = b;
   endfunction : new

   task execute();
      forever begin  : sampling_block
         @(negedge bfm.clk);
         A = bfm.A;
         B = bfm.B;
         op_set = bfm.op_set;
         op_cov.sample();
         zeros_or_ones_on_ops.sample();
      end : sampling_block
   endtask : execute
el archivo entero, 119 líneas
class coverage;

   virtual vtalu_bfm bfm;

   byte         unsigned        A;
   byte         unsigned        B;
   operation_t  op_set;

   covergroup op_cov;

      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

         // Transition bins. Verilator 5.052 does NOT implement them yet: any form
         // with "=>" or "[* n]" aborts with an Internal Error. Minimal repro in
         // code/verilator/repro-cg-transition.sv. They stay because they are part
         // of the topic; when Verilator supports them, drop the ifndef, nothing else.
`ifndef VERILATOR
         bins opn_rst[] = ([add_op:mul_op] => rst_op);
         bins rst_opn[] = (rst_op => [add_op:mul_op]);

         bins sngl_mul[] = ([add_op:xor_op],no_op => mul_op);
         bins mul_sngl[] = (mul_op => [add_op:xor_op], no_op);

         bins twoops[] = ([add_op:mul_op] [* 2]);
         bins manymult = (mul_op [* 3:5]);
`endif

      }

   endgroup

   covergroup zeros_or_ones_on_ops;

      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

      a_leg: coverpoint A {
         bins zeros = {8'h00};
         bins others= {[8'h01:8'hFE]};
         bins ones  = {8'hFF};
      }

      b_leg: coverpoint B {
         bins zeros = {8'h00};
         bins others= {[8'h01:8'hFE]};
         bins ones  = {8'hFF};
      }
      // VTALU rev2: the borrow of the subtraction, the row of the plan that the
      // scoreboard can only close by looking at TWO outputs. Without this bin, a
      // subtraction that never went negative looks exactly like one that did.
      borrow: coverpoint ((op_set == sub_op) && (A < B)) {
         bins hubo_borrow = {1};
         ignore_bins resto = {0};
      }


      op_00_FF:  cross a_leg, b_leg, all_ops {
         bins add_00 = binsof (all_ops) intersect {add_op} &&
                       (binsof (a_leg.zeros) || binsof (b_leg.zeros));

         bins add_FF = binsof (all_ops) intersect {add_op} &&
                       (binsof (a_leg.ones) || binsof (b_leg.ones));

         bins sub_00 = binsof (all_ops) intersect {sub_op} &&
                       (binsof (a_leg.zeros) || binsof (b_leg.zeros));

         bins sub_FF = binsof (all_ops) intersect {sub_op} &&
                       (binsof (a_leg.ones) || binsof (b_leg.ones));

         bins and_00 = binsof (all_ops) intersect {and_op} &&
                       (binsof (a_leg.zeros) || binsof (b_leg.zeros));

         bins and_FF = binsof (all_ops) intersect {and_op} &&
                       (binsof (a_leg.ones) || binsof (b_leg.ones));

         bins xor_00 = binsof (all_ops) intersect {xor_op} &&
                       (binsof (a_leg.zeros) || binsof (b_leg.zeros));

         bins xor_FF = binsof (all_ops) intersect {xor_op} &&
                       (binsof (a_leg.ones) || binsof (b_leg.ones));

         bins mul_00 = binsof (all_ops) intersect {mul_op} &&
                       (binsof (a_leg.zeros) || binsof (b_leg.zeros));

         bins mul_FF = binsof (all_ops) intersect {mul_op} &&
                       (binsof (a_leg.ones) || binsof (b_leg.ones));

         bins mul_max = binsof (all_ops) intersect {mul_op} &&
                        (binsof (a_leg.ones) && binsof (b_leg.ones));

         ignore_bins others_only =
                                  binsof(a_leg.others) && binsof(b_leg.others);

      }

endgroup

  // In a class, we don't need to declare variables to hold the covergroups, but we do need
  // to call a constructor to create them.
   function new (virtual vtalu_bfm b);
     op_cov = new();
     zeros_or_ones_on_ops = new();
     bfm = b;
   endfunction : new

   task execute();
      forever begin  : sampling_block
         @(negedge bfm.clk);
         A = bfm.A;
         B = bfm.B;
         op_set = bfm.op_set;
         op_cov.sample();
         zeros_or_ones_on_ops.sample();
      end : sampling_block
   endtask : execute

endclass : coverage

The op_cov = new() inside the constructor is the trap of the slide: in a module the covergroup gets instantiated on its own, in a class it does not. If you forget it, the code compiles, it runs, and the report says 0 %. It is the same silent trap as the conventional testbench's, with one more turn of the screw. The transition bins are still inside `ifndef VERILATOR, the same as in the conventional testbench, and for the same reason: Verilator 5.052 still does not compile them. What has to be said before the analysis ports: here the sampling happens on the **clock edge**, that is, cycles get counted. When the coverage is a subscriber it is going to sample when a command arrives, and there operations get counted — which is what the verification plan asked for from the conventional testbench on.

see this slide in the deck ›

A testbench without a single module

The map of day 3: what replaces what

Here, by hand From the tests on
testbench.execute() with three new() the build_phase of the env
passing the BFM through the constructor uvm_config_db
fork ... join_none UVM runs one run_phase per component
$finish inside the tester objections
picking the tester by editing the code set_type_override of the factory
$display that gets put in and deleted `uvm_info and the verbosity ceiling

This is the slide worth writing down, because it is the whole map of day 3. The strong claim has to be made explicit: UVM adds no functionality here. The testbench of this unit does everything the env's is going to do. What it adds is convention — that the left-hand column is solved the same way in every testbench in the world. And the honest question that is going to come up: "so what for?". So that whoever joins the project knows where to look without reading the whole testbench, and so that the new tester costs four lines instead of a class. Both benefits are of scale, and that is why in a hundred-line example UVM looks like too much — it has to be said that way, instead of selling smoke. At the close of day 3, with the env and the override already seen, you come back here and read the right-hand column straight through. That is where the day closes on its own.

see this slide in the deck ›

A testbench without a single module

Summary of the unit

The slide to close the whole of day 2 with, because it leaves the bridge built. The question to leave posed: which part replaces which? Day 3 answers it piece by piece, and it starts downhill if the student walks out of here able to draw the object tree on the board. And the argument for why the whole day was worth it: with modules, changing the stimulus is editing the file. With classes it is extending a class, and the two versions live side by side. That is the exercise that follows.

see this slide in the deck ›

Review · Day 2

1 of 8 · Handle and object

rectangle rectangle_h; — after that line, how many objects are there?

None — declaring a handle reserves nothing. There is the difference with a struct, which the simulator reserves the moment it sees it. And using the handle before the new() does not fail at compile time: it blows up in the middle of the simulation.

see this slide in the deck ›

Review · Day 2

2 of 8 · Polymorphism

A variable of type trago holds a fernet object. If servir() is not virtual, what gets executed?

trago's — without virtual, SystemVerilog looks at the type of the variable, not at the object's. It is literally what code/u3/polimorfismo/01-sin-virtual prints: "A generic trago cannot be served".

see this slide in the deck ›

Review · Day 2

3 of 8 · Abstract classes

What do I gain by using an abstract class with pure virtual methods instead of a base class whose method does $fatal?

The error moves to the compiler — with $fatal you find out halfway through the simulation that an override was missing. With pure virtual it does not compile. Catching it earlier is always cheaper.

see this slide in the deck ›

Review · Day 2

4 of 8 · Static variables

I instantiate 10 objects of a class that has a static variable. How many copies are there in memory?

A single one — and it exists even if you instantiate no object at all. That is what makes it useful for global TB data, and what makes it dangerous if you leave it public.

see this slide in the deck ›

Review · Day 2

5 of 8 · Static methods

Why is it worth declaring the static variable protected and exposing it with static methods?

To be able to change it later — if the queue is out in the open, the day you swap it for another structure you have to go and fix every place that touched it. Encapsulating is being able to change your mind.

see this slide in the deck ›

Review · Day 2

6 of 8 · Parameterized classes

bandeja#(fernet) and bandeja#(mojito) have a static queue inside. Do they share the queue?

They do not share it — SystemVerilog generates one class per combination of parameters. static is unique inside each one of those classes, not across all of them. UVM leans on this all the time.

see this slide in the deck ›

Review · Day 2

7 of 8 · The factory pattern

What problem does the factory pattern solve?

Deciding the subtype at runtime — without hardcoding the new: you ask the factory for an object and it decides which one. It is the piece that is later going to let you change the stimulus of a whole test without touching the code of the env.

see this slide in the deck ›

Review · Day 2

8 of 8 · $cast

When does a $cast(destination, source) succeed?

Only if the object allows it — $cast checks at runtime and returns 0 if it does not work. That is why UVM's factory is more comfortable than the cantina of the section: type_id::create() returns the right type and saves you the cast.

see this slide in the deck ›

Exercise · Day 2

A tester that only multiplies

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

The exercise is set up so that they fail: get_op() in the object-based testbench is not virtual, so execute() always calls the base class's one even if the object is a mult_tester. Until they add virtual, inheritance does not show — and that is understood much better when you suffer it than when you read it. It is on purpose the same problem as the day 3 exercise: there they are going to solve it with the factory, and only then do you see what the factory is for.

see this slide in the deck ›