Assembly Base con NASM

Capitolo 8: Struttura hardware della memoria


L'evoluzione tecnologica che interessa il mondo del computer, sta assumendo col passare degli anni ritmi sempre più vertiginosi; nel corso di questa evoluzione, uno dei componenti che ha acquisito una importanza sempre maggiore è sicuramente la memoria, al punto che, al giorno d'oggi, non la si considera più come una delle tante periferiche, ma come una delle parti fondamentali che insieme alla CPU formano il cuore del computer stesso. Lo scopo della memoria è quello di immagazzinare informazioni in modo temporaneo o permanente; questa distinzione porta a classificare le memorie in due grandi categorie: Le memorie di lavoro vengono così chiamate in quanto sono destinate a contenere temporaneamente tutte le informazioni (dati e istruzioni) che devono essere sottoposte ad elaborazione da parte del computer; la fase di elaborazione deve svolgersi nel più breve tempo possibile e quindi è fondamentale che le memorie di lavoro garantiscano velocità di accesso elevatissime nelle operazioni di I/O. Proprio per tale motivo, queste particolari memorie vengono realizzate mediante dispositivi elettronici a semiconduttore che, come abbiamo visto nel Capitolo 5, presentano tempi di risposta estremamente ridotti; si tenga presente comunque che la velocità operativa delle memorie di lavoro non può certo competere con le prestazioni vertiginose delle attuali CPU.

Le memorie di massa hanno lo scopo di immagazzinare in modo permanente grosse quantità di informazioni che possono essere utilizzate quando se ne ha bisogno; nel momento in cui si ha la necessità di elaborare queste informazioni, si procede al loro caricamento nella memoria di lavoro.
Le memorie di massa vengono realizzate principalmente ricorrendo a supporti ricoperti di materiale magnetico (ossidi di ferro) che attraverso un opportuno circuito elettrico possono essere facilmente magnetizzati (scritti) e smagnetizzati (cancellati). Ad esempio, gli Hard Disk sono costituiti da uno o più dischi di alluminio ricoperti da ossido di ferro, mentre gli ormai abbandonati Floppy Disk sono costituiti da un disco di materiale plastico ricoperto sempre da ossido di ferro; uno dei punti di forza dei supporti magnetici è dato sicuramente dalla enorme capacità di memorizzazione (centinaia di GiB), mentre lo svantaggio principale è dato dalla velocità di accesso non molto elevata.
Un altro tipo di memoria di massa è rappresentato dai Compact Disc e dai Digital Versatile Disc, scrivibili e riscrivibili attraverso tecnologie laser; i CD/DVD sono caratterizzati da discrete velocità di accesso, mentre la capacità di immagazzinare informazioni appare insufficiente.

In questo capitolo ci occuperemo delle memorie di lavoro che verranno analizzate dal punto di vista hardware; nel capitolo successivo, invece, si parlerà della memoria di lavoro dal punto di vista del programmatore.

8.1 Configurazione delle memorie di lavoro

Prima di analizzare la configurazione interna delle memorie di lavoro, è necessario affrontare un aspetto molto delicato che induce spesso in errore i programmatori meno esperti; questo errore molto frequente viene chiamato in gergo: fuori di uno. Supponiamo, ad esempio, di voler recintare un lato di un terreno lungo 9 metri disponendo una serie di paletti ad 1 metro di distanza l'uno dall'altro; la domanda che ci poniamo è: quanti paletti sono necessari?
Molte persone rispondono istintivamente 9, dimenticando così il paletto iniziale che potremmo definire come paletto numero 0; la Figura 8.1 chiarisce questa situazione. Come si può notare dalla Figura 8.1, assegnando l'indice 0 al primo paletto, il secondo paletto avrà indice 1, il terzo 2, il quarto 3 e così via, sino all'ultimo paletto (il decimo) che avrà indice 9; complessivamente abbiamo bisogno di 9 paletti (dal numero 1 al numero 9) più il paletto iniziale (il numero 0) per un totale di 10 paletti. Questa situazione trae in inganno molte persone che leggendo l'indice 9 sull'ultimo paletto, arrivano alla conclusione che siano presenti solo 9 paletti; questa valutazione errata è dovuta al fatto che gli indici partono da 0 anziché da 1.
È importantissimo capire questo concetto in quanto nel mondo del computer ci si imbatte spesso in situazioni di questo genere; nei precedenti capitoli abbiamo visto numerosi esempi di sequenze di elementi indicizzati a partire da 0. Consideriamo, ad esempio, un computer con Address Bus a 16 linee; abbiamo visto che per convenzione la prima linea dell'Address Bus viene indicata con A0 anziché A1, la seconda linea viene indicata con A1 anziché A2 e così via, sino all'ultima linea (la sedicesima) che viene indicata con A15 anziché A16. Complessivamente abbiamo quindi 15 linee (da A1 ad A15) più la linea di indice 0 (A0) per un totale di 16 linee.
Con 16 linee possiamo indirizzare 216=65536 celle di memoria; se assegnamo alla prima cella l'indirizzo 0, allora la seconda cella avrà indirizzo 1, la terza 2, la quarta 3 e così via, sino all'ultima cella che avrà indirizzo 65535 e non 65536. Complessivamente possiamo indirizzare 65535 celle (dalla 1 alla 65535) più la cella di indice 0 per un totale di 65536 celle.
Questa convenzione, largamente utilizzata nel mondo del computer, permette di ottenere enormi semplificazioni, sia software che hardware; osserviamo, ad esempio, che con il precedente Address Bus a 16 linee, la prima cella di memoria si trova all'indirizzo binario 0000000000000000b (0d), mentre l'ultima cella si trova all'indirizzo binario 1111111111111111b (65535d). Se avessimo fatto partire gli indici da 1, allora la prima cella si sarebbe trovata all'indirizzo binario 0000000000000001b (1d), mentre l'ultima cella si sarebbe trovata all'indirizzo binario 10000000000000000b (65536d); quest'ultimo indirizzo è formato da 17 bit e ci costringerebbe ad usare un Address Bus a 17 linee!
La situazione appena descritta si presenta spesso anche nella scrittura dei programmi; nel linguaggio C, ad esempio, gli indici dei vettori partono da 0 e non da 1. Supponiamo allora di avere la seguente definizione:
int vett[10]; /* vettore di 10 elementi di tipo int */
Il primo elemento di questo vettore è rappresentato da vett[0] e, di conseguenza, l'ultimo elemento (il decimo) è vett[9] e non vett[10]; complessivamente abbiamo 9 elementi (da vett[1] a vett[9]) più l'elemento di indice 0 (vett[0]) per un totale di 10 elementi.
In definitiva quindi, se abbiamo una generica sequenza di n elementi e assegnamo l'indice 0 al primo elemento, allora l'ultimo elemento avrà indice n-1 e non n (l'elemento di indice n quindi non esiste); complessivamente avremo n-1 elementi (dal numero 1 al numero n-1) più l'elemento di indice 0 per un totale di n elementi.

Dopo queste importanti precisazioni, possiamo passare ad analizzare l'organizzazione interna delle memorie di lavoro; tutte le considerazioni che seguono si riferiscono come al solito alle piattaforme hardware basate sulle CPU della famiglia 80x86.
La CPU vede la memoria di lavoro come un vettore di celle, cioè come una sequenza di celle consecutive e contigue; come già sappiamo, ciascuna cella ha una ampiezza pari a 8 bit. Nel caso, ad esempio, di una memoria di lavoro formata da 32 celle, indicando una generica cella con il simbolo Ck (k compreso tra 0 e 31) si verifica la situazione mostrata in Figura 8.2. Come si può notare, ogni cella viene individuata univocamente dal relativo indice che coincide esattamente con l'indirizzo fisico della cella stessa; se la CPU vuole accedere ad una di queste celle, deve specificare il relativo indirizzo che viene poi caricato sull'Address Bus. Se vogliamo accedere, ad esempio, alla trentesima cella, dobbiamo caricare sull'Address Bus l'indirizzo 29 (ricordiamoci che gli indici partono da 0); a questo punto la cella C29 viene messa in collegamento con la CPU che può così iniziare il trasferimento dati attraverso il Data Bus.
Il problema che si presenta è dato dal fatto che, come è stato detto nel precedente capitolo, qualunque operazione effettuata dal computer deve svolgersi in un intervallo di tempo ben definito e cioè, in un numero ben definito di cicli di clock; questo discorso vale naturalmente anche per gli accessi alla memoria di lavoro. In sostanza, l'accesso da parte della CPU ad una qualsiasi cella di memoria deve svolgersi in un intervallo di tempo costante, indipendente quindi dalla posizione in memoria (indice) della cella stessa; per soddisfare questa condizione, le memorie di lavoro vengono organizzate sotto forma di matrice di celle. Una matrice non è altro che una tabella di elementi disposti su m righe e n colonne; il numero complessivo di elementi è rappresentato quindi dal prodotto m*n. Volendo organizzare, ad esempio, il vettore di Figura 8.2 sotto forma di matrice di celle con 8 righe e 4 colonne (8*4=32), si ottiene la situazione mostrata in Figura 8.3. Osserviamo subito che le 8 righe vengono individuate attraverso gli indici da 0 a 7 (indici di riga), mentre le 4 colonne vengono individuate attraverso gli indici da 0 a 3 (indici di colonna); ciascun elemento della matrice viene univocamente individuato attraverso i corrispondenti indice di riga e indice di colonna che costituiscono le coordinate dell'elemento stesso.
Questa organizzazione a matrice permette alla CPU di accedere a qualsiasi cella di memoria in un intervallo di tempo costante; la CPU non deve fare altro che specificare l'indice di riga e l'indice di colonna della cella desiderata. Questi due indici abilitano la cella che si trova all'incrocio tra la riga e la colonna specificate dalla CPU; appare evidente che con questa tecnica, l'accesso ad una cella qualunque richiede un intervallo di tempo costante e quindi assolutamente indipendente dalla posizione della cella stessa.
In precedenza però è stato detto che la CPU vede la memoria sotto forma di vettore di celle; dobbiamo capire quindi come sia possibile ricavare un indice di riga e un indice di colonna da un indirizzo lineare destinato ad un vettore come quello di Figura 8.2.
A tale proposito osserviamo innanzi tutto che il vettore di Figura 8.2 può essere convertito nella matrice di Figura 8.3 raggruppando le celle a 4 a 4; le celle da C0 a C3 formano la prima riga della matrice, le celle da C4 a C7 formano la seconda riga della matrice e così via. In questo modo è semplicissimo passare da una cella della matrice di Figura 8.3 alla corrispondente cella del vettore di Figura 8.2; data, ad esempio, la generica cella Ci,j della matrice di Figura 8.3, l'indice k della corrispondente cella del vettore di Figura 8.2 si ricava dalla seguente formula:
k = (i * 4) + j
(4 rappresenta il numero di colonne della matrice).

Consideriamo, ad esempio, C2,1 che è la decima cella della matrice di Figura 8.3; la corrispondente cella del vettore di Figura 2 è quella individuata dall'indice:
(2 * 4) + 1 = 8 + 1 = 9
Infatti, C9 è proprio la decima cella del vettore di Figura 8.2.

Vediamo ora come si procede per la conversione inversa; dato cioè l'indice k di una cella del vettore di Figura 8.2, dobbiamo ricavare l'indice di riga i e l'indice di colonna j della corrispondente cella della matrice di Figura 8.3. Osserviamo subito che per indirizzare 32 celle abbiamo bisogno di un Address Bus a log232=5 linee; con 5 linee, infatti, possiamo rappresentare 25=32 indirizzi (da 0 a 31). Siccome la matrice di Figura 8.3 è formata da 8 righe e 4 colonne, suddividiamo le 5 linee dell'Address Bus in due gruppi; il primo gruppo è formato da log28=3 linee (A2, A3 e A4), mentre il secondo gruppo è formato da log24=2 linee (A0 e A1). Questi due gruppi di linee ci permettono di individuare l'indice di riga e l'indice di colonna della cella desiderata; per dimostrarlo ripetiamo il precedente esempio. Vogliamo accedere quindi alla cella C9 del vettore di Figura 8.2; in binario (a 5 bit) l'indirizzo 9 si scrive 01001b. Questo indirizzo viene caricato sull'Address Bus per cui otteniamo A0=1, A1=0, A2=0, A3=1, A4=0. Le tre linee da A2 a A4 contengono il codice binario 010b che tradotto in base 10 rappresenta il valore 2; questo valore rappresenta l'indice di riga. Le due linee da A0 a A1 contengono il codice binario 01b che tradotto in base 10 rappresenta il valore 1; questo valore rappresenta l'indice di colonna. La cella individuata nella matrice di Figura 8.3 sarà quindi C2,1; questa cella corrisponde proprio alla cella C9 del vettore di Figura 8.2.
Vediamo un ulteriore esempio relativo all'accesso alla cella C31 che è l'ultima cella del vettore di Figura 8.2; in binario (a 5 bit) l'indirizzo 31 si scrive 11111b. Questo indirizzo viene caricato sull'Address Bus per cui otteniamo A0=1, A1=1, A2=1, A3=1, A4=1. Le tre linee da A2 a A4 contengono il codice binario 111b che tradotto in base 10 rappresenta il valore 7; questo valore rappresenta l'indice di riga. Le due linee da A0 a A1 contengono il codice binario 11b che tradotto in base 10 rappresenta il valore 3; questo valore rappresenta l'indice di colonna. La cella individuata nella matrice di Figura 8.3 sarà quindi C7,3; come si può notare in Figura 8.3, questa è proprio l'ultima cella della matrice.

Traducendo in pratica le considerazioni appena svolte, otteniamo la situazione mostrata in Figura 8.4; questo circuito si riferisce ad un computer con una memoria di lavoro da 32 byte, Address Bus a 5 linee e Data Bus a 8 linee. La parte di indirizzo contenuta nelle linee A2, A3 e A4, raggiunge il circuito chiamato Decodifica Riga; questo circuito provvede a ricavare l'indice di riga della cella da abilitare. La parte di indirizzo contenuta nelle linee A0 e A1, raggiunge il circuito chiamato Decodifica Colonna; questo circuito provvede a ricavare l'indice di colonna della cella da abilitare. L'unica cella che viene abilitata è quella che si trova all'incrocio tra i due indici appena determinati; a questo punto il Data Bus viene connesso alla cella abilitata rendendo così possibile il trasferimento dati tra la cella stessa e la CPU.
Nella parte destra della Figura 8.4 si nota la presenza del Data Bus a 8 linee e della logica di controllo che comprende le tre linee CS, W e R; lungo la linea CS transita un segnale chiamato Chip Select (selezione chip di memoria). Se CS=1 la memoria di lavoro viene abilitata e la CPU può comunicare con essa attraverso il Data Bus; se CS=0 la memoria di lavoro è disabilitata e la CPU può comunicare con altri dispositivi di I/O attraverso il Data Bus.
Se viene richiesto un accesso in scrittura (write) alla memoria di lavoro, la logica di controllo invia i due segnali W=1 e R=0 (con CS=1); in questo modo, la porta AND di sinistra produce in uscita un livello logico 1 che abilita il Controllo Scrittura, mentre la porta AND di destra produce in uscita un livello logico 0 che disabilita il Controllo Lettura.
Se viene richiesto un accesso in lettura (read) alla memoria di lavoro, la logica di controllo invia i due segnali W=0 e R=1 (con CS=1); in questo modo, la porta AND di sinistra produce in uscita un livello logico 0 che disabilita il Controllo Scrittura, mentre la porta AND di destra produce in uscita un livello logico 1 che abilita il Controllo Lettura.

8.1.1 Accesso in memoria con Data Bus a 8 bit

La situazione mostrata dalla Figura 8.4 è molto semplice grazie al fatto che l'ampiezza del Data Bus (8 bit) coincide esattamente con l'ampiezza in bit di ogni cella di memoria; in questo caso le 8 linee del Data Bus vengono connesse agli 8 bit dell'unica cella di memoria abilitata. Per chiarire meglio questo aspetto osserviamo la Figura 8.5 che mostra le prime 8 celle del vettore di memoria di Figura 8.2. In questo esempio il Data Bus risulta connesso alla cella C3 (quarta cella) della memoria di lavoro; com'era prevedibile, la linea D0 risulta connessa al bit in posizione 0 di C3, la linea D1 risulta connessa al bit in posizione 1 di C3 e così via, sino alla linea D7 che risulta connessa al bit in posizione 7 di C3. Un Data Bus a 8 linee può essere posizionato su una qualunque cella secondo lo schema visibile in Figura 8.5; è assolutamente impossibile che il Data Bus possa posizionarsi a cavallo tra due celle.
Appare evidente il fatto che con un Data Bus a 8 linee è possibile gestire via hardware trasferimenti di dati aventi una ampiezza massima di 8 bit; se vogliamo trasferire un blocco di dati più grande di 8 bit, dobbiamo suddividerlo in gruppi di 8 bit. Un dato avente una ampiezza di 8 bit viene chiamato BYTE (da non confondere con l'unità di misura byte); il termine BYTE definisce quindi un tipo di dato che misura 1 byte, ossia 8 bit.
L'ampiezza in bit del Data Bus definisce anche la cosiddetta parola del computer; nel caso di Figura 8.4 e di Figura 8.5 abbiamo a che fare quindi con un computer avente parola di 8 bit.

8.1.2 Accesso in memoria con Data Bus a 16 bit

Passiamo ora al caso di un computer avente parola di 16 bit, cioè Data Bus a 16 linee; il problema che si presenta è dato dal fatto che la memoria di lavoro è suddivisa fisicamente in celle da 8 bit, mentre il Data Bus ha una ampiezza di 16 bit. Per risolvere questo problema, la memoria di lavoro viene suddivisa logicamente in coppie di celle adiacenti; nel caso, ad esempio, del vettore di memoria di Figura 8.2, otteniamo le coppie (C0, C1), (C2, C3), (C4, C5) e così via, sino alla coppia (C30, C31).
Un Data Bus a 16 linee può essere posizionato su una qualunque di queste coppie; è assolutamente impossibile quindi che il Data Bus possa posizionarsi a cavallo tra due coppie di celle. Per chiarire meglio questo aspetto osserviamo la Figura 8.6 che mostra le prime 8 celle del vettore di memoria di Figura 8.2, suddivise in coppie. In questo esempio il Data Bus risulta connesso alla coppia (C2, C3) della memoria di lavoro; le linee da D0 a D7 risultano connesse alla cella C2, mentre le linee da D8 a D15 risultano connesse alla cella C3.
Dalla Figura 8.6 si deduce che con un Data Bus a 16 linee è possibile gestire via hardware trasferimenti di dati aventi una ampiezza di 8 bit o di 16 bit; se vogliamo trasferire un blocco di dati più grande di 16 bit, dobbiamo suddividerlo in gruppi di 8 o 16 bit. In precedenza abbiamo visto che un dato avente una ampiezza di 8 bit viene chiamato BYTE; un dato avente ampiezza di 16 bit, invece, viene chiamato WORD. Non bisogna confondere il termine WORD con l'unità di misura word; infatti, il termine WORD indica un tipo di dato che misura 1 word, ossia 2 byte, ossia 16 bit.
Come già sappiamo, l'indirizzo di un dato di tipo BYTE coincide con l'indirizzo della cella che lo contiene; qual è, invece, l'indirizzo di un dato di tipo WORD?
Per rispondere a questa domanda bisogna tenere presente che le CPU della famiglia 80x86, nel disporre i dati in memoria, seguono una convenzione chiamata little-endian; questa convenzione prevede che i dati formati da due o più BYTE vengano disposti in memoria con il BYTE meno significativo che occupa l'indirizzo più basso.
Supponiamo, ad esempio, di avere il dato a 16 bit 0111001100001111b che si trova in memoria a partire dall'indirizzo 350; la convenzione little-endian prevede che questo dato venga disposto in memoria secondo lo schema mostrato in Figura 8.7. Come si può notare, il BYTE meno significativo 00001111b viene disposto nella cella 350, mentre il BYTE più significativo 01110011b viene disposto nella cella 351; altre CPU come, ad esempio, quelle prodotte dalla Motorola, utilizzano invece la convenzione inversa (big-endian). La Figura 8.7 ci permette anche di osservare che se vogliamo tracciare uno schema della memoria su un foglio di carta, ci conviene disporre gli indirizzi in ordine crescente da destra verso sinistra; in questo modo i dati binari (o esadecimali) contenuti nelle varie celle ci appaiono disposti nel verso giusto (cioè, con il peso delle cifre che cresce da destra verso sinistra).

Analizziamo ora come avviene il trasferimento dati di tipo BYTE e WORD con un Data Bus a 16 linee.
In riferimento alla Figura 8.6, supponiamo di voler leggere il BYTE contenuto nella cella C2; il Data Bus viene posizionato sulla coppia (C2, C3) e l'accesso alla cella C2 avviene attraverso le linee da D0 a D7.
Supponiamo di voler leggere il BYTE contenuto nella cella C3; il Data Bus viene posizionato sulla coppia(C2, C3) e l'accesso alla cella C3 avviene attraverso le linee da D8 a D15.
Supponiamo di voler leggere il BYTE contenuto nella cella C4; il Data Bus viene posizionato sulla coppia (C4, C5) e l'accesso alla cella C4 avviene attraverso le linee da D0 a D7.

Le considerazioni appena svolte ci fanno capire che con un Data Bus a 16 linee i dati di tipo BYTE non presentano alcun problema di allineamento in memoria; questo significa che un dato di tipo BYTE, indipendentemente dal suo indirizzo, richiede un solo accesso in memoria per essere letto o scritto.

In riferimento alla Figura 8.6, supponiamo di voler leggere la WORD contenuta nelle celle C2 e C3; il Data Bus viene posizionato sulla coppia (C2, C3) e l'accesso alle celle C2 e C3 avviene attraverso le linee da D0 a D15.
Supponiamo di voler leggere la WORD contenuta nelle celle C3 e C4; il Data Bus viene prima posizionato sulla coppia (C2, C3) e attraverso le linee da D8 a D15 viene letto il BYTE che si trova nella cella C3. Successivamente, il Data Bus viene posizionato sulla coppia (C4, C5) e attraverso le linee da D0 a D7 viene letto il BYTE che si trova nella cella C4; complessivamente vengono effettuati due accessi in memoria.

Le considerazioni appena svolte ci fanno capire che con un Data Bus a 16 linee i dati di tipo WORD non presentano alcun problema di allineamento in memoria purché il loro indirizzo sia un numero pari (0, 2, 4, 6, etc); un dato di tipo WORD che si trova ad un indirizzo dispari, richiede due accessi in memoria per essere letto o scritto!
Si tenga presente che il disallineamento dei dati in memoria può provocare sensibili perdite di tempo anche con le potentissime CPU dell'ultima generazione; proprio per evitare questi problemi, l'Assembly e molti linguaggi di programmazione di alto livello forniscono ai programmatori tutti gli strumenti necessari per allineare correttamente i dati in memoria.

8.1.3 Accesso in memoria con Data Bus a 32 bit

Anche se il meccanismo dovrebbe essere ormai chiaro, analizziamo un ulteriore caso che si riferisce ad un computer avente parola di 32 bit, cioè Data Bus a 32 linee; in questo caso il problema da affrontare riguarda il fatto che la memoria di lavoro è suddivisa fisicamente in celle da 8 bit, mentre il Data Bus ha una ampiezza di 32 bit. Per risolvere questo problema, la memoria di lavoro viene suddivisa logicamente in quaterne di celle adiacenti; nel caso, ad esempio, del vettore di memoria di Figura 8.2, otteniamo le quaterne: (C0, C1, C2, C3), (C4, C5, C6, C7) e cosi via, sino alla quaterna (C28, C29, C30, C31).
Un Data Bus a 32 linee può essere posizionato su una qualunque di queste quaterne; è assolutamente impossibile quindi che il Data Bus possa posizionarsi a cavallo tra due quaterne di celle. Per chiarire meglio questo aspetto osserviamo la Figura 8.8 che mostra le prime 8 celle del vettore di memoria di Figura 8.2, suddivise in quaterne. In questo esempio il Data Bus risulta connesso alla quaterna (C0, C1, C2, C3) della memoria di lavoro; le linee da D0 a D7 risultano connesse alla cella C0, le linee da D8 a D15 risultano connesse alla cella C1, le linee da D16 a D23 risultano connesse alla cella C2 e le linee da D24 a D31 risultano connesse alla cella C3.
Dalla Figura 8.8 si deduce che con un Data Bus a 32 linee è possibile gestire via hardware trasferimenti di dati aventi una ampiezza di 8 bit, di 16 bit o di 32 bit; se vogliamo trasferire un blocco di dati più grande di 32 bit, dobbiamo suddividerlo in gruppi di 8, 16 o 32 bit. Un dato avente ampiezza di 32 bit viene chiamato DWORD (da non confondere con l'unità di misura dword o double word); il termine DWORD indica quindi un tipo di dato che misura 1 dword, ossia 2 word, ossia 4 byte, ossia 32 bit. Analizziamo ora come avviene il trasferimento dati di tipo BYTE, WORD e DWORD con un Data Bus a 32 linee.
In riferimento alla Figura 8.8, supponiamo di voler leggere il BYTE contenuto nella cella C0; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alla cella C0 avviene attraverso le linee da D0 a D7.
Supponiamo di voler leggere il BYTE contenuto nella cella C1; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alla cella C1 avviene attraverso le linee da D8 a D15.
Supponiamo di voler leggere il BYTE contenuto nella cella C2; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alla cella C2 avviene attraverso le linee da D16 a D23.
Supponiamo di voler leggere il BYTE contenuto nella cella C3; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alla cella C3 avviene attraverso le linee da D24 a D31.
Supponiamo di voler leggere il BYTE contenuto nella cella C4; il Data Bus viene posizionato sulla quaterna (C4, C5, C6, C7) e l'accesso alla cella C4 avviene attraverso le linee da D0 a D7.

Anche in questo caso si nota subito che con un Data Bus a 32 linee i dati di tipo BYTE non presentano alcun problema di allineamento in memoria; un dato di tipo BYTE, indipendentemente dal suo indirizzo, richiede quindi un solo accesso in memoria per essere letto o scritto.

In riferimento alla Figura 8.8, supponiamo di voler leggere la WORD contenuta nelle celle C0 e C1; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alle celle C0 e C1 avviene attraverso le linee da D0 a D15.
Supponiamo di voler leggere la WORD contenuta nelle celle C1 e C2; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alle celle C1 e C2 avviene attraverso le linee da D8 a D23.
Supponiamo di voler leggere la WORD contenuta nelle celle C2 e C3; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alle celle C2 e C3 avviene attraverso le linee da D16 a D31.
Supponiamo di voler leggere la WORD contenuta nelle celle C3 e C4; il Data Bus viene prima posizionato sulla quaterna (C0, C1, C2, C3) e attraverso le linee da D24 a D31 viene letto il BYTE che si trova nella cella C3. Successivamente, il Data Bus viene posizionato sulla quaterna (C4, C5, C6, C7) e attraverso le linee da D0 a D7 viene letto il BYTE che si trova nella cella C4; complessivamente vengono effettuati due accessi in memoria.

Le considerazioni appena svolte ci fanno capire che con un Data Bus a 32 linee i dati di tipo WORD non presentano alcun problema di allineamento in memoria purché siano interamente contenuti all'interno di una quaterna di celle; se la WORD si trova a cavallo tra due quaterne, richiede due accessi in memoria per essere letta o scritta. Un metodo molto semplice per evitare questo problema consiste nel disporre i dati di tipo WORD ad indirizzi pari (0, 2, 4, 6, etc); in questo modo, infatti, il dato si viene a trovare, o nelle prime due celle, o nelle ultime due celle di una quaterna.

In riferimento alla Figura 8.8, supponiamo di voler leggere la DWORD contenuta nelle celle C0, C1, C2 e C3; il Data Bus viene posizionato sulla quaterna (C0, C1, C2, C3) e l'accesso alle celle C0, C1, C2 e C3 avviene attraverso le linee da D0 a D31.
Supponiamo di voler leggere la DWORD contenuta nelle celle C1, C2, C3 e C4; il Data Bus viene prima posizionato sulla quaterna (C0, C1, C2, C3) e attraverso le linee da D8 a D31 vengono letti i 3 BYTE che si trovano nelle celle C1, C2 e C3. Successivamente, il Data Bus viene posizionato sulla quaterna (C4, C5, C6, C7) e attraverso le linee da D0 a D7 viene letto il BYTE che si trova nella cella C4; complessivamente vengono effettuati due accessi in memoria.
La possibilità di leggere o scrivere un blocco di 3 BYTE è una caratteristica delle CPU Intel con architettura a 32 bit; in questo modo si riduce a 2 il numero massimo di accessi in memoria per la lettura o la scrittura di un dato di tipo DWORD.

Le considerazioni appena svolte ci fanno capire che con un Data Bus a 32 linee i dati di tipo DWORD non presentano alcun problema di allineamento in memoria purché siano interamente contenuti all'interno di una quaterna di celle; se la DWORD si trova a cavallo tra due quaterne, richiede due accessi in memoria per essere letta o scritta. Il modo più ovvio (e anche l'unico) per evitare questo problema consiste nel disporre i dati di tipo DWORD ad indirizzi multipli interi di 4 (0, 4, 8, 12, etc).

Dopo aver descritto la configurazione interna delle memorie di lavoro, possiamo passare ad analizzare i vari tipi di memoria di lavoro e le tecniche che vengono utilizzate per la fabbricazione dei circuiti di memoria; le memorie di lavoro vengono suddivise in due categorie principali: Prima di illustrare la struttura circuitale delle memorie RAM, dobbiamo fare la conoscenza con particolari circuiti logici chiamati reti sequenziali.

8.2 Le reti sequenziali

Nei precedenti capitoli abbiamo visto che le reti combinatorie sono formate da componenti come le porte logiche che presentano la caratteristica di fornire un segnale in uscita che è una logica conseguenza del segnale (o dei segnali) in ingresso; se cambiano i livelli logici dei segnali in ingresso, cambia (dopo un piccolo ritardo di propagazione) anche il segnale in uscita e questo segnale non viene minimamente influenzato dalla precedente configurazione ingresso/uscita assunta dalla porta. In pratica, si potrebbe dire che le porte logiche "non ricordano" lo stato che avevano assunto in precedenza; molto spesso però si ha la necessità di realizzare reti combinatorie formate da circuiti logici capaci di ricordare gli stati precedenti, producendo quindi un segnale in uscita che è una logica conseguenza, non solo dei nuovi segnali in ingresso, ma anche degli ingressi precedenti. Le reti combinatorie dotate di queste caratteristiche prendono il nome di reti sequenziali e vengono largamente impiegate nella realizzazione delle memorie di lavoro.

8.2.1 Il flip-flop set/reset

Per realizzare un circuito logico capace di ricordare gli stati assunti in precedenza, si sfrutta una tecnica molto usata in campo elettronico, chiamata retroazione; questa tecnica consiste nel prelevare il segnale in uscita da un dispositivo, riapplicandolo all'ingresso del dispositivo stesso (o di un altro dispositivo). Applicando, ad esempio, la "retroazione incrociata" a due porte NAND, si perviene al circuito logico di Figura 8.9a che rappresenta l'elemento fondamentale delle reti sequenziali e prende il nome di flip-flop set/reset (abbreviato in FF-SR); la Figura 8.9b mostra il simbolo del FF-SR che si utilizza negli schemi dei circuiti logici. Per capire il principio di funzionamento del FF-SR bisogna ricordare che la porta NAND produce in uscita un livello logico 0 solo quando tutti gli ingressi sono a livello logico 1; in tutti gli altri casi si otterrà in uscita un livello logico 1.
Ponendo ora in ingresso S=1, R=1, si nota che le due uscite Q e Q' assumono una configurazione del tutto casuale; si può constatare però che le due uniche possibilità sono Q=0, Q'=1, oppure Q=1, Q'=0. Infatti, se la porta che abbiamo indicato con A produce in uscita 1, attraverso la retroazione questa uscita si porta all'ingresso della porta B che avendo entrambi gli ingressi a 1 produrrà in uscita 0; viceversa, se la porta A produce in uscita 0, questa uscita si porta all'ingresso della porta B che avendo in ingresso la configurazione (0, 1) produrrà in uscita 1.
Partiamo quindi dalla configurazione R=1, S=1 e tenendo R=1 portiamo l'ingresso S a 0; osservando la Figura 8.9a si vede subito che la porta A, avendo un ingresso a 0, produrrà sicuramente in uscita un 1. La porta B trovandosi in ingresso la coppia (1, 1), produrrà in uscita 0; a questo punto, qualsiasi modifica apportata a S (purché R resti a 1) non modificherà l'uscita Q che continuerà a valere 1. Abbiamo appurato quindi che partendo dalla configurazione in ingresso (1, 1) e portando in successione S prima a 0 e poi a 1, il FF-SR memorizza un bit che vale 1 ed è disponibile sull'uscita Q; questa situazione permane inalterata finché R continua a valere 1.
Portando ora R a 0 e tenendo S=1, si vede che la porta B, avendo un ingresso a 0 produrrà in uscita 1, mentre la porta A ritrovandosi in ingresso la coppia (1, 1) produrrà in uscita 0; finché S rimane a 1 qualsiasi modifica apportata all'ingresso R non altera l'uscita Q che continuerà a valere 0. Abbiamo appurato quindi che partendo dalla configurazione in ingresso (1, 1) e portando in successione R prima a 0 e poi di nuovo a 1, il FF-SR memorizza un bit che vale 0 ed è disponibile sull'uscita Q.

Da tutte le considerazioni appena esposte, risulta evidente che il FF-SR rappresenta una unità elementare di memoria capace di memorizzare un solo bit; infatti:

8.2.2 Il flip-flop data

A partire dal FF-SR è possibile realizzare diversi altri tipi di flip-flop che trovano svariate applicazioni nell'elettronica digitale; in particolare, la Figura 8.10 mostra il flip-flop tipo D (dove la D sta per Data o Delay), che si presta particolarmente per la realizzazione di memorie di piccole dimensioni. La Figura 8.10a mostra lo schema circuitale del flip-flop data (abbreviato in FF-D); la Figura 8.10b mostra il simbolo logico attraverso il quale si rappresenta il FF-D. L'ingresso D viene chiamato Data ed è proprio da questo ingresso che arriva il bit da memorizzare; l'ingresso CLK fornisce al FF un segnale di clock (di cui si è parlato nel precedente capitolo) che permette di sincronizzare le operazioni di I/O. In Figura 8.10a si nota anche un ingresso CKI che serve solo per mostrare il principio di funzionamento del FF-D e che ha il compito di impedire (Clock Inhibit) o permettere al segnale di clock di arrivare al FF; infine, le due uscite Q e Q' sono le stesse del FF-SR visto prima.
Per descrivere il principio di funzionamento del FF-D notiamo innanzi tutto che, finché l'ingresso CKI è a 0, la porta AND a sinistra produrrà in uscita un livello logico 0 che andrà a raggiungere entrambe le porte NAND indicate con A e B, che a loro volta produrranno in uscita un livello logico 1; le due porte NAND indicate con A' e B' costituiscono un FF-SR che si troverà di conseguenza nella configurazione iniziale S=1, R=1. Partendo da questa configurazione iniziale e abilitando l'ingresso CKI, il segnale di clock può giungere alle due porte A e B e in un ciclo completo (cioè con il segnale di clock che passa da 0 a 1 e poi di nuovo a 0) il bit che arriva dall'ingresso D verrà copiato sull'uscita Q; riportando ora a livello logico 0 l'ingresso CKI, il bit appena copiato sull'uscita Q resterà "imprigionato" indipendentemente dal valore dell'ingresso D (per questo motivo il FF-D viene anche chiamato latch, che in inglese significa lucchetto).
Per la dimostrazione delle cose appena dette ci possiamo servire della Figura 8.11: la Figura 8.11a si riferisce al caso D=1, mentre la Figura 8.11b si riferisce al caso D=0. Cominciamo dal caso in cui dalla linea D arrivi un livello logico 1 (Figura 8.11a); non appena CKI passa da 0 a 1, il segnale di clock viene abilitato e può giungere alle due porte A e B. Analizzando la Figura 8.10 e la Figura 8.11a possiamo osservare che quando CLK passa da 0 a 1, le due porte A e B lasciano R a 1 e portano S da 1 a 0; quando CLK passa da 1 a 0, le due porte A e B lasciano R a 1 e portano S da 0 a 1. Applicando allora le cose esposte in precedenza sul FF-SR possiamo dire che l'operazione appena effettuata (set) ha quindi memorizzato (in un ciclo di clock) l'ingresso D=1 sul FF-SR; il bit memorizzato (1) è disponibile sull'uscita Q.
Passiamo ora al caso in cui dalla linea D arrivi un livello logico 0 (Figura 8.11b); non appena CKI passa da 0 a 1, il segnale di clock viene abilitato e può giungere alle due porte A e B. Analizzando la Figura 8.10 e la Figura 8.11b possiamo osservare che quando CLK passa da 0 a 1, le due porte A e B lasciano S a 1 e portano R da 1 a 0; quando CLK passa da 1 a 0, le due porte A e B lasciano S a 1 e portano R da 0 a 1. Applicando allora le cose esposte in precedenza sul FF-SR possiamo dire che l'operazione appena effettuata (reset) ha quindi memorizzato (in un ciclo di clock) l'ingresso D=0 sul FF-SR; il bit memorizzato (0) è disponibile sull'uscita Q.

8.2.3 Struttura circuitale di un FF

A titolo di curiosità analizziamo le caratteristiche costruttive di un FF destinato a svolgere il ruolo di unità elementare di memoria; in Figura 8.12 vediamo, ad esempio, un FF realizzato in tecnologia NMOS. Per capire il funzionamento di questo circuito bisogna ricordare che il MOS è un regolatore di corrente pilotato in tensione; regolando da 0 ad un massimo la tensione applicata sul gate, si può regolare da 0 ad un massimo la corrente che circola tra il drain e il source. Applicando al gate di un NMOS una tensione nulla (o inferiore alla tensione di soglia), il canale tra drain e source è chiuso e impedisce quindi il passaggio della corrente; in queste condizioni il transistor si comporta come un interruttore aperto (OFF). Applicando al gate di un NMOS una tensione superiore alla tensione di soglia, il canale tra drain e source è aperto e favorisce quindi il passaggio della corrente; in queste condizioni il transistor si comporta come un interruttore chiuso (ON).

Al centro della Figura 8.12 si nota il FF costituito dai due transistor T1 e T2; i due transistor T3 e T4 svolgono semplicemente il ruolo di carichi resistivi per T1 e T2. I due transistor T5 e T6 servono per le operazioni di lettura e di scrittura; la linea RG infine serve per abilitare la riga lungo la quale si trova la cella (o le celle) a cui vogliamo accedere.
Osserviamo subito che se T1 è ON, allora T2 deve essere per forza OFF; viceversa se T1 è OFF, allora T2 deve essere per forza ON. Infatti, se T1 è ON, allora la corrente che arriva dal positivo di alimentazione (+Vcc) si riversa a massa proprio attraverso T1; questa circolazione di corrente attraverso il carico resistivo T3 erode il potenziale +Vcc portando verso lo 0 il potenziale del punto A e quindi anche il potenziale del gate di T2 che viene spinto così allo stato OFF. Analogamente, se T2 è ON, allora la corrente che arriva dal positivo di alimentazione (+Vcc) si riversa a massa proprio attraverso T2; questa circolazione di corrente attraverso il carico resistivo T4 erode il potenziale +Vcc portando verso lo 0 il potenziale del punto B e quindi anche il potenziale del gate di T1 che viene spinto così allo stato OFF.

Vediamo ora come si svolgono con il circuito di Figura 8.12 le tre operazioni fondamentali e cioè: memorizzazione, lettura, scrittura:

Per la fase di memorizzazione (cioè per tenere il dato in memoria) si pone RG=0; in questo modo, sia T5 che T6 sono OFF, per cui il FF risulta isolato dall'esterno. Il valore del bit memorizzato nel FF è rappresentato dalla tensione Vds che si misura tra il drain e il source di T1; se T1 è ON allora Vds=0, mentre se T1 è OFF allora Vds=+Vcc. In base a quanto è stato detto in precedenza si può affermare quindi che la configurazione T1 ON e T2 OFF rappresenta un bit di valore 0 in memoria; analogamente, la configurazione T1 OFF e T2 ON rappresenta un bit di valore 1 in memoria.

Per la fase di lettura si pone RG=1; in questo modo, sia T5 che T6 sono ON. Se il FF memorizza un bit di valore 0 allora come già sappiamo T1 è ON e T2 è OFF; in queste condizioni la corrente che arriva dal positivo di alimentazione transita, sia attraverso T1, sia attraverso T5 riversandosi anche lungo la linea D che segnala la lettura di un bit di valore 0. Se il FF memorizza un bit di valore 1 allora come già sappiamo T1 è OFF e T2 è ON; in queste condizioni la corrente che arriva dal positivo di alimentazione transita, sia attraverso T2, sia attraverso T6 riversandosi anche lungo la linea D' che segnala la lettura di un bit di valore 1.

Per la fase di scrittura si pone RG=1; in questo modo, sia T5 che T6 sono ON. Per scrivere un bit di valore 0 si pone D=0 e D'=1; in questo modo la corrente che arriva dal positivo di alimentazione fluisce verso D attraverso T5. Il potenziale +Vcc viene eroso dal carico resistivo T3 e quindi il potenziale del punto A si porta verso lo zero spingendo T2 allo stato OFF e T1 allo stato ON; come abbiamo visto in precedenza, questa situazione rappresenta un bit di valore 0 in memoria. Per scrivere un bit di valore 1 si pone D=1 e D'=0; in questo modo la corrente che arriva dal positivo di alimentazione fluisce verso D' attraverso T6. Il potenziale +Vcc viene eroso dal carico resistivo T4 e quindi il potenziale del punto B si porta verso lo zero spingendo T1 allo stato OFF e T2 allo stato ON; come abbiamo visto in precedenza, questa situazione rappresenta un bit di valore 1 in memoria.

Appare evidente il fatto che il funzionamento dei FF è legato alla presenza della alimentazione elettrica; non appena si toglie l'alimentazione elettrica, il contenuto dei FF viene perso. Queste considerazioni si applicano naturalmente a tutte le memorie di lavoro realizzate con i FF; possiamo dire quindi che non appena si spegne il computer, tutto il contenuto delle memorie di lavoro di questo tipo viene perso.
Nei paragrafi seguenti vengono utilizzati i FF appena descritti per mostrare alcuni esempi relativi ad importanti dispositivi di memoria come i registri e le RAM.

8.3 Registri di memoria

Abbiamo visto che il FF rappresenta una unità elementare di memoria capace di memorizzare un bit di informazione; se si ha la necessità di memorizzare informazioni più complesse costituite da numeri binari a due o più bit, bisogna collegare tra loro un numero adeguato di FF, ottenendo così particolari circuiti logici chiamati registri.

8.3.1 Registri a scorrimento

Collegando più FF in serie, si ottiene la configurazione mostrata in Figura 8.13; in particolare, questo esempio si riferisce ad un registro formato da 4 FF-D che permette di memorizzare numeri a 4 bit (le uscite Q' sono state soppresse in quanto non necessarie). Per capire meglio il principio di funzionamento del circuito di Figura 8.13, bisogna tenere presente che i FF vengono realizzati con le porte logiche; come abbiamo visto nei precedenti capitoli, tutte le porte logiche presentano un certo ritardo di propagazione. In sostanza, inviando dei segnali sugli ingressi di una porta logica, il conseguente segnale di uscita viene ottenuto dopo un certo intervallo di tempo; questo ritardo è dovuto come sappiamo ai tempi di risposta dei transistor utilizzati per realizzare le porte logiche.
Nel circuito di Figura 8.13 i 4 bit da memorizzare arrivano in serie dall'ingresso SI (serial input); in una situazione del genere è praticamente impossibile quindi memorizzare un nibble in un solo ciclo di clock perché ciò richiederebbe una risposta istantanea da parte di tutti i 4 FF. Per risolvere questo problema, la frequenza del segnale di clock viene scelta in modo da minimizzare i tempi di attesa dovuti al ritardo di propagazione; il metodo più efficace per ottenere questo risultato consiste nel fare in modo che ad ogni ciclo di clock il registro di Figura 8.13 sia pronto per ricevere un nuovo bit da memorizzare.
Supponiamo, ad esempio, di voler memorizzare nel circuito di Figura 8.13 il numero binario 1010b; i 4 bit di questo numero arrivano in serie dall'ingresso SI a partire dal bit meno significativo. L'ingresso dei vari bit viene sincronizzato attraverso il segnale di clock in modo che ad ogni ciclo di clock arrivi un nuovo bit da memorizzare; analizziamo allora quello che accade attraverso la Figura 8.14. Inizialmente il segnale di clock è inibito (CKI=0) e tutti i 4 FF-SR contenuti nei 4 FF-D si troveranno quindi nella situazione iniziale S=1, R=1; per semplicità supponiamo che le 4 uscite Q0, Q1, Q2 e Q3 siano tutte a livello logico 0 (anche se ciò non è necessario).
Come si può notare dalla Figura 8.13, il segnale di clock una volta abilitato (CKI=1) giunge contemporaneamente a tutti i 4 FF-D; a questo punto si verifica la seguente successione di eventi: Come si nota dalla Figura 8.14, quando CKI viene riportato a 0 disabilitando il segnale di clock, si ottiene Q3=1, Q2=0, Q1=1, Q0=0; possiamo dire quindi che dopo 4 cicli di clock il circuito di Figura 8.13 ha memorizzato il nibble 1010b. Con CKI=0, qualunque sequenza di livelli logici in arrivo da SI non altera più la configurazione assunta dal circuito di Figura 8.13.
Le considerazioni appena esposte evidenziano il fatto che nel corso dei 4 cicli di clock, i bit da memorizzare scorrono da sinistra verso destra; per questo motivo il circuito di Figura 8.13 viene anche chiamato registro a scorrimento.

Una volta capito il metodo di scrittura nei registri a scorrimento diventa facile capire anche il metodo di lettura; basta, infatti, inviare altri 4 cicli di clock per far scorrere verso destra i 4 bit precedentemente memorizzati. Questi 4 bit si presentano quindi in sequenza (sempre a partire dal bit meno significativo) sull'uscita SO (serial output); dalla Figura 8.13 si può constatare che è anche possibile leggere i 4 bit direttamente dalle uscite Q0, Q1, Q2 e Q3.

8.3.2 Registri paralleli

I registri a scorrimento presentano il vantaggio di avere una struttura relativamente semplice in quanto richiedono un numero ridotto di collegamenti; come si nota, infatti, dalla Figura 8.13, è presente un'unica linea per l'input (SI) e un'unica linea per l'output (SO). Gli svantaggi abbastanza evidenti dei registri a scorrimento sono rappresentati, invece, dal tempo di accesso in I/O e dal metodo di lettura che distrugge il dato in memoria; per quanto riguarda i tempi di accesso in lettura/scrittura si può osservare che il numero di cicli di clock necessari è pari al numero di FF che formano il registro. Per quanto riguarda, invece, l'altro problema, risulta evidente che per leggere, ad esempio, il contenuto di un registro a 4 bit, bisogna inviare 4 cicli di clock; in questo modo escono dalla linea SO i 4 bit in memoria, ma dalla linea SI entrano altri 4 bit che sovrascrivono il dato appena letto.
Questi problemi vengono facilmente eliminati attraverso il collegamento in parallelo dei FF; la Figura 8.15 mostra appunto un cosiddetto registro parallelo costituito anche in questo caso da 4 FF-D. Come si può notare, ogni FF ha l'ingresso D e l'uscita Q indipendenti da quelli degli altri FF; in fase di scrittura, i bit arrivano in parallelo (e tutti nello stesso istante) sugli ingressi D, mentre in fase di lettura, i bit vengono letti sempre in parallelo (e tutti nello stesso istante) dalle uscite Q.
Ripetendo l'esempio del registro a scorrimento, si vede che in fase di memorizzazione del nibble 1010b, i 4 bit si presentano contemporaneamente sui 4 ingressi che assumeranno quindi la configurazione D3=1, D2=0, D1=1, D0=0; a questo punto basta inviare un solo ciclo di clock per copiare in un colpo solo i 4 bit sulle rispettive uscite.
Per leggere il dato appena memorizzato, basta leggere direttamente le 4 uscite Q3, Q2, Q1, Q0; la lettura avviene senza distruggere il dato stesso.
I registri paralleli risolvono il problema del tempo di accesso e della distruzione dell'informazione in memoria in fase di lettura, ma presentano lo svantaggio di una maggiore complessità circuitale; un registro parallelo a n bit richiederà, infatti, solo per l'I/O dei dati, n linee di ingresso e n linee di uscita.

Per le loro caratteristiche i registri vengono largamente utilizzati quando si ha la necessità di realizzare piccole memorie ad accesso rapidissimo; nel campo dei computer sicuramente l'applicazione più importante dei registri è quella relativa alle memorie interne delle CPU. Tutte le CPU sono dotate, infatti, di piccole memorie interne attraverso le quali possono gestire le informazioni (dati e istruzioni) da elaborare; a tale proposito si utilizzano proprio i registri paralleli che garantiscono le massime prestazioni in termini di velocità di accesso.

8.4 Memorie RAM

L'acronimo RAM significa Random Access Memory (memoria ad accesso casuale); come già sappiamo, il termine "casuale" non si riferisce al fatto che l'accesso a questo tipo di memoria avviene a caso, bensì al fatto che il tempo necessario alla CPU per accedere ad una cella qualunque (scelta a caso) è costante e quindi assolutamente indipendente dalla posizione in memoria della cella stessa. Al contrario, sulle vecchie unità di memorizzazione a nastro (cassette), il tempo di accesso dipendeva dalla posizione del dato (memorie ad accesso sequenziale); più il dato si trovava in fondo, più aumentava il tempo di accesso in quanto bisognava far scorrere un tratto di nastro sempre più lungo.
La RAM è accessibile, sia in lettura, sia in scrittura e il suo ruolo sul computer è quello di memoria principale (centrale) ad accesso rapido; un programma per poter essere eseguito deve essere prima caricato nella RAM, garantendo così alla CPU la possibilità di accedere ai dati e alle istruzioni in tempi ridottissimi. In base a quanto è stato esposto in precedenza, non appena si spegne il computer tutto il contenuto della RAM viene perso; se si vogliono conservare in modo permanente programmi e altre informazioni, bisogna procedere al loro salvataggio sulle memorie di massa.

8.4.1 Memorie RAM statiche (SRAM)

Nella sezione 8.1 di questo capitolo è stato detto che una memoria di lavoro è organizzata sotto forma di matrice di celle; se le varie celle vengono realizzate con i FF, otteniamo una memoria di lavoro organizzata sotto forma di matrice di registri. Ogni registro deve essere formato naturalmente da un numero fisso di FF; nel caso, ad esempio, delle architetture 80x86, ogni registro è formato da 8 FF.
Analizziamo ora a titolo di curiosità la struttura circuitale di una RAM; la Figura 8.16 illustra, ad esempio, una semplicissima RAM formata da 4 righe e una sola colonna da 4 FF. In questo caso essendo presente una sola colonna non abbiamo bisogno del circuito per la decodifica colonna; è presente, invece, sulla sinistra il circuito per la decodifica riga. Per poter selezionare una tra le 4 righe presenti abbiamo bisogno di un Address Bus formato da log24=2 linee (A0 e A1); analizzando il circuito per la decodifica riga si può constatare facilmente che, a seconda dei livelli logici presenti sulle due linee A0 e A1, viene selezionata una sola tra le 4 righe presenti.
Supponiamo, ad esempio, di selezionare la riga 1 (A0=1, A1=0); in questo caso possiamo osservare che solo sulla linea della riga 1 è presente un livello logico 1, mentre sulle altre 3 linee di riga è presente un livello logico 0. Se ora vogliamo leggere il nibble memorizzato nei 4 FF della riga 1, la logica di controllo pone CS=1, R/W=1, OE=1; sulla linea CS transita il segnale Chip Select (selezione circuito di memoria), sulla linea R/W transita il segnare Read/Write (lettura/scrittura), mentre sulla linea OE transita il segnale Output Enable (abilitazione lettura).
Se proviamo a seguire il percorso dei livelli logici che arrivano da CS, R/W e OE possiamo constatare che agli ingressi CLK (clock) di tutti i FF arriva un livello logico 0 (clock disabilitato); in questo caso come già sappiamo i FF non eseguono alcuna operazione di memorizzazione. Siccome CS=1, R/W=1 e OE=1, la porta AND più in basso nel circuito di Figura 8.16 produce un livello logico 1 che raggiunge il circuito per l'abilitazione della lettura; in questo modo vengono abilitate le 4 linee del Data Bus indicate in Figura 8.16 con Data Output. Dalle 4 uscite Out0, Out1, Out2 e Out3 vengono prelevati i 4 bit memorizzati nei 4 FF della riga 1; osserviamo che dalle altre 3 righe vengono letti 3 nibble che valgono tutti 0000b e vengono poi combinati con il nibble della riga 1 attraverso le porte OR a 4 ingressi. Supponiamo che la riga 1 memorizzi il nibble 1101b; in questo caso sulle 4 linee Data Output si ottiene proprio:
0000b OR 1101b OR 0000b OR 0000b = 1101b
A questo punto la logica di controllo pone CS=0 disabilitando il chip di memoria; in questo modo si pone fine all'operazione di lettura.

Supponiamo ora di voler scrivere un nibble nei 4 FF della riga 1; in questo caso la logica di controllo pone CS=1, R/W=0 e OE=0 disabilitando così il circuito Data Output per la lettura. Se proviamo a seguire il percorso dei livelli logici che arrivano da CS, R/W e OE possiamo constatare che agli ingressi CLK (clock) dei 4 FF della riga 1 arriva un livello logico 1; sugli ingressi CLK dei FF di tutte le altre righe arriva, invece, un livello logico 0 (clock disabilitato). In un caso del genere come già sappiamo ciascuno dei 4 FF della riga 1 copia sulla sua uscita Q il segnale presente sul suo ingresso D (ricordiamo che con i registri paralleli l'operazione di scrittura richiede un solo ciclo di clock); i 4 segnali che vengono memorizzati sono proprio quelli che arrivano dalle 4 linee del Data Bus indicate con In0, In1, In2 e In3 (Data Input).
A questo punto la logica di controllo pone CS=0 disabilitando il chip di memoria; in questo modo si pone fine all'operazione di scrittura.

In base a quanto è stato appena esposto, possiamo dedurre una serie di considerazioni relative alle memorie RAM realizzate con i FF; osserviamo innanzi tutto che le informazioni memorizzate in una RAM di questo tipo, si conservano inalterate senza la necessità di ulteriori interventi esterni. Si può dire che una RAM di questo tipo conservi staticamente le informazioni finché permane l'alimentazione elettrica; proprio per questo motivo, una memoria come quella appena descritta viene definita Static RAM (RAM statica) o SRAM.
Sicuramente, il vantaggio fondamentale di una SRAM è rappresentato dai tempi di accesso estremamente ridotti, mediamente dell'ordine di una decina di nanosecondi (1 nanosecondo = un miliardesimo di secondo = 10-9 secondi); questo aspetto è fondamentale per le prestazioni del computer in quanto buona parte del lavoro svolto dalla CPU consiste proprio nell'accedere alla memoria. Se la memoria non è in grado di rispondere in tempi sufficientemente ridotti, la CPU è costretta a girare a vuoto in attesa che si concludano le operazioni di I/O; in un caso del genere le prestazioni generali del computer risulterebbero nettamente inferiori a quelle potenzialmente offerte dal microprocessore.
Purtroppo questo importante vantaggio offerto dalle SRAM non riesce da solo a bilanciare i notevoli svantaggi; osservando la struttura della SRAM di Figura 8.16 e la struttura del FF di Figura 8.12, si nota in modo evidente l'enorme complessità circuitale di questo tipo di memorie. Finché si ha la necessità di realizzare RAM velocissime e di piccole dimensioni, la complessità circuitale non rappresenta certo un problema; in questo caso la soluzione migliore consiste sicuramente nello sfruttare le notevoli prestazioni velocistiche dei FF. Se però si ha la necessità di realizzare memorie di lavoro di grosse dimensioni (decine o centinaia di MiB), sarebbe teoricamente possibile utilizzare i FF, ma si andrebbe incontro a problemi talmente gravi da rendere praticamente improponibile questa idea; si otterrebbero, infatti, circuiti eccessivamente complessi, con centinaia di milioni di porte logiche, centinaia di milioni di linee di collegamento, elevato ingombro, costo eccessivo, etc.
Proprio per queste ragioni, tutti i moderni PC sono dotati di memoria centrale di tipo dinamico; come però viene spiegato più avanti, le RAM dinamiche pur essendo nettamente più economiche e più miniaturizzabili delle SRAM, presentano il grave difetto di un tempo di accesso sensibilmente più elevato. Con la comparsa sul mercato di CPU sempre più potenti, questo difetto ha cominciato ad assumere un peso del tutto inaccettabile; per risolvere questo problema, i progettisti dei PC hanno deciso allora di ricorrere ad un espediente che permette di ottenere un notevole miglioramento della situazione. A partire dall'80486 tutte le CPU della famiglia 80x86 sono state dotate di una piccola SRAM interna chiamata cache memory (memoria nascondiglio); il trucco che viene sfruttato consiste nel fatto che, nell'eseguire un programma presente nella memoria centrale, la CPU trasferisce nella cache memory le istruzioni che ricorrono più frequentemente. Queste istruzioni vengono quindi eseguite in modo estremamente efficiente grazie al fatto che la cache memory offre prestazioni nettamente superiori a quelle della memoria centrale.
A causa dello spazio esiguo dovuto alle esigenze di miniaturizzazione, la cache memory presente all'interno delle CPU è piuttosto limitata e ammonta mediamente a pochi KiB; questa memoria viene chiamata L1 cache o first level cache (cache di primo livello). Se si vuole ottenere un ulteriore miglioramento delle prestazioni, è possibile dotare il computer di una cache memory esterna alla CPU che viene chiamata L2 cache o second level cache (cache di secondo livello); tutti i moderni PC incorporano al loro interno una L2 cache che mediamente ammonta a qualche centinaio di KiB.

8.4.2 Memorie RAM dinamiche (DRAM)

La memoria centrale degli attuali PC ammonta ormai a diverse centinaia di MiB; le notevoli dimensioni della memoria centrale devono però conciliarsi con il rispetto di una serie di requisiti che riguardano, in particolare, la velocità di accesso, il costo di produzione e il livello di miniaturizzazione. Come abbiamo appena visto, le SRAM offrono prestazioni particolarmente elevate in termini di velocità di accesso, ma non possono soddisfare gli altri requisiti; proprio per questo motivo, gli attuali PC vengono equipaggiati con un tipo di memoria centrale chiamato Dynamic RAM o DRAM (RAM dinamica).
In una DRAM vengono sfruttate le caratteristiche dei condensatori elettrici; si definisce condensatore elettrico un sistema formato da due conduttori elettrici tra i quali si trova interposto un materiale isolante. Ciascuno dei due conduttori elettrici viene chiamato armatura; in Figura 8.17 vediamo, ad esempio, un condensatore (C) formato da due armature piane tra le quali si trova interposta l'aria. In Figura 8.17a notiamo che se l'interruttore T è aperto, non può circolare la corrente elettrica e quindi il generatore non può caricare il condensatore; in queste condizioni tra le due armature del condensatore si misura una tensione elettrica nulla (0V).
In Figura 8.17b notiamo che se l'interruttore T viene chiuso, la corrente elettrica può circolare e quindi l'energia elettrica erogata dal generatore comincia ad accumularsi nel condensatore; alla fine del processo di carica, tra le due armature del condensatore si misura una tensione elettrica pari a quella fornita dal generatore (+5V).
In Figura 8.17c notiamo che se l'interruttore T viene riaperto, il condensatore rimane isolato dall'esterno e quindi teoricamente dovrebbe conservare il suo stato di carica; anche in questo caso tra le due armature del condensatore si misura una tensione elettrica di +5V. Se vogliamo scaricare il condensatore, dobbiamo collegarlo, ad esempio, a una resistenza elettrica; in questo modo la resistenza assorbe l'energia elettrica dal condensatore e la converte in energia termica.

Dalle considerazioni appena esposte si deduce immediatamente che anche il condensatore rappresenta una semplicissima unità elementare di memoria capace di gestire un singolo bit; possiamo associare, infatti, il livello logico 0 al condensatore scarico (0V) e il livello logico 1 al condensatore carico (+Vcc). Mettendo in pratica questi concetti si perviene al circuito di Figura 8.18 che illustra in linea di principio la struttura di una unità elementare di memoria di tipo DRAM. Come si può notare, l'unità elementare di memoria è composta da un transistor T di tipo NMOS e da un condensatore C; il confronto tra la Figura 8.18 e la Figura 8.12 evidenzia la notevole semplicità di una unità DRAM rispetto a una unità SRAM. La linea indicata con D rappresenta la linea dati utilizzata per le operazioni di I/O, mentre la linea RG serve per abilitare o disabilitare la riga su cui si trova la cella desiderata; come al solito si assume che il potenziale di massa (GND) sia pari a 0V.
Se la linea RG si trova a livello logico 0, si vede chiaramente in Figura 8.18 che il transistor T si porta allo stato OFF (interdizione); in queste condizioni il condensatore C risulta isolato dall'esterno per cui teoricamente dovrebbe conservare indefinitamente il bit che sta memorizzando. Come è stato detto in precedenza, se il condensatore è scarico sta memorizzando un bit di valore 0; se, invece, il condensatore è carico sta memorizzando un bit di valore 1.
Per effettuare una operazione di lettura o di scrittura bisogna portare innanzi tutto la linea RG a livello logico 1 in modo che il transistor T venga portato allo stato ON (saturazione); a questo punto il condensatore può comunicare direttamente con la linea D per le operazioni di I/O.
L'operazione di scrittura consiste semplicemente nell'inviare un livello logico 0 o 1 sulla linea D; ovviamente questo livello logico rappresenta il valore del bit che vogliamo memorizzare. Ponendo D=0, il condensatore si ritrova con entrambe le armature a potenziale 0V; in queste condizioni il condensatore si scarica (se non era già scarico) e memorizza quindi un bit di valore 0. Ponendo, invece, D=1, il condensatore si ritrova con una armatura a potenziale +Vcc e l'altra a potenziale di massa 0V; in queste condizioni il condensatore si carica (se non era già carico) e memorizza quindi un bit di valore 1.
L'operazione di lettura con l'unità DRAM di Figura 8.18 consiste semplicemente nel misurare il potenziale della linea D; se il potenziale è 0V (condensatore scarico) si ottiene la lettura D=0, mentre se il potenziale è +Vcc (condensatore carico) si ottiene la lettura D=1.

Come è stato già evidenziato, l'unità elementare DRAM risulta notevolmente più semplice dell'unità elementare SRAM rendendo quindi possibile la realizzazione di memorie di lavoro di notevoli dimensioni, caratterizzate da una elevatissima densità di integrazione e da un costo di produzione estremamente ridotto; per quanto riguarda l'organizzazione interna, le DRAM vengono strutturate sotto forma di matrice rettangolare di celle esattamente come avviene per le SRAM.
Dalle considerazioni appena esposte sembrerebbe che le DRAM siano in grado di risolvere tutti i problemi presentati dalle SRAM, ma purtroppo bisogna fare i conti con un aspetto piuttosto grave che riguarda i condensatori; in precedenza è stato detto che un condensatore carico isolato dal mondo esterno conserva indefinitamente il suo stato di carica. Un condensatore di questo genere viene chiamato ideale ed ha un significato puramente teorico; in realtà accade che a causa degli inevitabili fenomeni di dispersione elettrica, un condensatore come quello di Figura 8.18 si scarica nel giro di poche decine di millisecondi. Per evitare questo problema tutte le memorie DRAM necessitano di un apposito dispositivo esterno che provvede ad effettuare periodicamente una operazione di rigenerazione (refreshing) delle celle; nelle moderne DRAM questo dispositivo viene integrato direttamente nel chip di memoria.
Al problema del refreshing bisogna aggiungere il fatto che le operazioni di carica e scarica dei condensatori richiedono tempi relativamente elevati; la conseguenza negativa di tutto ciò è data dal fatto che le DRAM presentano mediamente tempi di accesso pari a 60, 70 nanosecondi. Una CPU come l'80486 DX a 33 MHz è in grado di effettuare una operazione di I/O in memoria in appena 1 ciclo di clock, pari a:
1 / 33000000 = 0.00000003 secondi = 30 nanosecondi
Questo intervallo di tempo è nettamente inferiore a quello richiesto da una DRAM da 60 nanosecondi; per risolvere questo problema, i progettisti dei PC hanno escogitato i cosiddetti wait states (stati di attesa). In pratica, osservando che 60 nanosecondi sono il doppio di 30 nanosecondi, la CPU predispone la fase di accesso in memoria in un ciclo di clock, dopo di che gira a vuoto per un altro ciclo di clock dando il tempo alla memoria di rispondere; questa soluzione appare piuttosto discutibile visto che, considerando il fatto che la CPU dedica buona parte del suo tempo agli accessi in memoria, ci si ritrova con un computer che lavora prevalentemente a velocità dimezzata (nel caso dell'80486 si ha, infatti: 33/2=16.5 MHz).
Come è stato detto in precedenza, questo ulteriore problema viene parzialmente risolto attraverso l'uso della cache memory di tipo SRAM; l'idea di fondo consiste nel creare nella cache una copia delle informazioni che si trovano nella DRAM e che vengono accedute più frequentemente dalla CPU. La cache ha la precedenza sulla RAM per cui, ogni volta che la CPU deve accedere ad una informazione, se quell'informazione è presente nella cache l'accesso avviene in un solo ciclo di clock; in caso contrario, l'informazione viene letta dalla DRAM con conseguente ricorso agli stati di attesa.

8.4.3 le nuove memorie Double Data Rate (DDRx)

Nei computer moderni è stato introdotto il nuovo standard di memoria DDR (Double Data Rate), che introduce la possibilità di leggere i dati sul doppio fronte di clock, trasmettendoli sia sul fronte di salita che su quello di discesa; è possibile quindi raddoppiare la velocità di trasferimento dei dati verso la CPU senza per questo aumentare né la frequenza del clock interno alla memoria, né quella del bus dimezzando i tempi di attesa della CPU, questo sistema è molto vantaggioso rispetto alle DRAM (Dynamic Random Access Memory) che inviavano i dati verso la CPU solo sul fronte di salita e per questo avevano un tempo di latenza doppio.
A questo proposito viene introdotta l’unità di misura MT/s, un (megatransfer) rappresenta esattamente un milione di trasferimenti di dati, indipendentemente dalla frequenza di clock sottostante.
Questo rende i MT/s una misura più accurata e diretta delle effettive capacità di trasferimento dei dati della memoria. Per esempio, una Ram (Ddr4-3200) opera con una frequenza di base di 1600 Mhz, ma può effettuare 3200 MT/s grazie alla tecnologia Double Data Rate.

Poiché i dati sono trasferiti in pacchetti di 8 byte per volta (il bus è sempre a 64 bit) una RAM DDR consente una velocità teorica di trasferimento con una frequenza di clock di 100 MHz, con 2 trasferimenti per ogni ciclo di clock e un pacchetto di 8 byte ad ogni trasferimento, otteniamo così una banda di trasferimento dati da 1600 Mbit/s.
100 * (2 * 8) = 1600Mbit/s
l’unità di misura Mbps (megabit per secondo) tiene conto non solo del numero di trasferimenti, ma anche della larghezza del bus della memoria. Nel caso della Ram di un sistema moderno, che utilizza un bus a 64 bit, ogni trasferimento muove appunto 64 bit di dati. Quindi, una Ram che opera a 3200 MT/s su un bus a 64 bit avrà una larghezza di banda massima teorica di 25600 Mbit/s
3200MT/s * 64bit = 204800bit/s / 8byte = 25600Mbit/s
Nella realtà la situazione è più complessa, poiché la velocità di trasferimento è notevolmente influenzata dai fenomeni di latenza, che si verificano durante le operazioni di lettura/scrittura e che dipendono strettamente dal tipo e dalla qualità del chip, nonché dalla frequenza di funzionamento.

Per quantificare tali fenomeni, ad ogni banco di memoria vengono associati dei tempi caratteristici detti timing, misurati a parità di frequenza in unità di cicli per clock.
I timing della memoria sono espressi tipicamente attraverso quattro numeri (per esempio, 16-18-18-36); questi numeri indicano rispettivamente tCL (la latenza CAS), tRCD (il tempo necessario tra l’attivazione di una riga e l’accesso a una colonna), tRP (il tempo richiesto per disattivare una riga prima di attivarne un’altra) e tRAS (Il tempo minimo che una riga deve rimanere attiva per garantire l’accesso ai dati).

8.5 Memorie ROM

L'acronimo ROM significa Read Only Memory (memoria a sola lettura); come dice il nome, queste memorie sono accessibili solo in lettura in quanto il loro scopo è quello di conservare dati e istruzioni la cui modifica potrebbe compromettere il funzionamento del computer.
Il contenuto di queste memorie viene spesso impostato in fase di fabbricazione e permane anche in assenza di alimentazione elettrica; tutto ciò potrebbe sembrare impossibile visto che anche le ROM sono memorie elettroniche che per funzionare hanno bisogno di essere alimentate elettricamente. La conservazione delle informazioni all'interno di una ROM viene ottenuta attraverso svariati metodi; uno di questi metodi viene illustrato dalla Figura 8.19 che mostra una semplicissima ROM formata da 4 transistor di tipo BJT. La ROM di Figura 8.19 è formata da due linee di riga e due linee di colonna; le due righe sono collegate alle linee A0 e A1 dell'Address Bus, mentre le due colonne forniscono in uscita i livelli logici Out0 e Out1 che, come vedremo in seguito, sono una logica conseguenza dei valori assunti da A0 e A1.
Ricordando quanto è stato detto nel Capitolo 5 sul BJT, se sulla base arriva un livello logico 0, il transistor si porta in interdizione (OFF); il potenziale +Vcc (livello logico 1) presente sul collettore non può trasferirsi sull'emettitore e quindi sull'uscita di emettitore avremo un livello logico 0. Se, invece, sulla base arriva un livello logico 1, il transistor si porta in saturazione (ON); il potenziale +Vcc (livello logico 1) presente sul collettore si trasferisce sull'emettitore e quindi sull'uscita di emettitore avremo un livello logico 1.
In Figura 8.19 si nota anche la presenza di un transistor con l'emettitore non collegato alla linea di colonna; questi transistor forniscono alla corrispondente colonna sempre un livello logico 0 e quindi memorizzano di fatto un bit di valore 0.
A questo punto possiamo capire il principio di funzionamento della memoria ROM di Figura 8.19; i livelli logici assunti dai due ingressi A0 e A1 determinano in modo univoco i livelli logici ottenibili sulle uscite Out0 e Out1. Inviando, ad esempio, sull'Address Bus l'indirizzo 10b (cioè A0=0 e A1=1), si ottiene in uscita Out0=1 e Out1=0; questi due livelli logici che si ottengono in uscita sono legati univocamente all'indirizzo in ingresso 10b.
Possiamo dire quindi che in realtà la memoria ROM mostrata in Figura 8.19 non contiene alcuna informazione al suo interno; queste informazioni si vengono a creare automaticamente nel momento in cui la ROM viene alimentata e indirizzata.
La memoria appena descritta rappresenta una ROM propriamente detta; la struttura interna delle ROM propriamente dette viene impostata una volta per tutte in fase di fabbricazione e non può più essere modificata. Come abbiamo visto nel precedente capitolo, sui PC le ROM vengono utilizzate per memorizzare dati e programmi di vitale importanza per il corretto funzionamento del computer; in particolare, si possono citare le procedure del BIOS, il programma per il POST e il programma per il bootstrap.

Oltre alle ROM propriamente dette, esistono anche diverse categorie di ROM che possono essere programmate; analizziamo brevemente i principali tipi di ROM programmabili:

PROM o Programmable ROM (ROM programmabili).
Le PROM sono molto simili alle ROM propriamente dette, ma si differenziano per il fatto di contenere al loro interno dei microfusibili (uno per ogni transistor) posti tra l'uscita di emettitore e la corrispondente colonna; attraverso una apposita apparecchiatura è possibile far bruciare alcuni di questi microfusibili in modo da impostare la struttura interna della PROM a seconda delle proprie esigenze. I microfusibili vengono bruciati (e quindi interrotti) attraverso una corrente pulsante di circa 100 mA; da queste considerazioni risulta chiaramente il fatto che le PROM possono essere programmate una sola volta.

EPROM o Erasable Programmable ROM (ROM programmabili e cancellabili).
Nelle EPROM i BJT vengono sostituiti da transistor MOS che contengono al loro interno anche un secondo gate chiamato gate fluttuante; questo secondo gate viene immerso in uno strato di biossido di silicio (SiO2) e quindi si trova ad essere elettricamente isolato. Attraverso appositi impulsi di tensione applicati tra drain e source, è possibile determinare per via elettrostatica un accumulo di carica elettrica nel gate fluttuante; la carica così accumulata è isolata dall'esterno e quindi può conservarsi per decine di anni. I gate fluttuanti elettricamente carichi rappresentano un bit di valore 1, mentre quelli scarichi rappresentano un bit di valore 0.
Il contenuto delle EPROM può essere cancellato attraverso esposizione ai raggi ultravioletti; per questo motivo, i contenitori che racchiudono le EPROM sono dotati di apposite finestre trasparenti che favoriscono il passaggio degli ultravioletti. Una volta che la EPROM è stata cancellata può essere riprogrammata con la tecnica descritta in precedenza; generalmente sono possibili una cinquantina di riprogrammazioni.

EAROM o Electrically Alterable ROM (ROM alterabili elettricamente).
L'inconveniente principale delle EPROM descritte in precedenza è dato dal fatto che queste memorie per poter essere cancellate devono essere rimosse dal circuito in cui si trovano inserite; per ovviare a questo inconveniente sono state create appunto le EAROM. Le EAROM sono del tutto simili alle EPROM, ma si differenziano per la presenza di appositi circuiti che attraverso l'utilizzo di impulsi elettrici permettono, sia la programmazione, sia la cancellazione della memoria; in sostanza, le EAROM possono essere considerate delle vere e proprie RAM non volatili, cioè delle RAM che conservano il loro contenuto anche in assenza di alimentazione elettrica.
L'evoluzione delle EAROM ha portato alla nascita delle EEPROM chiamate anche E2PROM (Electrically Erasable PROM); nelle EEPROM lo strato isolante attorno al gate fluttuante viene ridotto a pochi centesimi di micron facilitando in questo modo le operazioni di scrittura e di cancellazione.

NOVRAM o Non Volatile RAM (RAM non volatili).
Le NOVRAM possono essere definite come le ROM dell'ultima generazione; queste memorie vengono ottenute associando una RAM con una EEPROM aventi la stessa capacità di memorizzazione in byte. In fase operativa, tutte le operazioni di I/O vengono effettuate ad alta velocità sulla RAM; nel momento in cui si vuole spegnere l'apparecchiatura su cui è installata la NOVRAM, tutto il contenuto della RAM viene copiato in modo permanente nella EEPROM.