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:
- Visible Area (640 px): Donde pintamos colores.
- Front Porch (16 px): Un respiro antes del pulso.
- Sync Pulse (96 px): El pulso H-Sync real.
- 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 = 4096colores.
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;
endmoduleCó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¿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.