Skocz do zawartości

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


Pomocna odpowiedź

 

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.

  • Lubię! 1

Liczniki (sumatory)

Licznik binarny rosnący:

CNT <= CNT + 1;

Licznik binarny malejący:

CNT <= CNT – 1;

Pełny kod VHDL dla licznika binarnego:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

 

entity LICZNIK is
Port (
           CLK : in STD_LOGIC;
           RST : in STD_LOGIC;
           Q : out STD_LOGIC_VECTOR (7 downto 0)
         );

end LICZNIK

architecture Behavioral of LICZNIK is

signal CNT : std_logic_vector (7 downto 0) := ”00000000”;

begin

   REG_INST : process (CLK, RST)
   begin
        if RST = '1' then
            CNT <= (others => '0');
        elsif rising_edge (CLK) then
            CNT <= CNT + 1;
       end if;

end process;

     Q <= CNT;

end Behavioral;

 

Czy to wszystko? W zasadzie tak, ale to nie daje nam pełnego obrazu co dzieje się za kurtyną algorytmu.

 

Prawda jest taka że liczniki w FPGA są tworzone z sumatora, układu przeniesienia i rejestrów.

Rejestry opisałem już wcześniej więc je pomijamy zakładając że ich funkcjonowanie jest zrozumiałe.

Chcę zauważyć tylko jedną różnicę w zapisie w przypisaniu do wyjścia CNT. Jest użyty znak sumy.

Czyli wynik przypisany do rejestru jest sumą rejestru i liczby 1. Czyli rejestr jest inkrementowany.

W sumatorze używany jest układ szybkiego przeniesienia CARRY:

image.thumb.png.fda8a484e92d957c43328559dec1c6ce.png

Logika CARRY jest połączona z rejestrami D :

image.thumb.png.8732672ef2c60fe560fc13bf58846f4c.png

układ przeniesienia CARRY przekazuje sygnał przeniesienia (Carry out) do następnego bitu licznika. Dzięki temu jest możliwe szybkie wykonanie operacji dodawania dla wielobitowych liczb binarnych.

W FPGA Spartan3a realizuje to dedykowana logika CARRY CHAIN.

Układ przeniesień generuje kaskadę połączeń między kolejnymi bitami sumatora i rejestru.

Dzięki temu licznik może być łatwo rozszerzany do długości zadeklarowanej przez wektor danych.

 

Kaskada połączeń w komórce SLICE:

image.thumb.png.f27d980a662c904ca9eb70cf13acabe4.png

Kaskada połączeń w komórce CLB:

image.thumb.png.fc4732df8de10d0391dc0aea23daf849.png

Podsumowując układ CARRY CHAIN bierze bezpośrednio udział w budowie liczników.

Konstrukcja zastosowana w układzie FPGA pozwala na konstruowanie innych typów liczników co jest zaletą. Możemy zastosować liczniki które zwiększają swoją zawartość nie tylko o liczbę 1 ale też o liczbę 2, 3, itd.

przykład licznika zwiększającego się o liczbę 3:

CNT <= CNT + 3;

jeżeli zapiszemy to w taki sposób:

SUM <= A + B;

otrzymamy sumator a jego budowa niczym nie różni się od licznika ponieważ te dwie struktury się niczym nie różnią oprócz danych wejściowych.

 

Następny wpis dotyczyć będzie rejestrów przesuwających.

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