vhdl Samouczek
Rozpoczęcie pracy z vhdl
Szukaj…
Uwagi
VHDL jest akronimem złożonym dla VHSIC (Very High Speed Integrated Circuit) HDL (Hardware Description Language). Jako język opisu sprzętu służy przede wszystkim do opisywania lub modelowania obwodów. VHDL jest idealnym językiem do opisu obwodów, ponieważ oferuje konstrukcje językowe, które łatwo opisują zarówno współbieżne, jak i sekwencyjne zachowanie, a także model wykonawczy, który usuwa niejednoznaczności wprowadzone podczas modelowania zachowań współbieżnych.
VHDL jest zazwyczaj interpretowany w dwóch różnych kontekstach: do symulacji i syntezy. Interpretowany do syntezy kod jest konwertowany (syntetyzowany) na równoważne modelowane elementy sprzętowe. Tylko podzbiór VHDL jest zazwyczaj dostępny do użycia podczas syntezy, a obsługiwane konstrukcje językowe nie są standaryzowane; jest to funkcja zastosowanego silnika syntezy i docelowego urządzenia sprzętowego. Kiedy VHDL jest interpretowany do symulacji, dostępne są wszystkie konstrukcje językowe do modelowania zachowania sprzętu.
Wersje
Wersja | Data wydania |
---|---|
IEEE 1076–1987 | 1988-03-31 |
IEEE 1076–1993 | 1994-06-06 |
IEEE 1076-2000 | 2000-01-30 |
IEEE 1076-2002 | 2002-05-17 |
IEEE 1076c-2007 | 2007-09-05 |
IEEE 1076-2008 | 26.01.2009 |
Instalacja lub konfiguracja
Program VHDL może być symulowany lub syntetyzowany. Symulacja jest najbardziej podobna do wykonywania w innych językach programowania. Synteza przekłada program VHDL na sieć bramek logicznych. Wiele narzędzi do symulacji i syntezy VHDL jest częścią komercyjnych pakietów Electronic Design Automation (EDA). Często obsługują także inne języki opisu sprzętu (HDL), takie jak Verilog, SystemVerilog lub SystemC. Istnieją pewne darmowe i otwarte aplikacje.
Symulacja VHDL
GHDL to prawdopodobnie najbardziej dojrzały darmowy i otwarty symulator VHDL. Występuje w trzech różnych smakach w zależności od zastosowanego backendu: gcc
, llvm
lub mcode
. Poniższe przykłady pokazują, jak używać GHDL (wersja mcode
) i Modelsim, komercyjnego symulatora HDL firmy Mentor Graphics, w systemie operacyjnym GNU / Linux. W przypadku innych narzędzi i innych systemów operacyjnych sytuacja wyglądałaby bardzo podobnie.
Witaj świecie
Utwórz plik hello_world.vhd
zawierający:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Jednostka kompilująca VHDL to kompletny program VHDL, który można skompilować samodzielnie. Jednostki są jednostkami kompilacji VHDL, które służą do opisu zewnętrznego interfejsu obwodu cyfrowego, to znaczy jego portów wejściowych i wyjściowych. W naszym przykładzie entity
nazywa się hello_world
i jest pusta. Obwód, który modelujemy, jest czarną skrzynką, nie ma wejść ani wyjść. Architektury są innym rodzajem jednostek kompilacji. Są one zawsze powiązane z entity
i służą do opisu zachowania obwodu cyfrowego. Jedna jednostka może mieć jedną lub więcej architektur opisujących jej zachowanie. W naszym przykładzie encja jest powiązana tylko z jedną architekturą o nazwie arc
która zawiera tylko jedną instrukcję VHDL:
assert false report "Hello world!" severity note;
Instrukcja zostanie wykonana na początku symulacji i wydrukuje Hello world!
wiadomość na standardowym wyjściu. Symulacja zakończy się, ponieważ nie będzie już nic więcej do zrobienia. Plik źródłowy VHDL, który napisaliśmy, zawiera dwie jednostki kompilacji. Moglibyśmy je rozdzielić na dwa różne pliki, ale nie moglibyśmy podzielić żadnego z nich na różne pliki: jednostka kompilacji musi być całkowicie zawarta w jednym pliku źródłowym. Zauważ, że tej architektury nie można zsyntetyzować, ponieważ nie opisuje ona funkcji, którą można bezpośrednio przełożyć na bramki logiczne.
Przeanalizuj i uruchom program za pomocą GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work hello_world.vhd
$ ghdl -r --workdir=gh_work hello_world
hello_world.vhd:6:8:@0ms:(assertion note): Hello world!
Katalog gh_work
to miejsce, w którym GHDL przechowuje generowane przez siebie pliki. Tak mówi opcja --workdir=gh_work
. Faza analizy sprawdza poprawność składni i tworzy plik tekstowy opisujący jednostki kompilacji znalezione w pliku źródłowym. Faza uruchamiania faktycznie kompiluje, łączy i wykonuje program. Zauważ, że w mcode
wersji ghdl żadne pliki binarne są generowane. Program jest rekompilowany za każdym razem, gdy go symulujemy. Wersje gcc
lub llvm
zachowują się inaczej. Zauważ też, że ghdl -r
nie przyjmuje nazwy pliku źródłowego VHDL, podobnie jak ghdl -a
, ale nazwę jednostki kompilacyjnej. W naszym przypadku przekazujemy mu nazwę entity
. Ponieważ ma tylko jedną architecture
, nie trzeba określać, która ma być symulowana.
Z Modelsim:
$ vlib ms_work
$ vmap work ms_work
$ vcom hello_world.vhd
$ vsim -c hello_world -do 'run -all; quit'
...
# ** Note: Hello world!
# Time: 0 ns Iteration: 0 Instance: /hello_world
...
vlib
, vmap
, vcom
i vsim
to cztery polecenia dostarczane przez Modelsim. vlib
tworzy katalog ( ms_work
), w którym będą przechowywane wygenerowane pliki. vmap
kojarzy katalog utworzony przez vlib
z logiczną nazwą ( work
). vcom
kompiluje plik źródłowy VHDL i domyślnie przechowuje wynik w katalogu powiązanym z logiczną nazwą work
. Wreszcie vsim
symuluje program i generuje ten sam rodzaj danych wyjściowych co GHDL. Zauważ ponownie, że pytanie o vsim
nie jest plikiem źródłowym, ale nazwą już skompilowanej jednostki kompilacyjnej. Opcja -c
nakazuje symulatorowi działanie w trybie wiersza poleceń zamiast domyślnego trybu graficznego interfejsu użytkownika (GUI). Opcja -do
służy do przekazania skryptu TCL do wykonania po załadowaniu projektu. TCL to język skryptowy bardzo często używany w narzędziach EDA. Wartością opcji -do
może być nazwa pliku lub, jak w naszym przykładzie, ciąg poleceń TCL. run -all; quit
poinstruuj symulator, aby uruchomił symulację, dopóki naturalnie się nie skończy - lub na zawsze, jeśli będzie trwała wiecznie - a następnie wyjdzie.
Licznik synchroniczny
-- File counter.vhd
-- The entity is the interface part. It has a name and a set of input / output
-- ports. Ports have a name, a direction and a type. The bit type has only two
-- values: '0' and '1'. It is one of the standard types.
entity counter is
port(
clock: in bit; -- We are using the rising edge of CLOCK
reset: in bit; -- Synchronous and active HIGH
data: out natural -- The current value of the counter
);
end entity counter;
-- The architecture describes the internals. It is always associated
-- to an entity.
architecture sync of counter is
-- The internal signals we use to count. Natural is another standard
-- type. VHDL is not case sensitive.
signal current_value: natural;
signal NEXT_VALUE: natural;
begin
-- A process is a concurrent statement. It is an infinite loop.
process
begin
-- The wait statement is a synchronization instruction. We wait
-- until clock changes and its new value is '1' (a rising edge).
wait until clock = '1';
-- Our reset is synchronous: we consider it only at rising edges
-- of our clock.
if reset = '1' then
-- <= is the signal assignment operator.
current_value <= 0;
else
current_value <= next_value;
end if;
end process;
-- Another process. The sensitivity list is another way to express
-- synchronization constraints. It (approximately) means: wait until
-- one of the signals in the list changes and then execute the process
-- body. Sensitivity list and wait statements cannot be used together
-- in the same process.
process(current_value)
begin
next_value <= current_value + 1;
end process;
-- A concurrent signal assignment, which is just a shorthand for the
-- (trivial) equivalent process.
data <= current_value;
end architecture sync;
Witaj świecie
Istnieje wiele sposobów na wydrukowanie klasycznego „Witaj świecie!” wiadomość w VHDL. Najprostszym ze wszystkich jest prawdopodobnie coś takiego:
-- File hello_world.vhd
entity hello_world is
end entity hello_world;
architecture arc of hello_world is
begin
assert false report "Hello world!" severity note;
end architecture arc;
Środowisko symulacji dla licznika synchronicznego
Środowiska symulacyjne
Środowisko symulacyjne dla projektu VHDL (projekt w trakcie testowania lub test testowy) to kolejny projekt VHDL, który co najmniej:
- Deklaruje sygnały odpowiadające portom wejściowym i wyjściowym testowanego urządzenia.
- Tworzy instancję DUT i łączy jego porty z zadeklarowanymi sygnałami.
- Tworzy instancję procesów sterujących sygnałami podłączonymi do portów wejściowych testowanego urządzenia.
Opcjonalnie środowisko symulacyjne może tworzyć instancje inne niż DUT, takie jak na przykład generatory ruchu na interfejsach, monitory sprawdzające protokoły komunikacyjne, automatyczne weryfikatory wyjść DUT ...
Środowisko symulacji jest analizowane, opracowywane i wykonywane. Większość symulatorów oferuje możliwość wyboru zestawu sygnałów do obserwacji, wykreślenia ich przebiegów graficznych, umieszczenia punktów przerwania w kodzie źródłowym, przejścia do kodu źródłowego ...
Idealnie, środowisko symulacyjne powinno być użyteczne jako solidny test bez regresji, to znaczy, powinno ono automatycznie wykrywać naruszenia specyfikacji DUT, zgłaszać przydatne komunikaty o błędach i gwarantować rozsądne pokrycie funkcji DUT. Gdy takie środowiska symulacyjne są dostępne, można je ponownie uruchomić przy każdej zmianie testowanego urządzenia, aby sprawdzić, czy jest ono funkcjonalnie poprawne, bez potrzeby żmudnych i podatnych na błędy kontroli wizualnych śladów symulacji.
W praktyce projektowanie idealnych, a nawet dobrych środowisk symulacyjnych jest trudne. Często jest to, a nawet trudniejsze, niż zaprojektowanie samego DUT.
W tym przykładzie przedstawiamy środowisko symulacyjne dla przykładu licznika synchronicznego . Pokażemy jak go uruchomić za pomocą ghdl i Modelsim i jak obserwować przebiegi graficzne za pomocą GTKWave z ghdl i wbudowanego w przeglądarkę fali z Modelsim. Następnie omawiamy interesujący aspekt symulacji: jak je zatrzymać?
Pierwsze środowisko symulacyjne dla licznika synchronicznego
Licznik synchroniczny ma dwa porty wejściowe i jeden port wyjściowy. Bardzo prostym środowiskiem symulacyjnym może być:
-- File counter_sim.vhd
-- Entities of simulation environments are frequently black boxes without
-- ports.
entity counter_sim is
end entity counter_sim;
architecture sim of counter_sim is
-- One signal per port of the DUT. Signals can have the same name as
-- the corresponding port but they do not need to.
signal clk: bit;
signal rst: bit;
signal data: natural;
begin
-- Instantiation of the DUT
u0: entity work.counter(sync)
port map(
clock => clk,
reset => rst,
data => data
);
-- A clock generating process with a 2ns clock period. The process
-- being an infinite loop, the clock will never stop toggling.
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
end process;
-- The process that handles the reset: active from beginning of
-- simulation until the 5th rising edge of the clock.
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
wait; -- Eternal wait. Stops the process forever.
end process;
end architecture sim;
Symulacja z GHDL
Kompilujmy i symulujmy to za pomocą GHDL:
$ mkdir gh_work
$ ghdl -a --workdir=gh_work counter_sim.vhd
counter_sim.vhd:27:24: unit "counter" not found in 'library "work"'
counter_sim.vhd:50:35: no declaration for "rising_edge"
Następnie komunikaty o błędach mówią nam dwie ważne rzeczy:
- Analizator GHDL odkrył, że nasz projekt tworzy instancję o nazwie
counter
ale ta jednostka nie została znaleziona wwork
biblioteki. Jest tak, ponieważ nie skompilowaliśmycounter
przedcounter_sim
. Podczas kompilacji VHDL wzorów, które podmioty instancji, poziom dolny zawsze musi być skompilowany przed najwyższych szczeblach (hierarchiczne projekty mogą być również zestawiane odgórnie, ale tylko wtedy, gdy instancjęcomponent
, a nie podmioty). - Funkcja
rising_edge
używana w naszym projekcie nie jest zdefiniowana. Wynika to z faktu, że funkcja ta została wprowadzona w VHDL 2008 i nie powiedzieliśmy GHDL, aby używała tej wersji języka (domyślnie używa VHDL 1993 z tolerancją składni VHDL 1987).
Naprawmy dwa błędy i uruchom symulację:
$ ghdl -a --workdir=gh_work --std=08 counter.vhd counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
^C
Zauważ, że do analizy i symulacji potrzebna jest opcja --std=08
. Zauważ też, że uruchomiliśmy symulację na encji counter_sim
, architekturze sim
, a nie na pliku źródłowym.
Ponieważ nasze środowisko symulacji ma niekończący się proces (proces generujący zegar), symulacja nie zatrzymuje się i musimy przerwać ją ręcznie. Zamiast tego możemy określić czas zatrzymania za pomocą opcji --stop-time
:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns
ghdl:info: simulation stopped by --stop-time
Ponieważ symulacja nie mówi nam wiele o zachowaniu naszego testera maszynowego. Zrzućmy zmiany wartości sygnałów w pliku:
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim --stop-time=60ns --vcd=counter_sim.vcd
Vcd.Avhpi_Error!
ghdl:info: simulation stopped by --stop-time
(zignoruj komunikat o błędzie, jest to coś, co należy naprawić w GHDL i to nie ma konsekwencji). Plik counter_sim.vcd
został utworzony. Zawiera w formacie VCD (ASCII) wszystkie zmiany sygnałów podczas symulacji. GTKWave może nam pokazać odpowiednie przebiegi graficzne:
$ gtkwave counter_sim.vcd
gdzie możemy zobaczyć, że licznik działa zgodnie z oczekiwaniami.
Symulacja przy pomocy Modelsim
Zasada jest dokładnie taka sama w Modelsim:
$ vlib ms_work
...
$ vmap work ms_work
...
$ vcom -nologo -quiet -2008 counter.vhd counter_sim.vhd
$ vsim -voptargs="+acc" 'counter_sim(sim)' -do 'add wave /*; run 60ns'
Zauważ, że -voptargs="+acc"
przekazana do vsim
: zapobiega symulacji optymalizacji sygnału data
i pozwala nam zobaczyć go na przebiegach.
Pełne wdzięku symulacje
W przypadku obu symulatorów musieliśmy przerwać niekończącą się symulację lub określić czas zatrzymania za pomocą dedykowanej opcji. To nie jest zbyt wygodne. W wielu przypadkach czas zakończenia symulacji jest trudny do przewidzenia. Znacznie lepiej byłoby zatrzymać symulację z wnętrza kodu VHDL środowiska symulacji, gdy zostanie spełniony określony warunek, na przykład gdy bieżąca wartość licznika osiągnie 20. Można to osiągnąć, twierdząc w proces, który obsługuje reset:
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
loop
wait until rising_edge(clk);
assert data /= 20 report "End of simulation" severity failure;
end loop;
end process;
Dopóki data
różnią się od 20, symulacja trwa. Gdy data
osiągną 20, symulacja ulega awarii z komunikatem o błędzie:
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:90:24:@51ns:(assertion failure): End of simulation
ghdl:error: assertion failed
from: process work.counter_sim(sim2).P1 at counter_sim.vhd:90
ghdl:error: simulation failed
Zauważ, że ponownie skompilowaliśmy tylko środowisko symulacyjne: jest to jedyny projekt, który się zmienił i jest to najwyższy poziom. Gdybyśmy zmodyfikowali tylko counter.vhd
, musielibyśmy ponownie skompilować zarówno: counter.vhd
ponieważ się zmienił, jak i counter_sim.vhd
ponieważ zależy od counter.vhd
.
Awaria symulacji z komunikatem o błędzie nie jest zbyt elegancka. Problemem może być nawet automatyczne analizowanie komunikatów symulacji w celu ustalenia, czy automatyczny test nie-regresyjny przeszedł pomyślnie. Lepszym i znacznie bardziej eleganckim rozwiązaniem jest zatrzymanie wszystkich procesów po osiągnięciu określonego warunku. Można to zrobić na przykład przez dodanie boolean
sygnału końca symulacji ( eof
). Domyślnie jest inicjowany na false
na początku symulacji. Jeden z naszych procesów sprawi, że stanie się to true
gdy nadejdzie czas zakończenia symulacji. Wszystkie pozostałe procesy będą monitorować ten sygnał i zatrzymają się na wieczne wait
kiedy się true
:
signal eos: boolean;
...
process
begin
clk <= '0';
wait for 1 ns;
clk <= '1';
wait for 1 ns;
if eos then
report "End of simulation";
wait;
end if;
end process;
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
eos <= true;
wait;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
counter_sim.vhd:120:24:@50ns:(report note): End of simulation
Wreszcie, w VHDL 2008 wprowadzono jeszcze lepsze rozwiązanie ze standardowym pakietem env
oraz deklarowanymi przez niego procedurami stop
i finish
:
use std.env.all;
...
process
begin
rst <= '1';
for i in 1 to 5 loop
wait until rising_edge(clk);
end loop;
rst <= '0';
for i in 1 to 20 loop
wait until rising_edge(clk);
end loop;
finish;
end process;
$ ghdl -a --workdir=gh_work --std=08 counter_sim.vhd
$ ghdl -r --workdir=gh_work --std=08 counter_sim sim
simulation finished @49ns
Sygnały a zmienne, krótki przegląd semantyki symulacji VHDL
Ten przykład dotyczy jednego z najbardziej podstawowych aspektów języka VHDL: semantyki symulacji. Jest przeznaczony dla początkujących w VHDL i przedstawia uproszczony widok, w którym pominięto wiele szczegółów (odroczone procesy, interfejs proceduralny VHDL, wspólne zmienne ...) Czytelnicy zainteresowani prawdziwą kompletną semantyką powinni zapoznać się z podręcznikiem językowym (LRM).
Sygnały i zmienne
Większość klasycznych imperatywnych języków programowania używa zmiennych. Są to kontenery wartości. Operator przypisania służy do przechowywania wartości w zmiennej:
a = 15;
wartość przechowywaną obecnie w zmiennej można odczytać i wykorzystać w innych instrukcjach:
if(a == 15) { print "Fifteen" }
VHDL używa również zmiennych i mają one dokładnie taką samą rolę jak w większości imperatywnych języków. Ale VHDL oferuje również inny rodzaj kontenera wartości: sygnał. Sygnały również przechowują wartości, można je także przypisywać i odczytywać. Typ wartości, które można przechowywać w sygnałach, jest (prawie) taki sam jak w zmiennych.
Po co więc mieć dwa rodzaje kontenerów wartości? Odpowiedź na to pytanie jest niezbędna i stanowi istotę języka. Zrozumienie różnicy między zmiennymi a sygnałami jest pierwszą rzeczą do zrobienia przed próbą zaprogramowania czegokolwiek w VHDL.
Zilustrujmy tę różnicę na konkretnym przykładzie: zamianie.
Uwaga: wszystkie następujące fragmenty kodu są częścią procesów. Zobaczymy później, jakie są procesy.
tmp := a;
a := b;
b := tmp;
zamienia zmienne a
i b
. Po wykonaniu tych 3 instrukcji nowa zawartość a
jest starą zawartością b
i odwrotnie. Podobnie jak w większości języków programowania, potrzebna jest trzecia zmienna tymczasowa ( tmp
). Gdyby zamiast zmiennych chcieliśmy zamienić sygnały, napisalibyśmy:
r <= s;
s <= r;
lub:
s <= r;
r <= s;
z tym samym rezultatem i bez potrzeby trzeciego tymczasowego sygnału!
Uwaga: operator przypisania sygnału VHDL
<=
różni się od operatora przypisania zmiennej:=
.
Spójrzmy na drugi przykład, w którym zakładamy, że podprogram print
drukuje dziesiętną reprezentację jego parametru. Jeśli a
jest zmienną całkowitą, a jej bieżąca wartość wynosi 15, wykonanie:
a := 2 * a;
a := a - 5;
a := a / 5;
print(a);
wydrukuje:
5
Jeśli mamy wykonać ten krok po kroku w debuggera widzimy wartości a
zmieniającym się od początkowego 15 do 30, 25 i wreszcie 5.
Ale jeśli s
jest sygnałem liczby całkowitej, a jego bieżąca wartość wynosi 15, wykonanie:
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
wydrukuje:
15
3
Jeśli wykonamy ten krok po kroku w debuggerze, nie zobaczymy żadnej zmiany wartości s
dopóki nie pojawi się instrukcja wait
. Co więcej, ostateczna wartość s
nie będzie wynosić 15, 30, 25 lub 5, ale 3!
To pozornie dziwne zachowanie wynika z zasadniczo równoległej natury sprzętu cyfrowego, co zobaczymy w kolejnych sekcjach.
Równoległość
VHDL jest sprzętowym językiem opisu (HDL), z natury jest równoległy. Program VHDL to zbiór programów sekwencyjnych, które działają równolegle. Te programy sekwencyjne nazywane są procesami:
P1: process
begin
instruction1;
instruction2;
...
instructionN;
end process P1;
P2: process
begin
...
end process P2;
Procesy, podobnie jak sprzęt, który modelują, nigdy się nie kończą: są nieskończonymi pętlami. Po wykonaniu ostatniej instrukcji wykonywanie jest kontynuowane od pierwszej.
Podobnie jak w przypadku każdego języka programowania, który obsługuje taką czy inną formę równoległości, harmonogram decyduje o tym, który proces wykonać (i kiedy) podczas symulacji VHDL. Ponadto język oferuje specyficzne konstrukcje do komunikacji i synchronizacji między procesami.
Planowanie
Program planujący utrzymuje listę wszystkich procesów i dla każdego z nich rejestruje bieżący stan, który może być running
, run-able
lub suspended
. Istnieje najwyżej jeden proces w stanie running
: ten, który jest aktualnie wykonywany. Tak długo, jak aktualnie uruchomiony proces nie wykonuje instrukcji wait
, kontynuuje działanie i uniemożliwia wykonanie jakiegokolwiek innego procesu. Program planujący VHDL nie ma charakteru zapobiegawczego: każdy proces ma obowiązek zawiesić się i zezwolić na uruchomienie innych procesów. Jest to jeden z problemów, które często napotykają początkujący VHDL: proces swobodnego działania.
P3: process
variable a: integer;
begin
a := s;
a := 2 * a;
r <= a;
end process P3;
Uwaga: zmienna
a
jest deklarowana lokalnie, podczas gdy sygnałys
ir
są deklarowane gdzie indziej, na wyższym poziomie. Zmienne VHDL są lokalne dla procesu, który je deklaruje i nie mogą być widoczne dla innych procesów. Inny proces mógłby również zadeklarować zmienną o nazwiea
, nie byłaby to ta sama zmienna, co zmienna procesuP3
.
Gdy tylko harmonogram wznowi proces P3
, symulacja utknie, aktualny czas symulacji nie będzie już postępował, a jedynym sposobem na zatrzymanie tego jest zabicie lub przerwanie symulacji. Powodem jest to, że P3
nie wait
oświadczenie i tym samym pozostanie w running
stan wiecznie, zapętlenie nad jego 3 instrukcji. Żaden inny proces nigdy nie będzie miał szansy na uruchomienie, nawet jeśli można go run-able
.
Nawet procesy zawierające instrukcję wait
mogą powodować ten sam problem:
P4: process
variable a: integer;
begin
a := s;
a := 2 * a;
if a = 16 then
wait on s;
end if;
r <= a;
end process P4;
Uwaga: operatorem równości VHDL jest
=
.
Jeśli proces P4
zostanie wznowiony, a wartość sygnału s
wynosi 3, będzie on działał wiecznie, ponieważ warunek a = 16
nigdy nie będzie prawdziwy.
Załóżmy, że nasz program VHDL nie zawiera takich patologicznych procesów. Gdy uruchomiony proces wykonuje instrukcję wait
, jest ona natychmiast zawieszana, a program planujący ustawia ją w stan suspended
. Instrukcja wait
również spełnia warunek ponownego run-able
procesu. Przykład:
wait on s;
środki zawiesza mi aż wartość sygnału s
zmian. Ten warunek jest rejestrowany przez program planujący. Następnie program planujący wybiera inny proces spośród możliwych do run-able
, ustawia go w stanie running
i wykonuje. To samo powtarza się, aż wszystkie run-able
procesy zostaną wykonane i zawieszone.
Ważna uwaga: gdy można
run-able
kilka procesów, standard VHDL nie określa, w jaki sposób program planujący powinien wybrać, który ma zostać uruchomiony. Konsekwencją jest to, że w zależności od symulatora, wersji symulatora, systemu operacyjnego lub czegokolwiek innego dwie symulacje tego samego modelu VHDL mogą w pewnym momencie dokonać różnych wyborów i wybrać inny proces do wykonania. Gdyby wybór ten miał wpływ na wyniki symulacji, moglibyśmy powiedzieć, że VHDL jest niedeterministyczny. Ponieważ niedeterminizm jest zwykle niepożądany, odpowiedzialnością programistów byłoby unikanie sytuacji niedeterministycznych. Na szczęście VHDL zajmuje się tym i to właśnie tam sygnały wchodzą do obrazu.
Sygnały i komunikacja międzyprocesowa
VHDL pozwala uniknąć niedeterminizmu, wykorzystując dwie specyficzne cechy:
- Procesy mogą wymieniać informacje tylko za pomocą sygnałów
signal r, s: integer; -- Common to all processes
...
P5: process
variable a: integer; -- Different from variable a of process P6
begin
a := s + 1;
r <= a;
a := r + 1;
wait on s;
end process P5;
P6: process
variable a: integer; -- Different from variable a of process P5
begin
a := r + 1;
s <= a;
wait on r;
end process P6;
Uwaga: komentarze VHDL rozciągają się od
--
do końca linii.
- Wartość sygnału VHDL nie zmienia się podczas wykonywania procesów
Za każdym razem, gdy przypisywany jest sygnał, przypisana wartość jest rejestrowana przez program planujący, ale bieżąca wartość sygnału pozostaje niezmieniona. Jest to kolejna ważna różnica w przypadku zmiennych, które przyjmują nową wartość natychmiast po przypisaniu.
Spójrzmy na wykonanie procesu P5
powyżej i załóżmy, że a=5
, s=1
i r=0
gdy zostanie wznowione przez program planujący. Po wykonaniu instrukcji a := s + 1;
, wartość zmiennej a
zmienia się i staje się 2 (1 + 1). Podczas wykonywania następnej instrukcji r <= a;
jest to nowa wartość a
(2) przypisana do r
. Ale r
jest sygnałem, aktualna wartość r
wynosi nadal 0. Zatem, wykonując a := r + 1;
, zmienna a
przyjmuje (natychmiast) wartość 1 (0 + 1), a nie 3 (2 + 1), jak by to mówiła intuicja.
Kiedy sygnał r
naprawdę przyjmie nową wartość? Kiedy program planujący wykona wszystkie działające procesy i wszystkie zostaną zawieszone. Jest to również określane jako: po jednym cyklu delta . Dopiero wtedy program planujący przyjrzy się wszystkim wartościom przypisanym do sygnałów i faktycznie zaktualizuje wartości sygnałów. Symulacja VHDL jest naprzemienną fazą wykonania i fazą aktualizacji sygnału. Podczas faz wykonania wartość sygnałów jest zamrożona. Symbolicznie mówimy, że między fazą wykonania a następną fazą aktualizacji sygnału upłynęła delta czasu. To nie jest czas rzeczywisty. Cykl delta nie ma fizycznego czasu trwania.
Dzięki temu mechanizmowi opóźnionej aktualizacji sygnału VHDL jest deterministyczny. Procesy mogą komunikować się tylko z sygnałami, a sygnały nie zmieniają się podczas wykonywania procesów. Kolejność wykonywania procesów nie ma zatem znaczenia: ich środowisko zewnętrzne (sygnały) nie zmienia się podczas wykonywania. Pokażmy to na poprzednim przykładzie z procesami P5
i P6
, gdzie stan początkowy to P5.a=5
, P6.a=10
, s=17
, r=0
i gdzie planista decyduje się na uruchomienie najpierw P5
a następnie P6
. Poniższa tabela pokazuje wartość dwóch zmiennych, bieżącą i następną wartość sygnałów po wykonaniu każdej instrukcji każdego procesu:
proces / instrukcja | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Stan początkowy | 5 | 10 | 17 | 0 | ||
P5 / a := s + 1 | 18 | 10 | 17 | 0 | ||
P5 / r <= a | 18 | 10 | 17 | 0 | 18 | |
P5 / a := r + 1 | 1 | 10 | 17 | 0 | 18 | |
P5 / wait on s | 1 | 10 | 17 | 0 | 18 | |
P6 / a := r + 1 | 1 | 1 | 17 | 0 | 18 | |
P6 / s <= a | 1 | 1 | 17 | 1 | 0 | 18 |
P6 / wait on r | 1 | 1 | 17 | 1 | 0 | 18 |
Po aktualizacji sygnału | 1 | 1 | 1 | 18 |
Przy tych samych warunkach początkowych, jeśli program planujący zdecyduje się najpierw uruchomić P6
a następnie P5
:
proces / instrukcja | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Stan początkowy | 5 | 10 | 17 | 0 | ||
P6 / a := r + 1 | 5 | 1 | 17 | 0 | ||
P6 / s <= a | 5 | 1 | 17 | 1 | 0 | |
P6 / wait on r | 5 | 1 | 17 | 1 | 0 | |
P5 / a := s + 1 | 18 | 1 | 17 | 1 | 0 | |
P5 / r <= a | 18 | 1 | 17 | 1 | 0 | 18 |
P5 / a := r + 1 | 1 | 1 | 17 | 1 | 0 | 18 |
P5 / wait on s | 1 | 1 | 17 | 1 | 0 | 18 |
Po aktualizacji sygnału | 1 | 1 | 1 | 18 |
Jak widać, po wykonaniu naszych dwóch procesów wynik jest taki sam bez względu na kolejność wykonywania.
Ta sprzeczna z intuicją semantyka przypisywania sygnałów jest przyczyną drugiego rodzaju problemów, które często napotykają początkujący VHDL: przypisanie, które najwyraźniej nie działa, ponieważ jest opóźnione o jeden cykl delta. Podczas prowadzenia procesu P5
krok po kroku, w debugera, gdy r
przypisano 18 i przypisano a
r + 1
, można oczekiwać, że wartość to 19 jednak debugera uporczywie mówi, że a
r=0
i a=1
...
Uwaga: ten sam sygnał może być przypisany kilka razy podczas tej samej fazy wykonania. W tym przypadku to ostatnie przypisanie decyduje o następnej wartości sygnału. Pozostałe zadania nie mają żadnego efektu, tak jakby nigdy nie zostały wykonane.
Czas sprawdzić nasze zrozumienie: wróć do naszego pierwszego przykładu zamiany i spróbuj zrozumieć, dlaczego:
process
begin
---
s <= r;
r <= s;
---
end process;
faktycznie zamienia sygnały r
i s
bez potrzeby trzeciego sygnału czasowego i dlaczego:
process
begin
---
r <= s;
s <= r;
---
end process;
byłoby ściśle równoważne. Spróbuj także zrozumieć, dlaczego, jeśli s
jest sygnałem całkowitym, a jego bieżąca wartość wynosi 15, a my wykonujemy:
process
begin
---
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
---
end process;
dwa pierwsze przypisania sygnału s
nie działają, dlaczego s
jest ostatecznie przypisane 3 i dlaczego dwie drukowane wartości to 15 i 3.
Czas fizyczny
Aby modelować sprzęt, bardzo przydatna jest możliwość modelowania czasu fizycznego wymaganego przez niektóre operacje. Oto przykład tego, jak można to zrobić w VHDL. Przykład modeluje licznik synchroniczny i jest to pełny, samodzielny kod VHDL, który można skompilować i zasymulować:
-- File counter.vhd
entity counter is
end entity counter;
architecture arc of counter is
signal clk: bit; -- Type bit has two values: '0' and '1'
signal c, nc: natural; -- Natural (non-negative) integers
begin
P1: process
begin
clk <= '0';
wait for 10 ns; -- Ten nano-seconds delay
clk <= '1';
wait for 10 ns; -- Ten nano-seconds delay
end process P1;
P2: process
begin
if clk = '1' and clk'event then
c <= nc;
end if;
wait on clk;
end process P2;
P3: process
begin
nc <= c + 1 after 5 ns; -- Five nano-seconds delay
wait on c;
end process P3;
end architecture arc;
W procesie P1
instrukcja wait
nie jest używana do czekania, aż wartość sygnału zmieni się, jak widzieliśmy do tej pory, ale do oczekiwania przez określony czas. Ten proces modeluje generator zegara. Sygnał clk
jest zegarem naszego systemu, jest okresowy z okresem 20 ns (50 MHz) i ma cykl pracy.
Proces P2
modeluje rejestr, który, jeśli pojawiło się zbocze narastające clk
, przypisuje wartość swojego wejścia nc
do wyjścia c
a następnie czeka na następną zmianę wartości clk
.
Proces P3
modeluje inkrementator, który przypisuje wartość swojego wejścia c
, zwiększoną o jeden, do jego wyjścia nc
... z fizycznym opóźnieniem 5 ns. Następnie czeka, aż zmieni się wartość jego wejścia c
. To jest także nowe. Do tej pory zawsze przypisywaliśmy sygnały za pomocą:
s <= value;
z powodów wyjaśnionych w poprzednich sekcjach możemy pośrednio przełożyć się na:
s <= value; -- after delta
Ten mały cyfrowy system sprzętowy może być reprezentowany przez następujący rysunek:
Wraz z wprowadzeniem czasu fizycznego i wiedząc, że mamy również czas symboliczny mierzony w delcie , mamy teraz dwuwymiarowy czas, który oznaczymy T+D
gdzie T
jest czasem fizycznym mierzonym w nanosekundach, a D
liczbą delt (bez fizycznego czasu trwania).
Pełny obraz
Jest jeden ważny aspekt symulacji VHDL, o którym jeszcze nie rozmawialiśmy: po fazie wykonania wszystkie procesy są w stanie suspended
. Nieformalnie stwierdziliśmy, że program planujący następnie aktualizuje wartości przypisanych sygnałów. Ale czy w naszym przykładzie licznika synchronicznego ma on jednocześnie aktualizować sygnały clk
, c
i nc
? Co z fizycznymi opóźnieniami? A co stanie się dalej ze wszystkimi procesami w stanie suspended
a żaden w run-able
?
Kompletny (ale uproszczony) algorytm symulacji jest następujący:
- Inicjalizacja
- Ustaw aktualny czas
Tc
na 0 + 0 (0 ns, 0 cykl delta) - Zainicjuj wszystkie sygnały.
- Wykonuj każdy proces, aż zawiesi się na instrukcji
wait
.- Zapisz wartości i opóźnienia przypisań sygnałów.
- Zapisz warunki wznowienia procesu (opóźnienie lub zmiana sygnału).
- Oblicz następnym razem
Tn
jako najwcześniejszy z:- Czas wznowienia procesów zawieszony przez
wait for <delay>
. - Następnym razem, kiedy wartość sygnału powinna się zmienić.
- Czas wznowienia procesów zawieszony przez
- Ustaw aktualny czas
- Cykl symulacji
-
Tc=Tn
. - Zaktualizuj sygnały, które muszą być.
- Ustaw w
run-able
wykonalnym wszystkie procesy, które czekały na zmianę wartości jednego z zaktualizowanych sygnałów. - Ustaw w
run-able
wykonalnym wszystkie procesy, które zostały zawieszone przez instrukcjęwait for <delay>
i dla których czas wznowienia wynosiTc
. - Wykonuj wszystkie działające procesy do momentu ich zawieszenia.
- Zapisz wartości i opóźnienia przypisań sygnałów.
- Zapisz warunki wznowienia procesu (opóźnienie lub zmiana sygnału).
- Oblicz następnym razem
Tn
jako najwcześniejszy z:- Czas wznowienia procesów zawieszony przez
wait for <delay>
. - Następnym razem, kiedy wartość sygnału powinna się zmienić.
- Czas wznowienia procesów zawieszony przez
- Jeśli
Tn
jest nieskończonością, zatrzymaj symulację. W przeciwnym razie rozpocznij nowy cykl symulacji.
-
Symulacja ręczna
Podsumowując, przeprowadźmy teraz ręcznie uproszczony algorytm symulacji na liczniku synchronicznym przedstawionym powyżej. Dowolnie decydujemy, że gdy kilka procesów będzie uruchomionych, kolejność będzie P3
> P2
> P1
. Poniższe tabele przedstawiają ewolucję stanu systemu podczas inicjalizacji i pierwszych cykli symulacji. Każdy sygnał ma własną kolumnę, w której wskazana jest bieżąca wartość. Po wykonaniu przypisania sygnału, wartość zaplanowana jest dołączana do bieżącej wartości, np. a/b@T+D
jeśli bieżącą wartością jest a
a następną wartością będzie b
w czasie T+D
(czas fizyczny plus cykle delta) . 3 ostatnie kolumny wskazują warunek wznowienia zawieszonych procesów (nazwa sygnałów, które muszą się zmienić lub czas, w którym proces zostanie wznowiony).
Faza inicjalizacji:
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 0 + 0 | |||||||
Zainicjuj wszystkie sygnały | 0 + 0 | „0” | 0 | 0 | ||||
P3/nc<=c+1 after 5 ns | 0 + 0 | „0” | 0 | 0/1 przy 5 + 0 | ||||
P3/wait on c | 0 + 0 | „0” | 0 | 0/1 przy 5 + 0 | c | |||
P2/if clk='1'... | 0 + 0 | „0” | 0 | 0/1 przy 5 + 0 | c | |||
P2/end if | 0 + 0 | „0” | 0 | 0/1 przy 5 + 0 | c | |||
P2/wait on clk | 0 + 0 | „0” | 0 | 0/1 przy 5 + 0 | clk | c | ||
P1/clk<='0' | 0 + 0 | „0” / „0” @ 0 + 1 | 0 | 0/1 przy 5 + 0 | clk | c | ||
P1/wait for 10 ns | 0 + 0 | „0” / „0” @ 0 + 1 | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c | |
Oblicz następnym razem | 0 + 0 | 0 + 1 | „0” / „0” @ 0 + 1 | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c |
Cykl symulacji nr 1
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 0 + 1 | „0” / „0” @ 0 + 1 | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c | |
Aktualizuj sygnały | 0 + 1 | „0” | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c | |
Oblicz następnym razem | 0 + 1 | 5 + 0 | „0” | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c |
Uwaga: podczas pierwszego cyklu symulacji nie ma fazy wykonania, ponieważ żaden z naszych 3 procesów nie spełnia warunku wznowienia.
P2
czeka na zmianę wartościclk
i nastąpiła transakcja naclk
, ale ponieważ stare i nowe wartości są takie same, nie jest to zmiana wartości.
Cykl symulacji nr 2
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 5 + 0 | „0” | 0 | 0/1 przy 5 + 0 | 10 + 0 | clk | c | |
Aktualizuj sygnały | 5 + 0 | „0” | 0 | 1 | 10 + 0 | clk | c | |
Oblicz następnym razem | 5 + 0 | 10 + 0 | „0” | 0 | 1 | 10 + 0 | clk | c |
Uwaga: ponownie nie ma fazy wykonania.
nc
zmieniło się, ale nanc
nie czeka proces.
Cykl symulacji nr 3
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 10 + 0 | „0” | 0 | 1 | 10 + 0 | clk | c | |
Aktualizuj sygnały | 10 + 0 | „0” | 0 | 1 | 10 + 0 | clk | c | |
P1/clk<='1' | 10 + 0 | „0” / „1” przy 10 + 1 | 0 | 1 | clk | c | ||
P1/wait for 10 ns | 10 + 0 | „0” / „1” przy 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Oblicz następnym razem | 10 + 0 | 10 + 1 | „0” / „1” przy 10 + 1 | 0 | 1 | 20 + 0 | clk | c |
Cykl symulacji # 4
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 10 + 1 | „0” / „1” przy 10 + 1 | 0 | 1 | 20 + 0 | clk | c | |
Aktualizuj sygnały | 10 + 1 | „1” | 0 | 1 | 20 + 0 | clk | c | |
P2/if clk='1'... | 10 + 1 | „1” | 0 | 1 | 20 + 0 | c | ||
P2/c<=nc | 10 + 1 | „1” | 0/1 przy 10 + 2 | 1 | 20 + 0 | c | ||
P2/end if | 10 + 1 | „1” | 0/1 przy 10 + 2 | 1 | 20 + 0 | c | ||
P2/wait on clk | 10 + 1 | „1” | 0/1 przy 10 + 2 | 1 | 20 + 0 | clk | c | |
Oblicz następnym razem | 10 + 1 | 10 + 2 | „1” | 0/1 przy 10 + 2 | 1 | 20 + 0 | clk | c |
Cykl symulacji nr 5
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 10 + 2 | „1” | 0/1 przy 10 + 2 | 1 | 20 + 0 | clk | c | |
Aktualizuj sygnały | 10 + 2 | „1” | 1 | 1 | 20 + 0 | clk | c | |
P3/nc<=c+1 after 5 ns | 10 + 2 | „1” | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | ||
P3/wait on c | 10 + 2 | „1” | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Oblicz następnym razem | 10 + 2 | 15 + 0 | „1” | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c |
Uwaga: można by pomyśleć, że aktualizacja
nc
będzie zaplanowana na15+2
, podczas gdy my zaplanujemy na15+0
. Po dodaniu niezerowego opóźnienia fizycznego (tutaj5 ns
) do bieżącego czasu (10+2
), cykle delta znikają. Rzeczywiście, cykle delta są użyteczne tylko do rozróżnienia różnych czasów symulacjiT+0
,T+1
... z tym samym czasem fizycznymT
Gdy tylko zmienia się czas fizyczny, cykle delta można zresetować.
Cykl symulacji # 6
Operacje | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Ustaw aktualny czas | 15 + 0 | „1” | 1 | 1/2 @ 15 + 0 | 20 + 0 | clk | c | |
Aktualizuj sygnały | 15 + 0 | „1” | 1 | 2) | 20 + 0 | clk | c | |
Oblicz następnym razem | 15 + 0 | 20 + 0 | „1” | 1 | 2) | 20 + 0 | clk | c |
Uwaga: ponownie nie ma fazy wykonania.
nc
zmieniło się, ale nanc
nie czeka proces.