In questa serie di paragrafi ci proponiamo di costruire un processore RISC-V che si in grado di eseguire:
add
, sub
, and
, or
ld
(load doubleword) e sd
(store doubleword)beq
La CPU si compone di diverse parti: unità di controllo, unità aritmetico logica e registri.
I registri, l'unità aritmetico-logica e alcuni bus che li collegano compongono il data path
Due registri particolarmente importanti sono il PC (program counter) e IR (instruction register).
La memoria principale contiene sia istruzioni, sia dati (sotto forma di sequenze di bit).
La CPU esegue ogni istruzione per mezzo di una serie di passi elementari:
Il PC fornisce l'indirizzo della memoria in cui si trova l'istruzione successiva.
Per semplificazione, chiamiamo questa zona della memoria, memoria delle istruzioni e assumiamo che sia solo leggibile e non modificabile.
Assumiamo anche che ci siano due memorie diverse: una per le istruzioni e una per i dati.
La ALU deve capire il tipo di istruzione (quale operazione è richiesta dall'istruzione recuperata dal PC) e quindi per esempio caricare gli operandi passati nell'istruzione. L'unità di controllo (non mostrata nella figura) è colei che effettivamente decodifica le istruzioni.
Caricati gli operandi, si può:
Nelle seguente figura mostriamo l'unità di controllo che contiene i circuiti che effettivamente decodificano le istruzioni, vedremo nel dettaglio ogni singola parte di questi circuiti, non è necessario comprendere adesso il funzionamento di tale unità. (Nella macchina di Von Neumann è la control unit).
La metodologia di temporizzazione usata per determina quando un dato è valido e stabile è la temporizzazione sensibile ai fronti (edge-triggered): una tecnica di temporizzazione nella quale tutti i cambiamenti di stato avvengono su un fronte del segnale di clock. In un sistema digitale sincrono, il segnale di clock (fronti in questo caso) determina quando gli elementi di stato scrivono la loro memoria interna.
Gli ingressi a un elemento di stato devono raggiungere un valore stabile prima che il fronte attivo del clock provochi l'aggiornamento dello stato, supporremo che tutti gli elementi, memorie comprese, siano sensibili al fronte di salita. E quindi gli input che vengono passati a dei dispositivi che memorizzano uno stato devono essere stabili prima che si verifichi un fronte di salita.
La metodologia sensibile ai fronti consente di leggere e scrivere un elemento di stato nello stesso ciclo di clock. Ovvero se l'input ad un elemento di stato è stabile prima del fronte di salita, allora viene cambiato lo stato di tale elemento e per la durata del clock corrente, tale cambiamento può essere letto.
Abbiamo bisogno di alcuni circuiti digitali già visti:
0x0..400020
alla memoria, da essa viene prelevata l'istruzione e viene sputata fuori, nello stesso tempo, il PC passa l'indirizzo della stessa istruzione al sommatore che aggiunge 4 byte per passare alla prossima istruzione. Per cui all'inizio al prossimo fronte, nel PC sarà contenuta l'istruzione successiva.Per questo tipo di istruzioni (R) ci servono altri due circuiti: Register File e la ALU.
Quando viene sputata dalla memoria l'istruzione (indirizzata dal PC)
Ai registri di lettura in ingresso del register file arrivano i due registri (rs1 e rs2).
Il register file restituisce i dati letti nei due registri passati in input.
Nel caso dell'istruzione specifica considerata add x1, x2, x3
è richiesta una somma tra due registri che vengono passati alla ALU.
Il risultato della somma deve essere scritto nel registro indicato nell'istruzione (x1
), tale registro viene passato nel registro di scrittura (nello stesso tempo vengono passati i registri di lettura):
Il dato viene scritto se il segnale di controllo RegWrite è abilitato. (La scrittura avviene sul fronte del clock, per cui prima che il clock si alzi, il dato da scrivere, il numero del registro e il segnale di controllo devono essere validi).
Essendo la scrittura attiva sui fronti, l'architettura può leggere e scrivere lo stesso registro nello stesso ciclo di clock, infatti add x1, x1, x2
è lecita.
Gli ingressi che specificano al register file il numero del registro hanno ampiezza di 5 bit (
Nella ALU vi sono (in azzurro nella figura) 4 bit di controllo per selezionare l'operazione da eseguire. L'uscita zero della ALU è utilizzata per i salti.
Ci servono altri circuiti digitali:
Il datapath diventa:
Diversamente da quanto avviene per il register file, la memoria dati ha necessità di un segnale di lettura esplicito (in azzurro: MemRead).
Come nel caso del register file, la scrittura della memoria è attiva su un fronte del segnale di clock
Quando viene richiesto il caricamento in un registro di un dato in memoria l'istruzione è del tipo I:ld rd, imm(rs1)
Ovvero viene caricato in rd il dato da leggere che viene prelevato da rs1 con offset pari a imm.
Ricordiamo il formato delle istruzioni del tipo I e S:
Quando nel register file arriva questo tipo di richiesta rs1 si trova sempre nello stesso posto (nella sequenza di bit delle istruzioni) e arriva sempre in registro di lettura 1.
Il registro rs1 contiene l'indirizzo di memoria da cui si vuole leggere.
La stessa istruzione viene passata al modulo di estensione del segno che estende il segno dei bit nel campo immediate, prende i 12 bit del campo immediato e ne estende il segno, il motivo è che dobbiamo sommare l'indirizzo base contenuto in rs1 con l'offset, contenuto nel campo immediate, ma l'ALU fa operazioni a 64 bit quindi è necessario estendere il segno del campo immediato.
Tale sequenza (estesa) viene passata come operando alla ALU, che ha ricevuto come operando anche (rs1), l'unità di controllo fa in modo che venga selezionata tra le operazioni della ALU una somma, in questo modo al contenuto del registro rs1 (indirizzo della memoria da cui si vuole leggere) si deve sommare l'offset (per prelevare il byte, la word, o la doubleword di cui si necessita). Tale somma restituisce l'indirizzo della memoria in cui si trova il dato che si vuole leggere.
Se è attivo MemRead allora il dato viene letto. E mandato indietro al register file nell'ingresso Dato da scrivere, RegWrite sarà abilitato e viene scritto il dato nel registro.
Nota che RegWrite è abilitato anche nel caso di una somma, di un or, di un and, poiché il risultato dell'operazione deve essere salvato, quindi sarà necessario scegliere quando si scrive se si scrivere perché è stata eseguita una operazione, o un caricamento da memoria (si risolverà tutto alla fine di questo documento, tieni da parte per ora il fatto che devono essere prese delle scelte, immagina che ogni operazione sia eseguita in autonomia, senza essere in concorrenza con le altre).
Nel caso dell'operazione di store invece:sd rs2, imm(rs1)
in rs1 + offset (imm) viene scritto ciò che è contenuto in rs2, in questo caso RegWrite deve essere non attivo, poiché vogliamo salvare in memoria un dato presente in un registro. In tal caso i registri rs1 e rs2 non devono essere operandi di una qualche operazione della ALU, RegWrite inattivo impedisce proprio questo. Invece la ALU, anche in questo caso, viene usata per estendere il segno del campo immediato da sommare all'indirizzo base (contenuto in rs1). MemWrite sarà attivo e sarà stato ricevuto in Dato scritto il valore del registro rs2 (dato da scrivere in memoria).
Il motivo è che in tal modo viene reso più semplice dirigere i segnali che riguarda tale registro (ed quando necessario anche rs2) verso la stessa porta, risparmiando logica aggiuntiva. Gli operandi di una qualche operazione sono sempre nello stesso posto.
L'istruzione di salto richiede il confronto fra due valori e che consente il trasferimento del controllo a un altro indirizzo del programma a seconda del risultato del confronto.
Ci serve adesso aggiornare il program counter, per passare ad altre istruzioni sparse per il codice che si sta leggende (col PC).
rs1 e rs2 ci serve per fare i confronti e vedere se sono uguali.
La ALU facendo la sottrazione tra due registri riesce a controllare se due registri sono uguali (vedi ALU) facendo la sottrazione e vedendo se il risultato è uguale a 0, mettendo in uscita 1 (nell'uscite zero) se il risultato è uguale a 0.
Nel caso dell'istruzione di branch quello che vogliamo fare è fare puntare il PC all'indirizzo specificato nel campo immediato dell'istruzione, sommato al valore attuale del PC. In questa situazione la ALU è già occupata, poiché è stata appena fatta la sottrazione tra i due registri. Quindi aggiungiamo un altro sommatore.
Con 12 bit riusciamo a fare salti da
Visto che ogni operazione RISC-V è di 32 bit, sappiamo che il numero di cui saltiamo è pari, per cui facendo uno shift a sinistra (abbiamo un bit in più posto a 0) e riusciamo a fare salti maggiori da (
Datapath operazioni di tipo R
Datapath operazioni di tipo I
Datapath operazioni di branch
Come unificare tutto in un unico processore?
Mettiamo dei multiplexer, nei punti in cui bisogna prendere delle scelte che vengono controllati dall'unità di controllo.
Adesso vediamo l'unità di controllo utilizzata da questo processore RISC-V.