Verify This! · el curso para leer

Día 5El dato

Las mismas slides del deck, con las notas del instructor adentro del texto y el código completo. Si estás haciendo el curso solo, empezá por acá y tené el deck al lado para los repasos.

Ayer quedó…

Dónde dejamos el testbench

La recap del día 5 tiene que instalar la incomodidad del dato suelto, porque es lo que justifica una unidad entera sobre objetos que no hacen nada más que llevar datos. El tercer bullet es literal: en el testbench de ayer, copiar un comando es tres asignaciones, y compararlo es tres ==. Con dos campos más son cinco y cinco. Hoy eso pasa a ser copy() y compare(), escritos una vez.

ver esta slide en el deck ›

Agenda

Día 5 · ≈ 4 h 30 · unidad 6 · el dato

Al final del día podés:

El segundo es la novedad del día y el que más rinde en el trabajo: medir es la mitad, cerrar es la otra. Los dos últimos ejercicios de la tarde son dos tercios de ese ciclo —medir el dist, cerrar el bin— y el día 7 lo cierra acumulando semillas. La 12 de la autoevaluación dice medir, no escribir, y es a propósito: un dist con los pesos bien puestos puede dar un histograma que miente, y el que no lo midió nunca no tiene forma de saberlo.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

El handle no es el objeto

Es una sección de OOP en el medio de UVM, y conviene decir por qué está acá y no en el día 2: porque recién ahora hace falta. Hasta reporting —el cierre del día 3— los datos eran una struct y se copiaban solos. El bug del segundo bullet lo escribe todo el mundo una vez, y el síntoma es horrible: mandás una transaction al scoreboard, seguís usando el mismo handle para la siguiente, la modificás, y el scoreboard compara contra datos que cambiaron después de que él los recibió. No falla siempre, y cuando falla parece un bug del DUT. La otra mitad —el super en cada nivel— tiene un síntoma distinto y más traicionero: el día que alguien mete una clase nueva en el medio de la jerarquía, el convert2string() deja de imprimir la mitad de los campos y nadie se entera, porque sigue imprimiendo algo. La pregunta para arrancar: si handle_a = handle_b, ¿cuántos objetos hay? Uno. Es la misma pregunta de clases y extensiones, ahora con consecuencias.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

Nunca dupliques código

“Si nos encontramos copiando código y haciéndole pequeñas modificaciones dentro de nuestro programa, estamos haciendo las cosas mal. Nunca hay que duplicar código.”

En OOP en vez de copiar código...

extendemos clases

ver esta slide en el deck ›

Copiar un objeto que contiene otro

convert2string(): el problema

Los cuatro niveles de la jerarquía

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

Esta versión funciona, y ése es el problema: se ve bien y está mal. Cada convert2string() imprime a mano todos los campos, incluidos los que heredó. Andá al $sformatf de la clase de más abajo y contá cuántas veces se repite el nombre de un campo de la clase de arriba. Cada una de esas repeticiones es una línea que hay que acordarse de tocar el día que se agregue un campo. La pregunta que arma la slide siguiente: ¿qué pasa si mañana alguien mete una clase nueva en el medio de la jerarquía? Los de abajo siguen imprimiendo lo que sabían, la del medio no aparece, y no hay error. El método sigue devolviendo un string.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

convert2string(): la versión deep

Un nivel nuevo en la jerarquía rompe convert2string(): graduacion no aparece en el 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

La regla, corta: cada método de la jerarquía se ocupa sólo de sus propios campos y le pide el resto a super. Nadie sabe qué hay más arriba, y por eso insertar una clase en el medio no rompe nada. El patrón se repite igual en los tres métodos de la sección —convert2string(), do_copy(), do_compare()— y también en los tres de UVM. Si te acostumbrás a escribir la llamada a super primero, antes de tocar tus campos, no te la olvidás nunca. Vale la pena señalar el paralelo con algo que ya vieron: es exactamente el mismo argumento que el super.new() de clases y extensiones. La parte de arriba del objeto también hay que construirla — y también hay que imprimirla, copiarla y compararla.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

El signo igual no copia nada

Copia el handle, no el objeto. Es el bug que todos escriben una vez: modificás la transaction que ya mandaste al scoreboard y el scoreboard ve el cambio a mitad de camino. De acá sale el clone() de las transactions.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

Copiar de verdad son dos pasos

Copiar de verdad son dos pasos, y hay que escribirlos: instanciar un objeto nuevo y pasarle los datos campo por campo

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

Vale hacer la comparación con la asignación de la slide anterior en voz alta: a = b es una línea y no copia nada; copiar de verdad son dos pasos y hay que acordarse de los dos. Que el lenguaje no traiga la copia profunda no es un olvido del comité: no existe una respuesta general. Si la clase tiene un handle a otro objeto, ¿copiás el handle o el objeto? Depende, y por eso lo tenés que decidir vos en cada do_copy(). Es exactamente el mismo dilema que van a tener las transactions si algún día llevan un array de objetos adentro. En este curso no pasa, pero conviene que la pregunta quede planteada.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

do_copy(): cada clase toca lo suyo

do_copy(): Cada clase en la jerarquía, debe llamar a su clase superior, por lo tanto, todos los métodos do_copy() necesitan el mismo tipo de argumento. Pero tenemos clases distintas... Usamos polimorfismo

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

Acá está el detalle que parece burocrático y no lo es: todos los do_copy() de la jerarquía reciben un argumento de la clase base, no de la suya. Si cada uno recibiera su propio tipo, la llamada a super.do_copy(rhs) no compilaría — los tipos no coincidirían. El precio es que adentro de cada método hay que hacer $cast para poder leer los campos propios. Y como vimos en la factory, ese $cast chequea en runtime: si alguien intenta copiar un mojito sobre un fernet, devuelve 0 y hay que atajarlo. Es la primera vez que se ve una convención de firma que existe sólo para que el polimorfismo funcione. En UVM va a ser lo mismo y con más reglas: el argumento tiene que ser un uvm_object —y por convención se lo llama rhs—, do_compare() recibe además un uvm_comparer, y así. Cuando en las transactions aparezcan esas firmas, la respuesta a "¿por qué así?" es esta slide.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

Resumen: la solución pésima

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

La regla que hay que dejar: una clase sólo escribe sobre sus propios campos. Todo lo demás se lo pide a super. Conviene contar en pantalla: hielos aparece en cuatro convert2string() distintos de este archivo y con_coca en tres. Siete lugares para acordarse, y ninguno falla si te olvidás de uno. Hay una bad_copy() un poco más abajo en el archivo que vale la pena abrir si sobra tiempo: toma un fernet_con_hielo como argumento en vez de un trago. Compila y anda... hasta que alguien la llama por un handle de la clase base, y ahí no hay polimorfismo que valga.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

Resumen: la solución deep

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

Cierre de la sección, y conviene decir adónde va todo esto: los tres métodos que acaban de escribir a mano son exactamente los tres que UVM pide en una transaction. do_copy(), do_compare() y convert2string(), con las mismas reglas de super y de $cast. La diferencia es que en UVM no se los llama directo: uno escribe do_copy() y el testbench llama a copy(), que es la que está en uvm_object y se encarga del resto. Misma idea que las fases — vos escribís la parte de abajo, la librería maneja el protocolo. Y la pregunta con la que hay que salir del día: si escribir esto bien cuesta tres métodos por clase, ¿por qué no lo genera alguien? Lo genera: son las macros `uvm_field_*. El curso no las usa, y el porqué está en la slide de super.build_phase() de los components — generan cientos de líneas que nadie lee y que aparecen en el stack cuando algo falla. Leerlas sí, escribirlas no.

ver esta slide en el deck ›

Copiar un objeto que contiene otro

Resumen de la unidad

El bug del handle es el que todos escriben una vez: modificás la transaction que ya mandaste al scoreboard, y el scoreboard ve el cambio a mitad de camino. No falla en el momento; falla después, y en otro lado. La forma corta de cerrar: el = copia el papelito con la dirección, no la casa. Y por eso UVM trae copy(), clone() y compare() hechos — que es exactamente la unidad que sigue.

ver esta slide en el deck ›

Transactions

El testbench está bien repartido, y el dato no

La pregunta honesta del alumno es "¿y por qué no una struct?". La respuesta: la struct no se randomiza sola, no se compara sola y no se imprime sola. Todo lo que el testbench hacía a mano con command_s pasa a ser un método de la clase, y el resto del testbench se achica. El cuarto bullet es el argumento más concreto y conviene apoyarse en él: hasta acá el analysis port del resultado lleva un shortint, o sea un número. El VTALU tiene dos salidas —result y ovf— y por eso el scoreboard de las unidades anteriores chequea la mitad del DUT y la otra mitad la deja pasar. Con un result_transaction con dos campos, el chequeo del ovf aparece solo. Es la primera vez en el curso que la clase no es prolijidad: es la única forma.

ver esta slide en el deck ›

Transactions

Una transaction es un dato con los métodos que operan sobre ese dato

La frase de la slide es la definición que hay que dejar escrita en el pizarrón: un dato con los métodos que operan sobre ese dato. El tercer bullet es el cambio de reparto de responsabilidades de la sección: hasta hoy el tester sabía qué valores eran legales —tenía un get_data() con el sesgo escrito a mano—. Desde hoy eso vive en la transaction, en una constraint. La pregunta para tirar: si mañana el DUT acepta operandos de 16 bits, ¿cuántas clases hay que tocar? Una.

ver esta slide en el deck ›

Transactions

Definir una transaction: de qué clase se extiende

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;

Ojo con la clase base, porque el material viejo dice otra cosa. La cadena es uvm_sequence_item → uvm_transaction → uvm_object, y se extiende siempre la primera. Dos razones. La chica: en IEEE 1800.2 uvm_transaction es una virtual class, no se puede instanciar sola. La grande: una uvm_sequence sólo sabe mandar uvm_sequence_item. Si la transaction extiende uvm_transaction a secas, todo lo del día 6 —start_item / finish_item— no compila. O sea que la clase base que elegimos hoy es la que nos deja seguir mañana.

ver esta slide en el deck ›

Transactions

Los campos randomizados: el "antes"

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

Conviene leer este código como el "antes" y hacer la pregunta antes de dar la respuesta: ¿qué parte de esto es del test y qué parte es del dato? El rango legal de A y B, y el sesgo a los bordes, son propiedades del dato — no tienen nada que ver con qué querés probar. El $random escrito a mano es el síntoma: cada componente que quiera un comando válido tiene que repetirlo. Y si el DUT cambia, hay que acordarse de todos. Un detalle que se cobra en la slide de semillas: $random es el de Verilog-95 y tiene una semilla global, así que dos hilos que lo llamen se pisan y la corrida no es reproducible. $urandom tiene semilla por hilo. El randomize() de la slide siguiente usa el segundo. El remate para la slide siguiente: SystemVerilog ya trae esto hecho, y no hace falta escribir ni un $urandom.

ver esta slide en el deck ›

Transactions

Los campos randomizados: el "después"

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

Tres cosas de este código, en orden de lo que se olvida. rand adelante de cada campo: sin eso el campo no entra en el sorteo y randomize() lo deja como estaba, sin avisar. Es el primer lugar donde mirar cuando un valor sale siempre igual. El dist con :/ y no :=: reproduce el sesgo a los bordes que el get_data() del testbench convencional hacía a mano. La diferencia entre los dos operadores es una unidad entera del curso, y no es un detalle de estilo — con := el bin 00 sale 1 vez cada 256 y los casos borde no se llenan nunca. Y el op sin constraint: un enum se randomiza sobre sus valores, así que también van a salir no_op y rst_op. Eso es a propósito — el plan de verificación pide operaciones después de un reset.

ver esta slide en el deck ›

Transactions

El constructor: un uvm_object no tiene padre

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

Es la distinción que hay que dejar clara: component es estructura —está en el árbol, tiene padre, tiene fases—; object es dato —se crea, viaja por el testbench y se tira—. Las dos cosas son clases de UVM, pero sólo una arma la jerarquía. Truco para acordarse: si lo podés dibujar en el diagrama de bloques del testbench, es un component. Si viaja por una flecha del diagrama, es un object.

ver esta slide en el deck ›

Transactions

do_copy(): copiar el objeto, no el 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

El patrón es el de las jerarquías de clases, con dos reglas de UVM encima. La primera, la que hace que compile: el argumento tiene que ser de tipo uvm_object —la clase base de todo—, porque lo que SystemVerilog exige para que el override virtual funcione es que coincidan el tipo y la dirección, no el nombre. rhs es el identificador que usa la firma de uvm_object::do_copy y por eso lo copia todo el mundo, pero podés llamarlo como quieras. Lo primero que hay que hacer es castear. Y como el $cast chequea en runtime, va con su uvm_fatal: si alguien intenta copiar un result_transaction sobre un command_transaction, querés enterarte ahí y no tres componentes más adelante. La segunda: el super.do_copy(rhs) va antes de tocar los campos propios. Es la misma disciplina de las jerarquías de clases y acá importa más, porque uvm_transaction —dos niveles más arriba— tiene campos propios que uno no ve: accept_time, begin_time, end_time e initiator. Y el detalle que confunde a todos: vos escribís do_copy(), pero nunca lo llamás. El testbench llama a copy(), que está en uvm_object, y ésa se encarga de invocar tu do_copy(). Es el mismo reparto que las fases — vos ponés la parte de abajo, la librería maneja el protocolo.

ver esta slide en el deck ›

Transactions

La regla MOOCOW: compartir sí, modificar no

El nombre es una joda del UVM Primer, pero la regla es la que evita el bug más caro de las jerarquías de clases: mandaste la transaction al scoreboard, seguís teniendo el handle, la modificás para la siguiente, y el scoreboard termina comparando contra datos que cambiaron después de que él los recibió. No falla siempre — falla cuando hay carga, que es cuando peor viene. La versión práctica, sin siglas: el que produce el dato no lo vuelve a tocar después de publicarlo. Si necesitás cambiarlo, cloná. Fijate cómo lo cumple el tester de esta sección: no reusa el handle. Cada vuelta del repeat hace un create() nuevo. Es más barato que clonar y evita la discusión. Y hay una excepción documentada que van a ver en el día 6: el driver escribe el resultado adentro del item que le prestó la sequence. Está acordado entre las dos partes y es el único lugar del testbench donde se hace.

ver esta slide en el deck ›

Transactions

clone_me(): el $cast escrito una sola vez

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

Es un método de tres líneas y existe por una sola razón: clone() devuelve un uvm_object, así que todo el que lo use tiene que castear. Escribir el $cast una vez adentro de la clase es mejor que escribirlo cincuenta veces afuera. El nombre no está en el estándar: clone_me() es una convención del UVM Primer. En un proyecto ajeno puede llamarse distinto o no existir — y ahí vas a ver el $cast repetido en cada llamador. Y la trampa que hay que nombrar: clone() llama a create() y después a copy(), que termina en tu do_copy(). Si do_copy() se olvida un campo, el clon sale incompleto y nadie avisa. Es el mismo agujero de las jerarquías de clases, con otro nombre.

ver esta slide en el deck ›

Transactions

do_compare(): comparar dos objetos sin escribir un if por campo

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

Dos cosas que se copian mal. El $cast acá no va con uvm_fatal: si el tipo no da, la respuesta correcta es same = 0 —son distintos, obvio, si ni siquiera son de la misma clase—, no matar la simulación. Es la diferencia con do_copy(), donde un tipo equivocado sí es un bug del testbench. Vale mostrarlo al lado. Y el super.do_compare() va encadenado con &&, no llamado y descartado. El error clásico es escribir super.do_compare(rhs, comparer); en una línea suelta y después same = (A == rhs.A) && ...: compila, corre, y silenciosamente deja de comparar todo lo de la clase de arriba. El uvm_comparer del segundo argumento es la política —cuántas diferencias reportar, con qué verbosidad—. Casi nadie lo toca, pero hay que declararlo porque la firma lo pide.

ver esta slide en el deck ›

Transactions

convert2string(): la transaction se imprime sola

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

Es el método que más se usa de los tres, y el que menos atención recibe: cada mensaje del scoreboard y de los monitores del resto del curso sale de acá. El .name() del enum es el detalle que cambia el día: sin él, el log dice op: 4 y hay que ir a buscar el typedef; con él dice op: mul_op. La regla para llevarse: si un campo es un enum, en el log va su nombre, nunca su valor. Vale nombrar el pariente que UVM trae y el curso no usa: print() y sprint(), que imprimen la transaction sola si registraste los campos con las macros `uvm_field_* — o si escribís do_print(), que es el equivalente manual de convert2string() y no aparece en ninguna slide del curso. Ojo con eso: sin las macros y sin do_print(), un t.print() de las transactions de acá imprime sólo el encabezado. El formato **sí** se cambia: por defecto es el uvm_table_printer —una fila por campo, con tipo y radix— y hay un uvm_line_printer que mete el objeto entero en un renglón. La razón para preferir convert2string() es más sencilla y hay que decirla así: escrito a mano entra en un renglón que dice lo que a vos te importa, y en un archivo de cien mil líneas eso importa.

ver esta slide en el deck ›

Transactions

Las field macros: leerlas aunque no las escribas

class command_transaction extends uvm_sequence_item;
   `uvm_object_utils_begin(command_transaction)
      `uvm_field_int (A,  UVM_DEFAULT | UVM_HEX)   // un campo integral
      `uvm_field_enum(operation_t, op, UVM_DEFAULT)
      `uvm_field_object(payload, UVM_DEFAULT | UVM_REFERENCE)  // handle, no copia
   `uvm_object_utils_end
endclass

Ésta es la slide del "y cuando abras el VIP del trabajo". La decisión del curso —escribir do_copy, do_compare y convert2string a mano— está tomada y bien tomada, y no se está revirtiendo acá: lo que se agrega es la mitad que faltaba, que es leer lo que escribió otro. La forma de leerlo es siempre la misma y conviene bajarla como receta: la lista de `uvm_field_* **es** la definición de qué significa "iguales" y qué significa "copiar" para esa clase. Si un campo no está en la lista, compare() no lo mira — y ése es el bug más caro que producen estas macros: un scoreboard que da verde porque el campo que importa se quedó afuera de la declaración. El UVM_REFERENCE merece su segundo, porque es la trampa de do_copy() otra vez en otro disfraz: sin él, copiar la transaction copia también el objeto colgado, y un clone() en un monitor de mucho tráfico se vuelve caro sin que nadie lo note. Con él, las dos transactions comparten el mismo payload — que es exactamente lo que MOOCOW dice que no hay que modificar. El costo real, para que la decisión del curso no parezca gusto: las macros expanden cientos de líneas por clase, y cuando algo sale mal el error apunta adentro de la expansión. Un do_compare de cuatro líneas escrito a mano se lee en el momento.

ver esta slide en el deck ›

Transactions

Los siete pasos, y por qué son siete

Lo que cuesta cambiarle el tipo de dato a un TB que ya existe. Vemos 2, 3, 6 y 7; 1, 4 y 5 son traducción directa y están en el repo:

# Qué Dónde
1 una result_transaction para la vuelta result_transaction.svh
2 una add_transaction, sólo sumas add_transaction.svh
3 los dos testers se funden en uno tester.svh
4 el command_monitor publica objetos command_monitor.svh
5 el result_monitor, lo mismo result_monitor.svh
6 el scoreboard usa compare() scoreboard.svh
7 add_test overridea el dato add_test.svh

Los siete pasos son la parte más aburrida de la sección y también la más honesta, así que conviene enmarcarla en vez de correrla: esto es lo que cuesta cambiar el tipo de dato de un testbench que ya existe. El consejo real es hacerlo desde el principio. El que hay que mirar de reojo es el 3, porque es el único que borra una clase: add_tester desaparece. Ahí está el resultado de la sección, y vale adelantarlo — mover la decisión al dato hizo sobrar una clase entera de estructura. Si el grupo va rápido, los pasos 4 y 5 se abren en el editor y se comparan con los de los analysis ports: la diferencia es que en vez de llenar una struct se hace un create() y se llenan campos. Nada más.

ver esta slide en el deck ›

Transactions

Paso 2 · una add_transaction que sólo suma

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

Ocho líneas, y de esas una sola hace algo: constraint add_only {op == add_op;}. Vale ponerla al lado del add_tester del env, que hacía lo mismo redefiniendo un método. Dos formas de decir "sólo sumas", y la de hoy no toca código de comportamiento — sólo describe qué valores son legales. El punto conceptual, que es el que ordena el día: las constraints se heredan y se acumulan. add_transaction no reemplaza a data, se le suma. El solver tiene que satisfacer las dos a la vez, así que sigue habiendo sesgo a los bordes en A y B — y eso es exactamente lo que queremos. De ahí sale la advertencia: dos constraints heredadas que se contradicen no dan error de compilación, dan un randomize() que devuelve 0. Es el caso 1 del experimento de Constrained Random.

ver esta slide en el deck ›

Transactions

Paso 3 · dos testers se vuelven uno

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

Es el resultado de la sección, medido en archivos: add_tester.svh deja de existir. Vale decirlo en voz alta, porque el env había gastado un rato en justificar esa jerarquía y ahora sobra la mitad. Y no es que el env estuviera mal: era la respuesta correcta mientras el dato fuera una struct muda. Cuando el dato sabe randomizarse, el escalón de la jerarquía deja de tener trabajo. Es un buen ejemplo de que las abstracciones se justifican contra el problema del momento, no para siempre. El if (!command.randomize()) uvm_fatal es la línea que hay que copiar bien: randomize() devuelve 0 si las constraints no tienen solución y no aborta nada. Hay una slide entera sobre eso más adelante.

ver esta slide en el deck ›

Transactions

Paso 6 · el scoreboard compara objetos, no números

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

Comparar el write() con el de los analysis ports es la mejor forma de cerrar la sección: allá había un case con la predicción escrita adentro y una comparación a mano; acá hay un predict_result() que devuelve un objeto y un predicted.compare(t). Lo que se ganó es concreto: el mensaje de error ahora se arma con convert2string() de las tres transactions, así que dice qué se mandó, qué salió y qué se esperaba, todo con los enums por nombre. Y el día que la transaction gane un campo, el log lo muestra solo. Un detalle que hay que señalar porque es la regla de la sección aplicada: el predicted sale de type_id::create(), no de new(). Es una transaction que viaja —aunque sólo viaje hasta el compare() de la línea siguiente—, así que sale de la factory como todas. Y el do ... while que saltea no_op y rst_op sigue igual que en los analysis ports: esas operaciones no producen resultado, y si no se descartan la comparación se corre un lugar y falla todo.

ver esta slide en el deck ›

Transactions

Paso 7 · el override, ahora sobre el dato

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

Vale poner esta slide al lado del add_test del env y contar líneas: la misma idea, un nivel más abajo. Antes se sustituía el que genera; ahora se sustituye lo generado. El orden del super es la trampa de la slide, y es hermana de la del env: la factory decide qué construir en el momento del create(). Si el override llega después, no hay error — el testbench corre con command_transaction y el test "de sumas" manda operaciones al azar. No rompe, miente. Y una pregunta que sale siempre: ¿por qué acá add_test sí llama a super.build_phase() si el curso dice que no hace falta? Porque su base es random_test, una clase nuestra que tiene un build_phase con trabajo real. La regla de los components hablaba de uvm_component, que no lo tiene.

ver esta slide en el deck ›

Transactions

El new() que se come el override

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

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

Esta slide sale de un bug que tenía el código de este mismo curso: el tester creaba las dos operaciones dirigidas —el reset y la multiplicación FF x FF— con new(), así que bajo add_test esas dos NO eran add_transaction. Nadie se daba cuenta porque el testbench funciona igual. La pregunta para tirar al grupo, que es la que ordena todo: si ahora el reset sale de la factory y bajo add_test es un add_transaction, ¿por qué la constraint op == add_op no lo convierte en una suma? Porque no lo randomizamos. La factory elige el tipo; las constraints sólo actúan en randomize().

ver esta slide en el deck ›

Transactions

Y randomize() nunca va solo

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

Es una línea de más que se paga sola la primera vez que alguien agrega una constraint contradictoria. Sin el else, el síntoma es un scoreboard que falla en transacciones con valores absurdos y media tarde buscándolo en el DUT. El `uvm_fatal no es exageración: una transaction sin randomizar no es un estímulo malo, es un estímulo que no elegiste. Mejor que la corrida muera con un mensaje que decirle a alguien que la regresión de anoche no significaba nada.

ver esta slide en el deck ›

Transactions

Resumen de la unidad

Cierre del día 5, y conviene decir qué falta: hoy el estímulo lo genera un tester que escribiste vos; en UVM moderno eso es una uvm_sequence corriendo sobre un sequencer, y el conjunto driver + monitor + sequencer se empaqueta en un agent. Eso es el día 6.

ver esta slide en el deck ›

Constrained random

La otra mitad de la pinza

Es el par que faltaba: hasta acá el curso midió cobertura y armó las transactions, pero el estímulo seguía saliendo de un get_op() con un case escrito a mano. La frase que ordena la sección: una constraint no elige un valor, describe el conjunto de valores legales. El que elige es el solver, y elige distinto de lo que uno espera más seguido de lo que a uno le gustaría. Toda la sección es medir eso en vez de suponerlo.

ver esta slide en el deck ›

Constrained random

rand y randomize(): lo que ya viste

class command_transaction extends uvm_sequence_item;
   rand byte unsigned A;      // rand  -> entra en el sorteo
   rand byte unsigned B;
   rand operation_t   op;     // un enum se randomiza sobre sus valores

   constraint data { ... }    // las reglas, en un bloque aparte
endclass

El bullet que hay que decir despacio es el tercero, porque es el modelo mental equivocado que trae todo el mundo: un constraint no se ejecuta. No es un if largo, no corre de arriba hacia abajo, y da lo mismo el orden en que estén escritas. Es un sistema de ecuaciones, y randomize() se lo pasa a un solver —en este curso, z3— para que le devuelva una solución al azar entre todas las que lo satisfacen. De ahí sale la consecuencia del último bullet, y es la trampa más cara de la sección: dos constraints incompatibles compilan perfecto. El error aparece en tiempo de simulación, randomize() devuelve 0, y si nadie chequea el valor de retorno la clase queda con los valores que tenía. El testbench sigue andando y manda basura. La regla que se lleva de acá, y que el curso cumple en todo code/: un randomize() va siempre adentro de un if que chequee el retorno. Nunca suelto.

ver esta slide en el deck ›

Constrained random

Las otras tres palabras: randc y los dos ganchos

class command_transaction extends uvm_sequence_item;
   rand  byte unsigned A;       // puede repetir: 3 3 6 1 3 4 6 2 ...
   randc bit [2:0]     indice;  // recorre los 8 y recién ahí repite

   function void pre_randomize();   // corre ANTES del sorteo
      if (!primera) A = A_anterior; // …con los valores todavía viejos
   endfunction

   function void post_randomize();  // corre DESPUÉS, con los valores nuevos
      checksum = A ^ B;             // lo que no se sortea, se calcula acá
   endfunction
endclass

Son las tres palabras del lenguaje que faltan para que el alumno lea cualquier transaction ajena sin frenar. Ninguna es difícil; lo que hace falta es saber cuándo se usa cada una. randc es la respuesta a "quiero pasar por todos los opcodes", y el argumento es de eficiencia: con rand sobre ocho valores hacen falta unos veintidós sorteos para ver los ocho —el problema del coleccionista de figuritas—, y con randc alcanzan ocho. Y la limitación de ancho tiene una razón concreta: el solver tiene que llevarse la lista de lo que ya salió, así que un randc de 32 bits sería una tabla de cuatro mil millones de entradas. Los dos ganchos se explican mejor por lo que resuelven. post_randomize() es para el campo que se deriva de los sorteados: un CRC, una paridad, la longitud de un payload que ya se sorteó. Ponerlo como rand con una constraint que lo ate al resto es pedirle al solver que resuelva un problema que se hace con un XOR. pre_randomize() se usa menos y tiene una trampa que vale nombrar: corre antes del sorteo, o sea que los campos todavía tienen los valores de la transacción anterior. Eso lo hace útil para lo que depende del pasado —guardar la dirección previa para pedir un acceso contiguo— y peligroso si uno cree que ya están los nuevos. Y el aviso de fondo, que es el mismo de toda la sección: un campo calculado en post_randomize() está fuera del sistema de ecuaciones. El solver no lo ve, las constraints no lo restringen, y randomize() nunca va a devolver 0 por él.

ver esta slide en el deck ›

Constrained random

dist: := no es lo mismo que :/

A dist {8'h00 := 1, [8'h01 : 8'hFE] := 1, 8'hFF := 1};   // el peso va a CADA valor
A dist {8'h00 :/ 1, [8'h01 : 8'hFE] :/ 2, 8'hFF :/ 1};   // el peso se REPARTE

La cuenta hay que hacerla en el pizarrón, porque leída no entra: con := los pesos son 1, 1 y 1, pero el del medio se le aplica a cada uno de sus 254 valores. Total 256 partes iguales, y A=00 sale una de cada 256. Con :/ las partes son tres —1, 2 y 1— y A=00 sale una de cada cuatro. Sesenta y cuatro veces más seguido, por dos caracteres. Y ahora la parte que la hace peligrosa: las dos versiones andan. Ninguna da warning, ninguna falla, el testbench corre las mil transacciones igual. Lo único que cambia es que con := el bin a_00 del covergroup tarda una eternidad en llenarse, y el que mira el reporte concluye que le falta un test. Es exactamente el tipo de bug que el curso llama trampa muda, y por eso la slide siguiente no explica: mide. La regla práctica que hay que bajar: si escribís un dist, corré un histograma antes de creerle.

ver esta slide en el deck ›

Constrained random

No lo supongas: medilo

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

Histograma medido: con := los bins de borde salen 0,2 %; con :/ salen 23 % y 27 %

Este experimento salió de un bug de este mismo curso: command_transaction tenía el dist escrito con :=, o sea que el sesgo a los casos borde que las transactions decía tener no existía. La distribución era prácticamente uniforme y nadie se enteraba, porque el testbench pasaba igual. Ése es el punto de fondo de la sección, y conviene decirlo así de crudo: una constraint mal escrita no falla, miente. No hay warning, no hay error de compilación, no hay un test en rojo. Lo único que lo delata es la cobertura que no sube — y eso recién se nota semanas después. Regla práctica para llevarse: cada vez que escribas un dist, corré unas cuantas miles de randomizaciones e imprimí el histograma una vez. Cuesta diez minutos y es la diferencia entre creer y saber.

ver esta slide en el deck ›

Constrained random

inside y 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

El número que sale de correr 02_with.sv es el argumento entero de la sección: 400 intentos al azar llenan mul_max un puñado de veces —6 con la semilla por defecto, entre 2 y 8 según cuál uses— porque el dist sesga a los bordes. La cuenta conviene hacerla en voz alta: op sale uniforme entre cuatro y cada pata cae en FF una de cada cuatro, o sea 1/4 × 1/4 × 1/4 = 1 de cada 64, que en 400 intentos son seis y pico. Con el dist mal escrito de la slide anterior, la probabilidad de que las dos patas caigan en FF es 1/65536 por operación: no lo tocás nunca. La secuencia mental es siempre la misma: corro random, miro qué bin quedó vacío, escribo un with {} de tres líneas, vuelvo a correr. Nunca "escribo 76 tests". La limitación de Verilator que la sección enuncia está puesta a propósito, y conviene enunciarla bien porque no es "anda o no anda". Verilator resuelve el dist eligiendo un valor concreto primero y recién después chequea el resto: si el sorteado no cumple el with, devuelve 0 en vez de buscar otro. La tasa de éxito es entonces la probabilidad del bin. Medido: with {A == 8'hFF} resuelve el 25 % de las veces —el peso de ese bin—, with {A inside {[1:10]}} el 2 %, y un campo sin dist el 100 %. Por qué importa más de lo que parece: el síntoma es un caso dirigido intermitente, que anda cuando lo probás y falla en la regresión de la noche. Los números están en code/verilator/repro-dist-with.sv, y el rodeo —apagar la constraint— es el mismo constraint_mode() que ves más adelante.

ver esta slide en el deck ›

Constrained random

soft: el default que un with {} puede pisar

class command_transaction extends uvm_sequence_item;
   rand byte unsigned A;
   constraint por_defecto { soft A == 8'h00; }   // "cero, salvo que me pidan otra cosa"
endclass

t.randomize();                          // A = 00
t.randomize() with { A inside {[1:10]}; };   // A entre 1 y 10: el soft se cae solo

Ésta es la pieza que hace que la slide anterior escale a un testbench de verdad. El randomize() with {} sirve para pedir un caso dirigido, pero si la clase ya tiene una constraint dura que dice otra cosa, el with no la pisa: la contradice, y randomize() devuelve 0. La salida vieja era constraint_mode(0) — que obliga al que escribe el test a saber cómo se llama la constraint de una clase que no escribió. La regla de diseño que hay que dejar, porque es la que se usa en todo VIP moderno: lo que es una regla del protocolo va duro; lo que es una preferencia del estímulo va soft. Una dirección alineada a 4 es dura, porque el DUT no acepta otra cosa. Que el burst_len sea 1 por defecto es soft, porque el test que quiere ráfagas largas tiene derecho a pedirlas sin permiso. El desempate entre dos soft conviene decirlo y no usarlo: gana la última declarada, y si eso te importa para algo, la constraint estaba mal escrita. Entraron en IEEE 1800-2012, así que cualquier simulador de la última década las tiene — y Verilator también, medido acá arriba.

ver esta slide en el deck ›

Constrained random

El orden de resolución sesga sin avisar

rand bit           es_reset;
rand byte unsigned A;
constraint c {es_reset -> A == 8'h00;}   // "si pido reset, los operandos en cero"

Una celda contra un bloque de 16 por 16: las 257 soluciones, y una sola tiene el reset

La frase que desarma la intuición es la del primer bullet, y conviene repetirla tal cual: el solver elige uniformemente entre las soluciones, no entre los valores de cada campo. Nadie escribió que es_reset fuera a salir 1 la mitad de las veces; salió de suponer que cada campo se sortea por separado, y no es así. Vale contar las 257 soluciones en voz alta, porque el número convence más que el argumento: es_reset=1 obliga a A=00, o sea una sola combinación; es_reset=0 deja A libre, o sea 256. El sorteo es sobre las 257, y el reset se lleva una. La conexión con el plan de verificación es lo que hace que esta slide valga: la fila "cualquier operación después de un reset" no se llena, la cobertura queda clavada, y el reporte no dice por qué. Un alumno sin esta slide agrega tests al azar durante una tarde. Y la advertencia sobre la cura, para que no se use de más: solve ... before no cambia qué soluciones son legales, sólo el orden en que el solver elige. Es una perilla de distribución, no de corrección — y cuesta tiempo de solver, así que va donde hace falta y no por costumbre.

ver esta slide en el deck ›

Constrained random

Medido, con el rodeo que Verilator sí respeta

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%

Es el segundo agujero de Verilator del curso, junto con los bins de transición, y se trata igual: se dice de frente, con repro mínimo y con un rodeo que funciona. El concepto es del lenguaje, no del simulador. Lo que sí hay que remarcar es la asimetría: la constraint sesgada da 0,3 %, que es exactamente lo que predice la teoría — o sea que el solver de Verilator está bien; lo que falta es la directiva de orden. Y la moraleja general, que vale para cualquier herramienta: si el estímulo tiene un campo de control (un modo, un tipo de operación, un "inyectar error"), no confíes en que salga parejo. Medilo.

ver esta slide en el deck ›

Constrained random

Cuando no hay solución

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

Acá se cierra el círculo con la slide de assert(randomize()): el caso 1 es precisamente lo que pasa cuando dos constraints se contradicen, y sin el else el testbench sigue mandando la transaction anterior. Y la pareja con la slide de soft, que conviene cerrar acá: una constraint soft es la que el solver descarta sola cuando estorba. constraint_mode(0) es lo mismo pedido a mano y desde afuera, y es lo que queda cuando la constraint que estorba es dura y la clase no es tuya.

ver esta slide en el deck ›

Constrained random

Cómo se debuggea una constraint que no cierra

// 1. Que el testbench lo diga. Sin esto no hay bug: hay silencio
if (!t.randomize()) `uvm_fatal("RAND", "la constraint no cerró")

// 2. Bisección: apagar de a una hasta que cierre. La última que apagaste es
foreach (nombres[i]) begin
   t.chico.constraint_mode(0);
   if (t.randomize()) `uvm_info("RAND", "cierra sin 'chico'", UVM_LOW)
   t.chico.constraint_mode(1);
end

// 3. Sacar el campo del sorteo y ver qué valor lo estaba trabando
t.A.rand_mode(0);

Es la media hora que todo el mundo pierde una vez, y hay una forma de perderla una sola vez. Conviene decir el orden en voz alta porque es el que sirve. Paso cero, y es el que la gente se saltea: hacerlo ruidoso. Un randomize() sin if no produce un bug visible, produce estímulo viejo. El curso lo pide en todo code/ y el apéndice de trampas lo tiene catalogado; acá es donde se cobra. Paso uno: bisección. Un simulador comercial imprime el conjunto de constraints en conflicto —Questa dice Constraint solver failed y lista los bloques—, pero Verilator devuelve 0 y se calla, así que la bisección a mano es la herramienta que hay. Es aburrida y toma dos minutos, que es exactamente por qué conviene tener el reflejo en vez de mirar el código fijo. Paso dos, cuando la bisección no alcanza: el conflicto no está entre dos constraints sino entre una constraint y un campo que ya tiene valor. Ahí rand_mode(0) sobre el sospechoso lo congela y el mensaje cambia. Y la moraleja de diseño, que es la que evita el problema en vez de resolverlo: si una clase base define constraints duras que un test razonable va a querer pisar, esas constraints tenían que ser soft. Una constraint dura es una regla del DUT, no una preferencia del que escribió la clase primero.

ver esta slide en el deck ›

Constrained random

Así se cierra la cobertura

El ciclo de closure: randomizar, mirar el reporte, llenar el bin que falta, otra semilla

Cerrar volviendo al testbench convencional: el 86,8 % de u2/convencional sale de 1000 operaciones al azar con el sesgo a los bordes puesto a mano. Ahora el alumno sabe escribir ese mismo sesgo en una constraint, medirlo, y agregarle el with {} de los casos que faltan. Y dejar tirada la pregunta del día 6: si el estímulo son constraints y no código, ¿para qué sigue existiendo una clase tester con un run_phase? No sigue existiendo. Se llama sequence.

ver esta slide en el deck ›

Constrained random

«Otra semilla, y de nuevo»: qué mueve y qué no

$ SEED=7 bash run.sh    # u6/transactions/constraints, 400 randomizaciones
    seed: 7
  :/   A=00  25.0%   A=FF  22.0%   # con SEED=8: 20.8% y 27.0%
$ SEED=7 bash run.sh    # u2/convencional, y de nuevo con SEED=8
  covergroup : 86.8% (66/76)       # las dos veces. Y el merge, igual

Esta slide existe para desactivar un malentendido que el ciclo anterior invita: "si no cierro, corro otra semilla". Se midió, y no: code/u2/convencional da 66 de 76 con la semilla por defecto, con la 7 y con la 8, y el merge de las tres da 66. Los 10 que faltan no faltan por suerte — faltan porque Verilator genera los bins automáticos del enum sobre el tipo base: 3'b110 no existe en operation_t, así que auto_5 queda en 0 para siempre, con sus nueve cruces. Ninguna semilla los va a tocar. La semilla sirve para otras dos cosas, y las dos son de oficio. Primera: reproducir. Un bug intermitente no se debuggea, se repite; y para repetirlo hay que saber con qué semilla salió, que es exactamente por qué run_sim la imprime en cada corrida. Segunda: acumular, cuando el estímulo todavía no saturó. Una regresión de verdad es el mismo test por cien semillas de noche, y la cobertura se mergea — verilator_coverage --write hace en Verilator lo que el merge de ucdb en Questa. Acá no se nota porque el ejemplo es chico; en un chip es la mitad del trabajo. Y el +verilator+rand+reset+2 que aparece en la documentación es otra cosa y conviene no mezclarlas: no toca randomize(), decide con qué arrancan las señales sin inicializar del DUT. Con 2 arrancan al azar en vez de en cero, que es la forma de descubrir el registro que nadie reseteó y que en silicio no arranca en cero.

ver esta slide en el deck ›

Constrained random

Resumen de la unidad

El último bullet es el que hay que dejar grabado y el que conecta con el apéndice de las trampas. Un randomize() sin chequear no rompe: deja los campos con el valor anterior y el testbench sigue mandando estímulo que no es el que creés. La cobertura te lo va a decir tres días después. Y el aviso de herramienta que hace falta desde hoy: sin z3 instalado, Verilator resuelve randomize() devolviendo 0 en silencio. Está en docs/verilator.md, y es la razón por la que el día 5 pide el solver.

ver esta slide en el deck ›

Repaso · Día 5

1 de 8 · Copia profunda

obj1_h = obj2_h. ¿Qué copié?

Nada — no hay copia, hay un segundo nombre para el mismo objeto. Si lo modificás por un handle, el otro lo ve. De ahí sale la regla del MOOCOW: si vas a modificar, copiá primero.

ver esta slide en el deck ›

Repaso · Día 5

2 de 8 · super.do_copy()

¿Por qué cada do_copy() de la jerarquía tiene que llamar a super.do_copy()?

Si no, se pierden los campos de arriba — es el mismo problema que convert2string(): el día que alguien mete un nivel nuevo en el medio, el método deja de ver la mitad de los datos y no te avisa nadie.

ver esta slide en el deck ›

Repaso · Día 5

3 de 8 · clone()

clone() devuelve un uvm_object. ¿Por qué se recomienda escribir además un clone_me()?

Para no repetir el $cast — sin él, cada lugar que clona termina escribiendo su propio casteo. Es la misma idea de siempre: si lo vas a repetir, encapsulalo.

ver esta slide en el deck ›

Repaso · Día 5

4 de 8 · dist: := contra :/

A dist {8'h00 := 1, [8'h01:8'hFE] := 1, 8'hFF := 1}; — ¿cada cuánto sale A = 8'h00?

1 de cada 256 — := le da peso 1 a cada uno de los 254 valores del medio, así que el rango pesa 254 contra 1 y 1 de los bordes. Para repartir el peso dentro del rango va :/. Las dos formas compilan y corren: la diferencia sólo aparece en la cobertura que no sube.

ver esta slide en el deck ›

Repaso · Día 5

5 de 8 · randomize() with {}

cmd.randomize() with { A == 8'hFF; } — ¿qué pasa con las constraints que la clase ya tenía?

Se suman — el with {} agrega constraints sólo para esa llamada y no borra nada. Por eso es la herramienta para cerrar un bin sin escribir una clase nueva: tres líneas en el punto de uso, y el resto del estímulo sigue siendo el de siempre. Y por eso también choca con un dist que ya sesgue el mismo campo: las dos tienen que dar a la vez.

ver esta slide en el deck ›

Repaso · Día 5

6 de 8 · constraint_mode()

randomize() with { A == 8'hFF; } sobre un campo que la clase reparte con dist devuelve 0 tres de cada cuatro veces. ¿Cuál es el rodeo?

Apagar la constraint que estorba — Verilator resuelve el dist eligiendo un valor primero y recién después chequea el resto, así que la probabilidad de éxito es la del bin: medido, with {A == 8'hFF} resuelve el 25 % de las veces. constraint_mode(0) la apaga sólo para ese objeto y no toca a nadie más. Es exactamente la línea que pide el ejercicio d5c.

ver esta slide en el deck ›

Repaso · Día 5

7 de 8 · rand_mode()

¿Qué hace cmd.A.rand_mode(0)?

Deja de ser rand — son las dos perillas de tiempo de ejecución y se confunden seguido: rand_mode(0) saca un campo del sorteo, constraint_mode(0) apaga una constraint. Una elige qué se sortea, la otra qué reglas valen. Sirve para fijar un operando a mano y seguir randomizando el resto.

ver esta slide en el deck ›

Repaso · Día 5

8 de 8 · El orden de resolución

rand bit es_reset; rand byte unsigned A; con constraint c { es_reset -> A == 8'h00; }. ¿Cada cuánto sale es_reset = 1?

1 de cada 257 — el solver elige uniformemente entre las soluciones, no entre los valores de cada campo: es_reset=1 deja una sola combinación (A = 00) y es_reset=0 deja 256. El bin "cualquier operación después de un reset" del plan no se llena, y el reporte no dice por qué. La respuesta del lenguaje es solve es_reset before A; Verilator la acepta y no la respeta, así que el rodeo portable es pedir el reparto del campo de control con un dist.

ver esta slide en el deck ›

Ejercicio · Día 5 · 1 de 3

El scoreboard grita y el DUT está sano

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

Es el ejercicio que más se parece a un día de trabajo: el scoreboard grita y hay que decidir a quién creerle. Dejalos arrancar sin la pista. Casi todos van a mirar el RTL primero — eso ya es la mitad de la lección. Cuando se traben, la pista es la verbosidad: los uvm_info del monitor son UVM_HIGH y muestran lo que el monitor dice que vio, que no coincide con lo que el scoreboard compara. El bug es una sola línea de command_monitor.svh: el monitor copia mal uno de los dos operandos.

ver esta slide en el deck ›

Ejercicio · Día 5 · 2 de 3

Medí tu dist

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

Es el ejercicio corto del curso y el que hay que dejar para el que se quedó sin tiempo: compila y corre solo, sin UVM. La gracia está en el orden: se corre antes de tocar nada. El archivo dice 10, 80 y 10, la salida dice 0,1 % en los bordes, y ahí aparece sola la pregunta de la sección — si los números están bien, ¿qué está mal? Está mal el operador, y no hay warning que lo diga. Conviene pedirles que lo corran dos o tres veces con SEED= distintas antes de darlo por cerrado: con 4000 muestras el casillero del 10 % se mueve un punto largo entre corridas, entre 9 y 11 — por eso el corrector acepta ±2. Un porcentaje medido es una muestra, no la distribución — que es la otra mitad de la regla de la sección. Detalle de infraestructura que vale mencionar si alguien pregunta por qué tarda veinte segundos un programa que no tiene DUT: Verilator resuelve cada randomize() con constraints llamando a z3 por afuera, así que son 4000 llamadas a un proceso externo. Y si no lo tienen instalado, randomize() devuelve 0 y todos los casilleros dan cero — otra que no rompe, miente.

ver esta slide en el deck ›

Ejercicio · Día 5 · 3 de 3

Cerrar un bin

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

Es el ejercicio que le faltaba al curso, porque coverage closure aparece dos veces como sustantivo y nunca como verbo. Acá el alumno hace el ciclo entero: corre, mira qué bin quedó vacío, escribe tres líneas, vuelve a correr. La trampa está puesta a propósito y es la de constrained random: command_transaction tiene un dist sobre A y sobre B, y Verilator resuelve el dist eligiendo un valor antes de mirar el with. Con el reparto activo, with {A == 8'hFF} resuelve una de cada cuatro veces y el resto devuelve 0. El que no lea la pista va a ver un ejercicio que a veces pasa y a veces no — que es exactamente el síntoma que la sección describe. El rodeo es constraint_mode(0), y conviene defenderlo como decisión de diseño y no como parche del simulador: un caso dirigido no quiere un reparto de probabilidades, quiere un valor. La semilla del run.sh está fijada. Sin fijarla, algunas corridas llenarían el bin solas con las 60 al azar — y eso es el ejercicio con el que arranca el día 7.

ver esta slide en el deck ›