A VHDL hardverspecifikus nyelvi elemeit egy példán keresztül szeretném bemutatni. Ez a példa egy nagyon egyszerű ALU (aritmetikai-logikai egység) megvalósítása. Ez nem egy tipikus gyakorlati példa, de jól szemléltethetők vele a hardvertervezés különböző nyelvi eszközei.
Az entitások (vagy alkatrészek) olyan blokkok, amik egy-egy a rendszerben létező elem „fekete doboz” megközelítésű leírását adják, tehát csupán egy interfész definíciót, ami – mint egy eljárás vagy függvény paraméterlistája – felsorolja az adott egység be- és kimeneti portjait. Ugyancsak itt adunk meg különböző inicializáló és hibajelző kifejezéseket is.
entity entitás_neve is generic(generikus_objektumok); port(a_felületet_megadó_portlista); egyéb_deklarációk begin (ellenőrző)_utasítások end entitás_neve;
Az első és utolsó soron kívül a többi rész elhagyható. Az üres entitás nem kapcsolható semmihez az interfészek hiányában, viszont alkalmas eszköz a szimulációs környezetek kialakítására.
Most nézzük, mit jelentenek az egyes elemek!
A leglényegesebb rész a port, amivel megadjuk a ki- és bemeneti portokat. A zárójelpáron belül elnevezzük, és iránnyal valamint típussal látjuk el az egyes csatlakozókat. A típus bármely a nyelv által támogatott típus lehet, de jellemzően a BIT és BIT_VECTOR, vagy az STD_LOGIC és STD_LOGIC_VECTOR típusok közül kerül ki, mivel ezeket lehet közvetlenül megfeleltetni a fizikai felépítésben létező ténylegesen huzalozott összeköttetési pontoknak. Persze, ha más típusok használatával élünk, akkor sem ütközünk hibába a leképezés során, csupán ilyen esetekben implicit típuskonverzió megy végbe.
Az irányra vonatkozó jelzések a következők lehetnek:
A porthoz hasonló másik listaszerkezetet mutató elem a generic. Ez teszi lehetővé, hogy az entitás definiálása során egyes objektumokat generikus (általánosított) módon használhassunk. A generic részben megadhatunk változtatható értékű konstansokat, módosítható típusokat vagy akár a feladatnak megfelelő működésű alprogramokat/függvényeket is. (Utóbbiakat csak az IEEE 1076-2008 szabványtól kezdve.)
Ezek nagyon hasonlók ahhoz az objektum-orientált programozásból ismert technikához, ahol az absztrakt osztályok függvényeit később a származtatott objektum típusának megfelelő kódrészlettel írjuk felül. A típusok dinamikus kezelése pedig a template-objektumokra emlékeztethet. Ezek a hasonlóságok csak a kódírás idejéig élnek, mivel a HDL-ek kimente egy statikus szerkezet, amiben nem lehetséges a futásidejű (késői) kötés.
Az entitások deklarációs szakaszának egyéb_deklarációk megevezéssel jelölt része olyan kifejezések írására ad lehetőséget, amik láthatósága az entitás működéseit definiáló blokkokra korlátozódik. Itt deklarálhatunk például (nem generikus) konstansokat, altípusokat és bármely más objektumot is.
Az entitás begin kulcsszavát csak abban az esetben kell kiírnunk, ha szeretnénk, hogy minden egyes példányosítás során végrehajtódjanak a blokkba írt utasítások. A kikötés csupán annyi, hogy az itt megjelenő utasításoknak passzívnak kell lenniük, tehát nem lehetnek értékadó viselkedésűek. Ilyenek a figyelmeztető utasítások (assert) vagy akár a passzív processek és a szintén passzív alprogramok hívásai is. Ezek mindegyike leginkább a hibák ellenőrzésére és kiküszöbölésére használható.
Ezek után már elkészíthetjük a korábban elképzelt ALU entitását:
entity ALU is generic(width : POSITIVE := 7); port( data_0, data_1 : in std_logic_vector(width downto 0); opcode : in std_logic_vector(1 downto 0); result : out std_logic_vector(width downto 0); flag : out std_logic ); end ALU;
Az ALU két változtatható szélességű (alapértelmezetten 7 bites) bemenő szót vár, valamint egy kétbites műveletkódot. A kimenet egy ugyanilyen széles szó és egy jelzőbit.
Az entity megírásával már kapcsolhatjuk az ALU-nkat más alkatrészekhez vagy rendszerekhez. Azt viszont még nem határoztuk meg, hogy a többi elem bizonyos kommunikációs igényeire hogyan reagáljon. Erre való az architecture blokk.
Ennek általános formája a következő:
architecture architektúra_neve of entitás_neve is deklarációs_szakasz begin utasítások end architektúra_neve;
Mint látjuk, az entitás_neve megadja, hogy melyik alkatrészre vonatkozik a leírás. Egy entitáshoz akár többet is írhatunk.
A deklarációs_szakasz tartalmazhatja vezetékek, komponensek, függvények, eljárások, típusok és konstansok deklarációját is. A törzsben elhelyezett utasítások pedig a konvencionálisan használt három tervezési stílustól (adatfolyam, felépítés, viselkedés alapú) függően lehetnek párhuzamos értékadások, komponens példányosítások vagy akár szekvenciális végrehajtású blokkok is.
A vezetékek olyan változókként használható objektumok, amik a rendszer statikus komponensei közötti összeköttetést ábrázolják. Akár egy változónak, a szignáloknak is értéket adhatunk, vagy olvashatunk ki belőlük. Az alkatrészek csatlakozópontjait is szignálokként kell kezelnünk, azzal a megkötéssel, hogy ezeknek az irányítását is figyelembe kell vennünk.
Egy egyedi vezetéket az architektúra blokkok deklarációs szakaszaiban hozhatunk létre. Láthatósága (fizikai elérhetősége) is az adott blokkra fog korlátozódni.
Deklaráció:
signal szignál_neve : feloldó_függvény szignál_típusa :=kezdőérték;
A feloldó_függvény és a kezdőérték nem kötelező elem. A feloldó függvényeket arra használhatjuk, ha az adott szignálnak több forrása is van, akkor ezek a függvények döntik el a vezetékre kerülő adat értékét.
Értékadás, ahol a késleltetési_idő nem kötelező:
szignál_neve <= értékadó_kifejezés after késleltetési_idő;
A szignálok használatára példaként elkészítjük az ALU egyik funkcióját, a shift_left műveletet külön megvalósító modult.
Ennek entity blokkja legyen a következő:
entity shift_left is generic(width : POSITIVE := 7); port( data_in : in std_logic_vector(width downto 0); data_out : out std_logic_vector(width downto 0); flag : out std_logic ); end shift_left;
Az adatfolyamot meghatározó architecture blokk:
architecture shl_behav of shift_left is begin data_out(width downto 1) <= data_in(width-1 downto 0); data_out(0) <= ’0’; flag <= data_in(width); end shl_behav;
A komponens alapú tervezés eszköze. Egy entitás felépítés alapú tervezésére akkor van lehetőségünk, ha az összes kívánt funkcióját meg tudjuk valósítani már meglévő vagy a jövőben megírandó – de ismert felülettel rendelkező – entitások egymáshoz kapcsolásával. Ehhez szükségünk van az architecture blokk deklarációi között a megfelelő összetevők felületének feltüntetésére és a törzsben ezek csatlakozópontjainak megfelelő kötésére.
A komponens deklarációja nagyon hasonlít egy entity blokkhoz, mivel az egyéb_deklarációk és az (ellenőrző)_utasítások kivételével minden elemet tartalmaz, amit a hivatkozott entitás, csupán a nyitó és záró kifejezés eltérő.
Az általános forma:
component komponens_neve generic(generikus_objektumok); port(portlista); end component;
A komponens_neve egy a csatolt könyvtárak valamelyikében fellelhető entitásnak kell, hogy megfeleljen.
Amikor használunk (példányosítunk) egy összetevőt, akkor tulajdonképpen a port és a generic listák elemeinek feleltetünk meg a tartalmazó entitásbeli objektumokat vagy értékeket.
Ilyen módon:
címke : komponens_neve generic map(generic_megfeleltetések) port map(port_megfeleltetések);
Tegyük fel, hogy az ALU minden műveletéhez elkészítettünk egy-egy külön entitást és még egy modult, ami a műveletkód alapján ezek kimenetei közül választja ki a megfelelőt! Ekkor a követező ábra szerinti felépítést alkalmazzuk. Az ábrán nem jelöltem az egyes vezetékek elnevezését, de az architecture blokkban meg fognak jelenni.
Íme a kód:
architecture ALU_behav of ALU is component shift_left generic(width : POSITIVE := 7); port( data_in : in std_logic_vector(width downto 0); data_out : out std_logic_vector(width downto 0); flag : out std_logic ); end component; -- a többi komponens hasonló deklarációja signal shl_d_sig, add_d_sig, and_sig, or_sig : std_logic_vector(width downto 0); signal shl_f_sig, add_f_sig : std_logic; begin comp_0 : shift_left generic map(width => width) port map( data_in => data_0, data_out => shl_d_sig, flag => shl_f_sig ); -- többi művelet entitásai comp_4 : switch generic(width => width) port map( data_0 => shl_d_sig, data_1 => add_d_sig, data_2 => and_sig, data_3 => or_sig, flag_0 => shl_f_sig, flag_1 => add_f_sig, opcode => opcode, data_out => result, flag_out => flag ); end ALU_behav;
A port és generic listák megfeleltetéseinél ügyeljünk egyrészt a típusok egyezésére, másrészt, hogy az előzetes elképzeléseinknek megfelelő helyre kössük az egyes vezetékeket! Egy igen hasznos segítséget nyújt ehhez a VHDL: Csakúgy, mint az Ada nyelvben, itt is megengedett a sorrend és a név szerinti paraméterátadás is. Utóbbi módszerrel könnyebben átlátjuk az összetevők közti viszonyokat.
A viselkedés alapú tervezés eszköze. Ezzel a tervezési módszerrel egészen bonyolult algoritmusokat is leírhatunk, amik különböző események hatására döntéseket hoznak a hardver pillanatnyi állapotának megfelelően, és eszerint járnak el a továbbiakban. A pillanatnyi állapot egy igen összetett fogalom. Ennek meghatározásához figyelembe kell vennünk legelőszöris, hogy az adott folyamatot melyik vezeték milyen eseménye váltotta ki. Ezek után, ha szükséges, azt is ellenőrizzük, hogy a többi vezeték vagy a folyamat saját regiszterei milyen értékeket tárolnak.
A processzek – vagy folyamatok – az architecture blokkok belsejében megjelenő szerkezetek, amikkel eseményvezérelt modellezést valósíthatunk meg. A process egy olyan, a nyelvre jellemző blokkszerkezet, aminek a deklarációs zónán és a magján kívül van még egy függőségi listája is. Ez opcionális eleme, de ezzel pontosítható, hogy mely szignálokon jelentkező események hatására lépjen működésbe az adott folyamat. Még egy lehetőség az all kulcsszó használata, ami bármely esemény hatására aktiválja a folyamatunkat.
Egyes processzek az alkatrészek funkcionális egységekre való bontásának eszközei. Ezek egymással és a külvilággal a belső szignálokon vagy a portokon keresztül kommunikálnak események hatására.
Egy folyamat általános felépítése így néz ki:
címke : process(függőségi_lista) deklarációk begin utasítások end process;
A processzekben elhelyezett utasítások végrehajtása szekvenciális, mint egy Neumann-elvű program futás során. Ez a viselkedés vonatkozik az összes itt megjelenő, egyébként párhuzamosan működő szignál értékadásokra is.
A process deklarációi között hozhatunk létre például csak itt deklarálható regiszterváltozókat (variable). A törzsben, mint láttuk, lehetnek párhuzamos értékadások is, ezenkívül a variable objektumok szekvenciális értékadásai, figyelő-figyelmeztető (assert, report), várakozó (wait), alprogramhívó (procedure, function), visszatérő (return), elágazásokat és iterációkat megvalósító (if, case, loop, next, exit) és üres (null) utasítások is megjelenhetnek.
Mint említettük, ezek az objektumok csak egy-egy process deklarácis szakaszában létrehozhatók, és a majdani regisztereket reprezentálják.
Sokcélú nyelvi elem. Kiválthatjuk vele például a processek opcionális függőségi listáját (wait on függőségi_lista), de adott feltétel teljesüléséig (wait until logikai_kifejezés) vagy meghatározott időtartamig (wait for időtartam) is várakozhatunk vele.
Mindezek után végül nézzünk meg egy viselkedés alapú tervezést megvalósító példát az ALU switch komponensét megírva.
architecture switch_behav of switch is begin process(data_0, data_1, data_2, data_3, flag_0, flag_1, opcode) begin case opcode is when ”00” => data_out <= data_0; flag_out <= flag_0; when ”01” => data_out <= data_1; flag_out <= flag_1; when ”10” => data_out <= data_2; flag_out <= ’0’; when ”11” => data_out <= data_3; flag_out <= ’0’; when others => null; end case; end process; end switch_behav;
Ezt az nyelvi elemet párhuzamosan végrehajtható utasítások automatizált létrehozására használhatjuk. Egyszóval tekinthetjük makró utasításnak is. Segítségével nagy, homogén (legfőképpen generikus méretű) struktúrák ismétlődő alkotóelemeinek példányosítsát, valamint a vezetékeik összekötését könnyíthetjük meg, és esetleg egyes utasítások paraméterezését vagy létrehozását feltételekhez is köthetjük.
Két változata létezik. A for-generate és az if-generate.
Ezek használatát szeretném szintén az ALU egy komponensén bemutatni. Ehhez az összeadást végző alkatrészünket képzeljük el úgy, mint egy saját magunk által felépített n-bites összeadót! Ehhez n vagyis a generikus width által meghatározott számú teljes összeadót kell példányosítanunk.
Először generate nélkül:
architecture add_behav of add is signal carry : std_logic_vector(4 downto 0) := (others => ’0’); component full_adder port( first_bit, second_bit, carry_in : in std_logic; sum, carry_out : out std_logic ); end component; begin flag <= carry(4); digit_3 : full_adder port map( data_0(3), data_1(3), carry(3), data_out(3), carry(4)); digit_2 : full_adder port map( data_0(2), data_1(2), carry(2), data_out(2), carry(3)); digit_1 : full_adder port map( data_0(1), data_1(1), carry(1), data_out(1), carry(2)); digit_0 : full_adder port map( data_0(0), data_1(0), carry(0), data_out(0), carry(1)); end add_behav;
Most generate-tel:
architecture add_behav of add is -- ugyanazok mint az előbb, csupán a carry -- méretében a 4 helyett width+1 szerepel begin flag <= carry(width+1); with_width : for i in width downto 0 generate digit : full_adder port map( data_0(i), data_1(i), carry(i), data_out(i), carry(i+1)); end generate; end add_behav;
Az utóbbi megoldás, amellett, hogy elegánsabb, már egy érték átírásával (width) könnyedén módosítható az általunk megtervezett nyolcbites ALU-val való együttműködéshez is.
Amint elkészítettünk egy hardverkomponenst, még mielőtt összekapcsolnánk más rendszerekkel, vagy leképeznénk egy valódi hardverre, ajánlott szoftveres szimulátor segítségével tesztelni a funkcionális helyességét. Szerencsére a VHDL eddig megismert nyelvi eszközeivel az ehhez szükséges futtatókörnyezet is megíható.
A megoldás igen egyszerű. Létrehozunk egy új "hardveregységet" (egy portlista nélküli entitást), és ezen belül példányosítjuk a tesztelendő modult, mint komponenst. Ezek után tesztadatokat kötünk a bemeneteire, és a kimenetein megjelenő értékeket figyeljük.
Mivel ezt a kódot nem képezzük le hardverre, ezért itt a megszokott módon "programozhatunk", tehát használhatunk szimulált időzítéseket (wait), vagy akár fájlkezelést is a tesztadatok kezeléséhez. Ezek után csak egy megfelelő szimulátorral végre kell hajtani a tesztet.
A példaprogramok között megtalálható az egész ALU implementációja, ami tartalmaz egy a tesztre alkalmas entitást is.