Uso combinado interfaz y Clocking Blocks en UVM



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

  1. Usa Clocking Blocks Siempre: Son la herramienta estándar para la sincronización segura entre el testbench y el DUT.
  2. 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.
  3. Respeta las Asignaciones: Usa no bloqueantes (<=) en el Driver para conducir señales y bloqueantes (=) en el Monitor para capturar valores.
  4. Impulsa la Cobertura por Transacciones: Muestrea tus covergroups llamando explícitamente a .sample() cuando recibas una transacción, no con triggers de reloj.