Diferență între revizuiri ale paginii „Laboratorul 2”
(Nu s-au afișat 26 de versiuni intermediare efectuate de alți 2 utilizatori) | |||
Linia 11: | Linia 11: | ||
{| class="wikitable" style="text-align: center; | {| class="wikitable" style="text-align: center; | ||
|+ Tabelul 1 | |+ Tabelul 1 | ||
− | !mnemonică!! detalii!! | + | !mnemonică!! opcode!! detalii!! |
|- | |- | ||
− | | <code> | + | | <code>NOP</code> || <code>4'b0000</code> || <code> No operation </code> || |
|- | |- | ||
− | | <code> | + | | <code>ADD</code> || <code>4'b0001</code> || <code> R[dest] <- R[sursa1] + R[sursa2] </code> || |
|- | |- | ||
− | | <code> | + | | <code>SUB</code> || <code>4'b0010</code> || <code> R[dest] <- R[sursa1] - R[sursa2] </code> || |
|- | |- | ||
− | | <code> | + | | <code>AND</code> || <code>4'b0011</code> || <code> R[dest] <- R[sursa1] & R[sursa2] </code> || |
|- | |- | ||
− | | <code> | + | | <code>OR</code> || <code>4'b0100</code> || <code> R[dest] <- R[sursa1] | R[sursa2] </code> || |
|- | |- | ||
− | | <code> | + | | <code>XOR</code> || <code>4'b0101</code> || <code> R[dest] <- R[sursa1] ^ R[sursa2] </code> || |
|- | |- | ||
− | | <code> | + | | <code>CMP</code> || <code>4'b0111</code> || <code> Z <- R[sursa1] == R[sursa2], N <- R[sursa1] >= R[sursa2] </code> || |
|- | |- | ||
− | | <code>LOAD</code> || <code> R[dest] <- memorie[R[sursa1]] </code> || | + | | <code>LOADC</code> || <code>4'b1000</code> || <code> R[dest] <- instr_data </code> || |
+ | |- | ||
+ | | <code>LOAD</code> || <code>4'b1001</code> || <code> R[dest] <- memorie[R[sursa1]] </code> || | ||
|- | |- | ||
− | | <code>STORE</code> || <code> memorie[R[sursa1]] <- R[sursa2] </code> || | + | | <code>STORE</code> || <code>4'b1010</code> || <code> memorie[R[sursa1]] <- R[sursa2] </code> || |
|- | |- | ||
− | | <code>HALT</code> || <code> halt </code> || | + | | <code>HALT</code> || <code>4'b1111</code> || <code> halt </code> || |
|} | |} | ||
+ | |||
+ | Toate instrucțiunile au 16 biți. Cei mai semnificativi 4 biți ai fiecărei instrucțiuni identifică unic instrucțiunea, valoarea acestui câmp fiind codul instrucțiunii (opcode). Ceilalți 12 biți au o semnificație ce depinde de tipul instrucțiunii, conform desenului de mai jos. Instrucțiunile SUB, AND, OR și XOR au același format ca instrucțiunea ADD, CMP are același format cu STORE, iar NOP are formatul identic cu al instrucțiunii HALT. | ||
+ | |||
+ | [[Fișier: instr_format.png]] | ||
== Procesorul == | == Procesorul == | ||
Linia 38: | Linia 44: | ||
Având o singură cale de acces atât pentru instrucțiuni, cât și pentru date, arhitectura von Neumann procesează o instrucțiune în mai mulți pași, în fiecare pas magistrala comună fiind configurată pentru un anumit transfer între blocurile și registrele conectate la aceasta. Secvențierea procesării instrucțiunilor se face cu ajutorul unității de control al procesorului (UCP), aceasta fiind implementată fie ca o mașină de stări, fie ca o structură microprogramabilă. | Având o singură cale de acces atât pentru instrucțiuni, cât și pentru date, arhitectura von Neumann procesează o instrucțiune în mai mulți pași, în fiecare pas magistrala comună fiind configurată pentru un anumit transfer între blocurile și registrele conectate la aceasta. Secvențierea procesării instrucțiunilor se face cu ajutorul unității de control al procesorului (UCP), aceasta fiind implementată fie ca o mașină de stări, fie ca o structură microprogramabilă. | ||
− | + | Schema bloc a procesorului conține o unitate aritmetico-logică cu registre (RALU), un contor de program (PC) ce se poate incrementa sau încărca cu o valoare dată, un registru pentru adresarea memoriei pentru transferul de date (ADDR), registrul instrucțiunii (IR) ce păstrează codul instrucțiunii pe toată durata procesării acesteia și unitatea de control a procesorului (UCP): | |
− | |||
− | Schema bloc a procesorului conține o unitate aritmetico-logică ( | ||
[[Fișier: asc_lab2_microneumann.png]] | [[Fișier: asc_lab2_microneumann.png]] | ||
+ | Pentru a nu complica desenul, în schema bloc au fost schițate doar traseele principale (căile de date). Semnalele de control generate de UCP controlează actualizarea tuturor registrelor (incrementarea PC, încărcarea PC, încărcarea ADDR, încărcarea instrucțiunii în IR byte cu byte, scrierea în registrul destinație din RALU precum și activarea scrierii în memoria externă. De asemenea semnalele de control configurează căile de date prin selecțiile corespunzătoare pentru multiplexoarele de acces. Adresa de memorie este fie adresa unui byte de instrucțiune (PC), fie adresa unei locații de date (ADDR). Data de pe magistrala comună este selectată din trei surse posibile, ea putând fi rezultatul RALU, sau data citită din memorie sau o dată imediată din instrucțiune (byte-ul inferior al acesteia). | ||
+ | Pentru a simplifica procesarea internă și pentru a păstra ordinea câmpurilor instrucțiunii (opcode, destinatie, surse) este prevăzut un multiplexor suplimentar care permite selecția sursei 1 pentru încărcarea adresei folosite de instrucțiunile de transfer cu memoria. | ||
+ | |||
=== PC === | === PC === | ||
Linia 50: | Linia 57: | ||
<syntaxhighlight lang="Verilog"> | <syntaxhighlight lang="Verilog"> | ||
− | always @( | + | always @(posedge clk) begin |
if(rst) | if(rst) | ||
pc <= 0; | pc <= 0; | ||
else begin | else begin | ||
if(pc_load) | if(pc_load) | ||
− | + | pc <= common_data; | |
− | else if (pc_incr) | + | else if(pc_incr) |
− | + | // incrementare // <<<<< COMPLETAȚI CODUL ! | |
else | else | ||
pc <= pc; | pc <= pc; | ||
Linia 69: | Linia 76: | ||
<syntaxhighlight lang="Verilog"> | <syntaxhighlight lang="Verilog"> | ||
− | always @( | + | always @(posedge clk) begin |
if(rst) | if(rst) | ||
− | + | data_addr <= 0; | |
else begin | else begin | ||
− | if( | + | if(addr_load) |
− | + | data_addr <= common_data; | |
else | else | ||
− | + | data_addr <= data_addr; | |
end | end | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === UCP | + | === IR === |
+ | |||
+ | Instrucțiunea fiind pe 16 biți este nevoie de două accese succesive la memorie, la adrese consecutive, pentru a încărca fiecare byte al registrului instrucțiunii. | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | always @(posedge clk) begin | ||
+ | if(rst) | ||
+ | instruction <= 0; | ||
+ | else begin | ||
+ | if(ir_load_high) | ||
+ | instruction[15:8] <= common_data; | ||
+ | else if(ir_load_low) | ||
+ | // încărcarea byte-ului inferior al instrucțiunii // <<<<< COMPLETAȚI CODUL ! | ||
+ | else | ||
+ | instruction <= instruction; | ||
+ | end | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Decodarea instrucțiunii este elementară și constă în separarea câmpurilor instrucțiunii: | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | assign opcode = instruction[15:12]; | ||
+ | assign dest = // dest este următorul câmp de 4 biți al instrucțiunii; // <<<<< COMPLETAȚI CODUL ! | ||
+ | assign sursa1 = instruction[ 7: 4]; | ||
+ | assign sursa2 = addr_load ? instruction[ 7: 4] : instruction[ 3: 0]; | ||
+ | // instr_data este byte-ul inferior al instrucțiunii // <<<<< COMPLETAȚI CODUL ! | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Byte-ul superior este la prima adresa și conține opcode-ul și câmpul destinație. | ||
+ | |||
+ | === Multiplexorul căii comune de date === | ||
+ | |||
+ | Selectează sursa pentru data ce tranzitează calea comună de date. | ||
+ | Aceasta poate fi byte-ul inferior al IR (pentru transferul unei constante din corpul instrucțiunii), intrarea de date dinspre memorie (pentru citirea instrucțiunii sau pentru citirea unei date din memorie) sau ieșirea RALU (pentru salvarea rezultatului ALU). | ||
+ | '''sel_mem_data''' selectează intrarea de date dinspre memorie, iar '''sel_instr_data''' selectează constanta. Dacă niciunul din cele două semnale de selecție nu este activ se selectează (implicit) rezultatul ALU: | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | always @(*) begin | ||
+ | if(sel_mem_data) | ||
+ | common_data = data_in; // se selectează data dinspre memorie | ||
+ | else if(sel_instr_data) | ||
+ | common_data = // se selectează constanta // <<<<< COMPLETAȚI CODUL ! | ||
+ | else | ||
+ | // se selectează rezultatul de la ALU // <<<<< COMPLETAȚI CODUL ! | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == UCP == | ||
Unitatea de control a procesorului controlează actualizarea fiecărui registru din procesor, accesul la magistrala comună de date și multiplexarea adreselor spre memorie. | Unitatea de control a procesorului controlează actualizarea fiecărui registru din procesor, accesul la magistrala comună de date și multiplexarea adreselor spre memorie. | ||
− | Ea se implementează ca un FSM cu câteva stări și tranziții ce depind de tipul instrucțiunii. | + | Ea se implementează ca un FSM cu câteva stări și tranziții ce depind de tipul instrucțiunii. Ieșirile UCP sunt semnalele de control generate de FSM. |
[[Fișier: asc_lab1_fsmneumann.png]] | [[Fișier: asc_lab1_fsmneumann.png]] | ||
+ | |||
+ | |||
+ | === stările === | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | localparam FETCH1 = 3'd0; | ||
+ | localparam FETCH2 = 3'd1; | ||
+ | localparam EXECUTE = 3'd2; | ||
+ | localparam LDADDR = 3'd3; | ||
+ | localparam LDCONST = 3'd4; | ||
+ | localparam LD_DATA = 3'd5; | ||
+ | localparam ST_DATA = 3'd6; | ||
+ | localparam HALT = 3'd7; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === tranzițiile === | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | always @(posedge clk) begin | ||
+ | if(rst) begin | ||
+ | state <= FETCH1; | ||
+ | end | ||
+ | else begin | ||
+ | case(state) | ||
+ | |||
+ | FETCH1: state <= FETCH2; | ||
+ | |||
+ | FETCH2: begin | ||
+ | case(opcode) | ||
+ | 4'b0000: state <= FETCH1; // NOP | ||
+ | 4'b0001: state <= EXECUTE; // ADD | ||
+ | 4'b1000: state <= LDCONST; // LOADC | ||
+ | 4'b1001: // LOAD // <<<<< COMPLETAȚI CODUL ! | ||
+ | 4'b1010: // STORE // <<<<< COMPLETAȚI CODUL ! | ||
+ | 4'b1111: // HALT // <<<<< COMPLETAȚI CODUL ! | ||
+ | endcase | ||
+ | end | ||
+ | |||
+ | LDCONST: state <= FETCH1; | ||
+ | |||
+ | LDADDR: begin | ||
+ | if(opcode == 4'b1001) | ||
+ | state <= LD_DATA; | ||
+ | if(opcode == 4'b1010) | ||
+ | state <= ST_DATA; | ||
+ | end | ||
+ | |||
+ | LD_DATA: state <= FETCH1; | ||
+ | ST_DATA: // <<<<< COMPLETAȚI CODUL ! | ||
+ | EXECUTE: // <<<<< COMPLETAȚI CODUL ! | ||
+ | HALT: // <<<<< COMPLETAȚI CODUL ! | ||
+ | default: state <= HALT; | ||
+ | endcase | ||
+ | end | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === semnalele de control === | ||
+ | |||
+ | Pentru a scrie compact codul generării tuturor semnalelor de control, acestea sunt grupate într-un vector de 10 biți, '''control_vector''', | ||
+ | fiecare semnal de control fiind un anumit bit al acestuia: | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | reg [9:0] control_vector; | ||
+ | |||
+ | assign pc_incr = control_vector[9]; | ||
+ | assign pc_load = control_vector[8]; | ||
+ | assign addr_load = control_vector[7]; | ||
+ | assign sel_addr = control_vector[6]; | ||
+ | assign sel_mem_data = control_vector[5]; | ||
+ | assign sel_instr_data = control_vector[4]; | ||
+ | assign ir_load_high = control_vector[3]; | ||
+ | assign ir_load_low = control_vector[2]; | ||
+ | assign regs_wen = control_vector[1]; | ||
+ | assign write = control_vector[0]; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Valorile biților de control sunt generate în fiecare stare corespunzător resurselor pe care procesorul le folosește sau le controlează în acea stare: | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | always @(*) begin | ||
+ | case(state) | ||
+ | FETCH1: control_vector = 10'b10_0010_1000; // PC <- PC + 1, IRH <- data_from_mem | ||
+ | FETCH2: // PC <- PC + 1, IRL <- data_from_mem // <<<<< COMPLETAȚI CODUL ! | ||
+ | LDCONST: control_vector = 10'b00_0001_0010; // R[dest] <- instrdata | ||
+ | LDADDR: control_vector = 10'b00_1000_0000; // ADDR <- result, | ||
+ | EXECUTE: // R[dest] <- result // <<<<< COMPLETAȚI CODUL ! | ||
+ | LD_DATA: control_vector = 10'b00_0110_0010; // addr = ADDR, R[dest] <- data_from_mem | ||
+ | ST_DATA: control_vector = 10'b00_0100_0001; // addr = ADDR, write | ||
+ | HALT: control_vector = 10'b00_0000_0000; // nothing to do | ||
+ | endcase | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Memoria == | ||
+ | |||
+ | Pentru lucrarea 2 se va considera o memorie simplă, de 2 kb, cu 256 locații de 8 biți fiecare, cu un singur port de date, cu intrare și ieșire separate. | ||
+ | Citirea are loc instantaneu, iar scrierea se execută sincron, la finalul ciclului de ceas de acces, dacă semnalul de scriere este activ. Memoria nu folosește semnalul de reset. | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | reg [7:0] memory [0:255]; | ||
+ | // citire din memorie | ||
+ | assign dout = memory[addr]; | ||
+ | // scriere in memorie | ||
+ | always @(posedge clk) if(write) memory[addr] <= din; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Inițializarea memoriei === | ||
+ | |||
+ | Memoria calculatorului, fiind unică, conține atât programul cât și datele. | ||
+ | Organizarea ei este la latitudinea programatorului, dar trebuie ținut cont de faptul că la resetare contorul de program (PC) ia valoarea 0. | ||
+ | Prima instrucțiune citită de procesor va fi cea de la locațiile cu adresele 0 și 1. | ||
+ | |||
+ | Pentru lucrarea de laborator se va inițializa memoria cu o secvență de instrucțiuni în locații succesive începînd cu adresa 0 (programul) | ||
+ | și cu câteva valori numerice întregi, de 8 biți, la ultimele adrese din memorie (datele programului). | ||
+ | Locațiile inițializate ca date trebuie să coincidă cu locațiile adresate prin program pentru a putea procesa acele date. | ||
+ | |||
+ | === Initializare program === | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | initial begin | ||
+ | {memory[00], memory[01]} = 16'b1000_0000_1111_1111; // LOADC R0 #255 | ||
+ | {memory[02], memory[03]} = 16'b1000_0001_1111_1110; // LOADC R1 #254 | ||
+ | // LOADC R2 #253 // <<<<< COMPLETAȚI CODUL ! | ||
+ | {memory[06], memory[07]} = 16'b1001_0100_0000_0000; // LOAD R4 R0 | ||
+ | // LOAD R5 R1 // <<<<< COMPLETAȚI CODUL ! | ||
+ | {memory[10], memory[11]} = 16'b0001_0110_0101_0100; // ADD R6 R5 R4 | ||
+ | {memory[12], memory[13]} = 16'b1010_0000_0010_0110; // STORE R2 R6 | ||
+ | {memory[14], memory[15]} = 16'b1111_1111_1111_1111; // HALT | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Programul de mai sus incarcă în primele trei registre trei adrese de memorie, date în program ca valori imediate în instrucțiunile LOADC (load constant), | ||
+ | apoi transferă două numere din ultimele doua locații de memorie în doua registre, R4 si R5. | ||
+ | Urmează adunarea celor două numere, rezultatul fiind salvat în registrul R6, rezultat ce apoi este stocat în memorie în antepenultima ei locație. | ||
+ | Ultima instrucțiune oprește definitiv procesorul (din starea halt procesorul nu mai poate fi reactivat decît prin resetare) | ||
+ | |||
+ | === Initializare date === | ||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | initial begin | ||
+ | memory[254] = 17; | ||
+ | memory[255] = 23; | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Modulul de top (calculatorul) == | ||
+ | |||
+ | Fiind un calculator simplu, format doar din procesor și memorie, modulul de top ('''neumann''') conține doar două instanțe, corespunzătoare celor două blocuri componente ale calculatorului. | ||
+ | Singurii pini ai modului de top sunt o intrare de ceas și una de reset. | ||
+ | Nu uitați să declarați conexiunile interne dintre cele două instanțe, conexiuni multibit, '''addr''', '''data_to_mem''' și '''data_from_mem''', | ||
+ | și să conectați semnalul de control al scrierii în memorie. | ||
+ | |||
+ | == Testarea calculatorului == | ||
+ | |||
+ | Scrieți modulul de testare, în care instanțiați calculatorul descris mai sus și generați cele două semnale de intrare, '''clk''' și '''rst'''. | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | module neumann_tb; | ||
+ | |||
+ | // declarații de variable | ||
+ | |||
+ | // instanțierea calculatorului | ||
+ | |||
+ | // generarea semnalului de ceas | ||
+ | |||
+ | // generarea semnalului de reset | ||
+ | |||
+ | // un bloc initial pentru oprirea simularii | ||
+ | initial begin | ||
+ | repeat (100) @(posedge clk); | ||
+ | $stop; | ||
+ | end | ||
+ | |||
+ | endmodule | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Activități suplimentare == | ||
+ | |||
+ | === Activitatea 1 === | ||
+ | |||
+ | Pentru procesarea instrucțiunilor NOP și HALT este suficient codul instrucțiunii. Setul de instrucțiuni optimizat folosește doar 8 biți pentru instrucțiunile NOP și HALT, primii 4 biți reprezentând codul instrucțiunii. Modificați calculatorul astfel încât acesta să poată procesa setul de instrucțiuni astfel optimizat: | ||
+ | * Unitatea de Control a procesorului are nevoie doar de starea FETCH1 pentru citirea instrucțiunilor NOP și HALT. După starea FETCH1 UCP trece în starea HALT pentru instrucțiunea omonimă, în aceeași stare FETCH1 pentru instrucțiunea NOP, respectiv în starea FETCH2 pentru orice altă instrucțiune. | ||
+ | * În memoria de program instrucțiunile NOP și HALT ocupă câte o singură locație de memorie (un octet), spre deosebire de celelalte instrucțiuni, ce ocupă două locații (doi octeți). Inserați în programul din memorie două instrucțiuni NOP, rezultând următoarea secvență de instrucțiuni: | ||
+ | |||
+ | <syntaxhighlight lang="Verilog"> | ||
+ | LOADC R0 #255 | ||
+ | LOADC R1 #254 | ||
+ | LOADC R2 #253 | ||
+ | LOAD R4 R0 | ||
+ | LOAD R5 R1 | ||
+ | NOP | ||
+ | ADD R6 R5 R4 | ||
+ | NOP | ||
+ | STORE R2 R6 | ||
+ | HALT | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Atenție la locațiile de memorie ale instrucțiunilor! |
Versiunea curentă din 24 octombrie 2021 22:27
Arhitectura von Neumann
Arhitectura de calculator von Neumann este caracterizată printr-o memorie unică, în care se află atât programul cât și datele, și o magistrală unică, folosită pentru transferul instrucțiunilor de la memorie la procesor, a datelor între procesor și memorie sau între procesor și oricare din dispozitivele de intrare/ieșire conectate la magistrală.
În această lucrare de laborator se va realiza un calculator simplu, având doar procesorul și memoria.
Setul de instrucțiuni
mnemonică | opcode | detalii | |
---|---|---|---|
NOP |
4'b0000 |
No operation |
|
ADD |
4'b0001 |
R[dest] <- R[sursa1] + R[sursa2] |
|
SUB |
4'b0010 |
R[dest] <- R[sursa1] - R[sursa2] |
|
AND |
4'b0011 |
R[dest] <- R[sursa1] & R[sursa2] |
|
OR |
4'b0100 |
R[dest] <- R[sursa1] | R[sursa2] |
|
XOR |
4'b0101 |
R[dest] <- R[sursa1] ^ R[sursa2] |
|
CMP |
4'b0111 |
Z <- R[sursa1] == R[sursa2], N <- R[sursa1] >= R[sursa2] |
|
LOADC |
4'b1000 |
R[dest] <- instr_data |
|
LOAD |
4'b1001 |
R[dest] <- memorie[R[sursa1]] |
|
STORE |
4'b1010 |
memorie[R[sursa1]] <- R[sursa2] |
|
HALT |
4'b1111 |
halt |
Toate instrucțiunile au 16 biți. Cei mai semnificativi 4 biți ai fiecărei instrucțiuni identifică unic instrucțiunea, valoarea acestui câmp fiind codul instrucțiunii (opcode). Ceilalți 12 biți au o semnificație ce depinde de tipul instrucțiunii, conform desenului de mai jos. Instrucțiunile SUB, AND, OR și XOR au același format ca instrucțiunea ADD, CMP are același format cu STORE, iar NOP are formatul identic cu al instrucțiunii HALT.
Procesorul
Având o singură cale de acces atât pentru instrucțiuni, cât și pentru date, arhitectura von Neumann procesează o instrucțiune în mai mulți pași, în fiecare pas magistrala comună fiind configurată pentru un anumit transfer între blocurile și registrele conectate la aceasta. Secvențierea procesării instrucțiunilor se face cu ajutorul unității de control al procesorului (UCP), aceasta fiind implementată fie ca o mașină de stări, fie ca o structură microprogramabilă.
Schema bloc a procesorului conține o unitate aritmetico-logică cu registre (RALU), un contor de program (PC) ce se poate incrementa sau încărca cu o valoare dată, un registru pentru adresarea memoriei pentru transferul de date (ADDR), registrul instrucțiunii (IR) ce păstrează codul instrucțiunii pe toată durata procesării acesteia și unitatea de control a procesorului (UCP):
Pentru a nu complica desenul, în schema bloc au fost schițate doar traseele principale (căile de date). Semnalele de control generate de UCP controlează actualizarea tuturor registrelor (incrementarea PC, încărcarea PC, încărcarea ADDR, încărcarea instrucțiunii în IR byte cu byte, scrierea în registrul destinație din RALU precum și activarea scrierii în memoria externă. De asemenea semnalele de control configurează căile de date prin selecțiile corespunzătoare pentru multiplexoarele de acces. Adresa de memorie este fie adresa unui byte de instrucțiune (PC), fie adresa unei locații de date (ADDR). Data de pe magistrala comună este selectată din trei surse posibile, ea putând fi rezultatul RALU, sau data citită din memorie sau o dată imediată din instrucțiune (byte-ul inferior al acesteia).
Pentru a simplifica procesarea internă și pentru a păstra ordinea câmpurilor instrucțiunii (opcode, destinatie, surse) este prevăzut un multiplexor suplimentar care permite selecția sursei 1 pentru încărcarea adresei folosite de instrucțiunile de transfer cu memoria.
PC
Contorul de program este folosit pentru citirea instrucțiunilor din memorie. Acesta poate fi incrementat sau încărcat cu valoarea de pe magistrala de date, dar numai la comanda UCP.
always @(posedge clk) begin
if(rst)
pc <= 0;
else begin
if(pc_load)
pc <= common_data;
else if(pc_incr)
// incrementare // <<<<< COMPLETAȚI CODUL !
else
pc <= pc;
end
end
ADDR
Registrul de adresare a memoriei pentru citirea datelor este un registru elementar ce poate fi doar încărcat:
always @(posedge clk) begin
if(rst)
data_addr <= 0;
else begin
if(addr_load)
data_addr <= common_data;
else
data_addr <= data_addr;
end
end
IR
Instrucțiunea fiind pe 16 biți este nevoie de două accese succesive la memorie, la adrese consecutive, pentru a încărca fiecare byte al registrului instrucțiunii.
always @(posedge clk) begin
if(rst)
instruction <= 0;
else begin
if(ir_load_high)
instruction[15:8] <= common_data;
else if(ir_load_low)
// încărcarea byte-ului inferior al instrucțiunii // <<<<< COMPLETAȚI CODUL !
else
instruction <= instruction;
end
end
Decodarea instrucțiunii este elementară și constă în separarea câmpurilor instrucțiunii:
assign opcode = instruction[15:12];
assign dest = // dest este următorul câmp de 4 biți al instrucțiunii; // <<<<< COMPLETAȚI CODUL !
assign sursa1 = instruction[ 7: 4];
assign sursa2 = addr_load ? instruction[ 7: 4] : instruction[ 3: 0];
// instr_data este byte-ul inferior al instrucțiunii // <<<<< COMPLETAȚI CODUL !
Byte-ul superior este la prima adresa și conține opcode-ul și câmpul destinație.
Multiplexorul căii comune de date
Selectează sursa pentru data ce tranzitează calea comună de date. Aceasta poate fi byte-ul inferior al IR (pentru transferul unei constante din corpul instrucțiunii), intrarea de date dinspre memorie (pentru citirea instrucțiunii sau pentru citirea unei date din memorie) sau ieșirea RALU (pentru salvarea rezultatului ALU). sel_mem_data selectează intrarea de date dinspre memorie, iar sel_instr_data selectează constanta. Dacă niciunul din cele două semnale de selecție nu este activ se selectează (implicit) rezultatul ALU:
always @(*) begin
if(sel_mem_data)
common_data = data_in; // se selectează data dinspre memorie
else if(sel_instr_data)
common_data = // se selectează constanta // <<<<< COMPLETAȚI CODUL !
else
// se selectează rezultatul de la ALU // <<<<< COMPLETAȚI CODUL !
end
UCP
Unitatea de control a procesorului controlează actualizarea fiecărui registru din procesor, accesul la magistrala comună de date și multiplexarea adreselor spre memorie. Ea se implementează ca un FSM cu câteva stări și tranziții ce depind de tipul instrucțiunii. Ieșirile UCP sunt semnalele de control generate de FSM.
stările
localparam FETCH1 = 3'd0;
localparam FETCH2 = 3'd1;
localparam EXECUTE = 3'd2;
localparam LDADDR = 3'd3;
localparam LDCONST = 3'd4;
localparam LD_DATA = 3'd5;
localparam ST_DATA = 3'd6;
localparam HALT = 3'd7;
tranzițiile
always @(posedge clk) begin
if(rst) begin
state <= FETCH1;
end
else begin
case(state)
FETCH1: state <= FETCH2;
FETCH2: begin
case(opcode)
4'b0000: state <= FETCH1; // NOP
4'b0001: state <= EXECUTE; // ADD
4'b1000: state <= LDCONST; // LOADC
4'b1001: // LOAD // <<<<< COMPLETAȚI CODUL !
4'b1010: // STORE // <<<<< COMPLETAȚI CODUL !
4'b1111: // HALT // <<<<< COMPLETAȚI CODUL !
endcase
end
LDCONST: state <= FETCH1;
LDADDR: begin
if(opcode == 4'b1001)
state <= LD_DATA;
if(opcode == 4'b1010)
state <= ST_DATA;
end
LD_DATA: state <= FETCH1;
ST_DATA: // <<<<< COMPLETAȚI CODUL !
EXECUTE: // <<<<< COMPLETAȚI CODUL !
HALT: // <<<<< COMPLETAȚI CODUL !
default: state <= HALT;
endcase
end
end
semnalele de control
Pentru a scrie compact codul generării tuturor semnalelor de control, acestea sunt grupate într-un vector de 10 biți, control_vector, fiecare semnal de control fiind un anumit bit al acestuia:
reg [9:0] control_vector;
assign pc_incr = control_vector[9];
assign pc_load = control_vector[8];
assign addr_load = control_vector[7];
assign sel_addr = control_vector[6];
assign sel_mem_data = control_vector[5];
assign sel_instr_data = control_vector[4];
assign ir_load_high = control_vector[3];
assign ir_load_low = control_vector[2];
assign regs_wen = control_vector[1];
assign write = control_vector[0];
Valorile biților de control sunt generate în fiecare stare corespunzător resurselor pe care procesorul le folosește sau le controlează în acea stare:
always @(*) begin
case(state)
FETCH1: control_vector = 10'b10_0010_1000; // PC <- PC + 1, IRH <- data_from_mem
FETCH2: // PC <- PC + 1, IRL <- data_from_mem // <<<<< COMPLETAȚI CODUL !
LDCONST: control_vector = 10'b00_0001_0010; // R[dest] <- instrdata
LDADDR: control_vector = 10'b00_1000_0000; // ADDR <- result,
EXECUTE: // R[dest] <- result // <<<<< COMPLETAȚI CODUL !
LD_DATA: control_vector = 10'b00_0110_0010; // addr = ADDR, R[dest] <- data_from_mem
ST_DATA: control_vector = 10'b00_0100_0001; // addr = ADDR, write
HALT: control_vector = 10'b00_0000_0000; // nothing to do
endcase
end
Memoria
Pentru lucrarea 2 se va considera o memorie simplă, de 2 kb, cu 256 locații de 8 biți fiecare, cu un singur port de date, cu intrare și ieșire separate. Citirea are loc instantaneu, iar scrierea se execută sincron, la finalul ciclului de ceas de acces, dacă semnalul de scriere este activ. Memoria nu folosește semnalul de reset.
reg [7:0] memory [0:255];
// citire din memorie
assign dout = memory[addr];
// scriere in memorie
always @(posedge clk) if(write) memory[addr] <= din;
Inițializarea memoriei
Memoria calculatorului, fiind unică, conține atât programul cât și datele. Organizarea ei este la latitudinea programatorului, dar trebuie ținut cont de faptul că la resetare contorul de program (PC) ia valoarea 0. Prima instrucțiune citită de procesor va fi cea de la locațiile cu adresele 0 și 1.
Pentru lucrarea de laborator se va inițializa memoria cu o secvență de instrucțiuni în locații succesive începînd cu adresa 0 (programul) și cu câteva valori numerice întregi, de 8 biți, la ultimele adrese din memorie (datele programului). Locațiile inițializate ca date trebuie să coincidă cu locațiile adresate prin program pentru a putea procesa acele date.
Initializare program
initial begin
{memory[00], memory[01]} = 16'b1000_0000_1111_1111; // LOADC R0 #255
{memory[02], memory[03]} = 16'b1000_0001_1111_1110; // LOADC R1 #254
// LOADC R2 #253 // <<<<< COMPLETAȚI CODUL !
{memory[06], memory[07]} = 16'b1001_0100_0000_0000; // LOAD R4 R0
// LOAD R5 R1 // <<<<< COMPLETAȚI CODUL !
{memory[10], memory[11]} = 16'b0001_0110_0101_0100; // ADD R6 R5 R4
{memory[12], memory[13]} = 16'b1010_0000_0010_0110; // STORE R2 R6
{memory[14], memory[15]} = 16'b1111_1111_1111_1111; // HALT
end
Programul de mai sus incarcă în primele trei registre trei adrese de memorie, date în program ca valori imediate în instrucțiunile LOADC (load constant), apoi transferă două numere din ultimele doua locații de memorie în doua registre, R4 si R5. Urmează adunarea celor două numere, rezultatul fiind salvat în registrul R6, rezultat ce apoi este stocat în memorie în antepenultima ei locație. Ultima instrucțiune oprește definitiv procesorul (din starea halt procesorul nu mai poate fi reactivat decît prin resetare)
Initializare date
initial begin
memory[254] = 17;
memory[255] = 23;
end
Modulul de top (calculatorul)
Fiind un calculator simplu, format doar din procesor și memorie, modulul de top (neumann) conține doar două instanțe, corespunzătoare celor două blocuri componente ale calculatorului. Singurii pini ai modului de top sunt o intrare de ceas și una de reset. Nu uitați să declarați conexiunile interne dintre cele două instanțe, conexiuni multibit, addr, data_to_mem și data_from_mem, și să conectați semnalul de control al scrierii în memorie.
Testarea calculatorului
Scrieți modulul de testare, în care instanțiați calculatorul descris mai sus și generați cele două semnale de intrare, clk și rst.
module neumann_tb;
// declarații de variable
// instanțierea calculatorului
// generarea semnalului de ceas
// generarea semnalului de reset
// un bloc initial pentru oprirea simularii
initial begin
repeat (100) @(posedge clk);
$stop;
end
endmodule
Activități suplimentare
Activitatea 1
Pentru procesarea instrucțiunilor NOP și HALT este suficient codul instrucțiunii. Setul de instrucțiuni optimizat folosește doar 8 biți pentru instrucțiunile NOP și HALT, primii 4 biți reprezentând codul instrucțiunii. Modificați calculatorul astfel încât acesta să poată procesa setul de instrucțiuni astfel optimizat:
- Unitatea de Control a procesorului are nevoie doar de starea FETCH1 pentru citirea instrucțiunilor NOP și HALT. După starea FETCH1 UCP trece în starea HALT pentru instrucțiunea omonimă, în aceeași stare FETCH1 pentru instrucțiunea NOP, respectiv în starea FETCH2 pentru orice altă instrucțiune.
- În memoria de program instrucțiunile NOP și HALT ocupă câte o singură locație de memorie (un octet), spre deosebire de celelalte instrucțiuni, ce ocupă două locații (doi octeți). Inserați în programul din memorie două instrucțiuni NOP, rezultând următoarea secvență de instrucțiuni:
LOADC R0 #255
LOADC R1 #254
LOADC R2 #253
LOAD R4 R0
LOAD R5 R1
NOP
ADD R6 R5 R4
NOP
STORE R2 R6
HALT
Atenție la locațiile de memorie ale instrucțiunilor!