Laboratorul 4

De la WikiLabs

Arhitectura Harvard

Arhitectura de calculator Harvard are magistrale distincte pentru accesul la program și la date, permițând astfel citirea unei instrucțiuni în paralel cu citirea sau scrierea unei date de către o altă instrucțiune. Programul și datele se află în memorii separate sau într-o memorie comună dar cu porturi multiple de acces.

Asc lab4 harvard.png


Procesorul pipeline

Procesorul are o structură pipeline cu trei niveluri:

  1. FETCH - citirea instrucțiunii din memoria de program
  2. READ - citirea operanzilor (din registrele sursă)
  3. EXECUTE - execuția operației instrucțiunii/accesul în memoria de date

Asc lab4 procesor.png

În această lucrare de laborator se va implementa un procesor pipeline fără gestiunea dependențelor de date sau de control, ce procesează setul de instrucțiuni din laboratoarele precedente. Implementarea va reutiliza integral blocurile ALU și REGS proiectate în laboratorul 1, și cu mici modificări memoria din laboratorul 2.

Schema detaliată a structurii pipeline pune în evidență toate căile de date și semnalele de control. Schema este desenată astfel încât fluxul de instrucțiuni și de date să fie de la stânga la dreapta, cu excepția căii de scriere în setul de registre și a semnalului special de control halt.

Asc lab4 pipeline.png

Modulul procesor va avea o descriere mixtă, structurală, pentru că instanțiază modulele ALU și REGS, și comportamentală, pentru că toate celelalte componente (contor de program, registre pipeline, multiplexoare, logica de generare de semnale de control) sunt descrise folosind construcții always și assign. Pentru a avea un modul verilog ușor de urmărit și de depanat, este de preferat ca ordinea instanțierilor și a diferitelor construcții always și assign din descriere să corespundă ordonării blocurilor în structura pipeline:

module processor (
  // interfața procesorului
);

// declarații semnale interne
// descrierea PC
// descrierea R1
// instanță de modul REGS
// ș.a.m.d.
endmodule


PC

Contorul de program incrementează în fiecare ciclu de ceas cu excepția cazului în care procesorul a fost oprit de instrucțiunea HALT.

always @(posedge clk) begin
    if(rst)
        pc <= 0;
    else if(halt)
        pc <= pc;
    else 
        pc <= pc + 1;
end


FETCH

Setul de instrucțiuni fiind foarte simplu pentru acest laborator, nu există logică suplimentară în etapa de citire a instrucțiunii. Procesorul trimite spre memoria de program adresa instrucțiunii, adică valoarea PC, și preia instrucțiunea de la ieșirea acesteia.

assign instr_addr = pc;


Registrul pipeline R1

Acesta salvează la finalul ciclului de ceas (de citire a instrucțiunii) instrucțiunea primită de la memoria de program. Actualizarea registrului pipeline este oprită dacă procesorul execută instrucțiunea HALT. La resetare registrul pipeline este încărcat cu 0, adică cu codul instrucțiunii NOP.

always @(posedge clk) begin
    if(rst)
        r1 <= 0;
    else if(halt)
        r1 <= r1;
    else
        r1 <= instr;
end


READ

În etapa de citire a operanzilor procesorul accesează setul de registre pe baza câmpurilor sursă ale instrucțiunii din registrul R1, preia datele din registrele sursă și livrează operanzii pentru a fi stocați în registrul pipeline următor, R2. Pentru instrucțiunea LOADC operandul 2 este preluat direct din câmpul constantă al instrucțiunii din R1. Logica de selecție a multiplexorului operandului 2 se bazează pe comparația codului instrucțiunii aflate în această fază de procesare, r1_opcode, cu codul instrucțiunii LOADC.

assign operand2 = (r1_opcode == 4'b1000) ? r1_instr_data : rdata2; // LOADC are în binar codul 1000

Instrucțiunea de atribuire condițională de mai sus este descrierea funcțională a multiplexorului de la intrarea registrului R2 și a blocului logic de generare a semnalului de selecție pentru multiplexor.

Pentru claritatea codului și pentru a identifica ușor semnalele pe formele de undă, folosiți nume dedicate pentru fiecare câmp al instrucțiunii din R1.

assign r1_opcode     = r1[15:12];
assign r1_dest       = r1[11: 8];
assign r1_sursa1     = r1[ 7: 4];
assign r1_sursa2     =              // <<<<< COMPLETAȚI CODUL !
assign r1_instr_data =              // <<<<< COMPLETAȚI CODUL !


Registrul pipeline R2

Acest registru pipeline salvează operanzii și o parte a instrucțiunii ce a trecut de etapa de citire, câmpurile sursă și constantă nemaifiind necesare. Spre deosebire de registrul R1 care era declarat ca o singură variabilă r1, registrul R2 este compus din patru variabile distincte. Este foarte important ca după execuția instrucțiunii HALT acest registru pipeline să nu se mai actualizeze - astfel instrucțiunea HALT rămâne în registrul pipeline R2 asigurând blocarea procesorului până la resetare. La resetare registrul pipeline este încărcat cu 0, adică cu codul instrucțiunii NOP.

always @(posedge clk) begin
    if(rst) begin
        r2_opcode     <= 0;
        r2_dest       <= 0;
        r2_operand1   <= 0;
        r2_operand2   <= 0;
    end
    else if(halt) begin
        r2_opcode     <=              // <<<<< COMPLETAȚI CODUL !
         . . . . .
    end
    else begin
        r2_opcode     <= r1_opcode;
        r2_dest       <=              // <<<<< COMPLETAȚI CODUL !
        r2_operand1   <= operand1;
        r2_operand2   <=              // <<<<< COMPLETAȚI CODUL !
    end
end


EXECUTE

Logica combinațională de execuție include unitatea aritmetico-logică (ALU). Aceasta preia operanzii salvați în registrul pipeline R2 și livrează rezultatul în baza codului instrucțiunii din același registru. Pentru instrucțiunile de acces la memoria de date (LOAD și STORE), operandul 1 este trimis spre aceasta ca adresă de date. Instrucțiunea STORE trimite și operandul 2, cel ce trebuie salvat în memoria de date, și activează semnalul de control de scriere, write. Instrucțiunea LOAD preia data citită din memorie, data_in, și o selectează drept rezultat ce trebuie scris în registrul destinație, selecție ce se face comparând codul instrucțiunii din registrul pipeline R2 (codul instrucțiunii aflate în execuție) cu codul instrucțiunii LOAD (1001 în binar).

assign data_addr = r2_operand1;
assign data_out  =                                                  // <<<<< COMPLETAȚI CODUL !
assign write     =  // activ numai cand STORE e in faza de executie // <<<<< COMPLETAȚI CODUL !
assign result    = (r2_opcode == 4'b1001) ? data_in : alu_result;

Tot logica combinațională din etapa de execuție generează și semnalul de control al scrierii în setul de registre. Instrucțiunile NOP și HALT nu trebuie să actualizeze nimic, la fel și instrucțiunea STORE.

always @(*) begin
    case(r2_opcode)
    4'b0000: regs_wen = 0; // NOP
    4'b0001: regs_wen = 1; // ADD
    4'b0010:               // SUB    // <<<<< COMPLETAȚI CODUL !
    4'b1000:               // LOADC  // <<<<< COMPLETAȚI CODUL !
    4'b1001:               // LOAD   // <<<<< COMPLETAȚI CODUL !
    4'b1010:               // STORE  // <<<<< COMPLETAȚI CODUL !
    4'b1111:               // HALT   // <<<<< COMPLETAȚI CODUL !
    // urmeaza implementarea altor operatii
    default: regs_wen = 0;
    endcase
end

Etapa de execuție trebuie să genereze și semnalul de control halt atunci când instrucțiunea curentă din execuție are codul HALT.

assign halt =                        // <<<<< COMPLETAȚI CODUL !

Memoria de date

Este identică cu memoria din laboratorul precedent, dar fără inițializarea programului, acesta fiind stocat în altă memorie.


Memoria de program

Are nevoie doar de logica de acces pentru citire și blocul de inițializare a programului. Atenție la dimensiunea locațiilor de memorie și a portului de ieșire, instrucțiunea fiind de 16 biți!

Initializare program

Programul este cel din laboratorul prededent. Programul incarcă în primele trei registre trei adrese de memorie, specificate în program ca valori imediate în instrucțiunile LOADC (load constant), apoi transferă două numere din ultimele doua locații ale memoriei de date în doua registre, R4 si R5. Urmează adunarea celor două numere, rezultatul fiind salvat în registrul R6, rezultat ce apoi este stocat în memoria de date î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).

Deoarece procesorul implementat nu gestionează dependențele de date este obligatorie introducerea în program a câte unei instrucțiuni NOP între oricare două instrucțiuni succesive care depind una de alta. Instrucțiunea ADD din program folosește operandul încărcat în registru de ultima instrucțiune LOAD, iar instrucțiunea STORE are ca operand rezultatul instrucțiunii ADD. Codul instrucțiunii NOP este 0.

initial begin
    memory[00] = 16'b1000_0000_1111_1111; // LOADC R0 #255
    memory[01] = 16'b1000_0001_1111_1110; // LOADC R1 #254
                                          // LOADC R2 #253         // <<<<< COMPLETAȚI CODUL !
                                          // LOAD  R4 R0           // <<<<< COMPLETAȚI CODUL !
                                          // LOAD  R5 R1           // <<<<< COMPLETAȚI CODUL !
                                          // NOP                   // <<<<< COMPLETAȚI CODUL !
                                          // ADD   R6 R5 R4        // <<<<< COMPLETAȚI CODUL !
                                          // NOP                   // <<<<< COMPLETAȚI CODUL !
                                          // STORE R2 R6           // <<<<< COMPLETAȚI CODUL !
                                          // HALT                  // <<<<< COMPLETAȚI CODUL !
end


Modulul de top (calculatorul)

Modului de top, a cărui schemă este dată în prima figură, conține trei instanțe, corespunzătoare celor trei blocuri componente ale calculatorului. Singurii pini ai modului de top sunt o intrare de ceas și una de reset.


Testarea calculatorului

Modulul de testare este identic cu modulul de testare folosit în laboratorul precedent. Practic se schimbă doar numele modulului de top care este verificat, interfața lui rămânând aceeași.


Activități suplimentare

Activitatea 1

Rearanjați instrucțiunile în memoria de program astfel încât să folosiți o singură instrucțiune NOP în loc de două. Verificați în simulare că rezultatul execuției programului este cel corect.


Activitatea 2

Implementați instrucțiunea de salt necondiționat JMP.

  1. Alegeți pentru opcode o combinație neutilizată (de exemplu 1110) și folosiți câmpul constantă (octetul inferior al instrucțiunii) pentru adresa de salt.
  2. Modificați logica de selecție a operandului 2 astfel încât constanta să fie selectată atât pentru instrucțiunea LOADC cât și pentru instrucțiunea JMP.
  3. Modificați logica de generare a semnalelor de control din faza de execuție astfel ca instrucțiunea JMP să nu activeze scrierea în memorie și nici actualizarea setului de registre.
  4. Adăugați logica de generare a semnalului load_pc în faza de execuție a instrucțiunii JMP.
  5. Adăugați în descrierea PC ramura condiționată de activarea semnalului load_pc, în care pc va lua valoarea operandului 2 din registrul pipeline R2.

Verificați corectitudinea implementării instrucțiunii de salt.

  1. Inserați instrucțiuni de salt în programul folosit în lucrarea de laborator. Pentru că procesorul nu are mecanisme de gestiune a dependențelor de control, orice instrucțiune de salt din program va fi urmată de două instrucțiuni NOP.
  0   LOADC R0 #255
  1   LOADC R1 #254
  2   LOADC R2 #253
  3   LOAD  R4 R0
  4   LOAD  R5 R1
  5   JMP   #9
  6   NOP
  7   NOP
  8   HALT
  9   ADD   R6 R5 R4
 10   NOP
 11   STORE R2 R6
 12   JMP   #8
 13   NOP
 14   NOP