Język C dla mikrokontrolerów 8051. Budujemy klawiaturę.

Język C dla mikrokontrolerów 8051. Budujemy klawiaturę.

Budując urządzenia z mikrokontrolerami wcześniej czy później staniesz przed zagadnieniem rozwiązania sposobu komunikacji z użytkownikiem. We wcześniejszych odcinkach „kursu” zajmowaliśmy się prezentacją wyników za pomocą wyświetlacza LED czy LCD. Czasami jednak trzeba również wprowadzić pewne parametry – na przykład nastawę czasu, wielkość napięcia itp. Możliwe jest tu wykorzystanie różnych technik, lecz jedna z nich przyjęła się u zarania dziejów urządzeń elektronicznych i króluje do dziś. Nic też nie wskazuje na to, aby szybko została wyparta przez inne rozwiązania. Mowa o klawiszu lub grupach klawiszy zwanych klawiaturami.

Ten artykuł w całości będzie poświęcony klawiszom, klawiaturom, będzie przeglądem stosowanych rozwiązań. Zaczniemy od prostych metod testowania naciśnięcia pojedynczego klawisza, poprzez testy dla większej ich liczby aż do klawiatur zbudowanych w formie matrycy, czy działających w oparciu o przerwania.

Testowanie stanu pojedynczego przycisku

Na początek zajmiemy się testowaniem stanu pojedynczego przycisku. Wbrew pozorom nie jest to zadanie łatwe, mimo iż tak na początku może się wydawać. Program musi uwzględnić fizyczne właściwości przycisku, zabezpieczyć się przed przypadkowym jego wciśnięciem czy zewnętrznym zakłóceniem. Funkcja obsługi pojedynczego przycisku ma Ci zdradzić podstawowe zasady odczytu stanu przełącznika. Dalej omawiane funkcje będą tylko rozwinięciem, pewną odmianą, omawianej tutaj funkcji podstawowej. Na rysunku 1 pokazano sposób podłączenia klawisza wykorzystywany w przykładzie. Pojedynczy przycisk został podłączony do mikrokontrolera z rodziny 8051 (AT89C2051). W tym celu użyto linii portu P1.2. Ty możesz zrobić to tak samo, możesz również wykorzystać inną linię portu. Zwróć tylko uwagę na to, czy nie potrzebujesz przypadkiem dołączyć do niej rezystora pullup. Potrzebne informacje można znaleźć w dokumentacji mikrokontrolera. W przykładzie używałem AT89C2051, który posiada wewnętrzny rezystor pullup dołączony do linii P1.2.

schemat 1

Rys. 1. Sposób dołączenia pojedynczego klawisza do mikrokontrolera z 1. przykładu

Przycisk dołączyłem w taki sposób, aby w momencie jego naciśnięcia linia portu P1.2 była zwierana do masy, ponieważ w „normalnej” sytuacji, gdy port pracuje jako wejściowy, linia ta jest zasilania z dodatniego napięcia przez rezystor pullup. W konsekwencji CPU mikrokontrolera testując stan bitu P1.2 odczyta stan wysoki. Można go zmienić (pamiętajmy, że linia pracuje jako wejściowa) podając stan niski, tak aby zmienił się potencjał wymuszany przez rezystor. Podanie stanu niskiego odpowiada zwarciu linii P1.2 do masy. To jeden z powodów: drugi to mogące pojawić się zakłócenia. W ten sposób znacznie łatwiej je wyeliminować, ponieważ impedancja linii w stanie niskim jest wielokrotnie mniejsza niż w stanie wysokim.
Dla pełnego obrazu muszę napisać coś niecoś o właściwościach fizycznych styków przełącznika. Podczas ich zwierania, na skutek właściwości mechanicznych, może pojawić się zjawisko tak zwanego drgania styków. Ilustruje je rysunek 2. Jak łatwo zauważyć, stan niski na wejściu portu pojawi się co najmniej kilkakrotnie i jeśli mikrokontroler będzie wystarczająco szybki a zadanie klawisza polegać ma np. na zwiększeniu wartości o 1 przy jego naciśnięciu, to może okazać się, że pojedyncze naciśnięcie klawisza owocuje przyrostem wartości zmiennej o kilka czy nawet kilkanaście kroków. We współcześnie używanych miniaturowych przełącznikach zjawisko drgania styków zostało prawie całkowicie wyeliminowane, jednak jego występowanie zależeć będzie od konstrukcji mechanicznej przełącznika - ponieważ tej nigdy nie możemy być pewni, lepiej się przed nim zabezpieczyć.

drgania

Rys. 2. Niepożądane drgania styków mikroprzełącznika

Lekarstwem na to zjawisko jest prosta metoda: reakcja na wciśnięty klawisz (czy to opadające zbocze, czy poziom niski  sygnału), odczekanie przez czas 20..50 ms i ponowny odczyt stanu portu. Jeśli nie zmienił się, to oznacza, że klawisz nadal jest wciśnięty i użytkownik żąda reakcji. Jeśli natomiast stan jest różny od niskiego, to oznaczać może, że albo odebrano zakłócenie, albo przypadkowo naciśnięto klawisz. Tę prostą zasadę działania wykorzystuje program umieszczony na listingu 1. Stan klawisza sygnalizowany jest przez bit 3 portu P1. Bit ten przyjmuje poziom logiczny niski, gdy klawisz jest wciśnięty.
Początek programu zawiera deklaracje bitów portu: bit 2 pracuje jako wejściowy – do niego podłączony jest klawisz, bit 3 pracuje jako wyjściowy – jego stan zmienia się w zależności od stanu klawisza. Funkcja void KeyPressed() zawiera polecenia wykonywane w sytuacji, gdy klawisz jest wciśnięty i odwrotnie: funkcja void KeyNotPressed() zawiera polecenia wykonywane, gdy klawisz jest zwolniony. Właściwy odczyt klawisza zdefiniowany został w programie głównym i sprowadza się do testowania stanu pojedynczego bitu portu przy pomocy polecenia if (jeśli). Wyrażenie if (!PortKey) oznacza: jeśli zmienna PortKey ma wartość „nie zero”, to wykonaj polecenia zawarte pomiędzy nawiasami klamrowymi. Czyli: odczekaj 20 milisekund, odczytaj ponownie stan bitu portu. Jeśli nadal jest on równy stanowi niskiemu, to wykonaj funkcję odpowiadającą wciśniętemu klawiszowi. Zadaniem pętli while (!PortKey) jest oczekiwanie aż klawisz zostanie zwolniony.
Zaznaczam – to jest jedna z najprostszych funkcji obsługi klawisza. W większości sytuacji jest wystarczy, ma jednak tę wadę, że po jego wciśnięciu oczekuje na zwolnienie blokując tym samym wykonywanie innych zadań, o ile nie są one obsługiwane przez przerwania.


List. 1. Prosty program odczytujący stan klawisza podłączonego do P1.2

/* prosty program demonstracyjny "odczyt pojedynczego klawisza"
   klawisz włączony pomiędzy masę a port P1.2, stan aktywny = L;
   rezonator kwarcowy 8 MHz */

#include <reg51.h>          //dołączenie definicji rejestrów mikrokontrolera
sbit PortKey = P1^2;        //definicja bitu portu klawisza
sbit Active = P1^3;         //port przyjmuje stan niski, gdy klawisz
                            //jest wciśnięty
//opóźnienie około 1 milisekundy dla kwarcu 8MHz
void Delay(unsigned int time)
{
    unsigned int j;     
    while (time >= 1)       //wykonanie pętli FOR zajmuje około 1 msek.
    {                       //pętla jest powtarzana TIME razy
        for (j=0; j<65; j++);
        time--;
    }
}

//funkcja - reakcja na naciśnięcie klawisza
//tylko ustawienie stanu portu wyjściowego
void KeyPressed(void)
{
    Active = 0;
}

//funkcja - reakcja, gdy klawisz nie jest wciśnięty
//tylko ustawienie stanu portu wyjściowego
void KeyNotPressed(void)
{
    Active = 1;
}

//początek programu głównego
void main(void)
{
    while (1)               //pętla nieskończona
    {
        if (!PortKey)
        {
            Delay(20);      //opóźnienie 20 ms
            if (!PortKey)   //ponowny odczyt klawisz i podjęcie akcji,
            {               //jeśli nadal wciśnięty
                KeyPressed();
                while (!PortKey);   //oczekiwanie na zwolnienie klawisza
            }
        }
        KeyNotPressed();           //akcja, gdy klawisz nie jest wciśnięty
     }
}

 

Automatyczne powtarzanie wciśniętego klawisza

Każdy użytkownik komputera PC zna funkcję „autorepeat” klawiatury. Polega ona na powtarzaniu kodu wciśniętego klawisza. Zrealizujemy taką funkcję w programie dla mikrokontrolera. Ustalmy najpierw metodę. Wydaje mi się, że spośród wielu sposobów obsługi klawiszy, najbardziej naturalnymi są:
- po wciśnięciu - jednokrotne wysłanie kodu wciśniętego klawisza, brak reakcji jeśli klawisz nie został zwolniony,
- po wciśnięciu – wysłanie kodu klawisza i jeśli nie został on zwolniony – powtarzanie kodu klawisza co pewien ustalony odstęp czasu,
- po wciśnięciu – wysłanie kodu klawisza i jeśli nie został on zwolniony – powtarzanie kodu klawisza początkowo wolno a po wykonaniu pewnej liczby powtórzeń – znacznie szybciej.
Teraz wykonamy program, który zrealizuje w praktyce działanie klawiatury w sposób numer 3. Z racji tego, że ilość klawiszy nie ma większego znaczenia dla zrozumienia zasady działania, zanim przejdziemy do bardziej zaawansowanych rozwiązań, posłużymy się (podobnie jak poprzednio) rozwiązaniem wykorzystującym pojedynczy przycisk. Tu jedna uwaga: to tylko propozycja rozwiązania. Pisząc programy często zauważysz, że jeden problem można rozwiązać na kilka możliwych sposobów.
Aby funkcja automatycznego powtarzania naciśniętego klawisza mogła działać, nie wolno zatrzymywać programu i oczekiwać na zmianę stanu przycisku. Mikrokontroler musi nieprzerwanie (nie bierz tego dosłownie) wykonywać program sprawdzając stan klawisza i sprawdzając warunki dla „autorepeat”. W związku z tym nie wolno nam użyć konstrukcji while (!PortKey) z poprzedniego przykładu. Trzeba zastosować zupełnie inną metodę: mój program liczy ilość cykli odczytu stanu przycisku i w zależności od tej liczby podejmuje odpowiednią akcję. Na listingu 2 zamieszczono część programu napisanego w języku C, w którym pozostawiłem tylko naprawdę niezbędne fragmenty kodu źródłowego.

List. 2. Podprogram realizujący automatyczne powtarzanie wciśniętego klawisza

if (!PortKey)                      //czy naciśnięto klawisz?
{
    Counter++;                     //tak, zwiększ licznik
    if (Counter > Time_3)          //czy licznik jest większy od warunku dla
    {                              //szybszego autorepeat?
        KeyPressed();              //tak – wykonaj fragment kodu
        Counter--;
        Delay(100);                //krótsze opóźnienie
    } else
    if (Counter > Time_2)          //czy licznik jest większy od warunku dla
    {                              //wolniejszego autorepeat?
        KeyPressed();              //tak – wykonaj fragment kodu
        Delay(500);                //dłuższe opóźnienie
    } else
    if (Counter == Time_1)         //czy licznik osiągnął wartość, gdy
    {                              //uznajemy klawisz za wciśnięty?
        KeyPressed();              //tak – wykonaj fragment kodu
    }
} else Counter = 0;                //zeruj licznik, jeśli zwolniono klawisz

Na początku program sprawdza stan klawisza. Jeśli jest on wciśnięty, to wartość zmiennej Counter jest inkrementowana i rozpatrywana przez konstrukcję if. W przeciwnym przypadku zmiennej nadawana jest wartość 0.
Zadaniem if jest zdecydowanie jaka akcja zostanie podjęta w zależności od wartości zmiennej Counter. Stała Time_1 to liczba pojedynczych operacji odczytu bitu przycisku, po której uznaję go za wciśnięty. Jej wartość w głównej mierze zależy od szybkości mikrokontrolera i musi być dobrana indywidualnie dla budowanego układu. Stała Time_2, to liczba przebiegów cykli odczytu, dla której uznaję warunek dla „wolniejszego” powtarzania za spełniony. Wykonywany jest wówczas fragment programu zawierający instrukcję Delay(500) powodującą przerwę około 0,5 sekundy pomiędzy powtórzeniami odczytu klawisza. Jeśli wartość zmiennej Counter osiągnie wartość stałej Time_3, to wykonywany jest fragment programu z instrukcją Delay(100): czas pomiędzy powtórzeniami akcji właściwej dla wciśniętego przycisku jest pięciokrotnie krótszy. Instrukcja Counter-- służy do zabezpieczenia przed przekroczeniem wartości dopuszczalnej dla typy zmiennej.
Do automatycznego powtarzania kodu klawisza wrócimy jeszcze przy okazji budowy menu. W tym momencie, proponuję wykorzystać program z listingu 3, aby zobaczyć jak nastawy wartości Time_1, Time_2 i Time_3 wpływają na pracę programu wykonywanego przez  mikrokontroler.
 

List. 3. Program odczytuje stan klawisza podłączonego do P1.2 wyposażony w rozbudowaną funkcję „autorepeat”

/* prosty program demonstracyjny "odczyt pojedynczego klawisza"
   z funkcją automatycznego powtarzania.
   Klawisz włączony pomiędzy masę a port P1.2, stan aktywny = L;
   rezonator kwarcowy 8MHz */

#include <reg51.h>                 //dołączenie definicji rejestrów mikrokontrolera
#define Time_1      900            //time 1 = około 15 ms
#define Time_2      9000           //około 1,5 sekundy
#define Time_3      9020           //po 20 wykonaniach pętli dla Time_2

unsigned int Counter = 0;          //deklaracja zmiennej licznika
sbit PortKey = P1^2;               //definicja bitu portu klawisza
sbit Active = P1^3;                //port przyjmuje stan niski, gdy klawisz jest wciśnięty

//opóźnienie około time x 1 ms dla kwarcu 8MHz
void Delay(unsigned int time)
{
    unsigned int j;
    while (time >= 1)              //wykonanie pętli FOR zajmuje około 1 msek.
    {                              //pętla jest powtarzana TIME razy
        for (j=0; j<65; j++);
        time--;
    }
}

//funkcja - reakcja na naciśnięcie klawisza
//tylko ustawienie stanu portu wyjściowego
void KeyPressed(void)
{
    Active = ~Counter;
}

//funkcja – reakcja, gdy klawisz nie jest wciśnięty
//tylko ustawienie stanu portu wyjściowego
void KeyNotPressed(void)
{
    Active = 1;
}

//początek programu głównego
void main(void)
{
    while (1)                      //pętla nieskończona
    {
        if (!PortKey)
        {
            Counter++;             //tak, zwiększ licznik
            if (Counter > Time_3)  //czy licznik większy od warunku dla
            {                      //szybszego autorepeat?
                KeyPressed();      //tak – wykonaj fragment kodu
                Counter--;

                Delay(100);        //krótsze opóźnienie
            } else;
            if (Counter > Time_2)  //czy licznik większy od warunku dla
            {                      //wolniejszego autorepeat?
                KeyPressed();      //tak – wykonaj fragment kodu
                Delay(500);        //dłuższe opóźnienie
            } else;
            if (Counter == Time_1) //czy licznik osiągnął wartość, gdy
            {                      //uznajemy klawisz za wciśnięty?
                KeyPressed();      //tak – wykonaj fragment kodu
            }
        }
        else;
        {
            Counter = 0;           //licznik jest zerowanym, gdy klawisz
            KeyNotPressed();       //zostaje zwolniony
        }
    }
}

 

Klawiatura zbudowana z 5 przycisków

Najprostszą klawiaturę można zbudować dołączając klawisze w ten sam sposób, co w poprzednim przykładzie, jednak używająca nie pojedynczego przycisku a całego ich szeregu (rysunek 3). Poszczególnym klawiszom można nadać funkcje adekwatne do potrzeb: ja w prezentowanym przykładzie tylko ponumerowałem je od 1 do 5. Ty możesz nazwać klawisze na przykład „dodaj”, „wprowadź”, „pomiar” i temu podobne. Funkcje klawiszy powinny mieć również swoje odzwierciedlenie w programie sugerując nazwy funkcji obsługi.
W poprzednich przykładach zajmowaliśmy się szczegółowo sposobem działania pojedynczego klawisza podłączonego do mikrokontrolera. Ponieważ zasada działania nadal jest taka sama – nie ma większych różnic związanych z odczytem stanu klawiszy. Przytoczę więc przykład programu (listing 4) i zajmę się opisem innych rozwiązań.
Ten program, podobnie jak poprzednio, to przykład pewnego rozwiązania. Jeśli będzie to twoim życzeniem, możesz indywidualnie testować każdy z bitów portu a nawet różnych portów. Wszystko zależy od sposobu połączeń używanego przez Ciebie mikrokontrolera z resztą układu. Z doświadczenia wiem jednak, że ostateczny kształt tzw. hardware mocno wpływa na rozmiar programu. Staram się więc dobierać takie rodzaje połączeń, aby pisany program był jak najmniejszy i aby warunki jego pracy były zoptymalizowane pod kątem szybkości wykonywania i łatwości implementacji kodu.

5 klawiszy

Rys. 3. Jedna z najprostszych metod dołączenia 5 klawiszy do mikrokontrolera.
 
 

List. 4. Program odczytuje stan klawiatury podłączonej do portu PortKey. Jest rozszerzeniem funkcji
odczytu pojedynczego klawisza i działa w bardzo zbliżony sposób.

/* prosty program demonstracyjny "odczyt klawiszy podłączonych do P1"
   klawisz włączony pomiędzy masę a bit portu P1, stan aktywny = L
   klawisze podłączone od P1.2 do P1.6; rezonator kwarcowy 8MHz */

#include <reg51.h>                 //dołączenie definicji rejestrów mikrokontrolera
#include <stdio.h>                 //biblioteka zawierająca funkcję printf()
#define PortKey     P1;            //definicja bitu portu klawisza
unsigned char buf;                 //zmienna przechowująca stan klawiszy

//opóźnienie około 1 ms dla kwarcu 8 MHz
void Delay(unsigned int time)
{
    unsigned int j;
    while (time >= 1)              //wykonanie pętli FOR zajmuje około 1 msek.
    {                              //pętla jest powtarzana TIME razy
        for (j=0; j<65; j++);
        time--;
    }
}

//odczyt klawiatury podłączonej do PortKey
char KbdRead()
{
    unsigned char b;
    b = PortKey;                   //odczytaj stan portu klawiatury
    b |= 3;                        //ustaw dwa najmłodsze, nie używane przez klawiaturę bity
    return (~b);                   //zwróć odczytaną wartość po zanegowaniu
}

//początek programu głównego
void main(void)
{
    while (1)                      //pętla nieskończona
    {
        buf = KbdRead();
        if (buf)                   //jeśli rezultat różny od 0 - sprawdź
        {
            Delay(20);             //opóźnienie 20ms
            if (buf == KbdRead())  //ponowny odczyt klawisza i akcja,
            {                      //jeśli nadal wciśnięty
                if (buf && 0x04) printf(“%s\n”, “Klawisz 1”);
                if (buf && 0x08) printf(“%s\n”, “Klawisz 2”);
                if (buf && 0x10) printf(“%s\n”, “Klawisz 3”);
                if (buf && 0x20) printf(“%s\n”, “Klawisz 4”);
                if (buf && 0x40) printf(“%s\n”, “Klawisz 5”);
            }
        }
    }
}

 

Dołączenie do mikrokontrolera dużej liczby klawiszy

Do tego momentu stosowana przez nas liczba klawiszy była na tyle mała, że nie przekraczała liczby wolnych linii portu wejścia/wyjścia mikrokontrolera. Co zrobić, gdy istnieje potrzeba dołączenia na przykład 8 klawiszy, a do dyspozycji jest tylko 6 wolnych bitów portu? Abstrahując od rozwiązań wykorzystujących układy transmisji szeregowej i dodatkowe rejestry, przedyskutujmy kilka rozwiązań.

Matryca z klawiszy

matryca

Rys. 4. Matryca zbudowana z przycisków to jedno z najprostszych rozwiązań stosowanych przy zwiększaniu liczby klawiszy.

Matryca zbudowana z przycisków to jedno z najprostszych rozwiązań. Przyciski wówczas łączy się tak, aby zwierały umowne wiersze z umownymi kolumnami. Zasadę tę ilustruje rysunek 4. W przykładzie wiersze stanowią bity 5, 6 i 7 portu P1, natomiast kolumny to bity 2, 3 i 4 tego samego portu. Często buduję również układy, w których klawiatura podłączona jest jako zewnętrzna pamięć danych w ten sam sposób z tą różnicą, że kolumny stanowią bity danych a wiersze bity wybranych adresów (lub odwrotnie). Liczbę możliwych do podłączenia w ten sposób klawiszy wylicza się jako wynik mnożenia liczby wierszy przez liczbę kolumn. Wróćmy do schematu z rysunku 4.
Wykorzystujemy 3 bity jako wiersze i 3 jako kolumny. Maksymalnie można więc podłączyć 9 klawiszy wykorzystując 6 bitów portu P1. Prawda, że to duża oszczędność? Nieco bardziej skomplikowane układowo rozwiązanie może wykorzystywać szynę adresową i danych. Typowo, ośmiobitowy mikrokontroler z rodziny 8051 ma możliwość zaadresowania do 64kB pamięci zewnętrznej. Daje to liczbę 16 linii adresowych (A0 do A15). Słowo danych ma długość 8 bitów (D0 do D7). Jeśli nie są wykorzystywane inne urządzenia znajdujące się w przestrzeni adresów zewnętrznych mikrokontrolera, klawiatura może mieć 16 x 8 = 128 (!) klawiszy i pracować jak pamięć zewnętrzna. Oczywiście procedura odczytu tej „pamięci” jest trochę bardziej skomplikowana.
Zasada działania klawiatury zbudowanej z matrycy przycisków jest bardzo prosta. Bity kolumn ustawione są do pracy jako linie wyjściowe, natomiast linie wierszy jako wejściowe. Mikrokontroler ustawia stan niski na jednej z linii kolumn i odczytuje stan linii wierszy. Pojawienie się stanu niskiego na jednym lub kilku bitach wierszy, zawsze oznacza w takiej sytuacji naciśnięcie jednego lub kilku klawiszy. Myślę, że najlepiej pokaże to przykład programu użytkowego z listingu.

 

List. 4. Program obsługi klawiatury matrycowej wyposażony jest w funkcję, która zwraca numer naciśniętego klawisza.

/* prosty program demonstracyjny "odczyt klawiatury matrycowej"
   wiersze P1.2 do P1.4, kolumny P1.5 do P1.6 (3x3 = 9 klawiszy)
   rezonator kwarcowy 8MHz */

#include <reg51.h>                //dołączenie definicji rejestrów mikrokontrolera
#include <stdio.h>                //dołączenie prototypu funkcji printf
#define PortKey     P1           //definicja bitu portu klawisza
#define Column_1    0b11111011   //definicje stanów bitów kolumn
#define Column_2    0b11110111
#define Column_3    0b11101111
#define Dummy       0b00011100   //wartość dla ustawienia bitów kolumn na "1"
#define Row_1       0b00100000   //definicje połączeń bitów wierszy
#define Row_2       0b01000000
#define Row_3       0b10000000

//opóźnienie około 1 milisekundy dla kwarcu 8 MHz
void Delay(unsigned int time)
{
    unsigned int j;
    while (time >= 1)          //wykonanie pętli FOR zajmuje około 1 msek.
    {                          //pętla jest powtarzana TIME razy
        for (j=0; j<65; j++);
        time--;
    }
}

//odczyt portu klawiatury
unsigned char PortKeyRead(unsigned char column)
{
    unsigned char curr, prev;
    PortKey |= Dummy;          //ustawienie na wartość "1" bitów kolumn
    PortKey &= column;         //stan niski kolumny 1
    prev = PortKey;            //odczyt portu klawiatury
    prev &= 0xE0;              //maskowanie wszystkich bitów oprócz wierszy

    if (prev == 0xE0) return (0);     //jeśli wszystkie bity są jedynkami dla
                                 //danej kolumny nie wciśnięto żadnego klawisza
    Delay(20);                 // powtórny odczyt i porównanie
    curr = PortKey;
    curr &= 0b11100000;
    if (curr == prev)
    {
        while (prev = curr)//czekaj na zwolnienia klawisza
        {
            prev = PortKey;
            prev &= 0xE0;
        }
        return(~curr);      //zamiana aktywnych bitów z 0 na 1
    } else return(0);            //zwróć 0 w przypadku różnic odczytów
}

//odczyt klawiatury podłączonej do PortKey
//funkcja zwraca numer wciśniętego klawisza
unsigned char KeyNumber()
{
    unsigned char b;
    b = PortKeyRead(Column_1);       //odczyt: kolumna 1 - wiersz 1, 2, 3
    if (!b)
    {
        if (b && Row_1) return(3);
        if (b && Row_2) return(6);
        if (b && Row_3) return(9);
    }

    b = PortKeyRead(Column_2);       //odczyt: kolumna 2 - wiersz 1, 2, 3
    if (!b)
    {
        if (b && Row_1) return(2);
        if (b && Row_2) return(5);
        if (b && Row_3) return(8);
    }
    b = PortKeyRead(Column_3);       //odczyt: kolumna 3 - wiersz 1, 2, 3
    if (!b)
    {
        if (b && Row_1) return(1);
        if (b && Row_2) return(4);
        if (b && Row_3) return(7);
    }
    return(0);                       //nie wciśnięto żadnego klawisza, zwróć 0
}

//początek programu głównego
void main(void)
{
    while (1)                        //pętla nieskończona
    {
        char buf = KeyNumber();      //odczyt klawisza połączony z deklaracją buf
        if (!buf) printf("%s\n",buf); //wysłanie numeru klawisza przez RS232
       }
}

Właściwa funkcja odczytu klawiatury podzielona jest na dwie części w celu optymalizacji kodu wynikowego. Pierwsza z nich o nazwie PortKeyRead() używana jest wielokrotnie dlatego też stanowi osobny fragment programu. Zatrzymajmy się przy niej na moment. Konstrukcja PortKey |= Dummy ustawia wyjścia portów kolumn na wartość logicznej „1”. Po tym poleceniu, logiczna operacja AND (PortKey &= Column) ustawia stan niski na wyjściu danej kolumny. Teraz należy zbadać, czy na którymś z wejść portu odpowiadającym wierszom klawiatury, pojawił się stan niski. Odczytywany jest więc port klawiatury prev = PortKey i maskowane wszystkie bity, które nie mają wpływu na testowanie stanów wierszy (prev &= 0xE0). Jeśli powstała w ten sposób wartość jest równa 0xE0 oznacza to, że wszystkie wejścia są w stanie wysokim i żadna z linii wejściowych nie jest w stanie niskim. Odpowiada to sytuacji, w której nie wciśnięto żadnego klawisza - wówczas funkcja zwraca wartość 0.
Inaczej jest w sytuacji, gdy wartość jest różna od 0xE0. Wykonywana jest wówczas instrukcja Delay(20) powodująca opóźnienie na czas około 20 milisekund. Po niej ponownie odczytywany jest z użyciem tej samej metody stan klawiatury. Odczytana wartość porównywana jest ze starą i jeśli są zgodne oznacza to, że klawisz jest nadal wciśnięty. Aby uniknąć automatycznego powtarzania, program czeka na zwolnienie klawisza a następnie zwraca zanegowany stan bitów wierszy. W przeciwnym wypadku uznaję, że było to przypadkowe naciśnięcie (ewentualnie zakłócenie) i funkcja zwraca wartość 0.
PortKeyRead()wykorzystywana jest przez funkcję KeyNumber(). Jej rolą jest obliczenie numeru naciśniętego klawisza. Kolejno odczytywane są poszczególne kolumny i jeśli zwrócona wartość jest różna od 0, rozpatrywane są bity zwróconej zmiennej. Tak więc wiersz po wierszu, kolumna po kolumnie przeglądana jest cała klawiatura. Metoda ta nosi nazwę odpytywania (z angielskiego pooling).
Program główny wywołuje funkcję KeyNumber() i używa jej do wyznaczenia numeru wciśniętego klawisza. Można w nim spotkać konstrukcję char buf = KeyNumber() – nie używałem jej do tej pory. Zmienna w języku C musi być zadeklarowana przed pierwszym użyciem. To jest właśnie dosłowny przykład, jednak nie polecam go do użytku. W pewnym momencie możesz przestać panować na ilością zmiennych i przez to również wykorzystaniem zasobów mikrokontrolera. Lepsza jest jawna deklaracja na początku programu czy funkcji, aniżeli ukryta w kodzie programu. Nie mniej jednak dobrze jest wiedzieć, że można to zrobić. Zwrócony numer klawisza zamieniany jest przez funkcję printf() na wartość typu łańcuch znaków i wysyłany przy pomocy UART.

 

Wykorzystanie dodatkowego układu multipleksera

Łatwą metodą rozszerzenia ilości podłączonych do mikrokontrolera klawiszy jest użycie dodatkowego zewnętrznego układu multipleksera. Przykład takiego rozwiązania umieszczono na rysunku 5.

74157

Rys. 5. Wykorzystanie zewnętrznego układu multipleksera dla zwiększenia liczby klawiszy dołączonych do portu mikrokontrolera.

Wykorzystuje ono układ 74157. Jest to multiplekser 4 z 8. Poziom logiczny na wyprowadzeniu 1 (A/B) wybiera które z czterech wejść (1A..4A / 1B..4B) podłączane są do wyjść układu (1Y..4Y) a tym samym do portu wejściowego mikrokontrolera. Proste rozwiązania lokalnych klawiatur nie wymagają rezystorów pullup, ponieważ układy z serii TTL traktują wejście nie podłączone jako znajdujące się w stanie wysokim. W celu wybrania odpowiedniej czwórki przycisków, posługuję się wyprowadzeniem P3.7 mikrokontrolera. Dwie czterobitowe połówki składane są do postaci jednego bajtu, który to następnie rozpatrywany jest przez program.

 

List. 5. Program do odczytu klawiszy podłączonych przy pomocy multipleksera 74157.

/* prosty program demonstracyjny "odczyt klawiszy podłączonych przez 74157"
   wykorzystane są bity P1.0 do P1.3 oraz P3.7 do sterowania wyborem
   połówki odczytywanego bajtu; rezonator kwarcowy 8MHz */

#include <reg51.h>                   //dołączenie definicji rejestrów mikrokontrolera
#include <stdio.h>                   //biblioteka zawierająca funkcję printf()
#define PortKey P1;                  //definicja bitu portu klawisza
sbit A_B = P3^7;                     //wybór połówki bajtu odczytywanej przez 74157

//opóźnienie około 1 milisekundy dla kwarcu 8MHz
void Delay(unsigned int time)
{
    unsigned int j;
    while (time >= 1)                //wykonanie pętli FOR zajmuje około 1 msek.
    {                                //pętla jest powtarzana TIME razy
        for (j=0; j<65; j++);
        time--;
    }
}

//odczyt klawiatury podłączonej do PortKey
char KbdRead()
{
    unsigned char L_nibble, H_nibble;
    A_B = 1;                         //odczyt starszej połówki bajtu (klawisze parzyste)
    H_nibble = PortKey;
    H_nibble &= 0x0F;
    H_nibble <<= 4;
    A_B = 0;                         //odczyt młodszej połówki bajtu (klawisze niep.)
    L_nibble = PortKey;
    L_nibble &= 0x0F;
    return (~(H_nibble | L_nibble)); //zwróć odczytaną wartość po zanegowaniu
}

//początek programu głównego
void main(void)
{
    while (1)                        //pętla nieskończona
    {
        buf = KbdRead();
        if (buf)                     //jeśli rezultat różny od 0- sprawdź
        {
            Delay(20);               //opóźnienie 20ms
            if (buf == KbdRead())    //ponowny odczyt klawisza i podjęcie
                                     //akcji, jeśli nadal wciśnięty
            {
                if (buf && 0x01) printf("%s\n", "Klawisz 1");
                if (buf && 0x02) printf("%s\n", "Klawisz 2");
                if (buf && 0x04) printf("%s\n", "Klawisz 3");
                if (buf && 0x08) printf("%s\n", "Klawisz 4");
                if (buf && 0x10) printf("%s\n", "Klawisz 5");
                if (buf && 0x20) printf("%s\n", "Klawisz 6");
                if (buf && 0x40) printf("%s\n", "Klawisz 7");
                if (buf && 0x80) printf("%s\n", "Klawisz 8");
            }
        }
    }
}

 

Przerwania i klawiatura

Dotychczas we wszystkich prezentowanych przykładach obsługi klawiatury wykorzystywałem technikę zwaną odpytywaniem lub przeglądaniem (z ang. pooling). Teraz omówię przykład klawiatury, która obsługiwana będzie na żądanie, po naciśnięciu klawisza. Do jej konstrukcji wykorzystam mikrokontroler z rodziny 8051 oraz przerwanie zewnętrzne INT0. Zatrzymajmy się przez chwilę przy rysunku 6 , na którym pokazano schemat ideowy klawiatury pracującej w oparciu o przerwania.
Podobnie jak poprzednio wykorzystujemy przyciski zajmujące bity od 0 do 4 portu P1. W sumie daje to pięć klawiszy. Identycznie jak w poprzednich przykładach stanem aktywnym jest stan niski – jego pojawienie się oznacza wciśnięcie klawisza. Do poszczególnych bitów portu podłączone są rezystory pullup, których zadaniem jest podniesienie odporności na zakłócenia. Jeśli używasz niewielkiej, lokalnej klawiatury podłączonej blisko układu mikrokontrolera (przewód nie dłuższy niż 15 .. 20 cm) – możesz ich nie stosować. Wejścia portów podłączone są do wejść ośmiowejściowej bramki NAND – 7430. Jej zadaniem jest wygenerowanie sygnału przerwania w momencie naciśnięcia klawisza. Na wyjściu bramki NAND (negacja iloczynu) pojawia się jednak sygnał o złej fazie: w sytuacji, gdy wszystkie wejścia bramki są w stanie wysokim – nie jest wciśnięty żaden z klawiszy -  wyjście bramki jest w stanie niskim. Gdy na dowolnym z wejść pojawi się stan logiczny niski, to wyjście bramki przyjmuje stan wysoki. Odpowiada to narastającemu zboczu sygnału na wejściu przerwania INT0 po wciśnięciu klawisza. Opadające zbocze, które wyzwala przerwanie (jest to związane z zasadą działania mikrokontrolera z rodziny 8051, który wymaga opadającego zbocza lub poziomu niskiego aby uruchomić procedurę obsługi przerwania), pojawi się dopiero po zwolnieniu przycisku. Niestety, jest to zbyt późno, ponieważ nie będziemy w stanie testować, który przycisk został wciśnięty. W związku z tym wymagane jest użycie układu inwertora dla odwrócenia fazy sygnału. Faktycznie realizowana jest więc funkcja AND a nie NAND.

Rys. 6. Klawisze generujące sygnał przerwania po naciśnięciu.

Można również pokusić się o wykonanie bramki AND przy pomocy diod. Jest to rozwiązanie bardzo oszczędne jeśli rozpatrywać je pod kątem kosztu użytych do konstrukcji podzespołów. Może nie tak „eleganckie” jak z wykorzystaniem układów scalonych, jednak tanie i równie skuteczne.
Nowatorska jest w tym przykładzie tylko metoda. Nie angażujmy mikrokontrolera w przeglądanie stanu klawiatury, zajmujemy się nią tylko wówczas, gdy jest to konieczne. Na życzenie możliwe jest również wyłączenie obsługi klawiszy poprzez prostą blokadę przyjmowania przerwania z INT0 (zerowanie bitu IE0). Metoda ta jak każda ma swoje wady i zalety: nie będziemy o nich tu dyskutować, zajmijmy się programem obsługi.

irq2

Rys. 7. Bramka AND zbudowana z diod w celu wygenerowania sygnału przerwania po wciśnięciu klawisza.

 

List. 6. Tak może wyglądać program obsługujący klawiaturę w przerwaniu INT0.

/* prosty program demonstracyjny "przerwanie generowane po wciśnięciu klawisza"
   wykorzystane są bity P1.0 do P1.4 oraz wejście zewnętrznego przerwania INT0
   rezonator kwarcowy 8MHz */

#include <reg51.h>                   //dołączenie definicji rejestrów mikrokontrolera
#include <stdio.h>                   //prototyp funkcji printf
#define PortKey P1                   //definicja bitu portu klawisza
#define Key_1 0b00000001             //maski dla poszczególnych bitów klawiszy
#define Key_2 0b00000010
#define Key_3 0b00000100
#define Key_4 0b00001000
#define Key_5 0b00010000
#define mask 0b00011111              //maska bitów klawiatury dla operacji logicznych
unsigned char status = 0;            //status, ustawiany w obsłudze przerwania

//odczyt klawiatury podłączonej do PortKey
//jest to funkcja obsługi przerwania używająca banku rejestrów 1
void KbdRead() interrupt 1 using 1
{
    int i;
    unsigned char p;
    p = PortKey;                     //odczyt bitów portu
    p = ~p & mask;
    for (i=0; i<1300; i++);          //pauza 20 ms dla rezonatora 8MHz
    status = PortKey;                //ponowny odczyt bitów klawiatury dla weryfikacji
    status = ~status & mask;
    if (p != status) status = 0;     //ustawienie zmiennej status
}

//początek programu głównego
void main(void)
{
    PortKey |= ~mask;                //ustawienie linii klawiatury
    EX0 = 1;                         //zezwolenie na przyjmowanie przerwań INT0
    EA = 1;                          //załączenie przerwań
    while (1)                        //pętla nieskończona
    {
        if (status)
        {
            if (status && Key_1) printf("%s\n","Klawisz 1");
            if (status && Key_2) printf("%s\n","Klawisz 2");
            if (status && Key_3) printf("%s\n","Klawisz 3");
            if (status && Key_4) printf("%s\n","Klawisz 4");
            if (status && Key_5) printf("%s\n","Klawisz 5");
            status = 0;
        }
    }
}

Najważniejszą częścią programu jest funkcja obsługująca przerwanie. Zajmuje się ona rozpoznaniem, który z klawiszy został naciśnięty, ustawia bity zmiennej status. Podobnie jak poprzednio funkcja printf wysyła korzystając z UART numer wciśniętego klawisza.

 

Jacek Bogusz

j.bogusz@easy-soft.net.pl

http://www.tomaszbogusz.blox.pl/

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.