Menu

Instanciación de módulos en Verilog: conectando submódulos

Cómo instanciar un módulo dentro de otro, la diferencia entre conexiones de puerto por nombre y por posición, y los patrones de múltiples instancias que usarás para construir diseños reales.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Módulos dentro de módulos

Un diseño Verilog es un árbol de módulos. El módulo de nivel superior (tu testbench, o el wrapper de nivel superior de un chip) instancia módulos de nivel inferior, que instancian módulos de nivel aún inferior, todo el camino hasta las primitivas de puertas suministradas por el proveedor. La instanciación es la sintaxis para ese anidamiento.

Ya has visto la forma - la usamos en Your First Module:

and_gate dut(.a(a), .b(b), .y(y));

Esa línea crea una única instancia de and_gate, la llama dut y conecta sus puertos a señales locales. Vamos a desempaquetar cada pieza.

La forma de una instanciación

module_name instance_name (port_connections);
  • module_name debe coincidir con el nombre de una declaración module en alguna parte de tu proyecto. Verilog distingue mayúsculas y minúsculas.
  • instance_name es una etiqueta que eliges - normalmente descriptiva del rol que esta instancia juega. La usarás en rutas jerárquicas y vistas de forma de onda.
  • port_connections cablea los puertos de la instancia a señales locales. Hay dos formas de escribir esto.

Conexiones de puerto por nombre (usa estas)

La forma por nombre se ve así:

my_module instance_name(
    .clk    (clk),
    .reset  (reset_n),
    .data_in(in_bus),
    .data_out(out_bus),
    .valid  (out_valid)
);

Cada par .port(signal) dice "conecta el puerto de esta instancia llamado port a la señal local llamada signal". El orden no importa. Si añades un nuevo puerto a la declaración del módulo, las instanciaciones existentes no se rompen mientras le des al nuevo puerto un valor por defecto o actualices cada sitio.

Dos notas prácticas:

  • El nombre del puerto (a la izquierda del paréntesis) debe coincidir exactamente con la declaración del módulo.
  • El nombre de la señal (dentro del paréntesis) es local al sitio donde vive la instanciación - normalmente el módulo padre.

Si un puerto no está conectado, deja el interior vacío: .optional_port(). La señal flota (z) dentro de la instancia. Algunas herramientas de síntesis avisan; la mayoría lo aceptan.

Conexiones por posición (evítalas)

La forma más concisa lista señales en orden de lista de puertos:

my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);

Esto es más corto pero frágil. Reordena la lista de puertos del módulo (una refactorización real que sucede) y cada instanciación posicional se cablea mal en silencio. No recurras a posicional a menos que la lista de puertos tenga exactamente uno o dos miembros y sea improbable que cambie alguna vez.

Donde sigue siendo aceptable: pequeños módulos utilitarios donde el orden de los puertos es parte de la API. Una puerta de dos entradas está bien posicional. Un controlador de memoria de 30 puertos está pidiendo problemas.

Un ejemplo jerárquico completo

Esa es una jerarquía real de tres niveles: testfull_adder → dos instancias de half_adder. Cada instancia tiene su propia copia de las puertas dentro de half_adder; la herramienta de síntesis emitirá un circuito por instanciación.

Múltiples instancias del mismo módulo

Cuando instancias el mismo módulo varias veces, cada instancia es hardware independiente. No comparten estado. No comparten puertas. Imagina cada instancia como una copia fresca producida a partir del diseño.

adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));

Esos son cuatro sumadores separados corriendo en paralelo. Si adder contuviera un registro, cada instancia tendría su propia copia de ese registro, con su propio estado.

Loops generate: producir hardware repetido

Escribir cuatro instancias a mano está bien. Escribir 64 es tedioso. El bloque generate deja que el elaborador haga el tecleo por ti:

Tres piezas de sintaxis nuevas:

  • genvar i declara una variable de loop usable en generate. No es una señal de runtime - solo existe en tiempo de elaboración.
  • generate ... endgenerate envuelve el loop. Algunas herramientas aceptan loops generate sin la palabra clave generate explícita, pero escribirla hace la intención obvia.
  • begin : invert_loop etiqueta el scope del generate. La etiqueta se convierte en parte del nombre jerárquico de cada instancia generada (dut.invert_loop[0].u_inv, dut.invert_loop[1].u_inv, etc.).

El sintetizador desenrolla el loop y produce WIDTH copias de bit_inverter. Cada copia es hardware independiente.

Sobrescritura de parámetros en la instanciación

Si el módulo tiene parámetros, puedes sobrescribirlos con #(.PARAM(value)) entre el nombre del módulo y el nombre de la instancia:

counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));

Las dos instancias usan el mismo código fuente counter pero tienen anchos distintos. Cubrimos la sintaxis en Parameters; encaja limpiamente en la instanciación.

Nombres jerárquicos

Una vez que tienes una jerarquía, cada señal tiene una ruta jerárquica:

test.dut.ha0.sum

Eso se lee como: en el módulo test, dentro de la instancia dut, dentro de la instancia ha0, la señal llamada sum. Verás estas rutas en visores de forma de onda, mensajes de error y la ocasional llamada $display que se mete profundamente en un submódulo desde un testbench:

$display("internal carry1 = %b", dut.carry1);

Las referencias jerárquicas así son solo para testbenches y depuración - el RTL sintetizable no alcanza dentro de otros módulos.

Errores comunes

Discrepancia de nombre de puerto. .clk_in(clk) conecta el clk local a un puerto llamado clk_in. Si el puerto del módulo en realidad es clk, el parser te lo dirá (algunas herramientas más claramente que otras).

Discrepancia de ancho en un puerto. Conectar una señal de 4 bits a un puerto de 8 bits hace zero-extend en silencio; al revés trunca en silencio. La mayoría de las herramientas avisan; si no ves avisos, mira con más atención.

Olvidar el # del parámetro. counter (.WIDTH(8)) c(.clk(clk)) parece una sobrescritura pero no lo es - el parser intenta tratar (.WIDTH(8)) como una conexión de puerto y falla. Correcto: counter #(.WIDTH(8)) c(.clk(clk)).

Reutilizar un nombre de instancia. Dos instancias no pueden tener el mismo nombre en el mismo scope. El mensaje de error normalmente es claro; la tentación de copiar y pegar es lo que te pilla.

Qué viene a continuación

Ya puedes cablear módulos juntos en una jerarquía real. El siguiente doc redondea el lado estructural de Verilog - Continuous Assignment - y profundiza en la sentencia assign que hemos estado usando libremente desde el primer capítulo.

Preguntas frecuentes

¿Cómo se instancia un módulo en Verilog?

Escribe el nombre del módulo, luego un nombre de instancia, luego una lista entre paréntesis de conexiones de puerto: my_module instance_name(.port(signal), ...);. El estilo más común usa conexiones por nombre (.port(signal)), que coinciden por nombre de puerto sin importar el orden. El estilo posicional más conciso (my_module instance(signal1, signal2)) depende del orden de la lista de puertos y es peligroso de mantener.

¿Cuál es la diferencia entre conexiones por nombre y por posición?

Las conexiones por posición listan señales en el mismo orden que la lista de puertos del módulo - la primera señal conecta al primer puerto, la segunda al segundo, y así. Las conexiones por nombre usan .port_name(signal_name), coincidiendo por nombre. Por nombre es verboso pero inmune a reordenamientos de puertos y se autodocumenta en el sitio de la llamada. Usa por nombre para cualquier cosa que pase de dos o tres puertos.

¿Se puede instanciar el mismo módulo Verilog varias veces?

Sí - de eso se trata. Cada instancia es hardware independiente con su propio estado. Si tienes un módulo adder, puedes instanciarlo 64 veces en una unidad SIMD, cada una con entradas distintas. El loop generate es la sintaxis canónica cuando las instancias son similares e indexadas.

¿Qué es un bloque generate en Verilog?

generate ... endgenerate es una construcción en tiempo de compilación que produce hardware repetido. Un loop for dentro de generate crea N instancias de lo que sea que esté en el cuerpo. generate se ejecuta en tiempo de elaboración, antes de que empiece la simulación - no es un loop en runtime, es un generador de código para el sintetizador.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR