În acest laborator vom învăța primele noțiuni de Verilog, un limbaj de descriere hardware. Îl vom folosi pe parcursul laboratorului pentru a exemplifica și implementa noțiuni legate de arhitectura calculatoarelor.
Verilog este un HDL (Hardware Description Language) folosit pentru specificarea formală a circuitelor electronice digitale. Alte limbaje similare sunt:
Un asemenea limbaj este utilizat pentru descrierea sistemelor digitale, de exemplu un calculator sau o componentă a acestuia.
…nor is it Pascal, C#, Java, (any kind of) Assembly, Lisp, Haskell, Prolog, PHP, etc.
Din punct de vedere sintactic, Verilog se aseamănă cu limbaje procedurale precum C, dar trebuie să ținem cont de faptul că instrucțiunile nu sunt executate secvențial, ca pe un procesor. În timp ce target-ul unui limbaj de programare uzual este să ruleze pe un procesor, target-ul unui cod scris în Verilog este să modeleze funcționalitatea hardware a unui FPGA sau un ASIC (Application Specific Integrated Circuit). Un FPGA (Field-Programmable Gate Array) este un circuit integrat care poate fi programat pentru a se comporta ca un alt circuit digital, descris prin codul Verilog scris de programator.
Un procesor execută o secvență de instrucțiuni, distribuind semnale de control și date diferitelor componente aritmetice și logice. Spre deosebire, un FPGA va cupla diferite componente interne pentru a simula o componentă aritmetică sau logică. În esență, un FPGA este pur și simplu o matrice de porți logice, legăturile dintre ele fiind configurabile. FPGA-urile reprezintă o platformă de dezvoltare pentru cei care doresc să construiască un circuit integrat de la zero. Prin reconfigurarea porților logice, pe un FPGA se poate construi orice în domeniul circuitelor digitale, de la drivere de LED-uri si LCD-uri, până la întregi chipset-uri și chiar procesoare. Mai multe detalii despre FPGA-uri vom învăța într-un laborator viitor.
Un limbaj precum Verilog conține o serie de abstractizări sau moduri de a genera, prin intermediul codului, porți logice. În comparație cu a proiecta “de mână” circuitele integrate, tocmai aceste abstractizări sunt cele care au permis electronicii digitale să se dezvolte în ritm rapid odată cu progresul tehnologiei de fabricație. Cu ajutorul lor putem descrie relativ ușor structuri complexe, divizându-le în componentele lor comune și de bază.
Însă apare întrebarea naturală: Ce aș putea face cu un FPGA și nu aș putea face cu un procesor? Pe scurt, există trei răspunsuri:
Atunci când proiectăm un circuit digital folosind un HDL, începem prin a face o descriere textuală a circuitului, adică scriem cod. Acesta este compilat, iar în urma procesului va rezulta un model al circuitului care poate fi apoi rulat într-un simulator cu scopul de a verifica funcționalitatea descrierii. O alternativă la simulare este folosirea unui utilitar de sintetizare, care preia codul HDL și generează fișiere de configurare pentru FPGA.
Unitatea de bază a limbajului Verilog este modulul, ce conține descrierea interfeței și a comportamentului unui circuit electronic. Un modul este cel mai apropiat de conceptul de “black box” ce constă în cunoașterea intrărilor și a ieșirilor, fără a ști însă detaliile de implementare și modul în care el funcționează.
Declararea unui modul se face, generic, în felul următor:
module <nume_modul>( <tip_port_1> <nume_port_1>, <tip_port_2> <nume_port_2>, ... <tip_port_n> <nume_port_n> ); // Implementarea modulului. endmodule
Această declarație constă în:
module
input
)output
)inout
);
(punct și virgulă)endmodule
Și un exemplu concret:
module test( output out, input in ); buf(out, in); endmodule
$
și _
. Primul caracter trebuie să fie o literă sau _
.module test_interface(output a, input b); // Implementare modul. endmodule
În cazul modulului de mai sus interfața este test_interface(output a, input b);
.
Implementarea modulului constă în descrierea funcționării acestuia utilizând:
wire
): reprezintă conexiuni fizice între componente. Se folosesc pentru transmisia semnalelor (doar în descrierea logicii combinaționale) și nu au capacitate de reținere a informației (nu au o stare)reg
): sunt folosite pentru a stoca date, care persistă chiar dacă registrul este deconectat. Registrul este echivalentul unei variabile interne dintr-un limbaj de programare. Reține o valoare și i se poate atribui o valoareinitial
: ne permit să definim o stare inițială. Acest bloc va fi declanșat o singură dată, la inițializarea modulului. Este nerecomandată folosirea lor în afara simulărilor
wire
. Putem să specificăm, dacă dorim, ca ieșirile să aibă tipul reg
.
Toate variabilele de tip wire
și reg
(implicit și cele de tip input
, output
și inout
) au, în mod implicit, lățimea de 1 bit. Putem specifica, dacă dorim, o lățime mai mare de atât folosind construcția [i:j]
. Atenție, i și j pot să nu fie niciuna 0 și pot fi i >= j sau j >= i.
input a; // Variabilă fir de intrare, pe 1 bit. output b; // Variabilă fir de ieșire, pe 1 bit. output reg c; // Variabilă registru de ieșire, pe 1 bit. inout d; // Variabilă fir bidirecțională, pe 1 bit. wire e; // Variabilă fir locală, pe 1 bit. reg f; // Variabilă registru locală, pe 1 bit. input [7:0] g; // Variabilă fir de intrare, pe 8 biți ordonați 7 -> 0. wire [0:7] h; // Variabilă fir locală, pe 8 biți ordonați 0 -> 7. reg [2:6] i; // Variabilă registru locală, pe 5 biți ordonați 2 -> 6.
Verilog permite folosirea unui modul în descrierea altui modul prin instanțierea acestuia. Instanțierea modulelor se face, generic, în felurile următoare:
nume_modul <nume_instanta>( <nume_argument_1>, <nume_argument_2>, ..., <nume_argument_n> ); nume_modul <nume_instanta>( .nume_port_1(<nume_argument_1>), .nume_port_7(<nume_argument_7>), .nume_port_2(<nume_argument_2>), ..., .nume_port_n(<nume_argument_n>) );
Și exemple concrete:
module test_instantiation_A(output a, output b, input c, input d); // Implementare modul. endmodule module test_instantiation_B(output out1, output out2, input in1, input in2); test_instantiation_A a1(out1, out2, in1, in2); // Am scris argumentele in ordinea porturilor. test_instantiation_A a2(.b(out2), .a(out1), .c(in1), .d(in2)); // Am scris argumentele intr-o ordine aleatoare, dar pentru fiecare am specificat numele portului la care trebuie legat. test_instantiation_A a3(out1, out2, in1); // Nu am legat portul "d". Nu este o eroare. // Implementare modul. endmodule
Pentru a descrie circuite folosind Verilog, avem la dispoziție și o serie de primitive care sunt asociate porților logice de bază. Fiecare primitivă are asociate porturi prin intermediul cărora se face legătura cu exteriorul. Astfel, există primitive predefinite care oferă posibilitatea conectării mai multor intrări (and, or, nor, nand, xor, xnor), sau a mai multor ieșiri (buf, not).
Folosirea unei primitive se face prin instanțierea cu lista de semnale care vor fi conectate la porturile ei.
Ca mediu de dezvoltare vom utiliza Xilinx Vivado, ce ne permite sintetizarea codului, simularea codului și programarea pe plăci. Puteți descărca Xilinx Vivado urmând acest tutorial.
Vom implementa în Verilog un modul ce are o intrare și o ieșire, ambele pe 1 bit. Ieșirea modulului va fi intrarea negată. De asemenea vom implementa un modul de test pentru cel precedent. Pentru crearea proiectelor și modulelor folosind Xilinx Vivado urmăriți acest tutorial.
module hello_world( output out, input in ); not(out, in); endmodule
Pentru a testa funcționarea unui modul trebuie să creăm un modul de test (test fixture). Pentru un exemplu de simulare a unui modul urmăriți acest tutorial.
`timescale 1ns / 1ps module test_hello_world; // Intrari. reg in; // Iesiri. wire out; // Instantiem Unit Under Test (UUT). hello_world uut ( .out(out), .in(in) ); initial begin // Initializam intrarile. in = 0; // Asteptam 100 ns pentru a termina resetul global. #100; // Adaugam stimuli aici. #100 in = 1; #100 in = 0; end endmodule
După cum vedeți, este un modul ca oricare altul. Două construcții ies totuși în evidență:
timescale
: definește unitatea de timp a simulării.#100;
: această construcție indică simulatorului să aștepte 100 de unități de timp. Când prefixăm o instrucțiune cu #n
simulatorul va aștepta n unități de timp, după care va executa instrucțiunea. Atenție, această construcție nu este validă decât într-o simulare.Task 0 (0p) [DEMO] Realizați un modul cu 2 intrări (a și b) și 2 ieșiri c = a & b și d = a | b. Testați funcționarea modulului.
Task 1 (1p) Simulați exemplul din scheletul de laborator.Tutorial simulare Vivado
Task 2 (3p) Implementați o poartă XOR
, folosind porți AND
, OR
și NOT
. Testați funcționarea modulului.
Task 3 (3p) Implementați un multiplexor 4:1. Testați funcționarea modulului.
Task 4 (3p) Implementați un circuit combinațional cu 2 intrări de date, 2 intrări de selecție și 1 ieșire. Intrările de selecție selectează ce funcție logică să se aplice pe cele două intrări de date pentru a rezulta ieșirea. Cele 4 funcții logice sunt XOR
(folosiți modulul de la task 2), NAND
, OR
și AND
. Modulul va funcționa astfel:
00
, se aplică NAND
01
, se aplică AND
10
, se aplică OR
XOR
Testați funcționarea modulului.
Task 5 (Bonus 1p) Implementați un sumator Half-Adder (sumator pe 1 bit fără intrare de transport) și unul Full-Adder (sumator pe 1 bit cu intrare de transport). Testați funcționarea modulelor.