Obsługa klawiatury matrycowej w Arduino

Często zdarza się, że w projekcie nad którym pracujemy musimy mieć do dyspozycji sporo przycisków. Już przy zwykłej klawiaturze numerycznej podłączając każdy przycisk pod osobny pin zużyjemy 12 pinów I/O (cyfry 0-9 oraz # i *). W dzisiejszym artykule dowiemy się jak podłączyć takie klawiatury używając jak najmniej pinów.

Podłączenie przycisków

Aby zaoszczędzić sobie pinów musimy odpowiednio połączyć ze sobą przyciski. Chodzi o to, aby ustawić je na wzór macierzy, wyprowadzając na zewnątrz wiersze oraz kolumny tak jak na poniższym schemacie:

Dzięki takiemu ułożeniu możemy podłączyć 16 przycisków wykorzystując do tego tylko 8 pinów GPIO.

Zasada działania

Aby odczytać stan każdego z przycisków musimy wszystkie piny do których podłączone są kolumny ustawić w stan INPUT_PULLUP. Następnie piny z wierszami ustawić jako wyjścia ze stanem WYSOKIM. Teraz pozostało już tylko za pomocą kilku pętli ustawić stan NISKI na jednym wierszy w danej chwili, a następnie odczytać stany wejść kolumn. Kiedy przeskanujemy w ten sposób każdy wiersz z osobna dowiemy się który przycisk jest wciśnięty :)

Implementacja

Całość kodu można pobrać tutaj.

Na początek zadeklarujmy sobie kilka stałych, aby kod był w każdym miejscu czytelny.

const int ROWS[] = {6,7,8,9};
const int COLS[] = {5,4,3,2};
const int NUM_ROWS = sizeof(ROWS)/sizeof(int);
const int NUM_COLS = sizeof(COLS)/sizeof(int);

const char KEYS[NUM_ROWS][NUM_COLS] = {
  {'A', 'B', 'C', 'D'},
  {'E', 'F', 'G', 'H'},
  {'I', 'J', 'K', 'L'},
  {'M', 'N', 'O', 'P'}
};

ROWS oraz COLS to piny, do których podłączone są piny klawiatury (kolejno wiersze oraz kolumny). NUM_ROWS i NUM_COLS oblicza nam się automatycznie na podstawie ROWS oraz COLS i oznacza liczbę wierszy oraz kolumn.

Ostatnią stałą jest KEYS, dwuwymiarowa tablica zawierająca reprezentację wciśniętego klawisza w postaci pojedynczego znaku.

Teraz czas na zainicjowanie klawiatury:

void setup() {
  Serial.begin(9600);
  for(int x = 0; x < NUM_ROWS; x++) {
    pinMode(ROWS[x], OUTPUT);
    digitalWrite(ROWS[x], LOW);
  }

  for(int x = 0; x < NUM_COLS; x++) {
    pinMode(COLS[x], INPUT_PULLUP);
  }
  
  Serial.println("Keypad initialized");
  printKeypadLayout();
}

Pierwsze co robimy to odpalamy sobie port szeregowy, aby móc oglądać później rezultaty.

Następnie ustawiamy wszystkie wiersze jako wyjścia ze stanem NISKIM (choć na tym etapie nie ma to znaczenia, czy stan będzie wysoki, czy niski), a kolumny jako wejścia podciągnięte do zasilania (INPUT_PULLUP). Na koniec jeszcze tylko wypisujemy na port szeregowy że klawiaturka pomyślnie zainicjowana i wyświetlamy layout klawiszy.

W głównej pętli programu w zasadzie mamy tylko wywołanie funkcji readKey(), która zwraca char’a odpowiadającego wciśniętemu klawiszowi.

void loop() {
  char key = readKey();
  if(key) Serial.println(key);
}

Zobaczmy więc, co kryje się pod tą funkcją:

char readKey() {
  for(int x = 0; x < NUM_ROWS; x++) {
    for(int row = 0; row < NUM_ROWS; row++) digitalWrite(ROWS[row], x != row);
    for(int y = 0; y < NUM_COLS; y++) {
      if(!digitalRead(COLS[y])) return KEYS[x][y];
    }
  }
  return 0;
}

Jak widać nic skomplikowanego. Iterujemy po wszystkich wierszach oraz w każdej iteracji wiersza iterujemy po kolumnach odczytując ich stany. Przed sprawdzeniem kolumn musimy zapewnić, że tylko jeden wiersz będzie w stanie NISKIM, a inne w stanie WYSOKIM. Całą robotę robi tu dla nas pętla

    for(int row = 0; row < NUM_ROWS; row++) digitalWrite(ROWS[row], x != row);

W każdej jej iteracji x jest niezmienne, a zmienna row zmienia się od 0 do NUM_ROWS-1. Z tego też powodu po wykonaniu się wszystkich iteracji tej pętli warunek logiczny x != row będzie fałszem tylko raz (wtedy, kiedy x będzie równe row), więc tylko jeden wiersz ustawi nam się w stan NISKI.

Teraz możemy już zabrać się za sprawdzanie kolumn. Jak widać od razu po wykryciu wciśniętego przycisku wychodzimy z funkcji zwracając pierwszy napotkany wciśnięty przycisk.

Można zadać pytanie po co pisać swój kod, skoro mamy dostępną bibliotekę do obsługi takich klawiatur. Są dwa proste powody:

  • Warto nauczyć się jak sememu zaimplementować taką funkcjonalność,
  • Zaglądając do wnętrza biblioteki możemy zauważyć że jest tam masa kodu, którego prawdopodobnie nigdy nie wykorzystamy – nasz kod zajmie mniej miejsca, co jest bardzo cenne przy programowaniu mikrokontrolerów.