Klawiatura powstała przez rozbudowę wyświetlacza matrycowego, który omawialiśmy w poprzednim odcinku kursu. Korzystając z zaledwie czterech dodatkowych sygnałów Rows[3:0], mamy możliwość podłączenia aż 32 przycisków!
Wyświetlaczem steruje moduł DisplayMultiplex z poprzedniego odcinka kursu. Steruje on katodami wyświetlaczy Cathodes[7:0], uaktywniając każdą z nich na pewien krótki okres czasu. Stan wysoki powoduje otwarcie tranzystora i zaświecenie tych segmentów, do których doprowadzono stan wysoki poprzez sygnały Segments[A-P]. Moduł aktywuje po kolei wszystkie cyfry wyświetlacza, zaczynając od zerowego i kończąc na siódmym.
Wejścia Rows[3:0] dostarczają układowi FPGA informacji o tym, który przycisk jest wciśnięty. Normalnie są w stanie niskim. Ten stan jest wymuszony przez rezystory R30...R33. W przypadku MachXO2 można te rezystory pominąć i użyć wewnętrznych rezystorów pull-down dostępnych wewnątrz FPGA (można je uaktywnić w narzędziu Spreadsheet). Wciśnięcie przycisku spowoduje przejście odpowiadającego mu sygnału Rows[3:0] w stan wysoki, ale tylko wtedy, kiedy stan wysoki jest również na odpowiadającej mu katodzie wyświetlacza.
Musimy opracować moduł, który będzie obserwował sygnały Cathodes[7:0] oraz Rows[3:0] i w rezultacie będzie informował o tym, że jakiś przycisk został wciśnięty oraz jaki jest numer wciśniętego przycisku (w formacie szesnastkowym).
Tworząc nowy projekt w programie Diamond, utwórz implementację o nazwie Combo. Dodaj do niej następujące pliki:
- top.v
- display_multiplex.v
- decoder_7seg.v
- strobe_generator.v
- matrix_keyboard.v
Moduły DisplayMultiplex, Decoder7seg i StrobeGenerator omawialiśmy szczegółowo w poprzednich odcinkach kursu i nie będziemy ich tutaj omawiać ponownie. Po tym, jak napiszemy i przetestujemy moduły top oraz MatrixKeyboard, skopiujemy całość i utworzymy nową implementację o nazwie StateMachine. Druga implementacja będzie różnić się zawartością pliku matrix_keyboard.v. Na końcu porównamy maksymalną częstotliwość sygnału zegarowego dla obu implementacji oraz jakie mają zapotrzebowanie na zasoby sprzętowe.
MatrixKeyboard
Moduł MatrixKeyboard obsługuje 32 przyciski, zorganizowane w matrycę 8 kolumn i 4 rzędów. Można by się pokusić o napisanie tego modułu w sposób sparametryzowany, aby obsługiwać dowolną liczbę rzędów i kolumn przycisków. Stwierdziłem jednak, że kod będzie prostszy do zrozumienia, jeżeli liczba kolumn i rzędów zostanie ustawiona na sztywno.
W drugim bloku always analizujemy stan bufora (linia #3). Jest to dość długie drzewo decyzyjne if-else, badające po kolei wszystkie bity w buforze. Jeżeli któryś z nich ma stan wysoki, to znaczy, że przycisk jest wciśnięty. W takiej sytuacji do rejestru wyjściowego KeyCode wpisujemy numer wciśniętego przycisku oraz ustawiamy KeyPressed w stan wysoki. Jeżeli wszystkie bity w buforze mają stan niski, oznacza to, że żaden przycisk nie jest wciśnięty. Wtedy zmienna KeyPressed jest ustawiana w stan niski, a stan KeyCode pozostaje niezmieniony (linia #4).
Zwróć uwagę, że zastosowana logika prowadzi do syntezy enkodera priorytetowego. Oznacza to, że przyciski o niższym numerze są ważniejsze niż te, o numerze wyższym. Inaczej mówiąc, jeżeli zostaną wciśnięte dwa przyciski jednocześnie, to do zmiennej KeyCode zostanie wpisany kod przycisku o niższym numerze. Dzieje się tak dlatego, że w pierwszej kolejności sprawdzany jest bit zerowy bufora, a jeżeli jest on w stanie niskim, to wtedy instrukcjami else if sprawdzane są kolejne bity. Można zamienić kierunek priorytetu, aby sprawdzać bity od najstarszego do najmłodszego, jeżeli byłaby taka potrzeba.
Spróbuj zmodyfikować kod w taki sposób, aby zamiast długiej listy warunków if-else zastosować pętlę for. Wykorzystaj fakt, że iterator pętli jest jednocześnie numerem bitu w rejestrze KeyBuffer i jest tą samą liczbą, jaka jest wpisywana do zmiennej KeyCode.
Aby uzyskać sygnał KeyStrobe, który jest ustawiany w stan wysoki na jeden cykl zegara po wciśnięciu przycisku, musimy zaimplementować konstrukcję nazywaną wykrywaczem zbocza (edge detector). W tym celu musimy utworzyć przerzutnik KeyPressedPrevious, który będzie przechowywać stan sygnału KeyPressed, jaki był w poprzednim takcie zegara (linia #5).
W trzecim, bardzo prostym bloku always, po prostu kopiujemy stan KeyPressed do zmiennej KeyPressedPrevious. Rozpoznawanie przejścia sygnału KeyPressed w stan wysoki sprawdzane jest w linii #7. Jeżeli w poprzednim cyklu zegara zmienna KeyPressedPrevious miała stan niski i jednocześnie KeyPressed ma stan wysoki to warunek z linii #7 zostanie spełniony i na wyjściu KeyStrobe pojawi się stan wysoki tylko na jeden cykl zegara. W kolejnym cyklu do KeyPressedPrevious zostanie wpisany stan wysoki, co sprawi, że warunek już przestanie być prawdziwy, więc KeyStrobe zostanie ustawiony w stan niski.
W zrozumieniu kodu z listingu 1 mogą być przydatne przebiegi zaprezentowane na rysunku 3. Jest to sytuacja z wciśniętym przyciskiem numer 6.