fpga-contadores-temporizadores-prescalers

Contadores en FPGA: temporizadores y divisores de frecuencia

  • 5 min

Un contador es un registro que cambia siguiendo una secuencia numérica en cada evento de reloj.

Saber hacer contar a una FPGA es fundamental. Es la base para:

  • Crear temporizadores (como el millis() de Arduino).
  • Generar señales PWM.
  • Bajar la frecuencia del reloj (Prescalers).
  • Recorrer direcciones de memoria.

Hoy vamos a aprender a dominar el tiempo.

Anatomía de un contador

Un contador no es más que un registro que se suma a sí mismo. Imagina un circuito con un registro (memoria) y un sumador.

En cada flanco de reloj, el registro lee su propio valor actual.

Le suma 1.

Guarda el nuevo resultado.

module contador_simple (
    input wire clk,
    output wire [3:0] leds
);

    reg [3:0] cuenta = 0; // Inicializamos a 0

    always @(posedge clk) begin
        cuenta <= cuenta + 1; // Sumamos uno en cada ciclo
    end

    assign leds = cuenta;

endmodule
Copied!

El Desbordamiento (Overflow) es tu amigo.

Cuando un contador de 4 bits llega a 15 (1111) y le sumas 1, automáticamente vuelve a 0 (0000). No da error, simplemente da la vuelta. Esto es ideal para crear ciclos repetitivos.

El divisor de frecuencia (prescaler)

El reloj de nuestra FPGA suele ir muy rápido (12 MHz, 50 MHz, 100 MHz). A veces necesitamos que las cosas ocurran más despacio (por ejemplo, parpadear un LED a 1 Hz o leer un sensor a 100 kHz).

Para esto usamos un Prescaler. Básicamente, contamos pulsos del reloj rápido hasta llegar a un número tope, y entonces hacemos “algo”.

Existen dos formas de hacerlo: la fea y la menos fea.

Relojes derivados mediante lógica

Los principiantes suelen intentar crear una señal de reloj nueva y usarla en otro bloque always.

// ⛔ PRÁCTICA NO RECOMENDADA
reg reloj_lento = 0;
always @(posedge clk) begin
    if (contador == LIMITE) begin
        reloj_lento <= ~reloj_lento; // Invertimos
        contador <= 0;
    end
end

// Usar este reloj generado en otro sitio
always @(posedge reloj_lento) begin ... end 
Copied!

¿Por qué está mal? Las FPGAs tienen “autopistas” especiales dedicadas exclusivamente a la señal de reloj principal (Clock Tree) para garantizar que llega a todos los rincones del chip a la vez. Si creas un reloj con lógica normal (flip-flops), la señal viajará por caminos normales, tendrá retardos (skew) y tu diseño se volverá inestable.

Pulsos de clock enable (strobe)

En lugar de crear un reloj nuevo, creamos un pulso de habilitación (Strobe) que dura exactamente un ciclo de reloj. Todo el sistema sigue funcionando a la velocidad máxima (ej: 12 MHz), pero nuestro circuito solo se “activa” cuando recibe ese pulso.

// ✅ PRÁCTICA RECOMENDADA
reg pulso_1hz = 0;

always @(posedge clk) begin
    // Valor por defecto (para que solo dure 1 ciclo)
    pulso_1hz <= 0; 

    if (contador == LIMITE) begin
        contador <= 0;
        pulso_1hz <= 1; // ¡Disparo!
    end else begin
        contador <= contador + 1;
    end
end

// En el otro módulo, seguimos usando clk, pero filtramos con el if
always @(posedge clk) begin
    if (pulso_1hz) begin
       // Esto se ejecuta solo 1 vez por segundo
       led <= ~led;
    end
end
Copied!

Cómo calcular el límite

Para saber hasta cuánto tenemos que contar, la fórmula es trivial:

Ejemplo:

  • Placa: IceStick / Alhambra (Reloj de 12 MHz = 12,000,000 Hz).
  • Objetivo: Cambiar el estado del LED una vez por segundo (un ciclo completo cada dos segundos, es decir, 0,5 Hz).

Necesitamos un registro capaz de almacenar el número 12 millones.

  • 2^23 = 8.388.608 (se queda corto).
  • 2^24 = 16.777.216 (es suficiente). Necesitamos un registro de 24 bits.

Ejemplo completo: un cambio por segundo

Vamos a implementar un módulo completo usando la técnica del Strobe.

module blinky_exacto (
    input wire clk,      // Entrada 12 MHz
    output reg led = 0   // Salida LED
);

    // Constante para el límite (usamos parameter para que sea configurable)
    // 12 MHz - 1 porque contamos el 0
    parameter FRECUENCIA_CLK = 12000000; 
    
    // Registro del contador (calculamos bits necesarios o ponemos de sobra)
    reg [23:0] contador = 0;
    
    // Señal de control (Strobe)
    reg tic_segundo = 0;

    // --- BLOQUE 1: El Prescaler ---
    always @(posedge clk) begin
        // Comportamiento por defecto
        tic_segundo <= 0; 
        contador <= contador + 1;

        if (contador == FRECUENCIA_CLK - 1) begin
            contador <= 0;      // Reiniciamos cuenta
            tic_segundo <= 1;   // Generamos el pulso de 1 ciclo
        end
    end

    // --- BLOQUE 2: La Lógica del LED ---
    // Fíjate que seguimos usando 'clk', NO 'tic_segundo' como reloj
    always @(posedge clk) begin
        if (tic_segundo) begin
            led <= ~led; // Cambiamos el estado del LED
        end
    end

endmodule
Copied!

Contadores descendentes y otros modos

Por supuesto, no estamos limitados a sumar 1.

  • Cuenta atrás: contador <= contador - 1;
  • Cuenta de 2 en 2: contador <= contador + 2;
  • Carga paralela: Podemos forzar el contador a un valor específico si se cumple una condición (útil para hacer temporizadores programables).
if (load_enable) begin
    contador <= valor_inicial; // Carga manual
end else begin
    contador <= contador - 1;  // Cuenta regresiva
end
Copied!