KursyPoradnikiInspirujące DIYForum

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).

Gotowe zestawy do kursów Forbota

 Komplet elementów  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:

-- dodajemy biblioteki
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

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

entity top_module is
Port ( Clk : in  std_logic;   -- sygnal zegarowy jest uzywany w blokach process
       Switch : in  std_logic_vector(2 downto 0);  -- wykorzystamy trzy przyciski typu Switch
       LED : out  std_logic_vector(3 downto 0));  -- 4 diody LED jako wyjscia
end top_module;

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

-- ponizsza instrukcja definiuje nowy rodzaj sygnalu, ktory moze przyjac jedynie te wartosci,
-- ktore sa wymienione w nawiasie - konstrukcja ta jest podobna do definicji enum znanej z jezyka programowania C
type typ_stanu is (S0, S1, S2, S3);   -- Definiowanie stanow

signal Stan : typ_stanu := S0; -- Tworzenie sygnalu - zmiennej mogacej przyjmowac wartosci zdefiniowane w typ_stanu;

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.
-- sygnal reset bedzie wykorzystany do przywrocenia domyslnego stanu naszego ukladu
signal reset : std_logic;

-- deklarujemy sygnaly, ktore przejma stany z modulu przeprowadzajacego debouncing;
signal przycisniecie_SW1 : std_logic;
signal przycisniecie_SW2 : std_logic;

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 sluzy do informowanie modulu nadrzednego (naszego top_module.vhd) 
-- jakimi typami wejsc i wyjsc dysponuje nasz podmodul - debouncer.
component debouncer is
Port ( Clk : in  std_logic;   -- sygnal zegarowy jest uzywany w bloku process
       Switch : in  std_logic; -- wejsciem naszego debouncera jest przycisk
       odfiltrowany_przycisk : out std_logic); -- to jest wyjscie okreslajace stan stabilny przycisniecia przycisku po filtracji debouncing;
end component;

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:

-- ponizszym bloku port jest przeprowadzane instancjonowanie debouncera przycisku SW1,
-- tzn. przypisanie wejsc i wyjsc modulu podrzednego z wejsciami i wyjsciami z modulu nadrzednego;
debouncer_SW1 : debouncer 
port map (Clk => Clk, 
          Switch => Switch(0), 
          odfiltrowany_przycisk => przycisniecie_SW1);

-- analogiczny blok 'port map' dla drugiego debouncera dla przycisku SW2
debouncer_SW2 : debouncer 
port map (Clk => Clk, 
          Switch => Switch(1), 
          odfiltrowany_przycisk => przycisniecie_SW2);

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.

-- przpyisanie przycisku SW3 do lokalnego sygnalu reset
reset <= not Switch(2);

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

-- ten proces odpowiada za funkcjonalnosc naszej aplikacji opisanej diagramem stanow
   -- blok proces jest uruchamiany poprzez kazda zmiane sygnalu zegarowego lub reset
   nasza_maszyna_stanow : process (Clk, reset) 

   -- tutaj jest blok deklaracji "zmiennych" lokalnych wykorzystywanych wylacznie wewnatrz procesu
   -- zadeklarowane ponizej zmienne sluza do detekcji zbocza - naciskania przycisku
   variable flaga_nacisniecia_SW1 : integer := 0;
   variable flaga_nacisniecia_SW2 : integer := 0;

   begin 
      -- ponizsze instrukcje beda wykonane po nacisnieciu SW3 - reset
      if (reset = '1') THEN
         flaga_nacisniecia_SW1 := 0;
         flaga_nacisniecia_SW2 := 0;
         Stan <= S0; -- po nacisnieciu reset - stan zostanie wyzerowany do S0

      -- dla kazdego narastajacego zbocza zegarowego...
      elsif rising_edge(Clk) then
            -- w tym warunku jest sprawdzanie zbocza naciskania przycisku SW1
            if flaga_nacisniecia_SW1 = 0 and przycisniecie_SW1 = '1' then
               flaga_nacisniecia_SW1 := 1; -- tak wygl przypisanie do zmiennej
            end if;
            -- w tym warunku jest sprawdzanie zbocza naciskania przycisku SW2            
            if flaga_nacisniecia_SW2 = 0 and przycisniecie_SW2 = '1' then
               flaga_nacisniecia_SW2 := 1;
            end if;

         -- Ponizsza konstrukcja "case" zaleznie od wartosci sygnalu / zmiennej umiejszczonej
         -- po "case" przechodzi do odpowiedniego bloku instrukcji stojacych za "when".
         -- Ta konstrukcja sluzy jako implementacja maszyny stanow dla naszego projektu'
         case Stan is

         when S0 => 
            -- Czy przycisk SW1 byl naciskany i zostal juz zwolniony?
            if flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' then
               Stan <= S1;
               flaga_nacisniecia_SW1 := 0;
            -- Analogicznie: Czy przycisk SW2 byl naciskany i zostal juz zwolniony?
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' then
               Stan <= S3;
               flaga_nacisniecia_SW2 := 0;         
            else
               Stan <= S0;
            end if; 

         when S1 => 
            if flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' then 
               Stan <= S2;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' then
               Stan <= S0;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S1;
            end if; 
  

         WHEN S2 => 
            IF flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' THEN 
               Stan <= S3;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' THEN
               Stan <= S1;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S2;
            END IF; 

         WHEN S3 => 
            IF flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' THEN 
               Stan <= S0;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' THEN
               Stan <= S2;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S3;
            END IF; 

         -- na wypadek wystapienia stanu nieustalonego, uklad ma powrocic do Stanu S0
         WHEN others =>
            Stan <= S0;
      END CASE; 

     end if;

   end process nasza_maszyna_stanow;

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:

deklaracja zmiennej w procesie nasza_maszyna_stanow - w module nadrzędnym.
variable flaga_nacisniecia_SW1 : integer := 0;

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:

przypisanie stanów do wyjść diod LED - w module nadrzędnym.
-- Przypisania odpowiednich Stanow do diod LED
LED(0) <= '1' WHEN Stan=S0 ELSE '0';
LED(1) <= '1' WHEN Stan=S1 ELSE '0';
LED(2) <= '1' WHEN Stan=S2 ELSE '0';
LED(3) <= '1' WHEN Stan=S3 ELSE '0';

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:

zawartość modułu nadrzędnego dla aplikacji większy projekt.
-- dodajemy biblioteki
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity top_module is
Port ( Clk : in  std_logic;   -- sygnal zegarowy jest uzywany w blokach process
       Switch : in  std_logic_vector(2 downto 0);  -- wykorzystamy trzy przyciski typu Switch
       LED : out  std_logic_vector(3 downto 0));  -- 4 diody LED jako wyjscia
end top_module;


architecture Behavioral of top_module is

-- ponizsza instrukcja definiuje nowy rodzaj sygnalu, ktory moze przyjac jedynie te wartosci,
-- ktore sa wymienione w nawiasie - konstrukcja ta jest podobna do definicji enum znanej z jezyka programowania C
type typ_stanu is (S0, S1, S2, S3);   -- Definiowanie stanow

signal Stan : typ_stanu := S0; -- Tworzenie sygnalu - zmiennej mogacej przyjmowac wartosci zdefiniowane w typ_stanu;

-- sygnal reset bedzie wykorzystany do przywrocenia domyslnego stanu naszego ukladu
signal reset : std_logic;

-- deklarujemy sygnaly, ktore przejma stany z modulu przeprowadzajacego debouncing;
signal przycisniecie_SW1 : std_logic;
signal przycisniecie_SW2 : std_logic;

-- Blok component sluzy do informowanie modulu nadrzednego (naszego top_module.vhd) 
-- jakimi typami wejsc i wyjsc dysponuje nasz podmodul - debouncer.
component debouncer is
Port ( Clk : in  std_logic;   -- sygnal zegarowy jest uzywany w bloku process
       Switch : in  std_logic; -- wejsciem naszego debouncera jest przycisk
       odfiltrowany_przycisk : out std_logic); -- to jest wyjscie okreslajace stan stabilny przycisniecia przycisku po filtracji debouncing;
end component;


begin

-- ponizszym bloku port jest przeprowadzane instancjonowanie debouncera przycisku SW1,
-- tzn. przypisanie wejsc i wyjsc modulu podrzednego z wejsciami i wyjsciami z modulu nadrzednego;
debouncer_SW1 : debouncer 
port map (Clk => Clk, 
          Switch => Switch(0), 
          odfiltrowany_przycisk => przycisniecie_SW1);

-- analogiczny blok 'port map' dla drugiego debouncera dla przycisku SW2
debouncer_SW2 : debouncer 
port map (Clk => Clk, 
          Switch => Switch(1), 
          odfiltrowany_przycisk => przycisniecie_SW2);

-- przpyisanie przycisku SW3 do lokalnego sygnalu reset
reset <= not Switch(2);

   -- ten proces odpowiada za funkcjonalnosc naszej aplikacji opisanej diagramem stanow
   -- blok proces jest uruchamiany poprzez kazda zmiane sygnalu zegarowego lub reset
   nasza_maszyna_stanow : process (Clk, reset) 

   -- tutaj jest blok deklaracji "zmiennych" lokalnych wykorzystywanych wylacznie wewnatrz procesu
   -- zadeklarowane ponizej zmienne sluza do detekcji zbocza - naciskania przycisku
   variable flaga_nacisniecia_SW1 : integer := 0;
   variable flaga_nacisniecia_SW2 : integer := 0;

   begin 
      -- ponizsze instrukcje beda wykonane po nacisnieciu SW3 - reset
      if (reset = '1') THEN
         flaga_nacisniecia_SW1 := 0;
         flaga_nacisniecia_SW2 := 0;
         Stan <= S0; -- po nacisnieciu reset - stan zostanie wyzerowany do S0

      -- dla kazdego narastajacego zbocza zegarowego...
      elsif rising_edge(Clk) then
            -- w tym warunku jest sprawdzanie zbocza naciskania przycisku SW1
            if flaga_nacisniecia_SW1 = 0 and przycisniecie_SW1 = '1' then
               flaga_nacisniecia_SW1 := 1; -- tak wygl przypisanie do zmiennej
            end if;
            -- w tym warunku jest sprawdzanie zbocza naciskania przycisku SW2            
            if flaga_nacisniecia_SW2 = 0 and przycisniecie_SW2 = '1' then
               flaga_nacisniecia_SW2 := 1;
            end if;

         -- Ponizsza konstrukcja "case" zaleznie od wartosci sygnalu / zmiennej umiejszczonej
         -- po "case" przechodzi do odpowiedniego bloku instrukcji stojacych za "when".
         -- Ta konstrukcja sluzy jako implementacja maszyny stanow dla naszego projektu'
         case Stan is

         when S0 => 
            -- Czy przycisk SW1 byl naciskany i zostal juz zwolniony?
            if flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' then
               Stan <= S1;
               flaga_nacisniecia_SW1 := 0;
            -- Analogicznie: Czy przycisk SW2 byl naciskany i zostal juz zwolniony?
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' then
               Stan <= S3;
               flaga_nacisniecia_SW2 := 0;         
            else
               Stan <= S0;
            end if; 

         when S1 => 
            if flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' then 
               Stan <= S2;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' then
               Stan <= S0;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S1;
            end if; 
  

         WHEN S2 => 
            IF flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' THEN 
               Stan <= S3;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' THEN
               Stan <= S1;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S2;
            END IF; 

         WHEN S3 => 
            IF flaga_nacisniecia_SW1 = 1 and przycisniecie_SW1='0' THEN 
               Stan <= S0;
               flaga_nacisniecia_SW1 := 0;
            elsif flaga_nacisniecia_SW2 = 1 and przycisniecie_SW2='0' THEN
               Stan <= S2;
               flaga_nacisniecia_SW2 := 0;  
            else
               Stan <= S3;
            END IF; 

         -- na wypadek wystapienia stanu nieustalonego, uklad ma powrocic do Stanu S0
         WHEN others =>
            Stan <= S0;
      END CASE; 

     end if;

   end process nasza_maszyna_stanow;


-- Przypisania odpowiednich Stanow do diod LED
LED(0) <= '1' WHEN Stan=S0 ELSE '0';
LED(1) <= '1' WHEN Stan=S1 ELSE '0';
LED(2) <= '1' WHEN Stan=S2 ELSE '0';
LED(3) <= '1' WHEN Stan=S3 ELSE '0';

end Behavioral;

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.

-- dodajemy biblioteki
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

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:

entity debouncer is
Port ( Clk : in  std_logic;    -- sygnal zegarowy jest uzywany w bloku process
       Switch : in  std_logic; -- wejsciem naszego debouncera jest przycisk
       odfiltrowany_przycisk : out std_logic); -- to jest wyjscie okreslajace stan stabilny przycisniecia przycisku po filtracji debouncing;
end debouncer;

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ł:

--  sygnal licznik uzytego do filtracji debounce
signal licznik : std_logic_vector(19 downto 0);

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.

-- ponizej jest proces kompensujacy zjawisko debouncing naszego Switch - ma to na celu odfiltrowanie
   -- detekcji przez nasz uklad tego ze przycisk bylby wciskany kilka razy w krotkim czasie;
   Debouncer_przycisku: process(Clk)
   begin
      -- dla kazdego narastajacego zbocza zegarowego...
      if rising_edge(Clk) then
         -- ten if bedzie spelniony dopoki SW1 nie bedzie wcisniety
         if (not Switch) = '0' then
            licznik <= (others => '1');
         else
            -- tutaj widzisz nowy operator "/=" - oznacza on "rozny od" i jest przeciwnoscia operatora "=",
            -- ktory oznacza "rowny";
            if licznik /= 0 then
               -- dekrementacja sygnalu licznik
               licznik <= licznik - 1;
            end if;
         end if;
      end if;
   end process Debouncer_przycisku;

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:

przypisanie wartości logicznej '1' gdy przycisk będzie stabilnie wciśnięty - dla modułu podrzędnego.
-- te sygnaly otrzymuja wartosci przycisnietych przyciskow juz po filtracji Debouncing
odfiltrowany_przycisk <= '1' when licznik = 0 else '0';

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:

przypisanie wartości logicznej '1' gdy przycisk będzie stabilnie wciśnięty
-- te sygnaly otrzymuja wartosci przycisnietych przyciskow juz po filtracji Debouncing
odfiltrowany_przycisk <= '1' when licznik = "00000000000000000000" else '0';

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:

-- dodajemy biblioteki
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;


entity debouncer is
Port ( Clk : in  std_logic;    -- sygnal zegarowy jest uzywany w bloku process
       Switch : in  std_logic; -- wejsciem naszego debouncera jest przycisk
       odfiltrowany_przycisk : out std_logic); -- to jest wyjscie okreslajace stan stabilny przycisniecia przycisku po filtracji debouncing;
end debouncer;


architecture Behavioral of debouncer is

--  sygnal licznik uzytego do filtracji debounce
signal licznik : std_logic_vector(19 downto 0);

begin

   -- ponizej jest proces kompensujacy zjawisko debouncing naszego Switch - ma to na celu odfiltrowanie
   -- detekcji przez nasz uklad tego ze przycisk bylby wciskany kilka razy w krotkim czasie;
   Debouncer_przycisku: process(Clk)
   begin
      -- dla kazdego narastajacego zbocza zegarowego...
      if rising_edge(Clk) then
         -- ten if bedzie spelniony dopoki SW1 nie bedzie wcisniety
         if (not Switch) = '0' then
            licznik <= (others => '1');
         else
            -- tutaj widzisz nowy operator "/=" - oznacza on "rozny od" i jest przeciwnoscia operatora "=",
            -- ktory oznacza "rowny";
            if licznik /= 0 then
               -- dekrementacja sygnalu licznik
               licznik <= licznik - 1;
            end if;
         end if;
      end if;
   end process Debouncer_przycisku;

-- te sygnaly otrzymuja wartosci przycisnietych przyciskow juz po filtracji Debouncing
odfiltrowany_przycisk <= '1' when licznik = 0 else '0';

end Behavioral;

Plik UCF

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

NET "Clk"                  LOC = P129  | IOSTANDARD = LVCMOS33 | PERIOD = 12MHz;

NET "LED[0]"             LOC = P46   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "LED[1]"             LOC = P47   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "LED[2]"             LOC = P48   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "LED[3]"             LOC = P49   | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;

NET "Switch[0]"          LOC = P80   | PULLUP  | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "Switch[1]"          LOC = P79   | PULLUP  | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;
NET "Switch[2]"          LOC = P78   | PULLUP  | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12;

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...