Dopo aver visto come progettare l’hardware di basso livello, in questo articolo della serie Realizzare un PC: da zero to hero, ci concentriamo sull’analizzare come questo stesso hardware possa essere utilizzato per “fare cose”.

Essenziale a tal proposito introdurre e comprendere la programmazione a basso livello in linguaggio macchina attraverso la quale un programmatore può far sì che il processore esegua operazioni aritmetiche e logiche, legga e scriva dati in memoria e così via.

Tutto ciò pone le basi, per completare la costruzione di un computer general-purpose pensato per eseguire programmi in questo linguaggio.

Linguaggio Macchina

Un linguaggio macchina altro non è che un formalismo utilizzato per manipolare:

  • Memoria:
    Tutti quei dispositivi che immagazzinano dati e istruzioni in un computer. Da un punto di vista software, la memoria è vista come un array continuo di celle, chiamate parole, di lunghezza fissata e raggiungibili attraverso un indirizzo.
  • Processore:
    CPU o Central Processing Unit, dispositivo capace di eseguire un set specifico di operazioni. La ALU ne rappresenta il cuore.
  • Registri:
    L’accesso in memoria è un’operazione relativamente lenta. Per questo motivo i processori sono normalmente equipaggiati con registri di ridotte dimensioni che però permettono un accesso molto più veloce ai dati in esso contenuti.

Una tipica istruzione in un’architettura a 16 bit può essere:

1010001100011001

Per poterla interpretare è necessario conoscere il così detto instruction set, ovvero quell’insieme di regole tale per cui si assegna un ordine ed un significato ai singoli bit di un’istruzione.

Ad esempio, un’architettura potrebbe dividere le parole in gruppi da 4 bit ciascuna. Partendo da sinistra, il primo gruppo può codificare un’operazione della CPU, mentre i restanti 3 gruppi possono essere dedicati ai registi coinvolti nell’istruzione.

1010 0011 0001 1001

Ammettiamo che 1010 sia il corrispettivo dell’operazione di somma, l’istruzione sopra riportata equivarrebbe a:

R3 = R1 + R9

Per semplificare l’utilizzo di queste istruzioni, viene inoltre sempre fatta un’associazione ad una rappresentazione simbolica. Ad esempio, nel caso precedente, 1010 sarebbe associata all’istruzione ADD.

Questa notazione simbolica prende il nome di linguaggio assembly, mentre il programma che si occupa di tradurre le istruzioni dall’assembly a binario, viene chiamato assembler.

Operazioni Tipiche

Ogni architettura ha il suo linguaggio, possiamo comunque identificare alcuni set di comandi che sono comuni a tutte le piattaforme.

  • Operazioni logiche e aritmetiche: somma, sottrazione, negazione bitwise,…
  • Accesso alla memoria:
    • Indirizzamento diretto: il modo più comune che consiste nello specificare l’indirizzo preciso a cui si vuole fare accesso.
      LOAD R1, 67 // R1 = Memory[67]

       

    • Indirizzamento immediato: usato per caricare costanti invece che indirizzi.
      LOADI R1, 67 // R1 = 67

       

    • Indirizzamento indiretto: in questo caso l’indirizzo non è specificato numericamente nel codice ma viene recuperato da un registro

      LOAD* R1, R2 //R1 = Memory[R2]

       

  • Controllo di flusso: salti condizionali, salti incondizionali sono strumenti essenziali per poter controllare l’esecuzione di un programma.

Hack Computer

Prima di affrontare il progetto di questo capitolo, è necessario presentare l’architettura che stiamo andando a sviluppare.

Il computer Hack è una piattaforma di Von Neumann a 16 bit composta da una CPU, 2 moduli di memoria separati (uno dedicato per le istruzioni e uno dedicato per i dati), e 2 dispositivi di I/O, ovvero uno schermo e una tastiera.

Più nello specifico, gli spazi di indirizzamento di memoria sono grandi 32K (spazio di indirizzamento a 15 bit) e ciascuna parola è di 16 bit.

La CPU può eseguire solo i programmi che risiedono nella instruction memory, la quale è di fatto una memoria di sola lettura. Caricare un nuovo programma, è possibile soltanto sostituendo la memoria con una nuova, un po’ come si fa quando si vuole cambiare gioco in una console.

Il computer ha poi 2 registri a 16 bit fondamentali: A e D. Entrambi possono essere manipolati tramite istruzioni logiche o aritmetiche. La differenza fondamentale tra i 2 è che D può essere utilizzato unicamente come registro per contenere dati. A invece può sia contenere dati che indirizzi per accessi in memoria.

Ad esempio, dovendo effettuare l’operazione:

D = Memory[516] + 1

dobbiamo eseguire le seguenti istruzioni:

A = 516 //Carico 516 in A
D = M + 1 //D = Memory[516] + 1

Il registro A viene anche utilizzato per caricare l’indirizzo a cui saltare dopo un’istruzione di salto.

Non è scopo di questo articolo quello di andare a fondo con le specifiche del linguaggio assembly di Hack presentato dagli autori.

Per questo livello di dettaglio ti rimando alla lettura del paragrafo 4.2 a questo indirizzo.

Il Progetto

In questo capitolo è richiesto lo sviluppo di 2 programmi in linguaggio assembly, i cui file di partenza sono contenuti nella cartella nand2tetrisprojects4 dell’archivio scaricato nella prima lezione:

I programmi da sviluppare sono:

  • moltiplicazione: l’input del programma è rappresentato dai registri R0 e R1. Il programma deve computare il prodotto R0 * R1 e salvarne il risultato in R2. E’ necessario controllare che sia R0 che R1 abbiano un dato >= 0 e che il prodotto sia < 32768 (il massimo numero rappresentabile da un intero a 15 bit).
  • gestione I/O: il programma deve restare in ascolto degli input da tastiera. Non appena un tasto viene premuto, è necessario riempire lo schermo di pixel neri, viceversa va riempito di pixel di colore bianco.

In entrambi i casi è possibile eseguire questi programmi grazie al simulatore raggiungibile nella cartella nand2tetristools in particolare in questo articolo dobbiamo considerare:

Assembler: che prende in pasto un file .asm, ovvero il file assembly da sviluppare, e ne effettua la compilazione in linguaggio macchina permettendone il salvataggio in .hack, il formato eseguibile per il sistema che stiamo sviluppando.

CPUEmulator: che prende in pasto un file .hack generato dall’assembler visto in precedenza e lo esegue. Anche in questo caso sono disponibili dei file .tst che dovremo caricare in questo emulatore per valutare la bontà della nostra soluzione.

Ovviamente, come accennato prima, per poter affrontare con successo questi esercizi, è necessario aver letto le specifiche del linguaggio a questo link.

Le soluzioni ad entrambi i problemi invece li potete trovare qui.

Moltiplicazione

Come di consueto, concentriamoci nel discutere una soluzione specifica. In questo caso analizziamo passo passo la soluzione del programma che effettua la moltiplicazione tra 2 operandi.

Innanzitutto raggiungiamo la cartella nand2tetrisprojects4mult e apriamo il file mul.asm:

Copy to Clipboard

La prima cosa da fare è azzerare il valore del registro su cui andremo a salvare il risultato (R2), dunque procediamo così:

Copy to Clipboard

Con la prima istruzione, carichiamo in A l’indirizzo del registro R2, dopodichè facciamo accesso alla cella di memoria e la sovrascriviamo con 0 (R2 = 0).

Adesso passiamo a caricare R0, salvarci il valore ivi contenuto su D e poi caricare l’indirizzo di memoria corrispondente all’etichetta END. Se il valore di R0 sarà 0, possiamo tranquillamente saltare alla fine del programma in quanto il valore della moltiplicazione sarà identico.

Copy to Clipboard

Se così non fosse, possiamo entrare dentro un loop che effettuerà una somma iterativa per calcolare così la moltiplicazione.

Copy to Clipboard

Inizialmente si carica R1, si aggiorna poi il valore di R1 decrementandolo di un’unità. Il valore di R1 viene sia sovrascritto nello stesso registro e sia salvato nel registro D (riga 3).

Se il valore di D è inferiore a 0, si salta ad una routine di controllo (CHECK) che vedremo tra poco, altrimenti si carica R0, si salva il suo valore nel registro D, si carica poi R2 e si somma il valore di R2 al valore di R0 appena salvato.

Una volta terminato, si salta all’inizio del ciclo e si ricomincia.

L’unico modo per uscire dal ciclo è che R1 decrementato sia inferiore a 0. In quel caso:

Copy to Clipboard

Carico R2 e salvo il valore in D. Controllo che questo valore, ovvero il risultato della moltiplicazione, sia inferiore a 32768. In tal caso salto alla fine del programma, altrimenti setto R2 a 0 e termino.

Vale la pena sottolineare che per convenzione ogni programma Hack termina con un salto incodizionale infinito. In altre parole, il classico return 0 di un programma C, viene qui modellato con l’istruzione 14.

Testiamo la Soluzione

Una volta terminata la scrittura del codice, salviamo tutto nel file .asm.

Apriamo il simulatore denominato Assembler e clicchiamo su File -> Load Source file. Importiamo il file mul.asm appena modificato.

Clicchiamo poi su Run -> Fast Translation per ottenere, a meno di errori di sintassi da sistemare, il file binario eseguibile visibile nel riquadro a destra.

A questo punto clicchiamo su File -> Save destination file e salviamo il prodotto della compilazione come mul.hack.

Lanciamo adesso il CPUEmulator, clicchiamo su File -> Load Program e carichiamo il binario mul.hack; successivamente clicchiamo su File -> Load Script e importiamo il file mul.tst.

Clicchiamo infine su Run -> Run. Se tutto è andato a buon fine, dopo alcune iterazioni dovremmo vedere in basso il messaggio: End of script – Comparison ended successfully.

Conclusioni

In questo articolo ci siamo concentrati sull’analizzare l’importanza del linguaggio assembly di una piattaforma, il quale nella sua semplicità, rappresenta il confine dove hardware e software si incotrano.

Abbiamo inoltre presentato l’architettura di Hack, il computer che vogliamo realizzare e di cui abbiamo già discusso i dettagli implementativi di ALU e RAM.

Nella prossima lezione andremo a vedere come costruire la CPU e come montare tutto insieme per avere il nostro primo computer funzionante!