ED aplicatii 1 : Instantiere si porti logice, circuite combinationale elementare, memorii ROM

De la WikiLabs
Jump to navigationJump to search

Teorie: incapsulare si instantiere

Unitatile constructive de baza din care se formeaza circuitele digitale se numesc porti logice. Acestea implementeaza functii logice si prin conectarea mai multor astfel de circuite simple complexitatea unui circuit poate creste pana la nivelul procesoarelor actuale. Pentru a putea controla cresterea complexitatii in proiectarea unui circuit de dimensiuni mari se folosesc 2 concepte cheie:

1) incapsularea functiei dorite intr-un modul

2) instantierea unor module mai mici si asamblarea acestora pentru a forma un modul mai mare.

Incapsularea se refera la a grupa elementele ce alcatuiesc o anumita functionalitate intre-un modul. Aceste elemente pot la randul lor sa fie alte module. Instantierea (asemanator cu POO) se refera la a apela un modul deja scris pentru a fi folosit efectiv in circuitul curent. Pentru a face o analogie cu programarea, cand se declara o variabila (sau un obiect), tipul variabilei este echivalent cu modulul si numele ei este numele instantei.

Pentru a intelege aceste concepte se da circuitul de mai jos:


Observatii:

1) Notatie aici: Numele modulului este scris in interior.

2) Notatie aici: Numele instantei este scris deasupra.

3) Idee fundamentala: prin instantierea si incapsularea unor circuite simple, mici, apar circuite mai complexe.

4) Idei esentiale (daca vreuna e neclara consultati cadrul didactic):

a) "modul_2" este instantiat o singura data in tot proiecul, iar instanta se cheama "x"
b) "modul_2" are o intrare numita "in0" si o iesire numita "out0"
c) "modul_2" este instantiat in cadrul "modul_6"


d) Modulul cel mai mare, ce cuprinde toata functionalitatea dorita a sistemului (aici "modul_6") se numeste uzual top.
e) Instanta top-ului care apare atunci cand se doreste testarea sa in simulare intr-un testbench (tb) se numeste uzual DUT (design under test)


f) "modul_0" apare instantiat de 3 ori. Circuitul final, cuprinde 3 subcircuite de tip "modul_0".
g) La nivel de top, instanta sa se cheama "b".
h) La nivel de "modul_5", cele 2 instante se cheama "a" si "c".
i) "a" si "c" sunt 2 circuite fizice diferite chiar daca ambele sunt de tipul "modul_0". Fiind instante ale aceluiasi modul, deci identice in alcatuire, luate separat ele fac acelasi lucru. Luate in contextul lui "modul_5", "a" genereaza datele de pe firul "w0" si "c" genereaza datele pentru iesire.


j) circuitul "b" de tip "modul_1" (instantiat in "modul_5") este complet diferit de circuitul "b" de tip "modul_0" (instantiat in top). Este permis ca ele sa aiba acelasi nume (cele 2 instante) deoarece se afla in locatii diferite.


k) Identic, mai multe module au o intrare numita "in_0". La sinteza circuitului nu se face confuzie intre acestea deoarece fiecare e vazut la nivelul altei instante.
l) Identic, firele de legatura "w0", "w1".


m) La nivelul "top", firul de legatura "w2" leaga iesirea "out_0" a instantei "b" la intrarea "in2" a instantei "y".
n) La nivelul "top", firul de legatura "w1" leaga iesirea "out_0" a instantei "x" la intrarea "in1" a instantei "y".
o) La nivelul "modul_5", firul de legatura "w1" leaga iesirea instantei "b" la intrarea instantei "c".
Pentru claritatea desenului, respectivele intrari si iesiri nu au fost denumite. In cod este obligatoriu ca ele sa fie definite si denumite.


p) La nivelul "top", "in0","in1","in2","in3","in4" si "out0" formeaza interfata modulului (semnalele care intra sau ies din modul).
q) La nivelul "top", "w0", "w1", "w2" sunt fire interne de legatura.
r) La nivelul "top", firul "in3" este conectat ca intrare pentru 3 submodule.

Teorie: testarea circuitelor

Pentru a se testa functionarea corecta a circuitului final, acesta este instantiat intr-un modul numit "test_bench" sau "tb". Acest modul este folosit strict in simulare. Testarea unui circuit are loc conform schemei urmatoare:

Generarea datelor de intrare se va face asemanator cu laboratorul 1.

Voi veti avea rolul modelului ideal si al comparatorului datelor de iesire, uitandu-va la datele de intrare veti calcula iesirea corecta si apoi veti compara aceasta valoare cu raspunsul circuitului ce se testeaza.


In cazul unui sistem automat, se genereaza mesaje de eroare sau mesaje ca functionarea este in regula.


Observatie: proiectarea si verificarea sunt 2 domenii diferite, firmele avand departamente separate pentru acestea.

Proiectarea/Design se ocupa cu scrierea in Verilog a modului de top si toate modulele ce se afla in acesta, avand ca scop final realizarea fizica pe placa a unui circuit.

Verificarea se ocupa de scrierea in SystemVerilog (limbaj format din Verilog cu concepte de POO) a modului de testbench si generarea de stimuli si scenarii care sa testeze functionarea design-ului.


Teorie: porti logice

Portile logice folosite uzual, impreuna cu tabelele lor de adevar si reprezentarea grafica sunt date mai jos.

Se folosesc porti logice cu 1 sau 2 intrari, cele cu mai mult de 2 intrari fiind construite din acestea.


Tip Simbol Tabel de adevăr Tip Simbol Tabel de adevăr
Buffer/Repetor Buffer symbol
INTRARE IEȘIRE
A A
0 0
1 1
NOT NOT symbol
INTRARE IEȘIRE
A NOT A
0 1
1 0
AND AND symbol
INTRARE IEȘIRE
A B A AND B
0 0 0
0 1 0
1 0 0
1 1 1
NAND NAND symbol
INTRARE IEȘIRE
A B A NAND B
0 0 1
0 1 1
1 0 1
1 1 0
OR OR symbol
INTRARE IEȘIRE
A B A OR B
0 0 0
0 1 1
1 0 1
1 1 1
NOR NOR symbol
INTRARE IEȘIRE
A B A NOR B
0 0 1
0 1 0
1 0 0
1 1 0
XOR XOR symbol
INTRARE IEȘIRE
A B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0
XNOR XNOR symbol
INTRARE IEȘIRE
A B A XNOR B
0 0 1
0 1 0
1 0 0
1 1 1

Despre FPGA

FPGA-ul (Field-Programmable Gate Array) este un circuit programabil, capabil sa implementeze circuite definit de utilizator. El este format dintr-o matrice de blocuri programabile, interconectate intre ele printr-o serie de conexiuni la randul lor programabile.


Cand se doreste implementarea unui circuit pe FPGA, acesta urmeaza urmatoarele etape:

Elaborarea este procesul prin care codul Verilog este transformat intr-un circuit la nivel de porti si registre (netlist) si este independent de modelul de FPGA folosit.
Sinteza este procesul în care se realizează transformarea circuitului descris într-un netlist dependent de tehnologie. Se vor folosi la acest pas primitivele disponibile pe FPGA.
Implementarea este procesul în care se preia netlist-ul ce conține primitivele FPGA și modul lor de interconectare realizat la pasul de sinteză și se realizează maparea lor efectivă în FPGA (place and route).
Generarea Bitstream-ului este procesul prin care informatiile din implementare sunt asamblate intr-un singur fisier.
Programarea este procesul prin care fisierul generat anterior este trimis efectiv catre placa cu FPGA (prin USB) unde determina modificarea valorilor si conexiunilor interne din aceasta.


O introducere mai detaliata poate fi gasita aici : FPGA - Introducere.

Observatii:

  • Pentru a putea controla circuitul propus si a putea vedea rezultatele, intrarile si iesirile (input si output din module) in/din acesta trebuie conectate la pini fizici ai FPGA-ului care sunt conectati la butoane/switch-uri/leduri.
  • Conexiunile dintre butoane/switch-uri si pinii FPGA sunt fixe. La fel si cele intre pinii FPGA si LED-uri. (in functie de PCB)
  • Conexiunile dintre porturile modulului Module si pinii FPGA sunt configurabile. (in functie de noi, prin fisierul XDC)
  • Legarea porturilor modulului Module la pinii fizici ai FPGA se realizeaza prin configurarea conexiunilor din FPGA conform contrangerilor de I/O pe care le vom mentiona in proiect, inainte de sinteza.

Exemple

Exemplul 1: Analiza circuitelor cu porti

Fie urmatorul circuit alcatuit din porti logice:


Se doreste exprimarea circuitului de mai sus ca formula de tip: iesire = f(intrari).

Pentru asta, abordarea consta in a porni de la iesire si a merge pas cu pas catre intrari, asa cum este exemplificat mai jos. Se vor folosi simbolurile "~" pentru NOT, "&" pentru AND, "|" pentru OR.

pas1: out0 = ?
pas2: out0 = w1 | w2
pas3: out0 = w1 | ( ? )
pas4: out0 = w1 | ( sel & in1 )
pas5: out0 = ( ? ) | ( sel & in1 )
pas6: out0 = ( in0 & w0 ) | ( sel & in1 )
pas7: out0 = ( in0 & ( ? ) ) | ( sel & in1 )
pas8: out0 = ( in0 & (~sel) ) | ( sel & in1 )

Exemplul 2: NAND2 din AND2 si NOT

In urmatorul exemplu se implementeaza o poarta NAND din o poarta AND si o poarta NOT.

Codul de mai jos exemplifica ideea de instantiere si contine comentarii legate de sintaxa verilog necesara.

In mod uzual fisierele sunt denumite dupa modulul ce se afla in ele, in fiecare fisier fiind un singur modul.

Schema circuitul care se doreste a fi creat este:

Descrierea portii NOT (fisierul not_gate.v):

// comentarii cu "//" sau cu /* .... */ ;
/* 
	ca in c/c++ 
*/ 

module not_gate	// cuvant cheie "module" apoi numele modulului (asemanator clase din c++)
	( // intre paranteze se pune interfata (firele care intra sau ies din modul)
		input wire in0,         // in0 este o intrare => input, in0 este fir => wire ;
		output wire out0	// out0 este o intrare => output, out0 este fir => wire 
	); // aici ";" sa nu il uitati

assign out0 = ~in0;	// cuvant cheie assign; 
					// semnalele de tip wire (cum e out0) iau valoare prin assign
					// ~ e semnul pentru negatie pe biti (ca in c/c++)

endmodule // cuvant cheie "endmodule". orice module se inchide cu endmodule.

Descrierea portii AND (fisierul and_gate.v):

module and_gate
	(
		input in0,	// aici sunt 2 intrari 
		input in1,	// se poate omite "wire". default e wire daca nu e pus nimic 
		output out0
	); // nu conteaza ordinea in care sunt puse intrarile si iesirile.
			// uzual si pentru usurinta se ordoneaza si grupeaza dupa functionalitate
			// in cazul de mai sus, am pus intai intrarile, apoi iesirile.

assign out0 = in0 & in1; // operatia propriu zisa 

endmodule

Descrierea modulului top (fisierul top.v):

module top
	(
		input a,
		input b,
		output c
    );
    
wire w0;    //declarat un fir intern de legatura 
    
and_gate and_gate_0	// instantiere: nume_modul nume_instanta (asemanator int x din c/c++)
	(
		.in0(a), // la intrarea "in0" a instantei "and_gate_0"  se conecteaza firul "a" din top
		.in1(b), // grija ca "in0", "in1", "out0" sa existe in declararea modulului "and_gate"
		.out0(w0) // grija ca "a", "b", "w0" sa existe la nivelul modulului in care se face instantierea
	);    
    
not_gate not_gate_0
	(
		.in0(w0), // "w0" care iese din "and_gate_0" intra in "not_gate_0"
		.out0(c) // "c" care iese din "not_gate_0" iese din modulul "top" (e iesire in interfata de sus)
	);   // se poate scrie si ".in0(w0),.out0(c)" dar se prefera fiecare fir pe randul sau (lizibilitate si loc de comentarii pt design-uri complexe)
	// Observatie: la varianta de mai sus de instantiere, nu conteaza ordinea firelor. 

/*
not_gate not_gate_0		
	(				// se poate instantia si in forma prescurtata ca aici 
		w0,				// in acest caz se pun conexiunile in ordinea in care sunt declarate intrarile si iesirile din modul
		c	// NU se recomanda stilul asta de instantiere
	); 			// apar greseli frecvent la ordinea firelor, la numarul lor, 
*/				// mai ales daca modulul e complex si are multe intrari si intrari
 
endmodule

Echivalent se pot folosi si "primitive" Verilog pentru scrierea top-ului.

Primitivele sunt porti logice care exista deja in limbaj, folosite atunci cand se doreste o descriere structurala a circuitului.

In instantiere acestora, iesirea se pune prima, urmata de intrari.

Observatie: daca se doreste scrierea acestor porti de catre voi (ca mai sus), numele modulului nu trebuie sa fie un cuvant cheie ocupat de primitive.


Descrierea alternativa a modulului top (fisierul top_v2.v):

module top_v2
    (
	    input a,
	    input b,
	    output c
    );
  
wire w0;

and and_gate_0(w0,a,b);	// primitiva pentru poarta and
not not_gate_0(c,w0);  	// primitiva pentru poarta not
   
endmodule

Fiind un modul simplu, intreaga functionalitate putea fi scrisa la nivel de "top" si simplificat ca mai jos.


Descrierea alternativa a modulului top (fisierul top_v3.v):

module top_v3
    (
	    input a,
	    input b,
	    output c
    );
   
assign c = ~ (a & b); // a si b, negate
   
endmodule

Testarea circuitul se face printr-un testbench, acesta fiind:


Descrierea testbench-ului (fisierul top_tb.v):

`timescale 1ns / 1ps

module top_tb(); // din/in tb nu intra si iese nimic. niciodata.

reg a_tb;	// ce va fi intrare pentru dut, aici e declarat ca "reg", ca sa poata tine valori 
reg b_tb;	
wire c_tb;	// ce e declarat ca iesire pentru dut, aici e declarat ca "wire" pentru ca device-ul ii va da valorile

top dut	// instantierea modulului de tip "top" sub numele "dut" 
	(
		.a(a_tb),
		.b(b_tb),
		.c(c_tb)
    );

initial
begin // in loc de { ... } din c/c++, in Verilog se pune begin ... end 
	#10;		// dupa 10 unitati de timp 
	a_tb = 0;		// a_tb ia valoarea 0
	b_tb = 0;
	#10;		// dupa inca 10 unitati de timp, deci in total la 20
	a_tb = 1;
	b_tb = 0;
	#10;
	a_tb = 0;
	b_tb = 1;
	#10;
	a_tb = 1;
	b_tb = 1;
	#10;
	a_tb = 0;
	b_tb = 0;
	
	#20 $stop();	// oprirea simularii 
end //end pentru initial 

endmodule

Formele de unda rezultate din simulare se pot vedea mai jos:


In chenarul verde se pot observa instantele din simulare.

Daca se doreste adaugarea de semnale noi pentru a fi vazute, se selecteaza modulul instantiat in care acestea se afla (chenar verde).

Apoi, din chenarul rosu se aleg semnalele si se adauga prin click dreapta->add to wave window.

Pentru o mai usoara vizualizare, semnalele se pot grupa asa cum se vede in chenarul mov prin click dreapta->new group.


Se observa o functionare corecta circuitului, acesta fiind o poarta NAND. El scoate "0" cand ambele semnale de intrare sunt "1" si scoate "1" in rest.


Implementarea circuitului pe FPGA

Dupa ce circuitul a fost testat in simulare, se doreste punerea sa fizica pe placa FPGA. Pentru aceasta, se tine cont de urmatoarele.

  • Pentru a controla intrarile a si b ale circuitului 'top, va trebui sa le conectam prin intermediul conexiunilor configurabile ale FPGA-ului la pini ce sunt mai departe conectati fizic la dispozitive ce pot controla valorile semnalelor (switch-uri, butoane).
  • Pentru a observa valoarea iesirii, vom conecta semnalul de iesire cla un dispozitiv de observare, cum ar fi un LED, care va fi aprins daca c este 1 si stins daca c este 0.

Pentru a realiza conectarea porturilor modulului top la pinii fizici ai FPGA ce sunt conectati mai departe la componentele de pe placa, va trebui sa specificam in utilitarul de sinteza maparea porturilor la acestia. Maparea se realizeaza prin mentionarea codurilor pinilor la care dorim conexiunea si poata numele de constrangeri de I/O. Aceste coduri sunt mentionate in documentatia placii de dezvoltare cu FPGA (in cazul nostru, PYNQ Z2) si pot fi regasite si in pagina PYNQ-Z2 - Pinout.

Conform schemei, la acest exemplu avem nevoie de codurile pinilor conectati mai departe la Switch0, Switch1 si LED0.

Observatie: Daca intrarile si iesirile ar fi semnale pe mai multi biti, fiecare bit al intrarilor va fi conectat la cate un switch/buton si fiecare bit al iesirii va fi conectat la un LED. Acest lucru este necesar deoarece switch-urile/butoanele pot avea doar doua stari (0 si 1) si astfel pot controla un singur bit. La fel, un LED poate avea doar doua stari (0 - stins si 1 - aprins) si poate afisa starea unui singur bit.

Dupa ce ati extras codurile necesare, urmati pasii descrisi in tutorialul Vivado.

Dupa programarea cu succes a placii, puteti testa functionalitatea circuitului prin modficarea valorilor switch-urilor si observarea starii LED-ului.

Proiectul complet se poate descarca de aici: W2_exemplu_nand.zip

Exercitii

Pentru urmatoarele exercitii se doreste atat testarea designurilor prin simulare cat si punerea acestora pe placa. Legat de placa, intrarile vor fi conectate la butoane, iar iesirile la leduri. Se va consulta tabelul cu pini disponibili (Pynq-Z2 - Pinout).

Exista moduri mai rapide de a scrie functionalitatea dorita (vezi exemplu), dar tema principala a acestui laborator este instantierea, asa ca sunteti rugati sa respectati desenele si sa instantiati fiecare poarta individual.

Exercitiul 1: AND4 din AND2

Acest exercitiu arata cum se construieste o poarta AND cu 4 intrari din porti AND mai simple, cu 2 intrari. Iesirea acesteia va fi "1" doar daca toate intrarile sunt "1".

Pentru testarea circuitului, generati in testbench urmatoarele forme de unda (liniile punctate reprezinta 5ns):


Exercitiul 2: OR4 din OR2

Asemanator cu exercitiul anterior, se poate construi si o poarta or cu 4 intrari din porti or cu 2 intrari. Iesirea acesteia va fi "1" doar daca oricare din intrari (cel putin una) este "1".

Pentru testarea circuitului se folosesc formele de unda de la exercitiul 1.


Exercitiul 3: AND4 din AND2 aranjat pe lung (nu arbore)

In multe cazuri, aceeasi functionalitate poate fi atinsa prin circuite care arata diferit. O alta varianta de a face o poarta AND cu 4 intrari este prezentata mai jos.

Comparati cele 2 variante.

Pentru testarea circuitului se folosesc formele de unda de la exercitiul 1.


Exercitiul 4: AND4 din NAND2

Orice poarta logica de baza poate fi construita doar din porti NAND sau doar din porti NOR.

Desenati si implementati circuitul pentru o poarta AND4 din porti NAND cu 2 intrari.

Tip: Incercati intai sa generati o poarta AND2 din NAND2.

Pentru testarea circuitului se folosesc formele de unda de la exercitiul 1.


Exercitiul 5: AND4 pe 4b din AND4 pe 1b

Se pot face operatii logice si pe mai multi biti deodata prin punerea in paralel a mai multor porti cu o singura iesire. Exemplu : pentru intrarile 0011 si 1110 iesirea va fi 0010

Pentru exersarea instantierii si intelegerea circuitului din spatele operatilor multibit implementati acest circuit prin instantierea a 4 porti AND2 pe 1 bit intr-o poarta AND2 pe 4 biti.

Pentru testarea circuitului, generati in testbench urmatoarele forme de unda (liniile punctate reprezinta 5ns):

Exista si un mod mai rapid si simplu de a scrie aceasta functionalitate, grupand intrarile si iesirile in "bus"-uri, ca in codul de mai jos. Acest mod de a grupa firele este desenat mai jos.

Fisierul and4_4b.v:

module and4_4b
	(
		input wire [3:0] in0,	// in0 are 4b, de la bit 3 la bit 0 inclusiv 
		input wire [3:0] in1,	// se noteaza msb:lsb (most significant bit: least significant bit) (echivalent cu conceptul de cifra sutelor, cifra unitatilor, pt binar)
		input wire [3:0] in2,
		input wire [3:0] in3,
		output wire [3:0] out0
	);
	
assign out0 = in0 & in1 & in2 & in3;
	
endmodule

Exercitiul 6: Desenati schemele logice pentru urmatoarele circuite:

a) Schema 1

module schema1 (a, b, c, d, f);
	input a, b, c, d;
	output f, g;
	
	wire w1;
	wire w2;
	
	and P1 ( w1, a, c );
	or P2 ( f, w1, w2, d);
	not P3 (w2, b);
	and P4 (g, w1, b, d);
endmodule

Observatie: In versiunea veche de verilog (si s-a pastrat si in cea curenta), se pot specifica directiile porturilor si in exteriorul parantezelor, ca mai sus. Aceasta scriere NU este recomandata.


b) Schema 2

module schema2 (a, b, c, d, f);
	input a, b, c, d;
	output f;
	
	wire w1, w2;
	
	nand P1 ( w1, a, b ), P2 (w2, c, d);
	and P3 ( f, w1, w2);
endmodule

Observatie: asemanator cu C/C++, se pot face declaratii in aceeasi linie a mai multor "variabile", aici fire sau instante, cum se poate vedea la w1 si w2 (ambele fiind fire) sau la P1 si P2 (ambele fiind porti nand). Aceasta scriere NU este recomandata.


Teorie

Acest laborator are ca scop scrierea unor circuite combinationale simple si explorarea variantelor de sintaxa diferite ce ajung sa ofere aceeasi functionalitate. Sintaxa va fi explicata in comentariile exemplului rezolvat mai jos.

Descrierea circuitelor in Verilog se face in una din doua forme:

1) structurala - descrie cum un modul e alcatuit din module sau primitive mai simple
2) comportamentala/behavioural - descrie cum se calculeaza iesirile din intrari


Exemplu

Decodorul este un circuit a carui iesire pe n biti are un singur bit de "1", anume cel selectat prin singura sa intrare pe log2(n) biti. Astfel, pentru un decodor cu iesire pe 8b, intrarea va avea 3b (logaritm in baza 2 din 8) si tabelul de adevar va arata astfel:

in(3b) out(8b)
000 0000_0001
001 0000_0010
010 0000_0100
011 0000_1000
100 0001_0000
101 0010_0000
110 0100_0000
111 1000_0000

Iesirea este numerotata de la bitul 7 (msb) pana la bitul 0 (lsb). Se observa ca daca intrarea are valoare "a", singurul bit de "1" din iesire este cel de pe pozitia "a".

Observatie: Atat in tabelul de mai sus, cat si in codul Verilog poate sa apara "_" (underscore) ca sa ajute vizual la separarea/numararea cifrelor. Se foloseste in mod uzual la scrierea in binar ca sa grupeze 4b (o cifra in hexa).


Circuitul exemplu al acestui laborator va fi un decodor mai mic, cu 2 biti de intrare si 4 de iesire, avand tabelul de adevar:

in(2b) out(4b)
00 0001
01 0010
10 0100
11 1000

O varianta de schema care implementeaza acest circuit este cea de mai jos:

In circuitul de mai sus, apare dubla negatie, respectivul semnal putand fi luat si direct de la intrare. Acest lucru se intampla din considerente electrice. Desi pe desene nu sunt trecute, fiecare poarta are si fire de alimentare si de masa. Astfel, pentru a evita fire de lungimi foarte mari sau fire care se propaga in multe intrari si probleme de cadere a tensiunilor, se folosesc astfel de buffere/repetoare.

Codul de mai jos exemplifica moduri diferite de a exprima aceasta functionalitate. El contine comentarii referitoare la sintaxa de baza Verilog, necesara implementarii modulelor in diverse moduri.

Descrierea modulului decodor - varianta structurala din porti, cu primitive (fisierul decodor_v1.v):

module decodor_v1
	(
		input wire [1:0] in,	// definirea intrarilor sub forma de "bus" 
									// bus-ul de intrare se numeste "in" si este pe 2 biti: in[1], in[0]
									// fizic sunt 2 fire distincte, grupate pentru o mai simpla folosire.
		output wire [3:0] out 	// avem o iesire, numita "out" pe 4 biti.
	);
	
wire in_0_inv;
wire in_1_inv;
wire in_0_nat;
wire in_1_nat;

not not_gate_0(in_0_inv,in[0]); // aleg bitul 0 al lui in sa intre in aceasta poarta not;

not not_gate_1(in_1_inv,in[1]); // observatie: cele 2 fire si cele 2 porti putea fi comprimate intr-un bus + poarta not pe 2 biti.

not not_gate_2(in_0_nat,in_0_inv);

not not_gate_3(in_1_nat,in_1_inv);

and and_gate_0(out[0],in_1_inv,in_0_inv);

and and_gate_1(out[1],in_1_inv,in_0_nat);

and and_gate_2(out[2],in_1_nat,in_0_inv);

and and_gate_3(out[3],in_1_nat,in_0_nat);
	
endmodule

Descrierea modulului decodor - varianta cu assign, urmand schema (fisierul decodor_v2.v):

module decodor_v2
	(
		input wire [1:0] in,
		output wire [3:0] out 
	);
	
	// fiecare bit din iesire calculat independent
assign out[0] = ~in[0] & ~in[1];
assign out[1] = in[0] & ~in[1];
assign out[2] = ~in[0] & in[1];
assign out[3] = in[0] & in[1];

endmodule

Descrierea modulului decodor - varianta cu assign, cu operatorul "?" (fisierul decodor_v3.v):

module decodor_v3
	(
		input wire [1:0] in,
		output wire [3:0] out 
	);

// operatorul "?": identic c/c++; (conditie) ? (if_true) : (if_flase);

assign out = (in==0) ? // if in == 0 
				1 : // if true: out = 1 
				(in==1) ? // else, verifica alt if. 
					2 : // if in !=0 si in == 1
					(in==2) ?
						4 :
						8; // last case in==3

endmodule

Descrierea modulului decodor - varianta cu always cu if (fisierul decodor_v4.v):

module decodor_v4
	(
		input wire [1:0] in,
		output reg [3:0] out // <<== !!!! important: orice semnal ia valoarea intrun "always", trebuie sa fie reg
	);						 // always => reg ; assign => wire

always @(*) // mereu cand se schimba ce se afla in paranteze, se executa (ish "executa") ce e mai jos
	// steluta inseamna "orice"
	// se putea pune si always @(in) => mereu cand se schimba "in", aici fiind singura intrare. daca sunt mai multe se poate pune "in0 or in1 ...or inx"
        // ce este intre paranteze se numeste lista de senzitivitati
begin	// begin si end pe post de acolade din c/c++; delimiteaza if/else si altele asemenea; inclusiv always cu totul
	if(in == 0)
		begin // nota autorului: eu prefer sa pun begin/end asa. ele se pot pune si dupa conditie sau aliniate altfel.
				// ^ preferinta personala legata de coding style, important e sa fiti consecventi in cum scrieti
		out = 1; 
		end
	else
		begin
		if(in == 1)
			out = 2;	// fiind o singura instructiune, begin/end se poate omite (identic c/c++)
		else
			begin	// nota autorului: eu personal le pun mereu. in caz ca mai adaug linii in if dupa, sa fie deja puse
			if(in == 2)
				begin
				out = 4;
				end
			else
				begin
				out = 8;
				end
			end
		end
end//end pt always

endmodule

Descrierea modulului decodor - varianta cu always cu case (fisierul decodor_v5.v):

module decodor_v5
	(
		input wire [1:0] in,
		output reg [3:0] out 
	);

always@(*)
begin
	case(in) // functioneaza ca switch/case din c/c++; sintaxa usor diferita
		0: // if in == 0
			begin
			out = 1;
			end
		2'd1: // if in == 1;	2'd1 se traduce: pe 2 biti, in baza zece (eng: decimal, "d"), valoarea 1
			begin	// pt "0" de mai sus: daca nu se pune nimic se considera in baza zece.
			out = 2;
			end
		2'b10: // if in == 2;	scris in binar "b", 2'b10 => adica valoarea 2(10 binar) pe 2 biti 
			begin
			out = 4;
			end
		2'h3: // if in == 3;	scris in hexa (baza 16 "h", se poate si "x"), fiind numar mic, nu se vede diferenta fata de zecimal.
			begin
			out = 8;
			end
		default: // daca in == un caz netrat; 
			begin // aici inutil pt ca am tratat toate cazurile posibile; pus ca exemplu de sintaxa 
			out = 0;
			end
	endcase // orice "case" se inchide cu "endcase"
end 

endmodule

Descrierea modulului decodor - varianta cu assign comportamental (fisierul decodor_v6.v):

module decodor_v6
	(
		input wire [1:0] in,
		output wire [3:0] out 
	);

assign out = 1 << in; 
	// prin shiftare
	// 1<<0 == 1 == 4'b0001; 
	// 1<<1 == 2 == 4'b0010; 
	// 1<<2 == 4 == 4'b0100; 
	// 1<<3 == 8 == 4'b1000; 

endmodule

Descrierea modulului top (fisierul top.v):

// observatie: 
	// puteti da in vivado: rtl analysis -> open elaborated design -> schematic pentru a vedea desenul rezultat
	// puteti vedea ca moduri diferite de scriere produc aparitia a diferite primitive de sinteza
module top
	(
		input wire [1:0] in,
		output wire [3:0] out_v1, // fiecare varianta de decodor cu iesirea lui
		output wire [3:0] out_v2, // toate ar trebui sa scoata acelasi rezultat 
		output wire [3:0] out_v3,
		output wire [3:0] out_v4,
		output wire [3:0] out_v5,
		output wire [3:0] out_v6
	);
	
decodor_v1 decodor_v1_0
	(
		.in(in),
		.out(out_v1)
	);

decodor_v2 decodor_v2_0
	(
		.in(in),
		.out(out_v2)
	);
	
decodor_v3 decodor_v3_0
	(
		.in(in),
		.out(out_v3)
	);
	
decodor_v4 decodor_v4_0
	(
		.in(in),
		.out(out_v4)
	);
	
decodor_v5 decodor_v5_0
	(
		.in(in),
		.out(out_v5)
	);

decodor_v6 decodor_v6_0
	(
		.in(in),
		.out(out_v6)
	);
	
endmodule

Proiectul complet se poate descarca de aici: W3_exemplu_decodor.zip

Exercitii

Exercitiul 1: Multiplexorul

Multiplexorul (mux) este un circuit ce are rolul de a selecta una dintre intrari pentru ca aceasta sa ajunga la iesire.

Imaginati-va o intersectie in care toate masinile vor sa se iasa catre aceasi strada. Pentru a evita accidente, este nevoie de un element de control, de selectie, prin care se hotaraste care dintre intrari este lasata sa ajunga la iesire, in respectivul moment de timp. Multiplexorul poate fi de asemenea vazut ca un comutator.

Observatie 1: conceptul de baza al multiplexarii este absolut vital si o sa apara des in multe zone ale ingineriei si la materii foarte diferite. Ca niste mici exemple, el se foloseste in:

a) telecomunicatii - cine are voie sa comunice pe un fir la un moment de timp
b) internet - routere - cine trimite pachetele tcp/ip catre destinatia x
c) panouri cu leduri - aprindere pe rand a ledurilor (de la instalatii de craciun pana la ecrane/televizoare care aprind randurile alternativ)

Observatie 2: numarul de intrari maxim posibile este 2^(nr_biti_selectie)

Observatie 3: multiplexoarele pot avea intrarile date si iesirea pe mai multi biti.

Observatie 4: este de ajutor sa incepeti sa vedeti circuitele ca avand o cale de date ce sunt procesate si o cale de control care decide cum se face asta. Aici calea de date este alcatuita din intrari si iesire, iar calea de control din firele de selectie. Acest principiu se propaga si in arhitectura microprocesoarelor unde vor aparea magistrale si memorii de date/instructiuni.

Pornind de la exemplul oferit, implementati multiplexorul in cat mai multe variante.

Multiplexorul cu 2 intrari pe un bit fiecare (mux elementar) este desenat in figurile de mai jos: cum este vazut din exterior (prima figura) si alcatuirea lui interna (a doua figura).

Pentru testarea circuitului, generati in testbench urmatoarele forme de unda (liniile punctate reprezinta 10ns):


Exercitiul 2: Implementati un mux4 (multiplexor cu 4 intrari)

Interfata lui este ca in figura de mai jos.

Pentru varianta lui structurala, mux4 se construieste din mai multe mux2 legate corespunzator. Incercati sa construiti voi aceasta schema.

Pentru testarea circuitului, generati in testbench urmatoarele forme de unda (liniile punctate reprezinta 10ns):


Exercitiul 3: Demultiplexorul

Demultiplexorul (demux) este un circuit ce are rolul de a selecta catre care iesire se va duce intrarea. Imaginati-va o intersectie in care intra o singura masina si trebuie sa decida daca iese catre stanga/inainte/dreapta.

Pentru acest exercitiu se va implementa un demux2 (cu 2 iesiri).

Observatie 1: celelalte iesiri, cele neselectate iau valoarea 0.

Observatie 2: numarul de iesiri maxim posibile este 2^(nr_biti_selectie)

Interfata demultiplexorului este data mai jos. Incercati sa construiti acest circuit in cat mai multe variante de sintaxa posibile (minim4).

Generati forme de unda pentru a vedea functionarea demultiplexorului.


Exercitiul 4: Sumatorul

Sumatorul are rolul de a scoate la iesire suma celor 2 intrari.

Uzual el mai are si intrare si iesire de "carry" adica de transport (cand se depaseste baza de numarare si e nevoie de inca o cifra, ex in zecimal: 88 + 20 = 108; apare o cifra in plus, cifra sutelor).

Observatie: desi scrierea cu assign si + este cea mai usoara si clara pentru un sumator, si celelalte moduri de scriere sunt posibile. La aceasta scriere, daca se doresta ca sumatorul sa aiba carry out, sumatorul se poate face cu un bit mai mare decat intrarile, iar acel bit sa fie conectat la carry out. Practic, obtii un rezultat pe "n" biti + un bit de depasire, anume msb-ul.

Generati formele de unda necesare testarii sumatorului pe 8b.

Pentru a vedea pas cu pas cum se construieste un sumator pe 8b puteti parcurge urmatoarea platforma de laborator: Sumator.pdf


Exercitiul 5: Comparatorul

Comparatorul are rolul de a vedea relatia dintre cele 2 intrari ale sale, daca cele 2 numere sunt egale, daca primul e mai mic sau mai mare.

Astfel el poate avea 3 iesiri dintre care se activeaza doar una:

- iesire de egalitate
- iesire de in0 < in1
- iesire de in0 > in1


Se mai poate face si o varianta de comparator cu 2 iesiri, asa cum veti studia la arhitectura microprocesoarelor.

- iesire de egalitate
- iesire care este 0 daca in0<in1 sau 1 daca in0>in1

Aceasta abordare este folosita la AMP la flag-urile din procesoare, folosind operatia de scadere si flag-urile de "zero" si "negativ".

Scrieti modulul de comparator, in ambele variante de interfata. Intrarile pot avea oricat de multi biti (comparator mai mic sau mai mare), pentru acest exercitiu sa il faceti pe 4b.

Scrieti un testbench care sa genereze toate combinatiile posibile de date de la intrare.

Discutie: Daca acest comparator are datele pe 32b sau mai mult (sau daca circuitul devine mult mai complex), nu se mai poate face o astfel de testare exhaustiva a combinatiilor posibile de date de la intrare pentru ca simularea ar dura prea mult. De situatia asta se ocupa inginerii de verificare ce lucreaza in paralel cu inginerii de design. Pentru mai multe detalii puteti cauta "constrained random functional verification". Incercati sa identificati cazurile/situatiile ce ar trebui testate pentru un comparator pe 32b inainte de a spune ca acesta functioneaza corect (minim 10).

Memoria ROM

Memoria este un dispozitiv electronic utilizat pentru stocarea datelor. Aceasta poate fi de mai multe feluri, având diverse caracteristici, cum ar fi capacitatea de a-și păstra sau nu conținutul după întreruperea alimentării sau capacitatea de a-și modifica sau nu conținutul după terminarea procesului de producție.

Memoria ROM (Read-Only Memory) este un circuit combinational folosit pentru stocarea unor date ce pot fi accesate cu ajutorul unei intrari de adresă. Asa cum sugerează și numele, memoria ROM clasică nu poate fi modificată. În plus, datorită modului în care este implementată, ea își păstrează conținutul după întreruperea alimentării (este nevolatilă).

Pentru a înțelege termenul de memorie, putem face următorul exercițiu de imaginație: să ne gândim la un dulap cu mai multe sertare. Conținutul memoriei este reprezentat de conținutul sertarelor, iar adresele sunt reprezentate de etichetele lipite pe aceste sertare pentru a le identifica. Dacă avem nevoie de conținutul unui sertar, va trebui să știm eticheta aferentă acestuia (adresa).

O memorie ROM are următorii parametri: numărul de biți ai adresei, care este legat de numarul de locații de memorie (cu n biți de adresă putem forma 2𝑛 combinații diferite, deci putem accesa maxim 2𝑛 locații de memorie) și dimensiunea locației de memorie, care ne spune cât de multă informație poate stoca o locație de memorie.


Să urmărim exemplul din figura următoare:


Aici, adresa are dimensiunea de 4 biți, asadar putem accesa cu aceasta 16 locații diferite de memorie. Dimensiunea fiecărei locații este de 8 biți, așadar fiecare locație de memorie poate stoca numere de 8 biți. Spunem că memoria ROM este o memorie 16x8. Capacitatea acestei memorii este de 16 x 8 biți = 128 biți.

Exemple

Exemplul 1: Decodorul

Decodorul este circuitul care, pentru o anumită valoare a intrării, va genera la ieșire un șir binar care conține 1 pe poziția cu indicele egal cu valorea intrării și 0 in rest. De exemplu, decodorul de mai jos pe 2 biți va avea următoarea corespondență intrare-iesire:

in out
2'b00 4'b0001
2'b01 4'b0010
2'b10 4'b0100
2'b11 4'b1000

Vom implementa acest circuit ca memorie ROM.

Implementarea Verilog a circuitului

module Decoder(
  input [1:0] in,
  output reg [3:0] out
  );

  always@(in) begin
    case(in)
      2'b00: out = 4'b0001;
      2'b01: out = 4'b0010;
      2'b10: out = 4'b0100;
      2'b11: out = 4'b1000;
      default: out = 4'b0000;
    endcase
  end

endmodule


Implementarea Verilog a modulului de test

`timescale 1ns/1ps

module Decoder_TB();
  reg [1:0] in_tb;
  wire [3:0] out_tb;
  
  integer idx;

  initial begin
    for(idx=0; idx<4; idx = idx + 1) begin
      in_t = idx;
      #1;
    end 
    #2 $stop();
  end

  Decoder DUT(
    .in(in_tb),
    .out(out_tb)
  );

endmodule

Observații:

  • Circuitul Decoder va avea o intrare pe 2 biți, care va reprezenta adresa memoriei ROM și o iesire pe 4 biți, reprezentând conținuturile memoriei ROM corespunzătoare fiecărei combinații de la intrare.
  • Chiar dacă în interiorul instrucțiunii case se acoperă toate cazurile posibile (toate combinațiile de 2 biți), este bine sa punem și un default pentru a evita inserarea în locul circuitului combinațional dorit (aici, memoria ROM) a unui latch.


Exemplul 2: Transcodorul

Trancodorul este o memorie ROM care asociază unei anumite intrări, reprezentată de adresă, un anumit cod, reprezentat de conținutului locației de memorie asociată acelei adrese.

In acest exemplu, vom folosi transcodorul pentru a realiza un modul ce controlează afișajul cu 7 segmente. Acest dispozitiv de afișare este format, așa cum sugerează și numele, din 7 segmente ce pot fi controlate separat. Putem aprinde sau stinge fiecare segment controlând tensiunea aplicată pe acesta. Pentru acest afișaj, comanda este negativă: aplicând 0 logic pe un segment acesta se va aprinde, iar aplicând 1 logic, acesta se va stinge.

Structura unui afisaj cu 7 segmente este prezentată în imaginea de mai jos:

Pentru a realiza controlul afișării, avem nevoie de o memorie ROM ce trebuie să asocieze codului binar al fiecărui element ce poate fi afișat un șir de biți care sting sau aprind diversele segmente ale afișajului, astfel încât să apară imaginea elementului dorit.

Pentru a reprezenta, de exemplu, cele 16 numere hexa, segmentele trebuie aprinse conform Anexei 1. De exemplu, pentru primele 8 numere (0 până la 7), tabelul de asociere este următorul:

Număr Cod intrare Șir de afisare
0 4'b0000 7'b100_0000
1 4'b0001 7'b111_1001
2 4'b0010 7'b010_0100
3 4'b0011 7'b011_0000
4 4'b0100 7'b001_1001
5 4'b0101 7'b001_0010
6 4'b0110 7'b000_0010
7 4'b0111 7'b111_1000

Implementarea Verilog a modulului Display7Seg

module Display7Seg(
  input [3:0] in,
  output reg [6:0] out
  );

  always@(in) begin
    case(in)
      4'd0: out = 7'b1000000;
      4'd1: out = 7'b1111001;
      4'd2: out = 7'b0100100;
      4'd3: out = 7'b0110000;
      4'd4: out = 7'b0011001;
      4'd5: out = 7'b0010010;
      4'd6: out = 7'b0000010;
      4'd7: out = 7'b1111000;
      default: out = 7'b0000110;
    endcase 
  end

endmodule

Exerciții:

Exercițiul 1

Completați codul din Exemplul 2 astfel încât să se poată reprezenta toate numerele în bază hexa, conform Anexei 1

Exercițiul 2

Un procesor are nevoie de un șir de biți numit instrucțiune pentru a știi ce trebuie să facă. Această instrucțiune codează în șirul său de biți toate informațiile necesare.

De exemplu, pentru un grup de instrucțiuni care operează cu registre, aceste informații pot fi:

  • codul instrucțiunii (operation code - opcode): un șir de biți care ne spune ce operație se execută.
  • destinația: un șir de biți care codează numărul registrului destinație.
  • sursa1: un șir de biți care codează numărul registrului folosit ca operand 1.
  • sursa2: un șir de biți care codează numărul registrului folosit ca operand 2.


Vom considera codul operației pe 4 biți, ceea ce inseamnă ca putem avea 16 instrucțiuni diferite. ]n plus, câmpurile care codează destinația și cei doi operanzi vor avea fiecare câte 4 biți, ceea ce inseamnă că vom avea 16 registre cu care putem lucra (R0 ... R15). Adunând dimensiunile fiecărui câmp, vom obține o instrucțiune pe 16 biți.

Instrucțiunile și codul operațiilor:

Vom considera ca procesorul nostru elementar poate suporta următoarele instrucțiuni:

Operație Cod operație Sintaxă Descriere
ADD 4'b0000 ADD Rd Rs1 Rs2 Rd = Rs1 + Rs2
SUB 4'b0001 SUB Rd Rs1 Rs2 Rd = Rs1 - Rs2
OR 4'b0010 OR Rd Rs1 Rs2 Rd = Rs1 OR Rs2
AND 4'b0011 AND Rd Rs1 Rs2 Rd = Rs1 AND Rs2
LOADC 4'b0100 LOADC Rd, const Rd = const

Pentru instrucțiunile prezentate mai sus, vom avea următoarele moduri de codare (de împărțire a informației în cadrul instrucțiunii):

Registrele:

Cele 16 registre vor fi codate cu un număr egal cu indexul său. De exemplu, R0 va avea codul 4'b000, R1 va avea codul 4'b0001 și așa mai departe.


Considerând cele spuse mai sus, vom avea următoarele exemple de instrucțiuni:


Având toate informațiile de mai sus, descrieți o memorie ROM cu adresă pe 4 biți și locații pe 16 biți, care să conțină următorul program, începând cu adresa 0:

LOADC R0, 2h
LOADC R1, 5h
LOADC R2, 7h
ADD R3, R1, R0
SUB R4, R2, R1
OR R5, R3, R4

Exercițiul 3

Proiectați un Codor Cezar cu cheie fixă sub forma unei memorii ROM.

Un Codor Cezar preia un mesaj și codează independent fiecare literă a acestuia pentru a obține un mesaj cifrat. El se folosește de o cheie fixă care se adună la fiecare literă a mesajului. De exemplu, pentru cheia "+3": a->d; b->e; c->f;...; y->b; z->c. Observați că atunci când alfabetul a ajuns la capăt, se reia de la început. Pentru decodare, se va folosi procedura inversă: pentru fiecare literă, vom aplica cheia cu semn invers (în exemplul nostru, cheia de decodare este "-3").

Observații, sfaturi, task-uri suplimentare:

  • Codorul va putea prelucra doar litere mici și spațiu gol (acesta rămâne neschimbat).
  • Cel mai usor mod de a scrie această funcționalitate este folosind un bloc de tip case.
  • Pentru testare, se pot folosi în testbench variabile de tip reg pe 8 biți, astfel:
reg [7:0] litera;
litera = "A";
  • Avansat: În SystemVerilog există și tipul de date string, ceea ce permite parcurgerea cu un for a mesajului. În Verilog clasic, astfel de parcurgeri sunt posibile, dar sintaxa este mai complicată.
  • Se va folosi codul ASCII pentru valori. Pentru o ușoara vizualizare a datelor în simulare, se poate schimba radix-ul în ASCII.
  • Avansat: Pentru a testa automat codul, se poate instanția încă un modul în testbench cu aceeași funcționalitate pe post de "golden model". Acest modul nu trebuie să fie sintetizabil și atunci sintaxa permite mai multă libertate (cum ar fi operația de modulo "%", utilă pentru testare, dar care determină un circuit complicat dacă ar fi sintetizată). Ieșirile celor două module din testbench sunt apoi comparate pentru a se verifica că ambele implementări oferă același rezultat.
  • Implementați și Decodorul Cezar și testați Codorul împreună cu acesta. Aveți grijă ca cele două module să aibă aceeași cheie.

Anexa1