Podstawy języka VHDL


Wstęp

Na samym początku warto zaznaczyć czym w ogóle jest języka programowania VHDL. Jest to skrót od VHSIC (Very-High-Speed Integrated Circuits) Hardware Description Language, co tłumacząc na język polski oznacza Język Opisu Sprzętu VHSIC (układów scalonych o bardzo dużej prędkości). Wynika z tego, że pomimo składni przypominającej nam języki programowania „tradycyjnego” (np. Pascal, Ada i inne pascalo-podobne języki) nie programujemy wykonującego się krok po kroku programu, a jedynie wspomagamy program syntezy w ułożeniu opisanego przez nas układu cyfrowego.

Dzięki tej cennej informacji możemy wywnioskować ważną uwagę – opisany przez nas układ jest w stanie działać równolegle, skoro nie wykonuje się „linia po linii”, tylko tworzy odpowiednie połączenie funkcji logicznych wewnątrz układu FPGA, CPLD czy ASIC. To jest właśnie przewaga jaką posiadają rekonfigurowalne układy scalone nad wykorzystywanymi aktualnie mikrokontrolerami (mikroprocesorami). Te równolegle wykonujące się części nazwane są w języku VHDL procesami ale o nich za chwilę.

Budowa pliku w języku VHDL

Plik VHDL składa się z dwóch bloków: entity oraz architecture. Poglądowy przykład umieszczony jest poniżej

Blok entity

Tutaj opisujemy jak ma wyglądać nasza jednostka (ang. entity), tzn. w jaki sposób będzie się komunikowała ze światem – czy magistrale będą 32-bitowe a może tylko 24? Ile będzie wejść i wyjść? Czy może będzie przesyłać dane konkretnego typu np. tylko naturalne liczby?

Wszystko co podałem wyżej – dla zobrazowania – jest tak jakby opisem obudowy naszego układu – typizacja danych określa nam jak szeroką magistralą zostaną przesłane dane. I tak – składa się on jak na powyższym rysunku z dwóch części generic oraz port. Generic odpowiada za stałe jakie mają być użyte przy ustawianiu jednostki, natomiast port  opisuje jak będą wyglądać wejścia i wyjścia.

entity NAZWA_JEDNOSTKI is
    generic(
        NAZWA_STALEJ1 : typ_sygnalu := wartosc;
        NAZWA_STALEJ2 : typ_sygnalu := wartosc;
        NAZWA_STALEJ3 : typ_sygnalu := wartosc;
        NAZWA_STALEJ4 : typ_sygnalu := wartosc );
 
    port(
        NAZWA_SYGNALU1 : tryb typ_sygnalu;
        NAZWA_SYGNALU2 : tryb typ_sygnalu;
        NAZWA_SYGNALU3 : tryb typ_sygnalu;
        NAZWA_SYGNALU4 : tryb typ_sygnalu );
end NAZWA_JEDNOSTKI;

Tryby pracy portu

Posiadamy możliwość pracy w 4 trybach :

  • IN – tryb wejściowy, można czytać informację z tego wejścia
  • OUT – tryb wyjściowy, można wpisywać informację do tego wyjścia
  • INOUT – tryb wejścia/wyjścia, można wpisywać i odczytywać informację
  • BUFFER – tryb wyjściowy – można zapisywać informację do wyjścia, ale można czytać również jego stan (posiada rejestr wyjściowy)

Typy sygnałów i zmiennych

Typizacja w języku VHDL jest bardzo rozbudowana. Głównie jest to sprawa tego, iż możemy tworzyć własne typy, i to nie tylko skalarne ale również tablicowe, macierzowe, rekordowe (coś a’la struktura w C) . Podstawowymi typami języka VHDL są

  • BOOLEAN – typ logiczny przyjmujący wartości: FALSE/TRUE
  • BIT – typ przyjmujący wartości : 0 i 1
  • BIT_VECTOR – wektor danych typu BIT np. „01010111”. Można go użyć do modelowania magistral.
  • CHARACTER – typ danych alfanumerycznych np. ‚a’ , ‚0’ , ‚Z’
  • STRING – wektor danych alfanumerycznych np. „Ala ma kota”
  • INTEGER – typ stałopozycyjny reprezentujący liczby całkowite
  • REAL– typ zmiennopozycyjny reprezentujący liczby rzeczywiste
    • UWAGA!!! Typ REAL nie ma odwzorowania w układach rzeczywistych – jest załączony tylko jako możliwość modelowania!!! Radzę o nim od razu zapomnieć.

Warto od razu wspomnieć o bibliotece IEEE.STD_LOGIC_1164 definiującej typy:

  • STD_LOGIC – typ wielowartościowy „resolved” – najczęściej wykorzystywany w implementacji układów w FPGA
  • STD_ULOGIC – typ wielowartościowy „unresolved”

Te dwa powyżej wspomniane typy mogą przyjmować jedną z 9 dostępnych wartości w zależności od siły sygnału, znajomości, itp.

  • ‚U’ – wartość nigdy nie była znana
  • ‚X’ – wartość była znana ale aktualnie nie można podać jej wartości – sygnał „strong drive”
  • ‚0’ – wartość logicznego „0” – typu „strong drive”
  • ‚1’ – wartość logicznego „1” – typu „strong drive”
  • ‚Z’ – wartość wysokiej impedancji – oznacza że sygnał nie ma „drivera”
  • ‚W’ – wartość była znana ale aktualnie nie można podać jej wartości – sygnał „weak drive”
  • ‚L’ – wartość logicznego „0” – typu „weak drive”
  • ‚H’ – wartość logicznego „1” – typu „weak drive”
  • ‚-‚  – wartość sygnału nie interesuje nas (nie mająca znaczenia)

Sprawa sygnałów strong drive i weak drive ma dla nas znaczenie jeżeli byśmy uwzględniali tzw. „iloczyny na drucie” (sprawa elektroniczna, nie programistyczna)

W bibliotece STD_LOGIC_1164 znajdują się również typy wektorowe STD_LOGIC_VECTOR oraz STD_ULOGIC_VECTOR.

Przykładowy wygląd bloku entity

Przyjmijmy że chcemy wykonać w naszym układzie przerzutnik flip-flop typu D z asychronicznym zerowaniem i ustawianiem.

entity NAND is
    port(
        CLK : IN STD_LOGIC;
        D : IN STD_LOGIC;
        Q : OUT STD_LOGIC;
        S : IN STD_LOGIC;
        R : IN STD_LOGIC );
end NAND;

Jak widać nie potrzebujemy części generic bo nie mamy żadnych stałych które moglibyśmy zdefiniować.

BLOK ARCHITECTURE

To własnie tutaj opisujemy jak ma się zachować nasz układ. Pokazujemy mu jakie operacje ma wykonywać cały czas, na jakie ma reagować tylko w przypadku zmiany sygnału, albo synchronizujemy wykonywanie się operacji.

Na początek – operatory

Priorytet Typ Operatora
1 Różne ** abs not
2 Mnożenia * / mod rem
3 Znaku +
4 Dodawania + &
5 Przesunięcia sll srl sla sra rol ror
6 Relacji = /= < <= > >=
7 Logiczne and or nand nor xor xnor

Na tą chwilę przyjmuję, że rozumiemy poszczególne operatory. Jeżeli nie – zapraszam do działu gdzie je dokładnie opisałem.

Przejdźmy więc do kolejnej kwestii – jak opisać sygnały wewnątrz układu. Mamy do wyboru dwie możliwości: opisanie danych jako sygnału albo jako zmiennej. Te dwie możliwości mają drobne różnice, ale bardzo ważne z punktu projektowania.

Sygnał – utworzenie sygnału polega na podaniu słowa kluczowego signal przed nazwą sygnału np.

 signal NAZWA_SYGAŁU : typ_sygnału := wartość_inicjalizujaca;

Po utworzeniu sygnału przypisanie wartości jest wykonywane za pomocą operatora <=

NAZWA_SYGNAŁU

Jest to przypisanie ciągłe, czyli nie potrzebujące żadnych czynników zewnętrznych do wykonania.

Zmienna – utworzenie zmiennej polega na podaniu słowa kluczowego variable przed nazwą zmiennej i może być ona wykorzystane wyłącznie na liście procesu np.

 variable NAZWA_SYGAŁU : typ_zmiennej := wartość_inicjalizujaca;

Po utworzeniu sygnału przypisanie wartości jest wykonywane za pomocą operatora :=

NAZWA_SYGNAŁU := wartość (lub sygnał lub wyrażenie);

Jest to przypisanie blokujące, czyli zmienna jest w stanie zapamiętywać swoją wartość przez okres wykonywania się procesu.

Procesy

Właśnie, cały czas piszę o procesach, że tylko tam zmienne, itd., itd. Co to właściwie jest?! Już tłumaczę – jest to obszar wykonywania sekwencyjnego. W tym miejscu możemy opisywać zachowanie układu w postaci sekwencyjnej tzn. jeżeli jest jakiś warunek spełniony to ustaw taką, a taką wartość, albo np. zmień wartość bramki NAND tylko i wyłącznie w czasie zmiany sygnału na wejściu 1, a wejście 2 niech nas nie interesuje. Najlepiej będzie to do wytłumaczenia na przykładach, tak więc radzę czytać sporo kodów.

Przykładowy kod procesu

[NAZWA_PROCESSU :] process(LISTA_CZUŁOŚCI_PROCESSU) is -- nazwa jest opcjonalna
    --TUTAJ DEKLARUJEMY LOKALNE SYGNAŁY I ZMIENNE
    signal NAZWA_SYGNAŁU : typ_sygnału := wartość_inicjalizująca;
    variable NAZWA_ZMIENNEJ : typ_zmiennej := wartość_inicjalizująca;
begin
    -- TUTAJ OPISUJEMY ZACHOWANIE np.
    NAZWA_ZMIENNEJ := NAZWA_SYGAŁU; --Przypisanie sygału do zmiennej
    NAZWA_ZMIENNEJ := NAZWA_ZMIENNEJ + 1;
    NAZWA_SYGAŁU

I tutaj może narodzić się pytanie co zostanie przypisane do naszego sygnału? Przecież mamy dwa przypisania – to jak? najpierw wpisze wartość sygnału zwiększoną o 1 a po chwili znowu jeszcze o 1? NIE! Tutaj właśnie odkrywamy różnicę między zmienną, a sygnałem – do wartości zmiennej mamy dostęp od razu po przypisaniu, natomiast do wartość sygnału jest przypisywana do ostatniej możliwości przypisania np. poprzez warunki niektóre kolejne są zablokowane.

Dobrze – to na razie tyle odnośnie podstaw. Mam nadzieję, że w miarę czasu na stronie znajdzie się o wiele więcej informacji. Liczę na mobilizujące komentarze, zwłaszcza mówiące co jeszcze dodać, czy zmienić.