Esta guía explica el papel fundamental de los clocking blocks de SystemVerilog en la construcción de entornos de verificación robustos bajo la Metodología de Verificación Universal (UVM). Cubriremos desde los conceptos básicos hasta las mejores prácticas para la sincronización, el manejo de datos y la cobertura funcional.
El Problema: Las Condiciones de Carrera
En la verificación, el entorno de prueba (Testbench) y el Diseño Bajo Prueba (DUT) se ejecutan en paralelo. Si ambos intentan leer y escribir en la misma señal en el mismo instante (por ejemplo, en un flanco de reloj), el resultado es impredecible. Esto se conoce como condición de carrera (race condition).
¿Qué valor lee el testbench? ¿El antiguo o el nuevo? La respuesta depende del orden interno en que el simulador ejecute los procesos, lo que hace que la simulación no sea determinista y, por tanto, no sea fiable.
La Solución: ¿Qué es un Clocking Block?
Un clocking block es una construcción de SystemVerilog que agrupa un conjunto de señales y define de forma estricta cómo y cuándo deben ser leídas (muestreadas) o escritas (conducidas) en relación con un evento de reloj específico.
Su objetivo principal es eliminar las condiciones de carrera introduciendo márgenes de seguridad temporales.
Sintaxis y Componentes Clave
// Definido dentro de una interface
interface mi_if(input bit clk);
logic req;
logic gnt;
logic [7:0] data;
// El clocking block define la temporización desde la perspectiva del Testbench
clocking tb_cb @(posedge clk);
// Margen de seguridad por defecto para entradas y salidas
default input #1step output #2ns;
// Dirección de las señales desde el punto de vista del TB
input gnt;
output req, data;
endclocking: tb_cb
endinterface
@(posedge clk)
: Especifica el evento de reloj que gobierna el bloque.default input #1step
: Skew de Entrada. Muestrea las señales de entrada un paso de simulación infinitesimal (1step
) antes del flanco de reloj. Esto garantiza que se lea el valor estable del ciclo anterior.default output #2ns
: Skew de Salida. Conduce las señales de salida 2 nanosegundos después del flanco de reloj. Esto da tiempo al DUT a muestrear los valores actuales antes de que el testbench los cambie.
Aplicación Práctica en UVM: Driver y Monitor
Los clocking blocks
son la pieza central para la sincronización en los componentes UVM que interactúan directamente con el DUT.
El Driver: Conducción Segura de Señales
El driver conduce transacciones hacia el DUT. Usa el clocking block para asegurarse de que lo hace sin conflictos.
- Sincronización: Espera al evento del clocking block con
@(vif.tb_cb)
. - Asignación: Utiliza asignaciones no bloqueantes (
<=
) para imitar el comportamiento concurrente de los registros en el hardware.
class my_driver extends uvm_driver #(my_transaction);
// ... (código uvm boilerplate) ...
virtual task run_phase(uvm_phase phase);
forever begin
// 1. Obtener la transacción a enviar
seq_item_port.get_next_item(req);
// 2. Sincronizarse con el reloj a través del clocking block
@(vif.tb_cb);
// 3. Conducir las señales usando asignaciones NO bloqueantes
// El skew de salida se aplica automáticamente
vif.tb_cb.req <= 1;
vif.tb_cb.data <= req.data;
// Esperar a la respuesta (grant) del DUT
wait (vif.tb_cb.gnt == 1);
@(vif.tb_cb);
vif.tb_cb.req <= 0;
// 4. Notificar que la transacción ha finalizado
seq_item_port.item_done();
end
endtask
endclass
El Monitor: Muestreo Fiable del Bus
El monitor observa el bus y convierte la actividad de las señales en objetos de transacción.
- Sincronización: También espera al evento
@(vif.tb_cb)
. - Asignación: Utiliza asignaciones bloqueantes (
=
) para capturar los valores muestreados en variables locales de forma inmediata.
class my_monitor extends uvm_monitor;
uvm_analysis_port #(my_transaction) analysis_port;
// ... (código uvm boilerplate) ...
virtual task run_phase(uvm_phase phase);
forever begin
// 1. Sincronizarse con el reloj
@(vif.tb_cb);
// 2. Comprobar si hay una transacción válida para muestrear
// El skew de entrada ya ha garantizado que leemos valores estables
if (vif.tb_cb.req && vif.tb_cb.gnt) begin
my_transaction trans = my_transaction::type_id::create("trans");
// 3. Capturar los valores usando asignaciones BLOQUEANTES
trans.data = vif.tb_cb.data;
// 4. Enviar la transacción capturada para su análisis
analysis_port.write(trans);
end
end
endtask
endclass
Cobertura Funcional: El Enfoque UVM
Un error común es vincular la cobertura a un evento de reloj. La filosofía UVM dicta que la cobertura debe ser impulsada por la transacción.
Práctica Recomendada
Se crea un componente suscriptor (o se usa el scoreboard) que recibe la transacción del monitor y entonces, y solo entonces, muestrea la cobertura.
class my_coverage_subscriber extends uvm_subscriber #(my_transaction);
// Covergroup que se muestrea con una transacción como argumento
covergroup data_cg with function sample(my_transaction t);
coverpoint t.data {
bins ZEROS = {0};
bins ONES = {'1};
bins OTHER = default;
}
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
data_cg = new();
endfunction
// Esta función se ejecuta automáticamente cuando el monitor llama a analysis_port.write()
virtual function void write(my_transaction t);
// El momento perfecto para muestrear: tenemos una transacción completa y validada.
data_cg.sample(t);
endfunction
endclass
Práctica Desaconsejada
Definir un covergroup
con un trigger de reloj (covergroup cg @(posedge clk);
) rompe la abstracción, ya que vuelve a acoplar la lógica de cobertura al hardware, ignorando la capa de transacciones y arriesgándose a muestrear datos en momentos incorrectos.
Conclusión: Principios Clave
- Usa Clocking Blocks Siempre: Son la herramienta estándar para la sincronización segura entre el testbench y el DUT.
- Abstrae la Temporización: Define la temporización una sola vez en la
interface
. El resto del entorno de prueba simplemente se sincroniza con el evento del clocking block. - Respeta las Asignaciones: Usa no bloqueantes (
<=
) en el Driver para conducir señales y bloqueantes (=
) en el Monitor para capturar valores. - Impulsa la Cobertura por Transacciones: Muestrea tus
covergroups
llamando explícitamente a.sample()
cuando recibas una transacción, no con triggers de reloj.