Objetivo y caso de uso
Qué construirás: Un prototipo de cerradura por PIN en FPGA con la Radiona ULX3S (ECP5-85F), usando un teclado PS/2 para introducir un código de 4 dígitos y un display TM1637 para mostrar estado, dígitos y mensajes breves. El sistema procesará pulsaciones con latencia visual típica <20 ms, refresco estable del display y uso de GPU no aplicable (0%).
Para qué sirve
- Simular un controlador básico de acceso para puerta, gabinete o compartimento con validación de PIN de 4 dígitos.
- Habilitar equipos de taller, como una fuente de laboratorio o un relé de banco, solo tras introducir una clave correcta.
- Practicar una HMI embebida real integrando entrada PS/2 y salida visual TM1637 en lugar de usar solo LEDs.
- Ejercitar diseño secuencial e integración digital: captura de scan codes, antirrebote lógico, máquina de estados y temporización.
Resultado esperado
- Ingreso de PIN numérico desde teclado PS/2 con respuesta por tecla en <20 ms.
- Visualización en 4 dígitos de números, máscara tipo **** o estados como OPEN/Err según la lógica implementada.
- Validación correcta/incorrecta del PIN y activación de una señal de desbloqueo para LED, relé o etapa externa.
- Funcionamiento fluido sin requisitos gráficos: 0 FPS / 0% GPU, con lógica hardware dedicada y temporización determinista.
Público objetivo: estudiantes de electrónica digital, FPGA y sistemas embebidos; Nivel: inicial–intermedio
Arquitectura/flujo: teclado PS/2 mini-DIN → decodificador de scan codes → filtro/registro de dígitos → máquina de estados de PIN → comparador con clave almacenada → salida de desbloqueo + controlador TM1637 de 4 dígitos.
Diagrama de bloques conceptual
Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.
Arquitectura funcional
Flujo conceptual de datos: entrada del usuario, lógica de decisión y salida visual/de desbloqueo.
Ruta de validación
La validación automática comprueba sintaxis, simulación/lint y compatibilidad con la toolchain ULX3S/ECP5.
Prerrequisitos
Antes de comenzar, deberías tener:
- Un PC con Linux o un entorno de shell similar.
- Familiaridad básica con:
- comandos de terminal
- copiar archivos
- conectar cables jumper
- la idea de un circuito digital secuencial
- Toolchain ECP5 de código abierto instalado:
verilatoryosysnextpnr-ecp5ecppackopenFPGALoader
Versiones recomendadas que comúnmente funcionan bien juntas:
- Verilator 5.x
- Yosys 0.3x o más reciente
- nextpnr-ecp5 con soporte para Project Trellis
- openFPGALoader en paquete estable reciente
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.
Nota educativa de seguridad
Este proyecto usa solo electrónica digital de bajo voltaje. Mantenlo así.
- No conectes este prototipo directamente a tensión de red, cerraduras alimentadas con alta corriente, motores o actuadores de puerta sin aislamiento adecuado y una etapa de control separada y revisada.
- El tutorial demuestra un prototipo lógico, no un sistema de cerradura certificado en seguridad.
- Los módulos PS/2 y TM1637 son interfaces de baja potencia; aun así, realiza todas las conexiones primero con la alimentación apagada.
Materiales
Usa exactamente estos dispositivos principales:
| Elemento | Modelo exacto | Cantidad | Propósito |
|---|---|---|---|
| Placa FPGA | Radiona ULX3S (Lattice ECP5-85F) | 1 | Controlador principal |
| Interfaz de teclado | módulo teclado PS/2 mini-DIN | 1 | Entrada del PIN del usuario |
| Módulo de display | display 7 segmentos TM1637 | 1 | Display de PIN/estado |
| Cables jumper | Hembra-hembra o mixtos según se necesite | 1 juego | Cableado |
| Cable USB | Compatible con ULX3S | 1 | Alimentación y programación |
| Opcional | Protoboard | 1 | Facilita la derivación de señales |
Sobre el comportamiento del prototipo
Este diseño implementa una cerradura por PIN de 4 dígitos con estas reglas:
- Se aceptan las teclas numéricas
0a9del teclado PS/2. - Cada dígito aceptado se desplaza dentro de un búfer de entrada de 4 dígitos.
- Después de introducir 4 dígitos:
- si coinciden con el PIN almacenado, el display muestra éxito y
unlockse activa - en caso contrario, el display muestra fallo
- Luego el sistema se limpia y espera el siguiente intento de PIN.
- Pulsar
Backspacelimpia inmediatamente la entrada actual.
Para un tutorial básico, el PIN está fijo en hardware en Verilog. Más adelante puedes extenderlo a almacenamiento configurable.
Configuración/Conexión
Aquí no se usa un diagrama del circuito; sigue cuidadosamente las instrucciones de cableado en texto.
1) Elegir pines de E/S de la ULX3S
La ULX3S expone muchos pines de E/S de la FPGA en conectores de cabecera, pero las revisiones de placa y el uso del breakout varían. Por eso, debes mapear los pines físicos exactos de cabecera que realmente uses en tu archivo de restricciones.
Para este tutorial, definiremos cuatro señales de nivel superior para módulos externos:
ps2_clkps2_datatm_clktm_dio
Y una salida opcional:
unlock_led
Las conectarás a GPIO libres compatibles con 3.3 V en la cabecera de tu ULX3S y luego asignarás esos pines de encapsulado en el archivo de restricciones.
2) Compatibilidad de alimentación
Comprueba tus módulos específicos:
- Muchos módulos TM1637 de 4 dígitos funcionan con 3.3 V a 5 V y a menudo aceptan lógica de 3.3 V.
- Muchos módulos de teclado PS/2 también funcionan a 5 V y pueden exponer resistencias pull-up a 5 V.
Para la seguridad de la FPGA:
- Prefiere usar versiones de módulos que puedan funcionar con 3.3 V.
- Si tu módulo PS/2 o el lado del teclado elevan clock/data a 5 V, no los conectes directamente a los pines de la FPGA de la ULX3S. Usa adaptación de nivel adecuada o verifica que la salida del módulo sea segura para 3.3 V.
- El camino más simple y seguro para principiantes es:
- alimentar el módulo TM1637 desde
3V3 - alimentar el módulo PS/2 desde
3V3si tu módulo lo soporta - masa común entre todos los dispositivos
3) Guía de cableado en texto
Conecta de la siguiente manera:
- Alimentación común
- ULX3S
GND->GNDdel módulo PS/2 - ULX3S
GND->GNDdel TM1637 - ULX3S
3V3->VCCdel módulo PS/2 si tu módulo lo soporta ULX3S
3V3->VCCdel TM1637Interfaz PS/2
- GPIO elegido de la ULX3S ->
CLKdel módulo PS/2 GPIO elegido de la ULX3S ->
DATAdel módulo PS/2Interfaz TM1637
- GPIO elegido de la ULX3S ->
CLKdel TM1637 GPIO elegido de la ULX3S ->
DIOdel TM1637Indicador opcional de desbloqueo
- GPIO de la ULX3S con capacidad para LED integrado o señal de LED onboard -> salida lógica
unlock_led - Si usas un LED externo, añade una resistencia y verifica los límites de tensión/corriente
4) Preparación del archivo de restricciones
Debes reemplazar los valores PACKAGE_PIN de abajo por los pines reales de la FPGA ULX3S correspondientes a los pines de cabecera que hayas cableado.
Crea un archivo llamado ulx3s_ps2_pin_lock.lpf:
BLOCK RESETPATHS;
BLOCK ASYNCPATHS;
LOCATE COMP "clk_25mhz" SITE "PCLK25";
IOBUF PORT "clk_25mhz" IO_TYPE=LVCMOS33;
LOCATE COMP "ps2_clk" SITE "A1";
IOBUF PORT "ps2_clk" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "ps2_data" SITE "B1";
IOBUF PORT "ps2_data" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "tm_clk" SITE "C1";
IOBUF PORT "tm_clk" IO_TYPE=LVCMOS33;
LOCATE COMP "tm_dio" SITE "D1";
IOBUF PORT "tm_dio" IO_TYPE=LVCMOS33;
LOCATE COMP "unlock_led" SITE "E1";
IOBUF PORT "unlock_led" IO_TYPE=LVCMOS33;
Nota importante sobre las restricciones
Las entradas A1/B1/C1/... anteriores son solo ejemplos para la estructura del archivo. No se garantiza que coincidan con la cabecera de tu placa. Debes consultar el pinout de la ULX3S para la revisión de tu placa y reemplazarlas con sitios reales del encapsulado. El código lógico de abajo está completo; solo la asignación física de pines depende de tu cableado exacto.
Código validado
Crea una carpeta de proyecto:
mkdir -p ps2-pin-lock-display
cd ps2-pin-lock-display
1) Diseño sintetizable: ps2_pin_lock_top.v
Este archivo contiene:
- divisor de reloj para temporización
- receptor PS/2
- conversión de scan-code a dígito
- verificador de PIN de 4 dígitos
- driver de display TM1637
- integración de nivel superior
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
module ps2_receiver (
input wire clk,
input wire rst,
input wire ps2_clk,
input wire ps2_data,
output reg [7:0] scan_code,
output reg scan_strobe
);
reg [2:0] ps2c_sync;
reg [10:0] shift;
reg [3:0] bit_count;
always @(posedge clk) begin
if (rst) begin
ps2c_sync <= 3'b111;
shift <= 11'd0;
bit_count <= 4'd0;
scan_code <= 8'd0;
scan_strobe <= 1'b0;
end else begin
ps2c_sync <= {ps2c_sync[1:0], ps2_clk};
scan_strobe <= 1'b0;
if (ps2c_sync[2:1] == 2'b10) begin
shift <= {ps2_data, shift[10:1]};
if (bit_count == 4'd10) begin
bit_count <= 4'd0;
scan_code <= shift[8:1];
scan_strobe <= 1'b1;
end else begin
bit_count <= bit_count + 4'd1;
end
end
end
end
endmodule
module key_decode (
input wire clk,
input wire rst,
input wire [7:0] scan_code,
input wire scan_strobe,
output reg [3:0] digit,
output reg digit_valid,
output reg clear_key
);
reg break_code;
always @(posedge clk) begin
if (rst) begin
break_code <= 1'b0;
digit <= 4'd0;
digit_valid <= 1'b0;
clear_key <= 1'b0;
end else begin
digit_valid <= 1'b0;
clear_key <= 1'b0;
if (scan_strobe) begin
if (scan_code == 8'hF0) begin
break_code <= 1'b1;
end else begin
if (!break_code) begin
case (scan_code)
8'h45: begin digit <= 4'd0; digit_valid <= 1'b1; end
8'h16: begin digit <= 4'd1; digit_valid <= 1'b1; end
8'h1E: begin digit <= 4'd2; digit_valid <= 1'b1; end
8'h26: begin digit <= 4'd3; digit_valid <= 1'b1; end
8'h25: begin digit <= 4'd4; digit_valid <= 1'b1; end
8'h2E: begin digit <= 4'd5; digit_valid <= 1'b1; end
8'h36: begin digit <= 4'd6; digit_valid <= 1'b1; end
8'h3D: begin digit <= 4'd7; digit_valid <= 1'b1; end
8'h3E: begin digit <= 4'd8; digit_valid <= 1'b1; end
8'h46: begin digit <= 4'd9; digit_valid <= 1'b1; end
8'h66: begin clear_key <= 1'b1; end // Backspace
default: begin end
endcase
end
break_code <= 1'b0;
end
end
end
end
endmodule
module tm1637_driver (
input wire clk,
input wire rst,
input wire [7:0] d3,
input wire [7:0] d2,
input wire [7:0] d1,
input wire [7:0] d0,
output reg tm_clk,
output reg tm_dio
);
reg [15:0] divcnt;
reg tick;
reg [7:0] frame [0:5];
reg [6:0] state;
reg [3:0] bitn;
reg [7:0] curbyte;
reg [2:0] phase;
always @(posedge clk) begin
if (rst) begin
divcnt <= 16'd0;
tick <= 1'b0;
end else begin
if (divcnt == 16'd249) begin
divcnt <= 16'd0;
tick <= 1'b1;
end else begin
divcnt <= divcnt + 16'd1;
tick <= 1'b0;
end
end
end
always @(posedge clk) begin
if (rst) begin
frame[0] <= 8'h40;
frame[1] <= 8'hC0;
frame[2] <= 8'h00;
frame[3] <= 8'h00;
frame[4] <= 8'h00;
frame[5] <= 8'h00;
state <= 7'd0;
bitn <= 4'd0;
curbyte <= 8'd0;
phase <= 3'd0;
tm_clk <= 1'b1;
tm_dio <= 1'b1;
end else begin
frame[0] <= 8'h40;
frame[1] <= 8'hC0;
frame[2] <= d0;
frame[3] <= d1;
frame[4] <= d2;
frame[5] <= d3;
if (tick) begin
case (state)
7'd0: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd1; end
7'd1: begin tm_dio <= 1'b0; state <= 7'd2; bitn <= 4'd0; curbyte <= frame[0]; phase <= 3'd0; end
7'd2,7'd3,7'd4,7'd5,7'd6,7'd7: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= state + 7'd1;
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd8: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd9; end
7'd9: begin tm_clk <= 1'b1; state <= 7'd10; end
7'd10: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd11; end
7'd11: begin tm_dio <= 1'b1; state <= 7'd12; end
7'd12: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd13; end
7'd13: begin tm_dio <= 1'b0; state <= 7'd14; bitn <= 4'd0; curbyte <= frame[1]; phase <= 3'd0; end
7'd14,7'd15,7'd16,7'd17,7'd18: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= state + 7'd1;
if (state == 7'd18) curbyte <= frame[2];
else curbyte <= frame[state - 7'd15 + 3];
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd19: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd20; end
7'd20: begin tm_clk <= 1'b1; state <= 7'd21; end
7'd21: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd22; end
7'd22: begin tm_dio <= 1'b1; state <= 7'd23; end
7'd23: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd24; end
7'd24: begin tm_dio <= 1'b0; state <= 7'd25; bitn <= 4'd0; curbyte <= 8'h8F; phase <= 3'd0; end
7'd25: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= 7'd26;
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd26: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd27; end
7'd27: begin tm_clk <= 1'b1; state <= 7'd28; end
7'd28: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd29; end
7'd29: begin tm_dio <= 1'b1; state <= 7'd0; end
default: state <= 7'd0;
endcase
end
end
end
endmodule
// ... continúa para miembros en el código completo validado ...module ps2_receiver (
input wire clk,
input wire rst,
input wire ps2_clk,
input wire ps2_data,
output reg [7:0] scan_code,
output reg scan_strobe
);
reg [2:0] ps2c_sync;
reg [10:0] shift;
reg [3:0] bit_count;
always @(posedge clk) begin
if (rst) begin
ps2c_sync <= 3'b111;
shift <= 11'd0;
bit_count <= 4'd0;
scan_code <= 8'd0;
scan_strobe <= 1'b0;
end else begin
ps2c_sync <= {ps2c_sync[1:0], ps2_clk};
scan_strobe <= 1'b0;
if (ps2c_sync[2:1] == 2'b10) begin
shift <= {ps2_data, shift[10:1]};
if (bit_count == 4'd10) begin
bit_count <= 4'd0;
scan_code <= shift[8:1];
scan_strobe <= 1'b1;
end else begin
bit_count <= bit_count + 4'd1;
end
end
end
end
endmodule
module key_decode (
input wire clk,
input wire rst,
input wire [7:0] scan_code,
input wire scan_strobe,
output reg [3:0] digit,
output reg digit_valid,
output reg clear_key
);
reg break_code;
always @(posedge clk) begin
if (rst) begin
break_code <= 1'b0;
digit <= 4'd0;
digit_valid <= 1'b0;
clear_key <= 1'b0;
end else begin
digit_valid <= 1'b0;
clear_key <= 1'b0;
if (scan_strobe) begin
if (scan_code == 8'hF0) begin
break_code <= 1'b1;
end else begin
if (!break_code) begin
case (scan_code)
8'h45: begin digit <= 4'd0; digit_valid <= 1'b1; end
8'h16: begin digit <= 4'd1; digit_valid <= 1'b1; end
8'h1E: begin digit <= 4'd2; digit_valid <= 1'b1; end
8'h26: begin digit <= 4'd3; digit_valid <= 1'b1; end
8'h25: begin digit <= 4'd4; digit_valid <= 1'b1; end
8'h2E: begin digit <= 4'd5; digit_valid <= 1'b1; end
8'h36: begin digit <= 4'd6; digit_valid <= 1'b1; end
8'h3D: begin digit <= 4'd7; digit_valid <= 1'b1; end
8'h3E: begin digit <= 4'd8; digit_valid <= 1'b1; end
8'h46: begin digit <= 4'd9; digit_valid <= 1'b1; end
8'h66: begin clear_key <= 1'b1; end // Backspace
default: begin end
endcase
end
break_code <= 1'b0;
end
end
end
end
endmodule
module tm1637_driver (
input wire clk,
input wire rst,
input wire [7:0] d3,
input wire [7:0] d2,
input wire [7:0] d1,
input wire [7:0] d0,
output reg tm_clk,
output reg tm_dio
);
reg [15:0] divcnt;
reg tick;
reg [7:0] frame [0:5];
reg [6:0] state;
reg [3:0] bitn;
reg [7:0] curbyte;
reg [2:0] phase;
always @(posedge clk) begin
if (rst) begin
divcnt <= 16'd0;
tick <= 1'b0;
end else begin
if (divcnt == 16'd249) begin
divcnt <= 16'd0;
tick <= 1'b1;
end else begin
divcnt <= divcnt + 16'd1;
tick <= 1'b0;
end
end
end
always @(posedge clk) begin
if (rst) begin
frame[0] <= 8'h40;
frame[1] <= 8'hC0;
frame[2] <= 8'h00;
frame[3] <= 8'h00;
frame[4] <= 8'h00;
frame[5] <= 8'h00;
state <= 7'd0;
bitn <= 4'd0;
curbyte <= 8'd0;
phase <= 3'd0;
tm_clk <= 1'b1;
tm_dio <= 1'b1;
end else begin
frame[0] <= 8'h40;
frame[1] <= 8'hC0;
frame[2] <= d0;
frame[3] <= d1;
frame[4] <= d2;
frame[5] <= d3;
if (tick) begin
case (state)
7'd0: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd1; end
7'd1: begin tm_dio <= 1'b0; state <= 7'd2; bitn <= 4'd0; curbyte <= frame[0]; phase <= 3'd0; end
7'd2,7'd3,7'd4,7'd5,7'd6,7'd7: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= state + 7'd1;
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd8: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd9; end
7'd9: begin tm_clk <= 1'b1; state <= 7'd10; end
7'd10: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd11; end
7'd11: begin tm_dio <= 1'b1; state <= 7'd12; end
7'd12: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd13; end
7'd13: begin tm_dio <= 1'b0; state <= 7'd14; bitn <= 4'd0; curbyte <= frame[1]; phase <= 3'd0; end
7'd14,7'd15,7'd16,7'd17,7'd18: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= state + 7'd1;
if (state == 7'd18) curbyte <= frame[2];
else curbyte <= frame[state - 7'd15 + 3];
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd19: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd20; end
7'd20: begin tm_clk <= 1'b1; state <= 7'd21; end
7'd21: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd22; end
7'd22: begin tm_dio <= 1'b1; state <= 7'd23; end
7'd23: begin tm_clk <= 1'b1; tm_dio <= 1'b1; state <= 7'd24; end
7'd24: begin tm_dio <= 1'b0; state <= 7'd25; bitn <= 4'd0; curbyte <= 8'h8F; phase <= 3'd0; end
7'd25: begin
case (phase)
3'd0: begin tm_clk <= 1'b0; tm_dio <= curbyte[bitn]; phase <= 3'd1; end
3'd1: begin tm_clk <= 1'b1; phase <= 3'd2; end
3'd2: begin
if (bitn == 4'd7) begin
bitn <= 4'd0;
phase <= 3'd0;
state <= 7'd26;
end else begin
bitn <= bitn + 4'd1;
phase <= 3'd0;
end
end
default: phase <= 3'd0;
endcase
end
7'd26: begin tm_clk <= 1'b0; tm_dio <= 1'b1; state <= 7'd27; end
7'd27: begin tm_clk <= 1'b1; state <= 7'd28; end
7'd28: begin tm_clk <= 1'b1; tm_dio <= 1'b0; state <= 7'd29; end
7'd29: begin tm_dio <= 1'b1; state <= 7'd0; end
default: state <= 7'd0;
endcase
end
end
end
endmodule
module ps2_pin_lock_top (
input wire clk_25mhz,
input wire ps2_clk,
input wire ps2_data,
output wire tm_clk,
output wire tm_dio,
output reg unlock_led
);
wire [7:0] scan_code;
wire scan_strobe;
wire [3:0] digit;
wire digit_valid;
wire clear_key;
reg rst = 1'b0;
reg [3:0] buf3, buf2, buf1, buf0;
reg [2:0] count;
reg [23:0] hold_counter;
reg hold_active;
reg success_mode;
reg fail_mode;
reg [7:0] seg3, seg2, seg1, seg0;
localparam [15:0] PIN = 16'h1234;
function [7:0] seg_num;
input [3:0] n;
begin
case (n)
4'd0: seg_num = 8'h3F;
4'd1: seg_num = 8'h06;
4'd2: seg_num = 8'h5B;
4'd3: seg_num = 8'h4F;
4'd4: seg_num = 8'h66;
4'd5: seg_num = 8'h6D;
4'd6: seg_num = 8'h7D;
4'd7: seg_num = 8'h07;
4'd8: seg_num = 8'h7F;
4'd9: seg_num = 8'h6F;
default: seg_num = 8'h00;
endcase
end
endfunction
localparam [7:0] SEG_BLANK = 8'h00;
localparam [7:0] SEG_O = 8'h3F;
localparam [7:0] SEG_P = 8'h73;
localparam [7:0] SEG_E = 8'h79;
localparam [7:0] SEG_N = 8'h37;
localparam [7:0] SEG_F = 8'h71;
localparam [7:0] SEG_A = 8'h77;
localparam [7:0] SEG_I = 8'h06;
localparam [7:0] SEG_L = 8'h38;
localparam [7:0] SEG_DASH = 8'h40;
ps2_receiver u_rx (
.clk(clk_25mhz),
.rst(rst),
.ps2_clk(ps2_clk),
.ps2_data(ps2_data),
.scan_code(scan_code),
.scan_strobe(scan_strobe)
);
key_decode u_key (
.clk(clk_25mhz),
.rst(rst),
.scan_code(scan_code),
.scan_strobe(scan_strobe),
.digit(digit),
.digit_valid(digit_valid),
.clear_key(clear_key)
);
tm1637_driver u_disp (
.clk(clk_25mhz),
.rst(rst),
.d3(seg3),
.d2(seg2),
.d1(seg1),
.d0(seg0),
.tm_clk(tm_clk),
.tm_dio(tm_dio)
);
always @(posedge clk_25mhz) begin
if (rst) begin
buf3 <= 4'd0; buf2 <= 4'd0; buf1 <= 4'd0; buf0 <= 4'd0;
count <= 3'd0;
hold_counter <= 24'd0;
hold_active <= 1'b0;
success_mode <= 1'b0;
fail_mode <= 1'b0;
unlock_led <= 1'b0;
end else begin
if (clear_key) begin
buf3 <= 4'd0; buf2 <= 4'd0; buf1 <= 4'd0; buf0 <= 4'd0;
count <= 3'd0;
success_mode <= 1'b0;
fail_mode <= 1'b0;
unlock_led <= 1'b0;
hold_active <= 1'b0;
hold_counter <= 24'd0;
end else if (hold_active) begin
if (hold_counter == 24'd12499999) begin
hold_counter <= 24'd0;
hold_active <= 1'b0;
success_mode <= 1'b0;
fail_mode <= 1'b0;
unlock_led <= 1'b0;
count <= 3'd0;
buf3 <= 4'd0; buf2 <= 4'd0; buf1 <= 4'd0; buf0 <= 4'd0;
end else begin
hold_counter <= hold_counter + 24'd1;
end
end else if (digit_valid) begin
buf3 <= buf2;
buf2 <= buf1;
buf1 <= buf0;
buf0 <= digit;
if (count < 3'd4)
count <= count + 3'd1;
if (count == 3'd3) begin
if ({buf2, buf1, buf0, digit} == PIN) begin
success_mode <= 1'b1;
fail_mode <= 1'b0;
unlock_led <= 1'b1;
end else begin
success_mode <= 1'b0;
fail_mode <= 1'b1;
unlock_led <= 1'b0;
end
hold_active <= 1'b1;
hold_counter <= 24'd0;
end
end
end
end
always @(*) begin
if (success_mode) begin
seg3 = SEG_O;
seg2 = SEG_P;
seg1 = SEG_E;
seg0 = SEG_N;
end else if (fail_mode) begin
seg3 = SEG_F;
seg2 = SEG_A;
seg1 = SEG_I;
seg0 = SEG_L;
end else begin
case (count)
3'd0: begin seg3 = SEG_DASH; seg2 = SEG_DASH; seg1 = SEG_DASH; seg0 = SEG_DASH; end
3'd1: begin seg3 = SEG_BLANK; seg2 = SEG_BLANK; seg1 = SEG_BLANK; seg0 = seg_num(buf0); end
3'd2: begin seg3 = SEG_BLANK; seg2 = SEG_BLANK; seg1 = seg_num(buf1); seg0 = seg_num(buf0); end
3'd3: begin seg3 = SEG_BLANK; seg2 = seg_num(buf2); seg1 = seg_num(buf1); seg0 = seg_num(buf0); end
default: begin seg3 = seg_num(buf3); seg2 = seg_num(buf2); seg1 = seg_num(buf1); seg0 = seg_num(buf0); end
endcase
end
end
endmodule
2) Testbench: tb_ps2_pin_lock.cpp
Esta simulación aplica formas de onda PS/2 al diseño Verilated. Envía códigos make para dígitos y comprueba el comportamiento de desbloqueo.
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
#include "Vps2_pin_lock_top.h"
#include "verilated.h"
#include <cstdio>
static vluint64_t main_time = 0;
double sc_time_stamp() { return main_time; }
static void tick(Vps2_pin_lock_top *dut, int cycles = 1) {
for (int i = 0; i < cycles; i++) {
dut->clk_25mhz = 0;
dut->eval();
main_time++;
dut->clk_25mhz = 1;
dut->eval();
main_time++;
}
}
static void ps2_send_byte(Vps2_pin_lock_top *dut, unsigned char b) {
int parity = 1;
dut->ps2_data = 1;
dut->ps2_clk = 1;
tick(dut, 2000);
auto send_bit = [&](int bit) {
dut->ps2_data = bit;
tick(dut, 500);
dut->ps2_clk = 0;
tick(dut, 1000);
dut->ps2_clk = 1;
tick(dut, 1000);
};
send_bit(0);
for (int i = 0; i < 8; i++) {
int bit = (b >> i) & 1;
parity ^= bit;
send_bit(bit);
}
// ... continúa para miembros en el código completo validado ...#include "Vps2_pin_lock_top.h"
#include "verilated.h"
#include <cstdio>
static vluint64_t main_time = 0;
double sc_time_stamp() { return main_time; }
static void tick(Vps2_pin_lock_top *dut, int cycles = 1) {
for (int i = 0; i < cycles; i++) {
dut->clk_25mhz = 0;
dut->eval();
main_time++;
dut->clk_25mhz = 1;
dut->eval();
main_time++;
}
}
static void ps2_send_byte(Vps2_pin_lock_top *dut, unsigned char b) {
int parity = 1;
dut->ps2_data = 1;
dut->ps2_clk = 1;
tick(dut, 2000);
auto send_bit = [&](int bit) {
dut->ps2_data = bit;
tick(dut, 500);
dut->ps2_clk = 0;
tick(dut, 1000);
dut->ps2_clk = 1;
tick(dut, 1000);
};
send_bit(0);
for (int i = 0; i < 8; i++) {
int bit = (b >> i) & 1;
parity ^= bit;
send_bit(bit);
}
send_bit(parity);
send_bit(1);
dut->ps2_data = 1;
tick(dut, 3000);
}
int main(int argc, char **argv) {
Verilated::commandArgs(argc, argv);
Vps2_pin_lock_top *dut = new Vps2_pin_lock_top;
dut->ps2_clk = 1;
dut->ps2_data = 1;
dut->clk_25mhz = 0;
tick(dut, 5000);
// Correct PIN: 1 2 3 4
ps2_send_byte(dut, 0x16);
ps2_send_byte(dut, 0x1E);
ps2_send_byte(dut, 0x26);
ps2_send_byte(dut, 0x25);
tick(dut, 10000);
std::printf("unlock_led after 1234 = %d\n", (int)dut->unlock_led);
tick(dut, 13000000 / 2);
// Wrong PIN: 9 9 9 9
ps2_send_byte(dut, 0x46);
ps2_send_byte(dut, 0x46);
ps2_send_byte(dut, 0x46);
ps2_send_byte(dut, 0x46);
tick(dut, 10000);
std::printf("unlock_led after 9999 = %d\n", (int)dut->unlock_led);
delete dut;
return 0;
}
3) Por qué este código es práctico
Esto no es solo una demo de decodificación. Implementa un panel frontal realista:
- Dispositivo de entrada: teclado PS/2 común
- Lógica de decisión: recogida de dígitos más comparación con PIN fijo
- Retroalimentación humana: el display muestra los dígitos tecleados y el resultado
- Salida de acción:
unlock_ledpuede más adelante controlar una interfaz de relé o una señal de permiso
Comandos de Build/Flash/Run
1) Lint con Verilator
Ejecuta primero lint sobre el diseño sintetizable más el contexto de uso del testbench:
verilator --lint-only -Wall -Wno-DECLFILENAME ps2_pin_lock_top.v
2) Compilar y ejecutar la simulación
verilator -Wall -Wno-DECLFILENAME --cc ps2_pin_lock_top.v --exe tb_ps2_pin_lock.cpp --top-module ps2_pin_lock_top
make -C obj_dir -f Vps2_pin_lock_top.mk Vps2_pin_lock_top
./obj_dir/Vps2_pin_lock_top
3) Síntesis con Yosys
Importante: la síntesis incluye solo la fuente sintetizable, no el testbench en C++.
Crea build.ys:
read_verilog ps2_pin_lock_top.v
synth_ecp5 -top ps2_pin_lock_top -json ps2_pin_lock_top.json
Ejecuta:
yosys build.ys
4) Place and route
Reemplaza CABGA381 por el encapsulado real de tu ULX3S si tu variante de placa difiere. La ULX3S con ECP5-85F comúnmente usa un encapsulado adecuado soportado por nextpnr; confirma tu placa/encapsulado exactos antes de ejecutar.
nextpnr-ecp5 --85k --json ps2_pin_lock_top.json --lpf ulx3s_ps2_pin_lock.lpf --textcfg ps2_pin_lock_top.config --package CABGA381
5) Empaquetado del bitstream
ecppack ps2_pin_lock_top.config ps2_pin_lock_top.bit
6) Programar la ULX3S
openFPGALoader -b ulx3s ps2_pin_lock_top.bit
Si tu sistema necesita una selección específica de cable/dispositivo, revisa los dispositivos conectados con:
openFPGALoader --detect
Validación paso a paso
La validación es esencial porque este proyecto afirma un comportamiento específico.
1) Validación por simulación
Ejecuta el testbench de Verilator y espera una salida similar a:
unlock_led after 1234 = 1
unlock_led after 9999 = 0
Lo que esto demuestra:
- Se capturaron tramas serie PS/2.
- Los scan codes
0x16 0x1E 0x26 0x25se decodificaron como1 2 3 4. - La lógica de comparación de 4 dígitos activó unlock al coincidir.
- La secuencia de PIN incorrecta no activó unlock.
2) Validación de encendido en hardware
Después de grabar:
- Conecta el teclado y el display.
- Alimenta la ULX3S por USB.
- Observa el TM1637 en reposo.
- Esperado: el display muestra cuatro guiones
----.
Si el display está en blanco:
– primero sospecha del cableado o de las restricciones para tm_clk y tm_dio
– luego comprueba alimentación y masa del módulo
3) Validación de entrada por teclado
Teclea dígitos individuales lentamente:
- Pulsa
1 - Esperado: el dígito más a la derecha se convierte en
1 - Pulsa
2 - Esperado:
12en las dos posiciones más a la derecha - Pulsa
3 - Esperado:
123 - Pulsa
Backspace - Esperado: el display vuelve a
----
Esto valida:
- cableado de clock/data PS/2
- reconocimiento de make-code
- tratamiento correcto de break-code ignorado
- ruta de limpieza/reset
4) Validación del PIN correcto
Introduce:
1,2,3,4
Resultado esperado:
- el display cambia de los dígitos a
OPEN unlock_ledse activa- después de aproximadamente medio segundo, el sistema vuelve al estado de reposo
----
5) Validación de PIN incorrecto
Introduce:
9,9,9,9
Resultado esperado:
- el display muestra
FAIL unlock_ledpermanece inactivo- después de aproximadamente medio segundo, el sistema vuelve al estado de reposo
6) Validación de uso repetido
Prueba esta secuencia:
1 2 3 4- espera al reinicio
2 2 2 2- espera al reinicio
1 2 3 4
Esperado:
- el primer intento tiene éxito
- el segundo intento falla
- el tercer intento vuelve a tener éxito
Esto confirma que la máquina de estados se limpia correctamente entre intentos.
Solución de problemas
El display no hace nada
Posibles causas:
- restricciones de pines
tm_clk/tm_dioincorrectas - falta masa común
- módulo de display alimentado incorrectamente
- el módulo TM1637 espera un pull-up más fuerte o un comportamiento diferente de tensión lógica
Acciones:
- Vuelve a comprobar el mapeo físico de cables frente a los nombres del LPF.
- Confirma
3V3yGNDcon un multímetro si está disponible. - Intercambia
tm_clkytm_diosolo si sospechas un error de etiquetado. - Verifica que el módulo realmente está basado en TM1637 y no en otra placa de display serie.
El teclado no es reconocido
Posibles causas:
- el módulo PS/2 o el teclado no es seguro para 3.3 V
- restricciones
ps2_clk/ps2_dataincorrectas - el teclado envía scan codes de un conjunto diferente al esperado
- no hay pull-up en las líneas
Acciones:
- Confirma que ambas líneas PS/2 están en reposo en alto.
- Asegúrate de que el módulo está alimentado y de que el propio teclado funciona bien.
- Prueba primero con las teclas de la fila numérica superior, no con el teclado numérico.
- Mantén
PULLMODE=UPen las restricciones. - Si los dígitos no coinciden con los valores esperados, captura los scan codes reales en una versión de depuración más adelante.
OPEN nunca aparece incluso con los dígitos correctos
Posibles causas:
- scan codes diferentes de los asumidos
- un dígito se está interpretando incorrectamente
- la constante PIN no coincide con la entrada que pretendes
Acciones:
- Verifica que el PIN previsto es
1234en:
verilog
localparam [15:0] PIN = 16'h1234; - Introduce los dígitos desde la fila superior:
1 2 3 4. - Si hace falta, cambia temporalmente el diseño para mostrar los dígitos decodificados en bruto y confirmar el mapeo.
Falla el place-and-route
Posibles causas:
- nombre de encapsulado no válido
- nombres de sitio LPF no válidos
- conflicto de pines con pines reservados o no disponibles de la ULX3S
Acciones:
- Confirma el encapsulado exacto de tu ULX3S y usa la opción correspondiente en
nextpnr-ecp5. - Sustituye los nombres de sitio LPF de ejemplo por pines reales del encapsulado.
- Evita los pines de función especial exclusiva a menos que sepas que son válidos como GPIO.
Mejoras
Una vez que la versión básica funcione, puedes ampliarla de formas útiles:
1) Enmascarar los dígitos tecleados
En lugar de mostrar los dígitos reales, muestra ---- o marcadores tipo **** usando patrones de segmentos. Esto hace que la entrada del PIN se parezca más a una cerradura real.
2) Añadir comportamiento con tecla Enter
Actualmente el sistema verifica automáticamente después del cuarto dígito. Puedes mejorar la usabilidad de esta forma:
- recoger hasta 4 dígitos
- validar solo cuando se pulse
Enter
3) Añadir bloqueo tras fallos repetidos
Mejora útil del mundo real:
- contar intentos fallidos
- después de 3 fallos, ignorar teclas durante 10 segundos
- mostrar
LOCKoWAITen el display
4) Salida de control externa
Reemplaza unlock_led o duplícala con una salida para una etapa de interfaz externa segura:
- driver con transistor
- optoacoplador
- módulo de relé con aislamiento adecuado
Recuerda: eso necesitaría alimentación separada y una revisión de seguridad.
5) Almacenar un PIN configurable
Para una versión más avanzada pero todavía práctica:
- usar interruptores DIP para configurar el PIN
- o añadir entrada de comandos UART
- o almacenar el PIN en memoria externa no volátil
6) Añadir buzzer o registro de eventos
Un pitido corto al pulsar una tecla y un pitido diferente en fallo/éxito crean un panel de control de acceso más realista.
Lista de verificación final
Usa esta lista antes de dar por terminada la compilación:
- [ ] Usé Radiona ULX3S (Lattice ECP5-85F) + módulo teclado PS/2 mini-DIN + display 7 segmentos TM1637.
- [ ] Todos los módulos comparten una masa común.
- [ ] Los niveles de señal de mi PS/2 y TM1637 son seguros para la E/S de la FPGA de la ULX3S.
- [ ] Reemplacé los sitios de pines LPF de ejemplo por pines reales del encapsulado de la ULX3S.
- [ ]
verilator --lint-onlyse ejecuta sin errores fatales. - [ ] La simulación con Verilator imprime:
- [ ]
unlock_led after 1234 = 1 - [ ]
unlock_led after 9999 = 0 - [ ] La síntesis con
yosysse completa correctamente. - [ ]
nextpnr-ecp5se completa con mi encapsulado correcto y mi LPF. - [ ]
ecppackgenera un archivo.bit. - [ ]
openFPGALoader -b ulx3s ps2_pin_lock_top.bitprograma la placa. - [ ] Al encender, el display muestra
----. - [ ] Introducir
1 2 3 4muestraOPEN. - [ ] Introducir un código incorrecto muestra
FAIL. - [ ]
Backspacelimpia la entrada actual. - [ ] El prototipo se reinicia limpiamente después de cada intento.
Con este proyecto, tienes un prototipo realista para principiantes con FPGA: un panel frontal de cerradura por PIN operado con teclado y con salida visible de estado, construido usando herramientas ECP5 de código abierto y bloques prácticos de diseño digital.
<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>




