În acest laborator vom proiecta o unitate fundamentală a oricarui procesor: Unitatea Aritmetică Logică (UAL, în literatura de specialitate Arithmetic Logic Unit sau ALU).
Unitatea aritmetică logică se ocupă de aproape toate calculele numerice de care are nevoie un procesor. În ciclul de prelucrare a instrucţiunilor, în diverse etape procesorul are nevoie de rezultatele unor calcule, fie că sunt solicitate de o intrucţiune explicită cum este add din limbajul de asamblare, fie dintr-un motiv intern (de exemplu adresa absolută a unei date se calculează în funcţie de segment şi offset, ceea ce implică nevoia unor calcule).
O UAL poate fi proiectată să execute, în principiu, orice operație. Totuși, cu cât operațiile devin mai complexe UAL devine mai scumpă, ocupă mai mult loc și disipă mai multă căldură. Operațiile care sunt, în general, suportate de toate UAL sunt:
Există întotdeauna un compromis pe care inginerii trebuie să îl facă la proiectarea unei UAL. Ideea este că o instrucţiune poate:
Procesoarele moderne folosesc întotdeauna prima variantă pentru instrucţiuni simple şi diverse implementări ale variantei a doua pentru operaţii de complexitate medie şi ridicată.
De asemenea, o UAL trebuie să fie în stare la orice moment să ruleze operaţia corespunzătoare la fiecare comandă din partea procesorului. Pentru aceasta fiecare operație are un cod al operației implementat în hardware, astfel încât la o instrucţiune add să intre în funcţiune modulul pentru adunare, la o instrucţiune and să intre în funcţiune poarta AND, etc.
În urma calculelor făcute de UAL pot apărea anumite situații, precum overflow la adunare/scădere, care este util să fie semnalate în cadrul sistemului. În acest scop procesoarele au un registru de status în care prin setarea unui bit anume este semnalat un anumit eveniment. Cel mai adesea, informațiile din registrul de status (sau Status Register) sunt folosite pentru a modifica fluxul de execuție al programului pe baza instrucțiunilor condiționale.
Spre exemplu, procesorul calculează A + B și dorește să sară la o anumită etichetă dacă rezultatul este 0. UAL va calcula rezultatul lui A + B și va seta bitul Z (Zero) din Status Register dacă rezultatul adunării este 0. Instrucțiunea de salt JZ (jump zero) verifică bitul Z din Status Register și sare la eticheta data dacă bitul este 1, altfel execută instrucțiunea imediat următoare.
În cadrul x86 Status Register se numește FLAGS și are 16 biți. El are o extensie de 32 de biți numită EFLAGS. Biții din cele două registre au următoarea semnificație:
Un exemplu mai simplu este registrul de status pentru Atmega324 (AVR în general), care se numește SREG și are 8 biți:
În ambele registre de status prezentate mai sus se regăsesc o serie de biți de interes pentru UAL:
CF
sau C
(carry flag) este setat pe 1 când operația executată pe UAL are carry. ZF
sau Z
(zero flag) este setat pe 1 când rezultatul operației de pe UAL este 0. N
(negative) este setat pe 1 când rezultatul operației de pe UAL este un număr negativ (bitul de semn al rezultatului este 1). OF
sau V
(overflow flag) este setat pe 1 când operația executată pe UAL produce overflow, mai exact există 2 cazuri: Înmulțirea a două numere cu semn pe UAL este o operație complexă și costisitoare. Ținând cont că operațiile de deplasare a biților sunt mai rapide ca cele de adunare, Andrew Booth a venit cu o propunere de algoritm pentru înmulțirea numerelor cu semn care acum îi poartă numele.
Ideea de la care pleacă algoritmul este că orice secvența continuă de 1 dintr-un număr binar poate fi rescrisă ca diferența a două numere binare:
În cazul înmulțirii a două numere, următoarele expresii sunt echivalente:
P = M x 10011 = M x (2⁴ + 2¹ + 2⁰) = M x (2⁵ - 2⁴ + 2² - 2⁰) = M << 5 - M << 4 + M << 2 - M << 0
Practic, algoritmul lui Booth se reduce la a identifica secvențele continue de biți de 1 din termenul R pentru P = M x R și a le înlocui cu o diferență. Astfel pentru o operație de înmulțire, numărul adunărilor este redus, realizându-se în schimb mai multe operații de deplasare a biților.
Dorim să calculăm produsul P = M x R unde
1. Formăm P
2. Determinăm două valori auxiliare M+ și M-
Valorile auxiliare M+ și M- vor fi adunate lui P în cadrul algoritmului. Ele vor avea tot 2 * n + 1 biți.
3. Verificăm primii 2 biți ai lui P
Verificăm primii 2 biți ai lui P (cei mai nesemnificativi biți):
4. Deplasăm aritmetic la dreaptă biții lui P cu o poziţie
5. Repetăm pașii 3 și 4 de n ori
6. Eliminăm bitul Z
Eliminăm bitul Z, adică cel mai nesemnificativ bit al lui P. Valoarea din P este rezultatul înmulțirii.
Asemenea înmulțirii, împărțirea pe UAL este o operație complexă și costisitoare. Acest algoritm reduce operația de înmulțire la operații de deplasare a biților, adunare și scădere.
Dorim să calculăm împărțirea Q / M = R[3:0] rest R[7:4] unde:
1. Initializam R
R = biții cei mai nesemnificativi (din dreapta) ai lui R vor lua valoarea lui Q, restul vor fi 0
2. Determinăm două valori auxiliare M- si M+
Valorile auxiliare M- și M+ vor fi adunate lui R in cadrul algoritmului (vor avea 8 biți)
3. Verificăm semnul lui R
Verificăm primul bit al lui R (cel mai semnificativ bit):
4. Verificăm semnul lui R
Verificăm primul bit al lui R (cel mai semnificativ bit) iar:
5. Repetăm pașii 3 și 4 de n (n = 4, numărul de biți) ori
6. Verificăm semnul lui R
Verificăm primul bit al lui R (cel mai semnificativ bit) o ultimă dată:
7. Extragem câtul si restul din R
AND
, OR
, NOT
, XOR
, NOR
, NAND
, etc.shift stânga
, shift dreapta
, shift circular
, etc.ADD
, SUB
, MUL
(nu toate), DIV
(nu toate).'ZF
/Z
- Zero, CF
/C
- Carry, N
- Negative, OF
/V
- Overflowshift
Task 0 (2p) - Implementați Algoritmul lui Booth pentru înmulțire în task0.v
.
Task 1 (2p) - Implementați Algoritmul Non-Restoring Division în task1.v
.
<<<
și >>>
pentru deplasarea aritmetică a biților. Valoarea bitului de fill se determină pe baza contextului (signed sau unsigned). Din acest motiv, pentru a deplasa aritmetic la dreapta un număr negativ va trebui să specificăm contextul signed
al valorii shiftate.
assign magic_shift = 4'b1010; assign logic_right_shift = (magic_shift >> 1); // 4'0101 assign logic_left_shift = (magic_shift << 1); // 4'b0100 assign arithmetic_right_shift = (magic_shift >>> 1); // 4'b0101 <- greșit pentru numere cu semn assign arithmetic_left_shift = (magic_shift <<< 1); // 4'0100 assign signed_arithmetic_right_shift = ($signed(magic_shift) >>> 1); // 4'b1101 <- corect, semnul se păstrează assign signed_arithmetic_left_shift = ($signed(magic_shift) <<< 1); // 4'b0100 <- nu este relevant aici
assign signed_arithmetic_right_shift = ($signed(magic_shift) >>> 1); // 4'b1101 <- corect, semnul se păstrează
Task 2 (6p) - Implementați un modul UAL task2.v
cu intrări pe 4 biți. Modulul va primi la intrare 2 numere (i_w_a și i_w_b) și indicatorul unei operații ce se va efectua asupra numerelor (i_w_op_sel). Ieșirea modului (o_w_result) va fi un număr pe 8 biți ce reprezintă rezultatul aplicării operației asupra numerelor i_w_a și i_w_b (Dacă rezultatul are mai puțin de 8 biți se va face padding cu zero pe cei mai semnificativi biți). Pentru valorile lui i_w_op_sel avem următoarele operații:
AND
XOR
OR
ADD
SUB
(folosind ADD)MUL
(folosind algoritmul lui Booth)DIV
(folosind algoritmul lui Booth pentru împărțire)MOD
(folosind algoritmul lui Booth pentru împărțire)
OPȚIONAL: Task 3 - Încărcați modulul task3.v
pe placa de dezvoltare din laborator.
i_w_a
, i_w_b
și i_w_op_sel
;pentru i_w_reset
;i_w_a
, i_w_b
și i_w_op_sel
;