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

  • Lubię! 2

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.

  • Lubię! 1

@kroszkanorber Jakie programy muszę zainstalować - w internecie znalazłem taką podpowiedź "

Xilinx ISE Project Navigator – główne środowisko do tworzenia projektu, pisania kodu w językach opisu sprzętu (VHDL, Verilog) oraz syntezy układu. [1]

iMPACT – program służący do wgrywania skompilowanego pliku konfiguracyjnego (.bit lub .mcs) bezpośrednio do układu FPGA lub pamięci Flash poprzez interfejs JTAG"

  • Lubię! 2

@Santiago Zainstaluj ISE WEBPACK. Po wybraniu tego instalator powinien zainstalować PlanAhead , Impact i sterowniki dla programatora. Zainstaluj po prostu cały dostępny pakiet. Program po instalacji zajmuje ok 20GB.

  • Lubię! 2

Rejestry przesuwne.

Rejestry przesuwne służą do przesuwania bitów w prawo lub w lewo w rejestrze.

 

Rejestr tupu SISO. (wejście i wyjście 1 bitowe i łańcuch rejestrów)

Spartan3 ma wbudowany rejestr SRL16 który tworzy bezpośrednie połączenia w łańcuchu co zmniejsza użycie zasobów wewnątrz układu. Nie będę pisać o konfiguracji SRL16 ponieważ jest to dość rozległy temat.

W przykładzie użyty został rejestr 16 bitowy SHIFT_REG z wejściem D i wyjściem Q.

Rejestr ma zastosowanie np.:

opóźnienie sygnału o kilkanaście taktów,

wyrównanie opóźnień w potoku danych (pipeline),

przesuwanie danych szeregowych,

małe bufory FIFO...

 

W przypadku tego rejestru dane na wejściu D są ładowane do SHIFT_REG(0) w pierwszym takcie. Następny takt przeładowuje SHIFT_REG(0) do SHIFT_REG(1) itd... aż dane pojawią się na wyjściu Q. Czyli mamy 16 taktów w których sygnał przechodzi przez łańcuch połączeń między kolejnymi stopniami w SHIFT_REG.

Zauważyć należy fakt że rejestr intuicyjnie przesuwa dane w lewo, ale to tylko złudzenie wynikające z zapisu. Taki rejestr może działać zarówno w prawo i w lewo, ponieważ o kierunku decydują dane na wejściu D i wyjściu Q.

 

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

 

entity srl16 is

        Port (
               CLK : in STD_LOGIC;
               D : in STD_LOGIC;
               Q : out STD_LOGIC
                );

end srl16

architecture Behavioral of srl16 is

       signal SHIFT_REG: std_logic_vector(15 downto 0);

begin

     process(CLK)
     begin
         if rising_edge (CLK) then
             SHIFT_REG <= SHIFT_REG(14 downto 0) & D;
         end if;
    end process;

          Q <= SHIFT_REG(15);

end Behavioral;

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

Rejestr PISO 8 bitowy przesuwny w lewo:

Ten rejestr nie jest wykrywany przez syntezer jako srl16 ponieważ ma wejście równoległe i potrzebne są dodatkowe rejestry typu D. W przykładzie jest zapis danych synchroniczny z zegarem.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity srl16 is

        Port (
                 CLK : in STD_LOGIC;
                 WE : in STD_LOGIC;
                 DATA : in STD_LOGIC_VECTOR (7 downto 0);
                 Q : out STD_LOGIC
               );

end srl16

architecture Behavioral of srl16 is

         signal SHIFT_REG: std_logic_vector(7 downto 0);

begin

     process(CLK)
     begin
           if rising_edge (CLK) then ← proces zegarowy
                if WE = '1' then ← sygnał zapisu aktywny
                      SHIFT_REG <= DATA; ← wprowadź dane do rejestru
               else ← sygnał zapisu nie aktywny
                      SHIFT_REG <= SHIFT_REG(6 downto 0) & '0'; ← przesuwaj w lewo
               end if;
          end if;
      end process;

        Q <= SHIFT_REG(7);

end Behavioral;

 

konstrukcja zapisu:

SHIFT_REG <= SHIFT_REG(6 downto 0) & '0';

SHIFT_REG                       ← rejestr 8 bit

SHIFT_REG(6 downto 0) ← rejestr 7 bit

& '0'                                   ← doklejony bit z prawej strony

Konstrukcja zapisu rejestru przesuwnego ma za zadanie zmienić zapis wektora bitów podmieniając bity miejscami

wektor bitowy:             b7 b6 b5 b4 b3 b2 b1 b0 = SHIFT_REG (7 downto 0)
                                             ↓  ↓  ↓   ↓  ↓  ↓  ↓
                                          ←  ←  ←   ←  ←  ←   ←
                                        ↓   ↓  ↓   ↓  ↓  ↓ ↓
przesuwamy w lewo:   b6 b5 b4 b3 b2 b1 b0 '0' = SHIFT_REG(6 downto 0) & '0'
                                        ↓  ↓  ↓   ↓   ↓  ↓ ↓  ↓
przypisane nowe bity: b7 b6 b5 b4 b3 b2 b1 b0

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

Rejestr SIPO 8 bitowy przesuwny w prawo:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity srl16 is

        Port (
                 CLK : in STD_LOGIC;
                 WE : in STD_LOGIC;
                 D : in STD_LOGIC;
                 DATA : out STD_LOGIC_VECTOR (7 downto 0)
                 );

end srl16

architecture Behavioral of srl16 is

       signal SHIFT_REG: std_logic_vector(7 downto 0);

begin

     process(CLK)
     begin
         if rising_edge (CLK) then ← proces zegarowy
              SHIFT_REG <= D & SHIFT_REG(7 downto 1); ← przesuwaj w prawo
         end if;
end process;

     DATA <= SHIFT_REG;

end Behavioral;

 

konstrukcja zapisu:

SHIFT_REG <= D & SHIFT_REG(7 downto 1);

SHIFT_REG                      ← rejestr 8 bit

D &                                    ← doklejony bit z lewej strony

SHIFT_REG(7 downto 1) ← rejestr 7 bit

Konstrukcja zapisu rejestru przesuwnego ma za zadanie zmienić zapis bitowy wektora podmieniając bity miejscami

 

wektor bitowy:              b7 b6 b5 b4 b3 b2 b1 b0 = SHIFT_REG (7 downto 0)
                                        ↓  ↓  ↓   ↓  ↓  ↓  ↓
                                         →   →   →   →  →  →  →
                                             ↓  ↓   ↓  ↓  ↓  ↓  ↓
przesuwamy w prawo: D b7 b6 b5 b4 b3 b2 b1 = D & SHIFT_REG(7 downto 1)
                                        ↓  ↓  ↓   ↓  ↓  ↓  ↓  ↓
przypisane nowe bity: b7 b6 b5 b4 b3 b2 b1 b0

 

Podałem trzy rejestry by pokazać prawidłowy zapis VHDL rejestru przesuwnego.

Rejestry można tworzyć o dowolnej długości i kierunku z ładowaniem danych synchronicznie i asynchronicznie. Rejestry mogą przesuwać dane o dowolnej długości bitów. np:

        signal SHIFT_REG: std_logic_vector(7 downto 0);

SHIFT_REG <= SHIFT_REG(3 downto 0) & DATA(3 downto 0); ← w tym miejscu przesuwamy rejestr o 4 bity a w puste miejsce dopisujemy 4 bity DATA.


Rejestry mogą też zamieniać dwie połowy jednego bajtu miejscami:


   signal SHIFT_REG: std_logic_vector(7 downto 0);

SHIFT_REG <= SHIFT_REG(3 downto 0) & SHIFT_REG(7 downto 4);

Widać w kolejnych przykładach, że zapis wektora bitów jest dowolny a jedynym ograniczeniem jest szerokość rejestru.

W następnym wpisie  multipleksery.

  • Lubię! 1

Multiplekser

W strukturze Spartan-3 dostępne są również dedykowane multipleksery FMUX, które umożliwiają łączenie wyjść kilku LUT w większe funkcje logiczne. Dzięki kaskadowemu połączeniu multiplekserów możliwe jest tworzenie logiki o większej liczbie wejść bez konieczności budowania jej wyłącznie z oddzielnych bramek logicznych. Syntezer wykorzystuje te zasoby automatycznie, gdy opis projektu tego wymaga.

Schemat blokowy przykładowego multipleksera 8:1 i 16:1

image.thumb.png.a0239332a1b628ef837f87e6c84806c0.png

Tabela pokazująca dedykowane multipleksery dostępne w spartan3:

image.thumb.png.a9b03039e7e6d43b2e8ea6ff6c6e3551.png

przykład kodu VHDL dla multipleksera 2:1:
Pomijam zapis deklaracji sygnałów.
Do wyboru stosuje się konstrukcję (when-else)

Q <= A when SELECT = '1' else B; ← jeżeli SELECT = 1 wybierz A w innym przypadku wybierz B (wybrane sygnały przypisz do wyjścia Q)

Innym ciekawym zastosowaniem multipleksera jest dekoder liczby HEX do wyświetlacza LED 7 segmentowego:

W kodzie zastosowana jest konstrukcja (with-select)

HEX: in STD_LOGIC_VECTOR (3 downto 0);

LED: out STD_LOGIC_VECTOR (6 downto 0);

      0
     ---
 5 |    | 1
     ---    <- 6
 4 |    | 2
     ---
      3

 with HEX select
           LED <= "1111001" when "0001", --1
                         "0100100" when "0010", --2
                         "0110000" when "0011", --3
                         "0011001" when "0100", --4
                         "0010010" when "0101", --5
                         "0000010" when "0110", --6
                         "1111000" when "0111", --7
                         "0000000" when "1000", --8
                         "0010000" when "1001", --9
                         "0001000" when "1010", --A
                         "0000011" when "1011", --b
                         "1000110" when "1100", --C
                         "0100001" when "1101", --d
                         "0000110" when "1110", --E
                         "0001110" when "1111", --F
                         "1000000" when others; --0

multiplekser 4:1:
W kodzie zastosowana jest konstrukcja (case-is)

library ieee;

use ieee.std_logic_1164.all;

entity multiplexers_1 is
           port (A, B, C, D : in std_logic;
                     S : in std_logic_vector (1 downto 0);
                     O : out std_logic);
end multiplexers_1;

architecture archi of multiplexers_1 is

begin

process (CLK)
begin
    case s is
         when "00" => O <= A;
         when "01" => O <= B;
         when "10" => O <= C;
         when "11" => O <= D;
         when others => O <= A;
    end case;
end process;

end archi;

 

Multipleksery są z natury asynchroniczne. Nic nie stoi na przeszkodzie by ich działanie zsynchronizować z zegarem. Dane wyjściowe zostaną aktualizowane w następnym zboczu zegara:

library ieee;

use ieee.std_logic_1164.all;

entity multiplexers_1 is
           port (CLK, A, B, C, D : in std_logic;
                     S : in std_logic_vector (1 downto 0);
                     O : out std_logic);
end multiplexers_1;

architecture archi of multiplexers_1 is

begin

process (CLK)
begin
    if rising_edge (CLK) then
    case s is
         when "00" => O <= A;
         when "01" => O <= B;
         when "10" => O <= C;
         when "11" => O <= D;
         when others => O <= A;
    end case;
    end if;
end process;

end archi;

Teorii na razie wystarczy. W kolejnych wpisach zaczniemy stopniowo wykorzystywać zdobytą wiedzę do budowy praktycznych układów. Wtedy okaże się, że rejestry, liczniki, LUT i multipleksery nie są odrębnymi tematami, lecz elementami większej całości.

  • Lubię! 1

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