Verify This! · el curso para leer

Día 3Entra UVM

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

El día 3 es donde entra UVM, así que la recap tiene una función extra: dejar claro que lo que viene reemplaza piezas que el alumno ya escribió, no piezas nuevas que hay que aprender de cero. Toda la unidad se puede leer como "esto que hiciste a mano, la librería lo trae". El tercer bullet es el gancho: el case del 060 es exactamente lo que `uvm_component_utils hace solo, y decirlo acá hace que el macro deje de ser una fórmula mágica.

ver esta slide en el deck ›

Agenda

Día 3 · ≈ 4 h 30 · unidad 4 · entra UVM

Al final del día podés:

El tercero es el que más rinde a largo plazo y el que más se subestima: es la fila 10 de la autoevaluación del cierre y el que apunta al 47 % del día 1. Un uvm_error que imprime "FAIL" deja al que debuggea donde estaba; uno que imprime la transaction, la predicción y el tiempo le ahorra media mañana. Los dos primeros son mecánica de la librería y son los que más se contestan con un "me suena". El segundo ejercicio del día existe para que dejen de sonar y pasen a haberse sufrido.

ver esta slide en el deck ›

Tests

Compilar una vez, elegir el test por línea de comandos

$ ./obj_dir/top/sim +UVM_TESTNAME=random_test
$ ./obj_dir/top/sim +UVM_TESTNAME=add_test     # el mismo binario

La cuenta es el argumento comercial de UVM, y conviene decirla con los números en la mano: no es que el testbench modular esté mal, es que no escala. Si se puede, en vivo: compilar una vez y correr las dos líneas de arriba. Que vean que la segunda no compila nada. Lo que se cobra del testbench en objetos: el testbench ya está partido en tester, coverage y scoreboard, y por eso se puede cambiar sólo el tester. Lo que se paga adelante: en los components esas tres clases dejan de ser objetos sueltos que el test hace new() y pasan a ser components del árbol.

ver esta slide en el deck ›

Tests

El top: instanciar, publicar la interface, arrancar

code/u4/tests/top.sv · #run-test
   initial begin
      // null and "*": from the root and visible to the whole tree. See the
      // uvm_config_db slide.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

      // UVM reads +UVM_TESTNAME, and builds THAT test with the factory.
      run_test();
el archivo entero, 31 líneas
module top;
   import uvm_pkg::*;
   `include "uvm_macros.svh"

   // Our classes and macros, same as in the modular version.
   import vtalu_pkg::*;
   `include "vtalu_macros.svh"

   vtalu_bfm bfm ();
   vtalu DUT (
       .A(bfm.A),
       .B(bfm.B),
       .op(bfm.op_set),
       .clk(bfm.clk),
       .reset_n(bfm.reset_n),
       .start(bfm.start),
       .done(bfm.done),
       .ovf(bfm.ovf),
       .result(bfm.result)
   );

   initial begin
      // null and "*": from the root and visible to the whole tree. See the
      // uvm_config_db slide.
      uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

      // UVM reads +UVM_TESTNAME, and builds THAT test with the factory.
      run_test();
   end

endmodule : top

La virtual interface se pasa por el config_db porque las clases no ven las señales del módulo: bfm vive en top, y random_test es un objeto que se crea en tiempo de simulación. No hay forma de pasárselo por el constructor — el de uvm_component sólo acepta name y parent. Vale la pena señalar el orden: primero el set, después run_test(). Al revés el test se construye antes de que el dato esté en la base y el get falla con el fatal. Es un error que se ve poco porque casi nadie escribe el top al revés, pero explica por qué el set no puede ir en un initial aparte. La slide que sigue es la que hay que explicar bien: es el error número uno del que arranca con UVM.

ver esta slide en el deck ›

Tests

uvm_config_db: el buzón del testbench

code/u4/tests/config_db.svh
// ILLUSTRATION -- not compiled. The two halves of the config_db, side by side:
// the set() lives in top.sv and the get() in tb_classes/driver.svh.
// -- IN THE TOP -- top is a module, not a uvm_component ---------------------
//   cntxt = null : the scope starts at the UVM root
//   inst  = "*"  : visible to the whole component tree
uvm_config_db #(virtual vtalu_bfm)::set(null, "*", "bfm", bfm);

// -- IN A COMPONENT -- always inside build_phase ----------------------------
//   cntxt = this : the scope is the hierarchical path of THIS component
//   inst  = ""   : with no suffix, the path is exactly the component's
if (!uvm_config_db #(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
  `uvm_fatal("DRIVER", "Failed to get BFM")

// The 3rd argument ("bfm") is the NAME of the datum inside the database.
// If the string in the set and the one in the get are not identical, this still
// compiles and blows up at run time with a null pointer.

Los dos primeros argumentos son dónde, el tercero es qué. En el set del top va null, "*" porque top es un módulo, no está en el árbol de UVM: la traducción es "desde la raíz, visible para todos". En el get va this, "", o sea "yo". Vale la pena escribir en el pizarrón la ruta que sale: uvm_test_top.env_h.driver_h. Y el atajo que hay que desaconsejar de entrada: get(null, "*", ...) también compila y también anda — busca en todo el árbol y se queda con lo primero que encuentra. Con una sola interface no se nota nunca. El día que el testbench tiene dos, el driver del bus A puede terminar manejando el bus B, y no falla: miente. Es de esos errores que se pagan tres meses después.

ver esta slide en el deck ›

Tests

Un test en tres partes: 1 · registrarlo

code/u4/tests/tb_classes/random_test.svh · #class-head
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

Es la primera vez que aparece la factory en serio, y todavía no se ve para qué sirve: acá sólo hace de directorio de nombres. La segunda mitad —crear por tipo y poder sustituirlo desde afuera— llega en el env con los overrides. El punto y coma después de la macro sobra y UVM lo tolera. Está en el código del libro y lo dejamos igual para que el que compare no se maree, pero si alguien pregunta: no hace falta.

ver esta slide en el deck ›

Tests

2 · el constructor y el build_phase

code/u4/tests/tb_classes/random_test.svh · #constructor-and-build
   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

La firma fija es lo primero que rompe todo el mundo: un constructor con un argumento de más y la factory no lo puede crear, porque llama siempre con (name, parent). build_phase es una fase, no una función que llames vos: UVM la recorre top-down sobre el árbol ya armado. En este test todavía no se nota, porque el árbol es un solo nodo; en los components, cuando el env construye sus hijos, el orden top-down es lo que hace que el padre exista antes que el hijo. El fatal no es paranoia: sin él, un typo en el nombre del dato —"bfm" contra "BFM"— da una simulación que corre, no manda un solo estímulo y termina con 0 errores.

ver esta slide en el deck ›

Tests

3 · el run_phase: acá pasa la simulación

code/u4/tests/tb_classes/random_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 41 líneas
// The first UVM test of the course. The five steps this class puts together
// --registration, constructor, build_phase, run_phase and objections-- are
// explained one by one in the slides of the tests section.
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   virtual vtalu_bfm bfm;

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

   // The get goes in build_phase and not in the constructor: when the
   // constructor runs, the tree does not exist yet.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("RANDOM TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      random_tester random_tester_h;
      coverage      coverage_h;
      scoreboard    scoreboard_h;

      phase.raise_objection(this);

      random_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      random_tester_h.execute();

      phase.drop_objection(this);
   endtask : run_phase

endclass

Conviene mostrarlo al lado del tb.execute() del testbench en objetos: el estímulo no cambió ni una línea. Lo único que hicimos fue moverlo adentro de una clase que UVM sabe crear y llamar. Ese es todo el trabajo de esta unidad. join_none y no join: coverage y scoreboard son bucles infinitos que miran las señales. Si esperáramos a que terminen, no termina nunca. Y el que se pierde: la simulación no dura lo que dura el run_phase, dura lo que duran las objections. Eso es lo que viene ahora.

ver esta slide en el deck ›

Tests

Objections: quién decide cuándo termina

task run_phase(uvm_phase phase);
   phase.raise_objection(this);    // "todavia tengo trabajo"
   ...                             // el estimulo
   phase.drop_objection(this);     // "listo, por mi que termine"
endtask

Es el mecanismo que decide cuánto dura la simulación, y es el que produce los dos cuelgues clásicos de la primera semana. Vale escribir las dos fallas en el pizarrón, porque el alumno las va a ver antes que a cualquier bug del DUT. La regla práctica: la objection la levanta el que tiene trabajo, y la baja el mismo. En este curso siempre es el test. Nunca la levantes en un componente y la bajes en otro — eso es cómo se cuelga un testbench de verdad. Y falta un tercer participante, que es la slide que sigue: la objection sabe cuándo el test terminó de mandar, no cuándo el scoreboard terminó de comparar. Ésa es la diferencia que paga el drain_time. Y el enganche del día 6: set_automatic_phase_objection(1) es esto mismo, hecho por el sequencer en vez de por el test.

ver esta slide en el deck ›

Tests

El final que no es el final: drain_time

task run_phase(uvm_phase phase);
   phase.get_objection().set_drain_time(this, 200ns);  // un colchon tras el ultimo drop
   phase.raise_objection(this);
   ...                                                 // el estimulo
   phase.drop_objection(this);
endtask

Es la contracara de la slide anterior y conviene decirlo con esas palabras: la objection mide el estímulo, no el análisis. El test sabe cuándo terminó de mandar; el scoreboard es el que sabe cuándo terminó de comparar, y nadie le preguntó. El drain_time es la respuesta barata y alcanza para el 90 % de los casos: un colchón fijo después del último drop_objection, en el orden de lo que tarda el DUT en contestar. En este curso serían un par de ciclos; en un bus con latencia, lo que diga la spec. phase_ready_to_end() es la respuesta cara y la que se usa en un testbench serio, porque no es un número: el scoreboard mira su propia FIFO, y si le quedan items levanta una objection más. La forma es siempre la misma —chequear phase.get_name() == "run", y levantar sólo si de verdad falta algo—, y hay que decir la trampa: si el que la implementa nunca la baja, colgó la fase. La tercera pieza, para el que quiera engancharse justo en ese instante, es all_dropped: un callback del objeto objection que se dispara cuando la cuenta llega a cero. Y el enganche hacia adelante: el scoreboard con FIFO de los analysis ports es exactamente el caso de esta slide, y el capstone lo pisa de nuevo.

ver esta slide en el deck ›

Tests

La línea de tiempo, de punta a punta

Línea de tiempo de una objection: raise, estímulo, drop, drain_time y fin de la fase, con las transacciones en vuelo

La figura es para pausarla y preguntar dónde termina la simulación. La respuesta que casi todo el mundo da la primera vez —*"cuando el test terminó de mandar"*— es justo la que produce el bug: ahí es donde cae la última objection, y ahí es donde la fase corta si nadie pidió un colchón. Vale marcar con el dedo las tres cajas vacías: son transacciones que el DUT todavía no contestó. No están perdidas por un error de nadie, están en vuelo, que es el estado normal de cualquier pipeline. Lo que decide si se comparan o no es cuánto tiempo más vive la fase. El detalle que conviene decir en voz alta: el drain_time se mide desde el último drop, no desde el principio. Si el test manda mil operaciones y el DUT tarda dos ciclos en contestar, el colchón son dos ciclos y no dos mil. Y la versión con criterio —phase_ready_to_end()— es esta misma figura con una diferencia: el largo del colchón lo decide el scoreboard mirando su FIFO, en vez de decidirlo de antemano el que escribió el test.

ver esta slide en el deck ›

Tests

El segundo test: lo mismo, con otro tester

code/u4/tests/tb_classes/add_test.svh · #run_phase
   task run_phase(uvm_phase phase);
      add_tester add_tester_h;
      coverage   coverage_h;
      scoreboard scoreboard_h;

      phase.raise_objection(this);

      add_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      add_tester_h.execute();
      phase.drop_objection(this);
   endtask : run_phase
el archivo entero, 37 líneas
class add_test extends uvm_test;
   `uvm_component_utils(add_test);

   virtual vtalu_bfm bfm;

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

   // Same build_phase as random_test: the virtual interface is read from the
   // config_db, with this and "" as the scope. See random_test.svh.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("ADD TEST", "Failed to get BFM")
   endfunction : build_phase

   task run_phase(uvm_phase phase);
      add_tester add_tester_h;
      coverage   coverage_h;
      scoreboard scoreboard_h;

      phase.raise_objection(this);

      add_tester_h = new(bfm);
      coverage_h = new(bfm);
      scoreboard_h = new(bfm);

      fork
         coverage_h.execute();
         scoreboard_h.execute();
      join_none

      add_tester_h.execute();
      phase.drop_objection(this);
   endtask : run_phase

endclass

No hay que disculpar la duplicación, hay que señalarla. El alumno tiene que terminar esta slide con la sensación de "esto no puede ser así", porque esa sensación es exactamente el motivo de los components y del env. Si alguien se adelanta y propone una clase base con el run_phase común y un método virtual para elegir el tester: está bien, y es más o menos lo que hace base_test en las sequences. Decírselo y seguir.

ver esta slide en el deck ›

Tests

Cómo se corre, y qué imprime

code/u4/tests/run.sh · #the-run
set -e
. "$(dirname "${BASH_SOURCE[0]}")/../../verilator/common.sh"
vlt_uvm top --coverage-user -Wno-fatal -f dut.f -f tb.f
run_sim +UVM_TESTNAME=random_test
run_sim +UVM_TESTNAME=add_test
cov_report
el archivo entero, 14 líneas
#!/bin/bash
# Runs the example with Verilator. See docs/verilator.md.
#
# --coverage-user: measures the covergroups and leaves out line/toggle/branch,
# which are not the topic. The number comes out at the end, with cov_report.
#
# -Wno-fatal: the examples have sloppy widths on purpose (WIDTHEXPAND/WIDTHTRUNC)
# and by default any warning stops the build. They still get printed.
set -e
. "$(dirname "${BASH_SOURCE[0]}")/../../verilator/common.sh"
vlt_uvm top --coverage-user -Wno-fatal -f dut.f -f tb.f
run_sim +UVM_TESTNAME=random_test
run_sim +UVM_TESTNAME=add_test
cov_report
code/u4/tests/output.txt · líneas 1-13 de 27
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 46340: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[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 46ns
el archivo entero, 27 líneas
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 46340: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[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 46ns
    seed: la de Verilator por defecto, que tambien es fija
UVM_INFO @ 0: reporter [RNTST] Running test add_test...
UVM_INFO $UVM_HOME/src/base/uvm_report_server.svh(1009) @ 40540: reporter [UVM/REPORT/SERVER] 
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[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 41ns

Vale correrlo en vivo y cronometrar: la compilación tarda minutos, los dos run_sim tardan segundos. Ése es el número que justifica toda la ceremonia de la unidad. El $finish at 46ns del final es del primer test; el add_test termina en 41 ns. Los dos mandan la misma cantidad de operaciones, pero la multiplicación toma más ciclos que la suma, así que el tiempo depende de qué salió al azar. Y una advertencia útil: UVM_ERROR : 0 quiere decir "nadie llamó a uvm_error", no "el DUT está bien". Acá el scoreboard sí reporta con `uvm_error, así que la cuenta vale — pero un scoreboard que nunca recibe nada da exactamente el mismo cero. Reporting muestra cómo distinguirlos.

ver esta slide en el deck ›

Tests

Resumen de la unidad

Cierre de la primera unidad de UVM de verdad. Vale reconocer la incomodidad en voz alta: hoy pusieron mucha ceremonia para hacer lo mismo que ya hacían. Lo que compraron es el primer bullet, y no se cobra hasta que hay dos tests — que es el env. El error que se van a comer y conviene anticipar: el get() que se olvida de chequear el valor de retorno. No falla, miente: deja la virtual interface en null y el testbench se cae tres capas más abajo. Por eso todos los get del curso van adentro de un if con su uvm_fatal.

ver esta slide en el deck ›

Components y fases

Un component es lo que está en el árbol, y al árbol lo recorre UVM

La frase de la slide es la que ordena la unidad: un component es lo que está en el árbol. Todo lo demás —el nombre, el padre, las fases, el print_topology de los agents— sale de ahí. La consecuencia práctica que hay que decir en voz alta: si algo no es un component, UVM no lo ve. No lo construye, no le llama fases y no aparece en la topología. Más adelante vamos a tener objetos que a propósito no son components —las transactions— y ahí la distinción se cobra. Los tres ejes —estructura, secuencias, datos— valen como mapa del resto del curso: components y env son estructura, transactions son datos, sequences son secuencias.

ver esta slide en el deck ›

Components y fases

Intro UVM phases

Tres cosas que hay que decir sí o sí: build_phase es top-down, connect_phase es bottom-up, y todos los run_phase corren en paralelo, cada uno en su thread. Y el error clásico: crear componentes después de que build terminó. UVM sí te avisa, y bien: ILLCRT te dice qué componente y bajo qué padre.

ver esta slide en el deck ›

Components y fases

El árbol, dos veces: una baja y la otra sube

El árbol recorrido dos veces: build_phase baja y connect_phase sube, y el orden real en que se construye un hijo

El dibujo contesta dos preguntas que en la tabla se leen como una convención arbitraria. La primera: por qué build_phase es top-down. Porque un padre tiene que existir para crear a sus hijos, y no hay otra forma de armar un árbol; las del medio necesitan lo contrario, que los hijos ya estén. La segunda es la que se pregunta en voz baja: si el padre crea al hijo adentro de su propio build_phase, ¿cuándo corre el build_phase del hijo? Después, y ésa es la parte que hay que decir despacio: create() llama al constructor, no a la fase. UVM recorre el árbol llamando la fase a los componentes que fueron apareciendo, así que el hijo se construye en dos tiempos. La consecuencia práctica es la del último bullet y es la que se cobra: un uvm_config_db::set() escrito después del create() del hijo llega tarde, el hijo ya leyó, y el campo queda en su default sin que nadie avise. Es el mismo modo de falla asimétrico de la slide que sigue. Si alguien pregunta por el código: uvm_topdown_phase::traverse ejecuta la fase en el componente y recién después recorre sus hijos —get_first_child()—, que para entonces ya existen.

ver esta slide en el deck ›

Components y fases

¿Y el super.build_phase()?

// uvm_component.svh:2385-2393 — uvm-core 2020.3.1, con el return; elidido
function void uvm_component::build_phase(uvm_phase phase);
   build();            // -> apply_config_settings(): la config automatica
endfunction

function void uvm_component::connect_phase(uvm_phase phase);
   connect();          // -> return;  esta vacia, igual que las demas
endfunction

Esta slide existe porque el alumno va a ver super.build_phase(phase) en todo el material de internet y en la mayoría de los build_phase del curso no está. La respuesta honesta no es "se olvidaron": es que sin las macros de campo la llamada al de uvm_component no hace nada. Los números, para que nadie tenga que creernos: grep -rn 'super\.build_phase' code/ --include='*.sv' --include='*.svh' --exclude-dir=.uvm da 15 llamadas reales —más 6 comentarios que hablan de ellas— sobre 306 build_phase. Las 15 llaman al super de un base_test o un random_test nuestro, que construye el env. Ninguna llama al de uvm_component, que es de lo que habla la slide. La otra diferencia de fondo: hay dos formas de configurar un componente. La automática (`uvm_field_* + config_db, magia por macros) y la manual (uvm_config_db::get() en el build_phase, que es la que usa el curso). La manual es más código y muchísimo más fácil de debuggear — las macros de campo generan cientos de líneas que no vas a leer nunca. Por qué la recomendación de afuera es la contraria a lo que hace el curso: el modo de falla es asimétrico. Ponerlo de más es cero efecto. No ponerlo cuando hacía falta es silencio: el campo queda en su default y la simulación corre. Este curso puede nombrar por qué en su caso es un no-op —no hay una sola macro `uvm_field_* en code/ fuera de la librería vendorizada —son 0, y el número lo cuenta el build—; el que entra a un testbench ajeno no puede. uvm_agent es el contraejemplo que tenemos a mano: además de uvm_component, lo implementan uvm_agent —que ahí lee is_active— y las dos bases del sequencer (seq/uvm_sequencer_base.svh:122, que lee wait_for_sequences_count del config_db, y seq/uvm_sequencer_param_base.svh:236). Vuelve en el día 6. Con las otras fases pasa lo mismo, más barato todavía: en uvm_component son return;, pero uvm_driver::end_of_elaboration_phase chequea que el seq_item_port esté conectado, y ése lo extendemos nosotros.

ver esta slide en el deck ›

Components y fases

El cuadro completo: son nueve, no cinco

Fase Para qué Orden
build_phase instanciar los componentes top-down
connect_phase conectar los ports bottom-up
end_of_elaboration_phase jerarquía lista, antes de simular bottom-up
start_of_simulation_phase último aviso antes del tiempo 0 bottom-up
run_phase task: acá pasa la simulación un thread c/u
extract_phase juntar los datos de la corrida bottom-up
check_phase decidir si pasó o no bottom-up
report_phase imprimir el veredicto bottom-up
final_phase cerrar archivos y salir top-down

La tabla está para que nadie se vaya creyendo que UVM tiene cinco fases: tiene nueve comunes y doce runtime. El curso usa cinco porque con un solo agent alcanza, y eso hay que decirlo — no es que las otras no importen. Las tres que más se van a cruzar afuera: start_of_simulation_phase es donde la gente imprime el banner de configuración del test; check_phase es donde un scoreboard prolijo decide el veredicto, en vez de contar errores sobre la marcha; y report_phase es la que ya vieron. Que extract / check / report estén separadas tiene una razón concreta: son bottom-up, así que un scoreboard hijo termina de extraer antes de que el env padre decida. Si hacés las tres cosas en report_phase, perdés esa garantía. La pregunta que ordena la tabla: ¿por qué build_phase es top-down? Porque un padre tiene que existir para poder crear a sus hijos. Las del medio necesitan lo contrario — que los hijos ya estén. La otra top-down es final_phase, que cierra el árbol en el mismo orden en que se construyó.

ver esta slide en el deck ›

Components y fases

Y adentro del run_phase, un cronograma

Las doce fases runtime corriendo en paralelo con run_phase, y dónde se engancha la default_sequence

Esta slide existe para que main_phase no aparezca por primera vez en el día 6. La distinción exacta, que confunde a todo el mundo: run_phase y el cronograma reset → configure → main → shutdown corren a la vez, no uno adentro del otro. Un componente puede implementar cualquiera de los dos caminos; lo que no conviene es mezclarlos en el mismo testbench, porque después nadie sabe qué corre cuándo. La pregunta honesta es "¿y entonces cuál uso?". Para un testbench de un solo bloque, run_phase, que es lo que hace el curso. El cronograma se gana el sueldo en integración: el agent del bus de configuración termina su configure y recién ahí el de datos arranca su main, sin que ninguno de los dos equipos haya tenido que escribir un uvm_event ni una bandera compartida.

ver esta slide en el deck ›

Components y fases

El scoreboard, ahora como component

code/u4/components/tb_classes/scoreboard.svh · #class-and-build
// The scoreboard of the TB in objects, now as a uvm_component. The four steps
// --extend, register, constructor, phases-- are in the slides of the components section.
class scoreboard extends uvm_component;
   `uvm_component_utils(scoreboard);

   virtual vtalu_bfm bfm;

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

   // Self-sufficient: the component asks the config_db for the BFM. Before, the
   // test passed it in through the constructor, and the test had to receive it
   // only to hand it out.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("SCOREBOARD", "Failed to get BFM")
   endfunction : build_phase
el archivo entero, 51 líneas
// The scoreboard of the TB in objects, now as a uvm_component. The four steps
// --extend, register, constructor, phases-- are in the slides of the components section.
class scoreboard extends uvm_component;
   `uvm_component_utils(scoreboard);

   virtual vtalu_bfm bfm;

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

   // Self-sufficient: the component asks the config_db for the BFM. Before, the
   // test passed it in through the constructor, and the test had to receive it
   // only to hand it out.
   function void build_phase(uvm_phase phase);
      if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
         `uvm_fatal("SCOREBOARD", "Failed to get BFM")
   endfunction : build_phase

   // run_phase is the only phase that is a task: the only one that consumes
   // time. UVM launches it in its own thread.
   task run_phase(uvm_phase phase);
      shortint predicted_result;
      bit      predicted_ovf;
      forever begin : self_checker
         @(posedge bfm.done) #1;
         case (bfm.op_set)
            add_op: predicted_result = bfm.A + bfm.B;
            sub_op: predicted_result = bfm.A - bfm.B;
            and_op: predicted_result = bfm.A & bfm.B;
            xor_op: predicted_result = bfm.A ^ bfm.B;
            mul_op: predicted_result = bfm.A * bfm.B;
         endcase  // case (op_set)

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

         if ((bfm.op_set != no_op) && (bfm.op_set != rst_op))
            if (predicted_result != bfm.result || predicted_ovf != bfm.ovf)
               `uvm_error("SCOREBOARD", $sformatf(
                          "FAILED: A: %0h  B: %0h  op: %s result: %0h ovf: %0b",
                          bfm.A,
                          bfm.B,
                          bfm.op_set.name(),
                          bfm.result,
                          bfm.ovf
                          ))
      end : self_checker
   endtask : run_phase
endclass : scoreboard

El punto de la slide es la autosuficiencia. Un componente que necesita que alguien le pase sus dependencias obliga a que ese alguien las conozca, y el test termina siendo un repartidor de handles que no usa. Con el config_db, el test no sabe que el scoreboard necesita una BFM. Eso es exactamente lo que hace que el env pueda existir: si cada componente se configura solo, el padre sólo tiene que construirlo. El precio también hay que decirlo: la dependencia dejó de estar en el constructor, donde se veía, y pasó a un string. "bfm" mal escrito compila. Por eso el if (!get(...)) uvm_fatal no es opcional.

ver esta slide en el deck ›

Components y fases

El test construye el árbol y se va

code/u4/components/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   random_tester tester_h;
   coverage      coverage_h;
   scoreboard    scoreboard_h;

   // Components get instantiated in build_phase, never in the constructor.
   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction : build_phase

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

   // No run_phase: the test's job ends once the tree is built.

endclass

Es el cambio de mentalidad de la unidad, y conviene decirlo con estas palabras: el test dejó de hacer el test. Ahora lo arma y se corre a un costado. La objection también se mudó: ya no la levanta el test, la levantan los componentes que tienen trabajo — o, en este ejemplo, el tester. Vale mostrar random_tester.svh un segundo para que se vea. Y la trampa que va a aparecer sola: new() acá funciona porque el tipo está escrito en la declaración. En el env eso se reemplaza por type_id::create(), y ése es el cambio que habilita los overrides. Todavía no, pero conviene sembrarlo.

ver esta slide en el deck ›

Components y fases

El segundo test: hereda, y aun así copia

code/u4/components/tb_classes/add_test.svh
// add_test extends random_test, redeclares tester_h with another type and
// REWRITES the whole build_phase. That duplication is the problem the env section solves
// with the factory override.

class add_test extends random_test;
   `uvm_component_utils(add_test);

   add_tester tester_h;

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

   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction

endclass

Ésta es la slide que hay que dejar incómoda. Si el alumno sale pensando "esto sigue estando mal", el env se explica sola. El shadowing del handle merece un minuto: en la clase base hay un random_tester tester_h y acá un add_tester tester_h. Cualquier método heredado de random_test que use tester_h va a ver el de la base, que quedó en null. Acá no molesta porque no hay ninguno, pero es una bomba de tiempo y es exactamente el tipo de error mudo que el curso persigue. El enganche: en el env no hay dos tests, hay uno; y add_test pasa a ser una línea de set_type_override_by_type.

ver esta slide en el deck ›

Components y fases

Resumen de la unidad

La unidad que convierte el testbench en algo que UVM puede recorrer, y con eso aparecen las herramientas que no existían antes: print_topology() muestra el árbol, y el uvm_config_db puede usar rutas porque ahora hay rutas. El super.build_phase() merece la vuelta que le dimos porque es la pregunta más repetida de los foros. Lo que hay que dejar dicho es la asimetría: no llamarlo apaga la asignación automática de los campos registrados con `uvm_field_*, y eso no da error, da un default. Este curso no usa esas macros y por eso puede no llamarlo; el alumno que cae en un testbench ajeno no sabe si puede, así que lo pone. Lo que no es válido es hacer media cosa de cada una.

ver esta slide en el deck ›

El env: estructura y estímulo

Dos cosas que cambian por motivos distintos

Sección bisagra del curso, y la frase que hay que dejar es ésta: lo que cambia junto va junto, lo que cambia por separado va separado. Todo lo que viene después —agents, sequences— es esta misma operación aplicada a otra cosa. Si el grupo viene lento, ésta es la unidad donde conviene gastar el tiempo que sobre, aunque haya que sacarlo de otro lado. Un alumno que no entienda por qué el env y el test son dos clases no va a poder leer ningún testbench de producción, porque todos están armados así. La pregunta para arrancar, antes de mostrar una línea de código: si mañana el diseño suma una segunda interface, ¿cuántos de tus diez tests hay que tocar? Ninguno, si la estructura vive en un solo lugar. Los diez, si cada test la arma.

ver esta slide en el deck ›

El env: estructura y estímulo

Intractable vs. adaptable

“Jugar al fútbol es muy simple, pero jugar un fútbol simple es lo más difícil que hay.” — Johan Cruyff

La misma jugada: enredada y simple

Cambiale fútbol por testbench y la frase de Cruyff es la unidad entera. Las tres reglas suenan a póster de oficina hasta que se las mide, así que conviene aterrizarlas en el testbench que tienen delante: Una clase, una cosa — el random_test de los components hace dos: arma la estructura y elige el estímulo. Por eso hay que copiarlo para hacer add_test. Nada hardcodeado — tester_h = new(...) clava el tipo en esa línea. El type_id::create() no. Contra la interface — el env va a pedir un base_tester, que es una clase abstracta: no sabe ni le importa cuál llega. Y una advertencia honesta, porque en el trabajo se abusa de esto: código adaptable no quiere decir código preparado para todo. Cada punto de flexibilidad se paga en indirección, y la indirección se paga a las tres de la mañana. Se separa lo que ya se demostró que cambia — y acá se demostró: son dos tests y ya hay copia y pega.

ver esta slide en el deck ›

El env: estructura y estímulo

El problema: dos tests, dos veces el mismo build_phase()

random_test y add_test escriben el mismo build_phase()

Ésta es la slide del problema, y conviene no adelantar la solución: que se vea la duplicación primero, y que se vea dónde está. No está en los testers —ésos ya heredan bien— sino en los tests, que es donde nadie la busca. La pregunta que ordena las dos slides que siguen: ¿qué parte de un tester es el test, y qué parte es infraestructura? El get_op() es el test —cuatro líneas—; todo el resto es infraestructura que no tiene nada que ver con qué querés probar. Ésa es la operación que ya se hizo abajo, y la que falta hacer arriba. Y el criterio general, que vale para cualquier lenguaje: cuando dos clases hermanas comparten un método idéntico, el método está en el lugar equivocado. Va al padre.

ver esta slide en el deck ›

El env: estructura y estímulo

La salida: una clase abstracta, y el estímulo en las hijas

base_tester abstracta con run_phase() compartido y métodos pure virtual

Acá se cobra polimorfismo: base_tester es una clase abstracta con métodos pure virtual, o sea que el run_phase() está escrito una sola vez y las hijas están obligadas a definir get_op() y get_data(). Si una se olvida, no compila — que es el argumento entero de aquella unidad. Y de paso se arregla la trampa del ejercicio del día 2: acá get_op() sí es virtual, porque es pure virtual. Vale volver dos días atrás y mostrarlo. El bullet que importa es el último, y es el que empuja a la slide siguiente: los dos testers siguen randomizando los operandos igual. Que dos hermanas compartan código es señal de que falta un escalón en la jerarquía — y el escalón es que add_tester no herede de base_tester sino de random_tester.

ver esta slide en el deck ›

El env: estructura y estímulo

El resultado: la jerarquía de tres pisos

random_tester como base y add_tester heredando de él

El salto que hay que señalar es el de la tercera flecha: add_tester no hereda del abstracto, hereda del concreto. Es contraintuitivo la primera vez y es la decisión que hace que el test nuevo cueste cuatro líneas. La regla, dicha de una forma que se recuerde: heredás de quien tiene lo que no querés volver a escribir. No del que "conceptualmente corresponde". Y el enganche hacia atrás: esto es polimorfismo usado en serio. pure virtual en el padre es lo que garantiza que ningún tester se olvide de definir get_op() — y si se olvida, no compila.

ver esta slide en el deck ›

El env: estructura y estímulo

add_tester

code/u4/env/tb_classes/add_tester.svh
class add_tester extends random_tester;
   `uvm_component_utils(add_tester)

   function operation_t get_op();
      return add_op;
   endfunction : get_op

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

endclass : add_tester

Ésta es la slide de la sección que conviene dejar un rato en pantalla: un test entero del VTALU son cuatro líneas útiles. Todo lo demás —mandar mil operaciones, randomizar operandos, hablar con la BFM— ya está escrito y no se toca. La regla que se lleva: si agregar un test cuesta cuatro líneas, vas a escribir muchos. Si cuesta copiar una clase de sesenta, vas a escribir tres y después inventar excusas. La cobertura del proyecto se decide acá, no en el covergroup. Y la pregunta que ordena la jerarquía: ¿por qué add_tester hereda de random_tester y no de base_tester? Porque quiere los operandos random y sólo cambia la operación. La herencia no se elige por parecido conceptual, se elige por qué querés no volver a escribir.

ver esta slide en el deck ›

El env: estructura y estímulo

La otra mitad: quién instancia a quién

code/u4/components/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   random_tester tester_h;
   coverage      coverage_h;
   scoreboard    scoreboard_h;

   // Components get instantiated in build_phase, never in the constructor.
   function void build_phase(uvm_phase phase);
      tester_h = new("tester_h", this);
      coverage_h = new("coverage_h", this);
      scoreboard_h = new("scoreboard_h", this);
   endfunction : build_phase

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

   // No run_phase: the test's job ends once the tree is built.

endclass

Vale hacer la cuenta en voz alta, porque el problema se ve mejor con números que con el argumento de diseño: si cada test instancia sus tres componentes, con diez tests hay diez build_phase que dicen lo mismo. El día que se agrega un cuarto componente al testbench, son diez ediciones — y la que te olvidás no falla al compilar, simplemente corre sin ese componente. uvm_env no es magia ni trae comportamiento nuevo: es un uvm_component con otro nombre. Lo que aporta es convención: cuando alguien abre un testbench UVM ajeno, sabe que la estructura está en el env. Eso es la mitad del valor de UVM y conviene decirlo así de crudo — no es la librería, es que todos ponen las cosas en el mismo lugar. Buena pregunta: ¿por qué el env casi nunca tiene run_phase? Porque no manda ni mira nada; sólo construye y conecta. Si tu env tiene un run_phase, es señal de que se metió lógica de test adentro de la estructura.

ver esta slide en el deck ›

El env: estructura y estímulo

El mapa después de separar

El diagrama de clases antes y después de separar estructura de estímulo con uvm_env

Vale dejar el diagrama en pantalla y pedir que ubiquen dónde quedó cada clase de los components. El ejercicio hace que se note lo que desapareció: ya no hay un build_phase por test que instancie tres componentes. La pregunta de control: si mañana el testbench suma un cuarto componente, ¿cuántos archivos se tocan? Uno, el env. Con la estructura de los components eran tantos como tests hubiera.

ver esta slide en el deck ›

El env: estructura y estímulo

La clase env: instanciar y conectar, nada más

code/u4/env/tb_classes/env.svh
// The structure of the TB, and nothing else: it instantiates the components and
// connects them. Which tester arrives is the test's call, with a factory override.
class env extends uvm_env;
   `uvm_component_utils(env);

   base_tester tester_h;
   coverage    coverage_h;
   scoreboard  scoreboard_h;

   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase

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

endclass

Sección bisagra del curso: separar QUÉ estructura tiene el testbench (env) de QUÉ estímulo se manda (test). Todo lo que viene después —agents, sequences— es esta misma idea llevada más lejos. Si el grupo viene lento, ésta es la slide donde conviene gastar el tiempo que sobre.

ver esta slide en el deck ›

El env: estructura y estímulo

La factory de UVM: la clase se anota sola

La macro es lo que reemplaza al case de la fábrica escrita a mano: cada clase se anota sola en el diccionario, y agregar un tipo no obliga a editar la fábrica. La diferencia entre las dos macros es la que se cobra en las transactions, y conviene sembrarla ahora: uvm_component_utils es para lo que vive en el árbol y recibe fases; uvm_object_utils es para lo que viaja —un dato— y no recibe ninguna. Elegir la equivocada compila y da errores raros después. Que no haya que castear no es cosmético: el $cast de la factory se podía olvidar, y olvidarlo era un null pointer en simulación.

ver esta slide en el deck ›

El env: estructura y estímulo

La fórmula, leída de izquierda a derecha

Anatomía de la línea tipo::type_id::create(nombre, this)

Vale leer la fórmula en voz alta una vez, despacio, porque se va a escribir cincuenta veces en lo que queda del curso: "clase, dos puntos dos puntos, type_id, dos puntos dos puntos, create, nombre, this". El último bullet es el que no tranquiliza, y hay que decirlo así. Si base_tester no se registró y nada arriba tampoco, base_tester::type_id no existe y no compila: ése es el caso bueno. Pero en la jerarquía de esta misma sección —las tres registradas— olvidarse la macro en add_tester no rompe nada: el type_id y el get_type() se heredan de random_tester, el testbench compila limpio, y el set_type_override del add_test termina registrando random_tester. Medido: add_tester::get_type().get_type_name() devuelve random_tester, el env construye un random_tester, y el test corre con UVM_ERROR : 0. Es de la familia «no rompe, miente» del apéndice de trampas mudas: el único caso en que sí falla al compilar es si el handle se declara del tipo derivado, y acá el handle es base_tester tester_h;. El nombre del primer argumento no es decorativo: es la ruta del componente, la que usa el config_db y la que imprime print_topology. Ponerle otro nombre que el del handle es una forma barata de perder media hora en los agents.

ver esta slide en el deck ›

El env: estructura y estímulo

La objeción que salta sola: base_tester es abstracta

code/u4/env/tb_classes/env.svh · #build_phase
   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase
el archivo entero, 22 líneas
// The structure of the TB, and nothing else: it instantiates the components and
// connects them. Which tester arrives is the test's call, with a factory override.
class env extends uvm_env;
   `uvm_component_utils(env);

   base_tester tester_h;
   coverage    coverage_h;
   scoreboard  scoreboard_h;

   function void build_phase(uvm_phase phase);
      // base_tester is abstract and this works anyway: create() does not build
      // it, it asks the factory what to hand out when somebody asks for a base_tester.
      tester_h = base_tester::type_id::create("tester_h", this);
      coverage_h = coverage::type_id::create("coverage_h", this);
      scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
   endfunction : build_phase

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

endclass

La objeción de la slide es correcta y hay que tomarla en serio, porque si no parece un truco sucio: base_tester es abstracta y ese código sí compila y corre. La explicación en una línea: type_id::create() no construye un base_tester; le pregunta a la factory qué construir cuando alguien pide un base_tester, y devuelve eso. La analogía que funciona: base_tester acá no es un molde, es un formulario de pedido. Nadie instancia el formulario. El corolario, que es el que se paga después: si ningún test hizo el override, la factory sí intenta construir la clase abstracta y ahí sí explota — y el mensaje no dice "te faltó el override", dice algo sobre una clase abstracta. Vale mostrarlo una vez comentando el set_type_override del random_test.

ver esta slide en el deck ›

El env: estructura y estímulo

El override: cambiar la pieza sin tocar el plano

La frase que ordena la slide: el override no cambia el código, cambia lo que la factory devuelve. El env sigue diciendo exactamente lo mismo. Conviene nombrar las dos formas que existen, porque las dos aparecen en código ajeno: set_type_override cambia todos los pedidos de ese tipo en todo el testbench; set_inst_override cambia sólo los de una ruta — "uvm_test_top.env_h.tester_h", absoluta—. El curso usa la primera porque hay un solo tester; con dos agents, la segunda es la que sirve, y tiene su slide dos más adelante. Y el enganche hacia adelante: es exactamente el mecanismo que en las transactions va a reemplazar una command_transaction por una maxmult_transaction. Misma llamada, otro tipo.

ver esta slide en el deck ›

El env: estructura y estímulo

El override de la factory

Anatomía del factory override con set_type_override()

El override es lo que hace que el env no cambie nunca: el test pide un base_tester y la factory devuelve el que ese test quiera. Cuando alguien pregunte si esto no es magia negra: es un diccionario de tipos, y por eso hay que registrar todo con `uvm_component_utils. Si te lo olvidás, la factory no encuentra la clase y el mensaje de error no ayuda nada.

ver esta slide en el deck ›

El env: estructura y estímulo

El override de la factory, en el test

code/u4/env/tb_classes/random_test.svh
class random_test extends uvm_test;
   `uvm_component_utils(random_test);

   env env_h;

   function void build_phase(uvm_phase phase);
      base_tester::type_id::set_type_override(random_tester::get_type());
      env_h = env::type_id::create("env_h", this);
   endfunction : build_phase

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

endclass

El test entero son dos líneas: el override y el create() del env. Vale mostrarlo al lado del random_test de los components, que instanciaba tres componentes a mano. Ese es el resultado de la sección, medido. Y hay un detalle de orden que se paga caro y que conviene decir ahora, porque vuelve como pregunta 5 del repaso: el set_type_override() va antes del create() del env. La factory decide qué construir en el momento del create(), no antes ni después. Si el override llega tarde, no hay error — el env ya construyó la clase base y el test corre con el estímulo equivocado. Otra vez lo mismo: no rompe, miente. Con eso queda dicho el arco: el env no cambia nunca, el test cambia siempre, y lo único que los une es un string de tipo en la factory.

ver esta slide en el deck ›

El env: estructura y estímulo

Por instancia, y la perilla para ver qué quedó puesto

base_tester::type_id::set_type_override(add_tester::get_type());          // TODOS
base_tester::type_id::set_inst_override(mult_tester::get_type(),
                                        "uvm_test_top.env_h.t2_h");      // uno solo

uvm_coreservice_t::get().get_factory().print(0);   // qué overrides hay puestos
Instance Overrides:
  Requested Type  Override Path            Override Type
  base_tester     uvm_test_top.env_h.t2_h  mult_tester
  base_tester     env_h.t3_h               mult_tester   <- registrado y NO aplicó
Type Overrides:
  base_tester     add_tester

Es la segunda mitad del override y la que aparece apenas hay dos agents del mismo tipo: no querés cambiar los dos, querés cambiar uno. La trampa de la ruta hay que decirla con el número de la medición, porque es exactamente la forma en que este mecanismo falla: el override con la ruta mal escrita se registra igual. factory.print(0) lo muestra en la tabla, prolijo, como si estuviera funcionando. No hay warning, no hay error, y el componente sale del tipo base. Es la misma familia que el set_type_override tardío del apéndice de trampas: la factory no valida rutas porque no puede — cuando se registra el override, el componente todavía no existe. La regla práctica: la ruta es la que imprime uvm_top.print_topology(), sin el <unnamed> de arriba. Si el árbol dice uvm_test_top.env_h.t2_h, eso es lo que va entre comillas. Y si no querés depender de dónde te instancian, *.env_h.t2_h, que también está medido y también aplica. El par de herramientas es lo que hay que llevarse: factory.print(0) dice lo que la factory tiene anotado, print_topology() dice qué se construyó de verdad. Un override que aparece en la primera y no se ve en la segunda es una ruta mal escrita o un create() que llegó antes.

ver esta slide en el deck ›

El env: estructura y estímulo

Resumen de la unidad

Es la unidad donde se cobra todo el día 2. Conviene hacerlo explícito señalando el add_test: son dos líneas —el override y nada más—, y cambia el estímulo completo sin tocar el env. Eso no se podía hacer en el testbench en objetos. La distinción intractable / adaptable es la que hay que dejar como vocabulario, porque vuelve con el agent y con las sequences. Código intractable es el que sale rápido y después no se deja tocar: casi siempre es estructura duplicada.

ver esta slide en el deck ›

Reporting

El 47 % del tiempo se va acá

Vale volver al gráfico del día 1 antes de arrancar, porque esta sección parece de plomería y es el que más horas devuelve. El argumento no es "los mensajes lindos": es que un mensaje que trae solo la ruta jerárquica te ahorra la pregunta "¿quién lo dijo?", que es la mitad de cualquier debug. La segunda idea, que es la que cuesta: los mensajes de debug no se borran. Un `uvm_info en UVM_HIGH no cuesta nada cuando el techo está en UVM_MEDIUM, y el día que lo necesitás está ahí. Que el ejercicio del día 5 se resuelva subiendo la verbosidad no es casualidad — está armado para que lo vivan. Y la distinción de la última línea conviene anotarla en el pizarrón de entrada, porque es la pregunta 6 del repaso: verbosidad e info por un lado, actions y error/warning/fatal por el otro. Nunca se cruzan.

ver esta slide en el deck ›

Reporting

Lo que $display no puede hacer

La pregunta que ordena la slide, y conviene tirarla al grupo antes de la respuesta: ¿qué tiene de malo $display? La respuesta que sale siempre es "es feo", y no es ésa. $display imprime o no existe: la decisión de si un mensaje sale se toma en el momento de escribirlo, editando el código. Lo que UVM agrega no es formato, es que la decisión se mueve al momento de correr, y se toma desde afuera con un plusarg. Ese cambio de tiempo es toda la sección: por eso los mensajes se pueden dejar puestos, y por eso el log de una regresión y el log de un debug pueden salir del mismo binario. El dato de contexto, para el que viene de VHDL o de Verilog puro: nada de esto es magia del simulador. `uvm_info es una macro que arma un string y se lo pasa a un objeto —el report server— que decide qué hacer. Todo lo que sigue en la sección es configurar ese objeto.

ver esta slide en el deck ›

Reporting

Las macros de reporting

code/u4/reporting/reporting.sv
// ILLUSTRATION -- not compiled. The signature of each macro, not a call.
`uvm_info   (Message ID String, Message String, Verbosity)
`uvm_warning(Message ID String, Message String)
`uvm_error  (Message ID String, Message String)
`uvm_fatal  (Message ID String, Message String)

La confusión de siempre: la verbosidad filtra `uvm_info, y nada más. Los warnings, errores y fatales no tienen verbosidad — se controlan con actions, que es la última parte de la sección.

ver esta slide en el deck ›

Reporting

Las macros de reporting: qué hace cada una

code/u4/reporting/tb_classes/scoreboard.svh · #the-report
      data_str = $sformatf(
          " %2h %0s %2h = %4h (%4h predicted)",
          cmd.A,
          cmd.op.name(),
          cmd.B,
          t,
          predicted_result
      );

      if (predicted_result != t) `uvm_error("SCOREBOARD", {"FAIL: ", data_str})
      else `uvm_info("SCOREBOARD", {"PASS: ", data_str}, UVM_HIGH)
el archivo entero, 45 líneas
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

   function void write(shortint t);
      shortint predicted_result;
      string data_str;
      command_s cmd;
      cmd.A = 0;
      cmd.B = 0;
      cmd.op = no_op;

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

      case (cmd.op)
         add_op: predicted_result = cmd.A + cmd.B + 1;  //force an error on adds
         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)
      data_str = $sformatf(
          " %2h %0s %2h = %4h (%4h predicted)",
          cmd.A,
          cmd.op.name(),
          cmd.B,
          t,
          predicted_result
      );

      if (predicted_result != t) `uvm_error("SCOREBOARD", {"FAIL: ", data_str})
      else `uvm_info("SCOREBOARD", {"PASS: ", data_str}, UVM_HIGH)
   endfunction : write

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new
endclass : scoreboard
code/u4/reporting/tb_classes/scoreboard_error.txt
UVM_ERROR tb_classes/scoreboard.svh(39) @ 290: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] FAIL:  5b add_op ff = 015a (015b predicted)

Vale detenerse en la línea del log y leerla en voz alta de izquierda a derecha, campo por campo, porque es la que el alumno va a mirar mil veces y nadie se la explica nunca: severidad, tiempo, archivo y línea del `uvm_error, y después uvm_test_top.env_h.scoreboard_h — la ruta de instancia. Recién ahí viene el ID y el mensaje. El punto pedagógico es lo que **no** está en el código: el scoreboard no imprimió el tiempo ni su propio nombre. Los puso la macro. Ése es todo el argumento a favor de no volver a escribir un $display en un testbench. Y la comparación honesta con el $error del día 2: aquel scoreboard también hacía fallar el test, pero el contexto había que escribirlo a mano — y ahí nadie escribe la ruta jerárquica, porque no la sabe.

ver esta slide en el deck ›

Reporting

Los seis niveles, y por qué el mensaje se queda

code/u4/reporting/tb_classes/verbosidad.svh
// ILLUSTRATION -- not compiled. uvm_object_globals.svh:382-390, uvm-core 2020.3.1,
// copied verbatim: code/.uvm/ is downloaded and gitignored, so the slide cannot
// pull the enum straight out of the library.
typedef enum {
   UVM_NONE   = 0,
   UVM_LOW    = 100,
   UVM_MEDIUM = 200,  // the default: with nothing set, UVM_MEDIUM is what you get
   UVM_HIGH   = 300,
   UVM_FULL   = 400,
   UVM_DEBUG  = 500
} uvm_verbosity;

// Any message above the verbosity that was set does not get printed

Los seis niveles, de menos a más ruidoso: UVM_NONE, UVM_LOW, UVM_MEDIUM —el que está por defecto—, UVM_HIGH, UVM_FULL y UVM_DEBUG. UVM_FULL es el que nadie usa y el que aparece en el enum de la slide, así que vale nombrarlo. Un mensaje se imprime si su verbosidad es menor o igual al techo, así que UVM_NONE sale siempre. La regla práctica que conviene bajar, porque si no cada uno inventa la suya: UVM_LOW para lo que querés ver en una regresión —qué test corrió, cuántas transacciones—; UVM_MEDIUM para el resumen de un componente; UVM_HIGH para cada transacción que pasa. Los monitores del curso son UVM_HIGH justamente por eso: en una corrida normal no se ven, y cuando algo falla se prenden con un plusarg. El detalle de forma que se copia mal: la verbosidad es el tercer argumento del `uvm_info, y si te la olvidás no compila. El ID —el primero— es un string libre, pero no lo elijas al azar: es la llave con la que después vas a filtrar. Que sea el nombre del componente, en mayúsculas, y siempre el mismo.

ver esta slide en el deck ›

Reporting

El techo global: un plusarg, sin recompilar

code/u4/reporting/tb_classes/verbosity_ceiling.txt
./obj_dir/top/sim +UVM_TESTNAME=random_test +UVM_VERBOSITY=UVM_HIGH

Conviene correrlo en vivo, porque es la demostración más barata de la sección: la misma línea de arriba —./obj_dir/top/sim +UVM_TESTNAME=random_test— con y sin +UVM_VERBOSITY=UVM_HIGH, y el log pasa de 7 uvm_info a 22. Con make son 6 y 21, porque run_sim agrega +UVM_NO_RELNOTES y eso se lleva el banner. Sin recompilar nada — es el mismo binario. Parecen pocos porque este ejemplo manda diez operaciones a propósito, para que el transcript entre en la pantalla; con las mil de las otras secciones la diferencia es de tres órdenes de magnitud. El detalle que hay que decir para que después no sorprenda: el plusarg fija el techo del árbol entero, desde uvm_top para abajo. No hay forma de pedir "alto, pero sólo el monitor" desde la línea de comando con este flag; para eso está +uvm_set_verbosity, que es más largo de escribir y casi nadie recuerda. La salida honesta, y la que enseña la slide siguiente, es fijarlo desde el código en el componente que te interesa. Y el argumento de fondo, por si el grupo lo subestima: éste es el mecanismo que hace que un log de regresión de mil tests sea legible. No es cosmética de aula.

ver esta slide en el deck ›

Reporting

uvm_cmdline_processor: la línea de comandos, entera

uvm_cmdline_processor clp = uvm_cmdline_processor::get_inst();
string valor;

if (clp.get_arg_value("+COUNT=", valor)) count = valor.atoi();

El curso usa $value$plusargs y $test$plusargs en los base_test porque son una línea y se leen solos. Vale decir por qué en un testbench de producción aparece siempre esta clase: $value$plusargs es una system task, así que no se puede heredar, no se puede sustituir por la factory y no se puede testear. El uvm_cmdline_processor es un objeto, y eso alcanza para las tres cosas. El argumento que más rinde es el último bullet y no es el que uno espera: get_args() imprimiendo la línea entera en el start_of_simulation_phase es lo que convierte un log viejo en algo reproducible. Sin eso, el log dice qué pasó pero no con qué se corrió, y una regresión de hace tres meses no se puede repetir. La diferencia del segundo bullet vale una anécdota: dos +COUNT= en la misma línea —porque el script los agrega y el usuario también— es un caso real, y $value$plusargs toma uno sin decir cuál. El uvm_cmdline_processor devuelve 2, así que al menos te podés dar cuenta. Es la clase de cosa que se paga una vez y se recuerda.

ver esta slide en el deck ›

Reporting

El techo por rama: dos métodos y un árbol

Jerarquía de instancias que UVM arma en build_phase

Este diagrama vale más de lo que parece, porque explica de dónde sale ese prefijo larguísimo que trae cada mensaje: uvm_test_top.env_h.scoreboard_h. No es decoración — es la ruta de instancia del componente que habló. Y es la misma ruta que usa el uvm_config_db para el ámbito. Vale decirlo explícito: cuando en los tests dijimos "el ámbito es una ruta jerárquica", era literalmente esta ruta. Un solo árbol sirve para las dos cosas. La aclaración del bullet no es un detalle: esta jerarquía no es la del DUT. Un scoreboard no está adentro de ningún módulo. Es el árbol que UVM arma llamando build_phase de arriba hacia abajo, y existe sólo en el testbench. Truco para el aula: uvm_top.print_topology() imprime este árbol de verdad, escrito por la librería. Es la mejor forma de mostrar que no es un dibujo. +UVM_CONFIG_DB_TRACE es otra cosa y conviene no mezclarlas: no dibuja el árbol, traza cada set() y cada get() del config_db con el ámbito que se usó — que es la perilla del otro problema, el del ámbito que no matchea.

ver esta slide en el deck ›

Reporting

El techo por rama: dónde va la llamada

code/u4/reporting/tb_classes/env.svh · #end_of_elaboration_phase
   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase
el archivo entero, 46 líneas
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester    random_tester_h;
   coverage  coverage_h;
   scoreboard scoreboard_h;
   driver    driver_h;
   command_monitor command_monitor_h;
   result_monitor result_monitor_h;
   uvm_tlm_fifo #(command_s) command_f;

   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

   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase

   function void connect_phase(uvm_phase phase);
      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

La ventana es lo que hay que dejar grabado, porque el error se comete en las dos direcciones. Si la llamada va en el build_phase, el componente al que le querés subir el techo todavía no existe —lo construye su padre más abajo— y la llamada no encuentra a nadie. Si va en el run_phase, la simulación ya arrancó y te perdiste los mensajes de las fases anteriores. end_of_elaboration_phase es exactamente el hueco entre las dos cosas: el árbol completo, y el tiempo todavía en cero. Es la primera vez en el curso que se usa para algo, y vale decirlo así — en la tabla de las nueve fases del día 3 no estaba de adorno. El sufijo _hier es el que se olvida, y falla en silencio: fijás el techo del env sin _hier, el env no imprime nada de todos modos, y el monitor que querías escuchar sigue callado. Regla práctica: si apuntás a una rama, _hier; si apuntás a una clase, sin.

ver esta slide en el deck ›

Reporting

Apagar warnings, errores y mensajes fatales

code/u4/reporting/scoreboard1.txt
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
UVM_INFO tb_classes/scoreboard.svh(40) @ 150: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] PASS:  43 and_op ff = 0043 (0043 predicted)
UVM_ERROR tb_classes/scoreboard.svh(39) @ 290: uvm_test_top.env_h.scoreboard_h [SCOREBOARD] FAIL:  5b add_op ff = 015a (015b predicted)
...
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    4
UVM_WARNING :    0
UVM_ERROR :    2
UVM_FATAL :    0
** Report counts by id
[RNTST]     1
[SCOREBOARD]     5
- $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 820ps

El log de la slide es el de un scoreboard que suma mal a propósito, y conviene mirar el Report Summary del final antes que el error: UVM_ERROR : 2, uno por cada add_op que salió en las diez operaciones del ejemplo. Ahí está la cuenta que hay que hacer en voz alta: con las mil operaciones de las otras secciones son cien y pico de errores, uno por cada suma. Ése es el problema real que la sección viene a resolver — no "el mensaje molesta", sino que el log deja de servir para buscar otra cosa. La pregunta para tirar al grupo, porque la respuesta equivocada es la intuitiva: "¿bajo el techo de verbosidad y listo?". No. Un `uvm_error no tiene verbosidad; el techo no lo toca. Es la misma distinción de la primera slide, y acá se cobra por primera vez. Y la aclaración honesta antes de enseñar a apagar errores, porque si no queda sonando raro: esto no es para que la regresión dé verde. Es para poder trabajar media tarde mientras otro arregla su clase, y se saca el mismo día.

ver esta slide en el deck ›

Reporting

Apagar mensajes: por dónde se hace

code/u4/reporting/tb_classes/UVM_report_actions.sv
// ILLUSTRATION -- not compiled. uvm_object_globals.svh:354-364, uvm-core 2020.3.1,
// copied verbatim: code/.uvm/ is downloaded and gitignored, so the slide cannot
// pull the enum straight out of the library.
typedef enum {
   UVM_NO_ACTION = 'b0000000,
   UVM_DISPLAY   = 'b0000001,
   UVM_LOG       = 'b0000010,
   UVM_COUNT     = 'b0000100,
   UVM_EXIT      = 'b0001000,
   UVM_CALL_HOOK = 'b0010000,
   UVM_STOP      = 'b0100000,
   UVM_RM_RECORD = 'b1000000
} uvm_action_type;

Las siete acciones son un OR de bits, no una lista de opciones excluyentes: un mismo mensaje puede imprimirse y escribirse a un archivo y contar para el resumen. Por eso se combinan con |. UVM_COUNT es la que más se explica mal, y conviene decirla completa: ya está puesta por defecto en todo `uvm_error, y lo que incrementa es **un contador global** de la corrida, no uno por ID. Sola no mata nada, porque el máximo por defecto es 0, que quiere decir "sin límite". La que corta es +UVM_MAX_QUIT_COUNT=N (uvm_root.svh:1187) —o set_report_max_quit_count(N), uvm_report_object.svh:578—, y ésa sí es la respuesta al log de cuatro gigas cuando un scoreboard falla en cada transacción: te enterás igual, y en treinta segundos en vez de en veinte minutos. UVM_NO_ACTION es la que usa la slide siguiente para apagar los errores, y hay que decir en voz alta para qué **no** sirve: no es para que la regresión dé verde. Es para seguir trabajando mientras otro arregla su clase, y se saca el mismo día. Un UVM_NO_ACTION que sobrevive a un commit es un bug esperando. Y el par que conviene nombrar: set_report_severity_action es por severidad, set_report_id_action es por ese string de ID que elegiste al escribir el mensaje. Ahí se paga la disciplina de haber usado siempre el mismo.

ver esta slide en el deck ›

Reporting

Apagar mensajes: el ejemplo completo, y el log que queda

code/u4/reporting/tb_classes/env.svh · #end_of_elaboration_phase
   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase
el archivo entero, 46 líneas
class env extends uvm_env;
   `uvm_component_utils(env);

   random_tester    random_tester_h;
   coverage  coverage_h;
   scoreboard scoreboard_h;
   driver    driver_h;
   command_monitor command_monitor_h;
   result_monitor result_monitor_h;
   uvm_tlm_fifo #(command_s) command_f;

   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

   // To use the UVM hierarchy to control verbosity, the reporting methods have to
   // be called after the UVM hierarchy has been built, but BEFORE the simulation
   // starts. That is what end_of_elaboration_phase is for
   function void end_of_elaboration_phase(uvm_phase phase);
      //scoreboard_h.set_report_severity_action_hier(UVM_ERROR, UVM_NO_ACTION);
      scoreboard_h.set_report_verbosity_level_hier(UVM_HIGH);
   endfunction : end_of_elaboration_phase

   function void connect_phase(uvm_phase phase);
      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
code/u4/reporting/scoreboard2.txt
UVM_INFO @ 0: reporter [RNTST] Running test random_test...
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO :    1
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[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 820ps

Decirlo en voz alta: apagar errores sirve para seguir trabajando mientras otro arregla su clase, no para que la regresión dé verde. Y mostrar el Report Summary del segundo log al lado del primero es la mejor advertencia que da la sección: el testbench dice 0 UVM_ERROR y el DUT sigue exactamente igual de roto que cuando lo rompimos. Ésa es la razón de que un UVM_NO_ACTION no pueda sobrevivir a un commit — es una trampa muda, y está en el apéndice. El scoreboard de esta sección suma de más A PROPÓSITO, es el bug que estamos mostrando. Por eso code/u4/reporting/run.sh exporta UVM_ERRORS_OK=1: por defecto un uvm_error hace fallar la corrida, y acá el error es el ejemplo.

ver esta slide en el deck ›

Reporting

uvm_report_catcher: demotar el error que esperabas

class demotar_pslverr extends uvm_report_catcher;
   virtual function action_e catch();
      if (get_severity() == UVM_ERROR && get_id() == "PSLVERR")
         set_severity(UVM_INFO);      // deja de contar como error
      return THROW;                   // y sigue viaje, ya degradado
   endfunction
endclass

demotar_pslverr c = new();
uvm_report_cb::add(null, c);          // null = todo el testbench; o un componente

Es la respuesta correcta a la slide anterior, y la diferencia es de honestidad, no de sintaxis. UVM_NO_ACTION sobre el scoreboard apaga el error y el log queda idéntico al de un testbench sano: nadie que lea ese log se entera. El catcher degrada ese mensaje, deja pasar todos los demás, y encima lo cuenta en una línea propia del UVM Report catcher Summary. Un revisor que abre el log ve que hubo un error y que alguien decidió que estaba bien. Cuándo se usa de verdad: en los tests negativos, que son la mitad de un plan de verificación serio. Escribir en un registro de sólo lectura tiene que dar PSLVERR; el test que lo prueba espera el error, y sin catcher ese test no puede dar verde nunca. Es exactamente el caso del capstone. El detalle de implementación que sorprende: un catcher es un uvm_callback, o sea la misma pieza de los callbacks del día 6. Por eso se registra con uvm_report_cb::add y no con un set_ del componente. Y la trampa: si el catcher es demasiado ancho —por severidad y nada más— apagaste todos los errores del testbench con más ceremonia que UVM_NO_ACTION. El get_id() del if no es opcional.

ver esta slide en el deck ›

Reporting

Resumen de la unidad

Cerrar el día volviendo al 47 %: de todo lo que se vio hoy —tests, components, fases, env— ésta es la sección que se usa todos los días, y la única que se nota cuando falta. Un testbench sin reporting funciona igual; debuggearlo cuesta el doble. La pregunta de control, que además es la 6 del repaso: *"bajé el techo de verbosidad y el `uvm_error sigue apareciendo, ¿por qué?"*. Si el grupo la contesta sin dudar, la sección cerró. Y el aviso para el ejercicio del día 5, que conviene dar ahora y no cuando estén trabados: el bug no se ve en el log por defecto. Está impreso, en UVM_HIGH. El que no se acuerde de esta sección va a leer el código media hora antes de subir la verbosidad.

ver esta slide en el deck ›

Repaso · Día 3

1 de 7 · uvm_test

¿Qué te permite hacer +UVM_TESTNAME=add_test que antes no podías?

Elegir el test sin recompilar — UVM lee ese plusarg y le pide el test a la factory por nombre. Es la diferencia entre 1000 tests × 5 minutos de compilación y una sola compilación.

ver esta slide en el deck ›

Repaso · Día 3

2 de 7 · UVM Phases

¿En qué orden recorre UVM la jerarquía en build_phase y en connect_phase?

Build top-down, connect bottom-up — y tiene sentido: no podés conectar un componente que todavía no existe, así que primero se construye toda la jerarquía y recién después se conecta.

ver esta slide en el deck ›

Repaso · Día 3

3 de 7 · Objections

¿Para qué sirve raise_objection() / drop_objection() en el run_phase?

Para que la fase no termine antes — todos los run_phase corren en paralelo, cada uno en su thread, y la fase termina cuando cae la última objection. Sin levantarla, la simulación se te termina en el tiempo 0.

ver esta slide en el deck ›

Repaso · Día 3

4 de 7 · El env

¿Qué le toca a cada uno: uvm_env y uvm_test?

Estructura vs. estímulo — cada clase hace una sola cosa bien. Por eso el env casi siempre tiene sólo build_phase y connect_phase, y el test casi siempre tiene sólo un override de la factory.

ver esta slide en el deck ›

Repaso · Día 3

5 de 7 · Factory override

set_type_override() reemplaza base_tester por add_tester. ¿Cuándo hay que llamarlo?

Antes del build_phase — la factory decide qué construir en el momento del create(). Si el override llega tarde, el env ya instanció la clase base y no hace nada.

ver esta slide en el deck ›

Repaso · Día 3

6 de 7 · Verbosidad

El techo de verbosidad (+UVM_VERBOSITY=UVM_HIGH), ¿sobre qué macros actúa?

Sólo sobre `uvm_info — warnings, errores y fatales son inmunes al techo de verbosidad, y está bien que así sea: nadie quiere apagar un error sin querer. Para esos hace falta el mecanismo de actions.

ver esta slide en el deck ›

Repaso · Día 3

7 de 7 · Report actions

Querés silenciar los `uvm_error de un scoreboard que otra persona está arreglando. ¿Dónde va el set_report_severity_action_hier()?

En end_of_elaboration_phase — tiene que ser después de que la jerarquía esté construida (si no, el componente todavía no existe) y antes de que arranque la simulación. Esa fase es exactamente esa ventana.

ver esta slide en el deck ›

Ejercicio · Día 3 · 1 de 2

Un test nuevo sin tocar la estructura

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

Antes de largarlos al ejercicio, volver a la slide "El mapa del día 3: qué reemplaza a qué" del testbench en objetos y leer la columna derecha de corrido: el build_phase del env, el config_db, los run_phase, las objections y el set_type_override y el $display que pasó a uvm_info. Las seis filas están tachadas, y el día cierra ahí. Es el ejercicio del día 2 otra vez, pero con la factory. Vale ponerlos a comparar los dos: en el día 2 el tipo se elegía en el código del testbench, acá lo elige la factory y el testbench no cambia una línea. Y que miren la cobertura al final: con puras multiplicaciones baja a 26 %. Un test enfocado cubre menos, y ésa es la razón de que hagan falta muchos — y de que agregar uno tenga que ser barato.

ver esta slide en el deck ›

Ejercicio · Día 3 · 2 de 2

El uvm_error que no dice nada

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

Es el ejercicio corto del día 3 y cierra el arco que abrió el ejercicio de las ondas del día 1: ahí el alumno era el que leía el FAILED, acá es el que lo escribe. Diez minutos de tipeo, y el punto no está en el $sformatf. El punto está en el segundo pedido, que es el que sorprende: el PASS también se escribe. La reacción típica es "¿para qué quiero mil líneas de PASS?", y la respuesta es que no las querés hoy: las querés el día que falla la operación 700 y necesitás saber qué pasó en la 699. Por eso se escribe y se esconde detrás de UVM_HIGH. Ésa es la diferencia entre severidad y verbosidad, dicha con las manos. El corrector corre el mismo testbench tres veces, y conviene contarlo porque es la forma en que se prueba un log: una con el DUT roto —+VTALU_BUG— para que el error dispare y se pueda leer, una con la verbosidad de siempre para exigir que el PASS no esté, y una con +UVM_VERBOSITY=UVM_HIGH para exigir que esté una vez por comparación. Un mensaje que no se probó de las tres formas es un mensaje que va a mentir en la regresión de la noche.

ver esta slide en el deck ›