Kurs FPGA – #9 – drgania styków, automaty cd.

Kurs FPGA – #9 – drgania styków, automaty cd.

Kolejnym projektem realizowanym podczas kursu podstaw FPGA będzie jeszcze większy automat skończony. Tym razem wykorzystamy przyciski, więc konieczne będzie również eliminowanie zjawiska drgań styków.

Oprócz tego zajmiemy się podziałem projektu na moduły. Dzięki czemu kod będzie czytelniejszy.

Tworzenie większego projektu w VHDL

Nowy projekt będzie wykorzystywał trzy przyciski tact switch (SW1, SW2, SW3) jako wejścia oraz 4 diody świecące jako wyjścia. Użycie tego typu przycisków wymaga implementacji mechanizmu, który pozwoli na eliminację drgań styków. Jest to konieczne, ponieważ zestaw Elbert nie posiada filtrów RC, które mogłoby eliminować drgania w sposób sprzętowy.

Diagram opisujący działanie układu widoczny jest poniżej:

Przycisk SW1 służy do zapalania kolejnych diod (w kierunku od D8 do D5), natomiast SW2 będzie "przesuwał" punkt świetlny w kierunku przeciwnym (od D5 do D8). Przycisk SW3 zostanie wykorzystany do resetu, jego wciśnięcie przywróci układ do stanu początkowego.

Struktura projektu w VHDL

Tym razem przygotujemy projekt hierarchicznie. Stworzymy nadrzędną część projektu, która wykorzysta w swoim wnętrzu inny moduł. Dzięki temu zwiększy się czytelność projektu, a fragmenty aktualnego programu będzie można łatwo wykorzystać w przyszłości.

Hierarchia docelowego projektu

Hierarchia docelowego projektu

Moduł nadrzędny

Zacznijmy od stworzenia nowego projektu z modułem VHDL o nazwie "top_module". Plik ten będzie przechowywał kod VHDL dla nadrzędnego poziomu naszej aplikacji. Generalnie zaleca się zarówno tworzenie jak i analizę całego projektu właśnie od nadrzędnego modułu. Jest pewnym zwyczajem, że nazywa się go jako top_module (z ang. moduł nadrzędny).

Zestaw elementów do kursu

 999+ pozytywnych opinii  Gwarancja pomocy  Wysyłka w 24h

Zestaw uruchomieniowy Elbert v2 - Spartan 3A z wszystkimi niezbędnymi peryferiami do wykonania ćwiczeń z kursu FPGA!

Zamów w Botland.com.pl »

Zawartość naszego pliku top_module.vhd będzie przedstawiała się następująco: zaczynamy od dodawania bibliotek podobnie jak w poprzednich projektach:

Poniżej zawartość naszego entity. Mamy tutaj sygnał zegarowy, 3 przyciski oraz 4 diody.

Wewnątrz bloku Architecture w obszarze sygnałów lokalnych definiujemy stany identycznie jak robiliśmy to w poprzedniej części kursu:

Dla przypomnienia, deklarujemy tutaj nowy typ sygnału, który może przyjmować wartości takie jak: S0, S1, S2 i S3. W drugiej linijce tworzymy sygnał Stan naszego nowo utworzonego typu z wartością początkową S0.

Poniżej, wciąż w obszarze deklarowania sygnałów lokalnych naszej architecture, deklarujemy następujące sygnały:

  • reset - wykorzystywany w celu przywrócenia stanu domyślnego naszej aplikacji,
  • sygnały "przycisniecie_SW1" oraz "przycisniecie_SW2" jako sygnały oznaczające naciśnięty przycisk (po filtracji drgań styków) - czym zajmiemy się trochę dalej.

Następnie dodajemy blok component, który będzie opisywał jakimi wejściami i wyjściami dysponuje nasz moduł podrzędny nazwany jako "debouncer".

Blok component pojawił się już podczas omawiania tworzenia symulacji dla projektu w VHDL. Za jego pomocą deklarujemy, że mamy styczność z zewnętrznym modułem, z którym nasz moduł nadrzędny będzie współpracował.


Tuż za słowem kluczowym begin naszego architecture umieszczamy blok port map. Jego działanie najlepiej zobrazować za pomocą pewnej analogii. Wyobraźmy sobie płytkę PCB, która ma pełnić pewne funkcje. Nasze moduły VHDL byłyby układami scalonymi umieszczonymi na tej płytce. Blok component mówi jakie scalaki tam będą i jakie podstawki będziemy potrzebować. Blok z instrukcją port map opisuje jak mają być podłączone poszczególne układy (który pin danego układu, do której ścieżki na PCB). Blok port map może zostać umieszczony wielokrotnie. Odpowiadałoby to kilku chipom tego samego typu na płytce.

W opisywanym przypadku bloki "port map" powinny wyglądać następująco:

Na początku podaje się nazwę danej "instancji", tzn. obiektu typu, podanego po dwukropku. W naszym przypadku debouncer. My mamy dwa obiekty debouncer_SW1 oraz debouncer_SW2. Poniżej nazwy umieszcza się słowo kluczowe "port map". W nawiasie wpisuje się instrukcje z przypisaniami do wejść i wyjść modułu podrzędnego (po lewej stronie), a wejściami/wyjściami modułu nadrzędnego (po prawej stronie przypisań). Warto zwrócić uwagę, że przypisania są zakończone przecinkami z wyjątkiem ostatniego - zakończonego nawiasem i średnikiem.


W dalszej części naszego modułu nadrzędnego przypisujemy stan przycisku SW3 do sygnału reset. Będzie on potrzebny w maszynie stanów do przywracania stanu domyślnego.

Następnie umieszczamy blok procesu nasza_maszyna_stanow, który opisuje działanie układu z wcześniejszego diagramu:

Na początku tego procesu przed słowem kluczowym begin zostały zawarte deklaracje zmiennych (ang. variable) dla danego procesu. Blok proces daje możliwość deklaracji własnych zmiennych, jednak mogą być one wykorzystywane wyłącznie wewnątrz bloku process.

W naszym przykładzie:

Zadeklarowaliśmy zmienną o nazwie flaga_nacisniecia_SW1, integer oznacza liczbę całkowitą. Zmienna została zainicjalizowana wartością zero. W naszym procesie zadeklarowaliśmy dwie takie zmienne dla każdego z przycisków.

W naszym przypadku zmienne wewnątrz procesów są wykorzystywane jako flagi do detekcji zbocza naciskania przycisków. Wykorzystaliśmy je, ponieważ w innym przypadku, gdybyśmy samo naciśnięcie przycisku użyli do przejścia do następnego stanu to układ przechodziłby ze stanu do stanu przy każdej zmianie sygnału zegarowego. Przy naszej częstotliwości 12 MHz dałoby to efekt taki, że wszystkie diody byłyby zapalone "jednocześnie".

W naszej maszynie stanów dla każdego przypadku do przejścia do następnego stanu wymagane jest pojawienie się zbocza "naciskania przycisku" oraz "zwolnienia przycisku". Powoduje to przejście do następnego stanu oraz wyzerowanie flagi. W ten sposób by przejść do kolejnego stanu należy jeszcze raz nacisnąć przycisk.

Na końcu, tuż przed end Behavioral umieszczamy przypisania stanów do diod:

Instrukcje te są analogiczne jak w poprzednim projekcie z maszyną stanów.

Gotowy kod modułu nadrzędnego

Gotowy plik "top_module" VHDL powinien wyglądać następująco:

Tworzenie modułu podrzędnego

Plik z modułem nadrzędnym jest już gotowy. Teraz wewnątrz projektu tworzymy nowy plik klikając PPM w hierarchii projektu New Source > VHDL Module. Plik ten nazywamy "debouncer".

Zaczynamy od bibliotek. Przyda nam się nowa: IEEE.STD_LOGIC_UNSIGNED.ALL, która Umożliwi nam porównanie sygnału typu std_logic_vector z wartością typu integer tzn. zwykłą liczbą wyrażoną w systemie dziesiętnym.

Wewnątrz modułu podrzędnego umieszczamy blok entity. Ogólnie rzecz ujmując, nasz moduł podrzędny mógłby również działać jako samodzielny, stąd jego własny blok entity, który wygląda następująco:

Projekt będzie wykorzystywał dwie instancje debouncera (takie mieliśmy założenia projektu). Każda z tych instancji będzie miała wejście Switch oraz wyjście odfiltrowany_przycisk. Oba te sygnały są pojedyncze - nie ma potrzeby stosowania typu std_logic_vector.

Poniżej w bloku architecture w obszarze deklarowania sygnałów lokalnych naszej architecture deklarujemy następujący sygnał:

Za słowem kluczowym begin naszego architecture umieszczono proces "Debouncer_przycisku", którego kod znajduje się poniżej. Każdy przycisk po jego naciśnięciu powoduje, że sygnał napięcia widziany przez mikrokontroler lub układ FPGA jest nieregularny i ma postać jak na poniższym rysunku:

Drgania styków w praktyce.

Drgania styków w praktyce.

Na rysunku widać wyraźnie, że podczas naciskania i zwalniania przycisku napięcie zmienia się gwałtownie kilka razy między wartościami maksymalnymi. Jakie konsekwencje ma to zjawisko? Otóż takie, że gdyby układ cyfrowy reagował na przyciśniecie przycisku jako na narastanie sygnału napięciowego to mógłby zinterpretować pojedyncze wciśnięcie tak jakby było ono wykonane wielokrotnie. W naszym projekcie oznaczałoby to, że po jednokrotnym wciśnięciu nasz układ przeskakiwałby naraz kilka stanów. Jak sobie z tym poradzić? Sposoby dzieli się na sprzętowe (np. filtr RC) oraz na programistyczne. My zajmiemy się teraz drugą opcją.

W momencie rozpoczynania się zmian sygnału wejściowego istnieje możliwość "przeczekania" zjawiska i ponownego sprawdzenia, czy przycisk jest naciśnięty, gdy sygnał będzie już stabilny. Poniżej blok kodu, który implementuje filtracje drgań styków w kodzie VHDL. Umieszczamy go tuż za słowem kluczowym begin w naszym architecture.

Działa to na następującej zasadzie: w momencie naciśnięcia przycisku następuje zliczanie sygnału "licznik" w dół. W momencie, gdy sygnał napięciowy z przycisku jest jeszcze w obszarze nieustalonym sygnał "licznik" będzie ustawiany na wartość maksymalną. Dopiero gdy sygnał napięciowy będzie stabilny sygnał "licznik" będzie zliczony do zera (zwróć uwagę na instrukcje, gdzie jest on zmniejszany o jeden).

Poniżej procesu Debouncer_przycisku umieść poniższą instrukcje:

W tym miejscu następuje przypisanie odpowiedniej wartości do sygnału "odfiltrowany_przycisk", czyli sygnału niosącego informacje na temat tego czy przycisk został już naciśnięty i czy znajduje się w stanie stabilnym. Przyjmuje on wartość logiczną 1 dopiero w momencie, gdy licznik osiągnie wartość 0, tzn. wszystkie jego bity będą wyzerowane. Moglibyśmy to również zapisać w poniższy sposób, jednak byłoby to mniej czytelne:

By użyć skróconego zapisu musieliśmy użyć biblioteki IEEE.STD_LOGIC_UNSIGNED.ALL, gdyż porównujemy wartość sygnału typu std_logic_vector z typem dziesiętnym typu integer.

Działanie kodu z filtracją drgań styków można przedstawić na poniższym przebiegu:

Filtrowanie drgań styków na FPGA w praktyce.

Filtrowanie drgań styków na FPGA w praktyce.

Jak widzisz układ po osiągnięciu przez przycisk stanu ustalonego poczeka jeszcze chwile by upewnić się, że ma na pewno do czynienia ze stanem ustalonym.

Jak długo powinna trwać ta "chwila"? Długość oczekiwania zależy od jakości i typu przycisków. Często  jest też ustalana doświadczalnie. W naszym przypadku ustawiliśmy długość licznika na 20 bitów (deklaracja sygnału licznik1). To oznacza, że do jego wyzerowania będzie potrzebne około miliona cykli zegarowych (2^20 = 1 048 576). Przy naszym sygnale zegarowym wynoszącym 12 MHz da to około 1/12 sekundy, czyli około 83 ms. Jest to wartość wystarczająca i podana z dużym zapasem, gdyż typowy czas drgania styków wynosi około 20 ms.

Gotowy kod debouncera w VHDL

Poniżej znajduje się zawartość pliku debouncer.vhd:

Plik UCF

Podobnie jak w poprzednich projektach konieczne jest dodanie pliku UCF. Można stworzyć go samodzielnie lub skopiować poniższy kod:

Pora uruchomić syntezę modułu top_module i sprawdzić jak układ działa w praktyce:

Wygląda na to, że wszystko działa poprawnie! Stany zmieniają się dopiero, gdy puścimy przycisk. Dobrym ćwiczeniem dla chętnych będzie rozbudowanie układu w taki sposób, aby wykorzystał wszystkie 8 diod świecących.

Podsumowanie

Za nami kolejny automat skończony, tym razem trochę większy. Głównym celem tej części było jednak pokazanie tego jak można organizować większe projekty i tworzyć podrzędne moduły. W kolejnej części kursu podstaw FPGA zajmiemy się zupełnie inną metodą budowania układów cyfrowych. Skorzystamy z edytora graficznego, który pozwoli na rysowanie połączeń w FPGA!

Nawigacja kursu

Autor kursu: Adam Bemski
Redakcja: Damian Szymański
Ilustracje, testy: Piotr Adamczyk

O autorze: Adam Bemski

Adam Bemski
Autorem kursu jest Adam Bemski, specjalista od systemow wbudowanych. Pracuje w obszarze automatycznego testowania urządzeń z funkcjonalnością IoT. Adam dodatkowo prowadzi zajęcia z techniki mikroprocesorowej na wyższej uczelni DHBW Stuttgart. Więcej szczegółów o Adamie na blogu adambemski.com.

debounce, drgania, kursFPGA, styków, vhdl

Trwa ładowanie komentarzy...