Kampis Elektroecke

Anschluss einer PS/2 Tastatur

Das PS/2-Interface alter Tastaturen (oder auch Mäuse) kann genutzt werden um ohne viel Aufwand eine einfache Eingabemöglichkeit für ein FPGA zu realisieren. In diesem Artikel möchte ich zeigen, wie eine PS/2 Tastatur an ein FPGA angeschlossen werden und die empfangenen Daten auf einer LED-Leiste ausgegeben werden können.

Aufbau der PS/2-Schnittstelle:

Bei der PS/2-Schnittstelle handelt es sich um eine einfache und (früher) sehr weit verbreitete serielle Schnittstelle für die Eingabegeräte eines Computers (z. B. Mäuse und Tastaturen). Als Steckverbinder wird Geräteseitig ein sechspoliger Mini-DIN-Stecker verwendet, welcher wie folgt belegt ist:

Pin Signal
1 Daten
2
3 GND
4 +5 V
5 Takt
6

Hinweis:

Bei sehr alten Mainboards mit einem AT-Format wird ein fünfpoliger Steckverbinder genutzt. Beide Schnittstellen sind in der Regel kompatibel zueinander.


Die Schnittstelle ist als Open-Collector ausgeführt, weswegen zwei zusätzliche Pull-up Widerstände für den Betrieb notwendig sind. Die Betriebsspannung der Geräte ist auf 5 V festgelegt.

Über eine Kombination von unterschiedlichen Pegeln kann der Empfänger dem Sender seinen aktuellen Status mitteilen und ggf. Übertragungen pausieren.

Takt Daten Beschreibung
1 1 Empfänger ist bereit neue Daten zu empfangen.
1 0 Beginn einer Übertragung (Startbit).
0 1 Empfänger ist beschäftigt.
0 0 Empfänger wird zurückgesetzt.

Ein PS/2 Datenpaket besteht aus 11 Bits, welche LSB First gesendet werden. Jede Nachricht ist dabei folgendermaßen aufgebaut:

Bit Funktion
0 Startbit (immer 0x00)
1 – 8 Datenbits
9 Parität (Ungerade)
10 Stopbit (immer 0x01)

Bei den übertragenen Daten handelt es sich Scancodes der gedrückten Tasten. Die Vergabe der Scancodes ist historisch gegeben und stammt von den früheren IBM-Tastaturen.

Scancodes Wikipedia

Der Wert des Paritätsbits hängt von der Anzahl übertragender Einsen im Datenpaket ab. Es wird gesetzt, wenn die Anzahl enthaltener Einsen gerade ist und wird dem entsprechend gelöscht, wenn die Anzahl enthaltener Einsen ungerade ist.

Wird eine Taste länger als 500 ms (standardmäßig) gedrückt, wiederholt die Tastatur das Senden des Codes in sehr schneller Folge, bis die Taste losgelassen wird. Beim Loslassen der Taste sendet die Tastatur ein 0xF0 gefolgt von dem Zahlencode der entsprechenden Taste. Auf diese Weise weiß der Host, dass die Taste losgelassen wurde.

Implementierung für das FPGA:

Das PS/2-Interface besteht aus zwei Komponenten:

  • Den Schieberegistern für die PS/2-Tastatur
  • Ein Zustandsautomat für die Erzeugung der Status-Signale
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity PS2_Interface is
    Generic (   TIMEOUT : INTEGER       := 250000
                );
    Port (  Clock       : in STD_LOGIC;
            nReset      : in STD_LOGIC;

            -- PS/2 interface signals
            PS2_Clk     : in STD_LOGIC;
            PS2_Data    : in STD_LOGIC;

            Valid       : in STD_LOGIC;
            RxComplete  : out STD_LOGIC;
            Busy        : out STD_LOGIC;

            RxData      : out STD_LOGIC_VECTOR(7 downto 0)
            );
end PS2_Interface;

architecture PS2_Interface_Arch of PS2_Interface is

    type PS2_State_t is (STATE_WAIT_START, STATE_RECEIVE, STATE_DATA_READY); 

    signal PS2_DataSR   : STD_LOGIC_VECTOR(1 downto 0)  := (others => '1');
    signal PS2_ClkSR    : STD_LOGIC_VECTOR(1 downto 0)  := (others => '1');
    signal PS2_Buffer   : STD_LOGIC_VECTOR(10 downto 0) := (others => '1');

    signal CurrentState : PS2_State_t                   := STATE_WAIT_START;
    
begin

    PS2_Shift_Proc : process
        variable TimeoutCounter : INTEGER := 0;
    begin
        wait until rising_edge(Clock);

        PS2_DataSR <= PS2_DataSR(0) & PS2_Data;
        PS2_ClkSR <= PS2_ClkSR(0) & PS2_Clk;

        if(PS2_ClkSR = "10") then
            PS2_Buffer <= PS2_DataSR(1) & PS2_Buffer(10 downto 1);
        end if;

        case CurrentState is
            when STATE_WAIT_START =>
                if((PS2_DataSR(1) = '0') and (PS2_ClkSR(1) = '1')) then
                    TimeoutCounter := TIMEOUT;
                    CurrentState <= STATE_RECEIVE;
                else
                    CurrentState <= STATE_WAIT_START;
                end if;

            when STATE_RECEIVE =>
                if(PS2_Buffer(0) = '0') then
                    RxData <= PS2_Buffer(8 downto 1);

                    CurrentState <= STATE_DATA_READY;
                elsif(Timeout = 0) then
                    PS2_Buffer <= (others => '1');

                    CurrentState <= STATE_WAIT_START;
                else
                    TimeoutCounter := Timeout - 1;

                    CurrentState <= STATE_RECEIVE;
                end if;

            when STATE_DATA_READY =>
                if(Valid = '1') then
                    PS2_Buffer <= (others => '1');

                    CurrentState <= STATE_WAIT_START;
                else
                    CurrentState <= STATE_DATA_READY;
                end if;

        end case;

        if(nReset = '0') then
            PS2_Buffer <= (others => '0');

            CurrentState <= STATE_WAIT_START;
        end if;
    end process;

    Busy <= '1' when (CurrentState = STATE_RECEIVE) else '0';
    RxComplete <= '1' when (CurrentState = STATE_DATA_READY) else '0';

end PS2_Interface_Arch;

Über das Schieberegister wird das Takt- und das Datensignal in das FPGA einsynchronisiert. Bei einer fallenden Flanke wird der aktuelle Zustand der Datenleitung in dem Vektor PS2_Buffer gespeichert:

wait until rising_edge(Clock);

PS2_DataSR <= PS2_DataSR(0) & PS2_Data;
PS2_ClkSR <= PS2_ClkSR(0) & PS2_Clk;

if(PS2_ClkSR = "10") then
    PS2_Buffer <= PS2_DataSR(1) & PS2_Buffer(10 downto 1);
end if;

Parallel wartet der Zustandsautomat so lange im Zustand STATE_WAIT_START bis ein Startbit empfangen wird. Sobald ein Startbit empfangen wurde wechselt der Zustandsautomat in den Zustand STATE_RECEIVE und verweilt dort so lange bis ein Timeout aufgetreten oder das Startbit einmal komplett durchgeschoben wurde (sprich alle Datenbits gesendet worden sind). 

Ist die Übertragung erfolgreich abgeschlossen worden, so werden die Datenbits aus dem Vektor PS2_Buffer an den Ausgang des PS/2-Interfaces gelegt und in den Zustand STATE_DATA_READY gewechselt. In diesem Zustand wird das RxComplete-Flag gesetzt und der Zustandsautomat wartet auf einen Handshake, welcher durch das Setzen des Valid-Eingangs eingeleitet wird.

Das fertige Interface kann nun in ein Top-Level Design eingefügt und auf die Hardware übertragen werden:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity Top is
    Port (  Clock       : in STD_LOGIC;
            nReset      : in STD_LOGIC;
            PS2_Clk     : in STD_LOGIC;
            PS2_Data    : in STD_LOGIC;
            Valid       : in STD_LOGIC;
            Status      : out STD_LOGIC_VECTOR(1 downto 0);
            Data        : out STD_LOGIC_VECTOR(7 downto 0)
            );
end Top;

architecture Top_Arch of Top is

    component PS2_Interface is
        Port (  Clock       : in STD_LOGIC;
                nReset      : in STD_LOGIC;
                PS2_Clk     : in STD_LOGIC;
                PS2_Data    : in STD_LOGIC;
                Valid       : in STD_LOGIC;
                RxComplete  : out STD_LOGIC;
                Busy        : out STD_LOGIC;
                RxData      : out STD_LOGIC_VECTOR(7 downto 0)
                );
    end component;

begin

    Interface : PS2_Interface port map (PS2_Clk => PS2_Clk,
                                        PS2_Data => PS2_Data,
                                        Clock => Clock,
                                        nReset => '1',
                                        Valid => Valid,
                                        Busy => Status(0),
                                        RxComplete => Status(1),
                                        RxData => Data
                                        );

end Top_Arch;

Alternativ kann das Desin auch mit der nachfolgenden Testbench verifiziert werden:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

architecture Top_TB_Arch of Top_TB is

    procedure PS2_Transmit( signal Data         : in STD_LOGIC_VECTOR(7 downto 0); 
                            signal PS2_Clock    : out STD_LOGIC;
                            signal PS2_Data     : out STD_LOGIC) is
        variable Parity : STD_LOGIC    := '1';
    begin
        wait for 1 ns;
        PS2_Clock <= '1';
        PS2_Data <= '1';
        wait for 5us;

        PS2_Data <= '0';
        wait for 20us;
        PS2_Clock <= '0';
        wait for 40us;
        PS2_Clock <= '1';
        wait for 20us;

        for i in 0 to (Data'length - 1) loop
            PS2_Data <= Data(i);

            if(Data(i) = '1') then 
                Parity := not Parity; 
            end if;

            wait for 20us;
            PS2_Clock <= '0';
            wait for 40us;
            PS2_Clock <= '1';
            wait for 20us;
        end loop;

        PS2_Data <= Parity;
        wait for 20us;
        PS2_Clock <= '0';
        wait for 40us;
        PS2_Clock <= '1';
        wait for 20us;

        PS2_Data <= '1';
        wait for 20us;
        PS2_Clock <= '0';
        wait for 40us;
        PS2_Clock <= '1';
        wait for 20us;
    end procedure;

    constant CLOCKPERIODE : TIME := 8 ns;

    signal SimulationClock  : STD_LOGIC                     := '0';
    signal SimulationResetN : STD_LOGIC                     := '1';

    signal PS2_Data         : STD_LOGIC                     := '1';
    signal PS2_Clk          : STD_LOGIC                     := '1';

    signal Valid            : STD_LOGIC                     := '0';
    signal Status           : STD_LOGIC_VECTOR(1 downto 0);
    signal Data             : STD_LOGIC_VECTOR(7 downto 0);
    signal KeyboardData     : STD_LOGIC_VECTOR(7 downto 0)  := (others => '0');

    component Top is
        Port (  Clock       : in STD_LOGIC;
                nReset      : in STD_LOGIC;
                PS2_Clk     : in STD_LOGIC;
                PS2_Data    : in STD_LOGIC;
                Valid       : in STD_LOGIC;
                Status      : out STD_LOGIC_VECTOR(1 downto 0);
                Data        : out STD_LOGIC_VECTOR(7 downto 0)
                );
    end component;

begin

    process begin
        wait for (CLOCKPERIODE / 2);
        SimulationClock <= '1';
        wait for (CLOCKPERIODE / 2);
        SimulationClock <= '0';
    end process;

   DUT : Top port map ( PS2_Data => PS2_Data,
                        PS2_Clk => PS2_Clk,
                        Valid => Valid,
                        Status => Status,
                        Data => Data,
                        Clock => SimulationClock,
                        nReset => SimulationResetN
                        );

    Stimulus : process
    begin
        KeyboardData <= x"AA";
        PS2_Transmit(KeyboardData, PS2_Clk, PS2_Data);
        wait;
   end process;

end Top_TB_Arch;

Das komplette Projekt kann bei GitLab heruntergeladen werden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.