Un parameter es una constante de elaboración que permite configurar la estructura de un módulo antes de sintetizarlo.
Imagina esta situación: Has diseñado un contador de 8 bits perfecto. Funciona de maravilla. A los diez minutos, te das cuenta de que para otra parte del proyecto necesitas un contador de 16 bits.
¿Qué haces? ¿Copias el archivo contador_8bits.v, lo pegas, lo renombras a contador_16bits.v y cambias manualmente todos los [7:0] por [15:0]?
¡Error! Eso es una pesadilla de mantenimiento. Si luego encuentras un bug, tendrás que arreglarlo en 20 archivos distintos.
En programación orientada a objetos existen las “Plantillas” (Templates) o los “Genéricos”. En Verilog, tenemos los Parámetros.
Hoy vamos a aprender a escribir código reutilizable, creando módulos elásticos que se adaptan a lo que necesitemos en el momento de instanciarlos.
¿Qué es un parámetro?
Un Parameter es una constante que es local a un módulo, pero que puede ser modificada al instanciar ese módulo desde fuera.
Es importante entender la diferencia con una entrada (input):
- Input: Es una señal que cambia mientras el circuito funciona (Runtime). Ej: Un botón pulsado.
- Parameter: Es un valor que se define antes de fabricar el circuito (Synthesis time). Define la estructura física del chip. Una vez sintetizado, no se puede cambiar.
Piensa en los parámetros como en los argumentos del constructor de una clase. Definen cómo nace el objeto.
Definición de un módulo genérico
Para usar parámetros, añadimos una sección especial #( ... ) antes de definir los puertos del módulo.
Vamos a crear el famoso Contador de N bits.
module contador_generico #(
// Definimos los parámetros y sus valores POR DEFECTO
parameter BITS = 8 // Si no me dices nada, seré de 8 bits
)(
input wire clk,
input wire rst,
output wire [BITS-1 : 0] cuenta // El ancho depende del parámetro
);
reg [BITS-1 : 0] registro_interno;
always @(posedge clk) begin
if (rst)
registro_interno <= 0;
else
registro_interno <= registro_interno + 1;
end
assign cuenta = registro_interno;
endmoduleFíjate en la línea output wire [BITS-1 : 0].
Si BITS vale 8, Verilog entenderá [7:0].
Si BITS vale 32, Verilog entenderá [31:0].
El ancho del bus es paramétrico: queda fijado para cada instancia durante la elaboración, antes de generar el hardware.
Configuración de cada instancia
Ahora, desde nuestro top_system, podemos crear copias de este contador con tamaños distintos. Usamos la sintaxis #( .PARAMETRO(valor) ).
module top_level (
input wire clk,
input wire rst,
output wire [7:0] leds,
output wire salida_lenta
);
// INSTANCIA 1: Usando el valor por defecto (8 bits)
contador_generico contador_peque (
.clk(clk),
.rst(rst),
.cuenta(leds) // Encaja con salida de 8 bits
);
// INSTANCIA 2: Sobreescribiendo el parámetro a 32 bits
wire [31:0] cuenta_grande;
contador_generico #(
.BITS(32) // <--- Aquí configuramos esta instancia específica
) contador_enorme (
.clk(clk),
.rst(rst),
.cuenta(cuenta_grande)
);
assign salida_lenta = cuenta_grande[31];
endmoduleEl sintetizador es lo suficientemente listo para crear físicamente dos circuitos distintos: uno pequeño de 8 flip-flops y otro grande de 32 flip-flops, ambos generados desde el mismo código fuente.
parameter vs localparam
A veces necesitamos constantes que nos ayuden a hacer el código legible (como los estados de una FSM: IDLE, RUN…), pero no queremos que nadie las cambie desde fuera. Son constantes privadas.
Para eso existe localparam.
- parameter: Público. Modificable al instanciar.
- localparam: Privado. Fijo dentro del módulo.
module mi_maquina_estados #(
parameter ANCHO_BUS = 8 // Configurable
)(
input clk
);
// Constantes internas (Nadie puede cambiarlas desde fuera)
localparam STATE_INIT = 0;
localparam STATE_RUN = 1;
// ...
endmoduleCálculos con parámetros
Lo realmente potente de los parámetros es que podemos usar fórmulas matemáticas.
Imagina que quieres hacer un Divisor de Frecuencia (Prescaler) para obtener 1 Hz. Normalmente calculas a mano: “Tengo 12 MHz, necesito contar hasta 6 millones… eso son 23 bits…”.
¿Y si cambiamos de placa a una de 50 MHz? Tienes que recalcular todo. Mejor dejemos que Verilog calcule por nosotros.
Usaremos la función del sistema $clog2() (Ceiling Logarithm base 2), que nos dice cuántos bits hacen falta para representar un número.
module generador_strobe #(
parameter FRECUENCIA_ENTRADA = 12000000, // 12 MHz por defecto
parameter FRECUENCIA_SALIDA = 1 // 1 Hz por defecto
)(
input wire clk,
input wire rst,
output reg strobe
);
// 1. Calculamos el tope de la cuenta automáticamente
localparam MAX_CUENTA = (FRECUENCIA_ENTRADA / FRECUENCIA_SALIDA);
// 2. Calculamos los bits necesarios automáticamente
// Si MAX_CUENTA es 12.000.000, $clog2 devolverá 24.
localparam N_BITS = (MAX_CUENTA <= 1) ? 1 : $clog2(MAX_CUENTA);
reg [N_BITS-1 : 0] contador = 0;
always @(posedge clk) begin
if (rst) begin
contador <= 0;
strobe <= 0;
end else begin
strobe <= 0;
if (contador == MAX_CUENTA - 1) begin
contador <= 0;
strobe <= 1;
end else begin
contador <= contador + 1;
end
end
end
endmoduleAhora, instanciar un generador de 9600 baudios para una UART es tan fácil como:
generador_strobe #(
.FRECUENCIA_ENTRADA(12000000),
.FRECUENCIA_SALIDA(9600)
) baud_gen ( ... );Si las frecuencias no son divisibles de forma exacta, la división entera introduce un pequeño error. Comprueba que quede dentro de la tolerancia del protocolo.
Las funciones como $clog2 o las divisiones en los parámetros no consumen recursos en la FPGA. El ordenador las calcula una sola vez durante la compilación y “pega” el resultado constante en el diseño.