Tutti i chip finora sviluppati si basano su una logica detta combinatoria, ovvero che dipende unicamente dagli input e che non mantiene alcuno stato nel tempo.

Un computer che si rispetti deve avere però degli elementi ritentivi, comunemente chiamati memorie.

Questi dispositivi danno vita ad un’altra famiglia di logica, quella sequenziale, che permette di introdurre il concetto di tempo all’interno di un sistema informatico e quindi anche il concetto di memorizzazione.

Un po’ come abbiamo fatto nel primo di questa serie di articoli, partiamo da un dispositivo che chiameremo primitivo e che non dovremo andare a sviluppare, il flip-flop. Su questo andremo a basare tutti gli sviluppi richiesti in modo da giungere alla realizzazione di una versione a 16 bit di memoria RAM.

Il Tempo Scorre

Per poter costruire dispositivi che ricordino è innanzitutto necessario sviluppare un modo per rappresentare lo scorrere del tempo.

In un sistema informatico si introduce questo concetto e quindi la capacità di ritenere informazioni attraverso 5 strumenti principali:

  • Clock:
    E’ il segnale che scandisce il passare del tempo in un sistema informatico. Viene realizzato attraverso un oscillatore che alterna costantemente tra 2 stati principali: il tick (segnale basso) e il tock (segnale alto). Il tempo che intercorre tra un tick e un tock viene chiamato ciclo. Questo segnale viene condiviso con tutta l’elettronica di un sistema, così da poterne sincronizzare il funzionamento.
  • Flip-Flops:
    Si tratta dell’elemento base che permette di passare da una logica combinatoria ad una sequenziale. In questi articoli faremo uso della variante chiamata data flip-flop o DFF. L’obiettivo di questo dispositivo è quello di realizzare una piccola macchina a stati che al tempo t ricordi lo stato in ingresso al tempo precedente t-1. Detto con una semplice equazione, un DFF realizza:

    out( t ) = in( t – 1 )

    Questo comportamento elementare forma le basi per tutti quei componenti hardware che necessitano di mantenere uno stato (celle, registri, RAM, …).

  • Registri:
    Un registro è un dispositivo che può memorizzare un valore nel tempo, dunque la funzione che realizzare è:out( t ) = out( t – 1 )Per poterlo costruire dobbiamo fare uso di un DFF, insieme a un circuito combinatorio composto da un multiplexer che ne gestisca il comportamento.
    Così facendo, fintanto che il bit di load è a 0, il DFF viene alimentato con un nuovo valore ad ogni ciclo. Quando invece load è a 1, il multiplexer fa sì che il DFF restituisca costantemente l’ultimo valore memorizzato in ingresso.
    Una volta sviluppato il meccanismo base per ricordare 1 singolo bit, è possibile costruire dei registri di larghezza variabile semplicemente affiancando N di queste celle, con N che può variare a seconda dell’architettura in 16, 32 o 64 (in gergo chiamate parole).
  • Memorie:
    Dopo aver realizzato i registri, abbiamo la strata spianata per costruire banchi di memoria di lunghezza arbitraria. Ciò può essere fatto principalmente affiancando vari registri così da realizzare una Memoria ad Accesso Casuale, detta più comunemente RAM.
    Come anche il nome suggerisce, una RAM deve permettere l’accesso a qualsiasi parola senza alcuna restrizione in termini di ordine di accesso e di velocità di lettura. Ciò può essere realizzato assegnando ad ogni registro un indirizzo logico univoco con il quale è possibile accedervi.
    Una memoria dunque avrà 3 elementi principali in input: un dato in ingresso, un indirizzo e un bit di load.
  • Le specifiche principali di una RAM infine sono la sua width (larghezza di ciascuna delle sue parole) e la sua size (il numero di parole che la compone).
  • Contatori:
    Un contatore è un chip sequenziale che, dato un valore in ingresso, lo incrementa normalmente di 1 unità rispetto al suo stato precedente.

    out ( t ) = out( t – 1 ) + c

    con c = 1.
    Viene tipicamente realizzato con alcune funzionalità addizionali, come un comando di reset per azzerare il registro interno, uno di load per far ripartire il conteggio da un valore preciso e all’occorrenza un bit per determinare il segno dell’incremento.

Sincronizzazione di un Sistema Informatico

La differenza principale tra la logica combinatoria e quella sequenziale è il modo di reagire al variare degli input.

In una logica combinatoria, l’output varia istantaneamente al variare dell’input in quanto non c’è alcun elemento che ne regola il comportamento.

Viceversa nella logica sequenziale, l’introduzione dei flip-flop permette di discretizzare il passare del tempo, così facendo siamo in grado di reagire alla variazione di input soltanto quando c’è una transizione di stato tra un tick e un tock.

Questa discretizzazione dei circuiti sequenziali porta un effetto fondamentale: può essere usato per sincronizzare l’intera architettura informatica.

Prendiamo ad esempio la ALU che abbiamo progettato nella scorsa lezione. E’ composta nella sua totalità da circuiti combinatori, ciò implica che quando un valore in input cambierà, l’output della rete cambierà istantaneamente.

Amettiamo di voler eseguire la funzione x + y, in un sistema complesso a causa di vari limiti fisici (distanza dei componenti, resistenze, interferenze, rumori casuali, …) i segnali rappresentanti x e y arriveranno alla ALU con tempi differenti.

La ALU dalla sua, non avendo alcun concetto di tempo, continuerà a sommare questi segnali instabili producendo per un certo lasso di tempo dei risultati incorretti.

Come risolvere questo problema? Beh, considera che la ALU è collegata sempre a una qualche sorta di circuito sequenziale (registri, RAM o altro), ci basterà semplicemente settare la durata del clock ad un tempo leggermente maggiore del tempo di percorrenza massimo tra chip. In questo modo siamo certi che tutti questi circuiti sequenziali si aggiorneranno solo quando i segnali della ALU saranno validi.

Il Progetto

Adesso che abbiamo un’idea chiara di quali sono i personaggi in scena, passiamo alla realizzazione pratica di questi sistemi.

Come già visto in precedenza, l’obiettivo è quello di sviluppare i chip presentati utilizzando i dispositivi relizzati nelle scorse lezioni e il DFF come elemento primitivo.

Anche in questo caso andremo ad usare il simulatore gratuito fornito e che abbiamo installato nella prima lezione.

Questa volta però ci focalizzeremo sulla cartella nand2tetris/projects/03/ dove sono contenute le specifiche dei vari dispositivi da realizzare.

L’obiettivo in questo capitolo è di realizzare nell’ordine proposto:

  • Bit: cella a singolo bit.
  • Registro: registro a 16 bit.
  • RAM8: memoria a 8 registri ciascuno a 16 bit.
  • RAM64: memoria a 64 registri.
  • RAM512: memoria a 512 registri.
  • RAM4K: memoria a 4096 registri.
  • RAM16K: memoria a 16384 registri.
  • PC: (program counter) contatore a 16 bit.

Come consigliato dagli autori, quando si costruisce la RAM è meglio utilizzare le versioni dei chip sottostanti fornite nella cartella nand2tetris/tools/builtInChips così facendo si evitano problemi di performance che renderebbero la simulazione più farragginosa.

La RAM

Abbiamo già visto come fare una cella che memorizzi 1 bit, lascio quindi a voi la codifica in HDL. Anche la realizzazione di un registro è pressochè banale, concentriamoci allora sulla progettazione di una RAM a 8 registri!

Come sempre, poichè il modo migliore per imparare è fare, non tratterò nel dettaglio la realizzazione degli altri circuiti.

Ovviamente potete scaricare le consuete soluzioni al capitolo a questo link.

Raggiungendo la cartella di progetto nand2tetris/projects/03/a apriamo il file RAM8.hdl, troveremo la definizione di questo dispositivo:

Copy to Clipboard

Dunque, la sudetta RAM deve essere comporta da 8 registri a 16 bit, deve avere un bit di load per specificare quando caricare un nuovo dato in memoria e infine deve avere 3 bit di indirizzamento con cui poter identificare il registro specifico su cui si vuole operare.

La soluzione a questo circuito è composta dai seguenti componenti:

Nello specifico, andiamo ad usare 1 demultiplexer a 8 vie che, dato un indirizzo a 3 bit, permette di selezionare il registro voluto e gli indirizza il valore attuale del bit di load.

L’input a 16 bit viene invece passato ad ogni registro i cui output confluiscono in un multiplexer a 8 vie che usa lo stesso indirizzo appena usato per canalizzare in uscita il valore del registro selezionato.

In HDL la soluzione è la seguente:

Copy to Clipboard

Testiamo la Soluzione

Ok, adesso abbiamo una possibile soluzione al nostro problema. Non ci resta che valutarne la correttezza.

Per fare questo, lanciamo il simulatore dalla cartella nand2tetris/tools e clicchiamo su File -> Load Chip. Carichiamo il file RAM8.hdl che abbiamo appena modificato.

Clicchiamo poi su File -> Load Script e selezioniamo il file RAM8.tst contenuto nella cartella nand2tetris/projects/03.

A questo punto, lanciamo il test cliccando Run -> Run e attendiamo che la procedura termini.

Se tutto è andato a buon fine dovremmo vedere il classico messaggio del tipo: End of script – comparison ended successfully.

Conclusioni

In questa lezione abbiamo discusso come introdurre il concetto di tempo in un’architettura informatica e come realizzare le memorie. Ci siamo poi concentrati sulla progettazione e test di una RAM a 8 registri.

Le altre soluzioni al capitolo le trovi a questo indirizzo.