Kursy • Poradniki • Inspirujące DIY • Forum
Czym jest rejestr przesuwny?
Tym razem pora na rejestry przesuwne (ang. shift register), które najczęściej wykorzystywane są do konwersji sygnałów z szeregowych na równoległe i odwrotnie. Przykładowo przy komunikacji z wykorzystaniem UART niezbędna jest zamiana informacji wystawionych na szynę danych, wprowadzenie ich do rejestru przesuwnego, a następnie wysyłanie kolejnych bitów z wyjścia rejestru zgodnie z sygnałem zegara taktującego.
Przykład działania rejestru przesuwnego, w którym z każdym taktem zegara przesuwamy jego zawartość, czyli "00000101" o jeden w lewo, widoczny jest poniżej:
Wraz z każdym taktem zegara zawartość rejestru przesuwana jest o jeden bit w lewo, a na zwolnionej pozycji pojawia się logiczne zero. Gdy dojdziemy do końca dane znajdują się już "poza rejestrem", więc cały rejestr wypełniony jest zerami.
Odmiana rejestru
Na potrzeby kursy (dla ciekawszych programów) zajmiemy się zmodyfikowanym "rejestrem", w którym dane przesunięte poza rejestr pojawiają się na jego początku. Przykładowo przesunięcie "101" będzie wyglądało następująco:
Kierunek pracy rejestru przesuwnego
Dane w rejestrze mogą być przesuwane w lewo lub w prawo. Przykładowo dla rejestru, w którym wpisano na początku "00000001" przesunięcie w lewo wygląda następująco:
Natomiast przesuwanie w prawo wyglądałoby tak:
W lepszym zrozumieniu tego zagadnienia pomocne będzie przejście do praktyki! Spróbujmy stworzyć rejestr, który będzie działał tak jak powyższa animacja (jedynka będzie reprezentowana przez świecącą diodę).
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 »Rejestr przesuwny w VHDL
Tak jak w poprzednich ćwiczeniach zacznijmy od gotowego programu, który następnie dokładnie omówimy. Poniżej widoczna jest kompletna implementacja rejestru przesuwnego w VHDL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
-- dolaczamy biblioteki library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; entity rejest_przesuwny is Port ( Clk : in STD_LOGIC; -- jako wejscie będzie nasz sygnal zegarowy LED : out STD_LOGIC_VECTOR (7 downto 0)); -- wyjsciem jest linijka LED na naszym zestawie ElbertV2 end rejest_przesuwny; architecture Behavioral of rejest_przesuwny is --------------------------------------------------------------------- -- ponizej definicje syg lokalnych --------------------------------------------------------------------- -- definiujemy rejestr o szerokosci 8 bitow o wart poczatkowej 01 (Hex), binarnie : "00000001"; -- kazdy bit wyjscia naszego rejestru przesuwnego bedzie podlaczony do jednej diody LED signal rej_przesuwny : STD_LOGIC_VECTOR(7 downto 0) := X"01"; -- wart max licznika constant LICZNIK_LIMIT : integer := 12000000; -- zdefiniowalismy zmienna o szerokosci 25 bitow signal licznik : STD_LOGIC_VECTOR(24 downto 0); --------------------------------------------------------------------- -- tutaj jest dyrektywa "begin" odnoszaca sie do "architecture"; -- od tego momentu mamy do czynienia z kodem opisujacym dzialanie naszego ukladu cyfrowego; begin -- nasz proces posiada nazwe "zliczanie_i_przesuwanie", proces jest wykonywany przy kazdej -- zmianie wartosci sygnalu zegarowego zliczanie_i_przesuwanie : process(Clk) begin -- ponizszy warunek oznacza "dla narastajacego zbocza syg zegarowego" if rising_edge(Clk) then -- nasz proces posiada dwie konstrukcje "if" jedna jest zagniezdzona w drugiej; if (licznik = LICZNIK_LIMIT) then -- Ponizsza instrukcja jest rownowazna przypisaniu wszystkim bitom danej magistrali wartosci po prawej stronie znaku '=>'; licznik <= (others => '0'); -- tutaj jest przeprowadzana esencja procesu przesuwania przeprowadzanego w rejestrze przesuwnym; -- nastepuje przeklejenie najmlodszego bitu na najstarsza pozycje. rej_przesuwny <= rej_przesuwny(0) & rej_przesuwny(7 downto 1); -- po slowie else nastepuje wykonanie instrukcji w syt gdy warunek z "if" nie bedzie spelniony; else licznik <= licznik + 1; end if; end if; end process zliczanie_i_przesuwanie; -- tutaj jest przeprowadzany proces przypisania kolejno wszystkich bitow magistrali "rej_przesuwny" do wyjsc linijki LED na zestawie ElbertV2. LED <= rej_przesuwny; end Behavioral; |
Pora omówić poszczególne fragmenty programu. Zacznijmy od samej góry:
Blok entity - sygnały wejściowe i wyjściowe
W bloku entity deklarujemy, że będziemy korzystać z jednego sygnału wejściowego (sygnału zegarowego) oraz 8 sygnałów wyjściowych (diod świecących):
1 2 3 4 |
entity rejest_przesuwny is Port ( Clk : in STD_LOGIC; -- jako wejscie będzie nasz sygnal zegarowy LED : out STD_LOGIC_VECTOR (7 downto 0)); -- wyjsciem jest linijka LED na naszym zestawie ElbertV2 end rejest_przesuwny; |
Blok architecture - sygnały lokalne
Na początku bloku architecture deklarujemy sygnały lokalne. W tym wypadku będzie to nasz rejestr przesuwny, licznik oraz limit dla licznika.
1 2 3 4 5 6 7 8 9 |
-- definiujemy rejestr o szerokosci 8 bitow o wart poczatkowej 01 (Hex), binarnie : "00000001"; -- kazdy bit wyjscia naszego rejestru przesuwnego bedzie podlaczony do jednej diody LED signal rej_przesuwny : STD_LOGIC_VECTOR(7 downto 0) := X"01"; -- wart max licznika constant LICZNIK_LIMIT : integer := 12000000; -- zdefiniowalismy zmienna o szerokosci 25 bitow signal licznik : STD_LOGIC_VECTOR(24 downto 0); |
Sygnał lokalny rej_przesuwny to wektor od długości 8 bitów. Długość ta została dobrana w taki sposób, aby możliwe było wykorzystanie wszystkich diod świecących z zestawu Elbert.
W tej deklaracji umieszczamy do razu
wartość inicjalizującą ten sygnał, czyli "00000001".
Binarnie ma ona wartość 00000001, czyli dziesiętnie "jeden". W systemie szesnastkowym (Hex) zapisywana jako 01. W języku VHDL, aby daną wartość podać w systemie binarnym należy poprzedzić ją znakiem b lub B (VHDL nie zwraca uwagi na wielkość liter). Gdy wartość podawana jest w systemie szesnastkowym to poprzedzamy ją x lub X.
Kolejne deklaracje sygnałów lokalnych są takie same jak w jednym z poprzednich ćwiczeń:
- deklaracja stałej o nazwie LICZNIK_LIMIT, która ma wartość 12 mln (zestaw wyposażony jest w sygnał zegarowy o częstotliwości około 12 MHz),
- kolejny sygnał to wektor licznik o długości 25 bitów. Jego długość wynika z możliwości przechowywania wartości o odpowiedniej wielkości (do około 33 mln dziesiętnie).
W dalszej części kodu zaczyna się opis funkcjonalności aplikacji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-- nasz proces posiada nazwe "zliczanie_i_przesuwanie", proces jest wykonywany przy kazdej -- zmianie wartosci sygnalu zegarowego zliczanie_i_przesuwanie : process(Clk) begin -- ponizszy warunek oznacza "dla narastajacego zbocza syg zegarowego" if rising_edge(Clk) then -- nasz proces posiada dwie konstrukcje "if" jedna jest zagniezdzona w drugiej; if (licznik = LICZNIK_LIMIT) then -- Ponizsza instrukcja jest rownowazna przypisaniu wszystkim bitom danej magistrali wartosci po prawej stronie znaku '=>'; licznik <= (others => '0'); -- tutaj jest przeprowadzana esencja procesu przesuwania przeprowadzanego w rejestrze przesuwnym; -- nastepuje przeklejenie najmlodszego bitu na najstarsza pozycje. rej_przesuwny <= rej_przesuwny(0) & rej_przesuwny(7 downto 1); -- po slowie else nastepuje wykonanie instrukcji w syt gdy warunek z "if" nie bedzie spelniony; else licznik <= licznik + 1; end if; end if; end process zliczanie_i_przesuwanie; |
W powyższym wycinku kodu mamy to co jest we wnętrzu bloku architecture za słowem kluczowym begin, czyli jest to blok process. Blok ten jest o tyle charakterystyczny, że instrukcje w nim zawarte wykonywane są w sposób sekwencyjny, jak np. w języku programowania C.
Jest to pewną "nowością" w odróżnieniu do np. struktury with-select, która wykonywana jest w sposób równoczesny/współbieżny.
Proces w języku VHDL deklaruje się słowem kluczowym process. Poprzedzać je może nazwa procesu (nie jest obowiązkowa, lecz dla zwiększenia czytelności kodu warto ją stosować), w naszym wypadku jest to zliczanie_i_przesuwanie. Za dyrektywą proces wewnątrz nawiasu umieszcza się nazwy sygnałów, których zmiana stanu wywołuje rozpoczęcie procesu. W naszym przypadku jest to sygnał zegarowy. Dalej umieszczona jest komenda begin oznaczająca początek bloku kodu z instrukcjami z wnętrza bloku process.
Wewnątrz procesu zliczanie_i_przesuwanie znajdują się dwie instrukcje warunkowe. Po słowie if umieszcza się warunek. W naszym przypadku jest to wystąpienie zbocza narastającego sygnału zegarowego, po warunku umieszcza się dyrektywę then. Działanie instrukcji warunkowej jest następujące: gdy warunek jest spełniony, to wykonaj instrukcje znajdujące się po słowie then.
Dla języka VHDL bardzo charakterystycznym jest fakt, że
instrukcja if może pojawić się jedynie wewnątrz procesu.
Nasz proces posiada dwie instrukcje warunkowe - jedna jest zagnieżdżona w drugiej. Pierwszy, nadrzędny warunek odpowiada za detekcję zbocza narastającego sygnału zegarowego. Drugi odpowiada za przeprowadzanie zerowania sygnału licznik i instrukcje przesuwania (dla sytuacji gdy będzie spełniony warunek, tzn. sygnał licznik osiągnie wartość LICZNIK_LIMIT).
Wśród instrukcji widać jeden, nowy zapis:
1 |
licznik <= (others => '0'); |
Powyższa instrukcja jest równoważna przypisaniu wszystkim bitom danej magistrali wartości po prawej stronie znaku '=>' (w naszym przypadku chodzi o zerowanie). Jest to po prostu wygodniejszy zapis od:
1 |
licznik <= b"0000000000000000000000000"; |
Poniższa instrukcja realizuje rdzeń funkcjonalności naszego rejestru przesuwnego:
1 |
rej_przesuwny <= rej_przesuwny(0) & rej_przesuwny(7 downto 1); |
Następuje skopiowanie najmłodszego bitu rej_przesuwny(0) na najstarszą pozycje. Tajemniczy operator & oznacza przeprowadzanie połączenia (konkatenacji), czyli "sklejanie bitów". Często tego pojęcia używa się do operacji na łańcuchach znakowych (stringach) w innych językach programowania. Nasza instrukcja powoduje przestawienie wartości z najmłodszego bitu na najstarszą pozycje.
W sytuacji, gdy drugi warunek nie będzie spełniony, to zostanie wykonana instrukcja inkrementacji wewnątrz instrukcji else. Blok ten służy do określenia, które instrukcje mają zostać wykonane w sytuacji, gdy warunek nie jest spełniony. Blok else jest domyślnie interpretowany jako należący do najgłębiej zagnieżdżonego warunku, który nie posiada jeszcze swojego else.
Umieszczanie bloku else stosuje się również w celu uniknięcia "zatrzasków" w procesie syntezy kodu VHDL. Więcej na ten temat w dalszej części kursu.
Po warunku i odpowiadającym im bloku else należy umieścić komendę end if. Dodajemy ją dla każdego poziomu zagnieżdżenia osobno. Jak mamy dwa poziomy zagnieżdżenia, jak w naszym przypadku, to umieszczamy dwa razy end if.
Podobnie wygląda sprawa kończenia bloku procesu - tam dodajemy analogiczną
komendę end process <nazwa procesu>.
Nazwę procesu wpisujemy, jeśli ją wcześniej podano!
Na końcu bloku architecture jest zapisana instrukcja przypisania rejestru do diod świecących. To właśnie tutaj jest przeprowadzany proces przypisania kolejno wszystkich bitów magistrali (wektora) rej_przesuwny do linijki LED. Oczywiście bity są przypisywane odpowiednio do swoich pozycji, tj. bit rej_przesuwny(0) będzie przypisany do led(0) i tak dalej.
1 |
LED <= rej_przesuwny; |
W nowo utworzonym projekcie wklejamy powyższy kod. Wynikiem jego działania będzie kolejne świecenie każdej z diod (od D1 do D8). Diody będą reprezentowały 8 bitowy rejestr, w którym będziemy przesuwać w prawo logiczną jedynkę, która zostanie na początku do niego wpisana.
Oczywiście konieczny będzie również nowy plik UCF. Można pobrać gotowy plik lub utworzyć własny z poniższą zawartością:
1 2 3 4 5 6 7 8 9 10 |
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 "LED[4]" LOC = P50 | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12; NET "LED[5]" LOC = P51 | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12; NET "LED[6]" LOC = P54 | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12; NET "LED[7]" LOC = P55 | IOSTANDARD = LVCMOS33 | SLEW = SLOW | DRIVE = 12; |
Jeśli zmienimy w programie wartość początkową rejestru z:
1 |
signal rej_przesuwny : STD_LOGIC_VECTOR(7 downto 0) := X"01"; |
Na:
1 |
signal rej_przesuwny : STD_LOGIC_VECTOR(7 downto 0) := X"05"; |
To na płytce zacznie nam wędrować 5, czyli binarnie "101":
Inspiracje na zadania dodatkowe dla chętnych
Zainteresowani samodzielną pracą mogą pokusić się o przebudowanie programu. Warto zacząć od czegoś łatwego czyli zmiany częstotliwości migania diod. Kolejny krok, to odwrócenie kierunku pracy rejestru (niech przesuwa dane w lewo).
Warto również spróbować stworzyć pierwotną wersję opisywanego rejestru, czyli taką, w której dane wychodzą "za rejestr" i nie wracają na jego początek:
Programowaniem vs. tworzeniem aplikacji w VHDL
W tym momencie warto jeszcze raz podkreślić różnice między programowaniem klasycznym, a VHDLem. Komputer/mikrokontroler wykonuje operacje według ściśle określonego przepisu. Przepis ten jest nazywany programem komputerowym. Procesory znane z komputerów PC, czy mikrokontrolery wykonują wszystkie operacje według rozkazów zapisanych w pamięci.
Wszystko sprowadza się do tego, że w danej chwili tylko jedna instrukcja może być przetwarzana przez nasz elektroniczny "mózg".
Z układami programowalnymi jest inaczej. Mamy tutaj potężne narzędzie jakim jest możliwość wydzielenia bloków, które umożliwią wykonywanie określonych funkcjonalności niezależnie od siebie. Podobną filozofie można dostrzec w układach peryferyjnych mikroprocesorów.
Dwie kluczowe różnice między programowaniem mikrokontrolerów, a tworzeniem kodu dla PLD:
- Możliwość tworzenia instrukcji współbieżnych (ang. concurrent statements). Pewne konstrukcje językowe w VHDL (np. while-select) mogą się wykonywać równocześnie i niezależnie od siebie.
Pozwala to na przepływ danych między
różnymi blokami, bez żadnej straty czasowej.
- Proces tłumaczenia kodu z postaci tworzonej przez człowieka do formy czytelnej dla układu w przypadku języka VHDL jest określany jako "Synteza". W klasycznych językach programowania nazywane jest to kompilacją. W przypadku Syntezy nie powstaje lista rozkazów, które układ miałby wykonać. Generowana jest konfiguracja wewnętrznych bloków logicznych.
Podsumowanie
Podstawy VHDL za nami. Bramki, multipleksery oraz różne warianty rejestrów przesuwnych, to bardzo dobry trening w nauce FPGA. Warto wykonać jak najwięcej takich prostych ćwiczeń, aby opanować w praktyce pracę z VHDL. Właśnie podczas takich zadań najłatwiej oswoić się z architekturami, blokami, procesami oraz innymi elementami specyficznymi dla tego języka.
W kolejnych odcinkach zajmiemy się tworzeniem bardziej rozbudowanych programów. Przed nami między innymi automaty stanów skończonych oraz robienie "porządków" w projektach.
Nawigacja kursu
Autor kursu: Adam Bemski
Redakcja: Damian Szymański
Testy, ilustracje: Piotr Adamczyk
Powiązane wpisy
fpga, kurs, kursFPGA, rejestr, vhdl
Trwa ładowanie komentarzy...