fpga-vga-hdmi-generacion-video

Generación de vídeo VGA y HDMI con FPGA

  • 6 min

Un controlador VGA es un circuito que genera coordenadas de píxel y pulsos de sincronismo siguiendo unos tiempos de vídeo concretos.

Si intentas generar una señal de video VGA de 640x480 píxeles a 60Hz con un Arduino Uno, el pobre chip tendrá que dedicar el 100% de su CPU solo para escupir bits a los pines, y aun así probablemente fallará.

Con una FPGA, podemos generar vídeo mientras otros bloques trabajan en paralelo, siempre que haya recursos y ancho de banda suficientes.

Hoy vamos a aprender a gobernar el rayo de electrones de un monitor. Vamos a diseñar un Controlador VGA.

El barrido de imagen

Aunque uses un monitor LCD moderno o un proyector HDMI, el estándar de transmisión de video sigue basado en cómo funcionaban las televisiones antiguas de tubo de rayos catódicos (CRT).

Imagina un pincel de luz que dibuja la pantalla:

Empieza arriba a la izquierda.

Dibuja una línea horizontal hacia la derecha.

Cuando llega al final, se apaga, vuelve rápido a la izquierda y baja un poquito.

Repite esto hasta llegar abajo del todo.

Vuelve arriba del todo para empezar la siguiente imagen (Frame).

Para que el monitor sepa cuándo “volver a la izquierda” o “volver arriba”, necesitamos dos señales de sincronización sagradas:

  • H-Sync (Horizontal Sync): “¡Fin de línea! Vuelve a la izquierda”.
  • V-Sync (Vertical Sync): “¡Fin de imagen! Vuelve arriba”.

Los tiempos de blanking (porches)

Aquí está la trampa. Una resolución de 640x480 no mide 640x480. El monitor necesita un tiempo extra “fuera de la pantalla” para estabilizar el rayo magnético antes de empezar a dibujar de nuevo.

A estas zonas muertas las llamamos Porches. Una línea horizontal completa se divide en:

  1. Visible Area (640 px): Donde pintamos colores.
  2. Front Porch (16 px): Un respiro antes del pulso.
  3. Sync Pulse (96 px): El pulso H-Sync real.
  4. Back Porch (48 px): Un respiro después del pulso para estabilizar.

Total: 640 + 16 + 96 + 48 = 800 tiempos de píxel por línea. Lo mismo aplica en vertical (480 líneas visibles vs 525 líneas totales).

La Pixel Clock:

Cada cuadro contiene 800 × 525 = 420.000 tiempos de píxel. El modo histórico de 640×480 utiliza un reloj de 25,175 MHz, que produce unos 59,94 cuadros por segundo. Con 25 MHz obtendríamos aproximadamente 59,52 Hz;

Muchos monitores lo aceptan, aunque tampocod debes darlo por supuesto.

Salidas analógicas de color

VGA es analógico. Necesitamos voltajes entre 0V (Negro) y 0.7V (Color Máximo). Como la FPGA solo saca 0V o 3.3V, usamos un DAC de Resistencias en el conector VGA.

Normalmente tenemos 3 canales: Rojo, Verde y Azul.

  • Si tenemos 1 bit por color (3 pines): Solo 8 colores posibles.
  • Si tenemos 4 bits por color (12 pines): 2^12 = 4096 colores.

Una entrada VGA suele presentar una terminación de 75 Ω. No conectes una salida de 3,3 V directamente: utiliza la red de resistencias calculada para tu número de bits y revisa el esquema de la placa. No siempre se trata de una escalera R-2R.

Implementación en Verilog

Vamos a crear un controlador para 640x480 @ 60Hz. Necesitamos dos contadores: uno para la X (0 a 799) y otro para la Y (0 a 524).

module vga_driver (
    input wire clk_25mhz,   // Reloj exacto de píxel
    input wire rst,
    output wire hsync,      // Pin VGA H-Sync
    output wire vsync,      // Pin VGA V-Sync
    output wire video_on,   // 1 = Estamos en zona visible
    output wire [9:0] x,    // Coordenada X actual (para que otros módulos pinten)
    output wire [9:0] y     // Coordenada Y actual
);

    // Parámetros estándar VGA 640x480 60Hz
    localparam H_VISIBLE = 640;
    localparam H_FRONT   = 16;
    localparam H_SYNC    = 96;
    localparam H_BACK    = 48;
    localparam H_TOTAL   = 800; // 640+16+96+48

    localparam V_VISIBLE = 480;
    localparam V_FRONT   = 10;
    localparam V_SYNC    = 2;
    localparam V_BACK    = 33;
    localparam V_TOTAL   = 525; // 480+10+2+33

    // Contadores
    reg [9:0] h_count = 0;
    reg [9:0] v_count = 0;

    // 1. Contador Horizontal y Vertical
    always @(posedge clk_25mhz) begin
        if (rst) begin
            h_count <= 0;
            v_count <= 0;
        end else begin
            // Avance horizontal
            if (h_count == H_TOTAL - 1) begin
                h_count <= 0;
                // Avance vertical (al terminar una línea)
                if (v_count == V_TOTAL - 1) begin
                    v_count <= 0;
                end else begin
                    v_count <= v_count + 1;
                end
            end else begin
                h_count <= h_count + 1;
            end
        end
    end

    // 2. Sincronismos activos a nivel bajo y alineados con los contadores
    assign hsync = ~((h_count >= H_VISIBLE + H_FRONT) &&
                     (h_count <  H_VISIBLE + H_FRONT + H_SYNC));
    assign vsync = ~((v_count >= V_VISIBLE + V_FRONT) &&
                     (v_count <  V_VISIBLE + V_FRONT + V_SYNC));

    // 3. Señal Video ON (Solo pintar cuando estamos en zona visible)
    assign video_on = (h_count < H_VISIBLE) && (v_count < V_VISIBLE);

    // 4. Exportar coordenadas para el dibujante
    assign x = h_count;
    assign y = v_count;

endmodule
Copied!

Cómo pintar cada píxel

Ahora que tenemos el controlador que nos dice DÓNDE estamos (x, y) y CUÁNDO pintar (video_on), solo necesitamos lógica combinacional para decidir QUÉ color poner.

module test_patron (
    input wire clk,         // Entrada 12 MHz (si usas PLL fuera) o 25 MHz
    output wire hsync,
    output wire vsync,
    output wire [2:0] rgb   // 3 pines: R, G, B (1 bit cada uno)
);

    wire video_on;
    wire [9:0] x, y;

    // Instancia del driver VGA (Asumimos que 'clk' ya es 25 MHz)
    vga_driver mi_vga (
        .clk_25mhz(clk),
        .rst(1'b0),
        .hsync(hsync),
        .vsync(vsync),
        .video_on(video_on),
        .x(x),
        .y(y)
    );

    // LÓGICA DE PINTADO
    // Si video_on es 0, OBLIGATORIO mandar NEGRO (000).
    // Si no, el monitor perderá el nivel de negro y se verá mal.
    
    reg [2:0] color_pintar;
    
    always @(*) begin
        if (!video_on) begin
            color_pintar = 3'b000;
        end else begin
            // Ejemplo: Bandera tricolor vertical
            if (x < 213)      color_pintar = 3'b100; // Rojo
            else if (x < 426) color_pintar = 3'b010; // Verde
            else              color_pintar = 3'b001; // Azul
            
            // Ejemplo 2: Tablero de ajedrez
            // color_pintar = (x[4] ^ y[4]) ? 3'b111 : 3'b000;
        end
    end

    assign rgb = color_pintar;

endmodule
Copied!

¿Y qué pasa con HDMI / DVI?

VGA es analógico y “viejo”. ¿Podemos sacar HDMI digital puro? Sí, pero es mucho más difícil.

HDMI (y DVI) usan una codificación llamada TMDS (Transition Minimized Differential Signaling). Cada canal codifica 8 bits de vídeo en símbolos TMDS de 10 bits, por lo que transmite a diez veces la frecuencia de píxel. Para 640×480, cada canal ronda los 251,75 Mbit/s.

En FPGAs básicas, esta velocidad puede quedar fuera de las especificaciones de los pines o requerir técnicas muy dependientes del dispositivo. Otras familias incluyen serializadores y recursos diferenciales dedicados que facilitan la salida DVI/HDMI.

Lo bueno es que la lógica de tiempos (h_count, v_count, video_on) es exactamente la misma. Solo cambia la etapa final de salida física.