Szukaj…


Wprowadzenie

W tym temacie proponujemy prostą metodę poprawnego projektowania prostych obwodów cyfrowych za pomocą VHDL. Metoda oparta jest na graficznych schematach blokowych i łatwej do zapamiętania zasadzie:

Najpierw pomyśl o sprzęcie, następnie napisz kod VHDL

Jest przeznaczony dla początkujących w cyfrowym projektowaniu sprzętu za pomocą VHDL, z ograniczonym zrozumieniem semantyki syntezy języka.

Uwagi

Cyfrowe projektowanie sprzętu za pomocą VHDL jest proste, nawet dla początkujących, ale jest kilka ważnych rzeczy, o których należy wiedzieć, i kilka zasad, których należy przestrzegać. Narzędziem służącym do transformacji opisu VHDL w sprzęt cyfrowy jest syntezator logiki. Semantyka języka VHDL wykorzystywanego przez syntezatory logiczne różni się raczej od semantyki symulacji opisanej w podręczniku Language Reference Manual (LRM). Co gorsza: nie jest znormalizowany i różni się w zależności od narzędzi do syntezy.

Proponowana metoda wprowadza kilka ważnych ograniczeń dla uproszczenia:

  • Brak zatrzasków wyzwalanych poziomem.
  • Obwody są synchroniczne na zboczu narastającym pojedynczego zegara.
  • Brak asynchronicznego resetowania lub ustawiania.
  • Brak wielu napędów na rozdzielonych sygnałach.

Przykład schematu blokowego , pierwszy z serii 3, krótko przedstawia podstawy sprzętu cyfrowego i proponuje krótką listę zasad projektowania schematu blokowego obwodu cyfrowego. Reguły pomagają zagwarantować proste tłumaczenie na kod VHDL, który symuluje i syntetyzuje zgodnie z oczekiwaniami.

Przykład kodowania wyjaśnia tłumaczenie ze schematu blokowego na kod VHDL i ilustruje go na prostym obwodzie cyfrowym.

Wreszcie przykład konkursu projektowego Johna Cooleya pokazuje, jak zastosować proponowaną metodę w bardziej złożonym przykładzie obwodu cyfrowego. Rozwija także wprowadzone ograniczenia i rozluźnia niektóre z nich.

Schemat blokowy

Sprzęt cyfrowy składa się z dwóch rodzajów prymitywów sprzętowych:

  • Bramki kombinatoryczne (falowniki i xor, 1-bitowe sumatory pełne, 1-bitowe multipleksery ...) Te bramki logiczne wykonują proste obliczenia logiczne na swoich wejściach i generują dane wyjściowe. Za każdym razem, gdy zmienia się jedno z wejść, zaczynają propagować sygnały elektryczne, a po krótkim czasie wyjście stabilizuje się do wartości wynikowej. Opóźnienie propagacji jest ważne, ponieważ jest silnie związane z prędkością, z jaką może działać obwód cyfrowy, to znaczy z jego maksymalną częstotliwością zegara.
  • Elementy pamięci (zatrzaski, przerzutniki D, pamięci RAM ...). W przeciwieństwie do kombinatorycznych bramek logicznych elementy pamięci nie reagują natychmiast na zmianę któregoś z ich wejść. Mają wejścia danych, wejścia sterujące i wyjścia danych. Reagują na określoną kombinację wejść sterujących, a nie na zmianę danych wejściowych. Na przykład wyzwalany D-flip-flop (DFF), na przykład, ma wejście zegara i dane. Na każdym rosnącym zboczu zegara próbkowane są dane wejściowe i kopiowane na wyjście, które pozostaje stabilne do następnego zbocza narastającego zegara, nawet jeśli dane wejściowe zmieniają się pomiędzy nimi.

Cyfrowy obwód sprzętowy jest kombinacją logiki kombinatorycznej i elementów pamięci. Elementy pamięci mają kilka ról. Jednym z nich jest umożliwienie ponownego wykorzystania tej samej logiki kombinatorycznej do kilku kolejnych operacji na różnych danych. Obwody wykorzystujące to są często nazywane obwodami sekwencyjnymi . Poniższy rysunek pokazuje przykład obwodu sekwencyjnego, który gromadzi wartości całkowite za pomocą tego samego sumatora kombinatorycznego, dzięki rejestrowi wyzwalanemu przez zbocze narastające. To także nasz pierwszy przykład schematu blokowego.

Obwód sekwencyjny

Podszewka rur jest kolejnym powszechnym zastosowaniem elementów pamięci i podstawą wielu architektur mikroprocesorowych. Ma on na celu zwiększenie częstotliwości taktowania obwodu poprzez rozdzielenie złożonego przetwarzania w szeregu prostszych operacji oraz równoległe wykonanie kilku kolejnych przetwarzania:

Wykładziny rurowe o złożonym procesie kombinatorycznym

Schemat blokowy jest graficzną reprezentacją obwodu cyfrowego. Pomaga podejmować właściwe decyzje i dobrze rozumieć ogólną strukturę przed kodowaniem. Jest to odpowiednik zalecanych wstępnych faz analizy w wielu metodach projektowania oprogramowania. Doświadczeni projektanci często pomijają ten etap projektowania, przynajmniej w przypadku prostych obwodów. Jeśli jednak jesteś początkującym w cyfrowym projektowaniu sprzętu i chcesz zakodować obwód cyfrowy w VHDL, przyjęcie poniższych 10 prostych zasad w celu narysowania schematu blokowego powinno pomóc w prawidłowym wykonaniu:

  1. Otocz swój rysunek dużym prostokątem. To jest granica twojego obwodu. Wszystko, co przekracza tę granicę, to port wejściowy lub wyjściowy. Jednostka VHDL opisze tę granicę.
  2. Wyraźnie oddziel rejestry wyzwalane zboczem (np. Bloki kwadratowe) od logiki kombinatorycznej (np. Bloki okrągłe). W VHDL zostaną one przetłumaczone na procesy, ale dwóch bardzo różnych rodzajów: synchronicznego i kombinatorycznego.
  3. Nie używaj zatrzasków wyzwalanych poziomem, używaj tylko rejestrów wyzwalanych zboczem. Ograniczenie to nie pochodzi od VHDL, który doskonale nadaje się do modelowania zatrzasków. To tylko rozsądna rada dla początkujących. Zatrzaski są rzadziej potrzebne, a ich stosowanie stwarza wiele problemów, których prawdopodobnie powinniśmy unikać, przynajmniej w przypadku naszych pierwszych projektów.
  4. Użyj tego samego pojedynczego zegara dla wszystkich rejestrów wyzwalanych rosnącym poziomem. Tutaj znowu to ograniczenie jest dla uproszczenia. Nie pochodzi od VHDL, który doskonale nadaje się do modelowania systemów z wieloma zegarami. Nazwij clock . Pochodzi z zewnątrz i stanowi wkład wszystkich kwadratowych bloków i tylko je. Jeśli chcesz, nawet nie przedstawiaj zegara, jest taki sam dla wszystkich kwadratowych bloków i możesz pozostawić go domyślnie na swoim schemacie.
  5. Reprezentuj komunikację między blokami za pomocą nazwanych i zorientowanych strzałek. Dla bloku, z którego pochodzi strzała, strzałka jest wyjściem. Dla bloku, do którego prowadzi strzała, strzałka jest wejściem. Wszystkie te strzałki staną się portami jednostki VHDL, jeśli przecinają duży prostokąt lub sygnały architektury VHDL.
  6. Strzały mają jedno pochodzenie, ale mogą mieć kilka miejsc docelowych. Rzeczywiście, gdyby strzała miała kilka źródeł, stworzylibyśmy sygnał VHDL z kilkoma sterownikami. Nie jest to całkowicie niemożliwe, ale wymaga szczególnej uwagi, aby uniknąć zwarć. W ten sposób na razie będziemy tego unikać. Jeśli strzała ma kilka miejsc docelowych, rozwidlaj ją tyle razy, ile potrzeba. Użyj kropek, aby odróżnić połączone i niepołączone skrzyżowania.
  7. Niektóre strzały pochodzą spoza dużego prostokąta. Są to porty wejściowe encji. Strzałka wejściowa nie może być również wynikiem żadnego z twoich bloków. Jest to wymuszone przez język VHDL: porty wejściowe encji mogą być odczytywane, ale nie zapisywane. Ma to na celu uniknięcie zwarć.
  8. Niektóre strzały wychodzą na zewnątrz. To są porty wyjściowe. W wersjach VHDL wcześniejszych niż 2008 r. Porty wyjściowe encji można zapisać, ale nie można ich odczytać. Strzała wyjściowa musi zatem mieć jedno pojedyncze źródło i jedno miejsce docelowe: zewnętrzną. Brak widelców na strzałach wyjściowych, strzałka wyjściowa nie może być również wejściem jednego z twoich bloków. Jeśli chcesz użyć strzałki wyjściowej jako danych wejściowych dla niektórych swoich bloków, wstaw nowy okrągły blok, aby podzielić go na dwie części: wewnętrzną z dowolną liczbą widelców i strzałkę wyjściową, która pochodzi z nowego blok i wychodzi na zewnątrz. Nowy blok stanie się prostym ciągłym przypisaniem w VHDL. Rodzaj przejrzystej zmiany nazwy. Od wersji VHDL 2008 można również czytać porty ouptut.
  9. Wszystkie strzałki, które nie przychodzą ani nie wychodzą z / na zewnątrz, są sygnałami wewnętrznymi. Zadeklarujesz je wszystkie w architekturze VHDL.
  10. Każdy cykl na schemacie musi zawierać co najmniej jeden kwadratowy blok. Nie wynika to z VHDL. Wywodzi się z podstawowych zasad cyfrowego projektowania sprzętu. Absolutnie należy unikać pętli kombinatorycznych. Z wyjątkiem bardzo rzadkich przypadków nie dają one żadnych użytecznych rezultatów. A cykl schematu blokowego, który składałby się tylko z okrągłych bloków, byłby pętlą kombinatoryczną.

Nie zapomnij dokładnie sprawdzić ostatniej reguły, jest ona tak samo niezbędna jak pozostałe, ale może być nieco trudniejsza do zweryfikowania.

O ile absolutnie nie potrzebujesz na razie wyłączonych przez nas funkcji, takich jak zatrzaski, wiele zegarów lub sygnały z wieloma sterownikami, powinieneś z łatwością narysować schemat blokowy obwodu, który jest zgodny z 10 regułami. Jeśli nie, problem prawdopodobnie dotyczy obwodu, który chcesz, a nie VHDL lub syntezatora logiki. I to prawdopodobnie oznacza, że obwód chcesz nie jest cyfrowy sprzęt.

Zastosowanie 10 reguł do naszego przykładu obwodu sekwencyjnego doprowadziłoby do schematu blokowego, takiego jak:

Przerobiono schemat blokowy obwodu sekwencyjnego

  1. Duży prostokąt wokół diagramu przecinają 3 strzałki reprezentujące porty wejściowe i wyjściowe jednostki VHDL.
  2. Schemat blokowy ma dwa okrągłe (kombinatoryczne) bloki - sumator i blok zmiany nazwy wyjściowej - i jeden kwadratowy (synchroniczny) blok - rejestr.
  3. Używa tylko rejestrów wyzwalanych zboczem.
  4. Jest tylko jeden zegar o nazwie clock i używamy tylko jego zbocza narastającego.
  5. Schemat blokowy ma pięć strzałek, jedna z widelcem. Odpowiadają dwóm wewnętrznym sygnałom, dwóm portom wejściowym i jednemu portowi wyjściowemu.
  6. Wszystkie strzałki mają jeden początek i jedno miejsce docelowe, z wyjątkiem strzałki o nazwie Sum która ma dwa miejsca docelowe.
  7. Data_in i Clock to nasze dwa porty wejściowe. Nie są wyjściem z naszych własnych bloków.
  8. Strzałka Data_out jest naszym portem wyjściowym. Aby zachować zgodność z wersjami VHDL wcześniejszymi niż 2008, dodaliśmy dodatkowy blok zmiany nazwy (okrągły) między Sum a Data_out . Data_out ma więc dokładnie jedno źródło i jedno miejsce docelowe.
  9. Sum i Next_sum to nasze dwa wewnętrzne sygnały.
  10. Na wykresie jest dokładnie jeden cykl i zawiera on jeden kwadratowy blok.

Nasz schemat blokowy jest zgodny z 10 zasadami. Przykład kodowania szczegółowo opisuje, w jaki sposób tłumaczyć tego typu diagramy blokowe w VHDL.

Kodowanie

Ten przykład jest drugim z serii 3. Jeśli jeszcze tego nie zrobiłeś, przeczytaj najpierw przykład schematu blokowego .

W przypadku schematu blokowego zgodnego z 10 regułami (patrz przykład schematu blokowego ) kodowanie VHDL staje się proste:

  • duży otaczający prostokąt staje się bytem VHDL,
  • wewnętrzne strzałki stają się sygnałami VHDL i są deklarowane w architekturze,
  • każdy kwadrat staje się procesem synchronicznym w bryle architektury,
  • każdy okrągły blok staje się procesem kombinatorycznym w bryle architektury.

Zilustrujmy to na schemacie blokowym obwodu sekwencyjnego:

Obwód sekwencyjny

Model obwodu VHDL obejmuje dwie jednostki kompilacji:

  • Jednostka opisująca nazwę obwodu i jego interfejs (nazwy portów, kierunki i typy). Jest to bezpośrednie tłumaczenie dużego otaczającego prostokąta schematu blokowego. Zakładając, że dane są liczbami całkowitymi, a clock używa bit typu VHDL (tylko dwie wartości: '0' i '1' ), bytem naszego obwodu sekwencyjnego może być:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • Architektura opisująca elementy wewnętrzne obwodu (co robi). To tutaj deklarowane są wewnętrzne sygnały i tworzone są wszystkie procesy. Szkielet architektury naszego obwodu sekwencyjnego może być:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Mamy trzy procesy do dodania do bryły architektury, jeden synchroniczny (blok kwadratowy) i dwa kombinatoryczne (bloki okrągłe).

Proces synchroniczny wygląda następująco:

process(clock)
begin
  if rising_edge(clock) then
    o1 <= i1;
    ...
    ox <= ix;
  end if;
end process;

gdzie i1, i2,..., ix to wszystkie strzałki, które wchodzą w odpowiedni kwadratowy blok diagramu, a o1, ..., ox to wszystkie strzałki, które wysyłają odpowiedni kwadratowy blok diagramu. Absolutnie nic się nie zmieni, oprócz nazw sygnałów, oczywiście. Nic. Ani jednej postaci.

Synchroniczny proces naszego przykładu jest zatem:

  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

Które można nieformalnie przełożyć na: jeśli clock zmienia, i tylko wtedy, gdy zmiana jest zboczem narastającym (od '0' do '1' ), przypisz wartość sygnału Next_sum do sygnału Sum .

Proces kombinatoryczny wygląda następująco:

process(i1, i2,... , ix)
  variable v1: <type_of_v1>;
  ...
  variable vy: <type_of_vy>;
begin
  v1 := <default_value_for_v1>;
  ...
  vy := <default_value_for_vy>;
  o1 <= <default_value_for_o1>;
  ...
  oz <= <default_value_for_oz>;
  <statements>
end process;

gdzie i1, i2,..., in to wszystkie strzałki, które wchodzą w odpowiedni okrągły blok diagramu. wszystko i nic więcej. Nie zapomnimy żadnej strzałki i nie dodamy niczego więcej do listy.

v1, ..., vy to zmienne, które mogą być potrzebne do uproszczenia kodu procesu. Mają dokładnie taką samą rolę, jak w każdym innym imperatywnym języku programowania: zachowują wartości tymczasowe. Należy je bezwzględnie przypisać przed odczytaniem. Jeśli tego nie zagwarantujemy, proces nie będzie już kombinatoryczny, ponieważ modeluje rodzaj elementów pamięci, aby zachować wartość niektórych zmiennych z jednego procesu do drugiego. To jest powód instrukcji vi := <default_value_for_vi> na początku procesu. Zauważ, że <default_value_for_vi> musi być stała. Jeśli nie, jeśli są to wyrażenia, moglibyśmy przypadkowo użyć zmiennych w wyrażeniach i odczytać zmienną przed przypisaniem jej.

o1, ..., om to wszystkie strzałki, które wyświetlają odpowiedni okrągły blok diagramu. wszystko i nic więcej. Wszystkie muszą być bezwzględnie przypisane przynajmniej raz podczas wykonywania procesu. Ponieważ struktury sterujące VHDL ( if , case ...) mogą bardzo łatwo zapobiec przypisaniu sygnału wyjściowego, zdecydowanie zalecamy bezwarunkowe przypisanie każdej z nich stałej wartości <default_value_for_oi> wartość domyślna_default_for_oi <default_value_for_oi> na początku procesu. W ten sposób, nawet jeśli instrukcja if maskuje przypisanie sygnału, i tak otrzyma wartość.

Absolutnie nic nie zostanie zmienione na ten szkielet VHDL, z wyjątkiem nazw zmiennych, jeśli takie istnieją, nazw danych wejściowych, nazw danych wyjściowych, wartości <default_value_for_..> wartości <default_value_for_..> stałych i <statements> . Nie zapomnij jeden przyporządkowanie wartości domyślne, jeśli nie synteza będzie wnioskować niechcianych elementów pamięci (najprawdopodobniej zatrzaski), a wynik nie będzie co początkowo chcieliśmy.

W naszym przykładowym układzie sekwencyjnym kombinatoryczny proces sumowania jest następujący:

  process(Sum, Data_in)
  begin
    Next_sum <= 0;
    Next_sum <= Sum + Data_in;
  end process;

Które można nieformalnie przetłumaczyć na: jeśli Sum lub Data_in (lub oba) zmienią, przypisz wartość 0 do sygnału Next_sum a następnie ponownie przypisz wartość Sum + Data_in .

Ponieważ po pierwszym przypisaniu (ze stałą wartością domyślną 0 ) natychmiast następuje kolejne przypisanie, które je zastępuje, możemy uprościć:

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

Drugi proces kombinatoryczny odpowiada okrągłemu blokowi, który dodaliśmy do strzałki wyjściowej z więcej niż jednym miejscem docelowym w celu zapewnienia zgodności z wersjami VHDL sprzed 2008 roku. Jego kod to po prostu:

  process(Sum)
  begin
    Data_out <= 0;
    Data_out <= Sum;
  end process;

Z tego samego powodu, co w przypadku innego procesu kombinatorycznego, możemy go uprościć:

  process(Sum)
  begin
    Data_out <= Sum;
  end process;

Pełny kod dla obwodu sekwencyjnego to:

-- File sequential_circuit.vhd
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;

architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

  process(Sum)
  begin
    Data_out <= Sum;
  end process;
end architecture ten_rules;

Uwaga: moglibyśmy napisać trzy procesy w dowolnej kolejności, nie zmieniłoby to niczego do końcowego wyniku w symulacji lub syntezie. Wynika to z faktu, że trzy procesy są równoległymi instrukcjami, a VHDL traktuje je tak, jakby były naprawdę równoległe.

Konkurs projektowy Johna Cooleya

Ten przykład pochodzi bezpośrednio z konkursu projektowego Johna Cooleya na SNUG'95 (spotkanie grupy użytkowników Synopsys). Konkurs miał przeciwstawić projektantom VHDL i Verilog ten sam problem projektowy. John miał na myśli prawdopodobnie określenie, który język był najbardziej wydajny. Rezultaty były następujące: 8 z 9 projektantów Verilog ukończyło konkurs projektowy, ale żaden z 5 projektantów VHDL nie mógł. Mamy nadzieję, że korzystając z proponowanej metody wykonamy znacznie lepszą robotę.

Dane techniczne

Naszym celem jest zaprojektowanie w syntezowalnym VHDL (encji i architekturze) synchronicznego licznika góra-dół, dół-5, ładowalny moduł 512, z wyjściem przeniesienia, wyjściem pożyczki i wyjściem parzystości. Licznik jest 9-bitowym bez znaku, więc zawiera się w przedziale od 0 do 511. Specyfikacja interfejsu licznika jest podana w poniższej tabeli:

Nazwa Szerokość bitu Kierunek Opis
ZEGAR 1 Wejście Zegar główny; licznik jest zsynchronizowany z rosnącym zboczem ZEGARA
DI 9 Wejście Magistrala wprowadzania danych; licznik jest obciążony DI, gdy UP i DOWN są niskie
W GÓRĘ 1 Wejście Polecenie zliczania o 3 kolejne; gdy UP jest wysokie, a DOWN jest niskie, licznik zwiększa się o 3, owijając się wokół jego maksymalnej wartości (511)
NA DÓŁ 1 Wejście Komenda zliczania w dół o 5; gdy DOWN jest wysokie, a UP jest niskie, licznik zmniejsza się o 5, owijając się wokół jego wartości minimalnej (0)
WSPÓŁ 1 Wynik Przeprowadzić sygnał; wysokie tylko przy zliczaniu powyżej wartości maksymalnej (511) i tym samym zawijaniu
BO 1 Wynik Pożycz sygnał; wysokie tylko przy odliczaniu poniżej wartości minimalnej (0) i zawijaniu
ROBIĆ 9 Wynik Magistrala wyjściowa; aktualna wartość licznika; gdy UP i DOWN są wysokie, licznik zachowuje swoją wartość
PO 1 Wynik Sygnał braku parzystości; wysoki, gdy bieżąca wartość licznika zawiera parzystą liczbę 1

Podczas odliczania powyżej jego wartości maksymalnej lub odliczania poniżej jego wartości minimalnej licznik otacza:

Przeciwprądowa wartość GÓRA DÓŁ Licz następną wartość Dalej CO Dalej BO Następny PO
x 00 DI 0 0 parzystość (DI)
x 11 x 0 0 parzystość (x)
0 ≤ x ≤ 508 10 x + 3 0 0 parzystość (x + 3)
509 10 0 1 0 1
510 10 1 1 0 0
511 10 2) 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 parzystość (x − 5)
4 01 511 0 1 0
3) 01 510 0 1 1
2) 01 509 0 1 1
1 01 508 0 1 0
0 01 507 0 1 1

Schemat blokowy

Na podstawie tych specyfikacji możemy rozpocząć projektowanie schematu blokowego. Najpierw przedstawmy interfejs:

Interfejs zewnętrzny

Nasz obwód ma 4 wejścia (w tym zegar) i 4 wyjścia. Kolejny krok polega na podjęciu decyzji, z ilu rejestrów i bloków kombinatorycznych będziemy korzystać i jakie będą ich role. W tym prostym przykładzie poświęcimy jeden blok kombinatoryczny do obliczenia następnej wartości licznika, wykonania i pożyczenia. Kolejny blok kombinatoryczny zostanie wykorzystany do obliczenia następnej wartości z parzystości. Bieżące wartości licznika, realizacji i pożyczki zostaną zapisane w rejestrze, a bieżąca wartość parytetu zostanie zapisana w osobnym rejestrze. Wynik pokazano na poniższym rysunku:

Dwa kombinatoryczne bloki i dwa rejestry

Sprawdzenie, czy schemat blokowy jest zgodny z naszymi 10 regułami projektowania, odbywa się szybko:

  1. Nasz zewnętrzny interfejs jest odpowiednio reprezentowany przez duży otaczający prostokąt.
  2. Nasze 2 bloki kombinatoryczne (okrągłe) i 2 rejestry (kwadratowe) są wyraźnie oddzielone.
  3. Używamy tylko rejestrów wyzwalanych zboczem narastającym.
  4. Używamy tylko jednego zegara.
  5. Mamy 4 wewnętrzne strzałki (sygnały), 4 strzałki wejściowe (porty wejściowe) i 4 strzałki wyjściowe (porty wyjściowe).
  6. Żadna z naszych strzał nie ma kilku źródeł. Trzy mają kilka miejsc docelowych ( clock , ncnt i do ).
  7. Żadna z naszych 4 strzałek wejściowych nie jest wyjściem naszych wewnętrznych bloków.
  8. Trzy nasze strzałki wyjściowe mają dokładnie jedno źródło i jedno miejsce docelowe. Ale do ma 2 cele: na zewnątrz i jeden z naszych bloków kombinatorycznych. Jest to niezgodne z zasadą numer 8 i musi zostać naprawione przez wstawienie nowego bloku kombinatorycznego, jeśli chcemy zachować zgodność z wersjami VHDL wcześniejszymi niż 2008:

Jeden dodatkowy blok kombinatoryczny

  1. Mamy teraz dokładnie 5 sygnałów wewnętrznych ( cnt , nco , nbo , ncnt i npo ).
  2. Na schemacie jest tylko jeden cykl utworzony przez cnt i ncnt . Cykl ma kwadratowy blok.

Kodowanie w wersjach VHDL wcześniejszych niż 2008

Tłumaczenie naszego schematu blokowego na VHDL jest proste. Aktualna wartość licznika wynosi od 0 do 511, więc użyjemy 9-bitowego sygnału bit_vector do jego przedstawienia. Jedyną subtelnością jest konieczność wykonywania operacji bitowych (takich jak obliczanie parzystości) i operacji arytmetycznych na tych samych danych. Standardowy pakiet numeric_bit biblioteki ieee rozwiązuje to: deklaruje typ unsigned z dokładnie taką samą deklaracją, jak bit_vector i przeciąża operatory arytmetyczne, tak że przyjmują dowolną mieszankę unsigned i liczb całkowitych. Aby obliczyć wykonanie i pożyczenie, użyjemy 10-bitowej wartości tymczasowej unsigned .

Deklaracje biblioteczne i jednostka:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

Szkielet architektury to:

architecture arc1 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
    <...processes...>
end architecture arc1;

Każdy z naszych 5 bloków jest modelowany jako proces. Synchroniczne procesy odpowiadające naszym dwóm rejestrom są bardzo łatwe do kodowania. Po prostu używamy wzoru zaproponowanego w przykładzie Kodowanie . Rejestr przechowujący na przykład flagę parzystości jest kodowany:

  poreg: process(clock)
  begin
    if rising_edge(clock) then
      po <= npo;
    end if;
  end process poreg;

oraz drugi rejestr przechowujący co , bo i cnt :

  cobocntreg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
    end if;
  end process cobocntreg;

Zmiana nazwy na proces kombinatoryczny jest również bardzo prosta:

  rename: process(cnt)
  begin
    do <= (others => '0');
    do <= bit_vector(cnt);
  end process rename;

W obliczeniach parzystości można użyć zmiennej i prostej pętli:

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

Ostatni proces kombinatoryczny jest najbardziej złożony ze wszystkich, ale ścisłe zastosowanie proponowanej metody tłumaczenia również ułatwia:

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;

Należy zauważyć, że dwa procesy synchroniczne można również połączyć, a jeden z naszych procesów kombinatorycznych można uprościć poprzez proste przypisanie równoległego sygnału. Pełny kod z deklaracjami bibliotek i pakietów oraz z proponowanymi uproszczeniami jest następujący:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc2 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
  reg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      po  <= npo;
      cnt <= ncnt;
    end if;
  end process reg;

  do <= bit_vector(cnt);

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;
end architecture arc2;

Idąc nieco dalej

Proponowana metoda jest prosta i bezpieczna, ale opiera się na kilku ograniczeniach, które można złagodzić.

Pomiń rysunek schematu blokowego

Doświadczeni projektanci mogą pominąć rysowanie schematu blokowego w przypadku prostych projektów. Ale nadal myślą przede wszystkim o sprzęcie. Rysują w głowie zamiast na kartce papieru, ale jakoś kontynuują rysowanie.

Użyj resetów asynchronicznych

Są okoliczności, w których asynchroniczne resety (lub zestawy) mogą poprawić jakość projektu. Proponowana metoda obsługuje tylko resetowanie synchroniczne (tzn. Resetowanie uwzględniane przy rosnących krawędziach zegara):

  process(clock)
  begin
    if rising_edge(clock) then
      if reset = '1' then
        o <= reset_value_for_o;
      else
        o <= i;
      end if;
    end if;
  end process;

Wersja z resetowaniem asynchronicznym modyfikuje nasz szablon, dodając sygnał resetowania do listy czułości i nadając mu najwyższy priorytet:

  process(clock, reset)
  begin
    if reset = '1' then
      o <= reset_value_for_o;
    elsif rising_edge(clock) then
      o <= i;
    end if;
  end process;

Scal kilka prostych procesów

Użyliśmy tego już w ostatecznej wersji naszego przykładu. Scalanie kilku procesów synchronicznych, jeśli wszystkie mają ten sam zegar, jest banalne. Scalanie kilku procesów kombinatorycznych w jednym jest również banalne i jest po prostu prostą reorganizacją schematu blokowego.

Możemy również łączyć niektóre procesy kombinatoryczne z procesami synchronicznymi. Ale w tym celu musimy wrócić do naszego schematu blokowego i dodać jedenastą zasadę:

  1. Zgrupuj kilka okrągłych bloków i co najmniej jeden kwadratowy blok, rysując wokół nich obudowę. Dołącz także strzałki, które mogą być. Nie pozwól, aby strzała przekroczyła granicę obudowy, jeśli nie wychodzi ani nie wychodzi z / na zewnątrz obudowy. Po wykonaniu tej czynności spójrz na wszystkie strzałki wyjściowe obudowy. Jeśli którykolwiek z nich pochodzi z okrągłego bloku obudowy lub jest również wejściem obudowy, nie możemy scalić tych procesów w procesie synchronicznym. W przeciwnym razie możemy.

W naszym kontrprzykładzie nie mogliśmy na przykład zgrupować dwóch procesów w czerwonej obudowie poniższego rysunku:

Procesy, których nie można scalić

ponieważ ncnt jest wyjściem z obudowy, a jego początkiem jest okrągły (kombinatoryczny) blok. Ale możemy zgrupować:

Procesy, które można łączyć

Sygnał wewnętrzny npo stałby się bezużyteczny, a wynikowy proces byłby następujący:

  poreg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process poreg;

który można również połączyć z innym procesem synchronicznym:

  reg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process reg;

Grupowanie może być nawet:

Więcej grupowania

Prowadząc do znacznie prostszej architektury:

architecture arc5 of cooley is
  signal cnt: unsigned(8 downto 0);
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
    variable tmp:  bit;
  begin
    if rising_edge(clock) then
      ncnt := '0' & cnt;
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po  <= not tmp;
      cnt <= ncnt(8 downto 0);
    end if;
  end process;

  do <= bit_vector(cnt);
end architecture arc5;

z dwoma procesami (równoczesne przypisanie sygnału do jest skrótem dla równoważnego procesu). Rozwiązanie zawierające tylko jeden proces pozostawia się jako ćwiczenie. Uwaga, rodzi ciekawe i subtelne pytania.

Idąc jeszcze dalej

Zatrzaski wyzwolone przez poziom, spadające krawędzie zegara, wiele zegarów (i resynchronizatory między domenami zegarowymi), wiele sterowników dla tego samego sygnału itp. Nie są złe. Czasami są przydatne. Jednak nauczenie się ich używania i unikania związanych z tym pułapek wykracza daleko poza to krótkie wprowadzenie do cyfrowego projektowania sprzętu w VHDL.

Kodowanie w VHDL 2008

VHDL 2008 wprowadził kilka modyfikacji, których możemy użyć w celu dalszego uproszczenia naszego kodu. W tym przykładzie możemy skorzystać z 2 modyfikacji:

  • porty wyjściowe można odczytać, nie potrzebujemy już sygnału cnt ,
  • do obliczenia parzystości można zastosować jednoargumentowy operator xor .

Kod VHDL 2008 może być:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc6 of cooley is
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
  begin
    if rising_edge(clock) then
      ncnt := unsigned('0' & do);
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      po <= not (xor ncnt(8 downto 0));
      do <= bit_vector(ncnt(8 downto 0));
    end if;
  end process;
end architecture arc6;


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow