Verify This! · el curso para leer

Día 1Por qué se verifica, y el testbench sin 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.

Verify This!

Introducción a UVM

github.com/leandrotozzi/verifythis · SystemVerilog · Verilator · UVM 2020.3.1

ver esta slide en el deck ›

Cómo usar este curso

Sobre todo si lo estás haciendo solo

Si estás dictando el curso, estas dos slides se saltean con la tecla 1. Están acá porque la mayoría de la gente que abra esto no va a tener un instructor al lado, y sin las notas del presentador se pierde la mitad del material — que además es la mitad más difícil de reconstruir leyendo el código.

ver esta slide en el deck ›

Cómo usar este curso

Correrlo, navegarlo, y dónde buscar cuando algo no anda

Las dos últimas son las que más rinden para el que estudia solo, y son las que nadie lee hasta que ya se trabó: el libro trae estas mismas notas adentro del texto, y los apéndices están escritos como tabla de síntomas, para buscar y no para leer de corrido.

ver esta slide en el deck ›

Agenda

Día 1 · ≈ 4 h · unidades 1 y 2

Unidad 1 · Por qué se verifica

Unidad 2 · El testbench sin UVM

Dos ejercicios, y el segundo se resuelve con el visor de ondas

Al final del día podés:

Los cuatro verbos de abajo son el contrato del día, y conviene leerlos en voz alta antes de empezar: en video son los diez segundos en los que alguien decide si éste es el que buscaba. Tres son las filas 2, 4 y 5 de la autoevaluación del cierre, traídas al principio; el del bug a propósito es la práctica que atraviesa el curso entero, de make mutante hoy a +BUG=1 en el capstone. El que los pueda contestar al terminar el día no necesita mirar nada más. Y una advertencia honesta para el que viene de VHDL o de Verilog-2001, que es la audiencia declarada: el día 2 nivela la OOP, pero nadie nivela SystemVerilog, y hoy aparecen interface, logic, enum, package, covergroup, clocking y una assert property. Está todo en una carilla en docs/systemverilog-para-el-que-viene-de-vhdl.md: media hora antes de arrancar, y el día 1 deja de tener dos escalones a la vez.

ver esta slide en el deck ›

Seis de cada siete

Y el bug que llegó al silicio pasó una regresión en verde

Cómo terminan los proyectos IC/ASIC en 2024: 14 % acierta el primer silicio y 75 % llega atrasado

Es la primera slide con contenido del curso a propósito: antes de explicar qué es UVM y antes de cualquier otro gráfico, una pregunta que el alumno no puede contestar todavía. El número es del Wilson Research Group 2024 y no es una impresión: sólo el 14 % sale bien en el primer silicio, o sea que seis de cada siete necesitan al menos un respin, y es el peor valor en veinte años de encuesta. Preguntar quién hizo un tape-out y cómo salió engancha más que el gráfico. Si preguntan de dónde salen los datos: res/trends/data.json, y las figuras se regeneran con make figs. La honestidad que hay que decir en voz alta: no todos los respins son funcionales —hay de timing, de analógica, de una spec que cambió tarde—. Pero el que lo es pasó por una regresión que dijo PASS, porque si hubiera dicho FAILED no se fabricaba. Ése es el enemigo del día, y no es el verificador ni su testbench: es un PASS que nadie sabía qué significaba. La pregunta del último bullet se tira al grupo y no se contesta. Las respuestas que van a salir son "cobertura al 100 %", "corrí mil operaciones" y "no falló nada", y las tres vuelven hoy mismo: la primera es la sección de cobertura, la segunda es el plan, y la tercera es la que más duele, porque no falló nada es exactamente lo que dijo la regresión del respin. La respuesta corta llega en el testbench convencional, cuando le metamos un bug al DUT a propósito; la larga es el día entero, y la última slide del día vuelve a esta pregunta con lo que el alumno escribió.

ver esta slide en el deck ›

Tendencias

Por qué vuelven: lo que hay adentro de un chip hoy

Qué tienen adentro los IC/ASIC de 2024: relojes asincrónicos, procesador embebido, seguridad, IA, RISC-V y safety

Es la primera mitad de la respuesta a por qué vuelven: adentro hay dos relojes asincrónicos en casi todos, un procesador en cinco de cada seis y un acelerador de IA en más de la mitad. No es que se verifique peor que hace veinte años; es que hay mucho más para verificar, y la pregunta de la primera slide es más difícil de contestar que nunca.

ver esta slide en el deck ›

Tendencias

Verificar no es una etapa, es la mitad del trabajo

Del tiempo del diseñador, 49 % va a verificación; del tiempo del verificador, 47 % va a debug

Acá está el argumento de por qué existe este curso: el esfuerzo de verificación ya empató al de diseño. Verificar no es el paso final antes del tape-out: es la mitad del proyecto, con su propio equipo y su propio lenguaje. El 47 % de abajo se siembra hoy y se cobra dos veces: en el segundo ejercicio de esta tarde, cuando el log diga FAILED y nada más, y en la sección de reporting del día 3, que es donde el log aprende a decir por qué.

ver esta slide en el deck ›

Tendencias

Y se verifica, sobre todo, con UVM

Con qué se verifica en 2024: UVM al 80 % en IC/ASIC y al 50 % en FPGA

Ésta contesta con qué: ocho de cada diez proyectos de ASIC verifican con UVM, y la mitad de los de FPGA. Lo que sigue —el chiste del COBOL y la introducción— es qué es ese UVM y por qué no asusta. Y una cosa que conviene dejar dicha ahora: UVM no contesta la pregunta de la primera slide. La contesta el testbench; UVM es la forma de escribirlo para que el de al lado lo reconozca.

ver esta slide en el deck ›

¿SystemVerilog es el COBOL de la electrónica?

Lenguaje Reserved Keywords
ANSI COBOL 85 357
SystemVerilog 248
VHDL 2008 115
Verilog 95 102
C# 102
C++ 20 92
Python 3 35

" No academic computer scientists participated in the design of COBOL; all of those on the committee came from commerce or government"* ¿Parecido no?

Es un chiste con moraleja: SystemVerilog tiene 248 keywords —las del Annex B del IEEE 1800-2017— y nadie las usa todas. En el curso vamos a usar unas treinta. Con UVM pasa lo mismo: la librería es enorme y con seis o siete clases se arma un testbench completo. Sirve para bajarle la ansiedad al que llega asustado por el tamaño de UVM. Y es el puente a la sección que sigue: ocho de cada diez verifican con UVM, UVM está escrito en este lenguaje, y en los días 1 y 2 se escribe a mano todo lo que UVM después va a dar hecho. Con treinta keywords alcanza.

ver esta slide en el deck ›

Introducción

¿Qué es UVM?

El enganche con lo anterior es de una línea: ocho de cada diez proyectos verifican con esto. Y la aclaración que evita que UVM parezca la respuesta a la pregunta del día: no lo es. La respuesta es un testbench con plan, chequeo y medida; UVM es la forma de escribirlo para que escale y para que otro lo reconozca. La frase que ordena la sección es la del segundo bullet, y conviene decirla fuerte porque baja la ansiedad del que llega asustado: UVM no hace nada que no puedas hacer vos. No hay magia adentro. Hay una fábrica, unos puertos, unas fases y mucho acuerdo sobre nombres. Si alguien pregunta por qué entonces molestarse: por el último bullet. Un verificador que cambia de proyecto —o de empresa— abre el testbench y reconoce la estructura el primer día. Eso es lo que se compra. No es velocidad de simulación ni menos líneas: es que la estructura sea la misma en todos lados. El dato histórico sirve para el que viene de la industria: hasta 2011 cada vendor tenía la suya, y portar un testbench de VMM a OVM era reescribirlo. UVM existe porque eso era insostenible, no porque alguien tuviera una idea mejor. Y una aclaración que evita un malentendido clásico: Open Source acá significa que la librería es libre (Apache 2.0). El simulador que la corre casi siempre no lo es — salvo en este curso, que es de lo que se trata todo esto.

ver esta slide en el deck ›

Introducción

De dónde viene

De eRM, VMM, AVM y OVM a UVM

El diagrama en una frase: seis siglas de cuatro vendors que convergen. eRM era de Verisity (lenguaje e), RVM y después VMM de Synopsys, AVM de Mentor y URM de Cadence; OVM fusionó AVM con URM, y es la base de la que salió UVM. Lo que hay que señalar es la fecha de abajo y el salto que significa: UVM 1.0 es de 2011, o sea que esto es joven. El que hoy tiene quince años de carrera empezó antes de que existiera. Y la moraleja que se usa el resto del curso: cuando encuentren código con ovm_component en vez de uvm_component, no es un error de tipeo — es un testbench anterior a 2011 que nadie migró. Pasa más de lo que parece.

ver esta slide en el deck ›

Introducción

Un testbench de SystemVerilog, por dentro

Anatomía de un testbench SystemVerilog

Éste es el testbench que vamos a escribir nosotros, a mano, en los días 1 y 2. Conviene decirlo así: las cajas de este diagrama no son teoría, son los archivos que van a estar abiertos entre hoy y el día 4. Señalar el tester, el driver, el monitor y el scoreboard, y hacer notar que ninguno de esos nombres es de UVM: son los nombres que cualquiera le pone a las piezas de un testbench, en cualquier lenguaje. UVM no los inventó; los estandarizó.

ver esta slide en el deck ›

Introducción

El mismo testbench, con estructura UVM

El mismo testbench con estructura UVM

Vale la pena quedarse un rato acá: este diagrama es el mapa del resto del curso. Señalar driver, monitor, scoreboard y sequencer, y decir que cada uno va a ser una sección. Al final del día 5 el alumno va a tener todo menos el agent y el sequencer, que son el día 6. La comparación que conviene hacer en vivo es contra el diagrama anterior: son las mismas cajas. Lo que agrega UVM es el árbol que las contiene, los puertos por los que se hablan y las fases que las construyen en orden. Nada más, y no es poco.

ver esta slide en el deck ›

Introducción

El árbol de clases

Jerarquía de clases base de UVM

Este diagrama asusta y por eso conviene desactivarlo de entrada: de todo esto vamos a extender diez: uvm_component, uvm_test, uvm_env, uvm_agent, uvm_driver, uvm_monitor, uvm_subscriber, uvm_sequence_item, uvm_sequence y uvm_callback. Son las ámbar del dibujo menos el scoreboard —que en el curso es un uvm_subscriber—, más la sequence y el callback. Lo que sí hay que dejar marcado es la división de más arriba, porque explica medio curso: de uvm_object cuelgan los datos —las transactions, las sequences, los configs— y de uvm_component cuelga la estructura —todo lo que vive en el árbol y tiene fases—. Un objeto se crea y se tira; un componente se construye una vez y dura toda la simulación. Los colores dicen qué hacés con cada una: las ámbar las extendés vos, las grises no se tocan nunca —están sólo para que las otras hereden— y el uvm_sequencer es la única verde porque es la única que se instancia sin extenderla, con un typedef parametrizado. Eso llega el día 6; la única vez que el curso la extiende es para el sequencer virtual del día 7. La pregunta para tirar al grupo cuando lleguemos a los agents: ¿una uvm_sequence es un object o un component? Object. Y por eso no aparece en print_topology(). Es el mismo árbol que está en el machete impreso, arriba a la izquierda, como texto, para volver a mirarlo cuando haga falta.

ver esta slide en el deck ›

Introducción

Correr más tests escribiendo menos código

Ésta es la slide que hay que poder repetir al final del curso, porque es la tesis entera. Y conviene ser honesto: "run more tests, write less code" es un eslogan de vendor, pero la parte verificable es cierta y se puede señalar con el dedo el día 6 — el add_test de las sequences es de dos líneas, y cambia todo el estímulo sin tocar una sola clase del env. El orden de los tres ganchos no es casual, es de menor a mayor: una constraint cambia valores, un override cambia una clase, un callback cambia comportamiento en un punto de la que ya existe. Los tres son una sección del curso —Constrained random el día 5, El patrón factory el día 2 y Callbacks el día 6—, así que esta slide se puede volver a poner tres veces. Y la última línea es la que más se subestima. Un test con mil semillas no es "el mismo test mil veces": es mil escenarios distintos por el precio de escribir uno. Es literalmente el ejercicio d7-semillas, con el que arranca el día 7. Y el cierre que lleva al DUT: nada de esta slide contesta todavía ¿cómo sabés que verificaste?. Contestarla es lo que sigue —la spec, el plan y un testbench que se pueda probar—, y recién con eso escrito a mano se ve qué le agrega UVM.

ver esta slide en el deck ›

La spec del VTALU

Protocolo del VTALU: start y operandos estables hasta done

El protocolo es todo lo que hay que respetar: start arriba y operandos estables hasta done. Ojo con la letra chica: done es un pulso en la multiplicación (se apaga solo) y un nivel en add/sub/and/xor, donde vale start && (op != no_op). Es la clase de detalle que aparece recién cuando el testbench se cuelga y nadie sabe por qué.

ver esta slide en el deck ›

La spec del VTALU

Operation Opcode Ciclos ovf
no_op 3'b000 — 0
add_op 3'b001 1 0
sub_op 3'b010 1 A < B
and_op 3'b011 1 0
xor_op 3'b100 1 0
mul_op 3'b101 4 0
libre 3'b110 1 0
rst_op 3'b111 — —

rst_op es del testbench, no del RTL: el DUT ve 3'b111 como unused. Está en el enum porque el tester pide el reset como si fuera una operación más, y así el reset entra en la cobertura funcional. Buen momento para adelantar que el plan de cobertura incluye "cualquier operación después de un reset". La columna del ovf es la que más discusión da, y está buena: con operandos de 8 bits y resultado de 16, ni la suma ni la multiplicación pueden desbordar — la resta sí, y sólo cuando A < B. O sea que hay una salida del DUT que para cinco de las seis operaciones vale siempre 0. Preguntar en voz alta: ¿cómo se verifica una señal que casi nunca se mueve? La respuesta es la de hoy, en cobertura funcional, y es un bin. Y el opcode libre: 3'b110 no está ahí por casualidad ni por olvido. Es el ejercicio de esta tarde, y conviene decirlo ahora para que nadie lo lea como un descuido del diseño.

ver esta slide en el deck ›

La spec del VTALU

Single Cycle: Add - Sub - AND - XOR

code/vtalu_dut/vtalu_1c.sv · #the-two-resets
   // The two resets are deliberately different: the one on the result is
   // SYNCHRONOUS and the one on done is ASYNCHRONOUS. Not an oversight: the spec.
   always_ff @(posedge clk) begin
      if (!reset_n) begin
         result_1c <= 16'h0000;
         ovf_1c    <= 1'b0;
      end else if (start) begin
         ovf_1c <= (op == 3'b010) && (A < B);
         case (op)
            3'b001:  result_1c <= {8'h00, A} + {8'h00, B};
            3'b010:  result_1c <= {8'h00, A} - {8'h00, B};
            3'b011:  result_1c <= {8'h00, A} & {8'h00, B};
            3'b100:  result_1c <= {8'h00, A} ^ {8'h00, B};
            default: ;  // 3'b110 is free: it is the day 1 exercise
         endcase
      end
   end

   // done goes high on the edge if start is high and the op is not no_op
   always_ff @(posedge clk or negedge reset_n) begin
      if (!reset_n) done_1c <= 1'b0;
      else done_1c <= start && (op != 3'b000);
   end
el archivo entero, 44 líneas
// The single-cycle operations: add, sub, and, xor. Registers A op B on the edge.
//
// Subtraction is the only one that can overflow: with 8-bit operands and a
// 16-bit result, neither the addition nor the multiplication can. A - B with
// A < B can, and that is why ovf_1c exists. The result in that case is the
// two's complement truncated to 16 bits -- it wraps to 0xFFxx -- which is what
// the hardware does and what the scoreboard has to predict correctly.
module vtalu_1c (
    input  logic [ 7:0] A,
    input  logic [ 7:0] B,
    input  logic        clk,
    input  logic [ 2:0] op,
    input  logic        reset_n,
    input  logic        start,
    output logic        done_1c,
    output logic        ovf_1c,
    output logic [15:0] result_1c
);

   // The two resets are deliberately different: the one on the result is
   // SYNCHRONOUS and the one on done is ASYNCHRONOUS. Not an oversight: the spec.
   always_ff @(posedge clk) begin
      if (!reset_n) begin
         result_1c <= 16'h0000;
         ovf_1c    <= 1'b0;
      end else if (start) begin
         ovf_1c <= (op == 3'b010) && (A < B);
         case (op)
            3'b001:  result_1c <= {8'h00, A} + {8'h00, B};
            3'b010:  result_1c <= {8'h00, A} - {8'h00, B};
            3'b011:  result_1c <= {8'h00, A} & {8'h00, B};
            3'b100:  result_1c <= {8'h00, A} ^ {8'h00, B};
            default: ;  // 3'b110 is free: it is the day 1 exercise
         endcase
      end
   end

   // done goes high on the edge if start is high and the op is not no_op
   always_ff @(posedge clk or negedge reset_n) begin
      if (!reset_n) done_1c <= 1'b0;
      else done_1c <= start && (op != 3'b000);
   end

endmodule : vtalu_1c

La asimetría de los dos resets no es un descuido: viene del VHDL original y está copiada tal cual a propósito. Es exactamente el tipo de detalle que un testbench tiene que exponer, y por eso el DUT no se "limpió" al traducirlo. Decirlo en voz alta: esto contradice el asíncrono a secas del bullet de la spec, y ninguna fila del plan mira qué deja el reset en las salidas: la fila del reset mira la operación que viene después. Es un hueco del plan, y por ahora se queda así. Que done sea un nivel acá y un pulso en la multiplicación es la letra chica que va a colgar al primer testbench del curso. Conviene volver al diagrama de ondas de la primera slide y señalarlo de nuevo. El default: ; del case es lo que hace que no_op y los opcodes sin usar no cambien el resultado: la ALU se queda con lo último que calculó.

ver esta slide en el deck ›

La spec del VTALU

Multi Cycle: Multiplicación

code/vtalu_dut/vtalu_mult.sv · #the-pipeline
   // Four register stages. done is pipelined alongside the data, and each
   // stage clears itself with (& ~done_mult): that is what makes done a
   // one-cycle pulse instead of staying high for as long as start does. It is
   // subtle -- it is copied straight from the VHDL.
   always_ff @(posedge clk or negedge reset_n) begin
      if (!reset_n) begin
         done_mult <= 1'b0;
         done3 <= 1'b0;
         done2 <= 1'b0;
         done1 <= 1'b0;
         a_int <= 8'h00;
         b_int <= 8'h00;
         mult1 <= 16'h0000;
         mult2 <= 16'h0000;
         result_mult <= 16'h0000;
      end else begin
         a_int <= A;
         b_int <= B;
         mult1 <= a_int * b_int;
         mult2 <= mult1;
         result_mult <= mult2;
         done3 <= start & ~done_mult;
         done2 <= done3 & ~done_mult;
         done1 <= done2 & ~done_mult;
         done_mult <= done1 & ~done_mult;
      end
   end
el archivo entero, 46 líneas
// Multi-cycle multiplication: done rises on the FOURTH clock edge after start
// (the one-cycle ops raise it on the first). The latency is deliberate: it is
// what forces the testbench to wait for 'done' instead of counting cycles.
module vtalu_mult (
    input  logic [ 7:0] A,
    input  logic [ 7:0] B,
    input  logic        clk,
    input  logic        reset_n,
    input  logic        start,
    output logic        done_mult,
    output logic [15:0] result_mult
);

   logic [7:0] a_int, b_int;  // input registers
   logic [15:0] mult1, mult2;  // pipeline registers
   logic done3, done2, done1;  // done travels down the same pipeline

   // Four register stages. done is pipelined alongside the data, and each
   // stage clears itself with (& ~done_mult): that is what makes done a
   // one-cycle pulse instead of staying high for as long as start does. It is
   // subtle -- it is copied straight from the VHDL.
   always_ff @(posedge clk or negedge reset_n) begin
      if (!reset_n) begin
         done_mult <= 1'b0;
         done3 <= 1'b0;
         done2 <= 1'b0;
         done1 <= 1'b0;
         a_int <= 8'h00;
         b_int <= 8'h00;
         mult1 <= 16'h0000;
         mult2 <= 16'h0000;
         result_mult <= 16'h0000;
      end else begin
         a_int <= A;
         b_int <= B;
         mult1 <= a_int * b_int;
         mult2 <= mult1;
         result_mult <= mult2;
         done3 <= start & ~done_mult;
         done2 <= done3 & ~done_mult;
         done1 <= done2 & ~done_mult;
         done_mult <= done1 & ~done_mult;
      end
   end

endmodule : vtalu_mult

La latencia de cuatro flancos es a propósito, y es la razón de ser del testbench convencional: obliga al testbench a esperar done en vez de leer el resultado al ciclo siguiente. Un DUT combinacional no enseñaría nada. El & ~done_mult es sutil y conviene leerlo despacio: sin eso, mientras start estuviera arriba la cadena se recargaría sola y done quedaría en 1. Está copiado del VHDL original tal cual. La pregunta que ordena la slide: ¿qué pasa si el testbench manda una multiplicación y lee el resultado al ciclo siguiente? Lee el resultado anterior, y el scoreboard reporta un error que no está en el DUT.

ver esta slide en el deck ›

La spec del VTALU

Top Level

code/vtalu_dut/vtalu.sv · #the-mux
   assign es_mult = (op == 3'b101);

   assign start_1c = es_mult ? 1'b0 : start;
   assign start_mult = es_mult ? start : 1'b0;
   assign result_mux = es_mult ? result_mult : result_1c;
   assign done = es_mult ? done_mult : done_1c;
el archivo entero, 75 líneas
// VTALU top: A, B, clk, op[2:0], reset_n, start -> done, ovf, result[15:0].
//
// The op is NOT decided by a single loose bit any more: there is a decode, which
// is what a real design does, and what lets the operations avoid having to sit
// in tidy halves of the opcode space.
//
//   no_op  3'b000      and_op 3'b011
//   add_op 3'b001      xor_op 3'b100
//   sub_op 3'b010      mul_op 3'b101
//                      3'b110 free -- the day 1 exercise
//   rst_op 3'b111 never reaches the DUT: the BFM turns it into lowering reset_n
module vtalu (
    input  logic [ 7:0] A,
    input  logic [ 7:0] B,
    input  logic        clk,
    input  logic [ 2:0] op,
    input  logic        reset_n,
    input  logic        start,
    output logic        done,
    output logic        ovf,
    output logic [15:0] result
);

   logic done_1c, done_mult;
   logic ovf_1c;
   logic [15:0] result_1c, result_mult, result_mux;
   logic start_1c, start_mult;
   logic es_mult;

   assign es_mult = (op == 3'b101);

   assign start_1c = es_mult ? 1'b0 : start;
   assign start_mult = es_mult ? start : 1'b0;
   assign result_mux = es_mult ? result_mult : result_1c;
   assign done = es_mult ? done_mult : done_1c;

   // The multiplication cannot overflow: 8 x 8 fits exactly in 16 bits.
   assign ovf = es_mult ? 1'b0 : ovf_1c;

   // The injectable mutation -- the only liberty taken in this DUT, and the same
   // one apb_regs.sv takes with its bug_en. With +VTALU_BUG, bit 0 of the result
   // comes out flipped for every operation. Nothing turns it on in a normal run:
   // it is 'make mutante' that runs the examples with it and DEMANDS that they
   // fail. A scoreboard that never caught anything is not tested, it is
   // decoration -- and until this existed there was no way of telling one from
   // the other.
   // It hangs off the decode above and not inside it so that the four lines the
   // day 1 slide quotes stay the plain mux they claim to be.
   bit bug;
   initial bug = $test$plusargs("VTALU_BUG");
   assign result = result_mux ^ {15'h0000, bug};

   vtalu_1c u_1c (
       .A        (A),
       .B        (B),
       .clk      (clk),
       .op       (op),
       .reset_n  (reset_n),
       .start    (start_1c),
       .done_1c  (done_1c),
       .ovf_1c   (ovf_1c),
       .result_1c(result_1c)
   );

   vtalu_mult u_mult (
       .A          (A),
       .B          (B),
       .clk        (clk),
       .reset_n    (reset_n),
       .start      (start_mult),
       .done_mult  (done_mult),
       .result_mult(result_mult)
   );

endmodule : vtalu

Que los dos bloques compartan A, B y clk y sólo se separen por el start es lo que hace que el DUT tenga una sola interface. Vale señalarlo porque es la forma que va a tener la vtalu_bfm de interfaces y BFM. El archivo completo, con las dos instancias y sus conexiones, está en code/vtalu_dut/vtalu.sv. Acá sólo están el decode y las cuatro líneas que deciden. Vale señalar por qué es un decode y no un bit suelto: con un op[2] los opcodes tendrían que quedar en mitades prolijas del espacio, y el diseño perdería la libertad de asignarlos como convenga. Un decode cuesta una compuerta y no ata la spec. Y de paso, el opcode libre 3'b110 cae del lado de un ciclo sin que nadie tenga que hacer nada. El DUT no valida el opcode; el que lo tiene que atajar es el plan de verificación.

ver esta slide en el deck ›

La spec del VTALU

Cómo se corre

make u4/tests          # un ejemplo con UVM:  ~1 min 30 la primera vez
make u4/tests          # la segunda, con ccache:      ~15 segundos
make               # los 38 ejemplos

Acá conviene salir del deck y correrlo en vivo. Los números de la slide son medidos en una laptop de 12 cores. En una máquina de 2 —un Codespaces gratis— la primera son 4 minutos; la segunda sigue siendo 15 segundos, porque un hit de ccache es copiar un archivo y eso no depende de cuántos cores tengas. O sea que lo caro se paga una vez. Lo que hay que explicar es de dónde sale la diferencia entre 1 min 30 y 15 segundos, porque no es magia: Verilator compila cada simulación a un binario nativo, y para eso genera unos 2300 archivos C++ —casi todos de la librería UVM—. ccache guarda el resultado de cada uno; la segunda vez no vuelve a compilar, copia. Está en el Dockerfile y en el devcontainer, y afuera alcanza con tenerlo instalado. Y el que hay que remarcar igual: Verilator es libre — el alumno se lo lleva a casa y sigue practicando sin pedirle licencia a nadie.

ver esta slide en el deck ›

La spec del VTALU

Ver las ondas de verdad

cd code/u2/convencional
VLT_TRACE=1 bash run.sh      # compila con --trace y vuelca vtalu.vcd
gtkwave vtalu.vcd            # o surfer, o el visor que uses

Vale la pena abrirlo en vivo una vez, aunque sea treinta segundos: cargar op_set, start y done, y mostrar el pulso de un ciclo de la multiplicación al lado del nivel de las de un ciclo. Es la letra chica del protocolo de la slide 1, pero vista. Y decirlo explícito: el que hace el curso solo tiene acá su red de seguridad. Cuando un ejercicio no da, la respuesta casi siempre está en las ondas antes que en el código.

ver esta slide en el deck ›

La spec del VTALU

Resumen de la unidad

Cierre de la sección que parece de RTL y en realidad es de verificación. La pregunta con la que conviene cerrar: ¿cuál de estos siete datos es el que más caro sale olvidarse? El del done, y se va a ver hoy mismo, en el ejercicio de las ondas. Vale dejar dicho por qué el DUT no se "limpió" al traducirlo del VHDL: la asimetría de los resets y el opcode sin validar son exactamente el tipo de cosa que un testbench tiene que exponer. Un DUT prolijo no enseña nada.

ver esta slide en el deck ›

El plan de verificación

El entregable más profesional de la disciplina, y el más barato

Ésta es la slide que ordena el curso entero, y por eso llega antes que la primera línea de testbench. Todo lo que viene después —el estímulo, el scoreboard, los covergroups, las assertions— son columnas de esta tabla. Conviene decirlo de frente: el plan no es burocracia. Es la única respuesta seria a "¿ya terminaste de verificar?", y es lo primero que pide un líder técnico cuando pregunta cómo va el bloque. La plantilla vacía está en docs/plan-de-verificacion.md, y se usa dos veces en el curso: acá para leerla, y el día 7 para llenarla de cero.

ver esta slide en el deck ›

El plan de verificación

Las cinco columnas

Columna La pregunta que contesta Qué no va
Feature ¿de qué parte de la spec sale esta fila? el nombre de un archivo del testbench
Escenario ¿qué situación concreta hay que provocar? "probar la ALU"
Estímulo ¿quién la provoca: el random o un caso dirigido? "a mano"
Chequeo ¿quién dice que estuvo bien? "se mira la onda"
Medida ¿qué bin se llena cuando pasa? "se ve en el log"

Vale leer la columna de la derecha en voz alta: son los cinco errores que se cometen la primera vez, y los cinco suenan razonables cuando uno los escribe. La columna que más cuesta es la de medida, porque obliga a decidir antes qué se va a contar. Es la que el día 1 todavía no se puede llenar —los bins no existen hasta la unidad que viene— y por eso el plan se llena a medida que el testbench crece.

ver esta slide en el deck ›

El plan de verificación

Dos reglas que salen de las columnas

La diferencia entre verificado y simulado es quién se entera cuando falla a las tres de la mañana en la regresión. Si el chequeo es un par de ojos, no se entera nadie: la regresión pasa en verde con el bug adentro. El tercer bullet es una promesa que el curso cumple el día 7: hasta entonces todos los chequeos son de scoreboard, y la mitad del protocolo queda sin chequear. Vale dejarlo dicho ahora para que el día 7 no parezca un agregado. Un plan serio tiene las dos columnas, y la tabla del VTALU —que se ve entera en la unidad que viene, ya con los bins— tiene ocho filas de scoreboard y cuatro de assertion.

ver esta slide en el deck ›

El testbench convencional

El plan del VTALU, con tres columnas por llenar

Feature Escenario Estímulo Chequeo Medida
ALU las seis operaciones
ALU operandos en 00 y en FF
reset operar después de un reset
mult una mult después de una de un ciclo
ALU la misma operación dos veces seguidas
sub restar de menos: A < B, y el ovf sube

El orden importa y es el de la unidad anterior: primero el plan, después el testbench. Si escribís el testbench primero, terminás midiendo lo que el testbench hace en vez de lo que la spec pide. Vale leer las seis filas de a una y preguntar cuál falta. La respuesta que suele aparecer —"probar valores intermedios"— es buena para discutir por qué no está: 256 × 256 × 5 combinaciones no se simulan, y ahí es donde entra el random. La última es la que trae el tema del día: el DUT tiene DOS salidas, y una fila del plan que sólo se cierra mirando las dos. Un scoreboard que compara result y nada más pasa en verde con el ovf clavado en cero. La tabla vuelve dos veces: en cobertura funcional con la columna de medida llena, fila por fila, y al final de esa sección entera, con las doce filas y las tres columnas. Conviene avisarlo ahora para que nadie la lea como decoración: es la misma tabla, llenándose.

ver esta slide en el deck ›

El testbench convencional

El estímulo: mil operaciones, y el protocolo a mano

code/u2/convencional/vtalu_tb.sv · #stimulus-loop
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
el archivo entero, 256 líneas
// Typical TB:
// ----------
//  * Functional Coverage
//  * Self Checking
//  * Constrained Random Stimulus

// Why is this bad?
//  * No modularity (everything in a single file)
//  * Almost nothing here can be reused
module top;

   typedef enum bit[2:0] {no_op  = 3'b000,
                          add_op = 3'b001,
                          sub_op = 3'b010,
                          and_op = 3'b011,
                          xor_op = 3'b100,
                          mul_op = 3'b101,
                          rst_op = 3'b111} operation_t;
   byte         unsigned        A;
   byte         unsigned        B;
   bit          clk;
   bit          reset_n;
   wire [2:0]   op;
   bit          start;
   wire         done;
   wire [15:0]  result;
   wire         ovf;
   operation_t  op_set;

   assign op = op_set;

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

`ifdef VLT_TRACE
   // Waves for GTKWave:  VLT_TRACE=1 bash run.sh  &&  gtkwave vtalu.vcd
   // Behind an `ifdef because $dumpvars does not compile without --trace, and
   // the define comes from the same VLT_TRACE that adds the flag (common.sh).
   initial begin
      $dumpfile("vtalu.vcd");
      $dumpvars;
   end
`endif

   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

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

   covergroup zeros_or_ones_on_ops;
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   op_cov oc;
   zeros_or_ones_on_ops c_00_FF;

   initial begin : coverage
      oc = new();
      c_00_FF = new();

      forever begin @(posedge clk);
         oc.sample();
         c_00_FF.sample();
      end
   end : coverage

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
        3'b000 : return no_op;
        3'b001 : return add_op;
        3'b010 : return sub_op;
        3'b011 : return and_op;
        3'b100 : return xor_op;
        3'b101 : return mul_op;
        3'b110 : return rst_op;
        3'b111 : return rst_op;
      endcase // case (op_choice)
   endfunction : get_op

   function byte get_data();
      // This is how the random gets biased
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00)
        return 8'h00;
      else if (zero_ones == 2'b11)
        return 8'hFF;
      else
        return $random;
   endfunction : get_data

   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

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

   // Randomize the stimulus
   // get_op and get_data are Constrained Random Data
   // The point of CRD is to build values that are random but legal
   initial begin : tester
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start = 1'b0;
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
   end : tester

   // The testbench checks itself: every operation sent has to come back
   // through the scoreboard. A tester that drops operations -- while the
   // coverage counts them anyway -- passes green and has measured nothing.
   final begin
      if (enviadas != chequeadas)
        $error ("sent %0d operations, the scoreboard checked %0d", enviadas, chequeadas);
      else
        $display ("%0d operations sent, %0d checked", enviadas, chequeadas);
   end
endmodule : top

Ésta es la slide del "antes" de todo el curso: el estímulo y el protocolo mezclados en el mismo bucle. Vale marcarlo con el dedo, porque las próximas cinco unidades son separaciones sucesivas de estas treinta líneas. El do … while (done == 0) del default es el que se cuelga si el DUT no responde, y es el lugar donde el alumno va a terminar la primera vez que rompa algo. La red de seguridad es VLT_TRACE=1 y las ondas. Y por qué no es un wait(done), que es lo primero que uno escribe: en las operaciones de un ciclo done es un nivel, y en el negedge en que el tester carga la operación siguiente todavía está arriba por la anterior. El wait(done) no bloquea, start baja en el mismo instante y el DUT nunca ve esa operación. No hay error: el scoreboard no dispara, y la cobertura —que muestrea op_set— la cuenta igual. Este testbench la tuvo, y descartaba cuatro de cada diez operaciones de un ciclo con el reporte en verde: en d1, 77 shifts chequeados contra 130 con el arreglo. La atrapa el contador de enviadas contra chequeadas, y es la trampa muda más barata de poner. El sesgo de get_data() —un cuarto en 00, un cuarto en FF, la mitad en el medio— es exactamente lo que las transactions van a escribir en una línea con dist y :/. Conviene nombrarlo ahora para que después se vea el ahorro.

ver esta slide en el deck ›

El testbench convencional

El self-checking: predecir y comparar

code/u2/convencional/vtalu_tb.sv · #scoreboard-block
   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

      if ((op_set != no_op) && (op_set != rst_op)) begin
        chequeadas++;
        if (predicted_result != result || predicted_ovf != ovf)
          $error ("FAILED: A: %0h  B: %0h  op: %s result: %0h ovf: %0b",
                  A, B, op_set.name(), result, ovf);
      end
   end : scoreboard
el archivo entero, 256 líneas
// Typical TB:
// ----------
//  * Functional Coverage
//  * Self Checking
//  * Constrained Random Stimulus

// Why is this bad?
//  * No modularity (everything in a single file)
//  * Almost nothing here can be reused
module top;

   typedef enum bit[2:0] {no_op  = 3'b000,
                          add_op = 3'b001,
                          sub_op = 3'b010,
                          and_op = 3'b011,
                          xor_op = 3'b100,
                          mul_op = 3'b101,
                          rst_op = 3'b111} operation_t;
   byte         unsigned        A;
   byte         unsigned        B;
   bit          clk;
   bit          reset_n;
   wire [2:0]   op;
   bit          start;
   wire         done;
   wire [15:0]  result;
   wire         ovf;
   operation_t  op_set;

   assign op = op_set;

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

`ifdef VLT_TRACE
   // Waves for GTKWave:  VLT_TRACE=1 bash run.sh  &&  gtkwave vtalu.vcd
   // Behind an `ifdef because $dumpvars does not compile without --trace, and
   // the define comes from the same VLT_TRACE that adds the flag (common.sh).
   initial begin
      $dumpfile("vtalu.vcd");
      $dumpvars;
   end
`endif

   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

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

   covergroup zeros_or_ones_on_ops;
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   op_cov oc;
   zeros_or_ones_on_ops c_00_FF;

   initial begin : coverage
      oc = new();
      c_00_FF = new();

      forever begin @(posedge clk);
         oc.sample();
         c_00_FF.sample();
      end
   end : coverage

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
        3'b000 : return no_op;
        3'b001 : return add_op;
        3'b010 : return sub_op;
        3'b011 : return and_op;
        3'b100 : return xor_op;
        3'b101 : return mul_op;
        3'b110 : return rst_op;
        3'b111 : return rst_op;
      endcase // case (op_choice)
   endfunction : get_op

   function byte get_data();
      // This is how the random gets biased
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00)
        return 8'h00;
      else if (zero_ones == 2'b11)
        return 8'hFF;
      else
        return $random;
   endfunction : get_data

   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

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

   // Randomize the stimulus
   // get_op and get_data are Constrained Random Data
   // The point of CRD is to build values that are random but legal
   initial begin : tester
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start = 1'b0;
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
   end : tester

   // The testbench checks itself: every operation sent has to come back
   // through the scoreboard. A tester that drops operations -- while the
   // coverage counts them anyway -- passes green and has measured nothing.
   final begin
      if (enviadas != chequeadas)
        $error ("sent %0d operations, the scoreboard checked %0d", enviadas, chequeadas);
      else
        $display ("%0d operations sent, %0d checked", enviadas, chequeadas);
   end
endmodule : top

El modelo de referencia son cuatro líneas de case porque el DUT es una ALU. En un proyecto de verdad puede ser un modelo en C o el RTL de la generación anterior, pero la forma es siempre ésta: predecir con algo que no sea el DUT, y comparar. Que el scoreboard lea A, B y op_set del cable en el momento del resultado funciona sólo porque el protocolo obliga a mantenerlos estables. Es frágil, y en los analysis ports se va a reemplazar por una cola de comandos que manda el monitor. Y el $error de acá va a ser un `uvm_error desde reporting: mismo concepto, con un contador y una política de severidad detrás.

ver esta slide en el deck ›

El testbench convencional

¿Cómo sabés que el scoreboard chequea algo?

code/u2/convencional/mutante.txt
FAILED: A: 15  B: ce  op: xor_op result: da ovf: 0

Correrlo en vivo, que son segundos porque no hay UVM: primero bash run.sh —mil operaciones, silencio, y el resumen de cobertura—, y después VTALU_BUG=1 bash run.sh, que termina en la línea de la slide y un $stop. Lo que hay que decir con todas las letras es qué prueba esa línea y qué no. Prueba que el scoreboard compara el bit que el bug tocó. No prueba que compare ovf: un bug que sólo tocara ovf es otra corrida, y por eso el plan tiene una fila para cada salida. Ésta es la respuesta corta a la primera slide del día: la regresión que dijo PASS y mandó el bug a fabricar nunca había visto un FAILED. No sabía si chequeaba. Un PASS no dice nada hasta que sabés qué habría dicho FAILED. Y no es un truco del día 1: es la práctica que atraviesa el curso. +BUG=1 en el capstone del día 7 y +GOLDEN_BUG en el modelo de referencia del día 8 son esto mismo, y los correctores de los ejercicios lo exigen.

ver esta slide en el deck ›

El testbench convencional

Resumen de la unidad

El último bullet es el que ordena el resto del día: este testbench funciona, está probado, y está mal repartido. El tester sabe cómo se menea start y el scoreboard sabe cuándo leer done: el protocolo vive en dos lugares del mismo archivo, y el día que cambie hay que tocar los dos. Ésa es toda la motivación de interfaces y BFM, y conviene dejarla como pregunta abierta en vez de contestarla acá. Antes, la sección que sigue llena la tercera columna.

ver esta slide en el deck ›

Cobertura funcional

¿Cuándo terminás de verificar?

La pregunta de la slide es la que le hacen a un verificador en la reunión de tape-out, y no se contesta con "corrí muchos tests". Si alguien pregunta por qué no alcanza la cobertura de código: porque mide el DUT, no la spec. Un DUT al que le falta una feature entera puede dar 100 % de líneas — las líneas que no escribió nadie no aparecen en el reporte.

ver esta slide en el deck ›

Cobertura funcional

Del plan de verificación al covergroup

Las seis filas del plan de la sección anterior, traducidas:

Del plan Cómo se mide
las seis operaciones coverpoint op_set con un bin por operación
operandos en 00 y en FF cross de A, B y la operación
operar después de un reset bin de transición rst_op => op
una mult después de una de un ciclo bin de transición
la misma operación dos veces seguidas bin de repetición [* 2]
restar de menos: A < B, y el ovf sube coverpoint borrow con el bin hubo_borrow

Vale la pena volver al plan de la sección anterior y leerlo en voz alta antes de mostrar esta tabla: la gracia es que no hay ni un punto del plan que se quede sin traducir. El orden importa — el plan primero, el covergroup después. Al revés terminás midiendo lo que el testbench hace, no lo que la spec pide.

ver esta slide en el deck ›

Cobertura funcional

Anatomía: son tres pasos, no uno

covergroup op_cov;                     // 1. QUE se mide
   coverpoint op_set;
endgroup

op_cov oc;                             // 2. se INSTANCIA: es un tipo, como una clase
initial oc = new();

always @(posedge clk) oc.sample();     // 3. CUANDO se cuenta

El error de todos la primera vez es declarar el covergroup y olvidarse del new() o del sample(). No hay warning: el reporte simplemente dice 0 % y uno se pasa media hora buscando el bug en los bins. El tercer punto es el que vale para el resto del curso: el momento del sample define qué estás midiendo. Muestrear por reloj cuenta ciclos; muestrear por transacción cuenta operaciones. En los analysis ports el coverage pasa a ser un subscriber y muestrea cuando llega un comando — que es lo correcto. Y el flanco no es capricho: muestrear en el mismo negedge en el que el tester escribe op_set es una carrera que el LRM no define —Verilator la resuelve siempre igual; otro simulador puede no—. Es la misma regla de las assertions del día 7: se lee en el flanco opuesto al que escribe.

ver esta slide en el deck ›

Cobertura funcional

bins: los casilleros que hay que llenar

coverpoint op_set;                   // bins automaticos: uno por valor del enum

coverpoint op_set {
   bins single_cycle[] = {[add_op : xor_op], rst_op, no_op};  // [] -> uno por valor
   bins multi_cycle    = {mul_op};                            // sin [] -> uno solo
}

Los corchetes son el detalle que más se copia mal, y conviene mostrarlo con los dos casos al lado: bins x[] = {[0:3]} son cuatro casilleros, y bins x = {[0:3]} es uno solo que se llena con que caiga cualquiera de los cuatro valores. La sintaxis se diferencia en dos caracteres y el número que reporta la herramienta cambia por completo. La consecuencia práctica hay que decirla fuerte porque es una trampa muda: un bin sin [] sobre un rango grande siempre da 100 %. Cubriste un valor de mil y la herramienta te dice que está listo. Nadie te avisa. Y el último bullet es la advertencia que ordena lo que viene: un bin que no se puede llenar no es un problema de la herramienta, es una decisión que alguien no escribió. La forma de escribirla se llama ignore_bins, y tiene su propia slide en esta sección.

ver esta slide en el deck ›

Cobertura funcional

El covergroup del VTALU

code/u2/convencional/vtalu_tb.sv · #op_cov
   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

         bins twoops[] = ([add_op:mul_op] [* 2]);
         bins manymult = (mul_op [* 3:5]);
`endif
      }
   endgroup
el archivo entero, 256 líneas
// Typical TB:
// ----------
//  * Functional Coverage
//  * Self Checking
//  * Constrained Random Stimulus

// Why is this bad?
//  * No modularity (everything in a single file)
//  * Almost nothing here can be reused
module top;

   typedef enum bit[2:0] {no_op  = 3'b000,
                          add_op = 3'b001,
                          sub_op = 3'b010,
                          and_op = 3'b011,
                          xor_op = 3'b100,
                          mul_op = 3'b101,
                          rst_op = 3'b111} operation_t;
   byte         unsigned        A;
   byte         unsigned        B;
   bit          clk;
   bit          reset_n;
   wire [2:0]   op;
   bit          start;
   wire         done;
   wire [15:0]  result;
   wire         ovf;
   operation_t  op_set;

   assign op = op_set;

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

`ifdef VLT_TRACE
   // Waves for GTKWave:  VLT_TRACE=1 bash run.sh  &&  gtkwave vtalu.vcd
   // Behind an `ifdef because $dumpvars does not compile without --trace, and
   // the define comes from the same VLT_TRACE that adds the flag (common.sh).
   initial begin
      $dumpfile("vtalu.vcd");
      $dumpvars;
   end
`endif

   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

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

   covergroup zeros_or_ones_on_ops;
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   op_cov oc;
   zeros_or_ones_on_ops c_00_FF;

   initial begin : coverage
      oc = new();
      c_00_FF = new();

      forever begin @(posedge clk);
         oc.sample();
         c_00_FF.sample();
      end
   end : coverage

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
        3'b000 : return no_op;
        3'b001 : return add_op;
        3'b010 : return sub_op;
        3'b011 : return and_op;
        3'b100 : return xor_op;
        3'b101 : return mul_op;
        3'b110 : return rst_op;
        3'b111 : return rst_op;
      endcase // case (op_choice)
   endfunction : get_op

   function byte get_data();
      // This is how the random gets biased
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00)
        return 8'h00;
      else if (zero_ones == 2'b11)
        return 8'hFF;
      else
        return $random;
   endfunction : get_data

   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

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

   // Randomize the stimulus
   // get_op and get_data are Constrained Random Data
   // The point of CRD is to build values that are random but legal
   initial begin : tester
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start = 1'b0;
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
   end : tester

   // The testbench checks itself: every operation sent has to come back
   // through the scoreboard. A tester that drops operations -- while the
   // coverage counts them anyway -- passes green and has measured nothing.
   final begin
      if (enviadas != chequeadas)
        $error ("sent %0d operations, the scoreboard checked %0d", enviadas, chequeadas);
      else
        $display ("%0d operations sent, %0d checked", enviadas, chequeadas);
   end
endmodule : top

Éste es el primer covergroup real del curso, y conviene leerlo de arriba hacia abajo contestando las tres preguntas de la slide anterior: qué se mide (los coverpoints), en qué casilleros (los bins) y cuándo se cuenta (el sample(), que está más abajo en el mismo archivo). Que multi_cycle sea un bin solo y single_cycle[] sean seis no es simetría rota: es el plan de verificación. Las seis que no son la multiplicación interesan una por una; la multiplicación interesa como caso aparte porque es la única que tarda más de un ciclo. La forma del covergroup es la tabla del plan, y por eso el plan se escribe primero. El `ifndef VERILATOR vale nombrarlo ahora y no esconderlo: esos bins son tema del curso, están escritos, y hoy Verilator no los compila. Está en docs/verilator.md con la versión y la fecha. Un curso que tapa lo que su herramienta no hace es un curso que miente.

ver esta slide en el deck ›

Cobertura funcional

ignore_bins: decir en voz alta lo que no se cubre

code/u2/convencional/vtalu_tb.sv · #legs-and-ops
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

      b_leg: coverpoint B {
         bins zeros = {8'h00};
         bins others= {[8'h01:8'hFE]};
         bins ones  = {8'hFF};
      }
      // VTALU rev2: the borrow of the subtraction, the row of the plan that the
      // scoreboard can only close by looking at TWO outputs. Without this bin, a
      // subtraction that never went negative looks exactly like one that did.
      borrow: coverpoint ((op_set == sub_op) && (A < B)) {
         bins hubo_borrow = {1};
         ignore_bins resto = {0};
      }
el archivo entero, 256 líneas
// Typical TB:
// ----------
//  * Functional Coverage
//  * Self Checking
//  * Constrained Random Stimulus

// Why is this bad?
//  * No modularity (everything in a single file)
//  * Almost nothing here can be reused
module top;

   typedef enum bit[2:0] {no_op  = 3'b000,
                          add_op = 3'b001,
                          sub_op = 3'b010,
                          and_op = 3'b011,
                          xor_op = 3'b100,
                          mul_op = 3'b101,
                          rst_op = 3'b111} operation_t;
   byte         unsigned        A;
   byte         unsigned        B;
   bit          clk;
   bit          reset_n;
   wire [2:0]   op;
   bit          start;
   wire         done;
   wire [15:0]  result;
   wire         ovf;
   operation_t  op_set;

   assign op = op_set;

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

`ifdef VLT_TRACE
   // Waves for GTKWave:  VLT_TRACE=1 bash run.sh  &&  gtkwave vtalu.vcd
   // Behind an `ifdef because $dumpvars does not compile without --trace, and
   // the define comes from the same VLT_TRACE that adds the flag (common.sh).
   initial begin
      $dumpfile("vtalu.vcd");
      $dumpvars;
   end
`endif

   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

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

   covergroup zeros_or_ones_on_ops;
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   op_cov oc;
   zeros_or_ones_on_ops c_00_FF;

   initial begin : coverage
      oc = new();
      c_00_FF = new();

      forever begin @(posedge clk);
         oc.sample();
         c_00_FF.sample();
      end
   end : coverage

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
        3'b000 : return no_op;
        3'b001 : return add_op;
        3'b010 : return sub_op;
        3'b011 : return and_op;
        3'b100 : return xor_op;
        3'b101 : return mul_op;
        3'b110 : return rst_op;
        3'b111 : return rst_op;
      endcase // case (op_choice)
   endfunction : get_op

   function byte get_data();
      // This is how the random gets biased
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00)
        return 8'h00;
      else if (zero_ones == 2'b11)
        return 8'hFF;
      else
        return $random;
   endfunction : get_data

   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

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

   // Randomize the stimulus
   // get_op and get_data are Constrained Random Data
   // The point of CRD is to build values that are random but legal
   initial begin : tester
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start = 1'b0;
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
   end : tester

   // The testbench checks itself: every operation sent has to come back
   // through the scoreboard. A tester that drops operations -- while the
   // coverage counts them anyway -- passes green and has measured nothing.
   final begin
      if (enviadas != chequeadas)
        $error ("sent %0d operations, the scoreboard checked %0d", enviadas, chequeadas);
      else
        $display ("%0d operations sent, %0d checked", enviadas, chequeadas);
   end
endmodule : top

Hay un pariente que conviene nombrar: illegal_bins. ignore_bins saca el bin de la cuenta; illegal_bins lo saca de la cuenta y además reporta un error si alguna vez pasa. Sirve para los casos que la spec prohíbe — un opcode reservado, un handshake ilegal. La regla práctica: si no se puede cubrir, ignore_bins; si no debe pasar, illegal_bins; si simplemente todavía no pasó, dejalo y escribí el test.

ver esta slide en el deck ›

Cobertura funcional

cross: las combinaciones, y cómo no ahogarse

op_00_FF : cross a_leg, b_leg, all_ops;    // 3 x 3 x 5 = 45 bins, casi todos sin sentido

La cuenta del comentario es lo que hay que hacer en voz alta, porque el número asusta y tiene que asustar: tres bins de A por tres de B por cinco operaciones son 45, y de esos 45 el plan pedía once. Los otros 34 no están mal — están de más, y un bin de más es tan caro como uno que falta: te clava la cobertura abajo del 100 % y nadie sabe si es un agujero real. binsof(x) intersect {v} conviene leerlo siempre en castellano antes de escribirlo, porque la sintaxis no ayuda: "los bins de x que contienen v". Después se combinan con && y || como cualquier condición. El error de escala que hay que anticipar: en un DUT real un cross de tres coverpoints con bins automáticos son miles de casilleros, y una regresión no los llena nunca. Cuando alguien dice "la cobertura no sube", en la mitad de los casos el problema es un cross sin filtrar, no un test que falta. Y el número que hay que tener a mano para la slide "Leer el número": los 45 son la cuenta del LRM, un bin por miembro del enum. Verilator mide 54, porque reparte los bins automáticos por el tipo base (bit [2:0], 8 valores menos los 2 de ignore_bins, o sea 3 × 3 × 6). Los 9 de más son el 3'b110 cruzado, y son exactamente los 9 que quedan en cero.

ver esta slide en el deck ›

Cobertura funcional

El cross del VTALU

code/u2/convencional/vtalu_tb.sv · #cross-mul-bins
         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);
el archivo entero, 256 líneas
// Typical TB:
// ----------
//  * Functional Coverage
//  * Self Checking
//  * Constrained Random Stimulus

// Why is this bad?
//  * No modularity (everything in a single file)
//  * Almost nothing here can be reused
module top;

   typedef enum bit[2:0] {no_op  = 3'b000,
                          add_op = 3'b001,
                          sub_op = 3'b010,
                          and_op = 3'b011,
                          xor_op = 3'b100,
                          mul_op = 3'b101,
                          rst_op = 3'b111} operation_t;
   byte         unsigned        A;
   byte         unsigned        B;
   bit          clk;
   bit          reset_n;
   wire [2:0]   op;
   bit          start;
   wire         done;
   wire [15:0]  result;
   wire         ovf;
   operation_t  op_set;

   assign op = op_set;

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

`ifdef VLT_TRACE
   // Waves for GTKWave:  VLT_TRACE=1 bash run.sh  &&  gtkwave vtalu.vcd
   // Behind an `ifdef because $dumpvars does not compile without --trace, and
   // the define comes from the same VLT_TRACE that adds the flag (common.sh).
   initial begin
      $dumpfile("vtalu.vcd");
      $dumpvars;
   end
`endif

   //op_cov: make sure every operation, and every interaction between them, is covered
   covergroup op_cov;
      coverpoint op_set {
         bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
         bins multi_cycle = {mul_op};

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

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

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

   covergroup zeros_or_ones_on_ops;
      // Check the corners where the inputs are all 0s or all 1s
      all_ops : coverpoint op_set {
         ignore_bins null_ops = {rst_op, no_op};}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   op_cov oc;
   zeros_or_ones_on_ops c_00_FF;

   initial begin : coverage
      oc = new();
      c_00_FF = new();

      forever begin @(posedge clk);
         oc.sample();
         c_00_FF.sample();
      end
   end : coverage

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
        3'b000 : return no_op;
        3'b001 : return add_op;
        3'b010 : return sub_op;
        3'b011 : return and_op;
        3'b100 : return xor_op;
        3'b101 : return mul_op;
        3'b110 : return rst_op;
        3'b111 : return rst_op;
      endcase // case (op_choice)
   endfunction : get_op

   function byte get_data();
      // This is how the random gets biased
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00)
        return 8'h00;
      else if (zero_ones == 2'b11)
        return 8'hFF;
      else
        return $random;
   endfunction : get_data

   // Sent by the tester, compared by the scoreboard: the final block below
   // demands that the two match.
   int enviadas, chequeadas;

   // Scoreboard loop: checks that everything came out right
   always @(posedge done) begin : scoreboard
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (op_set)
        add_op: predicted_result = A + B;
        sub_op: predicted_result = A - B;
        and_op: predicted_result = A & B;
        xor_op: predicted_result = A ^ B;
        mul_op: predicted_result = A * 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 = (op_set == sub_op) && (A < B);

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

   // Randomize the stimulus
   // get_op and get_data are Constrained Random Data
   // The point of CRD is to build values that are random but legal
   initial begin : tester
      reset_n = 1'b0;
      @(negedge clk);
      @(negedge clk);
      reset_n = 1'b1;
      start = 1'b0;
      repeat (1000) begin
         @(negedge clk);
         op_set = get_op();
         A = get_data();
         B = get_data();
         start = 1'b1;
         case (op_set) // handle the start signal
           no_op: begin
              @(posedge clk);
              start = 1'b0;
           end
           rst_op: begin
              reset_n = 1'b0;
              start = 1'b0;
              @(negedge clk);
              reset_n = 1'b1;
           end
           default: begin
              // Wait for an EDGE with done up, not for done itself: on the
              // one-cycle ops done is a level still up from the previous op,
              // and a plain wait(done) drops this one without a sound.
              enviadas++;
              do @(negedge clk); while (done == 0);
              start = 1'b0;
           end
         endcase // case (op_set)
      end
      $finish;
   end : tester

   // The testbench checks itself: every operation sent has to come back
   // through the scoreboard. A tester that drops operations -- while the
   // coverage counts them anyway -- passes green and has measured nothing.
   final begin
      if (enviadas != chequeadas)
        $error ("sent %0d operations, the scoreboard checked %0d", enviadas, chequeadas);
      else
        $display ("%0d operations sent, %0d checked", enviadas, chequeadas);
   end
endmodule : top

Vale leer mul_00 y mul_max en castellano, uno detrás del otro, porque la diferencia entre || y && es la que se copia mal, y acá los dos bins son de la misma operación: "una multiplicación en la que A o B valgan 0x00" contra "una multiplicación con A y B en 0xFF". El primero son dos casos, el segundo es uno solo — el producto máximo, la esquina de arriba del espacio de entrada. Y acá conviene matar el malentendido que viene solo, porque es caro: FF × FF no desborda nada. Da FE01, que entra exacto en los 16 bits de result —8 bits por 8 nunca pasan de 16— y por eso el DUT fuerza ovf a 0 en toda multiplicación: assign ovf = es_mult ? 1'b0 : ovf_1c;. El bin se llama mul_max y no mul_ovf justamente por eso: el caso vale por ser el máximo del espacio de entrada, no por desbordar. La única operación que desborda es la resta, y sólo cuando A < B. La honestidad de esta slide es parte del curso, así que conviene decirla y no pasarla rápido: el 86,8 % que reporta el ejemplo no es el número que daría Questa. Verilator ignora el filtro y mide el cross entero, y además le suma un valor que el enum no tiene: donde el LRM cuenta 45, Verilator cuenta 54 (los 9 de más son el 3'b110 cruzado, y son los 9 que quedan en cero). Por eso el porcentaje sale más bajo, y por un motivo que no es del testbench. Está escrito, fechado y con versión en docs/verilator.md. La pregunta útil para el aula: ¿eso invalida el ejercicio? No. Lo que se aprende —escribir el cross, filtrarlo, y saber qué bin corresponde a qué fila del plan— es idéntico. Lo único que no se puede hacer con esta herramienta es firmar el cierre de cobertura, y ése no es el objetivo del curso.

ver esta slide en el deck ›

Cobertura funcional

Bins de transición: cuando importa el orden

bins opn_rst[] = ([add_op:mul_op] => rst_op);   // A y despues B
bins sngl_mul[] = ([add_op:xor_op], no_op => mul_op);
bins twoops[]  = ([add_op:mul_op] [* 2]);       // dos veces seguidas
bins manymult  = (mul_op [* 3:5]);              // entre 3 y 5 veces seguidas

Es el único lugar del curso donde la herramienta libre no llega, y conviene decirlo de frente en vez de esconderlo: el concepto es del lenguaje, no del simulador, y en Questa o VCS estos bins corren. Hay dos primos más que vale nombrar: [-> n] (goto, n veces no necesariamente seguidas) y [= n] (non-consecutive). Están en coverage.svh de los analysis ports.

ver esta slide en el deck ›

Cobertura funcional

Las perillas: cuándo un bin cuenta como cubierto

coverpoint op_set {
   option.at_least = 10;                  // un bin cuenta recien con 10 hits
   bins single[] = {[add_op:xor_op]};
}
coverpoint A { option.auto_bin_max = 8; } // 8 casilleros automaticos, no 64

Ésta es la slide que le pone un asterisco a todos los porcentajes de la unidad, y por eso llega recién ahora: at_least = 1 quiere decir que la herramienta te dice cubierto con un solo hit. Para un bin que representa un valor de un enum está perfecto —o pasó o no pasó—. Para el bin que representa el producto máximo del multiplicador, un hit es una anécdota, no una verificación. La regla de campo, y conviene darla porque la pregunta viene sola: at_least alto en los bins que representan un caso raro, default en los que representan un valor. Subirlo para todo el covergroup no es rigor, es una regresión que no cierra nunca y un número que nadie mira. auto_bin_max es la contracara de la trampa de los corchetes, la de la slide de los bins. Ahí el problema era un bin de más; acá es al revés: si el coverpoint es un int y no le escribís bins, la herramienta inventa 64 rangos que no corresponden a ninguna fila del plan. El número que sale es real y no significa nada. Y la honestidad de siempre: las cuatro perillas son del LRM, no de Verilator. Lo que esta herramienta hace con cada una está medido, fechado y con repro en code/verilator/repro-cg-options.sv. La de at_least es la peor de las cuatro porque es muda: en el covergroup se escribe, compila, y no hace nada.

ver esta slide en el deck ›

Cobertura funcional

Leer el número

$ make u2/convencional
Coverage Summary:
  covergroup : 86.8% (66/76)

Acá es donde conviene correrlo en vivo y abrir el coverage.dat: el alumno tiene que ver que los diez ceros tienen el mismo nombre, y que ese nombre no está en el operation_t. La lección es leer el reporte y no el número: el número dice 86,8 % y el reporte dice "no falta nada, la herramienta inventó un casillero". En una herramienta comercial el bin automático de un enum es uno por miembro (IEEE 1800, 19.5) y esto da 100 % de entrada; está anotado en docs/verilator.md. Y el remate: el ejercicio del día usa justamente 3'b110 para el shift, así que la operación nueva llena el casillero fantasma y la cobertura pasa de 86,8 % a 100 % — 77 de 77, con el bin propio del shift sumado. El que la sube es el mismo que escribió el test, y el corrector cuenta bins y no porcentaje, porque el porcentaje ya vimos que no sabe leer.

ver esta slide en el deck ›

Cobertura funcional

El plan de verificación, entero y en una tabla

# Feature Escenario Estímulo Chequeo Medida
1 ALU las seis operaciones random scoreboard coverpoint op_set
2 ALU operandos en 00 y en FF dist sesgado a los bordes scoreboard cross op_00_FF
3 mult producto máximo: FF × FF caso dirigido scoreboard, 16 bits bin mul_max
4 reset operar después de un reset rst_op intercalado scoreboard bin rst_op => op
5 mult una mult después de una de un ciclo random scoreboard bin de transición
6 ALU la misma operación dos veces seguidas random scoreboard bin [* 2]
7 sub restar de menos: A < B, y el ovf sube random scoreboard, dos salidas bin hubo_borrow
8 sub A == B: el resultado es 0 y el ovf no sube random scoreboard, dos salidas bin sub_00/sub_FF
9 ovf ovf no se levanta para ninguna otra operación random assertion c_ovf
10 protocolo los operandos no se tocan con start arriba random assertion cover property
11 protocolo done llega, y antes de 5 ciclos random assertion cover property
12 protocolo no_op es la única que no contesta random assertion cover property

Ésta es la slide que contesta "¿y esto para qué?" del resto del día 1, y conviene decir de frente por qué llega recién ahora: las tres columnas de la derecha son estímulo, self-checking y cobertura — las tres partes que la unidad anterior mostró sueltas. El plan es la tabla que las hace una sola cosa. La fila que más discusión da es la del producto máximo, y está buena: FF × FF es el caso que un test corto no llena, y por eso su columna de estímulo dice caso dirigido: el plan se decide antes de saber cuánto va a correr el random. Ahí se ve que el plan no sólo mide, y también decide qué test hay que escribir. Es el ejercicio d5c, el último de la tarde del día 5, con esta misma fila. Las tres filas de protocolo conviene nombrarlas y seguir: son la mitad que el curso no toca hasta el día 7, y sirven para dejar sembrado que un scoreboard no chequea todo. Un plan de verificación serio tiene las dos columnas. Y el uso que se lleva el alumno: docs/plan-de-verificacion.md es la misma tabla como artefacto, con una plantilla vacía. El capstone del día 7 se entrega con el plan lleno — que es exactamente lo que se entrega en un proyecto.

ver esta slide en el deck ›

Cobertura funcional

Resumen de la unidad

Cierre del concepto que más se malinterpreta del curso. La frase para llevarse: la cobertura no te dice que el diseño está bien, te dice que lo probaste. Un DUT al que le falta una feature entera da 100 % de cobertura de código y 0 % de la funcional que nadie escribió. Y el error operativo que más se repite, para dejarlo grabado: el sample() que falta. Compila, corre, y el reporte da cero.

ver esta slide en el deck ›

Interfaces y BFM

Primero: las señales dejan de estar sueltas

code/u2/interfaces-bfm/vtalu_bfm.sv · #signals-and-clock
interface vtalu_bfm;
   // brings in operation_t. e.g. (no_op  = 3'b000)
   import vtalu_pkg::*;

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

   assign op = op_set;

   // The BFM is a first step towards modularity
   // It takes care of driving every low-level signal
   // and keeps the protocol in one single place

   // DUT clock
   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end
el archivo entero, 71 líneas
interface vtalu_bfm;
   // brings in operation_t. e.g. (no_op  = 3'b000)
   import vtalu_pkg::*;

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

   assign op = op_set;

   // The BFM is a first step towards modularity
   // It takes care of driving every low-level signal
   // and keeps the protocol in one single place

   // DUT clock
   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end

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

   // The protocol handling can now be changed in one single place.
   // Fix it here once, and the fix propagates through the rest of the code
   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint alu_result);

      op_set = 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);
         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

   endtask : send_op

endinterface : vtalu_bfm

Primer paso hacia UVM y no tiene una línea de UVM. La ganancia inmediata es de mantenimiento y conviene medirla: con las señales sueltas, partir el testbench en módulos obliga a pasar cada una por cada port list, y una señal nueva se cablea en todos. Acá se declara en la interface, y el único puerto que cambia es el del DUT. Un detalle de tipos que se cobra: op es un wire [2:0] y op_set es el operation_t. El assign op = op_set es el puente entre el enum del testbench y los cables del DUT. Es el único lugar del curso donde se ve la costura entre los dos mundos. Y el modport, que no aparece en el resto del curso y sí en cualquier RTL ajeno: es una vista de la interface con las direcciones declaradas —modport dut (input A, input B, output result)—, y el módulo la pide en su port list (vtalu_bfm.dut bus). Sirve para dos cosas, y las dos valen: que el compilador frene al DUT si escribe una señal que para él era de entrada, y que el que lee la interface sepa quién maneja qué sin abrir el RTL. Acá no está porque la BFM es del testbench y maneja todo; en el trabajo la interface casi siempre viene con dos o tres modports y lo que hay que saber es cuál pedir.

ver esta slide en el deck ›

Interfaces y BFM

Después: el protocolo se esconde en una task

code/u2/interfaces-bfm/vtalu_bfm.sv · #send_op
   // The protocol handling can now be changed in one single place.
   // Fix it here once, and the fix propagates through the rest of the code
   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint alu_result);

      op_set = 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);
         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

   endtask : send_op
el archivo entero, 71 líneas
interface vtalu_bfm;
   // brings in operation_t. e.g. (no_op  = 3'b000)
   import vtalu_pkg::*;

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

   assign op = op_set;

   // The BFM is a first step towards modularity
   // It takes care of driving every low-level signal
   // and keeps the protocol in one single place

   // DUT clock
   initial begin
      clk = 0;
      forever begin
         #10;
         clk = ~clk;
      end
   end

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

   // The protocol handling can now be changed in one single place.
   // Fix it here once, and the fix propagates through the rest of the code
   task send_op(input byte iA, input byte iB, input operation_t iop,
                output shortint alu_result);

      op_set = 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);
         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

   endtask : send_op

endinterface : vtalu_bfm

La frase que ordena la sección: el protocolo se escribe una vez. Si mañana el DUT pide dos ciclos de start, se toca esta task y nada más. El #1 después del @(posedge clk) es la misma precaución del scoreboard del testbench convencional: mover las señales un delta después del flanco, no sobre el flanco. Y el enganche del día 4: el driver de los put y get ports es exactamente esta task, pero adentro de una clase y alimentada por un port. La operación —esconder el cable— es la misma; lo que cambia es de dónde vienen los datos.

ver esta slide en el deck ›

Interfaces y BFM

El top, con la BFM en el medio

code/u2/interfaces-bfm/top.sv
module top;
   vtalu_bfm bfm ();
   // Tester: generates the stimulus
   tester tester_i (bfm);
   // Coverage: provides functional coverage
   coverage coverage_i (bfm);
   // ScoreBoard: checks the results
   scoreboard scoreboard_i (bfm);

   vtalu DUT (
       .A(bfm.A),
       .B(bfm.B),
       .op(bfm.op),
       .clk(bfm.clk),
       .reset_n(bfm.reset_n),
       .start(bfm.start),
       .done(bfm.done),
       .ovf(bfm.ovf),
       .result(bfm.result)
   );
endmodule : top

Conviene leer este top.sv al lado del de la sección anterior, porque el diff es la sección entera: donde había una lista de logic declarados en el top y cableados a mano al DUT, ahora hay una línea que instancia la BFM y una que la enchufa. Lo que hay que hacer notar, porque es lo que se repite el resto del curso: el DUT se conecta a bfm.A, bfm.B, bfm.op… La interface no es un objeto abstracto, son los mismos cables de siempre agrupados bajo un nombre. Nada se volvió más lento ni más raro — se volvió más difícil de conectar mal. Y el detalle que paga solo el día que el DUT crezca: agregar una señal al protocolo es tocar la interface, y todos los que la usan la ven aparecer. En la versión anterior había que acordarse de cablearla en cada instancia.

ver esta slide en el deck ›

Interfaces y BFM

Scoreboard: el mismo chequeo, ahora hablando con la BFM

code/u2/interfaces-bfm/scoreboard.sv
module scoreboard (
    vtalu_bfm bfm
);
   import vtalu_pkg::*;

   always @(posedge bfm.done) begin
      shortint predicted_result;
      bit      predicted_ovf;
      #1;
      case (bfm.op_set)
         add_op: predicted_result = bfm.A + bfm.B;
         sub_op: predicted_result = bfm.A - bfm.B;
         and_op: predicted_result = bfm.A & bfm.B;
         xor_op: predicted_result = bfm.A ^ bfm.B;
         mul_op: predicted_result = bfm.A * bfm.B;
      endcase  // case (op_set)

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

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

   end
endmodule : scoreboard

Lo que hay que hacer notar es lo que no cambió. La lógica de verificación es idéntica; lo que se modularizó es el cableado. Ese es toda la sección. El bfm. adelante de cada señal es la primera vez que el alumno ve una jerarquía de interface, y vale escribirla en el pizarrón: top.bfm.A. En los tests esa misma referencia va a llegar por el config_db a una clase, y ahí el prefijo va a ser un handle en vez de un nombre de instancia. La deuda que queda abierta: el scoreboard sigue mirando las señales directamente. Recién en los analysis ports va a recibir transactions de un monitor y dejar de saber que existe un clk.

ver esta slide en el deck ›

Interfaces y BFM

Tester: pide operaciones, no mueve cables

code/u2/interfaces-bfm/tester.sv · #stimulus-loop
   initial begin
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;
      shortint      result;

      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, result);
      end : random_loop
      $finish;
   end  // initial begin
el archivo entero, 46 líneas
// The Tester module is simpler now, because it no
// longer has to deal with low-level signals
module tester (
    vtalu_bfm bfm
);
   import vtalu_pkg::*;

   function operation_t get_op();
      bit [2:0] op_choice;
      op_choice = $random;
      case (op_choice)
         3'b000: return no_op;
         3'b001: return add_op;
         3'b010: return sub_op;
         3'b011: return and_op;
         3'b100: return xor_op;
         3'b101: return mul_op;
         3'b110: return rst_op;
         3'b111: return rst_op;
      endcase  // case (op_choice)
   endfunction : get_op

   function byte get_data();
      bit [1:0] zero_ones;
      zero_ones = $random;
      if (zero_ones == 2'b00) return 8'h00;
      else if (zero_ones == 2'b11) return 8'hFF;
      else return $random;
   endfunction : get_data

   initial begin
      byte unsigned iA;
      byte unsigned iB;
      operation_t   op_set;
      shortint      result;

      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, result);
      end : random_loop
      $finish;
   end  // initial begin
endmodule : tester

La comparación con el testbench convencional es la slide: allá el tester levantaba start, esperaba done y bajaba start, mezclado con la generación de estímulo. Acá sólo genera estímulo. Vale contar cuántos lugares del testbench convencional sabían del protocolo —el tester y el scoreboard— y cuántos mueven start ahora: uno, el send_op. El scoreboard sigue esperando done por su cuenta; esa deuda la paga el monitor de los analysis ports. Y el enganche del día 4: en los put y get ports este mismo tester va a ser una clase con un put_port, y la BFM va a quedar del otro lado de un driver. La operación es la misma que hicimos acá — esconder el cable— repetida un nivel más arriba.

ver esta slide en el deck ›

Interfaces y BFM

La regla del protocolo, escrita en el idioma del simulador

// INMEDIATA: una sentencia. Chequea el presente, donde el hilo pasa.
assert (op_set != no_op || done == 0)
   else $error("no_op no deberia levantar done");

// CONCURRENTE: una declaracion con reloj. Se enchufa una vez y
// chequea CADA flanco de la simulacion, sola, para siempre.
a_operandos_estables :
assert property (@(negedge clk) start |=> $stable(A) && $stable(B));

Esto es un anzuelo, no la sección: no hay que explicar la sintaxis acá, y menos el |=> o el flanco. Lo único que tiene que quedar es que la regla del protocolo se puede escribir de una forma que el simulador entienda. Vale hacer notar la asimetría que se abre: el BFM cumple el protocolo, y eso alcanza mientras todo el estímulo pase por el BFM. En cuanto aparezca un módulo heredado, un VIP de otro equipo o un driver escrito con apuro, cumplirlo deja de ser lo mismo que chequearlo. Si alguien pregunta por qué una dice negedge y la otra no, la respuesta corta es "porque la BFM escribe en el negedge, y eso solo se ve entero en las assertions". Es la pregunta que se quiere sembrar.

ver esta slide en el deck ›

Interfaces y BFM

Dónde se chequea cada cosa

La frase que conviene dejar escrita en el pizarrón todo el curso: el scoreboard chequea qué calcula el DUT; las assertions, cómo se habla con él. Lo que queda del día 1 y los seis días que siguen son la primera mitad; la segunda llega al final, y llega sobre este mismo ejemplo. No hay que prometer más que eso. Las assertions vuelven con la property de esta slide, la escribe de verdad, y muestra por qué el flanco que eligió no es un detalle.

ver esta slide en el deck ›

Interfaces y BFM

El agujero que deja la BFM: el temporizado

code/u2/clocking/sin_clocking.sv · #three-samples
      // 1. Sample AT the edge. The DUT always_ff updates d_out with a
      //    nonblocking assignment, which lands after this initial already ran:
      //    you read the OLD value.
      @(posedge clk);
      en_el_flanco = d_out;

      // 2. The same edge, one delta later. The nonblocking assignment landed
      //    already and you read the NEW value. That "#1" is visible nowhere and
      //    changes the result.
      @(posedge clk);
      #1;
      un_delta_despues = d_out;

      // 3. The opposite edge: the trick the whole course BFM uses. It works,
      //    but it asks whoever reads it to know why.
      @(negedge clk);
      en_el_flanco_opuesto = d_out;
el archivo entero, 50 líneas
// Sampling by hand: without a clocking block you have to pick the edge AND the
// delta, in every task, over and over. This top runs the three variants people
// write in their first week: one reads the OLD value, the other two the NEW.
//
//   bash run.sh
module dut_reg (input bit clk, input byte unsigned d_in, output byte unsigned d_out);
   // The DUT counts: on every edge, the output becomes the input + 1.
   always_ff @(posedge clk) d_out <= d_in + 8'd1;
endmodule

module top_sin;
   bit           clk = 0;
   byte unsigned d_in, d_out;

   always #5 clk = ~clk;
   dut_reg u_dut (.*);

   byte unsigned en_el_flanco, un_delta_despues, en_el_flanco_opuesto;

   initial begin
      d_in = 8'd10;

      // 1. Sample AT the edge. The DUT always_ff updates d_out with a
      //    nonblocking assignment, which lands after this initial already ran:
      //    you read the OLD value.
      @(posedge clk);
      en_el_flanco = d_out;

      // 2. The same edge, one delta later. The nonblocking assignment landed
      //    already and you read the NEW value. That "#1" is visible nowhere and
      //    changes the result.
      @(posedge clk);
      #1;
      un_delta_despues = d_out;

      // 3. The opposite edge: the trick the whole course BFM uses. It works,
      //    but it asks whoever reads it to know why.
      @(negedge clk);
      en_el_flanco_opuesto = d_out;

      $display("d_in = %0d, and the DUT computes d_in + 1 = %0d", 8'd10, 8'd11);
      $display("  sampled AT the posedge        : %0d", en_el_flanco);
      $display("  sampled one delta later       : %0d", un_delta_despues);
      $display("  sampled at the negedge        : %0d", en_el_flanco_opuesto);
      $display("");
      $display("Three lines that look the same and are not. And that decision");
      $display("is repeated in every task of the interface.");
      $finish;
   end
endmodule : top_sin

Correr esto en vivo, que tarda dos segundos: make u2/clocking. Las tres líneas que imprime son la slide entera. La primera es la que sorprende y conviene explicarla despacio: muestrear en el posedge da el valor viejo, porque el always_ff del DUT actualiza con una asignación no bloqueante y ésa se aplica después de que este initial ya corrió. No es un bug de Verilator ni una rareza: es el orden de regiones del LRM, y es igual en Questa. La segunda es la peor de las tres, y es la que hay que marcar como trampa muda: funciona, y funciona por accidente. El #1 no dice qué problema resuelve, no está documentado, y el día que alguien lo borra porque "no hacía nada" el testbench sigue compilando y empieza a mentir. Es el mismo #1 del scoreboard del testbench convencional, y la diferencia es una sola: allá la slide dice qué carrera evita. Un #1 explicado es un parche que se sabe parche; un #1 suelto es la trampa. Y la tercera es la nuestra. Vale ser honesto con el grupo: el curso maneja en negedge y muestrea en posedge porque es el truco que se entiende sin haber visto clocking blocks, no porque sea lo que se escribe en un proyecto.

ver esta slide en el deck ›

Interfaces y BFM

Por qué el flanco solo no alcanza: las regiones

Las regiones de un flanco: Preponed, Active y NBA, y los tres muestreos que dan valores distintos

Esta figura es la que hay que dejar en pantalla mientras se leen las tres lecturas de la slide anterior. Sin ella la explicación es "el valor viejo", que suena a rareza del simulador; con ella es una consecuencia del orden, y el orden está en el LRM. El scheduler tiene más regiones que las cuatro del dibujo —docs/clocking-blocks.md las lista todas—, pero con éstas se explican los tres casos, y agregar las otras no cambia ninguna respuesta. La pregunta que conviene hacer al grupo antes de mostrar la respuesta: si el DUT escribe en NBA y tu initial despierta en Active, ¿quién corre primero? Ahí se ve solo por qué muestrear en el flanco da el valor de antes. Y el enganche con el día 7: la barra ámbar de la izquierda es la región donde muestrea SVA. Es la misma figura que explica por qué una assertion no chequea lo que pasó sino lo que vio, y por qué el @() que se le pone cambia el resultado.

ver esta slide en el deck ›

Interfaces y BFM

El clocking block: el flanco y el delta, declarados una vez

code/u2/clocking/con_clocking.sv · #reg_bfm
interface reg_bfm (input bit clk);
   byte unsigned d_in, d_out;

   // The only two timing lines in the testbench:
   //   input  #1step  samples the STABLE value right before the edge, which is
   //                  what the hardware sees. Never what the nonblocking
   //                  assignment just wrote on that same edge.
   //   output #0      drives ON the edge, in the nonblocking region, so the DUT
   //                  does not see it until the next edge.
   clocking cb @(posedge clk);
      default input #1step output #0;
      output d_in;
      input  d_out;
   endclocking

   modport tb (clocking cb);
endinterface : reg_bfm
el archivo entero, 73 líneas
// The same DUT, with a clocking block: the edge and the delta are declared ONCE,
// inside the interface, and no task ever picks them again.
//
//   bash run.sh
module dut_reg (input bit clk, input byte unsigned d_in, output byte unsigned d_out);
   always_ff @(posedge clk) d_out <= d_in + 8'd1;
endmodule

interface reg_bfm (input bit clk);
   byte unsigned d_in, d_out;

   // The only two timing lines in the testbench:
   //   input  #1step  samples the STABLE value right before the edge, which is
   //                  what the hardware sees. Never what the nonblocking
   //                  assignment just wrote on that same edge.
   //   output #0      drives ON the edge, in the nonblocking region, so the DUT
   //                  does not see it until the next edge.
   clocking cb @(posedge clk);
      default input #1step output #0;
      output d_in;
      input  d_out;
   endclocking

   modport tb (clocking cb);
endinterface : reg_bfm

module top_con;
   bit clk = 0;
   always #5 clk = ~clk;

   reg_bfm bfm (clk);
   dut_reg u_dut (.clk(clk), .d_in(bfm.d_in), .d_out(bfm.d_out));

   byte unsigned primera, segunda, tercera;

   initial begin
      // Driving is "<=" against the clocking block. There is no edge to pick.
      bfm.cb.d_in <= 8'd10;

      // @(bfm.cb) is "wait for this clocking block's edge". There are THREE, and
      // the reason is the whole lesson:
      //   1st edge  the output #0 only now puts d_in at 10, in the nonblocking
      //             region, so the DUT has not seen it yet.
      //   2nd edge  now the DUT takes d_in=10 and computes d_out <= 11.
      //   3rd edge  the input #1step samples the STABLE value before this edge,
      //             which is already 11.
      // A testbench without a clocking block hides this behind a #1 and works by
      // accident. Here the arithmetic is visible and does not depend on the tool.
      @(bfm.cb);
      @(bfm.cb);
      @(bfm.cb);

      // And sampling is reading cb.d_out. It is read three times, at three
      // different points of the same cycle, and gives the same value all three.
      primera = bfm.cb.d_out;
      #2;
      segunda = bfm.cb.d_out;
      #3;
      tercera = bfm.cb.d_out;

      $display("d_in = %0d, and the DUT computes d_in + 1 = %0d", 8'd10, 8'd11);
      $display("  cb.d_out, right after the edge  : %0d", primera);
      $display("  cb.d_out, two units later       : %0d", segunda);
      $display("  cb.d_out, five later            : %0d", tercera);
      $display("");
      $display("The same number all three times, and not a single #1 in the testbench.");
      $display("The edge and the delta were declared once, in the clocking block.");

      if (primera != 8'd11 || segunda != 8'd11 || tercera != 8'd11)
        $fatal(1, "the clocking block should give 11 all three times");
      $finish;
   end
endmodule : top_con

Ésta es la slide que hay que poder repetir en una entrevista, porque clocking block es de las primeras cosas que se preguntan y la respuesta buena no es "sincroniza": es declara el instante de muestreo y de manejo una sola vez, en la interface, para que ninguna task lo vuelva a elegir. El detalle que confunde y conviene adelantar: en el ejemplo hacen falta tres @(cb) para ver el 11, y no dos. Uno para que el output #0 ponga el dato, otro para que el DUT lo tome, y el tercero porque el input #1step muestrea lo estable antes del flanco. La cuenta está a la vista en el código y no depende del simulador — que es exactamente la diferencia con el #1. Si alguien pregunta por qué el curso no lo usa desde el día 1: porque para entender qué problema resuelve hay que haber tenido el problema. Recién ahora, con la BFM escrita y las tres líneas corriendo, la pregunta tiene sentido. Verilator lo soporta desde la 5.x y el ejemplo termina en $fatal si el muestreo deja de dar 11: es la red que avisa si una versión cambia la semántica.

ver esta slide en el deck ›

Interfaces y BFM

¿Son obligatorios? No — y conviene saber por qué

La pregunta La respuesta
¿Se puede escribir un TB correcto sin clocking blocks? Sí. Con NBA en el driver y disciplina de scheduler, no hay race
¿Qué conviene que use un equipo? Usalos en interfaces síncronas — es lo que dice el style guide de lowRISC/OpenTitan

Esta slide es la que evita que el curso mienta por simplificar, y conviene decirla con todas las letras: "usá siempre clocking blocks" es una regla de estilo, no un teorema. El que la repite como si fuera lo segundo se lleva una discusión incómoda con el primer senior que se cruce. La frase de Dave Rich que ordena el tema —y vale citarla textual— es de 2014: "If you are already familiar with Verilog testbenches, then you probably don't need them." La sostuvo en 2022 y en 2024. No dice que estén mal: dice que no son una condición necesaria. Y del otro lado está lowRISC, que en el DV Coding Style Guide de OpenTitan los hace obligatorios para señales síncronas. No se contradicen: Dave contesta "¿puedo?" y lowRISC contesta "¿qué hacemos todos acá?". Es exactamente la diferencia entre malloc/free bien usados y un style guide que impone smart pointers — que un experto pueda manejar la memoria a mano no vuelve inútil al RAII. Dónde sí mueven la aguja de verdad, y es lo que hay que retener: agents y VIP reutilizables —el que los escribe está lejos del que los usa—, gate-level con SDF, y protocolos donde la spec define setup/hold. Ahí dejan de ser estilo. El material largo, con las fuentes, está en docs/clocking-blocks.md.

ver esta slide en el deck ›

Interfaces y BFM

El precio: dos nombres para el mismo cable

code/u2/clocking/mezcla.sv · #two-reads
      // The two reads, at the SAME instant, on the SAME wire.
      por_cb = bfm.cb.d_out;
      crudo  = bfm.d_out;

      $display("at the same instant, on the same wire:");
      $display("  bfm.cb.d_out (what the clocking block sampled) : %0d", por_cb);
      $display("  bfm.d_out    (the live wire)                   : %0d", crudo);
      $display("");
      $display("They differ by one cycle, and no warning says so. A monitor that");
      $display("reads through the clocking block and a scoreboard that reads the");
      $display("raw wire do not fail every time: they fail when the data changes.");
el archivo entero, 53 líneas
// The trap that comes with the clocking block: mixing the two domains.
//
// Once a signal belongs to a clocking block, there are TWO names for the same
// wire -- bfm.d_out and bfm.cb.d_out -- and they are NOT the same value. One is
// the live wire; the other is what the clocking block sampled #1step before the
// edge. On a signal that changes every cycle they differ by exactly one cycle.
//
// Nothing warns about this: it compiles, it runs, and a scoreboard that reads
// one of them while the monitor reads the other fails intermittently.
//
//   bash run.sh
module contador (input bit clk, output byte unsigned d_out);
   // Counts on every edge, so the live wire and the sampled one never agree.
   always_ff @(posedge clk) d_out <= d_out + 8'd1;
endmodule

interface cnt_bfm (input bit clk);
   byte unsigned d_out;
   clocking cb @(posedge clk);
      default input #1step;
      input d_out;
   endclocking
endinterface : cnt_bfm

module top_mezcla;
   bit clk = 0;
   always #5 clk = ~clk;

   cnt_bfm bfm (clk);
   contador u_cnt (.clk(clk), .d_out(bfm.d_out));

   byte unsigned por_cb, crudo;

   initial begin
      repeat (4) @(bfm.cb);

      // The two reads, at the SAME instant, on the SAME wire.
      por_cb = bfm.cb.d_out;
      crudo  = bfm.d_out;

      $display("at the same instant, on the same wire:");
      $display("  bfm.cb.d_out (what the clocking block sampled) : %0d", por_cb);
      $display("  bfm.d_out    (the live wire)                   : %0d", crudo);
      $display("");
      $display("They differ by one cycle, and no warning says so. A monitor that");
      $display("reads through the clocking block and a scoreboard that reads the");
      $display("raw wire do not fail every time: they fail when the data changes.");

      if (por_cb == crudo)
        $fatal(1, "they were expected to differ: the example stopped showing the trap");
      $finish;
   end
endmodule : top_mezcla

Ésta es la contracara de la slide anterior y la razón por la que el curso no los usa en su BFM: mal usados son peores que no usarlos, porque el modo de falla es intermitente y el waveform se ve bien. El error de verdad, el que aparece en los foros, tiene dos formas y conviene nombrar las dos. Una es mezclar el evento: @(posedge vif.clk) y después leer vif.cb.foo. La otra es mezclar el acceso: el monitor lee vif.cb.data y el scoreboard —o una assertion, o un $display de debug— lee vif.data. Las dos terminan igual: dos vistas temporales del mismo cable, y un mismatch que aparece una vez cada veinte corridas. Vale correr el ejemplo y dejar el número a la vista: 3 y 4. Nadie discute un 3 y un 4 sobre el mismo cable. Y la moraleja que engancha con el apéndice de las trampas mudas, donde ésta está anotada: en verificación el error caro no es el que rompe, es el que miente. Un clocking block a medias miente.

ver esta slide en el deck ›

Interfaces y BFM

Resumen de la unidad

Ésta es la sección más importante del día 1 y la que menos parece. Todo lo que viene después —clases, factory, agents, sequences— reordena piezas arriba de esta línea. La línea misma no se vuelve a tocar. La forma corta de decirlo: de acá en adelante el testbench deja de hablar en cables y empieza a hablar en operaciones. Ése es el primer paso hacia UVM, y ya está dado, sin haber escrito una sola clase. Si el grupo viene de RTL, la analogía cierra sola: la BFM es al testbench lo que un driver de dispositivo es al sistema operativo.

ver esta slide en el deck ›

Repaso · Día 1

1 de 7 · La pregunta del día

Mil operaciones al azar, el scoreboard no reportó ningún error y el log termina en PASS. ¿Qué te falta para decir que el DUT está verificado?

Verlo fallar — el PASS de un scoreboard que nunca vio un error no dice nada: pudo no haber comparado. VTALU_BUG=1 da vuelta un bit del resultado y el testbench tiene que fallar; make mutante lo exige para los tres testbenches de los días 1 y 2.

ver esta slide en el deck ›

Repaso · Día 1

2 de 7 · La spec de la ALU

Mientras la VTALU está ejecutando una operación, ¿qué tienen que hacer start y los operandos?

Estables hasta done — es el protocolo del DUT, y es exactamente el motivo por el que existe el BFM: encapsular esa regla en un solo lugar para que ningún test se la olvide. El primer distractor describe un protocolo real —arranque por pulso, operandos latcheados— que este DUT no tiene.

ver esta slide en el deck ›

Repaso · Día 1

3 de 7 · Cobertura funcional

El test corre 1000 operaciones al azar y la cobertura de código da 100 %. ¿Qué te dice eso sobre la verificación?

Muy poco — la cobertura de código mide el DUT; la funcional mide la spec. Una feature que el diseñador nunca escribió da 100 % de líneas y 0 % de lo que importa, y el reporte no te lo va a decir.

ver esta slide en el deck ›

Repaso · Día 1

4 de 7 · covergroup

Declarás un covergroup, le hacés new(), corrés mil operaciones y el reporte da 0 %. ¿Qué es lo primero que hay que mirar?

El sample() — el covergroup no se muestrea solo: alguien tiene que llamarlo, en el flanco o cuando llega una transacción. Sin esa llamada el código compila, corre, y el reporte da 0 sin una sola advertencia.

ver esta slide en el deck ›

Repaso · Día 1

5 de 7 · Interfaces y BFM

¿Qué gana el testbench cuando el protocolo se mueve a un BFM?

Deja de hablar en señales — el BFM traduce una operación a un handshake de señales. El tester no vuelve a tocar un cable; el scoreboard y el coverage siguen leyéndolos, por la interface, hasta que un monitor les mande transactions. Es el primer paso hacia UVM.

ver esta slide en el deck ›

Repaso · Día 1

6 de 7 · El plan de verificación

En el plan de verificación, ¿qué va en la columna Medida?

Qué bin se llena — las cinco columnas son Feature, Escenario, Estímulo, Chequeo y Medida, y ésta es la que más cuesta: obliga a decidir antes qué se va a contar. Si queda vacía, nadie se va a enterar de que el escenario nunca pasó — un caso que el random no tocó y que no tiene bin es indistinguible de uno que pasó mil veces.

ver esta slide en el deck ›

Repaso · Día 1

7 de 7 · clocking block

¿Hacen falta los clocking blocks para escribir un testbench UVM sin races?

No hacen falta, y conviene usarlos igual — son de SystemVerilog, no de UVM, y ningún uvm_driver muestrea por vos. Lo que evita la race es entender el scheduler: un driver que maneja con <= contra un DUT que registra con <= ya es determinista. El clocking block no reemplaza ese entendimiento, lo encapsula — y ahí es donde paga: agents reutilizables, VIP, gate-level y protocolos con setup/hold en la spec.

ver esta slide en el deck ›

Ejercicio · Día 1 · 1 de 2

Una operación nueva, de punta a punta

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

Media hora, y conviene dejarlos solos: el enunciado y el README alcanzan. El tropiezo garantizado es el ancho: el hardware desplaza por B[2:0], no por B entero, así que con B = 8'h20 corre cero lugares y no treinta y dos. El modelo del scoreboard tiene que hacer lo mismo o el TB que ellos mismos escribieron los va a acusar. Está avisado en el README, y aun así pasa. El tercer paso es el que más se saltea y el único que falla en silencio: bins single_cycle[] cubre el rango [add_op : xor_op], que llega hasta 3'b100. shr_op queda afuera de todos los bins, la simulación pasa en verde y —acá está lo peor— el reporte sigue diciendo 100 %: 76 de 76. El bin que falta no baja el porcentaje porque nunca llegó a entrar en el denominador. Ahí está la lección que vale para el resto del curso: un opcode que nadie mide es un opcode que nadie verificó, y el porcentaje es justo la métrica que no lo ve. Con la solución son 77 de 77, y se arranca en 86,8 % (66 de 76). Los diez que faltaban eran uno solo: el casillero que Verilator inventa para 3'b110, el valor que el enum no tenía — y el shift es justamente 3'b110, así que lo llena de paso. Vale mostrar los dos números, no sólo el porcentaje: por eso el run.sh exige, además del 100 %, que single_cycle tenga un bin más: el 100 % un trabajo a medias también lo alcanza.

ver esta slide en el deck ›

Ejercicio · Día 1 · 2 de 2

Las ondas: cuando el log no alcanza

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

Media hora, y es el ejercicio que justifica el apéndice de debug: ahí se dice que las ondas son la herramienta con la que se hace el 47 % del trabajo, y hasta acá el curso no las había hecho usar ni una vez. El bug es de protocolo y es el mismo que vuelve en el capstone: send_op cuenta flancos en vez de esperar el handshake. Para las operaciones de un ciclo da igual; la multiplicación tarda cuatro, así que send_op vuelve antes de tiempo, el estímulo siguiente pisa A y B, y cuando done finalmente sube el scoreboard compara los operandos nuevos contra el resultado viejo. Por eso los dos tiempos que pide la etapa 1 no son burocracia: el segundo es el done de la primera multiplicación, y el que lo busca en el visor ve que A y B ya cambiaron. Esa es la respuesta al por qué, y no hay forma de leerla en el log. La pregunta para tirar al grupo cuando lo terminen: si el multiplicador pasara de cuatro flancos a cinco, ¿cuál de las dos versiones de send_op se entera? Ninguna cuenta bien; sólo la que espera done sigue andando. Es literalmente la pista del wait state del APB del día 7. Y éste es el 47 % de la slide de tendencias, visto de cerca: el log dice falló y para el por qué hay que ir a otro lado. Hoy ese otro lado son las ondas; el día 3, en reporting, es el log el que aprende a decirlo.

ver esta slide en el deck ›

Seis de cada siete, otra vez

Cierre del día

Treinta segundos, y son los que cierran el arco del día: la primera slide hizo una pregunta y ésta la contesta con lo que el alumno escribió, no con teoría. Si el grupo está cansado, volver a poner la slide del 14 % un segundo antes de ésta: la pregunta era "¿cómo sabés que verificaste?", y la respuesta corta es "tengo el plan, lo vi fallar y sé qué bin falta". Lo que no hay que prometer de más: esto no evita el respin. Lo que evita es mandar a fabricar un PASS que nadie sabía qué significaba. El último bullet es el que el día 2 abre con "Ayer quedó": un tester que sólo multiplica es copiar el archivo y borrarle cinco líneas. Ése es el problema que las clases resuelven, y conviene dejarlo como problema y no como promesa.

ver esta slide en el deck ›