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.