Verify This! · el curso para leer

Día 4Cómo hablan los componentes

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 4 tiene que dejar servido el problema del observer, porque la unidad entera es eso: un productor que no conoce a sus oyentes. Vale hacer la pregunta y esperar: "si mañana quieren medir cobertura además de comparar, ¿dónde tocan?". La respuesta natural —"agrego una llamada en el monitor"— es exactamente la que la unidad viene a desarmar.

ver esta slide en el deck ›

Agenda

Día 4 · ≈ 4 h · unidad 5 · cómo hablan los componentes

Al final del día podés:

El primero es la fila 11 de la autoevaluación del cierre y es el que define la unidad: el observer existe para que agregar un oyente no toque al que publica. El tercero es el segundo ejercicio del día y vale como objetivo por sí solo: el #500 es el parche que todo el mundo escribe la primera vez, y sacarlo obliga a entender qué mantiene viva la simulación.

ver esta slide en el deck ›

Un productor, muchos oyentes

Dos formas de hablar entre objetos

Esta slide es el mapa del día: las cuatro secciones que vienen son las dos columnas de abajo, en orden. Vale escribir las dos columnas en el pizarrón y volver a ellas cada vez que aparezca un puerto nuevo, porque la pregunta que hay que hacerse frente a cualquier port de UVM es siempre la misma: ¿esto puede bloquear? Si corre en mi thread, no; si corre en otro, sí, y entonces tiene que ser una task. Ése es también el criterio que ordena el vocabulario del día: write() es una function —no consume tiempo—, put() y get() son task. La firma te dice de qué lado de la división estás antes de leer una línea de la implementación.

ver esta slide en el deck ›

Un productor, muchos oyentes

El que publica no sabe quién lo lee

El ejemplo de los dados es deliberado y conviene defenderlo si alguien pregunta por qué no arrancamos con el VTALU: se ve el patrón sin el ruido del protocolo. Cuando en los analysis ports aparezca sobre el DUT, el alumno ya no está aprendiendo dos cosas a la vez. La pregunta que ordena la sección: ¿cuál es la única línea que hay que escribir para agregar un cuarto observador? Un connect(). Ninguna en el productor. Ese es el criterio con el que hay que mirar todo el código de acá en adelante. Y conviene anticipar el límite, porque es la comunicación entre threads: esto es comunicación dentro de un solo thread. write() es una function, no consume tiempo, y corre en el thread del que publica. Para hablar entre threads hace falta otra cosa.

ver esta slide en el deck ›

Un productor, muchos oyentes

El problema: dos dados, tres cosas que hay que contar

Tirar 2d6: la suma va de 2 a 12 y el 7 es la más probable

Vale plantearlo como problema de programación antes de mostrar una línea: son tres consumidores del mismo dato, y cada uno hace algo distinto con él. Nadie piensa en hardware acá, que es exactamente la idea. Los tres informes tienen algo en común que conviene hacer notar ahora: ninguno se puede calcular con una sola tirada. Los tres acumulan y recién informan al final, y por eso los tres van a necesitar dos métodos, no uno. Y una pregunta rápida para el aula, que después se cobra: si mañana piden un cuarto informe —la tirada más repetida, digamos—, ¿qué archivos hay que tocar?

ver esta slide en el deck ›

Un productor, muchos oyentes

Los tres observadores: la misma forma, tres cuerpos

code/u5/varios-objetos/01-sin-analysis-port/average.svh
class average extends uvm_component;
   `uvm_component_utils(average);

   protected real dice_total;
   protected real count;

   function new(string name, uvm_component parent = null);
      super.new(name, parent);
      dice_total = 0.0;
      count = 0.0;
   endfunction : new

   function void write(int t);
      dice_total = dice_total + t;
      count++;
   endfunction : write

   function void report_phase(uvm_phase phase);
      `uvm_info("AVERAGE", $sformatf("DICE AVERAGE: %2.1f", dice_total / count), UVM_NONE)
   endfunction : report_phase

endclass : average

Vale mostrar la clase entera una sola vez —ésta— y después sólo los write(), porque lo que interesa es que la forma se repita. Dos detalles que se cobran después: el report_phase corre después del run_phase —y después de extract y check—, no adentro, y por eso el promedio sale completo. Y dice_total y count son protected: el que publica no puede tocarlos, sólo llamar a write(). Cuando esto sea el VTALU, en los analysis ports, average va a ser el scoreboard y el report_phase el veredicto de la corrida. Misma forma.

ver esta slide en el deck ›

Un productor, muchos oyentes

Los otros dos: cambia el cuerpo, no la forma

code/u5/varios-objetos/01-sin-analysis-port/histogram.svh · #write
   function void write(int t);
      rolls[t]++;
   endfunction : write
el archivo entero, 29 líneas
class histogram extends uvm_component;
   `uvm_component_utils(histogram);

   int rolls[int];

   function new(string name, uvm_component parent);
      super.new(name, parent);
      for (int ii = 2; ii <= 12; ii++) rolls[ii] = 0;
   endfunction : new

   function void write(int t);
      rolls[t]++;
   endfunction : write

   function void report_phase(uvm_phase phase);
      string bar;
      string message;

      message = "\n";
      foreach (rolls[ii]) begin
         string roll_msg;
         bar = "";
         repeat (rolls[ii]) bar = {bar, "#"};
         roll_msg = $sformatf("%2d: %s\n", ii, bar);
         message = {message, roll_msg};
      end
      `uvm_info("HISTOGRAM", message, UVM_NONE)
   endfunction : report_phase
endclass : histogram
code/u5/varios-objetos/01-sin-analysis-port/coverage.svh · #write
   function void write(int t);
      the_roll = t;
      dice_cg.sample();
   endfunction : write
el archivo entero, 24 líneas
class coverage extends uvm_component;
   `uvm_component_utils(coverage);
   int the_roll;

   covergroup dice_cg;
      coverpoint the_roll {bins twod6[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};}
   endgroup

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

   function void write(int t);
      the_roll = t;
      dice_cg.sample();
   endfunction : write

   function void report_phase(uvm_phase phase);
      `uvm_info("COVERAGE", $sformatf("COVERAGE: %2.0f%%", dice_cg.get_inst_coverage()),
                UVM_NONE)
   endfunction : report_phase

endclass : coverage

El punto de la slide es el último bullet: tres clases que no se parecen en nada exponen la misma puerta. Cuando aparezca uvm_subscriber, esa puerta va a ser un contrato del lenguaje en vez de una convención. El coverage de acá es la respuesta a una pregunta que quedó abierta en el testbench convencional: ¿dónde muestreo? Acá el sample() no está atado a un flanco de reloj, está atado a que llegó un dato. Eso es lo correcto, y es lo que hace el coverage de todos los testbenches del curso de acá en adelante. El archivo entero de cada uno está en code/u5/varios-objetos/01-sin-analysis-port/: lo que no se muestra es el report_phase, que sólo formatea.

ver esta slide en el deck ›

Un productor, muchos oyentes

El productor: tira los dados y devuelve un número

code/u5/varios-objetos/01-sin-analysis-port/dice_roller.svh · #dice_roller
class dice_roller extends uvm_component;
   `uvm_component_utils(dice_roller);

   rand byte die1;
   rand byte die2;

   constraint d6 {
      die1 >= 1;
      die1 <= 6;
      die2 >= 1;
      die2 <= 6;
   }

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

   function byte two_dice();
      byte the_roll;
      void'(randomize());
      the_roll = die1 + die2;
      return the_roll;
   endfunction : two_dice

endclass : dice_roller
el archivo entero, 25 líneas
class dice_roller extends uvm_component;
   `uvm_component_utils(dice_roller);

   rand byte die1;
   rand byte die2;

   constraint d6 {
      die1 >= 1;
      die1 <= 6;
      die2 >= 1;
      die2 <= 6;
   }

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

   function byte two_dice();
      byte the_roll;
      void'(randomize());
      the_roll = die1 + die2;
      return the_roll;
   endfunction : two_dice

endclass : dice_roller

Conviene detenerse en la ausencia: el productor ya está bien escrito. No hay que arreglarlo, y de hecho en la versión con analysis port casi no cambia. El void'(randomize()) merece un comentario al pasar: descarta el valor de retorno a propósito. En las transactions vamos a ver por qué eso es una mala costumbre —un randomize() que falla y nadie se entera— y cómo se escribe bien. La pregunta para dejar picando: si el productor no tiene la lista, ¿quién la tiene? La slide que sigue.

ver esta slide en el deck ›

Un productor, muchos oyentes

El que reparte: el test, y no debería

code/u5/varios-objetos/01-sin-analysis-port/dice_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      // This part needs attention.
      // What belongs here is:
      //	the Observer design pattern
      //	a UVM analysis port
      // Otherwise none of the OOP magic is being used:
      // these are plain functions passing data along
      repeat (20) begin
         the_roll = dice_roller_h.two_dice();
         coverage_h.write(the_roll);
         histogram_h.write(the_roll);
         average_h.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 38 líneas
class dice_test extends uvm_test;
   `uvm_component_utils(dice_test);

   dice_roller dice_roller_h;
   coverage coverage_h;
   histogram histogram_h;
   average average_h;

   function void build_phase(uvm_phase phase);
      coverage_h = new("coverage_h", this);
      histogram_h = new("histogram_h", this);
      average_h = new("average_h", this);
      dice_roller_h = new("dice_roller_h", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      // This part needs attention.
      // What belongs here is:
      //	the Observer design pattern
      //	a UVM analysis port
      // Otherwise none of the OOP magic is being used:
      // these are plain functions passing data along
      repeat (20) begin
         the_roll = dice_roller_h.two_dice();
         coverage_h.write(the_roll);
         histogram_h.write(the_roll);
         average_h.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase

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

endclass : dice_test

Esta slide es el "antes", y hay que dejar que se vea feo. Mirá el repeat (20): tres write() escritos a mano, uno por observador. Agregar un cuarto es tocar esta clase; sacar uno, también. Peor todavía, y es lo que conviene señalar: el que quedó a cargo de repartir el dato es el test. O sea que el dice_roller produce y el test distribuye — dos responsabilidades que no tienen por qué estar juntas, y la de distribuir es la que va a cambiar todo el tiempo. La pregunta para tirar antes de pasar a la siguiente: si esto fuera el VTALU, ¿quién sería el dice_roller y quiénes los tres write()? El monitor, y el scoreboard más la cobertura. Los analysis ports son literalmente esta slide con otros nombres.

ver esta slide en el deck ›

Un productor, muchos oyentes

Observer Design Pattern

Observer: un emisor publica y N observadores reciben

La analogía funciona mejor dada vuelta: el que publica no sabe quién lo lee, y ésa es justo la propiedad que querés en un monitor. Agregar un subscriber nuevo —otra cobertura, un log— no toca una línea del que produce el dato. El ejemplo de los dados es a propósito ajeno al hardware: se ve el patrón sin el ruido del protocolo.

ver esta slide en el deck ›

Un productor, muchos oyentes

El patrón, ya hecho: las dos puntas del cable

Notación: rombo = uvm_analysis_port, círculo = analysis_export

Los nombres cuestan un minuto y ahorran media hora después: port es la punta del que produce, export la del que consume, y el connect() siempre se llama sobre el port. uvm_analysis_port es un broadcast: no le importa si tiene cero, uno o diez subscribers, y escribir en un port sin conectar no es un error — el dato se pierde en silencio. Es una de las trampas mudas del curso, y el síntoma es un scoreboard que nunca reporta nada. uvm_subscriber es la otra punta y es una clase parametrizada: #(int) acá, #(command_transaction) en los analysis ports. La conexión queda tipada por el compilador, no por un string.

ver esta slide en el deck ›

Un productor, muchos oyentes

uvm_analysis_port

code/u5/varios-objetos/02-con-analysis-port/dice_roller.svh · #dice_roller
// Same as version 01, but publishing: instead of returning the number it writes
// it to an analysis port, and stops knowing who reads it.
class dice_roller extends uvm_component;
   `uvm_component_utils(dice_roller);

   rand byte die1;
   rand byte die2;

   constraint d6 {
      die1 >= 1;
      die1 <= 6;
      die2 >= 1;
      die2 <= 6;
   }

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

   // 1) declare the port, with the data type it will carry
   uvm_analysis_port #(int) roll_ap;

   // 2) instantiate it in build_phase, with new(): ports do not go through the factory
   function void build_phase(uvm_phase phase);
      roll_ap = new("roll_ap", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      repeat (20) begin
         void'(randomize());
         the_roll = die1 + die2;
         // 3) write: the port calls write() on every subscriber
         roll_ap.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase

endclass : dice_roller
el archivo entero, 40 líneas
// Same as version 01, but publishing: instead of returning the number it writes
// it to an analysis port, and stops knowing who reads it.
class dice_roller extends uvm_component;
   `uvm_component_utils(dice_roller);

   rand byte die1;
   rand byte die2;

   constraint d6 {
      die1 >= 1;
      die1 <= 6;
      die2 >= 1;
      die2 <= 6;
   }

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

   // 1) declare the port, with the data type it will carry
   uvm_analysis_port #(int) roll_ap;

   // 2) instantiate it in build_phase, with new(): ports do not go through the factory
   function void build_phase(uvm_phase phase);
      roll_ap = new("roll_ap", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      repeat (20) begin
         void'(randomize());
         the_roll = die1 + die2;
         // 3) write: the port calls write() on every subscriber
         roll_ap.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase

endclass : dice_roller

Tres pasos y ninguno más: declarar el port, instanciarlo en build_phase, escribir con write(). Del otro lado el subscriber implementa write() y se conecta en connect_phase. El detalle que sorprende: los ports no se crean con la factory, se instancian con new().

ver esta slide en el deck ›

Un productor, muchos oyentes

uvm_subscriber

code/u5/varios-objetos/02-con-analysis-port/coverage.svh
// uvm_subscriber:
//    extends the uvm_component class and lets you hook onto an analysis port
//    It is a parameterizable class!
//    The class gives you something and asks for something in return
//       * It gives you an object called analysis_export
//       * It asks you to write a write() method that handles the data

class coverage extends uvm_subscriber #(int);
   `uvm_component_utils(coverage);
   int the_roll;

   covergroup dice_cg;
      coverpoint the_roll {bins twod6[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};}
   endgroup

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

   function void write(int t);
      the_roll = t;
      dice_cg.sample();
   endfunction : write

   function void report_phase(uvm_phase phase);
      `uvm_info("COVERAGE", $sformatf("COVERAGE: %2.0f%%", dice_cg.get_inst_coverage()),
                UVM_NONE)
   endfunction : report_phase

endclass : coverage

uvm_subscriber es un trato en dos partes y conviene enunciarlo así: te doy un analysis_export, me devolvés un write(). Nada más. El analysis_export no se declara ni se instancia — viene con la clase. El #(int) del extends es las clases paramétricas cobrando otra vez: define de qué tipo es el t del write(), y el compilador se encarga de que un puerto de int no se pueda conectar a un subscriber de otra cosa. La conexión está tipada, no es un string. Comparar con la versión anterior de esta misma clase vale la pena: el write() es idéntico. Lo único que cambió es de quién hereda y quién lo llama. Ese es el punto entero de la sección — el observador no se entera de que ahora es un observador. Y un detalle que aparece en la salida: acá la cobertura se lee con get_inst_coverage(), no con get_coverage(). Es una limitación de Verilator —la type-wide devuelve siempre 0— y está en docs/verilator.md.

ver esta slide en el deck ›

Un productor, muchos oyentes

El productor, ahora publicando

code/u5/varios-objetos/02-con-analysis-port/dice_roller.svh · #port-and-run
   // 1) declare the port, with the data type it will carry
   uvm_analysis_port #(int) roll_ap;

   // 2) instantiate it in build_phase, with new(): ports do not go through the factory
   function void build_phase(uvm_phase phase);
      roll_ap = new("roll_ap", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      repeat (20) begin
         void'(randomize());
         the_roll = die1 + die2;
         // 3) write: the port calls write() on every subscriber
         roll_ap.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 40 líneas
// Same as version 01, but publishing: instead of returning the number it writes
// it to an analysis port, and stops knowing who reads it.
class dice_roller extends uvm_component;
   `uvm_component_utils(dice_roller);

   rand byte die1;
   rand byte die2;

   constraint d6 {
      die1 >= 1;
      die1 <= 6;
      die2 >= 1;
      die2 <= 6;
   }

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

   // 1) declare the port, with the data type it will carry
   uvm_analysis_port #(int) roll_ap;

   // 2) instantiate it in build_phase, with new(): ports do not go through the factory
   function void build_phase(uvm_phase phase);
      roll_ap = new("roll_ap", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      int the_roll;
      phase.raise_objection(this);
      repeat (20) begin
         void'(randomize());
         the_roll = die1 + die2;
         // 3) write: the port calls write() on every subscriber
         roll_ap.write(the_roll);
      end
      phase.drop_objection(this);
   endtask : run_phase

endclass : dice_roller

La comparación con la versión anterior es la slide entera: el dice_roller no ganó ningún handle a nadie. Ganó un port. Por qué los ports van con new() y no con type_id::create(): la factory está para sustituir tipos, y un port no se sustituye nunca. Es una pregunta que sale siempre y la respuesta corta es ésa. Y el write() de acá conviene mirarlo con la comunicación entre threads en mente: es una function, así que no consume tiempo y corre en el thread del productor. Cuando el que consume necesite hacer esperar al que produce, esto no alcanza.

ver esta slide en el deck ›

Un productor, muchos oyentes

connect_phase(): dónde se arma el cableado

El orden de las dos fases no es un detalle de implementación, es lo que hace que esto funcione: no se puede conectar lo que todavía no existe. Por eso build_phase es top-down y termina entera antes de que arranque el primer connect_phase. Que connect_phase sea bottom-up importa menos en la práctica, pero tiene su razón: un componente compuesto conecta sus hijos entre sí recién cuando cada hijo ya conectó lo suyo. En los agents, cuando el agent conecte driver y sequencer adentro suyo y el env conecte el agent hacia afuera, se va a ver. El analysis_export que aparece solo es de las pocas cosas de UVM que son gratis: viene con la clase, y olvidarse de instanciarlo no es un error porque no hay nada que instanciar.

ver esta slide en el deck ›

Un productor, muchos oyentes

connect_phase(): dónde se arma el cableado

code/u5/varios-objetos/02-con-analysis-port/dice_test.svh · #build-and-connect
   // build_phase: UVM calls it TOP-DOWN, the parent first.
   function void build_phase(uvm_phase phase);
      dice_roller_h = new("dice_roller_h", this);
      coverage_h = new("coverage_h", this);
      histogram_h = new("histogram_h", this);
      average_h = new("average_h", this);
   endfunction : build_phase

   // connect_phase: BOTTOM-UP, and always port.connect(export).
   function void connect_phase(uvm_phase phase);
      dice_roller_h.roll_ap.connect(coverage_h.analysis_export);
      dice_roller_h.roll_ap.connect(histogram_h.analysis_export);
      dice_roller_h.roll_ap.connect(average_h.analysis_export);
   endfunction : connect_phase
el archivo entero, 30 líneas
// The structure of the example: it instantiates the four components and WIRES the
// producer to the three observers. The run_phase is gone.
class dice_test extends uvm_test;
   `uvm_component_utils(dice_test);

   dice_roller dice_roller_h;
   coverage    coverage_h;
   histogram   histogram_h;
   average     average_h;

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

   // build_phase: UVM calls it TOP-DOWN, the parent first.
   function void build_phase(uvm_phase phase);
      dice_roller_h = new("dice_roller_h", this);
      coverage_h = new("coverage_h", this);
      histogram_h = new("histogram_h", this);
      average_h = new("average_h", this);
   endfunction : build_phase

   // connect_phase: BOTTOM-UP, and always port.connect(export).
   function void connect_phase(uvm_phase phase);
      dice_roller_h.roll_ap.connect(coverage_h.analysis_export);
      dice_roller_h.roll_ap.connect(histogram_h.analysis_export);
      dice_roller_h.roll_ap.connect(average_h.analysis_export);
   endfunction : connect_phase

endclass : dice_test

Éste es el "después", y hay que ponerlo al lado del "antes", la versión sin analysis port. El run_phase del test se quedó sin los tres write(): ahora sólo levanta el objection y arranca al productor. La distribución se mudó al connect_phase, que es donde va la estructura. La línea que hay que leer despacio es dice_roller_h.roll_ap.connect(coverage_h.analysis_export), y la dirección importa: el port se conecta al export, nunca al revés. Es el mantra que vuelve en threads, en put y get y en agents, y el compilador no siempre te ataja. Regla de bolsillo para acordarse de la dirección, y hay que decirla así porque la versión fácil se rompe enseguida: llama al connect() el que tiene el port, y se lo pasa el export del otro. Acá el que tiene el port es el productor; en put y get el uvm_get_port lo tiene el consumidor, y ahí es el consumidor el que conecta. "Conecta el que produce" funciona en esta slide y falla en la siguiente sección. Y la prueba de fuego de la sección: para agregar un cuarto observador hay que escribir la clase y una línea acá. Ni una en dice_roller.

ver esta slide en el deck ›

Un productor, muchos oyentes

La solución, cableada

Conexionado del ejemplo de los dados: roll_ap contra los tres analysis_export

Es el primer diagrama TLM del curso y conviene enseñar a leerlo ahora, porque los del resto del día y el de los agents usan la misma convención: rombo es analysis port, cuadrado es put/get port, círculo es export. La flecha va del que produce al que consume. Lo que hay que hacer notar del dibujo es que del roll_ap salen tres flechas y del dice_roller sale una sola línea de código: el write(). La multiplicidad no está en el productor, está en el cableado. Y el cierre de la sección, que enlaza con el siguiente: todo esto pasó adentro de un solo thread. write() es una function, no puede tener un @ ni un #, y volvió antes de que avanzara el tiempo. Cuando el que produce y el que consume tengan que esperarse, esto no alcanza — y eso es la comunicación entre threads.

ver esta slide en el deck ›

Un productor, muchos oyentes

Resumen de la unidad

La slide con la que conviene abrir y cerrar el día 4, porque es el mapa. Escribir las dos columnas en el pizarrón y volver a ellas cada vez que aparezca un puerto nuevo. Y el criterio que ordena el vocabulario entero: write() es una function —no consume tiempo—, put() y get() son task. En SystemVerilog eso no es una convención de nombres, es el lenguaje diciéndote de qué lado estás.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Un solo lugar que mira el cable

Es el mismo patrón de los dados, ahora sobre el DUT — y ésa es toda la sección. Lo que hay que remarcar es cuál es la línea divisoria del testbench, porque es la que ordena todo lo que viene: de un lado del monitor se habla en señales, del otro en transacciones. El BFM de interfaces y BFM hizo eso para el estímulo; el monitor lo hace para el análisis. La consecuencia práctica vale decirla en voz alta: a partir de acá, cualquier componente de análisis se puede escribir sin saber nada del protocolo. Un subscriber nuevo no necesita saber que existe un done. Y la que se ve recién en los agents: si todo lo que sabe hablar el protocolo está junto —BFM, monitores, driver—, esa caja se puede instanciar dos veces. Eso es el agent, y esta sección es el que junta las piezas.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Qué piezas aparecen

La slide es el inventario de la sección y conviene usarla como mapa: son cinco piezas y sólo dos son nuevas. Vale decir en voz alta cuáles, porque el alumno tiende a creer que un tema nuevo reescribe el testbench entero. Que haya dos monitores y no uno es la decisión que más se pregunta, así que mejor adelantarla: miran cosas distintas y en momentos distintos. El command_monitor mira cuando arranca una operación; el result_monitor, cuando el DUT contesta. Entre esos dos instantes hay ciclos de por medio, así que no pueden ser el mismo always. Y el último bullet es el que hay que subrayar, porque es la promesa que la sección cumple: el tester no se toca. Todo el trabajo de esta sección pasa del lado del análisis, y el lado del estímulo ni se entera. Es la primera vez que el curso separa las dos mitades del testbench, y en la sección siguiente se hace la otra.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Diagrama del Testbench

Testbench del VTALU con analysis ports

ver esta slide en el deck ›

Un solo lugar que mira el cable

Los handles que la BFM guarda

code/u5/analysis-ports/vtalu_bfm.sv · #handles
interface vtalu_bfm;
   import vtalu_pkg::*;

   // These handles get assigned in the build_phases of the classes
   command_monitor        command_monitor_h;
   result_monitor         result_monitor_h;

   byte unsigned          A;
   byte unsigned          B;
   bit                    clk;
   bit                    reset_n;
   bit                    start;
   wire                   done;
   wire            [15:0] result;
   wire                   ovf;
   operation_t            op_set;
el archivo entero, 87 líneas
interface vtalu_bfm;
   import vtalu_pkg::*;

   // These handles get assigned in the build_phases of the classes
   command_monitor        command_monitor_h;
   result_monitor         result_monitor_h;

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

   // Here is the first monitor (commands)
   // A very simple FSM. If start is up, check whether this is a new command
   // If it is, send it to the TB with commands_monitor.write_to_monitor()
   always @(posedge clk) begin : cmd_monitor
      bit new_command;
      if (!start) new_command = 1;
      else if (new_command) begin
         command_monitor_h.write_to_monitor(A, B, op_set);
         new_command = (op_set == 3'b000);  // handle no_op
      end
   end : cmd_monitor

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

   always @(posedge clk) begin : rslt_monitor
      if (done) result_monitor_h.write_to_monitor(result);
   end : rslt_monitor

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


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

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

   task send_op(input byte iA, input byte iB, input operation_t iop);
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            do @(negedge clk); while (done == 0);
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)

   endtask : send_op

endinterface : vtalu_bfm
code/u5/analysis-ports/tb_classes/command_monitor.svh · #build_phase
   function void build_phase(uvm_phase phase);
      virtual vtalu_bfm bfm;

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

      bfm.command_monitor_h = this;

      ap = new("ap", this);

   endfunction : build_phase
el archivo entero, 46 líneas
class command_monitor extends uvm_component;
   `uvm_component_utils(command_monitor);

   uvm_analysis_port #(command_s) ap;

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

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

      bfm.command_monitor_h = this;

      ap = new("ap", this);

   endfunction : build_phase

   function void write_to_monitor(byte A, byte B, bit [2:0] op);
      command_s cmd;
      cmd.A = A;
      cmd.B = B;
      cmd.op = op2enum(op);
      `uvm_info("COMMAND MONITOR", $sformatf("A:0x%2h B:0x%2h op: %s", A, B, cmd.op.name()
                ), UVM_HIGH)
      ap.write(cmd);
   endfunction : write_to_monitor

   function operation_t op2enum(bit [2:0] op);
      case (op)
         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'b111: return rst_op;
         default:
         `uvm_fatal("COMMAND MONITOR", $sformatf("Illegal operation on op bus: %3b", op))
      endcase  // case (op)
   endfunction : op2enum

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

endclass : command_monitor

Esta slide tiene la inversión que más cuesta de la sección y conviene decirla despacio: hasta ahora la clase leía la interface. Acá es al revés — la interface tiene un handle a la clase (bfm.command_monitor_h = this) y es ella la que llama al método. El motivo es que un always de la interface sí puede tener lista de sensibilidad, y una función de clase no. Necesitamos que el hardware avise; para eso el hardware tiene que saber a quién avisarle. La línea que hace todo es bfm.command_monitor_h = this, y va en el build_phase. Si te la olvidás, el monitor compila, corre, y no publica nunca: el scoreboard se queda sin comandos y el uvm_fatal aparece del otro lado, en una clase que no tiene la culpa. Es de esos errores que se buscan en el lugar equivocado. Detalle que ayuda: el handle es de la clase, no virtual. Por eso el always puede llamar directo a write_to_monitor() sin el agujero de Verilator que afecta a las tasks — acá el que declara la task es la clase, no la interface.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Monitoreando los comandos del VTALU

code/u5/analysis-ports/vtalu_bfm.sv · #monitors
   // Here is the first monitor (commands)
   // A very simple FSM. If start is up, check whether this is a new command
   // If it is, send it to the TB with commands_monitor.write_to_monitor()
   always @(posedge clk) begin : cmd_monitor
      bit new_command;
      if (!start) new_command = 1;
      else if (new_command) begin
         command_monitor_h.write_to_monitor(A, B, op_set);
         new_command = (op_set == 3'b000);  // handle no_op
      end
   end : cmd_monitor

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

   always @(posedge clk) begin : rslt_monitor
      if (done) result_monitor_h.write_to_monitor(result);
   end : rslt_monitor
el archivo entero, 87 líneas
interface vtalu_bfm;
   import vtalu_pkg::*;

   // These handles get assigned in the build_phases of the classes
   command_monitor        command_monitor_h;
   result_monitor         result_monitor_h;

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

   // Here is the first monitor (commands)
   // A very simple FSM. If start is up, check whether this is a new command
   // If it is, send it to the TB with commands_monitor.write_to_monitor()
   always @(posedge clk) begin : cmd_monitor
      bit new_command;
      if (!start) new_command = 1;
      else if (new_command) begin
         command_monitor_h.write_to_monitor(A, B, op_set);
         new_command = (op_set == 3'b000);  // handle no_op
      end
   end : cmd_monitor

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

   always @(posedge clk) begin : rslt_monitor
      if (done) result_monitor_h.write_to_monitor(result);
   end : rslt_monitor

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


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

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

   task send_op(input byte iA, input byte iB, input operation_t iop);
      if (iop == rst_op) begin
         @(posedge clk);
         reset_n = 1'b0;
         start = 1'b0;
         @(posedge clk);
         #1;
         reset_n = 1'b1;
      end else begin
         @(negedge clk);
         op_set = iop;
         A = iA;
         B = iB;
         start = 1'b1;
         if (iop == no_op) begin
            @(posedge clk);
            #1;
            start = 1'b0;
         end else begin
            do @(negedge clk); while (done == 0);
            start = 1'b0;
         end
      end  // else: !if(iop == rst_op)

   endtask : send_op

endinterface : vtalu_bfm
code/u5/analysis-ports/tb_classes/command_monitor.svh · #write_to_monitor
   function void write_to_monitor(byte A, byte B, bit [2:0] op);
      command_s cmd;
      cmd.A = A;
      cmd.B = B;
      cmd.op = op2enum(op);
      `uvm_info("COMMAND MONITOR", $sformatf("A:0x%2h B:0x%2h op: %s", A, B, cmd.op.name()
                ), UVM_HIGH)
      ap.write(cmd);
   endfunction : write_to_monitor
el archivo entero, 46 líneas
class command_monitor extends uvm_component;
   `uvm_component_utils(command_monitor);

   uvm_analysis_port #(command_s) ap;

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

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

      bfm.command_monitor_h = this;

      ap = new("ap", this);

   endfunction : build_phase

   function void write_to_monitor(byte A, byte B, bit [2:0] op);
      command_s cmd;
      cmd.A = A;
      cmd.B = B;
      cmd.op = op2enum(op);
      `uvm_info("COMMAND MONITOR", $sformatf("A:0x%2h B:0x%2h op: %s", A, B, cmd.op.name()
                ), UVM_HIGH)
      ap.write(cmd);
   endfunction : write_to_monitor

   function operation_t op2enum(bit [2:0] op);
      case (op)
         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'b111: return rst_op;
         default:
         `uvm_fatal("COMMAND MONITOR", $sformatf("Illegal operation on op bus: %3b", op))
      endcase  // case (op)
   endfunction : op2enum

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

endclass : command_monitor

Los tres always son la costura entre el hardware y las clases, y conviene leer la condición de cada uno: el de comandos dispara cuando start sube y no había un comando en vuelo; el de reset, en el flanco de bajada de reset_n; el de resultado, cuando el DUT levanta done. La guarda if (command_monitor_h != null) no es paranoia: en el tiempo 0 el árbol de UVM todavía no existe y el negedge reset_n ya llegó. Sin la guarda es un null pointer del simulador. El write_to_monitor() es corto a propósito: arma la command_s, la loguea con UVM_HIGH —o sea que sólo se ve con +UVM_VERBOSITY=UVM_HIGH— y llama a ap.write(). Todo lo demás pasa del otro lado del port, y el monitor no sabe quién está ahí. Y el detalle que se cobra en las transactions: lo que viaja todavía es una struct. El día que sea una transaction, esta misma línea va a ser un create().

ver esta slide en el deck ›

Un solo lugar que mira el cable

VTALU Coverage Class como Subscriber

code/u5/analysis-ports/tb_classes/coverage.svh · #write
   // With the analysis port this is far simpler,
   // because nothing here has to know about the ALU signals:
   // what arrives is a command_s struct that wraps the command
   //
   function void write(command_s t);
         A = t.A;
         B = t.B;
         op_set = t.op;
         op_cov.sample();
         zeros_or_ones_on_ops.sample();
   endfunction : write
el archivo entero, 127 líneas
class coverage extends uvm_subscriber #(command_s);
   `uvm_component_utils(coverage)

   byte         unsigned        A;
   byte         unsigned        B;
   operation_t  op_set;

   covergroup op_cov;

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

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

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

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

         bins rstmulrst   = (rst_op => mul_op [=  2] => rst_op);
         bins rstmulrstim = (rst_op => mul_op [-> 2] => rst_op);
`endif

      }

   endgroup

   covergroup zeros_or_ones_on_ops;

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

      }

endgroup

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

   // With the analysis port this is far simpler,
   // because nothing here has to know about the ALU signals:
   // what arrives is a command_s struct that wraps the command
   //
   function void write(command_s t);
         A = t.A;
         B = t.B;
         op_set = t.op;
         op_cov.sample();
         zeros_or_ones_on_ops.sample();
   endfunction : write

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

endclass : coverage

Vale abrir al lado la clase coverage del testbench en objetos y comparar: allá había un forever esperando flancos; acá hay un write() y nada más. La cobertura dejó de tener noción del tiempo. Y eso no es cosmético, es la respuesta a la pregunta que quedó abierta en el testbench convencional: dónde se muestrea. Antes se muestreaba por reloj, o sea que se contaban ciclos; ahora se muestrea cuando el monitor ve un comando, o sea que se cuentan operaciones. Es lo correcto, y el número de cobertura cambia de significado. La pregunta para el grupo: si el DUT quedara diez ciclos sin hacer nada, ¿cuántas muestras tomaba la versión vieja y cuántas ésta? Diez y cero. La vieja estaba llenando bins con la nada.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Suscribiéndose a múltiples analysis ports

El scoreboard necesita dos fuentes —el comando y el resultado— y un uvm_subscriber puede escuchar una sola. La FIFO convierte "me avisan cuando llega" en "lo busco cuando quiero", que es lo que necesita para comparar de a pares.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Suscribiéndose a múltiples analysis ports

code/u5/analysis-ports/tb_classes/scoreboard.svh · #two-ports
   uvm_tlm_analysis_fifo #(command_s) cmd_f;

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

   // Every time the DUT hands a result to the analysis port, this method
   // gets triggered
   function void write(shortint t);
      shortint  predicted_result;
      command_s cmd;
      cmd.op = no_op;

      // Here is where the prediction happens: look in our FIFO for the command
      // that was sent, skipping the resets and the no_ops with the
      // do....while
      // since whenever there is a result there must be an operation,
      // a result with no operation behind it is an error
      do
      if (!cmd_f.try_get(cmd)) `uvm_fatal("SCOREBOARD", "No command in self checker")
      while ((cmd.op == no_op) || (cmd.op == rst_op));

      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)

      if (predicted_result != t)
         `uvm_error("SCOREBOARD", $sformatf(
                    "FAILED: A: %2h  B: %2h  op: %s actual result: %4h   expected: %4h",
                    cmd.A,
                    cmd.B,
                    cmd.op.name(),
                    t,
                    predicted_result
                    ))
   endfunction : write
el archivo entero, 55 líneas
// The basic UVM analysis port mechanism lets a uvm_subscriber watch 1 port
// To analyze 2 ports, the simplest way is to instantiate another subscriber
// inside our class that listens to that port
// For that, UVM provides a class called uvm_tlm_analysis_fifo
//	that class gives us an analysis_export and the try_get() method

class scoreboard extends uvm_subscriber #(shortint);
   `uvm_component_utils(scoreboard);

   uvm_tlm_analysis_fifo #(command_s) cmd_f;

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

   // Every time the DUT hands a result to the analysis port, this method
   // gets triggered
   function void write(shortint t);
      shortint  predicted_result;
      command_s cmd;
      cmd.op = no_op;

      // Here is where the prediction happens: look in our FIFO for the command
      // that was sent, skipping the resets and the no_ops with the
      // do....while
      // since whenever there is a result there must be an operation,
      // a result with no operation behind it is an error
      do
      if (!cmd_f.try_get(cmd)) `uvm_fatal("SCOREBOARD", "No command in self checker")
      while ((cmd.op == no_op) || (cmd.op == rst_op));

      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)

      if (predicted_result != t)
         `uvm_error("SCOREBOARD", $sformatf(
                    "FAILED: A: %2h  B: %2h  op: %s actual result: %4h   expected: %4h",
                    cmd.A,
                    cmd.B,
                    cmd.op.name(),
                    t,
                    predicted_result
                    ))
   endfunction : write

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

endclass : scoreboard

El scoreboard es asimétrico y ahí está toda la gracia: el resultado le llega por write() —lo empujan— y el comando lo va a buscar él con try_get() a la FIFO. Uno es push, el otro es pull. Por qué así y no dos write(): un uvm_subscriber tiene un solo write(), y además el orden importa. El scoreboard no quiere que le avisen del comando cuando llega; lo quiere en el momento en que aparece el resultado, para poder comparar de a pares. La FIFO convierte "me avisan" en "lo busco cuando quiero". El do ... while que saltea no_op y rst_op es la parte que se copia mal: esas dos operaciones no producen resultado, así que si no se descartan, la comparación se desfasa un lugar y a partir de ahí falla todo. El síntoma es un scoreboard que grita en cada línea, y es exactamente el ejercicio del día 5. Y el uvm_fatal del try_get() no es paranoia: si hay un resultado y no hay comando, el que está roto es el testbench, no el DUT. Mejor morir ahí que reportar mil errores falsos.

ver esta slide en el deck ›

Un solo lugar que mira el cable

La otra forma: `uvm_analysis_imp_decl

`uvm_analysis_imp_decl(_cmd)        // fabrica la clase uvm_analysis_imp_cmd
`uvm_analysis_imp_decl(_result)

class scoreboard extends uvm_component;
   uvm_analysis_imp_cmd    #(command_s, scoreboard) cmd_imp;
   uvm_analysis_imp_result #(shortint,  scoreboard) result_imp;

   function void write_cmd(command_s t);    ... endfunction   // un write por puerto
   function void write_result(shortint t);  ... endfunction
endclass

Mismo trato que las macros `uvm_field_* de los components y que `uvm_do de las sequences: se nombran, se explican, y se dice por qué el curso no las usa. La regla del curso, otra vez: leer todas, escribir las explícitas. Lo que la macro esconde y conviene decir: uvm_analysis_imp es una clase que ya existe en UVM; lo único que hace el `uvm_analysis_imp_decl es fabricar variantes con nombre distinto, porque SystemVerilog no deja tener dos write() en la misma clase. O sea que la macro no agrega mecanismo, agrega nombres. Dónde va: en el package, fuera de toda clase, y una sola vez por sufijo en todo el testbench. Declararla dos veces con el mismo sufijo es un error de compilación que aparece lejos del lugar donde uno lo escribió. Y la comparación honesta: con la macro el scoreboard queda más corto y más acoplado —tiene que llevarse el comando a mano hasta que llegue el resultado—; con la FIFO queda una línea más largo y el orden lo decide él. Para un scoreboard que empareja de a pares, la FIFO gana. Para un componente que sólo cuenta cosas de dos puertos, la macro es mejor.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Suscribiendo los monitores

code/u5/analysis-ports/tb_classes/env.svh
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester   random_tester_h;
   coverage        coverage_h;
   scoreboard      scoreboard_h;
   command_monitor command_monitor_h;
   result_monitor  result_monitor_h;

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

   function void build_phase(uvm_phase phase);
      random_tester_h = random_tester::type_id::create("random_tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);

   endfunction : build_phase

   function void connect_phase(uvm_phase phase);

      result_monitor_h.ap.connect(scoreboard_h.analysis_export);
      command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export);
      command_monitor_h.ap.connect(coverage_h.analysis_export);

   endfunction : connect_phase

endclass

Tres connect() y ahí está el testbench entero. Vale leerlos en voz alta como frases: el resultado va al scoreboard, el comando va a la FIFO del scoreboard, el comando también va a la cobertura. El que sorprende es el segundo: command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export). El export no es del scoreboard, es de la FIFO que el scoreboard tiene adentro. Un uvm_analysis_port puede tener varios destinos —el ap del command_monitor alimenta dos— y no le cuesta nada: write() los recorre a todos. Y el detalle que se olvida y no avisa: cmd_f se instancia con new(), no con la factory. La FIFO sí está registrada —`uvm_component_param_utils en tlm1/uvm_tlm_fifos.svh:256—, pero el curso la crea con new() porque le pasa el size por constructor y create() no puede. Los que no están en la factory son los *ports* y los *exports*: extienden uvm_port_base sin macro de utils. Si te olvidás del new() el connect() explota con un null, y ese al menos es un error honesto.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Cuando el DUT no contesta en orden

transaccion esperado [int];              // asociativo, indexado por lo que aparea

function void write_cmd(transaccion c);  esperado[c.id] = c;  endfunction

function void write_result(respuesta r);
   if (!esperado.exists(r.id)) `uvm_error("SB", "una respuesta que nadie pidio")
   else begin comparar(esperado[r.id], r); esperado.delete(r.id); end
endfunction

La pregunta que abre la slide es la que hay que hacerle a cualquier scoreboard ajeno antes de confiar en él: "¿y si el DUT contesta desordenado?". El de esta sección se cae, y está bien que se caiga — la VTALU no lo hace. Lo que no está bien es no saberlo. El síntoma cuando pasa es cruel y conviene anticiparlo, porque manda a buscar al lugar equivocado: no falla una comparación, fallan todas las que vienen después de la primera que se cruzó. El testbench dice mil errores y el DUT tiene cero. Es la fila "el scoreboard grita en todas" del apéndice de debug, con otra causa. La tabla asociativa es la respuesta canónica y no tiene misterio; lo que se olvida siempre es el check_phase que verifica que quedó vacía. Sin eso, un DUT que deja de contestar pasa en verde: nunca hay una comparación mal, simplemente hay comparaciones que no se hicieron. Es el mismo agujero que el drain_time de los tests, visto desde el otro lado. Y la variante que aparece cuando no hay ID: aparear por contenido. Se guarda una cola de esperados y se busca el que matchea, en vez del primero. Es más caro y tiene una trampa propia —dos transacciones idénticas—, y por eso el ID existe.

ver esta slide en el deck ›

Un solo lugar que mira el cable

De un lado del monitor se habla en señales, del otro en transactions

La frase del título es el resumen real de la sección y conviene dejarla escrita: de un lado del monitor se habla en señales, del otro en transactions. Esa frontera es lo que hace que el scoreboard de las transactions pueda existir sin mirar un cable. El tercer bullet es la bisagra hacia la comunicación entre threads: como es todo una cadena de function, ningún eslabón puede esperar. Si el que consume necesita hacer esperar al que produce —o al revés—, hace falta otra cosa, y eso es la uvm_tlm_fifo. Vale hacer la pregunta antes de pasar: ¿qué pasaría si el write() de un subscriber tuviera un @(posedge clk) adentro? No compila. Una function no puede consumir tiempo, y ésa es la limitación entera.

ver esta slide en el deck ›

Un solo lugar que mira el cable

Resumen de la unidad

Cierre de la unidad donde el testbench toma la forma que va a tener hasta el final. Vale decirlo así: de acá en adelante, cada vez que haya que analizar algo nuevo —una cobertura más, un checker más— la respuesta va a ser siempre la misma, colgar un subscriber, y nunca tocar el monitor. Es literalmente el ejercicio del día, y es también lo que va a quedar afuera del agent en los agents.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Esto ya lo hacías, con módulos

code/u5/threads/01-modulos/modules.sv · #producer-and-consumer
// Sends the data through the shared variable and signals it by toggling get_it
// Then the producer blocks on the put_it signal
module producer (
    output byte shared,
    input  bit  put_it,
    output bit  get_it
);
   initial
      repeat (3) begin
         $display("Sent %0d", ++shared);
         get_it = ~get_it;
         @(put_it);
      end
endmodule : producer

// The consumer blocks on the get_it signal, waiting for data from the producer
// when the producer toggles that signal, it processes the data and toggles put_it
// when done, to unblock the producer
module consumer (
    input  byte shared,
    output bit  put_it,
    input  bit  get_it
);
   initial
      forever begin
         @(get_it);
         $display("Received: %0d", shared);
         put_it = ~put_it;
      end
endmodule : consumer
el archivo entero, 47 líneas
// InterThread Communication
// 	The initials of each module are 2 different threads

// Sends the data through the shared variable and signals it by toggling get_it
// Then the producer blocks on the put_it signal
module producer (
    output byte shared,
    input  bit  put_it,
    output bit  get_it
);
   initial
      repeat (3) begin
         $display("Sent %0d", ++shared);
         get_it = ~get_it;
         @(put_it);
      end
endmodule : producer

// The consumer blocks on the get_it signal, waiting for data from the producer
// when the producer toggles that signal, it processes the data and toggles put_it
// when done, to unblock the producer
module consumer (
    input  byte shared,
    output bit  put_it,
    input  bit  get_it
);
   initial
      forever begin
         @(get_it);
         $display("Received: %0d", shared);
         put_it = ~put_it;
      end
endmodule : consumer

module top;
   byte shared;
   producer p (
       shared,
       put_it,
       get_it
   );
   consumer c (
       shared,
       put_it,
       get_it
   );
endmodule : top

La slide existe para desactivar el susto antes de que aparezca la palabra TLM. Conviene señalar el código y decirlo así de crudo: acá hay dos procesos que corren a la vez y se pasan datos por un cable, y nadie llamó a eso "comunicación inter-thread" en veinte años de Verilog. Es lo mismo. Lo único que cambia en el resto de la sección es dónde vive cada proceso: en vez de dos always en dos módulos, dos run_phase() en dos objetos. Y como un objeto no tiene port list, hace falta reemplazar el cable por algo. La pregunta para dejar picando, que es exactamente la de la slide siguiente: ¿con qué se reemplaza el cable? Las respuestas que van a salir —una variable compartida, un semáforo, un mailbox— son todas correctas, y ése es justamente el problema que UVM viene a resolver.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Un objeto no tiene port list

Lo que hay que sacarle a la slide es que el tamaño 1 no es una limitación de la clase: es el argumento por defecto del constructor. Con new("f", this, 0) la FIFO es ilimitada, y ahí el que pone nunca se bloquea. El tamaño 1 es el que sincroniza: bloquea al productor hasta que el consumidor saca, y eso reemplaza al semáforo y a la bandera que cada uno se escribía a mano. Es el mismo handshake del start/done del BFM, pero entre objetos.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Productor como un objeto

code/u5/threads/02-bloqueante/producer.svh
// UVM solution to interthread communication
//	It has 2 parts:
//		*	Ports-Objects: instantiated in our uvm_components so that
//				run_phase can talk to other threads
//		*	TLM Fifos (they hold only 1 element)

class producer extends uvm_component;
   `uvm_component_utils(producer);

   int shared;
   // uvm_put_port is a parameterized class
   // this class takes care of the whole synchronization mess around
   // get_it and put_it
   uvm_put_port #(int) put_port_h;

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

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

   task run_phase(uvm_phase phase);
      phase.raise_objection(this);
      repeat (3) begin
         put_port_h.put(++shared);
         `uvm_info("PRODUCER", $sformatf("Sent %0d", shared), UVM_MEDIUM)
      end
      phase.drop_objection(this);
   endtask : run_phase
endclass : producer

Lo que hay que hacer notar es lo que no está en esta clase: no hay @(posedge algo), no hay wait, no hay una variable que diga "listo". El productor pone y sigue; si el otro no sacó todavía, el put() lo frena solo. El detalle de forma, que es el que se copia mal: el port se instancia en el build_phase como cualquier otro componente, con new("nombre", this). No es un campo de datos: lleva padre, y por eso aparece en print_topology(). Pero tampoco es un uvm_component — uvm_put_port extiende uvm_port_base #(IF), y el que se cuelga del árbol es un uvm_port_component interno que el port se arma solo (uvm_port_base.svh:137). La FIFO de la slide siguiente sí es un component de verdad. Y el parámetro: uvm_put_port #(T). Ese T tiene que ser el mismo en el port, en el get port y en la FIFO. Si no coinciden no compila, que es la buena noticia —es de los pocos errores de conexión de UVM que se ven en compilación y no a las tres de la mañana—.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Consumidor como un objeto

code/u5/threads/02-bloqueante/consumer.svh
class consumer extends uvm_component;
   `uvm_component_utils(consumer);

   // uvm_get_port handles all the interthread coordination
   uvm_get_port #(int) get_port_h;
   int shared;

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

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

   task run_phase(uvm_phase phase);
      forever begin
         get_port_h.get(shared);
         `uvm_info("CONSUMER", $sformatf("Received: %0d", shared), UVM_MEDIUM)
      end
   endtask : run_phase
endclass : consumer

get() es una task, y esa palabra es toda la sección. Una function no puede tener un @ ni un #, así que no puede esperar a nadie; una task sí. Si alguien se pregunta por qué los analysis ports de la sección anterior no bloqueaban, la respuesta está ahí: write() es una function. El argumento por referencia también vale nombrarlo: get(t) no devuelve el dato, lo escribe en la variable que le pasás. Es la convención de TLM y sorprende la primera vez, porque uno espera un t = get(). Y la advertencia práctica: un get() sobre una FIFO que nunca se llena es un thread colgado para siempre. No hay error ni warning — la simulación termina cuando se baja el objection y ese consumidor simplemente nunca corrió. Es hermano del item_done() olvidado del día 6.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Instanciar los tres: dos componentes y una FIFO

code/u5/threads/02-bloqueante/communication_test.svh
class communication_test extends uvm_test;
   `uvm_component_utils(communication_test)

   producer producer_h;
   consumer consumer_h;
   uvm_tlm_fifo #(int) fifo_h;

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

   function void build_phase(uvm_phase phase);
      producer_h = new("producer_h", this);
      consumer_h = new("consumer_h", this);
      fifo_h = new("fifo_h", this);
   endfunction : build_phase

   // Connect phase: the two ports and the fifo already exist -- build_phase
   // runs first, top-down -- so this is where they get wired together.
   function void connect_phase(uvm_phase phase);
      producer_h.put_port_h.connect(fifo_h.put_export);
      consumer_h.get_port_h.connect(fifo_h.get_export);
   endfunction : connect_phase

endclass : communication_test

Vale volver a decir por qué el orden es ése, porque acá se ve para qué sirve: el connect_phase conecta un port con un export, y para eso los dos objetos tienen que existir. Si connect fuera top-down, el test intentaría conectar hijos que todavía no se construyeron. El error clásico de esta slide, que compila y falla en tiempo de ejecución: crear la FIFO en el connect_phase en vez del build_phase. UVM lo ataja con un fatal ILLCRT —*"It is illegal to create a component ('x' under 'y') after the build phase has ended"* (uvm_component.svh:1721)—, y por lo menos avisa con el nombre. Olvidarse del connect() de un port de put/get también avisa, y bien: resolve_bindings() reporta un UVM_ERROR [Connection Error] connection count of 0 does not meet required minimum of 1 con el nombre del port, en end_of_elaboration y antes de que corra un solo ciclo (uvm_port_base.svh:886). El que se calla es el analysis port, porque su mínimo es 0: un write() a un port sin conectar es legal y no imprime nada. Es la misma asimetría de la sección anterior, y es la que hay que recordar. Y una que conviene señalar de paso: acá el productor y el consumidor son hijos del test, no de un env. Es a propósito, porque la sección es sobre el mecanismo. En el testbench real esto va adentro del agent, con el sequencer de un lado y el driver del otro.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Conectarlos: put_export y get_export

code/u5/threads/02-bloqueante/communication_test.svh · #connect_phase
   // Connect phase: the two ports and the fifo already exist -- build_phase
   // runs first, top-down -- so this is where they get wired together.
   function void connect_phase(uvm_phase phase);
      producer_h.put_port_h.connect(fifo_h.put_export);
      consumer_h.get_port_h.connect(fifo_h.get_export);
   endfunction : connect_phase
el archivo entero, 25 líneas
class communication_test extends uvm_test;
   `uvm_component_utils(communication_test)

   producer producer_h;
   consumer consumer_h;
   uvm_tlm_fifo #(int) fifo_h;

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

   function void build_phase(uvm_phase phase);
      producer_h = new("producer_h", this);
      consumer_h = new("consumer_h", this);
      fifo_h = new("fifo_h", this);
   endfunction : build_phase

   // Connect phase: the two ports and the fifo already exist -- build_phase
   // runs first, top-down -- so this is where they get wired together.
   function void connect_phase(uvm_phase phase);
      producer_h.put_port_h.connect(fifo_h.put_export);
      consumer_h.get_port_h.connect(fifo_h.get_export);
   endfunction : connect_phase

endclass : communication_test
code/u5/threads/02-bloqueante/result.txt
UVM_INFO @ 0: reporter [RNTST] Running test communication_test...
UVM_INFO producer.svh(28) @ 0: uvm_test_top.producer_h [PRODUCER] Sent 1
UVM_INFO consumer.svh(19) @ 0: uvm_test_top.consumer_h [CONSUMER] Received: 1
UVM_INFO producer.svh(28) @ 0: uvm_test_top.producer_h [PRODUCER] Sent 2
UVM_INFO consumer.svh(19) @ 0: uvm_test_top.consumer_h [CONSUMER] Received: 2
UVM_INFO producer.svh(28) @ 0: uvm_test_top.producer_h [PRODUCER] Sent 3
UVM_INFO consumer.svh(19) @ 0: uvm_test_top.consumer_h [CONSUMER] Received: 3
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 0: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    7
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[CONSUMER]     3
[PRODUCER]     3
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 0s

Dos cosas de la salida, que no son de esta sección pero se ven acá primero. Los mensajes salen con `uvm_info, no con $display: por eso cada línea trae sola el tiempo, la ruta jerárquica del componente y el archivo. Eso es gratis, y es reporting, que ya vieron cerrando el día 3. Y la otra: todos los @ dicen 0. En este ejemplo no hay ni un #delay —el handshake bloqueante suspende y reanuda dentro del mismo instante—, así que la sincronización de los dos threads no la da el reloj: la da la FIFO de un solo elemento. En el ejemplo que sigue aparecen los relojes y los números dejan de ser 0.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Comunicación NO bloqueante

code/u5/threads/03-no-bloqueante/consumer.svh
class consumer extends uvm_component;
   `uvm_component_utils(consumer);

   uvm_get_port #(int) get_port_h;
   virtual clk_bfm clk_bfm_i;
   int shared;

   function void build_phase(uvm_phase phase);
      get_port_h = new("get_port_h", this);
      clk_bfm_i = example_pkg::clk_bfm_i;
   endfunction : build_phase

   // Consumer and producer run on different clocks
   // The consumer has its first positive edge at 7ns
   //    and then checks the data every 14ns, because it uses bfm.clk (= 14ns)
   // The producer produces one datum every 17ns
   task run_phase(uvm_phase phase);
      forever begin
         @(posedge clk_bfm_i.clk);
         if (get_port_h.try_get(shared))
            `uvm_info("CONSUMER", $sformatf("Received: %0d", shared), UVM_MEDIUM)
      end
   endtask : run_phase

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

endclass : consumer
code/u5/threads/03-no-bloqueante/results.txt
UVM_INFO @ 0: reporter [RNTST] Running test communication_test...
UVM_INFO producer.svh(18) @ 17: uvm_test_top.producer_h [PRODUCER] Sent 1
UVM_INFO consumer.svh(21) @ 21: uvm_test_top.consumer_h [CONSUMER] Received: 1
UVM_INFO producer.svh(18) @ 34: uvm_test_top.producer_h [PRODUCER] Sent 2
UVM_INFO consumer.svh(21) @ 35: uvm_test_top.consumer_h [CONSUMER] Received: 2
UVM_INFO producer.svh(18) @ 51: uvm_test_top.producer_h [PRODUCER] Sent 3
UVM_INFO consumer.svh(21) @ 63: uvm_test_top.consumer_h [CONSUMER] Received: 3
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 68: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    7
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[CONSUMER]     3
[PRODUCER]     3
[RNTST]     1
- $UVM_HOME/src/base/uvm_root.svh:633: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 68ns

try_get() devuelve 0 y sigue: no espera. Es la diferencia entre un consumidor que se cuelga hasta que haya dato y uno que puede hacer otra cosa mientras tanto. Ojo que en results.txt los ceros no se ven —el consumer sólo imprime cuando le toca dato—: están en la línea de tiempo de la slide que sigue. El tiempo de cada línea no lo imprime el mensaje: lo pone UVM en el @, y por eso acá se puede leer la carrera entre los dos relojes sin haber escrito una sola línea de formateo.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Comunicación NO bloqueante: la línea de tiempo

Línea de tiempo del ejemplo no bloqueante: en 7 ns y 49 ns la FIFO está vacía y try_get() devuelve 0

Los números no son de un dibujo: son los @ de los Sent y Received de results.txt, la slide anterior. El flanco que hay que hacer ver es el de 49 ns. El dato 3 recién se pone a los 51, así que el consumer llega un poquito antes, se va con las manos vacías y vuelve 14 ns después. Con get() ese thread quedaba bloqueado justo ahí, y en un testbench con reloj eso es un flanco perdido.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Cómo se leen los diagramas TLM

Comunicación entre threads con put port, get port y uvm_tlm_fifo

Vale la pena que el alumno se lleve la convención, porque es la misma en el UVM User Guide, en la Verification Academy y en cualquier diagrama de testbench que le pasen en el trabajo. No es de este curso. La regla que ordena las tres figuras, y la que hace que un diagrama TLM se pueda leer sin leyenda: la punta cuadrada siempre apunta al círculo. El que tiene el cuadrado es el que llama; el que tiene el círculo es el que tiene el método escrito. Por eso un port se conecta a un export y nunca a otro port. El rombo es el caso especial que ya vieron: un analysis port puede apuntar a muchos círculos a la vez, y por eso se dibuja distinto. Los otros dos son uno a uno.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

fork: las tres formas de arrancar en paralelo

fork  wait_done();  count_cycles();  join        // sigue cuando terminaron LAS DOS
fork  wait_done();  count_cycles();  join_any    // sigue con LA PRIMERA; la otra sigue viva
fork  wait_done();  count_cycles();  join_none   // sigue YA; las dos quedan corriendo
Variante El padre sigue… Para qué se usa
join cuando terminan todas dos chequeos que tienen que cerrar los dos
join_any cuando termina la primera respuesta contra timeout
join_none enseguida lanzar threads que viven todo el test

Es la unidad que se llama threads y hasta acá los threads los puso UVM. Esta slide y las dos siguientes son las tres palabras del lenguaje con las que se escribe cualquier monitor o driver de producción, y las tres entran en una tabla. El punto que conviene repetir, porque es donde todo el mundo se confunde la primera vez: las tres arrancan igual. Los procesos del fork se lanzan todos en el mismo instante en las tres variantes. Lo único que cambia es qué hace el proceso padre inmediatamente después, y por eso la columna del medio es la que hay que leer. Un ejemplo por variante alcanza para que se fije. join: mandar el estímulo y contar los ciclos, y no seguir hasta que las dos terminen. join_any: esperar la respuesta del DUT o que venza un timeout, lo que pase primero. join_none: el run_phase del monitor, que arranca y se queda mirando para siempre mientras el resto del testbench sigue. Y el enganche con el día 2, que conviene hacer explícito: la clase testbench escrita a mano usaba fork ... join_none para arrancar los tres objetos. No era una casualidad ni un truco — es exactamente lo que hace UVM cuando corre la run_phase de cien componentes a la vez.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

disable fork y wait fork: apagar lo que quedó prendido

// El idiom de la respuesta contra el timeout. El fork de afuera AÍSLA
fork begin
   fork
      begin  wait_done();          `uvm_info("BFM", "llegó", UVM_LOW)  end
      begin  repeat (100) @(posedge clk);  `uvm_error("BFM", "timeout")   end
   join_any
   disable fork;      // mata a la hermana que perdió, y a nadie más
end join

wait fork;            // no mata a nadie: espera a TODOS los hijos de este thread

El disable fork sin el fork ... join de aislamiento es el bug clásico de esta construcción y vale dibujarlo: mata a todos los hijos del thread actual, no a los del fork de al lado. Si el run_phase ya había lanzado un monitor con join_none y después hace un disable fork suelto, el monitor se muere y el testbench sigue corriendo ciego. No hay error, no hay warning: hay un log que deja de tener líneas. La forma de acordarse es pensar en el alcance: disable fork no dice cuál fork. Dice "los hijos de este proceso". El fork begin ... end join de afuera crea un proceso nuevo cuyos únicos hijos son los dos del join_any, y por eso el disable no puede llegar más lejos. wait fork es la pareja tranquila y se usa mucho menos de lo que se debería: es lo que hace falta al final de una sequence o de un run_phase que lanzó cosas con join_none y no quiere que la fase termine con la mitad del estímulo en el aire. En UVM el mismo problema se resuelve con objections — pero adentro de una task, wait fork es la respuesta.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

⚠ La trampa: el índice del for adentro del fork

code/verilator/repro-fork-automatic.sv · #las-cuatro-variantes
    // A -- a single variable, shared by the three threads. A join_none runs
    // nothing: it schedules. By the time the three run, the for is over.
    for (i = 0; i < 3; i++)
      fork got = {got, $sformatf("%0d", i)}; join_none
    wait fork;
    check("int i outside the for, no copy", "333");

    // B -- declared inside the for: one per pass, and nothing to copy.
    for (int j = 0; j < 3; j++)
      fork got = {got, $sformatf("%0d", j)}; join_none
    wait fork;
    check("int j declared in the for", "012");

    // C -- the slide's one: the automatic as the first line of a begin/end.
    // Here the copy is NOT made when the fork is scheduled, so k reads the same
    // i that A does and gives the same answer as A. This is the finding.
    for (i = 0; i < 3; i++)
      fork begin
        automatic int k = i;
        got = {got, $sformatf("%0d", k)};
      end join_none
    wait fork;
    check("fork begin automatic k = i; end  (the slide's)", "333");

    // D -- the LRM 9.3.2 one: the automatic declared in the fork, no begin/end.
    // Same workaround, written where the LRM puts it, and this one does copy.
    for (i = 0; i < 3; i++)
      fork
        automatic int k = i;
        got = {got, $sformatf("%0d", k)};
      join_none
    wait fork;
    check("fork automatic k = i;  no begin/end (LRM 9.3.2)", "012");
el archivo entero, 88 líneas
// Minimal repro: what a `fork ... join_none` copies from the loop index, and when.
//
//   $ verilator --binary --timing --top-module top -o sim repro-fork-automatic.sv
//   ./obj_dir/sim
//
// Why this file exists. The trap slide of the threads section --
// slides/es/110-threads.md and its English twin -- shows the `for` index inside
// a `fork` in three variants and closes by claiming, with the seal of a
// measurement on Verilator 5.052, that the three lines above print 3 3 3,
// 0 1 2 and 0 1 2. The third one, the
// `automatic int k = i;` written as the FIRST LINE of a `fork begin ... end
// join_none`, prints 3 3 3 here: the copy is not made when the fork is
// scheduled. So the slide taught a workaround that does not work around
// anything in the only simulator of the course, and it taught it with the seal
// of a measurement -- which is worse than not measuring at all, because nobody
// re-checks a number that says it was measured.
//
// The form the LRM blesses (IEEE 1800-2017, 9.3.2) is the declaration inside
// the `fork` ITSELF, with no `begin`/`end` around it, and that one does print
// 0 1 2. That is the difference this file pins, so that the slide has a source
// instead of a claim: the block on the slide is code pasted into markdown, not
// a {{code:}}, so until this file existed nobody ever compiled it.
//
// Measured on 5.052 (2026-09-10), deterministic, three runs:
//
//   A  int i outside the for, fork with no copy       3 3 3
//   B  int j declared in the for                      0 1 2
//   C  fork begin automatic int k = i; ... end        3 3 3   <- the slide says 0 1 2
//   D  fork automatic int k = i; ... join_none (LRM)  0 1 2
//
// The four expected strings are asserted below, so a future Verilator that
// changes any of them ends this file in $fatal instead of printing OK -- and
// the day C starts printing 0 1 2 is the day the slide can go back to what it
// used to say. Like the other repros here, `make repros` runs it. Seconds.
module top;
  int    i;
  string got;

  // Each variant leaves its three digits glued together in `got`. The whole
  // string is compared, not digit by digit, because the ORDER is part of what
  // is at stake: "012" and "210" would both be "0 1 2" if we only counted.
  function automatic void check(string what, string want);
    $display("  %-48s %-4s (expected %s)", what, got, want);
    if (got != want) $fatal(1, "%s: printed '%s', expected '%s'", what, got, want);
    got = "";
  endfunction

  initial begin
    $display("Verilator 5.052, the for index inside the fork:");

    // A -- a single variable, shared by the three threads. A join_none runs
    // nothing: it schedules. By the time the three run, the for is over.
    for (i = 0; i < 3; i++)
      fork got = {got, $sformatf("%0d", i)}; join_none
    wait fork;
    check("int i outside the for, no copy", "333");

    // B -- declared inside the for: one per pass, and nothing to copy.
    for (int j = 0; j < 3; j++)
      fork got = {got, $sformatf("%0d", j)}; join_none
    wait fork;
    check("int j declared in the for", "012");

    // C -- the slide's one: the automatic as the first line of a begin/end.
    // Here the copy is NOT made when the fork is scheduled, so k reads the same
    // i that A does and gives the same answer as A. This is the finding.
    for (i = 0; i < 3; i++)
      fork begin
        automatic int k = i;
        got = {got, $sformatf("%0d", k)};
      end join_none
    wait fork;
    check("fork begin automatic k = i; end  (the slide's)", "333");

    // D -- the LRM 9.3.2 one: the automatic declared in the fork, no begin/end.
    // Same workaround, written where the LRM puts it, and this one does copy.
    for (i = 0; i < 3; i++)
      fork
        automatic int k = i;
        got = {got, $sformatf("%0d", k)};
      join_none
    wait fork;
    check("fork automatic k = i;  no begin/end (LRM 9.3.2)", "012");

    $display("OK");
    $finish;
  end
endmodule

Es el bug de threads que más caro sale y el que menos se ve leyendo el código, porque las tres versiones se parecen muchísimo. Conviene hacer la pregunta antes de mostrar la respuesta: "¿qué imprime la primera?". Casi todo el mundo dice 0 1 2. La explicación que hay que dejar es una sola frase: join_none no corre el thread, lo agenda. El for sigue de largo hasta el final, y recién ahí el scheduler le da lugar a los tres threads — que leen la variable ahora, no cuando se los lanzó. Con i afuera hay una sola variable, y ahora vale 3. La versión con for (int j …) funciona y conviene decir por qué, para que no parezca magia: el LRM declara automática la variable de un for que la declara, así que cada vuelta tiene su copia. Está medido acá, no es teoría. El caso donde igual hace falta el automatic es el que aparece en la vida real: el índice no es del for, es un campo de la clase o un argumento de la task. Ahí no hay copia por vuelta y hay que hacerla a mano — y acá está la parte que esta slide decía mal hasta ahora: el automatic como primera línea de un fork begin ... end no copia. En Verilator 5.052 la variante C imprime 3 3 3, igual que la A. La que copia es la D, la del LRM §9.3.2: la declaración adentro del fork, sin begin/end. Las dos se parecen tanto que es fácil escribir la que no anda. La regla práctica para llevarse: si un thread lanzado con join_none lee una variable de afuera, declarale la copia en el fork, no adentro de un begin.

ver esta slide en el deck ›

Cuando alguien tiene que esperar

Resumen de la unidad

Vale cerrar con la tabla de dos columnas con que abrió hablar con varios objetos, porque es la que ordena las cuatro unidades del día: Intra-thread — uvm_analysis_port + uvm_subscriber, write() es function, no consume tiempo, un solo thread, y sirve para el analysis layer. Inter-thread — uvm_put_port / uvm_get_port + uvm_tlm_fifo, put() y get() son task, bloquean, dos threads, y sirven para pasarle estímulo a un driver. La forma corta de distinguirlas, y es la pregunta 3 del repaso: si es una function, es intra-thread. Una función no puede tener un @ ni un #, así que no puede esperar a nadie. La palabra task es la que delata que hay dos threads.

ver esta slide en el deck ›

Quién espera a quién

Qué mandar y cómo mandarlo

Es la tercera vez que el curso hace la misma operación, y conviene decirlo así de explícito porque es lo que hay que llevarse: en interfaces y BFM sacamos el protocolo del test y lo pusimos en el BFM; en los analysis ports sacamos la observación de los analizadores y la pusimos en el monitor; acá sacamos la aplicación del estímulo del generador y la ponemos en el driver. La pregunta que ordena todo: ¿esto cambia cuando cambia el test, o cuando cambia el diseño? Si cambia con el test, es estímulo. Si cambia con el diseño, es estructura. No van en la misma clase. Y el aviso de hacia dónde va: en los agents este par put/get se llama sequencer y viene puesto, y el tester se llama sequence. Lo que estamos armando a mano acá es exactamente lo que UVM da hecho — por eso primero se arma a mano.

ver esta slide en el deck ›

Quién espera a quién

El tester hace dos trabajos, y uno no es suyo

code/u5/analysis-ports/tb_classes/base_tester.svh · #run_phase
   task run_phase(uvm_phase phase);
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;

      phase.raise_objection(this);
      bfm.reset_alu();
      repeat (1000) begin : random_loop
         op_set = get_op();
         iA = get_data();
         iB = get_data();
         bfm.send_op(iA, iB, op_set);
      end : random_loop
      #500;
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 35 líneas
virtual class base_tester extends uvm_component;
   `uvm_component_utils(base_tester)
   virtual vtalu_bfm bfm;

   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("BASE TESTER", "Failed to get BFM")
   endfunction : build_phase

   pure virtual function operation_t get_op();

   pure virtual function byte get_data();

   task run_phase(uvm_phase phase);
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;

      phase.raise_objection(this);
      bfm.reset_alu();
      repeat (1000) begin : random_loop
         op_set = get_op();
         iA = get_data();
         iB = get_data();
         bfm.send_op(iA, iB, op_set);
      end : random_loop
      #500;
      phase.drop_objection(this);
   endtask : run_phase

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

endclass : base_tester

Conviene leer el código señalando con el dedo dónde termina un trabajo y empieza el otro: get_op() y get_data() son el qué; todo lo que sigue —el bfm.send_op(), la espera, el ciclo— es el cómo. Están pegados en la misma task y por eso no se pueden cambiar por separado. La prueba de que están mal juntos es una pregunta: si mañana el DUT agrega una señal al protocolo, ¿cuántas clases hay que tocar? Todas las que heredan de base_tester, aunque ninguna sepa nada de protocolo. Eso es acoplamiento, y el síntoma clásico es exactamente éste — una clase que cambia por dos motivos distintos. El nombre driver conviene decirlo ya, aunque la clase todavía no exista, para que la palabra no aparezca por primera vez en el día 6. Un driver es lo que convierte un dato en señales. Nada más que eso, y por eso es la única clase del testbench que tiene derecho a escribir un @(negedge clk).

ver esta slide en el deck ›

Quién espera a quién

Cómo queda partido

El tester y el driver del VTALU separados por una uvm_tlm_fifo

La caja del medio es la que hay que señalar, porque es la que sorprende: entre el tester y el driver no hay una flecha. Hay una FIFO. El tester no llama al driver, y ni siquiera tiene un handle suyo. Vale hacer la comparación con el diagrama de los analysis ports, que está dos secciones atrás: allá el monitor tampoco conocía a sus oyentes. Es la misma idea aplicada del otro lado del testbench, y es la razón de que el día 6 se puedan cambiar todas las sequences sin tocar el driver. Y la diferencia que sí importa entre los dos diagramas, porque es la pregunta 3 del repaso: aquello era intra-thread —write() es una función y no espera a nadie—, esto es inter-thread. Acá el que pone puede quedarse esperando, y ésa es justamente la sincronización que antes había que escribir a mano.

ver esta slide en el deck ›

Quién espera a quién

El env: siete objetos y cinco connect()

code/u5/put-get/tb_classes/env.svh · #build-and-connect
   function void build_phase(uvm_phase phase);
      command_f = new("command_f", this);
      random_tester_h = random_tester::type_id::create("random_tester_h", this);
      driver_h = driver::type_id::create("driver_h", this);

      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      // PORTS CONNECT TO EXPORTS!
      driver_h.command_port.connect(command_f.get_export);
      random_tester_h.command_port.connect(command_f.put_export);

      result_monitor_h.ap.connect(scoreboard_h.analysis_export);

      command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export);
      command_monitor_h.ap.connect(coverage_h.analysis_export);
   endfunction : connect_phase
el archivo entero, 40 líneas
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester             random_tester_h;
   driver                    driver_h;
   uvm_tlm_fifo #(command_s) command_f;

   coverage                  coverage_h;
   scoreboard                scoreboard_h;

   command_monitor           command_monitor_h;
   result_monitor            result_monitor_h;

   function void build_phase(uvm_phase phase);
      command_f = new("command_f", this);
      random_tester_h = random_tester::type_id::create("random_tester_h", this);
      driver_h = driver::type_id::create("driver_h", this);

      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
      command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
      result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      // PORTS CONNECT TO EXPORTS!
      driver_h.command_port.connect(command_f.get_export);
      random_tester_h.command_port.connect(command_f.put_export);

      result_monitor_h.ap.connect(scoreboard_h.analysis_export);

      command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export);
      command_monitor_h.ap.connect(coverage_h.analysis_export);
   endfunction : connect_phase

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

endclass

Contá los objetos de este build_phase: son siete, más cinco connect(). Ese número es el que hay que recordar, porque en los agents el mismo testbench va a tener un agent y dos subscribers. Vale anotarlo en el pizarrón para poder volver. Las dos líneas nuevas del connect_phase son un par y se leen juntas: el tester pone en el put_export de la FIFO, el driver saca del get_export. Nadie conecta el tester con el driver — no se conocen, y ése es el punto. El mantra tiene una razón que conviene dar y no repetir como loro: el port es el que pide el servicio, el export es el que lo ofrece. La FIFO ofrece las dos puntas, así que tiene dos exports. Si lo escribís al revés no compila, y el mensaje del compilador es de los peores de UVM. Y el detalle que se olvida: command_f = new(...), no create(). No es que la FIFO falte en la factory —está registrada, tlm1/uvm_tlm_fifos.svh:62—: es que el size va por constructor y create() no lo sabe pasar. Los que de verdad no están en la factory son los ports y los exports.

ver esta slide en el deck ›

Quién espera a quién

El base_tester, sin una sola señal

code/u5/put-get/tb_classes/base_tester.svh · #class-and-run
virtual class base_tester extends uvm_component;

   `uvm_component_utils(base_tester)

   // the handle to the BFM is replaced by a uvm_put_port
   uvm_put_port #(command_s) command_port;

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

   pure virtual function operation_t get_op();

   pure virtual function byte get_data();

   task run_phase(uvm_phase phase);
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;
      command_s     command;

      phase.raise_objection(this);
      command.op = rst_op;
      command_port.put(command);
      repeat (1000) begin : random_loop
         command.op = get_op();
         command.A = get_data();
         command.B = get_data();
         command_port.put(command);
      end : random_loop
      #500;
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 43 líneas
// This version does not hold a handle to the BFM
// Since add_tester and random_tester inherit from this one
// and the connection is untouched, those classes get new behaviour
// just by changing this base class
virtual class base_tester extends uvm_component;

   `uvm_component_utils(base_tester)

   // the handle to the BFM is replaced by a uvm_put_port
   uvm_put_port #(command_s) command_port;

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

   pure virtual function operation_t get_op();

   pure virtual function byte get_data();

   task run_phase(uvm_phase phase);
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;
      command_s     command;

      phase.raise_objection(this);
      command.op = rst_op;
      command_port.put(command);
      repeat (1000) begin : random_loop
         command.op = get_op();
         command.A = get_data();
         command.B = get_data();
         command_port.put(command);
      end : random_loop
      #500;
      phase.drop_objection(this);
   endtask : run_phase

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

endclass : base_tester

Lo importante de esta clase es lo que desapareció: no hay un solo @(negedge clk), ni un bfm.start, ni la espera del done. El tester quedó reducido a elegir qué mandar y ponerlo en el puerto. Y sin embargo random_tester y add_tester no se tocaron: sólo definen get_op() y get_data(), y ninguna de las dos sabía cómo se aplicaba el estímulo. Eso es lo que se compra separando bien — el cambio se detuvo en la clase base. El put() es una task y bloquea: si el driver todavía está manejando la operación anterior, el tester espera ahí. La FIFO de un elemento de la comunicación entre threads es lo que sincroniza los dos threads, sin un solo semáforo. Y hay un #500 al final, antes del drop_objection, que conviene señalar como lo que es: un parche. Está para que el objection no se caiga mientras la última operación todavía viaja. En las sequences desaparece, porque finish_item() vuelve recién cuando la operación terminó de verdad. Un #500 a ojo en un testbench es siempre una pregunta sin responder.

ver esta slide en el deck ›

Quién espera a quién

El driver: el único que toca el cable

code/u5/put-get/tb_classes/driver.svh · #class-and-run
class driver extends uvm_component;
   `uvm_component_utils(driver)

   virtual vtalu_bfm bfm;

   uvm_get_port #(command_s) command_port;

   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("DRIVER", "Failed to get BFM")
      command_port = new("command_port", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_s command;

      forever begin : command_loop
         // Blocking: if there are no commands to send, it waits
         command_port.get(command);
         bfm.send_op(command.A, command.B, command.op);
      end : command_loop
   endtask : run_phase
el archivo entero, 28 líneas
class driver extends uvm_component;
   `uvm_component_utils(driver)

   virtual vtalu_bfm bfm;

   uvm_get_port #(command_s) command_port;

   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("DRIVER", "Failed to get BFM")
      command_port = new("command_port", this);
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      command_s command;

      forever begin : command_loop
         // Blocking: if there are no commands to send, it waits
         command_port.get(command);
         bfm.send_op(command.A, command.B, command.op);
      end : command_loop
   endtask : run_phase

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

endclass : driver

Esta división es la que UVM formaliza en los agents: el tester decide QUÉ mandar, el driver sabe CÓMO mandarlo. En un agent de verdad el tester se va a llamar sequence y este put/get se va a llamar get_next_item / item_done. Anticiparlo acá hace que el día 6 arranque cuesta abajo.

ver esta slide en el deck ›

Quién espera a quién

Resumen de la unidad

Vale cerrar el día con las tres capas escritas en el pizarrón, porque es el mapa que ordena todo lo que viene: estímulo, protocolo, análisis. Las cuatro secciones de hoy fueron construir esa separación, y las de mañana no agregan capas — le ponen nombres de UVM a las que ya están. La pregunta de control, que además es la que se contesta sola si el día cerró: ¿en cuál de las tres capas vive un @(negedge clk)? En una sola, el protocolo. Si aparece en otra, algo quedó mal separado. Y el aviso de lo que falta, que conviene dar ahora: entre el tester y el driver viajan command_s, que son structs. Andan, pero no se randomizan solas, no se comparan solas y no se imprimen solas — y eso es exactamente lo que el día 5 va a arreglar convirtiéndolas en transactions.

ver esta slide en el deck ›

Repaso · Día 4

1 de 5 · Observer Pattern

En el patrón Observer, ¿qué sabe el objeto observado sobre sus observadores?

No sabe nada — y esa ignorancia es la gracia. Agregar un cuarto subscriber no obliga a tocar una línea del que publica.

ver esta slide en el deck ›

Repaso · Día 4

2 de 5 · Analysis Ports

Un uvm_subscriber necesita datos de dos analysis ports distintos. ¿Cómo se resuelve?

Con una uvm_tlm_analysis_fifo — un uvm_subscriber tiene un solo write(), así que sólo puede escuchar un puerto. La FIFO da un analysis_export de un lado y un try_get() del otro. Es lo que hace el scoreboard del VTALU. La otra forma es `uvm_analysis_imp_decl, que fabrica un write_ por sufijo: no son dos write() en la misma clase, que SystemVerilog no permite.

ver esta slide en el deck ›

Repaso · Día 4

3 de 5 · Intra vs. inter thread

Cuando el monitor publica y se ejecutan los write() de los subscribers, ¿cuántos threads intervienen?

Uno solo, y es comunicación intra-thread — write() es una function, no una task: no consume tiempo y corre en el thread del que publica. Justamente por eso hace falta otro mecanismo (put/get + FIFO) para hablar entre threads.

ver esta slide en el deck ›

Repaso · Día 4

4 de 5 · Put y get ports

El consumidor llama a get() y la uvm_tlm_fifo está vacía. ¿Qué pasa?

Se bloquea — get() es bloqueante, y por eso se declara task y no function. Esa es toda la sincronización: no hace falta un handshake de señales a mano.

ver esta slide en el deck ›

Repaso · Día 4

5 de 5 · try_get()

¿Y try_get() con la FIFO vacía?

Devuelve 0 y sigue — es la versión no bloqueante, y por eso puede ser una function. Fijate que el scoreboard la usa en un do ... while para saltear los no_op y los rst_op.

ver esta slide en el deck ›

Ejercicio · Día 4 · 1 de 2

Un observador más, sin tocar a los que ya miran

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

El ejercicio corto del curso: veinte minutos. Lo que se está practicando no es escribir la clase —son diez líneas— sino el connect_phase: si se olvidan del connect, la clase compila, corre, y cuenta cero. Por eso la corrección es cruzada contra las líneas que imprime el command_monitor: no alcanza con que el número exista, tiene que coincidir con lo que otro componente vio.

ver esta slide en el deck ›

Ejercicio · Día 4 · 2 de 2

El #500 es un parche

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

Es el ejercicio que le da manos a las dos cosas que la sección afirma y no demuestra: que put() bloquea, y que el #500 del tester es un parche. El número está medido y conviene decirlo: con la FIFO de tamaño 1 el testbench manda las mil operaciones; con la FIFO sin tope, 13. Nadie tocó el DUT, nadie tocó el estímulo, y la diferencia es el argumento por defecto de un constructor. Lo que la contrapresión estaba haciendo sin que nadie lo hubiera diseñado así: mantener al tester al ritmo del bus. Sacala y el tester queda hablando solo. El arreglo hay que discutirlo, porque la respuesta obvia —subir el #500— es la que hay que rechazar. La pregunta que ordena: ¿quién sabe cuándo terminó el bus? No es el tester, que sabe cuándo terminó de poner. Es el driver, que es el que maneja. Por eso la objection va ahí, y por eso es el patrón de cualquier driver de UVM de verdad. Y el detalle que decide si el ejercicio anda, que también es de campo: el par raise/drop va después del get(), no alrededor. Alrededor, el driver sostiene una objection esperando trabajo que no va a llegar, y el test no termina nunca — que es el otro modo de falla de la tabla del apéndice de debug.

ver esta slide en el deck ›