Skocz do zawartości

FPGA dla początkujących: Język VHDL


Pomocna odpowiedź

Napisano

Witam.
Mój post jest skierowany do wszystkich którzy są zainteresowani układami programowalnymi CPLD i FPGA.

Materiał powstał na podstawie własnych doświadczeń z układem Spartan3a i ma charakter praktycznych notatek.

Materiał jest dla początkujących. Nie opisuje wszystkich szczegółów języka VHDL ani architektury FPGA. Celem jest zrozumienie podstawowych pojęć potrzebnych do rozpoczęcia własnych projektów.

CPLD i FPGA są układami programowalnymi posiadającymi wewnętrzną strukturę logiczną, która umożliwia budowę zarówno prostych bramek logicznych, jak i złożonych układów cyfrowych.

Programowanie tego typu układów nie jest porównywalne z pisaniem kodów komputerowych, ponieważ zastosowanie programu konfiguracyjnego nie ma nic wspólnego z programem wykonywanym sekwencyjnie.

W opisie skupię się na układzie FPGA XILINX Spartan3a, który ma względnie zrozumiałą architekturę.

Układ ten posiada programowalne komórki SLICE złożone z dwóch komórek górna LUT4_G i dolna LUT4_F, multiplekser łączący komórki LUT4 o nazwie FMUX5. Komórki LUT odpowiadają głównie za logikę kombinatoryczną i w tym układzie posiadają (LUT_4G i F czyli 2 x 4 wejścia).

Komórka SLICE posiada również logikę szybkiego przeniesienia o nazwie CARRYCHAIN składającej się z MUXCY, XORCY, MULTAND. CARRY CHAIN służy do szybkiej propagacji przeniesienia w sumatorach i licznikach. i oszczędza zasoby LUT zostawiając miejsce na dodatkową logikę.

Komórka SLICE posiada również 2 przerzutniki typu D FlipFlop są nazwane REGISTER. Służą do tworzenia rejestrów dla danych i są 1 bitowe.

Komórka SLICE posiada również dodatkowe multipleksery umożliwiające łączenie funkcji logicznych budowanych z komórek LUT.

Komórki SLICE umieszczone są po 4 sztuki w bloku CLB. Bloki są wykorzystywane dla wewnętrznych połączeń między komórkami SLICE co redukuje dodatkowo czas propagacji sygnałów.

Dodatkowo układ FPGA posiada wbudowany BRAM który można konfigurować tylko o ściśle określonych wektorach danych i adresów by został przez syntezę rozpoznany. W innym przypadku niestandardowej długości wektorów synteza stworzy pamięć z komórek LUT.

Układ FPGA posiada również dedykowane układy mnożenia, DCM(Digital Clock Manager).

 

Schemat blokowy komórek SLICE:

                         image.thumb.png.33c1516f9f59f80fe29fda3cd9f9e641.png

Schemat blokowy komórek CLB:

                                   image.thumb.png.9622409ee93a5813b95f3e097a144043.png

Logika przykładowego układu spartan3a jest przedstawiona w celach poglądowych i w projektowaniu użytkownik pisze kod opisujący funkcję a wewnętrzne połączenia wykonuje syntezer wbudowany w narzędzie XILINX. Mimo to warto wiedzieć co ten układ potrafi by nie napisać kodu który wymusi zbyt długie połączenia lub sygnały które mogą się na siebie nakładać powodując błędne wyniki. Syntezer ma też algorytmy które nas poinformują o błędnym kodzie lub da ostrzeżenie o sygnałach które są niestabilne.
 

 

 

Język konfiguracji który omówię nosi nazwę VHDL ( ang. Very High Speed Integrated Circuit Hardware Description Language).

 

Program syntezy używa bibliotek które umożliwiają interpretację kodu i sygnałów.
Osobiście używam przeważnie takich bibliotek i na początek uważam je za wystarczające.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
Biblioteki deklaruje się na samym początku przed kodem.

Ta biblioteka jest wymagana praktycznie zawsze:
use IEEE.STD_LOGIC_1164.ALL;

Natomiast biblioteki poniżej są popularne w starszych projektach Xilinxa i Spartana-3A:
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

Ale w nowych projektach stosuje się częściej:
use IEEE.NUMERIC_STD.ALL;
 

Następnie deklarujemy porty we, wy lub we/wy

entity PIERWSZY is
Port (
A : in STD_LOGIC;
B : in STD_LOGIC;
Q : out STD_LOGIC;
D : inout STD_LOGIC;
S : out STD_LOGIC_VECTOR (7 downto 0)
);
end PIERWSZY;

entity ← deklaracja nazwy modułu
PIERWSZY ← nazwa modułu
Port ( ← po tym znaku wpisujemy wszystkie nazwy sygnałów które będą używane w celu połączenia z zewnętrznym środowiskiem

A ← nazwa sygnału.
: ← oddzielenie nazwy od deklaracji
in ← sygnał jest wejściowy
STD_LOGIC ← logika bitowa (standardowa jak w innych układach logicznych)


Przykład deklaracji wyjścia danych 1 bit:
B : out STD_LOGIC;
Dane ← nazwa sygnału.
: ← oddzielenie nazwy od deklaracji
out ← sygnał jest wyjściowy
STD_LOGIC ← logika bitowa (standardowa jak w innych układach logicznych)


Przykład deklaracji wejścia/wyjścia danych 1 bit:
D : inout STD_LOGIC;
D ← nazwa sygnału.
: ← oddzielenie nazwy od deklaracji
inout ← sygnał jest wejściowy i wyjściowy zależnie od konfiguracji w kodzie
STD_LOGIC ← logika bitowa (standardowa jak w innych układach logicznych)

Przykład deklaracji wyjścia danych 8 bit:
S : out STD_LOGIC_VECTOR (7 downto 0);
S <– nazwa sygnału.
: <– oddzielenie nazwy od deklaracji
out <– sygnał jest wyjściowy
STD_LOGIC_VECTOR <– logika bitowa wektorowa opisująca wektor bitowy (7 downto 0) czyli lewy bit nr. = 7 a prawy bit nr.= 0 (standardowa jak w innych układach logicznych)
zapis logiki wektorowej używamy gdy potrzebny jest zapis magistrali większej jak 1 bit by uniknąć wielu nazw dla każdego sygnału. W tym przypadku zapis (7 downto 0) można rozumieć liczby w zbiorze od 7 do 0 włącznie, i są dodawane jako kolejny numer do sygnału. W ten sposób synteza rozpoznaje zapis wielu sygnałów o tej samej nazwie odpowiednio je numerując na podstawie deklaracji zapisanej w wektorze.

 

Pod deklaracją sygnałów zewnętrznych możemy zadeklarować sygnały, które będą użyte tylko w tym module:

architecture Behavioral of PIERWSZY is

Następnie nadajemy nazwy sygnałom. W wewnętrznych sygnałach nie inicjujemy ich jako we lub wy ponieważ syntezer sam wyznaczy sposób ich implementacji w logice układu.

signal I : std_logic_vector (1 downto 0) ;
signal F : std_logic;

Po zapisaniu wszystkich sygnałów wpisujemy początek zapisu różnych funkcji.

Begin
 

W tym miejscu opisujemy konfigurację łącząc sygnały przez funkcje.

Możemy podłączyć funkcję bezpośrednio do sygnałów zewnętrznych.

Przykład funkcji „and”:

                                 image.png.e66944b1835c23908d145123684a12e2.png


Q <= A and B;

Q ← wyjście funkcji
<= ← znak przypisujący funkcję do sygnału
A ← wejście sygnału o nazwie A
B ← wejście sygnału o nazwie B
and ← funkcja logiczna

W ten sposób tworzy się dwu wejściową bramkę „and”.
Wszystkie funkcje przypisujemy za pomocą znaku <= do wybranych przez nas sygnałów.

Po zakończonym opisie deklarujemy koniec słowem „end”
np.:
end Behavioral;

---------------------------------------------------------------------------

Tak powinien wyglądać prawidłowo napisany moduł w języku VHDL z użyciem sygnałów zewnętrznych:


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

 

entity PIERWSZY is

Port (

A : in STD_LOGIC;

B : in STD_LOGIC;

Q : out STD_LOGIC

);

end PIERWSZY;

 

architecture Behavioral of PIERWSZY is

 

begin

 

Q <= A and B;

 

end Behavioral;

 

---------------------------------------------------------------------------


Tak powinien wyglądać prawidłowo napisany moduł w języku VHDL z użyciem sygnałów wewnętrznych:


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity PIERWSZY is

Port (

A : in STD_LOGIC;

B : in STD_LOGIC;

Q : out STD_LOGIC

);

end PIERWSZY;

 

architecture Behavioral of PIERWSZY is

 

signal AB : std_logic_vector (1 downto 0);

signal F : std_logic;

 

begin
 

AB(1) <= A;

AB(0) <= B;
 

F <= AB(1) and AB(0);

 

Q <= F;

 

end Behavioral;

---------------------------------------------------------------------------
 

 

W drugim przykładzie przypisane są sygnały wejściowe zewnętrzne do sygnałów wewnętrznych, a sygnał F z funkcji and przypisałem do wyjścia Q.

 

Na pytanie dlaczego sygnały zewnętrzne i wewnętrzne, odpowiedź jest prosta.

Możemy mieć dużo modułów i je łączyć w większe bloki.

Wewnątrz każdego modułu mogą być używane różne sygnały które wykonują różne funkcje a my chcemy tylko wynik bez plątaniny połączeń wewnątrz. Stąd nazwa moduł.

Moduły są tworzone dla poprawy czytelności i przede wszystkim są pomocą dla korekty kodu, bez przepisywania całego bloku.

 

Plik UCF.

Jest to plik który łączy sygnały zewnętrzne modułu do fizycznych pinów w FPGA

 

Zapis w tym pliku wygląda następująco:

 

NET "A" LOC = "p3" ;

NET "B" LOC = "p4" ;

NET "Q" LOC = "p5" ;

 

NET ← Ścieżka sygnału zewnętrznego zadeklarowanego w module

"A" ← Nazwa sygnału zewnętrznego zadeklarowanego w module

LOC ← Przypisanie do fizycznego pinu. Można czytać ulokuj w...
"p3" ← można czytać dosłownie pin nr 3

Wstęp do programowania w języku VHDL jest długi i nie ma sensu uczyć się wszystkiego na pamięć. Opis nie jest dla nauki tylko służy jako informacja.

W następnych wpisach postaram się wyjaśnić jak opisywać różne struktury jak liczniki, sumatory, dekodery i inne elementy logiczne.

  • Lubię! 1

 

Witam.

W tym wpisie zajmiemy się opisem funkcji logicznych.

Funkcje logiczne można traktować jako operacje przekształcające sygnały wejściowe w sygnał wyjściowy zgodnie z określonymi regułami. Jednym z przykładów jest dekoder sygnałów.

 

Przykładowy dekoder:

deklaracja zewnętrznych sygnałów wejściowych wektorem 4 bitowym.

SELECT : in STD_LOGIC_VECTOR (3 downto 0);

deklaracja zewnętrznego sygnału wyjściowego

Q : out STD_LOGIC;

przykładowa funkcja ma za zadanie ustawić sygnał Q = 1 dla liczby binarnej = 0100 podany na wejściu SELECT.

W takim wypadku znajomość logiki bramek logicznych nie ma znaczenia ponieważ w

nowoczesnych FPGA funkcje logiczne są realizowane głównie przez komórki LUT. Podczas syntezy opis funkcji zapisany w VHDL jest przekształcany do zawartości LUT oraz dedykowanych zasobów układu. Na poziomie opisu projektant zwykle nie operuje pojedynczymi bramkami logicznymi.

Warto się zatrzymać na chwilę by zrozumieć czym jest ta tajemnicza komórka o 4 wejściach i 1 wyjściu. LUT można traktować jako małą pamięć ROM o 16 komórkach po 1 bicie każda.

W przypadku realizacji funkcji logicznych komórka LUT pełni rolę ROM i można ją tylko odczytywać a inicjuje się ją przez wczytanie pliku binarnego do FPGA.

Sygnał 4 bitowy SELECT jest dołączany do wejścia adresu komórki LUT a wyjście komórki LUT jest podłączone do sygnału Q. Tymi połączeniami zajmuje się synteza.

Opis dekodera:
Q <= '1' when SELECT = ”0100” else '0';
Q             ← sygnał wyjściowy.
<=           ← przypisanie funkcji do wyjścia Q
'1'            ← logiczne 1
when      ← jeżeli
SELECT ← sygnał wejściowy
=             ← jest równy
”0100”    ← liczba binarna 0100
else        ← w innym przypadku
'0'           ← logiczne 0

Prostymi słowami można czytać ten zapis w następujący sposób:
ustaw na wyjściu logiczne 1 jeżeli na wejściu liczba binarna jest równa 0100, w innych przypadkach ustaw na wyjściu logiczne 0

 

Inny możliwy zapis wyżej wymienionej funkcji:

Q <= not SELECT(3) and SELECT(2) and not SELECT(1) and not SELECT(0);

jest to zapis funkcją and o 4 wejściach, 1 wyjściu i 3 negatorami sygnałów.

Funkcja and ustawia logiczne 1 jeżeli na wszystkich wejściach jest logiczne 1 (więc wejścia = 0 musimy zanegować), w innych przypadkach funkcja and zwróci logiczne 0.

image.png.d1794a8c07e79a18e0ef00f07416aae6.png

Jeszcze inny możliwy zapis wyżej wymienionej funkcji:

Q <= not(SELECT(3) or SELECT(1) or SELECT(0)) and SELECT(2);

w tym zapisie używamy funkcji nor o 3 wejściach i funkcję and o 2 wejściach.

image.thumb.png.6805be9da438b45e0834320581aabc72.png

 

Jeszcze inny przykład implementacji wyżej wymienionej funkcji:

Jest to bezpośredni zapis funkcji w komórce LUT przez jej inicjację kodem VHDL.

Komórkę LUT możemy ręcznie mapować, czyli wskazujemy syntezerowi jak ma wyglądać zawartość komórki LUT.

 

Mapowanie komórki LUT by wykonała funkcję: Q <= '1' when SELECT = ”0100” else '0';

Trzeba zwrócić szczególną uwagę, że taki zapis podaje wartość przy której powinno wystąpić na wyjściu Q logiczne 1

 

      in            | out

SELECT       | Q

b3 b2 b1 b0 |

0   0   0   0   | 0                         ← adres 0

0   0   0   1   | 0                          ← adres 1

0   0   1   0   | 0                          ← adres 2

0   0   1    1   | 0                          ← adres 3

0   1   0   0   | 1                          ← adres 4 (w tym miejscu jest wzór binarny 0100 na wejściu SELECT)

0   1    0   1   | 0                          ← adres 5

0   1    1   0   | 0                          ← adres 6

0   1    1   1   | 0                          ← adres 7

1   0   0   0   | 0                          ← adres 8

1   0   0   1   | 0                          ← adres 9

1   0   1   0   | 0                          ← adres A

1   0   1   1   | 0                          ← adres B

1   1   0   0  | 0                          ← adres C

1   1   0   1   | 0                          ← adres D

1   1   1   0   | 0                          ← adres E

1   1   1   1    | 0                          ← adres F
                     ↓
                      → 0000 0000 0001 0000 (przepisujemy ciąg wyników od dołu do góry czyli od F do 0)

X”0010” zapisujemy ciąg liczb binarnych w formie hexadecymalnej (szesnastkowej)

wpisujemy znalezioną liczbę hex jako wartość inicjacji w komórce LUT

Kolejność bitów INIT zależy od producenta FPGA i sposobu numeracji wejść LUT. Przed ręcznym wyliczaniem wartości INIT zawsze należy sprawdzić dokumentację używanego układu.

Każda kombinacja wejść SELECT wskazuje jeden z 16 adresów w pamięci LUT.

LUT4_inst : LUT4

generic map (

INIT => X"0010") ← tutaj wpisujemy wyliczoną wartość inicjacji

port map ( ← deklaracja mapowania portów

O => Q, -- LUT general output

I0 => SELECT(0), -- LUT input

I1 => SELECT(1), -- LUT input

I2 => SELECT(2), -- LUT input

I3 => SELECT(3) -- LUT input

);

 

Pokazałem na koniec jak działa komórka LUT z przypisaną funkcją w celu lepszego zrozumienia jakie procesy zachodzą w budowaniu połączeń logicznych i jak implementowane są złożone funkcje logiczne.

W praktycznych projektach należy preferować zapis funkcjonalny, a nie ręczne instancjonowanie LUT. Syntezer zwykle wygeneruje taki sam lub lepszy układ. Ręczne mapowanie LUT jest przydatne głównie podczas nauki architektury FPGA lub w szczególnych przypadkach optymalizacji.

W następnym wpisie opiszę jak buduje się proste i złożone rejestry

Ten wpis dotyczy rejestrów.

W układzie spartan3a w 1 komórce SLICE znajdują się:

Dwa rejestry typu D o nazwie FFX i FFY.

Rejestry są powiązane z komórkami LUT i mogą być w różnej konfiguracji.

image.thumb.png.8b74a49f59c605dd897f08d002ba88f2.png

Przykład rejestru z synchronicznym SET , RST, i zezwoleniem zegara CE:

image.png.dc864a7d3e30a1d85bd35cfc8d0418f9.png

Tabela prawdy dla tej konfiguracji:

image.thumb.png.43812a1865d08e7f443929d4f385474e.png

Rejestry tworzymy za pomocą algorytmu zwanym „process” z użyciem zegara.

Rejestr 1 bit w procesie:

REG_INST : process (CLK)
    begin
        if rising_edge (CLK) then
   Q <= D;
       end if;
end process;

 

REG_INST : process (CLK) ← w nawiasie podajemy tak zwaną listę czułości. Dla rejestru umieszcza się zwykle sygnał zegarowy oraz sygnały asynchroniczne (np. reset)

begin ← rozpoczynamy algorytm konfiguracji przerzutnika

if rising_edge (CLK) then ← jeżeli zostanie wykryte narastające zbocze zegara

Q <= D; ← przypisz wartość podaną na wejściu D do wyjścia Q

end if; ← koniec wpływu zegara (na wszystkie algorytmy po tym zapisie zegar nie będzie mieć wpływu)

end process; ← kończymy zapis procesu (w tym miejscu kończą się wszystkie algorytmy związane z tym procesem)

 

Rejestr 1 bit w procesie z asynchronicznym reset:

 

REG_INST : process (CLK, RST) ← dopisujemy kolejny sygnał do listy mający wpływ na działanie przerzutnika
begin
    if RST = '1' then ← w tym przypadku reset ma priorytet nad sygnałem CLK
         Q <= '0';
    elsif rising_edge (CLK) then ← jeżeli reset nie jest aktywny wykonaj sekwencję CLK
        Q <= D;
    end if;
end process;

 

Rejestr 8 bit w procesie:

jak wcześniej opisałem korzystamy z wektora bitów danych

 

deklarujemy wektor danych D i Q (używam danych wewnątrz więc nie dopisuję in i out)

 

signal D : std_logic_vector (7 downto 0);

signal Q : std_logic_vector (7 downto 0);

 

 

REG_INST : process (CLK)
begin
     if rising_edge (CLK) then
         Q <= D; deklaracja wektora jest rozpoznana przez syntezer i mamy automatycznie utworzony rejestr 8 bit.
     end if;
end process;

 

rejestr 8 bit z asynchronicznym RST:

REG_INST : process (CLK, RST)
begin
     if RST = '1' then
          Q <= (others => '0'); mamy do czynienia z wektorem, dlatego wpisujemy słowo wszystkie bity = 0
     elsif rising_edge (CLK) then
         Q <= D;
    end if;
end process;

 

Q <= (others => '0'); ← others => '0' zastępuje w tym przypadku zapis ”00000000”.

Q <= (7 downto 0 => '0'); ← taki zapis pokazuje dokładnie które bity mają być ustawione = 0;

Q <= ”01101100”; ← ten zapis ustawia wartość stałą

By ułatwić zapis wartości stałej możemy użyć deklaracji constant:

constant STAŁA : std_logic_vector(7 downto 0) := ”01101100”; ← znak := inicjuje wartość zapisaną po nim.

Przypisanie za pomocą stałej:

Q <= STAŁA; ← do wyjścia Q przypisujemy wartość STAŁA zadeklarowaną jako ciąg 8 bitów = ”01101100”

 

rejestr przypisujący wartość STAŁA po wykryciu sygnału RST

 

REG_INST : process (CLK, RST)
begin
    if RST = '1' then
           Q <= STAŁA;
   elsif rising_edge (CLK) then
          Q <= D;
   end if;
end process;

 

W celu zobrazowania poprawnie skonstruowanego zapisu z użyciem rejestru poniżej przykład pełnego kodu VHDL:


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity PIERWSZY is

   Port (
               CLK : in STD_LOGIC;
               RST : in STD_LOGIC;
               D : in STD_LOGIC_VECTOR (7 downto 0);
               Q : out STD_LOGIC_VECTOR (7 downto 0)
            );

end PIERWSZY;

architecture Behavioral of PIERWSZY is

constant STAŁA : std_logic_vector (7 downto 0) := ”01101100”;

begin

   REG_INST : process (CLK, RST)
   begin
       if RST = '1' then
           Q <= STAŁA;
       elsif rising_edge (CLK) then
           Q <= D;
       end if;
   end process;

end Behavioral;

 

Rejestry możemy tworzyć o dowolnej długości mieszczącej się w ramach fizycznych możliwości połączeń w układzie FPGA

Nie opisuję innych możliwości zastosowania i konfiguracji rejestrów ponieważ są powiązane z licznikami i innymi strukturami. Jedyne co warto zapamiętać, że są podstawową jednostką w komórkach FPGA na której opiera się większość konfiguracji synchronicznych struktur. Rejestry i pamięci LUT tworzą praktycznie całą strukturę połączeń między sobą za pomocą MUX. (Nie jest to do końca prawdą, ale dla nas takie wyobrażenie połączeń w naszych zastosowaniach na początek jest w zupełności wystarczające)

Następnym naturalnym krokiem w rozumieniu jak działa  FPGA będzie poznanie budowy liczników.

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...