kompilator języka vhdl do projektowania układów logicznych

Transkrypt

kompilator języka vhdl do projektowania układów logicznych
KOMPILATOR JĘZYKA VHDL
DO PROJEKTOWANIA
UKŁADÓW LOGICZNYCH
Edytor: prof. dr hab. inż. Włodzimierz Bielecki
ii
Spis treści
ISBN 83-87362-50-6
Pracownia Poligraficzna
WYDZIAŁU INFORMATYKI
POLITECHNIKI SZCZECIŃSKIEJ
ul. Żołnierska 49, 71-210 Szczecin, tel. (091) 449 56 02
Wydanie I. Nakład 150+24. Ark. druk. 15,0
grudzień 2002 r.
© Copyright by Wydział Informatyki Politechniki Szczecińskiej
Szczecin 2002
Przedmowa
Książka ta jest przeznaczona zarówno dla projektantów, jak i dla
studentów, doktorantów i naukowców zainteresowanych wiadomościami
dotyczącymi metod projektowania kompilatorów języka VHDL.
Opisuje ona organizację kompilatora języka VHDL do projektowania
układów logicznych, który powstał w Katedrze Technik Programowania
Wydziału Informatyki Politechniki Szczecińskiej. Kompilator generuje
równania boolowskie ze źródeł napisanych w języku VHDL. Przedstawienie
układów logicznych w postaci matematycznej za pomocą równań
boolowskich generowanych przez kompilator ze źródeł VHDL umożliwia
zastosowanie wszystkich znanych technik minimalizacji, weryfikacji,
walidacji, symulacji i syntezy układów logicznych.
Brak jest w tej chwili kompilatorów, które wprost generują równania
boolowskie ze źródeł VHDL. Brak jest również publikacji opisujących
techniki tworzenia kompilatorów języka VHDL do syntezy układów
logicznych. Pod tym względem książka ta jest jedną z pierwszych publikacji
opisujących techniki tworzenia takich kompilatorów. Większość materiału,
zawartego w tej książce, stanowi nowe podejście do omawianego problemu.
Nie wszystkie metody zaprezentowane w książce są optymalne ale jest to
jeden z pierwszych kroków w pokonaniu wyzwania utworzenia teorii takich
kompilatorów i udostępnienie dla wszystkich osób i ośrodków
zainteresowanych tą teorią i narzędziami.
Opisywany kompilator jest bardzo skomplikowanym programem. Jego
złożoność jest porównywalna ze złożonością kompilatora języka C++.
Tworzenie kompilatora wymagało czasochłonnych prac badawczych oraz
implementacyjnych. Dla kilkuset rożnych problemów związanych
z organizacją i implementacją kompilatora znaleźliśmy rozwiązania
ii
i zaimplementowaliśmy je. Jednak nie jest możliwe od razu w ograniczonym
czasie znaleźć wszystkie rozwiązania w sposób optymalny i
zaimplementować wszystko bez usterek. Prace nad technikami tworzenia
takich kompilatorów oraz testowaniem istniejącego kompilatora trwają dalej
w naszym zespole.
Książka zawiera również artykuły, które opisują organizację narzędzi do
przetwarzania równań boolowskich, mianowicie tworzenia grafów zależności
dla równań boolowskich oraz emulator systemu wieloprocesorowego do
symulacji równań boolowskich. Narzędzia te umożliwiają symulacje równań
boolowskich za pomocą komputerów równoległych.
Celem tej książki jest udostępnienie uzyskanych wyników dotyczących
organizacji kompilatora dla wszystkich zainteresowanych.
Chciałbym serdecznie podziękować wielu osobom, które pomagały nam
na wszystkich etapach tworzenia kompilatora. Bez ich pomocy nie
moglibyśmy nawet rozpocząć tej pracy.
Przede wszystkim jestem wdzięczny prezydentowi firmy ALDEC(USA)
Stanley’owi Haydukowi, który zainicjalizował prace nad kompilatorem i
czuwał nad nimi.
Chcielibyśmy wyrazić swoją wdzięczność Dziekanowi Wydziału
Informatyki Politechniki Szczecińskiej, profesorowi Jerzemu Sołdkowi,
który od początku prac nad kompilatorem popierając nasze działania i
stwarzając znakomite warunki do pracy naukowo-badawczej na Wydziale
Informatyki Politechniki Szczecińskiej.
Dziękuję za owocną współpracę wszystkim kolegom z ośrodków firmy
ALDEC w Polsce (Warszawie, Zielonej Górze, Gdańsku), którzy
doprowadzili do powstania działającej wersji kompilatora.
Dziękuję głównemu menedżerowi projektu I. Wojtkowskiemu za
bezcenną pomoc podczas prac nad kompilatorem.
Chciałbym wyrazić swoje głębokie uznanie oraz wyrazy wdzięczności
wszystkim wykonawcom projektu, mianowicie Piotrowi Błaszyńskiemu,
Robertowi Drążkowskiemu, Pawłowi Jaworskiemu, Marcinowi Lierszowi,
Mirosławowi Mościckiemu,
Maciejowi Poliwodzie, Marcinowi
Radziewiczowi,
Sławomirowi
Wernikowskiemu
i
Tomaszowi
Wiercińskiemu, którzy poświęcili swój cenny czas i włożyli wiele wysiłku w
prace prowadzące do powstania kompilatora.
Będę wdzięczny wszystkim za uwagi do tej książki, które proszę wysyłać
pod adres [email protected].
Kierownik Katedry Technik Programowania
Wydziału Informatyki PS
prof. dr hab. inż. Włodzimierz Bielecki
Szczecin Marzec, 2002
iii
Spis treści
ORGANIZACJA KOMPILATORA JĘZYKA VHDL DO PROJEKTOWANIA
UKŁADÓW LOGICZNYCH.................................................................................. 1
ANALIZA LEKSYKALNA I SYNTAKTYCZNA JĘZYKA VHDL
UŻYWANEGO DO GENERACJI RÓWNAŃ BOOLOWSKICH.................... 13
ANALIZATOR SEMANTYCZNY KOMPILATORA JĘZYKA VHDL
DO GENERACJI RÓWNAŃ BOOLOWSKICH................................................ 27
ZASADY NAZEWNICTWA I MODYFIKACJI NAZW W ANALIZATORZE
SEMANTYCZNYM JĘZYKA VHDL ................................................................. 37
ALGORYTM I ZASADY DOTYCZĄCE IMPLEMENTACJI
KONSTRUKCJI BLOKU W KOMPILATORZE JĘZYKA VHDL................. 47
FUNKCJE REZOLUCJI – ZASADA DZIAŁANIA I SPOSÓB
IMPLEMENTACJI W KOMPILATORZE JĘZYKA VHDL........................... 55
INSTRUKCJE WSPÓŁBIEŻNEGO PRZYPASANIA SYGNAŁÓW
DO SYNTEZY CYFROWYCH UKŁADÓW LOGICZNYCH ......................... 61
ALGORYTM GENEROWANIA RÓWNAŃ BOOLOWSKICH DLA
INSTRUKCJI PRZYPISANIA ZAWIERAJĄCEJ ODWOŁANIA
DO TABLIC JĘZYKA VHDL .............................................................................. 67
iv
ALGORYTMY GENERACJI RÓWNAŃ BOOLOWSKICH OPERACJI
MNOŻENIA CAŁKOWITYCH LICZB BINARNYCH .................................... 75
ALGORYTM GENERACJI RÓWNAŃ BOOLOWSKICH OPERACJI
DZIELENIA DLA SYNTEZY UKŁADÓW LOGICZNYCH............................ 85
PRZEKŁAD INSTRUKCJI IF ORAZ CASE JĘZYKA VHDL DO POSTACI
RÓWNAŃ BOOLOWSKICH ............................................................................... 95
GENERACJA RÓWNAŃ BOOLOWSKICH DLA INSTRUKCJI FOR
JĘZYKA VHDL ................................................................................................... 109
GENEROWANIE RÓWNAŃ BOOLOWSKICH DLA FUNKCJI
I PROCEDUR JĘZYKA VHDL ......................................................................... 123
MECHANIZM MAPOWANIA........................................................................... 133
IMPLEMENTACJA BIBLIOTEK STANDARDOWYCH .............................. 139
TŁUMACZENIE INSTRUKCJI GENERATE .................................................. 145
GENEROWANIE RÓWNAŃ BOOLOWSKICH DLA PRZERZUTNIKÓW
W KOMPILATORZE JĘZYKA VHDL DO SYMULACJI I SYNTEZY
UKŁADÓW LOGICZNYCH.............................................................................. 151
GENERACJA MASZYNY STANÓW DLA PROCESU Z WIELOMA
INSTRUKCJAMI OCZEKIWANIA WAIT...................................................... 185
POSTPROCESOR KOMPILATORA JĘZYKA VHDL .................................. 197
WERSJE IMPLEMENTACYJNE POSTPROCESORA KOMPILATORA
JĘZYKA VHDL ................................................................................................... 207
A CREATION OF BOOLEAN EQUATION GRAPH FOR AUTOMATIC
PARALLELIZING............................................................................................... 217
MULTIPROCESSOR SYSTEM EMULATOR FOR DIGITAL DEVICES
MODELING ......................................................................................................... 223
Organizacja kompilatora języka VHDL
do projektowania układów logicznych
Włodzimierz Bielecki
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł zawiera opis organizacji i możliwości kompilatora języka VHDL do
generowania równań boolowskich, które opisują logikę kombinacyjną
i sekwencyjną układów logicznych. Kompilator używa ograniczonej gramatyki
języka VHDL, umożliwiającej syntezę układów logicznych. W skrócie opisano
podstawowe części kompilatora: analizatorów leksykalnego, syntaktycznego,
semantycznego, generatora równań boolowskich oraz postprocesora. Przytoczono
przykład generacji równań boolowskich za pomocą kompilatora. Przedstawiono
wyniki testowania i stan kompilatora.
Słowa kluczowe:
1.
Język VHDL, synteza układów logicznych, równania boolowskie
WSTĘP
Opracowanie teorii tworzenia kompilatorów generujących równania
boolowskie z programu źródłowego w języku VHDL (Very High Speed
Integrated Circuit (V) Hardware Description Language(HDL)) jest
aktualnym wyzwaniem. VHDL jest jednym z nielicznych języków HDL,
który jest rozpoznawany jako standard IEEE [1,2] oraz Ministerstwa Obrony
Stanów Zjednoczonych.
VHDL można stosować do symulacji i syntezy układów logicznych.
Jeżeli chodzi o syntezę, to na składnię (zbiór produkcji wykorzystywanych
do tworzenia zdań programu źródłowego) są nakładane ograniczenia, to
znaczy, że nie ze wszystkich możliwości i konstrukcji VHDL można
korzystać w źródłach przeznaczonych do syntezy układów logicznych. W tej
2
chwili jeszcze nie ma standardu dotyczącego wersji VHDL, stosowanej do
syntezy. Różni producenci sami określają ograniczenia nakładane na
gramatykę VHDL, spełnienie których umożliwia poprawną syntezę.
Istniejące kompilatory wspierające język VHDL produkują netlisty lub
inne reprezentacje układów logicznych. Brak jest kompilatorów, które
wprost generują równania boolowskie. Reprezentacja układów logicznych w
postaci równań boolowskich umożliwia: 1) minimalizację równań; 2)
symulację układów logicznych reprezentowanych równaniami za pomocą
komputerów zwykłych i równoległych; 3) weryfikację projektowanych
układów; 4) odwzorowanie na układy FPGA.
Ponieważ większość istniejących kompilatorów języka VHDL są to
narzędzia komercyjne, brak jest publikacji opisujących metody i algorytmy
tworzenia takich kompilatorów. Istnieje duże zapotrzebowanie na badania
związane z organizacją takich kompilatorów i utworzenie teorii takich
kompilatorów dostępnej dla środowiska naukowego i przemysłu
softwarowego.
Od kilku lat w Katedrze Technik Programowania Wydziału Informatyki
PS są wykonywane badania w kierunku tworzenia kompilatora
kompatybilnego ze specyfikacją dobrze znanego
kompilatora firmy
Synopsys Compiler II / FPGA Express[3] do syntezy układów logicznych.
Ograniczeniem tworzonego kompilatora jest to, że wspiera on tylko
następujące pakiety standardowe: std_logic_1164, std_logic_arith,
std_logic_signed i std_logic_unsigned.
Główne zadanie kompilatora polega na generowaniu równań
boolowskich ze źródeł napisanych w języku VHDL spełniających
wymagania specyfikacji. Kompilator zawiera analizatory: leksykalny,
syntaktyczny i semantyczny, generator kodu, postprocesor oraz program
główny łączący razem wszystkie programy kompilatora.
Praca nad kompilatorem wymagała dużego wysiłku związanego z
opracowaniem zasad, metod, algorytmów kompilacji, w tym algorytmów
formalnych generacji równań boolowskich dla wszystkich konstrukcji
VHDL: sekwencyjnych i współbieżnych.
Do testowania poprawności wygenerowanych równań stworzono
symulator równań boolowskich, który na wejściu otrzymuje równania
boolowskie oraz wartości sygnałów wejściowych i wykonuje symulację –
oblicza wartości wszystkich identyfikatorów występujących po lewej stronie
równań. Równania mogą być nieuporządkowane – wygenerowane w
dowolnej kolejności. Symulator sam wyszukuje prawidłową kolejność
obliczania równań. Wyniki symulacji można porównać z wynikami, które
daje symulator ACTIVE VHDL. Zgodność obu wyników wskazuje na to, że
wygenerowane przez kompilator równania są prawidłowe.
Kolejne rozdziały opisują szczegóły organizacji kompilatora.
3
2.
ANALIZATORY LEKSYKALNY,
SYNTAKTYCZNY I SEMANTYCZNY
Wejściem analizatora leksykalnego jest źródło w języku VHDL,
wyjściem jest plik zawierający leksemy (słowa kluczowe, operatory,
identyfikatory, stałe itd.) wraz z liczbami przechowującymi numery wierszy
źródła tych leksemów.
Analizator leksykalny czyta zdania programu źródłowego w języku
VHDL, rozpoznaje wszystkie konstrukcje VHDL i tworzy wewnętrzną
postać programu jako ciąg leksemów.
Analizator syntaktyczny bada poprawność zdań programu pod względem
składni – bada czy zdania programu należą do zdań generowanych przez
gramatykę VHDL, z uwzględnieniem ograniczeń możliwości syntezy
układów logicznych. Nie zmienia on postaci wewnętrznej programu,
informuje tylko o poprawności/niepoprawności źródła.
Analizatory leksykalny i syntaktyczny zostały zaimplementowane jako
dwa niezależne programy. Najpierw analizator leksykalny generuje plik z
leksemami, dalej analizator składniowy sprawdza poprawność zdań
zawartych w tym pliku.
Analizatory leksykalny i składniowy zostały zaimplementowane za
pomocą standardowych narzędzi systemu operacyjnego UNIX: lex i jacc
odpowiednio; odpowiednikami tych narzędzi w systemie operacyjnym
WINDOWS są flex i bison.
Analizator semantyczny bada poprawność semantyczną zdań programu
źródłowego i dodaje dodatkową informację (wartości semantyczne) do
poszczególnych leksemów – typy danych, nazwy identyfikatorów, wymiary
tablic itd. Wartości te są przechowywane za pomocą drzewa katalogów w
plikach. Zadaniem analizatora jest również wyszukiwanie wartości
semantycznych na żądanie generatora kodu. Wejściem do analizatora
semantycznego jest plik z leksemami tworzącymi zdania programu
źródłowego. Analizator zakłada, że zdania te już zostały sprawdzone przez
analizator syntaktyczny.
Analizator semantyczny został zaimplementowany jako niezależny
program w języku C++.
3.
GENERATOR RÓWNAŃ BOOLOWSKICH
Generator równań boolowskich czyta leksemy produkowane przez
analizator leksykalny, rozpoznaje instrukcje języka VHDL i wywołuje
odpowiednie funkcje generujące równania boolowskie. Każda taka funkcja
rozpoznaje leksemy zawarte w zdaniu reprezentującym instrukcję: słowa
4
kluczowe, sygnały, zmienne, operatory itd. Dalej generator wywołuje
funkcję analizatora semantycznego, która wyszukuje i zwraca wartości
semantyczne poszczególnych leksemów. Na podstawie tych wartości i
operacji instrukcji, funkcja ta produkuje równania boolowskie. Poniżej
przytoczono wykaz niektórych z tych funkcji do generowania równań:
a) wyrażeń arytmetycznych i logicznych;
b) następujących instrukcji sekwencyjnych(zawartych wewnątrz procesu):
– assignment statements and targets;
– variable assignment statements;
– signal assignment statements;
– if statements;
– case statements;
– loop statements;
– next statements;
– exit statements;
– subprograms;
– return statement;
– wait statements;
– null statements;
c) następujących instrukcji współbieżnych:
– process statements;
– block statements;
– concurrent procedure calls;
– concurrent signal assignments;
– component instantiation statements;
– direct instantiation;
– generate statements;
Dla większości wyrażeń i instrukcji języka VHDL po raz pierwszy zostały
opracowane metody i algorytmy umożliwiające generowanie równań, ponieważ
brak było publikacji rozpatrujących takie zagadnienie. To była najtrudniejsza i
najbardziej czasochłonna część pracy nad kompilatorem.
Najbardziej skomplikowana cześć generatora kodu to zbiór funkcji do
generowania równań z instrukcji procesu oraz instrukcji generate. Spośród
instrukcji procesu najtrudniejsze do implementacji są konstrukcje zawierające
instrukcje wait oraz instrukcje for. Instrukcja wait wymaga wygenerowania
równań maszyny stanów. Równania te reprezentują licznik z wejściami T, R, D
i CLK oraz dekoder, którego każde wyjście prezentuje jakiś stan oraz logikę
kombinacyjną, która zarządza przejściami w maszynie stanów za pomocą wejść
T, R, D. Przed wygenerowaniem równań generator kodu powinien znaleźć stan
dla każdej instrukcji przypisania znajdującej się wewnątrz procesu. Dalej
generator kodu produkuje przerzutniki dla każdej instrukcji przypisania.
Równanie dla wejścia zegara C przerzutnika ma postać:
5
C = (CLK) AND (State i),
gdzie CLK jest to sygnał zegara występujący w instrukcji wait; State i jest
to stan instrukcji przypisania; “AND” jest to operacja logiczna AND.
Instrukcja for jest implementowana za pomocą dwóch kroków. Najpierw
pętla for zostaje przekształcona w kod liniowy. Dalej dla każdej instrukcji
tego kodu są generowane równania.
Implementacja instrukcji while jest związana z koniecznością
generowania równań przerzutników dla wszystkich instrukcji przypisania,
do których zapis danych jest sterowany za pomocą wygenerowanej maszyny
stanów (równań tej maszyny) oraz równań logiki, która zarządza przejściami
maszyny stanów.
Generator kodu generuje równania dla następujących możliwości i
mechanizmów języka VHDL:
– generiki;
– hierarchia projektu;
– wykorzystanie pakietów i bibliotek;
– wykorzystanie funkcji rezolucji;
– przeciążenie subprogramów i operatorów;
– reguły widoczności obiektów języka VHDL.
Wszystkie funkcje i procedury pakietów standardowych (std_logic_1164,
std_logic_arith,
std_logic_signed,
std_logic_unsigned)
są
zaimplementowane jako wbudowane w kompilator czyli są wewnętrznymi
funkcjami kompilatora. Celem takiej implementacji było zredukowanie do
minimum czasu generowania równań dla tych funkcji i procedur ponieważ
projekty rzeczywisty bardzo często z nich korzystają.
Generator kodu jest najbardziej złożoną częścią kompilatora. Jego źródła
w języku C++ to ponad 2/3 wszystkich źródeł kompilatora.
4.
POSTPROCESOR
Postprocesor jest niezależną częścią kompilatora. Główne zadania
postprocesora są następujące: i) korygowanie równań boolowskich dla logiki
sekwencyjnej; ii) korygowanie równań dla logiki synchronicznieasynchronicznej; iii) połączenie równań o takich samych lewych stronach
reprezentowanych nazwami sygnałów typu resolved; iv) minimalizacja
równań boolowskich; v) eliminacja zmiennych roboczych w uzasadnionych
przypadkach; vi) wyszukiwanie dwóch lub więcej par sygnałów o takiej
samej nazwie po lewej stronie równań i nie zadeklarowanych jako resolved;
sygnalizacja błędów w takich przypadkach.
Wejściem dla postprocesora są równania boolowskie oraz pliki specjalne
generowane przez analizator semantyczny lub generator kodu. Postprocesor
6
produkuje końcową postać równań. Każda nazwa sygnału lub zmiennej,
która reprezentuje wyjście przerzutnika lub zatrzasku powinna mieć
parametr «t», na przykład var(t, ...). Wszystkie nazwy sygnałów i
zmiennych sekwencyjnych są generowane przez generator kodu i
przechowywane w specjalnym pliku. Zadaniem postprocesora jest odczyt
wszystkich nazw sygnałów i zmiennych sekwencyjnych z tego pliku i
korygowanie wszystkich równań boolowskich, które je zawierają przez
dodanie symbolu «t» jako parametru tych nazw.
Istnieje potrzeba korygowania równań boolowskich opisujących wejścia
synchroniczne przerzutników, na przykład wejście D, przez dodanie
parametru «t-1» do odpowiednich nazw wtedy, kiedy są one połączone z
wyjściami innych przerzutników. Generator kodu generuje specjalne pragmy
(dyrektywy) przed równaniami przerzutników z wejściami synchronicznymi.
Postprocesor rozpoznaje takie pragmy i dodaje parametr «t-1» do nazw w
równaniach boolowskich.
Ponieważ wyjścia przerzutników mogą być połączone z wejściami
synchronicznymi innych przerzutników za pomocą logiki kombinacyjnej,
istnieje potrzeba znalezienia i skorygowania równań boolowskich
opisujących taką logikę kombinacyjną. Zadanie to wykonuje postprocesor.
Wszystkie sygnały typu resolved[1] są rozpoznawane przez analizator
semantyczny i zapisywane do specjalnego pliku. Zadaniem postprocesora
jest odczyt wszystkich takich sygnałów, wyszukiwanie sygnałów o takich
nazwach po lewych stronach równań. Wszystkie równania z identycznymi
lewymi stronami są łączone wtedy w jedno równanie zgodnie z semantyką
zadeklarowanej funkcji rezolucji. Dozwolone funkcje rezolucji to funkcje
«OR», «AND», i «THREE STATE» [1].
Postprocesor dokonuje minimalizacji równań boolowskich. Konwertuje
on pewne człony równań na prostszą postać. Niektóre przykłady
minimalizacji dokonywane przez kompilator są następujące: 0 & ... = 0; 0 |
... = ... ; 1 & ... = ...; 1 |...= 1; a & a =a; a | a =a; !!a = a; gdzie &, | , !
reprezentują operacje logiczne odpowiednio AND, OR i NOT.
W uzasadnionych przypadkach należy wyeliminować zmienne robocze z
równań w celu zmniejszenia rozmiaru kodu. Postprocesor korzysta z
algorytmów heurystycznych, żeby podjąć decyzję, czy warto eliminować
zmienne robocze. Wyeliminowanie zmiennych roboczych ma sens wtedy,
kiedy zmniejsza to rozmiar równań. Nie zawsze wyeliminowanie zmiennych
roboczych powoduje taki efekt. Wtedy postprocesor rezygnuje z eliminacji.
Postprocesor wyszukuje równania o takich samych lewych stronach. Jeśli
ma to miejsce i sygnaly/zmienne po lewej stronie nie są zadeklarowane jako
resolved, to postprocesor sygnalizuje błąd w źródle VHDL.
7
5.
PRZYKŁAD KOMPILACJI
Dla następującego źródła VHDL
library IEEE;
use IEEE.std_logic_1164.all;
entity test is
port (
a,b: in STD_LOGIC_vector(0 to 1);
s : in STD_LOGIC;
z,d: out STD_LOGIC_vector(0 to 1)
);
end test;
architecture test3 of test is
begin
process (a, b,s)
Begin
if s ='1' then
z<=a;
d(0)<=a(0);
else
d(1)<=a(1);
z<=b;
end if;
End process;
end test3;
kompilator generuje równania boolowskie jak niżej
--process equations begin, line: 14
z(0)=(((s))&((a(0))))|((!s)&((b(0))));
z(1)=(((s))&((a(1))))|((!s)&((b(1))));
C_tmp0=(s);
D_tmp0(0)=((s))&((a(0)));
-- latch(C_high,D)
S_tmp0(0)=D_tmp0(0)&C_tmp0;
R_tmp0(0)=C_tmp0&!D_tmp0(0);
d(t,0)=S_tmp0(0)|(!R_tmp0(0)&d(t-1,0));
C_tmp1=!s;
D_tmp1(1)=(!s)&((a(1)));
-- latch(C_high,D)
S_tmp1(1)=D_tmp1(1)&C_tmp1;
8
R_tmp1(1)=C_tmp1&!D_tmp1(1);
d(t,1)=S_tmp1(1)|(!R_tmp1(1)&d(t-1,1));
--state.out file begin
--state.out file end
--process equations end, line: 14
Dla portu „z” kompilator generuje logikę kombinacyjną, natomiast dla
portu „d”- logikę sekwencyjną.
Równania boolowskie dla każdego zatrzasku poprzedza komentarz w postaci
-- latch(C_high,D).
Parametr “t” wewnątrz każdego portu „d” określa sygnał sekwencyjny
(wyjście zatrzasku).
Tylko 3 operacje logiczne są możliwe w równaniach boolowskich: NOT
(!), AND (&), OR (|). Każde równanie boolowskie ma na końcu średnik. Są
dozwolone komentarze. Dowolny tekst poprzedzony “--“ jest komentarzem.
Nazwy sygnałów i zmiennych w równaniach boolowskich są zgodne z
regułami tworzenia nazw w języku VHDL. Nazwy rozszerzone[1] są
również dozwolone. Czyli każda nazwa dopuszczalna w źródłach języka
VHDL jest również dopuszczalna w równaniach boolowskich.
Trzy dodatkowe narzędzia zostały opracowane do konwertowania równań
boolowskich na notację polską, formaty BLIF oraz SLIF. Są to najszerzej
stosowane formaty do reprezentacji równań boolowskich. Użytkownik ma
możliwość wygenerować równania w dowolnym z tych formatów.
6.
IMPLEMENTACJA
I TESTOWANIE KOMPILATORA
Kompilator został zaimplementowany jako 6 niezależnych narzędzi.
Poniższa tablica (tab. 1) reprezentuje wszystkie te narzędzia, ich rozmiary oraz
zastosowane kompilatory do kompilacji źródeł poszczególnych narzędzi:
Narzędzie
Vhmake
Vhdllex
Vhdlpars
Anseman
Generator
Postprocessor
Suma
Rozmiar źródła
16 KB
108 KB
139 KB
346 KB
1990 KB
163 KB
2762 KB
Tabela 1.
Narzędzie kompilacji
Microsoft Visual C++ 6.0
(+ flex/bison output)
(+ flex/bison output)
Microsoft Visual C++ 6.0
Microsoft Visual C++ 6.0
Microsoft Visual C++ 6.0
9
gdzie vhmake, vhdllex, vhdlpars, ansemam, generator, i postprocessor są to
odpowiednio program główny, leksykalny, syntaktyczny i semantyczny
analizatory, generator równań boolowskich oraz postprocesor.
Kilka narzędzi zostało utworzonych do testowania i weryfikacji
kompilatora: i) symulator równań boolowskich generowanych przez
kompilator; ii) narzędzie do automatycznego testowania funkcjonalności
kompilatora; umożliwia ono w sposób automatyczny odczytywanie i
wykonywanie pojedynczych testów; w przypadku, kiedy kompilator nie
radzi sobie z tymi testami, narzędzie wypisuje nazwę testów i rodzaj błędu
(jaka cześć kompilatora powoduje ten błąd); iii) narzędzie do
automatycznego porównywania wyników symulacji równań boolowskich
(wyjście narzędzia i)) z wynikami symulacji źródła w języku VHDL
(wyjście symulatora ACTIVE firmy ALDEC ).
Kompilator został przetestowany za pomocą kilku tysięcy pojedynczych
testów oraz około 10 projektach przemysłowych.
Rozmiary generowanych plików z równaniami wahały się od 0.05 KB
do 40 MB. Czas kompilacji zależy od źródeł w języku VHDL. Źródła które
nie zawierają instrukcji for, powodującej wiele iteracji oraz nie zawierające
wywołania dużej liczby funkcji z pakietów standardowych (standard IEEE
packages) kompilują się dość szybko (od ułamka sekundy do kilku minut).
Źródła zawierające pętle for z instrukcjami next i exit, kompilator
konwertuje na kod liniowy, który zawiera instrukcje warunkowe if. Kod ten
może być dość długi i może wymagać dużo czasu kompilacji – od kilku
minut do kilku godzin. Istnieje potrzeba optymalizacji funkcji
odpowiedzialnych za przekład takich instrukcji.
7.
MOŻLIWOŚCI ZASTOSOWANIA
KOMPILATORA
Tworzony kompilator może być wykorzystany do rozwiązywania
następujących zagadnień.
1. Konwertowania generowanych równań na formaty BLIF, SLIF i
wykorzystanie istniejących narzędzi (w tym i akademickich)
obsługujących te formaty do minimalizacji, symulacji, weryfikacji i
syntezy układów logicznych.
2. Weryfikacji układów logicznych za pomocą tworzenia BDD z
generowanych przez kompilator równań.
3. Weryfikacji układów logicznych (kombinacyjnych i sekwencyjnych) za
pomocą stworzonego symulatora równań boolowskich.
4. Weryfikacji układów logicznych poprzez symulację równań na
komputerach równoległych w celu zmniejszenia czasu symulacji. W tej
10
chwili został opracowany prototyp symulatora dla PC z wieloma
procesorami Pentium. Dla dwóch procesorów Pentium uzyskane
przyspieszenie wynosi od 1.7 do 1.97.
5. Weryfikacji kombinacyjnych układów logicznych za pomocą symulacji
równań w systemach rozproszonych (sieciach komputerowych). Cały
zestaw równań może być symulowany niezależnie na rożnych
komputerach dla różnych wartości sygnałów wejściowych. Ponieważ nie
jest tu wymagana wymiana danych i synchronizacji między komputerami
przyspieszenie będzie bliskie do liczby komputerów (w przypadku
środowiska homogenicznego).
6. Weryfikacji układów logicznych przez porównanie wyników symulacji
za pomocą symulatorów źródeł VHDL i symulatora równań
boolowskich.
7. Znalezienia opóźnień sygnałów wyjściowych za pomocą równań
boolowskich.
8. Oszacowania złożoności projektowanych układów (ilość bramek) na
podstawie wygenerowanych równań.
9. Konwersji równań do formatów, z którymi pracują istniejące narzędzia
syntezy (odwzorowanie na FPGA) co umożliwi syntezę układów
logicznych na podstawie wygenerowanych równań.
Istnieje możliwość modyfikacji formatu generowanych równań i
dopasowania do niego symulatorów równań w taki sposób aby wprowadzić
w składnie generowanych równań instrukcje if, for oraz możliwość
korzystania z funkcji i procedur. Taka modyfikacja umożliwi:
– znaczne zmniejszenie rozmiaru równań (w niektórych przypadkach mapowanie, instrukcje if, for, wywołanie procedur i funkcji - nawet 1000
razy);
– znaczną redukcję czasu generowania równań (w zaznaczonych wyżej
przypadkach do 100 razy);
– znaczne zmniejszenie wykorzystanej podczas kompilacji pamięci (10100 razy);
Moduły (części) kompilatora mogą być zastosowane dla szybkiego
tworzenia następujących narzędzi.
1. Graficznej reprezentacji układów logicznych kodowanych w VHDL.
Utworzenie takiego narzędzia wymaga wyboru jakiegoś edytora
graficznego i uzupełnienie generatora równań możliwością generowania
reprezentacji graficznej układów w formacie zgodnym z tym, z którym
pracuje edytor. Czyli wymaga to uzupełnienia tylko jednej części
kompilatora.
2. Konwerterów C2bool, C++2bool, Verilog2bool, C2VHDL, C++2VHDL.
Istnieje możliwość konwertowania źródeł C, C++, Verilog wprost na
leksemy reprezentujące instrukcje VHDL. Takie podejście umożliwi:
11
– przekład z języka wyższego na język wyższy, co jest zadaniem znacznie
prostszym niż przekład na równania boolowskie;
– zastosowanie w 100% (bez żadnych zmian) już istniejących analizatorów
syntaktycznego, semantycznego, generatora równań i postprocesora
VHDL2bool;
– wykorzystanie ograniczonego zbioru instrukcji i możliwości VHDL w
celu przyspieszenia testowania i powstania przemysłowych wersji
nowych kompilatorów; to może być tylko około 20%-30% procent
możliwości VHDL ale takich, które umożliwia poprawny i efektywny
przekład źródeł C, C++, VERILOG na równania boolowskie.
– zastosowanie już istniejących narzędzi weryfikacji kompilatora
VHDL2bool (symulator równań).
Takie podejście do tworzenia nowych kompilatorów wymaga głównie prac
nad algorytmizacją przekładu źródeł C, C++, VERILOG na leksemy VHDL.
Implementacja będzie znacznie mniej czasochłonna niż tworzenie
kompilatora VHDL2bool.
3. Konwertera VHDL2VERILOG. Dla tworzenia takiego kompilatora
można w 100% zastosować z VHDL2bool analizatory: leksykalny,
syntaktyczny i semantyczny. Wystarczy tylko dodać do nich nowy
generator kodu.
4. Konwertera VHDL2C_zwykly/rownolegly. Taki konwerter można
zastosować do bardzo szybkiej symulacji syntezowalnych źródeł VHDL
za pomocą przekładu na program w C ( nie symulator VHDL, lecz
program). Kompilacja takiego programu i jego wykonywanie dla różnych
zestawów wartości sygnałów wejściowych umożliwi znaczną redukcję
czasu symulacji (10- 1000 razy) w porównaniu ze zwykłymi
symulatorami zdarzeniowymi źródeł VHDL. Program w języku C może
być sekwencyjny lub równoległy. Ostatni umożliwi wykonywanie
symulacji na komputerach wieloprocesorowych. Przyspieszenie symulacji
dla logiki kombinacyjnej będzie rosło w sposób liniowy w zależności od
liczby procesorów, ponieważ w tym przypadku nie jest wymagany
podział równań miedzy procesory. Trzeba zwrócić uwagę na to, że nawet
za pomocą sekwencyjnego programu C można uzyskać znacznie mniejszy
czas symulacji, ponieważ nie jest tu tworzony symulator z jego dużymi
nakładami czasowymi lecz tylko pojedynczy wykonywalny program dla
każdego syntezowalnego źródła VHDL. Dla tworzenia takiego
konwertora można zastosować z VHDL2bool analizatory leksykalny,
syntaktyczny i semantyczny.
12
8.
ZAKOŃCZENIE
Postać matematyczna równań boolowskich generowanych przez
kompilator ze źródeł VHDL umożliwia zastosowanie wszystkich znanych
technik minimalizacji, weryfikacji, walidacji, symulacji i syntezy układów
logicznych.
Konwertowanie równań boolowskich generowanych przez kompilator na
inne formaty takie jak notacja polska, BLIF, SLIF umożliwia zastosowanie
wszystkich istniejących narzędzi i oprogramowania do pracy z tymi
formatami.
Istnieje potrzeba dalszych prac i badań nad kompilatorem w
następujących kierunkach: i) redukcja czasu generowania równań dla pętli
for; ii) zmniejszenie czasu wyszukiwania wartości semantycznych dla
dużych źródeł.
Pakiety standardowe inne niż std_logic_1164, std_logic_arith,
std_logic_signed, i std_logic_unsigned powinny być zaimplementowane i
dodane do kompilatora w celu rozszerzenia zakresu jego zastosowania.
Praca nad testowaniem, weryfikacją i walidacją kompilatora powinna być
przedłużona w celu wykrycia usterek w źródłach kompilatora aby
doprowadzić kompilator do wersji przemysłowej. Wszystkie te pracy są w
stanie wykonywania.
LITERATURA
[1] P.J. Ashenden, “The Designer's Guide to VHDL”, Morgan Kaufmann Publishing,
San Francisco, 1996.
[2] IEEE standard VHDL Language Reference Manual. IEEE std.1076-1993.
The Institute of Electrical and Electronic Engineers, Inc., 1994
[3] FPGA Compiler II / FPGA Express VHDL Reference Manual, Version 1999.05
Analiza leksykalna i syntaktyczna języka VHDL
używanego do generacji równań boolowskich
Robert Drążkowski
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Niniejszy artykuł opisuje analizatory: leksykalny oraz syntaktyczny,
będące częścią składową kompilatora języka VHDL, którego zadaniem
będzie wygenerowanie równań boolowskich, będących podstawą syntezy
układów cyfrowych. W artykule omówiono gramatykę tej wersji języka
VHDL. Na podstawie tej gramatyki w programie źródłowym
wyszukiwane są ciągi znaków, będące poprawnymi symbolami
leksykalnymi języka VHDL (tzn. słowa kluczowe, identyfikatory
zmiennych, itp.). Takie ciągi znaków zamieniane są następnie na
odpowiadające im unikalne kody (liczby całkowite). Po tym
przekształceniu plik źródłowy reprezentowany jest przez ciąg kodów,
który sprawdzany jest pod względem poprawności syntaktycznej, czyli
sprawdzane jest, czy następstwo kodów jest zgodne z regułami podanej
gramatyki. Uzyskany ciąg znaków jest podstawą do tworzenia przekładu,
czyli generowania równań boolowskich dla wybranych konstrukcji
języka VHDL.
Słowa kluczowe:
składnia VHDL, kompilator VHDL, synteza układów cyfrowych
1.
WSTĘP
Język VHDL jest popularnym standardem opisu układów cyfrowych.
Pozwala na definiowanie układów scalonych (np. procesorów)
i modelowanie ich zachowania. Jego bardzo rozbudowane konstrukcje
pozwalają w prosty sposób opisywać bardzo złożone funkcje układów
cyfrowych. Jednak próby syntezy układów cyfrowych na podstawie źródeł
w języku VHDL trafiają na duże przeszkody, ponieważ bardzo
14
wyrafinowane konstrukcje językowe wymagają bardzo złożonego opisu przy
pomocy bramek logicznych.
W celu umożliwienia syntezy układów logicznych należy wyodrębnić
pewien podzbiór konstrukcji języka VHDL, wystarczający do opisu
układów, które znajdują się w obszarze zainteresowań przemysłu
elektronicznego, nie powodujący jednak nadmiernej rozbudowy logiki tych
układów. Istotne jest także zachowanie zgodności z przyzwyczajeniami
projektantów, posługujących się językiem VHDL.
Na Wydziale Informatyki Politechniki Szczecińskiej realizowany jest
kompilator języka VHDL generujący na podstawie źródła VHDL zbiór
równań boolowskich (tzn. opierających się na algebrze Boole’a w formie
arytmetyki bitowej, z wykorzystaniem bitowych operacji negacji, koniunkcji
i alternatywy). Rezultatem kompilacji nie jest więc program składający się
z poleceń pewnego procesora, przeznaczony do uruchamiania na komputerze
wyposażonym w taki procesor. Rezultatem kompilacji jest zbiór równań
boolowskich definiujących działanie pewnego układu scalonego.
W prezentowanym kompilatorze zostały nałożone pewne ograniczenia na
język VHDL, umożliwiające syntezę układów logicznych, polegające na
tym, że zostały wyróżnione trzy klasy konstrukcji językowych:
konstrukcje wspierane zarówno przez standard VHDL jak i przez
realizowany kompilator;
konstrukcje ignorowane (ignored), czyli poprawne w rozumieniu
standardu VHDL, w przypadku których kompilator nie zgłasza błędu
a jedynie je ignoruje;
konstrukcje niewspierane (unsupported), czyli poprawne w rozumieniu
standardu VHDL, w przypadku których kompilator zgłasza błąd kompilacji.
Przyczyny zaliczenia poszczególnych konstrukcji do każdej z klas nie są
istotne z punktu widzenia kompilatora. Warto dodać, że w miarę rozwoju
kompilatora jako produktu, wiele konstrukcji językowych zmieniało swoją
przynależność do wymienionych klas.
W referacie omówione są dwa analizatory wchodzące w skład
kompilatora: analizator leksykalny, rozpoznający symbole leksykalne oraz
analizator syntaktyczny, sprawdzający poprawność składniową, czyli
zgodność kolejności występowania symboli leksykalnych w programie z
gramatyką.
Kolejnym ważnym elementem kompilatora jest analizator semantyczny:
mając na wejściu kod programu poprawny pod względem składniowym,
ustalane są typy (i inne właściwości semantyczne) wszystkich
identyfikatorów, po czym sprawdzane jest, czy operacje wpisane w kod
źródłowy mogą być wykonane na identyfikatorach o takich właściwościach.
Analiza semantyczna wykrywać też powinna wszystkie te ograniczenia
prezentowanej wersji języka VHDL, których nie udało się odnaleźć
15
w trakcie analizy syntaktycznej. Kod, który bezbłędnie przeszedł oba etapy:
analizę syntaktyczną i semantyczną, można uznać za poprawny i
potraktować jako wejście do generowania przekładu, czyli odpowiednich
równań boolowskich.
2.
ANALIZA LEKSYKALNA
Analiza leksykalna znajduje wszystkie symbole leksykalne języka
VHDL, czyli słowa kluczowe (w tym te charakterystyczne dla konstrukcji
ignorowanych i niewspieranych), operatory arytmetyczne, logiczne,
podstawienia itp. jednoznakowe i wieloznakowe, identyfikatory zmiennych,
stałych, typów itd. Każdy symbol leksykalny zamieniany jest na
odpowiadający mu kod (dodatnią liczbę całkowitą), która zapisywana jest do
pliku wynikowego. Dzięki temu program źródłowy zamieniany jest na ciąg
liczb co wydatnie upraszcza konstrukcję automatu generującego przekład.
Słowom kluczowym i operatorom kody są przydzielane w sposób jawny
i są one niezależne od kompilowanego programu. Kody identyfikatorów
ustalane są w trakcie pracy analizatora, gdy natrafi on na pierwsze
wystąpienie danego identyfikatora. Informacja o tym identyfikatorze
umieszczana jest w tablicy identyfikatorów oraz zapisywana w pliku
informacji o zmiennych, dzięki czemu przy następnym wystąpieniu
identyfikatora jego kod będzie już znany. Wystąpienie identyfikatora typu
(zdefiniowanego w analizowanym programie lub w pakiecie czy bibliotece)
jest traktowane tak, jak wystąpienie dowolnego innego identyfikatora, gdyż
interpretacja tego identyfikatora wymagałaby wykorzystania informacji
semantycznej, której brak na tym etapie analizy źródła. W przypadku
stałych, nadawany im kod jest po prostu pierwszym wolnym kodem
identyfikatora, nie jest natomiast sprawdzane poprzednie wystąpienie tej
stałej, zaś w pliku informacji o zmiennych wpisywane jest określenie typu
stałej.
Na etapie analizy leksykalnej trudno o uchwycenie jakichkolwiek
błędów, ponieważ większość nieznanych symboli leksykalnych może zostać
potraktowana jak identyfikatory, zaś kontrola właściwej kolejności symboli
leksykalnych jest domeną analizatora syntaktycznego.
W chwili obecnej tablica identyfikatorów zaimplementowana jest jako
stos, przy tym zapewniono operacje przeszukiwania stosu bez usuwania
analizowanych elementów. Gwarantuje to zazwyczaj szybkie znajdowanie
poszukiwanych identyfikatorów, szczególnie tych ostatnio używanych.
Niemniej ważnym argumentem jest łatwość implementacji. W miarę
rozwoju produktu zostaną przeprowadzone prace badawcze nad
zastosowaniem bardziej wyszukanych struktur. Szczególnie atrakcyjna
16
wydaje się być struktura zrównoważonego drzewa, umożliwiająca szybkie
przeszukiwanie nawet bardzo dużych tablic.
Wynikiem analizy leksykalnej są dwa pliki: w pierwszym znajdują się
kody wszystkich symboli leksykalnych w kolejności ich wystąpienia w pliku
źródłowym VHDL, w drugim pliku znajdują kody wszystkich zmiennych
wraz z ich nazwami oraz informacją na temat stałych i ich typów, pod
warunkiem, że możliwe jest ich ustalenie.
2.1
Implementacja
Analizator leksykalny został zaimplementowany w oparciu o możliwości
oferowane przez program narzędziowy lex (a dokładnie flex, który jest jego
rozszerzeniem rozpowszechnianym bezpłatnie). Na podstawie spisu symboli
leksykalnych została napisana gramatyka rozpoznająca symbole leksykalne,
zapisana w formacie programu lex, uzupełniona rozszerzeniami
umożliwiającymi wykrywanie ponownych wystąpień identyfikatorów
i stałych.
W celu umożliwienia dalszej diagnostyki błędów występujących w
programach w języku VHDL, zostały zaimplementowane dodatkowo
mechanizmy odpowiedzialne za umieszczanie w plikach wynikowych
informacji o numerze wiersza i nazwie pliku źródłowego.
Dodatkowo
dołączono
do
specyfikacji
języka
dyrektywy:
„—aldec_t_off” rozpoczynającą blok komentarza oraz „––aldec_t_on”
kończącą blok komentarza. Wymagane jest, aby obie dyrektywy
umieszczone były na początku wiersza (w pierwszej kolumnie) odpowiednio
przed i na końcu komentowanego bloku.
2.2
Spis rozpoznawanych symboli leksykalnych
Kody symboli leksykalnych zarezerwowanych słów języka VHDL
przyjmują wartości od 1 do 97:
1
2
3
4
5
6
7
8
9
10
abs
access
after
alias
all
and
architecture
array
assert
attribute
34
35
36
37
38
39
40
41
42
43
if
impure
in
inertial
inout
is
label
library
linkage
literal
67
68
69
70
71
72
73
74
75
76
register
reject
rem
report
return
rol
ror
select
severity
signal
17
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
begin
block
body
buffer
bus
case
component
configuration
constant
disconnect
downto
else
elsif
end
entity
exit
file
for
function
generate
generic
group
guarded
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
loop
map
mod
nand
new
next
nor
not
null
of
on
open
or
others
out
package
port
postponed
procedure
process
pure
range
record
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
shared
sla
sll
sra
srl
subtype
then
to
transport
type
unaffected
units
until
use
variable
wait
when
while
with
xnor
xor
Kody symboli leksykalnych znaków specjalnych języka VHDL
przyjmują wartości od 101 do 165:
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"
#
&
'
(
)
*
+
,
.
/
:
;
<
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
?
@
[
\
]
^
`
{
}
~
¡
¢
£
¤
¥
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
®
o
±
2
3
'
µ
¶
•
1
°
»
18
116
117
118
119
120
121
122
=
>
_
|
!
$
%
138
139
140
141
142
143
144
¦
§
©
«
¬
160
161
162
163
164
165
¼
½
¾
¿
×
÷
Kody symboli leksykalnych zarezerwowanych symboli dwuznakowych
języka VHDL przyjmują wartości od 166 do 172:
166
167
168
169
170
171
172
=>
**
:=
/=
>=
<=
<>
Kody symboli leksykalnych od 201 w górę zarezerwowane są dla: nazw
zmiennych, stałych, nazw funkcji itp.; czyli wszystkich słów poprawnych w
rozumieniu języka VHDL, ale nieznanych na etapie analizy leksykalnej.
Numery przyznawane są wg kolejności wystąpienia; każdy symbol po
wprowadzeniu go do bazy symboli staje się znanym symbolem leksykalnym
i jest wyszukiwany w programie źródłowym.
2.3
Formaty plików
Plik wejściowy jest zwykłym plikiem tekstowym, zawierającym program
w języku VHDL. Na wyjściu tworzone są co najmniej trzy pliki:
1. właściwy plik z wynikami analizy leksykalnej, zawierający wszystkie
słowa kluczowe itp., o nazwie takiej samej jak plik wejściowy, z
rozszerzeniem lex; dla każdego pliku wejściowego tworzony jest jeden
plik z leksemami;
2. plik z listą identyfikatorów, wspólny dla wszystkich kompilowanych
plików źródłowych;
3. plik z listą stałych, wspólny dla wszystkich kompilowanych plików
źródłowych.
19
2.3.1
Plik z wynikami analizy leksykalnej
Plik z leksemami jest plikiem tekstowym, w każdym wierszu znajduje się
jeden leksem oraz dodatkowa informacja wykorzystywana na etapie analizy
syntaktycznej. Jego format jest następujący:
kod rodzaj_leksemu: ciąg_znaków
gdzie: „kod” jest liczbą całkowitą charakterystyczną dla danego ciągu
znaków (leksemu), „rodzaj_leksemu” jest ciągiem znaków reprezentującym
rodzaj
leksemu,
przyjmuje
następujące
wartości:
keyword,
special_character, double_char_symbol, identifier, integer_constant,
integer_based_constant,
character_constant,
string_constant,
bit_string_constant_bin, bit_string_constant_oct, bit_string_constant_hex,
odpowiednio dla każdego z rozpoznanych literałów, na końcu zawsze
dopisany jest znak „:”; „ciąg_znaków” reprezentuje ciąg znaków
składających się na rozpoznany literał.
Oprócz tego w pliku tym umieszczane są wiersze w formacie:
#line numer_wiersza
nazwa_pliku
gdzie „numer_wiersza” jest liczbą całkowitą określającą numer wiersza w
pliku źródłowym, zaś „nazwa_pliku” jest nazwą pliku źródłowego.
Informacja ta jest użyteczna dla diagnostyki.
2.3.2
Plik z listą identyfikatorów
Plik z listą identyfikatorów jest plikiem tekstowym, w każdym wierszu
znajduje się znaleziony nowy identyfikator. Jego format jest następujący:
kod literał
gdzie „kod” jest liczbą całkowitą charakterystyczną dla identyfikatora (jego
kodem), a „literał” jest ciągiem znaków identyfikatora.
2.3.3
Plik z listą stałych
Plik z listą stałych jest plikiem tekstowym, w każdym wierszu znajduje
się opis jednego literału stałej. Format i interpretacja pól tego pliku jest
ściśle związana z analizą semantyczną i jako taki nie jest przedmiotem
niniejszego opisu.
2.4
Przykład
Działanie analizatora
następującym przykładzie:
leksykalnego
entity test is
port( a: in bit; b: out bit);
można
zaprezentować
na
20
end test;
--Comment
architecture small of test is
begin
b<='0' and a;
end;
W wyniku analizy leksykalnej powstaje plik postaci:
25 keyword: entity
228 identifier: test
39 keyword: is
#line 1 EXAMPLE.vhd
60 keyword: port
105 special_character: (
229 identifier: a
113 special_character: :
36 keyword: in
202 identifier: bit
114 special_character: ;
230 identifier: b
113 special_character: :
58 keyword: out
202 identifier: bit
106 special_character: )
114 special_character: ;
#line 2 EXAMPLE.vhd
24 keyword: end
228 identifier: test
114 special_character: ;
#line 3 EXAMPLE.vhd
#line 4 EXAMPLE.vhd
#line 5 EXAMPLE.vhd
7 keyword: architecture
231 identifier: small
53 keyword: of
228 identifier: test
39 keyword: is
#line 6 EXAMPLE.vhd
11 keyword: begin
#line 7 EXAMPLE.vhd
230 identifier: b
171 double_char_symbol: <=
232 character_constant: '0'
21
6 keyword: and
229 identifier: a
114 special_character:
#line 8 EXAMPLE.vhd
24 keyword: end
114 special_character:
;
;
Powyższy plik uzupełnia spis identyfikatorów zawarty w osobnym pliku,
którego format można przedstawić jak na przykładzie:
1 integer
2 bit
3 bit_vector
4 boolean
5 character
6 string
7 natural
8 positive
9 std_ulogic
10 std_logic
11 std_ulogic_vector
12 std_logic_vector
13 'base
14 'left
15 'right
16 'high
17 'low
18 'range
19 'reverse_range
20 'length
21 'stable
22 'event
23 'unstable
24 false
25 true
26 rising_edge
27 falling_edge
28 test
29 a
30 b
31 small
32 '0'
22
W osobnym pliku zapisywane są wszystkie literały stałych napotkanych
na etapie analizy leksykalnej; w powyższym przykładzie plik ów wygląda
następująco:
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
232
3.
'base
0 5 0 0
'left
0 5 0 0
'right
0 5 0
'high
0 5 0 0
'low
0
'range
0
'reverse_range 0
'length
0 5
'stable
0 5
'event
0
'unstable
0 5
false
204 5
true
204
rising_edge
204
falling_edge 204
'0'
205 5
0
0
0
0
5
5
5
0
0
5
0
0
5
5
5
0
-1
-1
0 -1
-1
0 0 0 -1
0 0 0 -1
0 0 0 -1
0 0 -1
0 0 -1
0 0 0 -1
0 0 -1
1 0 0
0 1 0 0
0 0 0 -1
0 0 0 -1
3 0 2
0
1
'0'
ANALIZA SYNTAKTYCZNA
Analiza składniowa stanowi istotny etap kompilacji, sprawdzając czy
symbole leksykalne występują w źródle VHDL w kolejności zgodnej z
gramatyką języka, z uwzględnieniem ograniczeń związanych z syntezą
układów cyfrowych.
Nie wszystkie ograniczenia języka VHDL mogą zostać wykryte na etapie
analizy syntaktycznej, np. niewspierane użycie zmiennych typu fizycznego
wymaga informacji o atrybutach zmiennych (tutaj: o tym, że zmienna jest
typu fizycznego), które na etapie analizy syntaktycznej nie są jeszcze znane,
zaś sama deklaracja zmiennych typu fizycznego jest jedynie ignorowana.
Rozdział ten poświęcony będzie głównie określeniu, które z ograniczeń
można wykryć już w trakcie analizy syntaktycznej, a które dopiero w trakcie
analizy semantycznej.
Wygodnym sposobem określania konstrukcji języka ignorowanych lub
niewspieranych przez kompilator jest wskazanie kontekstu składniowego,
w którym one występują oraz wskazanie słowa kluczowe stanowiące
początek oraz koniec tych konstrukcji. (Tak dokładne określenie fragmentu
kodu pozwala go uchwycić już w trakcie analizy syntaktycznej.) Choć nie
23
zawsze jest to możliwe, w miarę możności przestrzegany będzie taki właśnie
sposób określania konstrukcji ignorowanych i niewspieranych.
W dalszej części rozdziału używany będzie zwrot, iż analizator
syntaktyczny „wykrywa” jakąś ignorowaną lub niewspieraną konstrukcję tej
wersji języka VHDL. Należy to rozumieć w ten sposób, że:
– konstrukcja ignorowana jest w pełni poprawna składniowo (ze
wszystkimi elementami zgodności z pełnym standardem VHDL) i fakt jej
zignorowania ma swoje następstwa dopiero po analizie syntaktycznej
(np. w trakcie analizy semantycznej);
– konstrukcja niewspierana nie jest w ogóle obecna w gramatyce tej wersji
języka VHDL, czyli analizator syntaktyczny zgłosi fakt „nieoczekiwanego
wystąpienia symbolu leksykalnego” lub wyświetli komunikat, że
„oczekiwany jest inny symbol leksykalny” niż ten, który wystąpił.
Nie wydaje się natomiast celowe na tym etapie rozwoju tego kompilatora
informowanie w czasie kompilacji, że „wystąpił element ignorowany” lub
„niewspierany”.
3.1
Konstrukcje ignorowane przez kompilator
Konstrukcją ignorowaną przez kompilator jest tzw. entity statement part,
występująca w deklaracji jednostki (entity) po jej części deklaratywnej,
rozpoczynająca się słowem begin, kończąca słowem kluczowym end.
W deklaracji jednostki ignorowane są także występujące w jej nagłówku
(entity header) wartości domyślne dla portów (rozpoczynające się
dwuznakowym symbolem kluczowym „:=”).
We wszystkich deklaracjach ignorowane są przez kompilator deklaracje
rozpoczynające się od słowa kluczowego alias. Dodatkowo w deklaracjach
atrybutów (oraz w ich specyfikacjach) ignorowane są atrybuty zdefiniowane
przez użytkownika (dopuszczalne są jedynie standardowe atrybuty).
Wśród konstrukcji ignorowanych dużą grupę stanowią definicje i
deklaracje typów oraz zmiennych:
– fizycznych (physical types) zawierających słowo kluczowe unit;
– zmiennoprzecinkowych (floating-point types) określone granicami w postaci
liczb zmiennoprzecinkowych lub predefiniowanym typem REAL;
– plikowych (file types) zawierające słowo kluczowe file oraz
– dostępowych (access types) zawierające słowo kluczowe access.
Fakt, że są one ignorowane na etapie deklaracji, oznacza, iż zarówno
deklaracje jak i ich użycie są syntaktycznie poprawne, ale odpowiednie
identyfikatory reprezentujące te typy w programach VHDL będą przez
kompilator traktowane dwojako:
– jako identyfikatory niezdefiniowane lub niezadeklarowane (ponieważ ich
definicje lub deklaracje zostały zignorowane);
24
– jako identyfikatory, które nie mogą być użyte w bieżącym kontekście,
ponieważ ich właściwości na to nie pozwalają (np. niezgodność typu danych).
Obie sytuacje powodują wystąpienie błędu semantycznego; w związku z
ignorowaniem typów fizycznych należy uwzględnić konieczność
ignorowania literałów zawierających nazwy jednostek (units), wchodzących
w ich skład.
Wśród wyrażeń (statements) ignorowane są wyrażenia rozpoczynające
się słowem kluczowym assert (zarówno sekwencyjne jak i współbieżne)
oraz report. Ignorowane są także słowa kluczowe guard i transport, w
szczególności mechanizmy opóźnienia w wyrażeniach podstawienia
sygnałów (signal assignment statement) rozpoczynające się od słowa
kluczowego transport.
Wszystkie wymienione dotąd konstrukcje mają dobrze określone miejsca
w składni VHDL, stąd uwzględnienie faktu, że są ignorowane nie nastręcza
specjalnych trudności.
3.2
Konstrukcje niewspierane przez kompilator
Istotną cechą konstrukcji niewspieranych przez kompilator jest fakt, iż
uniemożliwiałyby one dokonanie przekładu. Stąd ich wskazanie jest
istotniejsze z punktu widzenia przekładu. Wykrycie konstrukcji
niewspieranych może jednak wymagać informacji semantycznej, o typie,
własnościach semantycznych, czy choćby o zasięgu danej zmiennej. Główny
nacisk położony więc zostanie na te konstrukcje, które mogą zostać wykryte
w czasie analizy syntaktycznej.
Na etapie analizy syntaktycznej można niewątpliwie wykryć użycie
specyfikacji atrybutów (attribute specification) zdefiniowanych przez
użytkownika: ich nazwy nie będą elementami zbioru atrybutów
dopuszczalnych. Niewspierane i wykrywalne przez analizator syntaktyczny
są wyrażenia wartości domyślnych dla parametrów podprogramów
i procedur, niepełnych deklaracji typów (incomplete type declaration),
odroczonych deklaracji stałych. Niewspierane i łatwo wykrywalne są także
sytuacje wskazywania wartości początkowych zmiennych i sygnałów,
użycia atrybutów zdefiniowanych przez użytkownika, użycia słów
kluczowych others i all w specyfikacjach atrybutów, wystąpienia
specyfikacji konfiguracji (configuration specification) i rozłączenia
(disconnection specification), stosowania słowa kluczowego groups.
Nie potrzeba dostępu do informacji semantycznej w przypadku ograniczeń
związanych z użyciem stałych, ponieważ analizator leksykalny rozpoznaje
ogólny typ stałej (tzn. znakowy, całkowity, tekstowy itp.), w związku z czym na
etapie analizy syntaktycznej możliwe jest wykrycie niewspieranej instrukcji
dzielenia. Niezwykle złożone jest stwierdzenie, że prawy argument przesunięcia
25
jest obliczalny (stwierdzenie tego faktu przekracza nawet możliwości
podstawowej analizy semantycznej, wymaga bowiem przeprowadzenia redukcji
wyrażeń stałych, czyli optymalizacji kodu źródłowego).
Wszystkie konstrukcje związane z użyciem ignorowanych lub
niewspieranych typów (deklaracje zmiennych takich typów a potem operacje
na takich zmiennych) są możliwe do wykrycia jedynie w trakcie analizy
semantycznej, gdy właściwości semantyczne zmiennych są już określone.
Warto dodać, że składnia jezyka VHDL wykazuje dalekie pokrewieństwa
ze składnią języka ADA, dziedzicząc z niej szereg niedogodności. Można tu
wymienić np. brak jednoznacznych reguł zakończenia niektórych
konstrukcji składniowych, np. poprawnymi zakończeniami dla konstrucji
zaczynającej się od „architecture nazwa_architektury” są zarówno „end;”
jak i „end architecture;” jak i „end architecture nazwa_architektury;”
czy „end nazwa_architektury;”. Znakomicie utrudnia to stworzenie
jednoznacznej gramatyki opisującej ciało architektury.
4.
PODSUMOWANIE
Niniejszy referat stanowi opis pewnego stanu prac nad kompilatorem.
Oprócz wspomnianych badań nad implementacją tablicy identyfikatorów
jako drzewa zrównoważonego, rozważana jest też możliwość połączenia obu
analizatorów w jeden program.
Omówiona wyżej gramatyka jest zarówno podstawą analizy
syntaktycznej, jak i generowania przekładu, który to etap kompilacji jest
sterowany składnią języka (następstwo określonych symboli w kodzie
źródłowym implikuje określone akcje ze strony generatora kodu). Wartości
semantyczne symboli stanowią jedynie niezbędne uzupełnienie danych do
poprawnego generowania kodu.
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994
[2] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
[3] Synopsys: FPGA Express VHDL Reference Manual, December 1997
[4] W. Bielecki, S. Hyduke: Kompilator języka VHDL do syntezy układów logicznych,
Materiały II krajowej konferencji naukowej RUC’99, Szczecin 14-16 marca 1999
[5] R. Drążkowski: Analiza leksykalna i syntaktyczna podzbioru języka VHDL używanego
do generacji wyrażeń boolowskich, Materiały II krajowej konferencji naukowej
RUC’99, Szczecin 14-16 marca 1999
Analizator semantyczny kompilatora języka VHDL
do generacji równań boolowskich
Piotr Błaszyński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Przedstawiono opis techniczny analizatora semantycznego wchodzącego
w skład kompilatora języka VHDL. Opisano szczegółowo wszystkie
informacje generowane przez ten analizator w czasie analizy plików
źródłowych w języku VHDL. Wyszczególniono również dane wejściowe
i wyjściowe, na których operuje analizator semantyczny. Opisany został
również sposób implementacji wykorzystania pakietów standardowych
oraz sposób sygnalizacji błędów w trakcie pracy analizatora.
Słowa kluczowe:
język VHDL, kompilatory, analiza semantyczna, równania boolowskie
1.
ZADANIA ANALIZATORA SEMANTYCZNEGO
Głównym zadaniem omawianego analizatora semantycznego jest
przygotowanie informacji semantycznej dotyczącej kompilowanego
programu w języku VHDL [5]. Informacja ta może posłużyć jako podstawa
do generacji równań boolowskich, ale również (po stworzeniu
odpowiedniego generatora) do tworzenia innych form wyjściowych.
Analizator semantyczny ma również za zadanie sprawdzić poprawność
semantyczną przekładanego kodu źródłowego. Ze względu na specyficzne
zasady programowania w języku VHDL, część tej pracy musi być jednak
wykonana w module odpowiedzialnym za generacje kodu. Analizator
semantyczny ma także za zadanie połączenie wszystkich kompilowanych
plików źródłowych na podstawie analizy konstrukcji use[6].
28
2.
ORGANIZACJA KOMPILATORA
Analizator semantyczny wchodzi w skład kompilatora języka VHDL.
Kompilator ten ma modularną budowę. Analizator semantyczny jest trzecim
wykonywanym (po analizatorze leksykalnym i syntaktycznym) modułem.
Jest też ostatnim modułem, który nie determinuje formy wyjściowej
generowanej przez kompilator. Modułem z którym analizator semantyczny
jest najściślej związany jest generator równań boolowskich (w tworzonym
aktualnie kompilatorze) [5]. Korzysta on z drzewa plików i katalogów
tworzonego na etapie analizy semantycznej. Działanie tych dwóch modułów
nachodzi na siebie a stworzenie poprawnie działającego generatora kodu nie
byłoby możliwe bez poprawnie działającego analizatora semantycznego,
oraz bez kompletnej wiedzy na temat sposobu jego wykorzystywania.
3.
STRUKTURA PROGRAMU
Program wykonuje się w dwóch fazach: w fazie pierwszej w źródłach
VHDL wyszukiwane są wszystkie pakiety i miejsca, w których te pakiety
dołączane są przy pomocy konstrukcji use [3]. Kompilacja poszczególnych
plików odbywa się od tego miejsca już w kolejności ustalonej przez
analizator semantyczny, na podstawie analizy miejsc dołączania pakietów.
W związku z tym programista zostaje zwolniony z obowiązku dbania o
odpowiednią kolejność dołączania pakietów, zarówno użytkownika jak i
systemowych.
W drugiej fazie kompilacji analizator tworzy drzewo plików i katalogów
zawierające informacje semantyczną na podstawie analizy kodu źródłowego
programu. Odbywa się tutaj także łączenie jednostki (entity) z
odpowiadającą jej architekturą na podstawie kolejności występowania
(dopasowywana jest ostatnia występująca architektura) lub na podstawie
konstrukcji configuration (jeśli konstrukcja ta występuje to jest
rozpoznawana w 1 kolejności).
4.
WEWNĘTRZNA STRUKTURA DANYCH
Dane wewnątrz programu są przechowywane zarówno w pamięci jak i w
postaci plików. Większość danych przechowywanych w pamięci jest
również zapisywana do plików w końcowej fazie działania analizatora.
29
4.1
Dane wejściowe
Danymi wejściowymi dla analizatora semantycznego są pliki (plik) z
leksemami będące wynikiem analizy leksykalnej plików w języku VHDL.
W plikach tych, poza samymi leksemami znajdują się także dodatkowe
informacje dotyczące numerów linii. Pozwala to późniejszym modułom
kompilatora na sygnalizację numeru linii, w której wystąpił błąd. Pliki z
leksemami są także sprawdzane przez analizator syntaktyczny co pozwala na
założenie ich poprawności składniowej w analizatorze semantycznym, co z
kolei umożliwia przyjęcie pewnych uproszczeń i nie sprawdzanie
wszystkich warunków w czasie pracy analizatora semantycznego.
Dodatkowym plikiem wejściowym dla analizatora jest także plik z opisem
projektu, zawierający kolejność kompilacji pakietów
4.2
Dane wyjściowe
W czasie pracy analizator semantyczny generuje następujące dane
wyjściowe:
– plik z opisem projektu design.dgn, który zawiera informacje o tym, jakie
pliki należą do kompilowanego projektu (plik ten jest tworzony
wcześniej, analizator jedynie sortuje nazwy plików w odpowiedniej
kolejności, w zależności od analizy konstrukcji use),
– plik index.idx, który zawiera ścieżki dostępu do poszczególnych części
kompilowanego projektu, oraz oznaczenia jakiego typu jest dany element
(czy jest to funkcja, architektura, jednostka itp.),
– plik topent.idx, który zawiera opis portów wszystkich jednostek
najwyższego poziomu, opis ten zawiera nazwę portu, jego szerokość,
oraz typ portu (wejściowy, wyjściowy, itp.),
– plik (pliki) *.res, gdzie gwiazdka oznacza wszystkie jednostki
najwyższego poziomu, pliki te zawierają identyfikatory sygnałów
będących typu resolved,
– plik entity.adj zawierający pary: jednostka-architektura, dopasowane w
czasie kompilacji, pary te pozwalają na generacje równań tylko dla
faktycznych implementacji (architektur) jednostek najwyższego poziomu
(dla architektury nie będącej parą dla jednostki najwyższego poziomu nie
są później generowane równania),
– w przypadku tworzenia nowych wartości semantycznych (np. typy
złożone) analizator modyfikuje plik alu.var, dopisując nowo utworzone
leksemy na końcu tego pliku,
– katalogi: toplevel i (w przypadku utworzenia pakietu użytkownika)
library,
30
– w katalogu toplevel są tworzone pliki zawierające wartości semantyczne
dla jednostek (entities) oraz katalogi, w których będą umieszczone ciała
i wartości semantyczne odpowiadających im architektur,
– w katalogach architektur umieszczane są wartości semantyczne i ciała
konstrukcji, które są zdefiniowane w części deklaracyjnej architektury
oraz w jej ciele,
– analogicznie dla wszystkich konstrukcji typu proces, funkcja czy procedura
w odpowiednich katalogach umieszczane są wartości semantyczne i ciała
konstrukcji zdefiniowanych w ich częściach deklaracyjnych,
– w katalogu library są umieszczane w podany wyżej sposób ciała i
wartości semantyczne odpowiadające pakietom użytkownika.
– w plikach bez żadnego rozszerzenia przechowywane są źródła
poszczególnych konstrukcji występujących w programie (architektura,
blok, proces, itd.),
– wartości semantyczne dla typów i podtypów (subtype) są
przechowywane w plikach z rozszerzeniem .tdf, strukturę tych plików
przedstawia poniższa tabela, wartości ograniczające zaczerpnięto ze
standadu języka [1]:
Lp.
1
Znaczenie
2
Nr typu
3
Nazwa
Typ w C:
Int
char *
Dopuszcz
0-n
„.*„
RootType
alne.wart
5
6
7
8
ValidBits
Start
End
Elem
SubType
Type
Type
char *
Int
int
int
int
Int
INT, BVECT, BIT,
1-2^32
-n-n
-n-n
0-n
0-n
14
1
409
0
201
BOOL, ARRAY,
osci:
Przykład:
4
STRING, CHAR
729
Spositiv
INT
7
Tak samo w pliku .tdf będą traktowane subtypy, jak i typy, rozróżniane
będą jedynie po ostatnich dwóch polach.
– wartości semantyczne dla sygnałów, stałych, zmiennych, funkcji itp. są
przechowywane w plikach z rozszerzeniem .val, strukturę tych plików
przedstawia poniższa tabela:
1
2
3
4
5
6
7
Znaczenie
Lp.
Nr lexemu
Nazwa
Type
State
InOut
ValidBits
Start
End
8
Value
Typ w C:
Int
char *
Int
int
int
int
int
int
Char *
Dopuszczalne.
200-n
„.*„
0-n
0-30
0-3
1-??
-n - n
-n - n
„.*”
722
z_b
203
1
2
9
8
0
wartości:
Przykład:
9
31
– dodatkowo, w przypadku wystąpienia w programie w VHDLu
konstrukcji niemożliwych do obliczenia w czasie kompilacji (np. stała
zależna od parametrów aktualnych wywołania funkcji), tworzone są pliki
z rozszerzeniami .tnc i .ncp, pliki te przechowują (podobnie jak w plikach
.val i .tdf) sposób obliczenia wartości semantycznych w czasie generacji
równań, w plikach *.tnc znajdują się wartości semantyczne i wskazanie
na plik .ncp, natomiast w plikach .ncp są przechowywane ciągi leksemów
pozwalających na obliczenie prawidłowej wartości semantycznej.
5.
KOMPILACJA PRZY UŻYCIU PAKIETÓW
SYSTEMOWYCH
Wraz z kompilatorem dołączany jest katalog syslib, który zawiera
prekompilowane pakiety systemowe. W tej chwili, w skład kompilatora
wchodzą pakiety std_logic_arith, std_logic_signed, std_logic_unsigned.
Natomiast pakiet IEEE std_logic_1164 jest w dużej części pakietem
wbudowanym w kompilator. Pakiety te mogą zostać dołączone do projektu
za
pomocą
instrukcji
use [2],
Przykładowo
instrukcja:
use
IEEE.std_logic_arith.all umieszczona w programie umożliwia korzystanie
ze wszystkich funkcji i typów (ewentualnie stałych itp.) zadeklarowanych w
pakiecie std_logic_arith. Katalog syslib swoją strukturą przypomina drzewo,
w jego korzeniu znajdują się informacje konfiguracyjne (nazwy pakietów i
ścieżki dostępu do nich), natomiast w poszczególnych podkatalogach
znajdują się biblioteki systemowe. Przykładowo w katalogu IEEE znajdują
się katalogi std_logic_arith, std_logic_signed, std_logic_unsigned
odpowiadające poszczególnym pakietom. Wewnątrz tych katalogów
znajdują się wartości semantyczne dotyczące funkcji, procedur, stałych,
typów itp. zdefiniowanych w danym pakiecie.
6.
PRZYKŁADY DZIAŁANIA ANALIZATORA
SEMANTYCZNEGO
Dla następującego przykładu w języku VHDL:
Entity test is
Port (
a: in BIT_vector(0 to 7);
z: out BIT_vector(0 to 7)
);
end test;
32
architecture arch of test is
begin
process (a) is
variable tt: bit_vector(0 to 7);
function Tr_2(Val: bit_vector(0 to 7))
return bit_vector is
variable tt: bit_vector(0 to 7);
begin
tt:="11110011";
aaa: return (val and tt);
end Tr_2;
Begin – ciało procesu
tt:="11110000";
z<=tr_2(a);
End process;
end arch;
Analizator semantyczny wygeneruje następujące pliki:
topent.idx:
entity test
input [0:7] a
output [0:7] z
entity.adj:
1:test 2:arch
index.idx:
1
2
3
4
6
7
2
3
test
toplevel\test
arch
toplevel\test_1\arch
Tr_2 toplevel\test_1\arch_2\__prid__0_3\Tr_2
__prid__0 toplevel\test_1\arch_2\__prid__0
Oraz odpowiednie drzewo katalogów i plików:
+work
|
| design.dgn
| entity.adj
| index.idx
| topent.idx
|
+ toplevel
|
| test
| test.tdf
| test.val
33
|
+ test_1
|
| arch
| arch.val
|
+ arch_2
|
| __prid__0
| __prid__0.val
|
+ __prid__0_3
|
| tr_2
| tr_2.val
Natomiast dla przykładu:
entity test is
port (
z: out bit_vector(0 to 6)
);
end test;
architecture arch1 of test is
begin
z<=('1','0',others => '1') ;
end arch1;
architecture arch2 of test is
begin
z<=('0','1',others => '0') ;
end arch2;
configuration tst of test is
for arch1
end for;
end tst;
Znajdują się tutaj 2 architektury, każda z nich w nieco odmienny sposób
implementuje zawartość jednostki test. W przypadku gdy podana jest jawna
instrukcja konfiguracji, do jednostki przypisywana jest architektura arch1, i
tylko dla niej są generowane równania. Natomiast gdyby konstrukcja ta nie
34
wystąpiła, do jednostki przyporządkowana zostałaby architektura arch2,
czyli ostatnia z pasujących.
Plik index.idx przedstawia się następująco:
1 6 test
toplevel\test
2 7 arch1 toplevel\test_1\arch1
3 7 arch2 toplevel\test_1\arch2
topent.idx:
entity test
output [0:6] z
entity.adj:
1:test 2:arch1
Odpowiednie drzewo katalogów i plików:
+work
|
| design.dgn
| entity.adj
| index.idx
| topent.idx
|
+ toplevel
|
| test
| test.tdf
| test.val
|
+ test_1
|
|arch1
|arch1.val
|arch2
|arch2.val
7.
BŁĘDY GENEROWANE PRZEZ
ANALIZATOR SEMANTYCZNY
W czasie analizy semantycznej kodu zawierającego błędy semantyczne,
analizator może zgłosić następujące błędy semantyczne:
– niezgodność nazwy na początku i na końcu pewnej konstrukcji
(jednostki, architektury, funkcji itp.),
35
–
–
–
–
–
–
niezgodność zakresu typu tablicowego ze specyfikacją języka,
użycie nieistniejącej wartości semantycznej,
występowanie konstrukcji na niewłaściwym poziomie,
użycie niedozwolonego typu generic’a,
brak pakietu podanego w use,
wielokrotne występowanie jednostki o tej samej nazwie.
Zgłaszane są także ostrzeżenia dotyczące braku jednostek najwyższego
poziomu, oraz braku jednostki dla istniejącej architektury.
W przypadku wystąpienia błędów, oprócz wypisania ich komunikatów
na ekran, zapisywane są one również do pliku errors.log.
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994
[2] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
[3] W. Wrona: VHDL język opisu sprzętu i projektowania układów cyfrowych,
Gliwice 1998
[4] K. Skahill: Język VHDL Projektowanie programowalnych układów logicznych, WNT
Warszawa 2001
[5] W. Bielecki, S. Hyduke: Kompilator języka VHDL do syntezy układów logicznych,
Materiały II krajowej konferencji naukowej RUC’99 Szczecin 14-16 marca 1999
[6] Piotr Błaszyński, Robert Drążkowski: Organizacja analizatora semantycznego
kompilatora języka VHDL do syntezy układów logicznych, Materiały III krajowej
konferencji naukowej RUC’2000 Szczecin 10-11 kwietnia 2000
Zasady nazewnictwa i modyfikacji nazw
w analizatorze semantycznym języka VHDL
Piotr Błaszyński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Przedstawiono zasady dotyczące modyfikacji nazw w źródłach
programów w języku VHDL w trakcie analizy semantycznej. Zostały
również zaprezentowane zasady nazewnictwa przyjęte w języku VHDL
i konsekwencje przyjęcia takiej konwencji. W formie uproszczonej
zostały również zaprezentowane algorytmy modyfikacji nazw przez
analizator semantyczny oraz sposób wykorzystywania zmodyfikowanych
nazw w późniejszym etapie generacji równań boolowskich. Opisane
podejście zostało zaimplementowane w kompilatorze języka VHDL do
generacji równań boolowskich.
Słowa kluczowe:
język VHDL,
boolowskie
1.
kompilatory,
nazewnictwo
zmiennych,
równania
WPROWADZENIE
W ostatnich latach proces implementacji algorytmu w postaci programu
uległ znacznemu uproszczeniu. Dzięki językom opisu sprzętu (ang.
Hardware Description Language, HDL) uproszczone zostało również
projektowanie układów cyfrowych. Języki te, do których zalicza się VHDL
(Very High Speed Integrated Circuit HDL), pozwalają na opis układu
cyfrowego za pomocą programu w języku wysokiego poziomu [2].
W porównaniu do drugiego najbardziej popularnego języka opisu sprzętu,
jakim jest Verilog, język VHDL jest bardziej oddalony od fizycznej
implementacji układu cyfrowego, przez co umożliwia projektantowi-
38
programiście na skupienie się na sposobie działania samego układu. Zysk ten
osiągnięto między innymi dzięki skomplikowaniu procesu kompilacji kodów
źródłowych w tym języku i przyjęciu struktury bardziej zbliżonej do
nowoczesnych języków programowania wysokiego poziomu. Celem tego
artykułu jest przedstawienie zasad nazewnictwa przyjętych w języku VHDL
i skutecznych algorytmów umożliwiających operowanie nazwami
zmiennych w programie według tych zasad.
2.
WYSTĘPOWANIE NAZW I ICH ZASIĘG
W JĘZYKACH PROGRAMOWANIA
W różnych językach programowania ich twórcy wykorzystują bardzo
różnorodne podejścia do zasięgu nazw i znaczenia tych nazw. Przykładowo
w języku C nazwy zadeklarowane w niższych zasięgach przesłaniają
deklaracje znajdujące się w zasięgach wyższych. W przypadku języka C++
zastosowano nowocześniejsze podejście korzystające z przestrzeni nazw
(ang. namespaces) pozwalających na umieszczenie nazwy w określonej
przestrzeni nazw i następnie korzystanie z tej nazwy poprzez nazwę
skwalifikowaną (ang. qualified name, pełna nazwa funkcji poprzedzona
pełnym łańcuchem nazw przestrzeni nazw). Przestrzenie nazw pozwalają na
uniknięcie konfliktów nazw, łatwiejsze panowanie nad kodem przez
programistę i dają możliwość korzystania z danej nazwy wielokrotnie.
Dodatkowym aspektem, który należy wziąć pod uwagę jest wielkość liter w
nazwach zmiennych. Niektóre języki umożliwiają całkowitą dowolność w
tym zakresie, w przypadku np. języków C/C++ rozróżniając dwie nazwy
różniące się tylko wielkością liter, w przypadku zaś np. języków Pascal,
VHDL nie rozróżniając dwóch nazw różniących się wielkością liter. W
innych językach programowania (np. Haskell) sposób zapisu nazwy może
dodatkowo być zdeterminowany przez cel wykorzystania tej nazwy.
3.
WYSTĘPOWANIE NAZW, ICH ZASIĘG
I ZNACZENIE W JĘZYKU VHDL
W języku VHDL nazwy mogą występować na wielu poziomach, co
znacznie komplikuje posługiwanie się tymi nazwami. Nazwy w języku
VHDL mogą oznaczać zarówno zmienne, stałe, aliasy, sygnały jak i porty.
Ta sama nazwa w różnych zasięgach może być przesłonięta przez nazwę z
innych zasięgów. Język VHDL pozwala również na stosowanie nazw
kwalifikowanych odnoszących się do nazwy z konkretnej biblioteki
39
i pakietu[1]. Nie są w tym języku rozróżniane 2 nazwy różniące się tylko
wielkością liter. Interesujące są w tym momencie tylko nazwy będące
oznaczeniami portów, zmiennych, sygnałów i stałych. Zarówno obsługa
nazw kwalifikowanych, jak i obsługa aliasów nie wchodzą w skład
zagadnień poruszanych w tym artykule, aczkolwiek implementacja obsługi
tych aspektów języka wpływa znacząco na algorytm modyfikujący nazwy.
Konieczność modyfikacji nazw okazuje się niepotrzebna w przypadku
narzędzi służących do syntezy języka VHDL[3]. Przykładowo, w
narzędziach tych, nie ma konieczności zmiany powtarzających się nazw
portów przy mapowaniu. Kompilator, w skład którego wchodzi omawiany
analizator semantyczny, generuje jednak równania boolowskie, które mogą
być poddane symulacji. W tym przypadku równania odpowiadające
pojedynczej jednostce najwyższego poziomu (ang. top-level entity) zostaną
umieszczone w jednym pliku wynikowym i nazwy zmiennych boolowskich
wchodzących w skład poszczególnych równań muszą się od siebie różnić.
Jest to sprzeczne z założeniem uniwersalności analizatora semantycznego i
możliwości użycia go również w narzędziach syntezy, istnieje więc
konieczność umożliwienia przywrócenia oryginalnej nazwy z pliku
źródłowego.
4.
WYKORZYSTANIE ANALIZATORA
SEMANTYCZNEGO DO MODYFIKACJI NAZW
W ŹRÓDŁACH JĘZYKA VHDL
Generalnie najwięcej uwagi przy implementacji algorytmu modyfikacji
nazw powinno się poświęcić na poprawną modyfikację nazw portów
jednostek projektowych przy mapowaniach (konkretyzacji komponentu).
Instrukcja mapowania może mieć dwie formy[4, 5]: w pierwszym przypadku
jest to mapowanie przez pozycje a w drugim przez nazwę.
Mapowanie z parametrami ogólnymi (generic), powiązanie parametrów
lokalnych i aktualnych za pomocą nazw:
nagłówek_mapowania:
nazwa komponentu
generic map(
nazwa_ogólna => nazwa_sygnału
| wyrażenie
| nazwa zmiennej
| open
{, nazwa_ogólna => nazwa_sygnału
| wyrażenie
40
| nazwa zmiennej
| open}
)
port map(
nazwa_portu => nazwa_sygnału
| wyrażenie
| nazwa zmiennej
| open
{, nazwa_portu => nazwa_sygnału
| wyrażenie
| nazwa zmiennej
| open}
);
Mapowanie z parametrami ogólnymi (generic), powiązanie parametrów
lokalnych i aktualnych według pozycji na liście:
nagłówek_mapowania:
nazwa komponentu
generic map(nazwa_sygnału | wyrażenie
| nazwa zmiennej | open
{, nazwa_sygnału | wyrażenie
| nazwa zmiennej | open}
)
port map( nazwa_sygnału | wyrażenie
| nazwa zmiennej | open
{, nazwa_sygnału | wyrażenie
| nazwa zmiennej | open}
);
Przed zapoznaniem z omawianymi algorytmami konieczne jest również
przedstawienie opisu jednostki projektowej, architektury i komponentu:
Jednostka projektowa w języku VHDL zawiera deklarację interfejsu
dostępu do tej jednostki projektowej, w skład której wchodzą opis wejścia i
wyjścia projektowanego układu. Deklaracja interfejsu może zawierać
również opis wartości parametrów (generic). Jest to opis sposobu dostępu do
jednostki projektowej przez użytkownika.
entity nazwa_jednostki_projektowej is
generic(
[signal] identyfikator
{, identyfikator}:[tryb] typ_sygnału
[:=wyrażenie_statyczne]
{; [signal] identyfikator
{, identyfikator}:[tryb]
typ_sygnału
[:=wyrażenie_statyczne] }
41
);
port(
[signal] identyfikator
{, identyfikator}:[tryb] typ_sygnału
{; [signal] identyfikator
{, identyfikator}:[tryb] typ_sygnału }
);
end [entity] [nazwa_jednostki_projektowej];
Sposób implementacji układu dostępnego poprzez jednostkę projektową
jest opisany w architekturze. Do jednej jednostki projektowej może być
przypisanych wiele architektur.
architecture nazwa_architektury of
nazwa_jednostki_projektowej is
deklaracja_typu
deklaracja_sygnału
deklaracja_stałej
deklaracja_komponentu
deklaracja_aliasu
deklaracja_atrybutu
deklaracja_podprogramu
begin
instrukcja_procesu
instrukcja_współbieżnego_przypisania
wartości_do_sygnału
instrukcja_konkretyzacji_komponentu
instrukcja_generacji
end [architecture] [nazwa_architektury];
Komponentami są jednostki projektowe, które są używane przez inne
jednostki projektowe. Zanim jednak określona jednostka będzie mogła być
użyta przez inną, należy udostępnić i uczynić widoczną deklaracje
komponentu. Deklaracja komponentu definiuje interfejs służący do
konkretyzacji tego komponentu. Dodatkowo rozmiar i późniejsze ustawienie
komponentu może być zdefiniowany za pomocą parametru lub wartości
ogólnej (generic). Parametryzacji komponentów można dokonać po prostu
przez zastosowanie nieograniczonych tablic dla portów, jednak dzięki
parametrom ogólnym parametryzacja jest jawna.
component nazwa_komponentu
generic (
[signal] identyfikator
{, identyfikator}: [tryb] typ_sygnału
[:=wyrażenie_statyczne]
{; [signal] identyfikator
42
{, identyfikator}:[tryb] typ_sygnału
[:=wyrażenie_statyczne] }
);
port(
[signal] identyfikator
{, identyfikator}:[tryb] typ_sygnału
{; [signal] identyfikator
{, identyfikator}:[tryb] typ_sygnału }
);
end [component] [nazwa_komponentu];
Poniżej przedstawiono algorytm modyfikowania nazw działający dla
obydwu rodzajów mapowań, uwzględniający możliwość występowania
identycznych nazw w architekturze oraz w jednostce mapującej i kilku
jednostkach mapowanych (podrzędnych).
Założenia wejściowe: Funkcja modyfikująca ma dostęp do listy
jednostek, listy architektur oraz listy dotychczas istniejących (w tym też tych
utworzonych przez analizator semantyczny) identyfikatorów.
Funkcja przekładająca mapowanie portu jednostki działa w następujący
sposób:
a) wyszukanie odpowiadającej jednostki mapującej na podstawie nazwy,
wszystkie nazwy jednostek muszą być unikalne;
b) jeśli nie istnieją zmodyfikowane nazwy to przekład mapowania odbywa
się w sposób tradycyjny;
c) wyszukanie odpowiadającej jednostki mapowanej również na podstawie
nazwy;
d) sprawdzenie, czy ta jednostka posiada zmodyfikowane nazwy;
e) zebranie leksemów wchodzących w skład przekładanego mapowania;
f) decyzja o rodzaju mapowania na podstawie zebranych leksemów –
mapowanie przez pozycje (g), mapowanie przez nazwę (h);
g) dla wszystkich leksemów z zebranej listy: sprawdzanie na liście
leksemów zmodyfikowanych, ewentualna zmiana identyfikatora, zapis
do pliku wyjściowego;
h) dla wszystkich leksemów z zebranej listy: sprawdzanie na liście
leksemów zmodyfikowanych, ewentualna zmiana identyfikatora, zapis
do pliku wyjściowego, dla leksemów po prawej stronie mapowania
(utożsamianych z nazwą w jednostce mapowanej) sprawdzenie listy
jednostki mapowanej.
Kolejnym ważnym elementem algorytmu jest funkcja sprawdzająca
unikalność nazw w poszczególnych częściach deklaracyjnych składników
programu w VHDL. Funkcja ta generuje w przypadku powtarzania się
identyfikatorów nowe, unikalne w całym programie, nazwy:
43
a) sprawdzenie czy nazwa znajduje się w komponencie, czy wewnątrz innej
konstrukcji,
b) sprawdzenie, czy wewnątrz przekładanej aktualnie konstrukcji wystąpiły
modyfikacje nazw na poziomie komponentu,
c) pobranie nazwy z listy nazw zmodyfikowanych
d) sprawdzenie, czy nazwa znajduje się na liście zmodyfikowanych, lub
została zmodyfikowana w komponencie,
e) nie są modyfikowane nazwy, które wystąpiły w komponencie o takiej
samej nazwie,
f) nie są modyfikowane identyfikatory, które wystąpiły wcześniej w
komponencie o tej samej nazwie, ale nie wystąpiła jeszcze jednostka o
odpowiadającej mu nazwie
g) w pozostałych przypadkach następuje modyfikacja nazwy przez dodanie
do niej łańcucha znaków „__modif__” oraz unikalnego numeru,
zapamiętywany jest również kontekst w którym nazwa została
zmodyfikowana,
h) jeśli są jeszcze nazwy na liście nazw zmodyfikowanych, przejście do
punktu c.
Ostatnim z miejsc w których algorytm modyfikowania nazw jest
wykorzystywany, jest przekład ciał poszczególnych architektur, bloków,
funkcji, procedur i pakietów. W trakcie przekładu tych konstrukcji
sprawdzana jest lista zmodyfikowanych identyfikatorów w poszukiwaniu
odpowiadającego aktualnie przetwarzanemu leksemowi. Jeśli leksem jest
unikalny, to nie następuje modyfikacja jego nazwy. Poniżej został
przedstawiony algorytm tej modyfikacji:
a) sprawdzenie czy dana nazwa nie znajduje się na liście lokalnych aliasów,
b) pobranie nazwy z listy nazw zmodyfikowanych
c) sprawdzenie czy nazwa znajduje się na liście zmodyfikowanych i czy
kontekst jej wystąpienia jest wewnątrz kontekstu, w którym nazwa
została zmodyfikowana, jeśli tak to następuje odczyt identyfikatora i
nazwy po modyfikacji,
d) jeśli są jeszcze nazwy na liście nazw zmodyfikowanych, przejście do
punktu.
5.
ZASADY PRAWIDŁOWEGO KORZYSTANIA
ZE ZMODYFIKOWANYCH NAZW
W PLIKACH ŹRÓDŁOWYCH
Kompilator w trakcie generacji kodu korzysta z plików z leksemami
przygotowanych przez analizator semantyczny. Aby proces ten był możliwie
44
najłatwiejszy, konieczne było korzystanie z ujednoliconego interfejsu
dostępu do kolejnych leksemów w pliku źródłowym oraz odpowiadających
im wartości semantycznych. Pośród tych leksemów znajdują się też takie,
których nazwy zostały zmodyfikowane w trakcie analizy semantycznej.
Analizator semantyczny kopiuje informację semantyczną z oryginalnego
miejsca jej umieszczenia i umieszcza ją w pliku z wartościami semantycznymi
odpowiednim dla analizowanej konstrukcji. Generator kodu wynikowego nie
musi więc dbać o odpowiednią modyfikację nazwy, wystarczy że odczyta
odpowiednie informacje plików z informacją semantyczną.
6.
PRZYKŁAD DZIAŁANIA ALGORYTMU
MODYFIKACJI NAZW
W poniższym przykładzie zaprezentowane zostało działanie algorytmu
modyfikującego nazwy dla programu zawierającego 2 mapowania,
zawierające identyczne identyfikatory sygnałów i zmiennych. Ze względu na
dużą wielkość rzeczywistych przykładów, w których algorytm modyfikacji
znajduje zastosowanie, przykład został uproszczony do formy, dzięki której
przejrzysty staje się sposób działania algorytmu.
library IEEE;
use IEEE.std_logic_1164.all;
entity MyOREntity is
port (SIGINA, SIGINB : in STD_LOGIC;
SIGOUT : out STD_LOGIC
);
end entity MyOREntity;
architecture myarch of MyOREntity is
signal MyValue: std_logic;
begin
SIGOUT <= SIGINA or SIGINB or MyValue;
end architecture myarch;
entity MyANDEntity is
port (SIGINA, SIGINB : in STD_LOGIC;
SIGOUT : out STD_LOGIC
);
end entity MyANDEntity;
architecture myarch of MyANDEntity is
signal MyValue: std_logic;
begin
SIGOUT <= SIGINA AND SIGINB AND MyValue;
end architecture myarch;
45
entity MyDesign is
port (SIGINA, SIGINB : in STD_LOGIC;
SIGOUT, SIGOUT2 : out STD_LOGIC
);
end entity MyDesign;
architecture myarch of MyDesign is
component MyOREntity is
port (SIGINA, SIGINB : in STD_LOGIC;
SIGOUT : out STD_LOGIC
);
end component;
component MyANDEntity is
port (SIGINA, SIGINB : in STD_LOGIC;
SIGOUT : out STD_LOGIC
);
end component;
begin
ORMap:
MyOrEntity port map (
SIGOUT => SIGOUT,
SIGINA => SIGINA,
SIGINB => SIGINB
);
ANDMap:
MyANDEntity port map (
SIGOUT => SIGOUT2,
SIGINA => SIGINA,
SIGINB => SIGINB
);
end architecture myarch;
W wyniku kompilacji powyższego kodu źródłowego uzyskano
następujący plik z równaniami boolowskimi:
--port_map equations begin
SIGOUT_modif__6=
((SIGINA_modif__4|SIGINB_modif__5)|tmp__id_1072);
--port_map equations end
--port_map equations begin
SIGOUT2=((SIGINA_modif__4&SIGINB_modif__5)
&tmp__id_1080_);
--port_map equations end
Jak widać zmodyfikowane zostały nazwy portów wejściowych i
wyjściowych jednostki MyDesign, za wyjątkiem portu SIGOUT2, którego
46
identyfikator nie występuje nigdzie poza jednostką MyDesign. Widać także,
że wewnętrzne sygnały architektur są również rozróżniane w pliku
wynikowym, gdyż mogą to być sygnały o różnych wartościach.
7.
PODSUMOWANIE
Przedstawione
w
referacie
algorytmy
zostały
skutecznie
zaimplementowane w analizatorze semantycznym wchodzącym w skład
kompilatora języka VHDL. W kompilatorze tym, ze względu na specyficzną
formę generowanego kodu wyjściowego (równania boolowskie) było
konieczne zaimplementowanie algorytmu, który pozwalałby na
jednoznaczną identyfikację nazw w wyjściowych równaniach. Nawet przy
dużych kompilowanych plikach źródłowych, zawierających wiele miejsc,
gdzie istnieje konieczność zmodyfikowania nazwy, algorytmy te nie
powodują znaczącego spowolnienia działania całego analizatora
semantycznego, dodatkowo upraszczając implementacje generatora kodu.
Dodatkowo istnieje możliwość przeszukiwania modyfikowanych nazw
opartego na wyszukiwaniu binarnym, a nie jak dotychczas, liniowym, co
mogłoby jeszcze bardziej przyśpieszyć analizę semantyczną. Stosowanie
wszystkich tu zaprezentowanych algorytmów jest konieczne tylko w
przypadku, gdy wszystkie zmienne i sygnały z programu źródłowego znajdą
się na jednym poziomie w pliku wynikowym. W przypadku narzędzi do
syntezy (w których skład może wchodzić również omawiany analizator
semantyczny) rozróżnianie nazw stanowi jedynie formę uporządkowania
formy wyjściowej, gdyż nazwy znajdują się na różnych poziomach plików
wynikowych, a każdy poziom w poprawnych źródłach programu VHDL
stanowi osobną przestrzeń nazw, co zapewnia unikalność nazwy.
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994
[2] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
[3] Synopsys: FPGA Express VHDL Reference Manual, December 1997
[4] W. Wrona: VHDL język opisu sprzętu i projektowania układów cyfrowych,
Gliwice 1998
[5] K. Skahill: Język VHDL Projektowanie programowalnych układów logicznych,
WNT Warszawa 2001
Algorytm i zasady dotyczące implementacji
konstrukcji bloku w kompilatorze języka VHDL
Piotr Błaszyński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Przedstawiono zasady działania instrukcji bloku w języku VHDL. Został
również zaprezentowany algorytm przekładu instrukcji bloku przez
analizator semantyczny. Pokazano również w jaki sposób informacja
wygenerowana prze analizator semantyczny jest wykorzystywana
w późniejszym etapie generacji równań boolowskich. Opisane poniżej
algorytmy zostały zaimplementowane w kompilatorze języka VHDL do
generacji równań boolowskich.
Słowa kluczowe:
język VHDL, kompilatory, hierarchia programu, równania boolowskie
1.
SKŁADNIA INSTRUKCJI BLOK
W JĘZYKU VHDL
W języku VHDL występuje współbieżna instrukcja block. Instrukcja ta
pozwala na łączenie innych instrukcji współbieżnych, w tym też instrukcji
bloku, w wyodrębnione fragmenty kodu pozwalające na polepszenie
czytelności i przejrzystości opisu struktury projektu. Instrukcja bloku nie
wpływa bezpośrednio na wyniki działania projektu, więc również kod
wynikowy wyprodukowany prze kompilator powinien być zgodny z kodem
wyprodukowanym z projektu nie zawierającego instrukcji bloku. Składnię
instrukcji bloku można opisać przy pomocy notacji Backusa-Naura
w następujący sposób [1, 3]:
instrukcja_bloku ::= etykieta:
block [(wyrażenie_dozoru)] [is]
48
nagłówek_bloku
część_deklaracyjna_bloku
begin
instrukcje_współbieżne
end block [etykieta_bloku];
nagłówek_bloku ::=
[sekcja_parametrów[sekcja_mapowania_parametrów;]]
[sekcja_portów [sekcja_mapowania_portów;]]
część_deklaracyjna_bloku::={ deklaracja_podprogramu
| deklaracja_ciała_podprogramu
| deklaracja_typu | deklaracja_podtypu
| deklaracja_stałej | deklaracja_sygnału
| deklaracja_zmiennej_dzielonej
| deklaracja_pliku | deklaracja_aliasu
| deklaracja_składnika
|deklaracja_atrybutu
| specyfikacja_atrybutu
| specyfikacja_konfiguracji
| specyfikacja_rozłączenia | use_clause
| deklaracja_wzorca_grupy | deklaracja_grupy }
instrukcje_współbieżne ::= { instrukcja_bloku
| instrukcja_procesu
| instrukcja_konkretyzacji składnika
| instrukcja_powielania
| współbieżna_instrukcja_przypisania_sygnałów
| współbieżna_instrukcja_wywołania_procedury
| współbieżna_instrukcja_założenia }
Wyrażenie dozorujące (ang. guard expression), które może zostać
umieszczone po słowie kluczowym block definiuje niejawnie sygnał o nazwie
GUARD. Sygnał ten jest typu Boolean i może być wykorzystywany podobnie
jak inne sygnały dostępne w instrukcji bloku, lecz nie może być uaktualniany
przez żadną instrukcję wchodzącą w skład bloku. Sygnał ten ma zakres
widoczności zamykający się tylko w obrębie bloku. W momencie wykonania
instrukcji dla sygnału wchodzącego w skład wyrażenia dozoru wartość tego
wyrażenia jest obliczana a wartość sygnału GUARD powinna zostać
natychmiast uaktualniona. Sygnał GUARD może być również zadeklarowany w
części deklaracyjnej bloku w sposób jawny przez programistę. Musi być on
jednak typu Boolean i musi być uaktualniany w jednej z instrukcji procesu,
zawartej wewnątrz bloku. Brak jawnej deklaracji sygnału GUARD i wyrażenia
dozorującego oznacza, że sygnał GUARD ma wewnątrz danego bloku ustaloną
wartość True. Omawiany sygnał jest stosowany do synchronizacji
współbieżnych instrukcji przypisania sygnałów wchodzących w skład bloku.
49
Każda z tych instrukcji musi zawierać słowo kluczowe guarded, które jest
umieszczane po symbolu przypisania. Takie instrukcje przypisują wartość z
prawej strony do sygnału znajdującego się po lewej stronie przypisania tylko
wtedy gdy wartość sygnału GUARD jest równa True. W innym wypadku taka
instrukcja przypisania nie zmienia wartości sygnału.
2.
ODPOWIEDNIKI INSTRUKCJI BLOK
W INNYCH JĘZYKACH PROGRAMOWANIA
W innych językach programowania wysokiego poziomu występują
również różne instrukcje pozwalające na łączenie wielu instrukcji w jeden
logiczny blok. Przykładowo w języku C/C++ blok instrukcji jest oznaczany
poprzez nawiasy klamrowe a w języku Pascal przez słowa kluczowe begin i
end. Jednak nie wszystkie cechy konstrukcji bloku są w pozostałych
językach podobne do cech i zasad obowiązujących w języku VHDL. Ze
znanych dobrze autorowi języków programowania najbardziej zbliżone
reguły rządzące instrukcją bloku i jej semantyką obowiązują w języku C (w
przypadku C++ daje się zauważyć większe różnice). Dlatego zostanie on
użyty jako materiał do porównania. W obu językach:
– zasięg zmiennych zadeklarowanych w instrukcji bloku zamyka się w
obrębie tego bloku,
– deklaracje zmiennych i sygnałów muszą występować na początku bloku
(w przypadku języka VHDL służy do tego celu specjalnie wydzielona
część deklaracyjna),
– konstrukcje bloku mogą być wielokrotnie zagnieżdżane.
Mimo największego podobieństwa, dają się też zauważyć istotne z
punktu widzenia programisty oraz semantyki kompilatora różnice:
– zarówno w języku C, jak i w żadnych innych językach wysokiego
poziomu nie zostało zdefiniowane jawnie pojęcie przypisania
warunkowego odpowiadające temu pojęciu w języku VHDL, możliwa jest
jednak sztuczna implementacja tego przypisania przy pomocy instrukcji if,
nie można jednak skorzystać z konstrukcji z języka C: „warunek ?
wyrażenie1 : wyrażenie2”, gdyż powoduje ona zawsze zwrócenie wartości, a
co za tym idzie wykonanie ewentualnego przypisania,
– w części deklaracyjnej bloku z VHDL można zagnieżdżać inne
konstrukcje tego języka, takie jak funkcje, procedury i procesy, natomiast
w języku C nie ma takiej możliwości,
– w innych językach nie istnieje pojęcie wyrażenia dozorującego, a jego
sztuczna implementacja jest skomplikowana ze względu na możliwość
występowania wielu zależności wartości składników tego wyrażenia od
innych wartości w programie.
50
3.
UZASADNIENIE WYKORZYSTANIA
INSTRUKCJI BLOK W PROGRAMACH VHDL
Konstrukcja bloku umożliwia podzielenie projektu na łatwiejsze do
przetwarzania jednostki, czyli tworzenie hierarchii, pozwalającej na [4]:
– możliwość zdefiniowania detali tylko jednej części projektu w danym
momencie (cecha ta jest szczególnie ważna przy projektowaniu
równoległym realizowanym przez wiele osób),
– ponieważ instrukcje bloku mogą być zagnieżdżane, możliwa jest
dekompozycja funkcji jednostki projektowej na podfunkcje, a struktury
na podstruktury,
– poprzez korzystanie z przypisań dozorowanych do sygnałów, sygnału
GUARD i wyrażenia dozorującego, można uprościć zapis programu a
logikę projektu uczynić bardziej przejrzystą,
– skupienie się tylko na pewnej przerabianej części projektu całego
systemu, co prowadzi do mniejszej liczby początkowych błędów oraz
przyspieszenia czasu uruchamiania projektu,
– oddzielną weryfikację każdego komponentu (gdy jest dostępna
możliwość symulacji określonego fragmentu kodu w języku VHDL),
– tworzenie projektu etapami, poprzez kolejne definiowanie pojedynczych
interfejsów elementów składowych projektu,
– dobry podział zadań pomiędzy wyspecjalizowane zespoły.
4.
IMPLEMENTACJA INSTRUKCJI BLOKU
W ANALIZATORZE SEMANTYCZNYM
Poniżej przedstawiony jest uproszczony algorytm przekładu bloku
zastosowany w analizatorze semantycznym:
– przygotowanie plików wyjściowych,
– dla wszystkich leksemów w części deklaracyjnej bloku:
– w przypadku napotkania nawiasu otwierającego wyrażenie dozorujące:
– zapamiętanie całego wyrażenia dozorującego, do zamykającego nawiasu,
– utworzenie leksemu odpowiadającemu sygnałowi GUARD,
– przetwarzanie części deklaracyjnej,
– w przypadku napotkania słowa kluczowego begin zapis sposobu
obliczania sygnału dozorującego,
– parsowanie ciała bloku:
– w trakcie parsowania dopuszczalne jest napotkanie zagnieżdżonego
bloku lub procesu,
51
– sprawdzanie czy nie napotkano mapowań, jeśli tak, to przekład
mapowania,
– normalne traktowanie przypisań dozorowanych.
5.
GENERACJA KODU DLA BLOKÓW
Generowanie kodu odpowiadającego instrukcjom zawartym w bloku jest
uproszczone dzięki wcześniejszemu etapowi analizy semantycznej. Zostaną
przedstawione tylko szczegóły, różniące generowanie kodu wynikowego dla
bloku od generowania tego kodu dla architektury [2]:
– leksemy wchodzące w skład bloku są umieszczone w oddzielnym pliku,
znajdującym się w podkatalogu przygotowanym przez analizator
semantyczny,
– na początku bloku należy obliczyć wartość wyrażenie dozorujące,
– należy zapamiętać wszystkie leksemy wchodzące w skład wyrażenia
dozorującego,
– dla wszystkich przypisań należy sprawdzać czy jest to wyrażenie
dozorowane,
– jeśli wartość wyrażenia dozorującego zmieniła się od momentu
ostatniego jej obliczenia (czyli zmieniła się wartość któregoś z leksemów
wchodzących w skład tego wyrażenia), obliczyć tę wartość,
– jeśli wartość wyrażenia dozorującego wynosi True wykonać przypisanie,
w przeciwnym wypadku, wartości wchodzące w skład przypisania nie
powinny być obliczane.
Jeśli kod nie zawierał żadnych przypisań dozorowanych, to kod
wynikowy nie powinien się różnić od kodu wygenerowanego dla
analogicznej instrukcji architektury, w przeciwnym wypadku dołożone
zostaną równania boolowskie odpowiadające przypisaniom dozorowanym,
oraz obliczeniom wyrażenia dozorującego.
6.
PRZYKŁAD DZIAŁANIA ANALIZATORA
DLA INSTRUKCJI BLOKU
W poniższym przykładzie zawarto cztery instrukcje block, z których
każda realizuje inne przypisanie:
library ieee;
use ieee.std_logic_1164.all;
entity BTest is
port (
52
in1: in STD_LOGIC_VECTOR (1 downto 0);
in2: in STD_LOGIC_VECTOR (1 downto 0);
in3: in STD_LOGIC;
in4: in STD_LOGIC;
out1: out STD_LOGIC_VECTOR (1 downto 0);
out2: out STD_LOGIC_VECTOR (2 downto 0);
out3: out STD_LOGIC_VECTOR (3 downto 0);
out4: out STD_LOGIC_VECTOR (4 downto 0)
);
end BTest;
architecture BlockTest of BTest is
begin
blok01:block
begin
out1<=(in1 and in2) or(in3&in4);
end block;
blok02: block
begin
out2<=(in1 and in2)&'0';
end block;
blok03:block
begin
out3<=(in1 xor in2)&in4&'0';
end block;
blok04:block
begin
out4<=(in1 or in2)&(in3 and in4)&"10";
end block;
end BlockTest;
Po kompilacji powyższego przykładu uzyskano następujący plik
wynikowy:
--block equations begin, line: 18
out1(1)=((in1(1)&in2(1))|in3);
out1(0)=((in1(0)&in2(0))|in4);
--block equations end, line: 21
--block equations begin, line: 22
out2(2)=(in1(1)&in2(1));
out2(1)=(in1(0)&in2(0));
out2(0)=0;
--block equations end, line: 25
--block equations begin, line: 26
out3(3)=((in1(1)&!in2(1))|(!in1(1)&in2(1)));
53
out3(2)=((in1(0)&!in2(0))|(!in1(0)&in2(0)));
out3(1)=in4;
out3(0)=0;
--block equations end, line: 29
--block equations begin, line: 30
out4(4)=(in1(1)|in2(1));
out4(3)=(in1(0)|in2(0));
out4(2)=(in3&in4);
out4(1)=1;
out4(0)=0;
--block equations end, line: 33
W celu zaprezentowania zgodności formy wyjściowej generowanej przy
użyciu instrukcji block z formą generowaną bez użycia tej instrukcji
skasowane zostały wszystkie wystąpienia konstrukcji bloku i ciało
architektury wygląda w sposób nastepujący:
architecture BlockTest of BTest is
begin
out1<=(in1 and in2) or(in3&in4);
out2<=(in1 and in2)&'0';
out3<=(in1 xor in2)&in4&'0';
out4<=(in1 or in2)&(in3 and in4)&"10";
end BlockTest;
I zgodnie z wcześniejszymi założeniami otrzymano następujący plik
wyjściowy:
out1(1)=((in1(1)&in2(1))|in3);
out1(0)=((in1(0)&in2(0))|in4);
out2(2)=(in1(1)&in2(1));
out2(1)=(in1(0)&in2(0));
out2(0)=0;
out3(3)=((in1(1)&!in2(1))|(!in1(1)&in2(1)));
out3(2)=((in1(0)&!in2(0))|(!in1(0)&in2(0)));
out3(1)=in4;
out3(0)=0;
out4(4)=(in1(1)|in2(1));
out4(3)=(in1(0)|in2(0));
out4(2)=(in3&in4);
out4(1)=1;
out4(0)=0;
Z porównania wynika, że obydwa pliki różnią się jedyni o dodane przez
kompilator komentarze mające podnosić czytelność generowanych równań
boolowskich.
54
7.
PODSUMOWANIE
Implementacja obsługi instrukcji bloku zawarta w kompilatorze VHDL
została przetestowana zarówno pod kątem poprawności działania, jak i pod
względem zgodności generowanej formy wyjściowej z wytycznymi
zawartymi w standardzie języka VHDL. Jak już wcześniej wspomniano,
głównymi cechami zachęcającymi do korzystania z konstrukcji block w
źródłach programów VHDL są:
– możliwość utworzenia hierarchii bez potrzeby tworzenia wielu jednostek
i rozbudowywania ich hierarchii (czasami może to utrudnić generacje
optymalnego kodu przez narzędzia syntezy),
– możliwość korzystania z przypisań dozorowanych i sygnałów guarded.
Cechy te sprawiły, iż konieczne było pełne zaimplementowanie obsługi
konstrukcji bloku (pierwotna wersja kompilatora nie zakładała
implementacji tej konstrukcji).
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994
[2] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
[3] W. Wrona: VHDL język opisu sprzętu i projektowania układów cyfrowych,
Gliwice 1998
[4] K. Skahill: Język VHDL Projektowanie programowalnych układów logicznych,
WNT Warszawa 2001
Funkcje rezolucji – zasada działania i sposób
implementacji w kompilatorze języka VHDL
Piotr Błaszyński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Przedstawiono zasady działania funkcji rezolucji w języku VHDL.
Zostały również zaprezentowane podejście kompilatora języka VHDL
w przypadku napotkania w plikach źródłowych na typy związane z funkcjami
rezolucji. Zaprezentowano zarówno algorytm zastosowany w analizatorze
semantycznym jak i w postprocesorze równań boolowskich. Opisane poniżej
algorytmy zostały całkowicie zaimplementowane w kompilatorze języka
VHDL do generacji równań boolowskich.
Słowa kluczowe:
język VHDL, kompilatory, funkcje rezolucji, logika wielowartościowa,
równania boolowskie
1.
FUNKCJE REZOLUCJI I ICH ODPOWIEDNIKI
W LOGICE
W języku VHDL do jednego sygnału nie może być przypisany więcej niż
jeden nośnik (ang. driver), chyba że dany sygnał ma związaną z nim funkcje
rezolucji (rozstrzygającą, ang. resolved). Funkcja rezolucji jest używana do
obliczenia wartości sygnału opartego na dwóch nośnikach [2, 4]. Poniżej jest
przedstawiony przykładowy układ opisany w języku VHDL, w którym
konieczne jest zastosowanie funkcji rezolucji do rozstrzygnięcia, jaka ma
być wartość sygnału wyjściowego:
architecture rozstrzygana of toplevel is
begin
z <= x and y;
56
z <= x or y;
end rozstrzygana
W zależności od wartości aktualnych przenoszonych przez dwa
występujące w zaprezentowanej architekturze przypisania, wartość logiczna
sygnału może wynosić ‘0’, ‘1’ lub ‘X’, gdzie ‘X’ oznacza stan nieokreślony.
Przypisania współbieżne powodują, że w tym przypadku jest konieczne
utworzenie dwóch nośników dla sygnału z. W przypadku, kiedy te dwa
przypisania będą występować wewnątrz jednego procesu, nie powstaną dwa
osobne nośniki, a jedynie zostanie wykonane przypisanie, które występuje jako
ostatnie. Natomiast w przypadku, kiedy te przypisania występują w dwóch
oddzielnych procesach, powinny one przynieść ten sam skutek, jak na
przedstawionym powyżej przykładzie. Jeśli programista chce stosować tego
typu instrukcje, z sygnałem z musi być związana funkcja rezolucji. Jest ona
stosowana do obliczania wartości sygnału na podstawie wartości jego nośników.
2.
FUNKCJE SYSTEMOWE ZWIĄZANE Z TYPAMI
ROZSTRZYGANYMI
W standardzie języka VHDL jest zawarta definicja typu std_logic,
który jest podtypem typu std_ulogic, i ma takie same wartości jak ten
typ. Dodatkowo jednak z typem std_logic jest związana funkcja
rezolucji. Typu tego można używać jako typu z już zdefiniowaną funkcją
rezolucji. Dotyczy to również typów: X01, X01Z, UX01, UX01Z. Definicje
wymienionych typów są zawarte w standardowym pakiecie
ieee.std_logic_1164 i wyglądają następująco [2]:
SUBTYPE std_logic IS
resolved std_ulogic;
SUBTYPE X01
IS
resolved std_ulogic RANGE
SUBTYPE X01Z IS
resolved std_ulogic RANGE
SUBTYPE UX01 IS
resolved std_ulogic RANGE
SUBTYPE UX01Z IS
resolved std_ulogic RANGE
'X' TO '1';
'X' TO 'Z';
'U' TO '1';
'U' TO 'Z';
57
3.
FUNKCJE REZOLUCJI DEFINIOWANE
PRZEZ UŻYTKOWNIKA
Możliwe jest jednak zdefiniowanie przez projektanta własnego typu z
oddzielną funkcją rezolucji. Jest to możliwe poprzez zdefiniowanie podtypu
i użycie słowa kluczowego resolved oraz zaimplementowanie specjalnej
funkcji rozstrzygającej. Użytkownik może zadeklarować funkcje rezolucji w
czterech następujących krokach.
Deklaracja bazowego typu sygnału.
type TYP_SYGNAŁU is ...;
Deklaracja funkcji rezolucji. TYP_TABLICOWY jest tablicą o
nieograniczonym rozmiarze (ang. unconstrained array) składającą się z
elementów typu TYP_SYGNAŁU. Dodatkowo wewnątrz funkcji musi
znaleźć się komentarz opisujący jakiego rodzaju rozstrzygniecie wykonuje
funkcja rezolucji.
function FUNKCJA_REZOLUCJI
(DATA: TYP_TABLICOWY)
return TYP_SYGNAŁU is
W opisywanym analizatorze semantycznym, a co za tym idzie w całym
kompilatorze, dozwolone są następujące komentarze opisujące rodzaj
funkcji rezolucji:
Sygnał wyjściowy łączony przy pomocy funkcji and:
-- aldec resolution_method wired_and
-- synopsys resolution_method wired_and
-- pragma resolution_method wired_and
Sygnał wyjściowy łączony przy pomocy funkcji or:
-- aldec resolution_method wired_or
-- synopsys resolution_method wired_or
-- pragma resolution_method wired_or
Sygnał wyjściowy łączony przy pomocy funkcji trzystanowej:
-- aldec resolution_method three_state
-- synopsys resolution_method three_state
-- pragma resolution_method three_state
Deklaracja podtypu (ang. subtype) sygnału rozstrzyganego jako podtypu
sygnału bazowego. Deklaracja ta musi zawierać nazwę związanej z
sygnałem funkcji rezolucji.
subtype
TYP_REZOLUCJI
is
FUNKCJA_REZOLUCJI
TYP_SYGNAŁU;
Deklaracja sygnałów rozstrzyganych jako zadeklarowane wcześniej
podtypy
signal
NAZWA_SYGNAŁU_ROZSTRZYGANEGO: TYP_REZOLUCJI;
58
4.
ZASADY IMPLEMENTACJI FUNKCJI
REZOLUCJI W ANALIZATORZE
SEMANTYCZNYM
Dla każdej z napotkanych deklaracji podtypu, które składniowo
odpowiadają opisowi w punkcie 3c, wyszukiwana jest odpowiadająca temu
sygnałowi funkcja. Jeśli istnieje taka funkcja podtyp ten jest dodawany do
listy zawierającej wszystkie rozstrzygane podtypy. Analizator wyszukuje
również typ bazowy przekładanego podtypu i umieszcza o nim informację
semantyczną w opisie podtypu. Po napotkaniu na funkcję następuje
sprawdzenie czy jest ona przypisana do sygnału rozstrzyganego. Ciało takiej
funkcji jest ignorowane. Pod uwagę brany jest tylko zawarty w niej
komentarz określający rodzaj funkcji rezolucji, jaka należy zastosować.
Następuje sprawdzenie czy tekst komentarza zaczyna się od któregoś ze
słów kluczowych: pragma, aldec, synopsys. Jeśli tak jest to na podstawie
dalszej części komentarza sprawdzany jest rodzaj rozstrzygania przypisany
do przekładanej funkcji. W tym miejscu dozwolone są tylko słowa
kluczowe: wired_and, wired_or, three_state. Rodzaj ten jest zapamiętywany
przy deklaracji funkcji, oraz na liście funkcji rozstrzyganych. Przy deklaracji
korzystającej z typu rozstrzyganego konieczne jest dodatkowe sprawdzenie
czy deklaracja ta jest stosowana w przypadku sygnału, gdyż według
standardu języka VHDL tylko sygnały mogą mieć przypisaną funkcję
rozstrzygająca. Po przekładzie wszystkich plików źródłowych wchodzących
w skład projektu dla każdej jednostki są zapisywane sygnały będące typu
resolved. Dotyczy to zarówno sygnałów korzystających z typów
systemowych, jak i sygnałów korzystających z typów zdefiniowanych przez
programistę. Informacje te zapisywane są w pliku o nazwie odpowiadającej
nazwie jednostki i z rozszerzeniem .res
5.
ZASADY WYKORZYSTANIA ZAPISANEJ
INFORMACJI NA ETAPIE GENERACJI
RÓWNAŃ BOOLOWSKICH
I W POSTPROCESORZE RÓWNAŃ
BOOLOWSKICH
Na etapie generacji równań boolowskich wszystkie przypisania na
sygnały są traktowane jako oddzielne wyrażenia. Zarówno w przypadku gdy
przez użytkownika nie są zdefiniowane żadne funkcje rezolucji, jak i wtedy
gdy użytkownik zdefiniuje własne typy i funkcje rozstrzygające nie ma
potrzeby sprawdzania czy dany sygnał występujący po lewej stronie
59
przypisania jest związany z jakąkolwiek funkcją rezolucji. Dopiero
postprocesor zajmuje się łączeniem przypisań na sygnały będące typu
resolved w jedno wyrażenie na podstawie typu rezolucji. Odczytuje on z
plików z rozszerzeniem .res nazwy sygnałów podlegających rezolucji, a
także rodzaj tej rezolucji. Na tej podstawie potrafi połączyć przypisania na
sygnały w plikach z równaniami boolowskimi.
6.
PRZYKŁAD
Do zademonstrowania poprawności implementacji funkcji rezolucji
zastosowano następujący kod żródłowy:
package res_pack is
function res_func
(data: in bit_vector) return bit;
subtype resolved_bit is
res_func bit;
end;
package body res_pack is
function res_func
(data: in bit_vector) return bit is
-- aldec resolution_method wired_and
begin
return '1';
end function;
end package body;
use work.res_pack.all;
entity wand_vhdl is
port (x, y: in BIT;
z: out resolved_bit
);
end wand_vhdl;
architecture wand_vhdl of wand_vhdl is
begin
z<=x;
z<=y;
end wand_vhdl;
Po kompilacji tego przykładu w pliku z równaniami boolowskimi
znajdują się następujące wyrażenia:
z=x;
z=y;
60
Natomiast w pliku wand_vhdl.res znajduje się informacja o typie
sygnałów wchodzących w skład tych równań:
z AND 0;
Postprocesor, bazując na powyższych informacjach generuje następujące
równanie:
-- result of resolution
z=(y)&(x);
7.
WNIOSKI
Funkcje rezolucji są bardzo ważnym elementem języka VHDL i ich
implementacja w kompilatorze tego języka jest elementem wymaganym do
pełnego wykorzystania możliwości jakie daje język. Na zamieszczonych
powyżej przykładach widać, że konieczne jest rozpatrywanie wszystkich
możliwości implementacji funkcji rezolucji. Konieczne jest także założenie,
iż mogą pojawić się inne rodzaje komentarzy opisujących funkcje rezolucji,
w przypadku pojawienia się narzędzi specyficznych dla danego producenta.
Jest to spowodowane tym, że standard [1] nie określa dokładnie jaki może
być tekst komentarza oznaczający funkcję rezolucji. W przypadku
pojawienia się konieczności dołożenia nowego rodzaju komentarza, zadanie
to zostało uproszczone w analizatorze semantycznym do niezbędnego
minimum.
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994
[2] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
[3] W. Wrona: VHDL język opisu sprzętu i projektowania układów cyfrowych,
Gliwice 1998
[4] K. Skahill: Język VHDL Projektowanie programowalnych układów logicznych,
WNT Warszawa 2001
Instrukcje współbieżnego przypasania sygnałów
do syntezy cyfrowych układów logicznych
Marcin K. Liersz
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Prezentowane opracowanie przedstawia algorytm generowania równań
boolowskich dla współbieżnej instrukcji przypisania sygnałów
(Concurrent Signal Assignments) z języka VHDL. Algorytm ten jest to
element kompilatora tego języka za pomocą którego możliwa jest synteza
układów logicznych.
Słowa kluczowe:
kompilator VHDL, synteza układów FPGA
1.
WSTĘP DO PROBLEMU
Języki HDL (Hardware Description Languages) są używane aby opisać
architekturę i sposób działania dyskretnych systemów elektronicznych. Wraz
z postępem technologicznym za ich pomocą opisywane były coraz to
trudniejsze, bardziej złożone projekty. Można by tu zastosować analogie do
języków programowania. Jeśli język maszynowy odpowiadałby
tranzystorom i lutom w układach to język wysokiego poziomu (np. C++)
korespondowałby właśnie z HDL-em. Jego wysoki poziom abstrakcji
umożliwia zarówno opis całych układów jak i poszczególnych bramek na
poziomie implementacji. Dalej może to być opis reprogramowalnych
układów FPGA w celu automatycznej syntezy danych układów.
Język VHDL (Very High Speed Integrated Circuit (VHSIC) HDL,
w skrócie VHDL) jest jednym z kilku języków HDL szeroko użytkowanych
dzisiaj. Jest on standardem wypracowanym przez Instytut Elektrycznych
62
i Elektroniki Inżynierów (IEEE standard 1076, ratyfikowany w 1987 ) oraz
przez Departament Obrony USA[1,2].
Język ten wyróżnia istnienie poziomów od zewnętrznych, widocznych
elementów (obwody, układy, systemy) po wewnętrzne, ukryte (np.
algorytmy i implementacje). Zdefiniowany zewnętrzny interfejs może w
dalszej kolejności służyć jako np. element biblioteczny. Takie podejście do
układów jest cechą charakterystyczną języka, którą można porównać do
programowania obiektowego w języka takich jak C czy Pascal. Dla każdego
elementu można zdefiniować jedno lub więcej linii wejściowych,
wyjściowych, bądź też wejściowo-wyjściowych łączących dany element
z otoczeniem. Może to proces, dowolny jego składnik, wszystkie one mogą
pracować jednocześnie. W VHDL-u można wyróżnić procesy opisujące
model układu sekwencyjnego (mierzalny czas) oraz układy połączeniowe
(niemierzalne czasowo) używające tylko bramek logicznych. Procesy mogą
powoływać pod procesy. Komunikacja między procesami odbywa się za
pomocą sygnałów, których typ jest dowolnie definiowany.
2.
OPIS INSTRUKCJI WSPÓŁBIEŻNEGO
PRZYPISANIA DLA SYGNAŁÓW
Niezależne przypisanie dla sygnałów (ang. Concurrent Signal
Assignments) jest równoważne zdefiniowanemu procesowi, który zawiera
przypisanie sekwencyjne, różnica polega na tym, że każde współbieżne
przypisanie definiuje nowy sterownik dla przypisywanego sygnału.
Najprostszą formą takiego sygnału przypisania jest:
cel <= wyrażenie;
Instrukcja ta oznacza, że cel otrzymuje wartość wyrażenia. Przykładem
takiego przypisania może być:
BLK: block
signal A, B, Z: BIT;
begin
Z <= A and B;
end block BLK;
Sygnałowi Z przypisywana jest wartość wyrażenia A and B.
Inne formy współbieżnego przypisania to przypisanie sygnału
z warunkiem (Conditional Signal Assignment) i drugie z wyborem (Selected
Signal Assignment).
Składnia warunkowego sygnału przypisania jest następująca:
cel <= { wyrażenie1 when warunek else }
wyrażenie2;
Jak widać jest to rozszerzona forma prostego przypisania. Wartość
sygnału cel określa wyrażenie pierwsze wtedy gdy jest spełniony warunek.
63
Warunkiem może być wyrażenie którego wynikiem jest wartość typu
Boolean. W przypadku, gdy warunek nie przyjmuje wartości TRUE cel
określa wyrażenie drugie. W trakcie wykonywania tej instrukcji sprawdzane
są wszystkie warunki w kolejności ich występowania, wyrażenie przyjmuje
wartość pierwszego spełnionego warunku (pierwszy spełniony warunek
przerywa dalsze sprawdzanie). Jeżeli żaden warunek nie jest prawdziwy,
wyrażenie przyjmuje wartość końcowego wyrażenia. Jeżeli dwa albo więcej
warunki są Prawdziwe, tylko pierwszy z nich jest efektywny, każdy
następny jest pomijany [1].
Przykład pokazuje warunkowe przypisanie sygnału, gdzie celem jest
sygnał Z. Jest on przypisany do jednego z sygnałów A, B , lub C. Sygnał
zależy od wartości wyrażeń ASSIGN_A i ASSIGN_B. Należy pamiętać, że
pierwszeństwo ma przypisanie A przed B, B natomiast ma pierwszeństwo
przed przypisaniem C.
Z <=
A when ASSIGN_A = '1' else
B when ASSIGN_B = '1' else
C;
Przykład przedstawiony powyżej, można zapisać przy pomocy procesu
i instrukcji warunkowej, będzie to zapis w pełni ekwiwalentny do
warunkowego sygnału przypisania.
proces (A, ASSIGN_A , B , ASSIGN_B , C )
begin
if ASSIGN_A = '1' then
Z <= A;
elsif ASSIGN_B = '1' then
Z <= B;
else
Z <= C;
end if; end process;
Końcowym rodzaje współbieżnego przypisania sygnału jest przypisanie
sygnału z wyborem. Składnia wygląda następująco:
with wyrażenie_wyboru select
celu <= { wyrażenie when wybór, }
wyrażenie when wybór;
Cel jest sygnałem, który otrzymuje wartość wyrażenia. Wyrażenie będzie
wybrane w zależności od "wyboru" spełniającego warunek wyrażenia
wyboru. Składnia wyborów może zawierać kilka wyrażeń:
wybór {|| wybór}.
Każdym wyborem może być albo statyczne wyrażenie (np. 2 ), albo też
statyczny zasięg (taki jak 1 do 3). Wyrażenie wyboru określa typ danych dla
każdego "wyboru". Każdej wartość z zakresu wyrażenia wyboru musi
odpowiadać jedna wartość wyboru.
Wybór odpowiedniego wyrażenia odbywa się poprzez wyliczenie
wyrażenia wyboru i porównania go ze wszystkimi możliwościami. Kiedy
64
klauzula taka zostanie odnaleziona w wyniku otrzymujemy przypisanie
wyrażeniu odpowiadającej wartość. Powoduje to ograniczenie istnienia tylko
jednego poprawnego wyboru, nie mogą istnieć dwa wyrażenia o takich
samych wyborach. Jeżeli żaden inni wybór nie występuje, to muszą być
wyczerpane wszystkie możliwości wyrażenia wyboru.
Przykład pokazuje, że cel Z uzyskuje wartości A, B, C, lub D, a samo
przypisanie zależy od aktualnej wartości zmiennej "KONTROL".
signal A, B, C, D, Z: BIT;
signal CONTROL: bit_vector(1 down to 0);
. . .
with CONTROL select
Z <= A when "00",
B when "01",
C when "10",
D when "11"
Dla tego przypisania, także można napisać odpowiadający mu proces
z instrukcją wyboru CASE. Przykład taki ma postać:
process(CONTROL, A, B, C, D)
begin
case CONTROL is
when 0 =>
Z <= A;
when 1 =>
Z <= B;
when 2 =>
Z <= C;
when 3 =>
Z <= D;
end case;
end process;
3.
GENEROWANIE RÓWNAŃ BOOLOWSKICH
DLA INSTRUKCJI WSPÓŁBIEŻNEGO
PRZYPISANIE DLA SYGNAŁÓW
Algorytm zaimplementowany w języku ANSI C składa się z następujących punktów:
1. Sprawdzenie czy jest to prosta forma podstawienia postaci cel <=
wyrażenie;
2. Jeżeli tak:
3. Wywołanie procedury generującej wyrażenia boolowskie;
65
4. Generowanie równania (układu równań) dla podstawienia
postaci
cel = wyrażenie boolowskie;
5. Koniec procedury.
6. Jeśli nie punkt 7.
7. Sprawdzenie czy jest to podstawienie z warunkiem;
8. Jeżeli tak:
9. Wywołanie procedury generującej wyrażenia boolowskie dla
wszystkich wyrażeń występujących w przypisaniu: E1, E2, E3, ...
En;
10. Wywołanie procedury generującej wyrażenia boolowskie dla
wszystkich warunków występujących w przypisaniu F1, F2, F3,
... Fn-1;
11. Generowanie równania wyjściowego postaci:
cel = E1*F1 + E2*!F1*F2 + E3*!F1*!F2*F3 + En*!F1*!F2*...*!Fn-1
12. Koniec procedury.
13. Jeżeli nie:
14. Wywołanie procedury generującej wyrażenia boolowskie dla
wszystkich wyrażeń występujących w przypisaniu: E1, E2, E3, ...
En;
15. Wywołanie procedury generującej wyrażenia boolowskie dla
wszystkich wyborów występujących w przypisaniu F1, F2, F3, ...
Fn;
16. Generowanie równania wyjściowego postaci:
cel = E1*F1 + E2*F2 + E3*F3 + ... + En*Fn
17. Koniec procedury
18. Wyprowadzenie wyniku w postaci równania boolowskiego
En jak i Fn mogą być zarówno prostymi identyfikatorami typu bitowego
jaki i złożonymi równaniami opisującymi wszystkie bity identyfikatora (np.
typu integer).
Procedury generujące poszczególne wyrażenia jako parametr wejściowy
przyjmują postać wyprodukowaną przez analizę leksykalną. Są to kody
opisujące poszczególne leksemy języka. Wyjście stanowią równania
boolowskie opisujące poszczególne zmienne, a dokładniej każdy z bitów
analizowanych zmiennych. Równania boolowskie zawierać mogą tylko trzy
operacje "+", "*", oraz "!", odpowiadają one kolejno operacjom logicznym
or, and i not. Możliwe jest także grupowanie i ustalanie kolejności
wykonywania za pomocą nawiasów.
66
4.
ZAKOŃCZENIE
Zaprezentowany element kompilatora umożliwia generowanie równań
boolowskich. Przy ich pomocy możliwa jest synteza cyfrowych układów
logicznych w tym również w postaci FPGA. Przedstawione algorytmy są już
zaimplementowane i stanowią część kompilatora VHDL.
LITERATURA:
[1] VHDL'93 IEEE Standard VHDL Lenguage Reference Manual, IEEE Std. 1076-1993
[2] FPGA Express, VHDL Referance Manual, 1997
Algorytm generowania równań boolowskich dla
instrukcji przypisania zawierającej odwołania do
tablic języka VHDL
Marcin K. Liersz
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Referat zawiera opis algorytmu konstruowania przypisań, jaki został
zastosowany w kompilatorze języka VHDL do syntezy układów
logicznych. Szczególna uwaga została skierowana na problem generacji
równań dla przypisań, w których występują tablice zarówno z prawej jak
i z lewej strony.
Słowa kluczowe:
język VHDL, synteza układów logicznych, kompilatory.
1. WPROWADZENIE
Na Wydziale Informatyki Politechniki Szczecińskiej tworzony jest
kompilator do syntezy układów logicznych. Postacią wejściową dla tego
kompilatora jest program źródłowy napisany w języku VHDL. Język ten jest
jednym z kilku języków HDL szeroko użytkowanych do opisu sprzętu. Jest
on jednak jednym z nielicznych, opisanych standardem IEEE oraz
Departamentu Obrony USA[1,2]. VHDL znajduje zastosowanie zarówno do
symulacji, jak i do syntezy układów logicznych. W przypadku syntezy na
zbiór produkcji języka nakładane są ograniczenia tworzące syntezowalny
podzbiór języka. Dla prezentowanego kompilatora taki podzbiór został
opracowany i opisany w pracy [3]. Na podstawie tego podzbioru została
opracowana gramatyka, analizatory i w dalszej część generator równań
boolowskich, które to są wynikiem działania całego kompilatora. Równania
68
wynikowe opierają się na algebrze Boolea w sferze arytmetyki bitowej.
W równaniach wykorzystuje się operacje negacji zapisaną w równaniach
jako „!”, operacje alternatywy („|”) oraz operacje koniunkcji („&”).
Jedną z podstawowych instrukcji języka VHDL jest instrukcja
przypisania. Rozpatrując mogące wystąpić przypisania należy wyróżnić
przypisania dla zmiennych oraz przypisania dla sygnałów, a w nich np.
instrukcję współbieżnego przypisania dla sygnałów [4]. Mogą się one
pojawić w wielu różnych miejscach programu, na poziomie: architektury,
w procesach, w funkcjach i procedurach. Wszystkie bazują na wspólnym
mechanizmie generacji równań, który został opisany w [5]. W niniejszym
artykule zostanie zaprezentowane podejście do rozwiązania problemu
generacji równań dla tablic.
2. INSTRUKCJA PRZYPISANIA JĘZYKA VHDL
W zależności od tego czy zapisane ma być przypisanie dla sygnału, czy
dla zmiennej w języku VHDL stosowany jest inny znak samej operacji
przypisania. Operacja przypisania sygnału ma postać:
[ etykieta: ] nazwa sygnału <= wyrażenie;
Natomiast przypisanie dla zmiennej prezentuje się następująco:
[ etykieta: ] nazwa zmiennej := wyrażenie;
Innym jeszcze sposobem przypisania wartości do sygnałów są
przypisania konkurencyjne. Warunkowe przypisanie konkurencyjne ma
postać:
[etykieta:] nazwa sygnału <= [wyrażenie1 when
warunek else] wyrażenie2;
składnia instrukcji konkurencyjnego przypisania wyboru prezentuje się
następująco;
with wyrażenie wyboru select
nazwa sygnału <= [wyrażenie when wybór,]
wyrażenie when wybór;
Jak widać w każdej z przedstawionych konstrukcji języka pojawia się
wyrażenie. Wyrażeniem jest dowolna poprawna semantycznie konstrukcja,
zawierająca niezerową ilość operacji określonych na argumentach, gdzie
argumenty to stałe, zmienne, sygnały bądź funkcje [5]. VHDL umożliwia
deklarowanie tablic zarówno dla typów zdefiniowanych, jak i dla typów
zdefiniowanych przez użytkownika [7]. W ten sposób tablice oraz elementy
tablic mogą pojawić się jako elementy dowolnego wyrażenia, jako sygnał
lub zmienna. Szczególnym przypadkiem jest przypisanie z wykorzystaniem
agregatów, które to wypełniają wszystkie elementy zadanej tablicy; jest to
także jedno z możliwych przypisań.
69
Generacja równań wynikowych dla tablic (tak jak i dla wszystkich
wyrażeń) opiera się na wartościach semantycznych stworzonych wcześniej
przez analizatory. Wejściem do generatora jest ciąg leksemów (stworzony
przez analizator leksykalny), opisujących dane przypisanie. Każdemu
elementowi wyrażenia odpowiada ściśle określony leksem. Dla przykładu
proste przypisanie z wyrażeniem:
Z <= A and B;
w postaci leksemów będzie ciągiem liczb postaci:
231 171 232 6 233 114.
W zaprezentowanym przykładzie wartości leksemów 231, 232, 233
odpowiadają kolejno sygnałom: Z, A, B. W stworzonym kompilatorze
założono, że wartościom ponad 200 odpowiadać będą identyfikatory
zdefiniowane przez użytkownika oraz predefiniowane typy danych.
Natomiast wartości mniejsze od 200 zarezerwowane są dla elementów
języka VHDL (operatory, słowa kluczowe).
Nie możliwe jest jednak tworzenie równań boolowskich na bazie tylko
wartości leksykalnych, niezbędne są jeszcze wartości semantyczne
wszystkich identyfikatorów. Wartości te wynikają z deklaracji danego
elementu (definicja typu, deklaracja zmiennej lub sygnał) i są tworzone
przez analizator semantyczny[6]. Każdej zmiennej, sygnałowi odpowiada
dokładnie jeden zbiór wartości semantycznych. Dla przykładu wcześniej
przedstawionemu przykładowi operacji może odpowiadać następująca
deklaracja sygnałów:
signal A, B, Z: BIT;
Na podstawie takiej deklaracji zostaną utworzone cztery zbiory wartości
semantycznych: trzy dla sygnałów A,B,Z i jedna dla typu BIT. Pojedynczy
zbiór informacji semantycznej dla sygnału A ma następującą postać:
232 A 202 1 3 1 0 1 0.
Linia ta zawiera następującą informację: numer leksemu (231), nazwa
(A), numer typu (BIT), informacje o tym czy jest to sygnał (1), czy zmienna
bądź funkcja, tryb dostępu do sygnału (inout), ilość bitów danego sygnału,
początek (0), koniec (1), ostatnie pole przygotowane jest do zapisywania
wartości w trakcie generacji. Trzecie pole zawiera numer typu danego
elementu, jest to równocześnie indeks do tablicy opisującej typy danych. Dla
typów również tworzone są wartości semantyczne o zbliżonej konstrukcji.
Typ BIT prezentuje się następująco:
202 bit BIT 1 0 1 0 0.
Jest tu zawarta informacja o numerze leksemu, nazwie typu, klasie typu,
ilości bitów, która reprezentuje ten typ, zakres (początek i koniec); dla typu
bit wyrażony w liczbie bitów(1); oraz o numerze typu elementów (dla tablic)
i numerze typu, z którego wywodzi się dany typ (dla subtypów) - wartości
zero oznaczają, że dla tego typu dane pola są nieistotne. W przypadku
70
deklaracji typu tablicowego zgodnego ze specyfikacją [3] struktura zapisu
wartości semantycznych nie ulega zmianie, jednakże sens interpretacyjny
poszczególnych pól ulega zmianie. Dla przykłady mamy na poziomie
architektury zdefiniowane dwa typy danych myint i mytype oraz sygnał tab
typu maytype. Fragment kodu w języku VHDL byłby następujący:
type myint is range 0 to 7;
type mytype is array (0 to 8) of myint;
signal tab : mytype;
Informacje semantyczną dla sygnału z powyższego przykładu prezentuje
poniższa linia:
240 tab 238 1 3 9 0 8 0.
Zawiera ona taką sama informację jak w poprzednim przykładzie, z tym
tylko, że pola opisujące ilość bitów oznaczają teraz ilość elementów tablicy,
a kolejne pola informują o zakresach tablicy. Wiedzy o tym, że jest to
zmienna tablicowa dostarczana jest wraz z wartością semantyczną typu:
237 myint
238 mytype
INT 4 0 7 0 0
ARRAY 8 0 7 237 0.
Typ o numerze 238 i nazwie mytype jest tablicą dziewięcioelementową
o indeksach od 0 do 8 i elementach typu o numerze 237. Z kolei typ 237 jest
typu całkowitego (INT) o wartościach zapisywanych na 4 bitach i zakresie
od 0 do 7.
Z tak przygotowanymi wartościami semantycznymi można przystąpić do
generowania równań boolowskich, w których występują tablice.
3. GENERACJA RÓWNAŃ BOOLOWSKICH
Parametrem wejściowym dla generatora równań boolowskich jest tablica
zawierająca leksemy zawarte w przypisaniu. Przy generacji równań,
w których występują tablice i są one indeksowane, wyróżnić można trzy
podstawowe sytuacje:
– indeks tablicy jest wartością stałą np. tab(1),
– indeks tablicy jest sygnałem (zmienną), któremu wcześniej została
przypisana wartość,
– indeks tablicy jest sygnałem (zmienną), któremu nie przypisano wartości.
Dla dwóch ostatnich przypadków można jeszcze wyróżnić sytuację,
w której indeks tablicy jest wyrażeniem, dla którego generowane są
równania (wartość nieokreślona) lub wyrażenie, które w wyniku daje
określoną wartość (np. 2+3*a, gdzie a ma już wcześniej przypisaną wartość
1). Takie odwołania do tablic mogą pojawić się zarówno z prawej, jak
i z lewej strony przypisania.
71
W sytuacji, gdy indeks tablicy jest wartością stałą, problem sprowadza
się do zamiany leksemów i generacji wewnętrznej zmiennej tymczasowej,
dla przykładu przypisanie:
tab(2) <= ’1’;
gdzie tab jest tablicą bitów. W postaci leksemów ma postać:
241 105 242 106 171 243 144.
Generator kodu po rozpoznaniu, że sygnał o leksemie 241 jest typu
tablicowego i zaraz po nim występuje kompensująca się para nawiasów
zeruje pierwsze trzy leksemy, a pod czwarty podstawia wewnętrzną zmienna
tymczasową o kodzie np. 270 i nazwie tab(2), tak więc od dalszej generacji
kierowana jest następująca tablica leksemów:
0 0 0 270 171 243 114.
Analogicznie generacja odbywa się, gdy elementy takie tablicy znajdują
się z prawej strony przypisania. Na przykład kod postaci:
tab(2) <= tab(1) and tab(3);
po zamianie na leksemy może wyglądać następująco:
241 105 242 106 171 241 105 243 106 6 241 105 244
106 114
(przed generacją),
0 0 0 270 171 0 0 0 271 6 0 0 0 272 114
(w trakcie generacji po zamienia elementów tablicy).
W sytuacji, gdy indeksem tablicy jest wyrażenie, w pierwszej kolejności
generowane jest wyrażenie indeksujące. Jeśli w wyniku takiej operacji
otrzymany wynik nie jest równaniem, a określoną wartością, postępowanie
jest takie samo jak dla pojedynczej stałej wartości. Może się jednak zdarzyć,
że w wyniku otrzymane zostanie równanie boolowskie. W takim przypadku
równanie to jest zapisywane do pliku wynikowego jako przypisanie do
zmiennej tymczasowej. Dla uproszczenia generacji w sytuacji, gdy tablica
jest indeksowana tylko jedną zmienną o nie znanej wartości, zmienna ta
także przypisywana jest do zmiennej tymczasowej. Jeśli taki element
występuje po prawej stronie przypisania to zamiast leksemów opisujących
element tablicy tworzone jest równanie opisujące wszystkie możliwe
warianty takiego elementu:
tab(i),
zastąpione zostanie na:
(tab(0)&(i=0)) | (tab(1)&(i=1)) | ... |
(tab(n)&(i=n)),
gdzie (i=0),(i=1), ..., (i=n) są to porównania zmiennej indeksowej
z odpowiednimi wartościami. Takie postępowanie dotyczyć będzie
wszystkich elementów tablicowych w wyrażeniu. Dla przykładu z prostym
przypisaniem:
...
type myint is range 0 to 7;
type mytype is array (0 to 2) of myint;
72
signal tab : mytype;
signal mb : myint;
signal i
: integer range 0 to 2;
begin
mb <= tab(i);
end;
zostanie wygenerowany następujący zbiór równań:
tmp__id_242_(0)=i(0);
tmp__id_242_(1)=i(1);
tmp__id_242_(2)=i(2);
mb(0)=(tab(0,0)&((!tmp__id_242_(0)&!tmp__id_242
_(1))&!tmp__id_242_(2)))|(tab(1,0)&((!tmp__id_2
42_(0)&!tmp__id_242_(1))&tmp__id_242_(2)))|(tab
(2,0)&((!tmp__id_242_(0)&tmp__id_242_(1))&!tmp_
_id_242_(2)));
mb(1)=(tab(0,1)&((!tmp__id_242_(0)&!tmp__id_242
_(1))&!tmp__id_242_(2)))|(tab(1,1)&((!tmp__id_2
42_(0)&!tmp__id_242_(1))&tmp__id_242_(2)))|(tab
(2,1)&((!tmp__id_242_(0)&tmp__id_242_(1))&!tmp_
_id_242_(2)));
mb(2)=(tab(0,2)&((!tmp__id_242_(0)&!tmp__id_242
_(1))&!tmp__id_242_(2)))|(tab(1,2)&((!tmp__id_2
42_(0)&!tmp__id_242_(1))&tmp__id_242_(2)))|(tab
(2,2)&((!tmp__id_242_(0)&tmp__id_242_(1))&!tmp_
_id_242_(2)));
mb(3)=(tab(0,3)&((!tmp__id_242_(0)&!tmp__id_242
_(1))&!tmp__id_242_(2)))|(tab(1,3)&((!tmp__id_2
42_(0)&!tmp__id_242_(1))&tmp__id_242_(2)))|(tab
(2,3)&((!tmp__id_242_(0)&tmp__id_242_(1))&!tmp_
_id_242_(2)));
Podobną sytuację można zaobserwować, gdy następuje przypisanie do
elementu tablicy o nie określonym indeksie. W takim przypadku
generowane są równania dla wszystkich elementów tablicy, a do wyniku
przypisania (prawej strony) dodawany jest warunek analogiczny, jak we
wcześniej przedstawionym przykładzie. Równania dla prostego przypisania:
tab(i) <= mb;
mają następującą strukturę:
tab(0)=mb&(i=0);
tab(1)=mb&(i=1);
...
tab(n)=mb&(i=n);
gdzie (i=0),(i=1), ..., (i=n) są to porównania zmiennej indeksowej
z kolejnymi wartościami z zakresu indeksów tablicy. Dla przykładu (typy
danych takie same jak wcześniej):
tab(i) <= mb;
generator równań stworzy następujący blok:
73
tmp__id_242_(0)=i(0);
tmp__id_242_(1)=i(1);
tmp__id_242_(2)=i(2);
tab(0,0)=mb(0)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&!tmp__id_242_(2));
tab(0,1)=mb(1)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&!tmp__id_242_(2));
tab(0,2)=mb(2)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&!tmp__id_242_(2));
tab(0,3)=mb(3)&((!tmp__id_242_(0)&!tmp__id_242_
(1))&!tmp__id_242_(2));
tab(1,0)=mb(0)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&tmp__id_242_(2));
tab(1,1)=mb(1)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&tmp__id_242_(2));
tab(1,2)=mb(2)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&tmp__id_242_(2));
tab(1,3)=mb(3)&((!tmp__id_242_(0)&!tmp__id_242_(
1))&tmp__id_242_(2));
tab(2,0)=mb(0)&((!tmp__id_242_(0)&tmp__id_242_(1
))&!tmp__id_242_(2));
tab(2,1)=mb(1)&((!tmp__id_242_(0)&tmp__id_242_(1
))&!tmp__id_242_(2));
tab(2,2)=mb(2)&((!tmp__id_242_(0)&tmp__id_242_(1
))&!tmp__id_242_(2));
tab(2,3)=mb(3)&((!tmp__id_242_(0)&tmp__id_242_(1
))&!tmp__id_242_(2));
W przypadku gdy elementy tablicy o nie określonych indeksach znajdują
się zarówno po lewej, jak i po prawej stronie przypisań, generowane jest
oczywiście złożenie obu przypadków.
Przedstawione przykłady dotyczą bardzo prostych przypisań, jednakże
zasada konstrukcji równań jest taka sama również i dla bardziej złożonych
przypisań.
4. PODSUMOWANIE
Zaprezentowany element kompilatora umożliwia generowanie równań
boolowskich dla przypisań z występującymi odwołaniami do tablic.
Poprawność
zaprezentowanego
algorytmu
potwierdzają
badania
symulacyjne. Testowanie odbywa się za pomocą specjalnie do tego celu
stworzonego narzędzia. Dzięki takim równaniom możliwa jest
minimalizacja funkcji logicznych i ich weryfikacja dostępnymi metodami.
Synteza układów logicznych na bazie wygenerowanych równań może
odbywać się za pomocą narzędzi programowania urządzeń (ASIC, FPGA).
74
LITERATURA
[1] IEEE Std 1076-1993: VHDL’93. IEEE Standard VHDL Language Reference Manual,
The Intitute of Electrical and Electronic Engineers, Inc., 1994,
[2] Synopsys: FPGA Express, VHDL Referance Manual, 1997,
[3] W. Bielecki, S. Hyduke: Kompilator języka VHDL do syntezy układów logicznych,
Materiały II krajowej konferencji naukowej RUC’99, Szczecin 14-16 marca 1999,
[4] M. Liersz „Kompilator VHDL dla instrukcji równoczesnego przypisania sygnałów
w celu syntezy cyfrowych układów logicznych” Sesja naukowa Instytutu Informatyki
15.01.1999,
[5] M. Liersz: „Kompilacja wyrażeń języka VHDL w kompilatorze do syntezy układów
logicznych”, Materiały II krajowej konferencji naukowej RUC’99,
Szczecin 14-16 marca 1999,
[6] R. Drążkowski, „Analiza leksykalna i syntaktyczna podzbioru języka VHDL użytego do
generacji wyrażeń boolowskich”, Materiały II krajowej konferencji naukowej RUC’99,
Szczecin 14-16 marca 1999,
[7] P. J. Ashenden: The Designer’s Guide to VHDL, Morgan Kaufmann Publishers, Inc.,
San Fransisco 1996
Algorytmy generacji równań boolowskich operacji
mnożenia całkowitych liczb binarnych
Tomasz Wierciński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł przedstawia analizę wybranych metod realizacji operacji
mnożenia. Ukazuje ich rozwój potrzebny do zastosowania
w programowalnych układach cyfrowych dotyczący przede wszystkim
sposobu generacji wyniku w postaci łatwo optymalizowalnych
i przetwarzalnych na postać bramek logicznych równań boolowskich.
Wreszcie opisane są dwa algorytmy operacji mnożenia, z których jeden
dotyczy przypadku gdy oba argumenty są zmiennymi całkowitymi
o wartościach nie znanych a drugi – przypadku kiedy jednym z argumentów
jest wartość stała. Wynikiem obu algorytmów jest iloczyn wygenerowany
w postaci zbioru równań logicznych. Artykuł porusza także kwestię potrzeby
rozróżnienia opisanych dwóch przypadków oraz optymalność tych rozwiązań
tzn. ilości generowanych równań oraz czasu kompilacji.
Słowa kluczowe:
VHDL, układy logiczne, syntezy, równania boolowskie
1.
WSTĘP
Opisane w niniejszej pracy algorytmy mnożenia powstały dla potrzeb
kompilatora języka VHDL tworzonego w Katedrze Technik Programowania
Wydziału Informatyki Politechniki Szczecińskiej. Język VHDL służy do
projektowania, syntezy i symulacji specjalizowanych układów logicznych
FPGA (ang. Field Programmable Gate Arrays). Układy te stanowią nowe
podejście do tworzenia systemów cyfrowych umożliwiające umieszczenie
w jednej strukturze logicznej całego skomplikowanego systemu. Kompilator,
na podstawie źródeł w języku VHDL, generuje kod wynikowy w postaci
76
zbioru równań boolowskich opisujących projektowany układ. Rozwiązanie
to zostało przyjęte z powodu zalet jakie posiadają równania logiczne, tzn.:
– możliwości optymalizacji prowadzącej do zmniejszenia ilości elementów
projektowanego układu a tym samym obniżenia jego kosztów i
przyśpieszenia działania,
– możliwości bezpośredniego przetworzenia na postać bramek logicznych,
a następnie zawarcia w układzie scalonym FPGA.
Algorytmy dotyczą operacji mnożenia argumentów typu całkowitego. Do
obliczeń używane są poszczególne bity tych argumentów a wynikiem jest
zbiór równań logicznych opisujący każdy bit zmiennej wynikowej. W
trakcie kompilacji generowane są również równania pośrednie, do tworzenia
których wykorzystywane są zmienne tymczasowe. Maksymalna ilość bitów
na jakiej można zapisać wartość całkowitą wynosi 32.
Istnieje szereg metod mnożenia liczb binarnych. W niniejszej pracy
zostaną omówione dwie z nich. Bezpośrednie zastosowanie tych metod do
budowy algorytmów rozpatrywanych w tym przypadku nie jest jednak
możliwe. Ponieważ mamy tu do czynienia z kompilatorem, należy w taki
sposób je zmodyfikować aby możliwe było wykonywanie działań na
zmiennych o wartościach nieznanych w momencie kompilacji czyli w
sposób symboliczny. Najważniejszym jednak zadaniem jest uzupełnienie
opisywanych metod o konieczność generowania wyniku w postaci równań
boolowskich.
2.
MNOŻENIE LICZB CAŁKOWITYCH
W ZAPISIE DWÓJKOWYM
Mnożenie liczb polega na ogół na wykonaniu ciągu kolejnych dodawań i
przesunięć, których liczba i kolejność w jakiej występują, uzależniona jest
od wartości cyfr mnożnika. Podstawową metodą realizacji mnożenia jest
algorytm sekwencyjny polegający na wyliczeniu kolejnych iloczynów
częściowych xiA mnożnej (multiplicand) A = {a0,...,am} oraz i-tej cyfry xi
mnożnika (multiplier) X = {x0,...,x-m}, a następnie ich zsumowaniu [1].
Wykonanie mnożenia według powyższej zależności polega na wielokrotnym
dodawaniu odpowiednio przesuniętej mnożnej w zależności od wartości
bitów mnożnika [2] (rysunek 2.1).
77
am
…
a
a
xm
…
x
x
ax
ax
amx
.
1
0
0 0
ax
1 1
1
0
1 0
ax
amx
amxm
0
1
0 1
.
.
a xm
a xm
0
1
amxm amx1+…+ a xm amx +…+ a x + a x m … a x + a x
1
1
0 1
0
1 1
0
ax
0 0
Rysunek 2.1. Sekwencyjna metoda mnożenia
Modyfikacją tego algorytmu jest dodawanie do kolejno obliczanych sum
częściowych Si przesuniętych o i pozycji w lewo wielokrotności xiA mnożnej
A przez i-tą cyfrę xi mnożnika X. Przesunięcie to odpowiada wadze i-tej
pozycji mnożnika oraz i-tego iloczynu częściowego, równej βι [2,
4](rysunek 2.2). Zgodnie z tym algorytmem dodaj – przesuń (add – and –
shift) kolejnymi sumami częściowymi są
Si+1 = Si + xi β i A,
2.1
i = 0,1,...,m − 1,
S1 = 0.
Mnożenie liczb ze znakiem
Adaptacja algorytmu sekwencyjnego do mnożenia liczb znakowych w
kodach uzupełnieniowych ZU1 i ZU2 wymaga zastosowania pewnych
modyfikacji. Pierwszą z nich jest użycie do tworzenia sumy częściowej
rozszerzenia znakowego mnożnej oraz poprzedniej sumy częściowej.
Oznacza to, iż powstałą w kolejnym kroku sumowania poprzedniej sumy Si1 oraz przesuniętej o odpowiednią pozycję mnożnej, sumę częściową Si
należy uzupełnić z lewej strony znakiem mnożnej. Drugą modyfikację
stosuje się w przypadku ujemnego mnożnika. Polega ona na odjęciu
mnożnej pomnożonej przez wagę najstarszej pozycji mnożnika (bit znaku)
od poprzedniej sumy częściowej[2]. Stosując powyższe modyfikacje
musimy rozważyć cztery przypadki [3]:
– kiedy mnożna i mnożnik są dodatnie algorytm sekwencyjny nie ulega
zmianie;
78
– gdy mnożna jest ujemna, a mnożnik dodatni, uzupełniamy sumę
częściową bitem znaku mnożnej;
– kiedy mnożna jest dodatnia, a mnożnik ujemny odejmujemy od
dotychczasowej sumy częściowej mnożną pomnożoną przez wagę bitu
znaku mnożnika;
– gdy oba argumenty są ujemne, wykonujemy czynności opisane w
punkcie 2 i 3.
am
…
a
a
xm
…
x
x
ax
ax
amx
1
1
0
1 0
amx
ax
ax
s
s
s
amxm
a xm
.
.
a xm
s
s
s
1
0m
mm
1
m3
1 1
0
0
0 0
0 1
02
01
s
00
.
0
m2
s
m1
s
m0
Rysunek 2.2. Zmodyfikowany algorytm mnożenia
3.
OPIS ALGORYTMÓW
Do realizacji funkcji mnożenia zostały opracowane dwa algorytmy.
Stanowią one rozszerzenie algorytmu sekwencyjnego dla liczb w kodzie
ZU2. Pierwszy z nich (Algorytm 3.1) wykonuje operację mnożenia dwóch
argumentów, z których jeden jest zmienną o nieznanej wartości, a drugi stałą
policzalną. Natomiast argumentami drugiego algorytmu (Algorytm 3.2) są
zmienne nieznane w momencie kompilacji. Podział ten został wprowadzony
ze względu na możliwość optymalizacji wyniku w pierwszym przypadku.
Optymalizacja ta polega zarówno na zmniejszeniu ilości generowanych
równań jak i skróceniu czasu obliczeń.
Znajomość wartości jednego z argumentów pozwala na wygenerowanie
mniejszej ilości równań, co z kolei wpływa na zmniejszenie złożoności
układu realizującego tą operację. Korzyść zastosowanej metody uwidacznia
się w przypadku gdy stała (mnożnik) zawiera pewien ciąg bitów o
wartościach zerowych. Wówczas zamiast generować równania dla sum
79
częściowych gdzie dodawana jest mnożna pomnożona przez wartość 0
mnożnika do poprzedniej sumy częściowej oraz równania przesuwające bity
nowo powstałej sumy częściowej, zapamiętuje się ilość przesunięć i
realizuje je dopiero w przypadku napotkania wartości 1 mnożnika. Podejście
takie nie zmienia wartości wyniku.
W drugim algorytmie nie znamy wartości żadnego z argumentów dlatego
nie możemy opuścić zbędnych obliczeń dla bitów zerowych. Należy zatem
wygenerować równania dla wszystkich bitów mnożnika przez co zwiększa
się ilość równań, a więc rośnie wielkość sprzętu przeznaczonego do
wykonywania tej operacji.
Gdy ilość bitów sumy częściowej przekracza zakres 32 zostaje ona
obcięta do maksymalnej wartości zakresu. W tym przypadku konieczne jest
przesunięcie mnożnej w lewo o wartość przekraczającą zakres wyniku przed
każdym dodaniem jej do sumy częściowej.
Algorytm 3.1.
1. Zamień argumenty tak aby mnożnik był wartością stałą
2. Jeżeli wartość mnożnika op2Val wynosi 0 to wynik podstaw 0
result = 0
3. Jeżeli wartość mnożnika wynosi 1 to wynik podstaw wartość mnożnej
op1
result = op1
4. Jeżeli wartość mnożnika wynosi –1 to wynik jest równy wartości
mnożnej z przeciwnym znakiem
result = !op1 + 1
5. W przeciwnym wypadku
5.1. Ilość bitów mnożnej wynosi op1BitsNr
5.2. Ilość bitów mnożnika wynosi op2BitsNr
5.3. Ilość przesunięć sumy częściowej shrNr ustaw na zero
shrNr = 0
5.4. Początkowe obcięcie mnożnej op1Shl wynosi op1BitsNr-1
op1Shl = op1BitsNr-1
5.5. Obliczaj dla i-tego bitu mnożnika op2BitsNr-1 >= i > 0
5.5.1. Jeżeli wartość i-tego bitu mnożnika wynosi 0 to zwiększ ilość
przesunięć shrNr
shrNr++
5.5.2. W przeciwnym razie jeżeli i-ty bit mnożnika wynosi 1
5.5.2.1. Jeżeli jest to pierwsza jedynka
5.5.2.1.1. Jeżeli nie było przesunięć (shrNr = 0) to suma częściowa jest
równa wartości mnożnej
5.5.2.1.2. W przeciwnym razie
80
5.5.2.1.2.1. Jeżeli suma op1BitsNr+shrNr nie przekracza wartości 32 to jako
wartość sumy częściowej podstaw mnożną przesuniętą o ilość
shrNr w lewo i uzupełnioną bitami o wartościach zero
a) Ilość bitów sumy częściowej wynosi
outBitsNr = op1BitsNr + shrNr
5.5.2.1.2.2. W przeciwnym wypadku jeśli op1BitsNr+shrNr > 32
a) Jako wartość sumy częściowej podstaw mnożną pomniejszoną o liczbę
(op1BitsNr+shrNr)-32 najstarszych bitów i uzupełnioną z prawej strony
shrNr bitami zerowymi
b) Ilośc przesunięć op1Shl wynosi
op1Shl = 32 - shrNr
c) Ilość bitów sumy częściowej wynosi
outBitsNr = 32
5.5.2.1.3. Ilość przesunięć shrNr ustaw na jeden
5.5.2.2. W przeciwnym razie
5.5.2.2.1. Jeżeli były jakieś przesunięcia (shrNr <> 0)
5.5.2.2.1.1. Jeżeli suma outBitsNr+shrNr przekracza wartość 32 to
a) Uwzględnij w sumie częściowej przesunięcia shrNr pomniejszone o
wartość (outBitsNr+shrNr)-32
b) Przesuń obcięcie op1Shl
op1Shl = op1Shl – (shrNr + outBitsNr– 32)
c) Ilość bitów sumy częściowej wynosi 32
5.5.2.2.1.2. W przeciwnym razie
a) Przesuń sumę częściową o wartość shrNr
b) Ilość bitów sumy częściowej wynosi
outBitsNr = outBitsNr + shrNr
5.5.2.2.2. Dodaj do sumy częściowej mnożną op1 obciętą o wartość
op1Shl
tmpRes = tmpRes + op1[0, op1Shl]
5.5.2.2.3. Ilość przesunięć shrNr wynosi 1
5.6. Przesuń sumę częściową jeżeli shrNr różne od zera i uzupełnij bitem
znaku mnożnej
5.6.1. Jeżeli outBitsNr + shrNr > 32
5.6.1.1. shrNr = outBitsNr + shrNr – 32
5.6.1.2. Obcięcie op1Shl = op1Shl - (shrNr + outBitsNr - 32)
5.7. Jeśli mnożnik jest ujemny
5.7.1. Wyznacz wartość przeciwną mnożnej uwzględniając obcięcie
op1Shl
unOp1 = !op1[0, op1Shl] + 1
5.7.2. Dodaj do sumy częściowej wartość unOp1
tmpRes = tmpRes + unOp1
5.8. Wynik result = tmpRes
81
Algorytm 3.2.
1. Ilość bitów mnożnej op1 jest równa op1BitsNr
2. Ilość bitów mnożnika op2 jest równa op2BitsNr
3. Jeżeli ilość bitów mnożnej jest mniejsza od 32 to
3.1. Wartość początkowa sumy częściowej jest równa wartości mnożnej
powiększonej o bit znaku pomnożonej przez najmłodszy bit mnożnika
tmpRes = (op1Sign_op1) & op2(0)
3.2. Ilość bitów sumy częściowej wynosi
outBitsNr = op1BitsNr + 1
3.3. Przesunięcie mnożnej op1Shl = op1BitsNr - 1
4. W przeciwnym wypadku
4.1. Wartość sumy jest równa mnożnej pomnożonej przez najmłodszy bit
mnożnika
tmpRes = op1 & op2(0)
4.2. Ilość bitów sumy wynosi
outBitsNr = op1BitsNr
4.3. Przesuniecie mnożnej op1Shl = op1BitsNr - 2
5. Obliczaj dla i-tego bitu mnożnika 1<= i < op2BitsNr-1
5.1. Jeżeli ilość bitów wyniku przekracza wartość 32 to zmniejsz ilość bitów
mnożnej
op1Shl-5.2. Jeśli op1Shl > 0
5.2.1. Pomnóż mnożną przez i-ty bit mnożnika
mulOp1 = op1[0, op1Shl] & op2(i)
5.2.2. Dodaj do sumy częściowej mnożną pomnożoną przez i-ty bit
mnożnika i przesuniętą o wartość op1Shl
tmpRes = tmpRes + mulOp1
5.3. W przeciwnym wypadku przerwij wykonywanie pętli
5.4. Jeżeli ilość bitów sumy jest mniejsza od wartości 32 to uzupełnij sumę
bitem znaku mnożnej pomnożonym przez sumę logiczną bitów
mnożnika od 0 do i
tmpRes = (op1Sign & (op2(0) | ... | op2(i)))_tmpRes
6. Jeżeli ilość bitów sumy przekracza wartość 32 to obetnij liczbę bitów
mnożnej
op1Shl-7. Jeżeli op1Shl > 0
7.1. Wyznacz wartość przeciwną do wartości mnożnej w zakresie [0,
op1Shl]
unOp1 = !op1[0, op1Shl]+1
7.2. Pomnóż unOp1 przez bit znaku mnożnika
mulUnOp1 = unOp1 & op2(op2BitsNr-1)
7.3. Dodaj do sumy częściowej wartość mulUnOp1
82
tmpRes = tmpRes + mulUnOp1
8. Sprawdź czy mnożnik różny od zera
notEqu = op2(0)|...|op2(op2BitsNr-1)
9. Wynik jest równy
result = notEqu & tmpRes
4.
WYNIKI DZIAŁANIA PROGRAMÓW
OPARTYCH NA POWYŻSZYCH
ALGORYTMACH
Ze względu na przeznaczenie algorytmów główne znaczenie pod
względem optymalności mają dwa kryteria: ilość równań jakie zostaną
wygenerowane oraz czas ich generacji.
4.1
Oszacowanie ilości równań w zależności od liczby
bitów argumentów
Dla algorytmu 3.2 ilość wygenerowanych równań zależy od ilości bitów
argumentów w sposób logarytmiczny (O(log2n)) gdy ilość bitów wyniku
przekracza zakres 32 i w związku z tym wynik jest obcinany w każdym
kroku do maksymalnego zakresu. Z każdym krokiem zmniejsza się różnica
między ilością równań w bieżącym i poprzednim kroku. Dzieje się tak
dlatego, że nie jest konieczna generacja równań dla odrzucanych bitów a
jedynie dla pozostałej części, która zmniejsza się w każdym kroku.
Zależność tę można przedstawić wzorem
R0 = P
R1 = R0 + ∆ 1
R 2 = R1 + ∆ 2 , gdzie ∆ 2 = ∆ 1 − S
R3 = R 2 + ∆ 3 , gdzie ∆ 3 = ∆ 2 − S = ∆ 1 − 2 S
L
R n = R n −1 + ∆ n , gdzie ∆ n = ∆ n −1 − S = ∆ 1 − (n − 1)S
Ri – ilość równań dla i-tej liczby bitów
P- ilość równań dla najmniejszej liczby bitów
∆ι − różnica liczby bitów w kroku i-tym oraz i-1-wszym
S – liczba o jaką zmniejsza się różnica ∆ w każdym kroku
83
Jeżeli wynik mnożenia nie przekracza zakresu to ilość równań wzrasta
wykładniczo (O(2n)) w przypadku zmiany zakresu mnożnika
Rn = Rn−1 + ∆ n , gdzie ∆ n = ∆ n−1 + S = ∆1 + (n − 1)S
oraz liniowo (O(n)) w przypadku zmiany zakresu mnożnej
Rn = Rn−1 + ∆1
W przypadku algorytmu 3.1 ilość wygenerowanych równań nie zależy
tyle od ilości bitów argumentów, co od wartości mnożnika a ściślej mówiąc
od ilości bitów ‘0’. Dla przykładu mnożenie argumentu 4 bitowego przez
wartość –65535, której reprezentacja bitowa ma postać 10000000000000001
a więc składa się z 17 bitów, z których tylko dwa mają wartość ‘1’, rozmiar
wygenerowanego pliku z równaniami wynosi około 4 kB. Jeżeli wartość
mnożnika zastąpimy zmienną o jednakowej ilości bitów i wykonamy
mnożenie przy użyciu algorytmu 3.2 to ilość wygenerowanych równań
wzrośnie do około 40 kB czyli dziesięciokrotnie. Mnożąc zmienną 4 bitową
przez wartość –65533 (17 bitów w tym trzy o wartościach ‘1’) otrzymamy
plik o rozmiarze około 5 kB.
4.2
Czas kompilacji
Czas wykonywania operacji mnożenia zmiennej 4 bitowej przez zmienną
17 bitową według algorytmu 3.2 stanowi około 20% całego czasu
kompilacji. Mnożąc zmienną 4 bitową przez wartość –665535
(10000000000000001) przy użyciu algorytmu 3.1 uzyskujemy 10-cio krotne
przyspieszenie. Jeżeli poprzednią wartość zastąpimy wartością 65535
(01111111111111111), czas generacji równań wydłuża się do około 20%
czyli wartości zbliżonej do tej z algorytmu 3.2. Wynika stąd, że dla takiego
przypadku, zgodnie z założeniami opisanymi wyżej, nie uzyskujemy
żadnego przyspieszenia.
5.
PODSUMOWANIE
W artykule zostały przedstawione algorytmy mnożenia generujące wynik
w postaci zbioru równań boolowskich. Zagadnienie to jest dedykowane
kompilatorowi języka VHDL służącemu do symulacji i syntezy
programowalnych układów cyfrowych FPGA. Sprecyzowany został także
84
powód rozróżnienia algorytmów ze względu na rodzaj argumentów.
Podejście to okazało się słusznym i w pełni uzasadnionym gdyż pozwoliło,
w przypadku znanej wartości jednego z argumentów, osiągnąć znaczne
zmniejszenie liczby generowanych równań oraz przyśpieszyć pracę
kompilatora.
LITERATURA
[1] Bolesław Pochopień: Arytmetyka Systemów Cyfrowych, Wydawnictwo Politechniki
Śląskiej, Gliwice 1997
[2] Janusz Biernat: Arytmetyka Komputerów, Wydawnictwo Naukowe PWN,
Warszawa 1996
[3] Charles H. Roth, Jr.: Digital Systems Design Using VHDL,
PWS Publishing Company 1998
[4] IEEE standard VHDL Language Reference Manual, IEE std 1076-1993, The Institute
of Electrical and Electronic Engineers Inc., 1994
Algorytm generacji równań boolowskich operacji
dzielenia dla syntezy układów logicznych
Tomasz Wierciński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł przedstawia analizę krytyczną istniejących metod realizacji
operacji dzielenia. Ukazuje ich rozwój potrzebny do zastosowania
w reprogramowalnych układach cyfrowych dotyczący przede wszystkim
sposobu generacji wyniku w postaci łatwo optymalizowalnych
i przetwarzalnych na postać bramek logicznych równań boolowskich.
Wreszcie opisany jest algorytm operacji dzielenia dwóch zmiennych
całkowitych, którego wynikiem jest iloraz lub reszta z dzielenia
wygenerowane w postaci zbioru równań logicznych. Artykuł porusza
także kwestię optymalności takiego rozwiązania tzn. ilości generowanych
równań oraz czasu kompilacji.
Słowa kluczowe:
VHDL, układy logiczne, syntezy, równania boolowskie
1.
WSTĘP
Opisany w niniejszej pracy algorytm dzielenia powstał dla potrzeb
kompilatora języka VHDL tworzonego w Katedrze Technik Programowania
Wydziału Informatyki Politechniki Szczecińskiej. Kompilator zawiera
nowatorskie rozwiązanie dotyczące generacji kodu wynikowego, który ma
postać pliku równań boolowskich opisujących projektowany układ. Istnieją
dwa podstawowe powody zastosowania tego typu rozwiązania:
– równania boolowskie mogą być w łatwy sposób optymalizowane, co
zmniejsza ilość elementów układu a tym samym obniża jego koszty
i przyśpiesza działanie,
86
– równania takie można w prosty sposób przetworzyć na postać bramek
logicznych, a następnie zawrzeć w układzie scalonym FPGA.
Algorytm dotyczy operacji dzielenia argumentów typu całkowitego.
Mogą to być zarówno stałe jak i zmienne o wartościach nie znanych w
momencie kompilacji. Do obliczeń używane są poszczególne bity tych
argumentów a wynikiem jest zbiór równań opisujący każdy bit zmiennej
wynikowej. W trakcie kompilacji generowane są również równania pośrednie,
do tworzenia których wykorzystywane są zmienne tymczasowe. Maksymalna
ilość bitów na jakiej można zapisać wartość całkowitą wynosi 32.
Na podstawie analizy stanu problemu można stwierdzić, że znane są
metody wykonywania dzielenia na liczbach binarnych. Bezpośrednie
zastosowanie tych metod do budowy algorytmu rozpatrywanego w tym
przypadku nie jest jednak możliwe. Ponieważ mamy tu do czynienia z
kompilatorem, należy w taki sposób je zmodyfikować aby możliwe było
wykonywanie działań na zmiennych o wartościach nieznanych w momencie
kompilacji czyli w sposób symboliczny. Najważniejszym jednak zadaniem
jest uzupełnienie opisywanych metod o konieczność generowania wyniku w
postaci równań boolowskich.
2.
DZIELENIE LICZB CAŁKOWITYCH
W ZAPISIE DWÓJKOWYM
Dzielenie realizowane jest zwykle przez wykonywanie kolejnych
odejmowań i przesunięć lub też dodawań i odejmowań. Wynikiem dzielenia
dzielnej X (dividend) przez dzielnik Y (divisor) są: iloraz Z (quotient) i reszta
R (remainder), przy tym
X = YZ + R
lub
X
R
=Z + ,
Y
Y
R < Z.
87
2.1
Metody odtwarzające (restytucyjne)
Dzielenie odtwarzające lub restytucyjne (restoring division) jest oparte na
jednej z poniżej opisanych metod i w każdym kroku wymaga wykonania
jednej lub dwóch operacji arytmetycznych:
– odjęcia i ewentualnego dodania dzielnika;
– porównania i ewentualnego odjęcia dzielnika.
W metodzie porównawczej, w celu wyznaczenia ilorazu
Z=
X
Y
porównywane są z modułem dzielnika przesunięte odpowiednio reszty
częściowe. Jeżeli podwojona poprzednia reszta częściowa jest mniejsza od
dzielnika, to kolejną cyfrą ilorazu jest 0. W przeciwnym razie kolejną cyfrą
ilorazu jest 1, a w celu otrzymania nowej reszty należy odjąć dzielnik od
podwójnej poprzedniej reszty częściowej [1,2] (rysunek 2.1).
Moduł reszty początkowej stanowi moduł dzielnej, tzn. :
r0 = X
Kolejne reszty częściowe - pierwsza, druga itd. określają zależności:
r1 = 2 r0 − z1 Y ,
r2 = 2 r1 − z 2 Y ,
M
ri = 2 ri −1 − z i Y ,
przy czym i-ta cyfra ilorazu:
1 gdy 2 ri −1 ≥ Y
zi = 
0 gdy 2 ri −1 < Y
Obliczenia prowadzi się aż do uzyskania reszty równej zero lub otrzymania
żądanej ilości cyfr ilorazu.
Inną metodą jest odjęcie dzielnika od podwojonej poprzedniej reszty
częściowej i badanie znaku wyniku. Jeśli znak jest dodatni, to kolejną cyfrą
88
ilorazu jest 1, a obliczona reszta częściowa jest poprawna. Jeśli wynik jest
ujemny, to kolejną cyfrą ilorazu jest 0 i konieczne jest odtworzenie
poprawnej reszty przez dodanie dzielnika [2].
i
N
Zi
Ri+1
0
R<Y
i
Zi
Ri+1
1;
2(Ri -Y)
i
N
T
0;
2Ri
inc i
i=n
T
Koniec obliczania
cyfr ilorazu Z
Rysunek 2.3. Algorytm dzielenia metodą odtwarzającą
2.2
Metoda nieodtwarzająca (nierestytucyjna)
W metodzie nierestytucyjnej kolejne cyfry zi ilorazu Z określa się
realizując dodawanie lub odejmowanie dzielnika Y od reszty częściowej ri w
zależności od znaku poprzedniej reszty częściowej ri-1. Równanie opisujące
sposób tworzenia kolejnych cyfr ilorazu oraz reszt częściowych można
przedstawić w następujący sposób [2]:
ri +1 = [2ri − Y ] + (1 − z i +1 )Y = {2[2ri −1 − Y ] + (1 − 2 z i )Y } + (1 − z i +1 )Y .
Wynika stąd, że jeżeli ri=2ri-1-Y jest niepoprawną resztą częściową, co
oznacza, że zi=0, to w kolejnym kroku należy obliczyć resztę chwilową
89
ri +1 = 2(2ri −1 ) − Y = 2(2ri −1 − Y ) + Y
i zamiast wykonywać korekcję i-tej reszty i w kolejnym kroku algorytmu
znów odejmować dzielnik od podwojonej poprzedniej reszty, można opuścić
korekcję, a w kolejnym (i+1) kroku do niepoprawnie obliczonej poprzedniej
reszty dodać dzielnik. Zatem wartość kolejnej cyfry ilorazu można odnieść
do znaku obliczonej reszty częściowej, a kolejną resztę wyznaczyć, zależnie
od wartości poprzedniej cyfry ilorazu, jako ri+1=2ri-Y, gdy zi=1 lub
ri+1=2ri+Y, gdy zi=0 (rysunek 2.2)[2].
Ri
N
Zi
Ri+1
i
1
2Ri-1 -Y
R<0
i
Zi
0;
Ri+1 2Ri +Y
1;
2Ri -Y
i
N
T
inc i
i=n
T
Koniec obliczania
cyfr ilorazu Z
Rysunek 4.2. Algorytm dzielenia metodą nieodtwarzającą
3.
KRYTYKA ISTNIEJĄCYCH METOD
Powyżej zostały przedstawione dwie podstawowe metody dzielenia liczb
dwójkowych. Żadna z nich nie może być jednak bezpośrednio wykorzystana
do budowy algorytmu o założonych cechach. Zastosowanie metody
nieodtwarzającej powoduje generowanie nadmiernej ilości równań, które
powinny obejmować zarówno odejmowanie jak i dodawanie dzielnika do
przesuniętej odpowiednio reszty częściowej w każdej iteracji. Konieczne jest
90
to gdyż nie znając wartości argumentów nie jesteśmy w stanie wybrać
odpowiedniej drogi w algorytmie. W drugiej metodzie (odtwarzającej)
wystarczy wygenerować równania dla odejmowania, które są również
wykorzystywane do porównania dzielnika z resztą częściową. Rozpatrując
bit znaku wyniku odejmowania ustalana jest wartość i-tego bitu ilorazu oraz
nowa reszta częściowa. Do realizacji naszego algorytmu nie można jednak
wykorzystać bezpośrednio metody odtwarzającej z faktu, iż nie jest znana
rzeczywista ilość bitów na których jest przechowywana wartość dzielnika.
Metoda ta nie gwarantuje w tym przypadku otrzymania poprawnej ilości
bitów wyniku oraz poprawnych ich wartości. Aby możliwa była adaptacja
metody porównawczej do realizacji opisywanego algorytmu konieczna jest
jej modyfikacja. Polega ona na uzupełnieniu dzielnej bitami o wartościach
zero, których ilość jest równa ilości bitów modułu dzielnika. Zapewnia to
otrzymanie zerowego bitu znaku oraz poprawnej wartości bitów modułu
wyniku. Prawidłowa będzie także ilość bitów wyniku równa ilości bitów
dzielnej.
4.
ALGORYTM DZIELENIA
Proces dzielenia wykonywany jest dla wartości dodatnich toteż, przed
jego wywołaniem, konieczna jest generacja równań zmieniająca w pierwszej
fazie znak argumentów a w fazie końcowej, korygująca wynik zgodnie z
tabelą 4.1. Ilość bitów ilorazu jest równa ilości bitów dzielnej a ilość bitów
reszty pokrywa się z ilością bitów dzielnika.
Znak dzielnej
+
+
-
Znak dzielnika
+
+
-
Znak ilorazu
+
+
Znak reszty
+
+
-
Tabela 1. Znak ilorazu i reszty z dzielenia
Opisany algorytm dzielenia przedstawia się następująco:
1. Wygeneruj równania warunkowe zamieniające dzielną na dodatnią a
dzielnik na ujemny
1.1. Zamień dzielną X na kod ZU2
XZU2 = !X+1
1.2. Wygeneruj równanie tak aby nowa wartość dzielnej była dodatnia
tmpX = X & !signX | XZU2 & signX
gdzie signX jest bitem znaku dzielnej
91
1.3. Zamień dzielną Y na kod ZU2
YZU2 = !Y + 1
1.4. Wygeneruj równanie tak aby nowa wartość dzielnika była ujemna
tmpY = Y & signY | YZU2 & !signY
gdzie signY jest bitem znaku dzielnika
2. Ilość bitów dzielnej jest równa XBitsNr
3. Ilość bitów dzielnika jest równa YBitsNr
4. Ilość bitów ilorazu ZBitsNr = XbitsNr
5. Ilość bitów reszty RBitsNr = YBitsNr
6. Najstarszy bit ilorazu (bit znaku) jest równy zero
Z[ZBitsNr-1] = 0
7. Początkowa wartość reszty
7.1. I-tym bitom reszty dla YbitsNr - 1 > i > 0 przypisz wartość 0
tmpR[i] = 0
7.2. Najmłodszy bit reszty tymczasowej ma wartość najstarszego bitu
dzielnej
tmpR[i] = tmpX[XBitsNr-1]
8. Obliczaj i-te bity ilorazu oraz reszty tymczasowe dla XBitsNr - 2 > i >= 0
8.1. Porównaj wartości reszty tymczasowej i dzielnika dodając je do siebie
addRes = tmpR + tmpY
8.2. Wygeneruj równania dla bitów nowej reszty tymczasowej
8.2.1. Jeżeli i > 0
8.2.1.1. Najmłodszy bit nowej reszty ma wartość i-tego bitu dzielnej
tmpR[0] = tmpX[i]
8.2.1.2. J-tym bitom nowej reszty dla 1<= j < YBitsNr - 1 przypisz
tmpR [j] = tmpR [j] & signAdd | addRes[j] & !signAdd
gdzie signAdd jest bitem znaku wyniku porównania
8.2.2. W przeciwnym razie
8.2.2.1. J-tym bitom nowej reszty dla 0<=j<YBitsNr przypisz
tmpR [j] = tmpR [j] & signAdd | addRes[j] & !signAdd
8.3. Wartość i-tego bitu wyniku jest przeciwna do wartości bitu znaku
porównania
Z[i] = !signAdd
9. Reszta z dzielenia jest równa tmpR
10. Skoryguj znak ilorazu i reszty zgodnie z wartościami bitów znaku
argumentów
10.1. Zamień iloraz na kod ZU2
ZZU2 = !Z + 1
10.2. Poprawna wartość ilorazu jest równa
Z = (signX & signY | !signX & !signY) & Z | (!signX & signY | signX
& !signY) & ZZU2
10.3. Zamień resztę na kod ZU2
92
RZU2 = !tmpR + 1
10.4. Poprawna wartość reszty jest równa
R = (tmpR & !signX) | (RZU2 & signX)
W szczególnym przypadku, gdy dzielnik jest wartością stałą, nie jest
konieczne rozszerzenie dzielnej zerowymi bitami o długość dzielnika, a
jedynie o jeden bit zerowy (bit znaku) [3].
5.
TESTOWANIE OPISANEGO ALGORYTMU
Ze względu na przeznaczenie algorytmu główne znaczenie pod
względem optymalności mają dwa kryteria: ilość równań jakie zostaną
wygenerowane oraz czas ich generacji czyli kompilacji.
5.1
Oszacowanie ilości równań w zależności od liczby
bitów argumentów
Na liczbę równań wygenerowanych w wyniku operacji dzielenia ma
wpływ ilość bitów, na której zapisane są jej argumenty. Rozpatrywana
zależność rośnie liniowo (z jedynie małym skokiem po przekroczeniu 12
bitów) zarówno w przypadku zmiany zakresu dzielnej jak i dzielnika (O(n)).
Oznacza to, że dla każdego bitu jest generowana jednakowa liczba równań.
Różna jest natomiast wartość tego przyrostu i jest on większy w przypadku
zmiany zakresu dzielnej. Zależności te przedstawia tabela 5.1.
5.2
Czas kompilacji
Czas kompilacji programu napisanego w języku VHDL, w którym
wykorzystywana jest operacja dzielenia, zależy tak jak w poprzednim
przypadku od ilości bitów dzielnika i dzielnej i jest on rzędu kilku sekund.
Natomiast czas kompilacji samej tylko funkcji dzielenia stanowi zaledwie około
1% czasu kompilacji całego programu. W porównaniu z innym podejściem do
realizacji tego zadania, polegającym na wykonaniu obliczeń z wykorzystaniem
funkcji kompilowanych, czas ten jest nawet ponad 100 razy krótszy.
6.
PODSUMOWANIE
Artykuł miał na celu przedstawienie algorytmu i realizacji operacji
dzielenia argumentów typu całkowitego dla potrzeb kompilatora języka
93
VHDL. Innowacją opisanej metody jest sposób generacji wyniku w postaci
zbioru równań boolowskich. Podejście takie umożliwia optymalne
zaprogramowanie struktury FPGA. Testy przeprowadzone dla opisanej
operacji potwierdzają słuszność takiego rozwiązania.
Rozmiar pliku wynikowego
Liczba bitów w bajtach
Dzielna <>
Dzielnik <>
Dzielnik 32 b Dzielna 32 b
2
28534
41251
3
45205
56949
4
61876
72647
5
78547
88345
6
95218
104043
7
111889
119741
8
128560
135439
9
145231
151137
10
161902
166835
11
178602
183075
12
195304
199564
13
212006
216053
14
228708
232542
15
245410
249031
16
262112
265520
17
278814
282009
Liczba bitów
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Rozmiar pliku wynikowego
w bajtach
Dzielna <>
Dzielnik <>
Dzielnik 32 b Dzielna 32 b
295516
298498
312218
314987
328920
331476
345622
347965
362324
364454
379026
380943
395728
397432
412430
413921
429132
430410
445834
446889
462536
463388
479238
479877
495940
496366
512642
512855
529344
529344
Tabela 2. Zależność ilości równań od liczby bitów argumentów
LITERATURA
[1] Bolesław Pochopień: Arytmetyka Systemów Cyfrowych, Wydawnictwo Politechniki
Śląskiej, Gliwice 1997
[2] Janusz Biernat: Arytmetyka Komputerów, Wydawnictwo Naukowe PWN,
Warszawa 1996
[3] IEEE standard VHDL Language Reference Manual, IEE std 1076-1993, The Institute
of Electrical and Electronic Engineers Inc., 1994
Przekład instrukcji if oraz case języka VHDL
do postaci równań boolowskich
Marcin Radziewicz
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł opisuje zagadnienia przekładu instrukcji if i case, do postaci równań
boolowskich. Przedstawione zostaną obie instrukcje, podstawowe zalety
zapisu w postaci równań, a później wszelkie aspekty przekładu, ze
szczególnym uwzględnieniem problemów. Na końcu znajdzie się ogólny
algorytm metody konwersji kodu w języku VHDL do równań boolowskich.
Słowa kluczowe:
VHDL, if, case, równania boolowskie, kompilacja, logika sekwencyjna
1.
WPROWADZENIE
Powyższy temat jest jednym z wielu zagadnień jakie występują podczas
realizacji kompilatora języka VHDL do postaci równań boolowskich.
Artykuł ten powstał na bazie doświadczeń autora, w czasie prac nad tego
typu narzędziem, przeznaczonym do syntezy układów logicznych, na
Wydziale
Informatyki
Politechniki
Szczecińskiej.
Poprawność
przytoczonego rozwiązania została sprawdzona w praktyce, na dużej liczbie
przykładów testowych. Podstawowym ograniczeniem projektu było
założenie pełnej zgodności z produktem FPGA Express firmy Synopsis[2].
96
1.1
Zalety równań boolowskich
Tak jak już zostało to wspomniane wcześniej, wynikiem działania
kompilatora są równania logiczne. Zadecydowały o tym następujące zalety
równań boolowskich:
– powszechnie znany format i zasady tworzenia,
– znakomite możliwości optymalizacji,
– możliwość symulacji,
– możliwość bezpośredniego mapowania na układ FPGA.
1.2
Instrukcja if
Składnia instrukcji if, zgodnie ze standardem[1] przedstawia się
następująco:
if warunek1 then
instrukcje_sekwencyjne
[elsif warunek2 then
instrukcje_sekwencyjne]
[else
instrukcje_sekwencyjne]
end if;
Umożliwia warunkowe wykonanie bloku instrukcji sekwencyjnych.
Jeżeli warunek po słowie kluczowym if jest spełniony to wykonywane są
instrukcje znajdujące się po słowie kluczowym then. Jeżeli sprawdzamy jest
warunek wejścia do następnej gałęzi (jeżeli gałąź istnieje) lub sterowanie
przekazywane jest do następnej instrukcji sekwencyjnej. Gałęzie elsif i else
mogą, ale nie muszą wystąpić. Gałąź elsif może wystąpić więcej niż jeden
raz. Po wykonaniu instrukcji w danej gałęzi, nie są sprawdzane warunki w
pozostałych gałęziach.
1.3
Instrukcja case
Składnia instrukcji case [1] przedstawia się następująco:
case wyrażenie_sterujące is
when wyrażenie_wejściowe ⇒
[ instrukcje sekwencyjne ]
[ when wyrażenie_wejściowe ⇒
[ instrukcje sekwencyjne ] ]
[ when others ⇒
[ instrukcje sekwencyjne ] ]
end case;
97
Wyrażenie wejściowe może przyjąć następującą postać:
– wyrażenie_wejściowe := wartość1 | wartość2|...,
– wyrażenie_wejściowe := wartość1 to wartość,
– wyrażenie_wejściowe := wartość1 dowto wartość.
Możliwe jest łączenie powyższych postaci.
Słowo kluczowe others oznacza te wszystkie wartości wyrażenia
sterującego które nie wystąpiły w żadnej gałęzi. Może być tylko jedna gałąź
z others.
Instrukcja case umożliwia wybór jednej z wielu alternatyw. W zależności
od wartości wyrażenia sterującego występującego po słowie kluczowym
case aktywowana jest jedna z gałęzi, ta której wyrażenie wejściowe
występujące po when ma wartość taką jak wyrażenie sterujące. Ograniczenia
(wynikające ze specyfikacji FPGA Express[2]) instrukcji case:
– wyrażenie sterujące może być typu integer, typu wyliczeniowego, lub
jednowymiarową tablicą elementów typu wyliczeniowego,
– wyrażenia wejściowe muszą być statyczne, tzn. ich wartość musi być
określona na etapie kompilacji,
– rozmiar wyrażenia wejściowego (w bitach) musi być taki sam jak
wyrażenia sterującego,
– każda z możliwych wartości wyrażenia sterującego, musi być
uwzględniona w wyrażeniach wejściowych,
– nie dopuszczalne jest, aby dwa wyrażenia wejściowe miały takie same
wartości (zakresy wartości).
2.
GENERACJA RÓWNAŃ
Przekład instrukcji if i case odbywa się bardzo podobnie. Różnice
dotyczą głównie reguł tworzenia równań dla warunków. Dla każdego
sygnału i zmiennej występującej jako cel przypisania, w którejkolwiek z
gałęzi if (case) zostanie utworzone jedno równanie, lub jedna grupa równań.
Grupa równań powstaje wtedy, gdy dla danej zmiennej (sygnału) konieczne
jest wygenerowanie przerzutnika[3][4]. W artykule tym, nie będzie opisana
metoda generacji równań dla samych przypisań. Ten temat wykracza
znacznie poza ramy artykułu.
2.1
Ogólna postać generowanego równania
Ogólna postać równania generowanego dla sygnału, gdy występuje on
wewnątrz jednej z gałęzi instrukcji if, jako cel przypisania:
98
n
X = ∑ Wwei ⋅ X i
i =1
gdzie:
– i – numer gałęzi,
– n – ilość gałęzi,
– X – lewa strona przypisania,
– Xi – prawa strona przypisania w i-tej gałęzi,
– Wwei – warunek wejścia do i-tej gałęzi.
2.1.1
Postać warunku wejścia do i-tej gałęzi Wwei dla instrukcji if:
Od spełnienia tego warunku zależy czy dana gałąź zostanie aktywowana.
Uwzględnia on warunki wszystkich, gałęzi które poprzedzają gałąź o
numerze i. Wejście do i-tej gałęzi będzie możliwe tylko wtedy, gdy jej
elementarny warunek wejścia będzie spełniony i nie nastąpiła aktywacja
żadnej z gałęzi poprzedzających.
j <i
Wwei = (∏ !W j ) ⋅ Wi
j =1
gdzie:
– i – numer gałęzi,
– Wj – elementarny warunek wejścia do gałęzi if która poprzedza daną
gałąź,
– Wi – elementarny warunek wejścia do bieżącej gałęzi (w przypadku
else nie występuje).
2.1.2
Postać pełnego warunku wejściowego do i-tej gałęzi Wwei
dla instrukcji case:
Warunek ten musi zostać spełniony, aby nastąpiła aktywacja danej
gałęzi. Ponieważ każda z gałęzi case jest traktowana oddzielnie, nie trzeba
tak jak to miało miejsce przy instrukcji if, uwzględniać warunków
wcześniejszych gałęzi.
99
m
Wwei = ∑ Wk
k =1
gdzie:
– Wk – kolejna wartość wejściowa dla danej gałęzi,
– k – numer kolejnej wartości,
– m – liczba wszystkich wartości wejściowych dla danej gałęzi,
– i – numer gałęzi.
Jeżeli przez S oznaczymy wyrażenie sterujące, to postać równania
wartości wejściowej będzie następująca:
p −1
 S (l ) ⇔ S l = 1 
Wk = ∏ 

l = 0 ! S (l ) ⇔ S l = 0 
gdzie:
– S – wyrażenie sterujące,
– Sl – kolejny bit wyrażenia sterującego,
– p – ilość bitów wyrażenia sterującego,
– l – numer kolejnego bitu.
Mimo iż powyższe równania są prawidłowe dla wszystkich przypadków,
dla others korzystniejsze wydaje się zastosowanie nieco innej postaci
warunku wejściowego. Others obejmuje wszystkie te wartości wyrażenia
wejściowego które nie zostały uwzględnione w pozostałych gałęziach.
n −1
Wweothers =!(∑ Wwei )
i =1
Takie równanie jest łatwiejsze do wygenerowania, gdyż korzystamy z
wcześniej obliczonych wartości. W przypadku stosowania standardowego
wzoru istnieje potrzeba znalezienia tych wszystkich wartości, które nie
zostały uwzględnione w pozostałych gałęziach. O ile typ integer i jego
pochodne nie stanowią większego problemu, to typy wyliczeniowe już
niestety tak.
100
2.2
Sytuacje szczególne
Równanie ogólne stanowi punkt wyjścia do dalszych rozważań. W swej
niezmienionej formie może być stosowane tylko, wtedy gdy w każdej z
gałęzi if(case) występuje przypisanie dla danego X. Dodatkowo wartość
logiczna warunków wejścia do poszczególnych gałęzi , nie może być
określona na etapie kompilacji.
2.2.1
Brak przypisań we wszystkich gałęziach
Jest to pierwsza sytuacja szczególna. Może wystąpić w dwóch
odmianach:
2.2.1.1
Przypadek I
Brak przypisania w którejś z gałęzi if(case) dla danego X. Przed
instrukcją warunkową występuje przypisanie dla danego X. W tej sytuacji
mimo tego że X nie występuje we wszystkich gałęziach if(case) tworzymy
równanie logiki kombinacyjnej. Umożliwia to przypisanie które występuje
przed instrukcją warunkową.
Postać równanie w takim przypadku:
n
n
i =1
i =1
X = ∑ Wwei ⋅ X i ⋅ Z i + ∑ (!Wwei ⋅ Z i ) ⋅ Y
gdzie:
– X, Wwei, Xi, i, n – tak jak równaniu ogólnym,
– Zi – zmienna przyjmująca wartość jeden gdy w i-tej gałęzi występuje
przypisanie do X,
– Y – prawa strona przypisania, równania występującego przed
instrukcją warunkową.
2.2.1.2
Przypadek II
Brak przypisania w którejś z gałęzi if(case) dla danego X. Przed
instrukcją warunkową nie występuje przypisanie dla danego X. Ta sytuacja
wymaga wygenerowanie równań przerzutnika. Konieczne jest bowiem
zapamiętywanie wartości X, między kolejnymi wywołaniami procesu.
Utworzony przerzutnik będzie typu zatrzask[4] (ang. latch). Wygenerowane
zostaną następujące dwa równania:
101
Równanie wejścia D przerzutnika:
n
D = ∑ Wwei ⋅ X i ⋅ Z i
i =1
Równanie wejścia zegarowego C przerzutnika:
n
C = ∑ Wwei ⋅ Z i
i =1
2.2.2
Warunek if jest stały
Niekiedy możliwe jest określenie wartości logicznej warunku gałęzi if
już na etapie kompilacji. Taka sytuacja musi zostać wykryta i odpowiednio
potraktowana. Warunek może przyjmować tylko dwie wartości: 0 lub 1.
Zero oznacza że taki warunek nigdy nie zostanie spełniony, zatem gałąź z
takim warunkiem nie będzie nigdy aktywna. Należy więc taką gałąź
całkowicie pominąć w procesie przekładu. W drugim przypadku, gdy
warunek jest równy 1, oznacza to, że jest on zawsze spełniony. Tak więc
sterowanie wejdzie tylko do tej gałęzi, a wszystkie inne zostaną pominięte.
Jeżeli okaże się, że kilka gałęzi ma warunek wejściowy równy 1, to tylko
pierwsza taka gałąź zostanie poddana analizie. Sytuacja taka będzie
sygnalizowana przez kompilator, gdyż najprawdopodobniej nie jest ona
zamierzona przez projektanta.
2.2.3
Wyjątek case
Warunki wystąpienia tego wyjątku:
– kolejne wartości wejściowe ze wszystkich gałęzi, pokrywają wszystkie
możliwe wartości wyrażenia sterującego,
– case posiada gałąź others.
W takim przypadku, gałąź others nigdy nie zostanie aktywowana, należy
ją więc pominąć. Taka sytuacja nie jest traktowana jako błąd.
Reguły pozwalające na prawidłowe wykonanie przekładu instrukcji if.
1. Wygenerować równanie dla każdego z warunków elementarnych Wi.
2. Utworzyć warunki wejściowe Wwe.
102
3. Dla każdej gałęzi wygenerować równania, dla wszystkich znajdujących
się tam sygnałów (zmiennych). Oprócz samych równań należy
zapamiętać informacje czy jest to logika kombinacyjna czy sekwencyjna.
4. Utworzyć równania wynikowe. Należy utworzyć równanie według
wzorów na postać normalną, oraz według wzoru na równanie
sekwencyjne. Dlaczego tak? Ponieważ w momencie tworzenia równania
wynikowego, nie wiemy jeszcze jakiego będzie ono typu. Generujemy
więc obie postacie, a następnie jedną odrzucamy.
5. Określić czy równanie wynikowe ma być w postaci kombinacyjnej czy
sekwencyjnej. To najtrudniejsza część przekładu. Równanie dla danego
sygnału (zmiennej) będzie w postaci kombinacyjnej jeżeli spełnione
zostaną następujące warunki:
– występuje po lewej stronie przypisania we wszystkich gałęziach, oraz
instrukcja if zawiera gałąź else,
– brak jest przypisania w jednej lub więcej gałęzi, ale występuje
przypisanie przed instrukcją if,
– warunek wejścia do gałęzi ma wartość logiczną 1,
– W pozostałych przypadkach otrzymujemy równania logiki
sekwencyjnej.
3.
OCENA ROZWIĄZANIA
Poniżej przedstawię ocenę opisanej przeze mnie metody. Określona
zostanie ilość operacji generacji równań, oraz szacunkowy rozmiar pamięci
potrzebny do wykonania przekładu instrukcji if(case).
3.1
Ilość operacji
Niech dana będzie instrukcja if(case) o n gałęziach. Przez mI oznaczymy
ilość instrukcji przypisania w każdej gałęzi, a przez Mi ich zbiór. Wówczas
ilość operacji generacji równań będzie wyrażać się liczbą:
n
Io = ∑ mi + n
i =1
gdzie n oznacza ilość warunków instrukcji if(case).
Jak widać jest to suma ilości operacji w poszczególnych gałęziach,
powiększona o ilość operacji niezbędną do wygenerowania równań dla
warunków if(case).
103
Aby obliczyć ilość równań wynikowych, najpierw określamy zbiór tych
równań:
n
M = UMi
i =1
Zbiór równań wynikowych:
m=M
Ilość równań wynikowych (ilość elementów zbioru wynikowego). Jest to
liczba wszystkich różnych sygnałów, znajdujących się w poszczególnych
gałęziach instrukcji if(case).
3.2
Zajętość pamięci
Niech założenia będą takie same jak w punkcie poprzednim. Niech Lj
oznacza całkowitą długość równania wynikowego (w bajtach, dla
określonego sygnału), Lji długość równania w gałęzi o numerze i, a Lwi
długość warunku wejściowego i-tej gałęzi. Wówczas:
n
L j = ∑ ( L ji + Lwi )
i =1
a całkowita ilość pamięci jaką zajmą wszystkie wygenerowane równania
można wyrazić wzorem:
m
L = ∑ Lj
j =1
4.
PRZYKŁADY
Aby opis algorytmu generacji równań boolowskich dla instrukcji if(case),
był kompletny przedstawione zostaną przykłady zastosowania go w
praktyce. Wszystkie wyniki otrzymano korzystając z kompilatora
tworzonego na Wydziale Informatyki Politechniki Szczecińskiej.
104
4.1
Przypisania występują w wszystkich gałęziach.
Pierwszy przykład dotyczy sytuacji, gdy instrukcja if(case) zawiera
przypisanie dla danego sygnału, we wszystkich swoich gałęziach.
Generowana jest logika kombinacyjna.
4.1.1
Przykład dotyczący instrukcji if
Źródło programu w VHDL:
entity test is
port (
A:in bit;
O:out bit_vector(1 downto 0)
);
end test;
architecture component_1 of test is
begin
process(A)
begin
if A='1' then
O<="11";
elsif A='0' then
O<="00";
else
O<="10";
end if;
end process;
end component_1;
Równania wygenerowane przez kompilator:
O(1)=((((A))&((1)))|((((!A))&((!A)))&((0))))|((0)&((1)
));
O(0)=((((A))&((1)))|((((!A))&((!A)))&((0))))|((0)&((0)
));
4.1.2
Przykład dotyczący instrukcji case
Źródło programu w VHDL:
105
entity test is
port (
a,b,c,d: in BIT_vector(0 to 1);
s1 : in integer range 0 to 3;
z: out BIT_vector(0 to 1)
);
end test;
architecture arch of test is
begin
process (a, b)
Begin
case s1 is
when 2 | 3 => z<=a;
when 1 => z<=b;
when others => z<=d;
end case;
End process;
end arch;
Równania wygenerowane przez kompilator:
z(0)=(((((!s1(0))&(s1(1))|(s1(0))&(s1(1))))&((a(0))))|
((((s1(0))&(!s1(1))))&((b(0)))))|((!((((!s1(0))&(s1(1))|
(s1(0))&(s1(1))))|(((s1(0))&(!s1(1))))))&((d(0))));
z(1)=(((((!s1(0))&(s1(1))|(s1(0))&(s1(1))))&((a(1))))|
((((s1(0))&(!s1(1))))&((b(1)))))|((!((((!s1(0))&(s1(1))|
(s1(0))&(s1(1))))|(((s1(0))&(!s1(1))))))&((d(1))));
4.2
Brak przypisania w którejś z gałęzi
W tym przypadku, w jednej lub kilku gałęziach brakuje przypisania dla
danego sygnału. Konieczne jest zatem wygenerowanie logiki sekwencyjnej
(przerzutnik typu latch).
4.2.1
Przykład dotyczący instrukcji if
Źródło programu w VHDL:
entity test is
port (
106
A:in bit;
O:out bit_vector(1 downto 0)
);
end test;
architecture component_1 of test is
begin
process(A)
begin
if A='1' then
O<="11";
elsif A='0' then
O<="00";
end if;
end process;
end component_1;
Równania wygenerowane przez kompilator:
--process equations begin, line: 26
-- pragma asyn(t-1);
--{
C_tmp0=((A))|(((!A))&((!A)));
D_tmp0(1)=(((A))&((1)))|((((!A))&((!A)))&((0)));
-- latch(C_high,D)
S_tmp0(1)=D_tmp0(1)&C_tmp0;
R_tmp0(1)=C_tmp0&!D_tmp0(1);
O(t,1)=S_tmp0(1)|(!R_tmp0(1)&O(t-1,1));
--}
-- pragma asyn(t-1);
--{
C_tmp1=((A))|(((!A))&((!A)));
D_tmp1(0)=(((A))&((1)))|((((!A))&((!A)))&((0)));
-- latch(C_high,D)
S_tmp1(0)=D_tmp1(0)&C_tmp1;
R_tmp1(0)=C_tmp1&!D_tmp1(0);
O(t,0)=S_tmp1(0)|(!R_tmp1(0)&O(t-1,0));
--}
--state.out file begin
--state.out file end
--process equations end, line: 26
107
4.2.2
Przykład dotyczący instrukcji case
Źródło programu w VHDL:
entity test is
port (
a,b,c,d: in BIT_vector(0 to 1);
s1 : in integer range 0 to 3;
z: out BIT_vector(0 to 1)
);
end test;
architecture arch of test is
begin
process (a, b)
Begin
case s1 is
when 2 | 3 => z<=a;
when 1 => z<=b;
end case;
End process;
end arch;
Równania wygenerowane przez kompilator:
--process equations begin, line: 15
-- pragma asyn(t-1);
{
C_tmp0=(((!s1(0))&(s1(1))|(s1(0))&(s1(1))))|(((s1(0))&
(!s1(1))));
D_tmp0(0)=((((!s1(0))&(s1(1))|(s1(0))&(s1(1))))&((a(0)
)))|((((s1(0))&(!s1(1))))&((b(0))));
-- latch(C_high,D)
S_tmp0(0)=D_tmp0(0)&C_tmp0;
R_tmp0(0)=C_tmp0&!D_tmp0(0);
z(t,0)=S_tmp0(0)|(!R_tmp0(0)&z(t-1,0));
}
-- pragma asyn(t-1);
{
C_tmp1=(((!s1(0))&(s1(1))|(s1(0))&(s1(1))))|(((s1(0))&
(!s1(1))));
108
D_tmp1(1)=((((!s1(0))&(s1(1))|(s1(0))&(s1(1))))&((a(1)
)))|((((s1(0))&(!s1(1))))&((b(1))));
-- latch(C_high,D)
S_tmp1(1)=D_tmp1(1)&C_tmp1;
R_tmp1(1)=C_tmp1&!D_tmp1(1);
z(t,1)=S_tmp1(1)|(!R_tmp1(1)&z(t-1,1));
}
--state.out file begin
--state.out file end
--process equations end, line: 15
Jak widać ilość równan w tym przypadku jest o wiele większa.
Przerzutniki utworzone zostaly na podstawie szablonu. Wszystko poza
równaniami C_tmpx, D_tmpx jest częścią szablonu. Te dwa równania
generowane są na podstawie opisanego wyżej algorytmu.
5.
ZAKOŃCZENIE
Powyższe rozwiązanie zostało przetestowane na specjalnie
przygotowanych testach, a także na istniejących już projektach
komercyjnych. Wyniki uwiarygodnia fakt, iż procesem testowania zajmował
się zupełnie odrębny zespół ludzi.
Przedstawiony algorytm z pewnością nie jest rozwiązaniem optymalnym
i na pewno, w toku dalszych prac zostanie ulepszony.
LITERATURA:
[1]
[2]
[3]
[4]
VHDL’93 IEEE Standard VHDL Language Reference Manual, IEEE Std. 1076-1993
FPGA Express Reference Manual, 1997
C. H. Roth, Jr – Digital Systems Design Using VHDL, ITP 1997
J. Bhasker – A VHDL Sythesis Primer Second Edition, Star Galaxy Publishing
Generacja równań boolowskich dla instrukcji for
języka VHDL
Marcin Radziewicz
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł przedstawia instrukcje for i zagadnienia związane z jej
przekładem na język VHDL. Opis jest kompletny, uwzględnione zostały
między innymi przypadki występowania wewnątrz pętli instrukcji next, oraz
exit. Całość kończy algorytm zamiany instrukcji for na postać liniową,
a której przekład na postać równań boolowskich nie jest już problemem.
Słowa kluczowe:
VHDL, for, kompilacja, równania boolowskie
1.
WPROWADZENIE
Instrukcja pętli - for jest jednym z trudniejszych problemów na jakie
można natrafić podczas tworzenia kompilatora języka VHDL. Wprawdzie jej
zwykła postać nie stanowi wielkiego wyzwania, dla ludzkiego umysłu, to
w sytuacji gdy zawiera instrukcje exit lub next jej analiza znacznie się
komplikuje. Wszystko to powoduje, że stworzenie automatycznego narzędzia,
które radziłoby sobie w każdej sytuacji nie jest zadaniem prostym.
Artykuł przedstawia metodę generacji równań boolowskich dla dowolnej
pętli for, także w przypadku gdy zawiera ona instrukcje next i exit. Nie
pominięto także sytuacji w której mamy do czynienia z pętlami
zagnieżdżonymi. Całość zilustrowana została przykładami poglądowymi,
oraz co ważniejsze, przedstawiono wyniki działania istniejącego
i działającego według opisanych poniżej zasad kompilatora języka VHDL.
Narzędzie to jest rozwijane na Wydziale Informatyki Politechniki
110
Szczecińskiej. Głównym założeniem było uzyskanie pełnej zgodności
z programem FPGA Express firmy Synospis[2].
1.1
Instrukcja for
Zgodnie ze standardem[1] języka VHDL składna pętli for przedstawia się
następująco:
[etykieta:] for identyfikator in zakres loop
instrukcje
end loop [etykieta];
Pętla wykona się tyle razy, ile jest określone w zakresie. Identyfikator
jest zmienną iteracyjną pętli, przyjmuje kolejne wartości z zakresu pętli, nie
musi być wcześniej zadeklarowany, co więcej wewnątrz pętli przysłoni
ewentualną zmienną lub sygnał o takiej samej nazwie. Zakres jest
wyrażeniem range języka VHDL. Można zastosować wszystkie postacie
tego wyrażenia dopuszczane przez gramatykę języka.
1.2
Ograniczenia pętli for w syntezie logicznej
Jeżeli program zapisany w języku VHDL ma być syntezowalny muszą
zostać spełnione założenia odnośnie for:
– zakres musi być statyczny, tzn. musi istnieć możliwość wyznaczenia go
na etapie kompilacji,
– wewnątrz pętli nie może znajdować się instrukcja wait.
Oba warunki mają zapobiec sytuacji w której pętla wykonywałaby się w
nieskończoność. Ograniczenia te są zgodne ze specyfikacją pakietu FPGA
Express firmy Synopsis [2].
2.
KONWERSJA PĘTLI FOR DO POSTACI
LINIOWEJ
Pierwszym etapem generacji równań dla instrukcji for, jest zamiana jej
na alternatywną postać liniową.
Alternatywną postacią liniową(APL) pętli for określać będziemy
równoważny pod względem logicznym blok instrukcji sekwencyjnych nie
zawierający jednak ani jednej instrukcji for.
111
Aby zbudować algorytm konwersji należy zbadać zachowanie pętli we
wszystkich sytuacjach z jakimi możemy mieć do czynienia. Te przypadku to:
– pojedyncza pętla for,
– jak wyżej, lecz dodatkowa z instrukcją next(exit),
– dwie pętle (zagnieżdżone), instrukcjami next(exit),
– trzy pętle (zagnieżdżone) z instrukcjami next(exit).
Powyższe przypadki pozwalają zapoznać się z wszelkimi możliwymi
sytuacji zachowania się pętli. Analiza więcej niż trzech pętli zagnieżdżonych
nie ma sensu, gdyż nie wnosi nic nowego. Do rozpoznania wszystkich aspektów
działania pętli z instrukcjami next i exit wystarcza blok trzech pętli.
We wszystkich przypadkach instrukcje next(exit) muszą być w postaci
warunkowej. Z postacią warunkową instrukcji next(exit) mamy do czynienia
wtedy gdy jej aktywacja jest zależna od spełnienia określonego warunku
logicznego. Istnieją dwie odmiany postaci warunkowej:
– zwykła - instrukcja next(exit) zawiera warunek wykonania,
– pośrednia – instrukcja next(exit) znajduje się wewnątrz jednej z gałęzi
if(case).
Przejdźmy zatem do szczegółowej analizy powyższych przypadków.
2.1
Pojedyncza pętla for
Najprostsza sytuacja. Konwersja polega na zapisaniu kolejno iteracji
pętli, podstawiając w miejsce wystąpienia zmiennej iteracyjnej konkretną
wartość liczbową (literał). Np.:
for i in 1 to 3 loop
a(i) := b(i);
c(i) := d(i);
end loop;
a(1)
c(1)
a(2)
c(2)
a(3)
c(3)
:=
:=
:=
:=
:=
:=
b(1); *(1)
d(1);
b(2); *(2)
d(2);
b(3); *(3)
d(3);
Przykład 1.Generacja równań boolowskich dla pojedynczej pętli for
Jak widać, po lewej stronie znajduje się przykładowa pętla for, po prawej
natomiast mamy do czynienia z jej postacią liniową (w nawiasach *()
podano numery iteracji).
112
2.2
Pojedyncza pętla for z instrukcją next, lub exit
w postaci warunkowej
Dla lepszego pokazania co dzieje się wewnątrz takiej pętli, posłużę się
rysunkiem 1.
For(i=0;i<n;i++)
For(j=0;j<m;j++)
For(k=0;k<p;k++)
Warunek wykonanie
iteracji spełniony?
Warunek wykonanie
iteracji spełniony?
Warunek wykonanie
iteracji spełniony?
T
T
T
Instrukcje sekwencyjne
w tym pętla
Instrukcje sekwencyjne
w tym pętla
Instrukcje sekwencyjne
Czy wewnęt. pętla
przerywa pętle
zewnętrzną?
Czy wewnęt. pętla
przerywa pętle
zewnętrzną?
Warunek next/exit
spełnony
N
N
N
N
N
N
Instrukcje sekwencyjne
(w tym next,exit)
Instrukcje sekwencyjne
(w tym next,exit)
Instrukcje sekwencyjne
Pętla zewnętrzna
T
Pętla środkowa
T
T
Pętla wewnętrzna
Rysunek 1. Schemat powiązań zagnieżdżonych pętli for.
W tej chwili rozważać będziemy tylko pętle wewnętrzną, wszystko
pozostałe na razie ignorujemy. Widać, że wykonanie jej zależy od dwóch
warunków logicznych:
– warunku wykonania iteracji,
– warunku next(exit).
Warunek wykonania iteracji wystąpi tylko wtedy, gdy mamy do
czynienia z instrukcją exit. Jeżeli warunek aktywacji zostanie spełniony, to
pętla zostanie przerwana. W związku z tym należy uzależnić wykonanie
poszczególnych iteracji (po za oczywiście pierwszą) od warunku aktywacji
exit. Warunek ten można wyrazić poniższym wzorem:
113
i −1
Wi = ∏ !We j
j =0
,
gdzie:
– i – numer iteracji,
– Wj – warunek wykonania i-tej iteracji,
– Wej - warunek aktywacji instrukcji exit w kolejnej iteracji.
Oczywiste jest, że aby wykonała się iteracja i, w żadnej z iteracji ją
poprzedzających nie może dojść do spełnienia warunku aktywacji exit. Jeżeli
w pętli jest kilka instrukcji exit, to Wej, powstanie przez połączenie
warunków, wszystkich tych instrukcji.
Warunek next(exit). Decyduje o tym, czy to co znajduje się za tymi
instrukcjami zostanie wykonane, czy też nie.
Aby lepiej zrozumieć opisane powyżej przykłady proponuje przyjrzeć się
poniższym przykładom.
For i in 1 to 3 loop
a(i) := b(i);
next when a(i)=b(i);
c(i) := d(i);
end loop;
a(1) := b(1);
*(1);
if not (a(1)=b(1)) then
c(1) := d(1);
end if;
a(2) := b(2);
*(2);
if not (a(2)=b(2)) then
c(3) := d(3);
end if;
a(3) := b(3);
*(3);
if not (a(3)=b(3)) then
c(3) := d(3);
end if;
Przykład 2. Konwersja pętli for do jej postaci liniowej – wersja z pojedynczą instrukcją next,
*() oznacza numery iteracji.
W przykładzie tym mamy przypadek pętli for(lewa strona) zawierającej
jedną instrukcje next w postaci warunkowej. Po prawej stronie mamy tą
samą pętle zapisaną w postaci liniowej. Widać, że w miejsce zmiennej
iteracyjnej podstawiono literały liczbowe. Dodatkowo instrukcje next
zastąpiono blokiem if, wewnątrz którego umieszczone zostało wszystko to
co znajdowało się za next.
114
for i in 1 to 3 loop
a(i) := b(i);
exit when a(i)=b(i);
c(i) := d(i);
end loop;
a(1) := b(1);
*(1)
if not (a(1)=b(1)) then
c(1) := d(1);
end if;
if not (a(1)=b(1)) then
*(2)
a(2) := b(2);
if not (a(2)=b(2)) then
c(2) := d(2);
end if;
end if;
*(3)
if(not(a(1)=b(1))and
(not(a(2)=b(2))) then
a(3) := b(3);
if not (a(3)=b(3)) then
c(3) := d(3);
end if;
end if;
Przykład 3. Konwersja pętli for do jej postaci liniowej – wersja z pojedynczą instrukcją exit,
*() oznacza numery iteracji.
Podobnie jak w przykładzie 1 po lewej stronie znajduje się pętla for, tym
razem jednak z instrukcją exit. Aktywacja exit przerywa permanentnie
wykonanie bieżącej iteracji oraz wszystkich następnych. Dlatego też każda
iteracja, z wyjątkiem pierwszej, zamknięta jest w bloku instrukcji if, którego
warunkiem wykonania jest koniunkcja zanegowanych warunków exit, z
wszystkich iteracji poprzedzających.
2.3
Dwie pętle for (zagnieżdżone), instrukcjami
next(exit)
Tą sytuacje przedstawiają pętle wewnętrzna i środkowa z rysunku 1.
Ignorujemy na razie trzecią pętle (pętla środkowa będzie pętlą zewnętrzną).
Wykonaniem obu pętli sterują trzy warunki (główne):
– Warunek wykonania iteracji pętli zewnętrznej. Powstaje w przypadku
wystąpienia następujących sytuacji:
– pętla ta zawiera instrukcje exit,
– pętla wewnętrzna zawiera instrukcje exit z etykietą pętli zewnętrznej.
– Warunek wykonania instrukcji znajdujących się za wewnętrzną pętlą:
– pętla wewnętrzna zawiera instrukcje exit z etykietą pętli zewnętrznej,
115
– wewnątrz pętli wewnętrznej znajduje się instrukcja next z etykietą
pętli zewnętrznej,
– Warunek wykonania iteracji pętli wewnętrznej:
– pętla wewnętrzna zawiera instrukcję exit,
– pętla wewnętrzna zawiera instrukcje next z etykietą pętli zewnętrznej.
Oprócz powyższego, każda pojedyncza instrukcja next(exit) uzależnia
wykonanie kodu znajdującego się za nią od swojego warunku. Tworząc
postać liniową pętli trzeba to wszystko uwzględnić. Najlepiej to pokażą
poniższe przykłady.
loop1: for i in 0 to 1 loop
----------------------------------------------- cześć pętli loop1 przed instrukcją next
a1(i) := b1(i);
---------------------------------------------loop2: for j in 0 to 1 loop
a2(i,j) := b2(i,j);
next loop1 when a2(i,j)>b2(i,j);
c2(i,j) := d2(i,j);
end loop loop2;
---------------------------------------------- część pętli loop1 za instrukcją next
c1(i) := d1(i);
--------------------------------------------end loop loop1;
{1} a1(0) := b1(0);
*(I)
{2} a2(0,0) := b2(0,0);
*(1)
{3} if not (a2(0,0) > b2(0,0)) then
{4} c2(0,0) := d(0,0);
{5} end if;
{6} if not(a2(0,0) > b2(0,0)) then
*(2)
{7} a2(0,1) := b2(0,1);
{8} if not (a2(0,1) > b2(0,1)) then
{9}
c2(0,1) := d(0,1);
{10}
end if;
{11} end if;
{12} if not( a2(0,0) > b2(0,0)) and not (a2(0,1) >
b2(0,1)) then
*(I)
{13}
c1(0) := d1(0);
{14} end if;
{15} a1(1) := b1(1);
*(II)
{16} a2(1,0) := b2(1,0);
*(1)
116
{17} if not (a2(1,0) > b2(1,0)) then
{18}
c2(1,0) := d(1,0);
{19} end if;
{20} if not(a2(1,0) > b2(1,0)) then
*(2)
{21}
a2(1,1) := b2(1,1);
{22}
if not (a2(1,1) > b2(1,1)) then
{23}
c2(1,1) := d(1,1);
{24}
end if;
{25} end if;
{26} if not( a2(1,0) >b2(1,0)) and not (a2(1,1) >
b2(1,1)) then
*(II)
{27}
c1(1) := d1(1);
{28} end if;
Przykład 4. Konwersja dwóch pętli for do postaci liniowej – wersja z pojedynczą instrukcją
next, *() oznacza numery iteracji.
----------------------------------------------- cześć pętli loop1 przed instrukcją exit
a1(i) := b1(i);
---------------------------------------------loop2: for j in 0 to 1 loop
a2(i,j) := b2(i,j);
exit loop1 when a2(i,j)>b2(i,j);
c2(i,j) := d2(i,j);
end loop loop2;
---------------------------------------------- część pętli loop1 za instrukcją exit
c1(i) := d1(i);
--------------------------------------------end loop loop1;
{1} a1(0) := b1(0);
*(I)
{2} a2(0,0) := b2(0,0);
*(1)
{3} if not (a2(0,0) > b2(0,0)) then
{4} c2(0,0) := d(0,0);
{5} end if;
{6} if not(a2(0,0) > b2(0,0)) then
*(2)
{7} a2(0,1) := b2(0,1);
{8} if not (a2(0,1) > b2(0,1)) then
{9}
c2(0,1) := d(0,1);
{10}
end if;
{11} end if;
117
{12} if not( a2(0,0) > b2(0,0)) and not (a2(0,1) >
b2(0,1)) then
*(I)
{13}
c1(0) := d1(0);
{14} end if;
{15} if not( a2(0,0) > b2(0,0)) and not (a2(0,1) >
b2(0,1)) then
*(II)
{16}
a1(1) := b1(1);
{17}
a2(1,0) := b2(1,0);
*(1)
{18}
if not (a2(1,0) > b2(1,0)) then
{19}
c2(1,0) := d(1,0);
{20}
end if;
{21}
if not(a2(1,0) > b2(1,0)) then
*(2)
{22}
a2(1,1) := b2(1,1);
{23}
if not (a2(1,1) > b2(1,1)) then
{24}
c2(1,1) := d(1,1);
{25}
end if;
{26}
end if;
{27}
if not( a2(1,0) >b2(1,0)) and not (a2(1,1) >
b2(1,1)) then *(II)
{28}
c1(1) := d1(1);
{29}
end if;
{30} end if;
Przykład 5. Konwersja dwóch pętli for do postaci liniowej – wersja z pojedynczą instrukcją
exit, *() oznacza numery iteracji.
2.4
Trzy pętle for (zagnieżdżone), instrukcjami next(exit)
Poprzedni przykład, mimo że stosunkowo skomplikowany, nie pokazał
jeszcze wszystkich problemów, z jakimi możemy mieć do czynienia gdy
dokonujemy przekładu pętli for. Wróćmy zatem jeszcze raz do rysunku 1.
Teraz rozpatrujemy wszystkie pętle. Jak widać wykonaniem całego bloku
instrukcji steruje pięć warunków. Oto one (wraz z informacją od czego
zależy ich powstanie):
– Warunek wykonania iteracji pętli zewnętrznej powstaje gdy:
– istnieje chociaż jedna instrukcja exit przerywająca tą pętle.
– Warunek wykonania instrukcji znajdujących się za pętlą środkową
tworzony jest gdy:
– każda instrukcja exit, przerywająca działanie pętli zewnętrznej, a
znajdująca się pętli środkowej lub wewnętrznej,
118
– instrukcja next znajdująca się w pętli środkowej, lub wewnętrznej, a
której celem jest pętla zewnętrzna.
– Warunek wykonania iteracji pętli środkowej powstaje gdy:
– istnieje chociaż jedna instrukcja exit przerywająca tą pętle(w tej pętli
lub wewnętrznej),
– w pętli wewnętrznej jest instrukcja next, której celem jest pętla
zewnętrzna.
– Warunek wykonania instrukcji znajdujących się za pętlą wewnętrzną
powstaje gdy:
– w pętli wewnętrznej znajduje się instrukcja exit, przerywająca
działanie pętli środkowej, lub zewnętrznej,
– w pętli wewnętrznej znajduje się instrukcja next, odnosząca się do
pętli środkowej, lub zewnętrznej.
– Warunek wykonania iteracji pętli wewnętrznej powstaje gdy:
– w pętli wewnętrznej znajduje się instrukcja exit odnosząca się do
dowolnej pętli,
– w pętli wewnętrznej znajduje się instrukcja next, a jej celem jest pętla
środkowa, lub zewnętrzna.
Tak oto zakończyliśmy omawianie wpływu instrukcji next i exit na
przebieg wykonania pętli. Opisane przypadki w pełni wyczerpują temat.
Następnym krokiem jest przedstawienie zasad generacji poszczególnych
warunków sterujących wykonywaniem się pętli.
3.
OGÓLNE ZASADY GENERACJI WARUNKÓW
STERUJĄCYCH PĘTLI
3.1
Podstawowe pojęcia
Część dalsza. Niech będą dane dwie pętle a i b, przy czym niech b będzie
wewnątrz a. Częścią dalszą pętli a, określać będziemy wszystkie te jej
instrukcje, które znajdują się za blokiem pętli b. Dla pętli b, część dalsza nie
występuje.
Niech dane będzie n pętli for o numerach od 1 do n, przy czym pętla o
numerze n będzie najbardziej zagnieżdżona. W każdej pętli z wyjątkiem tej
o numerze n, mogą wystąpić dwa warunki:
– warunek wykonania iteracji,
– warunek wykonania części dalszej.
Dla pętli o numerze n, warunek nie wystąpi.
119
Niech dane będą instrukcje nextij, lub exitij przy czym indeksy oznaczają:
– i – numer pętli w której znajduje się instrukcja,
– j – numer pętli do której odnosi się instrukcja,
przy czym zawsze j <= i.
Uwzględniając powyższe:
– Warunek danej instrukcji exitij wystąpi w warunkach wykonania iteracji
pętli o numerach k = i..j.
– Warunek danej instrukcji exitij wystąpi w warunkach wykonania części
dalszej wszystkich pętli o numerach k = i.. j-1.
– Warunek danej instrukcji nextij będzie częścią składową warunku
wykonania iteracji dla wszystkich pętli o numerach k = i+1..j.
– Warunek danej instrukcji nextij będzie częścią składową warunku
wykonania części dalszej dla pętli o numerach k = i..j-1.
Do powyższych warunków dochodzą jeszcze warunki pojedynczych
instrukcji next i exit.
3.2
Algorytm generacji równań boolowskich dla
instrukcji for
1. Utworzyć warunki wykonania iteracji, oraz wykonania części dalszej dla
każdej z pętli i każdej iteracji, zgodnie z opisanymi wcześniej zasadami.
2. Dla każdego niepustego warunku wykonania iteracji utworzyć instrukcję
if, o jednej gałęzi, w której znajdą się wszystkie instrukcje danej pętli.
Warunkiem aktywacji tej instrukcji będzie warunek wykonania iteracji.
Operacje należy powtórzyć dla wszystkich iteracji.
3. Dla każdego niepustego warunku wykonania części dalszej utworzyć
instrukcję if, o jednej gałęzi, która zawierać będzie wszystkie instrukcje
stanowiące część dalszą pętli. Warunkiem aktywacji tej instrukcji będzie
warunek wykonania części dalszej. Tak jak poprzednio operację należy
powtórzyć dla wszystkich iteracji.
4. Dla wszystkich instrukcji next i exit we wszystkich iteracjach utworzyć
instrukcje if, o jednej gałęzi. Będzie ona zawierać w sobie wszystkie te
instrukcje, które znajdują się za next(exit). Warunkiem aktywacji będzie
zanegowany warunek next(exit).
5. Wygenerować równania boolowskie.
120
4.
PRZYKŁAD
Na koniec chciałbym pokazać jak radzi sobie z przekładem pętli for
(według opisanych powyżej zasad) rzeczywisty kompilator języka VHDL.
Źródło programu:
entity test is
port (
a : in BIT_vector(0 to 3);
b: out bit_vector(0 to 3);
z: out BIT
);
end test;
architecture test6 of test is
begin
process (a)
variable tmp: bit;
Begin
tmp:='1';
b<="0000";
for i in 0 to 3 loop
b(i)<=a(i) ;
next when tmp=a(i);
b(i):='1';
end loop;
end process;
end test6;
Postać liniowa pętli:
begin
tmp:='1';
b<="0000";
b(0)<=a(0);
if then
b(0):='1';
end if;
b(1)<=a(1);
121
if then
b(1):='1';
end if;
b(2)<=a(2);
if then
b(2):='1';
end if;
b(3)<=a(3);
if then
b(3):='1';
end if;
end process;
Wygenerowanie równania boolowskie dla powyższego przykładu:
--process equations begin, line: 15
tmp__id_1060_(0)=a(0);
tmp__id_1062_(0)=a(0);
tmp__id_1066_(0)=a(1);
tmp__id_1068_(0)=a(1);
tmp__id_1072_(0)=a(2);
tmp__id_1074_(0)=a(2);
tmp__id_1077_(0)=a(3);
tmp__id_1079_(0)=a(3);
tmp=(1);
b(0)=!tmp__id_1062_(0)&(1)|!(!(tmp__id_1062_(0)))&tmp_
_id_1060_(0);
b(1)=!tmp__id_1068_(0)&(1)|!(!(tmp__id_1068_(0)))&tmp_
_id_1066_(0);
b(2)=!tmp__id_1074_(0)&(1)|!(!(tmp__id_1074_(0)))&tmp_
_id_1072_(0);
b(3)=!tmp__id_1079_(0)&(1)|!(!(tmp__id_1079_(0)))&tmp_
_id_1077_(0);
--state.out file begin
--state.out file end
--process equations end, line: 15
122
5.
PODSUMOWANIE
Przedstawione rozwiązanie było testowane na dużej liczbie przykładów
testowych. Pomogło to znaleźć słabe punkty i je usunąć. W przypadku
przekładu kilku zagnieżdżonych pętli znacznie wzrasta zapotrzebowanie na
zasoby systemowe. Problem udało się, w znacznym stopniu ograniczyć
zmniejszając liczbę niezbędnych generacji równań. Dalsza optymalizacja
będzie polegała na zmianie sposobu zapisu równań boolowskich.
LITERATURA:
[1]
[2]
[3]
[4]
VHDL'93 IEEE Standard VHDL Language Reference Manual, IEEE Std. 1076-1993
FPGA Express Reference Manual, 1997
C. H. Roth, Jr - Digital Systems Design Using VHDL, ITP 1997
J. Bhasker - A VHDL Sythesis Primer Second Edition, Star Galaxy Publishing
Generowanie równań boolowskich dla funkcji
i procedur języka VHDL
Mirosław Mościcki
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
W opracowaniu zaprezentowany został sposób szybkiego generowania
równań boolowskich dla wielokrotnie powtarzających się wywołań tych
samych funkcji oraz procedur. Algorytm ten opiera się na zapisie raz
wygenerowanych równań dla podprogramu w odpowiednim metapliku.
Dla każdej funkcji może istnieć wiele metaplików zawierających
równania. Oprócz plików z równaniami tworzony jest dodatkowy plik
zawierający informacje o argumentach przekazywanych podczas
wywołania podprogramu. W omówionym algorytmie pełny proces
generowania równań boolowskich dla takich samych argumentów
odbywa się tylko raz. Argumentami mogą być zarówno zmienne jak
i stałe. W niniejszym opracowaniu przedstawione zostały również
możliwości modyfikowania głównego algorytmu. Pokazano praktyczne
zastosowanie opisanej metody.
Słowa kluczowe:
język VHDL, układy FPGA, równania boolowskie
1.
WSTĘP
W Katedrze Technik Programowania Wydziału Informatyki Politechniki
Szczecińskiej realizowany jest projekt, którego celem jest stworzenie
kompilatora dokonującego konwersji pliku zawierającego program napisany
w języku VHDL[1] na równania boolowskie. Równania boolowskie są
bardzo dobrym materiałem wyjściowym do dalszej pracy ponieważ jest to
forma matematyczna. Na ich podstawie można stworzyć układy cyfrowe
realizujące określone zadania, lub poddać je minimalizacji [2].
124
Rozwiązując bardziej złożony problem wyodrębnia się zwykle pewne
jego części, dla których formułuje się rozwiązania oddzielnie. Wydzielenie
podproblemów ma istotne zalety, gdyż umożliwia prowadzenie
rozumowania na ustalonych poziomach abstrakcji [4]. We wszystkich
językach programowania istnieją mechanizmy umożliwiające dzielenie
rozwiązywanego problemu na części. Podprogramy umożliwiają
definiowanie algorytmów, które jako oddzielne moduły programu mogą
reprezentować wybrany element zachowania się układu lub pozwalają
wyliczać określone wartości. W języku VHDL występują dwa rodzaje
podprogramów: procedury i funkcje. W języki VHDL występują następujące
zasady dotyczące podprogramów. Jeżeli podprogram jest umieszczony w
pakiecie, to jego deklaracja musi wystąpić w części deklaracyjnej pakietu a
ciało podprogramu musi znajdować się w ciele pakietu. Podprogram
zdefiniowany wewnątrz architektury ma ciało ale nie ma odpowiadającej mu
deklaracji [3].
Procedurą nazywamy algorytm z przyporządkowanym mu
identyfikatorem, za pośrednictwem którego można się do tego algorytmu
odwołać i spowodować jego wykonanie dla określonych argumentów.
Algorytm ten jest na ogół zapisany w postaci sparametryzowanej, tzn. przy
użyciu pomocniczych nazw, zwanych parametrami formalnymi.
Bezpośrednio przed rozpoczęciem przykładu procedury parametry formalne
są zastępowane parametrami aktualnymi.
Funkcje podobnie jak procedury można traktować jako sekwencje
deklaracji i instrukcji, które mogą być wielokrotnie wywoływane z różnych
miejsc programu. Wywołanie funkcji jest wyrażeniem, dlatego po
zakończeniu obliczeń funkcja zwraca pojedynczą wartość, która może być
typu złożonego. Definicja funkcji składa się z dwóch części:
– deklaracji funkcji, która zawiera nazwę funkcji, listę parametrów
formalnych oraz typ wartości zwracany przez funkcję,
– ciała funkcji, które może zawierać deklaracje zmiennych lokalnych oraz
instrukcje które są wykonywane sekwencyjnie.
Proces generowania równań boolowskich składa się z wielu etapów [5].
W niniejszym opracowaniu zostaną omówione poszczególne kroki jakie
należy wykonań aby wygenerować równania boolowskie dla funkcji oraz
procedury. W procesie generowania równań boolowskich korzystamy z
informacji wygenerowanej przez analizator semantyczny [6].
125
2.
PROCES GENEROWANIA RÓWNAŃ DLA
FUNKCJI I PROCEDUR
Proces generowania równań boolowskich dla funkcji zostanie omówiony
na przykładzie poniższego programu napisanego w języku VHDL.
library IEEE; use IEEE.std_logic_1164.all;
entity test is
port (
a,b: in BIT_vector(0 to 7);
z: out BIT_vector(0 to 7)
);
end test;
architecture arch of test is
function Tr_2(V1, V2: bit_vector) return bit_vector is
variable tmp : bit_vector (v1'range);
begin
tmp := v1 and v2;
return tmp;
end Tr_2;
begin
z<=Tr_2(a,b);
end arch;
Przed przystąpieniem do procesu generowania równań boolowskich dla
podprogramów program w języku VHDL musi zostać poddany analizie
leksykalnej,
syntaktycznej
oraz
semantycznej
[7].
Algorytmy
wykorzystywane w programie powinny być przede wszystkim szybkie
ponieważ pliki źródłowe w języku VHDL mogą mieć bardzo duże rozmiary
i składać się z wielu funkcji i procedur, które są wielokrotnie wywoływane.
Przy wolnych algorytmach czas generowania równań mógłby wykluczać
taki program z możliwości przemysłowego zastosowania. Na schemacie 1
pokazano główne funkcje wykorzystywane podczas przekładu podprogramu:
126
PrepareParameterTab
FindFitFunction
procParametry
procOrderParameter
FunctionToTmp
FileCopyByName
PrepareFileForFunction
FunctionCutAndReplace
CutToEnd
CutAndReplace
TranslatePreparedFile
EvaluateConstants
PrepareLocalVariables
CutAndReplace
analyseProc
Schemat 5.
127
W języku VHDL, podczas wywołania funkcji, parametry aktualne mogą
być podane przez pozycje (w takiej kolejności w jakiej występują parametry
formalne lub przez nazwę) [1].
W specyfikacji która została przyjęta podczas pisania kompilatora
założono, ze część funkcji nie będzie kompilowana tylko program skorzysta
z gotowych wzorców i na ich podstawie wygeneruje równania boolowskie. Z
tego powodu pierwszą rzeczą która musi być wykonana jest sprawdzenie,
czy szukana funkcja jest funkcją wbudowaną. Wszystkie funkcje z pakietów:
std_logic_1164, std_logic_arith, std_logic_signed, std_logic_unsigned są
funkcjami wbudowanymi i równania boolowskie dla nich nie powstają z
tłumaczenia źródła w języku VHDL. Jeśli to nie jest funkcja wbudowana, to
funkcja FindFitFunction przeszukuje drzewo katalogów w katalogu work w
poszukiwaniu funkcji o szukanej nazwie. Po znalezieniu funkcji musimy
sprawdzić zgodność parametrów formalnych z aktualnymi oraz sprawdzić
czy typy zwracane przez funkcje zgadzają się.
Pierwszym krokiem który wykonuje funkcja sprawdzająca parametry jest
odczytanie informacji o typie zwracanym przez funkcje i sprawdzenie
zgodności odczytanego typu z typem który powinna zwracać szukana
funkcja. Jeśli typy są zgodne to odczytywane są parametry formalne i
sprawdzana jest ich zgodność z parametrami aktualnymi. Sprawdzane są
przy tym następujące cechy parametrów:
– zgodność typów,
– zgodność kierunku przepływu danych (in, out, inout),
– początek oraz koniec zakresu typów;
Gdy została znaleziona tyko jedna pasująca funkcja to zmieniany jest
bieżący kontekst na kontekst znalezionej funkcji. Jeśli znaleziono więcej niż
jedną pasującą funkcje to zwracany jest błąd. W przypadku nie znalezienia
pasującej funkcji również zwracany jest błąd.
W programie napisanym w języku VHDL może wystąpić sytuacja w
której część wartości semantycznych musi zostać obliczona przed generacją
równań boolowskich dla podprogramu. Taka sytuacja ma miejsce w
programie 1.
variable tmp : bit_vector (v1'range);
Gdy wszystkie wartość semantyczne są już obliczone to należy
zmodyfikować źródło podprogramu tak, aby funkcja poprawnie zwracała
wartość. Poniższy fragment programu zostanie zmodyfikowany następująco:
c <= a or b;
128
return c;
Powstanie:
c <= a or b;
zmienna_tymczasowa <= c;
return zmienna_tymczasowa;
Kolejny przykład modyfikacji:
return a or b;
Forma po modyfikacji:
zmienna_tymczasowa <= a or b;
return zmienna_tymczasowa;
Kolejną istotną rzeczą która musi być wykonana jest modyfikacją nazw
zmiennych i sygnałów zadeklarowanych wewnątrz podprogramu. Przyjęta
specyfikacja określa, że w równaniach nie może występować wielokrotne
przypisanie do tej samej zmiennej. Z tego powodu konieczna jest
każdorazowa modyfikacja nazw zmiennych i sygnałów zadeklarowanych
lokalnie w podprogramie. Rozważmy następujący program:
library IEEE;
use IEEE.std_logic_1164.all;
entity test is
port (
a,b: in BIT_vector(0 to 2);
a2,b2: in BIT_vector(0 to 1);
z2: out BIT_vector(0 to 1);
z: out BIT_vector(0 to 2)
);
end test;
architecture arch of test is
function Tr_2(V1, V2: bit_vector) return bit_vector is
variable tmp : bit_vector (v1'range);
begin
tmp := v1 and v2;
return tmp;
end Tr_2;
begin
129
z<=Tr_2(a,b);
z2<=Tr_2(a2,b2);
end arch;
Po skompilowaniu uzyskujemy następujące równania:
--function translation begin: "Tr_2" line 23,
program4.vhd
tmp__id_1066_tmp(0)=((a(0)&b(0)));
tmp__id_1066_tmp(1)=((a(1)&b(1)));
tmp__id_1066_tmp(2)=((a(2)&b(2)));
tmp__id_1065_(0)=(tmp__id_1066_tmp(0));
tmp__id_1065_(1)=(tmp__id_1066_tmp(1));
tmp__id_1065_(2)=(tmp__id_1066_tmp(2));
--function translation end: "Tr_2" line 23,
program4.vhd
z(0)=tmp__id_1065_(0);
z(1)=tmp__id_1065_(1);
z(2)=tmp__id_1065_(2);
--function translation begin: "Tr_2" line 24,
program4.vhd
tmp__id_1073_tmp(0)=((a2(0)&b2(0)));
tmp__id_1073_tmp(1)=((a2(1)&b2(1)));
tmp__id_1072_(0)=(tmp__id_1073_tmp(0));
tmp__id_1072_(1)=(tmp__id_1073_tmp(1));
--function translation end: "Tr_2" line 24,
program4.vhd
z2(0)=tmp__id_1072_(0);
z2(1)=tmp__id_1072_(1);
file
file
file
file
Jeśli nie wykonalibyśmy modyfikacji nazw to równania wyglądałyby
następująca:
--function translation begin:
program4.vhd
tmp(0)=((a(0)&b(0)));
tmp(1)=((a(1)&b(1)));
tmp(2)=((a(2)&b(2)));
tmp__id_1072_(0)=(tmp(0));
tmp__id_1072_(1)=(tmp(1));
tmp__id_1072_(2)=(tmp(2));
"Tr_2"
line
23,
file
130
--function translation end: "Tr_2" line 23,
program4.vhd
z(0)=tmp__id_1072_(0);
z(1)=tmp__id_1072_(1);
z(2)=tmp__id_1072_(2);
--function translation begin: "Tr_2" line 24,
program4.vhd
tmp(0)=((a2(0)&b2(0)));
tmp(1)=((a2(1)&b2(1)));
tmp__id_1078_(0)=(tmp(0));
tmp__id_1078_(1)=(tmp(1));
--function translation end: "Tr_2" line 24,
program4.vhd
z2(0)=tmp__id_1078_(0);
z2(1)=tmp__id_1078_(1);
file
file
file
Jak możemy zauważyć w równaniach występuje dwukrotne przypisanie
do zmiennej tmp. Taka forma równań jest niedopuszczalna.
Na tym etapie zakończone są wszystkie przygotowania podprogramu do
przekładu na równania boolowskie. Generacją równań zajmuje się
odpowiednia funkcja.
3.
WNIOSKI
Bardzo istotną sprawą podczas generowania równań boolowskich jest
opracowanie szybkich i skutecznych algorytmów przekładu funkcji i
procedur. Tylko szybkie algorytmy umożliwią przemysłowe zastosowanie
powstającego w Katedrze Technik Programowania Wydziału Informatyki
Politechniki
Szczecińskiej
kompilatora.
Zaprezentowany
sposób
generowania równań boolowskich dla podprogramów jest poprawny lecz
przy wielokrotnie powtarzających się wywołaniach podprogramów
powoduje duże spowolnienie czasu kompilacji. Problem generowania
równań dla funkcji i procedur nie jest problemem prostym. Chcąc
przyspieszyć ten proces musimy spróbować modyfikować raz
wygenerowane równania dla podprogramu. Jeśli chcemy uzyskać algorytmy
szybkie, to ilość wygenerowanych równań będzie duża, gdy zmniejszymy
ilość równań to niestety przy małej liczbie wywołań tej samej funkcji
wydłuży się czas potrzebny na wygenerowanie równań boolowskich. Proces
przyspieszania generowania równań jest tematem mojej pracy doktorskiej i
zostanie szczegółowo omówiony w dalszych publikacjach.
131
LITERATURA
[1] FPGA Express, VHDL Reference Manual, 1997
[2] Jerzy Sołdek, Miejsce układów reprogramowalnych w informatyce, Materiały I Krajowej
Konferencji Naukowej. Reprogramowalne układy cyfrowe.
[3] Włodzimierz Wrona, VHDL język opisu i projektowania układów cyfrowych,
Wydawnictwo pracowni komputerowej Jacka Skalmierskiego, Gliwice 1998
[4] Pascal, M. Iglewski, J. Madej, S. Matwin, WNT1992
[5] Organizacja kompilatora do syntezy układów logicznych z syntezowalnego podzbioru
języka VHDL, W. Bielecki, S. Hayduke, R. Drążkowski, M. Liersz, M. Radziewicz,
P. Błaszyński, Materiały IV Sesji Naukowej Informatyki, INFORMA, Szczecin 1999
[6] Generacja i wyszukiwanie wartości semantycznych w kompilatorze języka VHDL
służącym do generacji równań boolowskich, Piotr Błaszyński, Materiały V Sesji
Naukowej Informatyki, INFORMA, Szczecin 2000
[7] Organizacja analizatora semantycznego kompilatora języka VHDL do syntezy układów
logicznych, P. Błaszyński, R. Drążkowski, Materiały III krajowej konferencji naukowej
RUC’2000, INFORMA, Szczecin2000
Mechanizm mapowania
Mirosław Mościcki
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
W przedstawionym opracowaniu zaprezentowany został sposób
generowania równań boolowskich dla wielokrotnie powtarzających się
mapowań na tą samą jednostkę. Algorytm ten opiera się na zapisie raz
wygenerowanych równań dla mapowanej jednostki w odpowiednim
metapliku. Dla każdej jednostki może istnieć wiele metaplików
zawierających równania. Oprócz plików z równaniami tworzony jest
dodatkowy plik zawierający informacje o mapowanych sygnałach
jednostki. W omówionym algorytmie pełny proces generowania równań
boolowskich dla takich samych argumentów odbywa się tylko raz.
Pokazano praktyczne zastosowanie opisanej metody.
Słowa kluczowe:
język VHDL, układy FPGA, równania boolowskie
1.
WSTĘP
Od ponad 2 lat w Katedrze Technik Programowania Wydziału
Informatyki Politechniki Szczecińskiej realizowany jest projekt, którego
celem jest stworzenie kompilatora dokonującego konwersji pliku
zawierającego program napisany w języku VHDL[1] na równania
boolowskie. Równania boolowskie są bardzo dobrym materiałem
wyjściowym do dalszej obróbki ponieważ jest to forma matematyczna. Na
ich podstawie można stworzyć układy cyfrowe realizujące określone
zadania, lub poddać je minimalizacji [2]. Równania boolowskie mogą być
wykorzystywane przy produkcji układów FPGA.
Rozwiązując bardziej złożony problem wyodrębnia się zwykle pewne jego
części, dla których formułuje się rozwiązania oddzielnie. Wydzielenie
134
podproblemów ma istotne zalety, gdyż umożliwia prowadzenie rozumowania na
ustalonych poziomach abstrakcji [4]. We wszystkich językach programowania
istnieją mechanizmy umożliwiające dzielenie rozwiązywanego problemu na
części. W języku VHDL możemy korzystać z podprogramów
oraz
komponentów. Podprogramy umożliwiają definiowanie algorytmów, które jako
oddzielne moduły programu mogą reprezentować wybrany element zachowania
się układu lub pozwalają wyliczać określone wartości. W języku VHDL
występują dwa rodzaje podprogramów: procedury i funkcje.
Język VHDL służy do projektowania cyfrowych układów logicznych [3].
Specyfika cyfrowych układów zachęca do stosowania podziału
projektowanego układu na komponenty [5]. Język VHDL umożliwia
projektowanie komponentów, specyfikacja komponentów jest szczegółowo
opisana w wielu pozycjach. Projektowanie komponentów prostą czynnością
i umożliwia projektantom wielokrotne wykorzystanie raz zaprojektowanego
układu. Ponadto wiele firm oferuje gotowe komponenty które możemy
wykorzystać w projektowanych układach.
Komponent może być zaprojektowany w sposób uniwersalny i z tego
powodu może być wielokrotnie wykorzystywany w tym samym projektowanym
układzie. Ze względu na złożoność zadań jakie wykonują komponenty ich
rozmiar może być znaczny, dlatego pojawia się problem szybkiego generowania
równań boolowskich dla nich. W niniejszym opracowaniu zostanie omówiony
problem generowania równań boolowskich dla komponentów.
2.
MECHANIZM MAPOWANIA
Składnia mapowania jest następująca:
Instance_name: component_name
[ generic map (
generic_name => expression
{ , generic_name => expression }
) ]
port map (
[ port_name => ] expression
{ , [ port_name => ] expression }
);
Podczas mapowania może występować generic, który powoduje
przypisanie wartości do odpowiadającej zmiennej zadeklarowanej w
jednostce. Dla każdej deklaracji komponentu musi istnieć jednostka o tej
samej nazwie. Przy mapowaniu możemy korzystać z notacji pozycyjnej
(positional notation) lub przez nazwę (name notation). Gdy chcemy
korzystać z mapowania musimy stworzyć komponent którego nazwa jest
135
taka sama jak nazwa mapowanej jednostki. Sygnały wewnątrz komponentu
muszą być zgodne z sygnałami w jednostce. Proces mapowania zostanie
opisany na podstawie poniższego przykładu:
Jednostka:
entity RAM is
generic( inout_range : integer );
port (
CLK: in STD_LOGIC;
RST: in STD_LOGIC;
OUT_DATA: out STD_LOGIC_VECTOR
downto 0)
);
end RAM;
(inout_range
-1
(inout_range
-1
Komponent:
component RAM is
generic( inout_range : integer );
port (
CLK: in STD_LOGIC;
RST: in STD_LOGIC;
OUT_DATA: out STD_LOGIC_VECTOR
downto 0)
);
end component ;
Przykładowe mapowania:
U_RAM_0: RAM
generic map (inout_range => 8)
port map(
CLK => CLK, RST =>RST,
OUT_DATA => Out_DataR0
);
U_RAM_1: RAM
generic map (inout_range => 8)
port map(
CLK => CLK, RST =>RST,
OUT_DATA => Out_DataR1
);
Przed przystąpieniem do generowania równań boolowskich program
źródłowy w języku VHDL musi zostać poddany analizie leksykalnej,
syntaktycznej oraz semantycznej [6] [7]. Gdy w programie występuje
mapowanie to musimy sprawdzić czy istnieje komponent oraz jednostka o
136
odpowiedniej nazwie. Następnie sprawdzana jest zgodność sygnałów
zadeklarowanych wewnątrz komponentu i jednostki, oraz zgodność
sygnałów występujących przy mapowaniu z sygnałami komponentu. Jeśli
sygnały są zgodne to można przeprowadzić proces mapowania.
Podczas mapowania generowane są równania boolowskie dla mapowanej
jednostki. Jeśli na tą samą jednostkę występuje kilka identycznych mapowań
to muszą być wygenerowane równania dla tej samej jednostki. Jedyna
różnica jaka występuje między równaniami to zmienione nazwy. Ponieważ
tak zakłada specyfikacja przyjęta podczas powstawania kompilatora. Czas
potrzebny na wygenerowanie równań dla jednego mapowania zależy od
złożoności mapowanej jednostki i jej architektury. Może to być kilka sekund
lub kilkadziesiąt minut. Jeśli dla każdego mapowania będziemy
przeprowadzali pełny proces przekładu źródła VHDL na równania
boolowskie, to wielokrotne mapowanie na tą samą jednostkę może
wykluczyć taki produkt z możliwości przemysłowego zastosowania z
powodu strasznie długiego czasu kompilacji. W takiej sytuacji należy
pominąć najbardziej czasochłonne operacje. Gdy choć raz wygenerujemy
równania boolowskie dla danej jednostki to możemy wielokrotnie korzystać
z tych równań dokonując w nich niezbędnych zmian.
Wykorzystanie gotowych równań jest możliwe tylko w przypadku gdy
wartość generic’a jest taka sama jak dla wygenerowanych poprzednio równań.
Oczywiście musimy zmienić nazwy we wszystkich zmiennych występujących
w równaniach, związane jest to z wymogami zapisanymi w specyfikacji:
przypisanie do tej samej zmiennej może występować tylko raz. Modyfikacją
nazw zajmuje się jedna z funkcji. Zastępuje ona nazwy sygnałów występujących
w mapowanej jednostce oraz modyfikuje nazwy pozostałych zmiennych.
Wykorzystanie wygenerowanych raz równań umożliwia skrócenie czasu
kompilacji. Zmniejszenie czasu kompilacji zależy od złożoności mapowanych
jednostek. Jeśli architektura mapowanej jednostki zawiera niewiele instrukcji to
przyspieszenie może pozostać niezauważone. Związane to jest z czasem
potrzebnym na sprawdzenie czy dla danej jednostki były już generowane
równania oraz jakie były wartości generic’a.
Po modyfikacji kompilatora dla każdego mapowania sprawdzamy czy
równania były już wygenerowane dla danej jednostki z danymi wartościami
generic’a. Jeśli tak to dokonujemy w nich koniecznych modyfikacji. Jeśli nie
to generujemy równania oraz zapisujemy je do oddzielnego pliku wraz z
informacją jakiej jednostki dotyczą oraz jakie były wartości generic’a, jeśli
on występował. Oczywiście przy tak skonstruowanym algorytmie należy
szczególną wagę przyłożyć do funkcji modyfikującej nazwy. Musi być ona
napisana z wykorzystaniem algorytmów których czas działania jest bardzo
krótki, gdyż tylko to umożliwia przyspieszenie czasu kompilacji projektów.
137
3.
WNIOSKI
Bardzo istotną sprawą podczas generowania równań boolowskich jest
opracowanie szybkich i skutecznych algorytmów wykonujących mapowania. Z
kilkunastu przeanalizowanych komercyjnych projektów wynika, że mapowania
występują znacznie częściej niż wywołania podprogramów. Dla kilkunastu
komercyjnych projektów napisanych w języku VHDL przyspieszenie było
niewidoczne lub w najbardziej korzystnym przypadku wyniosło 4 razy. Jest to
satysfakcjonująca wartość która może być zwiększona poprzez modyfikację
formatu w jakim są przechowywane wyniki kompilacji jednostek. Jeśli
opracowalibyśmy odpowiedni format w którym przechowywane byłyby
równania boolowskie np. rozbicie nazw zmiennych w równaniach na leksemy,
to proces przekładu powinien ulec dalszemu skróceniu. Związane jest to z tym,
że w zaimplementowanym algorytmie procesor dość dużo czasu spędza na
zmianie nazw zmiennych. Dla dużej liczby równań musimy wykonać wiele
zmian nazw a to powoduje wiele operacji na pamięci, które są czasochłonne.
Rozmiar plików z równaniami dla niektórych jednostek może wynosić nawet
kilka megabajtów. Jak widać istnieje jeszcze wiele sposobów modyfikacji
procesu generowania równań boolowskich dla instrukcji mapowania. Tylko
szybkie algorytmy umożliwią przemysłowe zastosowanie powstającego w
Katedrze Technik Programowania Wydziału Informatyki Politechniki
Szczecińskiej kompilatora.
LITERATURA
[1] FPGA Express, VHDL Reference Manual, 1997
[2] Jerzy Sołdek, Miejsce układów reprogramowalnych w informatyce, Materiały I Krajowej
Konferencji Naukowej. Reprogramowalne układy cyfrowe.
[3] Włodzimierz Wrona, VHDL język opisu i projektowania układów cyfrowych,
Wydawnictwo pracowni komputerowej Jacka Skalmierskiego, Gliwice 1998
[4] Pascal, M. Iglewski, J. Madej, S. Matwin, WNT1992
[5] Organizacja kompilatora do syntezy układów logicznych z syntezowalnego podzbioru
języka VHDL, W. Bielecki, S. Hayduke, R. Drążkowski, M. Liersz, M. Radziewicz,
P. Błaszyński, Materiały IV Sesji Naukowej Informatyki, INFORMA, Szczecin 1999
[6] Generacja i wyszukiwanie wartości semantycznych w kompilatorze języka VHDL
służącym do generacji równań boolowskich, Piotr Błaszyński, Materiały V Sesji
Naukowej Informatyki, INFORMA, Szczecin 2000
[7] Organizacja analizatora semantycznego kompilatora języka VHDL do syntezy układów
logicznych, P. Błaszyński, R. Drążkowski, Materiały III krajowej konferencji naukowej
RUC’2000, INFORMA, Szczecin2000
Implementacja bibliotek standardowych
Mirosław Mościcki
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
W opracowaniu zaprezentowany został sposób generowania równań
boolowskich dla funkcji z bibliotek standardowych. Algorytm ten opiera się
wewnętrznym wbudowaniu w generator równań szablonu równań dla
wszystkich koniecznych operacji zdefiniowanych w bibliotekach
standardowych. Dzięki zastosowaniu omówionego algorytmu proces
generowania równań boolowskich uległ znacznemu przyspieszeniu. W
niniejszym opracowaniu pokazano praktyczne zastosowanie opisanej metody.
Słowa kluczowe:
język VHDL, układy FPGA, równania boolowskie
1.
WSTĘP
W Katedrze Technik Programowania Wydziału Informatyki Politechniki
Szczecińskiej od kilku lat realizowany jest projekt, którego celem jest
stworzenie kompilatora języka VHDL[1] dokonującego konwersji programu
napisanego w tym języku na równania boolowskie. Równania boolowskie
umożliwiają prowadzenie dalszej optymalizacji układu ponieważ jest to
forma matematyczna. Możemy je minimalizować pod względem czasu lub
objętości. Równania boolowskie mogą być wykorzystywane przy produkcji
układów FPGA[2].
Jeżeli spojrzymy na kilka dowolnych programów napisanych w języku
VHDL[3] to od razu zauważymy, że pewne operacje wykonywane są
częściej od pozostałych. Operacje te są zdefiniowane w pakiecie
standardowym oraz w innych pakietach. Najczęściej wykorzystywanymi
pakietami są:
140
STD_LOGIC_1164
STD_LOGIC_ARITH
STD_LOGIC_SIGNED
STD_LOGIC_UNSIGNED
NUMERIC_BIT
NUMERIC_STD
Pakiety te definiują szereg różnych operacji: operacje arytmetyczne (+,,*) oraz operacje przesunięcia o określoną ilość bitów. Operacje te mogą być
wykonywane na wielu bitach. W pakietach tych zdefiniowane są również
nowe typy danych np. SIGNED, UNSIGNED. Ponieważ operacje
zdefiniowane w tych pakietach wywoływane są bardzo często to istnieje
potrzeba zaimplementowania ich w wewnętrzną strukturę kompilatora.
Pozwala to na znaczne skrócenie czasu kompilacji. Takie rozwiązanie jest
niezbędne ponieważ z przeprowadzonych obserwacji wynika, że w
przeciwnym razie długi czas kompilacji wykluczałby taki produkt z
możliwości przemysłowego zastosowania.
W niniejszym opracowaniu zostanie omówiony problem implementacji
bibliotek standardowych.
Od ponad 2 lat w Katedrze Technik Programowania Wydziału
Informatyki Politechniki Szczecińskiej realizowany jest projekt, którego
celem jest stworzenie kompilatora dokonującego konwersji pliku
zawierającego program napisany w języku VHDL[1] na równania
boolowskie. Równania boolowskie są bardzo dobrym materiałem
wyjściowym do dalszej obróbki ponieważ jest to forma matematyczna. Na
ich podstawie można stworzyć układy cyfrowe realizujące określone
zadania, lub poddać je minimalizacji [2]. Równania boolowskie mogą być
wykorzystywane przy produkcji układów FPGA.
Rozwiązując bardziej złożony problem wyodrębnia się zwykle pewne
jego części, dla których formułuje się rozwiązania oddzielnie.. Wydzielenie
podproblemów ma istotne zalety, gdyż umożliwia prowadzenie
rozumowania na ustalonych poziomach abstrakcji [4]. We wszystkich
językach programowania istnieją mechanizmy umożliwiające dzielenie
rozwiązywanego problemu na części. W języku VHDL możemy korzystać z
podprogramów oraz komponentów. Podprogramy umożliwiają definiowanie
algorytmów, które jako oddzielne moduły programu mogą reprezentować
wybrany element zachowania się układu lub pozwalają wyliczać określone
wartości. W języku VHDL występują dwa rodzaje podprogramów:
procedury i funkcje.
Język VHDL służy do projektowania cyfrowych układów logicznych [3].
Specyfika cyfrowych układów zachęca do stosowania podziału
projektowanego układu na komponenty [5]. Język VHDL umożliwia
141
projektowanie komponentów, specyfikacja komponentów jest szczegółowo
opisana w wielu publikacjach. Projektowanie komponentów jest prostą
czynnością i umożliwia projektantom wielokrotne wykorzystanie raz
zaprojektowanego układu. Ponadto wiele firm oferuje gotowe komponenty
które możemy wykorzystać w projektowanych układach.
Komponent może być zaprojektowany w sposób uniwersalny i z tego
powodu może być wielokrotnie wykorzystywany w tym samym
projektowanym układzie. Ze względu na złożoność zadań jakie wykonują
komponenty ich rozmiar może być znaczny, dlatego pojawia się problem
szybkiego generowania równań boolowskich dla nich. W niniejszym
opracowaniu zostanie omówiony problem generowania równań boolowskich
dla komponentów.
2.
MECHANIZM WYWOŁYWANIA FUNKCJI
W skład wyżej wymienionych pakietów wchodzą równe funkcji w tym
również funkcje przeciążające podstawowe operatory. Postać syntaktyczna
deklaracji funkcji jest następująca:
Deklaracja_funkcji ::= specyfikacja_funkcji ;
Specyfikacja_funkcji ::= [ pure | impure ] function
nazwa funkcji
[ ( lista_parametrów_formalnych ) ] return
nazwa_typu
nazwa_funkcji ::= identyfikator | symbol_operatora
Funkcja w swojej deklaracji posiada nazwę, listę parametrów formalnych
oraz typ wyrażenia zwracany przez funkcję. Po słowie function znajduje się
nazwa funkcji lub symbol operacji oraz lista parametrów formalnych, która
jest opcjonalna. W przeciwieństwie do procedury parametry funkcji mogą
być tylko wejściowe (in). Postać syntaktyczna deklaracji parametrów jest
taka sama jak w przypadku deklaracji portów:
[ nazwa_parametru : rodzaj_przesyłu_danych
typ_parametru
{ ; nazwa_parametru : rodzaj_przesyłu_danych
typ_parametru } ]
Poniżej pokazano kilka deklaracji funkcji pochodzących z pakietu
STD_LOGIC_ARITH.
142
function
"+"(L:
UNSIGNED;
R:
UNSIGNED)
return
UNSIGNED;
function
"-"(L:
UNSIGNED;
R:
UNSIGNED)
return
UNSIGNED;
function "ABS"(L: SIGNED) return STD_LOGIC_VECTOR;
function SHL(ARG: UNSIGNED; COUNT: UNSIGNED) return
UNSIGNED
W procesie przekładu funkcji z bibliotek standardowych poza
generatorem kodu bierze udział również analizator semantyczny. W kod
analizatora semantycznego wbudowana jest definicja typów zdefiniowanych
w tych pakietach[4]. Jest to konieczne do przeprowadzenia pełnej i
poprawnej analizy semantycznej programu napisanego w języku VHDL.
Jeżeli w programie napotkamy funkcję lub operator następuje sprawdzenie
czy dla takich argumentów istnieje odpowiednia funkcja. Jeśli tak to
kolejnym krokiem jest sprawdzenie czy znaleziona funkcja jest funkcją
pochodzącą z biblioteki standardowej. Jeśli tak to są generowane
odpowiednie równania boolowskie których postać jest uzależniona od
rodzaju operacji oraz przekazanych argumentów. Rozważmy cały proces na
odpowiednim przykładzie.
library IEEE;
use IEEE.std_logic_1164.all;
entity test is
port (
l : in std_ulogic;
r : in std_ulogic;
wyj : out UX01
); end test;
architecture test of test is
Begin
test: wyj<= "xor"(l=>l,r=>r);
end test;
W powyższym przykładzie występuje wywołanie funkcji xor z pakietu
STD_LOGIC_1164. Jako pierwsze sprawdzamy czy funkcja o takiej nazwie
istnieje w kodzie źródłowym programu lub w dołączonych pakietach.
Pakiety są dołączane przy wykorzystaniu dyrektywy use. Ponieważ definicja
funkcji o takiej nazwie istnieje w pakiecie STD_LOGIC_1164 więc drugim
krokiem jest sprawdzenie czy argumenty stanowiące wywołanie tej funkcji
zgadzają się z argumentami występującymi w deklaracji tej funkcji w
143
pakiecie. Podczas sprawdzania zgodności sprawdzana jest zgodność typów
oraz w niektórych przypadkach długość przekazywanych argumentów. W
powyższym przykładzie typy są zgodne więc odpowiednia funkcja została
znaleziona. Ostatnim krokiem jest wywołanie odpowiedniej funkcji. Dla
każdej funkcji z pakietów wbudowanych istnieje odpowiednia funkcja która
zajmuje się generowaniem równań boolowskich. Dla powyższego przykładu
zostaną wygenerowane następujące równania:
wyj=((l&!r)|(!l&r));
3.
WNIOSKI
Funkcje z omówionych pakietów są bardzo często wywoływane, dlatego
bardzo istotną sprawą podczas generowania równań boolowskich jest
opracowanie szybkich i skutecznych algorytmów generujących równania
boolowskie dla nich. Istotną rolę w tym procesie odgrywa analizator
semantyczny[5][6]. Proces wbudowania tych funkcji w kod kompilatora
spowodował wielokrotne przyspieszenie procesu kompilacji. Z
kilkudziesięciu wybranych losowo i przeanalizowanych przykładów wynika,
że wywołania funkcji z pakietów jest najczęściej wykonywaną operacją.
Osiągnięte przyspieszenie w zależności od stopnia skomplikowania operacji
wyniosło od kilkunastu do kilkudziesięciu razy. Jest to bardzo
satysfakcjonująca wartość która może być jeszcze poprawiona lecz niestety
tylko w minimalnym stopniu. Związane jest to z tym, że w
zaimplementowanym algorytmie procesor dość dużo czasu spędza na
poszukiwaniu odpowiedniej funkcji. Przyspieszania procesu kompilacji jest
bardzo istotne, ponieważ tylko szybkie algorytmy umożliwią przemysłowe
zastosowanie powstającego w Katedrze Technik Programowania Wydziału
Informatyki Politechniki Szczecińskiej kompilatora.
LITERATURA
[1] FPGA Express, VHDL Reference Manual, 1997
[2] Jerzy Sołdek, Miejsce układów reprogramowalnych w informatyce, Materiały I Krajowej
Konferencji Naukowej. Reprogramowalne układy cyfrowe.
[3] Włodzimierz Wrona, VHDL język opisu i projektowania układów cyfrowych,
Wydawnictwo pracowni komputerowej Jacka Skalmierskiego, Gliwice 1998
[4] Organizacja kompilatora do syntezy układów logicznych z syntezowalnego podzbioru
języka VHDL, W. Bielecki, S. Hayduke, R. Drążkowski, M. Liersz, M. Radziewicz,
P. Błaszyński, Materiały IV Sesji Naukowej Informatyki, INFORMA, Szczecin 1999
144
[5] Generacja i wyszukiwanie wartości semantycznych w kompilatorze języka VHDL
służącym do generacji równań boolowskich, Piotr Błaszyński, Materiały V Sesji
Naukowej Informatyki, INFORMA, Szczecin 2000
[6] Organizacja analizatora semantycznego kompilatora języka VHDL do syntezy układów
logicznych, P. Błaszyński, R. Drążkowski, Materiały III krajowej konferencji naukowej
RUC’2000, INFORMA, Szczecin2000
Tłumaczenie instrukcji generate
Sławomir Wernikowski
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Prezentowany tekst przedstawia algorytm tłumaczenia instrukcji generate
stanowiącej element języka VHDL układów logicznych.
Słowa kluczowe:
kompilator VHDL, synteza układów FPGA
1.
WSTĘP
Artykuł opisuje część kompilatora języka VHDL wykonującą wstępne
tłumaczenie instrukcji generate. Charakter tej instrukcji sprawia, że można ją
traktować jako specyficzną formę makrogeneracji co powoduje, że
przedmiotowy fragment kompilatora wykonuje czynności związane
z rozwinięciem ciała instrukcji i usunięciem samej instrukcji z wejściowego
ciągu leksemów a ciąg powstający na wyjściu przekazywany jest do dalszej
analizy i tłumaczenia pozostałym komponentom kompilatora. W artykule
podano informacje na temat składni instrukcji, zastosowanego algorytmu
rozwijania instrukcji oraz podano przykładowe fragmenty kodu zawierającego
instrukcję generate wraz z przedstawieniem efektów jej rozwinięcia.
2.
SKŁADNIA INSTRUKCJI
Instrukcja generate występuje w języku VHDL w dwóch rożnych
formach składniowych. Pierwsza z nich to postać iterowana, którą można
opisać w poniższy sposób:
146
[<etykieta> :]
for <zmienna> in <zakres> generate
<instrukcja>...
end generate [etykieta];
Poniższy przykład obrazuje wykorzystanie tej formy instrukcji.
for i in 1 to 10 generate
s(i) <= ’0’;
end generate;
Ta postać instrukcji generate używana jest do skracania zapisu ciągu
instrukcji, które różnią się użyciem iteracyjnie zmieniających się wartości
np. indeksów tablic czy parametrów wywołania funkcji.
Druga forma instrukcji generate to postać warunkowa, opisana poniżej:
[<etykieta> :]
if <wyrażenie_boolowskie> generate
<instrukcja>...
end generate [etykieta];
Poniższy przykład obrazuje wykorzystanie tej formy instrukcji.
if i > 2 generate
s(i) <= ’1’;
end generate;
Powyższa forma instrukcji służy do warunkowego włączania ciągu
instrukcji.
Obie postaci instrukcji mogą być dowolnie w sobie zagnieżdżane.
3.
PROCES TŁUMACZENIA
Tłumaczenie obu postaci instrukcji przebiega w odmienny sposób.
Tłumaczenie instrukcji iterowanej polega na n-krotnym (gdzie n jest
liczebnością zbioru wartości opisanych przez zakres) wygenerowaniu ciągu
leksemów stanowiących ciało instrukcji generate z jednoczesnym
zastępowaniem wszystkich wystąpień zmiennej sterującej jej kolejnymi
wartościami (leksemami reprezentującymi literały). Można stwierdzić, iż
takie postępowanie nosi pewne cechy makrogeneracji z tym, że w trakcie
147
tłumaczenia należy brać pod uwagę kwestie związane z semantyką języka,
czego klasyczne makrogeneratory (preprocesory) z reguły nie analizują.
Tłumaczenie instrukcji warunkowej sprowadza się do wygenerowania
bądź nie ciągu instrukcji stanowiących ciało instrukcji generate w zależności
od wartości wyrażenia boolowskiego. Nie zachodzą żadne modyfikacje
konstrukcji wewnątrz ciała instrukcji.
Całość czynności związanych z rozwinięciem instrukcji generate
wykonuje funkcja o nagłówku:
int analyzeGenerateStatement(int toktab[])
Argumentem funkcji jest zakończona znacznikiem końca tablica
leksemów toktab zawierająca wszystkie leksemy składające się na
kompletną instrukcję generate. Wyodrębnienia instrukcji generate dokonuje
analizator semantyczny, dzięki czemu można założyć, że przekazany ciąg
leksemów jest już sprawdzony pod względem składniowym, co pozwala
ograniczyć czynności kontroli syntaktycznej do niezbędnego minimum.
Powodzenie
bądź
niepowodzenie
tłumaczenia
funkcja
analyzeGenerateStatement sygnalizuje zwracaną wartością: “0” oznacza
wykrycie błędu, “1” poprawne zakończenie tłumaczenia oraz “2” które
oprócz takiego znaczenia jak “1” dodatkowo sygnalizuje, że wewnątrz
rozwijanej instrukcji generate wykryto istnienie konstrukcji port map.
W przypadku pomyślnego zakończenia tłumaczenia w tablicy toktab
zostaje umieszczony ciąg leksemów zawierający rozwinięte postaci
wszystkich instrukcji generate zawartych w ciągu wejściowej. Leksemy
składające się na samą instrukcję generate (tzn. jej nawiasy syntaktyczne)
zostają usunięte i nie pojawiają się w ciągu wyjściowym.
Algorytm rozwijania instrukcji generate opisać można w następujący
sposób:
– wejściowy ciąg leksemów przeglądany jest od lewej do prawej w
poszukiwaniu wystąpienia podciągów sygnalizujących początek
instrukcji if..generate bądź for...generate.
– jeśli wykryto iteracyjną postać instrukcji to:
– rozpoznaje się i zapamiętuje leksem reprezentujący zmienną sterującą;
informacja ta będzie potrzebna w kolejnych krokach, jako że zmienne
sterujące instrukcji generate wytwarzają własny zasięg i tym samym
przesłaniają ewentualnie istniejące zmienne o tych samych nazwach
– z wykorzystaniem funkcji evalIntExpression oraz iGetRangeAttr
(udostępnianych przez moduł generatora kodu) ustala się wartości
dolnego i górnego końca zakresu; ustala się też kierunek przyrostu
wartości zmiennej sterującej;
148
– dysponując powyższymi informacjami przystępuje się do cyklicznego
przeglądania ciągu leksemów w ciele instrukcji generate;
– w każdym przebiegu przeglądania ma miejsce wygenerowanie
leksemu reprezentującego literał odpowiadający bieżącej wartości
zmiennej sterującej (przy użyciu funkcji TmpVarNumber analizatora
leksykalnego a następnie zastąpienie leksemu reprezentującego
zmienną sterującą leksemem literału bieżącej wartości zmiennej
– jeżeli wewnątrz ciała instrukcji generate zostanie wykryte istnienie
bloku (słowo kluczowe block) lub procesu (słowo kluczowe process)
to podmiana leksemów zostanie dokonana tylko wtedy, gdy wewnątrz
zasięgów wytworzonych przez te konstrukcje nie istnieją obiekty
(zmienne, sygnały etc) o nazwie przesłaniającej nazwę zmiennej
sterującej; komplet czynności związanych z tymi działaniami
wykonuje wyodrębniona funkcja, która jednocześnie wprowadza do
tablicy wartości semantycznych informację o kolejno
wygenerowywanych kopiach bloków i/lub procesów (należy pamiętać,
że każdy przejście instrukcji generate wytwarza nową kopię bloku lub
procesu); funkcja ta odpowiedzialna jest również za
wygenerowywanie unikalnych nazw dla nowoutworzonych procesów i
bloków; funkcja ta może być wywoływana rekurencyjnie, jeśli
analizowane konstrukcje zawierają w sobie dalsze zagnieżdżenia;
– czynności powyższe wykonuje się aż do wyczerpania zakresu
zmiennej sterującej
– jeśli wykryto warunkową postać instrukcji to:
– przy wykorzystaniu funkcji evalBoolExpression z modułu generatora
kodu wartościuje się wyrażenie logiczne
– jeśli wartość wyrażenia wynosi true to kopiuje się na wyjście ciąg
leksemów zawartych w ciele instrukcji if..generate;
– jeśli wartość wyrażenia wynosi false to nie wykonuje się żadnego
kopiowania leksemów na wyjście a działanie algorytmu sprowadza się
jedynie do usunięcia nawiasów syntaktycznych instrukcji if..generate.
4.
PRZYKŁADY
Ponizej zostaną podane przykłady obrazujące sposób tłumaczenia
(rozwijania) intrukcji generate.
Przykład 1.
W poniższym programie
149
library IEEE;
use IEEE.std_logic_1164.all;
entity test is
port(
A:in std_logic_vector(0 to 3);
B:out std_logic_vector(0 to 3));
end;
architecture beh of test is
begin
ET1: for i in 0 to 3 generate
ET2: if (i<1) generate
B(i) <= '0';
end generate;
ET3: if (i>1) generate
B(i) <= not A(i);
end generate;
ET4: if (i=1) generate
B(i) <= A(i);
end generate;
end generate;
end;
ciąg leksemów odpowiadających leksykalnie ciału najbardziej
zewnętrznej instrukcji generate: zostanie w wyniku działania funkcji
analyzeGenerateStatement rozwinięty do postaci:
B(0)
B(1)
B(2)
B(3)
<=
<=
<=
<=
’0’;
A(1);
not A(2);
not A(3);
Przykład 2.
W poniższym programie
entity test is
port (
A:in STD_LOGIC_vector(7 downto 0);
B:in STD_LOGIC;
C:in STD_LOGIC;
O: out STD_LOGIC_vector(7 downto 0)
);
end test;
architecture test3 of test is
begin
A0: for i in 0 to 7 generate
B0: if (i >= 3 and i <= 5) generate
150
O(i) <= A(i) and B;
end generate;
B1: if (i < 3 or i > 5) generate
O(i) <= A(i) or C;
end generate;
end generate;
end test3;
ciąg leksemów odpowiadających leksykalnie ciału najbardziej
zewnętrznej instrukcji generate zostanie w wyniku działania funkcji
analyzeGenerateStatement rozwinięty do postaci:
O(0)
O(1)
O(2)
O(3)
O(4)
O(5)
O(6)
O(7)
5.
<=
<=
<=
<=
<=
<=
<=
<=
A(0)
A(1)
A(2)
A(3)
A(5)
A(5)
A(6)
A(7)
or C;
or C;
or C;
and B;
and B;
and B;
or C;
or C;
PODSUMOWANIE
Zastosowany algorytm dowiódł swojej poprawności w praktycznych
testach a jego wydajność silnie zależy od liczby odwołać do procedur
analizatora semantycznego. Każde takie odwołanie związane jest z
przeszukaniem tablicy wartości semantycznych i to w głównej mierze
rzutuje na efeklywność działania algorytmu.
Generowanie równań boolowskich dla
przerzutników w kompilatorze języka VHDL
do symulacji i syntezy układów logicznych
Włodzimierz Bielecki, Tomasz Wierciński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Artykuł przedstawia rodzaje przerzutników i zatrzasków generowane
w kompilatorze języka VHDL tworzonego na Wydziale Informatyki
Politechniki Szczecińskiej. Typ generowanego przerzutnika lub zatrzasku
jest dopasowany do danej instrukcji języka VHDL, która wymaga użycia
logiki sekwencyjnej. Każdy z opisanych przerzutników został
zaprojektowany w dwóch wersjach. Elementy w pierwszej wersji
opóźniają o jeden takt zegara zmianę wartości wyjścia względem wejścia,
natomiast w wersji drugiej zmiana wartości wejściowej jest przenoszona
na wyjście w tym samym takcie zegara. W pracy przedstawione są
ponadto przebiegi czasowe oraz równania boolowskie opisujące wejścia
poszczególnych elementów sekwencyjnych dla podanych przykładów.
Słowa kluczowe:
VHDL, przerzutniki, układy logiczne, syntezy, równania boolowskie
1.
WSTĘP
Język VHDL służy do projektowania, symulacji i syntezy układów
logicznych FPGA (ang. Field Programmable Gate Arrays). Układy te
stanowią nowe podejście w tworzeniu systemów cyfrowych umożliwiające
umieszczenie w jednej strukturze logicznej całego skomplikowanego
systemu. Zaprogramowanie takiego układu jest możliwe przez użytkownika
bez konieczności angażowania wyspecjalizowanych przedsiębiorstw.
Stosując odpowiednie konstrukcje języka VHDL, można opisywać zarówno
152
kombinacyjne jak i sekwencyjne, tj. pamiętające elementy układów
cyfrowych. Za pomocą języka VHDL można opisywać zarówno pojedyncze
elementy jak i całe, złożone struktury logiczne [3].
Opisane w niniejszej pracy rozwiązania powstały dla potrzeb kompilatora
VHDL2BOOL języka VHDL tworzonego w Katedrze Technik
Programowania Wydziału Informatyki Politechniki Szczecińskiej dla firmy
Aldec. Jest on oparty na specyfikacji firmy Synopsys dotyczącej
kompilatorów do syntezy logicznej. Kompilator ten wyróżnia się jednak
sposobem generowania wyniku w postaci zbioru równań boolowskich
opisującego projektowany układ. Istnieją dwa podstawowe powody
zastosowania tego typu rozwiązania:
– równania boolowskie mogą podlegać optymalizacji, co zmniejsza ilość
elementów układu a tym samym obniża jego koszty i przyśpiesza
działanie,
– równania takie są odpowiednikiem sieci bramek logicznych, które można
bezpośrednio zawrzeć w układzie scalonym FPGA.
Kompilator generuje równania logiczne zarówno dla logiki
asynchronicznej (kombinacyjnej), synchronicznej (sekwencyjnej) jak i
mieszanej. Logika synchroniczna w przeciwieństwie do asynchronicznej
posiada układy pamiętające w postaci przerzutników oraz zatrzasków
korzystające z wartości wyjść z obecnego i poprzedniego taktu zegara.
W niniejszej pracy zostały opisane rodzaje przerzutników i zatrzasków
generowane przez kompilator VHDL2BOOL. Do każdej, wymagającej
logiki synchronicznej, konstrukcji języka VHDL dopasowano odpowiedni
układ sekwencyjny tak aby odzwierciedlał on właściwości funkcjonalne tej
konstrukcji a zarazem zawierał optymalną liczbę elementów. Każdy z
układów został zaprojektowany w dwóch wersjach. W pierwszej z nich, w
przypadku zmiany sygnału wejściowego w czasie zmiany sygnału
zegarowego, zmiana sygnału wyjściowego jest opóźniona względem zmiany
na wejściu o jeden takt zegara. Druga wersja układów nie zawiera opisanego
opóźnienia co oznacza, że zmiana sygnału wejściowego zachodząca w
czasie zmiany sygnału zegarowego jest przenoszona na wyjście w tym
samym takcie zegara.
153
2.
RODZAJE GENEROWANYCH ZATRZASKÓW
I PRZERZUTNIKÓW
2.1
Zatrzask typu D wyzwalany poziomem wysokim
Zatrzaski tego typu są generowane w dwóch poniższych przypadkach.
2.1.1
Instrukcja case, w której przypisanie do danego sygnału
nie występuje we wszystkich jej gałęziach [1, 2]
case wyrażenie is
when wartość_wyboru => cel_przypisania1
{cel_przypisaniaN
when wartość_wyboru => cel_przypisania2
{cel_przypisaniaN
end case;
2.1.2
<=
<=
<=
<=
wyrażenie;
wyrażenie}
wyrażenie
wyrażenie};
Instrukcja warunkowa if, w której wyrażenie warunkowe jest
nieokreślone oraz przypisanie do danego sygnału
nie występuje we wszystkich jej gałęziach i poza nią
(przypisanie domyślne) [1, 2]
if wyrażenie1 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenie2 then
cel_przypisania2 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania2 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
Na rysunkach 1a i 1b przedstawiony jest schemat i przebieg czasowy
zatrzasku typu D wyzwalanego poziomem wysokim [4, 5].
154
Rysunek 6a. Zatrzask typu D wyzwalany poziomem wysokim
Równania boolowskie opisujące przerzutnik mają postać:
--latch (C_high, D)
S = D & C;
R = C & !D;
Q(t) = S | (!R & Q(t-1));
gdzie &, |, ! oznaczają odpowiednie operacje logiczne: AND, OR oraz NOT.
Przykład 1.
Dla źródła:
if clk = ‘1’ then
Q <= A;
end if;
równania boolowskie dla wejść C i D mają postać:
C = clk;
D = clk & A;
Rysunek 1b. Przebieg czasowy
155
2.2
Przerzutnik typu D wyzwalany zboczem
narastającym
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.2.1
Instrukcja warunkowa if zawierająca wyrażenie w postaci
sygnału z atrybutem event lub funkcją rising_edge [1, 2]
if wyrażenie then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
end if
wyrażenie
:=
(sygnal’event
and
sygnal’rising_edge
2.2.2
sygnal=’1’)
|
Instrukcja oczekiwania wait wraz z instrukcją przypisania [1, 2]
wait wyrażenie
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
wyrażenie := (sygnal’event and sygnal=’1’) | sygnal =
‘1’
Przykład 2.
Dla źródła:
if clk’event and clk=’1’ then
Q <= A;
end if;
równania boolowskie dla wejść C i D mają postać:
C = clk;
D = A;
2.2.3
Przerzutnik D opóźniający
Na rysunkach 2.1a i 2.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D wyzwalanego zboczem narastającym z opóźnieniem
czasowym [4, 5].
156
Rysunek 2.1a. Przerzutnik typu D z opóźnieniem czasowym wyzwalany zboczem
narastającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop (C_high,D)
S = C & Q1(t);
R = C & !Q1(t);
Q(t) = S | (!R & Q(t-1));
S1 = D & !C;
R1 = !C & !D;
Q1(t) = S1 | (!R1 & Q1(t-1));
Rysunek 2.1b. Przebieg czasowy
2.2.4
Przerzutnik D nie opóźniający
Na rysunkach 2.2a i 2.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D wyzwalanego zboczem narastającym bez opóźnienia
czasowego [4, 5].
157
Rysunek 2.2a. Przerzutnik typu D bez opóźnienia czasowego wyzwalany zboczem
narastającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_high,D)
S2 = C & Q3(t-1);
Q2(t) = !S2 | (D & Q2(t-1));
R3 = !(D & Q2(t-1));
Q3(t) = !C | (R3 & Q3(t-1));
Q(t) = !Q3(t) | (Q2(t) & Q(t-1));
Rysunek 2.2b. Przebieg czasowy
2.3
Przerzutnik typu D wyzwalany zboczem opadającym
Przerzutniki tego typu są generowane w poniższych przypadkach.
158
2.3.1
Instrukcja warunkowa if zawierająca wyrażenie w postaci
sygnału z atrybutem event lub funkcją falling_edge [1, 2]
if wyrażenie then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
end if
wyrażenie
:=
(sygnal’event
and
sygnal’falling_edge
2.3.2
sygnal=’0’)
|
Instrukcja oczekiwania wait wraz z instrukcją przypisania [1, 2]
wait wyrażenie
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
wyrażenie := (sygnal’event and sygnal=’0’) | sygnal =
‘0’
Przykład 3.
Dla źródła
if clk’event and clk=’0’ then
Q <= A;
end if;
równania boolowskie dla wejść C i D mają postać:
C = clk;
D = A;
2.3.3
Przerzutnik D opóźniający
Na rysunkach 3.1a i 3.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D wyzwalanego zboczem opadającym z opóźnieniem
czasowym [4, 5].
Rysunek 7.1a. Przerzutnik typu D z opóźnieniem czasowym wyzwalany zboczem opadającym
159
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_low,D)
S = !C & Q1(t);
R = !C & !Q1(t);
Q(t) = S | (!R & Q(t-1));
S1 = D & C;
R1 = C & !D;
Q1(t) = S1 | (!R1 & Q1(t-1));
Rysunek 3.1b. Przebieg czasowy
2.3.4
Przerzutnik D nie opóźniający
Na rysunkach 3.2a i 3.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D wyzwalanego zboczem opadającym bez opóźnienia
czasowego [4, 5].
Rysunek 3.2a. Przerzutnik typu D bez opóźnienia czasowego wyzwalany zboczem opadającym
160
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_low,D)
S2 = !C & Q3(t-1);
Q2(t) = !S2 | (D & Q2(t-1));
R3 = !(D & Q2(t-1));
Q3(t) = C | (R3 & Q3(t-1));
Q(t) = !Q3(t) | (Q2(t) & Q(t-1));
Rysunek 3.2b. Przebieg czasowy
2.4
Przerzutnik typu D wyzwalany zboczem
narastającym z asynchronicznymi wejściami R i S
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.4.1
Instrukcja warunkowa if zawierająca w części elseif wyrażenie
w postaci sygnału z atrybutami event, rising_edge [1, 2]
if wyrażenie1 then
cel_przypisania1 <= wyrażenie
{ cel_przypisaniaN <= wyrażenie }
elseif wyrażenie2 then
cel_przypisania1 <= wyrażenie
{ cel_przypisaniaN <= wyrażenie }
end if
wyrażenie2
:=
(sygnal’event
and
sygnal’rising_edge
Przykład 4.
Dla źródła
if St=’0’ then
sygnal=’1’)
|
161
Q <= ‘0’;
elsif clk’event and clk=’1’ then
Q <= A;
end if;
równania boolowskie dla wejść C, D, R i S mają postać:
C
D
R
S
2.4.2
=
=
=
=
clk;
A;
!St & !0;
!St & 0;
Przerzutnik opóźniający
Na rysunkach 4.1a i 4.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D z asynchronicznymi wejściami R i S wyzwalanego
zboczem narastającym z opóźnieniem czasowym [4, 5].
Rysunek 4.1a. Przerzutnik typu D z asynchronicznymi wejściami R i S wyzwalany zboczem
narastającym z opóźnieniem czasowym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(R,S,C_high,D)
S1 = S | (C & Q1(t));
R1 = R | (C & !Q1(t));
Q(t) = S1 | (!R1 & Q(t-1));
S2 = S | (D & !C & !(R | S));
R2 = R | (!D & !C & !(R | S));
Q1(t) = S2 | (!R2 & Q1(t-1));
162
Rysunek 4.1b. Przebieg czasowy
2.4.3
Przerzutnik nie opóźniający
Na rysunkach 4.2a i 4.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D z asynchronicznymi wejściami R i S wyzwalanego
zboczem narastającym bez opóźnienia czasowego [4, 5].
Rysunek 4.2a. Przerzutnik typu D z asynchronicznymi wejściami R i S wyzwalany zboczem
narastającym bez opóźnienia czasowego
163
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(R,S,C_high,D)
R2 = D & !R;
S2 = C & Q3(t-1);
Q2(t) = !S2 | (R2 & Q2(t-1));
R3 = !S & !(R2 & Q2(t-1));
S3 = C & !R;
Q3(t) = !S3 | (R3 & Q3(t-1));
R1 = !R & Q2(t);
S1 = !S & Q3(t);
Q(t) = !S1 | (R1 & Q(t-1));
Rysunek 4.2b. Przebieg czasowy
2.5
Przerzutnik typu D wyzwalany zboczem opadającym
z asynchronicznymi wejściami R i S
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.5.1
Instrukcja warunkowa if zawierająca w części elseif wyrażenie w
postaci sygnału z atrybutem event lub funkcją falling_edge [1, 2]
if wyrażenie1 then
cel_przypisania1 <= wyrażenie
164
{cel_przypisaniaN <= wyrażenie}
elseif wyrażenie2 then
cel_przypisania1 <= wyrażenie
{ cel_przypisaniaN <= wyrażenie }
end if
wyrażenie2
:=
(sygnal’event
and
sygnal’falling_edge
sygnal=’0’)
|
Przykład 5.
Dla żródła:
if St=’0’ then
Q <= ‘0’;
elsif clk’event and clk=’0’ then
Q <= A;
end if;
równania boolowskie wejść C, D, R i S mają postać:
C
D
R
S
2.5.2
=
=
=
=
clk;
A;
!St & !0;
!St & 0;
Przerzutnik opóźniający
Na rysunkach 5.1a i 5.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D z asynchronicznymi wejściami R i S wyzwalanego
zboczem opadającym z opóźnieniem czasowym [4, 5].
Rysunek 5.1a. Przerzutnik typu D z asynchronicznymi wejściami R i S wyzwalany zboczem
opadającym z opóźnieniem czasowym
165
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(R,S,C_low,D)
S1 = S | (!C & Q1(t));
R1 = R | (!C & !Q1(t));
Q(t) = S1 | (!R1 & Q(t-1));
S2 = S | (D & C & !(R | S));
R2 = R | (!D & C & !(R | S));
Q1(t) = S2 | (!R2 & Q1(t-1));
Rysunek 5.1b. Przebieg czasowy
2.5.3
Przerzutnik nie opóźniający
Na rysunkach 5.2a i 5.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika typu D z asynchronicznymi wejściami R i S wyzwalanego
zboczem opadającym bez opóźnienia czasowego [4, 5].
166
Rysunek 5.2a. Przerzutnik typu D z asynchronicznymi wejściami R i S wyzwalany zboczem
opadającym bez opóźnienia czasowego
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(R,S,C_low,D)
R2 = D & !R;
S2 = !C & Q3(t-1);
Q2(t) = !S2 | (R2 & Q2(t-1));
R3 = !S & !(R2 & Q2(t-1));
S3 = !C & !R;
Q3(t) = !S3 | (R3 & Q3(t-1));
R1 = !R & Q2(t);
S1 = !S & Q3(t);
Q(t) = !S1 | (R1 & Q(t-1));
167
Rysunek 5.2b. Przebieg czasowy
2.6
Przerzutnik synchroniczny RS wyzwalany
zboczem narastającym
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.6.1
Zagnieżdżona instrukcja warunkowa if zawierająca wyrażenie w
postaci sygnału z atrybutem event lub funkcją rising_edge [1, 2]
if wyrażenie1 then
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenie3
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
end if
wyrażenie1
:=
(sygnal’event
and
sygnal’rising_edge
sygnal=’1’)
|
168
2.6.2
Instrukcja oczekiwania wait wraz z instrukcją warunkową if [1, 2]
wait wyrażenie1
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenie3
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
wyrażenie1 := (sygnal’event and sygnal=’1’) | sygnal =
‘1’
Przykład 6.
Dla źródła:
if clk’event and clk=’1’ then
if St=’1’ then
Q <= Rt;
end if;
end if;
równania boolowskie dla wejść C, D, R i S mają postać:
C = clk;
R = St & !Rt;
S = St & Rt;
2.6.3
Przerzutnik opóźniający
Na rysunkach 6.1a i 6.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem narastającym z
opóźnieniem czasowym [4, 5].
169
Rysunek 6.1a. Przerzutnik synchroniczny RS z opóźnieniem czasowym wyzwalany
zboczem narastającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_high,R,S)
S1 = (S & !C);
R1 = (R & !C);
S2 = C & Q1(t);
R2 = C & !Q1(t);
Q1(t) = S1 | (!R1 & Q1(t-1));
Q(t) = S2 | (!R2 & Q(t-1));
Rysunek 6.1b. Przebieg czasowy
170
2.6.4
Przerzutnik nie opóźniający
Na rysunkach 6.2a i 6.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem narastającym bez
opóźnienia czasowego [4, 5].
Rysunek 6.2a. Przerzutnik synchroniczny RS bez opóźnienia czasowego wyzwalany
zboczem narastającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_high,R,S)
Q3(t) = !S1 | (R1 & Q3(t-1));
Q2(t) = !S2 | (R2 & Q2(t-1));
Q(t) = !Q3(t) | (Q2(t) & Q(t-1));
S1 = C & Q2(t-1);
S2 = C & Q3(t-1);
R1 = !S & !(!R & Q(t-1));
R2 = !R & !(!S & !Q(t-1));
171
Rysunek 6.2b. Przebieg czasowy
2.7
Przerzutnik synchroniczny RS wyzwalany
zboczem opadającym
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.7.1
Zagnieżdżona instrukcja warunkowa if zawierająca wyrażenie
w postaci sygnału z atrybutem event lub funkcją falling_edge
[1, 2]
if wyrażenie1 then
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenia3
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
end if
wyrażenie1
:=
(sygnal’event
and
sygnal’falling_edge
sygnal=’0’)
|
172
2.7.2
Instrukcja oczekiwania wait wraz z instrukcją warunkową if [1, 2]
wait wyrażenie1
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenie3
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
wyrażenie1 := (sygnal’event and sygnal=’0’) | sygnal =
‘0’
Przykład 7.
Dla źródła:
if clk’event and clk=’0’ then
if St=’1’ then
Q <= Rt;
end if;
end if;
równania boolowskie dla wejść C, R i S mają postać:
C = clk;
R = St & !Rt;
S = St & Rt;
2.7.3
Przerzutnik opóźniający
Na rysunkach 7.1a i 7.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem opadającym z
opóźnieniem czasowym [4, 5].
173
Rysunek 7.1a. Przerzutnik synchroniczny RS z opóźnieniem czasowym wyzwalany
zboczem opadającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_low,R,S)
S1 = S & C;
R1 = R & C;
S2 = !C & Q1(t);
R2 = !C & !Q1(t);
Q1(t) = S1 | (!R1 & Q1(t-1));
Q(t) = S2 | (!R2 & Q(t-1));
Rysunek 7.1b. Przebieg czasowy
174
2.7.4
Przerzutnik nie opóźniający
Na rysunkach 7.2a i 7.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem opadającym bez
opóźnienia czasowego [4, 5].
Rysunek 7.2a. Przerzutnik synchroniczny RS bez opóźnienia czasowego wyzwalany
zboczem opadającym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(C_low,R,S)
Q3(t) = !S1 | (R1 & Q3(t-1));
Q2(t) = !S2 | (R2 & Q2(t-1));
Q(t) = !Q3(t) | (Q2(t) & Q(t-1));
S1 = !C & Q2(t-1);
S2 = !C & Q3(t-1);
R1 = !S & !(!R & Q(t-1));
R2 = !R & !(!S & !Q(t-1));
175
Rysunek 7.2b. Przebieg czasowy
2.8
Przerzutnik synchroniczny RS wyzwalany zboczem
narastającym z wejściami asynchronicznymi RA i S.A.
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.8.1
Zagnieżdżona instrukcja warunkowa if zawierająca w części
elseif wyrażenie w postaci sygnału z atrybutem event lub
funkcją rising_edge [1, 2]
if wyrażenie1 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
elseif wyrażenie2 then
if wyrażenie3 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif wyrażenie4
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
176
end if
wyrażenie2
:=
sygnal’rising_edge
(sygnal’event
and
sygnal=’1’)
|
Przykład 8.
Dla źródła:
if RES=’1’ then
Q <= ‘0’;
elsif clk’event and clk=’1’ then
if St=’1’ then
Q <= Rt;
end if;
end if;
równania boolowskie dla wejść C, R, S, RA oraz SA mają postać:
C = clk;
R = St &
S = St &
RA = RES
SA = RES
2.8.2
!Rt;
Rt;
& !0;
& 0;
Przerzutnik opóźniający
Na rysunkach 8.1a i 8.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem narastającym z
wejściami asynchronicznymi RA i SA z opóźnieniem czasowym [4, 5].
Rysunek 8.1a. Przerzutnik synchroniczny RS wyzwalany zboczem narastającym z wejściami
asynchronicznymi RA i S.A. z opóźnieniem czasowym
Równania boolowskie opisujące przerzutnik mają postać:
177
--flip-flop(RA,SA,C_high,R,S)
S1 = (S & !C) | SA;
R1 = (R & !C) | RA;
S2 = (C & Q1(t)) | SA;
R2 = (C & !Q1(t)) | RA;
Q1(t) = S1 | (!R1 & Q1(t-1));
Q(t) = S2 | (!R2 & Q(t-1));
Rysunek 8.1b. Przebieg czasowy
2.8.3
Przerzutnik nie opóźniający
Na rysunkach 8.2a i 8.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem narastającym z
wejściami asynchronicznymi RA i SA bez opóźnienia czasowego [4, 5].
178
Rysunek 8.2a. Przerzutnik synchroniczny RS wyzwalany zboczem narastającym z wejściami
asynchronicznymi RA i S.A. bez opóźnienia czasowego
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(RA,SA,C_high,R,S)
Q3(t) = !S1 | (R1 & Q3(t-1));
Q2(t) = !S2 | (R2 & Q2(t-1));
S3 = Q3(t) & !SA;
R3 = Q2(t) & !RA;
Q(t)=!S3|(R3&Q(t-1));
S1 = C & Q2(t-1) & !RA;
S2 = C & Q3(t-1);
R1 = !S & !(!R & Q(t-1)) & !SA;
R2 = !R & !(!S & !Q(t-1)) & !RA;
179
Rysunek 8.2b. Przebieg czasowy
2.9
Przerzutnik synchroniczny RS wyzwalany zboczem
opadający z wejściami asynchronicznymi RA i S.A.
Przerzutniki tego typu są generowane w poniższych przypadkach.
2.9.1
Zagnieżdżona instrukcja warunkowa if zawierająca w części
elseif wyrażenie w postaci sygnału z atrybutem event lub
funkcją falling_edge [1, 2]
if wyrażenie1 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
elseif wyrażenie2 then
if wyrażenie3 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
180
[elseif wyrażenie4
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
end if
wyrażenie2
:=
(sygnal’event
and
sygnal’falling_edge
sygnal=’0’)
|
Przykład 9.
Dla źródła:
if RES=’1’ then
Q <= ‘0’;
elsif clk’event and clk=’0’ then
if St=’1’ then
Q <= Rt;
end if;
end if;
równania boolowskie dla wejść C, R, S, RA i SA mają postać:
C = clk;
R = St &
S = St &
RA = RES
SA = RES
2.9.2
!Rt;
Rt;
& !0;
& 0;
Przerzutnik opóźniający
Na rysunkach 9.1a i 9.1b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem opadającym z
wejściami asynchronicznymi RA i SA z opóźnieniem czasowym [4, 5].
181
Rysunek 9.1a. Przerzutnik synchroniczny RS wyzwalany zboczem opadającym z wejściami
asynchronicznymi RA i S.A. z opóźnieniem czasowym
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(RA,SA,C_low,R,S)
S1 = (S & C) | SA;
R1 = (R & C) | RA;
S2 = (!C & Q1(t)) | SA;
R2 = (!C & !Q1(t)) | RA;
Q1(t) = S1 | (!R1 & Q1(t-1));
Q(t) = S2 | (!R2 & Q(t-1));
Rysunek 9.1b. Przebieg czasowy
182
2.9.3
Przerzutnik nie opóźniający
Na rysunkach 9.2a i 9.2b przedstawiony jest schemat i przebieg czasowy
przerzutnika synchronicznego RS wyzwalanego zboczem opadającym z
wejściami asynchronicznymi RA i SA bez opóźnienia czasowego [4, 5].
Rysunek 9.2a. Przerzutnik synchroniczny RS wyzwalany zboczem opadającym z wejściami
asynchronicznymi RA i S.A. bez opóźnienia czasowego
Równania boolowskie opisujące przerzutnik mają postać:
--flip-flop(RA,SA,C_low,R,S)
Q3(t) = !S1 | (R1 & Q3(t-1));
Q2(t) = !S2 | (R2 & Q2(t-1));
S3 = Q3(t) & !SA;
R3 = Q2(t) & !RA;
Q(t) = !S3 | (R3 & Q(t-1));
S1 = !C & Q2(t-1) & !RA;
S2 = !C & Q3(t-1);
R1 = !S & !(!R & Q(t-1)) & !SA;
R2 = !R & !(!S & !Q(t-1)) & !RA;
183
Rysunek 9.2b. Przebieg czasowy
3.
IMPLEMENTACJA I TESTOWANIE
Wszystkie wyżej opisane zatrzaski i przerzutniki zostały
zaimplementowane w kompilatorze VHDL2BOOL za pomocą Visual C++
6.0 firmy Microsoft.
Do testowania funkcjonalności i poprawności zatrzasków i
przerzutników wykorzystano zestaw kilku tysięcy testów dostarczonych
przez firmę Aldec Co. Użyto także, powstałe specjalnie w tym celu w
Gdańsku i Zielonej Górze, dwa programy umożliwiające automatyczną
weryfikację poprawności wygenerowanych równań porównujące wyniki ze
wzorcem za jaki obrano produkt FPGAExpres firmy Synopsys.
Przeprowadzano również symulacje równań, sprowadzonych z powrotem do
kodu VHDL, przy użyciu programu AHDL firmy Aldec Co. W jednym z
automatów wykorzystano opracowany na Wydziale Informatyki PS
184
symulator równań boolowskich obliczający wynik na podstawie pliku
zawierającego równania oraz wartości wejściowych.
Testowanie udowodniło poprawność zaproponowanych zatrzasków i
przerzutników.
4.
PODSUMOWANIE
W pracy opisano rodzaje przerzutników i zatrzasków generowanych na
podstawie konstrukcji języka VHDL przez kompilator tego języka tworzony
na Wydziale Informatyki Politechniki Szczecińskiej. Elementy sekwencyjne
pozwalają zarówno na synchronizację układu jak i na wprowadzenie pamięci
w jego strukturze przez możliwość korzystania z wartości z poprzedniego
taktu zegara. Każdy z opisanych elementów sekwencyjnych został
zaprojektowany w dwóch wersjach: opóźniającej o jeden takt zegara zmianę
wartości wyjścia względem wejścia oraz przekazującej zmianę wartości
wejściowej na wyjście w tym samym takcie zegara.
Wszystkie zaprojektowane układy oraz algorytmy ich wywoływania
zostały sprawdzone za pomocą bazy testów dostarczonych przez firmę
Aldec. z wynikiem poprawnym.
LITERATURA
[1] Synopsys Inc.: FPGA Express VHDL Reference Manual, December 1997
[2] The Institute of Electrical and Electronic Engineers, Inc.: IEEE standard VHDL
Language Reference Manual. IEEE std. 1076-1993, 1994
[3] Włodzimierz Wrona: VHDL język opisu i projektowania układów cyfrowych, Gliwice
1998
[4] Józef Kalisz: Podstawy Elektroniki Cyfrowej, Wydawnictwa Komunikacji i Łączności,
Warszawa 1998
[5] W. Majewski: Układy logiczne, Wydawnictwo Naukowo-Techniczne, Warszawa 1999
Generacja maszyny stanów dla procesu z wieloma
instrukcjami oczekiwania wait
Włodzimierz Bielecki, Tomasz Wierciński
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
W artykule opisane są algorytmy i zasady generacji równań boolowskich
maszyny stanów dla procesu z wieloma instrukcjami oczekiwania wait
zastosowane w kompilatorze języka VHDL tworzonego na Wydziale
Informatyki PS. Uwzględniono tu jedynie przypadek, w którym
instrukcja wait nie jest uwikłana w inne instrukcje języka VHDL takie
jak instrukcja pętli, przypadku czy warunkowa. W takich sytuacjach
wymagane są dodatkowe równania sterujące w sposób niecykliczny
zmianą stanów. W pracy przedstawiono także warianty generowania
równań w różnych przypadkach występowania pojedynczej instrukcji
wait. Opisano budowę maszyny stanów oraz równania generowane dla jej
poszczególnych sygnałów wejściowych i wyjściowych. Na koniec
pokazano sposób testowania wyników działania funkcji utworzonych na
podstawie tych algorytmów. Do testowania funkcjonalności
i poprawności maszyny stanów wykorzystano zestaw testów dostarczonych
przez firmę Aldec oraz program do automatycznej weryfikacji poprawności
powstały specjalnie w tym celu w Zielonej Górze.
Słowa kluczowe:
VHDL, maszyna stanów, układy logiczne, syntezy, równania boolowskie
1.
WSTĘP
Instrukcja oczekiwania zawiesza wykonywanie procesu do czasu
wykrycia zbocza narastającego lub opadającego sygnału, zgodnie
z warunkiem umieszczonym po instrukcji wait [1, 3]. Instrukcja ta może
przyjmować następujące postaci syntaktyczne:
186
wait until signal = value ;
wait until signal’event and signal = value ;
wait until not signal’stable and signal = value ;
gdzie signal jest nazwą jednobitowego sygnału, a value - literałem stałym o
wartości ‘0’ dla zbocza opadającego lub ‘1’ dla zbocza narastającego.
Wystąpienie w treści procesu instrukcji wait implikuje logikę
sekwencyjną, gdzie signal jest najczęściej nazwą sygnału zegarowego.
Przypisania pod instrukcją oczekiwania są wykonywane tylko dla zbocza
zegara określonego w warunku, a następnie zatrzaskiwane w przerzutnikach
do momentu pojawienia się kolejnego zbocza spełniającego warunek [1, 2].
Użycie w kodzie programu VHDL wielu instrukcji oczekiwania wait
wymusza generację maszyny stanów, która z każdym wystąpieniem tej
instrukcji, generuje nowy stan układu. Oznacza to, że instrukcje występujące
pod instrukcją wait zostaną wykonane dopiero w przypadku wystąpienia
odpowiedniego dla nich stanu sygnału licznika. Liczba stanów w układzie
odpowiada liczbie instrukcji oczekiwania plus ewentualny stan początkowy,
tzw. stan „-1”.
Opisane w niniejszej pracy rozwiązania powstały dla potrzeb kompilatora
VHDL2BOOL języka VHDL tworzonego w Katedrze Technik
Programowania Wydziału Informatyki Politechniki Szczecińskiej dla firmy
Aldec. Jest on oparty na specyfikacji firmy Synopsys dotyczącej
kompilatorów do syntezy logicznej.
Przedstawione algorytmy nie dotyczą przypadku kiedy instrukcje wait są
zanurzone w instrukcjach pętli, warunkowych oraz przypadku. W takiej
sytuacji wymagane są dodatkowe sygnały i równań sterujące zmianą stanów.
2.
WARIANTY GENEROWANIA
MASZYNY STANÓW DLA RÓŻNYCH
PRZYPADKÓW WYSTĘPOWANIA
POJEDYNCZEJ INSTRUKCJI WAIT
Maszyna stanów może być generowana również w przypadku
wystąpienia pojedynczej instrukcji oczekiwania wait. Sytuacja taka ma
miejsce kiedy przypisanie do danego sygnału występuje w kodzie
źródłowym zarówno przed jak i po instrukcji wait. Pierwsze wykonanie
przypisania z przed instrukcji wait nastąpi w tzw. stanie „-1”, kiedy układ
nie posiada jeszcze żadnego ustalonego stanu. Dopiero kolejne wykonania
tego przypisania będą odbywały się w stanie ustalonym.
Poniżej opisane są możliwe wystąpienia pojedynczej instrukcji wait wraz
z podziałem na wymagające i niewymagające generacji maszyny stanów.
187
2.1
Brak generacji maszyny stanów
2.1.1
Instrukcja oczekiwania wait wraz z instrukcją przypisania
wait wyrażenie
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
wyrażenie := (sygnal’event and sygnal=’1’ (| ‘0’)) |
sygnal = ‘1’ (|’0’)
W wyniku generowany jest przerzutnik typu D.
2.1.2
Instrukcja oczekiwania wait wraz z instrukcją warunkową if
wait wyrażenie1
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
wyrażenie1 := (sygnal’event and sygnal=’1’ (|’0’)) |
sygnal = ‘1’ (|’0’)
W wyniku generowany jest przerzutnik synchroniczny RS.
2.2
Generacja maszyny stanów
2.2.1
Przypisanie do danego sygnału występuje zarówno przed jak
i po instrukcji oczekiwania wait
cel_przypisania1 <= wyrażenie
{ cel_przypisaniaN <= wyrażenie }
wait wyrażenie
cel_przypisania1 <= wyrażenie
{ cel_przypisaniaN <= wyrażenie }
wyrażenie := (sygnal’event and sygnal=’1’ (| ‘0’)) |
sygnal = ‘1’ (|’0’)
188
Przypadek wymaga generacji maszyny stanów z dwoma stanami: „–1” i
stanem z wait. Przypisania poprzedzające instrukcję wait są wykonywane w
sposób asynchroniczny jedynie w początkowym stanie układu (stan „–1”).
Następnie przypisania wykonywane są cyklicznie po każdym wystąpieniu
stanu z instrukcji wait. W części wykonawczej generowany jest przerzutnik
typu D z asynchronicznymi wejściami R i S.
2.2.2
Przypisanie do danego sygnału występuje przed instrukcją
oczekiwania wait, po której następuje instrukcja warunkowa if
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
wait wyrażenie1
if wyrażenie2 then
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
[elseif
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}
else
cel_przypisania1 <= wyrażenie
{cel_przypisaniaN <= wyrażenie}]
end if
wyrażenie1 := (sygnal’event and sygnal=’1’ (|’0’)) |
sygnal = ‘1’ (|’0’)
Przypadek wymaga generacji maszyny stanów tak jak wyżej. W stanie „–
1” przypisania poprzedzające instrukcję wait zostaną wykonane
asynchronicznie. W części wykonawczej generowany jest przerzutnik
synchroniczny RS z wejściami asynchronicznymi RA i SA
3.
OPIS MASZYNY STANÓW
Ogólny schemat logiki generowanej w przypadku gdy proces zawiera
więcej niż jedną instrukcje oczekiwania wait przedstawia się następująco:
189
Rysunek 1. Ogólna postać maszyny stanów
Jednostka sterująca składa się z dwóch bloków:
– licznika,
– dekodera typu „1 z n”.
Rozmiar licznika jest określany na podstawie liczby stanów układu według
wzoru:
M = [log2N]
Wartość na wyjściu licznika to zakodowany binarnie stan układu cyfrowego,
który jest podawany na wejście dekodera. Licznik jest zbudowany z
przerzutników typu T. Jeden przerzutnik odpowiada jednemu bitowi
licznika. Szablon dla pierwszego bitu licznika wygląda następująco:
Ti = 1;
Qi(t) = Si | (!Ri & Qi(t-1));
Si = (T | C) & Qi_i(t-1);
Ri = R(t) | ((T|C) & Qi_i(t-1));
Qi_i = Si_i | (Ri_i & Qi_i(t-1));
Si_i = (!T & Ti & Qi(t-1) & !Enab & !Ri_i) | (!C & Di &
Enab);
Ri_i = R(t) | (!T & Ti & Qi(t-1) & !Enab) | (!C & Di &
Enab);
Przy czym wejście Ti każdego następnego przerzutnika wynosi:
Ti = Qi-1 (t-1); i =1...(M-1)
Na wejście T licznika podawany jest sygnał zwiększający wartość
licznika o jeden a tym samym powodujący zmianę stanu układu. Ogólna
postać równania sygnału podawanego na wejście T wygląda następująco:
T = St0(t-1) & !CLK & !St-1(t-1) | St1(t-1) & !CLK &
!St-1(t-1) | ... | Stn-1(t-1) & !CLK & !St-1(t-1),
gdzie:
St0...Stn-1 – stany jakie może przyjmować układ oprócz stanu ostatniego
Stn,
clk – sygnał zegarowy,
190
St-1 – jest to stan jaki posiada układ gdy nie wystąpi jeszcze żadna
instrukcja oczekiwania. Taką sytuacje nazywamy stanem „-1” a równania
tego stanu mają następującą postać:
St-10(t) = !CLK & St-10(t-1)
St-11(t) = CLK & St-11(t-1)
St-1(t) = St-10(t) | St-11(t)
Osiągnięcie przez układ ostatniego stanu (Stn) powoduje wyzerowanie
licznika a tym samym przejście układu do stanu pierwszego (St0). Równanie
resetujące licznik jest podawane na wejście R i ma następującą postać:
R(t)=RR(t-1) | (!CLK & R (t-1))
Sygnał RR powstaje według poniższego szablonu:
RR(t) = Stn(t-1) & CLK,
gdzie Stn jest ostatnim stanem jaki przyjmuje układ.
Dekoder typu „1 z n” charakteryzuje się tym, że zawsze tylko jedno z
jego wyjść może być w stanie wysokim, pozostałe znajdują się w tym czasie
w stanie niskim. To które wyjście będzie aktywowane zależy od wartości
wejść. Dla każdej liczby jaka może się pojawić na wejściu dekodera
określone jest inne wyjście które znajdzie się w stanie wysokim. Nie ma
sytuacji w której dwóm różnym wartościom sygnałów wejściowych
przyporządkowane zostanie to samo wyjście. Każde wyjście dekodera
odpowiada innemu stanowi układu. Równania dekodera mają następującą
postać:
St0(t-1)=!Q0(t) & !Q1(t) & ... & !Qi(t) & ... & !QM-1(t)
& !St-1(t)
St1(t-1)=Q0(t) & !Q1(t) & ... & !Qi(t) & ... & !QM-1 (t)
& !St-1(t)
...
Stn(t-1)=Q0(t) & Q1(t) & ... & Qi(t) & ... & QM-1(t) &
!St-1(t)
W części wykonawczej, dla każdego sygnału, tworzony jest przerzutnik
typu flip-flop, który sterowany jest sygnałem zegarowym ogólnym dla
całego układu i sygnałem z dekodera. Dzięki temu przerzutnik ten zostanie
aktywowany tylko gdy, automat znajdzie się w odpowiednim stanie.
191
4.
ALGORYTM GENERACJI
RÓWNAŃ BOOLOWSKICH MASZYNY
STANÓW DLA PROCESU Z INSTRUKCJĄ
OCZEKIWANIA WAIT
1. Określenie stanów w procesie oraz generacja danych potrzebnych do
zbudowania maszyny stanów i dodatkowych atrybutów dla każdej
instrukcji przypisania w procesie z instrukcją wait.
2. Generacja równań boolowskich dla maszyny stanów.
3. Generacja równań boolowskich dla każdej instrukcji przypisania w
procesie zgodnie z atrybutami z p. 1 oraz stanami z p. 2.
4. Złożenie równań boolowskich dla maszyny stanów (p. 2) z równaniami
dla instrukcji przypisania (p. 3).
4.1
Analiza stanów procesu oraz generacja danych
potrzebnych do zbudowania maszyny stanów
i dodatkowych atrybutów dla każdej instrukcji
przypisania w procesie z instrukcją wait
Wejście:
i = 0 – liczba stanów,
state = -1 – bieżący stan,
T = 0 – wejście licznika,
CLK – sygnał zegara z instrukcji wait,
value – wartość z instrukcji wait
1. Pobierz pierwszą instrukcję w procesie.
2. Czy to jest instrukcja wait? Tak – idź do 3, nie – idź do 11.
3. Czy state = „-1”? Tak – idź do 4, nie – idź do 9.
4. Stan state = St0, CLK = sygnał zegara z instrukcji wait, value = wartość
z instrukcji wait.
5. Czy to ostatnia instrukcja w procesie? Tak – idź do 7, nie – idź do 6.
6. Pobierz następną instrukcję. Idź do 2.
7. Jeżeli stan state <> „-1” to N = i+1, w przeciwnym razie N=0.
8. Wygeneruj parametry wyjściowe: state, i, N, T, CLK. Idź do 12.
9. Zapamiętaj nazwę sygnału.
10. Wygeneruj równanie dla wejścia licznika:
T = T | !CLK & state & state-1
i = i + 1,
state = st i,
value i = wartość z instrukcji wait
Idź do 5
192
11. Znajdź wszystkie przypisania w bieżącym stanie i dla wszystkich
sygnałów z lewej strony przypisań wygeneruj atrybuty: state, CLK i
value. Idź do 5.
12. Czy liczba stanów N = 0? Tak – idź do 14, nie – idź do 13.
13. Wygeneruj równanie dla wejścia Reset licznika:
RR = !CLK & state
14.Koniec.
4.2
Generacja równań boolowskich dla maszyny stanów
Wejście:
N – liczba stanów
Równania boolowskie dla instrukcji wait
1. Czy liczba stanów N = 0? Tak – idź do 7, nie – idź do 2
2. Wyznacz ilość bitów licznika n = [log2N]
3. Wygeneruj równania dla wszystkich bitów licznika uwzględniając liczbę
bitów n. Dla pierwszego bitu równania te mają postać:
Ti = 1;
Qi(t) = Si | (!Ri & Qi(t-1));
Si = (T | C) & Qi_i(t-1);
Ri = R(t) | ((T|C) & Qi_i(t-1));
Qi_i = Si_i | (Ri_i & Qi_i(t-1));
Si_i = (!T & Ti & Qi(t-1) & !Enab & !Ri_i) | (!C & Di &
Enab);
Ri_i = R(t) | (!T & Ti & Qi(t-1) & !Enab) | (!C & Di &
Enab);
Wejście Ti każdego następnego przerzutnika wynosi:
Ti = Qi-1 (t-1); i =1...(M-1)
4. Wygeneruj równania stanów dla dekodera:
St0 = !Q0(t) & !Q1(t)&...&!St-1(t)
St1 = Q0(t) & !Q1(t) &...&!St-1(t)
...
5. Wygeneruj równanie boolowskie dla sygnału T uwzględniając równania
powstałe w trakcie analizy stanów
T = St0(t-1) & !CLK & !St-1(t-1) | St1(t-1) & !CLK &
!St-1(t-1) | ... | Stn-1(t-1) & !CLK & !St-1(t-1)
6. Wygeneruj równanie dla stanu początkowego „-1”
St-10(t) = CLK & St-10(t-1)
St-11(t) = CLK & St-11(t-1)
St-1(t) = St-10(t) | St-11(t)
7. Wygeneruj równanie dla wejścia R licznika wykorzystując równanie RR:
193
R(t) = RR(t-1) | !CLK & R(t-1)
8. Koniec
4.3
Zasady generacji równań boolowskich dla instrukcji
przypisania w procesie z instrukcją wait
1. Każda zmienna lub sygnał będące celem przypisania posiadają informacje
dodatkowe: stan, clk, wartość, wygenerowane na etapie analizy stanów.
2. Jeżeli dane przypisanie nie zależy od instrukcji oczekiwania to wartość
atrybutu stan w takiej sytuacji wynosi „-1”.
3. Dla każdego sygnału którego atrybut stan, różny jest od „-1” powinien
zostać wygenerowany przerzutnik. W zależności od tego czy atrybut
wartość wynosi 0 czy też 1, przerzutnik ten wyzwalany jest zboczem
opadającym lub też narastającym.
4. Dla każdej zmiennej za stanem różnym od „-1”, której odczyt występuje
przed zapisem generowany jest przerzutnik.
5. Dla każdego sygnału z atrybutem stan równym „-1” generowana jest
logika kobinacyjna.
6. Dla wielu przypisań do tego samego sygnału o tej samej wartości atrybutu
stan brane jest pod uwagę jedynie ostatnie przypisanie.
7. Dla wielu przypisań do tego samego sygnału o tej różnej wartości
atrybutu stan generowane jest równanie będące sumą logiczną równań
każdego z przypisań.
8. W przypadku gdy sygnał występuje zarówno po lewej jak i prawej stronie
przypisania w generowanym równaniu podstawiana jest wartość tego
sygnału z poprzedniego taktu zegara.
9. Jeżeli występuje przypisanie do sygnału którego atrybut stan jest równy
„-1” oraz występują, przypisania do tego samego sygnału, których
atrybut stan jest różny od „-1” to generowany jest przerzutnik z
wejściami asynchronicznymi.
4.4
Przykład
Dla źródła w języku VHDL
entity test is
port(
D:in STD_LOGIC;
C:in STD_LOGIC;
Q:out STD_LOGIC
);
end test;
194
architecture test of test is
begin
process
begin
wait until C'event and C='1';
Q<=D;
wait until C'event and C='1';
Q<='1';
wait until C='1';
Q<='0';
end process;
end test;
po analizie stanów są generowane równania:
T = St0(t-1) & !C & !St_1(t-1) | St1(t-1) & !C &
!St_1(t-1);
RR(t) = C & St2(t-1));
Równania końcowe maszyny stanów mają postać:
– równania stanów
St0_tmp__id_1054_(t)=!Q0_tmp__id_1054_(t)&!Q1_tmp__id_
1054_(t)&!St_1_tmp__id_1054_(t);
St1_tmp__id_1054_(t)=Q0_tmp__id_1054_(t)&!Q1_tmp__id_1
054_(t)&!St_1_tmp__id_1054_(t);
St2_tmp__id_1054_(t)=!Q0_tmp__id_1054_(t)&Q1_tmp__id_1
054_(t)&!St_1_tmp__id_1054_(t);
– równania przerzutnika
C_tmp0=(((St2_tmp__id_1054_(t))&(C))|((St1_tmp__id_105
4_(t))&(C)))|((St0_tmp__id_1054_(t))&(C));
D_tmp0=((((0))&(St2_tmp__id_1054_(t)))|(((1))&(St1_tmp
__id_1054_(t))))|(((D))&(St0_tmp__id_1054_(t)));
-- flip-flop(C_high,D)
Q1_tmp0(t)=S1_tmp0|(!R1_tmp0&Q1_tmp0(t-1));
S_tmp0=C_tmp0&Q1_tmp0(t);
R_tmp0=C_tmp0&!Q1_tmp0(t);
Q(t)=S_tmp0|(!R_tmp0&Q(t-1));
S1_tmp0=D_tmp0&!C_tmp0;
R1_tmp0=!C_tmp0&!D_tmp0;
– równanie sygnału T
T_tmp__id_1054_=(St0_tmp__id_1054_(t1))&!C&!St_1_tmp__id_1054_(t-1)|(St1_tmp__id_1054_(t1))&!C&(1)&!St_1_tmp__id_1054_(t-1);
– równanie sygnału RR
RR_tmp__id_1054_(t)=(C)&(St2_tmp__id_1054_(t-1));
195
– równania dla stanu –1
St_10_tmp__id_1054_(t)=(!C&St_10_tmp__id_1054_(t-1));
St_11_tmp__id_1054_(t)=(C&St_11_tmp__id_1054_(t-1));
St_1_tmp__id_1054_(t)=St_10_tmp__id_1054_(t)|St_11_tmp
__id_1054_(t);
– równania licznika
T0_tmp__id_1054_=1;
Q0_tmp__id_1054_(t)=S0_tmp__id_1054_|(!R0_tmp__id_1054
_&Q0_tmp__id_1054_(t-1));
S0_tmp__id_1054_=T_tmp__id_1054_&Q0_0_tmp__id_1054_(t1);
R0_tmp__id_1054_=R_tmp__id_1054_(t)|(T_tmp__id_1054_&!
Q0_0_tmp__id_1054_(t-1));
Q0_0_tmp__id_1054_(t)=S0_0_tmp__id_1054_|(!R0_0_tmp__i
d_1054_&Q0_0_tmp__id_1054_(t-1));
S0_0_tmp__id_1054_=(!T_tmp__id_1054_&T0_tmp__id_1054_&
!Q0_tmp__id_1054_(t-1)&!R0_0_tmp__id_1054_);
R0_0_tmp__id_1054_=R_tmp__id_1054_(t)|(!T_tmp__id_1054
_&T0_tmp__id_1054_&Q0_tmp__id_1054_(t-1));
T1_tmp__id_1054_=Q0_tmp__id_1054_(t-1);
Q1_tmp__id_1054_(t)=S1_tmp__id_1054_|(!R1_tmp__id_1054
_&Q1_tmp__id_1054_(t-1));
S1_tmp__id_1054_=(T_tmp__id_1054_)&Q1_1_tmp__id_1054_(
t-1);
R1_tmp__id_1054_=R_tmp__id_1054_(t)|((T_tmp__id_1054_)
&!Q1_1_tmp__id_1054_(t-1));
Q1_1_tmp__id_1054_(t)=S1_1_tmp__id_1054_|(!R1_1_tmp__i
d_1054_&Q1_1_tmp__id_1054_(t-1));
S1_1_tmp__id_1054_=(!T_tmp__id_1054_&T1_tmp__id_1054_&
!Q1_tmp__id_1054_(t-1)&!R1_1_tmp__id_1054_);
R1_1_tmp__id_1054_=R_tmp__id_1054_(t)|(!T_tmp__id_1054
_&T1_tmp__id_1054_&Q1_tmp__id_1054_(t-1));
– równanie wejścia R licznika
R_tmp__id_1054_(t)=RR_tmp__id_1054_(t1)|(!C&R_tmp__id_1054_(t-1));
5.
IMPLEMENTACJA I TESTOWANIE
Opisane algorytmy generacji maszyny stanów zostały zaimplementowane
w kompilatorze VHDL2BOOL w języku C za pomocą Visual C++ 6.0 firmy
Microsoft.
196
Do testowania funkcjonalności i poprawności maszyny stanów
wykorzystano zestaw testów dostarczonych przez firmę Aldec oraz program
do automatycznej weryfikacji poprawności powstały specjalnie w tym celu
w Zielonej Górze. Wyniki porównywane były ze wzorcem za jaki obrano
produkt FPGAExpres firmy Synopsys. Wykorzystano również opracowany
na Wydziale Informatyki PS symulator równań boolowskich obliczający
wynik na podstawie pliku zawierającego równania oraz wartości
wejściowych.
Testowanie udowodniło poprawność zaproponowanych rozwiązań
generacji maszyny dla procesu z wieloma instrukcjami wait.
6.
PODSUMOWANIE
W pracy przedstawiono algorytmy dotyczące generacji maszyny stanów
dla procesu z wieloma instrukcjami oczekiwania wait języka VHDL przez
kompilator tego języka tworzony na Wydziale Informatyki Politechniki
Szczecińskiej. Opisane rozwiązania dotyczą jedynie przypadku, w którym
instrukcje wait nie są zanurzone w instrukcjach pętli, warunkowych oraz
przypadku. W przeciwnym wypadku wymagana jest generacja dodatkowych
równań nie opisanych w tej pracy.
Zaproponowane algorytmy zostały sprawdzone za pomocą bazy testów
dostarczonych przez firmę Aldec z wynikiem poprawnym.
LITERATURA
[1] Synopsys Inc.: FPGA Express VHDL Reference Manual, December 1997
[2] The Institute of Electrical and Electronic Engineers, Inc.: IEEE standard VHDL
Language Reference Manual. IEEE std. 1076-1993, 1994
[3] Włodzimierz Wrona: VHDL język opisu i projektowania układów cyfrowych,
Gliwice 1998
Postprocesor kompilatora języka VHDL
Paweł Jaworski
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Niniejszy artykuł przedstawia zagadnienia związane z przekładem źródeł
programów w języku VHDL na postać równań boolowskich.Do opisanych
zagadnień należy generowanie równań boolowskich dla logiki
asynchroniczno-synchronicznej, minimalizacja równań boolowskich,
eliminowanie zmiennych tymczasowych oraz rezolucja równań boolowskich.
Słowa kluczowe:
VHDL, równania boolowskie, logika asynchroniczno-synchroniczna,
minimalizacja równań boolowskich.
1.
WSTĘP
Postprocesor jest częścią tworzonego kompilatora języka VHDL [1] do
syntezy układów logicznych. Wykonywane przez postprocesor zadania
dzielą się na dwie grupy, jedna modyfikująca równania boolowskie w taki
sposób, by zbiór tych równań poprawnie opisywał układy logiczne, druga
grupa, modyfikująca zbiór równań boolowskich tak aby uzyskać
zwiększenie efektywności ich symulacji i przetwarzania.
W niniejszym artykule opisuję kolejno te grupy oraz zadania do nich
należące.
Postprocesor wykonuje następujące zadania na zbiorze równań
boolowskich:
– weryfikacja równań boolowskich dla logiki asynchronicznosynchronicznej rezolucja równań boolowskich
– eliminacja zmiennych tymczasowych
– częściowa minimalizacja równań boolowskich
198
Problemy pojawiające się podczas przekładu programów napisanych w
języku VHDL do postaci równań boolowskich dla każdego z tych zadań są
opisane w odpowiednich sekcjach niniejszego artykułu.
2.
PROBLEM GENEROWANIA RÓWNAŃ
BOOLOWSKICH DLA LOGIKI
ASYNCHRONICZNO-SYNCHRONICZNEJ
Główny generator kodu generuje równania boolowskie dla następujących
logik [2]:
– logiki asynchronicznej, która nie zawiera przerzutników (latchów lub
flip-flopów),
– logiki synchronicznej, zawierającej przerzutniki, ale w taki sposób, że
wejścia tych przerzutników pochodzą z sygnałów wyjściowych innych
przerzutników i wartości których są generowane na poprzednim takcie
zegarowym (CLK),
– logiki asynchroniczno-synchronicznej, gdzie wejścia przerzutników
mogą korzystać z sygnałów wyjściowych innych przerzutników,
wygenerowanych tak na poprzednim jak i na bieżącym takcie
zegarowym.
Przykładem logiki synchronicznej jest następujące źródło w języku
VHDL:
Library IEEE;
Use IEEE.std_logic_1164.all;
Entity diff_neg is
Port(DATA,CLK: in std_logic; Q: out std_logic);
End diff_neg;
Architecture rtl of diff_neg is
Begin
Infer: process(CLK)
Begin
If CLK’event and CLK=’0’ then
Q<=DATA;
End if;
End process Infer;
End rtl;
W powyższym przykładzie wejście „DATA” jest synchroniczne.
Jeżeli z tym wejściem jest połączone wyjście jakiegoś przerzutnika to
wartość sygnału z tego wejścia zawsze ma być pobierana z poprzedniego
taktu sygnału zegarowego.
199
Przykładem logiki synchroniczno-asynchronicznej jest następujące
źródło w języku VHDL:
Library IEEE;
Use IEEE.std_logic_1164.all;
Entity diff_async_reset is
Port(DATA,CLK,
RESET:
in
std_logic);
End diff_neg;
std_logic;
Q:
out
Architecture rtl of diff_async_reset is
Begin
Infer: process(CLK, RESET)
Begin
If (RESET=’1’) then
Q<=’0’;
Else If CLK’event and CLK=’1’ then
Q<=DATA;
End if;
End process infer;
End rtl;
W tym przykładzie wejście „DATA” jest synchroniczne, natomiast
wejście „RESET” jest asynchroniczne, które uwzględnia wartość na wyjściu
przerzutnika wygenerowaną na bieżącym takcie sygnału zegarowego.
Główny problem przy generowaniu równań boolowskich przez główny
generator równań boolowskich dla logiki asynchroniczno-synchronicznej
polega na tym, że wynik przebiegu pojedynczej kompilacji, nie generuje
wszystkich równań w sposób poprawnie opisujący funkcjonowanie logiki
synchroniczno-asynchronicznej. Trudność wynika z tego, iż wejścia
synchroniczne wykorzystują wartości wygenerowane na poprzednim takcie
sygnału zegarowego, natomiast wejścia asynchroniczne korzystają z
sygnałów wygenerowanych na bieżącym takcie sygnału zegarowego.
W języku VHDL układy asynchroniczno-synchroniczne mogą być
opisywane za pomocą wielu procesów, z których każdy może generować
przerzutniki. Kompilator generuje równania boolowskie niezależnie dla
każdego procesu, co uniemożliwia wygenerowanie poprawnych równań
boolowskich za pomocą pojedynczego przebiegu kompilacji. Dla logiki
synchronicznej (przerzutników) kompilator zawsze generuje równania,
wykorzystujące wartości z poprzedniego taktu zegarowego. Przy
generowaniu równań boolowskich dla logiki asynchroniczno-synchronicznej
takie podejście jest niewłaściwe, ponieważ dla wejść asynchronicznych,
potrzebne są bieżące wartości sygnału na wyjściu przerzutników. Znaczy to,
że czasami powstaje potrzeba generowania dwóch równań: jednego,
200
korzystającego z sygnałów o wartościach wygenerowanych na bieżącym
takcie zegarowym oraz drugiego, korzystającego z sygnałów o wartościach
wygenerowanych na poprzednim takcie zegarowym.
Możliwym rozwiązaniem jest generowanie zawsze dwóch równań dla
każdego wyjścia przerzutnika, ale takie podejście jest nieefektywne,
ponieważ może prowadzić do wygenerowania dużej ilości zbędnych równań
boolowskich.
Zadaniem postprocesora jest wygenerowanie dodatkowych równań
boolowskich tam gdzie mamy do czynienia z układami mającymi zarówno
wejścia synchroniczne jak i asynchroniczne. Wygenerowanie dodatkowych
równań dla takich układów powoduje prawidłowe uzupełnienie zbioru
równań boolowskich o logikę synchroniczno-asynchroniczną. Rozwiązanie
powyższego problemu polega na tym, że w zbiorze wejściowym równań
boolowskich, układy asynchroniczno-synchroniczne oznaczane są przez
główny generator dyrektywą „pragma” oraz nawiasami klamrowymi, które
określają zbiór pragmy.
Przykład:
--pragma syn(t-1)
{
R(1)=C*!Q1(t,1)+D(1);
Q(t,1)=S(1)+(!R(1)*Q(t,1));
S(1)=C*Q1(t, 1);
QQ(5,4,3)=C*!Q1(t,1);
Q1(t,1)=S1(1)+(!R1(1)*Q1(t,1));
S1(1)=D(1)*!C;
-- to jest opis rejestru
R1(1)=!C*!D(1);
}
D(1)=D(2);
D(2)=D(3);
D(3)=Q1(1);
Zbiorem zmiennych synchronicznych niech będzie poniższy przykład:
Q(1);
Q(2);
Q(3);
Q(4);
Q(5);
Q(6);
Q1(1);
Q1(2);
Q1(3);
Q1(4);
Q1(5);
Q1(6);
Analiza równań boolowskich zbioru pragmy dla powyższego przykładu,
wskazuje, że równanie „R(1)” nie opisuje logiki synchronicznoasynchronicznej w sposób poprawny ponieważ wejście „D(1)” korzysta z
201
wartości wygenerowanych na bieżącym sygnale taktu zegarowego.
Postprocesor wygeneruje zatem dodatkowe równanie dla „D(1)”, którego
wejścia wygenerowane będą na poprzednim sygnale taktu zegarowego.
Rówanie kolejne, „Q(t,1)”, także jest nieprawidłowe, ponieważ wejście
„Q(1)” i „R(1)” nie są poprawne z punktu widzenia logiki asynchronicznosynchronicznej.
Wynik postprocesora dodaje nowe równania dla zbioru wejściowego
równań boolowskich. Równania zbioru pragmy po analizie postprocesora
dawać będzie poprawny układ synchroniczno-asynchroniczny. Wynik
przedstawiona poniższy zbiór.
--pragma syn(t-1)
\D(1)\=\D(2)\;
\D(2)\=\D(3)\;
\D(3)\=Q1(t-1,1);
--{
R(1)=C*!Q1(t-1,1)+\D(1)\;
Q(t,1)=S(1)+(!R(1)*Q(t-1,1));
S(1)=C*Q1(t-1,1);
QQ(5,4,3)=C*!Q1(t-1,1);
Q1(t,1)=S1(1)+(!R1(1)*Q1(t-1,1));
S1(1)=\D(1)\*!C;
-- to jest opis rejestru
R1(1)=!C*!\D(1)\;
--}
D(1)=D(2);
D(2)=D(3);
D(3)=Q1(t,1);
W ten sposób wygenerowane zostały tylko te równania, które potrzebne
są by poprawnie opisywać logikę synchroniczno-asynchroniczną.
3.
PROBLEM REZOLUCJI RÓWNAŃ
BOOLOWSKICH
Główny generator równań boolowskich generuje równania boolowskie
niezależnie dla każdego zdefiniowanego procesu w języku VHDL. Z tego
powodu pojawia się problem jeżeli kilka procesów wykorzystuje tą samą
magistralę. Nastąpi wtedy wygenerowanie dla tych samych określonych
zmiennych wiele równań boolowskich. Proponowanym rozwiązaniem jest
połączenie wszystkich równań boolowskich dla zmiennych za pomocą
funkcji logicznej. Dostępne są funkcje logiczne OR oraz AND. Zadaniem
postprocesora jest połączenie równań boolowskich dla zmiennych, dla
202
których określono funkcję rezolucji. Na wyjściu tego zadania, zbiór równań
boolowskich jest opisany w sposób formalnie poprawny.
4.
ELIMINACJA ZMIENNYCH TYMCZASOWYCH
Zadania dla grupy zwiększającej wydajność przetwarzania równań
boolowskich to eliminacja zmiennych tymczasowych oraz minimalizacja
równań boolowskich.
Problemem zbioru wejściowego równań boolowskich jest ich
nadmiarowość.
Powstaje ona, podczas obliczania wyrażeń arytmetycznych, logicznych,
generowania wyniku wywołania funkcji. Optymalizacja ilości równań na
poziomie głównego generatora równań boolowskich bez dużych nakładów
pamięciowych i obliczeniowych jest niemożliwa, dlatego zrezygnowano z
niej na tym poziomie.
Postprocesor wykonuje eliminację zmiennych tymczasowych równań
boolowskich. Zmienną tymczasową nazywamy zmienną, posiadającą w
identyfikatorze podwójny znak podkreślenia. Bierze się to stąd, iż podwójny
znak podkreślenia jest niedozwolony w języku VHDL przy tworzeniu
identyfikatorów zmiennych lub stałych. Główny generator tworzy zmienne
tymczasowe używając tego symbolu. Przykładem zmiennych tymczasowych
stanowi poniższy zbiór identyfikatorów.
tmp__id_1060_(0)
tmp__id_1060_(1)
tmp__id_1061_(0)
tmp__id_1061_(1)
tmp__id_1062_(1)
Ilość zmiennych tymczasowych w większości przypadkach może być
bardzo duża, a ilość wyeliminowanych równań boolowskich przez
postprocesor może sięgać 50% a nawet więcej. Dla poniższego przykładu
tmp__id_1060_(0)=a(31);
tmp__id_1060_(1)=a(30);
tmp__id_1061_(0)=!tmp__id_1060_(0);
tmp__id_1061_(1)=!tmp__id_1060_(1);
tmp__id_1062_(1)=((!tmp__id_1061_(1)&tmp__id_1063_(2))
|(tmp__id_1061_(1)&!tmp__id_1063_(2)));
tmp__id_1063_(1)=tmp__id_1061_(1)&tmp__id_1063_(2);
tmp__id_1063_(2)=tmp__id_1060_(0)&tmp__id_1060_(0);
tmp__id_1062_(0)=((!tmp__id_1061_(0)&tmp__id_1063_(1))
|(tmp__id_1061_(0)&!tmp__id_1063_(1)));
203
tmp__id_1064_(0)=(tmp__id_1060_(0)&!tmp__id_1060_(0))|
(tmp__id_1062_(0)&tmp__id_1060_(0));
z(0)=tmp__id_1064_(0);
Wynikiem wyeliminowania zmiennych tymczasowych będzie
następujący zbiór równań boolowskich:
tmp__id_1063_(1)=!a(30)&tmp__id_1063_(2);
tmp__id_1063_(2)=a(31);
z(0)=(((((a(31)&tmp__id_1063_(1))|(!a(31)&!tmp__id_1063_(1))))&a(3
1)));
Zysk widać wyraźniej dla przykładów przemysłowych, gdzie ilość
równań boolowskich wynosi kilkadziesiąt tysięcy, z czego duży procent
stanowią zmienne tymczasowe.
Eliminowanie zmiennych tymczasowych odbywa się według ściśle
określonych reguł. Nie są eliminowane wszystkie zmienne tymczasowe,
gdyż w niektórych przypadkach zbiór wynikowy równań boolowskich byłby
większy niż zbiór wejściowy tych równań. Reguły eliminowania zmiennych
tymczasowych określa się następująco :
– eliminowane są równania zmiennych tymczasowych, które definiują
równanie proste, bez względu na ilość referencji do tego równania;
– eliminowane są równania zmiennych tymczasowych, które definiują
równania złożone ale ilość referencji do tego równania wynosi co
najwyżej jeden;
– eliminowane są zmienne tymczasowe, których ilość referencji wynosi
zero;
Eliminowanie oznacza usuwanie równania boolowskiego dla zmiennej
tymczasowej (w trzecim przypadku) lub usuwanie zmiennej a w miejscach
występowania tej zmiennej, wstawiane jest ciało równania usuwanej
zmiennej.
Eliminacja zmiennych tymczasowych zmienia ilość równań boolowskich
w zbiorze wejściowym oraz modyfikuje równania boolowskie.
5.
CZĘŚCIOWĄ MINIMALIZACJA RÓWNAŃ
BOOLOWSKICH
Postprocesor wykonuje prostą minimalizację równań boolowskich,
biorąc pod uwagę następujące reguły proste, złożone oraz reguły dla logiki
Don’t Carry:
Reguły proste:
A&0 = 0
0&A = 0
A&!0 = A
204
!0&A = A
A&1 = A
1&A = A
A&!1 = 0
!1&A = 0
A|0 = A
0|A = A
A|!0 = 1
!0|A = 1
A|1= 1
1|A = 1
A|!1 = A
!1|A = A
A&!A = 0
!A&A = 0
A|!A = 1
!A|A = 1
A&A = A
A|A = A
Reguły złożone:
!A|A&B=
A|A&B =
!A|!A&B
A|A&B =
!A|B
A
= !A
A|B
Reguła dla logiki Don’t Carry polega na minimalizacji równań w
przypadku gdy równanie boolowskie zawiera wartości Don’t Carry.
Wartości takie traktuje się albo jako prawdę albo jako fałsz, w zależności od
tego która z tych wartości prowadzi do bardziej zminimalizowanego
równania boolowskiego. Jeżeli traktowanie sygnału don’t carry jako
logiczną jedynkę lub zero daje ten sam wynik, domyślnie bierze się logiczne
zero.
Przykład minimalizacji równań boolowskich wykorzystując reguły
proste:
Równanie wejściowe :
O(15)=((EN)&(((((((((((((((((((((!I(3))&(!I(2)))&(!I(1
)))&(!I(0)))&(0))|(((((!I(3))&(!I(2)))&(!I(1)))&(I(0)))&
(0)))|(((((!I(3))&(!I(2)))&(I(1)))&(I(0)))&(0)))|(((((!I
(3))&(!I(2)))&(I(1)))&(!I(0)))&(0)))|(((((!I(3))&(I(2)))
&(I(1)))&(!I(0)))&(0)))|(((((!I(3))&(I(2)))&(I(1)))&(I(0
)))&(0)))|(((((!I(3))&(I(2)))&(!I(1)))&(I(0)))&(0)))|(((
((!I(3))&(I(2)))&(!I(1)))&(!I(0)))&(0)))|(((((I(3))&(I(2
)))&(!I(1)))&(!I(0)))&(0)))|(((((I(3))&(I(2)))&(!I(1)))&
205
(I(0)))&(0)))|(((((I(3))&(I(2)))&(I(1)))&(I(0)))&(0)))|(
((((I(3))&(I(2)))&(I(1)))&(!I(0)))&(0)))|(((((I(3))&(!I(
2)))&(I(1)))&(!I(0)))&(0)))|(((((I(3))&(!I(2)))&(I(1)))&
(I(0)))&(0)))|(((((I(3))&(!I(2)))&(!I(1)))&(I(0)))&(0)))
|(((((I(3))&(!I(2)))&(!I(1)))&(!I(0)))&(1)))|(!(((((((((
(((((((((((!I(3))&(!I(2)))&(!I(1)))&(!I(0))))|(((((!I(3)
)&(!I(2)))&(!I(1)))&(I(0)))))|(((((!I(3))&(!I(2)))&(I(1)
))&(I(0)))))|(((((!I(3))&(!I(2)))&(I(1)))&(!I(0)))))|(((
((!I(3))&(I(2)))&(I(1)))&(!I(0)))))|(((((!I(3))&(I(2)))&
(I(1)))&(I(0)))))|(((((!I(3))&(I(2)))&(!I(1)))&(I(0)))))
|(((((!I(3))&(I(2)))&(!I(1)))&(!I(0)))))|(((((I(3))&(I(2
)))&(!I(1)))&(!I(0)))))|(((((I(3))&(I(2)))&(!I(1)))&(I(0
)))))|(((((I(3))&(I(2)))&(I(1)))&(I(0)))))|(((((I(3))&(I
(2)))&(I(1)))&(!I(0)))))|(((((I(3))&(!I(2)))&(I(1)))&(!I
(0)))))|(((((I(3))&(!I(2)))&(I(1)))&(I(0)))))|(((((I(3))
&(!I(2)))&(!I(1)))&(I(0)))))|(((((I(3))&(!I(2)))&(!I(1))
)&(!I(0)))))&(0))))|!((EN))&(0);
Równanie wyjściowe:
O(15)=(EN&((((((I(3)&!I(2))&!I(1))&!I(0))))));
Przykład dla reguły don’t carry:
R=b&a&3|b;
Wartość trzy traktowana jest tutaj jako sygnał don’t carry. Jeżeli za ten
sygnał podstawimy logiczne zero, otrzymamy:
R=b&a&0|b=b&0|b=0|b=b
Jeżeli natomiast za wartość don’t carry podstawimy logiczną jedynkę,
otrzymamy następujący wynik:
R=b&a&1|b=b&a|b
Jak można zauważyć, traktowanie sygnału don’t carry jako wartość zero
lub jeden, prowadzi do różnej minimalizacji. Potraktowanie don’t carry jako
wartości daje wynik R=b co jest lepszą minimalizacją niż R=b&a|b, dlatego
za wartość trzy zostanie podstawione logiczne zero.
6.
PODSUMOWANIE
Postprocesor jest bardzo ważnym etapem w projekcie kompilatora języka
VHDL do syntezy układów logicznych. Weryfikuje on równania boolowskie
w taki sposób by poprawnie opisywały ułady logiczne. Ponadto
minimalizuje równania boolowskie oraz eliminuje zmienne tymczasowe,
zwiększając w ten sposób efektywność przetwarzania zbioru równań
boolowskich.
206
Zadania przewidziane na przyszłość to dalsze prace nad zwiększeniem
efektywności a także zwiększenie funkcjonalności postprocesora. Obecnie
trwają prace nad konwersją równań boolowskich do postaci BLIF (Berkeley
Logic Interchange Format).
LITERATURA
[1] IEEE standard VHDL Language Reference Manual. IEEE std.1076-1993. The Institute
of Electrical and Electronic Engineers, Inc. 1994
[2] W. Bielecki, S. Hayduke, P. Jaworski, Generowanie równań boolowskich dla logiki
asynchroniczno-synchronicznej w syntezowalnej wersji języka VHDL,
RUC 2000, Szczecin
Wersje implementacyjne postprocesora kompilatora
języka VHDL
Paweł Jaworski
Katedra Technik Programowania, Wydział Informatyki, Politechnika Szczecińska
ul. Żołnierska 49, 71-210 Szczecin
Abstrakt:
Niniejszy artykuł opisuje wersje, zmiany zastosowanych algorytmów do
rozwiązania zadań postprocesora kompilatora języka VHDL do syntezy
układów logicznych. Podczas trwania ponad dwuletnich prac nad
postprocesorem zmieniały się implementacje postprocesora by sprostać
zwiększeniu efektywności i niezawodności. Niniejszy artykuł zawiera
rozdziały opisujące oprócz zastosowanych strategicznych algorytmów,
opis funkcjonalny postprocesora, która także ulegał modyfikacji.
Słowa kluczowe:
język VHDL, równania boolowskie, logika
synchroniczna, minimalizacja równań boolowskich
1.
asynchroniczno-
WSTĘP
Prace nad postprocesorem trwały około dwóch lat, doprowadzając do
powstania bieżącej wersji. Celem rozwijania projektu, było zwiększenie
niezawodności, wydajności oraz efektywności przetwarzania równań
boolowskich. Ponadto zwiększała się funkcjonalność postprocesora, na
przykład poprzez dodanie nowych reguł dla zadania minimalizacji równań
boolowskich.
208
2.
WERSJA PIERWSZA POSTPROCESORA
KOMPILATORA JĘZYKA VHDL
Pierwsza wersja postprocesora wykonywała następujące zadania:
– uzupełnienie równań boolowskich o logikę asynchronicznosynchroniczną [1],
– łączenie równań boolowskich dla sygnałów typu resolved,
– eliminację zmiennych tymczasowych równań boolowskich,
– częściową minimalizację równań boolowskich.
Sposób, w jaki zostały zaimplementowane powyższe zadania w różnych
wersjach postprocesora, bardzo się między sobą różnił. Chodzi tutaj o
zastosowane dynamiczne struktury danych, algorytmy przeszukiwania oraz
sposób przechowywania równań boolowskich.
Najważniejszą różnicą wersji pierwszej od pozostałych było to, że nie
przeprowadzała ona wstępnej obróbki równań boolowskich na postać
powiązanych leksemów (analiza leksykalna). Pierwszą czynnością dla
postprocesora wersji pierwszej było stworzenie indeksów do równań
boolowskich, co umożliwiało szybszą ich lokalizację. Indeksy do równań
boolowskich miało postać pary : nazwa zmiennej dla równania, indeks
położenia fizycznego tego równania w zbiorze wejściowym. Indeksy do
równań boolowskich były zorganizowane strukturę dynamiczną - listę
jednokierunkową [3].
Algorytm uzupełniania równań boolowskich o logikę asynchronicznosynchroniczną bazował na rekurencyjnym przeszukiwaniu równań
boolowskich w celu znalezienia zmiennych sekwenyjnych. Dla każdego
równania boolowskiego z bloku dyrektywy pragmy, które spełniały warunki
by je analizować, badane były kolejne zmienne, jeżeli znaleziono zmienną
sekwencyjną, zmieniano jej zależność czasową z bieżącego taktu sygnału
zegarowego (t) na poprzednią (t-1). Dla pozostałych zmiennych, badało się
ich równania boolowskie (wyszukiwało się je na podstawie indeksów do
równań boolowskich). Jeżeli dla takiej zmiennej zdefiniowano równanie
boolowskie (istniał indeks do równania) wtedy równanie takie było
analizowane w ten sam sposób co poprzednie. Jeżeli, którekolwiek z
analizowanych równań boolowskich leżało poza blokiem dyrektywy pragmy
i zawierało zmienne sekwencyjne, tworzone było nowe, dodatkowe
równanie, które uwzględniało już zmienne sekwencyjne o wartościach
wygenerowanych na poprzednim takcie sygnału zegarowego. W ten sposób
generowane były tylko te równania, które faktycznie były potrzebne.
Analiza problemu wykazała rekurencyjność w
rozwiązaniu tego
problemu, dlatego zdecydowałem się zastosować ten typ algorytmu. Zadanie
209
zostało rozwiązane, niestety, w późniejszym czasie okazało się, że
zaproponowane rozwiązanie jest zbyt skomplikowane dla większości testów
przemysłowych, wykorzystywało zbyt dużo zasobów komputera oraz co jest
wadą algorytmów rekurencyjnych, rozwiązanie posiadało zbyt małą
efektywność i wydajność.
Algorytm do łączenia równań boolowskich dla zmiennych typu resolved
opierało się także na wykorzystaniu listy indeksów do równań boolowskich.
Dla każdego elementu ze zbioru wejściowego, zawierającego definicje
zmiennych typu resolved, sprawdzano, czy istnieje taki element w liście
indeksów do równań oraz ile takich elementów na tej liście występuje. Na
podstawie tej informacji, łączono te równania dla których określono typ
resolved oraz ilość wystąpień była większa od jeden. Ilość operacji
porównań, jakie algorytm musiał wykonać była zgodna z poniższym
wzorem: O = m * n, gdzie m- ilość elementów typu resolved, n – ilość
elementów listy indeksów do równań.
Każda operacja porównania opierała się na porównywaniu ciągu bajtów,
reprezentujących nazwę zmiennej. Chociaż zadanie zostało wykonywane,
efektywność tego rowziązania była niska. Już wtedy jednym z rozwiązań,
które zwiększyłoby wydajność tego zadania było porównanie tych
elementów z listy indeksów do równań boolowskich, które występowały
więcej niż jeden raz, z elementami zdefiniowanymi jako resolved. Ilość
operacji porównań uległa by znacznemu obniżeniu.
Zadanie eliminacji zmiennych i równań boolowskich poprzedzone było
wykonaniem zebrania pewnych informacji na temat równań boolowskich
zbioru wejściowego. Polegało to, na stworzeniu dwóch dynamicznych
struktur danych, przechowujących indeksy do równań boolowskich. W
jednej ze struktur, przechowywane były indeksy do równań boolowskich
zmiennych tymczasowych, w drugiej do równań boolowskich zmiennych
nietymczasowych. Następnie dla każdego równania boolowskiego z obu
struktur wykonywana była operacja inkrementacji ilości referencji każdej
zmiennej tymczasowej występującej w równaniu boolowskim. Na wyjściu
tego etapu algorytmu, posiadałem informacje o tym ile wynosi liczba
referencji danej zmiennej tymczasowej.
Następnie, dla każdego elementu (równania) z listy indeksów do równań
boolowskich zmiennych nietymczasowych, sprawdzano czy zawiera
zmienne tymczasowe, jeżeli nie, analizowano kolejne z równań dla
zmiennych nietymczasowych, w przypadku gdy równanie zawierało
zmienne tymczasowe, prowadzono analizę tego równania. Analiza równań
boolowskich, zawierających zmienne tymczasowe, polegała na analizie tych
zmiennych czy spełniają kryterium minimalizacji równań boolowskich.
Kryterium było następujące:
210
– jeżeli liczba referencji zmiennej jest mniejsze niż dwa, wtedy eliminuj
zmienną
– jeżeli zmienna definiuje równanie proste
– jeżeli liczba referencji zmiennej (równania) wynosi zero, eliminuj
równanie
Jeżeli, którekolwiek z tych kryterium było spełnione, dana zmienna lub
równanie boolowskie było eliminowane. Cały proces oparty był o analizę
liczby referencji zmiennej oraz wyznaczania, czy zmienna definiuje
równanie proste.
Wadą implementacji tego zadania, było niewątpliwie, operowanie
napisami zamiast wartościami numerycznymi. Operowanie wartościami
numerycznymi reprezentującymi zmienne, jest o wiele wydajniejsze i
zwiększa niezawodność.
Częściowa minimalizacja równań boolowskich została ujęta jako
częściowa, gdyż nie zastosowanych klasycznych czy najnowszych osiągnięć
w zakresie minimalizacji równań boolowskich. Ponieważ generowane
równania mogą, a właściwie często posiadają kilkaset zmiennych (nie
mówiąc już o kilkudziesięciu tysiącach, co nie jest rzadkością)
minimalizacja takich równań używając najnowszych algorytmów oraz
najszybszych komputerów nie nadawała by się do realnego zastosowania w
przemyśle. Zastosowane w postprocesorze w wersji pierwszej, algorytmy
minimalizacji uwzględniają następujące reguły :
A&0 = 0
0&A = 0
A&!0 = A
!0&A = A
A&1 = A
1&A = A
A&!1 = 0
!1&A = 0
A|0 = A
0|A = A
A|!0 = 1
!0|A = 1
A|1= 1
1|A = 1
A|!1 = A
!1|A = A
A&!A = 0
!A&A = 0
Minimalizacja równań boolowskich dla postprocesora wersji pierwszej
nie bazowała na budowie odpowiedniej struktury przechowującej równania
211
boolowskie ze zbioru wejściowego. Poszczególne człony reguł były
usuwane i zamieniane na odpowiednie im człony zgodnie z regułami
minimalizacji.
Algorytm operował
na
równaniu boolowskich,
reprezentowanym jako łańcuch znaków. Porównywanie nazw zmiennych i
wyrażeń obniżało wydajność i efektywność tego rozwiązania.
3.
WERSJA DRUGA POSTPROCESORA
KOMPILATORA JĘZYKA VHDL
Celem, powstania wersji drugiej postprocesora było zwiększenie
efektywności oraz wydajności. Z większością testów przemysłowych,
postprocesor w wersji pierwszej, nie dawał sobie rady, ze względu na duże
wymagania dotyczące systemu. Funkcjonalnie, nowa wersja była identyczna
z wersją poprzednią.
Wersja druga postprocesora przyczniła się do zwiększenia
niezawodności, wydajności i efektywności w stosunku do wersji pierwszej.
Spowodowane to było wstępnym przetwarzaniem równań boolowskich.
Przetwarzanie to polegało na odwzorowaniu równań boolowskich ze zbioru
wejściowego na odpowiednią strukturę dynamiczną.
Tworzona była lista indeksów do równań [2], gdzie każdy element tej
listy zawiera ciało równania, czyli kolejne zmienne, stałe i operatory
równania, oraz wskaźnik do listy zależności. Lista indeksów nie była
posortowana, służyła tylko do przechowywania równań, dostęp do niej był
zawsze sekwencyjny, nie wykonywano na niej operacji wyszukiwania. Lista
zależności była tworzona dla każdego równania i zawiera następujące
informacje:
– lista zmiennych równania,
– wskaźnik do ciała funkcji z listy indeksów do równań,
– znacznik czy element zawiera zmienne sekwencyjne,
Lista zmiennych równania to wykaz wszystkich zmiennych a wskaźnik
do ciała, wskazuje na całą definicję równania z listy indeksów do równań.
Lista zależności była posortowana, gdyż na niej wykonywało się operacje
wyszukiwania.
Sposób tworzenia tej listy przedstawia poniższy algorytm :
a) dla każdego równania z pliku z równaniami boolowskimi;
b) dodaj równanie do listy zależności (jeżeli nie zostało dodane wcześniej w
wyniku analizy innego równania zawierającego zmienną aktualnie
analizowanego równania), oraz zaznacz początkowo, że równanie nie
zawiera zmiennych sekwencyjnych;
c) analizuj wszystkie zmienne tego równania;
212
d) dodaj analizowaną zmienną do listy zmiennych tego równania ze stanem
określającym, że równanie dla tej zmiennej nie zawiera zmiennych
synchronicznych;
e) jeżeli zmienna jest sekwencyjna to zaznacz, że równanie zawiera ten
rodzaj zmiennych. skok do (c);
Powstała w wyniku tego algorytmu lista – list była bardzo poręcznym
narzędziem do generowania równań. Na jej podstawie, bardziej efektywnie
można było generować równania dla układów opisujących logikę
asynchroniczno-synchroniczną.
Poniższy algorytm przedstawia sposób rozwiazania zadania uzupełniania
równań dla układów opisujących logikę asynchroniczno-synchroniczną.
1. dla każdego równania bloku pragmy;
2. jeżeli lista zmiennych pragmy jest pusta lub jeżeli aktualne równanie
(jego lewa strona) jest na liście zmiennych pragmy to analizuj, w
przeciwnym razie przejdź do następnego równania bloku pragmy, skok
do (1);
3. koryguj równanie, modyfikując zmienne synchroniczne według schematu
zmienna(t,....) -> zmienna(t-1,.....);
4. zmienne aktualnego równania, jeżeli pośrednio lub bezpośrednio
zawierają zmienne sekwencyjne to modyfikuj ich nazwy według
schematu zmienna -> \zmienna\. Nowa nazwa zmiennej objęta w
ukośniki, ma odwoływać do nowego równania, które zostanie
wygenerowane;
5. dla każdej równania zmodyfikowanej zmiennej wykonaj kroki od 3;
Algorytm jest bardziej efektywny od rozwiązania stosowanego w
postprocesorze w wersji pierwszej.
Zadanie wykonywania łączenia równań dla zmiennych typu resolved dla
wersji drugiej postprocesora jest identyczne z wersją pierwszą. Ta część
algorytmu została dokładnie zaadoptowana z wersji poprzedniej i jest
wykonywane przed analizą leksylną.
Algorytm wykonujący zadanie eliminacji zmiennych tymczasowych,
został zmodyfikowany w celu zwiększenia efektywności i wydajności.
Przede wszystkim wykorzystano inny model przechowywania równań
boolowskich. Równania boolowskie tworzyły leksemy, będące stałymi,
zmiennymi i operatorami. Każde równanie, które także było reprezentowane
jako leksemem posiadało listę takich leksemów (rysunek 1). Na podstawie
takiej struktury, zbierane były te same informacje co w wersji poprzedniej
postprocesora oraz wykonywane były te same algorytmy. Przejście na postać
zaprezentowanej struktury, zwiększyło kilkakrotnie wydajność rozwiązania
tego zadania.
Algorytm częściowej minimalizacji równań boolowskich, został
zmodyfikowany w taki sposób, że nie są porównywane łańcuchy znaków
213
reprezentujących zmienne, oraz człony wyrażeń do minimalizacji. Po
analizie leksykalnej zbioru wejściowego i stworzeniu struktury tak jak w
zadaniu poprzednim, równania zapisane były w formie numerycznej.
Porównania liczb jest o wiele bardziej efektywne niż łańcuchów znaków,
składających się z ciągu wartości liczbowych. To samo dotyczy członów
wyrażen do minimalizacji.
4.
WERSJA TRZECIA POSTPROCESORA
KOMPILATORA JĘZYKA VHDL
Postprocesor w wersji trzeciej, zwiększył wydajność dwukrotnie w
stosunku do wersji drugiej w następujących zadaniach:
– uzupełnienie równań boolowskich o logikę;
– łączenie równań boolowskich dla sygnałów typu resolved;
Ponadto dodano nowe cechy funkcjonalne do częściowej minimalizacji
równań boolowskich poprzez dodanie nowych reguł minimalizacji:
!A|A&B=
A|A&B =
!A|!A&B
A|A&B =
!A|B
A
= !A
A|B
oraz reguły dla logiki Don’t Carry, która polega na minimalizacji równań w
przypadku gdy równanie boolowskie zawiera wartości Don’t Carry.
Wartości takie traktuje się albo jako prawdę albo jako fałsz, w zależności od
tego która z tych wartości prowadzi do bardziej zminimalizowanego
równania. Jeżeli traktowanie sygnału don’t carry jako logiczna jedynka lub
zero daje ten sam wynik, domyślnie bierze się logiczne zero.
Zwiększenie efektywności i wydajności osiągnięto poprzez
odwzierciedlenie równań boolowskich ze zbioru wejściowego w model
opisany za pomocą poniższego rysynku.
Model ten tworzy z równań boolowskich strukturę grafu, w którym
możliwe są cykle. Element stanowi leksem, będący zmienną, stałą lub
operatorem. Na rysunku, kolorem szarym oznaczono leksem będący
operatorem. Element będący zmienną posiada wskaźnik na swoje,
definiowane równanie.
214
Rysunek 8a. Schemat wewnętrznej reprezentacji równań boolowskich w postprocesorze
w wersji trzeciej
Algorytm analizy pragm poprzedzony jest wykonaniem pewnej
czynności na takiej siatce powiązanych leksemów. Chodzi o oznaczenie
zmiennych definiujących równania, posiadające zmienne sekwencyjne bądź
bezpośrednio lub pośrednio, poprzez inne równanie. Po zgromadzeniu takiej
informacji, ogólny algorytm generowania dodatkowych równań dla układów
sekwencyjno-kombinacyjnych przedstawia się następująco:
1. Dla każdego równania z bloku pragmy, które ma być analizowane;
2. Przeprowadź korekcję równania, polegające na modyfikacji nazwy
zmiennej, której równanie posiada pośrednio lub bezpośrednio zmienne
sekwencyjne według schematu:
Nazwa_zmiennej -> /nazwa_zmiennej/
Każdą zmodyfikowaną zmienną dodaj na stos;
3. Dla każdego elementu z utworzonego stosu utwórz nowe równanie na
podstawie wcześniej zdefiniowanego. Wykonaj operację (2) na nowym
równaniu;
W ten sposób powstają nowe równania tam gdzie jest to wymagane.
215
5.
ANALIZA EFEKTYWNOŚCI RÓŻNYCH WERSJI
POSTPROCESORA
W celu udokumentowania wzrostu wydajności i efektywności kolejnych
wersji postprocesora, stworzyłem zbiór testów wejściowych.
Poniższa sekcja zawiera porównania czasów wygenerowania wyników
przez różne wersje postprocesora dla poszczególnych zadań.
Testy dla poszczególnych wersji i zadań przeprowadzane były w tych
samych warunkach, na tej samej konfiguracji sprzętowej i programowej i
przy podobnym – średnim obciążeniu procesora.
Charakterystyka testowanych przykładów została zmieszczona w
poniższej tabeli.
Nazwa
Ilość równań
boolowskich
Przykład 1
Przykład 2
Przykład 3
Przykład 4
Przykład 5
28736
456
644
3075
9
Ilość zmiennych typu
resolved
Ilość
bloków
pragmy
Ilość
zmiennych
sekwencyjnych
Rozmiar
zbioru
wejściowego
(w bajtach)
10
0
11
202
138
4100
37
64
20
1
4100
37
64
20
1
1942924
34616
265924
259195
5944715
Tabela 1. Rozmiar wygenerowanych równań boolowskich
Oczywiście na wyniki nie wpływały tylko ilość bloków pragm czy
równań boolowskich, ale także złożoność równań boolowskich, np.
zależności między nimi.
Tabela następna przedstawia wyniki czasów analiz bloków pragm oraz
zadania rezolucji równań boolowskich dla różnych wersji postprocesora.
Czasy podane są w milisekundach.
wersja 1
Przykład 1
Przykład 2
Przykład 3
Przykład 4
Przykład 5
377062
140
790
591
Nieokreślony
wersja 2
10435
161
510
911
11296
wersja3
4727
90
360
521
6429
Tabela 2. Porównanie czasów przetwarzania równań boolowskich
dla różnych wersji postprocesora
Dla przykładu 5, czas analizy pragm dla postprocesora w wersji 1 jest
oznaczony jako nieokreślony. Oznacza to czas powyżej 500000 milisekund.
216
Jak prezentuje tabela, w kolejnych wersjach postprocesora, nastąpiło
znaczne zwiększenie efektywności zadań postprocesora.
Nie zamieszczono wyników minimalizacji równań boolowskich oraz
eliminacji zmiennych tymczasowych, ponieważ w kolejnych wersjach obok
zwiększenia wydajności dodawano funkcjonalność, zatem porównywanie
wyników dwóch różnych implementacji nie daje możliwości ocenienia
zwiększenia wydajności.
6.
PODSUMOWANIE
Niniejszy artykuł zaprezentował postprocesor kompilatora języka VHDL
oraz rozwój jego wersji. Głównym celem kolejnych wersji było zwiększenie
niezawodności oraz efektywności. Cel został osiągnięty i jest nadal
głównym kryterium dalszej pracy nad postprocesorem.
Zadania przewidziane na przyszłość to dalsze prace nad zwiększeniem
efektywności a także zwiększenie funkcjonalności postprocesora. Obecnie
trwają prace nad konwersją równań boolowskich do postaci BLIF (Berkeley
Logic Interchange Format).
LITERATURA
[1] W. Bielecki, S. Hayduke, P. Jaworski, Generowanie równań boolowskich dla logiki
asynchroniczno-synchronicznej w syntezowalnej wersji języka VHDL,
RUC 2000, Szczecin
[2] www.sgi.com/tech/stl , Standard Template Library Programmer's Guide
[3] C++ księga eksperta, Jesse Liberty, Helion 1999
A Creation Of Boolean Equation Graph For
Automatic Parallelizing
Alexander Chemeris, Elena Nowatska, Swetlana Reznikowa
Institute of Problems of Modeling in Energetics, Kiev, Ukraine
Abstract:
1.
Some features of Boolean equations, which are very important for
parallelizing process, are presented. Using an example a way for logic
equations converting into graph representation for further paralleling
is considered. We are planning we will use distributed parallel computers for
modeling of digital devices for FPGA projects. A general algorithm of
paralleling is presented.
INTRODUCTION
A creation of Boolean equation graph is very important task for the
paralleling of logic systems during modeling of logic circuits. Basing on the
flow graph we develop methods of Boolean equations paralleling. The input
information for paralleling program is the system of Boolean equations. It’s
syntaxes we consider shortly in the next chapter.
The system of Boolean equations has some features we have to take into
consideration. The first chapter describes logic devices and Boolean
equations representing them. The next one is devoted to features of logic
equation graph. These features determine an algorithm of graph creation and
paralleling. An example is presented in the last chapter.
218
2.
LOGIC DEVICES AND EQUATIONS
DESCRIBING THEM
In general, digital devices can be represented as two parts. The first part
is the combinatorial logic device and the second one is the sequential circuit.
These two parts are shown on Fig. 1 where the device has Xi inputs and Yj
outputs. Q(t) are the outputs of sequential circuit and the index t means that
Q belong to cycle t. D(t+1) is the value of sequential circuit input belonging
to the next cycle.
Xi
Q (t)
Combinatorial
logic
Sequential
circuit
Yj
D (t+1)
Clock
Figure 1.
Any digit device may be described as a system of Boolean equations
taking into account the fact that signals on input and output are separated in
the consecutive cycles.
Let’s consider a circuit shown on Fig. 2. It has some logic elements
and two elements with memory (flip-flop) Q and Q1. D and C are the
inputs and output of flip-flop Q is the output of the device.
For the modeling of this one we can describe it by the following
system of Boolean equations where ‘|’ is OR operation, ‘&’ is AND
operation and ‘!’ is NOT one.
Q = S | (!R & Q);
S = C & Q1;
R = C & !Q1;
Q1 = S1 | (!R1 & Q1);
S1 = D & !C;
R1 = !C & !D;
More detailed description of Boolean equations syntax is in [1].
219
D
0
&
0
S1
&
0
Q1
0
0
&
0
0
&
0
0
>=1
S
SET
0
Q
>=1
0
R1
0
S
0
R
CL R
Q
R
0
&
0
0
0
&
0
S
SET
Q
Q
0
R
CL R
Q
0
0
0
0
C
0
Figure 2.
In general, systems of Boolean equations we may present as the
following expression.
F = Ψ ( x1 , x2 ,..., xn , F1 , F2 ,..., Fm ); (1)
where Ψ is the set of logic equations with AND, OR and NOT operations.
A main goal of the automatic paralleling is a partitioning of the system
(1) and scheduling of logic equations on several processors. It is very
important to realize an interchange of data between processors. So we need
the graph of Boolean equation system to solve the problem of equation
partitioning. The graph defines links between equations and we may define
the sequence of their execution on a processor and the order of data
interchange. Further we consider some features of Boolean equations and
graphs described them.
3.
FEATURES OF LOGIC EQUATION
PARALLELING
When we realize the modeling process, we use two steps of paralleling.
First of all we need to create a graph consists of nodes and links describing
dependencies between nodes. This graph represents the system of Boolean
equations, which describe modeling device. The second step is the step of
graph nodes scheduling when we plan the tasks for every processor of
multiprocessor computer we use for modeling. So we have to keep two
principles during scheduling. The first one consists in guaranteeing of
220
processor balancing and the second one have to minimize inter-processor
communications. Describing some graph features we suppose that we will
use distributed computer systems for modeling process. The next statement
we use consists in the following. We guess one node of graph presents one
Boolean equation. So the way to solve the problem of graph creation consists
in retrieval of dependencies between Boolean equations.
Distributed computers don’t have common memory and thus we don’t
need to consider some graph dependencies such as output dependencies and
anti-dependencies taking into account in computer systems with common
memory. So we consider only data dependencies for Boolean equations. But
we have to remember that modeling device has feedbacks that bring to
cycles in the graph. As an example we may write following equations.
Q = X | !Y;
R = Z & W | !Q;
Y = R & U;
X
Z
Q
U
W
R
Y
Figure 3.
Fig. 3 shows a unit described by the system of equations above. The
circuit Q→R→ Y→Q makes a loop both in the real device and in the graph.
Paralleling of such subgraphs makes the task of scheduling more complexes
and it brings to appearance of multiple inter-connections between data in
various processors. Algorithms of processor task balancing will demand to
put these subgraphs into the same processor. So it will be right to put these
nodes of graph on the same level.
The Boolean equations define the structure of digital device. There is no
meaning what order of Boolean equations we have. Modeling device will
function right regardless of the order of equations. So constructing graph we
don’t take into view the lexicographical order of the equations. We must to
look for dependencies of each node with the rest both before and after
current node.
221
The main feature is the following. Analyzing memory elements (flipflops) we have to consider dependencies of another kind. They define the
using of memory elements on the next cycle but not now. So the example
above (see Fig. 2) shows such dependence in equation Q = S | (!R & Q);
where it is distinguished the variables Q in left and in right parts. The value
of Q in the right part is regarded to the cycle t but Q in the left part is
regarded to the t+1 cycle. There are various variables which may be
paralleled in time but we have to take into view that the value of Q(t) have to
be forwarded to Q(t+1). So it is important for inter-processor exchange. We
will call such sort of dependencies as dependencies of second kind.
So we may formulate an algorithm of Boolean equation’s transformation
for calculating on a parallel computer.
– We make numbering of graph nodes taking into view that one Boolean
equation corresponds to one graph node.
– We calculate workload on processor for every node of graph. We need
such information to balance parallel processor’s work.
– There are defined data dependencies for every graph node. Here we define
the second kind dependencies too.
– There are defined cycles in the graph and they are joined into complex
nodes. So we get the acyclic graph.
– Using information about dependencies we assign the graph nodes to
levels.
– There are used any scheduling method to plan graph nodes to processors
of multiprocessor system.
4.
EXAMPLE
Continuing the example on fig. 2 we represent a graph for this system of
Boolean equation.
Here the second kind links are represented by dotted lines. Practically the
nodes 2 and 3 may be performed in parallel with nodes 5 and 6. But if the
nodes 4 and 2, 3 will be placed on various processors during scheduling then
the variable Q1 have to be forwarded from processor with node 4 to
processor with nodes 2,3.
222
C
D
6
5
R1
S1
4
3
Q1
2
S
R
1
Q
Figure 4.
5.
CONCLUSIONS
Paralleling is the way to decrease the time of digital device’s modeling.
One of the tasks we need to solve is the creation of Boolean equation graph.
If we have the graph then we may use any scheduling method to plan the
work of every processor of multiprocessor system. So the graph creation is
the very important process.
The creation of Boolean equation graph has some features we need to
take into consideration. These features allow to increase the paralleling level
and to build the right program for graph creation.
Our group has made the program, which create the Boolean equation
graph. This program uses the unique inner data structure to decrease the time
of program work. There is very important to model large systems of Boolean
equations describing real FPGA projects.
REFERENCES
[1] W. Bielecki, P. Jaworski “Generating Boolean equations for synchronous-asynchronous
logic described in VHDL language.” Advanced Computer Systems ACS’2000, Szczecin,
Poland, 2000.- pp. 397-400
Multiprocessor System Emulator
For Digital Devices Modeling
Alexander Chemeris*, Svetlana Reznikova*, Andrzej Rotkiewicz** ,
Krzysztof Szczepanski**, Zbigniew Zalewski**
*Institute of problems of modeling in power engineering NAS of Ukraine, 15, General
Naumov Str., 03680, Kiev, Ukraine, e-mail: [email protected]
**Aldec-Gdansk, Gdansk, Poland, e-mail: [email protected]
Abstract:
1.
A program model (the emulator) of specialized multiprocessor computer for
synthesized digital devices simulation is described in the paper. Functioning of
the multiprocessor computer and its software model is based on the solution of
Boolean equations systems, which describe the structure and behavior of the
prototyped device. The structure of emulator is described and an instance of
digital device modeling is given. The work is supported by PRUS Inc. (USA).
INTRODUCTION
FPGA technology is widely used for construction of computers,
managing devices, gauge devices, used in manufacture, medicine, household
devices etc. [1]. At present FPGAs are widely applied as interface circuits, in
micro-systems for organization of exchange and mating of various largescale integrated circuits among themselves and with input-output device. On
the basis of various logic blocks and systems, converters of codes, peripheral
controllers, micro-program control unit and also other devices such as
multipliers, small processors and processors of Fourier fast transformation
can be made. FPGA is widely used in digital signal processing (DSP) [2].
FPGA adds design flexibility and adaptability with optimal device utilization
while conserving both board and system power, which is often not the case
with DSP chips.
In addition, the FPGA’s application gives other advantages
223
224
1st diminution of number of used chips types (as one type FPGA can be
used for design of various devices);
2nd small time of development and manufacturing;
3rd guard of the circuit from copying (the programming of “privacy bit"
does not allow to read contents of FPGA).
With increase of FPGA complexity the necessity of elaboration and
usage of the new systems of automatic design, which are more productive,
becomes more actual. One of the methods of the elaboration acceleration of
digital devices based on FPGA is the usage of multiprocessor specialized
computers, which permit to reduce time of modeling up to minutes, at the
time when it can reach hours in usage of modern single processor personal
computers. It is obviously that such computers must be provided with
automatic means of paralleling, the aim of which is distribution of the tasks
for processors. Herewith the principle of minimization of data sending
between the processors must be taken into account. Otherwise resulting
program of modeling will work in the multiprocessor computer longer than
its single-processing analogue.
It is obviously that the program complex used for the work in
multiprocessing computer is quite complex for elaboration and usage. The
program model that changes hardware is designed for checking of the
software being elaborated, for definition of errors and working out the tests
for hardware. Properties of emulator (for example, program adjustment by
commands), which are similar to the properties of the known compilers
programs, make the emulator a unique instrument during its work with
software of the multiprocessor computer.
Besides, another aspect of the emulator usage is possible. We mean the
emulator usage in educational system. The user, who is new to applicable
architecture, in particular, to parallel programming, feels certain problems.
Work with emulator will allow to feel the possibilities of the parallel system,
feel the processes occurring in it, in particular, process of data interchange
between processors. And finally, it is possible to consider the emulator as a
part of demonstration complex of programs for acquaintance with
architecture of multiprocessor computer and with process of programming
itself.
One section of the article is dedicated to the description of the project of
the multiprocessor computer creation for modeling of digital devices named
PRUS. Further, the PRUS emulator will be investigated in more detail, the
process of work is described, and a sample of modeling of decoder for
septisegmental display is given in the last section.
225
2.
MULTIPROCESSOR SYSTEM OF DIGITAL
DEVICES MODELING
Figure 1 shows block scheme of the components of the project PRUS.
The project is based on the usage of Boolean equations systems, which is
generated by the high-level compilers languages as VHDL and Verilog.
VHDL2Bool
Compiler
Verilog2Bool
Compiler
Boolean equations
Bool2PRUS
Compiler
PRUS program
Software PRUS
Emulator
PRUS on board
(Hardware)
VHDL PRUS
model
Figure 1. PRUS system flow chart
The main program is a program for compiling of systems of Boolean
equations into binary code of multiprocessor computer considering
distribution of the equations on the processors. Testing and usage of the
compiled program is possible on any of three systems. First of all, it is a
computer realized in a chip (hardware). Besides, it is possible to use the
PRUS model, which is created in VHDL. The emulator supports the same
files with a model of digital device, which is a software model realized for
working on personal computer as an independent program.
It is necessary to dwell upon description of multiprocessor computer,
which is a prototype of a software model before description of the emulator.
Functional scheme of the PRUS computer is represented on the fig. 2. The
computer contains N processors that have separate inputs and outputs.
Common instruction counter connects all processors, that is, operations with
the same address in all processors are executed synchronously.
226
input
LS #1
LS #2
LS #3
LS #N
output
Reset
Counter
+1
Figure 2. PRUS structure
All processors named Logic Sequence are linked between each other by
additional relationships, by means of which the data transfer is realized in
nearby processor. Considered computer PRUS configuration represents an
encircled structure, where the last processor in chain is connected with the
first one. Each Logic Sequence is a single-bit processor, which is designed
for execution of the logical operations AND, OR over operands, entering on
entry, or from nearby processor, or from memory of the processor itself.
Additional arithmetical block Logic Sequence executes the operations of
summation, forming the values of sum and carry-over, and summations by
modulo 2 (XOR). Structural scheme of a separate Logic Sequence is
depicted on the fig. 3.
Except of above-mentioned processors, Logic Sequence includes the
program and data memory, input and output circuits and instruction decoder.
The address value enters memory of the programs from the common for all
Logic Sequence counter. Address value of the memory data is formed in the
Logic Sequence itself depending on the command being executed. The
program memory is designed for storage of single bit operands, which can be
recorded beforehand or have been got in a process of modeling program
functioning.
Block ALB is an additional device for performing of the arithmetical
operations. Function ALB represents a single-order adder. Its peculiarity is a
way of connection with other processors by means that, depending on the
executed command, either the sum value of the previous processor or the
Data MUX value enters ALB. The Result of such connection consists in the
performing of the arithmetical operations by means it is realized in systolic
structures.
227
Input
MUX
Data
MUX
Single
Bit
Proc.
Out
Dec
Out
Buffer
Data
RAM
Sum
LS(i-1)
Program
Memory
Address
Instr.
Dec
to LS(i+1)
ALB
Carry
Control
signals
Figure 3. Logic Sequence structure
Software that is elaborated for functioning with computer PRUS is worth
mentioning separately. We mean software complex, whose destination is to
prepare files for loading into program memory depending on the digital
device model, which is written in VHDL. VHDL-program is undergoing
several processing stages; the first one is generation of the Boolean
equations system. Further this system of equations, which is practically a
program for modeling of digital device being elaborated, is distributed on
processors. Internal code for each Logic Sequence is generated based on the
distributed on processors systems of equations.
The emulator works directly with files that are generated by compiler.
And since emulator manipulates with variables, which are determined in the
equations system, then compiler generates additional files, necessary only
for emulator. In additional files, in particular, information is kept on
appointment of input (output) variables to definite input (output) of
processors of the computer PRUS. For instance, information is determined
about the fact that "variable Х is given to input of the processor Р with
number I".
Further we will consider the description of the emulator PRUS and the
process of working with it more in detail.
228
3.
MULTIPROCESSOR SYSTEM EMULATOR
The main purpose in PRUS emulator development was the creation of the
facilities for adjustment of emulator PRUS for debugging and testing of the
software. That is why some peculiarities were placed in emulator applicable
in facilities of the compilers debugging. In particular, there were provided
several modes of modeling of PRUS computer functioning. So, the user is
given the possibility on-command program performance that allows
analyzing values in all computer PRUS units at each cycle.
The main unit of measurement of model time of work is a machine cycle.
One instruction is executed during one machine cycle in parallel in all
processors. The command address is defined by value of common for all
processors instruction counter. The system of Boolean equations and,
accordingly, the program in binary code defines values on element output,
forming a prototyped device. These are the values, which correspond to the
state of elements after one machine cycle of prototyped device i.e. one
performance of the program corresponds to one machine cycle of prototyped
device. We will name one-shot execution of the program as a system cycle.
Except of command performance of the program more modes of
execution of one system cycle are provided (when the program is executed
once from the beginning till the end) and the mode of N system cycles
execution. In this case the program is executed N times and depending on
input sequence we get N values as a result. Differentiation between two
regimes consists in the fact that execution of one system cycle is realized on
current state of the prototyped device while execution of N system cycles
must be executed on the initial state of device.
The Emulator is developed as Win32 MFC Dialog-Based Application for
functioning in Windows 9x/NT. There is a depiction of PRUS computer
emulator on fig. 4.
229
Figure 4. PRUS emulator main window
The window of emulator is divided into several sections. The first section
(Common) contains the common control elements, namely, button of the
program start for execution and Reset button and number indicators of
current cycle and number of instruction executed.
The second section is an image of PRUS computer configuration where
processors and relationships between them are presented. The relationships
are additionally used for indication of values that are sent from processor to
processor. It is visible in use of the one step mode - blue arrow indicates that
'0' is sent while red arrow tells about ‘1’. The processors on the picture are
active elements of the interface, which activate data indication given in the
third section where the whole information for selected processor is
presented. All elements are displayed here, which form computer processors
such as program memory, data memory, logical and arithmetical processors,
in and out circuits. Besides the section includes indication of the current
command, which is executed in this processor and its mnemonics in
assembler language.
The emulator has the status bar and menu except of these sections. In
status bar current information is displayed concerning current process of
modeling. It is possible here to see the name of the project, which is loaded
now for simulation, simulation mode and type of connection of the transfer
230
carrying the arithmetical block. By means of menu the user can load a new
project for simulation, execute the manipulations with input data and
program of initialization of the prototyped device state, load the modeling
program, start the process of simulation, view the result of modeling and set
the mode of simulation and connection type of carry transfer in the
arithmetical block.
We examine the process of simulation in more detail. Here it is possible
to select three phases, namely, the project loading, execution of modeling
process, performing the process of simulation and the results review. In the
first phase the following processes are fulfilled: a) project choice; b)
program load, c) data load. The process of emulator functioning starts with
the configuration project file load, which is generated by the compiler, and
general information is entered into it: project name, number of commands in
the modeling program, number of in and out variables and others. This
information is necessary for determination of emulator internal parameters,
for instance, dimensionality of area for program memory modeling of PRUS
processor on personal computers.
Determination of input variable values is realized in the input variable
editor where appointment of test sequences to specific variables is produced.
The editor has its own menu, in which manipulation with files is realized
since the generated sequences may be saved for the next usage. Menu “New”
illuminates the variables list and further it is necessary to enter the value set
of {0,1}, which determine the test sequences. So the matrix of values is
connected with the set of variables names. The first column of matrix defines
variable values, which are given to inputs of the prototyped device in the
first system cycle, the second column - variable values, which are given in
the second one and etc. Values entered are possible to save using menu
points “Save” and “Save As”.
Analogous editor is used for determination of variable values, which
present the flip-flops in the system of Boolean equations. The main
distinction of editor for input variables consists in the fact that the
determination of flip-flop state is executed only at the beginning of the
process of simulation and, consequently, it is necessary to define only one
set of values. For simplification of this process additional option is entered
into editor - generation of variable values: either every '0', or all '1`.
The following step is produced by program load (menu: Load Program)
when input variables are determined. After this command execution a)
significant values appear in program memory; b) cycle amount necessary for
program load is displayed in the CLK field and c) the button Run becomes
active in the field Common that means the possibility to start the process of
simulation. The program is executed in step-by-step mode by default. For
231
mode change it is necessary to address the point in menu Settings and to set
the necessary mode.
Program performance in the step-by-step mode is produced in the case
when it is necessary to trace the states of processor blocks in the process of
program performing. It is necessary if the result of prototyped program gives
not the corresponding result. Besides it is an opportunity to understand the
modeling process, but particularly, the process of multiprocessor architecture
functioning in the sense of data exchanges.
4.
SAMPLE OF SIMULATION
We will consider the simulation sample of simple digital device, namely,
decoder of septisegmental display in this section. The display (see fig.5) has
seven elements, which are activated depending on values at the input
decoder and forms the numeral from 0 to 9.
Z1
Z2
Z3
Z4
Z5
Z6
Z7
Figure 5. Output variables
The system of Boolean equations is given in table 1 in which у1, у2, у3
and у4 are input variables being bits of displayed digit. Values of equations
are marked as upper-cased letters, amongst which there are variables
required for segments activation. These are variables Z1-Z7, values of that
are formed in accordance to table 2. Other variables define intermediate
values, which define output of the internal elements of the decoder.
232
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
A1=!y1|!y2|!y3;
A=!A1;
B1=y1|y2|!y3;
B=!B1;
C1=y2|y4|y3|!y1;
C=!C1;
D1=y1|y3|!y2;
D=!D1;
E1=y1|B;
E=!E1;
F1=y2|y3|y4;
F=!F1;
G1=y1|!y2|!y3;
G=!G1;
H1=y2|!y3|!y1;
H=!H1;
I1=y3|y4|!y1;
I=!I1;
J1=B|C;
J=!J1;
ZZ7=A|B|C;
Z7=!ZZ7;
Z6=D1;
Z5=E;
ZZ4=A|F;
Z4=!ZZ4;
ZZ3=G|H;
Z3=!ZZ3;
ZZ2=D|A|I;
Z2=!ZZ2;
Z1=J;
Table 1.
Work with behavior modeling of septisegmental display by means of
emulator is executed not by Boolean equations system, given in the table 1,
but by the program with PRUS binary code, which is received by compiling
of this system of equations considering the task distribution on processors.
The feature of this task consists in the fact that the prototyped device is the
device of combinatory type i.e. the system of Boolean equations does not
contain flip-flop variables. Output variable values depend on only the values
233
of input variables, and modeling result for one set of input variable is got for
one system cycle.
у4
0
1
2
3
4
5
6
7
8
9
у3
0
0
0
0
0
0
0
0
1
1
у2
0
0
0
0
1
1
1
1
0
0
у1
0
0
1
1
0
0
1
1
0
0
Z1
0
1
0
1
0
1
0
1
0
1
Z2
1
0
1
1
0
1
1
1
1
1
Z3
1
0
0
0
1
1
1
0
1
1
Z4
1
1
1
1
1
0
0
1
1
1
Z5
0
0
1
1
1
1
1
0
1
1
Z6
1
0
1
0
0
0
1
0
1
0
Z7
1
1
0
1
1
1
1
1
1
1
1
0
1
1
0
1
1
0
1
1
Table 2.
5.
CONCLUSION
Functioning of the emulator of multiprocessor PRUS computer intended
for acceleration of the simulation process of designed digital devices is
examined simplified. Program model of this computer can be used either for
adjustment and testing of the PRUS computer software or for the
demonstration of simulation with possibility of execution time determination
of program simulation. The user gets the opportunity to estimate beforehand
the simulation acceleration comparing with other program packages. The
emulator with processor structure as 4x4 array was created too.
REFERENCES
[1] Scott Hauck The roles of FPGA’s in reprogrammable systems. // Proceedings of
the IEEE, vol. 86, No 4, April 1998.
[2] Xilinx DSP. High Performance Signal Processing – Journal of Xilinx Inc., January 1998.

Podobne dokumenty