verilog-modularidad-instanciacion-jerarquia

Modularidad en FPGA: instanciación y diseño jerárquico

  • 5 min

La jerarquía en Verilog es la organización de un diseño mediante módulos conectados entre sí desde un módulo raíz o top-level.

Hasta ahora, hemos escrito todo nuestro código en un solo archivo, dentro de un único module. Para un LED parpadeando está bien, pero imagina diseñar un procesador o una tarjeta gráfica escribiendo todo en un solo archivo de 10.000 líneas. Sería una locura.

En el diseño de hardware, la clave del éxito es Divide y Vencerás.

Al igual que en programación usamos funciones o clases para encapsular código, en Verilog usamos la Jerarquía. Creamos pequeños módulos que hacen una sola cosa muy bien (un contador, un filtro, un controlador de memoria) y luego los conectamos entre sí en un módulo superior.

Hoy vamos a aprender el arte de la Instanciación: cómo “soldar” virtualmente estos bloques para crear sistemas complejos.

Jerarquía y top-level

Imagina que estás diseñando una Placa de Circuito Impreso (PCB).

Tienes varios chips (Timer 555, Microcontrolador, Sensores).

Tienes pistas de cobre que los unen.

En Verilog:

  • Los Chips son los Módulos (archivos .v que ya has diseñado).
  • Poner un chip en la placa se llama Instanciar.
  • Las Pistas de cobre son los wire que conectan las instancias.
  • La PCB completa es el módulo Top-Level.

El Top-Level es el “jefe”. Es el módulo raíz que tiene acceso a los pines físicos reales de la FPGA (LEDs, botones, puertos GPIO). Dentro de él, instanciamos sub-módulos, que a su vez pueden tener otros sub-módulos dentro.

Sintaxis de instanciación

Supongamos que tenemos nuestro módulo debounce (del artículo anterior) y queremos usarlo en nuestro diseño principal.

// Definición del módulo original (La "clase" o "plano")
module debounce (
    input wire clk,
    input wire btn_in,
    output reg btn_out
);
    // ... lógica interna ...
endmodule
Copied!

Para usarlo, tenemos que instanciarlo (crear un objeto). La sintaxis es:

NombreDelModulo NombreDeLaInstancia ( ...conexiones... );

Existen dos formas de conectar los cables (puertos), y elegir la correcta es cuestión de vida o muerte para tu proyecto.

Conexión por posición

Funciona igual que las funciones en C: el orden importa.

// ⛔ NO RECOMENDADO
// Conectamos: clk -> reloj_sys, btn_in -> pin_boton, btn_out -> cable_limpio
debounce u1 (reloj_sys, pin_boton, cable_limpio);
Copied!

¿Por qué es malo? Si mañana modificas el módulo debounce y cambias el orden de las entradas, o añades un puerto nuevo en medio, todas tus conexiones fallarán silenciosamente. Puedes acabar conectando el reloj al reset sin darte cuenta.

Conexión por nombre

Aquí decimos explícitamente: “Conecta el puerto A del módulo al cable B de mi diseño”. Usamos la sintaxis .puerto(señal).

// ✅ RECOMENDADO
debounce u1 (
    .clk(reloj_sys),     // Puerto 'clk' conectado a 'reloj_sys'
    .btn_in(pin_boton),  // Puerto 'btn_in' conectado a 'pin_boton'
    .btn_out(cable_limpio) // Puerto 'btn_out' conectado a 'cable_limpio'
);
Copied!
  • No importa el orden.
  • Es autodocumentado.
  • Si dejas un puerto sin conectar, es más fácil de detectar.

Consejo práctico: Siempre usa conexión por nombre. Te ahorrará horas de depuración cuando tus proyectos crezcan.

Ejemplo práctico: uniendo piezas

Vamos a construir un sistema completo. Queremos un contador que se incremente con un botón. Necesitamos:

  1. Un módulo debounce (para limpiar el botón).
  2. Un módulo contador (la lógica).
  3. Un módulo top_system (que une todo y sale a los pines).

El cableado interno (wire)

Para conectar la salida del debouncer a la entrada del contador, necesitamos un “cable interno”. Este cable no sale fuera de la FPGA, vive dentro del silicio. Lo declaramos como wire.

module top_system (
    input wire clk_12mhz,  // Pin físico reloj
    input wire rst,
    input wire btn_fisico, // Pin físico botón
    output wire [3:0] leds // Pines físicos LEDs
);

    // --- CABLES INTERNOS (Interconnects) ---
    wire boton_limpio;     // Cable que une debounce -> contador
    wire pulso_enable;     // Un ciclo por cada pulsación
    reg boton_anterior;

    // --- INSTANCIA 1: El filtro antirrebote ---
    debounce instancia_filtro (
        .clk(clk_12mhz),
        .rst(rst),
        .btn_in(btn_fisico),
        .btn_out(boton_limpio) // La salida se vuelca a nuestro wire interno
    );

    // Convertimos el nivel limpio en un pulso de un ciclo
    always @(posedge clk_12mhz) begin
        if (rst)
            boton_anterior <= 1'b0;
        else
            boton_anterior <= boton_limpio;
    end

    assign pulso_enable = boton_limpio & ~boton_anterior;

    // --- INSTANCIA 2: El contador ---
    // (Asumimos que tenemos un módulo contador hecho)
    contador_4bits instancia_contador (
        .clk(clk_12mhz),
        .enable(pulso_enable), // Incrementa una vez por pulsación
        .cuenta(leds)          // La salida va directa a los pines físicos
    );

endmodule
Copied!

Fíjate en lo limpio que queda. El top_system no sabe cómo se filtra el botón ni cómo se cuenta. Solo sabe que tiene dos componentes y que debe conectarlos entre sí.

Puertos sin conectar y conexiones constantes

A veces no necesitamos usar todos los puertos de un módulo, o queremos fijar una entrada a un valor constante.

mi_modulo u2 (
    .clk(clk),
    .entrada_datos(8'hFF), // Fijamos la entrada a 255 (Hardcoded)
    .reset(1'b0),          // Desactivamos el reset permanentemente
    .salida_debug()        // Dejamos este puerto ABIERTO (sin conectar)
);
Copied!

Dejar entradas sin conectar es peligroso (pueden flotar o tomar valores aleatorios). Dejar salidas sin conectar es seguro (simplemente la señal se pierde y el sintetizador optimizará esa lógica borrándola).

Errores comunes al instanciar

Múltiples drivers

No puedes conectar las salidas de dos módulos diferentes al mismo wire.

// ❌ ERROR GRAVE
modulo_A u1 (.out(cable_comun));
modulo_B u2 (.out(cable_comun)); 
// ¿Quién gana? El sintetizador normalmente rechazará el diseño.
Copied!

Si necesitas combinar señales, describe explícitamente la lógica necesaria, por ejemplo un multiplexor o una puerta OR. Dos salidas internas enfrentadas suelen producir un error de síntesis; el riesgo de daño físico aparece al enfrentar pines externos reales.

Confundir el módulo con la instancia

El nombre del módulo (debounce) es el “tipo”. El nombre de la instancia (instancia_filtro) es el nombre único de esa copia. Puedes tener muchas instancias del mismo tipo:

debounce btn1 (.btn_in(pin1), ...);
debounce btn2 (.btn_in(pin2), ...);
debounce btn3 (.btn_in(pin3), ...);
Copied!