You dont have javascript enabled! Please enable it!

Caso práctico: cerradura PIN FPGA con Radiona ULX3S Lattice ECP5-85F

Caso práctico: cerradura PIN FPGA con Radiona ULX3S Lattice ECP5-85F — hero

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

Teclado PS/2

Receptor PS/2

Decodificador de tecla

Registro de 4 dígitos

Máquina de estados PIN

Comparador de clave

Salida desbloqueo

Driver TM1637

Display 4 dígitos

Flujo conceptual de datos: entrada del usuario, lógica de decisión y salida visual/de desbloqueo.

Ruta de validación

Verilog fuente

Verilator lint/testbench

Yosys síntesis

nextpnr-ecp5

ecppack bitstream

ULX3S programada

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:
  • verilator
  • yosys
  • nextpnr-ecp5
  • ecppack
  • openFPGALoader

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:

ElementoModelo exactoCantidadPropósito
Placa FPGARadiona ULX3S (Lattice ECP5-85F)1Controlador principal
Interfaz de tecladomódulo teclado PS/2 mini-DIN1Entrada del PIN del usuario
Módulo de displaydisplay 7 segmentos TM16371Display de PIN/estado
Cables jumperHembra-hembra o mixtos según se necesite1 juegoCableado
Cable USBCompatible con ULX3S1Alimentación y programación
OpcionalProtoboard1Facilita 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 0 a 9 del 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 unlock se activa
  • en caso contrario, el display muestra fallo
  • Luego el sistema se limpia y espera el siguiente intento de PIN.
  • Pulsar Backspace limpia 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_clk
  • ps2_data
  • tm_clk
  • tm_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 3V3 si tu módulo lo soporta
  • masa común entre todos los dispositivos

3) Guía de cableado en texto

Conecta de la siguiente manera:

  1. Alimentación común
  2. ULX3S GND -> GND del módulo PS/2
  3. ULX3S GND -> GND del TM1637
  4. ULX3S 3V3 -> VCC del módulo PS/2 si tu módulo lo soporta
  5. ULX3S 3V3 -> VCC del TM1637

  6. Interfaz PS/2

  7. GPIO elegido de la ULX3S -> CLK del módulo PS/2
  8. GPIO elegido de la ULX3S -> DATA del módulo PS/2

  9. Interfaz TM1637

  10. GPIO elegido de la ULX3S -> CLK del TM1637
  11. GPIO elegido de la ULX3S -> DIO del TM1637

  12. Indicador opcional de desbloqueo

  13. GPIO de la ULX3S con capacidad para LED integrado o señal de LED onboard -> salida lógica unlock_led
  14. 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 ...

🔒 Parte del código validado es premium. Con el pase de 7 días o la suscripción mensual podrás consultar el archivo 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 ...

🔒 Parte del código validado es premium. Con el pase de 7 días o la suscripción mensual podrás consultar el archivo 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_led puede 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 0x25 se decodificaron como 1 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:

  1. Conecta el teclado y el display.
  2. Alimenta la ULX3S por USB.
  3. Observa el TM1637 en reposo.
  4. 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:

  1. Pulsa 1
  2. Esperado: el dígito más a la derecha se convierte en 1
  3. Pulsa 2
  4. Esperado: 12 en las dos posiciones más a la derecha
  5. Pulsa 3
  6. Esperado: 123
  7. Pulsa Backspace
  8. 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_led se 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_led permanece inactivo
  • después de aproximadamente medio segundo, el sistema vuelve al estado de reposo

6) Validación de uso repetido

Prueba esta secuencia:

  1. 1 2 3 4
  2. espera al reinicio
  3. 2 2 2 2
  4. espera al reinicio
  5. 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_dio incorrectas
  • 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:

  1. Vuelve a comprobar el mapeo físico de cables frente a los nombres del LPF.
  2. Confirma 3V3 y GND con un multímetro si está disponible.
  3. Intercambia tm_clk y tm_dio solo si sospechas un error de etiquetado.
  4. 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_data incorrectas
  • el teclado envía scan codes de un conjunto diferente al esperado
  • no hay pull-up en las líneas

Acciones:

  1. Confirma que ambas líneas PS/2 están en reposo en alto.
  2. Asegúrate de que el módulo está alimentado y de que el propio teclado funciona bien.
  3. Prueba primero con las teclas de la fila numérica superior, no con el teclado numérico.
  4. Mantén PULLMODE=UP en las restricciones.
  5. 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:

  1. Verifica que el PIN previsto es 1234 en:
    verilog
    localparam [15:0] PIN = 16'h1234;
  2. Introduce los dígitos desde la fila superior: 1 2 3 4.
  3. 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:

  1. Confirma el encapsulado exacto de tu ULX3S y usa la opción correspondiente en nextpnr-ecp5.
  2. Sustituye los nombres de sitio LPF de ejemplo por pines reales del encapsulado.
  3. 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 LOCK o WAIT en 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-only se ejecuta sin errores fatales.
  • [ ] La simulación con Verilator imprime:
  • [ ] unlock_led after 1234 = 1
  • [ ] unlock_led after 9999 = 0
  • [ ] La síntesis con yosys se completa correctamente.
  • [ ] nextpnr-ecp5 se completa con mi encapsulado correcto y mi LPF.
  • [ ] ecppack genera un archivo .bit.
  • [ ] openFPGALoader -b ulx3s ps2_pin_lock_top.bit programa la placa.
  • [ ] Al encender, el display muestra ----.
  • [ ] Introducir 1 2 3 4 muestra OPEN.
  • [ ] Introducir un código incorrecto muestra FAIL.
  • [ ] Backspace limpia 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>

Quiz rápido

Pregunta 1: ¿Qué placa FPGA se utiliza en este proyecto?




Pregunta 2: ¿Qué dispositivo se utiliza para introducir el código de 4 dígitos?




Pregunta 3: ¿Qué componente se usa para mostrar el estado y los mensajes breves?




Pregunta 4: ¿Cuál es la latencia visual típica esperada al procesar las pulsaciones?




Pregunta 5: ¿Qué porcentaje de uso de GPU requiere este sistema?




Pregunta 6: ¿Cuál es uno de los casos de uso mencionados para este proyecto?




Pregunta 7: ¿Qué tipo de mensajes o estados puede mostrar el display según la lógica implementada?




Pregunta 8: ¿Qué conceptos de diseño digital se ejercitan en este proyecto?




Pregunta 9: ¿Cuántos dígitos conforman el código PIN que valida el sistema?




Pregunta 10: ¿Por qué se considera una práctica de HMI embebida real este proyecto?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio