This shows you the differences between two versions of the page.
soc:laboratoare:08 [2024/04/06 20:18] george_mircea.grosu [5. Algoritmul lui Booth pentru împărțire] |
soc:laboratoare:08 [2025/05/14 18:56] (current) mihai_catalin.stan |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laboratorul 8 - Unitatea aritmetică logică ===== | + | ===== Laboratorul 8 - Proiectul SOC-1 ===== |
- | ==== 1. Scopul laboratorului ==== | + | În acest laborator vom pune în aplicare cunoștințele despre calculatorul învățat la curs - SAP-1 - și vom implementa propriul nostru sistem de calcul pe care îl vom denumi SOC-1. |
- | În acest laborator vom proiecta o unitate fundamentală a oricarui procesor: Unitatea Aritmetică Logică (UAL, în literatura de specialitate Arithmetic Logic Unit sau ALU). | + | ==== Module ==== |
- | {{ :soc:laboratoare:08:ual.jpg?300 | }} | + | Scheletul de laborator cuprinde următoarele module: |
+ | * Clock - semnalul de ceas | ||
+ | * Controller | ||
+ | * Instruction Register | ||
+ | * Program Counter | ||
+ | * Memory (ROM) | ||
+ | * Registers | ||
+ | * ALU | ||
+ | * Flags | ||
- | 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). | + | Pentru a vedea cum se unesc aceste module și cum interacționează între ele, avem următorul schematic: |
- | ==== 2. Cu ce se ocupă o UAL? ==== | + | {{ :soc:laboratoare:08:sap-2-block.png?750 | }} |
- | 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: | + | ==== Semnalul de ceas ==== |
- | * Operații logice: AND, OR, NOT, XOR, NOR, NAND, etc. | + | |
- | * Operații de shift: shift stânga, shift dreapta, shift circular, etc. | + | |
- | * Operații aritmetice: adunare, scădere, înmulțire (nu toate), împărțire (nu toate). | + | |
- | Există întotdeauna un compromis pe care inginerii trebuie să îl facă la proiectarea unei UAL. Ideea este că o instrucţiune poate: | + | Toate modulele din modelul nostru de calculator sunt dependente de semnalul de ceas. Din acest motiv, am implementat un modul cât de simplist se poate pentru a simula acest semnal. |
- | * să fie executată //într-un singur ciclu de ceas//, ceea ce ar fi //rapid//, dar foarte //costisitor// din punctul de vedere al circuitului fizic (complex), cât şi din alte considerente (de exemplu, disiparea de căldură), | + | |
- | * să fie executată //în mai mulţi paşi//, ceea ce e evident mai //lent//, dar //uşor de proiectat şi de adăugat funcţionalităţi unui acelaşi circuit//, | + | |
- | * să nu fie executată ca o operaţie de sine stătătoare în UAL, ci implementată prin software pe baza operaţiilor deja suportate de UAL, ceea ce ar fi //foarte lent//, dar dă //o oarecare flexibilitate// celor care scriu programe la nivel mai înalt. | + | |
- | 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ă. | + | <code verilog> |
+ | module clock( | ||
+ | input hlt, | ||
+ | input clk_in, | ||
+ | output clk_out); | ||
- | 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. | + | assign clk_out = (hlt) ? 1'b0 : clk_in; |
- | ==== 3. Status Register ==== | + | endmodule |
+ | </code> | ||
- | Î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. | + | De regulă, acest modul va trimite spre ieșire semnalul de ceas de intrare, cu excepția momentului primirii unui semnal de halt (oprire a execuției programului - se setează la 0). |
- | 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. | + | <note tip> |
- | + | Cea mai simplă metodă de a opri un computer din a mai executa instrucțiuni este oprirea ceasului acestuia. | |
- | Î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: | + | </note> |
- | {{:soc:laboratoare:08:eflags.png?|x86 Status Register}} | + | |
- | Un exemplu mai simplu este registrul de status pentru **Atmega324** (AVR în general), care se numește **SREG** și are 8 biți: | + | ==== Registre ==== |
- | {{:soc:laboratoare:08:sreg_avr.png?700|SREG }} | + | Registrele sunt o metodă simplă, scumpă și foarte rapidă de a stoca date de mare importanță cu care procesorul urmează să lucreze sau cu care abia a lucrat. |
- | În ambele registre de status prezentate mai sus se regăsesc o serie de **biți de interes pentru UAL**: | + | SOC-1 va folosi 3 registre identice, denumite simplu: A, B, C. Acestea vor fi reprezentate pe 8 biți (foarte mici comparativ cu registrele din calculatoarele moderne care au 32 sau 64 de biți, dar suficient pentru procesorul nostru), și în funcție de valorile semnalelor de intrare vor suporta operațiile de: |
- | * **''CF'' sau ''C'' (carry flag)** este setat pe 1 când operația executată pe UAL are carry. | + | * Reset - Dat de semnalul de rst |
- | * **''ZF'' sau ''Z'' (zero flag)** este setat pe 1 când rezultatul operației de pe UAL este 0. | + | * Load de pe bus - Dat de semnalul de load |
- | * **''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). | + | * Incrementare - Dat de semnalul de ''inc'' |
- | * **''OF'' sau ''V'' (overflow flag)** este setat pe 1 când operația executată pe UAL produce overflow, mai exact există 2 cazuri: | + | * Decrementare - Dat de semnalul de ''dec'' |
- | * dacă adunăm 2 numere pozitive și bitul de semn al rezultatului este 1 (rezultatul este număr negativ) | + | |
- | * dacă adunăm 2 numere negative și bitul de semn al rezultatului este 0 (rezultatul este număr pozitiv) | + | |
- | ==== 4. Algoritmul lui Booth pentru înmulțire ==== | + | <code verilog> |
+ | always @(posedge clk, posedge rst) begin | ||
+ | if (rst) begin | ||
+ | data <= 8'b0; | ||
+ | end else if (load) begin | ||
+ | data <= bus[7:0]; | ||
+ | end else if (inc) begin | ||
+ | data <= data + 1; | ||
+ | end else if (dec) begin | ||
+ | data <= data - 1; | ||
+ | end | ||
+ | end | ||
+ | </code> | ||
- | Înmulțirea a două numere cu semn pe UAL este o operație complexă și costisitoare. Ținând cont că operațiile de shift 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. | + | ==== Program Counter (PC) ==== |
- | === 4.1. Cum funcționează algoritmul? === | + | Pentru a executa instrucțiunile (o serie de biți care codifică o anumită acțiune) stocate în memoria ROM, procesorul SOC-1 utilizează Program Counter-ul (PC), o componentă esențială care reține adresa următoarei instrucțiuni ce urmează a fi executată. Instrucțiunile sunt stocate secvențial, începând de la adresa 0, fiecare instrucțiune fiind codificată într-un singur octet. |
- | 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: | + | PC-ul asigură parcurgerea acestor instrucțiuni în ordine, incrementând automat adresa la fiecare front de ceas pozitiv, atunci când semnalul inc este activ. În funcție de semnalele de control (inc, rst și load), comportamentul PC-ului este următorul: |
- | {{ :soc:laboratoare:08:booth_binary.png?direct&350 |}} | + | * Dacă ''inc'' este activ, valoarea PC-ului se incrementează cu 1 |
- | + | * Dacă ''rst'' este activ, PC-ul se resetează la 0 | |
- | În cazul înmulțirii a două numere, următoarele expresii sunt echivalente: | + | * Dacă ''load'' este activ, PC-ul încarcă o valoare de adresă de pe magistrala de date |
- | <code> | + | <code verilog> |
- | P = M x 10011 = M x (2⁴ + 2¹ + 2⁰) | + | always @(posedge clk, posedge rst) begin |
- | = M x (2⁵ - 2⁴ + 2² - 2⁰) | + | if (rst) begin |
- | = M << 5 - M << 4 + M << 2 - M << 0 | + | pc <= 16'b0; |
+ | end else if (load) begin | ||
+ | pc <= bus; | ||
+ | end else if (inc) begin | ||
+ | pc <= pc + 1; | ||
+ | end | ||
+ | end | ||
</code> | </code> | ||
- | 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 shiftari. | + | ==== Instruction Register (IR) ==== |
- | + | ||
- | Grafic, algoritmul face următoarea transformare: | + | |
- | {{ :soc:laboratoare:08:booth_multiplication_example.png?direct&600 |}} | + | |
- | === 4.2. Prezentarea algoritmului === | + | În timp ce PC-ul se ocupă de stocarea adresei următoarei instrucțiuni, IR-ul încarcă din memorie următoarea instrucțiune de executat sau o resetează în cazul unui semnal de rst. |
- | Dorim să calculăm produsul **P = M x R** unde | + | ==== Magistrala ==== |
- | * **M, R** sunt numere cu semn reprezentate pe **n biți** | + | |
- | * **P** este un număr cu semn reprezentat pe **2 * n** biți | + | |
- | + | ||
- | **1. Formăm P** | + | |
- | + | ||
- | * P = biții cei mai nesemnificativi (din dreapta) ai lui P vor lua valoarea lui R, restul vor fi 0. | + | |
- | * Adăugăm un bit de '0' în dreapta lui P (LSB). Numim acest bit Z. | + | |
- | <note undefined> | + | Magistrala este modul în care modulele își transmit date, respectiv instrucțiuni între acestea. În timp ce un modul pune date pe magistrală, cel căruia îi sunt dedicate datele începe să citească de pe aceasta. Magistrala implementată în proiectul SOC-1 fiind implementat pe FPGA folosește niște semnale pentru a alege modulul de pe care citește datele. Spre exemplu, magistrala va primi semnalul de instruction register enable (ir_en) pentru a citi date de la output-ul ir-ului (ir_out). |
- | P are acum 2 * n + 1 biți. El va fi folosit astfel în cadrul algoritmului, iar bitul extra va fi eliminat la sfârșit pentru a obține rezultatul final. | + | |
- | </note> | + | |
- | **2. Determinăm două valori auxiliare M<sub>+</sub> și M<sub>-</sub>** | + | <code verilog> |
+ | always @(*) begin | ||
+ | // [...] | ||
+ | end else if (pc_en) begin | ||
+ | bus = pc_out; | ||
+ | // [...] | ||
+ | end else begin | ||
+ | bus = 16'b0; | ||
+ | end | ||
+ | end | ||
+ | </code> | ||
- | Valorile auxiliare M<sub>+</sub> și M<sub>-</sub> vor fi adunate lui P în cadrul algoritmului. Ele vor avea tot 2 * n + 1 biți. | + | ==== Memorie ==== |
- | * M<sub>+</sub> = biții cei mai semnificativi (din stânga) vor lua valoarea lui M, restul vor fi 0. | + | |
- | * M<sub>-</sub> = biții cei mai semnificativi (din stânga) vor lua valoarea lui -M, restul vor fi 0. | + | |
- | + | ||
- | **3. Verificăm primii 2 biți ai lui P** | + | |
- | Verificăm primii 2 biți ai lui P (cei mai nesemnificativi biți): | + | Memoria modelului nostru de sistem de calcul este de 64K (65536 bytes sau 524288 bits), având adrese pe 16 biți într-un spațiu de adrese ce se extinde de la 0x0000 până la 0xFFFF. |
- | - dacă sunt 01 => P = P + M<sub>+</sub> (ignorăm overflow) | + | |
- | - dacă sunt 10 => P = P + M<sub>-</sub> (ignorăm overflow) | + | |
- | - dacă sunt 00 => nu facem nimic | + | |
- | - dacă sunt 11 => nu facem nimic | + | |
- | <spoiler De ce?> | + | În comportamentul acestui modul puteți observa două registre importante: MAR (Memory Address Register) și MDR (Memory Data Register), ce vor reține adresa din memorie de la care citim/scriem, respectiv datele pe care le scriem în memorie sau citim din memorie. |
- | Tabelul de mai jos surprinde motivul pentru care algoritmul verifică biții LSB ai lui P: | + | |
- | * Începutul unei secvențe înseamnă că trebuie să scădem -M * 2<sup>i</sup>, unde i e indexul bitului de început secvenței de 1 | + | |
- | * Sfârșitul unei secvențe înseamnă că trebuie să adunăm M * 2<sup>j</sup>, unde j e indexul bitului de sfârșit secvenței de 1 | + | |
- | ^ Bitul curent ^ Bitul din dreapta ^ Explicație ^ Exemplu ^ | + | Un ultim aspect important este faptul că memoria permite apelul de funcții - dar nu și recursivitate - datorită ultimelor două adrese din memorie 0xFFFF si 0xFFFE ce pot memora valoarea PC-ului la intrarea într-o funcție (call) și restituirea acestuia la întoarcerea dintr-un astfel de apel (ret). |
- | | 1 | 0 | Începutul unei secvențe continue de 1 | 000111**10**00 | | + | |
- | | 1 | 1 | Interiorul unei secvențe continue de 1 | 00011**11**000 | | + | |
- | | 0 | 1 | Sfârșitul unei secvențe continue de 1 | 00**01**111000 | | + | |
- | | 0 | 0 | Interiorul unei secvențe continue de 0 | **00**01111000 | | + | |
- | + | ||
- | De asemenea, observam ca motivul pentru care lui P i se adauga un bit Z pe pozitia LSB este ca sa putem verifica inceputul unei secvente continue de 1 ce porneste de pe indexul 0 (ea poate fi identificata corect doar prin secventa de tip "10"). | + | |
- | </spoiler> | + | |
- | + | ||
- | **4. Shiftam aritmetic la dreaptă pe P cu o poziţie** | + | |
- | <note important> | + | <code verilog> |
- | Atenție la [[https://en.wikipedia.org/wiki/Arithmetic_shift | Arithemtic Shift]] vs [[https://en.wikipedia.org/wiki/Logical_shift | Logical Shift]]. | + | always @(posedge clk) begin |
- | </note> | + | if (mar_loadh) begin |
+ | mar[15:8] <= bus[15:8]; | ||
+ | end | ||
- | **5. Repetăm pașii 3 și 4 de n ori** | + | if (mar_loadl) begin |
- | + | mar[7:0] <= bus[7:0]; | |
- | **6. Eliminăm bitul Z** | + | end |
- | Eliminăm bitul Z, adică cel mai nesemnificativ bit al lui P. Valoarea din P este rezultatul înmulțirii. | + | if (mdr_load) begin |
+ | mdr[7:0] <= bus[7:0]; | ||
+ | end | ||
- | {{ :soc:laboratoare:08:booth_multiplication.png?direct450 |}} | + | if (ret) begin |
+ | mdr[15:8] <= ram[16'hFFFE]; | ||
+ | mdr[7:0] <= ram[16'hFFFF]; | ||
+ | end else if (call) begin | ||
+ | ram[16'hFFFE] <= bus[15:8]; | ||
+ | ram[16'hFFFF] <= bus[7:0]; | ||
+ | end else if (ram_enh) begin | ||
+ | mdr[15:8] <= ram[mar]; | ||
+ | end else if (ram_enl) begin | ||
+ | mdr[7:0] <= ram[mar]; | ||
+ | end else if (ram_load) begin | ||
+ | ram[mar] <= mdr; | ||
+ | end | ||
+ | end | ||
+ | </code> | ||
- | === 4.3. Exemplu înmulțire folosind algoritmul lui Booth === | + | ==== Controller ==== |
- | <spoiler Exemplu înmulțire folosind algoritmul lui Booth> | + | |
- | Fie M = 00010 (2), -M = ~M + 1 = 11110 (-2) și R = 10011 (-13). | + | |
- | + | ||
- | **Pasul 1** | + | |
- | Îl construim pe P prin concatenarea valorilor n{1'b0}, R și Z. | + | Unitatea de control (UC): decodifică instrucțiunea curentă și pe baza acesteia și a stării interne, generează semnale de control pentru toate resursele procesorului (e.g. registrele de intrare/ieșire și operația aritmetică necesare unei instrucțiuni) și coordonează activarea acestor resurse. |
- | <code> | + | În SOC-1, fiecare instrucțiune este reprezentată pe 8 biți, iar execuția acesteia se desfășoară în mai multe etape. |
- | P = {(00000), (R), (Z)} = 00000 10011 0 | + | |
- | </code> | + | |
- | + | ||
- | **Pasul 2** | + | |
- | Formăm M<sub>+</sub> și M<sub>-</sub>. | + | Etapele executării unei instrucțiuni |
+ | Orice procesor poate avea un ciclu de prelucrare a instrucțiunilor diferit, în funcție de ISA-ul pe care îl implementează, însă toate vor urma următoarea structură: | ||
+ | * IF (Instruction Fetch) - următoarea instrucțiune este adusă din memorie de la adresa către care pointează registrul Program Counter (PC) și este stocată în registrul Instruction Register (IR). PC este apoi incrementat pentru a pointa către următoarea instrucțiune de încărcat. | ||
+ | * ID (Instruction Decode) - instrucțiunea din IR este decodificată. | ||
+ | * EX (Execute) - execuția efectivă a instrucțiunii. Aceasta etapă variază în funcție de tipul instrucțiunii curente. | ||
+ | * MEM (Memory Access) - ciclu folosit în cazul în care instrucțiunea accesează memoria. | ||
+ | * WB (Write-Back) - scrierea noilor valori în registre. | ||
- | <code> | + | Vom studia mai multe despre acestea în Laboratorul 9. |
- | M<sub>+</sub> = { (M), (00000), (Z)} = 00010 00000 0 | + | |
- | M<sub>-</sub> = {(-M), (00000), (Z)} = 11110 00000 0 | + | |
- | </code> | + | |
- | + | ||
- | **Pașii 3 și 4** | + | |
- | Pașii 3 și 4 vor fi efectuați de **n = 5** ori: | + | Adresa în ROM este formată prin concatenarea codului instrucțiunii (8 biți) cu etapa curentă a execuției (4 biți), rezultând o adresă pe 12 biți. Conținutul de la acea adresă este un cuvânt de control, care indică exact ce semnale trebuie activate pentru instrucțiunea respectivă, în etapa respectivă. |
- | - P = 00000 1001//**1 0**// | + | |
- | * Ultimii 2 biți sunt 10 (am identificat începutul unei secvențe continue de biți de 1) | + | |
- | * P = P + M<sub>-</sub> = 00000 10011 0 + 11110 00000 0 = 11110 10011 0 | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 11111 01001 1 | + | |
- | - P = 11111 0100//**1 1**// | + | |
- | * Ultimii 2 biți sunt 11 (suntem în interiorul unei secvențe continue de biți de 1) => Nu modificăm P | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 11111 10100 1 | + | |
- | - P = 11111 1010//**0 1**// | + | |
- | * Ultimii 2 biți sunt 01 (am identificat finalul unei secvențe continue de biți de 1) | + | |
- | * P = P + M<sub>+</sub> = 11111 10100 1 + 00010 00000 0 = 00001 10100 1 | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 00000 11010 0 | + | |
- | - P = 00000 1101//**0 0**// | + | |
- | * Ultimii 2 biți sunt 00 (suntem în interiorul unei secvențe continue de biți de 0) => Nu modificăm P | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 00000 01101 0 | + | |
- | - P = 00000 0110//**1 0**// | + | |
- | * Ultimii 2 biți sunt 10 (am identificat începutul unei secvențe continue de biți de 1) | + | |
- | * P = P + M<sub>-</sub> = 00000 01101 0 + 11110 00000 0 = 11110 01101 0 | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 11111 00110 1 | + | |
- | + | ||
- | **Pasul 5** | + | |
- | Eliminăm bitul Z, iar P = 11111 00110 (-26) este rezultatul. | + | ==== UAL ==== |
- | </spoiler> | + | |
- | <hidden> | + | Unitatea Aritmetică Logică (UAL sau ALU în engleză) este modulul ce definește potențialul unui sistem de calcul. Calculatorul SAP-1 despre care ați învățat la curs era capabil să efectueze operații de adunare și scădere. Spre deosebire de acesta, SOC-1 este capabil să efectueze 8 tipuri de operații diferite: |
- | Fie M = 0011 (3) (-M = 1101 (-3)) și R = 1100 (-4). | + | * Adunare (ADD) |
- | + | * Scădere (SUB) | |
- | **Pasul 1** | + | * ȘI logic (AND) |
+ | * SAU logic (OR) | ||
+ | * SAU Exclusiv (XOR) | ||
+ | * Complement (CMA) | ||
+ | * Shiftare la stânga (RAL) | ||
+ | * Shiftare la dreapta (RAR) | ||
- | Îl construim pe P prin concatenarea valorilor R și Z. Restul biților vor fi padding cu 0. Practic adunăm valoarea 2R la P. | + | {{ :soc:laboratoare:08:ual_sap2.png?300 | }} |
- | <code> | + | De menționat este faptul că shiftările se vor face aritmetic, nu logic. Spre exemplu pentru un shift la stânga, dacă ultimul bit înainte de shift era 1 acesta nu va fi pierdut, ci va deveni primul bit. Semnalul de op codifică operația pe care calculatorul trebuie să o efectueze, operații pe care voi va trebui să le implementați. |
- | P = {(0000), (R), (Z)} = 0000 1100 0 | + | |
- | </code> | + | |
- | + | ||
- | **Pasul 2** | + | |
- | Formăm A și S. | + | De asemenea, în cele două blocuri always se poate observa faptul că registrul intern al UAL-ului este updatat sincron cu frontul pozitiv al semnalului de ceas, în timp ce operațiile efectuate de acesta sunt executate asincron. |
- | <code> | + | ==== Flags ==== |
- | A = {(M), (0000), (Z)} = 0011 0000 0 | + | |
- | S = {(-M), (0000), (Z)} = 1101 0000 0 | + | |
- | </code> | + | |
- | + | ||
- | **Pașii 3 și 4** | + | |
- | Pașii 3 și 4 vor fi efectuați de **n = 4** ori: | + | Ultimul modul necesar pentru ca sistemul de calcul făcut să funcționeze normal este registrul de flag-uri, un registru special pentru a memora diverse caracteristici ale operației anterioare. Un astfel de exemplu este Zero Flag, un bit ce este setat pe 1 în registrul nostru în momentul în care rezultatul ultimei operații efectuate a fost 0. |
- | - P = 0000 110//**0 0**// | + | |
- | * Ultimii 2 biți sunt 00 (ne aflăm într-o secvența continuă de biți de 0) => Nu modificăm P | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 0000 0110 0 | + | |
- | - P = 0000 011//**0 0**// | + | |
- | * Ultimii 2 biți sunt 00 => Nu modificăm P | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 0000 0011 0 | + | |
- | - P = 0000 001//**1 0**// | + | |
- | * Ultimii 2 biți sunt 10 (am identificat începutul unei secvențe continue de biți de 1) => P = P + S = 1101 0011 0 | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 1110 1001 1 | + | |
- | - P = 1110 100//**1 1**// | + | |
- | * Ultimii 2 biți sunt 11 (suntem în interiorul unei secvențe continue de biți de 1) => Nu modificăm P | + | |
- | * Îl shiftam aritmetic la dreapta pe P => P = 1111 0100 1 | + | |
- | + | ||
- | **Pasul 5** | + | |
- | Eliminăm bitul Z, iar P = 1111 0100 (-12) este rezultatul. | + | Proiectul SOC-1 are doar două astfel de flag-uri la momentul curent: Zero Flag și Sign Flag, ale căror valori sunt setate la finalul fiecărui ciclu de ceas (deci pe frontul negativ al semnalului de ceas). Modul în care sunt setate poate fi examinat în secvența de cod de mai jos: |
- | </hidden> | + | |
- | ==== 5. Algoritmul lui Booth pentru împărțire ==== | + | <code verilog> |
- | + | reg[2:0] flags; | |
- | Asemenea înmulțirii, împărțirea pe UAL este o operație complexă și costisitoare. Întrucât operațiile de shift sunt mai rapide ca cele de scădere, similar ca în cazul înmulțirii, Andrew Booth a creat un algoritm pentru împartirea numerelor. | + | |
- | + | ||
- | === 5.1. Cum funcționează algoritmul? === | + | |
- | + | ||
- | Similar algoritmului pentru înmulțire, și în cazul împărțirii pornim de la rescrierea secvențelor continue de 1 dintr-un număr binar ca diferență a două numere puteri ale lui 2: | + | |
- | {{ :soc:laboratoare:08:booth_binary.png?direct&350 |}} | + | |
- | + | ||
- | În cazul împărțirii a două numere, următoarele expresii sunt echivalente: | + | |
- | <code> | + | always @(negedge clk, posedge rst) begin |
- | M = 5'b10011 | + | if (rst) begin |
- | {R, C} = Q / M = Q / (2⁴ + 2¹ + 2⁰) | + | flags <= 2'b0; |
- | = Q / (2⁵ - 2⁴ + 2² - 2⁰) | + | end else if (load_a) begin |
- | = Q >> 5 - Q > 4 + Q >> 2 - Q >> 0 | + | flags[FLAG_Z] <= (a == 0) ? 1'b1 : 1'b0; |
+ | flags[FLAG_S] <= (a[7] == 1) ? 1'b1 : 1'b0; | ||
+ | end else if (load_b) begin | ||
+ | flags[FLAG_Z] <= (b == 0) ? 1'b1 : 1'b0; | ||
+ | flags[FLAG_S] <= (b[7] == 1) ? 1'b1 : 1'b0; | ||
+ | end else if (load_c) begin | ||
+ | flags[FLAG_Z] <= (c == 0) ? 1'b1 : 1'b0; | ||
+ | flags[FLAG_S] <= (c[7] == 1) ? 1'b1 : 1'b0; | ||
+ | end | ||
+ | end | ||
</code> | </code> | ||
- | === 5.2 Prezentarea algoritmului === | + | <spoiler Descriere extinsa flag-uri> |
+ | * Z (Zero) - indică dacă rezultatul unei operații aritmetice este zero. | ||
+ | * C (Carry) - indică faptul că s-a realizat un transport la nivelul bitului cel mai semnificativ in cazul unei operații aritmetice. Altfel spus, a avut loc o depășire în aritmetica modulo N considerată. | ||
- | Dorim să calculăm împărțirea **Q / M = R[3:0] rest R[7:4]** unde: | + | **Explicație:** În procesorul nostru pe 8 biți, 255 + 1, deși ar trebui să aibă rezultatul 256, de fapt acesta este 0 din cauza aritmeticii modulo 256 (28). Pentru a diferenția dintre un 0 apărut real și unul cauzat de o depășire, se utilizează acest semnal de carry. |
- | * Q este deîmpărțitul | + | |
- | * M este împărțitorul | + | |
- | * R[3:0] este câtul | + | |
- | * R[7:4] este restul | + | |
- | **1. Initializam R** | + | * V (Overflow) - arată că, în cazul unei operații aritmetice cu semn, complementul față de doi al rezultatului nu ar încăpea în numărul de biți folosiți pentru a reprezenta operanzii. |
- | R = biții cei mai nesemnificativi (din dreapta) ai lui R vor lua valoarea lui Q, restul vor fi 0 | + | **Explicație:** Cu alte cuvinte, se poate întampla ca adunând două numere pozitive, să obținem unul negativ (127signed + 1signed = -128signed), dar și adunând două numere negative să obținem unul pozitiv (-128signed + (-1)signed = +127signed). Evident, rezultatul nu este corect în aceste situații, și semnalarea se face prin flag-ul de overflow. Dacă am aduna un număr pozitiv (MSB = 0) cu unul negativ (MSB = 1), atunci nu obțin overflow. |
- | **2. Determinăm două valori auxiliare M<sub>-</sub> si M<sub>+</sub>** | + | * N (Negative) - semnul rezultatului unei operații aritmetice (este pur și simplu valoarea bitului de semn al rezultatului) |
+ | * S (Sign) - este un flag unic AVR, calculat după formula S = N xor V, ce reprezintă “care ar fi trebuit să fie semnul corect al rezultatului”. | ||
- | Valorile auxiliare M<sub>-</sub> și M<sub>+</sub> vor fi adunate lui R in cadrul algoritmului (vor avea 8 biți) | + | **Explicație:** Cu alte cuvinte, dacă N == 1, dar și V == 1, înseamnă că rezultatul este negativ, dar din cauza unei depășiri S este setat în acest caz pe 0, semnalând că semnul “corect” al operației ar fi trebuit să fie pozitiv. |
+ | </spoiler> | ||
- | * M<sub>-</sub> = biții cei mai semnificativi (din stânga) vor lua valoarea lui -M, iar restul vor fi 0 | + | ==== TL;DR ==== |
- | * M<sub>+</sub> = biții cei mai semnificativi (din stânga) vor lua valoarea lui M, iar restul vor fi 0 | + | |
- | **3. Verificăm primul bit al lui R** | + | Arhitectura include următoarele module: |
+ | * Clock - semnalul de ceas | ||
+ | * Controller | ||
+ | * Instruction Register | ||
+ | * Program Counter | ||
+ | * Memory (ROM) | ||
+ | * Registers | ||
+ | * ALU | ||
+ | * Flags | ||
- | Verificăm primul bit al lui R (cel mai semnificativ bit): | + | == Clock: == |
- | * dacă este 0 => shiftăm aritmetic la stânga pe R cu o poziție și adaugăm pe M<sub>-</sub> la R, R = R + M<sub>-</sub> | + | Generează semnalul de ceas pentru sincronizarea tuturor modulelor. |
- | * dacă este 1 => shiftăm aritmetic la stânga pe R cu o poziție și adaugăm pe M<sub>+</sub> la R, R = R + M<sub>+</sub> | + | Se oprește la primirea semnalului hlt. |
- | **4. Verificăm primul bit al lui R** | + | == Controller / Unitatea de Control: == |
- | Verificăm primul bit al lui R (cel mai semnificativ bit) iar: | + | Decodifică instrucțiunea curentă și generează semnalele de control necesare execuției. |
+ | Folosește o memorie ROM cu adrese pe 12 biți (8 pentru instrucțiune, 4 pentru etapă) → returnează cuvântul de control. | ||
- | * dacă este 0 => setăm ultimul bit (cel mai nesemnificativ) al lui R pe 1 | + | == Registre == |
- | * dacă este 1 => setăm ultimul bit (cel mai nesemnificativ) al lui R pe 0 | + | |
- | **5. Repetăm pașii 3 și 4 de n (n = 4, numărul de biți) ori** | + | Trei registre (A, B, C) pe 8 biți – suportă reset, load, inc, dec. |
+ | Utilizate pentru stocarea temporară a datelor. | ||
- | **6. Verificam primul bit al lui R** | + | == Program Counter (PC) == |
- | Verificăm primul bit al lui R (cel mai semnificativ bit) o ultimă dată: | + | Păstrează adresa următoarei instrucțiuni. |
- | * dacă este 0 => nu modifcăm nimic | + | == Instruction Register (IR) == |
- | * dacă este 1 => adăugăm la R pe M<sub>+</sub>, R = R + plus_M | + | |
- | **7. Extragem câtul si restul din R** | + | Încarcă instrucțiunea curentă adusă din memorie. |
- | * câtul = R[3:0] | + | == Magistrala (Bus) == |
- | * restul = R[7:4] | + | |
- | {{ :soc:laboratoare:08:booth_division.png?direct&350 |}} | + | Canal comun de date între module. |
- | ==== 6. TL;DR ==== | + | |
- | * **Unitatea Aritmetică Logică** se ocupă de aproape toate calculele cerute de procesor, solicitate de o instrucțiune explicită sau necesară intern. | + | |
- | * Instrucțiuni uzuale: | + | |
- | * **Operații Logice**: ''AND'', ''OR'', ''NOT'', ''XOR'', ''NOR'', ''NAND'', etc. | + | |
- | * **Operații de shift**: ''shift stânga'', ''shift dreapta'', ''shift circular'', etc. | + | |
- | * **Operații aritmetice**: ''ADD'', ''SUB'', ''MUL'' (nu toate), ''DIV'' (nu toate). | + | |
- | * Există un compromis între //viteză// și //cost (complexitate hardware)// | + | |
- | * **Status Register**: Registru de flag-uri: '''ZF''/''Z'' - Zero, ''CF''/''C'' - Carry, ''N'' - Negative, ''OF''/''V'' - Overflow | + | |
- | * Setate de UAL în momentul producerii unor evenimente. | + | |
- | * **Algoritmul lui Booth** este o metodă eficientă de a înmulți două numere cu semn, care folosește operații de ''shift'' | + | |
- | ==== 7. Exerciții ==== | + | == Memorie == |
- | <note warning> | + | Spațiu de 64K adrese (16 biți) – accese prin registrele MAR/MDR. |
- | Va trebui sa faceti atat **implementarea** cat si **simularea** cu seturi de date relevante. | + | Suportă apeluri de funcții (call, ret) cu salvarea/restaurarea PC-ului din adresele 0xFFFE și 0xFFFF. |
- | </note> | + | |
- | **Task 0 (2p)** - Implementati Algoritmul lui Booth pentru inmultire in ``task0.v``. | + | == ALU (UAL) == |
- | **Task 1 (2p)** - Implementati Algoritmul lui Booth pentru impartire (non-restoring division) in ``task1.v``. | + | Realizează 8 operații: ADD, SUB, AND, OR, XOR, CMA, RAL, RAR. |
- | <note tip> | + | == Flags == |
- | În Verilog folosim operatorii ''<nowiki><<<</nowiki>'' și ''<nowiki>>>></nowiki>'' pentru shiftări aritmetice. Valoarea bitului de fill se determină pe baza contextului (signed sau unsigned). Din acest motiv, pentru a shifta aritmetic la dreapta un număr negativ va trebui să specificăm contextul ''signed'' al valorii shiftate. | + | |
- | <code verilog> | + | Reține rezultatul logic al ultimei operații: |
- | assign magic_shift = 4'b1010; | + | * Z (Zero): rezultat = 0 |
- | assign logic_right_shift = (magic_shift >> 1); // 4'0101 | + | * S (Sign): bitul de semn al rezultatului |
- | assign logic_left_shift = (magic_shift << 1); // 4'b0100 | + | * Alte flag-uri posibile în arhitecturi mai avansate: C (Carry), V (Overflow), N (Negative), S (semn logic: N xor V) |
- | 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 e relevant aici | + | |
- | </code> | + | |
- | </note> | + | |
- | <note important> | + | |
- | În implementarea algoritmului lui Booth aveți nevoie de: | + | |
- | <code verilog> | ||
- | assign signed_arithmetic_right_shift = ($signed(magic_shift) >>> 1); // 4'b1101 <- corect, semnul se păstrează | ||
- | </code> | ||
- | </note> | ||
- | **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 (Daca rezultatul are mai putin de 8 biti se va face padding cu zero pe cei mai semnificativi biti). Pentru valorile lui i_w_op_sel avem urmatoarele operatii: | + | ==== Exerciții de laborator ==== |
+ | - (5p) Implementați un adder carry-look ahead, astfel: urmăriți codul din ALU, ce se instanțiază și completați modulul corespunzător (cla_adder), conform cunoștințelor de la labul precedent. | ||
+ | - (5p) Implementați flagul de Carry folosind indicatiile din modulul de flags, dar si logica prezentata in descrierea flag-urilor. | ||
+ | - (Bonus) Implementați flagul de Overflow asemeni celui de Carry de la task-ul anterior. | ||
- | * **(0.5p)** 0 : ''AND'' | + | ==== Resurse ==== |
- | * **(0.5p)** 1 : ''XOR'' | + | <hidden> |
- | * **(0.5p)** 2 : ''OR'' | + | * [[https://github.com/cs-pub-ro/SOC-1|Solutie laborator]] |
- | * **(0.5p)** 3 : ''ADD'' | + | |
- | * **(0.5p)** 4 : ''SUB'' (folosind ADD) | + | |
- | * **(1.5p)** 5 : ''MUL'' (folosind algoritmul lui Booth) | + | |
- | * **(1p)** 6 : ''DIV'' (folosind algoritmul lui Booth pentru împărțire) | + | |
- | * **(1p)** 7 : ''MOD'' (folosind algoritmul lui Booth pentru împărțire) | + | |
- | <note tip> | ||
- | În acest laborator aveți fișiere de testare pentru fiecare task. Acesta are hardcodate operanzi și rezultatele unor operații, iar rolul lui este de a vă ajuta pentru testare. Pentru a-l folosi, tratați modulul test_ual.v drept unul de simulare. | ||
- | </note> | ||
- | **BONUS: Task 3 (2p)** - Incarcati modulul ``task3.v`` pe placa de dezvoltare din laborator. | + | Daca nu aveti acces la repo, mai jos este arhiva solutiei: |
- | + | * {{soc:laboratoare:soc-1-master.zip | Solutie laborator}} | |
- | * Switch-urile for fi folosite pentru a da valoare celor doua intrari i_w_a si i_w_b | + | |
- | * Butoanele vor fi folosite pentru reset si selectia operatiei | + | |
- | * Displayul 7-LED va afisa rezultatul operatiei in zecimal (utilizati modului din display_7_segment_driver.v) | + | |
- | * LED-urile din dreptul switch-urilor vor afisa rezultatul operatiei in format binar | + | |
- | * Folositi doar 4 dintre cele 8 cifre de pe afisaj (spre exemplu cele cu anozii AN0-AN3) | + | |
- | + | ||
- | <hidden> | + | |
- | Recomandat sa faceti la tabla cate un exemplu atat de inmultire (factorii de max 4 biti) si de impartire (deimpartitul pe max 8 biti) cu algoritmul lui Booth; prezentati-le exemplul din lab, cu explicatii. La UAL si Status Register nu e nevoie sa intrati la fel de mult in detalii. | + | |
</hidden> | </hidden> | ||
- | <ifauth @user> | ||
- | </ifauth> | ||
- | |||
- | ==== 8. Resurse ==== | ||
- | * [[https://gitlab.cs.pub.ro/calculatoare-numerice/soc-public/-/tree/main/lab08|Scheletul de laborator]] | ||
- | |||
- | ==== 9. Linkuri utile ==== | ||
- | * [[https://en.wikipedia.org/wiki/Booth%27s_multiplication_algorithm|Booth algorithm Wiki]] | + | {{soc:laboratoare:sap-2_skel.zip | Scheletul de laborator}} |
- | * [[http://www.vlsiip.com/download/booth.pdf|Booth algorithm]] | + | |
- | * [[http://www.massey.ac.nz/~mjjohnso/notes/59304/l5.html|Exemplu Booth]] | + | |
- | * [[https://en.wikipedia.org/wiki/Logical_shift|Logical Shift]] | + | |
- | * [[https://en.wikipedia.org/wiki/Arithmetic_shift|Arithmetic Shift]] | + | |
- | * [[https://digilent.com/reference/_media/reference/programmable-logic/nexys-a7/nexys-a7_rm.pdf|Datasheet Digilent Nexys A7]] | + | |
- | * [[https://digilent.com/reference/_media/reference/programmable-logic/nexys-a7/nexys-a7-sch.pdf|Schema Digilent Nexys A7]] | + |