Un módulo es la unidad de Verilog
Todo en Verilog vive dentro de un module. Un módulo envuelve una pieza de circuito: declara qué señales entran, qué señales salen y qué wires/registros/lógica hay entre ellas. Cada chip que has visto es un árbol de módulos instanciando otros módulos, todo el camino hacia abajo hasta las primitivas de puertas suministradas por el proveedor.
La forma siempre es la misma:
module name(port_list);
// declaraciones: wire, reg, parameter
// cuerpo: assigns, instanciaciones, bloques always
endmodule
Construiremos un módulo completo por etapas.
El módulo útil más pequeño: una puerta AND de dos entradas
Necesitamos algo simple y autocontenido. Una puerta AND de dos entradas es perfecta: dos entradas, una salida, una línea de lógica.
Pulsa Ejecutar. Deberías ver las cuatro filas de una tabla de verdad AND. Vamos a recorrer cada pieza.
Leyendo la declaración del módulo
module and_gate(
input wire a,
input wire b,
output wire y
);
module and_gatedeclara un módulo llamadoand_gate. El nombre es como otros módulos lo instanciarán.- La lista entre paréntesis es la lista de puertos - las señales visibles desde fuera.
input wire a-aes un puerto de entrada; es unwire(excitado desde fuera).output wire y-yes un puerto de salida excitado por algo dentro del módulo.
Si quisieras ser conciso podrías escribir input a en vez de input wire a - la dirección sola hace que el tipo por defecto sea wire. Pero ser explícito es un hábito que vale la pena formar. Module Ports cubre el conjunto completo de formas de puertos.
El cuerpo
assign y = a & b;
Eso es una asignación continua. Dice "cuando cambie a o b, recalcula y como el AND bit a bit de los dos". No hay reloj, ni timing - la relación siempre es verdadera. Eso es lógica combinacional pura.
endmodule cierra el bloque. El módulo está terminado.
El testbench
No puedes ejecutar and_gate solo. Necesitas un segundo módulo que excite sus entradas y observe sus salidas. Eso es el testbench, y por convención lo llamamos test, tb, o <design>_tb.
module test;
reg a, b;
wire y;
and_gate dut(.a(a), .b(b), .y(y));
...
endmodule
Tres cosas que observar:
- Sin lista de puertos. Un testbench es la cima de la simulación - nada por encima.
reg a, bywire y. Las entradas al diseño bajo prueba (DUT) sonregen el testbench porque las estamos excitando desde un bloque procedural. La salida es unwireporque la excita el DUT.and_gate dut(.a(a), .b(b), .y(y)). Esto es instanciación. Estamos creando una copia deand_gatey llamándoladut(un nombre común - "design under test"). La sintaxis.a(a)dice "conecta el puerto llamadoade la instancia a la señal local llamadaa". Module Instantiation profundiza más.
El estímulo
initial begin
a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
$finish;
end
El bloque initial se ejecuta una vez al inicio de la simulación. Dentro:
a = 0; b = 0;excita las entradas. Estas son asignaciones bloqueantes - el orden importa, y cada una sucede antes que la siguiente.#1avanza el tiempo simulado en 1 unidad. Lo necesitamos para queytenga tiempo de estabilizarse tras el cambio en las entradas. Sin el#1,$displayimprimiría el valor anterior dey.$display(...)imprime en la consola de simulación. La cadena de formato funciona como elprintfde C:%bes binario,%des decimal,%hes hexadecimal,%tes tiempo de simulación.$finishtermina la simulación. Sin él, el simulador seguiría avanzando el tiempo para siempre esperando un evento que nunca llega.
Intenta romperlo
Modifica el módulo para que sea una puerta OR (cambia & por |) y vuelve a ejecutar. La tabla de verdad se da la vuelta. Ahora prueba un XOR:
El mismo esqueleto. Distinto operador. Ese es todo el juego para la lógica combinacional - declara puertos, escribe assigns, barre entradas, observa salidas.
Un módulo de dos salidas
Los módulos pueden tener más de una salida. Aquí hay un medio sumador - dos entradas, suma y acarreo de salida:
Las dos sentencias assign viven una al lado de la otra pero suceden en paralelo - no hay "primero calcula sum y luego calcula carry". Ambas son siempre verdaderas. Esa es la concurrencia de la que hablamos en Hardware vs Software, hecha concreta.
Lo que ahora sabes
Has visto todo el esqueleto de un archivo Verilog: un módulo de diseño con puertos declarados y un cuerpo, más un módulo testbench que lo instancia, excita sus entradas en un bloque initial y reporta resultados. Casi cada archivo fuente Verilog que vas a leer encaja en esta plantilla. El resto del lenguaje es solo rellenar lógica más rica, señales multibit, comportamiento sincronizado por reloj y testbenches más grandes.
A continuación: comentarios y estilo de código, para que tus módulos sigan siendo legibles a medida que crecen.
Preguntas frecuentes
¿Cuál es el módulo Verilog más simple?
El módulo Verilog legal más pequeño es simplemente module name; endmodule - sin puertos, sin cuerpo. El más pequeño útil es uno de una sola salida: module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule. Eso es una pieza real de lógica combinacional que puedes meter en cualquier diseño más grande.
¿Cómo ejecuto un módulo Verilog?
No puedes ejecutar un módulo por sí solo - es una descripción de un circuito, no un programa. Escribes un módulo testbench que instancia tu diseño y excita sus entradas, luego compilas ambos con iverilog -o sim design.v test.v y ejecutas vvp sim. El editor del navegador en esta página hace ambos pasos por ti cuando pulsas Ejecutar.
¿Qué es un testbench en Verilog?
Un testbench es un segundo módulo - normalmente sin puertos - cuyo trabajo es ejercitar tu diseño. Instancia el diseño, mueve sus entradas mediante un bloque initial, observa las salidas con $display o $monitor, y llama a $finish cuando termina. Los testbenches no son sintetizables; existen solo para verificar comportamiento.
¿Por qué mi código Verilog necesita $finish?
Porque el hardware nunca para. Un simulador finge que el tiempo está pasando, y sin un $finish explícito seguiría avanzando para siempre esperando nuevos eventos. $finish le dice al simulador 'ya terminamos, sal limpiamente'. En un testbench es la última línea del bloque initial - ejecuta la prueba, luego termina.