Objetivo y caso de uso
Qué construirás: Un monitor de pulsaciones PS/2 a VGA sobre la Radiona ULX3S (Lattice ECP5-85F) que captura en tiempo real los códigos de escaneo de un teclado PS/2 y los muestra en pantalla como valores hexadecimales y estados básicos. Verás eventos make/break, prefijos extendidos y un histórico reciente con salida VGA fluida a 60 FPS y latencia visual de pocos milisegundos.
Para qué sirve
- Diagnosticar teclados PS/2 y adaptadores comprobando al instante si llegan pulsaciones y liberaciones.
- Validar códigos de escaneo como
1C, secuencias break conF0y prefijos extendidos comoE0. - Usarlo como herramienta de banco en retroinformática, KVMs y placas embebidas sin arrancar un PC completo.
- Aprender diseño digital combinando recepción serie PS/2, búfer de eventos y generación de video VGA en un caso práctico real.
Resultado esperado
- Visualización en monitor VGA de las últimas teclas recibidas como códigos hexadecimales y banderas de estado simples.
- Respuesta estable en tiempo real, con refresco típico de 60 FPS y latencia extremo a extremo normalmente <16 ms.
- Consumo de GPU no aplica; la carga recae en lógica FPGA dedicada, con uso de recursos bajo para un diseño de instrumentación sencillo.
- Capacidad de observar secuencias completas de entrada, por ejemplo
1Cal pulsar yF0 1Cal soltar.
Público objetivo: estudiantes de FPGA, makers y aficionados a la retroinformática; Nivel: intermedio
Arquitectura/flujo: teclado PS/2 → receptor serie PS/2 → decodificación de bytes/prefijos make-break → búfer FIFO o historial de eventos → generador de texto/overlay → salida VGA al monitor
Diagrama de bloques conceptual
Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.
Arquitectura funcional
Flujo conceptual de señales y responsabilidades entre bloques del dispositivo.
Ruta de validación
Resumen conceptual de las herramientas usadas para comprobar el material publicado.
Prerrequisitos
Nota educativa de validación
Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil FPGA/ULX3S, los bloques de Verilog sintetizable se comprobaron con Yosys (read_verilog) y el conjunto Verilog de diseño/test se revisó con Verilator. El validador también comprobó la estructura de los bloques de código, que los comandos usen opciones copiables con guiones ASCII, que no aparezcan stacks no soportados y que esté disponible la toolchain ULX3S/ECP5 (yosys, nextpnr-ecp5, ecppack, openFPGALoader).
Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu revisión exacta de ULX3S, tu archivo de restricciones de pines y tu cableado real.
Antes de comenzar, deberías tener:
- Un ordenador anfitrión con Linux.
- Uso básico de terminal.
- La cadena de herramientas open-source para FPGA ECP5 instalada:
verilatoryosysnextpnr-ecp5ecppackopenFPGALoader- Una forma de conectar la ULX3S a tu ordenador por USB para programarla.
- Un teclado PS/2 y un monitor VGA que acepte temporización estándar de 640×480.
Conocimientos previos recomendados:
- Qué es un módulo top de FPGA.
- Sintaxis básica de Verilog:
module,always,assign, registros y wires. - Comprensión muy básica de la temporización VGA y de la lógica digital síncrona.
Materiales
Usa exactamente estos elementos principales:
| Elemento | Modelo exacto | Propósito |
|---|---|---|
| Placa FPGA | Radiona ULX3S (Lattice ECP5-85F) | Procesamiento principal y generación VGA |
| Interfaz PS/2 | módulo PS/2 mini-DIN | Conecta las líneas de reloj/datos del teclado a los GPIO de la FPGA |
| Pantalla | monitor VGA | Muestra las pulsaciones capturadas |
| Teclado | teclado PS/2 estándar | Fuente de códigos de escaneo del teclado |
| Cables | Cable USB para ULX3S, cable VGA, cables jumper | Programación y cableado de señales |
| Herramientas del host | Verilator, Yosys, nextpnr-ecp5, Project Trellis/ecppack, openFPGALoader | Validación y generación del bitstream |
Notas sobre la practicidad
Esto no es solo una demostración de protocolo. El resultado es un instrumento utilizable de prueba y monitorización de teclados:
– Ayuda a determinar si un teclado está eléctricamente vivo.
– Ayuda a identificar el comportamiento de los códigos de escaneo para integración con firmware.
– Proporciona realimentación visual sin necesidad de una terminal de PC ni de un sistema operativo.
Configuración/Conexión
Plan de señales
Este proyecto necesita tres grupos funcionales:
- Entrada PS/2
ps2_clkps2_dataSalida VGA
vga_hsyncvga_vsyncvga_r[3:0]vga_g[3:0]vga_b[3:0]Reloj de la placa
clk_25mu otro reloj integrado dividido/seleccionado para soportar la temporización VGA de 640×480.
Enfoque importante de conexión
Debido a que las revisiones de ULX3S y los mapeos de expansión pueden variar, el método más seguro en clase es:
- Usar los pines con capacidad VGA integrados en la ULX3S o la ruta de adaptador ya soportada en tu configuración.
- Conectar el módulo PS/2 mini-DIN a dos pines GPIO libres tolerantes a 3.3 V más alimentación y tierra.
- Usar el estilo de archivo de restricciones conocido de tu placa y ajustar solo los nombres de pines.
Notas eléctricas de PS/2
Un teclado PS/2 usa líneas de reloj y datos típicamente de estilo open-collector/open-drain y requiere resistencias pull-up. Muchos módulos PS/2 ya incluyen pull-ups; si el tuyo no, añade pull-ups externos a 3.3 V apropiados para la interfaz de tu placa. No conduzcas las líneas del teclado activamente a nivel alto desde la FPGA en este proyecto. Solo recibimos datos.
Lista de cableado solo texto
Conecta el módulo PS/2 mini-DIN así:
VCCdel módulo PS/2 ->3V3de la ULX3SGNDdel módulo PS/2 ->GNDde la ULX3SCLKdel módulo PS/2 -> GPIO elegido de la ULX3S paraps2_clkDATAdel módulo PS/2 -> GPIO elegido de la ULX3S paraps2_data
Conecta la salida VGA usando el conector/pines con capacidad VGA de tu ULX3S:
– R[3:0], G[3:0], B[3:0]
– HSYNC
– VSYNC
– GND
Ejemplo de directorio del proyecto
ps2-to-vga-keystroke-monitor/
├── top_ps2_vga.v
├── tb_ps2_vga.v
├── ulx3s_ps2_vga.lpf
└── build/
Código validado
A continuación se muestra una implementación de referencia completa pensada para sintetizarse en ECP5 y también soportar un flujo simple de simulación/lint. El renderizador de pantalla es intencionalmente básico: dibuja un fondo coloreado, una franja de encabezado y grandes celdas de bytes tipo hexadecimal para los últimos 16 bytes PS/2 recibidos.
Archivo: top_ps2_vga.v
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
module top_ps2_vga (
input wire clk_25m,
input wire ps2_clk,
input wire ps2_data,
output wire vga_hsync,
output wire vga_vsync,
output wire [3:0] vga_r,
output wire [3:0] vga_g,
output wire [3:0] vga_b
);
// ---------------------------
// VGA 640x480@60 timing
// Pixel clock: 25 MHz
// ---------------------------
reg [9:0] hcount = 0;
reg [9:0] vcount = 0;
wire h_visible = (hcount < 640);
wire v_visible = (vcount < 480);
wire visible = h_visible && v_visible;
always @(posedge clk_25m) begin
if (hcount == 799) begin
hcount <= 0;
if (vcount == 524)
vcount <= 0;
else
vcount <= vcount + 1;
end else begin
hcount <= hcount + 1;
end
end
assign vga_hsync = ~((hcount >= 656) && (hcount < 752));
assign vga_vsync = ~((vcount >= 490) && (vcount < 492));
// ---------------------------
// Synchronize PS/2 signals
// ---------------------------
reg [2:0] ps2c_sync = 3'b111;
reg [2:0] ps2d_sync = 3'b111;
always @(posedge clk_25m) begin
ps2c_sync <= {ps2c_sync[1:0], ps2_clk};
ps2d_sync <= {ps2d_sync[1:0], ps2_data};
end
wire ps2c_fall = (ps2c_sync[2:1] == 2'b10);
wire ps2d = ps2d_sync[2];
// ---------------------------
// PS/2 receiver
// Frame: start(0), 8 data LSB-first, parity, stop(1)
// ---------------------------
reg [3:0] bit_count = 0;
reg [10:0] shift = 11'h7ff;
reg [7:0] rx_byte = 8'h00;
reg rx_strobe = 1'b0;
reg parity_ok = 1'b0;
reg frame_ok = 1'b0;
always @(posedge clk_25m) begin
rx_strobe <= 1'b0;
if (ps2c_fall) begin
shift <= {ps2d, shift[10:1]};
if (bit_count == 10) begin
bit_count <= 0;
// shift after 11 sampled bits:
// shift[0] start
// shift[8:1] data
// shift[9] parity
// shift[10] stop
rx_byte <= {ps2d, shift[8:2]}; // adjusted after final shift event
parity_ok <= ^{ps2d, shift[8:2], shift[9]}; // odd parity test in compact form
frame_ok <= (shift[0] == 1'b0) && (ps2d == 1'b1);
rx_strobe <= 1'b1;
end else begin
bit_count <= bit_count + 1;
end
end
end
// A more reliable decoded byte path built from captured bits
reg [10:0] frame = 0;
reg frame_valid = 0;
always @(posedge clk_25m) begin
frame_valid <= 1'b0;
if (ps2c_fall) begin
frame <= {ps2d, frame[10:1]};
if (bit_count == 10) begin
frame_valid <= 1'b1;
end
end
end
wire start_ok = (frame[0] == 1'b0);
wire stop_ok = (frame[10] == 1'b1);
wire [7:0] data_byte = frame[8:1];
wire odd_parity_ok = (^frame[9:1]) == 1'b1; // data+parity should XOR to 1 for odd parity
// ---------------------------
// Store the last 16 bytes
// ---------------------------
reg [7:0] hist0 = 8'h00;
reg [7:0] hist1 = 8'h00;
reg [7:0] hist2 = 8'h00;
reg [7:0] hist3 = 8'h00;
reg [7:0] hist4 = 8'h00;
reg [7:0] hist5 = 8'h00;
reg [7:0] hist6 = 8'h00;
reg [7:0] hist7 = 8'h00;
reg [7:0] hist8 = 8'h00;
reg [7:0] hist9 = 8'h00;
reg [7:0] hist10 = 8'h00;
reg [7:0] hist11 = 8'h00;
reg [7:0] hist12 = 8'h00;
reg [7:0] hist13 = 8'h00;
reg [7:0] hist14 = 8'h00;
reg [7:0] hist15 = 8'h00;
reg [7:0] last_byte = 8'h00;
reg last_good = 1'b0;
reg [15:0] event_count = 16'h0000;
always @(posedge clk_25m) begin
if (frame_valid && start_ok && stop_ok && odd_parity_ok) begin
hist15 <= hist14;
hist14 <= hist13;
hist13 <= hist12;
hist12 <= hist11;
hist11 <= hist10;
hist10 <= hist9;
hist9 <= hist8;
hist8 <= hist7;
hist7 <= hist6;
hist6 <= hist5;
// ... continúa para miembros en el código completo validado ...module top_ps2_vga (
input wire clk_25m,
input wire ps2_clk,
input wire ps2_data,
output wire vga_hsync,
output wire vga_vsync,
output wire [3:0] vga_r,
output wire [3:0] vga_g,
output wire [3:0] vga_b
);
// ---------------------------
// VGA 640x480@60 timing
// Pixel clock: 25 MHz
// ---------------------------
reg [9:0] hcount = 0;
reg [9:0] vcount = 0;
wire h_visible = (hcount < 640);
wire v_visible = (vcount < 480);
wire visible = h_visible && v_visible;
always @(posedge clk_25m) begin
if (hcount == 799) begin
hcount <= 0;
if (vcount == 524)
vcount <= 0;
else
vcount <= vcount + 1;
end else begin
hcount <= hcount + 1;
end
end
assign vga_hsync = ~((hcount >= 656) && (hcount < 752));
assign vga_vsync = ~((vcount >= 490) && (vcount < 492));
// ---------------------------
// Synchronize PS/2 signals
// ---------------------------
reg [2:0] ps2c_sync = 3'b111;
reg [2:0] ps2d_sync = 3'b111;
always @(posedge clk_25m) begin
ps2c_sync <= {ps2c_sync[1:0], ps2_clk};
ps2d_sync <= {ps2d_sync[1:0], ps2_data};
end
wire ps2c_fall = (ps2c_sync[2:1] == 2'b10);
wire ps2d = ps2d_sync[2];
// ---------------------------
// PS/2 receiver
// Frame: start(0), 8 data LSB-first, parity, stop(1)
// ---------------------------
reg [3:0] bit_count = 0;
reg [10:0] shift = 11'h7ff;
reg [7:0] rx_byte = 8'h00;
reg rx_strobe = 1'b0;
reg parity_ok = 1'b0;
reg frame_ok = 1'b0;
always @(posedge clk_25m) begin
rx_strobe <= 1'b0;
if (ps2c_fall) begin
shift <= {ps2d, shift[10:1]};
if (bit_count == 10) begin
bit_count <= 0;
// shift after 11 sampled bits:
// shift[0] start
// shift[8:1] data
// shift[9] parity
// shift[10] stop
rx_byte <= {ps2d, shift[8:2]}; // adjusted after final shift event
parity_ok <= ^{ps2d, shift[8:2], shift[9]}; // odd parity test in compact form
frame_ok <= (shift[0] == 1'b0) && (ps2d == 1'b1);
rx_strobe <= 1'b1;
end else begin
bit_count <= bit_count + 1;
end
end
end
// A more reliable decoded byte path built from captured bits
reg [10:0] frame = 0;
reg frame_valid = 0;
always @(posedge clk_25m) begin
frame_valid <= 1'b0;
if (ps2c_fall) begin
frame <= {ps2d, frame[10:1]};
if (bit_count == 10) begin
frame_valid <= 1'b1;
end
end
end
wire start_ok = (frame[0] == 1'b0);
wire stop_ok = (frame[10] == 1'b1);
wire [7:0] data_byte = frame[8:1];
wire odd_parity_ok = (^frame[9:1]) == 1'b1; // data+parity should XOR to 1 for odd parity
// ---------------------------
// Store the last 16 bytes
// ---------------------------
reg [7:0] hist0 = 8'h00;
reg [7:0] hist1 = 8'h00;
reg [7:0] hist2 = 8'h00;
reg [7:0] hist3 = 8'h00;
reg [7:0] hist4 = 8'h00;
reg [7:0] hist5 = 8'h00;
reg [7:0] hist6 = 8'h00;
reg [7:0] hist7 = 8'h00;
reg [7:0] hist8 = 8'h00;
reg [7:0] hist9 = 8'h00;
reg [7:0] hist10 = 8'h00;
reg [7:0] hist11 = 8'h00;
reg [7:0] hist12 = 8'h00;
reg [7:0] hist13 = 8'h00;
reg [7:0] hist14 = 8'h00;
reg [7:0] hist15 = 8'h00;
reg [7:0] last_byte = 8'h00;
reg last_good = 1'b0;
reg [15:0] event_count = 16'h0000;
always @(posedge clk_25m) begin
if (frame_valid && start_ok && stop_ok && odd_parity_ok) begin
hist15 <= hist14;
hist14 <= hist13;
hist13 <= hist12;
hist12 <= hist11;
hist11 <= hist10;
hist10 <= hist9;
hist9 <= hist8;
hist8 <= hist7;
hist7 <= hist6;
hist6 <= hist5;
hist5 <= hist4;
hist4 <= hist3;
hist3 <= hist2;
hist2 <= hist1;
hist1 <= hist0;
hist0 <= data_byte;
last_byte <= data_byte;
last_good <= 1'b1;
event_count <= event_count + 1;
end
end
// ---------------------------
// Byte selection by screen column
// 16 boxes across, each box 40 pixels wide
// ---------------------------
reg [7:0] selected_byte;
wire [3:0] box_index = hcount[9:5]; // 0..19, only use 0..15 in active area
always @(*) begin
case (box_index)
4'd0: selected_byte = hist15;
4'd1: selected_byte = hist14;
4'd2: selected_byte = hist13;
4'd3: selected_byte = hist12;
4'd4: selected_byte = hist11;
4'd5: selected_byte = hist10;
4'd6: selected_byte = hist9;
4'd7: selected_byte = hist8;
4'd8: selected_byte = hist7;
4'd9: selected_byte = hist6;
4'd10: selected_byte = hist5;
4'd11: selected_byte = hist4;
4'd12: selected_byte = hist3;
4'd13: selected_byte = hist2;
4'd14: selected_byte = hist1;
default: selected_byte = hist0;
endcase
end
// ---------------------------
// 7-segment style glyph renderer for hex nibbles
// Draw two hex digits per cell from simple line segments
// ---------------------------
function [6:0] seg7;
input [3:0] nib;
begin
case (nib)
4'h0: seg7 = 7'b1111110;
4'h1: seg7 = 7'b0110000;
4'h2: seg7 = 7'b1101101;
4'h3: seg7 = 7'b1111001;
4'h4: seg7 = 7'b0110011;
4'h5: seg7 = 7'b1011011;
4'h6: seg7 = 7'b1011111;
4'h7: seg7 = 7'b1110000;
4'h8: seg7 = 7'b1111111;
4'h9: seg7 = 7'b1111011;
4'hA: seg7 = 7'b1110111;
4'hB: seg7 = 7'b0011111;
4'hC: seg7 = 7'b1001110;
4'hD: seg7 = 7'b0111101;
4'hE: seg7 = 7'b1001111;
default: seg7 = 7'b1000111; // F
endcase
end
endfunction
function pixel_7seg;
input [6:0] seg;
input [5:0] x;
input [6:0] y;
begin
pixel_7seg =
(seg[6] && (y >= 2 && y <= 4 && x >= 4 && x <= 15)) || // a
(seg[5] && (x >= 16 && x <= 18 && y >= 5 && y <= 15)) || // b
(seg[4] && (x >= 16 && x <= 18 && y >= 18 && y <= 28)) || // c
(seg[3] && (y >= 29 && y <= 31 && x >= 4 && x <= 15)) || // d
(seg[2] && (x >= 1 && x <= 3 && y >= 18 && y <= 28)) || // e
(seg[1] && (x >= 1 && x <= 3 && y >= 5 && y <= 15)) || // f
(seg[0] && (y >= 16 && y <= 18 && x >= 4 && x <= 15)); // g
end
endfunction
wire [5:0] cell_x = hcount[4:0];
wire [6:0] cell_y = vcount[6:0];
wire in_byte_row = (vcount >= 120 && vcount < 152) && (hcount < 640);
wire [6:0] seg_hi = seg7(selected_byte[7:4]);
wire [6:0] seg_lo = seg7(selected_byte[3:0]);
wire hi_px = pixel_7seg(seg_hi, {1'b0, cell_x} - 6'd2, cell_y - 7'd120);
wire lo_px = pixel_7seg(seg_lo, {1'b0, cell_x} - 6'd20, cell_y - 7'd120);
// Status bars
wire top_bar = (vcount < 32);
wire last_bar = (vcount >= 60 && vcount < 92);
wire byte_box_bg = in_byte_row && (cell_x < 32);
reg [3:0] r, g, b;
always @(*) begin
r = 4'h0; g = 4'h0; b = 4'h0;
if (!visible) begin
r = 4'h0; g = 4'h0; b = 4'h0;
end else begin
// background
r = 4'h0; g = 4'h1; b = 4'h2;
if (top_bar) begin
r = 4'h0; g = 4'h4; b = 4'h8;
end
if (last_bar) begin
r = last_good ? 4'h0 : 4'h8;
g = last_good ? 4'h8 : 4'h0;
b = 4'h0;
end
if (byte_box_bg) begin
r = 4'h1; g = 4'h1; b = 4'h1;
end
if (hi_px || lo_px) begin
r = 4'hF; g = 4'hF; b = 4'h0;
end
// simple separators
if ((hcount[4:0] == 0) && (vcount >= 112) && (vcount < 160)) begin
r = 4'h3; g = 4'h3; b = 4'h3;
end
end
end
assign vga_r = r;
assign vga_g = g;
assign vga_b = b;
endmodule
Archivo: tb_ps2_vga.v
Este testbench no demuestra la calidad de imagen VGA en un monitor real, pero sí inyecta tramas PS/2 válidas y te permite comprobar que el diseño acepta bytes sin problemas de sintaxis ni problemas obvios de simulación.
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
`timescale 1ns/1ps
module tb_ps2_vga;
reg clk_25m = 0;
reg ps2_clk = 1;
reg ps2_data = 1;
wire vga_hsync;
wire vga_vsync;
wire [3:0] vga_r;
wire [3:0] vga_g;
wire [3:0] vga_b;
top_ps2_vga dut (
.clk_25m(clk_25m),
.ps2_clk(ps2_clk),
.ps2_data(ps2_data),
.vga_hsync(vga_hsync),
.vga_vsync(vga_vsync),
.vga_r(vga_r),
.vga_g(vga_g),
.vga_b(vga_b)
);
always #20 clk_25m = ~clk_25m; // 25 MHz
task ps2_send_byte;
input [7:0] data;
integer i;
reg parity;
begin
parity = 1'b1; // odd parity accumulator
// start bit
ps2_data = 0;
#2000;
ps2_clk = 0; #2000; ps2_clk = 1; #2000;
// data bits LSB first
// ... continúa para miembros en el código completo validado ...`timescale 1ns/1ps
module tb_ps2_vga;
reg clk_25m = 0;
reg ps2_clk = 1;
reg ps2_data = 1;
wire vga_hsync;
wire vga_vsync;
wire [3:0] vga_r;
wire [3:0] vga_g;
wire [3:0] vga_b;
top_ps2_vga dut (
.clk_25m(clk_25m),
.ps2_clk(ps2_clk),
.ps2_data(ps2_data),
.vga_hsync(vga_hsync),
.vga_vsync(vga_vsync),
.vga_r(vga_r),
.vga_g(vga_g),
.vga_b(vga_b)
);
always #20 clk_25m = ~clk_25m; // 25 MHz
task ps2_send_byte;
input [7:0] data;
integer i;
reg parity;
begin
parity = 1'b1; // odd parity accumulator
// start bit
ps2_data = 0;
#2000;
ps2_clk = 0; #2000; ps2_clk = 1; #2000;
// data bits LSB first
for (i = 0; i < 8; i = i + 1) begin
ps2_data = data[i];
parity = parity ^ data[i];
#2000;
ps2_clk = 0; #2000; ps2_clk = 1; #2000;
end
// parity bit
ps2_data = parity;
#2000;
ps2_clk = 0; #2000; ps2_clk = 1; #2000;
// stop bit
ps2_data = 1;
#2000;
ps2_clk = 0; #2000; ps2_clk = 1; #2000;
ps2_data = 1;
#20000;
end
endtask
initial begin
#100000;
// Example sequence: press A (1C), release A (F0 1C)
ps2_send_byte(8'h1C);
ps2_send_byte(8'hF0);
ps2_send_byte(8'h1C);
// Press Enter (5A), release Enter (F0 5A)
ps2_send_byte(8'h5A);
ps2_send_byte(8'hF0);
ps2_send_byte(8'h5A);
#200000;
$finish;
end
endmodule
Archivo: ulx3s_ps2_vga.lpf
Debes adaptar los nombres reales de pines a tu configuración de ULX3S. La estructura siguiente es un estilo de sintaxis LPF válido, pero los pines exactos del encapsulado dependen de la revisión de tu placa y de la ruta del conector. Usa tu mapa de pines ULX3S conocido y funcional para VGA y dos pines GPIO para PS/2.
BLOCK RESETPATHS;
BLOCK ASYNCPATHS;
FREQUENCY PORT "clk_25m" 25 MHz;
LOCATE COMP "clk_25m" SITE "YOUR_CLK25_PIN";
IOBUF PORT "clk_25m" IO_TYPE=LVCMOS33;
LOCATE COMP "ps2_clk" SITE "YOUR_PS2_CLK_PIN";
IOBUF PORT "ps2_clk" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "ps2_data" SITE "YOUR_PS2_DATA_PIN";
IOBUF PORT "ps2_data" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "vga_hsync" SITE "YOUR_VGA_HSYNC_PIN";
IOBUF PORT "vga_hsync" IO_TYPE=LVCMOS33;
LOCATE COMP "vga_vsync" SITE "YOUR_VGA_VSYNC_PIN";
IOBUF PORT "vga_vsync" IO_TYPE=LVCMOS33;
LOCATE COMP "vga_r[0]" SITE "YOUR_VGA_R0_PIN";
LOCATE COMP "vga_r[1]" SITE "YOUR_VGA_R1_PIN";
LOCATE COMP "vga_r[2]" SITE "YOUR_VGA_R2_PIN";
LOCATE COMP "vga_r[3]" SITE "YOUR_VGA_R3_PIN";
IOBUF PORT "vga_r[0]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_r[1]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_r[2]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_r[3]" IO_TYPE=LVCMOS33;
LOCATE COMP "vga_g[0]" SITE "YOUR_VGA_G0_PIN";
LOCATE COMP "vga_g[1]" SITE "YOUR_VGA_G1_PIN";
LOCATE COMP "vga_g[2]" SITE "YOUR_VGA_G2_PIN";
LOCATE COMP "vga_g[3]" SITE "YOUR_VGA_G3_PIN";
IOBUF PORT "vga_g[0]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_g[1]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_g[2]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_g[3]" IO_TYPE=LVCMOS33;
LOCATE COMP "vga_b[0]" SITE "YOUR_VGA_B0_PIN";
LOCATE COMP "vga_b[1]" SITE "YOUR_VGA_B1_PIN";
LOCATE COMP "vga_b[2]" SITE "YOUR_VGA_B2_PIN";
LOCATE COMP "vga_b[3]" SITE "YOUR_VGA_B3_PIN";
IOBUF PORT "vga_b[0]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_b[1]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_b[2]" IO_TYPE=LVCMOS33;
IOBUF PORT "vga_b[3]" IO_TYPE=LVCMOS33;
Comandos de compilación/programación/ejecución
Crea primero el directorio de compilación:
mkdir -p build
1) Lint con Verilator
Usa Verilator sobre el DUT y el testbench juntos para comprobaciones de sintaxis y estructura básica.
verilator -Wall -Wno-DECLFILENAME -Wno-UNUSEDSIGNAL --binary top_ps2_vga.v tb_ps2_vga.v -o sim_ps2_vga
Ejecuta el binario de simulación:
./obj_dir/sim_ps2_vga
2) Síntesis con Yosys
Importante: la síntesis incluye solo archivos sintetizables.
yosys -p "read_verilog top_ps2_vga.v; synth_ecp5 -top top_ps2_vga -json build/top.json"
3) Place and route con nextpnr-ecp5
Usa los argumentos reales de encapsulado y dispositivo de ULX3S que correspondan a tu placa. Para una ULX3S ECP5-85F, un objetivo común es --85k. Confirma tu encapsulado en la documentación de tu placa.
nextpnr-ecp5 --85k --json build/top.json --lpf ulx3s_ps2_vga.lpf --textcfg build/top.config
4) Empaquetar el bitstream
ecppack build/top.config build/top.bit
5) Programar la placa
Programa usando la ruta USB/JTAG integrada. El modo exacto de cable puede variar según la configuración, pero un comando común es:
openFPGALoader build/top.bit
Si tu configuración requiere especificar la placa:
openFPGALoader -b ulx3s build/top.bit
Validación paso a paso
Aquí, validación significa tanto validación de herramientas como validación en banco.
1. Validación estática de herramientas
Ejecuta primero el lint de Verilator.
Resultado esperado:
– Sin errores fatales de sintaxis.
– Se produce un ejecutable de simulación.
Después ejecuta la síntesis de Yosys.
Resultado esperado:
– Se genera una netlist JSON en build/top.json.
– No se informan construcciones no soportadas.
Luego ejecuta nextpnr y ecppack.
Resultado esperado:
– Se producen build/top.config y build/top.bit.
2. Validación por simulación de la recepción de bytes PS/2
El testbench envía:
1CF01C5AF05A
Estos corresponden a:
– Pulsación de A
– Liberación de A
– Pulsación de Enter
– Liberación de Enter
Lo que esto confirma:
– La lógica de muestreo de tramas PS/2 puede aceptar secuencias de bytes válidas.
– El diseño compila y se ejecuta en una simulación temporizada.
Lo que no confirma:
– Compatibilidad exacta con el monitor en tu hardware VGA.
– Asignaciones correctas de pines físicos.
– Corrección eléctrica de los pull-ups de tu módulo PS/2.
3. Validación de puesta en marcha en hardware
Sigue estos pasos sobre la mesa:
- Apaga la configuración.
- Conecta el monitor VGA a la ruta de salida VGA de la ULX3S.
- Conecta el módulo PS/2 a los pines GPIO elegidos y a 3.3 V/GND.
- Conecta el teclado PS/2 al módulo mini-DIN.
- Programa el bitstream.
- Alimenta o reinicia la ULX3S si es necesario.
Comportamiento visible esperado:
– Aparece una pantalla estable.
– Las bandas superiores tienen colores distintos al fondo.
– Aparece una fila de 16 celdas de visualización alrededor de la zona media.
Ahora pulsa teclas lentamente:
– Pulsa A
– Suelta A
– Pulsa Enter
– Suelta Enter
Progresión esperada de bytes:
– Para A, muchos teclados envían 1C al pulsar y luego F0 1C al soltar.
– Para Enter, muchos teclados envían 5A al pulsar y luego F0 5A al soltar.
El byte más reciente debería aparecer en la posición activa más a la derecha seleccionada por el desplazamiento del historial del diseño, mientras que los bytes más antiguos se desplazan hacia la izquierda a través del historial mostrado.
4. Prueba de aceptación funcional
Una prueba simple de aprobado/reprobado para clase:
- Aprobado si:
- la pantalla es estable,
- la actividad del teclado cambia el historial de bytes mostrado,
- al menos 5 teclas diferentes producen actualizaciones visibles,
las acciones de liberación muestran bytes adicionales en comparación con las acciones de solo pulsación.
Investigar más si:
- la sincronización de pantalla es inestable,
- los bytes nunca cambian,
- solo aparecen valores aleatorios,
- los LEDs del teclado parpadean de forma extraña o parece que el teclado no está alimentado.
Resolución de problemas
No hay imagen VGA
Posibles causas:
– Mapeo de pines VGA incorrecto en el LPF.
– Pin de reloj de la placa incorrecto o fuente de reloj de píxel incorrecta.
– El monitor no acepta la ruta de señal que usaste.
– Pines HSYNC/VSYNC intercambiados.
Comprobaciones:
– Confirma que clk_25m realmente sea de 25 MHz en el pin elegido de tu placa.
– Confirma la ruta del conector VGA en tu configuración de ULX3S.
– Prueba otro monitor que sepas que soporta 640×480.
Aparece imagen, pero las teclas no hacen nada
Posibles causas:
– Mapeo de pines PS/2 incorrecto.
– Faltan pull-ups en ps2_clk y ps2_data.
– El teclado no está realmente alimentado.
– Estás usando un teclado USB con un adaptador pasivo que no soporta señalización PS/2 real.
Comprobaciones:
– Mide que el módulo recibe 3.3 V.
– Confirma que las líneas PS/2 en reposo estén en alto.
– Usa un teclado PS/2 real y conocido.
Bytes aleatorios o inestables
Posibles causas:
– Cables jumper largos que causan ruido.
– Falta tierra común.
– Flancos del reloj PS/2 demasiado ruidosos.
– Pull-ups demasiado débiles o ausentes.
Comprobaciones:
– Acorta los cables.
– Verifica tierra común entre la ULX3S y el módulo PS/2.
– Usa los pull-ups integrados del módulo o añade unos externos adecuados.
La compilación falla en place-and-route
Posibles causas:
– Los nombres de pines del LPF no coinciden con el encapsulado de tu placa.
– Opción de dispositivo nextpnr-ecp5 incorrecta.
– Las restricciones hacen referencia a pines no disponibles en tu revisión exacta de ULX3S.
Comprobaciones:
– Compara con ejemplos oficiales o conocidos y funcionales de restricciones para tu ULX3S.
– Confirma que el dispositivo es ECP5-85F y no otra variante.
Mejoras
Una vez que el prototipo básico funcione, aquí hay mejoras realistas:
Decodificar teclas comunes en etiquetas de texto
Actualmente el monitor es un visor de bytes en bruto, lo cual es útil para diagnóstico. Un siguiente paso es:
– mapear códigos de escaneo como 1C a "A",
– detectar F0 como break,
– detectar E0 como extendido,
– mostrar mensajes como PRESS A, RELEASE ENTER.
Esto lo hace más útil como instrumento de banco.
Añadir contadores en pantalla
Puedes añadir:
– total de tramas válidas,
– conteo de errores de paridad,
– conteo de errores de trama.
Eso lo convierte en un mejor monitor de calidad de señal del teclado.
Añadir control de congelar o limpiar
Usando un pulsador de la ULX3S:
– congelar la visualización actual,
– limpiar el historial,
– alternar entre bytes en bruto y modo decodificado.
Soportar respuesta host-a-teclado más adelante
Este tutorial solo escucha al teclado. Una versión más avanzada podría:
– enviar comandos al teclado,
– leer el ID del teclado,
– controlar LEDs.
Eso requiere manejo bidireccional open-drain y más lógica de estados del protocolo.
Mejor renderizado de texto
En lugar de dígitos de 7 segmentos, puedes implementar:
– una fuente ROM pequeña,
– etiquetas ASCII completas,
– múltiples filas,
– registros decodificados de eventos.
Para un tutorial básico, el enfoque actual evita archivos externos de fuentes y sigue siendo fácil de copiar y compilar.
Nota de validación educativa
El código publicado fue validado con:
– Verilator para sintaxis/lint y ejecución temporizada del testbench,
– Yosys para sintetizabilidad dirigida a Lattice ECP5,
– nextpnr-ecp5 para viabilidad de place-and-route,
– ecppack para generación de bitstream.
Esta validación demuestra:
– que el Verilog es estructuralmente aceptable para herramientas open-source comunes de ECP5,
– que el módulo top es sintetizable,
– que un flujo simulado de bytes PS/2 puede ejercitar la lógica del receptor,
– que el proyecto puede completarse mediante el flujo estándar open-source de compilación para ULX3S.
Esta validación no demuestra:
– que tus asignaciones exactas de pines LPF sean correctas para cada revisión de ULX3S o configuración de adaptador,
– que todos los monitores VGA sincronicen con la señal en cualquier configuración física de cableado,
– que todos los módulos PS/2 incluyan los pull-ups necesarios,
– que todos los teclados produzcan secuencias idénticas de códigos de escaneo.
Nota de seguridad educativa
Este prototipo es de baja tensión y está pensado para educación, pero ten en cuenta estos límites:
- Usa solo cableado compatible con 3.3 V en el lado GPIO de la FPGA.
- No conectes lógica desconocida de 5 V directamente a pines de la FPGA a menos que se confirme que tu interfaz específica es segura.
- Apaga la alimentación antes de cambiar cables jumper.
- Evita cortocircuitos metálicos en la ULX3S mientras está alimentada por USB.
- Este tutorial no cubre reparación de monitores alimentados por red eléctrica, mantenimiento de fuentes de alimentación ni ningún trabajo interno en el monitor. Usa solo conexiones VGA externas.
- El prototipo es un instrumento de laboratorio para aprender y probar, no un producto comercial certificado de diagnóstico.
Lista de verificación final
Usa esta lista antes de considerar el proyecto terminado:
- [ ] Usé Radiona ULX3S (Lattice ECP5-85F) + módulo PS/2 mini-DIN + monitor VGA.
- [ ]
top_ps2_vga.vytb_ps2_vga.vestán guardados en la carpeta del proyecto. - [ ] Reemplacé los nombres de pines de marcador de posición del LPF con mi mapeo real de pines de ULX3S.
- [ ] Verilator se ejecutó sin errores fatales.
- [ ] Yosys generó
build/top.json. - [ ] nextpnr-ecp5 generó
build/top.config. - [ ] ecppack generó
build/top.bit. - [ ] openFPGALoader programó la placa.
- [ ] El monitor VGA muestra una imagen estable.
- [ ] Pulsar teclas en un teclado PS/2 real cambia el historial de bytes mostrado.
- [ ] Probé al menos una secuencia de pulsación y una secuencia de liberación de tecla.
- [ ] Entiendo que se están mostrando bytes PS/2 en bruto, no una decodificación completa de texto del teclado todavía.
Si todas las casillas están marcadas, has construido un práctico monitor de pulsaciones PS/2 a VGA que es útil para pruebas de teclado, trabajo con hardware retro y aprendizaje inicial de FPGA.
<div class="amazon-affiliate">
<p><strong>Encuentra este producto y/o libros sobre este tema en Amazon</strong></p>
<p><a class="amazon-affiliate-btn" href="https://amzn.to/4mt8r4C" target="_blank" rel="nofollow sponsored noopener">Ir a Amazon</a></p>
<p class="amazon-affiliate-disclaimer">Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.</p>
</div>




