Podłączamy starą klawiaturę do Arduino

Robiąc ostatnio porządki w swoich rzeczach wygrzebałem wśród morza elektroniki starą klawiaturę do komputera, taką ze złączem PS/2. Od razu pomyślałem, że pewnie dałoby się ją jakoś wykorzystać i nie myliłem się. Po zrobieniu małego researchu w internecie znalazłem opis działania portu PS/2 i okazało się, że jest on banalnie prosty w implementacji na Arduino. Zobaczmy więc z czym to się je :)

Pierwsze co należy zrobić, to zapoznać się z tą anglojęzyczną stroną: http://www.computer-engineering.org/ps2protocol/, a właściwie z rozdziałami The Physical Interface oraz Communication: Device-to-Host.

W pierwszej części dowiadujemy się jak wygląda złącze PS/2, oraz jakie sygnały są wyprowadzone na każdym z pinów. Jak łatwo zauważyć sygnały sterujące są tylko dwa: linia zegarowa (Clock) oraz linia danych (Data). Komunikacja jest o tyle wygodna, że to klawaitura steruje linią zegarową, więc do nas należy jedynie odczyt danych ;)

A jak odczytać te dane?

Tego możemy dowiedzieć się w rozdziale Communication: Device-to-Host. Właściwie wszystkie przydatne informacje z tego rozdziału możemy odczytać z wykresu linii danych i zegara

Widać tutaj, że dane z linii DATA powinniśmy odczytywać, kiedy CLOCK przechodzi ze stanu wysokiego w stan niski. Można także zauważyć, że ramka składa się z 11 bitów z czego 8 to bity przeznaczone na dane, jeden to bit startu, który jest zawsze zerem, jeden bit stopu, który jest zawsze jedynką i bit kontrolny, który mówi nam czy ilość jedynek w bitach od 1 do 8 jest parzysta, czy nie.

Mając już taką wiedzę możemy przystąpić do realizacji naszego projektu :)

Podłączenie

Urządzenia na PS/2 zasilane są z 5V, więc VCC podłączamy do pinu 5V Arduino. Linię zegara należy podłączyć do pinu obsługującego przerwania (ja w swoim Uno podłączyłem do pinu numer 2). Linię danych możemy podłączyć do dowolnego wolnego pinu cyfrowego naszego mikrokontrolera.

Kod programu

Cały kod można pobrać tutaj.

Na początku programu jak zwykle definiujemy sobie kilka tałych i zmiennych:

#define CLK 2
#define DAT 8

const int BUF_SIZE = 11;
bool buffer[BUF_SIZE] = {0};
int pos = 0;
bool ignoreNext = false;
unsigned long lastRead = 0;

CLK oraz DAT to numery pinów, do których podłączamy interfejs PS/2. Zmienna buffer posłuży nam jako bufor na całą ramkę z danymi, a pos będzie aktualnie przetwarzaną pozycją.

Aby wszystko działało w funkcji setup należy ustawić tryby działania pinów na wejścia, uruchomić port szeregowy oraz skonfigurować przerwanie na pinie drugim:

void setup() {
  Serial.begin(9600);
  pinMode(CLK, INPUT);
  pinMode(DAT, INPUT);
  attachInterrupt(digitalPinToInterrupt(CLK),readData , FALLING);
}

Kod ten spowoduje, że przy każdym opadającym zboczu na pinie CLK zostanie wywołana funkcja o sygnaturze void readData();

która to wygląda tak:

void readData() {
  lastRead = millis();
  buffer[pos++ % 11] = digitalRead(DAT);
}

Funkcje, które obsługują przerwania powinny zajmować jak najmniej czasu procesora, dlatego też w readData ustawiamy tylko kiedy był ostatni odczyt danych oraz wpisujemy dane do bufora. Przetwarzaniem zajmiemy się w głównej pętli programu:

void loop() {
  if(pos != 0 && millis() - lastRead > 1000) {
    pos = 0;
  }
  if(pos == 11) {
    pos = 0;
    int keyCode = getKeyCode(buffer);
    if(ignoreNext) {
      ignoreNext = false;
      return;
    }
    if(keyCode == 0xF0) {
      ignoreNext = true;
      return;
    }
    
    Serial.print(getKeyChar(keyCode));
  }
}

Pierwsza instrukcja warunkowa służy nam za swoisty timeout. Jeżeli otrzymamy niepełną ramkę, to po sekundzie braku odczytów na linii danych wyzerujemy ją.

Następnie porównaniem pos == 11 sprawdzamy, czy mamy już pełną ramkę z danymi. Jeżeli tak to ustawiamy pos z powrotem na 0 dając programowi w następnej pętli do zrozumienia, że już odczytaliśmy bufor. Funkcją getKeyCode() odczytujemy kod klawisza a następnie sprawdzamy, czy mamy go zignorować, czy też nie (ignorujemy każdy klawisz po kodzie 0xF0 – dlaczego? to wyjaśni się później :) ). Jeżeli nie zignorowaliśmy kodu, to wypisujemy odpowiadający mu znak z klawaitury za pomocą funkcji getKeyChar() oraz Serial.print();

Zobaczmy, co dzieje się pod maską funkcji getKeyCode():

int getKeyCode(bool * buf) {
  bool parity = 1;
  int result = 0;
  if(buf[0] != 0) return -1;
  if(buf[10] != 1) return -2;
  for(int x = 0; x < 8; x++) {
    result |= buf[1+x] << x;
    if(buf[1+x]) parity = !parity;
  }

  if(buf[9] != parity) return -3;
  return result;
}

Tutaj najpierw sprawdzamy, czy bit startu jest zerem, a bit stopu jedynką. Jeżeli tak nie jest to zwracamy odpowiednie kody błędów. Jeżeli wszystko ok, to przepisujemy liczbę z bitów danych do zmiennej typu int. Wiadomo, że bit 1 to najmniej znacząca cyfra, dlatego zapisujemy ją na bicie 0 zmiennej wynikowej i tak dalej. Konsternację może wzbudzić tutaj zapis |= oraz <<. Operacja | to po prostu logiczny OR na bitach, a y << x to przesunięcie liczby y o x bitów w lewo. | oraz |= to zapis analogiczny do + oraz +=.

W tej samej pętli sprawdzamy także czy liczba jedynek jest parzysta zmieniając z 0 na 1 i z 1 na 0 zmienną parity za każdym razem, kiedy w ciągu danych wystąpi jedynka.

Na sam koniec sprawdzamy, czy nasza zmienna parity zgadza się z bitem parzystości. Jeżeli nie to zwracamy kod błędu, a jeżeli tak to zwracamy zdekodowaną liczbę :)

Pozostała już tylko funkcja do dekodowania kodów w znaki. Posłużymy się tutaj stroną http://www.computer-engineering.org/ps2keyboard/scancodes2.html na której znajdziemy wszystkie kody w formacie szesnastkowym. Teraz już wiemy, dlaczego ignorowaliśmy kod F0 oraz jeden występujący po nim. Jest to sekwencja oznaczająca puszczenie klawisza. Przykładowo wciskając i puszczając klawisz A uzyskamy kody 1C, F0, 1C. Chcemy wypisać na prot szeregowy tylko jedno A, więc ignorujemy kod, któy przyjdzie po F0.