» Historia «
Język VHDL jest jednym z nowszych języków opisu i projektowania układów cyfrowych. Już w lipcu 1983 roku zespół złożony z przedstawicieli firm Intermetrics, IBM oraz Texas Instruments rozpoczął pierwszy etap pracy nad nowym językiem opisu i projektowania układów VLSI. Po roku pracy zaimplementowano dany język, dzięki czemu w grudniu 1985 otrzymano pierwszą wersję narzędzia napisanego w języku Ada dla komputerów klasy VA 11/780 i IBM 370.
Projekt VHDL był częścią programu Departamentu Obrony USA o nazwie VHSIC, którego zadaniem było opracowanie metod projektowania oraz wykorzystanie najbardziej złożonych i bardzo szybkich układów scalonych. W roku 1987 VHDL stał się obowiązującym standardem w dziedzinie języków opisu i projektowania układów VLSI. Ulepszona wersja języka pojawiła się dopiero w 1993 roku i obecnie jest stosowana przez większość projektantów układów cyfrowych na świecie (niestety jednak, w Polsce język VHDL nie jest jeszcze zbyt popularny).
» Podstawy języka «
Język VHDL spełnia podobną funkcję w dziedzinie projektowania sprzętu, jak język C++ w dziedzinie programowania. Ma on następujące właściwości:
wspiera hierarchiczność projektowanego sprzętu,
umożliwia opis projektu i jego sprawdzenie w całym procesie jego powstania,
umożliwia tworzenie nowych wersji projektowych realizowanych w nowych technologiach na postawie rozwiązań projektowych przechowywanych w bibliotece projektów - jest zatem niezależny od konkretnej technologii, metody projektowania, narzędzi wspomagających projektowanie,
umożliwia reprezentację dynamiki układu cyfrowego oraz współbieżnych operacji w sprzęcie - można stworzyć równoważne modele funkcjonalne,
ułatwia dokumentowanie projektu, a najlepsze rozwiązania można gromadzić w bibliotekach projektów,
ułatwia wymianę informacji między projektantami oraz całymi zespołami projektowymi,
W języku VHDL można reprezentować układy cyfrowe na poziomach: od bramkowego do systemowego. Oznacza to, że najmniejszym elementem naszego projektu jest bramka logiczna. Nie mamy więc dostępu do poziomu analogowego (topografii bramki logicznej), chociaż w kolejnych wersjach języka VHDL prawdopodobnie się to zmieni.
» Zasady gramatyki «
Dla opisu składni używanej w języku VHDL używa się podstawowej notacji BNF o następujących cechach:
1. elementy syntaktyczne języka są przedstawione w postaci słów połączonych znakiem podkreślenia,
część_deklaracyjna_jednostki
2. każda "produkcja" gramatyki języka VHDL składa się z:
części lewej reprezentującej wybrany element syntaktyczny języka,
symbolu "::=",
części prawej przedstawiającej sposób rozkładu elementu syntaktycznego na części składowe,
znaku ";"
jednostka_podstawowa ::= identyfikator ;
3. znak "|" występujący między dwoma elementami gramatyki oznacza, że możemy wybrać albo jeden, albo drugi element syntaktyczny,
literał_wyliczeniowy ::= identyfikator | literał znakowy ;
4. nawias klamrowy "{...}" oznacza, że elementy syntaktyczne występujące wewnątrz nawiasu mogą tworzyć listę tych elementów,
lista_czułości ::= nazwa_sygnału { , nazwa_sygnału } ;
5. słowa wytłuszczone oznaczają słowa kluczowe języka VHDL,
definicja_typu_rzeczywistego ::= range zakres_prosty ;
6. nawias kwadratowy "[...]" oznacza, że elementy gramatyczne języka będące wewnątrz nawiasu mogą nie występować w produkcji,
instrukcja_założenia ::= [ etykieta: ] założenie ;
7. komentarze umieszczamy po dwóch myślnikach "--''.
-- funkcja zwracająca wartość A ;
» Budowa projektu «
Każda jednostka projektowa (ang. desing entity) w języku VHDL składa się z:
Zależność między nimi jest ściśle określona. Do jednego sprzęgu może być przypisanych kilka ciał architektonicznych. A z kolei jedno ciało architektoniczne może być powiązane tylko i wyłącznie z jednym sprzęgiem. Dzięki takiemu rozwiązaniu jeden projekt, np.: dekodera, może działać w różny sposób w zależności od użytego ciała architektonicznego jednostki projektowej.
Najprostszy projekt układu cyfrowego musi zawierać jeden sprzęg i jedno ciało jednostki projektowej. Jednak w bardziej skomplikowanych projektach znaleźć można kilka jednostek projektowych tj. więcej niż jeden sprzęg i ponad dwa ciała architektoniczne. Projekty takie mogą także dodatkowo zawierać:
Dzięki nim można znacznie przyśpieszyć proces projektowania wykorzystując w swoim projekcie poszczególne części innych projektów gromadzonych w bibliotekach.
» Sprzęg «
Sprzęg jednostki projektowej nazywany jest interfejsem (ang. interface) lub deklaracją jednostki projektowej. Składnia sprzęgu jest następująca:
deklaracja_jednostki_projektowej ::=
entity identyfikator is
[generic ( lista_parametrów); ]
[port ( lista_portow); ]
część_deklaracyjna_jednostki
[begin
instrukcje_współbieżne ]
end [entity] [nazwa_jednostki_projektowej] ;
Jak widać z syntaktycznej postaci interfejsu, udostępnia on porty wejściowe i wyjściowe oraz parametry formalne innym jednostkom.
Sprzęg jednostki projektowej składa się zatem z:
identyfikatora jednostki projektowej - reprezentującego jednostkę w środowisku projektowym,
nagłówka jednostki projektowej - zawierającego listę parametrów i listę portów,
części deklaracyjnej jednostki projektowej - zawierającej typy i obiekty używane w danej jednostce projektowej,
pasywnych instrukcji współbieżnych - działań opisujących zachowanie się (funkcjonowanie) jednostki projektowej, ale nie zmieniających wartości sygnałów.
Oprócz identyfikatora wszystkie pozostałe części interfejsu mogą wystąpić opcjonalnie. Zatem najprostszy sprzęg jednostki projektowej może wyglądać następująco:
entity Dekoder is end;
Lista portów
Porty to inaczej sygnały zewnętrzne, które połączone są z wyprowadzeniami naszego projektu. Innymi słowy, jeżeli naszą jednostkę projektową potraktujemy jako "czarną skrzynkę", to wszelkie jej wyprowadzenia nazywamy portami. Charakter takiego portu (tzn. kierunek przepływu danych) określamy przy deklaracji portu.
Przykład:
entity Dekoder is
port (
wejscie : in bit_vector
wyjscie : out bit_vector );
end entity Dekoder;
Rys. Sprzęg jednostki projektowej 'Dekoder'
Deklaracja portów jest jednoznaczna z deklaracją sygnałów. Sygnały te różnią się jednak od zwykłych sygnałów "rodzajem" umieszczanym przed wskaźnikiem typu sygnału.
Wyróżniamy pięć typów portów:
1. wejściowe - charakteryzujące się:
słowem kluczowym "in";
jednokierunkowym przepływem informacji do jednostki projektowej;
gą być tylko i wyłącznie odczytywane;
nie można odczytywać ich atrybutów takich jak: stable, quiet, delayed, transaction;
wejscie1 : in bit_vector (0 to 3) ;
2. wyjściowe - charakteryzujące się:
słowem kluczowym "out";
jednokierunkowym przepływem informacji z jednostki projektowej;
wartości ich są wyliczane wewnątrz danej jednostki;
mogą być tylko uaktualniane;
nie można odczytywać ich atrybutów takich jak: stable, quiet, delayed, transaction, event, active, last_event, last_active, last_value;
wyjscie1 : out bit ;
3. wejściowo-wyjściowe z wieloma źródłami - charakteryzujące się:
słowem kluczowym "inout";
dwukierunkowym przepływem informacji z i do jednostki projektowej (możliwy jest np. odczyt sygnału a potem jego aktualizacja);
mogą być uaktualniane przez więcej niż jedno źródło;
nie można odczytywać ich atrybutów takich jak: stable, quiet, delayed, transaction;
sgnA : inout bit_vector ;
4. wejściowo-wyjściowe z jednym źródłami - charakteryzujące się:
słowem kluczowym "buffer";
dwukierunkowym przepływem informacji z i do jednostki projektowej;
mogą być uaktualniane tylko przez jedno źródło;
można odczytywać ich wszystkie atrybuty;
sgnB : buffer bit_vector (5 downto 1) ;
5. wejściowo-wyjściowe o nie określonym kierunku przepływu informacji - charakteryzujące się:
słowem kluczowym "linkage";
stosuje się je do komunikacji z jednostkami projektowymi definiowanymi w innym środowisku niż język VHDL;
Lista parametrów
Parametry to rodzaj zmiennych globalnych, które zasadniczo wpływają na działanie jednostki projektowej. Różnią się od innych zmiennych tym, że deklaruje się je po słowie kluczowym "generic", a także tym że parametry mogą być mapowane (tzn. przyporządkowane) przez inne jednostki projektowe przy pomocy konfiguracji. Innymi słowy, są pewnego rodzaju zmiennymi wejściowymi.
Parametry w jednostkach projektowych stosujemy głównie po to, aby tworzone przez nas jednostki projektowe były bardziej wszechstronne, a także aby przyśpieszyć proces projektowania. Chodzi o to, aby jeden projekt z parametrem zastąpił co najmniej dwa inne projekty bez parametru, ale o bardzo zbliżonym działaniu. Przykładem mogą być wszelkiego rodzaju dekodery, których sposób dekodowania jest taki sam (np. z kodu BCD na postać umożliwiającą sygnalizację na diodach LED), jednak są one w stanie zdekodować tylko i wyłącznie sygnał wejściowy o określonej długości słowa. Zastosowanie parametru umożliwia stworzenie jednego, uniwersalnego dekodera, który nie jest uzależniony od długości sygnału wejściowego. Projekt taki z odpowiednio zmapowanymi parametrami staje się konkretnym dekoderem, który może być samodzielnym projektem lub też stanowić element jakiegoś projektu.
» Ciało architektoniczne «
Ciało architektoniczne jednostki projektowej zawiera własności funkcjonalne (behawioralne) i strukturalne układu cyfrowego. Składnia ciała architektonicznego jednostki projektowej wygląda następująco:
ciało_architektoniczne ::=
architecture identyfikator of nazwa_jednostki_projektowej is
część deklaracyjna_ciała
begin
instrukcje współbieżne
end [ architecture ] [ nazwa_ciała ] ;
Ciało architektoniczne jednostki projektowej składa się zatem z:
identyfikatora - reprezentującego ciało jednostki w środowisku projektowym,
nazwy jednostki projektowej - wiążącej dane ciało z określonym interfejsem (sprzęgiem jednostki projektowej),
części deklaracyjnej ciała jednostki projektowej - zawierającej m.in typy, obiekty, podprogramy używane w danym ciele jednostki projektowej,
instrukcji współbieżnych - działań opisujących funkcjonalne i/lub strukturalne własności ciała jednostki projektowej.
Oprócz identyfikatora i nazwy jednostki projektowej wszystkie pozostałe części interfejsu mogą wystąpić opcjonalnie.
Ciało architektoniczne może wyglądać różnie. W zależności od jego struktury ciała architektoniczne dzielimy na:
ciało strukturalne - zawierające strukturę danej jednostki projektowej (czyli skonkretyzowane i połączone ze sobą składniki)
ciało behawioralne (funkcjonalne) - zawierający opis działania danej jednostki projektowej,
ciało mieszane - zawierające zarówno strukturę jak i opis działania danej jednostki projektowej.
Różnice między tymi typami ciał można zademonstrować na prostym przykładzie sumatora:
Rys.2. Graficzny opis przykładowego sumatora
Dany sumator można przedstawić w języku VHDL w postaci ciała:
a. strukturalnego
architecture Budowa_Sumatora1 of Sumator is
signal S1, S2 : bit;
begin
Bramka_Not1 : entity Work.Gate_Not port map (wej1, S1);
Bramka_Not2 : entity Work.Gate_Not port map (wej2, S2);
Bramka_And1 : entity Work.Gate_And port map (S1, S2, wyj);
end Budowa_Sumatora1;
Ciało architektoniczne zawiera skonkretyzowane składniki, które wcześniej zostały zadeklarowane i umieszczone w bibliotece "Work". Zastosowanie tego typu ciała pozwala na wykorzystanie wcześniej stworzonych projektów i tym samym przyśpieszenie procesu projektowania. Jego wadą natomiast jest to, że aby dowiedzieć się czegokolwiek o działaniu poszczególnych składników należy odwołać się do konkretnej jednostki projektowej.
b. funkcjonalnego
architecture Budowa_Sumatora2 of Sumator is
signal S1, S2 : bit;
begin
process (wej1, wej2)
S1 <= not wej1 after 2 ns;
S2 <= not wej2 after 2 ns;
wyj <= S1 and S2 after 2 ns;
end process;
end Budowa_Sumatora2;
Ciało architektoniczne zawiera dokładny opis działania (zachowania się) układu, dzięki czemu możemy go wnikliwie analizować. Nie jest on jednak bardzo czytelny przy projektowaniu skomplikowanych i rozbudowanych układów.
c. mieszanego
architecture Budowa_Sumatora3 of Sumator is
signal S1, S2 : bit;
begin
process (wej1, wej2)
S1 <= not wej1 after 2 ns;
S2 <= not wej2 after 2 ns;
end process;
Bramka_And1 : entity Work.Gate_And port map (S1, S2, wyj);
end Budowa_Sumatora3;
Ciało mieszane zawiera częściowy opis działania układu, a także konkretyzację składnika "Bramka_And1". W bardziej skomplikowanych układach jest to dużą zaletą, gdyż jest to w pewnym sensie kompromis między czytelnością funkcjonowania układu i szybkością projektowania. Niezbędne okazują się tu także komentarze, które poprawiają w znacznym stopniu przejrzystość programu.
» Pakiety «
W języku VHDL część deklaracji wspólnych dla różnych projektów może być przechowywana w pakietach, które są umieszczane tak, jak i całe projekty, w bibliotekach. Dzięki nim możliwe jest znaczne przyśpieszenie procesu projektowania. Zawartość raz określonego pakietu i umieszczonego w konkretnej bibliotece można wykorzystać w dowolnej części własnego projektu. Warunkiem użycia danego pakietu jest odpowiednia deklaracja w postaci zakresu dostępu do pakietu i biblioteki.
Każdy pakiet składa się z dwóch części:
części deklaracyjnej - zawierająca tylko nagłówki deklaracji (mogą tu się znaleźć m.in. deklaracje podprogramów, typy, podtypy, obiekty i aliasy, składniki i atrybuty);
deklaracja_pakietu ::=
package identyfikator is
część_deklaracyjna_pakietu
end [ package ] [ nazwa_pakietu ] ;
ciała pakietu - zawierająca głównie ciała podprogramów, czyli instrukcje sekwencyjne opisujące działanie podprogramów (mogą tu się znaleźć także elementy wchodzące w skład części deklaracyjnej pakietu np. typy, podtypy, deklaracje podprogramów);
ciało_pakietu ::=
package body nazwa_pakietu is
część_deklaracyjna_ciała_pakietu
end [ package body ] [ nazwa_pakietu ] ;
Zależność między częścią deklaracyjną i ciałem pakietu nie jest taka sama jak w przypadku sprzęgu jednostki projektowej i jej architekturą. Oznacza to, że do jednej części deklaracyjnej pakietu można przyporządkować tylko i wyłącznie jedno ciało pakietu.
W języku VHDL (dokładniej w bibliotece STD) są predefiniowane (wbudowane) dwa pakiety:
pakiet Standard - zawierający predefiniowane typy i operatory języka VHDL,
pakiet TextIO - zawierający operacje zapisu i odczytu plików tekstowych przy komunikacji ze środowiskiem zewnętrznym.
» Deklaracje konfiguracji «
Deklaracje konfiguracji są stosowane do łączenia ze sobą poszczególnych składników. W wyniku połączenia otrzymujemy jednostkę projektową z ciałem strukturalnym projektu. Deklaracja konfiguracji wyznacza jednostkę projektową i ciało architektoniczne dla każdego komponentu, który został utworzony przy pomocy instrukcji konkretyzacji (przyporządkowanie bezpośrednie).
deklaracja_konfiguracji ::=
configuration identyfikator of nazwa_jednostki_projektowej is
część_deklaracyjna_konfiguracji
konfiguracja_bloku
end [ configuration ] [ nazwa_konfiguracji ];
Deklaracja konfiguracji rozpoczyna się od podania nazwy konfiguracji (identyfikatora) oraz skojarzenia jej z daną nazwą jednostki projektowej. Trzeba tu jednak zauważyć, że jedna deklaracja konfiguracji może być związana tylko i wyłącznie z jedną wybraną jednostką projektową i z jednym ciałem architektonicznym tej jednostki. Deklaracja konfiguracji dla określonej jednostki projektowej musi także być w tej samej bibliotece co dana jednostka projektowa. Kolejnym elementem deklaracji konfiguracji jest część deklaracyjna, która może zawierać informacje dotyczące: zakresu widzialności bibliotek, specyfikacji atrybutów oraz deklaracji grup.
Wyróżniamy dwa typy konfiguracji: konfiguracja bloku i konfiguracja składnika.
Konfiguracja bloku
Konfiguracja bloku określa blok lub ciało architektoniczne jednostki projektowej, którą konfigurujemy. Postać składniowa konfiguracji bloku wygląda następująco:
konfiguracja_bloku ::=
for specyfikacja_bloku
{ zakres_widzialności }
{ konfiguracja_bloku | konfiguracja_składnika }
end for ;
Występująca "specyfikacja bloku" wskazuje na ciało architektoniczne jednostki projektowej lub instrukcję bloku. Mogą tu także występować deklaracje zakresu widzialności oraz jednostki konfigurowalne. Konfiguracja bloku może więc zawierać też inne konfiguracje bloku lub konfiguracje składnika.
Konfiguracje bloku dzielimy na:
konfigurację hierarchiczną - gdy konfiguracja bloku zawiera inne konfiguracje bloku reprezentujące ciało architektoniczne,
konfigurację podstawową - gdy konfiguracja bloku nie zawiera innych konfiguracji bloku reprezentujących ciało architektoniczne.
Konfiguracja składnika
Konfiguracja składnika przyporządkowuje konkretną jednostkę projektową do składnika występującego w danym bloku lub ciele architektonicznym. Postać składniowa konfiguracji składnika wygląda następująco:
konfiguracja_składnika ::=
for specyfikacja_składnika
[ wiązanie_składnika ]
[ konfiguracja_bloku ]
end for ;
Konfiguracja składnika składa się ze specyfikacji składnika oraz część wiążącej składnik z jednostką projektową. Specyfikacja składnika określa składnik w bloku lub ciele architektonicznym, który konfigurujemy. Część wiążąca składnika wskazuje, którą jednostkę projektową wybraliśmy, aby przyporządkować ją do danych składnika.
wiązanie_składnika ::=
[ use entity nazwa_jednostki_projektowej [(nazwa_ciała_architektonicznego)]
| use configuration nazwa_konfiguracji
| use open ]
[ generic map (mapowanie_parametrów) ]
[ port map (mapowanie_portów) ]
Jak widać, wiązanie składnika może się odbyć na trzy sposoby:
bezpośrednio; w przypadku, gdy wskazujemy bezpośrednio na wybraną jednostkę projektową, w miejsce składnika przyporządkowana zostaje dana jednostka projektowa (nazwa jej może być złożona tzn. że jednostka może znajdować się w dowolnej bibliotece),
za pomocą innej konfiguracji; nasza konfiguracja wykorzystuje inną, wcześniej zdefiniowaną konfigurację, która wiąże wybrany składnik,
za pomocą słowa kluczowego "null", które oznacza, że składnik nie jest połączony z żadną jednostką projektową; taką sytuację nazywamy opóźnionym wiązaniem składnika.
» Biblioteki «
Język VHDL pozwala tworzyć biblioteki projektowe. Biblioteki są to, jak sama nazwa wskazuje, pewne zbiory elementów bibliotecznych, które możemy wykorzystać. Mogą one zawierać zarówno całe jednostki projektowe (także z wieloma architekturami) jak i wszelkiego rodzaju pakiety.
Biblioteki i elementy biblioteczne nie są bezpośrednio udostępnione w innych jednostkach projektowych. Dlatego też aby skorzystać z zawartości danej biblioteki należy posłużyć się klauzulą biblioteczną i klauzulą dostępu.
klauzula_biblioteczna ::=
library lista_nazw_bibliotek ;
klauzula_dostępu ::=
use nazwa_wybrana { , nazwa wybrana } ;
Klauzula biblioteczna zapewnia nam dostęp do konkretnej biblioteki lub grupy bibliotek. Ich nazwy umieszczamy po słowie kluczowym "library". Natomiast klauzula dostępu umożliwia nam wybranie konkretnych lub wszystkich elementów określonej biblioteki. Ich nazwy umieszczamy w postaci aliasu (nazwy złożonej, ścieżki) po słowie kluczowym "use". Na dostęp do wszystkich elementów bibliotecznych danej biblioteki pozwala klauzula dostępu ze słowem kluczowym "all".
Działanie klauzul można pokazać na następujących przykładach:
a. dostęp do wszystkich elementów biblioteki "Biblioteka1" i całych ich zawartości:
library Biblioteka1 ;
use STD.all ;
b. dostęp do konkretnego elementu bibliotecznego Biblioteka1 (np. pakietu Pakiet_biblioteki1) i całej jego zawartości:
library Biblioteka1 ;
use Biblioteka1.Pakiet_biblioteki1.all ;
c. dostęp do konkretnego elementu bibliotecznego Biblioteka1 (np. pakietu Pakiet_biblioteki1) i określonej jego zawartości (np. procedury Test):
library Biblioteka1 ;
use Biblioteka1.Pakiet_biblioteki1.Test ;
Klauzulę biblioteczną i dostępu umieszczamy przed jednostką projektową, w której wykorzystujemy określone elementy biblioteczne.
library ... ;
use ... ;
entity ... is
...
end;
W języku VHDL wbudowane są dwie podstawowe biblioteki:
biblioteka Work - do której zwykle dołączane są nasze projekty oraz pakiety, i do których to (dzięki tej bibliotece) mamy dostęp w innych projektach i pakietach,
biblioteka STD - zawierająca predefiniowane pakiety, w których znajdują się deklaracje typów i procedur podstawowych, wykorzystywanych przy definiowaniu operacji arytmetycznych, logicznych oraz zapisu i odczytu danych.
W/w biblioteki nie wymagają żadnych klauzul, ponieważ dostęp do nich jest nieograniczony w każdym elemencie definiowanym w języku VHDL.
» Typy i podtypy «
W języku VHDL, podobnie jak w przypadku innych języków programowania, można definiować typy i podtypy. Są to niezbędne elementy języka, dzięki którym mamy pewność co do poprawności działania projektu tj. np. zgodności parametrów aktualnych, portów wejściowych lub wyjściowych. Typy określają zatem własności deklarowanych w projekcie obiektów (tzn. stałych, zmiennych, sygnałów i plików). Innymi słowy, typ to odpowiednio nazwany zbiór wartości o wspólnych charakterystycznych cechach. Z typami wiążą się także ściśle określone operacje, które mogą być wykonywane tylko i wyłącznie na danych obiektach konkretnego typu.
Każdy typ możemy zdeklarować w dwojaki sposób stosując: pełną lub niepełną deklarację typu. Różnica w tych deklaracjach polega na tym, że nie trzeba od razu określać własności danego typu, a jedynie zasygnalizować jego istnienie. Taki typ musi być jednak później (w naszym projekcie) precyzyjnie zdefiniowany. Deklarację pełną i niepełną tworzymy w następujący sposób:
pełna_deklaracja_typu ::=
type identyfikator is definicja_typu ;
np.: type typ_danych1 is bit_vector (0 to 9);
niepełna_deklaracja_typu ::=
type identyfikator ;
np.: type typ_danych2 ;
Ze składni wynika, że niezbędnym elementem typu jest identyfikator. Może być nim dowolna nazwa, która nie została wcześniej użyta w projekcie, ponieważ od momentu przypisania jej określonemu typowi, będzie ona reprezentować dany typ w środowisku projektowym. Dzięki niej możemy określać później obiekty, które charakteryzować się będą właściwościami zdefiniowanymi po słowie kluczowym "is". Definicję typu określa odpowiednia składnia, zależna od konkretnego typu.
Mając już określony typ możemy wykorzystać go do stworzenia podtypu, który będzie posiadał tylko niektóre właściwości danego typu. Inaczej mówiąc podtypy to jakby "podzbiory typów". Podtyp tworzymy za pomocą:
deklaracja_podtypu ::=
subtype identyfikator is wskaźnik_typu ;
Występujący tu wskaźnik typu określa nam typ bazowy, na którym tworzymy podtyp, np.:
subtype podtyp_typu_danych is typ_danych1 ;
Jednak oprócz samej nazwy typu bazowego może on także zawierać pewne ograniczenie, które chcemy zastosować, np.:
subtype podtyp_typu_danych is typ_danych1 (0 to 4) ;
W ten sposób powstał przykładowy podtyp jednowymiarowej tablicy, przy pomocy którego możemy tworzyć sygnały w postaci słowa 5-bitowego.
Typy, które możemy zdefiniować dzielimy na:
W języku VHDL (w pakiecie Standard) są już predefiniowane typy, podtypy i operacje podstawowe, z których można korzystać przy definiowaniu nowych typów i podtypów. Są to:
Tabela. Predefiniowane typy i podtypy języka VHDL
Rodzaj typu lub podtypu |
Opis |
Bit |
wartości "0" i "1" |
Bit_Vector |
typ tablicowy złożony z elementów typu Bit |
Boolean |
wartości logiczne False i True (Fałsz i Prawda) |
Character |
znaki kodu ASCII |
Delay_Length |
podtyp typu Time, w którym ograniczono zakres |
File_Open_Kind |
rodzaj dostępu do pliku |
File_Open_Status |
status w jakim znajduje się plik |
Integer |
liczby całkowite określone przez implementację |
Natural |
liczby całkowite nieujemne |
Positive |
liczby całkowite większe od zera |
Real |
liczby rzeczywiste określone przez implementację |
Severity_Level |
poziomy ważności komunikatu |
String |
typ tablicowy złożony ze znaków kodu ASCII |
Time |
typ fizykalny definiujący jednostki czasu |
Typ skalarny
Wartości typu skalarnego są uporządkowane wg określonej skali, co pozwala na określenie zależności między nimi. Podstawową własnością typów skalarnych jest ich zakres, który określa granice zbioru wartości oraz kierunek zmian wartości w tym zbiorze. Zakres najłatwiej określić przy pomocy zakresu prostego:
zakres_prosty ::= wyrażenie_proste kierunek wyrażenie_proste ;
kierunek ::= to | downto;
Należy zauważyć, że kierunek jest ściśle określony z granicami zakresu. W ten sposób kierunek określony słowem "to" zawsze wskazuje od wartości najniższej do najwyższej. A kierunek "downto" odwrotnie. Podanie kierunku w innym kontekście spowoduje określenie zbioru pustego.
Praca z typami skalarnymi jest możliwa przez wykorzystanie operatorów i atrybutów predefiniowanych w pakiecie Standard.
Typy skalarne mogą być składnikami innych typów np.: złożonych oraz typów wskaźnikowych.
Wśród typów skalarnych wyróżniamy:
a. typ skalarny wyliczeniowy
Jest to typ definiowany przez wartości nienumeryczne złożone z identyfikatorów i znaków ASCII. Jest on definiowany w następujący sposób:
definicja_typu_wyliczeniowego ::= (literał_wyliczeniowy { , literał_wyliczeniowy }) ;
W całym zakresie typu nie może się powtórzyć żaden element zbioru. Definicję typu wyliczeniowego tworzymy za pomocą wypisania kolejnych elementów zbioru w nawiasach okrągłych.
Każdy element w zbiorze posiada przyporządkowaną liczbę naturalną, dzięki czemu możliwym jest odwołanie do tej liczby jak do konkretnego elementu. Kierunek tak utworzonego ciągu liczb naturalnych (zakres typu wyliczeniowego) jest zawsze narastający.
Zdarzyć się może, że ten sam element (np.: '1' lub '0') powtarza się w wielu typach. Następuje wtedy tzw. przeciążenie tych literałów, a co za tym idzie ich typ wynika z kontekstu w jakim ten literał wystąpił.
Przykłady:
type Bity is ('0', '1');
type Alfabet is (A, B, C, D, E, F);
b. typ skalarny całkowity
Jest to typ numeryczny, który składa się z liczb całkowitych należących do zakresu określonego przez implementacje (standardowo od -2147483647 do +2147483647). Jest on definiowany w następujący sposób:
definicja_typu_całkowitego ::= range zakres_prosty ;
Typ całkowity tworzony przez nas, to inaczej podtyp liczb całkowitych z użyciem zakresu. Są to nadal liczby całkowite z tym, że ich wybór ograniczony zostaje przez określony przez nas przedział.
Typ ten można także zmieniać poprzez zastosowanie wyrażenia lokalnie statycznego (tzn. przed próbą wykorzystania takiego typu, wartość zakresu musi być wyliczona, a co za tym idzie parametr użyty przez nas musi być wartością stałą).
Przykłady:
type liczbaABC is range 1 to 3;
type wyraz is range 10 downto 0;
type liczby_plus_minus is range -10 to 10;
type typ_wyliczany is range -10 to 7;
c. typ skalarny rzeczywisty
To kolejny typ numeryczny. Zawiera on liczby rzeczywiste z zakresu określonego przez implementację (standardowo od -1.0E38 do +1.0E38). Definiuje się go tak jak typ całkowity:
definicja_typu_całkowitego ::= range zakres_prosty ;
Typ ten ma takie same własności jak typ całkowity z tą różnicą, że liczby występujące w typie rzeczywistym zawierają symbol kropki.
Dopuszcza się także aby zakres był wyliczany (podobnie jak w typie całkowitym). Pamiętać jednak należy aby użyty parametr był wyrażeniem stałym przed operacją wyliczania.
Przykłady:
type typ_Liczba_A is range 3.5 to 10.3 ;
type Poziom_Halasu is range -0.01 downto -10.66 ;
d. typ skalarny fizykalny
Umożliwia zdefiniowanie jednostek miary dla zadanej wielkości występującej w badanym procesie fizycznym, tj. długości, czasu, napięcia, itp.. W deklaracji występuje jednostka podstawowa oraz kilka jednostek będących wielokrotnością jednostki podstawowej.
definicja_typu_fizykalnego ::= range zakres_prosty
units jednostka_podstawowa {jednostka_wielokrotna}
end units [nazwa_typu_fizykalnego];
W pakiecie Standard jest predefiniowany typ fizykalny TIME. Typ ten posiada przyporządkowany zestaw operatorów. Te same operatory są określone także dla typów zadeklarowanych przez użytkownika: =, /=, <, <=, >, >=, +, -, abs, *, /
Przykład:
type typ_Napiecie is range 0 to 1E18
units
nV;
uV = 1000 nV;
mV = 1000 uV;
V = 1000 mV;
KV = 1000 V;
end units typ_Napiecie;
Typ złożony
Są to typy, dzięki ktorym możemy tworzyć obiekty złożone. Wśród tych typów wyróżniamy:
a. typ złożony tablicowy
Typ ten grupuje elementy tego samego typu, każdy element przyporządkowany ma swój indeks, dzięki któremu możemy odwołać się do niego w naszym programie. Typ ten możemy podzielić na typ tablicowy ograniczony (gdy podamy jego zakres) i typ tablicowy nieograniczony (gdy nie znamy wielkości przyszłej tablicy).
definicja_typu_tablicowego_ograniczonego ::=
array ( ograniczenie_indeksowe )
of wskaźnik_typu_elementu_tablicy ;
definicja_typu_tablicowego_nieograniczonego ::=
array ( definicja_podtypu_indeksu_tablicy {, definicja_podtypu_indeksu_tablicy} )
of wskaźnik_typu_elementu_tablicy ;
W pakiecie Standard zdefiniowano dwa typy jednowymiarowe nieograniczone: STRING (nieograniczony ciąg znaków) i BIT_VECTOR (nieograniczony ciąg bitów).
Przykłady:
type bajt is array (7 downto 0) of bit ;
type pamiec is array (natural range <>) of bit_vector ;
b. typ złożony rekordowy
Jest typem za pomocą którego możemy łączyć ze sobą różne inne typy. Pomocny jest np. w sytuacji gdy chcemy używając jednego typu zapisać dane personalne jakiejś osoby. Stworzony obiekt typu rekordowego może zawierać zatem "Imię" (ciąg znaków - string), "Nazwisko" (ciąg znaków - string), "Wiek" (liczba naturalna - natural).
definicja_typu_rekordowego ::= record lista_identyfikatorów :
wskaźnik_typu {, lista_identyfikatorów : wskaźnik_typu}
end record ;
Ważnym jest, że kolejność występowania poszczególnych identyfikatorów jest ustalana raz - przy deklaracji - a w związku z tym dostęp do nich musi być tak samo precyzowany.
Przykład:
type dane_personalne is record
Imie, Nazwisko : string ;
Wiek : natural ;
end record ;
Typ wskaźnikowy
Powstaje z typu skalarnego, złożonego lub innego wskaźnikowego. Dużą zaletą tego typu jest to, że obiekt typu wskaźnikowego jest wyliczany w procesie alokacji. Zmienia się zatem dynamicznie.
definicja_typu_wskaźkowego ::= access wskaźnik_typu ;
Obiektami typu wskaźnikowego są tylko zmienne. Początkowa wartość takiego obiektu określona jest jako "null", a dopiero w programie za pomocą operatora "new" tworzymy jego nową wartość.
Przykłady:
type parametry is access parametry_temp1 ;
new parametry_temp1 (x, y) ;
Typ plikowy
Pozwala na deklarację obiektów reprezentujących w środowisku pliki. W plikach mogą być przechowywane wartości typu, który został podany w deklaracji.
definicja_typu_plikowego ::= file of nazwa_typu ;
Typ podany w deklaracji może być tylko typem skalarnym lub złożonym (bez typów wskaźnikowego i plikowego).
Posługując się obiektami typu plikowego, niejawnie generujemy procedury związane z ich obsługą. Są to procesy otwierania, odczytywania, zapisywania, zamykania plików.
Przykład:
type plik_16bit is file of bit_vector range 15 downto 0 ;
» Wyrażenia «
Wyrażenia w języku VHDL określają głównie operatory, nazwy i operandy. Inaczej mówiąc słowy są to wszystkie te elementy języka, które towarzyszą słowom kluczowym, ale pełnią one nie mniej ważną funkcję.
Operatory
Operatory w języku VHDL są bardzo zróżnicowane. Znajdziemy tu wiele popularnych operatorów z algebry (dodawanie to "+", mniejszość to "<") ale i wiele charakterystycznych dla tego języka (przesunięcie logiczne w prawo to "srl", sklejanie to "&"). Kolejność wykonania zależy od priorytetu jaki posiada dany operator.
Tabela. Priorytety operatorów
1. |
** |
abs |
not |
|
|
|
|
2. |
* |
/ |
mod |
rem |
|
|
|
3. |
+ |
- |
|
|
|
|
|
4. |
+ |
- |
& |
|
|
|
|
5. |
sll |
srl |
sla |
sra |
rol |
ror |
|
6. |
= |
\= |
< |
<= |
> |
>= |
|
7. |
and |
or |
nand |
nor |
xor |
xnor |
Zmiana kolejności wykonania może nastąpić po zastosowaniu nawiasów okrągłych "( )", których liczba zagęszczenia jest dowolna (należy jednak pamiętać, że zbyt rozległe stosowanie nawiasów prowadzi do braku czytelności projektu).
a. operatory logiczne
Realizują działania logiczne. Operatory logiczne są zdefiniowane dla typów BIT ('0' lub '1') oraz BOOLEAN (True lub False).
not |
Neguje wartość logiczną |
and |
Wynik jest prawdą gdy wszystkie czynniki są prawdą |
or |
Wynik jest prawdą gdy przynajmniej jeden ze składników jest prawdą |
xor |
Wynik jest prawdą gdy występuje nieparzysta ilość składników będących prawdą |
nand |
Wynik jest fałszem gdy wszystkie czynnik nie jest prawdą |
nor |
Wynik jest fałszem gdy przynajmniej jeden ze składników jest prawdą |
nxor |
Wynik jest fałszem gdy występuje nieparzysta ilość składników będących prawdą |
Przykłady:
Y <= not A;
Signal <= G and H;
Op := A nxor B;
b. operatory relacji
Porównują dwa argumenty (lewy i prawy). Operandy zawsze muszą być tego samego typu, natomiast wynik jest zawsze typu BOOLEAN (True lub False); Operator '=' i '/=' są zdefiniowane dla wszystkich dostępnych typów oprócz typu plikowego.
= |
Określa równość dwóch operandów |
/= |
Określa nierówność dwóch operandów |
< |
Określa mniejszość pierwszego operandu od drugiego |
<= |
Określa mniejszość równość pierwszego operandu od drugiego lub ich równość |
> |
Określa większość pierwszego operandu od drugiego |
>= |
Określa większość pierwszego operandu od drugiego lub ich równość |
Przykłady:
10 < 19 -- True;
"10" >= "11" -- False;
c. operatory przesunięć
Służą do wykonywania działań na jednowymiarowych tablicach (z elementami typu BIT lub BOOLEAN); Lewy operand to dana tablica; Prawy operand to liczba pozycji do przesunięcia lub obrócenia.
sll |
Przesuwa logicznie w lewo (miejsca zwolnione zapisywane są wartością '0') |
srl |
Przesuwa logicznie w prawo (miejsca zwolnione zapisywane są wartością '0') |
sla |
Przesuwa arytmetycznie w lewo (miejsca zwolnione zapisywane są wartością '1') |
sra |
Przesuwa arytmetycznie w prawo (miejsca zwolnione zapisywane są wartością '1') |
rol |
Obraca logicznie w lewo (pierwszy bit staje się drugim, drugi - trzecim, itd.) |
ror |
Obraca logicznie w prawo (pierwszy bit staje się ostatnim, drugi - pierwszym, itd.) |
Przykłady:
A <= "0110" ;
A sll 2 - A <= "1000";
A sra 1 - A <= "1011";
d. operatory addytywne
Realizują działania arytmetyczne, z wyjątkiem operatora sklejania, który "łączy" bity tworząc jednowymiarowe tablice; Typy operandów mogą być dowolnego typu numerycznego;
+ |
Dodaje arytmetycznie operandy |
- |
Odejmuje arytmetycznie operandy |
& |
Skleja operandy |
Przykłady:
Q := 2 + W;
Y := I - J;
U := '0' & '1'; -- U := "01"
e. operatory znaku
Określają znak argumentu. Typy operandów mogą być dowolnego typu numerycznego; Ze względu na niski priorytet tego operatora zalecane jest stosowanie ich w nawiasach okrągłych.
+ |
Określa dodatni znak operandu |
- |
Określa ujemny znak operandu |
Przykłady:
Z := A ** (-B);
X := -Z;
f. operatory multiplikatywne
Realizują działania mnożenia. Operandami są wszystkie typy całkowite, zmiennoprzecinkowe oraz fizykalne; Natomiast operatory mod oraz rem zostały zdefiniowane tylko dla całkowitych.
* |
Mnoży operandy |
/ |
Dzieli operandy |
mod |
Oblicza moduł operandu |
rem |
Oblicza resztę z dzielenia |
Przykłady:
C := 2 * 2;
A := B mod 2;
g. operatory różne
Operator potęgowania jest przeznaczony dla typu całkowitego i zmiennoprzecinkowego; Prawy operand musi być typu całkowitego.
** |
Potęguje operandy |
abs |
Oblicza wartość bezwzględną operandu |
Przykłady:
2 ** 3 = 8
abs 6.8 = 6
Nazwy
W deklaracjach jednostek języka VHDL występują identyfikatory, które następnie określają daną jednostkę. Dostęp do takiej jednostki jest możliwy poprzez użycie nazwy, która wskazuje tę jednostkę. Nazwy mogą również wskazywać na obiekty typu wskaźnikowego, elementy obiektu typu złożonego, części obiektów typu złożonego lub atrybuty jednostki, która w swej deklaracji posiada identyfikator.
Wśród nazw wyróżniamy:
a. nazwa prosta
b. symbol operatora
c. nazwa wybrana
d. nazwa indeksowa
e. nazwa wyciskowa
f. nazwa atrybutowa
a. nazwa prosta
Składa się tylko z identyfikatora, który został przyporządkowany w czasie deklaracji nazwy.
Przykłady:
variable X : integer -- nazwą prostą jest X
X := 2 * 2;
b. symbol operatora
Nazwa ta ma postać napisu i wskazuje na funkcję, która została zadeklarowana z takim symbolem operacji
Przykłady:
function "+"(a,b : integer) return real; -- nazwą jest symbol dodawania
Y := "+"(2,3);
c. nazwa wybrana
Wskazuje na jednostkę, która została zadeklarowana w innej jednostce lub bibliotece projektów. Składa się z trzech części: prefiksu (określającego dowolną jednostkę), kropki (.), sufiksu (precyzującego wybór elementu w danej jednostce). Gdy chcemy wybrać wszystkie elementy jednostki, w części sufiksu używamy słowa "all".
Przykłady:
Work.FunkcAnd;
Std_Logic.all;
Work.AA.Band;
d. nazwa indeksowa
Wskazuje na element tablicy, który wyznaczany jest za pomocą listy wyrażeń będących indeksami tablicy. Składa się z prefiksu (j.w.) i wyrażenia w nawiasach okrągłych za pomocą którego następuje wybór.
Przykłady:
TablicaA(6);
TabZ(4);
e. nazwa wycinkowa
Wskazuje na część obiektu jednowymiarowych tablic. Budową jest ona bardzo podobna do nazwy indeksowej. Różnica tkwi w podaniu zakresu w części precyzującej wybór.
Przykłady:
Tabela(7 downto 5);
TblO(4 to 6);
f. nazwa atrybutowa
Wskazuje na pewne własności jednostki (np.: wartości, funkcje, typy, zakresy, sygnały lub stałe). Składnią jest podobna do nazwy wybranej. Różnica to apostrof zamiast kropki.
Przykłady:
Clk'event;
WartoscX'left(2);
» Obiekty «
Język VHDL jest wyposażony w następujące obiekty: stałe, zmienne, sygnały i pliki. Każdy obiekt ma przyporządkowany określony typ oraz może mieć przypisaną wartość początkową (jeśli nie jest ona przypisana, przypisywana jest automatycznie najniższa wartość określonego typu). Charakterystycznym dla obiektów jest ich jawność (tzn. niektóre z obiektów generują się bez wiedzy projektującego - obiekty niejawne).
a. stała
Obiekt, którego wartość jest zawsze stała.
deklaracja_stałej ::= constant lista_identyfikatorów : wskaźnik_typu [ := wyrażenie ];
Przykłady:
constant A : integer := 7 ;
constant GJ : bit;
b. zmienna
Obiekt ten może przyjmować wartości, które mogą być zmieniane w czasie symulacji. Służą do przechowywania informacji lokalnych w procesach i podprogramach.
deklaracja_zmiennej ::= variable lista_identyfikatorów : wskaźnik_typu [ := wyrażenie ];
Przypisanie wartości do zmiennej jest realizowane natychmiast po instrukcji przypisania.
Przykłady:
variable delay : time;
variable x : real := 2.5;
c. sygnał
Obiekt ten może przyjmować wartości, które mogą być zmieniane w trakcje symulacji. Wykorzystuje się je do połączenia pomiędzy składnikami projektu.
deklaracja_sygnału ::= signal lista_identyfikatorów : wskaźnik_typu [rodzaj_sygnału] [ := wyrażenie ];
Rodzaj sygnału może przyjąć wartość "register" (dla sygnałów typu rejestrowego) i "bus" (dla sygnałów typu magistralowego). Różnica polega na możliwości wykorzystania niejawnego sygnału GUARD dla sygnału typu "register" (Przypisanie sygnału nastąpi dopiero gdy GUARD = True).
Przypisanie wartości do sygnału może być opóźnione o żądany czas.
Przykłady:
signal SgnA : bit;
signal SgnB : bit_vector (7 downto 0);
d. plik
Obiekt ten reprezentuje w programie pliki fizyczne, w których mogą być przechowywane wartości określonego typu.
deklaracja_pliku ::= file lista_identyfikatorów : wskaźnik_typu
[ [ open rodzaj_dostępu_do_pliku ] is logiczna_nazwa_pliku ];
Przykłady:
file plik1 : bit_vector;
file plik2 : bit_vector open WRITE_MODE is file of "\vhdl\test.hgj"
» Instrukcje sekwencyjne «
Wszystkie operacje realizowane przez nasz projekt mogą być definiowane przy pomocy instrukcji sekwencyjnych. Podstawowe cechy tych instrukcji to:
są one wykonywane sekwencyjnie, czyli kolejno, jedno po drugim w porządku, w jakim zostały one zapisane w projekcie,
mogą być stosowane tylko w procesach lub podprogramach,
mogą być poprzedzone etykietą.
Wśród instrukcji sekwencyjnych wyróżniamy:
a. instrukcja sekwencyjna jeżeli
b. instrukcja sekwencyjna następna
c. instrukcja sekwencyjna oczekiwania
d. instrukcja sekwencyjna pętli
e. instrukcja sekwencyjna powrotu
f. instrukcja sekwencyjna przypadku
g. instrukcja sekwencyjna przypisania sygnału
h. instrukcja sekwencyjna przypisania zmiennej
i. instrukcja sekwencyjna pusta
j. instrukcja sekwencyjna raportu
k. instrukcja sekwencyjna wyjścia
l. instrukcja sekwencyjna wywołania procedury
m. instrukcja sekwencyjna założenia
a. instrukcja sekwencyjna jeżeli
Można nią sterować kolejnością wykonywania innych instrukcji sekwencyjnych.
instrukcja_jeżeli ::= [etykieta :] if warunek then
instrukcje sekwencyjne
{elsif warunek then
instrukcje sekwencyjne}
[else instrukcje sekwencyjne]
end if [etykieta] ;
W swej budowie zawiera może zawierać szereg warunków umożliwiających wykonanie odpowiedniej sekwencji poleceń. Warunki te umieszczamy po słowach: pierwszy warunek - słowo "if", każdy kolejny - słowo "elsif". Operacja nie spełniająca żadnego ze zdefiniowanych warunków, powoduje wykonanie sekwencji po słowie "else", lub w przypadku jego braku kończy wykonywanie instrukcji przechodząc do kolejnych instrukcji po słowach "end if".
Przykłady:
if Sgn = A then
X := 1;
end if;
if Clk = '1' then
Sg <= SgA and SgB else Sg <= '0'
end if;
if signal = '01' then
Y <= A after 2ns;
elsif signal = "10" then
Y <= B after 2ns;
else
Y <= C;
end if;
b. instrukcja sekwencyjna następna
Pozwala zmienić kolejność wykonywania instrukcji zadeklarowanych w pętli.
instrukcja_następna ::= [etykieta :] next [etykieta_pętli] [when warunek];
Gdy występuje słowo "when" i warunek, to dopiero spełnienie warunku spowoduje wykonanie danej instrukcji wyjścia. Gdy występuje po słowie "next" etykieta, to niespełnienie warunku spowoduje przejście do tej etykiety i realizacje instrukcji po niej;
Przykład:
X0 : for i in 1 to 3 loop
X1 : SgnA(i) <= '0';
next X0 when warunek1 = '1'; -- jeśli warunek1 = False, to wykonane
X2 : SgnB(i) <= '1'; -- zostaną instr. po etykiecie X0, jeśli True - po X2
end loop X0;
c. instrukcja sekwencyjna oczekiwania
Powoduje zawieszenie wykonywania procesu lub procedury do czasu gdy zostanie spełniony warunek wznowienia działania.
instrukcja_oczekiwania ::= [etykieta :] wait [until warunek_logiczny]
[for wyrażenie_czasu] [on nazwa_sygnału { , nazwa_sygnału}] ;
Wznowienie może nastąpić po spełnieniu warunku po słowie "until" lub upłynięciu czasu podanym po słowie "for" lub zmianie sygnałów zadeklarowanych po słowie "on". W przypadku braku jakiegokolwiek warunku wznowienia proces zostanie zawieszony na czas nieokreślony - nigdy nie zostanie wznowiony;
Przykłady:
wait until clk = '1';
wait for 10ns;
wait on SgnA, SgnB, SgnC;
wait;
d. instrukcja sekwencyjna pętli
Wielokrotnie uruchamia sekwencję instrukcji zawartych w pętli.
instrukcja_pętli ::= [etykieta :] [schemat_iteracji] loop
instrukcje sekwencyjne
end loop [etykieta];
Liczba powtórzeń zależy od schematu iteracji poprzedzającego słowo "loop". Jeśli schemat iteracji jest nie określony pętla będzie cały czas realizowana.
schemat_iteracji ::= while warunek | for identyfikator in zakres_dyskretny
Przy schemacie iteracji ze słowem "while" pętla będzie realizowana do czasu spełnienia warunku. Przy schemacie iteracji ze słowem "for" liczba powtórzeń jest określona przez zakres dyskretny.
Przykłady:
L1 : loop
clk <= not clk after 1us;
end loop L1;
L2 : while x = 2 loop
SgnA <= '1';
end loop L2;
L3 : for z in 1 to 4
Tbl(z) = '0';
end loop L3;
e. instrukcja sekwencyjna powrotu
Umożliwia zakończenie wykonywania nawet najbardziej zagnieżdżonego podprogramu.
instrukcja_powrotu ::= [etykieta :] return [wyrażenie] ;
Może ona być użyta tylko w procedurach lub funkcjach.
Przykłady:
return;
return rezultat;
f. instrukcja sekwencyjna przypisania zmiennej
Umożliwia zmianę wartości wybranej zmiennej. Nowa wartość zostaje wyliczana z wyrażenia po prawej stronie i przypisana bezpośrednio w chwili zakończenia wyliczania.
instrukcja_przypisania_zmiennej ::= [etykieta :] cel_przypisania := wyrażenie ;
Cel przypisania musi być identyczny co wyrażenia.
Przykłady:
X := 5;
S(2) := '1';
G(7 downto 3) := 9;
RekordA.f := 34;
g. instrukcja sekwencyjna przypisania sygnału
Umożliwia zmianę wartości wybranego sygnału. Nowa wartość zostaje wyliczana z wyrażenia po prawej stronie i przypisana w momencie zdefiniowanym przez projektanta.
instrukcja_przypisania_sygnału ::=
[etykieta :] cel_przypisania <= [rodzaj opóźnienia] kształt_przebiegu;
Cel przypisania musi być identyczny co wyrażenia.
rodzaj_opóźnienia ::= transprt | [reject wyrażenie_czasowe] inertial
Rodzaj opóźnienia uzależniony od słowa "transprt", które powoduje nadanie tego samego opóźnienia niezależnie od szerokości sygnału. Natomiast "inertial" powoduje nadanie opóźnienia uzależnionego od szerokości sygnału (np.: gdy impuls jest mniejszy od 1ns, jest on w sygnale wynikowym pomijany). Dodatkowe zastosowanie słowa "reject" umożliwia dowolną regulację warunku szerokości sygnału. W przypadku nie wystąpienia słów "transprt" lub "inertial" zakłada się, że jest słowo "inertial";
kształt_przebiegu ::= wyrażenie [after wyrażenie czasowe] | null
Wyrażenie, które jest przypisywane może być opóźnione o zadany czas umieszczony po słowie "after". Możliwe jest zastosowanie w jednej instrukcji przypisania sygnału kilku wartości z zadanym różnym opóźnieniem.
Przykłady:
SgnA <= '1';
SgnB <= SgnA after 1ns;
SgnC <= SgnA after 2ns, SgnB after 3ns;
SgnD <= "011" after 2ms;
SgnD <= transport S_in after 1ns;
SgnE <= inertial S_in after 1ns;
SgnF <= reject 1us inertial S_in after 1ns;
h. instrukcja sekwencyjna przypadku
Steruje kolejnością sekwencji instrukcji w zależności od wartości zadanego warunku.
instrukcja_przypadku ::= [etykieta :] case wyrażenie is
when wartości_wyboru => instrukcje sekwencyjne
{ when wartości_wyboru => instrukcje_sekwencyjne }
end case [etykieta] ;
Wyrażenie, które podlega warunkowi musi być typu dyskretnego lub może być jednowymiarową tablicą, której elementy są typu złożonego ze znaków kodu ASCII (character, string, bit_vector). Wartości wyboru muszą obejmować wszystkie możliwości. W przypadku gdy interesujące nas wartości wyboru zostały określone, i mimo to zostały nam jeszcze wolne możliwości, określamy sytuację określamy sytuację ze słowem "others". Ważnym jest aby słowo to było wymienione jako ostania możliwość wyboru.
Przykład:
case A is
when "00" => ster <= '1' after 1ps;
when "01" => ster <= '0';
when others => null;
end case;
i. instrukcja sekwencyjna pusta
Nic nie wykonuje.
instrukcja_pusta ::= [etykieta :] null ;
Przykład:
null;
j. instrukcja sekwencyjna raportu
Służy do wyświetlania komunikatów w czasie procesu symulacji.
instrukcja_raportu ::= [etykieta :] report wyrażenie [severity wyrażenie] ;
Pole "report" służy do umieszczenia zawartości komunikatu, a pole "severity" służy do określenia ważności danego komunikatu. Są cztery poziomy severity_level: note (notatka), warning (ostrzeżenie), failure (błąd), error (błąd krytyczny). W przypadku zaistnienia błędu krytycznego zatrzymywany jest proces symulacji. W przypadku nie wystąpienia słowa "severity" uznaje się poziom ważności jako "note".
Przykłady:
report "Koniec pętli";
report "Liczba A mniejsza od zera" severity FAILURE;
k. instrukcja sekwencyjna wyjścia
Przerywa wykonanie instrukcji pętli, w której jest zagnieżdżona.
instrukcja_wyjścia ::= [etykieta :] exit [etykieta_pętli] [when warunek] ;
Przerwanie następuje po spełnieniu warunku po słowie "when". Nie podanie etykiety pętli powoduje przerwanie tylko tej pętli, w której instrukcja wyjścia się znajduje. Jeśli chcemy przerwać pętle zagnieżdżone, musimy podać etykietę.
Przykłady:
exit when clk = '1';
exit L3 when reset = '1'; -- jeśli warunek będzie równy True nastąpi skok do etykiety L3;
l. instrukcja sekwencyjna wywołania procedury
Służy do uruchamiania ciała procedury w trakcie procesu symulacji
instrukcja_wywołania_procedury ::=
[etykieta :] nazwa_procedury [(paramentry_aktualne)] ;
Występowanie i typ parametrów aktualnych jest ściśle uwarunkowane od definicji danej procedury.
Przykłady:
Procedura2000(a, b, c);
StoLatProcedura ;
m. instrukcja sekwencyjna założenia
Sprawdza podany warunek i w przypadku jego spełnienia wyświetla komunikat. Instrukcja ta jest bardzo podobna do instrukcji raportu.
instrukcja_założenia ::=
[etykieta :] assert warunek [report wyrażenie] [severity wyrażenie] ;
(Informacje o częściach instrukcji ze słowem "report" i "severity" tak jak w instrukcji raportu)
Przykłady:
assert test = "11" report "Sygnał posiada wartość 11" severity NOTE;
assert Bum = 1 report "Przeciążenie sygnału" severity ERROR;
» Instrukcje współbieżne «
Instrukcje współbieżne umożliwiają modelowanie struktury i zachowania projektowanych układów cyfrowych. Podstawowe cechy tych instrukcji to:
są one wykonywane asynchronicznie, niezależnie od siebie, w nieokreślonej kolejności,
nie mogą wystąpić w podprogramach,
mogą być poprzedzone etykietą,
Każda z instrukcji współbieżnej może być zastąpiona równoważną instrukcją sekwencyjną. Dlatego też wydawać by się mogło, że są one zbędne. W praktyce tak nie jest. Różnica między instrukcjami współbieżnymi polega na tym, że instrukcje współbieżne są na wyższym szczeblu hierarchii języka niż instrukcje sekwencyjne. Instrukcje współbieżne dotyczą bezpośrednio operacji na jednostkach projektowych. Określają ich działanie (instrukcja procesu z wykorzystaniem instrukcji sekwencyjnych, instrukcja wywołania procedury, założenia, przypisania sygnału) i zależności (instrukcja konkretyzacji składnika). Instrukcje współbieżne także ułatwiają projektowanie (instrukcja konkretyzacji składnika, instrukcja powielania) i poprawiają czytelność projektu (instrukcja bloku).
Wśród instrukcji współbieżnych wyróżniamy:
a. instrukcja współbieżna bloku
b. instrukcja współbieżna konkretyzacji składnika
c. instrukcja współbieżna powielania
d. instrukcja współbieżna procesu
e. instrukcja współbieżna przypisania sygnału
f. instrukcja współbieżna wywołania procedury
g. instrukcja współbieżna założenia
a. instrukcja współbieżna bloku
Grupuje inne instrukcje współbieżne, definiuje fragment ciała jednostki projektowej, który zawarty jest pomiędzy słowami "block" oraz "end block".
instrukcja_bloku ::= etykieta block [(wyrażenie_dozoru)] [is]
nagłówek_bloku
jednostka_deklaracyjna_bloku
begin
instrukcje_współbieżne
end block [etykieta];
Stosuje się ją głównie po to aby polepszyć czytelność projektu.
Przykłady:
B1 : block (clk'event and clk = '1')
signal S : bit;
begin
S <= '1' after 2ns;
Y <= A and S;
end block B1;
b. instrukcja współbieżna konkretyzacji składnika
Umożliwia tworzenie projektu w postaci strukturalnego opisu jednostki projektowej.
instrukcja_konkretyzacji_składnika ::= etykieta : jednostka_konkretyzacji
[generic map aktualne_wartości_parametrów] [port map aktualne_porty] ;
Instrukcja ta może przybierać różne formy w zależności od tego co stanowi jednostkę konkretyzacji.
intantacja bezpośrednia - występuje gdy jednostka konkretyzacji jest poprzedzona słowem "entity"
entity nazwa_jednostki_projektowej [(nazwa_ciała_jedostki_projektowej)]
Jej dużą zaletą jest prostota budowy. Ma on jednak poważną wadę - uniemożliwia ona ingerowanie wewnątrz konkretyzowanej jednostki z poziomu naszego projektu.
Brak nazwy ciała jednostki projektowej spowoduje konkretyzację do tej architektury jednostki projektowej, która była ostatnio kompilowana.
Przykład:
Dec1 : entity Work.DecBcdLed (DecBody) port map (S1, S2);
specyfikacja konfiguracji - występuje gdy jednostka konkretyzowana jest wskazywana pośrednio przez składnik.
[component] nazwa_składnika
Aby jednak zastosować ten rodzaj konkretyzacji musimy zadeklarować składnik w naszym projekcie.
deklaracja_składnika ::= component identyfikator [is]
[sekcja_parametrów]
[sekcja_portów]
end component [identyfikator] ;
Strukturą deklaracja składnika jest bardzo zbliżona do sprzęgu jednostki projektowej i w taki sam sposób należy wypełniać odpowiednie sekcje w deklaracji składnika.
Przykład:
component AND_AB is
port (a, b : in bit; y : out bit);
end component AND_AB;
A1 : AND_AB port map (In1, In2, Out1);
A2 : AND_AB port map (In3, In4, In5, Out2);
Gdy już mamy składnik możemy go użyć w specyfikacji konkretyzacji.
specyfikacja_konfiguracji ::=
for lista_składników : nazwa_składnika wiązanie_składnika ;
Przykład:
for A1 : AND_AB use entity Work.AND2 (AND2_arch);
for others : AND_AB use entity Work. AND3;
Zastosowanie składnika znacznie poprawia wszechstronność naszego projektu, ponieważ każdy składnik raz zadeklarowany można w różnoraki sposób konkretyzować. Zaletą takiej metody konkretyzacji jest to, że raz określone parametry i porty jednostki konkretyzowanej możemy zmieniać za pomocą zmiany parametrów i portów składnika.
konfiguracja - występuje gdy jednostka konkretyzowana jest wskazywana pośrednio przez konfigurację.
configuration nazwa_konfiguracji
Jest to bardziej skomplikowana struktura od konkretyzacji z wykorzystaniem komponentów (patrz wyżej), lecz posiada te same zalety co i ona.
Przykład:
Cnt : configuration Work.Count port map (clk, Q);
c. instrukcja współbieżna powielania
Umożliwia szybsze i łatwiejsze tworzenie struktur projektowych, których ilość w projekcie jest znaczna. Dotyczyć to może np.: liczników zbudowanych z kilku takich samych przerzutników.
instrukcja_powielania ::= etykieta : schemat_generacji generate
[część_deklaracyjna
begin]
isnstrukcje_współbieżne
end generate [etykieta];
Mamy dostępne dwa typy generacji:
schemat pętlowy - wykorzystujemy gdy powielane struktury są w pełni regularne.
schemat_generacji_pętlowy ::= for identyfikator in zakres_dyskretny
schemat warunkowy - wykorzystujemy gdy powielane struktury nie są w pełni regularne.
schemat_generacji_warunkowy ::= if warunek
Przykłady:
G1 : for i in 1 to 8 generate
P1 : Przerz port map (S(i), S(i+1));
end generate;
G2 : if x = 1 generate
P2 : Przerz port map (S(x), S(x-1));
end generate;
d. instrukcja współbieżna procesu
Jest to podstawowa instrukcja współbieżna. Definiuje niezależny sekwencyjny proces reprezentujący zachowanie pewnej części projektu za pomocą instrukcji sekwencyjnych.
instrukcja_procesu ::= [etykieta :] [posponed] process [(lista_czułości)][is]
część_deklaracyjna_procesu
begin
instrukcje_sekwencyjne
end [posponed] process [etykieta] ;
Słowo "posponed" lub jego brak określa proces, którego wznowienie jest możliwe dopiero w następnym cyklu symulacji. Wznowienie w tym samym cyklu symulacji jest możliwe przez użycie słowa "nonpostponed".
Lista czułości opisuje w jakich przypadkach dany proces się wznowi. Można to zrobić np.: za pomocą listy sygnałów (dowolna zmiana któregoś z nich powoduje wzbudzenie). Lista ta może znajdować się po słowie "process" lub w miejscu instrukcji sekwencyjnych - jako polecenie "wait" (patrz sekwencyjna instrukcja oczekiwania).
Przykłady:
process (a)
signal x : integer ;
begin
y <= x and '1' after 1ns;
z <= y or x after 2ns;
end process;
e. instrukcja współbieżna przypisania sygnału
Działa analogicznie do sekwencyjnej instrukcji przypisania sygnału.
instrukcja_przypisania_sygnału ::=
[etykieta :] [postponed] warunkowe_przypisanie_sygnału
| [etykieta :] [postponed] selektywne_przypisanie_sygnału ;
Jak widać współbieżne przypisanie sygnału jest bardziej złożone od sekwencyjnego.
warunkowe_przypisanie_sygnału ::=
cel_przypisania <= [guarded] [mechanizm_opóźnienia]
{charakterystyka_przypisania when warunek else }
charakterystyka_przypisania [when warunek] ;
selektywne_przypisanie_sygnału ::=
with wyrażenie select
cel_przypisania <=
{charakterystyka_przypisania when wyrażenie_wyboru , }
charakterystyka_przypisania when wyrażenie_wyboru ;
Może się ono odbyć na cztery sposoby:
podstawowe przypisanie sygnału - jest to uproszczone warunkowe przypisanie sygnału (brak w nim sekcji warunkowej ze słowem "when"), ogranicza się zatem tylko do podania celu przypisania i wartości jaka ma być przypisana (opóźnienie po słowie "after" może wystąpić opcjonalnie).
Przykłady:
Syg12 <= '1';
Gap1 <= "011" after 1ns;
Gap2 <= "101" after 2ns, "010" after 6ns;
warunkowe przypisanie sygnału - jest to przypisanie sygnału uzależnione od podanych warunków po słowach "when".
Przykłady:
S4 <= '1' when clk = '1';
S99 <= "01" after 1us when clk = '0';
Sg0 <= '0' when clk = '1', '1' when clk ='0';
selektywne przypisanie sygnału - przypisywany sygnał wybierany jest z listy wyszczególnionych przypadków i odpowiadającym im warunkom.
Przykład:
with Sg_in select
y <= a and b when "001",
a or b when "010",
a xor b when "100",
'0' after 1us when others;
dozorowane przypisanie sygnału - przypisanie sygnału jest uwarunkowane od niejawnego sygnału GUARD, który jest wyliczany zawsze dla każdej instrukcji przypisania sygnału. Gdy wartość GUARD jest równa True następuje normalne przypisanie sygnału, gdy False nośnik sygnału jest odłączany od wartości (przypisanie wartości null). Zastosowanie tego typu przypisania jest możliwe po zastosowaniu słowa "guarded" (patrz warunkowe przypisanie sygnału)
Przykład:
SG_dozor <= guarded A nxor B when C = '1' ;
f. instrukcja współbieżna wywołania procedury
Działa i wygląda analogicznie do sekwencyjnej instrukcji wywołania procedury.
instrukcja_wywołania_procedury ::= [etykieta :] [posponed]
nazwa_procedury [(paramentry_aktualne)] ;
Jedyna różnica tkwi w możliwości zastosowania słowa "posponed" lub "nonposponed" (patrz współbieżna instrukcja procesu).
Przykłady:
procedura77 (Sgn1, A);
nonposponed DzialanieNaSyg (6, D, '1');
g. instrukcja współbieżna założenia
Działa i wygląda analogicznie do sekwencyjnej instrukcji założenia.
instrukcja_założenia ::= [etykieta :] [posponed]
assert warunek [report wyrażenie] [severity wyrażenie] ;
Jedyna różnica tkwi w możliwości zastosowania słowa "posponed" lub "nonposponed" (patrz współbieżna instrukcja procesu).
Przykład:
nonposponed assert Z = 13 report "Zmienna Z jest równa 13";
» Podprogramy «
Pewne działania projektu mogą się powtarzać. Aby uniknąć żmudnego powtarzania całych fragmentów programu opracowano takie struktury jak podprogramy. Raz zadeklarowane i zdefiniowane można wykorzystać w dowolnym miejscu naszej jednostki projektowej za pomocą odpowiedniej instrukcji wywołania sekwencyjnej lub współbieżnej (należy zauważyć że wywołanie funkcji odbywa się analogicznie do wywołania procedury).
W podprogramach używamy tylko i wyłącznie instrukcji sekwencyjnych.
a. procedura
Jest to podprogram, który może opisywać dowolne zachowanie się projektu (dotyczy to operacji na wszelkiego typu obiektach). Dużą zaletą procedury jest możliwość zastosowania w niej nieograniczonej liczby parametrów wejściowych i wyjściowych. Możliwe to jest dzięki strukturze procedury, która przypomina odrębną jednostkę projektową.
Każda procedura składa się z deklaracji procedury i ciała procedury.
deklaracja_procedury ::= procedure identyfikator
[(lista_parametrów_formalnych)] ;
Na liście parametrów formalnych mogą się znaleźć wszystkie typy obiektów. Dodatkowo dla stałych, zmiennych i sygnałów definiuje się kierunek przepływu informacji ("in", "out", "inout") oraz decyduje się o tym, jakie parametry formalne mogą być dostępne wewnątrz procedury. Procedura może także wystąpić bezparametrowo.
ciało_procedury ::= procedure identyfikator
[(lista_parametrów_formalnych)] is
część_deklaracyjna_podprogramu
begin
instrukcje_sekwencyjne
end [procedure] [identyfikator] ;
Jak widać ciało procedury ma początek identyczny ze składnią deklaracji procedury. Dlategoteż pisząc procedury można pominąć deklarację procedury unikając tym samym powtórzeń.
W części deklaracyjnej możemy zadeklarować wszystkie dodatkowe obiekty, których będziemy używać tylko i wyłącznie w procedurze.
Czasem zdarza się, że zaistnieją w projekcie procedury o tej samej nazwie, lecz różnych pod względem listy parametrów formalnych. Mówimy wtedy, że dane procedury są przeciążone. W wyniku próby realizacji takiej procedury kompilator wybierze tę procedurę, której wywołanie jest zgodne z liczbą i typami parametrów użytych w procedurze.
Przykład:
procedure Dekoder (sg_wej : in bit_vector; sg_wyj : out bit_vector) is
begin
case sg_wej is
when "001" => sg_wyj <= "100";
when "010" => sg_wyj <= "010";
when "011" => sg_wyj <= "001";
when others => sg_wyj <= "000";
end case;
end procedure;
b. funkcja
Jest to podprogram stosowany głównie do obliczeń. Wynika to budowy funkcji, która może mieć dowolną liczbę parametrów wejściowych i jedną wartość wyjściową.
deklaracja_funkcji ::= [pure | impure] functionnazwa_funkcji
[(lista_parametrów_formalnych)]
return nazwa_typu ;
Nazwą funkcji może być zarówno identyfikator jak i symbol operatora.
Listę parametrów formalnych określamy tak jak w procedurach. Należy jednak zauważyć, że dostępny jest tu tylko kierunek określony słowem "in".
Charakterystycznym jest także opcjonalne występowanie słów "pure" (funkcja która dla tych samych podanych parametrów wejściowych zawsze zwraca tą samą wartość) i "impure" (funkcja która dla tych samych parametrów wejściowych może zwracać różną wartość; dzieje się tak gdy wartość zwracana jest zależna od obiektów wyliczanych poza funkcją, mogących mieć różne wartości w różnym czasie).
ciało_funkcji ::= [pure | impure] function nazwa_funkcji
[(lista_parametrów_formalnych)] is
część_deklaracyjna_podprogramu
begin
instrukcje_sekwencyjne
end [function] [nazwa_funkcji] ;
Ponieważ większość elementów deklaracji funkcji występują w ciele funkcji można się nie powtarzać i użyć ich jeden raz w ciele funkcji (tak jak w procedurze).
Podobnie do procedur, funkcje także mogą być przeciążone. Kompilator w takich przypadkach zachowuje się analogicznie jak w przypadku procedur.
Ponieważ nazwę funkcji może stanowić także symbol operatora, w przypadku funkcji przeciążane mogą być operatory. Oznacza to że zdefiniowanie funkcji np. o nazwie "+", zmusi kompilator do wybrania odpowiedniej funkcji do zrealizowania, ponieważ funkcja o tej nawie jest predefiniowana i realizuje sumę dwóch argumentów. Wybór funkcji przez kompilator odbywa się na takiej samej zasadzie co przy przeciążaniu zwykłych funkcji.
Przykłady:
function "+"(a, b : in integer; return integer) is
begin
y := (a + b) / a;
return y ;
end function;