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
Cómo usar este curso
Sobre todo si lo estás haciendo solo
- Qué hay que saber antes: Verilog o VHDL, y haber simulado algo. Programar
orientado a objetos no hace falta — el día 2 es exactamente eso. Lo único
que el curso da por sabido es SystemVerilog: si venís de VHDL o de
Verilog-2001, media hora con
docs/systemverilog-para-el-que-viene-de-vhdl.mdantes de empezar - Son siete días de clase, de 4 a 5 horas y cuarto — ≈ 31 h, y cada agenda trae el suyo. Y un día 8 opcional, después del cierre. Solo, calculá el doble: la mitad se va en correr los ejemplos, y ésa es la que enseña
- s abre las notas del presentador en otra ventana · n las muestra acá abajo, sin salir de la página. Ahí está lo que el instructor diría en voz alta: la trampa clásica, el porqué del número, el error que comete todo el mundo la primera vez. No las saltees
- Cada día cierra con un repaso de opciones clickeables y con un
ejercicio que se corrige solo:
cd code/ejercicios/d1 && bash run.sh. Los nombres de los otros dieciocho están encode/ejercicios/README.es.md
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.
Cómo usar este curso
Correrlo, navegarlo, y dónde buscar cuando algo no anda
- Corré los ejemplos:
make u4/testscorre uno,make u4la unidad entera. Un curso de verificación que sólo se lee no sirve para nada. Un ejemplo con UVM tarda un minuto y medio la primera vez y quince segundos la segunda, si tenésccacheinstalado — y si no tenés Linux, el camino corto es GitHub Codespaces, que ya lo trae - 0–8 saltan a la portada y a cada día · i abre el índice · Esc muestra el deck entero · y el ribbon de la esquina despliega el machete del día
- Si estás solo, leé el libro:
libro/dia1.htmles este mismo curso de corrido, con las notas ya adentro del texto. Tené el deck al lado para los repasos - Cuando algo no ande, andá al final: los apéndices La caja de herramientas de debug —qué mirar según el síntoma— y Las 21 trampas mudas —todo lo que compila, corre y miente—. Son las dos slides para imprimir
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.
Agenda
Día 1 · ≈ 4 h · unidades 1 y 2
Unidad 1 · Por qué se verifica
- Tendencias
- Qué es UVM
- La spec del VTALU
- El plan de verificación
Unidad 2 · El testbench sin UVM
- El testbench convencional
- Cobertura funcional
- Interfaces y BFM · el
clocking block
Dos ejercicios, y el segundo se resuelve con el visor de ondas
- Y el cierre del día, que vuelve a la primera slide
Al final del día podés:
- Escribir un plan de verificación de cinco columnas para un DUT que no viste antes
- Probar que tu scoreboard chequea algo: meterle un bug al DUT a propósito y verlo fallar
- Leer el número que sale de un
covergroup, decir qué fila del plan falta, y qué mirar primero cuando da 0 % - Explicar qué race evita un
clocking block— y por qué igual son opcionales
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.
Seis de cada siete
Y el bug que llegó al silicio pasó una regresión en verde
- El 14 % de los proyectos acierta el primer silicio. Seis de cada siete vuelven a la fábrica, y es el peor valor en veinte años de encuesta
- Cada bug funcional que llegó al silicio pasó antes por un testbench. Ese testbench corrió, terminó, y dijo que estaba bien
- Nadie manda a fabricar con un
FAILEDen el log. Se manda con unPASS - La pregunta de hoy es una sola: ¿cómo sabés que verificaste? Y "corrí muchos tests" no es una respuesta
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ó.
Tendencias
Por qué vuelven: lo que hay adentro de un chip hoy
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.
Tendencias
Verificar no es una etapa, es la mitad del trabajo
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é.
Tendencias
Y se verifica, sobre todo, con UVM
É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.
¿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.
Introducción
¿Qué es UVM?
- Universal Verification Methodology: una librería de clases escrita en SystemVerilog, más un montón de convenciones sobre cómo usarla
- No es un lenguaje y no es un simulador. Todo lo que vamos a ver en el curso se puede escribir a mano — y en los días 1 y 2 lo vamos a escribir a mano
- La hace Accellera, el mismo consorcio que estandarizó SystemVerilog, y es
IEEE 1800.2. El curso usa la implementación de referencia,
uvm-core 2020.3.1 - Viene de OVM (Cadence + Mentor), con ideas de VMM (Synopsys) y de eRM (Verisity): las metodologías de cuatro vendors que competían, hasta que la industria se cansó de traducir testbenches entre herramientas
- Y ahí está el objetivo, que es uno solo: que el testbench del de al lado se parezca al tuyo
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.
Introducción
De dónde viene
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.
Introducción
Un testbench de SystemVerilog, por dentro
É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ó.
Introducción
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.
Introducción
El árbol de clases
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.
Introducción
Correr más tests escribiendo menos código
- Ésa es la promesa, y tiene una condición: el env y los componentes casi no cambian de un test a otro. Lo que cambia es el estímulo
- Para que eso funcione, el testbench tiene que dejar ganchos: métodos virtuales, la factory y los callbacks. Un test engancha ahí y no toca la estructura
- Un test nuevo entonces es una de estas tres cosas, y ninguna toca el env:
- agregar constraints para llegar a un caso de borde
- hacer un override de la factory para cambiar una clase por otra
- inyectar errores o delays con callbacks
- Y arriba de todo eso, la palanca que más rinde: el mismo test con cientos de semillas distintas
É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.
La spec del VTALU
- start debe permanecer en 1 y los operandos estables hasta
que termina la operación - done se levanta cuando termina la operación: en la multiplicación
es un pulso de un ciclo; en add/sub/and/xor es un nivel, y se queda
arriba mientras start siga arriba - reset_n es asíncrono y activo bajo; el diagrama arranca con el DUT
ya fuera de reset - ovf sale junto con done, y es el borrow de la resta: vale 1
cuandoA < B. Para las otras operaciones vale siempre 0
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é.
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 | — | — |
- Ciclos cuenta flancos de
clkconstartarriba, hasta el que levantadoneinclusive: uno en las de un ciclo, cuatro en la multiplicación. El plan y las assertions del día 7 cuentan igual rst_opno lo decodifica el RTL — el DUT lo ve como unused. Es la convención del testbench para pulsarreset_n, y por eso está en eloperation_tdel testbench3'b110está libre a propósito: es el ejercicio de hoy
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.
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);
endel 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- Dos
always_ffy nada más: uno registraA op B, el otro levantadone - Los resets no son iguales: el del resultado es síncrono —sólo
clken la lista de sensibilidad—, el deldonees asíncrono done <= start && (op != no_op): por eso en estas cuatro operacionesdonees un nivel y se queda arriba mientrasstartlo esté
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ó.
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
endel 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- Es un pipeline: los operandos se registran, se multiplican, y el producto
atraviesa dos registros más antes de salir por
result_mult: cuatro flancos - El
doneviaja por la misma cadena —done3,done2,done1— así que llega exactamente con el dato y no antes - El
& ~done_multde cada etapa es lo que apaga la cadena sola: por eso acádonees un pulso de un ciclo, aunquestartsiga arriba
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.
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- El top no calcula nada: instancia los dos bloques y decodifica el opcode
es_mult = (op == mul_op). Elstartse rutea a uno solo, yresult_muxydonesalen del mismo lado. Elovfsigue el mismo patrón justo abajo del recorte —assign ovf = es_mult ? 1'b0 : ovf_1c;— yresult_muxsale porresultrecién después del XOR de+VTALU_BUG- Por eso el protocolo del bus es uno solo aunque adentro haya dos latencias
distintas: lo que el testbench ve es
start→done
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.
La spec del VTALU
Cómo se corre
- El DUT vive en
code/vtalu_dut/, en SystemVerilog - El simulador del curso es Verilator (libre, sin licencia):
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
- Los que usan UVM compilan la librería entera la primera vez. Instalá
ccacheantes de empezar: el curso lo detecta solo y no se recompila dos veces - Cada ejemplo tiene su
run.sh; los flags comunes están encode/verilator/common.sh
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.
La spec del VTALU
Ver las ondas de verdad
- El diagrama de la primera slide está dibujado. El de verdad lo generás vos, y es la herramienta con la que se hace el 47 % del trabajo: debuggear
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
VLT_TRACE=1prende dos cosas a la vez: el--tracede Verilator y el$dumpfile/$dumpvarsdel top, que sin el flag no compilaría- Sale gratis y es libre, igual que el simulador. Cuando el testbench se cuelgue
en
while (done == 0), esto es lo que te va a decir por qué
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.
La spec del VTALU
Resumen de la unidad
- El protocolo es una sola frase, y hay que respetarla:
starten 1 y los operandos estables hasta que subedone - La letra chica que va a colgar al primer testbench:
donees un pulso de un ciclo en la multiplicación y un nivel en las de un ciclo - Adentro hay dos bloques con latencias distintas —uno y cuatro ciclos— y el top decodifica el opcode. Afuera, el bus es uno solo
ovfes una salida más, y es delsub_opy de nadie másrst_opno existe para el RTL: es una convención del testbench para pulsarreset_n, y por eso entra en la cobertura- El DUT no valida el opcode: el que está libre levanta
doneigual, con elresultde la operación anterior. Atajarlo es trabajo del plan de verificación, no del diseño - Y la herramienta que se usa el 47 % del tiempo ya está instalada:
VLT_TRACE=1y GTKWave
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.
El plan de verificación
El entregable más profesional de la disciplina, y el más barato
- Es una tabla. Se escribe antes que el testbench — si se escribe después, describe lo que el testbench ya hace en vez de lo que la spec pide
- Sale de la spec que acabás de leer, no del testbench que todavía no existe
- Y es lo que se entrega: el capstone del día 7 se entrega con el plan lleno, que es exactamente lo que se entrega en un proyecto
É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.
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" |
- Una fila por escenario, no por feature: una feature con tres casos borde son tres filas, y se cierran de a una
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.
El plan de verificación
Dos reglas que salen de las columnas
- *Si la columna de chequeo dice "a ojo", el escenario no está verificado.* Está simulado, que es otra cosa
- Si la columna de medida está 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
- Y una distinción que ordena la columna de chequeo: el scoreboard chequea qué calcula el DUT; las assertions chequean cómo se habla con él
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.
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 |
- Seis filas que salen de la spec, y todavía ninguna columna de la derecha. Esta sección llena dos: el estímulo las provoca y el scoreboard dice si el resultado estuvo bien
- La tercera —qué bin se llena cuando pasa— es la sección que sigue
- El testbench tiene tres partes porque el plan tiene tres columnas por llenar
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.
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- Mil vueltas: elegir operación, elegir operandos, levantar
start, esperar un flanco condonearriba, bajarstart - El
caseestá ahí por la letra chica de la spec:no_opno levantadone,rst_oppulsareset_n, y las demás esperan get_op()yget_data()sesgan el random hacia los bordes —00 y FF— para llegar a los casos que el plan pide. Es constrained random escrito a manoenviadasse cuenta acá ychequeadasen el scoreboard, y unfinalexige que sean iguales: es el testbench chequeándose a sí mismo- Fijate quién sabe del protocolo acá: el tester. En interfaces y BFM eso se muda
a la BFM y en el cuerpo del
repeatse achica a una línea
É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.
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 : scoreboardel 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- Un
always @(posedge done): cada vez que el DUT dice que terminó, el scoreboard predice el resultado y lo compara - El
#1no es un adorno: sin él se leen las señales en el mismo instante en quedonesube y se puede comer una carrera de deltas no_opyrst_opno levantandone, así que este bloque no debería correr con ellas. Elifes defensivo —sin predicción para esas dos compararía basura— y adentro vachequeadas++, la otra mitad del contador- El covergroup —la tercera pata— es la sección que sigue
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.
El testbench convencional
¿Cómo sabés que el scoreboard chequea algo?
code/u2/convencional/mutante.txtFAILED: A: 15 B: ce op: xor_op result: da ovf: 0- Mil operaciones y ningún error. Ahora el mismo testbench con el DUT roto a
propósito:
VTALU_BUG=1 bash run.shda vuelta el bit 0 del resultado - Falla en la primera comparación. Eso —y no el
PASSde antes— es lo que prueba que el scoreboard mira el resultado - Un scoreboard que nunca vio un error no está probado: pudo haber comparado contra sí mismo, o no haber comparado nada
make mutantehace esto con los tres testbenches de los días 1 y 2, y falla si alguno no falla. Es la primera respuesta a la pregunta del día
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.
El testbench convencional
Resumen de la unidad
- El testbench tiene tres partes porque el plan tiene tres columnas por llenar: estímulo, self-checking y medida. Hoy están sueltas en un archivo
- El estímulo sesga el random hacia los bordes —
00yFF— porque el azar uniforme casi nunca los visita - El
#1antes de leer las señales no es adorno: sin él se lee en el mismo instante en que el DUT escribe, y el resultado es indefinido - Un
PASSno prueba nada hasta que viste elFAILED:VTALU_BUG=1es la forma de verlo, ymake mutantela de no olvidarse - Y lo que hay que mirar para interfaces y BFM: el tester sabe del
protocolo. Mueve
starty esperadonecon la mano
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.
Cobertura funcional
¿Cuándo terminás de verificar?
- Cobertura de código —líneas, ramas, toggle— la da el simulador gratis. Dice qué RTL se ejecutó
- Cobertura funcional la escribís vos. Dice qué escenarios de la spec pasaron por el DUT
- No son lo mismo, y la primera engaña: un test que manda siempre
add_opconA=B=8'h01ejecuta casi todas las líneas del sumador y no verificó nada - La cobertura funcional es el contrato entre la spec y el testbench: si un caso de la spec no tiene un bin, nadie se va a enterar de que falta
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.
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 |
- Las seis filas dejan de ser buenas intenciones: cada una es una línea de código, y el simulador te dice cuáles todavía no pasaron
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.
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
- Un
covergroupes un tipo: declararlo no mide nada, hay que hacerlenew() sample()es el que cuenta. Sinsample()el reporte da 0 % y el código compila igual- Dónde muestrear es una decisión de diseño. Acá, el flanco en el que lee el DUT, nunca el mismo en el que escribe el tester; cuando el TB tenga monitores va a ser "cada vez que el monitor ve un comando"
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.
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
}
- Sin
{ }, SystemVerilog crea un bin por valor. Anda para unenum; para unintla herramienta corta el rango en 64 casilleros —auto_bin_max— que no corresponden a ninguna fila del plan - Los corchetes
[]reparten: un casillero por valor del rango. Sin ellos, todos los valores caen en el mismo casillero - Un coverpoint está cubierto cuando todos sus bins tienen al menos un hit. Un bin que no se puede llenar te clava el número para siempre
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.
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
}
endgroupel 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 : topsingle_cycle[]genera seis bins —uno por operación—,multi_cycleuno solo- Lo que está entre
`ifndef VERILATORson los bins de transición, que vienen más adelante en esta misma sección
É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.
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 : topall_opsignorarst_opyno_op: no tiene sentido pedir "una suma con los operandos en 0x00" cuando la operación es un reseta_legyb_legbajan 256 valores a los tres casilleros que importan: todo ceros, el medio, todo unos- Un bin imposible que dejás puesto te deja la cobertura clavada abajo del 100 %
y nadie sabe por qué.
ignore_binslo vuelve una decisión escrita
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.
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
- Un
crosses el producto cartesiano de sus coverpoints: crece rápido y la mayoría de las combinaciones no está en ninguna spec binsof(x) intersect {v}se lee "los bins dexque contienenv"- Se combinan con
&&y||, y lo que sobra se tira conignore_bins - Así los 45 bins quedan en los 11 que el plan pidió de verdad
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.
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 : topmul_00se lee de corrido: una multiplicación en la que A o B valgan 0x00. Los otros ocho bins del cross son estas dos líneas con otroopmul_maxes el único con&&: pide las dos patas en 0xFF: el producto máximo,FF×FF=FE01. No desborda — 8 bits por 8 entran en 16- Verilator 5.052 ignora
binsof/intersect(%Warning-COVERIGN) y mide el cross completo: por eso el número no es el de una herramienta comercial
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.
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
- Tres filas del plan no son valores, son secuencias: "después de un reset", "una mult después de una de un ciclo"
- Verilator 5.052 todavía no los compila (Internal Error). En el código
están entre
`ifndef VERILATOR: son parte del tema y se leen igual - Repro mínimo en
code/verilator/repro-cg-transition.sv. El día que Verilator los soporte, se borra elifndefy nada más
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.
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
option.at_leastes la que más importa: por defecto vale 1, así que un bin que pasó una sola vez ya figura cubierto. El caso borde que salió una vez en mil corridas no está verificado, y el 100 % dice que síoption.auto_bin_maxacota los bins automáticos. Sin{ }y sin esta perilla, un coverpoint de 8 bits no da 256 casilleros: da 64, repartidos por la herramienta y no por la specoption.weightcambia cuánto pesa un coverpoint en el total, ytype_option.merge_instancessuma todas las instancias en un solo número en vez de reportarlas por separado- Verilator 5.052 respeta
auto_bin_max, yat_leastsólo escrito en el coverpoint: en el covergroup lo ignora sin avisar. Está endocs/verilator.md
É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.
Cobertura funcional
Leer el número
$ make u2/convencional
Coverage Summary:
covergroup : 86.8% (66/76)
- 66 de 76 bins llenos, con 1000 randomizaciones y 615 operaciones al bus
—el
no_opy elrst_opse sortean y no se envían, y el testbench lo imprime justo antes del reporte de Verilator—. Las otras filas salen0/0:--coverage-userdeja afuera la cobertura de código - Los 10 que faltan son un solo valor:
all_ops.auto_5y sus nueve cruces. Es3'b110, que el enum no tiene — Verilator reparte los bins automáticos por el tipo de base,bit [2:0], y no por miembro del enum - O sea que ninguna fila del plan quedó sin cubrir, y en Questa esto da 100 %.
El número no es la meta: la pregunta que sirve es cuál bin falta, y eso
sólo lo dice el reporte por bin,
obj_dir/top/coverage.dat - Correr → mirar qué falta → escribir el test dirigido → volver a correr. Eso se llama coverage closure, y es a lo que un verificador le dedica el día
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.
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 |
- Las tres columnas de la derecha son las tres partes del testbench de esta sección. Lo que las ata es el plan, y hasta ahora no lo habías visto junto
- Las dos filas de la resta son las que obligan al scoreboard a mirar dos
salidas: si sólo compara
result, pasa en verde con elovfclavado en cero - Las cuatro últimas no las chequea ningún scoreboard: son reglas de
ovfy de protocolo, y se chequean donde ocurren —las assertions - Las doce filas van numeradas igual que en
docs/plan-de-verificacion.md, que además dice en qué archivo vive cada una y trae la plantilla del capstone
É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.
Cobertura funcional
Resumen de la unidad
- Cobertura de código ≠ cobertura funcional. La primera mide qué RTL se ejecutó; la segunda, qué escenarios de la spec pasaron
- Un
covergroupes un tipo: hay que instanciarlo connew()y alguien tiene que llamar asample(). Sin eso el reporte da 0 % sin una advertencia - Los bins son los casilleros.
[]reparte uno por valor; sin corchetes, todo el rango entra en uno solo ignore_binses documentación ejecutable: dice en voz alta qué no se cubre y por qué. Un bin imposible que dejás puesto te clava el número abajo- Un
crosses el producto cartesiano y crece rapidísimo: sin acotarlo, la cobertura no cierra nunca option.at_leastvale 1 por defecto: un bin que pasó una sola vez ya figura cubierto, y un caso raro con un hit no está verificado- El número no es la meta. La meta es el plan; la cobertura sólo dice cuánto del plan se cumplió
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.
Interfaces y BFM
Primero: las señales dejan de estar sueltas
code/u2/interfaces-bfm/vtalu_bfm.sv · #signals-and-clockinterface 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
endel 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- Una
interfacees un bundle de señales con nombre propio. Nueve cables que antes estaban declarados en eltopahora viven juntos - El reloj se genera adentro: la interface no es un cable, es un modelo del bus
- Conectar el DUT pasa a ser
.A(bfm.A),.clk(bfm.clk)… y una señal nueva se declara una vez: los módulos que la usan la ven aparecer sin tocar sus puertos - Lo que acá no hace falta y en un RTL ajeno vas a ver: el
modport, que es la misma interface con las direcciones puestas para un lado del cable
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.
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_opel 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_bfmsend_op(A, B, op, result)traduce "hacé una suma" al meneo de señales que el DUT espera. Eso es un Bus Functional Model- Adentro está toda la letra chica de la spec: el pulso de
reset_n, elno_opque no esperadone, y eldo ... while (done == 0)de las demás - De acá en adelante nadie mueve
bfm.starta mano. El que quiere una operación la pide
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.
Interfaces y BFM
El top, con la BFM en el medio
- El testbench sigue partido en las mismas tres piezas —tester, scoreboard y cobertura—, y ninguna cambió de nombre
- Lo que cambió es cómo se conectan al DUT: ya no hay señales sueltas viajando, hay una sola interface que las tres comparten
code/u2/interfaces-bfm/top.svmodule 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 : topConviene 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.
Interfaces y BFM
Scoreboard: el mismo chequeo, ahora hablando con la BFM
code/u2/interfaces-bfm/scoreboard.svmodule 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- Es el
scoreboarddel testbench convencional: predice y compara con la misma lógica - Lo único que cambió es de dónde saca las señales: antes eran variables del
mismo módulo, ahora son
bfm.A,bfm.B,bfm.op_set - Y por eso ahora es un módulo aparte: puede vivir en su propio archivo porque no comparte nada con el tester salvo la interface
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.
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 beginel 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- El tester ya no toca
startni esperadone: llama abfm.send_op(...)y se olvida del protocolo - El handshake quedó escrito una sola vez, adentro de la BFM. Si mañana el
DUT pide dos ciclos de
start, se toca un archivo - Ésa es la definición de Bus Functional Model: la tarea traduce "hacé una suma" al meneo de señales que el DUT espera
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.
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));
- La regla —mientras
startestá arriba, los operandos no se tocan— ya está escrita dos veces: en prosa en la spec, y encapsulada adentro desend_op()acá - Una assertion es esa misma frase por tercera vez, pero ejecutable: no la respeta, la chequea
- Y la chequea donde ocurre —en la interface, en el flanco exacto— y no al final, comparando resultados
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.
Interfaces y BFM
Dónde se chequea cada cosa
- El scoreboard del testbench convencional chequea el resultado:
A op Bcontra lo que devolvió el DUT. Es la mitad que este curso desarrolla hasta el final - Una assertion chequea el protocolo: quién se mueve, cuándo, y por cuántos ciclos. Es la otra mitad
- Un DUT que devuelve bien el
resultpero bajadoneun ciclo antes pasa todos los tests de los próximos seis días. Nadie lo está mirando - Se ve entero en la sección de assertions, con el mismo VTALU y el mismo
run.sh
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.
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- La BFM encapsuló el protocolo. Lo que no encapsuló es cuándo se maneja y cuándo se muestrea: eso sigue decidido task por task
- Tres formas de leer el mismo dato:
0,11y11. Dos valores, pero tres mecanismos, y el del medio depende de un#1que no se ve en ningún lado - El
@(negedge clk)de nuestra BFM es la tercera. Anda — y le pide al que la lee que sepa por qué
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.
Interfaces y BFM
Por qué el flanco solo no alcanza: las regiones
- Un flanco no es un instante indivisible: adentro hay regiones, en orden fijo
- El
<=delalways_ffaterriza en NBA: el que lee en Active lee el viejo - El
#1cruza el NBA, el flanco opuesto lo cruza sobrado, y SVA no necesita ninguno de los dos: muestrea en preponed, antes de todo
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.
Interfaces y BFM
El clocking block: el flanco y el delta, declarados una vez
code/u2/clocking/con_clocking.sv · #reg_bfminterface 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_bfmel 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_coninput #1step— muestreá el valor estable justo antes del flanco, que es el que ve el hardware. Nunca el que la no bloqueante acaba de escribiroutput #0— manejá en el flanco, con las no bloqueantes, así el DUT no lo ve hasta el flanco siguiente- Manejar pasa a ser
cb.d_in <= valor; muestrear, leercb.d_out; esperar,@(cb). Ningún#1en ninguna task - Y el dato leído es el mismo en cualquier momento del ciclo: la carrera se cerró por declaración, no por costumbre
É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.
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 |
- No son de UVM: son de SystemVerilog. UVM no los pide ni los conoce
- Dave Rich —la referencia de Verification Academy— sostiene desde 2014 que son opcionales: lo que evita la race es entender el scheduler, no el clocking block
- Las dos posturas contestan preguntas distintas: una es corrección del lenguaje, la otra es política de equipo. Las dos son correctas
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.
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- Con la señal adentro de un clocking block hay dos formas de leerla:
bfm.d_outes el cable en vivo,bfm.cb.d_outes lo que se muestreó#1stepantes del flanco - En una señal que cambia cada ciclo difieren en un ciclo: el ejemplo
imprime
3y4, en el mismo instante y sobre el mismo cable - Nadie avisa. Compila, corre, y el scoreboard falla sólo cuando el dato cambia
- La regla: si una señal entró al clocking block, todo el protocolo la lee
por ahí — y se espera
@(bfm.cb), no@(posedge bfm.clk)
É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.
Interfaces y BFM
Resumen de la unidad
- Una
interfacees un bundle de señales con nombre: agregar un cable deja de ser tocar seis port lists - Un BFM es la interface más el protocolo:
send_op(A, B, op, result)traduce "hacé una suma" al meneo de señales - Toda la letra chica de la spec —el pulso de reset, esperar
done, elno_opque no responde— queda escrita una sola vez - Desde acá nadie mueve
bfm.starta mano. El que quiere una operación, la pide - El tester, el scoreboard y la cobertura pasan a ser módulos aparte, cada uno en su archivo, conectados por la BFM
- Y esto no se abandona: el driver del agent sigue llamando a esta misma task
- La regla que el BFM cumple se puede además chequear, con una assertion que vive en esta misma interface. Ésa es la sección de assertions
- Y el
clocking blockcierra el último agujero: el flanco y el delta dejan de decidirse task por task y se declaran una vez
É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.
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?
- Nada: mil operaciones sin un error es un DUT verificado
- Correr más semillas hasta que la cobertura de código llegue al 100 %
- Saber qué habría dicho el log con un bug adentro: correrlo con el DUT mutado
- Reemplazar el scoreboard por assertions, que chequean el protocolo en el flanco exacto y no al final
Verlo fallar — el
PASSde un scoreboard que nunca vio un error no dice nada: pudo no haber comparado.VTALU_BUG=1da vuelta un bit del resultado y el testbench tiene que fallar;make mutantelo exige para los tres testbenches de los días 1 y 2.
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?
-
startbaja enseguida: es un pulso de arranque, y el DUT ya latcheó todo - Da igual: el DUT los registra en el primer flanco
- Los operandos tienen que cambiar en cada ciclo
-
starten 1 y los operandos quietos hasta que subedone
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.
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?
- Que el DUT está verificado y el plan de verificación se puede cerrar
- Que el testbench no tiene bugs
- Que faltan pocos escenarios: el 100 % ya recorrió el diseño entero
- Muy poco: mide el RTL que se ejecutó, no la spec
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.
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?
- Que nadie esté llamando a
sample() - Que los bins estén mal definidos y no matcheen ningún valor
- Que el DUT no esté respondiendo
- Que falten
ignore_bins
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.
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 y pasa a hablar en operaciones
- Simula más rápido: una tarea del BFM le cuesta menos al simulador que mover cables
- Se puede sintetizar el testbench
- Se ahorra tener que declarar un
clk
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.
Repaso · Día 1
6 de 7 · El plan de verificación
En el plan de verificación, ¿qué va en la columna Medida?
- Cuánto tarda el escenario en correr, para poder estimar la regresión
- El nombre del archivo del testbench que cubre esa fila
- Qué bin se llena cuando el escenario pasa
- Cuántas veces hay que correr el test para darlo por cubierto
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.
Repaso · Día 1
7 de 7 · clocking block
¿Hacen falta los clocking blocks para escribir un testbench UVM sin races?
- Sí: sin clocking block, driver y DUT compiten siempre en el mismo flanco
- Sí, y además son parte de la librería UVM
- No: alcanza con NBA en el driver y disciplina de scheduler
- No: los reemplaza el
uvm_driver, que ya muestrea en la región correcta
No hacen falta, y conviene usarlos igual — son de SystemVerilog, no de UVM, y ningún
uvm_drivermuestrea 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.
Ejercicio · Día 1 · 1 de 2
Una operación nueva, de punta a punta
cd code/ejercicios/d1 && bash run.sh
- La VTALU tiene un opcode libre,
3'b110. Enseñale a desplazar, y enseñale también al TB que lo tiene que verificar - Se toca en tres lugares: el RTL, el estímulo con su chequeo, y la medida — el bin que hoy no existe
- El
run.shfalla hasta que imprimeEXERCISE OKy el reporte cierra con 77 bins cubiertos; la solución está al lado
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.
Ejercicio · Día 1 · 2 de 2
Las ondas: cuando el log no alcanza
cd code/ejercicios/d1b && bash run.sh
- La corrida aborta con una sola línea:
FAILED: A: e5 B: 0 op: mul_op result: fe01 ovf: 0.e5 * 00es0, nofe01. El scoreboard tiene razón, y el DUT también: el que se equivocó es el handshake de la BFM - Ahí se termina lo que el log te da. El
run.shdejaondas.vcdal lado:gtkwave ondas.vcd - Etapa 1: dos tiempos que sólo están en el visor, en
respuesta.txt. Etapa 2: la línea de la BFM que los explica - Es el único ejercicio del curso que no se resuelve leyendo el log
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.
Seis de cada siete, otra vez
Cierre del día
- La regresión que mandó el bug a fabricar dijo
PASS. Hoy tenés tres formas de saber si esePASSvalía algo, y las tres son columnas de una tabla - ¿Qué había que probar? Un plan: una fila por escenario, escrito antes que el testbench, desde la spec
- ¿Quién dijo que estuvo bien? Un scoreboard que predice y compara — y que viste fallar con un bug adentro
- ¿Cuánto del plan pasó? Un
covergroupcon un bin por fila, y el reporte por bin, no el porcentaje - El tester ya pide operaciones en vez de mover cables: eso fue la BFM. Lo que todavía no se puede es cambiar el estímulo sin editar el archivo. Mañana es eso
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.