În acest laborator, vom învăța despre tipurile de descriere a modulelor în Verilog. Acestea ne ajută să creăm module într-un mod cât mai ușor și eficient.
Comportamentul unui modul poate fi descris în trei moduri:
Descrierea unui modul la nivel structural se face folosind primitive și module. Acest tip de descriere l-am folosit in laboratorul precedent și ne ajută să specificăm structura internă a modulului (elemente de circuit și legăturile dintre ele).
Acest tip de descriere specifică relațiile dintre intrări si ieșiri sub forma unor expresii.
Atribuirile continue (assign
) reprezintă relații directe între semnalele. Ele urmează câteva reguli:
wire
.wire
și reg
.initial
sau always
wire a; reg b; wire c; assign c = ~(a & b);
Operatorii pe care îi putem folosi în Verilog sunt:
-
(schimbare semn - complementul față de 2)+
, -
, *
, /
, %
(modulo)!
(negare logică - orice nenul devine 0, 0 devine 1)&&
(și logic), ||
(sau logic)>
, <
, >=
, <=
, ==
, !=
~
(negare pe biți - complementul față de 1)&
(și pe biți), |
(sau pe biți), ^
(xor pe biți), ~^
sau ^~
(exclusive nor - xnor) {<var0>, …, <varn>}
(prin concatenarea unei variabile a
, pe 3 biți, cu o variabilă b
, pe 4 biți, se obtine o variabilă pe 7 biti ai cărei primi 3 biți sunt cei din a
iar următorii 4 din b
)<<
>>
(<cond>) ? <expr_true> : <expr_false>;
(evaluează condiția iar, dacă ea este adevărată, returnează valoarea primei expresii, altfel returnează valoarea celei de-a doua expresii)Folosim acest tip de descriere când dorim să specificăm funcționalitatea modulului într-un mod algoritmic.
Blocurile initial
și always
marchează secțiunile de cod procedural ale modulului, iar în interiorul lor se pot folosi construcții de control similare celor din limbajele procedurale.
Un bloc initial
este un bloc ce va fi executat o singură dată și se folosește frecvent pentru inițializări sau resetarea circuitului.
initial begin A = 8'b01010101; // Atribuim registrului A o valoare binara ('b) pe 8 biti. B = {A[0:3], 4’b0000}; // Atribuim registrului B concatenarea intre primii 4 biti // ai registrului A si 4 biti de 0. C = 8'h4D; // Registrul C primeste o valoare in hexazecimal ('h) pe 8 biti. end
initial
poate să nu fie sintetizabil. Pentru a inițializa registrele cu anumite valori este recomandat să folosiți o linie explicită de reset.
Un bloc always
este un bloc ce va fi executat continuu, ca o buclă infinită. El poate fi executat încontinuu sau la apariția unui eveniment.
input clk; reg a; reg b; always @(posedge clk) begin a <= b; end
Construcția @(…)
definiște lista de sensizitivitate a blocului always
respectiv. Dacă ea lipsește, atunci blocul se va rula încontinuu. Dacă ea conține vreun semnal atunci blocul va rula doar la apariția unei schimbări a acelui semnal. Fiecare semnal poate fi prefixat cu posedge
sau negedge
, pentru a specifica execuția doar la fronturile pozitive sau negative ale semnalului.
always begin ... end // Se va executa încontinuu. always @(a) begin ... end // Se va executa la orice tranzitie a semnalului a. always @(posedge a) begin ... end // Se va executa la orice tranzitie pozitiva a semnalului a. always @(negedge a) begin ... end // Se va executa la orice tranzitie negativa a semnalului a. always @(a or b) begin ... end // Se va executa la orice tranzitie a semnalului a sau b. always @(*) begin b = a; end // Se va executa la orice tranzitie a oricarui semnal care poate // influenta rezultatul blocului (orice semnal citit in interiorul // blocului). In acest caz, cum o schimbare a semnalului a ar duce la // schimbarea valorii lui b, blocul se va executa pentru orice // tranzitie a semnalului a.
Construcțiile de control sunt utilizate în secțiunile procedurale de cod, adică în cadrul blocurilor initial
și always
, pentru a descrie comportamentul circuitului. Ele sunt:
1. Condiționale:
if
:if (sig == 0) begin a = 2; end else begin a = 1; end
case
:case (sig) 1’b0: a = 2; 1’b1: a = 1; default: a = 0; endcase
2. Bucle:
for
for (i = 0; i < 10; i = i + 1) begin a = a + 5; end
while
i = 0; while (i < 10) begin a = a + 5; i = i + 1; end
repeat
repeat (10) begin a = a + 5; end
3. Sincronizare:
#10 a = a + 1; // Specifica o intarziere de 10 unitati de timp de simulare inainte de a executa incrementarea. a = a + 1; #20; // Specifica o intarziere de 20 unitati de timp de simulare intre incrementari. a = a + 1; #15 // Asteptam 15 unitati de timp a = a + 1; // Incrementez a b = b + 1; // Incrementez b
4. Eveniment: execuția unei instrucțiuni este determinată de apariția unui eveniment (este de obicei folosit împreună cu blocul always
).
@r begin a = b & c; // Instrucțiunea se executa doar la modificarea variabilei r. end
5. Așteptare: instrucțiunea wait
permite întârzierea unei alte instrucțiuni sau a unui bloc până ce condiția specificată devine adevarată.
wait (a == 3) begin a = b & c; end
Diferenta dintre wait
si @r
este explicata aici.
În cadrul blocurilor initial
și always
atribuirile pot fi de două feluri, cu semantici diferite:
=
- sunt folosite pentru descrierea logicii combinaționale (porți logice). Atribuirile blocante sunt interpretate ca executându-se secvențial, semantica fiind identică cu cea din limbajele de programare procedurale.// Initial a = 0 si b = 1. a = b; // a ia valoarea lui b. b = a; // b ia valoarea lui a. // Final a = 1 si b = 1.
* Atribuiri non-blocante: <=
- sunt folosite pentru descrierea logicii secvențiale. Atribuirile non-blocante sunt interpretate ca executându-se în paralel, procesul având doi pași:
// Initial a = 0 si b = 1. a <= b; // Valoarea lui b este evaluata, dar nu atribuita lui a. b <= a; // Valoarea lui a este evaluata, dar nu atribuita lui b. // Lui a si b le sunt atribuite valorile in acelasi timp. // Final a = 1 si b = 0.
În cadrul blocurilor always
folosite pentru descrierea logicii secvențiale trebuie folosite doar atribuiri non-blocante. Pentru a implementa aceast tip de circuite logice, se folosesc blocuri always ce se execută în funcție de semnalul de ceas.
always @(posedge clk) begin a <= b; end
În cadrul blocurilor always
ce descriu logică combinațională se pot folosi doar atribuiri blocante. Blocurile pentru logica combinațională se execută la schimbarea unuia dintre semnalele citite în cadrul acestora.
always @(B or C) begin // ASA NU ! a = b & c; // B != b și C != c end
*
indică toate semnalele care vor fi citite în cadrul blocului.
always @(*) begin a = b & c; end
În Verilog putem declara variabile de tipul reg
(registru) si wire
(fir). Atunci când se fac atribuiri avem următoarele restricții:
wire
nu putem face decât atribuiri continue (assign). Acestea trebuie să fie în afara blocurilor always
sau initial
. Valoarea atribuită poate fi o constantă, o expresie, valoarea unui fir sau a unui registru.reg
nu putem face decât atribuiri blocante/non-blocante. Acestea trebuie să fie în interiorul unui bloc initial
sau always
. Valoarea atribuită poate fi o constantă, o expresie, valoarea unui fir sau a unui registru.Task 0 (tutorial). Descărcați scheletul de laborator. Observați implementarea porții not.
Task 1 (4p). Implementați o poartă XOR și testați comportamentul ei prin simulare.
Task 2 (4p). Implementați un demultiplexor 1:4 și testați comportamentul lui prin simulare.
Task 3 (2p). Implementați un divizor de ceas cu factor de divizare de 1:2 și testați comportamentul lui prin simulare.
Un divizor de ceas este un modul ce primește ca intrare un semnal de ceas și are ca ieșire un alt semnal de ceas a cărui frecvență, în cazul nostru, trebuie să fie de două ori mai mică decât a semnalului de intrare. Folosiți orice tip de descriere a modulului doriți.
reg clk; always begin #5 clk = ~clk; end initial begin clk = 0; end