The Intel 4004 Microprocessor emulator | Cerca per titolo, autore, parola chiave | ||||||||
The Intel 4004 Microprocessor emulator L'emulatore e4004 simula il sistema INTEL 4004, il circuito integrato, nato nel 1971, ad opera di Federico Faggin, di Intel, che è considerato il primo microprocessore della storia. L'emulatore e4004 è un ottimo strumento per comprendere il funzionamento di un processore. Per eseguire un programma, è sufficiente inserire il codice assembly o il codice Object nel blocco della memoria ROM ( ROM block ), premere il pulsante LOAD ed infine il pulsante ANIMATE o RUN del pannello 4004 CPU. Il pulsante STEP, invece, esegue il codice passo dopo passo. Il microprocessore 4004 era uno dei 4 circuiti che costituivano il chip-set MCS-4, che comprendeva il chip 4001 ROM ( una memoria di sola lettura, o Read Only Memory Chip ), il chip 4002 RAM ( Random Access Memory ) ed il chip 4003 Shift Register. Il cuore di questo microcomputer era proprio la CPU 4004, che aveva un set di istruzioni che permetteva al sistema di eseguire diverse operazioni aritmetiche e di controllo. Tutti i processori di oggi, anche se infinitamente più potenti, derivano da questa prima CPU a 4 bit. L'emulatore è scritto in Javascript, così da poter essere utilizzato in qualsiasi piattaforma o sistema operativo. A cura di Maciej Szyc 2007. Per conoscere il processore 4004 e capire, quindi, l'emulatore e4004, consigliamo vivamente di leggere il manuale ufficiale "MCS-4 Assembly Language Programming Manual", di Intel, del quale presentiamo qualche estratto. Il processore 4004 mette a disposizione del programmatore 16 registri a 4 bit, che possono essere trattati singolarmente, riferendosi ad essi con numeri sequenziali, da zero a 15, oppure come 8 coppie di registri, riferendosi ad essi con numeri sequenziali pari, da zero a 14, oppure con numeri sequenziali da 0P ( pair 0 ) a 7P ( pair 7 ): register pair 0 o 0P: 0, 1 register pair 2 o 1P: 2, 3 register pair 4 o 2P: 4, 5 register pair 6 o 3P: 6, 7 register pair 8 o 4P: 8, 9 register pair 10 o 5P: 10, 11 register pair 12 o 6P: 12, 13 register pair 14 o 7P: 14, 15 L'accumulatore è uno speciale registro a 4 bit, nel quale i dati possono essere elaborati, attraverso le istruzioni contenute nel programma. Per quanto riguarda la memoria, il processore 4004 può essere utilizzato con tre differenti tipi di memoria: ROM, PROGRAM RAM, DATA RAM. La Read-Only Memory ( ROM ) viene usata per memorizzare istruzioni e costanti che non possono cambiare, nel corso dell'esecuzione del programma, visto che un programma non può apportare alcuna modifica a queste celle di memoria. La memoria ROM può essere visualizzata come una sequenza di byte, ciascuno dei quali è composto da otto bit. La ROM ha 4096 byte disponibili. Ciascun byte ha un indirizzo numerico, compreso tra zero e 4095. La memoria ROM, inoltre, è ulteriormente suddivisa in "pagine", ciascuna delle quali è composta da 256 byte. Questo significa che le locazioni di memoria ROM comprese tra zero e 255 costituiscono la pagina zero, mentre le locazioni comprese tra 256 e 511 costiutiscono la pagina 1, e così via. La memoria RAM riservata al programma in esecuzione è organizzata esattamente come la memoria ROM, ma può essere modificata dal programma stesso. La RAM riservata al programma, come la ROM, è composta da 4096 byte e contiene le istruzioni e i dati del programma. La RAM riservata ai dati, infine, viene usata per la memorizzazione temporanea dei dati. Ci sono 8 banchi di memoria RAM, ciascuno dei quali è composto da 4 chip ( 0 - 3 ). Ciascun chip contiene 4 registri, ciascuno dei quali contiene 16 caratteri di 4 bit ciascuno ( locazioni di memoria ), più quattro caratteri di stato, sempre a 4 bit. Quindi, ciascun chip 4002 ( RAM ) contiene 320 bit, suddivisi in 4 registri, composti, a loro volta, da 20 caratteri a 4-bit ciascuno. Per le comunicazioni con i dispositivi esterni, ciascun chip RAM è dotato di 4 linee di output. Lo stack è costituito da tre registri a 12 bit, che conterranno gli indirizzi delle istruzioni. Visto che i programmi verranno sempre eseguiti nella ROM o nella RAM dedicata ai programmi, i registri di stack punteranno sempre a locazioni di memoria ROM o RAM, che sono 4096, sia per la ROM che per la RAM dedicata ai programmi ( ecco perchè i 12 bit dello stack ). Le funzioni di Input/Output ( I/O ), per le comunicazioni con i dispositivi esterni ( tastiera, mouse, etc. ), pur restando completamente separate dalle funzioni ROM o RAM, sono fisicamente allocate nei circuiti ROM e RAM. Ciascun chip 4001 ( ROM ) è organizzato come 256 word a 8 bit ciascuna ( 2048 bit totali ), utilizzabili per memorizzare programmi o tabelle. Ciascun chip, poi, contiene una porta a 4 bit di input-output ( I/0 ), per portare le informazioni verso l'esterno o dall'esterno verso l'interno. La CPU comunica con le memorie RAM e ROM attraverso un data BUS a 4 linee ( D0, D1, D2, D3 ), utilizzato per il flusso di dati tra i chip, con la sola esclusione dei segnali di controllo, che vengono inviati su 5 linee addizionali. Una CPU 4004 è in grado di controllare fino a 16 chip ROM ( 256 word per chip x 16 = 4096 word a 8 bit ciascuna ), fino a 16 chip RAM ( 80 locazioni a chip x 16 = 1280 locazioni a 4 bit ciascuna ) e fino a 128 linee di I/O ( Input/Output: 16 porte a 4 bit ROM + 16 porte a 4 bit RAM ) senza richiedere circuiti addizionali. Un programma altro non è che una sequenza di istruzioni. Ciascuna istruzione esegue un'operazione elementare, quale lo spostamento di un dato, un'operazione aritmetica, un cambiamento nella sequenza delle istruzioni. Un programma verrà memorizzato nella memoria ROM o nella memoria RAM riservata ai programmi. Un programma sarà costituito da una sequenza di istruzioni, scritte in notazione esadecimale. L'indirizzo della locazione di memoria contenente l'istruzione da eseguire viene registrato in un registro a 12 bit della CPU, chiamato Program Counter ( PC ). Al termine dell'esecuzione di un'istruzione, il registro PC viene incrementato di uno, ad indicare la locazione dell'istruzione successiva da eseguire. L'esecuzione di un programma, infatti, procede in modo sequenziale ( locazione dopo locazione ), a meno che non venga eseguita un'istruzione che trasferisce il controllo ad un altro programma o ad una diversa sequenza di istruzioni ( JUMP o SKIP ). In questo caso, il valore del Program Counter verrà impostato per uno specifico indirizzo di memoria. L'esecuzione delle istruzioni, quindi, riprenderà, sempre in modo sequenziale, da questo nuovo indirizzo di memoria. Anche esaminando il contenuto della memoria ROM o della memoria RAM riservata ai programmi, non c'è modo di capire se un byte contenga un'istruzione o un dato. Per esempio, il codice esadecimale F2 può rappresentare l'istruzione IAC ( Increment Accumulator ), oppure, più semplicemente, il valore F2, inteso come dato. È una responsabilità del programmatore assicurarsi che i dati non siano erroneamente interpretati come istruzioni. Ogni programma ha un indirizzo di memoria ( locazione ) in cui viene memorizzata la prima istruzione da eseguire. Subito prima dell'esecuzione di questa prima istruzione, il registro Program Counter verrà impostato con il valore corrispondente a questa locazione di memoria. Questa procedura verrà, poi, eseguita per ciascuna istruzione prevista dal programma. Le istruzioni per il processore 4004 possono richiedere 8 o 16 bit. Quindi, ipotizziamo che la prima locazione di memoria memorizzata nel registro Program Counter sia 13A: se l'istruzione corrispondente richiede solo 8 bit, l'indirizzo successivo che troveremo nel Program Counter sarà 13B. Se l'istruzione contenuta nella locazione di memoria 13B richiede 2 byte ( 16 bit ), il Program Counter passerà, successivamente, da 13B a 13D. Per evitare problemi con i dati, è sufficiente evitare che in una locazione di memoria nella quale ci si aspetta un'istruzione venga a trovarsi un semplice dato. Uno degli aspetti più importanti della programmazione è proprio la corretta identificazione delle locazioni di memoria. Con la CPU 4004, è possibile identificare una locazione di memoria in vari modi. Il primo metodo è l'indirizzamento diretto. L'istruzione: 43A2per esempio, contiene l'istruzione JUMP ( 4: Jump Uncoditional ) e la locazione di memoria ( 3A2 ) sulla quale eseguire il JUMP. Trattandosi di un sistema a 4 bit, questa istruzione occupa due locazioni di memoria di 8 bit ciascuna: 43 A2 In alcuni casi, un'istruzione contiene solo due cifre esadecimali, come indirizzo di memoria, mentre una locazione di memoria dovrebbe contenerne tre ( dodici bit, pari alla dimensione del registro Program Counter ). Questo accade quando la locazione di memoria di destinazione si trova nella stessa pagina in cui si trova l'istruzione. Ricordiamo che due locazioni di memoria si trovano nella stessa pagina quando hanno la stessa cifra esadecimale di ordine più alto ( i primi quattro bit, sui dodici totali ) nel loro indirizzo. Sappiamo che una pagina è composta da 256 byte ( 100 in notazione esadecimale ). Questo significa, per esempio, che la pagina numero 2 avrà inizio alla locazione di memoria ( in esadecimale ): 300 = 100 x 3e fine alla locazione: 3FF poichè alla locazione di memoria 400 ha inizio la pagina 3. Espresso in notazione binaria, la pagina 2, quindi, avrà come prima ed ultima locazione gli indirizzi: 001100000000 001111111111 dove i primi quattro bit ( i più significativi ) restano invariati. Quindi, se nelle locazioni di memoria: 3A0 3A1trovassimo un'istruzione del tipo: 12 0F dovremmo decodificare l'istruzione come: esegui un Jump Back ( istruzione 12: 1100 ) alla locazione ( relativa ) 0F. Trovandosi, l'istruzione, alla locazione 3A0, quegli otto bit ( 0F ) dovranno essere interpretati come: 30F Se, invece, la stessa istruzione si trovasse nella locazione di memoria 501, quell'indirizzo relativo, 0F, sarebbe da interpretare come locazione 50F. L'indirizzo relativo, 0F, può essere fornito in modo indiretto, facendo seguire l'istruzione non da una locazione relativa di memoria, bensì da una coppia di registri, dove trovare gli otto bit dell'indirizzo della locazione di memoria di destinazione. L'istruzione: 35 è un'istruzione di Jump Indirect ( 3: 0011 ), con indicata la coppia di registri ( quattro bit ciascuno ) 4 e 5, nei quali dovremmo trovare i valori 0F. Piuttosto che indicare, in modo diretto o indiretto, una locazione di memoria in cui trovare il valore da dare in pasto ad un'istruzione, è possibile indicare direttamente quel valore. Un'istruzione immediata è un'istruzione che dichiara esplicitamente i suoi dati. L'istruzione: D3 è da interpretare come: carica immediatamente ( Load Immediate, LDM: D ) il valore 3 nel registro accumulatore. Un altro concetto fondamentale nella programmazione è l'utilizzo delle subroutine. Prendiamo, come esempio, una semplice operazione aritmetica, come l'addizione. La CPU 4004 mette a disposizione le istruzioni adeguate per sommare due numeri a 4 bit. Questo significa che le istruzioni 4004 ci permetteranno di eseguire questa semplice operazione tra numeri compresi tra 0 e 15. Cosa accade quando dobbiamo sommare due numeri più grandi? Un'operazione del genere richiede una sequenza di istruzioni che, se ripetuta continuamente, comporterebbe un utilizzo intensivo della memoria. A meno che non si decida di memorizzare, una sola volta, la routine dell'addizione da qualche parte, trovando, poi, un modo per eseguirla nel momento del bisogno. Una routine ( o funzione ) frequentemente eseguita, come la nostra addizione, viene chiamata subroutine. La CPU 4004 mette a disposizione le istruzioni adeguate per eseguire le subroutine. L'esecuzione di una subroutine comporterà la temporanea interruzione della normale sequenza di istruzioni del programma in esecuzione, poichè il set di istruzioni della subroutine si troverà in un'area della memoria diversa da quella occupata dal programma in esecuzione. L'istruzione che permette l'esecuzione di una subroutine è l'istruzione CALL. Quando viene eseguita l'istruzione CALL, l'indirizzo di memoria dell'istruzione successiva del programma in esecuzione viene scritto nel registro chiamato STACK, mentre nel registro PROGRAM COUNTER viene scritto l'indirizzo di memoria in cui trovare la prima istruzione della subroutine da eseguire. La subroutine, quindi, viene eseguita. L'ultima istruzione di una subroutine sarà sempre un'istruzione speciale, Return Instruction, che legge l'indirizzo memorizzato nello STACK e lo trascrive nel PROGRAM COUNTER, permettendo al programma in esecuzione di ripartire. Visto che lo stack è composto da ben 3 registri a dodici bit, è possibile annidare fino a tre subroutine. La subroutine dell'addizione, per esempio può, a sua volta, chiamare un'altra subroutine. E così via. Passiamo, ora, al linguaggio di programmazione Assembly ed al set di istruzioni disponibile per la CPU 4004. Per coloro che hanno una certa familiarità con il linguaggio assembly, è disponibile l'appendice A, contenente l'intero set di istruzioni per la CPU 4004. Per tutti gli altri, questa sezione presenta le singole istruzioni, accompagnate da alcuni esempi e corredate dal codice macchina equivalente. A chi non ha una gran familiarità con il linguaggio assembly, è bene ricordare che, per poter inviare istruzioni alla CPU, è necessario conoscere il set di istruzioni eseguibili, fornito dal costruttore della CPU. Le istruzioni, infatti, non possono essere inventate, elaborate o modificate dal programmatore: ciascuna istruzione corrisponde ad una specifica implementazione hardware circuitazionale e solo il costruttore dei circuiti può illuminarci su modalità e sintassi di interazione. Scrivere i programmi in linguaggio assembly è il primo e più importante passo verso una programmazione economica ed efficiente. Il linguaggio assembly, infatti, mette a disposizione un set di istruzioni leggibili e comprensibili ed esime il programmatore dalla necessità di specificare indirizzi di memoria assoluti. I programmi in assembly sono scritti come una sequenza di istruzioni, che verrano convertite in codice eseguibile da uno speciale programma chiamato ASSEMBLER. Il programma, scritto, dal programmatore, in linguaggio assembly, viene chiamato SOURCE PROGRAM, o source code, o codice sorgente. L'assembler lo converte in codice macchina, OBJECT PROGRAM, che potrà essere caricato in memoria ( ROM oppure RAM ). Nell'emulatore e4004, l'OBJECT PROGRAM consiste in una sequenza di codici in notazione esadecimale. Vediamo un semplice esempio. La sequenza di istruzioni, espressa in notazione esadecimale ( OBJECT PROGRAM ): F0 43 56 ... 20 FF 60 è una possibile trasposizione, effettuata dall'assembler, del codice sorgente: NOW, CLB / Clear accumulator and carry JUN NEXT FIM 0, 255 NEXT, INC 0 Un'istruzione assembly contiene 4 campi distinti: la LABEL ( etichetta ), il CODE ( codice ), l'OPERAND ( indirizzo di memoria o dato da assegnare al CODE ), il COMMENT. Nel nostro esempio, abbiamo due etichette ( label ), NOW e NEXT, e quattro istruzioni: CLB o Clear both, che imposta a zero il bit di carry ( il bit che viene attivato quando un'operazione aritmetica termina con un riporto ) ed il registro accumulatore. Il codice OBJECT di questa istruzione è: 11110000 equivalente alla descrizione esadecimale: F0 Per dirla tutta, il codice dell'istruzione ( CODE ) è solo il primo nibble ( l'insieme dei primi quattro bit ) dell'istruzione: 1111 F mentre il secondo nibble dell'istruzione: 0000 0 rappresenta l'OPERAND, l'entità sulla quale va eseguita l'istruzione. Il valore zero indica il registro accumulatore ed il bit di carry, Per mpostare a zero il solo bit di carry, l'istruzione completa ( CODE + OPERAND ) diventa: CLC 11110001 F1 Per incrementare di uno il valore del registro accumulatore, l'istruzione completa ( CODE + OPERAND ) diventa: IAC 11110010 F2 Per impostare a uno il valore del bit di carry, l'istruzione completa ( CODE + OPERAND ) diventa: STC 11111010 FA La seconda istruzione presente nel nostro esempio è: JUN NEXT dove JUN indica l'istruzione Jump Uncoditional ( salta all'istruzione contenuta nella locazione di memoria indicata successivamente ): 0100 4 mentre l'etichetta NEXT ( OPERAND ) indica la locazione di memoria in cui si trova la successiva istruzione da eseguire. Nel nostro esempio: 0011 01010110 3 56 È presumibile, quindi, che l'istruzione contenuta nella LABEL NEXT: INC 0 0110 0000 60 sia contenuta proprio nella locazione di memoria: 0011 01010110 3 56 L'istruzione NEXT chiede di incrementare ( INC ) di uno il valore contenuto nel registro numero zero. L'ultima istruzione contenuta nel nostro esempio: FIM 0, 255 o Fetch Immediate, carica un valore a 8 bit in una coppia di registri. L'istruzione ( CODE ) è composta da 4 bit: 0010 2 mentre il secondo nibble ( gruppo di 4 bit ) indica la coppia di registri in cui caricare il valore a 8 bit: 0000 0 ed è seguito dagli otto bit del valore da caricare nei registri zero ed uno: 11111111 FF Naturalmente, gli ultimi 12 bit rappresentano i due OPERAND richiesti dall'istruzione FIM. Il campo LABEL, in un'istruzione assembly, è facoltativo ed identifica un indirizzo di memoria in cui trovare un'istruzione. Quando il campo LABEL è assente, l'istruzione deve iniziare con almeno uno spazio bianco. Un'etichetta ( LABEL ) può avere qualsiasi lunghezza, ma i primi tre caratteri devono differenziarsi da qualsiasi altra LABEL presente nel programma, altrimenti l'assembler non sarebbe in grado di distinguere l'una dall'altra. Il campo OPERAND contiene infrormazioni da utilizzare con il campo CODE, al fine di definire con esattezza l'operazione che deve essere eseguita dall'istruzione ( CODE ). Il campo OPERAND può non esserci oppure può consistere di uno o due oggetti, a seconda del tipo di istrruzione. Un campo OPERAND può contenere cinque tipi di informazione: un registro ( 4 bit ) di sorgente o di destinazione; una coppia di registri ( 4 + 4 bit ) di sorgente o di destinazione; un dato o una coppia di dati; un indirizzo di memoria, a 12 bit, oppure un'etichetta ( LABEL ) che identifichi una locazione di memoria; un codice condizionale, al fine di eseguire un Jump. Il tipo di informazione, inoltre, può essere specificato in cinque notazioni differenti: 1. un numero decimale: LDM 14 In questo esempio, il codice LDM ( Load Immediate ) carica il valore decimale 14 nel registro accumulatore; 2. il valore corrente del registro Program Counter, indicato con un asterisco. Per esempio, l'istruzione: JUN *+6 esegue un Jump Uncoditional dalla locazione di memoria contenente l'istruzione in esecuzione ( registrata nel Program Counter ) alla locazione di memoria situata a 6 locazioni di distanza. Se la locazione attuale è la 213, per esempio, il Jump Uncoditional verrà eseguito alla locazione di memoria 219; 3. un'etichetta ( LABEL ) alla quale l'assembler ha assegnato un numero decinmale. Per esempio, con: VAL = 42 ZER = 0 in notazione decimale, tutte le seguenti istruzioni caricano il numero decimale 42 ( 2A in notazione esadecimale ) nella coppia di registri zero ( registri 1 e 2 ): FIM 0, 42 FIM ZER 42 FIM ZER VAL 4. un'etichetta ( LABEL ) che appaia in un'altra istruzione, come abbiamo già visto in un esempio precedente: JUN NEXT NEXT, INC 0 5. un'espressione aritmetica. Un'espressione aritmetica accetterà argomenti a 12 bit e restituirà risultati a 12 bit, troncando alla sinistra l'eventuale risultato superiore ai 12 bit. Se consideriamo un'istruzione come l'insieme di CODE e OPERAND, è possibile identificare, nel processore 4004, istruzioni di lunghezza variabile: a otto bit ( 1-word instruction ) e a 16 bit ( 2-word instruction ). Le istruzioni 1-word ( 8 bit ) richiedono, per la loro esecuzione, 8 intervalli di clock ( 1 instruction cycle ), mentre le istruzioni 2-word ( 16 bit ) ne richiedono 16. Un'istruzione 1-word ( 8 bit ) occupa una locazione di memoria ROM ( dove ciascuna locazione di memoria è composta da 8 bit ), mentre un'istruzione 2-word occupa due locazioni di memoria ROM adiacenti. Ciascuna istruzione è divisa in due nibble ( gruppi di 4 bit ): i 4 bit più significativi contengono il cosiddetto operation code ( OPR ), mentre i 4 bit meno significativi contengono il modificatore ( OPERAND ) e costituiscono la cosiddetta OPA. Quando presente, il secondo byte dell'istruzione può contenere o un indirizzo di memoria ROM, oppure direttamente un dato. Abbiamo già visto alcuni esempi di istruzioni ad un byte. L'istruzione LDM ( contenuta nell'area dell'OPR ), per esempio, carica il valore riportato nel secondo nibble ( OPA ) nel registro accumulatore ( 14, decimale, nel nostro esempio: E in notazione esadecimale ): LDM 14 OPR OPA 1101 1110 D E Un esempio di istruzione a due byte è l'istruzione, appena incontrata: FIM 0, 255 OPR OPA 0010 0000 1111 1111 2 0 F F Nel primo caso, il dato su cui eseguire l'istruzione ( LDM ) è un dato a 4 bit ( E ), mentre nel secondo esempio il dato su cui eseguire l'istruzione è un dato a otto bit ( FF ). Occorre ricordare che la CPU 4004 ha un'architettura a 4 bit. Questo significa che anche le istruzioni a 8-bit vengono prelevate in due operazioni successive, 4-bit alla volta, in due intervalli di clock successivi. Anche quando un dato occupa una o più locazioni di memoria RAM, l'unità di misura restano i 4 bit. Le istruzioni aritmetiche assumono che i caratteri ( 4 bit ) sui quali operano siano memorizzati in uno speciale formato, chiamato "two's complement", complemento a due. Quando un carattere con segno ( signed ) viene interpretato come numero a complemento a due, i 3 bit meno significativi rappresentano la magnitudo del numero, mentre il bit più significativo viene interpretato come segno del numero. Quindi, pur avendo disponibili 16 numeri, con i 4 bit di base, nel caso dei numeri con segno i numeri positivi disponibili sono solo 8, a partire da zero: 0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 Il bit più significativo, sempre a zero, rappresenta il segno + ( numeri positivi ). Per rappresentare un numero negativo, però, non è sufficiente impostare il bit più significativo a 1. In questo caso, infatti, diventerebbero molto complicate le operazioni di addizione e sottrazione: 0101 + 1011 = 0000 In questo esempio, stiamo sottraendo 3 a 5: +5 + -3 = Sappiamo tutti, almeno per esperienza, che il risultato di questa semplice operazione non può essere che 2, mentre nel nostro sistema binario, in cui abbiamo solo modificato il bit più significativo del numero 3 ( 0011 ) per rappresentare il suo controvalore negativo ( 1011 ), il risultato è zero ( con un riporto, o carry ). La notazione two's complement permette di rappresentare i numeri interi negativi in modo tale da restituire zero quando sommati alla loro controparte positiva. Questo non accade quando neghiamo un numero, semplicemente modificando il primo bit da zero a 1: 0101 + 1101 = 0010 In questo caso, stiamo eseguendo: +5 + -5 = ottenendo un risultato alquanto bizzarro: 2, con riporto ( carry ). Come si calcola il controvalore negativo di un numero positivo, con la notazione two's complement? Si prende il numero positivo, per esempio 5: 0101 si esegue un NOT su tutti i suoi bit: 1010 e si aggiunge il valore 1: 1010 + 0001 = 1011 Quindi, se: 0101 rappresenta il valore positivo +5, la sequenza di bit: 1011 rappresenta il valore negativo -5. Per dimostrarlo, proviamo ad eseguire la sottrazione precedente: 0101 + 1011 = 0000 Sempre con riporto, che può essere ignorato. Quindi, nella notazione two's complement, il numero -5 è rappresentato dal numero esadecimale B ( 1011 in binario ). Vediamo se tornano i conti anche eseguendo l'operazione precedente: +5 + -3 = Il valore binario di +5 già lo conosciamo: 0101 Calcoliamo, ora, il valore -3 in notazione two's complement: NOT 0011 = 1100 1100 + 0001 = 1101 e rieseguiamo la nostra sottrazione: 0101 + 1101 = 0010 che corrisponde, correttamente, a 2, seppur con riporto ( candidamente ignorato ). Qual'è la rappresentazione in notazione two's complement del numero negativo -6? Partiamo dal valore positivo, +6: 0110 eseguiamo un NOT: 1001 e sommiamo il risultato a uno: 1001 + 0001 = 1010 che rappresenta il valore esadecimale A, ma in notazione two's complement rappresenta il numero negativo -6. Qual è il valore del numero esadecimale C, interpretato come numero in notazione two's complement? Il bit più significativo è impostato ( 1 ): 1100 Quindi, questa sequenza di bit rappresenta un numero negativo. Per calcolare la sua controparte positiva si eseguono le stesse operazioni che abbiamo eseguito sui numeri positivi: eseguiamo un NOT: 0011 e aggiungiamo uno: 0011 + 0001 = 0100 che equivale a 4. Quindi, il valore esadecimale C equivale, in notazione two's complement, al valore negativo -4. L'intervallo di numeri negativi che possono essere rappresentati in notazione two's complement, nella CPU 4004, a 4 bit, è il seguente: 1111 ( -1 ) 1110 ( -2 ) ... 1001 ( -7 ) 1000 ( -8 ) Naturalmente, nel caso di numeri senza segno, positivi, l'intervallo disponibile, con 4 bit, diventa: 0000 ( 0 ) 0001 ( 1 ) ... 1111 ( 15 ) Quando si esegue un'addizione, il carry bit è impostato ( 1 ) se l'operazione restituisce un risultato superiore a 15 ( decimale ). Quando si esegue una sottrazione, invece, il carry bit viene impostato ( 1 ), nel caso in cui il risultato sia positivo, mentre viene azzerato, nel caso in cui il risultato sia negativo. Proviamo a sottrarre 3 da 10, che significa eseguire un'addizione con un addendo negativo: +10 + - 3 = + 7 1010 + 1101 = 0111 In questo caso, abbiamo il risultato corretto ( + 7 ) ed il carry bit ( riporto ) impostato a 1, visto che 1+1 è uguale a zero, con riporto. Proviamo, ora, a sottrarre 15 da 12, operazione che dovrebbe restituire un risultato negativo: +12 + -15 = - 3 1100 + 0001 = 1101 La sequenza di bit: 1101 rappresenta, nella notazione two's complement, il valore negativo -3. Il valore restituito, quindi, è corretto. Il carry bit, inoltre, non è impostato ( quindi, è a zero ), poichè l'addizione non ha comportato alcun riporto. Proviamo, ora, ad eseguire una semplice addizione, 3+2, utilizzando l'emulatore 4004. Dal set delle istruzioni 4004, scopriamo che per eseguire un'addizione dobbiamo usare l'istruzione ADD, che è un'istruzione ad un solo byte: 1000RRRR dove: 1000 è il codice istruzione ( 8 ) e RRRR rappresenta un registro ( a 4 bit ). Il manuale 4004 spiega che: il contenuto a 4 bit del registro RRRR viene sommato al contenuto, sempre a 4 bit, del registro accumulatore, generando anche il riporto ( carry bit ). Il risultato dell'addizione viene scritto nell'accumulatore. Questo significa che per poter sommare 3 a 2, dovremo caricare il numero 3 in un registro a 4 bit, il numero 2 nell'accumulatore ed eseguire l'istruzione ADD: FIM 0, 3 LDM 2 ADD 1 Inserite questo codice, in notazione esadecimale, nella finestra ROM Block del simulatore: 2003D281 premete il tasto LOAD della stessa finestra, poi premete il tasto STEP della finestra 4004 CPU, tante volte quante sono le istruzioni da eseguire. Al primo STEP, vedrete apparire il valore 3 nel registro R1. Al secondo STEP, vedrete apparire il valore 2 nel registro accumulatore. Nel terzo STEP, vedrete apparire il valore 5 nel registro accumulatore. Fine del nostro primo programma. L'istruzione FIM già la conosciamo: carica nella coppia di registri 0, che indica la coppia dei registri 0 e 1, il valore a otto bit 3: 0010RRR0 DDDDDDDD 00100000 00000011 20 03 L'istruzione FIM ( Fetch Immediate ) lavora su coppie di registri. Anche l'istruzione LDM ( Load Immediate ), ad un solo byte, è a noi già nota: carica il dato indicato ( 4 bit ) nel registro accumulatore: 1101DDDD 11010010 D2 Infine, abbiamo l'istruzione ADD, anch'essa ad un solo byte: 1000RRRR 10000001 81 con la quale indichiamo al processore il registro numero 1, dal quale prelevare il dato da sommare al valore trovato nell'accumulatore. Il registro numero 1 è stato riempito dalla precedente istruzione FIM, che ha occupato il registro numero zero con i 4 bit più significativi del numero 3 ( quindi, 4 bit a zero ) ed il registro numero 1 con i 4 bit meno significativi: 0011 I registri vengono indicati, o a coppie, o singolarmente, nei 4 bit meno significativi ( OPA ) di un'istruzione ad un byte. Quando li si vuole indicare come coppie, si occupano solo i primi tre bit dell'OPA, mentre il quarto bit, il meno significativo è sempre a zero: 0000 ( coppia 0, 0/1 o 0P ) 0010 ( coppia 2, 2/3 o 1P ) 0100 ( coppia 4, 4/5 o 2P ) 0110 ( coppia 6, 6/7 o 3P ) 1000 ( coppia 8, 8/9 o 4P ) 1010 ( coppia 10, 10/11 o 5P ) 1100 ( coppia 12, 12/13 o 6P ) 1110 ( coppia 14, 14/15 o 7P ) Quando, invece, li si vuole indicare come singoli registri, li si numera in ordine sequemziale: 0000 ( 0 ) 0001 ( 1 ) 0010 ( 2 ) 0011 ( 3 ) ... 1111 ( 15 ) Vediamo, ora, cosa significa caricare un valore in un registro. Un registro è un circuito composto da un certo numero di elementi di memoria ( 4, per la CPU 4004 ). Se dovesse contenere solo un valore booleano ( VERO o FALSO: 1 o zero ), l'elemento di memoria più semplice sarebbe realizzato come un oggetto capace di assumere due stati, nel nostro caso zero o uno. Si parla in questo caso di elemento di memoria a 1 bit. L'implementazione fisica di un elemento di memoria a 1 bit si chiama flip-flop. Elementi di memoria a più bit sono composti da più flip-flop ( uno per bit ) affiancati, ed insieme costituiscono il cosìddetto registro. In genere in un processore tutti i registri usano come comando di memorizzazione il così detto clock ( CK ). Detta così, ad ogni transizione da zero a uno tutti i registri del processore memorizzerebbero ciò che vedono in ingresso. Quindi, il comando: LDM 2 memorizzerebbe il valore 2 in tutti i registri presenti nella CPU. Nella realtà ogni registro ha, oltre all'ingresso dei dati ( 4 bit ), all'uscita dei dati ( output: 4 bit ), all'ingresso di comando ( CLOCK: 1 bit ), anche un altro ingresso speciale di abilitazione ( EN: 1 bit ), la cui funzione è appunto abilitare la memorizzazione: il comando di memorizzazione viene eseguito solamente se l'ingresso di abilitazione ha il valore uno ( VERO ). Per eseguire la nostra somma, occorre scrivere nei registri 1 e ACCumulatore i numeri da sommare, e poi memorizzare il risultato S all'interno del registro ACCumulatore. Queste operazioni sono tutte eseguite dal processore, una alla volta. Prima di tutto, il processore mette sul bus dei dati il primo valore ( 3 ) e abilita la memorizzazione nel registro 1. Quindi mette sul bus dei dati il secondo valore ( 2 ) e abilita il registro ACCumulatore. Non appena scritto il secondo numero, la rete logica del sommatore produce in uscita la somma dei due numeri, per cui al processore non resta che dare il comando di memorizzazione al registro ACCumulatore, che memorizzerà la somma. Queste tre operazioni dovranno essere eseguite separatamente, una dopo l'altra, in tre diversi cicli del clock, in quanto rappresentano operazioni distinte. Se abilitassi il registro ACCumulatore insieme al registro 1, non memorizzerei la somma del registro 1 con il nuovo valore da immettere nell'ACCumulatore, ma quella del registro 1 con il numero che era memorizzato originariamente nel registro ACCumulatore. In ultima analisi, il codice assembler che viene eseguito dal processore non è altro che la sequenza di valori da presentare sui bus interni, abbinati ai registri da abilitare. Vediamo, ora, altre due istruzioni che coinvolgono i registri o coppie di registri: FIN 0011 RRR0 INC 0110 RRRR L'istruzione INC incrementa di 1 il valore del registro indicato con i 4 bit finali ( RRRR ). Quindi, per incrementare di uno il registro 4, il codice ( assembly, binario, esadecimale ) è il seguente: INC 4 0110 0100 64 Attenzione: il carry bit ( bit di riporto ) non viene in alcun modo coinvolto dall'istruzione INC. Nel caso di un overflow, quindi, il valore del registro tornerà a zero. L'overflow avviene quando si esegue l'istruzione INC su un registro che contiene il valore massimo esprimibile con il numero dei bit a disposizione: nel caso della CPU 4004, i bit che compongono un registro sono solo 4 ed il numero massimo rappresentabile è 15. Quindi, se il registro 6 contenesse il valore 15, l'istruzione: INC 6 0110 0110 66 porterebbe il registro 6 a zero, senza modificare il carry bit ( l'emulatore, in realtà, attiva il carry bit, ma credo si tratti di una disattenzione ). L'istruzione FIN ( Fetch Indirect ) lavora, anch'essa, su coppie di registri, come evidenziato dalla sintassi, dove il quarto bit è chiaramente sempre a zero. A differenza dell'istruzione FIM, che indica anche il valore a 8 bit da caricare nella coppia di registri indicata, l'istruzione FIN indica la coppia di registri in cui caricare il valore a 8 bit che si trova nella locazione di memoria memorizzata nella coppia di registri 0 ( registri zero ed uno ). La locazione di memoria deve trovarsi nella stessa pagina in cui si trova l'istruzione FIN stessa, visto che viene indicata con soli 8 bit. Il registro PC ( PROGRAM COUNTER ) contiene l'indirizzo completo a 12 bit dell'istruzione successiva da eseguire. Se l'istruzione FIN si trovasse nell'ultima locazione di memoria di una pagina, per esempio all'indirizzo relativo: 1111 1111 gli otto bit di dati verrebbero prelevati dalla pagna successiva, non dalla stessa pagina in cui si trova l'istruzione FIN. Quindi, se l'istruzione FIN si trovasse nella locazione di memoria 1FF: 0001 1111 1111 e i registri 0 e 1 contenessero i valori 3C: 0011 1100 l'indirizzo di memoria dal quale caricare il valore da trasferire sarebbe: 0010 0011 1100 23C, e non: 0001 0011 1100 13C. Come asserito dallo stesso manuale ufficiale della CPU 4004, questa è una pratica piuttosto pericolosa e andrebbe evitata, quando possibile. Per eseguire l'istruzione FIN, è necessario scegliere una locazione di memoria, in cui salvare il valore da trasferire alla coppia di registri indicata e scrivere quel valore nella locazione di memoria selezionata. L'indirizzo della locazione di memoria verrà, a sua volta, salvato nella coppia zero dei registri ( registri 0 e 1 ). Come si seleziona una locazione di memoria? Come abbiamo già visto, la scheda madre 4004 può contenere fino a 8 banchi di memoria RAM, ciascuno dei quali contiene 4 unità di dati ( chip ). Ciascun chip contiene 4 registri, ciascuno dei quali contiene 16 caratteri di 4 bit ciascuno ( locazioni di memoria ), più quattro caratteri di stato, sempre di 4 bit ciascuno. L'istruzione DCL ( Designate Command Line ) utilizza i 3 bit meno significativi del registro Accumulatore per selezionare uno degli otto banchi di memoria RAM disponibili, secondo il seguente schema: 0000 Bank 0 0001 Bank 1 0010 Bank 2 0011 Bank 3 0100 Bank 4 0101 Bank 5 0110 Bank 6 0111 Bank 7 Attenzione: l'emulatore di Maciej Szyc, inspiegabilmente, utilizza uno schema differente, per identificare i singoli banchi di memoria RAM: 0000 Bank 0 0001 Bank 1 0010 Bank 2 0100 Bank 3 0011 Bank 4 0101 Bank 5 0110 Bank 6 0111 Bank 7 Invertendo le sequenze di bit per i banchi 3 e 4. Inoltre, lo stesso emulatore rappresenta solo 4 banchi di RAM e non 8. Quindi, per selezionare il banco di memoria numero 2 ( a partire da zero ), occorre caricare, sia nella realtà che nell'emulatore, il valore 2 nell'accumulatore, per poi eseguire il comando DCL: LDM 2 DCL Ecco la stessa sequenza di istruzioni, in formato binario ed esadecimale: 11010010 11111101 D2 FD Il banco di memoria selezionato ( numero 2, nel nostro esempio ) resterà selezionato fino al prossimo comando DCL, oppure fino al prossimo segnale RESET. Il comando DCL, come abbiamo visto, occupa un solo byte e non richiede alcun parametro. Ora che abbiamo selezionato un banco di memoria RAM, dobbiamo specificare l'indirizzo esatto della locazione di memoria, all'interno del banco selezionato, che desideriamo selezionare. Per fare questo, esiste l'istruzione SRC ( Send Register Control ), una istruzione ad un solo byte, in cui i 4 bit meno significativi indicano una coppia di registri: 0010 RRR1 Negli otto bit della coppia di registri indicati, troveremo l'indirizzo della locazione di memoria da selezionare. Il solo banco di memoria RAM che riceverà l'indirizzo inviato dall'istruzione SRC sarà il banco precedentemente selezionato con l'istruzione DCL. Gli otto bit dell'indirizzo vengono così interpretati: i primi due bit ( a partire dal bit più significativo ) rappresentano il chip selezionato ( ogni banco di memoria contiene 4 chip ). I secondi due bit rappresentano un registro di memoria dei quattro presenti in ciascun chip. Gli ultimi 4 bit ( i meno significativi ) rappresentano uno dei 16 caratteri, a 4 bit, indirizzabili nel registro selezionato. Per esempio, l'indirizzo: 10110100 indica, all'interno del banco di memoria selezionato precedentemente, il carattere ( a 4 bit ) numero 4: 0100 del registro numero 3: 11 del chip numero 2: 10 Questo, almeno, fino a quando parliamo di dati RAM. Perchè un indirizzo, in realtà, identifica, contemporaneamente, anche una porta di I/O ( Input/Output ) RAM ( i primi due bit più significativi ), oppure una porta di I/O ( Input/Output ) ROM ( i primi quattro bit più significativi ). Nel nostro esempio, l'indirizzo identifica la porta RAM del chip numero 2 e la porta ROM numero 11 ( in notazione decimale ). I programmi comunicano con il mondo esterno attraverso porte a 4 bit. Le porte associate alla RAM possono essere utilizzate solo per l'output. Tornando ai dati RAM, è evidente che, all'interno di ciascun banco di memoria, gli indirizzi che vanno da zero a 63 ( in notazione decimale ) indicano i 64 indirizzi del chip zero: 00000000 ... ... 00111111 mentre gli indirizzi che vanno da 64 a 127 ( in notazione decimale ) indicano i 64 indirizzi del chip uno: 01000000 ... ... 01111111 mentre gli indirizzi che vanno da 128 a 191 ( in notazione decimale ) indicano i 64 indirizzi del chip due: 10000000 ... ... 10111111 mentre gli indirizzi che vanno da 192 a 255 ( in notazione decimale ) indicano i 64 indirizzi del chip tre: 11000000 ... ... 11111111 A questo punto, siamo in grado di proseguire nel nostro programmino di esempio, con il quale avevamo selezionato il banco di memoria numero 2, ma avevamo trascurato la selezione di un preciso indirizzo di memoria. Scegliamo l'indirizzo di memoria visto precedentemente: 10110100 CHIP 2, REGISTER 3, CHAR 4. Carichiamo l'indirizzo di memoria nella seconda coppia di registri ed eseguiamo l'istruzione SRC: LDM 2 DCL FIM 1P 180 SRC 1P 11010010 11111101 00100010 10110100 00100011 D2 FD 22 B4 23 Ricordarsi che l'istruzione SRC vuole una coppia di registri ( 001 ), seguita da un bit 1: 0010 RRR1 Tutte le istruzioni successive all'istruzione SRC, fino alla successiva istruzione SRC, verranno, quindi, riferite al carattere RAM ( dei dati ) numero 4, del registro numero 3 del chip numero 2 del banco di memoria RAM precedentemente selezionato con l'istruzione DCL. Oppure, ad uno dei 4 caratteri di stato associati al registro RAM ( dei dati ) numero 3 del chip numero 2 del banco di memoria RAM precedentemente selezionato con l'istruzione DCL. Oppure, alla porta RAM di output numero 2 ( associata al chip numero 2 ) del banco di memoria RAM precedentemente selezionato con l'istruzione DCL. Oppure, alla porta ROM numero 11 del banco di memoria RAM precedentemente selezionato con l'istruzione DCL. Le istruzioni che permettono di accedere ad una locazione di memoria RAM o che eseguono operazioni di input o output sono molteplici. Per scrivere il valore contenuto nell'accumulatore nella locazione di memoria precedentemente selezionata con l'istruzione SRC, per esempio, si usi l'istruzione ad un byte WRM: LDM 2 DCL FIM 1P 180 SRC 1P WRM 11010010 11111101 00100010 10110100 00100011 11100000 D2 FD 22 B4 23 E0 Inserite questo codice, in notazione esadecimale, nella finestra ROM Block del simulatore, premete il tasto LOAD della stessa finestra, poi premete il tasto STEP della finestra 4004 CPU, tante volte quante sono le istruzioni da eseguire. Al primo STEP, vedrete apparire il valore 2 nel registro accumulatore. Al secondo STEP, non vedrete nulla, poichè il secondo STEP esegue l'istruzione di selezione del banco di memoria 2 ( DCL ). Al terzo STEP, vedrete apparire il valore B4 nella coppia di registri 2 e 3 ( 1P ). Anche al quarto STEP non vedrete nulla, poichè qui viene eseguita l'operazione di selezione dellla locazione di memoria ( SRC ). Al quinto STEP viene eseguita l'operazione di scrittura del valore 2, contenuto nell'accumulatore, nella locazione di memoria che si trova nel banco 2, chip 2, registro 3, carattere 4. Per vedere il valore 2 scritto nel quarto carattere del registro 3, del chip 2 del banco RAM 2, dovrete selezionare banco e chip in una delle due finestre dell'emulatore denominate 4002 RAM ( le due finestre, inspiegabilmente, sono identiche ). Fine del nostro secondo programma. Le altre istruzioni che permettono di accedere alla RAM o alle porte di input/output sono tutte istruzioni ad un solo byte e utilizzano l'indirizzo precedentemente selezionato con le istruzioni DCL e SRC: 1110 0000 WRM (Write Main Memory) 1110 0001 WMP (Write RAM Port) 1110 0010 WRR (Write ROM Port) 1110 0011 WPM (Write program RAM) 1110 0100 WR0 (Write Status Char 0) 1110 0101 WR1 (Write Status Char 1) 1110 0110 WR2 (Write Status Char 2) 1110 0111 WR3 (Write Status Char 3) 1110 1000 SBM (Subtract Main Memory) 1110 1001 RDM (Read Main Memory) 1110 1010 RDR (Read ROM Port) 1110 1011 ADM (Add Main Memory) 1110 1100 RD0 (Read Status Char 0) 1110 1101 RD1 (Read Status Char 1) 1110 1110 RD2 (Read Status Char 2) 1110 1111 RD3 (Read Status Char 3) Vediamo alcuni esempi. Il codice: FIM 2P 5 SRC 2P RDM 00100100 00000101 00100101 11101001 24 05 25 E9 legge ( RDM ) il contenuto della RAM di dati carattere 5, registro zero, chip zero del banco di memoria precedentemente selezionato, trasferendolo nell'accumulatore. Per vedere all'opera l'istruzione RDM, utilizziamo l'esempio precedente dopo aver modificato il valore contenuto nell'accumulatore: LDM 2 DCL FIM 1P 180 SRC 1P WRM LDM 6 RDM 11010010 11111101 00100010 10110100 00100011 11100000 11010110 11101001 D2 FD 22 B4 23 E0 D6 E9 Nononstante il valore dell'accumulatore venga modificato da 2 a 6, l'istruzione RDM lo riporterà al valore originale ( 2 ), prendendo tale valore dal carattere 4, registro 3, chip 2 del banco di RAM 2, dove lo avevamo appena memorizzato, con l'istruzione WRM. Se è possibile scrivere o leggere singole locazioni di memoria RAM, è anche possibile scrivere o leggere ciascuno dei 4 caratteri di stato che si trovano in ciascun registro. Le istruzioni: 111001 00 WR0 (Write Status Char 0) 111001 01 WR1 (Write Status Char 1) 111001 10 WR2 (Write Status Char 2) 111001 11 WR3 (Write Status Char 3) permettono di scrivere il valore presente nel registro accumulatore nel carattere di stato specificato ( zero, 1, 2, 3 o 4 ) del registro di memoria precedentemente selezionato, mentre le istruzioni: 111011 00 RD0 (Read Status Char 0) 111011 01 RD1 (Read Status Char 1) 111011 10 RD2 (Read Status Char 2) 111011 11 RD3 (Read Status Char 3) permettono di leggere, trasferendolo nel registro accumulatore, il valore presente nel carattere di stato specificato ( zero, 1, 2, 3 ) del registro di memoria precedentemente selezionato: LDM 2 DCL FIM 1P 180 SRC 1P WRM WR1 11010010 11111101 00100010 10110100 00100011 11100000 11100101 D2 FD 22 B4 23 E0 E5 Grazie a questa sequenza di istruzioni, il valore presente nel registro accumulatore verrà scritto sia nel carattere 4 ( locazione di memoria ), sia nel carattere di stato numero 1 del registro numero 3 del chip numero 2 del banco di memoria 2. Per leggere e scrivere le porte ROM e scrivere le porte RAM, sono disponbili le istruzioni: 1110 1010 RDR (Read ROM Port) 1110 0010 WRR (Write ROM Port) 1110 0001 WMP (Write RAM Port) Naturalmente, queste istruzioni verranno eseguite sulla porta del chip e del banco di memoria precedentemente selezionati: LDM 2 DCL FIM 1P 180 SRC 1P WMP 11010010 11111101 00100010 10110100 00100011 11100001 D2 FD 22 B4 23 E1 Questo codice scrive ( WMP ) il valore contenuto nell'accumulatore ( 2, nel nostro caso ) nella porta RAM a 4 bit del chip numero 2 del banco di memoria numero 2. Questo perchè il numero del chip interessato dall'istruzione è indicato dai due bit più significativi del numero caricato nella coppia di registri numero 1. Il valore caricato è 180 ( decimale ): 10110100 i cui due bit più significativi: 10 110100 indicano il chip numero 2. Se si trattasse di una porta ROM, invece, è bene tenere a mente che il chip selezionato è indicato dai quattro bit più significativi del numero caricato nella coppia di registri. Nel nostro caso: 1011 0100 si tratterebbe del chip ROM numero 11.
|
|||||||||
The Intel 4004 Microprocessor emulator | Disclaimer: questo è un link a contenuti ospitati su server esterni. |