Universita' degli Studi di Roma - La Sapienza Corso di laurea in Fisica
DISPENSE DEL CORSO DI CALCOLATORI ELETTRONICI (Modulo A) ======================================================== Tenuto dal Prof. Roberto Biancastelli
N.B. Per ridurre le dimensioni del file tutta la grafica e' stata realizzata in modalita? testo, usando i caratteri grafici che devono essere disponibili nel programma di visualizzazione. Si puo' usare il programma NotePad, con caratteri Terminal.
Anno Accademico 2003-2004 Copyright 1988-2004 !-----------------------------------------------------------------------
PREFAZIONE ══════════ Il corso e quindi anche le dispense sono suddivisi in tre 'moduli' A, B e C, organizzati come indicato nel programma. I primi due sono per la laurea quadriennale, mentre il modulo C (capitoli 6-14) e' per la laurea triennale (richiede la propedeuticita' dei corsi di Laboratorio di Calcolo e di Comunicazione Scientifica e Tecnologica). La relativa didattica e' stata organizzata in una serie di dispense in vista di un'informatizzazione completa del corso, con tecnologie multimediali e telematiche. Queste dispense, con tutti i programmi delle esercitazioni pratiche, sono distribuite gratuitamente su floppy disk in copia unica e per uso personale esclusivo, ai soli studenti regolarmente iscritti al corso del prof.Roberto Biancastelli. Contenendo dei caratteri grafici, le dispense sono visualizzabili e stampabili con i programmi editor standard (in MS-DOS con EDIT o in Windows con NotePad, caratteri 'Terminal' e Formato senza "A capo automatico"). Se la dimensione di un file risultasse 'troppo estesa', occorre modificare le impostazioni di memoria della finestra DOS, cliccando prima con il pul-
sante destro sull'icona DOS o sulla barra del titolo della finestra DOS e poi con il pulsante sinistro su Proprieta' ed impostando quindi tutte le opzioni in 'Automatico'). I files scritti in formato Word invece possono essere letti dagli studenti, che non possiedono il programma MS-Word, scaricando gratuitamente dal sito della Microsoft ( www.microsoft.com --> Office --> Word Downloads ) il programma WordViewer97/2000 (wd97vwr32.exe). E' opportuno precisare che, essendo il resto del corso di laurea in fisica piuttosto povero di insegnamenti informatici, il compito che attende lo studente del corso di Calcolatori Elettronici e' piuttosto impegnativo. Cio' perche' questa materia, a differenza di altre, e' soggetta ad un continuo e rapido progresso tecnologico, che ha ampliato sempre di piu' negli ultimi anni il campo di insegnamento. Poiche' molti studenti arrivano a questo corso con una conoscenza iniziale della materia praticamente nulla, il docente deve risolvere due problemi di non poco conto: insegnare sempre di piu' nello stesso numero di lezioni e gestire didatticamente la crescente complessita' delle nuove tecnologie informatiche. La soluzione finora adottata e' stata quella di inserire gradualmente nel programma una parte su tecnologie avanzate e programmazione ad oggetti ed in rete. Gli studenti che, non avendo avuto una conoscenza iniziale della materia, si trovassero in difficolta' nella preparazione all'esame su quest'ultima parte non dovranno preoccuparsi, perche' nell'esame finale non verra' loro richiesta in modo fiscale una conoscenza approfondita di questi argomenti. Si tenga inoltre presente che in questo corso l'apprendimento non deve essere finalizzato al voto, ma all'acquisizione di una professionalita'in questa materia, che sia spendibile poi nel mondo del lavoro e della ricerca). Tuttavia gli studenti arrivati a questo corso senza precedenti conoscenze propedeutiche all'informatica di base (algebra di Boole, elettronica digitale, ecc.) sono invitati a studiare dall'inizio tutti gli argomenti esposti nel programma e spiegati nelle lezioni, tralasciando eventualmente gli ultimi argomenti riguardanti la programmazione in rete, applet java, ecc. Invece gli studenti arrivati a questo corso, con le conoscenze di base ed una certa esperienza di programmazione in un qualche linguaggio di alto livello precedentemente acquisita, potranno rinfrescare velocemente le loro conoscenze di base, esposte nella prima parte del programma e nelle lezioni, dedicando poi un tempo maggiore sulle ultime parti del programma riguardanti la programmazione ad oggetti, in rete, Internet, ecc. La modalita' di svolgimento delle esercitazioni e' stata organizzata in modo da permettere anche il lavoro parallelo di gruppi di studenti, che trattano argomenti diversificati in funzione della loro preparazione e degli obiettivi proposti (nell'ambito degli argomenti trattati nel corso). Ove possibile, con le cautele sotto indicate per impedire la diffusione di virus informatici, la prosecuzione del lavoro pratico sul computer di casa e' vivamente incoraggiata. !----------------------------------------------------------------------INDICE GENERALE ═══════════════ INDICE PROGRAMMA DEL CORSO DI CALCOLATORI ELETTRONICI PROGRAMMA LOGICO DEL CORSO
INTRODUZIONE Capitolo
1 - Elementi del linguaggio Pascal
Capitolo
2 - Sistemi di numerazione e rappresentazione delle informazioni
Capitolo
3 - Strutture di dati
Capitolo
4 - Introduzione all'algebre di Boole
Capitolo
5 - Circuiti logici digitali
Capitolo Capitolo Capitolo Capitolo Capitolo Capitolo Capitolo Capitolo Capitolo
Modulo C 6 - Architettura di un calcolatore elettronico <──────────┐ │ 7 - Sottoprogrammi e passaggio dei parametri │ │ │ 8 - Programmazione Assembly │ Fine Modulo A │ 9 - Sistema di Input/Output <───────────────> │ Inizio Modulo B │ 10 - Programmazione concorrente │ │ 11 - Sistema di memoria │ │ 12 - Sistemi operativi │ │ 13 - Sistemi in rete │ │ 14 - Programmazione ad oggetti e linguaggio Java <─────────┘
Capitolo 15 - Programmazione sul Web: Apache, HTML, CGI, Java apple Capitolo 16 - Architetture avanzate Capitolo 17 - Acquisizione di dati sperimentali APPENDICE A - Sigle e definizioni utili APPENDICE B - Il simulatore H6809 APPENDICE C - Programma delle esercitazioni !----------------------------------------------------------------------Programma del Corso di CALCOLATORI ELETTRONICI ══════════════════════════════════════════════ Prof.R.Biancastelli - Dipartimento di Fisica Universita' di Roma - La Sapienza ────────── MODULO A ─────────── RAPPRESENTAZIONE DELLE INFORMAZIONI (D,W): Sistemi numerici - Rappresentazioni posizionali - Conversioni di base - Rappresentazioni dei negativi Aritmetica binaria in complemento a due -Carry e Overflow - Rappresentazione dei numeri frazionari - Conversioni in virgola fissa e virgola mobile - Rappresentazione BCD - Codici binari. ELEMENTI DI PASCAL (W,D,G): Intestazioni, tipi e dichiarazioni di variabili, istruzioni semplici e composte, diagrammi sintattici - Procedure
│
e funzioni - Parametri formali e passaggio dei parametri. STRUTTURE DI DATI (D,W): Vettori e matrici - Pile (Stack) - Code - Liste (a singolo e doppio 'link') - Gestione delle liste: free list e garbage collection. INTRODUZIONE ALL'ALGEBRA DI BOOLE (C,D): Operazioni e loro proprieta' Teorema di De Morgan e legge di dualita' - Teorema di sviluppo: Mintermini, Maxtermini e forma canonica - Minimizzazione: il metodo delle mappe di Karnaugh - Operatori universali - Operatore OR-esclusivo (coincidenza e anticoincidenza). CIRCUITI LOGICI DIGITALI (C,D): Circuiti combinatori SSI: Analisi e sintesi - Circuiti combinatori MSI, LSI - Sintesi a ROM e PLA Circuiti sequenziali - Memorie ROM, RAM statiche, dinamiche e associative. Modulo C <────────┐ ARCHITETTURA DI UN ELABORATORE (D,W): Organizzazione interna - Bus │ di memoria - Registri - Contatori di programma e unita' di controllo │ Unita' aritmetica - I/O bus - Istruzioni - Modi di indirizzamento: a │ registro , a registro indiretto , assoluto , assoluto indiretto, imme│ diato, autoincremento, relativo, a registro base e indexato - Sistema │ di I/O: sistemi a bus dedicato e memory mapped - Porte di I/O - Proto│ colli Handshake - Interruzioni - Microcomputer H6809 e sua simulazione │ in Pascal. │ │ PROGRAMMAZIONE ASSEMBLY (D,W) - Pseudo-istruzioni - Compilazione: PLC, │ tabella dei simboli e modulo oggetto - Correlatore (Link Editor) │ Caricatore (Loader) - Rilocazione - Macro-istruzioni. │ │ ─────────── MODULO B ─────────── │ │ ELABORATORI NUMERICI (D,W): Organizzazione interna dell'unita' centrale │ Microcomputer H6809 e sua simulazione in Pascal - Unita' di controllo │ canalizzate - Gestione della memoria - Memory Management Unit │ Paginazione (memory mapping): mappa di traduzione degli indirizzi │ Classificazione delle macchine: ad accumulatori (H6809), a registri │ generali (H8000) ed a stack (H11) - Processori RISC. │ │ SISTEMA DI INPUT/OUTPUT (W,D): Sistemi a bus dedicato e memory mapped │ - Porte di I/O - Protocolli Handshake - Interfacciamenti: Paralleli │ (Centronix, IEEE 488) e seriali (asincrono RS232C e sincroni BSC) │ Programmazione I/O e sovrapposizione - I/O drivers - Sistema di inter│ ruzione - Gestione delle priorita': geografica, daisy chain, mask bits, │ round-robin, polling e interruzioni vettorizzate - Interruzioni interne │ (traps) e software (system calls) - Programmazione delle interruzioni │ Accesso diretto alla memoria (DMA) - Sottosistemi a dischi - Canali di │ I/O controllati da processori. │ │ PROGRAMMAZIONE CONCORRENTE (D,W): Processi e loro stati - Concorrenza │ con I/O in sovrapposizione - Variabili condivise e sezioni critiche │ Errori timing-dependent e stallo (dead-lock) - Protezione delle sezioni │ critiche - Semafori - Bloccaggio di strutture di dati - Condivisione di │ struzioni. │ │ SISTEMA DI MEMORIA (D,P): Organizzazione gerarchica - Memoria a banchi, │ interleaved, multiport - Fetch multiplo - Registro di istruzioni a │ buffer - Memoria cache - Memoria virtuale - Gestione della memoria: │ partizioni fisse e variabili, paginazione, memoria virtuale. │ │
SISTEMI OPERATIVI E RETI (C,D,I): Dedicati, a lotti (batch) e interattivi │ (time-sharing) - Multitasking e multiprogrammazione - Reti locali │ Software di rete │ │ ELEMENTI DEL LINGUAGGIO JAVA (D,J): Programmazione ad oggetti - Fondamenti │ del linguaggio Java: classi, oggetti, variabili e metodi - I/O in Java │ Interfaccia grafica Swing (GUI) - Gestione degli eventi e delle eccezioni - │ Applicazioni e applet - Connessioni in rete e programmazione client-server. │ <─────────┘ INTERCONNESSIONE DI RETI E INTERNET (D,I,H) - Indirizzamento IP - Protocolli TCP - Inter-reti - Internet - Browser e linguaggio HTML - Web server Script CGI e Javascript ARCHITETTURE AVANZATE (C,D): Classificazione dei sistemi di elaborazione parallela: MISD, SIMD,MIMD - Tipi di accoppiamento: Multiprocessor, multicomputer e reti locali - Esempi ACQUISIZIONE DI DATI SPERIMENTALI (D): Sistemi standard di acquisizione di dati (NIM, CAMAC, VME e FASTBUS) - Organizzazione di un sistema multitask in tempo reale - Supervisione dell'elaborazione parallela (centralizzata e distribuita) - Multiprocessing (esempi su VME e FASTBUS). ─────────── ESERCITAZIONI ─────────── Modulo A: Programmazione in Pascal - Simulatore del computer H6809 Uso pratico del linguaggio binario e mnemonico Assembly del computer H6809 con simulatore e debugger. Modulo B: Programmazione concorrente su rete locale di PC (farm per la simulazione Montecarlo dello scattering coulombiano) - Uso pratico del linguaggio Java - Istallazione ed uso di Web server con uso di applet Java, Javascript e script CGI - Programmazione concorrente (client-server) su rete Intranet. Modulo C: Uso pratico del linguaggio binario e mnemonico Assembly del computer H6809 con simulatore e debugger - Programmazione concorrente su rete locale di PC (farm per la simulazione Montecarlo dello scattering coulombiano) - Uso pratico del linguaggio Java - Programmazione concorrente (client-server) su rete Intranet.
PRINCIPALI RIFERIMENTI BIBLIOGRAFICI: ──────────────────────────────────── Testo base: D: Dispense, esercizi e fotocopie di appunti/trasparenze distribuite gratuitamente dal docente su floppy disk. Testi per consultazione: W: J.Wakerly - Microcomputer Architecture and Programming - Ed. Wiley G: M.Gori et al. - Pascal e C - Ed. Mc Graw Hill C: G.Cioffi,V.Falzone - Manuale di Informatica - Ed. Calderini P: D.A.Patterson et al.- Struttura e progetto...- I/F Hard/Softw. J: Cay Horstmann - JAVA 2, i fondamenti - Ed. Mc Graw Hill I: D.Comer - Internet e reti di calcolatori - Ed. Addison-Wesley H: G.B.Shelly et al. - HTML imparare per progetti Per informazioni, spiegazioni e chiarimenti e' possibile concordare durante tutto l'anno accademico incontri con il professore al
349.5553010 anche al di fuori degli orari di ricevimento (studenti lavoratori). Si puo' comunicare con il professore anche all'indirizzo di posta elettronica
[email protected] !----------------------------------------------------------------------PROGRAMMA LOGICO DEL CORSO ========================== (Grafo che evidenzia le connessioni logiche tra i vari argomenti) RAPPRESENTAZIONE INFORMAZIONI ──────┬────── │ ┴ STRUTTURE DATI ───────┬────── │ ┴ ALGEBRA DI BOOLE ────────┬─────── │ ┴ ELETTRONICA DIGITALE ──────────┬───────── │ ┴ ARCHITETTURA ELABORATORE ^ ┌──────────────────────────┐ │ │ UNITA' CENTRALE │ │ │ ┬ │ ┴ ┌───│─< SISTEMA │ SISTEMA >──│───┐ Modulo A │ │ I/O │ MEMORIA │ │ │ └────────────┼─────────────┘ │ ......... │ │ │ │ PROGRAMMAZIONE │ Modulo B │ (ASSEMBLY,PASCAL,JAVA...) │ ┬ │ │ │ │ │ │ │ │ SISTEMA ────> PROGRAMMAZIONE MEMORY │ D'INTERRUZIONE CONCORRENTE MANAGEMENT │ │ │ │ │ │ │ │ v │ ┌─────────┴────────┐ │ └─────>│ SISTEMI OPERATIVI│<───────┘ │ MONOUTENTE │ └─────────┬────────┘ │ │ SPECIALI ACCORGIMENTI HARDWARE │ │ ┌───────────┴───────────┐ │ SISTEMI OPERATIVI │
┌───────────────│ MULTIUTENTE,MULTITASK │────────────┐ │ │ SOFTWARE DI RETE │ │ │ └───────────┬───────────┘ │ │ │ │ │ │ │ │ MULTIPROGRAMMAZIONE <──┼──> MULTIPROCESSING │ │ ─┬ │ ┬─ │ │ │ │ │ │ ────┴──────────────── │ │ │ ──────────────┴────────────── ARCHITETTURE AVANZATE │ │ │ RETI LOCALI/INTERNET/INTRANET │ ─┬── │ │ │ ─┬─ │ │ │ │ │ │ │ ┌─┴─────┴───────┴──────┴─────────┴──┐─────> CAMAC │ │ ACQUISIZIONE DI DATI SPERIMENTALI │─────> VME │ │ ( ed altre applicazioni ) │─────> FASTBUS . └───────────────────────────────────┘─────> ECC. . . !----------------------------------------------------------------------INTRODUZIONE ════════════ Il campo dei calcolatori elettronici e' forse quello che piu' di tutti negli ultimi decenni ha mostrato un progresso sfrenato. Infatti non solo ha raccolto i frutti del rapidissimo progresso della microelettronica, ma ha anche contribuito al progresso della nostra societa' con le grandi innovazioni nel campo del software e della telematica. Questo enorme progresso, unito alla grande rapidita' di evoluzione di idee e sistemi, condiziona a nostro parere le modalita' di una didattica istituzionale in questa materia. Infatti, mentre da un lato gli studenti sono desiderosi di arrivare rapidamente all'apprendimento delle macchine e sistemi piu' moderni, dall'altro questi non risultano adatti ad un efficiente apprendimento di base della materia, poiche' mentre sono destinati ad una rapida obsolescenza, inevitabilmente la loro complessita' pone degli ostacoli, complicando e rallentando inutilmente la comprensione dei concetti di base, la cui acquisizione, anche se fatta in maniera piu' semplice, permette poi di capire meglio e piu' rapidamente il funzionamento non solo dei sistemi attuali (che hanno una VITA LIMITATA) ma anche e soprattutto di quelli futuri. Per questi motivi nel nostro corso adotteremo come macchina di riferimento il miglior processore tra quelli piu' semplici ad 8 bit prodotto alla fine degli anni settanta e addirittura lo semplificheremo ulteriormente riducendone il numero dei registri allo stretto indispensabile. In questo modo, per il nostro corso, il processore ideale H6809 che ne deriva assume lo stesso ruolo che il punto materiale svolge in Meccanica o che il gas perfetto svolge in Termodinamica: un'idealizzazione che non ha riscontro nella realta' ma che ci aiuta a comprendere i meccanismi che stanno alla base della materia. Per meglio comprendere i meccanismi di funzionamento impiegati nei moderni calcolatori elettronici, pur nella loro complessita', seguiremo nella nostra trattazione un criterio di evoluzione 'storica', con cui progrediremo con gradualita', dai sistemi uniprocessor con
║ ║ ║ ║ ║ ║ ║ ║ ║ ║
sistema d'interruzione alla programmazione concorrente ed alla multiprogrammazione per arrivare infine alle architetture avanzate di elaborazione parallela multiprocessore. Essendo nostra convinzione che un apprendimento di questa materia non puo' essere disgiunto dall'acquisizione di capacita' operative sull'uso dei computers, abbiamo associato alle lezioni teoriche delle esercitazioni pratiche svolte su personal computer dotati di un programma simulatore che fa funzionare il PC come se dentro ci fosse un processore H6809. In questo modo lo studente puo' meglio comprendere i principi della programmazione dei calcolatori elettronici che studia a lezione. Un'ultima osservazione riguarda gli studenti del corso di laurea ║ in fisica, ai quali questo corso e' particolarmente diretto: come ║ deve essere gia' loro noto, la fisica si occupa di scoprire e stu- ║ diare le leggi fisiche della Natura ovvero i rapporti quantitativi ║ che intercorrono tra le grandezze osservabili, cioe' suscettibili ║ di misurazione e quindi quantificabili con numeri: per questo, ║ come la matematica ed i calcoli numerici hanno un ruolo fondamentale║ in fisica, cosi' anche i calcolatori elettronici sono fondamentali ║ per i fisici, come strumenti di calcolo (OFF-LINE). ║ Inoltre i calcolatori elettronici sono anche l'elemento centrale di ║ analisi e controllo in linea (ON-LINE) di tutti i moderni apparati ║ sperimentali e vanno quindi visti come parte essenziale dello stru- ║ mento con cui il fisico misura le grandezze fisiche osservabili, ║ che sono oggetto della fisica sperimentale. ║ Cosi' lo studio dei calcolatori elettronici e dei loro campi e modalita' di impiego va associato allo studio dei trasduttori della informazione fisica (per esempio i rivelatori di particelle), ed alle metodologie di acquisizione ed analisi dei dati sperimentali, che concorrono a formare lo strumento con cui indaghiamo il mondo fisico. !----------------------------------------------------------------------AVVERTENZE IMPORTANTI PER LE ESERCITAZIONI ========================================== Licenza d'uso: Le dispense per loro natura devono fare riferimento ai libri di testo consigliati, sui quali lo studente puo' cosi' trovare, in modo omogeneo, le informazioni per gli ulteriori approfondimenti. Cio' e' stato fatto dal docente con la massima attenzione al rispetto delle disposizioni di legge sul diritto di autore e pertanto il materiale didattico consegnato allo studente non deve essere in nessun caso alterato ne' duplicato o diffuso senza specifica autorizzazione scritta dell'autore. Il software contenuto nei dischetti consegnati dal docente e' di proprieta' dei rispettivi fornitori e concesso in licenza all'Universita' di Roma-La Sapienza. Puo' essere usato solo presso questa Universita' nel rispetto delle relative licenze, sui computer assegnati per le esercitazioni, per fini didattici e non puo' essere ne' asportato ne' copiato con qualsiasi mezzo. Lo stesso vale per il testo delle dispense, la cui circolazione deve essere limitata agli studenti iscritti al corso del prof.R.Biancastelli. Eventuali responsabilita' civili e penali previste dalle leggi vigenti, conseguenti al mancato rispetto
delle disposizioni impartite al riguardo dal docente e dagli organi universitari, sono solo dei trasgressori. Per il materiale scaricato dalla rete devono essere sempre rispettate le restrizioni di copyright indicate dai relativi fornitori. Poiche' spesso tra gli studenti si subiscono questi doveri come ingiuste vessazioni, il docente ritiene utile esprimere la propria opinione al riguardo: nei Paesi in cui e' consentito a liberi imprenditori di investire nella ricerca sul software, ripagandosi poi degli investimenti fatti con i GIUSTI proventi della vendita, una efficace tutela giuridica del software e dei diritti di autore permette, al Paese che la pratica, di sviluppare l'industria del software e di creare con essa numerosi posti di lavoro, che in prospettiva valgono molto di piu' del software trafugato (purche' le Autorita' sappiano contrastare le tendenze ai monopoli nel software per salvaguardare, con la concorrenza, il livello e la giustezza dei prezzi). Virus: Le esercitazioni si svolgeranno usando un dischetto personale per ogni studente: su questo dischetto vi saranno sia il file contenente le dispense del corso, che appunti (LEZ??.PAS) e programmi per le esercitazioni. Lo studente dovra' riconsegnare questo dischetto al docente al termine delle esercitazioni e non usare dischetti propri per ragioni di sicurezza e protezione dai 'virus'. Un virus e' un programma nascosto all'interno di un altro programma usato dell'utente. Quando viene lanciato in esecuzione esso modifica gli altri programmi eseguibili presenti sul dischetto e nel computer installandosi in maniera invisibile nella memoria e nel sistema operativo, in modo da rimanere stabilmente installato nella macchina, che risulta cosi' 'infettata' ed in grado di infettare tutti i programmi eseguibili presenti sui dischetti di utenti ignari. Questo processo di contagio dei dischetti del nostro corso e' possibile anche nei vari computers dei nostri laboratori, poiche' sono aperti a tutti e qualcuno puo' portare dischetti infetti dall'esterno. I vostri dischetti, una volta infettati, se portati fuori del laboratorio possono infettare il vostro computer dell'ufficio, di casa, ecc. Per questi motivi lo studente deve riconsegnare il suo dischetto al docente al termine delle esercitazioni e non portare mai dischetti propri in laboratorio. I tecnici di laboratorio sono i custodi responsabili del funzionamento e del coordinamento nell'uso dei computer. Gli studenti devono pertanto attenersi alle disposizioni da essi impartite e segnalare loro eventuali anomalie riscontrate. PROGRAMMA DI LAVORO DURANTE LE ESERCITAZIONI PRATICHE: -----------------------------------------------------Lo svolgimento delle esercitazioni e' basato sul materiale didattico contenuto nel dischetto consegnato dal professore e su software scaricabile gratuitamente da Internet (dopo essersi registrati indicando il proprio indirizzo di E.mail: se non lo avete potete richiederlo ad un fornitore di servizio gratuito di posta elettronica, per esempio Netscape). Bisognera' quindi, al momento stabilito dal professore, eseguire il download dei programmi seguenti, dai siti indicati:
Delphi Java Development Kit Apache Web-Server Perl
www.borland.com http://java.sun.com www.apache.org www.activestate.com
(eserc. (eserc. (eserc. (eserc.
Pascal) Java) Web) CGI)
Il programma di lavoro, configurato in base alla preparazione dello studente, dovra' contenere, nell'ordine, i seguenti argomenti: 1) Uso dei comandi elementari del Sistema Operativo MS-DOS 2) Uso dell'Editor del TURBOPASCAL 3) Prove di uso delle istruzioni Pascal in semplici programmi (xxxDEMO.PAS) 4) Comprensione ed ampliamento del programma ADDING.PAS 5) Studio del programma Monitor 6) Studio del programma di simulazione H6809BUG.PAS (con BINPROG.PAS); ricerca ed eliminazione degli errori ivi contenuti 7) Studio del microcomputer H6809 simulato su IBM-PC, scrivendo ed eseguendo un semplice programma in linguaggio macchina 8) Ampliamento del simulatore H6809 introducendovi altre istruzioni come quelle di branch condizionato o di I/O o il sistema d'interruzione 9) Uso dell'Editor dell'ambiente JDK1.3 (Java Developmenti Kit) 10) Prove di uso delle istruzioni Java in semplici programmi (xxxdemo.java): FirstSample, Welcome, BigDebt, Test0, Root, NewRoot, SwapTest, SwapTest2, ShellSort, VirusLook, LotteryOdds: esempio con input da tastiera e chiamata a metodo EmployeeTest, H6809g: Monitor H6809 scritto in java Root, RootApplet: esempio di applet Prova: programma contenente un errore da trovare 11) Studio del programma Monitor H6809g.java, con particolare attenzione all'uso dell' oggetto Byte (esempio di programmazione ad oggetti) 12) Realizzazione in Basic di un simulatore dello scattering coulombiano con il metodo di Montecarlo 13) Programmazione concorrente del Montecarlo su una LAN in elaborazione parallela ('farm' di computer). 14) Istallazione ed uso del Web server Apache con uso di applet Java e script CGI. 15) Programmazione concorrente su rete Intranet (facoltativo). AVVERTENZA IMPORTANTE ===================== Le esercitazioni si svolgeranno usando un dischetto personale per ogni studente: su questo dischetto vi saranno sia il file contenente le dispense del corso, che appunti (LEZ??.PAS) e programmi per le esercitazioni. Gli esercizi e le dispense su Internet, sulla programmazione ad
oggetti e sul linguaggio Java saranno consegnati agli studenti all'inizio del modulo B (subito dopo Pasqua). Lo studente dovr? riconsegnare questo dischetto al docente al termine delle esercitazioni e non portare dischetti propri per ragioni di sicurezza e protezione dai 'virus'. Un virus e' un programma nascosto all'interno di un altro programma usato dell'utente. Quando viene lanciato in esecuzione esso modifica gli altri programmi eseguibili presenti sul dischetto e nel computer, installandosi in maniera invisibile nella memoria e nel sistema operativo in modo da rimanere stabilmente installato nella macchina. Questa risulta così 'infettata' ed in grado di infettare tutti i programmi eseguibili, presenti sui dischetti in essa inseriti da utenti ignari . Questo potenziale processo di contagio dei dischetti del nostro corso e' possibile sia nei computers del nostro laboratorio che in quelli del CATTID, poiche' sono centri aperti a tutti e qualcuno puo' portare dischetti infetti dall'esterno. I vostri dischetti, una volta infettati, se portati fuori dall'Universita' possono infettare il vostro computer di casa, dell'ufficio, ecc. Per questi motivi lo studente deve riconsegnare il suo dischetto al docente al termine delle esercitazioni e non portare mai dischetti propri al laboratorio o al CATTID. !------------------------------------------------------------------Per le esercitazioni si puo' utilizzare anche la sala computers del CATTID, dotata di 20 PC-IBM, ubicata come indicato nella piantina seguente: ═══════════════════════════════════════════════════════════════════ VIALE DEL POLICLINICO ═════════════════════════════════ ══════════════════════════════ ....─────────────────┐ ╔═════════════════════════╗ │ ║ EDIFICIO ║ │ ║ CATTID ║ │ ║ <--- ingresso ║ ...──────────────────┘ ╚═════════════════════════╝ ...──────────────────┐ ┌───────────────────────... │ │ │ │ MINERALOGIA GEOLOGIA │ │ SCIENZE │ │ │ POLITICHE └────────────────────────────┘ │ │ ───┐ ┌───────────┘ F │ │ I │ │ GIURISPRUDENZA S │ │ I │ └────┐ C │ Piazza │ A │ della │ RETTORATO ───┘ Minerva │ !-----------------------------------------------------------CAPITOLO 1 - Elementi del linguaggio PASCAL ══════════
Riassunto: Scopo essenziale di questo capitolo e' quello di fornire gli elementi minimi necessari per l'uso del linguaggio Pascal nelle esercitazioni di laboratorio di questo corso. Il linguaggio Pascal viene usato per una prima presa di contatto con la programmazione procedurale di alto livello. Nella seconda parte del corso (modulo B) useremo invece il linguaggio Java per comprendere la programmazione ad oggetti (in ambiente multitask ed in rete). Un calcolatore elettronico e' una macchina che si guida con dei comandi che sono le istruzioni. Il loro uso per la soluzione dei vari problemi pratici che si incontrano richiede pero' l'apprendimento di una tecnica particolare, basata sull'uso SEQUENZIALE delle istruzioni, che non essendo intuitiva, si deve acquisire soprattutto con l'esperienza pratica. Per questo motivo costituiscono parte essenziale di questo corso le esercitazioni pratiche di laboratorio, organizzate anche con l'impiego di un programma simulatore di un processore Motorola, scritto in linguaggio Pascal, su PC IBM, per lo studio del linguaggio Assembly. Questo corso, pur non includendo la trattazione dei vari linguaggi di programmazione, che possono essere studiati a parte, comprende tuttavia questo capitolo, che intende fornire gli elementi di base per la comprensione e l'uso dei programmi scritti in Pascal, usati nelle esercitazioni, il cui scopo e' l'apprendimento dell'uso delle istruzioni dei computer per la soluzione dei vari problemi pratici. Il linguaggio Pascal, usato in moderni sistemi di sviluppo quale Delphi della Borland, e' stato preferito ad altri in questo corso, perche' meglio si adatta all'insegnamento della materia: infatti mentre da un lato abitua lo studente ad una disciplina nella programmazione, dall'altro ci e' anche comodo come linguaggio rigoroso e sintetico per esprimere alcuni concetti dell'informatica, meglio dell'italiano o dell'inglese. Inoltre l'Assembly ed il Pascal insegnati in questo corso vanno ad aggiungersi al Fortran ed al C insegnati in altri corsi. ⌡ 1.1.1 - Descrizione della sintassi del linguaggio Pascal: i diagrammi sintattici La sintassi del linguaggio Pascal e' ben descritta da una serie di diagrammi, che per questo vengono chiamati diagrammi sintattici. In essi con una cornice a doppio tratto indichiamo i caratteri o le parole chiave (in maiuscolo) del linguaggio, che devono comparire come sono scritte. In cornice a singolo tratto sono gli elementi definiti dall'utente (per esempio i nomi delle variabili) nel rispetto della sintassi descritta dal relativo diagramma. Un programma risulta descritto dal diagramma seguente: ┌─────────────────┐ ┌────────────┐ ╔═══╗ │ Program heading │───────>│ Block │──────>║ . ║ └─────────────────┘ └────────────┘ ╚═══╝ Fig.1 - Programma Pascal Esempio di programma Pascal: Intestazione ---> PROGRAM Example (input,output); (=heading) ┌ VAR x,square: real; <--- Variable declaration │ BEGIN ┐
│ read (x); │ Block ----> │ square := x*x; │ write ('square of',x,'is',square) │ └ END. ┘
│Statements
Le parole-chiave BEGIN...END svolgono il ruolo di parentesi aperta e chiusa e racchiudono un insieme di istruzioni (che abbiamo 'indentato', cioe' scritto un po' piu' a destra, per evidenziare visivamente l'insieme delle istruzioni che compongono il blocco tra BEGIN ed END). Sia il 'Program heading' che il 'Block' devono essere scritti rispettando delle regole sintattiche definite dai due diagrammi seguenti: ╔═════════╗ ┌────────────┐ Program heading ─────>║ PROGRAM ║─────>│ Identifier │───┐ ╚═════════╝ └────────────┘ │ ┌────────────────────────────────────────┘ │ ╔═══╗ ┌────────────┐ ╔═══╗ ╔═══╗ └──║ ( ║──┬──>│ Identifier │───┬──>║ ) ║────║ ; ║───> ╚═══╝ │ └────────────┘ │ ╚═══╝ ╚═══╝ │ ╔═══╗ │ └───────║ , ║<───────┘ ╚═══╝ L'Identifier e' un simbolo introdotto dal programmatore a suo piacere (generalmente e' un nome mnemonico che aiuta a ricordare il significato della costante o variabile) ma nel rispetto della sintassi definita dal diagramma seguente (impone che inizi con una lettera, senza distinzione tra maiuscole e minuscole): ┌─────────┐ ┌<─────│ Lettera │<─────┐ │ └─────────┘ │ │ ┌─────────┐ │ Identifier ───>┼─────>│ Lettera │──────┼────> │ └─────────┘ │ │ ┌─────────┐ │ └<─────│ Cifra │<─────┘ └─────────┘ Il 'Block' costituisce il corpo del programma e puo' contenere gli elementi indicati nel diagramma sintattico seguente: ┌───────────────────┐ Block ──────────┬───>│ Label declaration ├────>┐ │ └───────────────────┘ │ ├<─────────────────────────────┘ │ ┌─────────────────────┐ ├───>│ Constant definition ├──>┐ │ └─────────────────────┘ │ ├<─────────────────────────────┘ │ ┌─────────────────┐ ├───>│ Type definition ├──────>┐ │ └─────────────────┘ │ ├<─────────────────────────────┘ │ ┌──────────────────────┐ ├───>│ Variable declaration ├──>┐ │ └──────────────────────┘ │ ├<──────────────────────────────┘ │ ┌────────────────────┐ │ │ Procedure/Function │
├───>│ declaration ├────>┐ │ └────────────────────┘ │ ├<──────────────────────────────┘ │ ┌───────────────────┐ └───>│ Statements ├──────────────> └───────────────────┘ A loro volta definizioni, dichiarazioni ed istruzioni vanno scritte tutte nel rispetto delle regole sintattiche definite dai rispettivi diagrammi sintattici che seguiranno. Esempio di programma Pascal: PROGRAM Example (input,output); <--- Intestazione ┌ VAR x,square: real; <--- Dichiaraz. delle variabili │ BEGIN ┐ │ read (x); │ Block ---> │ square := x*x; │ Statements │ write ('square of',x,'is',square) │(=istruzioni) └ END. <-------> <--> ┘ Testo Testo N.B.
read,write sono procedure standard (parole riservate) x,square sono le variabili del programma
⌡ 1.1.2 - Pascal semplificato: istruzioni Ai fini di questo corso e delle relative esercitazioni, elenchiamo le istruzioni che e' sufficiente imparare per iniziare a programmare in Pascal (i nomi usati come identificatori sono esempi): ┌ PROGRAM Adding (input,output); Intestazioni │ PROCEDURE PrintNums (x,y: real); └ FUNCTION DigitVal (c:char): integer; ┌ LABEL 10,6850,355; Dichiarazioni│ CONST PI=3.14159265; │ TYPE direction=(nord,sud,est,ovest); └ VAR i,j:integer; x:real; road:direction; │ L Tutte le variabili usate vanno dichiarate. I tipi standard (predefiniti) che useremo sono: boolean, integer,real,char,text. │ ┌ BEGIN │ statements; Istruzioni │ ...... └ END;
┐ │ │ │ B │ O │ C │ K │ │ │ ┘
ISTRUZIONI (Statements): ════════════════════════ Si dividono in semplici e strutturate (quelle che contengono uno o piu' statements componenti): ┌
:=
<---
Assegnazione
│ │ SEMPLICI
GOTO label <--- Salto (da evitare) │ │ procedure (a,b,c); │ │ read (var1,var2...); o readln (var1,var2...); ┐ └ write (var1,var2..); o writeln (var1,var2..); │ I/O ┘
┌ IF condizione THEN stat1 ELSE stat2; │ │ FOR n:=n1 TO n2 DO statement; ┐ STRUTTURATE │ WHILE condizione DO statement; │ Loop │ REPEAT statement UNTIL condizione; ┘ │ │ CASE selector OF val1:stat1; val2,val3:stat2; END; │ └ composte: BEGIN...(statements)...END La sintassi delle istruzioni strutturate e' bene descritta dai relativi diagrammi sintattici: Istruzione IF
╔════╗ ┌────────────┐ ─────>║ IF ║─────>│ condizione │──>┐ ╚════╝ └────────────┘ │ ┌───────────────────────────────┘ │ ╔════╗ ┌────────────┐ └─>║THEN║─────>│ istruzione │──>┐ ╚════╝ └────────────┘ │ ┌───────────────────────────────┤ │ ╔════╗ ┌────────────┐ │ └─>║ELSE║─────>│ istruzione │───┴─────> ╚════╝ └────────────┘
╔═══╗ ┌─────────┐ ╔══╗ ┌───────────┐ ───>║FOR║──>│variabile│──>║:=║──>│espressione│──>┐ ╚═══╝ └─────────┘ ╚══╝ └───────────┘ │ ┌─────────────────────────────────────────────────┘ │ ╔══╗ ├───>║TO║───>┐ │ ╚══╝ │ ┌───────────┐ ╔══╗ ┌──────────┐ │ ├──┤espressione│─>║DO║──┤istruzione│──> │ ╔══════╗ │ └───────────┘ ╚══╝ └──────────┘ └─>║DOWNTO║─>┘ ╚══════╝
Istruzione FOR
Istruzione WHILE
╔═════╗ ┌──────────┐ ╔══╗ ┌──────────┐ ───>║WHILE║──>│condizione│──>║DO║──>│istruzione│──> ╚═════╝ └──────────┘ ╚══╝ └──────────┘
╔══════╗ ┌──────────┐ Istruzione REPEAT ───>║REPEAT║───┬──>│istruzione│──>┐ ╚══════╝ │ └──────────┘ │ │ ╔═══╗ │ └──────║ ; ║<──────┤ ╚═══╝ │
┌─────────────────────────────────┘ │ ╔═════╗ ┌──────────┐ └───>║UNTIL║──────>│condizione│──> ╚═════╝ └──────────┘ (N.B. REPEAT, a differenza di WHILE, esegue sempre l'istruzione almeno una volta) ╔════╗ ┌────────┐ ╔══╗ ───>║CASE║───>│selector│───>║OF║────>┐ ╚════╝ └────────┘ ╚══╝ │ ┌──────────────────────────────────┘ │ ┌──────────┐ ╔═══╗ └────┤lista casi│───────>║END║───────> └──────────┘ ╚═══╝
Istruzione CASE
dove la 'lista casi' ha la sintassi seguente: ┌────────────────────>──────────────────────┐ │ ┌────────┐ ╔═══╗ ┌──────────┐ │ Lista casi ──┼─┬─>│costante│──┬──>║ : ║──>│istruzione│──>┼──> │ │ └────────┘ │ ╚═══╝ └──────────┘ │ │ │ ╔═══╗ │ │ │ └─────║ , ║<───┘ │ │ ╚═══╝ ╔═══╗ │ └────────────────────║ ; ║<─────────────────┘ ╚═══╝ ⌡ 1.1.3 - Tipi di variabili Ogni variabile usata va dichiarata. I tipi usabili nella dichiarazione delle variabili sono i seguenti: ESEMPI integer,real, ┌ ┌───────────────┐ boolean,char, │ ────┬─>│Tipo 'standard'├─>┬──> string(=array of char) Tipi │ │ └───────────────┘ │ text(=file of char) scalari │ │ ┌───────────────┐ │ │ ├─>│Tipo enumerated├─>┤ TYPE vocale=(a,e,i,o,u); └ │ └───────────────┘ │ Sottoinsieme │ ┌───────────────┐ │ di un tipo ---> ├─>│Tipo 'subrange'├─>┤ TYPE indice=1..100; scalare │ └───────────────┘ │ (sottoins.di integer) (non real) │ ╔════════╗ │ ├─────>║ PACKED ║──>┐ │ │ ╚════════╝ │ │ ├<──────────────────┘ │ │ ┌─────────────┐ │ Tipo matrice---> ├──>│ Tipo 'array'├──>┤ TYPE vet=ARRAY[1..8] OF real ▌ └─────────────┘ ▌ ▌ ▌ ┌ ▌ ┌─────────────┐ ▌ │ ├──>│ Tipo 'file' ├──>┤ │ │ └─────────────┘ │ Tipi │ │ ┌─────────────┐ │ │ ├──>│Tipo 'record'├──>┤ strutturati │ │ └─────────────┘ │ │ │ ┌─────────────┐ │
(non trattati)│ ├──>│ Tipo 'set' ├──>┤ │ │ └─────────────┘ │ │ │ ┌──────────────┐ │ │ └──>│Tipo 'pointer'├─>┘ └ └──────────────┘ Esempio di dichiarazioni (vedi programma Arraydem.pas delle esercitazioni): TYPE bit = 0..1; byte=ARRAY[0..7] OF bit; word=ARRAY[0..15] OF bit; VAR mem : ARRAY[0..4095] OF byte; PC : word; IR,A: byte; Z : bit; himem: integer; ecc....... ⌡ 1.1.4 - Procedure e funzioni Anche in Pascal, come negli altri linguaggi di alto livello, esistono due tipi di sottoprogrammi, cioe' di insiemi di istruzioni che possono essere richiamati con una solo simbolo. La Function differisce dalla Procedura in quanto calcola un determinato valore e quindi puo' anche essere usato all'interno di formule. Per esempio: y:=2*Fact(i)+1 dove Fact(i) e' il valore di uscita della Function Fact corrispondente al valore del parametro di ingresso i. Una Procedura invece e' un blocco di istruzioni che si richiama con una sola istruzione costituita dal nome della Procedura, seguito dalla lista dei parametri passati tra la Procedura ed il programma che la richiama (alcuni parametri costituiscono le variabili di ingresso del sottoprogramma, altre i risultati in uscita del calcolo). La sintassi e' definita dai seguenti diagrammi: ╔═════════╗ ┌──────────────────────┐ ┌─>║PROCEDURE║───>│intestazione procedura│──┐ │ ╚═════════╝ └──────────────────────┘ │ ┌─────┐ ────┬──┤ ├───│block│──┬──> │ │ ╔═════════╗ ┌──────────────────────┐ │ └─────┘ │ │ └─>║FUNCTION ║───>│intestazione function │──┘ │ │ ╚═════════╝ └──────────────────────┘ │ │ ╔═══╗ │ └────────────────────────────║ ; ║<─────────────────────────┘ ╚═══╝ ┌────┐ ┌───────────────┐ ╔═══╗ Intestazione procedura ───>│nome│───>│lista parametri│───>║ ; ║───> └────┘ └───────────────┘ ╚═══╝ ┌────┐ ┌───────────────┐ ╔═══╗ ┌────┐ ╔═══╗ Intestazione ──>│nome│──>│lista parametri│──>║ : ║──>│tipo│──>║ ; ║──> function └────┘ └───────────────┘ ╚═══╝ └────┘ ╚═══╝ ┌────────────────────────>────────────────────────┬──> │ ╔═══╗ ╔════════╗ ┌────┐ ╔═══╗ │ Lista parametri──┴─>║ ( ║──┬─>║FUNCTION║──┬─┬─>│nome│──┐ ┌─>║ ) ║──┘ ╚═══╝ │ ╚════════╝ │ │ └────┘ │ │ ╚═══╝
│ ╔═══╗ │ │ ╔═══╗ │ │ ├───>║VAR║─────┤ └──║ , ║<──┤ │ │ ╚═══╝ │ ╚═══╝ │ │ ├──────>───────┘ │ │ │ ┌─────────────────────────┘ │ │ │ ╔═══╗ ┌──────┐ │ │ └─>║ : ║─────>│ tipo │─────>┤ │ ╚═══╝ └──────┘ │ │ ╔═════════╗ ┌──────┐ │ ├─>║PROCEDURE║─┬─│ nome │─┬──>┤ │ ╚═════════╝ │ └──────┘ │ │ │ │ ╔═══╗ │ │ │ └──║ , ║<──┘ │ │ ╔═══╗ ╚═══╝ │ └──────║ ; ║<─────────────────┘ ╚═══╝ ⌡ 1.1.5 - Passaggio di parametri in Pascal Esistono due modalita' di passaggio dei parametri in Pascal. Corrispondentemente parleremo di due tipi di parametri: tipo 'valore' e tipo 'variabile'. Nel primo caso il parametro e' passato al sottoprogramma copiandone il valore in una distinta locazione di memoria in cui lavora il sottoprogramma (Procedura o Function che sia). Quindi le eventuali alterazioni che questo parametro subisce all'interno del sottoprogramma non risultano nel programma chiamante. Invece il passaggio dei parametri di tipo 'variabile' avviene passando al sottoprogramma anziche' il valore l'indirizzo della memoria in cui si trova la variabile passata. In questo modo il sottoprogramma lavora direttamente sulla stessa variabile usata dal programma chiamante e conseguentemente, a differenza dal caso precedente, ogni variazione apportata dal sottoprogramma risultera' anche nel programma chiamante. Esempio di una Procedura che usa parametri di tipo 'variabile': PROGRAM Swapping (input,output); VAR a,b: integer; <---- a,b=variabili globali PROCEDURE Swap (VAR x,y: integer); <-- x,y=param.tipo variabile VAR t:integer; <---- t=variabile locale BEGIN t := x; x := y; y := t; END; BEGIN a := 1; b := 2; Swap (a,b); write (a,b); <----a,b risultano variati da Swap END. perche' passati di tipo variabile Esempio di una Function che usa parametri di tipo 'valore': PROGRAM Esempio (input,output); VAR n: integer; FUNCTION Fact (i: integer): real; VAR prod: real; BEGIN prod := 1;
<---- n=variabile globale <---- i=parametro di tipo valore <---- prod=variabile locale
WHILE i>1 DO BEGIN prod:=prod*i; i:=i-1 END; Fact := prod; END; BEGIN read (n); WHILE n>0 DO BEGIN writeln(n,Fact(n),n*n); read(n) END; END. <-----------> n non risulta alterato perche' passato tipo valore ⌡ 1.1.6 - Esempi di semplici programmi Pascal I seguenti programmi, usati nelle esercitazioni, sono dimostrativi dell'uso delle istruzioni usabili per eseguire dei 'loop'. PROGRAM Forloop (input,output); Output prodotto: VAR i := integer; ---------------n := real; n= 1.0 BEGIN n= 2.0 n := 0; ...... FOR i := 0 TO 10 DO n=11.0 BEGIN fine lavoro n := n+1; > writeln ('n=',n:4:1); <-- Formato=4 car., 1 decimale END; writeln; writeln ('fine lavoro'); END. PROGRAM Whildemo (input,output); VAR i,n := integer; BEGIN i := 1024; n := 10; WHILE i >= 1 DO BEGIN writeln ('i=',i,' (=2 alla ',n,')'); i := trunc (i/2); n := n - 1; END; END. PROGRAM RepetDemo (output); VAR n := integer; BEGIN n := 64; REPEAT n := n + 1; writeln (chr(n),' ASC=',n) UNTIL n = 75; END.
Output prodotto: ---------------i=1024 (=2 alla 10) i=512 (=2 alla 9) ......... i=1 (=2 alla 0) >
Output prodotto: ---------------A ASC=65 B ASC=66 ........ K ASC=75 >
⌡ 1.1.7 - Differenze tra i linguaggi Pascal e C Le differenze a livello sintattico tra i due linguaggi sono poche e sono
ben evidenziate nel testo "Pascal e C" (Ed. McGraw-Hill). Per averne un'idea ne elenchiamo alcune: 1) Il C (come il Java) distingue tra lettere minuscole e maiuscole nei nomi delle variabili, a differenza del Pascal. 2) Le differenze tra i tipi predefiniti di variabili sono: TIPO ---Intero con segno Intero senza segno Virgola mobile (singola precis.) Virgola mobile (doppia precis.)
C ----int unsigned int float double
Pascal semplif. --------------integer non esiste real non esiste
3) L'assegnazione e' fatta: in C con = (esempio a=b[i]+c;) in Pascal con := (esempio a:=b[i]+c;) 4) Operatori logici in aggiunta a quelli aritmetici (+-*/) che esistono in C e ma non in Pascal, dove sono fatti con apposite procedure: OPERATORE C Pascal ------------------AND & non esiste OR | non esiste NOT ~ non esiste XOR ^ non esiste Shift Left << non esiste Shift Right >> non esiste Siccome in C il simbolo = e' usato per l'assegnazione, per l'uguaglianza nelle espressioni si usa ==, come mostra la tabella seguente: CONFRONTO --------Uguale Diverso Minore Minore o uguale Maggiore Maggiore o uguale Esempio:
C ----if (i==j) f=g+h; else f=g-h;
C ----== != < <= > >=
Pascal ------= <> < <= > >=
Pascal -----if i=j then f:=g+h else f:=g-h;
5) Gli operatori tra condizioni logiche sono: OPERATORE --------Prodotto logico Somma logica Negazione
C ----&& || !
Pascal ------and or not
6) In C si usano le parentesi graffe al posto di BEGIN...END: C ----switch (k) { case 0: f=i+j; break; case 1: f=i+g; break;
Pascal -----case k of 0: f:=i+j; 1: f:=i+g;
case 2: f=i-h; break; };
2: f:=i-h; end;
7) Esempi di istruzioni di loop: C ----while (i==j) i=i+1; for (i=0, i
Pascal -----while i=j do i:=i+1; for i:=0 to n-1 do begin...end; if i<>j then goto 10;
8) Anche le procedure sono molto simili: C ----swap(int i) { float temp; ........ };
Pascal -----procedure swap (i:integer); var temp:real; begin ........ end;
L'evoluzione C++ del linguaggio C (++ e' il simbolo dell'incremento) e' invece molto simile al Java, che spiegheremo piu' avanti, perche' trattandosi di programmazione ad oggetti, costituisce un livello di apprendimento successivo, che risulta un poco piu' complesso per l'introduzione di tre nuovi concetti: incapsulamento (ottenuto con le classi), ereditarieta' e polimorfismo (late binding).
⌡ 1.2 - CENNI ALLA PROGRAMMAZIONE IN AMBIENTE GRAFICO Nella programmazione di un computer vi sono due approcci, secondo che si usi o no l'interfaccia grafica (mouse, ecc.): 1) La programmazione cosiddetta procedurale, in cui si sviluppa il codice dell'applicazione assemblando le istruzioni che saranno eseguite dalla CPU in maniera sequenziale. Per esempio si scrive su video un menu' di comandi dell'applicazione ed in base all'input ricevuto da tastiera si esegue un salto alla procedura prescelta (ogni programmatore professionale ha dei suoi moduli predisposti per questa, come per altre funzionalita' di uso frequente). 2) La programmazione grafica, in cui il menu' viene sostituito da una serie di pulsanti o da un menu' a tendina in una finestra grafica. In tal caso l'input da tastiera e' sostituito dai pulsanti del mouse. L'interfaccia con l'operatore e' in genere piu' gradevole, ma nell'esempio descritto poco cambia dal punto di vista funzionale. La maggiore complessita' della programmazione grafica impone pero' l'utilizzo di un sistema di sviluppo che sgravi il programmatore dall'onere di gestire i componenti grafici. Nelle esercitazioni intendiamo apprendere l'uso delle istruzioni Pascal e quindi lavoreremo in programmazione procedurale la piu' semplice possibile. Per questo viene proposto come ambiente di sviluppo una delle prime e piu' semplici versioni di TurboPascal degli anni '80 (TURBO.COM ha solo 36Kbytes di codice a 16 bit!) che ha realizzato per primo un editor di pagina con compilatore e parte run-time in linea (tecnica ancora oggi largamente usata). Per chi volesse lavorare con l'ambiente grafico diamo comunque un
minimo di informazioni su uno dei moderni sistemi in uso (Delphi), che puo' essere scaricato gratuitamente dal sito della Borland (www.borland.com) in versione dimostrativa. Dall'help on-line si possono ottenere poi tutte le altre informazioni necessarie. Raccomandiamo comunque allo studente di addentrarsi in questo tipo di programmazione, piu' complessa, solo dopo aver completato il programma di esercitazioni pratiche in TurboPascal.
⌡ 1.2.1 - UN ESEMPIO: L'AMBIENTE DI SVILUPPO DELPHI Programmare applicazioni che usino l'interfaccia grafica (mouse, finestre, pulsanti, ecc.) risulta molto agevolato usando uno degli ambienti di sviluppo (spesso indicati con la sigla IDE=Integrated Development Environment) con interfaccia grafica integrata (dove cioe' con semplici movimenti di mouse il programmatore puo' creare e posizionare finestre e pulsanti sapendo che le relative istruzioni ed i parametri grafici necessari viene generato automaticamente dall'IDE). Al programmatore resta il compito di programmare le azioni da intraprendere in risposta agli eventi di I/O (per esempio pressione di un pulsante). Delphi e' uno di questi ambienti di sviluppo, talora indicati anche con la sigla RAD (Rapid Application Developer). Iniziamo con la definizione di alcuni termini usati: APPLICAZIONE e' l'insieme dei programmi e dei dati di istallazione predisposti dal programmatore per eseguire i compiti prestabiliti, elaborando i dati inseriti dall'utente secondo le regole prestabilite. Un'applicazione e' composta da numerosi 'oggetti' (form, menu', controlli, ecc.) e dai moduli di programma che controllano tali oggetti). E' desiderabile quindi una organizzazione razionale di tutti questi componenti. PROGETTO e' l'organizzazione razionale di tutti i files dell'applicazione: ad esso corrisponde un file (diverso in ogni sistema di sviluppo: per esempio .DPR=Delphi PRoject in Delphi oppure .VBP=Visual Basic Project in Visual Basic) che e' una 'scatola' organizzativa usata per contenere tutte le componenti dell'applicazione (form, files di codice, sorgenti, documentazione, ecc.) UNIT sono i files dei moduli di programmazione (.PAS in Delphi) usati nel programma di controllo (.DPR). Quando si crea un New Project Delphi genera un nuovo programma .DPR ed una Unit che definisce il form iniziale. Ad ogni nuovo form aggiunto al programma viene aggiunta una nuovo modulo di programmaziione Pascal (Unit .PAS). Le units vengono compilate separatamente e 'linked' ai files del programma principale .DPR FORM e' la finestra grafica, contenitore dei componenti di I/O, che possono attivare le varie procedure in risposta alle azioni che avvengono. Un'applicazione e' composta da vari Form a cui corrispondono le Unit di programmazione. Una versione dimostrativa di Delphi puo' essere scaricata dal sito della Borland (www.borland.com) gratuitamente. Altri link dove scaricare materiale didattico di Pascal e Delphi si raggiungono dal sito Borland, tra cui: www.borland.com/devsupport/delphi/downloads/index.html#Delphi5 www.marcocantu.com/epascal/default.htm
www.geocities.com/SiliconValley/Park/3230/index.html Delphi produce 3 tipi di file per ogni applicazione sviluppata: 1) Il file Main del progetto: Deplhi PRoject (.DPR) 2) I files dei moduli di programmazione 'unit' (.PAS) (definiscono l'effetto delle azioni sul sistema) 3) I files descrittori dei 'form', in binario (.DFM) (contengono i valori delle proprieta' del form e delle sue componenti, cioe' lo stato iniziale) Essi sono costruiti automaticamente durante il lavoro del programmatore (i files binari .DFM sono visibili in chiaro con appositi comandi in Delphi). Per esempio, quando si aggiunge un pulsante nel Form, Delphi inserisce la definizione del relativo tipo: type TForm1 = class (TForm) Button1: TButton; ..... Se poi nella finestra Object Inspector cambio il nome del pulsante (da Button1 a BtnHello), Delphi aggiusta automaticamente il codice: type TForm1 = class (TForm) BtnHello: TButton; ..... (le altre proprieta' vengono invece riportate nel file descrittore del form .DFM). Se poi nella finestra 'Object Inspector' aggiungo un'azione di risposta ad un evento, Delphi aggiunge nella 'unit' di definizione del Form una riga di definizione della procedura nella dichiarazione di 'type' e un metodo vuoto viene aggiunto nella parte 'implementation' (anche il form description file .DFM viene modificato). Una unit inizia con la dichiarazione delle altre units usate ('uses'), dei nuovi tipi di dati (classi) ed una nuova variabile (oggetto della classe) per ogni nuova classe. Per esempio il tipo TForm1, derivato dalla classe TForm, e la variabile Form1 del tipo TForm1: unit Hello; interface uses SysUtils, WinTypes, WinProcs, Messages,Classes, Graphics, Controls,Forms, Dialogs, StdCtrls; type TForm1 = class(TForm); var Form1: TForm1; Per capire meglio il metodo di lavoro, una volta istallato Delphi, si puo' scrivere il piu' semplice programma, che scrive un messaggio in risposta al click su un pulsante. Per fare questo bisogna aprire un nuovo progetto: File | New Project A questo punto Delphi predispone una finestra vuota (form) in cui possiamo inserire i componenti disponibili nella programmazione grafica (box, pulsanti, menu',ecc.). Inseriamo nel form un pulsante con un doppio click sul pulsante 'Button' nella barra dei comandi standard (la posizione nel form puo' essere variata nel modo usuale drag&drop di Windows). Nella pagina Properties della finestra Object Inspector di Delphi si puo' inserire il testo nel pulsante: Property = Caption ---> inserire 'Hello'
Nella pagina Events della finestra Object Inspector di Delphi si puo' inserire l'evento che deve attivare l'azione voluta. Con un doppio click sul pulsante nel form possiamo inserire la 'Unit' di programmazione con il codice che stabilisce le azioni di risposta all'evento (per esempio la pressione del pulsante da parte dell'utente). Nel nostro semplice esempio sara' sufficiente inserire la sola riga seguente: MessageDlg ('Hello world', mtInformation, [mbOK], 0); Questa procedura standard apre un box con il messaggio 'Hello world', ed equivale ai writeln(...), che abbiamo usato nei test di programmazione procedurale in Pascal. Come secondo parametro, dopo il testo del messaggio, troviamo le opzioni possibili per il tipo di message box: mtInformation, mtWarning, mtError, mtConfirmation. Il terzo parametro determina il pulsante/i che si vuole usare: mbYes, mbNO, mbOK, mbCancel, mbHelp. Il quarto parametro indica la pagina dell'HelpOnLine richiamata se l'utente batte il tasto F1 (0=no help). L'istruzione BtnHello.Caption := 'Ripete Hello'; e' stata aggiunta nel codice della "unit Hello" per mostrare come una proprieta', in questo caso la Caption, puo' essere variata durante l'elaborazione. Per comprendere il modo di lavorare dell'ambiente di sviluppo Delphi, elenchiamo i corrispondenti listati da esso prodotti in questo semplice esempio. ⌡ 1.2.2 - Esempio di programma: Form con Button A) Listato del file di progetto HELLOBTN.DPR: program Hellobtn; uses Forms, Hello in 'HELLO.PAS' {Form1}; {$R *.RES} begin Application.CreateForm(TForm1, Form1); Application.Run; end. B) Listato dell'Unit file HELLO.PAS: unit Hello; interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) BtnHello: TButton; procedure BtnHelloClick(Sender: TObject); private { Private declarations } public { Public declarations }
end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.BtnHelloClick(Sender: TObject); begin MessageDlg ('Hello world', mtInformation, [mbOK], 0); BtnHello.Caption := 'Ripete Hello'; end; end. Come abbiamo inserito un pulsante, si puo' provare ad inserire nel form anche gli altri componenti (cliccando due volte sul pulsante corrispondente nella barra di controllo standard di Delphi). Dovremo poi attribuire le proprieta' volute (per esempio il nome 'Caption'), l'evento a cui dovra' corrispondere un'azione (per esempio un click del mouse) ed il codice del programma che esegue questa azione (cliccando due volte sul componente nel form). Per esercitarsi con la programmazione (procedurale) Pascal nell'ambiente di sviluppo Delphi si suggerisce di attivare un pulsante sullo standard 'form1' che appare quando si apre un nuovo progetto e di inserire il programma che si vuole scrivere e collaudare come 'evento' dell'azione 'OnClick' su Object Inspector (oppure di 'OnActivate' per lanciarlo all'avvio). Per lavorare in modalita' Console (cioe' per scrivere con write,writeln e leggere con read e readln, come in TurboPascal) occorre usare le 'Console routines' contenute nella unit WinCrt, che Delphi include sempre nella dichiarazione delle units usate (statement 'uses'). Le procedure disponibili sono indicate nell'Help di Delphi (le stringhe I/O vanno dichiarate come type TextFile). Tra queste la procedura AssignCrt () associa un TextFile alla finestra CRT sul video, funzionante in modalita' testo (permette una maggiore velocita' di I/O su video). !---------------------------------------------------CAPITOLO 2 - Sistemi di numerazione e rappresentazione delle informazioni ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di spiegare come si possano rappresentare tutte le varie informazioni (numeri, istruzioni, testi, ecc.) all'interno di un computer, che per ragioni costruttive puo' essere necessariamente dotato solo di un numero limitato di cifre (bit) e come, malgrado questa limitazione (anche i numeri interi richiederebbero da soli un numero illimitato di bit per poter essere rappresentati tutti) si possano eseguire calcoli di precisione elevata quanto si vuole. ⌡ 2.1 - Sistemi di numerazione Cosi' come un corso di analisi matematica inizia con un capitolo 'noioso' sui numeri reali, perche' tutta la materia tratta poi con i numeri reali, cosi' noi dobbiamo iniziare il nostro corso con un capitolo altrettanto noioso poiche' e' indispensabile capire come numeri e informazioni possano essere rappresentati all'interno di una macchina elettronica dotata, per ragioni costruttive, solo di un numero finito di bit. La risposta risiede nel modo in cui rappresenteremo i numeri e le altre informazioni, utilizzando solo il numero limitato di bit, di cui dispone
la macchina. Iniziamo chiarendo lo schema di rappresentazione dei numeri. Chiameremo SISTEMA DI NUMERAZIONE una regola che associ ad ogni numero un sottoinsieme ordinato di simboli presi da un alfabeto, dotato di un insieme finito di simboli (per esempio le dieci cifre 0...9). Non potra' essere finito invece, essendo infiniti i numeri rappresentabili, il numero di elementi nei sottoinsiemi, come invece dovra' necessariamente essere, per ragioni costruttive, nei calcolatori elettronici. Sono esempi sia il sistema di numerazione decimale, che tutti noi usiamo quotidianamente, in cui alla posizione delle cifre si associano le potenze della base 10 (e per questo si chiama 'rappresentazione posizionale'), che il sistema di numerazione usato dagli antichi romani (che posizionale non e'). Nel sistema di numerazione decimale si usano i dieci simboli 0123456789, piu' il punto che individua l'inizio dei decimali, per rappresentare i numeri razionali (base 10). Esempio:
634.85 = 6*10² + 3*10√ + 4*10° + 8*10ε√+ 5*10ε²
ovvero:
6 centinaia + 3 decine + 4 unita' + 8 decimi + 5 centesimi
Analogamente se usiamo un'alfabeto di soli due simboli (binario) potremo rappresentare i numeri nello stesso modo: Esempio: 110.01 = 1*2² + 1*2√ + 0*2° + 0*2ε√ + 1*2ε²
(=6.25 in base 10)
cioe', in generale: D = d
d p-1
.....
d
p-2
d 1
0
p-1____ \ i d ..... d = -1 -n -n i
(dove il significato dei simboli e':
> d * b /___ i d=digit,
(1.1)
b=base)
⌡ 2.1.2 - Conversioni di base Se invece di un alfabeto di due soli simboli (binario) se ne sceglie uno con otto simboli (01234567) chiameremo la rappresentazione ottale, mentre se useremo sedici simboli (0123456789ABCDEF) la chiameremo esadecimale. Poiche' in queste rappresentazioni il numero di simboli e' potenza di 2 esse sono sostanzialmente equivalenti alla rappresentazione binaria in quanto la conversione tra binario-ottale o tra binario-esadecimale si fa semplicemente raggruppando i bit contigui tre a tre (ottale) o quattro a quattro (esadecimale), poiche' nella definizione di rappresentazione posizionale (1.1) risulta: i Ottale:
i i b = 8 = ( 2ⁿ)
3*i = 2
i Esadecimale:
b
i = 16
4*i = 2
Quindi le conversioni ottale <---> binario <---> esadecimale eseguono come nell'esempio seguente: (binario)
si
101011000110 = 101 011 000 110 = 5306 O (ottale) = 1010 1100 0110 = AC6 H (esadecimale)
Diverso e' il caso delle conversioni tra queste rappresentazioni binarie e quella decimale, poiche' 10 non e' potenza di 2. Le conversioni al decimale si possono eseguire in base alla definizione
(1.1), eseguendo le moltiplicazioni per le potenze della base 10. Per esempio: (esadecimale)
1BE8 = 1*16ⁿ + 11*16² + 14*16√ + 8*16° = 7144
(decimale)
Sempre in base alla definizione (1.1), le conversioni dal decimale invece si dovranno eseguire in maniera inversa ricavando le cifre binarie, ottali od esadecimali come resti delle successive divisioni per le potenze della base 2, 8 o 16. Infatti la (1.1) puo' essere riscritta, per la proprieta' associativa, nel modo seguente D = ((...(( d p-1 Per esempio:
) * b + d p-2
) * b +...) * b + d ) * b + d 1 0
179 = (((((((1)*2+0)*2+1)*2+1)*2+0)*2+0)*2+1)*2+1 = 10110011 = B3 (decimale) ^ ^ ^ ^ ^ ^ ^ ^ (esadecimale) (Resti delle successive divisioni per 2) ⌡ 2.2 - Rappresentazione dei numeri all'interno di un calcolatore Nel sistema di numerazione posizionale il numero di caratteri dello alfabeto usato puo' essere qualsiasi. L'interesse dei progettisti di macchine di calcolo elettronico si e' subito diretto verso il sistema binario, basato sull'uso di due soli simboli (0,1) poiche' i circuiti elettrici normalmente possono trovarsi in due stati possibili: acceso o spento (ON/OFF ovvero 0/1). Cosi' come i nostri antenati che contavano con le dieci dita delle mani hanno adottato il sistema di numerazione decimale (con un'alfabeto di 10 simboli 0,1,2,...,9), cosi' noi adotteremo il sistema binario (con un'alfabeto di soli due simboli 0,1) o l'ottale o l'esadecimale che, come abbiamo visto, sono ad esso equivalenti. Un sistema di numerazione puo' rappresentare qualsiasi numero intero ma richiede per questo la possibilita' d'uso di un numero illimitato di simboli (cifre). Questo non e' ovviamente possibile all'interno di un computer che per ragioni costruttive deve operare su di un numero limitato di cifre (bit). Nasce allora un serio problema: come e' possibile eseguire calcoli quali servono nelle varie applicazioni con un calcolatore che in realta' esegue solo operazioni tra numeri con un numero limitato di bit (8,16,32,64)? La risposta e' nel fatto che in tutte le applicazioni pratiche ci ║ possiamo accontentare di una precisione, magari molto elevata, ma ║ comunque limitata. ║ Cosi' come nell'uso di una calcolatrice da tavolo posizioniamo il numero di cifre decimali che vogliamo (e che ci sono sufficienti) cosi' nel computer eseguiremo dei calcoli in precisione semplice (con otto cifre significative) o doppia, a secondo delle nostre esigenze: il calcolatore, appositamente programmato, eseguira' allora i calcoli su tutte le cifre significative che ci interessano, ovviamente con piu' operazioni in sequenza, se necessario, ma usando solo il numero di bit di cui dispone per la rappresentazione dei numeri al suo interno. Rimane comunque da capire come rappresentare in una macchina che opera ad N bit (ad esempio 16) numeri maggiori di 2^N-1 o come rappresentare i numeri negativi o frazionari. Vediamo come sono stati risolti questi problemi. ⌡ 2.2.1 - Rappresentazione dei numeri negativi Diverse sono le rappresentazioni possibili dei numeri negativi all'in-
terno di un calcolatore elettronico. A) Rappresentazione dei numeri negativi in modulo e segno: e' il modo piu' semplice ma non il piu' usato per i motivi che diremo in seguito; si associa il segno al bit piu' significativo 0=+ e 1=- e si usano i restanti n-1 bit per il modulo in forma binaria. Pertanto nel caso di n=8 bit: 01111111 =+127
00000000 =+0
10000000 =-0
11111111 =-127
Come si vede in questa rappresentazione esistono due diverse rappresentazioni per lo zero. B) Rappresentazione dei numeri negativi in complemento alla base: Si opera sempre con un numero fisso n di cifre; se la base e' b l'opposto di X viene definito come n -X = b - X Esempio (con base b=10): 4 X = 1849 ---> -X = opposto di 1849 = 10 - 1849 = 8151 _ Dopo l'opposto di X (-X) possiamo definire anche il complemento (X) come: _ n X = ( b - 1 ) - X (cioe' in decimale 9-X) Esempio (con base b=10): _ 4 X = 1849 ---> X = complemento a 10 di 1849 = 10 - 1 - 1849 = 8150 Dalle definizioni quindi risulta: _ -X = X + 1 I numeri relativi rappresentabili con 4 cifre in base 10 e' cosi': complemento a 10: 5000 ..... 9999 0 1 ..... 4999 (corrispondenti a) -5000 -1 un 1 4999 <-- negativi --> solo <-- positivi --> (10000-X) zero! (X) I numeri rappresentabili con 8 cifre in base 2 e' cosi': compl. a 2: 10000000....11111111 00000000 00000001.....01111111 (corrisp. a) -128 -1 un 1 +127 <--- negativi ---> solo <---- positivi ----> (256-X) zero! (X) I vantaggi di questa rappresentazione dei negativi sono: 1) una sola rappresentazione per lo zero; 2) somme e sottrazioni si eseguono come per i naturali, ignorando il segno (infatti l'attraversamento dello zero negli incrementi e decrementi non pone problemi, essendovi un solo zero). C) Rappresentazione dei numeri negativi in complemento alla (base-1): Operando sempre con un numero fisso n di cifre, l'opposto di X e' definito come n -X = (b - 1) - X Esempio (base 10): X = 1849 ---> -X = 9999 - 1849 = 8150 I numeri relativi rappresentabili con 4 cifre in base 10 e' cosi':
complemento a 9: 5000 ..... 9998 9999 0 1 ..... 4999 (corrispondenti a) -4999 -1 <--------> +1 +4999 <-- negativi--> DUE ZERI <--positivi--> (9999-X) (X) I numeri rappresentabili con 8 cifre in base 2 e' cosi': compl.a 1: 10000000...11111110 11111111 00000000 00000001...01111111 (corrisp. a) -127 -1 <---------------> +1 +127 <--- negativi ---> DUE ZERI <---- positivi ----> (255-X) (X) (n-1) D) Rappresentazione dei numeri negativi in eccesso - 2 : n-1 Si ottiene dalla rappresentazione in complemento a 2 sommandoci 2 . Con 8 bit (n=8) l'opposto di X viene cosi' definito come -X = X + 128
( con -128 <= X <= +127 )
I numeri rappresentabili con 8 cifre in base 2 e' cosi': Eccesso 128: 00000000...01111111 10000000 10000001...11111111 (corrisp. a) -128 -1 <----------> +1 +127 <---- negativi ----> UN SOLO ZERO <---- positivi ---> (-X+128) (X) Si noti che differisce dal complemento a 2 solo per il segno, che e' sempre l'opposto. Per meglio comprendere le differenze tra i vari modi (A...D) di rappresentare i negativi, confrontiamo le quattro rappresentazioni nella tabella seguente (per numeri binari di soli 4 bit, per semplicita'): Decimale
Compl. a 2
Compl. a 1
Modulo e segno
Eccesso 8
──────────┬────────────────────────────────────────────────────── -8 │ 1000 ------0000 -7 │ 1001 1000 1111 0001 -6 │ 1010 1001 1110 0010 -5 │ 1011 1010 1101 0011 -4 │ 1100 1011 1100 0100 -3 │ 1101 1100 1011 0101 -2 │ 1110 1101 1010 0110 -1 │ 1111 1110 1001 0111 0 │ 0000 1111 ║ DUE ║ 1000 1000 │ 0000 ║ ZERI ║ 0000 +1 │ 0001 0001 0001 1001 +2 │ 0010 0010 0010 1010 +3 │ 0011 0011 0011 1011 +4 │ 0100 0100 0100 1100 +5 │ 0101 0101 0101 1101 +6 │ 0110 0110 0110 1110 +7 │ 0111 0111 0111 1111 Si osservi come la rappresentazione in complemento a 2 corrisponda alla rappresentazione posizionale in cui il bit piu' significativo (quello del segno) ha peso negativo.
⌡ 2.3 - Operazioni aritmetiche ⌡ 2.3.1 - Addizioni e sottrazioni di numeri interi Queste operazioni aritmetiche si eseguono per le altre basi come per la base decimale. Ad esempio nel caso della somma in esadecimale: 19B9 C7E6 19B9 + C7E6 = E1AF perche', operando come per i decimali ---(ma con riporto a 16), ho ------> E19F ⌡ 2.3.2 - Addizioni e sottrazioni di numeri relativi in complemento a 2 Abbiamo gia' sottolineato come la presenza di un solo zero nella rappresentazione dei numeri relativi in complemento a 2 comporta che addizioni e sottrazioni possano eseguirsi come per i numeri binari naturali (senza segno), ignorando l'(n+1)-mo bit eventualmente generato quando il risultato eccede il massimo rappresentabile con gli n bit disponibili. Sono esempi di addizioni di numeri di 4 bit in complemento a 2 i seguenti (il risultato e' di 4 bit): +3 0011 -2 1110 +6 0110 +4 0100 ++4 0100 +-6 1010 +-3 1101 +-7 1001 ── ──── ── ──── ── ──── ── ──── +7 0111 -8 11000 +3 10011 -3 1101 └─ 5°bit ignorato─┘ Naturalmente puo' anche capitare che l'operazione dia un risultato che eccede i 4 bit disponibili. In tal caso l'operazione produce un errore di overflow (trabocco in italiano) poiche' i 4 bit disponibili non possono contenere il risultato dell'operazione che richiederebbe un bit in piu', che nei registri del nostro calcolatore non c'e'. Il problema si risolve identificando l'errore e predisponendo in tal caso le adeguate istruzioni. Una semplice regola per rivelare l'overflow, suscettibile di una facile implementazione hardware, e' la seguente: SI HA OVERFLOW SE GLI ADDENDI HANNO LO STESSO SEGNO, MENTRE IL RISULTATO HA SEGNO OPPOSTO se invece gli addendi hanno segni opposti l'overflow non puo' verificarsi. Siccome l'opposto e' uguale al complemento incrementato di 1, cioe' _ -X = X + 1 la sottrazione puo' essere eseguita con gli stessi circuiti di addizione usando il complemento del sottraendo (tutti i bit complementati) con un riporto iniziale posto a 1 anziche' a 0. Anche l'overflow viene quindi rivelato come per l'addizione. Concludendo valgono le stesse regole per addizione e sottrazione sia per i numeri interi senza segno (unsigned binary) ( da 0 a 255) che per i numeri interi in complemento a 2 (signed binary) ( da -128 a +128). ⌡ 2.3.3 - Moltiplicazioni Per i numeri interi senza segno si procede come per i decimali: 11 x 13 ── 33
1011 X x 1101 Y ──── 1011
┌────────<───────┐ ┌─────┐ ┌──┴──┐ ┌─────┐ │ │ X │ │PARZ ├───>┤ LSB │ │ └───┬─┘ └─┬───┘ └─────┘ │
11 ──── 143
0000 1011 1011 ──────── 10001111 PARZ LSB
┌─────┐ ┌┴─────┴┐ │ │ Y ├─>┤ ADD │ │ └─────┘ └───┬───┘ │ └────────────>───────┘
Analogamente si procede per i numeri interi con segno ma, siccome in complemento a 2 il bit piu' significativo ha peso negativo, all'ultimo passo devo sottrarre anziche' sommare. Esempio con numeri di 3 bit (per semplicita'): +2 x -3 ─── -6
010 ² √ ° x 101 ( N.B. 101 = -1x2 + 0x2 +1x2 = -4+0+1 = -3 ) ─── 010 + 000 + ──── √ ° ² 0010 <--- Risultato somma 0x2 +1x2 Sottraz. -1x2 ---> 010 ────── 111010 <--- Risultato finale (6 bit) ⌡ 2.3.4 - Divisioni Anche in questo caso per i numeri interi senza segno si procede come per i decimali (esempio con parole di 4 bit): dividendo=217 divisore=11 (double word) (single word) 217 │ 11 11011001 │ 1011 11 ├──── 1011.... ├─────── ─── │ 19 ────.... │ 1011 <──── quoziente=19 107 │ 00101... │ 99 │ 0000... │ ─── │ ─────... │ 8 │ 1010.. │ 00000. ─────. 10100. 1011. ─────. 10011 1011 ───── 1000 <──── resto=8 La condizione di overflow si verifica se il divisore e' zero oppure se il divisore e' minore o uguale all'high-word del dividendo. Nel caso di numeri interi con segno si puo' procedere cosi': 1) si cambiano di segno i negativi; 2) si esegue la divisione tra positivi; 3) si converte il risultato al giusto segno. ⌡ 2.4 - Rappresentazione dei numeri frazionari ⌡ 2.4.1 - Rappresentazione in 'virgola fissa' (fixed point)
Si lavora praticamente con i numeri interi, tenendo conto di un fattore di scala implicito (preferibilmente una potenza di 2, cosicche' moltiplicazione e divisione si riducono ad uno spostamento del punto 'decimale'). Esempi: 1.100 = ² In virgola fissa e' quindi 2 alla
1.5 0.010 = 0.25 √° ² √° il massimo numero rappresentabile (range) con n bit n volte il minimo (che e' il fattore di scala).
⌡ 2.4.2 - Rappresentazione in 'virgola mobile' (floating point) Il limite della rappresentazione in virgola fissa e' nella limitatezza del range che risulta tanto piu' limitato quanto piu' si spinge la risoluzione, cioe' quanto piu' piccolo e' il fattore di scala. Questa limitazione viene rimossa con la rappresentazione in virgola mobile in cui si usa la forma esponenziale, cioe' si rappresenta il numero frazionario con mantissa ed esponente: in questo modo bastano un numero limitato di bit dell'esponente per rappresentare numeri molto piu' grandi di quelli rappresentabili in virgola fissa. Per esempio, con 16 bit disponibili, dedicandone 6 all'esponente si guadagna nel range rappresentabile, perdendo pero' in precisione dato che restano solo 10 bit per la mantissa (0...1023): Esponente 000000 000000 001000 111111
Mantissa
15 ....... 10 9 ........ 0 0 ┌─────────────┬──────────────┐ 0000000000 = 0x2 = 0 │ ESPONENTE │ MANTISSA │ 0 └─────────────┴──────────────┘ 1000000101 = 517x2 = 517 8 0100101101 = 301x2 = 77056 6 21 1111111111 = 1023x2 = 9....x10
Da questo esempio si comprende anche che 16 bit sono insufficienti poiche' 10 bit per la mantissa consentirebbero calcoli con sole 3 cifre significative. Per ottenere una rappresentazione soddisfacente ci occorrono quindi: -
piu' bit per la mantissa piu' bit per l'esponente un bit per il segno della mantissa un bit per il segno dell'esponente
Esistono diversi formati scelti dai vari costruttori. A titolo di esempio ne riportiamo uno (quello del PDP11 della DEC). Esempio di formato Floating Point in precisione singola: 31 30 ....... 23 22 ......... 0 ┌──┬─────────────┬──┬──────────────┐ │MS│ ESPONENTE │HB│ MANTISSA │ dove: └──┴─────────────┴──┴──────────────┘ <-----------> <------------> 8 bit in 23+1 bit eccesso 128 'normalizzati' (-128÷128) (.1···ecc.) Esempio:
MS=Segno: 0=positivo 1=negativo HB=Hidden bit (sottinteso)
┌─┬────────┬─┬───────────────────────┐ +2 +3 = │0│10000010│1│10000000000000000000000│ = +0.75x2
└─┴────────┴─┴───────────────────────┘ <------><------------------------> +2 +0.75 Nella mantissa il punto 'decimale' dev'essere in posizione prestabilita. Se come in questo formato si stabilisce che sia alla sinistra del primo bit (piu' significativo) in tutte le rappresentazioni essa avra' la forma .1··· con il primo bit a sinistra sempre 1 (salvo che per la rappresentazione dello zero che potra' essere rappresentato da tutti bit a zero; in queste condizioni e' chiaramente inutile mettere questo bit che e' sempre 1 nel formato, ma e' meglio sottintenderlo e impiegare il bit disponibile per rappresentare un bit significativo in piu'. Per questo i bit della mantissa sono 23 piu' il bit sottinteso (hidden bit). Oltre al formato a 4 byte altre rappresentazioni standard IEEE ad 8 (double) e 10 byte (extended). Alcuni ambienti di sviluppo (es. Delphi) definiscono il tipo 'real' direttamente con il formato a 64 bit. ⌡ 2.5 - Operazioni in virgola mobile ⌡ 2.5.1 - Conversione virgola fissa ----> virgola mobile In base alle definizioni si devono eseguire i passi seguenti: 1) Convertire il numero alla rappresentazione usata per la mantissa (modulo e segno, complemento a 2, ecc.); 2) Traslare il punto binario fino ad ottenere la mantissa in forma normalizzata (es. 0.1···) ricavando cosi' l'esponente; 3) Convertire l'esponente nella rappresentazione usata (per esempio eccesso 128); 4) Impacchettare nel formato finale: segno, esponente e mantissa normalizzata. ⌡ 2.5.2 - Conversione virgola mobile ----> virgola fissa Si procede in modo inverso alla conversione precedente. Possono pero' verificarsi due casi: - Overflow: il numero e' troppo grande per essere rappresentato nel formato in virgola fissa; - Troncamento: il numero non ha una rappresentazione esatta nel formato in virgola fissa (si approssima arrotondando o troncando). ⌡ 2.5.3 - Addizioni e sottrazioni Si eseguono nel modo seguente: 1) Traslare il punto binario dell'operando piu' piccolo in modo da uguagliare i due esponenti; 2) Addizionare o sottrarre le mantisse; 3) Normalizzare il risultato (la mantissa puo' risultare >1 nella somma o <0.5 nella sottrazione); ⌡ 2.5.4 - Moltiplicazioni e divisioni Si eseguono nel modo seguente:
1) Sommare o sottrarre gli esponenti; 2) Moltiplicare o dividere le mantisse; 3) Normalizzare il risultato; Queste operazioni in virgola mobile possono essere eseguite via hardware, con dei circuiti appositi, ottenendo cosi' una notevole guadagno in velocita' rispetto ad un'esecuzione software. ⌡ 2.6 - Rappresentazione delle informazioni: codici Abbiamo visto come i numeri possano rappresentarsi in binario allo interno di un calcolatore elettronico. In realta' il binario risulta un po' scomodo per l'elevato numero di cifre (bit) con cui abbiamo a che fare; per questo motivo si preferisce spesso trattare con gruppi di 4, 8, 16 o 32 bit per i quali si usano i termini seguenti: Bit Nibble Byte Word Double
= Cifra binaria (binary digit) = 0,1 = 4 bit = 8 bit = 16 bit (tipico) Word = 32 bit (tipico)
In realta' una locazione di memoria puo' contenere non soltanto numeri ma anche altri tipi di informazioni, quali: - istruzioni (8 o piu' bit) - indirizzi (8 o piu' bit) - dati di vario tipo (corrispondenti alle variabili di tipo integer, real, boolean, char): a) numeri interi con o senza segno (8 o piu' bit); b) numeri frazionari in virgola fissa o mobile (32 o piu' bit); c) uno o piu' dati logici booleani (1 o piu' bit). d) uno o piu' caratteri di testo (8 o piu' bit); ⌡ 2.6.1 - Codici binari Poiche' all'interno di un calcolatore elettronico possiamo usare dei bit per rappresentare queste informazioni sara' necessario introdurre una codifica binaria dei caratteri per memorizzare dei testi (formati da sequenze di caratteri). In altre parole occorre introdurre una corrispondenza biunivoca tra i simboli che si vogliono rappresentare e i vari insiemi ordinati di bit. Se i caratteri da rappresentare sono N ovviamente occorrono, per ogni carattere, almeno n bit con n tale che n 2 >= N Per esempio per rappresentare i caratteri delle 10 cifre decimali (N=10) occorrono stringhe (cioe' sequenze di caratteri) di almeno 4 bit (n=4), poiche' con 3 bit ne potremmo rappresentare solo 8. Esempio di codici binari possibili per N=10 cifre con n=4 bit: Cifra
BCD
Eccesso 3
2421
GRAY
──────────┬────────────────────────────────────────────────────── 0 │ 0000 0011 0000 0000 1 │ 0001 0100 0001 0001 2 │ 0010 0101 0010 0011 3 │ 0011 0110 0011 0010 4 │ 0100 0111 0100 0110
5 6 7 8 9
│ │ │ │ │
0101 0110 0111 1000 1001
1000 1001 1010 1011 1100
1011 1100 1101 1110 1111
0111 0101 0100 1100 1101
CODICE BCD (Binary Coded Decimal): ══════════ Permette di associare ad ogni 4 bit (nibble) una cifra decimale: cosi' risulta immediata la conversione in decimale. E' usato soprattutto quando si ha tanto I/O decimale, perche' evita le conversioni tra binario e decimale (cioe' si lavora direttamente sulle cifre decimali codificate in binario (BCD) all'interno del calcolatore. Con un byte si ha un range ridotto di 0÷99 (anziche' 0÷255). Le rappresentazioni usate per i negativi sono: - modulo e segno; - complemento a 10. Le operazioni aritmetiche si eseguono come per i numeri binari di 4 bit senza segno, correggendo automaticamente di +6 o -6 quando il risultato attraversa 1001. CODICE ASCII (American Standard Code for Information Interchange): ════════════ E' un codice binario di 7 bit universalmente usato per rappresentare i 128 caratteri mostrati nella tabella seguente. Siccome l'elettronica dei calcolatori in genere tratta con insiemi di almeno 8 bit, spesso si usa questo ottavo bit per aggiungere una informazione di parita' o un'informazione grafica). CODICE ASCII ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ B(4..6)│ 000 │ 001 │ 010 │ 011 │ 100 │ 101 │ 110 │ 111 │ │ │ │ │ │ │ │ │ │ B(3..0)│ 0 │ 16 │ 32 │ 48 │ 64 │ 80 │ 96 │ 112 │ ┌──────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │ 0000 ║ NUL │ DLE ║ SP │ 0 │ @ │ P │ ' │ p │ │ 0001 ║ SOH │ DC1 ║ ! │ 1 │ A │ Q │ a │ q │ │ 0010 ║ STX │ DC2 ║ " │ 2 │ B │ R │ b │ r │ │ 0011 ║ ETX │ DC3 ║ # │ 3 │ C │ S │ c │ s │ │ 0100 ║ EOT │ DC4 ║ $ │ 4 │ D │ T │ d │ t │ │ 0101 ║ ENQ │ NAK ║ % │ 5 │ E │ U │ e │ u │ │ 0110 ║ ACQ │ SYN ║ & │ 6 │ F │ V │ f │ v │ │ 0111 ║ BEL │ ETB ║ ' │ 7 │ G │ W │ g │ w │ │ 1000 ║ BS │ CAN ║ ( │ 8 │ H │ X │ h │ x │ │ 1001 ║ HT │ EM ║ ) │ 9 │ I │ Y │ i │ y │ │ 1010 ║ LF │ SUB ║ * │ : │ J │ Z │ j │ z │ │ 1011 ║ VT │ ESC ║ + │ ; │ K │ [ │ k │ { │ │ 1100 ║ FF │ FS ║ , │ < │ L │ \ │ l │ | │ │ 1101 ║ CR │ GS ║ - │ = │ M │ ] │ m │ } │ │ 1110 ║ SO │ RS ║ . │ > │ N │ ^ │ n │ ~ │ │ 1111 ║ SI │ US ║ / │ ? │ O │ _ │ o │ DEL │ └──────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ CODICE EBCDIC (Extended Binary Coded Decimal Interchange Code): ═════════════ E' un codice binario di 8 bit usato per rappresentare 256 caratteri (usato da alcuni costruttori, come IBM, in sostituzione del codice
ASCII). CODICE UNICODE ══════════════ E' un codice progettato per rappresentare tutti i caratteri usati nei vari Paesi del mondo. Richiede 16 bit per ogni carattere e interessa chi programma applicazioni di uso internazionale. Dei 65536 caratteri rappresentabili ne sono finora usati circa 35000. I 255 caratteri del codice ASCII/ANSI (quello usato in Windows) corrispondono ai primi 255 caratteri della codifica Unicode. Ulteriori informazioni riguardanti Unicode sono reperibili al sito WEB www.unicode.org CODICE GRAY: ═══════════ Il codice Gray ha la caratteristica di avere un cambiamento di un solo bit tra cifre contigue e questo lo rende preferibile nei casi in cui, per errore di acquisizione o trasmissione dei dati, si puo' perdere un bit: con la codifica Gray ci si sposta di una sola cifra, minimizzando cosi' l'effetto dell'errore (con gli altri codici, se il bit errato fosse il piu' significativo, l'errore produrrebbe conseguenze maggiori). I vari bit (g) del codice Gray possono essere generati dai bit (b) del codice BCD con le formule seguenti: g = b n n e poi, per gli altri bit meno significativi ( i < n ), g = ( b + i i+1
b ) / 2 i
⌡ 2.6.2 - Codici ridondanti Un codice e' detto ridondante se il numero di bit usati nella codifica e' maggiore del minimo usabile. Per esempio, i codici BCD e ASCII sono codici irridondanti, mentre il codice ASCII con l'ottavo bit di parita' e' un codice ridondante. Un bit in piu' nel codice, come nell'esempio del bit di parita', puo' essere usato per fornire informazione sull'eventualita' errore (si/no). Due o piu' bit in piu' consentono, oltre alla capacita' di rivelazione, anche la possibilita' di autocorrezione. Per esempio aggiungendo al bit di parita' (trasversale) un secondo bit di parita' longitudinale si puo' individuare quale bit e' errato in un blocco di dati e quindi permetterne la correzione per mezzo di un'elettronica associata; nell'esempio seguente si riporta un blocco di 6 byte di dati come ricevuti dopo una trasmissione che ha prodotto un errore (cioe' un bit e' ricevuto errato): l'aggiunta di un byte contenente gli 8 bit delle parita' longitudinali permette all'apparato ricevente, oltre alla rivelazione dell'errore, anche l'identificazione del bit coinvolto correggendo automaticamente i dati evitando la ritrasmissione. <--- Dati---> Parita' 0 0 1 1 0 1 0 1 0 1 0 1 1 1 0 1 0 0 0 1 0
1 1 0
0 0 1 1 0 1 0 1 1 0 0 0 1 1 1 1 <--- Byte con un bit errato 0 1 1 0 1 0 0 1 (parita' trasversale errata) ───────────────── 0 0 1 0 1 1 0 1 <--- Byte delle parita' longitudinali ^ | | Colonna con errore (la parita' longitudinale risulta errata) !------------------------------------------------------------
CAPITOLO 3 - Strutture di dati ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di definire le strutture di dati che saranno poi usate nel seguito del corso (per esempio gli stack usati da specifiche istruzioni e nelle interruzioni). Questo capitolo concerne l'organizzazione dei dati in memoria e le relative regole di accesso. Infatti i dati possono essere strutturati in maniera da facilitare il loro uso e accesso, come ora vedremo. La scelta della struttura migliore dipende da vari fattori, quali: 1) l'uso dei dati (per esempio: un albero genealogico contiene piu' informazioni di una semplice lista di nomi); 2) la rapidita' di accesso (che puo' risultare diversa, per esempio, per un elenco telefonico nominativo o stradale); 3) lo spazio di memoria usato (per esempio per memorizzare i 168 numeri primi tra 1 e 1000, anziche' memorizzare 168 numeri di 10 bit, pari a 1680 bit, posso memorizzare una mappa di 1000 bit, dove sono messi a 1 quelli corrispondenti ai 168 numeri primi, impegnando cosi' solo 1000 bit anziche' 1680). Per questi scopi vengono introdotte varie strutture di dati, quali quelle che ora discuteremo. ⌡ 3.1 - Vettori e matrici (Array) Vengono organizzati in memoria sequenzialmente ordinando per righe (o colonne), ma nei linguaggi di alto livello, come in Pascal, lo utente usa la familiare notazione matematica di un simbolo con uno (vettore) o piu' indici (matrice). Esempi in Pascal sono i seguenti: VAR VAR VAR
a : ARRAY [10..20] OF char; a3x2 : ARRAY [1..3] OF ARRAY [1..2] OF real; a3x2 : ARRAY [1..3,1..2] OF real;
Un elemento di questa matrice sara' indicato con a3x2 [i,j]
(riga 1, colonna j)
⌡ 3.2 - Pila (Stack) Una struttura molto importante per i molteplici usi e' lo stack (useremo il termine americano perche' piu' comunemente usato). Si tratta di una struttura monodimensionale su cui si opera in modo LIFO (Last In First Out=il primo dato in uscita e' l'ultimo entrato). Lo stack si presenta all'utente come un serbatoio dove inserire dei dati per poi riprenderli con modalita' LIFO (che evidentemente deve essere richiesta dall'applicazione in corso, altrimenti non si usera' uno stack ma altre strutture). Le operazioni a disposizione dell'utente sono quella di inserimento nello stack (PUSH) e di prelievo dallo stack (POP). La modalita' LIFO e' resa possibile dall'uso di un puntatore SP (Stack Pointer), cioe' una locazione di memoria dove viene mantenuto l'indirizzo della prima locazione libera nello stack. Ad ogni inserimento (PUSH) lo stack pointer SP viene incrementato, mentre ad ogni prelievo viene decrementato. In questo uso dello stack l'utente potrebbe commettere errori: per esempio se preleva piu' dati di quanti ne ha inseriti (underflow), oppure se cerca di inserirne piu' di quanti lo stack puo' contenerne (overflow), in base allo spazio di memoria ad esso assegnato. In tali casi bisogna prevedere degli appositi messaggi con le conseguenti azioni (in genere l'errore e' irrecuperabile e comporta la interruzione dell'operazione). La programmazione di uno stack ovviamente puo' essere fatta in qualsiasi linguaggio. Qui mostriamo l'esempio di uno stack a cui sono riservate 100 locazioni di memoria, realizzato in Pascal: ⌡ 3.2.1 - Inizializzazione dello stack (da fare una sola volta allo inizio del programma): | | | | CONST max := 100; │ │ VAR stack: ARRAY [1..max] OF tipo; ├───┤ sp : integer; │ │ BEGIN ├───┤ sp := 1; {stack vuoto} │ │ ........ ├───┤ sp ───> │ │ └───┘ ⌡ 3.2.2 - PUSH x | | | | │ │ ├───┤ │ │ ├───┤ sp ───> │ │ ├───┤ │ x │ └───┘ ⌡ 3.2.3 - POP x | | | |
nello stack
(da fare tutte le volte che serve)
........... IF sp <= max THEN BEGIN stack [sp] := x; sp := sp + 1; END ELSE ........; {errore di overflow} ...........
dallo stack
(da fare tutte le volte che serve)
IF sp > 1 THEN
│ │ ├───┤ │ │ ├───┤ │ │ ├───┤ sp ───> │ │ └───┘
BEGIN sp := sp - 1; x := stack [sp] END ELSE ........; {errore di underflow} ............
⌡ 3.3 - Coda (Queue) Un'altra struttura molto importante e' la coda. Si tratta di una struttura monodimensionale su cui si opera in modo FIFO (First In First Out=il primo dato in uscita e' il primo entrato). La coda si presenta all'utente come un serbatoio dove inserire dei dati per poi riprenderli con modalita' FIFO (che evidentemente deve essere richiesta dall'applicazione in corso, altrimenti non si usera' una coda ma altre strutture). Come nello stack anche nella coda le operazioni a disposizione dello utente sono quella di inserimento e di prelievo dalla coda. La modalita' FIFO e' resa possibile dall'uso di due puntatori 'head' e 'tail', cioe' due locazioni di memoria dove vengono mantenuti gli indirizzi della prima locazione libera nella coda (tail) e della prima locazione prelevabile dalla coda (head). Le modalita' di funzionamento saranno le seguenti: - all'inizio (=coda vuota) sia 'head' che 'tail' punteranno alla prima locazione della coda (cioe' head=tail=1); - ad ogni inserimento viene incrementato il puntatore 'tail'; - ad ogni prelievo viene incrementato il puntatore 'head'. Siccome con queste modalita' i due puntatori, crescendo sempre di piu', ci porterebbero inevitabilmente fuori dei limiti di memoria assegnati alla coda, possiamo farli funzionare in modo ciclico, cioe' facendogli assumere di nuovo il valore 1 dopo il massimo (corrispondente alla ultima locazione di memoria assegnata alla coda. In questo modo durante il funzionamento il puntatore 'tail' va avanti rincorso dal puntatore 'head': la distanza tra i due indica quanti dati sono contenuti nella coda e quando 'head' raggiunge 'tail' la coda e' svuotata; la condizione di coda piena si verifica quando 'tail', andando sempre piu' avanti in modo ciclico, arriva a precedere di uno la posizione di 'head' (per cui avanzando ancora di uno si realizzerebbe erroneamente la situazione di coda vuota (cioe' head=tail). Le condizioni di errore si verificano quando l'utente tenta di memorizzare dati in una coda piena oppure quando l'utente tenta di prelevare dati da una coda vuota. In tali casi bisogna prevedere degli appositi messaggi con le conseguenti azioni (in genere l'errore e' irrecuperabile e comporta la interruzione dell'operazione). La programmazione di una coda ovviamente puo' essere fatta in qualsiasi linguaggio. Qui mostriamo l'esempio di una coda a cui sono riservate 100 locazioni di memoria, realizzata in Pascal: ⌡ 3.3.1 - Inizializzazione della coda (da fare una sola volta allo inizio del programma): | | | | CONST max := 100; │ │ VAR queue: ARRAY [1..max] OF tipo; ├───┤ head,tail,temp : integer;
│ │ BEGIN ├───┤ │ │ ├───┤ head=tail───>│ │ (coda vuota) └───┘ ⌡ 3.3.2 - Inserisce | | | | │ │ ├───┤ │ │ ├───┤ tail───> │ │ ├───┤ head───> │ x │ └───┘
x
head := 1; tail := 1; ........
nella coda
{coda vuota}
(da fare tutte le volte che serve)
........... temp := tail + 1; IF temp > max THEN temp:=1; {funz.ciclico} IF temp = head THEN ....... {check overflow} ELSE BEGIN queue [tail] := x; tail := temp END; ...........
⌡ 3.3.3 - Preleva x dalla coda (da fare tutte le volte che serve) | | | | .......... │ │ IF head = tail THEN .... {check underflow} ├───┤ ELSE BEGIN │ │ x := queue[head]; ├───┤ head := head + 1; head=tail───>│ │ IF head > max THEN head:=1 {funz.cicl.} ├───┤ END; │ │ ............ └───┘ ⌡ 3.4 - Liste monodimensionali Supponiamo si avere un insieme di molti dati scritti su disco in maniera ordinata (per esempio alfabeticamente) e di volervi inserire un ulteriore dato. Dopo aver trovato la posizione che gli compete dovremmo spostare tutti i dati seguenti di una posizione, riscrivendoli su disco con gran dispendio di tempo. Questo puo' essere evitato associando ad ogni elemento dell'insieme l'indirizzo dell'elemento seguente nell'ordinamento (successore): cosi' facendo creiamo una lista. Una lista monodimensionale e' quindi un insieme di dati (di tipo qualsiasi, eventualmente anche blocchi di dati) in cui ad ogni elemento e' associato un numero intero che individua l'elemento seguente secondo un ordinamento prestabilito. L'ultimo elemento della lista , che non ha successori , avra' associato un numero convenzionale che chiamiamo 'null' , mentre il primo elemento della lista sara' individuato da un primo puntatore che chiameremo 'inizio' della lista. Esempio:
LISTA CONTENENTE GLI ELEMENTI A,B...Z (nell'ordine) memorizzati negli indirizzi a,b...z:
inizio
┌────┐ ┌────┐ ┌────┐ ┌────┐ │ a │───>│ b │───>│ c │───>....───>│null│ └────┘ ├────┤ ├────┤ ├────┤
│dati│ │ A │ │ │ └────┘ Indirizzo dei dati: a
│dati│ │ B │ │ │ └────┘ b
....
│ │ │ Z │ │ │ └────┘ z
Anche in questo caso le operazioni possibili sono quelle di inizializzazione, inserimento e cancellazione di un elemento dalla lista. La loro programmazione ovviamente puo' essere fatta in qualsiasi linguaggio. Nel seguito mostreremo l'esempio di gestione di una lista, a cui sono riservate 100 locazioni di memoria, realizzata in Pascal. ⌡ 3.4.1 - Inizializzazione della lista (da fare una sola volta allo inizio del programma): LISTA VUOTA (nessun successore): ┌────┐ inizio │null│ └────┘ CONST max := 100; null := -1; VAR nrec: ARRAY [1..max] OF tipo; link: ARRAY [1..max] OF integer; BEGIN link[0] := null; {inizio della lista} ........ ⌡ 3.4.2 - Inserimento di un elemento
Q
nella lista
Supponiamo di avere la lista seguente: ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ p │───>│ t │───>│ v │───>....───>│null│ └────┘ ├────┤ ├────┤ ├────┤ │dati│ │dati│ │ │ │ P │ │ T │ .... │ Z │ │ │ │ │ │ │ └────┘ └────┘ └────┘ Indirizzo dei dati: p t z inizio
Se vogliamo inserire l'elemento Q (memorizzato nell'indirizzo q) tra P e T dobbiamo modificare la sequenza dei puntatori 'link' come segue: ┌───────────────────┐ ┌────┐ ┌────┐ │ ┌────┐ │ ┌────┐ inizio │ p │───>│ q │─┘ ┌─>│ v │──>... └─>│ t │─>┐ └────┘ ├────┤ │ ├────┤ ├────┤ │ │dati│ │ │dati│ │dati│ │ │ P │ │ │ T │ ... │ Q │ │ │ │ │ │ │ │ │ │ └────┘ │ └────┘ └────┘ │ └────────────────────────────┘ Indirizzo dei dati: p t q La programmazione Pascal che esegue questa modifica e' la seguente: link[q]:=link[p]; {successore di q diventa il vecchio successore di p}
link[p]:=q;
{successore di p diventa q} ..............
⌡ 3.4.3 - Cancellazione di un elemento
Y
dalla lista:
Supponiamo di avere la lista seguente: ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ x │───>│ y │───>│ z │───>│ .. │───>.... └────┘ ├────┤ ├────┤ ├────┤ │dati│ │dati│ │dati│ │ X │ │ Y │ │ Z │ │ │ │ │ │ │ └────┘ └────┘ └────┘ Indirizzo dei dati: x y z inizio
Se vogliamo cancellare l'elemento Y (cioe' eliminarlo dalla lista) basta modificare la sequenza dei puntatori 'link' come segue: ┌────────────────┐ ┌────┐ ┌────┐ │ ┌────┐ │ ┌────┐ inizio │ x │───>│ z │─┘ ..─>│ │─>.. └─>│ .. │───>.... └────┘ ├────┤ ├────┤ ├────┤ │dati│ │dati│ │dati│ │ X │ │ Y │ │ Z │ .... │ │ │ │ │ │ └────┘ └────┘ └────┘ Indirizzo dei dati: x y z (non piu' usato) La programmazione Pascal che esegue questa modifica e' la seguente: IF link[x] <> null THEN .... {controlla che esista il successore di x} link[x] := link[link[x]] {succ.di x diventa il succ.del succ.di x} ELSE ........ {errore: nessun successore} .......... ⌡ 3.5 - Liste bidimensionali La ricerca di un dato elemento in una lista si esegue leggendo i dati ordinati nella lista in base ai puntatori 'link', partendo dall'inizio. Questo metodo pero' risulta poco efficiente quando l'elemento si trova verso la fine della lista, poicche' siamo obbligati a leggere tutti gli elementi dall'inizio. Meglio sarebbe allora mantenere in memoria, anziche' un solo vettore di puntatori 'link[x]', due vettori di puntatori 'predlink[x]' e 'succlink[x]' indicanti sia il successore di una dato elemento (come 'link[x]' della lista monodimensionale) che il predecessore dello stesso elemento. In tal caso abbiamo la possibilita' di eseguire la scansione della lista in entrambe le direzioni (cioe' anche dalla fine verso l'inizio). Una lista bidimensionale e' quindi un insieme di dati (di tipo qualsiasi, eventualmente anche blocchi di dati) in cui ad ogni elemento sono associati DUE numeri interi che individuano gli elementi contigui seguente secondo l'ordinamento prestabilito. I due elementi estremi della lista, avranno associato un numero convenzionale che chiamiamo 'null'. Questi sono il primo elemento di 'succlink[x]' che chiameremo 'inizio' della lista (contiene l'indirizzo del primo elemento) e di 'predlink[x]' che chiameremo
'fine' della lista (contiene l'indirizzo dell'ultimo elemento). Una lista bididimensionale e' quindi rappresentata dallo schema seguente: Esempio:
LISTA BIDIMENSIONALE CONTENENTE GLI ELEMENTI A,B...Z (nell'ordine) memorizzati negli indirizzi a,b...z:
┌────┐ ┌────┐ ┌────┐ succlink ┌────┐ inizio │ a │───>│ b │───>│ c │──>....──>│null│ └────┘ ├────┤ ├────┤ ├────┤ ┌────┐ (succlink) │null│<───│ a │<──....<──│ y │<───│ z │ fine ├────┤ ├────┤ predlink ├────┤ └────┘ │dati│ │dati│ │ │ (predlink) │ A │ │ B │ .... │ Z │ │ │ │ │ │ │ └────┘ └────┘ └────┘ Indirizzo dei dati: a b z Anche in questo caso le operazioni possibili sono quelle di inizializzazione, inserimento e cancellazione di un elemento dalla lista, soltanto che la programmazione deve prevedere l'aggiornamento di entrambi i puntatori 'succlink' e 'predlink'. La lista vuota e' rappresentata dai due puntatori contenenti 'null'. LISTA BIDIMENSIONALE VUOTA (nessun successore): ┌────┐ inizio │null│───> ┌────┐ └────┘ <───│null│ fine └────┘ ⌡ 3.6 - Gestione della 'Free List' La memorizzazione dei dati della lista comporta l'impiego di aree di memoria che vanno impegnate quando si inserisce un elemento nella lista e recuperate quando lo si cancella, altrimenti con il succedersi degli inserimenti si verificherebbe inivitabilmente un errore di trabocco di memoria (overflow). Questo comporta la necessita' di una gestione dei blocchi di memoria usati per la memorizzazione degli elementi della lista. Questa gestione si esegue solitamente in uno dei due modi seguenti: 1) aggiornamento on-line: ad ogni operazione di inserimento o cancellazione si eseguono le istruzioni che aggiornano la 'free list'; 2) aggiornamento off-line: si evita l'aggiornamento on-line per guadagnare in velocita' dell'elaborazione, rimandando questo lavoro ai momenti in cui l'unita' di elaborazione e' sottoutilizzata (o quando durante un inserimento la free list risulta vuota); si esegue allora il processo chiamato 'garbage collection' (cioe' 'raccolta dei rifiuti'), che individua i blocchi di memoria non piu' usati dalla lista e li reinserisce nella 'free list'. ⌡ 3.7 - RIEPILOGO SULLE STRUTTURE DI DATI Riportiamo di seguito per comodita' il riepilogo contenuto nel dischetto delle esercitazioni (LEZ60.PAS).
Organizzazione di dati in memoria e regole per accedervi. -------------------------------------------------------La scelta della struttura dei dati dipende dai seguenti elementi: 1) l'uso che se ne deve fare (Es. un'albero genealogico contiene piu' informazioni di una lista di nomi); 2) la rapidita' di accesso (Es. elenco telefonico ed elenco stradale); 3) lo spazio di memoria usato (Es. i 168 numeri primi tra 1 e 1000 con map); ============================================================================== ARRAY (Mono e Multidimensionali): VAR matrix: ARRAY [1..5,1..9] OF real; ============================================================================== PILA (Push-Down Stack = LIFO): | CONST max = 100; ---| VAR stack : ARRAY [1..max] OF tipo; ( sp punta alla prima loc. libera ) | sp : integer; | BEGIN | sp := 1; {stack vuoto} | ........ ----------------------------------------|------------------------------------{ check and push x } | { check and pop x } IF sp <= max THEN | IF sp > 1 THEN BEGIN stack[sp]:=x; sp:=sp+1 END | BEGIN sp:=sp-1; x:=stack[sp] END ELSE ...{ overflow }... | ELSE ...{ underflow }... ........ | ....... | ============================================================================== CODA (Queue = FIFO): | CONST max = 100; ---| VAR queue : ARRAY [1..max] OF tipo; | head,tail : integer; | BEGIN | head := 1; tail := 1; ----------------------------------------|------------------------------------{ check e mette in coda x } | { check e toglie dalla coda x } temp := tail + 1; | IF head := tail THEN ...{underflow} IF temp > max THEN temp := 1; | ELSE BEGIN IF temp := head THEN ...{overflow}... | x := queue[head]; head:=head+1; ELSE BEGIN | IF head > max THEN head:=1; queue[tail]:=x; tail:=temp END; | END; ....... | ........ | ============================================================================== LISTA (One-Link): | CONST max = 100; null = -1; ----| VAR nrec: ARRAY[1..max] OF integer; Consente di inserire e cancellare | link: ARRAY[0..max] OF integer; elementi nel mezzo della lista senza | BEGIN spostare gli altri. | link [0] := null; {head of list} ----------------------------------------|------------------------------------{ inserisce elemento q dopo il p } | {elimina successore dell'elemento x} link [q] := link [p]; | IF link [x] <> null THEN link [p] := q; | link [x] := link [ link[x] ] ......... | ELSE ...{error, no successor}... ,------, ,------, ,------, ,------, | head | ----> | | ----> | | ----> | null | `------' |------| |------| |------| | A | | B | | C | `------' `------' `------' ============================================================================== LISTA (Two-Links): Consente la scansione bidirezionale della lista. ----,------, ,------, ,------, ,------, ,------, | | ----> | | ----> | succ | ----> | | ----> | |
| head | |------| |------| |------| | tail | | | <---- | | <---- | pred | <---- | | <---- | | `------' |------| |------| |------| `------' | A | | B | | C | `------' `------' `------' Free List: ---------
E' la lista dei blocchi non usati. E' necessario recuperare la memoria non piu' impegnata dopo la cancellazione di un blocco dalla lista altrimenti si andrebbe in 'memory overflow'. Garbage Collection: E' il processo di recupero dei blocchi di memoria liberi ------------------ (dopo averli individuati vengono reinseriti nella Free List) !----------------------------------------------------------------------CAPITOLO 4 - Introduzione all'algebra di Boole ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di introdurre gli elementi di quest'algebra che e' lo strumento matematico usato nell'elettronica digitale. Quest'algebra, introdotta dal matematico George Boole (1815-64) per gli studi di logica delle proposizioni, e' stata con successo poi impiegata da C.Shannon nel 1936 per lo studio dei circuiti digitali a rele' ed elettronici. Per questo ci interessa impadronirci di questo strumento algebrico. Siamo in presenza di un'algebra booleana quando su un insieme di due o piu' elementi introduciamo le operazioni di prodotto logico (AND), somma logica (OR) e complemento (NOT), soddisfacenti alle seguenti proprieta': ┌─────────────────┬──────────────────────────────────────────────┐ │ PROPRIETA' │ SOMMA (OR) PRODOTTO (AND) │ ├─────────────────┼──────────────────────────────────────────────┤ │ │ │ │ Associativa │ (a+b)+c = a+(b+c) (a.b).c = a.(b.c) │ │ │ │ │ Commutativa │ a + b = b + a a . b = b . a │ │ │ │ │ Esistenza │ a + 0 = a a . 1 = a │ │ elemento neutro │ │ │ │ _ _ │ │ Esistenza │ a + a = 1 a . a = 0 │ │ complemento │ ═════════ ═════════ │ │ │ │ │ Distributiva │ a+(b.c) = (a+b).(a+c) a.(b+c) = a.b+a.c │ │ │ ═════════════════════ │ └─────────────────┴──────────────────────────────────────────────┘ Le proprieta' sottolineate sono quelle che distinguono l'algebra di Boole dall'algebra ordinaria: _ 1) Esiste un unico complemento a sia per somma che per prodotto (anziche' -a e 1/a); 2) Proprieta' distributiva simmetrica rispetto a somma e prodotto.
⌡ 4.1.1 - Diagrammi di Venn Anche l'insiemistica rientra tra i campi di applicazione dell'algebra booleana poiche' le operazioni di unione e intersezione tra insiemi soddisfano le proprieta' suddette. Per questo motivo gli insiemi possono essere usati a scopo dimostrativo delle relazioni algebriche booleane (diagrammi di Venn): ┌───────┐ A │ │ │ │ └───────┘ ┌───────┐ │ │ B │ │ └───────┘
┌──────────┐ A │░░░░░░░░░░│ │░░░░┌─────┼────┐ │░░A░│░░+░░│░B░░│ │░░░░│░░░░░│░░░░│ └────┼─────┘░░░░│ │░░░░░░░░░░│ B └──────────┘
A
░░░░░░░░░░░░ ░░░░░░░░░░░░ ░░░░░███████▒▒▒▒▒ ░░░░░██A.B██▒▒▒▒▒ ░░░░░███████▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒ B ▒▒▒▒▒▒▒▒▒▒▒▒
Le varie relazioni algebriche che legano 3 variabili booleane A,B e C possono essere visualizzate dalle varie aree nel diagramma seguente (dove 0 rappresenta l'insieme vuoto e 1 l'universo, rappresentato dalla cornice): ╔═════════════════════════════════════╗ ║ ║ ║ ┌─────────────┐ ║ ║ │ A │ ║ ║ │ ┌────────┼────┐ ║ ║ │ │ │ │ ║ ║ ┌───┼────┼─────┐ │ │ ║ ║ │ │ │ │ │ │ ║ ║ │ └────┼─────┼──┘ │ ║ ║ │ │ │ B │ ║ ║ │ └─────┼───────┘ ║ ║ │ C │ ║ ║ └──────────────┘ ║ ║ ║ ╚═════════════════════════════════════╝ Per esempio: _____ _ _ a + b = a . b ╔════════════════════════════╗ a + a.b ║░░░░░░░░░░░░░░░░░░░░░░░░░░░░║ ┌──────────┐ ║░░░░░░┌──────────┐░░░░░░░░░░║ A │░░░░░░░░░░│ ║░░░░░░│ A │░░░░░░░░░░║ │░░░░┌─────┼────┐ ║░░░░░░│ ┌─────┼────┐░░░░░║ │░░░░│▓▓▓▓▓│ │ ║░░░░░░│ │ │ │░░░░░║ └────┼─────┘ │ ║░░░░░░└────┼─────┘ │░░░░░║ │ │ B ║░░░░░░░░░░░│ B │░░░░░║ └──────────┘ ║░░░░░░░░░░░└──────────┘░░░░░║ ║░░░░░░░░░░░░░░░░░░░░░░░░░░░░║ ╚════════════════════════════╝ L'uguaglianza di due espressioni booleane puo' cosi' anche essere dimostrata anche verificando che entrambe corrispondono alla stessa area nel diagramma di Venn. Si verifichi, per esempio, che entrambi i membri della proprieta' distributiva corrispondono allo stesso insieme (=area scura).
⌡ 4.1.2 - Proprieta' e teoremi Elenchiamo ora una serie di proprieta' delle operazioni booleane, con le relative dimostrazioni, che ci saranno utili anche al fine di prendere confidenza con le regole algebriche che si usano nelle manipolazioni algebriche delle espressioni booleane. 1)
a + a = a Dimostrazione:
2)
a.a = a Dimostrazione:
3)
come per la (2)
a + a.b = a Dimostrazione:
6)
_ _ _ a+1=(a+1).1=(a+1).(a+a)=a+(1.a)=a+a=1
a.0 = 0 Dimostrazione:
5)
_ _ a.a=(a.a)+0=a.a+a.a=a.(a+a)=a.1=a
a + 1 = 1 Dimostrazione:
4)
_ _ a+a=(a+a).1=(a+a).(a+a)=a+(a.a)=a+0=a
a+ab=a.1+ab=a.(1+b)=a.(b+1)=a.1=a
a.(a + b) = a Dimostrazione:
7)
8)
a.(a+b)=(a+0).(a+b)=a+(0.b)=a+0=a ┌─ _ ┌─_ = │ a.a=0 commutativa │ a.a=0 a = a Dimostrazione: │ _ ═══════════════> │ _ │ a+a=1 │ a+a=1 └─ └─ _ ╔═════════════════╗ ( ovvero: a e' complemento di a ) ║ _______ _ _ ║ ║ (a + b) = a . b ║ ( Teorema di De Morgan ) ║ ║ ╚═════════════════╝ _ _ Per dimostrarlo basta dimostrare che a.b e' il complemento di (a+b) ovvero che: ┌─ _ _ │ (a+b)+a.b = 1 │ _ _ │ (a+b).a.b = 0 └─ _ _ _ _ _ _ (a+b)+a.b=((a+b)+a).((a+b)+b)=(b+(a+a)).(a+(b+b))=(b+1)(a+1)=1 _ _ _ _ _ _ _ _ _ _ _ _ (a+b).a.b= a.(a.b) + b.(a.b) = (a.a).b + (b.b).a = 0.b + 0.a=0 La dimostrazione risulta anche piu' immediata usando i diagrammi di Venn.
9)
╔═══════════════╗ ║ _____ _ _ ║ ║ a . b = a + b ║ ( Teorema di De Morgan duale) ║ ║ ╚═══════════════╝ Per dimostrarlo basta complementare la (8). Conseguenza diretta
delle (8) e (9) e' la Legge di dualita': Ogni espressione algebrica e' equivalente a ═════════════════ quella che si ottiene: - scambiando i + con i . - complementando ogni variabile e l'espressione risultante. _ a + a.b = a + b _ _ Dimostrazione: a+ab=(a+a).(a+b)=1.(a+b)=a+b _ 11) a.(a + b) = a.b 10)
Dimostrazione:
per dualita' dalla (10).
⌡ 4.1.3 - Tabella della verita' e teorema di sviluppo Essendo i valori possibili delle variabili booleane in numero finito (0/1, vero/falso, ecc.) e' possibile definire un'espressione booleana, oltre che con la sua forma algebrica, anche con una tabella, chiamata 'Tabella della verita'', che descriva i valori assunti in corrispondenza di tutte le possibili disposizioni di valori delle variabili di ingresso. In questo modo le operazioni elementari possono essere definite con le corrispondenti tabelle della verita': AND a b │ a.b ───────┼────── 0 0 │ 0 0 1 │ 0 1 0 │ 0 1 1 │ 1
a 0 0 1 1
OR b │ a+b ───────┼────── 0 │ 0 1 │ 1 0 │ 1 1 │ 1
NOT _ a a ─────┼────── 0 │ 1 1 │ 0
Una volta che una funzione di due variabili a,b sia definita con la sua tabella della verita' a b │ f(a,b) ───────┼────── 0 0 │ f(0,0) 0 1 │ f(0,1) 1 0 │ f(1,0) 1 1 │ f(1,1) risulta immediata la scrittura della sua espressione algebrica grazie al teorema di sviluppo che nella sua forma canonica si scrive: _ _ _ _ f(a,b) = a.b.f(0,0) + a.b.f(0,1) + a.b.f(1,0) + a.b.f(1,1) ovvero per n variabili x , x ,...x : 1 2 n 1 f(x ,x ,...x ) = 1 2 n 1
1 2 n 1 1 i i i S S S f(i ,i ,...i ).x x ... x i=0 i=0 ... i=0 1 2 n 1 2 n 2 n ╚════════════╝ mintermini n
cioe' la somma di tutti i 2 prodotti (chiamati 'mintermini') delle n variabili x....x , con gli indici i variabili tra 0 e 1, 1 n ╔═ ╔═ ╔═ ║ 0 _ ║ 0 _ ║ 0 _ ║ x = x ║ x = x ...... ║ x = x ║ 1 1 ║ 2 2 ║ n n essendo: ║ ║ ║ ║ 1 ║ 1 ║ 1 ║ x = x ║ x = x ...... ║ x = x ║ 1 1 ║ 2 2 ║ n n ╚═ ╚═ ╚═ Con simboli semplificati possiamo scrivere: n f(x ,x ,...x ) = Somma ( m ) (dove gli m sono tutti i 2 1 2 n i i
mintermini)
Naturalmente da questa forma del teorema di sviluppo possiamo dedurne un'altra per dualita': n f(x ,x ,...x ) = Prodotto ( M ) (dove gli M sono tutti i 2 Maxtermini) 1 2 n i i chiamando Maxtermini i duali dei mintermini. Osserviamo che nelle due forme del teorema di sviluppo risultano presenti solo i termini in cui il termine f(i ,i ,...i ) risulta = 1 (=0 nella forma duale). 1 2 n Quindi lo sviluppo di una funzione booleana ha due forme possibili a secondo degli 1 e 0 presenti nella tabella della verita': - somma di tutti i mintermini corrispondenti agli 1; - prodotto di tutti i Maxtermini corrispondenti agli 0. Quello dei due contenente meno termini da la forma piu' semplice del teorema di sviluppo. Basta quindi esaminare la tabella della verita' per vedere se ci sono piu' 1 o piu' 0, per scegliere la forma piu' conveniente del teorema di sviluppo. Per esempio, con due variabili a,b abbiamo: Tabella della verita' a b │ f(a,b) ───────┼─────── 0 0 │ 1 │ 0 1 │ 0 │ 1 0 │ 1 │ 1 1 │ 1
mintermini Maxtermini Teorema di sviluppo ══════════ ══════════ ═══════════════════ _ _ _ _ _ a.b a+b f(a,b) = a.b + a.b + a.b _ _ (somma di mintermini) a.b a+b _ _ e sua forma duale: a.b a+b _ _ _ f(a,b) = a+b a.b a+b (prodotto di Maxtermini)
L'espressione canonica del teorema di sviluppo puo' poi essere semplificata mediante manipolazioni algebriche: _ _ _ _ _ _ _ _ _ f(a,b) = a.b + a.b + a.b = a.b + a.(b+b) = a.b +a = a + b Essendovi nella tabella della verita' tre 1 ed un solo 0 potevamo pre-
vedere che la forma piu' semplice sarebbe stata quella con il prodotto di Maxtermini. Comunque in generale anche la forma piu' semplice del teorema di sviluppo e' suscettibile di ulteriori minimizzazioni. Quando pero' le variabili sono piu' numerose e l'espressione piu' complicata le manipolazioni algebriche risultano piu' complesse e la relativa minimizzazione viene allora a dipendere dall'abilita' dell'operatore. In tali casi si possono usare degli appositi metodi di minimizzazione, come quello delle mappe di Karnaugh, che possono essere poi programmati in appositi computer (sistemi di sviluppo) per comodita' di uso. ⌡ 4.1.4 - Campi di applicazione dell'algebra di Boole Molteplici sono i campi di applicazione dell'algebra di Boole. Citiamo tra i piu' importanti: a) b) c) d) e)
Classi di persone o di oggetti (insiemistica); Logica delle proposizioni; Sistemi a rel?; Circuiti logici digitali; ..........
Come esempio riportiamo l'applicazione ad un problema di logica delle proposizioni. Un club si e' dato le seguenti regole: 1) i membri del comitato Finanziario devono essere scelti tra i membri del comitato Generale; 2) nessuno puo' essere contemporaneamente membro dei comitati Generale e di Biblioteca senza essere membro del comitato Finanziario; 3) nessun membro del comitato di Biblioteca deve appartenere al comitato Finanziario. Problema: - E' possibile esprimere le regole in modo piu' semplice? - Le regole sono in contraddizione tra loro? - C'e' qualche regola superflua? Risolviamo il problema usando il formalismo dell'algebra di Boole. Chiamiamo f,g,b gli insiemi dei membri dei comitati Finanziario, Generale e di Biblioteca; le tre regole possono allora essere espresse come segue: _ 1) f e' contenuto in g ────> f . g = 0 _ 2) (g intersezione b) e' contenuto in f ────> g.b.f = 0 3)
g intersezione f
e'
vuoto
────>
b . f = 0
Il sistema delle tre espressioni e' equivalente al loro OR: _ _ f.g + g.b.f + b.f = 0 che puo' essere semplificato con le seguenti manipolazioni algebriche: _ _ _ f.g + g.b.f + (g+g).b.f = 0 _ _ _ f.g + g.b.f + g.b.f + g.b.f = 0 _ _ f.g.(1 + b) + g.b.(f + f) = 0
┌─
_ │ ────> │ g.b └─ che corrispondono a due sole regole _ f.g + g.b = 0
A)
f
e' contenuto in
B)
g intersezione b
f.g = 0 │ = 0 (equivalenti alle 3 originarie):
g e'
vuoto
che possono essere espresse come: A) i membri del comitato Finanziario devono essere scelti tra i membri del comitato Generale; B) nessun membro del comitato di Biblioteca deve appartenere al comitato Generale. Altro campo di applicazione e' quello dei circuiti a rel?, in cui i due stati possibili sono circuito aperto/chiuso e le operazioni booleane di somma e prodotto corrispondono ai collegamenti serie e parallelo dei rel?, mentre il complemento e' rappresentato dal deviatore. Come esempio riportiamo la rappresentazione di un sistema di rel? con un'espressione booleana: ┌─────■ \■─────┐ │ b │ ┌─────■ \■──────┤ ├───────┐ │ a │ │ │ │ └─────■ \■─────┘ │ ─────────┤ c ├──────── │ │ │ │ └─────■ \■────────────■ \■─────────────┘ d e L'espressione booleana che lo rappresenta e': a . ( b + c ) + d . e Come esempio di semplice problema, che si risolve con la sola applicazione del teorema di sviluppo, troviamo lo schema dei collegamenti necessari per accendere una lampadina in una stanza con due interruttori indipendenti. Il circuito deve realizzare la seguente funzione f(a,b) delle due variabili di ingresso a,b (rele'): a b │ f(a,b) ───────┼────── 0 0 │ 0 0 1 │ 1 1 0 │ 1 1 1 │ 0 In base al teorema di sviluppo risulta: _ _ f(a,b) = a . b + a . b e quindi il circuito cercato e': _ a b
■──────────■ ╔══════╗ ──────■\ /■──────║ LAMP.║────── ■──────────■_ ╚══════╝ a b Il campo di applicazione che piu' ci interessa in questo corso e' quello dei circuiti logici digitali. Per esempio il circuito seguente:
x y z
_ _ ┌─────┐ x ┌─────┐ xz ┌─────>│ NOT │──────>│ AND │────┐ │ └─────┘ ┌──>└─────┘ │ │ │ │ │ ┌─────┐xy │ xy └───>┌────┐ ────┼─────>│ AND │──────────────────────>│ OR │───> f(x,y,z) ────│──┬──>└─────┘ │ ┌───>└────┘ ────│──│─────────────┤ _ │ │ │ ┌─────┐ └──>┌─────┐ xyz│ │ └──>│ NOT │──────>│ AND │────┘ │ └─────┘ ┌──>└─────┘ └────────────────┘
e' rappresentato dall'espressione booleana seguente: _ _ f(x,y,z) = x.y.z+x.y+x.z Questa pero' puo' essere semplificata mediante manipolazioni algebriche: _ _ _ _ _ _ f(x,y,z) = x.y.z+x.y+x.z = x.(y+yz)+xz = x.(y+z)+xz = xy+z.(x+x) = x.y+z e la forma risultante, molto piu' semplice, corrisponde al circuito equivalente: x y z
┌─────┐xy ┌────┐ ───────>│ AND │──────────>│ OR │──────> ───────>└─────┘ ┌───>└────┘ ─────────────────────┘
f(x,y,z) = x.y + z
L'equivalenza delle due espressioni booleane puo' essere evidenziata anche con i diagrammi di Venn (corrispondono alla stessa area): _ _ f(x,y,z) = x.y.z + x.y + x.z f(x,y,z) = x.y + z ╔═══════════════════════════════╗ ╔═══════════════════════════════╗ ║ ┌─────────────┐ ║ ║ ┌─────────────┐ ║ ║ │ x ┌────────┼────┐ ║ ║ │ x ┌────────┼────┐ ║ ║ │ │▓▓▓▓▓▓▓▓│ │ ║ ║ │ │▓▓▓▓▓▓▓▓│ │ ║ ║ ┌───┼────┼▓─▓─▓┐▓▓│ │ ║ ║ ┌───┼────┼▓─▓─▓┐▓▓│ │ ║ ║ │░░░│████│▓▓▓▓▓│▓▓│ │ ║ ║ │░░░░░░░░│░░░░░│▓▓│ │ ║ ║ │░░░└────┼─────┼──┘ │ ║ ║ │░░░└─░─░┼░─░─░┼──┘ │ ║ ║ │░░░░░░░░│░░░░░│ y │ ║ ║ │░░░░░░░░│░░░░░│ y │ ║ ║ │░░░░░░░░└░─░─░┼───────┘ ║ ║ │░░░░░░░░└░─░─░┼───────┘ ║ ║ z │░░░░░░░░░░░░░░│ ║ ║ z │░░░░░░░░░░░░░░│ ║ ║ └──────────────┘ ║ ║ └──────────────┘ ║ ╚═══════════════════════════════╝ ╚═══════════════════════════════╝ _ ▓▓▓= x.y ███= x.y.z ░░░= x.z ▓▓▓▓= x.y ░░░░= z
!----------------------------------------------------------------------CAPITOLO 5 - Circuiti logici digitali ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di descrivere i principali circuiti elettronici digitali (combinatori e sequenziali) che costituiscono i 'mattoni' con cui viene costruito il calcolatore elettronico. Un circuito elettronico digitale con n ingressi e m uscite puo' essere rappresentato dallo schema seguente: ┌────────────────┐ ───────>│ CIRCUITO │───────> 1 Ingressi 2 ───────>│ LOGICO DIGITALE│───────> 2 ( x ) ...... │ n INGRESSI │....... n ───────>│ m USCITE │───────> m └────────────────┘ 1
y = f(x , x ,... x ) i 1 2 n Esempio: a b c d
Uscite ( y )
(per i = 1...m)
┌─────────────────┐ ───────>│ _ _ │ ───────>│ y = a.b + c + d │───────> y ───────>│ │ ───────>│ │ └─────────────────┘
I circuiti digitali si dividono in combinatori e sequenziali. Un circuito si dice COMBINATORIO se le uscite y sono determinate solo dalla configurazione degli ingressi x. Un circuito si dice SEQUENZIALE se le uscite y sono determinate oltre che dalla configurazione degli ingressi x ad un certo istante, anche dallo stato ........ (vedi Cioffi) Il contenuto di questo capitolo e' tutto sul Cioffi e sulle fotocopie delle trasparenze. Riportiamo qui' soltanto alcuni elementi riguardanti la sintesi di circuiti combinatori e sequenziali con ROM e PLA, la descrizione delle memorie RAM e di quelle associative, che non sono trattate sul libro. Sintesi di circuiti combinatori con ROM --------------------------------------Supponete che vi venga chiesto di realizzare un circuito combinatorio che fornisca un'uscita di 1 o 0 secondo che un evento, proveniente dall'apparato sperimentale e caratterizzato da 16 livelli digitali su 16 linee distinte, sia buono o no (per esempio potrebbero essere i segnali di 16 rivelatori di particelle). Vi viene fornita la tabella della verita' della funzione delle 16 variabili e si vuole che la risposta sia fornita entro 10 ns. Il teorema di sviluppo contiene fino a 65536 prodotti delle 16 variabili di ingresso (mintermini). Chiaramente se la funzione non e' molto minimizzabile il problema non e' semplice. Possiamo pero' trovare una soluzione semplice ed elegante utilizzando un circuito di memoria di 65536x1 bit: basta connettere le 16 linee ai 16 ingressi dell'indirizzo per avere, con il tempo di accesso alla memoria, la risposta voluta, purche' la tabella della verita' sia stata precedentemente caricata nella memoria.
Si ottengono cosi' i seguenti vantaggi: - semplicita' di realizzazione, senza progettazione ad hoc di circuiti complessi; - massima flessibilita': ogni cambiamento successivo nella funzione comporta solo una riscrittura nella memoria della nuova tabella della verita'; - costi minimi, soprattutto se i tempi di accesso richiesti sono compatibili con memorie ROM di basso costo. Se la funzione booleana e' invece molto minimizzabile (cioe' con relativamente pochi mintermini o maxtermini) si puo' sintetizzare direttamente l'espressione ottimale con una PLA (Programmable Logic Array). Si tratta di realizzare i collegamenti necessari sulla matrice degli AND per avere tutti i mintermini da collegare nella matrice degli OR al fine di sintetizzare il teorema di sviluppo:
Ingressi: (n linee)
┌────────────────┐ ───────>│ │ ───────>│ Matrice di AND │ ───────>│ │ └──┬──┬──┬──┬──┬─┘ │ │ │ │ │ <---- (p termini prodotto) ┌──┴──┴──┴──┴──┴─┐ │ │───────> │ Matrice di OR │───────> Uscite (m linee) │ │───────> └────────────────┘
Le PLA sono convenienti, in termini di complessita' circuitale, quando la funzione e' molto minimizzata, cioe' con pochi termini prodotto (p piccolo). La sintesi a ROM e PLA di circuiti sequenziali, usata per la realizzazione delle unita' di controllo microprogrammate, si ottiene inviando tra gli ingressi, come variabili di stato, una parte delle variabili di uscita (vedi Cioffi). Per la descrizione del funzionamento dei circuiti sequenziali e dei loro stati sono particolarmente utili i diagrammi degli stati (vedi macchina sequenziale di Moore sul Cioffi). Esempio di diagramma di un circuito sequenziale con: 3 ingressi I, L e B 3 uscite U, N e O 5 stati possibili: A, C, D, E e G L=0 ┌───────────────────────────┐ │ │ ┌<───╔═══════╗<──┘ I=1 L=1 B=1 ╔═══════╗ └<───╔═══════╗───>┐ I=0 │ ║ A/001 ║──────────────────>║ D/011 ║───────>║ E/010 ║ │L=1 └───>╚═╦═══╦═╝─────────────>─┐ ╚═════╦═╝ ╚═══════╝<───┘ │ │ I=1 L=1 B=0│ │ ┌────>─┘ └───>─┐ │ │B=1 │ │I=1 ╔═┴═════╗ │ │ ╔═══════╗ │L=0 ║ C/000 ║──>┘ └<───║ G/100 ║<──┘ ╚═╦═══╦═╝ ╚═══════╝ │ │B=0 └──>┘ Gli stati corrispondono ai blocchi, all'interno dei quali sono indicate le uscite nell'ordine indicato. Le transizioni, che avvengono agli impulsi di clock, sono determinate dallo stato e dalla condizione delle linee di ingresso, come indicato
sui rami del grafo (dove non indicata la transizione e' incondizionata). Memorie ROM ----------Una memoria ROM (Read Only Memory) si ottiene da una matrice di connessioni come quella nell'esempio seguente, in cui i collegamenti corrispondenti ai bit 0 sono stati interrotti (per esempio bruciandoli con un impulso di corrente oltre il limite di rottura del singolo collegaento). Sono quindi memorie scrivibili una sola volta ma particolarmente economiche. Sono usate per la memorizzazione del firmware, cioe' di quel software, predisposto dal costruttore e scritto in fabbrica, che risulta subito disponibile non appena acceso il dispositivo (per esempio e' su ROM il firmware di avvio del processo di bootstrap di un PC). Le versioni EPROM (Erasable Programmable Read Only Memory) memorizzano invece il contenuto con una carica elettrica su una capacita' che, se si vuole, puo' essere scaricata con una lampada a raggi UV. E' cosi' possibile sulle EPROM ripetere la scrittura sullo stesso chip con un altro contenuto. : : : Address Contenuto ┌──┐ ┌─┤ \─┤ ┌─┤ Address │ ├───────┴─│───────┴─│────────┴─│─────--- 0 ┌──┐ ├──┤ \─┤ ┌─┤ \─┤ │ │ │ ├───────┴─│───────┴─│────────┴─│─────--- 1 ├──┤───>├──┤ ┌─┤ ┌─┤ ┌─┤ │ │ │ ├───────┴─│───────┴─│────────┴─│─────--- 2 └──┘ ├──┤ \─┤ \─┤ ┌─┤ MAR │ ├───────┴─│───────┴─│────────┴─│─────--- 3 └──┘ │ │ │ 2 to 4 1 2 3 decoder <---- Output bits ----->
101 010 111 001
Memorie RAM ----------Una memoria RAM (Random Access Memory) si compone di tante celle elementari contenenti oltre al flip-flop JK necessario per la memorizzazione del contenuto anche di una logica per la selezione della lettura o della scrittura. R/W │ ┌─┴──┐ Schema di una cella elementare: IN ───>│ MC ├───> OUT └─┬──┘ │ R/W Enable ┌───┐ │ ┌<────│NOT├<─────┴──────────┐ │ └───┘ │ ├────>┌───┐ ┌────┐ │ ┌──────────│────>│AND├─────>│J │ │ │ │┌───>└───┘ │ │ └─>┌───┐ │ ┌───┐ └│───>┌───┐ │ Q├───────>│AND├───>OUT IN ───┴──>│NOT├───│───>│AND├─────>│K │ ┌─>└───┘ └───┘ ├───>└───┘ └────┘ │ │ │ └───────────────┬──────────┘ │ Enable
Schema di 4 parole di 3 bit (3x4 celle elementari MC): <────────── OUTPUT (to MD) ──────────> bit 1 bit 2 bit 3 Function ┌──┴─┐ ┌──┴─┐ ┌──┴─┐ R/W │ OR │ │ OR │ │ OR │ │ └┬┬┬┬┘ └┬┬┬┬┘ └┬┬┬┬┘ ├───────┬──││││───────┬───││││───────┬---││││ │ ┌─┴─┐││││ ┌─┴─┐ ││││ ┌─┴─┐ ││││ │ ┌──>│ MC├┘│││ ┌──>│ MC├─┘│││ ┌──>│ MC├─┘│││ Word 0 │ │ └─┬─┘ │││ │ └─┬─┘ │││ │ └─┬─┘ │││ ┌───│─│─────┴───│││─│─────┴────│││─│─────┴─---│││ │ │ │ │││ │ │││ │ │││ │ ├─│─────┬───│││─│─────┬────│││─│─────┬─---│││ ┌──┐ │ │ │ ┌─┴─┐ │││ │ ┌─┴─┐ │││ │ ┌─┴─┐ │││ Address │ ├─┘ │ ├──>│ MC├─┘││ ├──>│ MC├──┘││ ├──>│ MC├──┘││ Word 1 ┌──┐ ├──┤ │ │ └─┬─┘ ││ │ └─┬─┘ ││ │ └─┬─┘ ││ │ │ │ ├─────│─│─────┴────││─│─────┴─────││─│─────┴─--- ││ ├──┤───>├──┤ │ │ ││ │ ││ │ ││ │ │ │ ├───┐ ├─│─────┬────││─│─────┬─────││─│─────┬─--- ││ └──┘ ├──┤ │ │ │ ┌─┴─┐ ││ │ ┌─┴─┐ ││ │ ┌─┴─┐ ││ MAR │ ├─┐ │ │ ├──>│ MC├──┘│ ├──>│ MC├───┘│ ├──>│ MC├───┘│ Word 2 └──┘ │ │ │ │ └─┬─┘ │ │ └─┬─┘ │ │ └─┬─┘ │ 2 to 4 │ └─│─│─────┴─────│─│─────┴──────│─│─────┴─--- │ decoder│ │ │ │ │ │ │ │ │ └─│─────┬─────│─│─────┬──────│─│─────┬─--- │ │ │ I┌─┴─┐O │ │ ┌─┴─┐ │ │ ┌─┴─┐ │ │ ├──>│ MC├───┘ ├──>│ MC├────┘ ├──>│ MC├────┘ Word 3 │ │ └─┬─┘ │ └─┬─┘ │ └─┬─┘ └─────│─────┴───────│─────┴────────│─────┴─---│ E │ │ bit 1 bit 2 bit 3 <────────────── INPUT ──────────────> (from MD) Schema di impiego: l'uso dellla RAM comporta l'impiego dei due registri seguenti: MAR=Memory Address Register e MD=Memory Data. Registri Modalita' di uso: ┌─────┐ ┌─────┐ Lettura: 1) Address to MAR Address │ MAR │─────>│ │ 2) R/W=0, E=1 └─────┘ │ │ ... tempo di lettura... ┌─────┐ │ │ 3) Dato pronto in MD Data │ MD │<────>│ RAM │ └─────┘ │ │ Scrittura: 1) Dato messo in MD Control: R/W ────────────>│ │ 2) Address in MAR lines Enable ────────────>│ │ 3) R/W=1, E=1 └─────┘ ... tempo di scrittura... Memorie associative ------------------Le CAM (Content Addressable Memory) differiscono dalle RAM perche' si referenziano con il contenuto anziche' con l'indirizzo. Cio' significa che si fornisce un contenuto da ricercare nella CAM come dato in input ed i circuiti associati alla memoria CAM ritornano con l'indicazione se il dato e' contenuto in memoria e dov'e'. La ricerca si svolge nei tempi di propagazione dei segnali elettronici nella CAM e quindi viene completata in tempi incomparabilmente piu' brevi di quelli necessari per eseguire una ricerca (sequenziale) su
una RAM. Vedremo in seguito un'applicazione delle CAM nella realizzazione delle memorie virtuali. Nello schema che segue compare anche un registro di Mask che permette di disabilitare alcuni bit limitando la ricerca solo al sottoinsieme dei bit rimanenti. La memoria CAM si compone di tante celle elementari contenenti oltre al flip-flop JK necessario per la memorizzazione del contenuto (come nelle RAM statiche) anche una logica di comparazione (XOR) mascherabile con un bit M. Cominciamo a mostrare lo schema e tabella della verita' di una cella CAM: ┌───┐ D M ───>│NOT├───┐ ┌──────┬──────┬─────┬────────┐ │ └───┘ │ │D=Dato│M=Mask│ Q ║Risposta│ │ ┌───┐ │ ├──────┼──────┼─────║────────┤ ┌────┐ ├────────>│AND├─┐ │ │ 0 │ 0 │ 0 ║ 1 │ │J Q├─│────────>└───┘ │ │ │ 0 │ 0 │ 1 ║ 1 │ │ │ │ ┌───┐ │ └─>┌──┐ │ 0 │ 1 │ 0 ║ 1 │ │ │ └─>│NOT├─>┌───┐ └───>│OR├─>Risposta │ 0 │ 1 │ 1 ║ 0 │ │ _│ └───┘ │AND├─────>└──┘ │ 1 │ 0 │ 0 ║ 1 │ │K Q├──────────>└───┘ │ 1 │ 0 │ 1 ║ 1 │ └────┘ │ 1 │ 1 │ 0 ║ 0 │ _ _ _ │ 1 │ 1 │ 1 ║ 1 │ Risposta = M + D.Q + D.Q └──────┴──────┴─────┴────────┘ La memoria associativa e' composta da tante celle CAM e dai due registri: D per il dato da ricercare e M per la maschera. All'uscita si ha una linea per ogni parola della CAM, con la risposta del risultato del confronto tra parola e D (=dato), mascherati da M (=mask). Per comprenderne il funzionamento presentiamo lo schema di una memoria di 4 parole da 3 bit, con il risultato della ricerca del dato D=001 con la maschera M=110: Data Register Mask Register ┌───┬───┬───┐ ┌───┬───┬───┐ │ 0 │ 0 │ 1 │ │ 1 │ 1 │ 0 │ └─┬─┴─┬─┴─┬─┘ └─┬─┴─┬─┴─┬─┘ │ │ │ ┌───────┘ │ └────────────┐ │ │ └─│───────────│─┐ │ │ └─────│─┐ │ │ │ │ │ │ │ │ │ │ D┌───┐M │ │ D┌───┐M │ │ D┌───┐M │ Word 1 ├─>┤ 1 ├<─┤ ├─>┤ 0 ├<─┤ ├──────>┤ 1 ├<─┤ │ └─┬─┘ │ │ └─┬─┘ │ │ └─┬─┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────│─│──>╔═══╗ └────│──>╔═══╗ 0 │ └────│─│─────────│─│──>║AND║──────│──>║AND║───── │ │ │ │ │ ╚═══╝ │ ╚═══╝ │ │ │ │ │ │ │ D┌───┐M │ │ D┌───┐M │ │ D┌───┐M │ R Word 2 ├─>┤ 0 ├<─┤ ├─>┤ 0 ├<─┤ ├──────>┤ 1 ├<─┤ │ └─┬─┘ │ │ └─┬─┘ │ │ └─┬─┘ │ I │ │ │ │ │ │ │ │ │ │ │ │ │ └────│─│──>╔═══╗ └────│──>╔═══╗ 1 S │ └────│─│─────────│─│──>║AND║──────│──>║AND║───── │ │ │ │ │ ╚═══╝ │ ╚═══╝ U │ │ │ │ │ │ │ D┌───┐M │ │ D┌───┐M │ │ D┌───┐M │ L Word 3 ├─>┤ 0 ├<─┤ ├─>┤ 1 ├<─┤ ├──────>┤ 1 ├<─┤
│ └─┬─┘ │ │ └─┬─┘ │ │ └─┬─┘ │ T │ │ │ │ │ │ │ │ │ │ │ │ │ └────│─│──>╔═══╗ └────│──>╔═══╗ 0 A │ └────│─│─────────│─│──>║AND║──────│──>║AND║───── │ │ │ │ │ ╚═══╝ │ ╚═══╝ T │ │ │ │ │ │ │ D┌───┐M │ │ D┌───┐M │ │ D┌───┐M │ O Word 4 ├─>┤ 1 ├<─┤ ├─>┤ 0 ├<─┤ ├──────>┤ 0 ├<─┤ │ └─┬─┘ │ │ └─┬─┘ │ │ └─┬─┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────│─│──>╔═══╗ └────│──>╔═══╗ 0 │ └────│─│─────────│─│──>║AND║──────│──>║AND║───── │ │ │ │ │ ╚═══╝ │ ╚═══╝ ..................... ecc. ............. Per filtrare i bit in uscita, che possono essere numerosi, si usano appositi circuiti (multiple match resolver), che presentano gli 1 del risultato uno per volta. !----------------------------------------------------------------------CAPITOLO 6 - Architettura di un calcolatore elettronico ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di spiegare come puo' essere costruito un calcolatore elettronico con i circuiti logici digitali e quali sono le istruzioni a cui risponde. Dopo l'intuizione di Von Neumann di collegare l'unita' di calcolo di un elaboratore elettronico ad una memoria di lettura/scrittura nella quale fossero memorizzate anche le istruzioni, comprese quelle di controllo del flusso del programma, si e' arrivati alla moderna concezione di calcolatore elettronico programmabile. Questo e' quindi rappresentabile da due unita': l'unita' di calcolo CPU (Central Processing Unit) e la memoria. Siccome pero' per essere utilizzabile il sistema deve poter comunicare con il mondo esterno (per ricevere problemi e fornire risposte) si deve prevedere anche un'unita' di I/O (Input e Output). ┌─────┐ ┌─────┐ ┌─────────┐ │ I/O │<─────────>│ CPU │<─────────>│ MEMORIA │ └─────┘ └─────┘ └─────────┘ ⌡ 6.1.1 - Bus di comunicazione Il collegamento tra queste unita' deve permettere lo scambio di dati ed il modo piu' conveniente per praticita' e velocita' e' attraverso un 'bus' parallelo di comunicazione. Con questo termine intendiamo un gruppo di linee (che possono essere realizzate con fili o piste su un circuito stampato) a cui sono elettricamente collegate sia l'unita' 'master', quella che richiede di comunicare (per ora una sola, la CPU), e le altre unita' 'slave' su cui il dato, oggetto della comunicazione, deve essere letto o scritto. Questa comunicazione avviene secondo modalita' (protocollo di comunicazione) prefissate dal costruttore ovvero mediante una sequenza di stati iniziata dall'unita' master che presenta, su apposite linee ('Indirizzi'),
l'indirizzo dell'unita' slave con cui intende comunicare; tutte le unita' slave decodificano l'indirizzo presente sul bus e solo quella che riconosce l'indirizzo entro l'intervallo di quelli da essa accettati (che deve essere diverso per ogni unita', in modo da assicurare che sempre un solo slave prenda parte alla transazione sul bus) risponde al master su apposite linee di controllo usate per la sincronizzazione dello scambio del dato (per esempio con modalita' asincrona 'handshake'), che sara' messo su apposite linee ('dati') dall'unita' scrivente e prelevato poi sulle stesse linee dall'unita' leggente. Le linee del bus di comunicazione possono cosi' dividersi in tre categorie: linee di indirizzamento, di dati e di controllo: ┌─────────┐ │ │ BUS DI COMUNICAZIONE │ C P U │═══════╦════════╦═════════════╦══════ │ │ ═════╦│═══════╦│════════════╦│══════ │ (Master)│ ════╦││══════╦││═══════════╦││══════ │ │ │││ │││ │││ └─────────┘ ┌─┴┴┴─┐ ┌─┴┴┴─┐ ┌─┴┴┴─┐ │Slave│ │Slave│ ..... │Slave│ └─────┘ └─────┘ └─────┘ <------- Memorie, I/O ------->
Indirizzi Dati Controllo
⌡ 6.1.2 - Organizzazione interna di un calcolatore elettronico Cominciamo ora lo studio dell'architettura di un calcolatore elettronico iniziando dal primo dei tre blocchi che lo costituiscono: unita' centrale (CPU), sistema di memoria e sistema di I/O (che verrnno studiati nel seguito). Essendo costituito da piu' elementi anch'esso deve disporre di un bus di comunicazione che chiamiamo 'bus interno', attraverso il quale i vari organi interni possono scambiarsi i dati. Lo schema di principio e' il seguente: B U S I N T E R N O ╔══════╦═════╦═════╦═══════╦════════╦══════╗ ┌───┐ ║ │ │ │ │ │ ║ ┌───┐ │ ├───┐ ║ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌──┴─┐ ┌──┴─┐ ║ ┌───┤ M │ │ I │ ED├───╣ │ A │ │ B │ │ T │ │ IR │ │ PC │ ╠───┤ MD│ E │ │ ├───┘ ║ └─┬─┘ └─┬─┘ └─┬─┘ └──┬─┘ └────┘ ║ └───┤ M │ │ / │ ║ └─┐ ┌─┘ │ ┌────┴────┐ ║ │ O │ │ ├───┐ ║ ┌─┴─┴─┐ ┌───┘ │ CONTROL │ ║ ┌───┤ R │ │ O │EAR├───╣ │ MUX │ │ │ UNIT │ ╠───┤MAR│ I │ │ ├───┘ ║ └──┬──┘ │ └────┬────┘ └───┤ A │ └───┘ ║ ┌──┴────┴──┐ ┌─┴─┐ └───┘ ║ │ ALU ├──────┤ F │ ║ └────┬─────┘ └─┬─┘ ╚═══════════╩══════════════╩ <--------> <-------------------------------------------> <----------> Sistema Unita' centrale Sistema di I/O (CPU) memoria Significato dei simboli: A,B,T ALU = MUX = PC = IR =
= Registri operativi (contengono gli operandi) Arithmetic Logic Unit (esegue le operazioni) Multiplexer (seleziona su quale registro opera l'ALU) Program Counter (contiene l'indirizzo della prossima istruzione) Instruction Register (contiene il codice dell'istruzione da eseguire: dopo la decodifica la Control Unit imposta gli organi
F
interni della CPU per eseguire la giusta operazione) = Registro di stato (memorizza informazioni sul risultato dell'ALU; per es. risultato=0, usato dalla Control Unit nei salti condiz.)
MAR,MD,EAR,ED = Registri di accesso al sistema di memoria o di I/O Il funzionamento dell'unita' centrale CPU e' quello tipico di una macchina sequenziale e avviene in tre fasi, ognuna delle quali si sviluppa in piu' intervalli elementari di tempo (periodi di clock): 1) Fase di FETCH (estrae dalla memoria il codice dell'istruzione da eseguire e lo carica nel registro IR); 2) Fase di DECODE (la Control Unit decodifica IR e imposta quindi gli organi interni della CPU per eseguire l'istruzione); 3) Fase di EXECUTE (la CPU esegue l'istruzione secondo la sequenza di passi stabilita dalla Control Unit). Schematicamente la sequenza dei passi elementari eseguiti dalla CPU per ogni istruzione puo' essere descritta nella tabella seguente: ┌───────┬──────┬──────────────────────────┐ │ Fase │Passo │ Operazione eseguita │ ├───────┼──────┼──────────────────────────┤ │ │ 1 │ (PC) ──> MAR │ │ FETCH │ 2 │ Mem(MAR) --> MD │ │ │ 3 │ (MD)──>IR; (PC)+1──>PC │ ├───────┼──────┼──────────────────────────┤ │DECODE │ 4 │ Decodifica istruzione │ ├───────┼──────┼──────────────────────────┤ │EXECUTE│ 5 │ Esegue istruzione │ │ │ 6 │ Torna al passo 1 │ └───────┴──────┴──────────────────────────┘ La durata della fase di execute dipende dall'istruzione che si deve eseguire e puo' essere brevissima, come per una semplice operazione sui registri (per esempio azzeramento dell'accumulatore A), o molto lunga con ripetuti accessi alla memoria (come con gli indirizzamenti assoluti, che vanno letti dalla memoria durante la fase di execute o con i ritorni dalle interruzioni RTI in cui vanno ripristinati tutti i registri). ⌡ 6.1.3 - Il processore H6809 Come spiegato nella prefazione, in questo corso ci interesseremo non alle nozioni sui piu' recenti processori, che ci complicherebbero in maniera non conveniente la comprensione dei concetti fondamentali, ma ci interesseremo invece ad un processore che sia il piu' semplice possibile, pur essendo dotato di tutte le caratteristiche di funzionamento dei moderni processori (CISC). Tratteremo quindi con processori a 8 bit e tra questi sceglieremo il Motorola 6809 che risale alla fine degli anni '70 e che e' succeduto al Motorola 6800 il quale, insieme al Rockwell 6507, quello del famoso primo personal computer Apple II, risale alla meta' degli anni '70 (l'Intel 8080 apri' la strada all'inizio degli anni '70). Non solo, ma per migliorare ulteriormente la comprensione, semplificheremo questo processore togliendo i registriB e T (a 8 bit) che hanno le stesse funzioni dell'accumulatore A, ed i registri Y ed U a 16 bit che hanno le stesse funzioni del registro indice X; inoltre elimineremo anche il registro DP usato per un particolare modo di indirizzamento (Direct Page).
Cosi' il processore reale Motorola 6809 si riduce al processore semplificato (ideale) che chiameremo H6809, che puo' essere rappresentato dallo schema seguente: ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ Registri │ X │ │ SP │ │ PC │ │ EAR │ <─── a 16 bit └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ ╔══════╩═╦════╦═╩═════╦══╩════════╩═══╗ ║ │ │ │ Bus interno ║ ┌───┐ ║ ┌─┴─┐ │ ┌──┴─┐ ║ ┌───┤ M │ ║ │ A │ │ │ IR │ ╠───┤ MD│ E │ ║ └─┬─┘ │ └──┬─┘ ║ └───┤ M │ ║ │ │ ┌───┴───┐ ║ │ O │ ║ │ │ │CONTROL│ ║ ┌───┤ R │ ║ │ │ │ UNIT │ ╠───┤MAR│ I │ ║ │ │ └───┬───┘ ║ └───┤ A │ ║ ┌──┴────┴──┐ ┌─┴─┐ └───┘ ║ │ ALU ├──┤ Z │ ║ └─────┬────┘ └───┘ ╚═══════════╩═══ Dove la lunghezza dei registri e' la seguente: X, SP, PC, EAR, MAR = 16 bit A, IR, MD = 8 bit Z = 1 bit I registri direttamente accessibili con le istruzioni nel processore ipotetico H6809 sono quindi solo i seguenti: MODELLO DI PROGRAMMAZIONE H6809 ═══════════════════════════════ 15 8 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──┘ A ┌──┐ └──┘ Z
PC SP X
Ne consegue una notevole semplificazione nel numero di istruzioni rispetto al Motorola 6809, il cui modello di programmazione era piu' complesso (e quindi le istruzioni piu' numerose): MODELLO DI PROGRAMMAZIONE Motorola 6809 ═══════════════════════════════════════ 15 8 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘
PC SP X
┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ (Y) ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ (U) ┌──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──┘ (DP) ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ A(,B) A B ┌──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──┘ (CC) E F H I N Z V C ╔══════════════════════════════════════════════════════════════════ ╗ ║ ║ ║ Istruzioni macchina del processore ipotetico H6809 ║ ║ ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ Code Operand Lungh. Opcode Descrizione ║ ║ Mnem. (bytes) (Hex) ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ NOP 1 12 No operation O ║ ║ CLRA 1 4F Clear A P ║ ║ COMA 1 43 Complementa A E ║ ║ NEGA 1 40 Nega A (=complemento a 2) R ║ ║ LDA #dato 2 86 Load A con dato A ║ ║ LDA @X 1 A6 Load A con Mem(X) Z ║ ║ LDA addr 3 B6 Load A con Mem(addr) I ║ ║ STA @X 1 A7 Store A in Mem(X) O ║ ║ STA addr 3 B7 Store A in Mem(addr) N ║ ║ ADDA #dato 2 8B Add dato ad A I ║ ║ ADDA addr 3 BB Add Mem(addr) ad A ║ ║ ANDA #dato 2 84 And dato ed A S ║ ║ ANDA addr 3 B4 And Mem(addr) ed A U ║ ║ CMPA #dato 2 81 Set Z secondo A-dato ║ ║ CMPA addr 3 B1 Set Z secondo A-Mem(addr) A ║ ║────────────────────────────────────────────────────────────────── ║ ║ LDX #addr 3 8E Load X con addr O ║ ║ LDX addr 3 BE Load X con MemW(addr) P ║ ║ STX addr 3 BF Store X in MemW(addr) E ║ ║ CMPX #addr 3 8C Set Z secondo X-addr R ║ ║ CMPX addr 3 BC Set Z secondo X-MemW(addr) . ║ ║ ADDX #addr 3 30 Add addr ad X S ║ ║ ADDX addr 3 31 Add MemW(addr) ad X U ║ ║ LDS #addr 3 8F Load SP con addr X,S ║ ║────────────────────────────────────────────────────────────────── ║ ║ BNE offset 2 26 Branch Not Equal zero (Z=0) C ║ ║ BEQ offset 2 27 Branch EQual zero (Z=1) O ║ ║ BRA offset 2 20 Branch incondizionato N ║ ║ JMP addr 3 7E Jump to addr T ║
║ JSR ║ RTS ║
addr
3 1
BD 39
Jump to subroutine at addr Return from subroutine
R O L
║ ║ ║
╚══════════════════════════════════════════════════════════════════ ╝ I modi di indirizzamento qui previsti sono: 1) Implicito: l'indirizzo dell'operando e' implicito nell'opcode (NOP,CLRA,COMA,NEGA) 2) Immediato: l'operando e' memorizzato nell'istruzione stessa (LDA#,STA#,ADDA#,ANDA#,CMPA#,LDX#,CMPX#,ADDX#,LDS#) 3) Assoluto : l'indirizzo dell'operando e' memorizzato nell'istruzione (LDA,STA,ADDA,ANDA,CMPA,LDX,STX,CMPX,ADDX,JMP,JSR) 4) Indiretto (a registro): l'indirizzo dell'operando e' memorizzato in X (LDA@X,STA@X) 5) Relativo: l'indirizzo dell'operando si ricava sommando al PC l'offset memorizzato nell'istruzione (BNE,BEQ,BRA) Gli altri modi di indirizzamento possibili saranno discussi piu' avanti. Vediamo un semplice esempio di uso delle istruzioni H6809, in un programma che azzera 5 locazioni di memoria: ╔══════════════════════════════════════════════════════════════════ ╗ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ ORG 3000H Origine a 3000H ║ ║ 3000 4F INIT CLRA ║ ║ 3001 B7 31 00 STA Q ║ ║ 3004 B7 31 01 STA Q+1 ║ ║ 3007 B7 31 02 STA Q+2 ║ ║ 300A B7 31 03 STA Q+3 ║ ║ 300D B7 31 04 STA Q+4 ║ ║ 3010 7E 10 00 JMP 1000H Torna al sistema op. ║ ║ 3013 ORG 3100H ║ ║ 3100 ?? Q RMB 5 Riserva 5 bytes ║ ║ 31005 END INIT ║ ╚══════════════════════════════════════════════════════════════════ ╝ Questo metodo non sarebbe conveniente se dovessimo azzerare molti bytes (anziche' solo 5), perche' richiederebbe molte istruzioni STA. In questi casi si usa l'indirizzamento indiretto con l'istruzione STA @X, come nell'esempio che segue: ╔══════════════════════════════════════════════════════════════════ ╗ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ ORG 3000H Origine a 3000H ║
║ ║ ║ ║ ║ ║ ║ ║ ║ ║
3000 3001 3004 3005 3008 300B 300D 3010 3100 3105
4F 8E A7 30 8C 26 7E ??
INIT 31 00 00 01 31 05 F7 10 00
CLRA LDX LOOP ┌─>STA │ ADDX │ CMPX └──BNE JMP ORG Q RMB END
#Q @X #1 #Q+5 LOOP 1000H 3100H 5 INIT
Inizializza X Azzera Mem(X) Raggiunta la fine ? Torna al sistema op. Riserva 5 bytes
║ ║ ║ ║ ║ ║ ║ ║ ║ ║
╚══════════════════════════════════════════════════════════════════ ╝ In questi esempi abbiamo incontrato anche dei simboli come ORG,RMB, END che non sono istruzioni di macchina, ma sono delle direttive per il programma compilatore (si chiamano 'Pseudo-istruzioni'), che deve ricavare il linguaggio macchina (binario) a partire dal linguaggio mnemonico Assembly. In questo lavoro di traduzione il compilatore ha bisogno di conoscere gli indirizzi assoluti da usare: per questo scopo ORG stabilisce a 3000H (H sta per Hexdecimal) l'inizio del programma e a 3100H l'inizio dell'array Q; RMB (Reserve Memory Bytes) riserva 5 bytes di memoria per Q; END segnala la fine dell'input da compilare e l'entry point (prima istruzione del programma da eseguire). Questi argomenti saranno trattati piu' estesamente nel capitolo riguardante il linguaggio Assembly. Per consentire l'annidamento di chiamate multiple a subroutines (cioe' la chiamata ad un'altra subroutine fatta dall'interno di una subroutine), la macchina viene dotata di un apposito registro SP (Stack Pointer) per la gestione automatica in uno stack degli indirizzi di ritorno delle subroutines. L'esempio seguente ne mostra il funzionamento: ╔══════════════════════════════════════════════════════════════════ ══╗ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ══╣ ║ ORG 3000H Origine a 3000H ║ ║ 3000 ........ SUBR2 ┌─... ║ ║ .... ........ │ ... ║ ║ 3055 39 └─RTS ║ ║ 3056 ........ SUBR1 ┌─... ║ ║ .... ........ │ ... ║ ║ 30A2 BD 30 00 │ JSR SUBR2 <─── Chiamata annidata ║ ║ 30A5 ........ RET2 │ ... ║ ║ .... .. .. .. │ ... ║ ║ 30C2 39 └─RTS ║ ║ 30C5 8F 3F F8 MAIN ┌─LDS #STK+8 Inizializza stack ║ ║ .... .. .. .. │ ... ║ ║ 312F BD 30 56 │ JSR SUBR1 ║ ║ 3132 .. .. .. RET1 │ ... ║ ║ .... .. .. .. │ ... ║ ║ 31A7 7E 10 00 └─JMP 1000H Va al sistema operativo ║ ║ .... .. .. .. ORG 3FF0H ║ ║ 3FF0 .. .. .. STK RMB 8 <─── Stack ║ ║ .... .. .. .. END MAIN ║
╚══════════════════════════════════════════════════════════════════ ══╝ Ad ogni JSR l'indirizzo di ritorno viene memorizzato automaticamente nelle locazioni (SP) e (SP)+1 (con due PUSH) e ad ogni RTS viene recuperato (con due POP) e messo nel PC per il salto all'indirizzo di ritorno della subroutine: .... .... .... S ├──────┤ ├──────┤ ├──────┤ T │ │ │ │ │ │ A ├──────┤ ├──────┤ ├──────┤ C │ │ │ │ SP───> │Return│ K ├──────┤ ├──────┤ ├ ─ ─ ─┤ │ │ │ │ │Addr2 │ A ├──────┤ ├──────┤ ├──────┤ R │ │ SP───> │Return│ │Return│ E ├──────┤ ├ ─ ─ ─┤ ├─ ─ ─ ┤ A │ │ │Addr1 │ │Addr1 │ └══════┘ └══════┘ └══════┘ SP───> Stack vuoto Dopo JSR SUBR1 Dopo JSR SUBR2 ⌡ 6.2 - Modi di indirizzamento Numerosi sono i modi di indirizzamento usati dai vari processori. Elenchiamo di seguito in sintesi quelli piu' diffusi. ⌡ 6.2.1 - Indirizzamento a registro (position independent code): 7 0 Registri ┌──┬──┬──┬──┬──┬──┬──┬──┐ ┌════════┐ │ O p c o d e │ R│ │ │ 0 └──┴──┴──┴──┴──┴──┴──┴──┘ ├────────┤ ║ . . .. ║ ├────────┤ ╚═══════>│Operando│ R ├────────┤ Notazione assembly: OP reg . . ├────────┤ │ │ n └════════┘ ⌡ 6.2.2 - Indirizzamento assoluto (position dependent code): 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ O p c o d e │ . Memoria. ├──┼──┼──┼──┼──┼──┼──┼──┤ . . │ │ ├────────┤ ├── A d d r e s s ──┤ │ │ │ │ ├────────┤ └──┴──┴──┴──┴──┴──┴──┴──┘ │ │ ║ ├────────┤ ╚═════════════════>│Operando│ Address ├────────┤ Notazione assembly: OP addr │ │ ├────────┤ . .
⌡ 6.2.3 - Indirizzamento a pagina zero (position dependent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ O p c o d e │ Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤ │ A d d r e s s │ . Memoria. └─┴─┴─┴─┴─┴─┴─┴─┘ . . ├────────┤ Calcolo indirizzo effettivo: │ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ ├────────┤ │ 0 ││ A d d r e s s │════> │Operando│ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ ├────────┤ │ │ Notazione assembly: OP addr ├────────┤ . . ⌡ 6.2.4 - Indirizzamento indiretto (position dependent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ . Memoria. │ O p c o d e │ . . ├─┼─┼─┼─┼─┼─┼─┼─┤ ├─────────┤ Formato istruzione: │ │ │Indirizzo│ Address ├ A d d r e s s ┤════> ├─ ─┤ ═══╗ │ │ │indiretto│ ║ └─┴─┴─┴─┴─┴─┴─┴─┘ ├─────────┤ ║ . . ║ . . ║ Notazione assembly: OP @addr ├─────────┤ ║ │ Operando│<═══╝ ├─────────┤ │ │ ├─────────┤ . . Alcuni processori (es. Data General) hanno anche istruzioni che permettono indirizzamenti indiretti multipli: con MSB=1 (MSB=Most Significant Bit) nell'operando questo viene interpretato come nuovo indirizzo indiretto a cui leggere l'operando e se questo ha anch'esso MSB=1 il processo indiretto continua. ⌡ 6.2.5 - Indirizzamento immediato (position independent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ Formato istruzione: │ Opcode│Operand│ (SHORT) └─┴─┴─┴─┴─┴─┴─┴─┘ ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ O p c o d e │ Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤ (NORMAL) │ O p e r a n d │ └─┴─┴─┴─┴─┴─┴─┴─┘ ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ O p c o d e │ ├─┼─┼─┼─┼─┼─┼─┼─┤ Formato istruzione: │ │ (DOUBLE) ├ O p e r a n d ┤ │ │ └─┴─┴─┴─┴─┴─┴─┴─┘ Notazione assembly:
OP #dato
⌡ 6.2.6 - Indirizzamento indiretto a registro (position independent code): . Registri. . . ┌─┬─┬─┬─┬─┬─┬─┬─┐ ├─────────┤ Formato istruzione: │ O p c o d e │R│════> │ Address │══╗ R └─┴─┴─┴─┴─┴─┴─┴─┘ ├─────────┤ ║ │ │ ║ ├─────────┤ ║ ║ . Memory . ║ Notazione assembly: OP @reg ├─────────┤ ║ OP (reg) │ Operando│<════╝ Address ├─────────┤ │ │ ├─────────┤ . . ⌡ 6.2.7 - Indirizzamento ad autoincremento (position independent code): .Registri. . . ┌─┬─┬─┬─┬─┬─┬─┬─┐ ├────────┤ R Formato istruzione: │ O p c o d e │R│═══>│ Address│══╦<────┐ └─┴─┴─┴─┴─┴─┴─┴─┘ ├────────┤ ║ │ │ │ ║ │ ├────────┤ ║ ╔═══╗ ║──>║ + ║ . Memory . ║ ╚═══╝ Notazione assembly: OP (reg)+ ├────────┤ ║ │Operando│<══════╝ Address ├────────┤ (prima di increm.) STA @X ═╗ │ │ ADDX #1 ║ ───> STA (X)+ ├────────┤ .... ═╝ . . ⌡ 6.2.8 - Indirizzamento ad autodecremento (position independent code): .Registri. . . ┌─┬─┬─┬─┬─┬─┬─┬─┐ ├────────┤ R Formato istruzione: │ O p c o d e │R│═══>│ Address│════╦<───┐ └─┴─┴─┴─┴─┴─┴─┴─┘ ├────────┤ ║ │ │ │ ║ │ ├────────┤ ╔═══╗ │ ║ - ║──┘ . Memory . ╚═══╝ Notazione assembly: OP -(reg) ├────────┤ ║ │Operando│<════════╝ Address-1 ├────────┤ (dopo decremento) STA @X ═╗ │ │ ADDX #-1║ ───> STA -(X) ├────────┤ .... ═╝ . . Alcuni processori hanno anche istruzioni ad autoincremento ed autodecremento indiretto, al secondo livello (es. M6809 e PDP11). ⌡ 6.2.9 - Indirizzamento indexato (position dependent code):
.Registri. ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │ │ O p c o d e │R│ ├────────┤ R Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤═══>│ Offset │═══>╗ │ Base Address │ ├────────┤ ║ └─┴─┴─┴─┴─┴─┴─┴─┘ . . ║ ║ ╔═══╗ ╚═══════════════════════>║ + ║ ╚═══╝ . Memoria. ║ Notazione assembly: OP reg ├────────┤ ║ │Operando│<════════╝ Address-1 Esempio M6809: ├────────┤ (dopo decremento) LDX J . . LDA DATI-1(X) <─── legge DATI(J) . . ........ ⌡ 6.2.10 - Indirizzamento con base (position independent code): . Registri. ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │ │ O p c o d e │R│ ├─────────┤ R Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤═══>│Base Addr│═══>╗ │ O f f s e t │ ├─────────┤ ║ └─┴─┴─┴─┴─┴─┴─┴─┘ . . ║ ║ ╔═══╗ ╚════════════════════════>║ + ║ ╚═══╝ . Memoria. ║ Notazione assembly: OP reg ├────────┤ ║ │Operando│<════════╝ Address ├────────┤ (=Base Addr+Offset) . . Se 'offset' e 'base address' hanno lunghezza uguale questo modo di indirizzamento e' indistinguibile da quello indexato. Nei casi in cui invece 'offset' e 'base address' hanno lunghezza diversa, prima della somma l'offset viene esteso come numero di bit tenendo conto del segno (il bit di segno pesa come -2^N: vedasi la spiegazione della rappresentazione dei negativi in complemento a 2). ⌡ 6.2.11 - Indirizzamento indexato con base (position independent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ . Registri. │ O p c o d e │ │ │ Formato istruzione: ├─┴─┴─┼─┼─┴─┴─┼─┤ ├─────────┤ R │ │B│ │R│ ╔═>│ Offset │═════>╗ └─┴─┴─┴┬┴─┴─┴─┴┬┘ ║ ├─────────┤ ║ ║ ║ ║ . . ║ ║ ╚══╝ ├─────────┤ B ╔═══╗ ╚════════════>│Base Addr│═══>║ + ║ ├─────────┤ ╚═══╝ . . ║ ║ . Memoria. ║ Notazione assembly: OP reg1,reg2 ├────────┤ ║ │Operando│<═══════════╝ Address
├────────┤ (=Base Addr+Offset) . . Nel processore M6809 l'offset viene preso dall'accumulatore A o B di 8 bit (portato a 16 bit con la giusta estensione di segno, cioe' con il byte piu' significativo messo a -1, con tutti 1, se l'offset e' negativo; messo a +1, con tutti 0, se positivo), mentre la base viene presa dal registro X o Y (di 16 bit). ⌡ 6.2.11 - Indirizzamento relativo (position independent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ O p c o d e │ Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤ . . │ O f f s e t │ . Memoria. └─┴─┴─┴┬┴─┴─┴─┴─┘ │ │ ║ ├────────┤ ║ │ │ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ ╚══>╔═══╗ ├────────┤ │ P r o g r a m C o u n t e r│═════>║ + ║════>│Operando│ Address └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ ╚═══╝ ├────────┤(=PC+Offset) │ │ Notazione assembly:
OP offset
Nei casi in cui invece 'offset' e PC hanno lunghezza diversa, prima della somma l'offset viene esteso come numero di bit tenendo conto del segno (in complemento a 2 il bit di segno pesa come -2^N, per cui un offset di 8 bit viene portato a 16 bit eseguendo l'estensione di segno con il byte piu' significativo messo a -1, con tutti 1, se l'offset e' negativo; messo a +1, con tutti 0, se positivo). L'uso di un offset di soli 8 bit permette istruzioni piu' corte e quindi anche piu' veloci (si riducono i cicli di lettura della memoria). ⌡ 6.2.12 - Indirizzamento a pagine (position independent code): ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ O p c o d e │ Formato istruzione: ├─┴─┴─┴─┴─┴─┴─┴─┤ │ A d d r e s s │ . Memoria. └─┴─┴─┴─┴─┴─┴─┴─┘ . . ├────────┤ Calcolo indirizzo effettivo: │ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ ├────────┤ │ P a g e ││ A d d r e s s │════> │Operando│ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ ├────────┤ │ │ Notazione assembly: OP addr ├────────┤ . . Citiamo tre tipi di indirizzamento a pagine, secondo cosa viene caricato nel MSB (Most Significant Byte) chiamato 'Page': 1) 'Page'= 0 : coincide con l'indirizzamento a pagina zero gia' visto in cui 'Page'=0 (Base Page addressing); 2) 'Page'= MSB di PC : il MSB del Program Counter viene caricato in 'Page' (Current Page addressing); e' diverso dall'indi-
rizzamento relativo in quanto non si somma PC+Address; 3) 'Page'= DP : il contenuto di un apposito registro DP (Direct Page) viene caricato in 'Page' (Page Register addressing); Questi modi di indirizzamento consentono l'accesso alla memoria a pagine: per esempio se 'Address' e' di 8 bit si accede a 256 locazioni della stessa pagina, fissata da 'Page'. Inoltre consentendo l'uso di istruzioni piu' corte godono degli stessi vantaggi descritti per lo indirizzamento relativo (programmi piu' corti e veloci). ⌡ 6.2.13 - Indipendenza dalla locazione di caricamento del programma Per ogni modo di indirizzamento abbiamo segnalato se le istruzioni che lo usano producono programmi indipendenti dalla locazione di caricamento. Questa indipendenza e' una caratteristica importante sia per programmi 'firmware' scritti su ROM, suscettibili di caricamento in indirizzi diversi (si parla allora di indipendenza 'statica' dalla locazione di caricamento, che nei casi in cui, essendo il processore impegnato contemporaneamente in piu' compiti, dobbiamo poter caricare il nostro programma in una area di memoria qualsiasi (quella libera al momento). Si parla in questi casi di indipendenza 'dinamica' dalla locazione di caricamento. In tali applicazioni scriveremo il programma usando solo istruzioni che usano modi di indirizzamento del tipo 'position independent code'. In altre parole bisogna: 1) non usare alcun indirizzo assoluto che referenzi locazioni del programma (possono essere usati per referenze assolute esterne, come nei richiami alle utilities del sistema operativo); 2) usare gli indirizzamenti di tipo 'con base' a registro (indipendenza statica) e 'relativo' a PC (indipendenza dinamica); 3) l'indirizzamento immediato e' usabile solo per le costanti, non per gli indirizzi che non siano relativi. L'indipendenza dinamica ottenuta con la gestione della memoria resa possibile dalla unita' MMU (Memory Management Unit), che sara' introdotta nel capitolo sull'I/O, e' necessaria nei sistemi multiprogrammati, in cui uno stesso programma puo' essere caricato in parti diverse della memoria fisica, ogni volta che viene eseguito. ⌡ 6.3 - Elenco delle istruzioni di macchina Le istruzioni sono i comandi con cui pilotare il funzionamento del calcolatore elettronico. E' di fondamentale importanza quindi conoscerle tutte per sapere cosa il nostro processore puo' fare. La loro descrizione puo' essere abbastanza diversa da computer a computer, per cui l'apprendimento pratico va fatto con esperienza diretta sulla macchina con cui si lavora. In una descrizione generale possiamo solo passare quindi in rassegna l'insieme delle istruzioni di cui dispongono in genere i vari calcolatori, dividendole per tipologie omogenee. I codici mnemonici possono ovviamente essere diversi per i vari linguaggi assemblatori (con S,D indichiamo i registri o memorie Sorgente o Destinazione). ⌡ 6.3.1 - Istruzioni di trasferimento dati:
S ══> D
MOV D,S LD D,S ST D,S ecc...
oppure
MOVE D,S LDA S STB D ecc....
con
⌡ 6.3.2 - Istruzioni aritmetiche: Somma D+S ══> D Sottrae D-S ══> D Sottrae D-S, set PSW Incrementa D+1 ══> Decrementa D-1 ══> Azzera 0 ══> Complementa -D-1 ══> Nega -D ══>
ADD D,S SUB D,S CMP D,S INC D DEC D CLR D COM D NEG D ecc...
S=Source; D=Destinazione ( registri o memorie )
X op S ══> D
(Processor Status Word), non salva in D D D D D D
Al termine dell'operazione viene memorizzato in un'apposito registro PSW la condizione risultante, utile per eseguire successivamente dei salti condizionati: ┌─┬─┬─┬─┬─┬─┬─┬─┐ Processor Status Word: │E F H I N Z V C│ (esempio del M6809) └─┴─┴─┴─┴─┴─┴─┴─┘ N Z V C I H F E
= = = = = = = =
Negativo (= MSB del risultato = segno) Zero (=1 se il risultato e' zero) Overflow (=1 se il risultato e' fuori del range permesso) Carry (= riporto aritmetico) Interrupt mask bit Half Carry Fast interrupt mask bit Entire state on stack
⌡ 6.3.3 - Istruzioni logiche: AND D,S OR D,S XOR D,S COM D BIT D ecc...
AND logico OR logico eXclusive OR Complementa Bit test
(bit (bit (bit (bit
X op S ══> D a a a a
bit) bit) bit) bit)
D+S D+S D+S D+S
══> ══> ══> ══>
D D D D
Tra le istruzioni logighe vanno annoverate anche quelle che permettono la manipolazione dei singoli bit di un registro (o memoria) D: BCLR D,b BSET D,b BCHG D,b BTST D,b ecc...
Bit Bit Bit Bit
clear (b=bit number) set change test
⌡ 6.3.4 - Istruzioni di scorrimento e rotazione:
RL
D
Rotate Left
┌───────────────────────┐ └<──┌─┬─┬─┬─┬─┬─┬─┬─┐<──┘ 7└─┴─┴─┴─┴─┴─┴─┴─┘0 ┌───────────────────────┐ └>──┌─┬─┬─┬─┬─┬─┬─┬─┐──>┘
RR
D
Rotate Right
RRC D
7└─┴─┴─┴─┴─┴─┴─┴─┘0 ┌─────────────────────────────┐ └<──┌─┐<──┌─┬─┬─┬─┬─┬─┬─┬─┐<──┘ Rotate Left with Carry C└─┘ 7└─┴─┴─┴─┴─┴─┴─┴─┘0 ┌─────────────────────────────┐ └──>┌─┐──>┌─┬─┬─┬─┬─┬─┬─┬─┐──>┘ Rotate Right with Carry C└─┘ 7└─┴─┴─┴─┴─┴─┴─┴─┘0
SLL D
perso<──┌─┐<──┌─┬─┬─┬─┬─┬─┬─┬─┐<── 0 Shift Left Logical C└─┘ 7└─┴─┴─┴─┴─┴─┴─┴─┘0
SRL D
0 ──>┌─┬─┬─┬─┬─┬─┬─┬─┐───>┌─┐───>perso Shift Right Logical 7└─┴─┴─┴─┴─┴─┴─┴─┘0 C└─┘
RLC D
perso<──┌─┐<──┌─┬─┬─┬─┬─┬─┬─┬─┐<── 0 Shift Left Arithmetic C└─┘ 7└─┴─┴─┴─┴─┴─┴─┴─┘0 ┌──>┐ └<─┌┴┬─┬─┬─┬─┬─┬─┬─┐───>┌─┐───>perso Shift Right Arithmetic 7└─┴─┴─┴─┴─┴─┴─┴─┘0 C└─┘ (segno)
SLA D
SRA D
⌡ 6.3.5 - Istruzioni di salto incondizionato: JMP
D
CALL D RET
Salta all'indirizzo D Salta alla subroutine all'indirizzo D e PUSH PC nello stack
POP indirizzo dallo stack e lo mette in PC (salta alla locazione immediatamente seguente al CALL precedente)
Tra i salti andranno annoverate anche le interruzioni sia hardware (trap) che software (system call), che studieremo nel capitolo che tratta il sistema di I/O, poiche' anch'esse alterano il contenuto del Program Counter PC. ⌡ 6.3.6 - Istruzioni di salto condizionato: Vi sono poi le istruzioni di salto condizionato (cioe' quelle in cui il salto e' eseguito se risulta vera una data condizione). Tipici esempi di salto condizionato sono nella tabella seguente (a scopo esemplificativo, nella condizione usiamo i simboli gia' usati per l'M6809 al ⌡ 6.3.2): N Z V C
= = = =
Negativo Zero Overflow Carry
(= MSB del risultato = segno) (=1 se il risultato e' zero) (=1 se il risultato e' fuori del range permesso) (= riporto aritmetico)
╔══════════════════════════════════════════════════════════════════ ╗ ║ Tipo Codice Mnem. Salta se... Condizione ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ Singolo bit ║ ║ BRA offset sempre vera ║ ║ BCS offset Carry Set C = 1 ║
║ ║ ║ ║ ║ ║ ║ ║ Con segno ║ ║ ║ ║ ║ Senza segno ║ ║ ║ ║
BCC BVS BVC BMI BPL BEQ BNE
offset offset offset offset offset offset offset
Carry Clear Overflow set Overflow clear Minus Plus Equal to zero Not Equal to zero
BLT BGE BLE BGT
offset offset offset offset
Less Then zero N Greater or Equal N Less or Equal zero (N Greater Than zero (N
BLO BHS BLS BHI
offset offset offset offset
Lower Higher or the Same Lower or the Same Higher
║ ║ ║ ║ ║ ║ ║ ║ XOR V = 1 ║ XOR V = 0 ║ XOR V) OR Z = 1 ║ XOR V) OR Z = 0 ║ ║ C = 1 ║ C = 0 ║ C OR Z = 1 ║ C OR Z = 0 ║ C V V N N Z Z
= = = = = = =
0 1 0 1 0 1 0
╚══════════════════════════════════════════════════════════════════ ╝ 15...12 11...8 7....0 ┌───────┬──────┬───────┐ Formato istruzione di salto condizionato: │ Opcode│ Cond.│ Offset│ └───────┴──────┴───────┘ Queste istruzioni sono spesso usate per eseguire dei loop, come nell'esempio seguente: LOOP ┌──> ... REPEAT │ ... .... │ DEC CNT cnt := cnt - 1; └─── BNE LOOP UNTIL cnt = 0; Altre istruzioni di salto condizionato sono: ISZ
D
Incrementa D e salta l'istruzione seguente se D=0
DSZ
D
Decrementa D e salta l'istruzione seguente se D=0
Esse hanno la caratteristica di eseguire test e modifica del contenuto di una memoria D in un'unica istruzione. Potendo questa essere realizzata come un'unica operazione indivisibile (anche a livello di singolo ciclo di accesso alla memoria: vedremo in seguito infatti i cicli di 'read-modify-write' che supportano operazioni di questo tipo), queste istruzioni risulteranno utili per l'implementazione delle cosiddette primitive di sincronizzazione dei processi in ambiente multiprocessor. ⌡ 6.3.7 - Istruzioni particolari: Apposite istruzioni sono previste per la moltiplicazione e divisione: MUL
D,S
Esegue la moltiplicazione n n+1
DIV
D,S
Esegue la divisione n+1 n D := D , n n
D ,D n
:= D * S
D := D , D n+1 D MOD S n+1
DIV
S
Un'altra istruzione DAA (Decimal Adjust Accumulator) e' prevista per l'aritmetica decimale: LDA P Load P in A ADDA Q Add Q to A DAA Decimal Adjust sum byte in A L'aritmetica decimale si esegue elaborando una coppia di cifre decimali ogni byte. Per esempio l'addizione di cui sopra avviene secondo lo schema seguente: 7 ..... 4 3 ..... 0 ┌────────┬─────────┐ P │ PH │ PL │ ( 2 cifre decimali ) └────────┴─────────┘ ┌────────┬─────────┐ Q │ QH │ QL │ ( altre 2 cifre decimali ) └────────┴─────────┘ C <─┐ ┌<──┐DAA ┌─┴────┴─┬─┴───────┐ A=P+Q │ AH │ AL │ ( addizione decimale in A ) └────────┴─────────┘ ⌡ 6.4 - Classificazione dei processori Vari possono essere i criteri di classificazione dei processori. Scegliamo qui un criterio che privilegi il modo di programmazione e quindi il numero dei registri direttamente accessibili con le istruzioni di macchina. In base a questo criterio possiamo distinguere tre categorie: 1) Processori ad accumulatori (1/2 registri) 2) Processori a registri generali (8/16 registri) 3) Processori a stack (0 registri) ⌡ 6.4.1 - Macchine ad accumulatori Un esempio di macchina ad accumulatori e' l' H6809, gia' descritto in precedenza, che e' rappresentato sinteticamente dallo schema seguente: ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ Registri │ X │ │ SP │ │ PC │ │ EAR │ <─── a 16 bit └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ ╔══════╩═╦════╦═╩═════╦══╩════════╩═══╗ ║ │ │ │ Bus interno ║ ┌───┐ ║ ┌─┴─┐ │ ┌──┴─┐ ║ ┌───┤ M │ ║ │ A │ │ │ IR │ ╠───┤ MD│ E │ ║ └─┬─┘ │ └──┬─┘ ║ └───┤ M │ ║ │ │ ┌───┴───┐ ║ │ O │ ║ │ │ │CONTROL│ ║ ┌───┤ R │ ║ │ │ │ UNIT │ ╠───┤MAR│ I │ ║ │ │ └───┬───┘ ║ └───┤ A │ ║ ┌──┴────┴──┐ ┌─┴─┐ └───┘ ║ │ ALU ├──┤ Z │ ║ └─────┬────┘ └───┘ ╚═══════════╩═══
MODELLO DI PROGRAMMAZIONE H6809 ═══════════════════════════════ 15 8 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──┘ A ┌──┐ └──┘ Z
PC SP X
⌡ 6.4.2 - Macchine a registri generali Una macchina a registri generali e' invece caratterizzata da un processore dotato di un maggior numero di registri elettronici direttamente accessibili alle istruzioni (e quindi presenti nel modello di programmazione). Questo comporta una maggiore complessita' costruttiva e quindi maggiori costi, rispetto ad una macchina ad accumulatori, ma permette al programmatore di mantenere memorizzate nei registri le informazioni di uso piu' frequente, come i risultati parziali dei calcoli, senza doverle salvare in memoria con istruzioni 'Store' (e poi riprenderle con istruzioni 'Load'). Cosi' si ottiene un duplice vantaggio: oltre a ridurre l'occupazione di memoria del programma, se ne velocizza anche l'esecuzione (meno istruzioni da eseguire). Un esempio di macchina a registri generali e' l'H8000, che otteniamo semplificando lo Zilog 8000, in maniera analoga con quanto abbiamo fatto creando l'H6809 a partire dal Motorola 6809. La macchina ipotetica che cosi' si ottiene, con il relativo modello di programmazione, puo' essere rappresentata sinteticamente dallo schema seguente: ┌─────┐ ┌─────┐ Registri │ PC │ │ EAR │ <─── a 16 bit └──┬──┘ └──┬──┘ ╔══════╦══════╦═══════╦══╩════════╩═══╗ ║ │ │ │ Bus interno ║ ┌───┐ ║ │ │ ┌──┴─┐ ║ ┌───┤ M │ ║ ┌───┴────┐ │ │ IR │16 bit ╠───┤ MD│ E │ ║ │Registri│ │ └──┬─┘ ║ └───┤ M │ ║ │generali│ │ ┌───┴───┐ ║ │ O │ ║ │ 16x16 │ │ │CONTROL│ ║ ┌───┤ R │ ║ └─────┬──┘ │ │ UNIT │ ╠───┤MAR│ I │ ║ │ │ └───┬───┘ ║ └───┤ A │ ║ ┌──┴────┴──┐ ┌─┴─┐ └───┘ ║ │ ALU ├──┤ Z │1 bit ║ └─────┬────┘ └───┘ ╚═══════════╩════ MODELLO DI PROGRAMMAZIONE H8000 ═══════════════════════════════ 15 8 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ ┌───────────────────────┬───────────────────────┐ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ └───────────────────────┴───────────────────────┘ ┌──┐ └──┘ Z
PC R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 (SP)
Le istruzioni sono composte da una o due parole (Word). La prima puo' avere uno dei seguenti sei formati (la seconda, se c'e', contiene l'operando dell'indirizzamento immediato o un indirizzo assoluto): Formato 1: (LD,ST,...)
15 14 13 9 8 7 4 3 0 ┌─────┬──────────────┬──┬───────────┬───────────┐ └─────┴──────────────┴──┴───────────┴───────────┘ Mode Opcode W/B Reg1 Reg2
15 14 13 9 8 7 4 3 0 Formato 2: ┌─────┬──────────────┬──┬───────────┬───────────┐ (CLR,NEG,...) └─────┴──────────────┴──┴───────────┴───────────┘ Mode Opcode W/B Reg1 AuxOp 15 14 13 8 7 4 3 0 Formato 3: ┌─────┬─────────────────┬───────────┬───────────┐ (JP,CALL,...) └─────┴─────────────────┴───────────┴───────────┘ Mode Opcode Reg1 Reg2 15
12 11 8 7 0 ┌───────────┬───────────┬───────────────────────┐ └───────────┴───────────┴───────────────────────┘ Opcode Condition Offset
15
12 11 8 7 6 0 ┌───────────┬───────────┬──┬────────────────────┐ └───────────┴───────────┴──┴────────────────────┘ Opcode Reg1 W/B Displacement
Formato 4: (JR,...)
Formato 5: (DJNZ,...)
15 0 Formato 6: ┌───────────────────────────────────────────────┐ (RET,NOP,...) └───────────────────────────────────────────────┘ Opcode 'Mode ' rappresenta il modo di indirizzamento come descritto nella tabella seguente (Expr e' in Word2): ┌──────────────────────────────────────────────────────────────┐
│ Mode Reg1 Assembly Nome Operando │ ├──────────────────────────────────────────────────────────────┤ │ 00 R0 #Expr Immediato Expr │ │ 00 R1..R15 @Reg1 Indiretto a reg. MEM(Reg1) │ │ 01 R0 Expr(Expr) Assoluto MEM(Expr) │ │ 01 R1..R15 Expr Indexato MEM(Expr+Reg1) │ │ 10 R0..R15 Reg1 Registro Reg1 │ │ 11 R0..R15 (illegale) │ └──────────────────────────────────────────────────────────────┘ Il set di istruzioni dell'H8000 e' il seguente: ╔══════════════════════════════════════════════════════════════════ ════╗ ║ Istruzioni macchina del processore ipotetico H8000 ║ ╠══════════════════════════════════════════════════════════════════ ════╣ ║ Cod. Operandi Formato Opcode AuxOp Descrizione ║ ║ Mnem. (Hex) (Hex) ║ ╠══════════════════════════════════════════════════════════════════ ════╣ ║ LD(B) Reg2,Op1 1 10 Load Reg2 con Op1 ║ ║ ST(B) Reg2,Op1 1 17 Store Reg2 in Op1 ║ ║ CMP(B) Reg2,Op1 1 05 Set Z by Reg2-Op1 ║ ║ ADD(B) Reg2,Op1 1 00 Add Reg2=Reg2+Op1 ║ ║ AND(B) Reg2,Op1 1 03 And Reg2=Reg2.Op1 ║ ║ CLR(B) 2 06 8 Clear Op1 ║ ║ COM(B) 2 06 0 Complementa Op1 ║ ║ NEG(B) 2 06 2 Nega Op1 ║ ║ JP Op1 3 1E 8 Salta ad Op1 ║ ║ CALL Op1 3 1F 0 Salta a subroutine in Op1 ║ ║ JR CC,Offset 4 E Salta se condiz.CC e' vera ║ ║ D(B)JNZ Reg1,Displ. 5 F Decrem.Reg1 salta se Non Zero║ ║ RET 6 9E08 Ritorna da subroutine ║ ║ NOP 6 8D07 No Operation ║ ╚══════════════════════════════════════════════════════════════════ ════╝ Per meglio comprendere i vantaggi della programmazione con una macchina a registri generali mostriamo come uno stesso programma venga scritto in una macchina ad accumulatori (H6809) ed in una macchina a registri generali (H8000). Cominciamo a vedere il programma assembly nell'H6809: ╔══════════════════════════════════════════════════════════════════ ╗ ║ PROGRAMMA H6809 CHE ESEGUE LA MOLTIPLICAZIONE CON SOMME RIPETUTE ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ ORG 2A40H Origine a 2A40H ║
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
2A40 2A41 2A44 2A47 2A4A 2A4D 2A4F 2A51 2A54 2A57 2A5A 2A5D 2A5F 2A62 .... 2C00 2C01 2C02 2C03 ....
4F B7 B6 B7 B6 27 8B B7 B6 BB B7 20 B6 7E ?? ?? 05 17
START 2C 2C 2C 2C 10 FF 2C 2C 2C 2C EB 2C 10
00 02 01 01
01 00 03 00 00 00
CLRA STA LDA STA LOOP ┌─>LDA │ BEQ │ ADDA │ STA │ LDA │ ADDA │ STA └──BRA OUT LDA JMP ORG PROD RMB CNT RMB MPY FCB MCND FCB END
PROD MPY CNT CNT OUT #-1 CNT PROD MCND PROD LOOP PROD 1000H 2C00H 1 1 5 23 START
║ ║ ║ Inizializza CNT ║ Raggiunta la fine ? ║ Se si, esce ad OUT ║ ║ Decrementa CNT ║ ║ Somma moltiplicando ║ ║ ║ Mette risultato in A ║ Torna al sistema op. ║ Area dati e' in 2C00H ║ Riserva 1 byte (PROD) ║ Riserva 1 byte (CNT) ║ Moltiplicatore=5 ║ Moltiplicando=23 ║ ║ Azzera PROD
*
* * *
╚══════════════════════════════════════════════════════════════════ ╝ Nell'H8000 invece il programma risulta piu' compatto e quindi anche piu' veloce perche' si eliminano le istruzioni LDA,STA (segnate con *) di salvataggio temporaneo dei dati nell'accumulatore: ╔══════════════════════════════════════════════════════════════════ ╗ ║ PROGRAMMA H8000 CHE ESEGUE LA MOLTIPLICAZIONE CON SOMME RIPETUTE ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ╣ ║ ORG 2A40H Origine a 2A40H ║ ║ 2A40 8C18 START CLRB RH1 Azzera RH1=prodotto ║ ║ 2A42 6000 2A52 LDB RH0,MPY Carica RH0 con MPY ║ ║ 2A46 E603 JR EQ,OUT Test se zero ║ ║ 2A48 4001 2A53 ┌─>ADDB RH1,MCND Somma MCND a RH1 ║ ║ 2A4C F003 └──DBJNZ RH0,LOOP Raggiunta la fine ? ║ ║ 2A4E 5E08 1000 OUT JP 1000H Esce con prod. in RH1 ║ ║ 2A52 05 MPY FCB 5 ║ ║ 2A53 17 MCND FCB 23 ║ ║ 2A54 END START ║ ╚══════════════════════════════════════════════════════════════════ ╝ La lunghezza di questo programma e' di 2A54-2A40=14H bytes mentre il precedente programma per l'H6809 aveva una lunghezza di 2A65-2A40=25H bytes piu' i 4 dell'area dati in 2C00..2C003. Con l'H8000 questo programma e' quindi piu' corto di 21 bytes, cioe' circa la meta' (ovviamente dipende dal programma e dall'uso che si fa dei registri generali).
⌡ 6.4.3 - Macchine a stack Una macchina a stack e' invece caratterizzata dalla sostituzione dei registri operativi con le locazioni di memoria allocate allo stack. Ne consegue un vantaggio per il maggior numero di registri disponibili al programmatore, anche rispetto ad una macchina a registri generali, ma una maggiore lentezza per l'esecuzione dei cicli dei memoria necessari per accedere allo stack. Inoltre si ottiene una semplificazione costruttiva poiche' il numero di registri elettronici direttamente accessibili alle istruzioni (e quindi presenti nel modello di programmazione) e' ridotto e quindi la costruzione e' piu' semplice. L'esempio di macchina a stack che consideriamo a fini didattici e' l'H11, che possiamo ottenere per derivazione dal PDP11 considerando una macchina dotata solo di un sottoinsieme delle istruzioni del PDP11 (quelle che usando il registro di stack pointer come registro indice, permettono di implementare il set di istruzioni tipico di una macchina a stack). La macchina ipotetica che cosi' si ottiene, con il relativo modello di programmazione, puo' essere rappresentata sinteticamente dallo schema seguente: ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ Registri │ X │ │ SP │ │ PC │ │ EAR │ <─── a 16 bit └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ ╔══════╩═╦════╦═╩═════╦══╩════════╩═══╗ ║ │ │ │ Bus interno ║ ┌───┐ ║ │ │ ┌──┴─┐ ║ ┌───┤ M │ ║ │ │ │ IR │ ╠───┤ MD│ E │ ║ ┌──┴──┐ │ └──┬─┘ ║ └───┤ M │ ║ │ TEMP│ │ ┌───┴───┐ ║ │ O │ ║ └──┬──┘ │ │CONTROL│ ║ ┌───┤ R │ ║ │ │ │ UNIT │ ╠───┤MAR│ I │ ║ │ │ └───┬───┘ ║ └───┤ A │ ║ ┌──┴────┴──┐ ┌─┴─┐ └───┘ ║ │ ALU ├──┤ Z │ ║ └─────┬────┘ └───┘ ╚═══════════╩═══ (TEMP e' un registro di memorizzazione temporanea non accessibile con le istruzioni) MODELLO DI PROGRAMMAZIONE H11 ═════════════════════════════ 15 8 7 0 ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┬──┬──┬──┬──┬──┬──┬──╦──┬──┬──┬──┬──┬──┬──┬──┐ └──┴──┴──┴──┴──┴──┴──┴──╩──┴──┴──┴──┴──┴──┴──┴──┘ ┌──┐ └──┘ Z
PC SP X
Nell'H11 lo stack e' allocato nella parte alta della memoria come indicato nella mappa seguente: 15 8 7 0 Indirizzo ┌───────────────────────┬───────────────────────┐
(Hex)
├───────────────────────┼───────────────────────┤ ├───────── AREA PROGRAMMI E DATI ────────────┤ 0002 ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ................................................. .... ├───────────────────────┼───────────────────────┤ ├───────────────────────┼───────────────────────┤ ├───────── AREA RISERVATA ALLO STACK ────────┤ FFFA Iniz. ├───────────────────────┼───────────────────────┤ SP-->└───────────────────────┴───────────────────────┘
0000 0004 0006 FFF6 FFF8 FFFC FFFE
Il set di istruzioni dell'H11 e' il seguente: ╔══════════════════════════════════════════════════════════════════ ═╗ ║ Istruzioni macchina del processore ipotetico H11 ║ ║ ( TOS = Top Of Stack; SOS = Second Of Stack ) ║ ╠══════════════════════════════════════════════════════════════════ ═╣ ║ Cod. Operandi Lenght Opcode Descrizione ║ ║ Mnem. (bytes) (Hex) ║ ╠══════════════════════════════════════════════════════════════════ ═╣ ║ ║ ║ Istruzioni come LDA, STA: ║ ║ PUSH Addr 3 01 Push MEMW(Addr) nello stack ║ ║ PUSH #dato 3 02 Push dato nello stack ║ ║ PUSH Offset(X) 2 03 Push MEMW(X+Offset) in stack ║ ║ PUSHT 1 04 Push TOS nello stack ║ ║ PUSHS 1 05 Push SOS nello stack ║ ║ PUSHX 1 06 Push X nello stack ║ ║ POP Addr 3 07 Pop TOS e store in MEM(Addr) ║ ║ POP Offset(X) 2 08 Pop TOS e store in MEM(X+Offset)║ ║ ║ ║ Operazioni aritmetico-logiche: ║ ║ CLR 1 10 Clear TOS ║ ║ COM 1 11 Complementa TOS ║ ║ NEG 1 12 Nega TOS ║ ║ SWAB 1 13 Swap bytes of TOS ║ ║ ADD 1 14 Pop TOS, add to new TOS ║ ║ AND 1 15 Pop TOS, and to new TOS ║ ║ CMP 1 16 Pop TOS, compare to new TOS ║ ║ MUL 1 17 Pop TOS,SOS e Push TOS*SOS ║ ║ ║ ║ Manipolazioni di X, SP: ║ ║ LD r,#dato 3 20+r Load r con dato ║ ║ LD r,Addr 3 22+r Load r con MEMW(Addr) ║ ║ LDXS 1 24 Load X con una copia di SP ║ ║ ST r,Addr 3 26+r Store r con MEMW(Addr) ║ ║ ADD r,#Offset 2 28+r Add Offset to r ║ ║ ║ ║ LDA, STA indiretti: ║ ║ VAL 1 30 Load TOS con MEMW(TOS) ║ ║ STOW 1 31 Store TOS in MEMW(TOS) e ║ ║ ║ ║ Istruzioni di salto: (e pop entrambi)║
║ ║ ║ ║ ║ ║ ║ ║
BNE BEQ BRA JMP JSR RTS NOP
Offset Offset Offset Addr Addr
2 2 2 3 3 1
40 41 42 50 51 52
Branch se Z=0 Branch se Z=1 Branch sempre Jump to Addr Jump to subroutine at Addr Return from subroutine
1
00
No Operation
║ ║ ║ ║ ║ ║ ║ ║
╚══════════════════════════════════════════════════════════════════ ═╝ L'esempio seguente mostra come operano le istruzioni dell'H11, visualizzando lo stato corrente dello stack, le cui locazioni svolgono il ruolo di registri operativi: ├──────┤ ├──────┤ ├──────┤ ├──────┤ │ │ │ │ │ │ │ │ ├──────┤ ├──────┤ ├──────┤ ├──────┤ SP══>│ 0002 │ │ │ │ │ │ │ ├──────┤ ├──────┤ ├──────┤ ├──────┤ │ 1234 │ SP══>│ 2468 │ │ │ │ │ ├──────┤ ├──────┤ ├──────┤ ├──────┤ │ 5555 │ │ 5555 │ SP══>│ 79BD │ SP══>│ 8643 │ ├──────┤ ├──────┤ ├──────┤ ├──────┤ PUSH #5555H MUL ADD NEG PUSH #1234H PUSH #0002H Per meglio comprendere i vantaggi della programmazione con una macchina a stack, mostriamo come lo stesso programma gia' usato per confrontare una macchina ad accumulatori (H6809) ed una macchina a registri generali (H8000), venga scritto con le istruzioni dell'H11. Dal confronto possiamo trarre le seguenti conclusioni: - su H11 occorrono piu' istruzioni che su H8000 per lo stesso programma; - le istruzioni su H11 sono piu' corte (programmi piu' compatti); - poiche' la velocita' di esecuzione e' limitata dai cicli di accesso alla memoria, H11 e' piu' lento. ╔══════════════════════════════════════════════════════════════════ ═╗ ║ PROGRAMMA H11 CHE ESEGUE LA MOLTIPLICAZIONE CON SOMME RIPETUTE ║ ╠══════════════════════════════════════════════════════════════════ ═╣ ║ Addr. Contenuto Label Opcode Operando Commento ║ ╠══════════════════════════════════════════════════════════════════ ═╣ ║ ORG 2A40H Origine a 2A40H ║ ║ 2A40 SPROD EQU -2 Offset of PROD in stack║ ║ 2A40 SCNT EQU -4 Offset of CNT in stack║ ║ 2A40 SMCND EQU -6 Offset of MCND in stack║ ║ 2A40 * Si assume che SP sia gia' inizializzato ║ ║ 2A40 24 START LDXS Metti pointer SP in X ║ ║ 2A41 04 PUSHT Avanza SP:=SP+1 ║ ║ 2A42 10 CLR Azzera PROD ║
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
2A43 2A46 2A49 2A4C 2A4F 2A50 2A52 2A53 2A55 2A56 2A58 2A5A 2A5C 2A5E 2A61 2A61 2C00 2C02 2C04
01 01 50 02 14 08 04 03 14 08 03 40 29 50
2C 2C 2A FF
00 02 58 FF
LOOP
FC FE FE FC F0 06 10 00
IN DONE * Il
05 17
MPY MCND
PUSH PUSH JMP ┌─>PUSH │ ADD │ POP │ PUSHT │ PUSH │ ADD │ POP │ PUSH └──BNE ADD JMP risultato ORG FCW FCW END
MPY MCND IN #-1 SCNT(X) SPROD(X) SPROD(X) SCNT(X) LOOP SP,#6 1000H e' nel TOS 2C00H 5 23 START
║ ║ ║ Prepara decremento ║ Calcola CNT-1 ║ Store CNT decrementato ║ Copia MCND in TOS ║ Prepara 2° addendo PROD║ Calcola PROD:=PROD+MCND║ Store new PROD ║ Load CNT in TOS ║ Esce se zero ║ Rilascia 6 loc.in stack║ Salta al sistema op. ║ ║ ║ Moltiplicatore ║ Moltiplicando ║ ║ Load CNT (:=MPY) Load MCND
╚══════════════════════════════════════════════════════════════════ ═╝ Cosi' come in una macchina a registri generali bisogna tenere costantemente nota del contenuto dei registri operativi, cosi' con una macchina a stack bisogna annotare il contenuto delle varie locazioni dello stack. Per capire questo riscriviamo ora il programma indicando a fianco di ogni istruzione lo stato corrente dello stack: SPROD SCNT SMCND
EQU EQU EQU
-2 -4 -6
Offset of PROD in stack Offset of CNT in stack Offset of MCND in stack
X=SP ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ │ │ │ │ START LDXS Metti SP in X ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ │ │ │ │ PUSHT Avanza SP ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ │ │ 0 │ │ CLR Azzera PROD ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ │ MPY │ 0 │ │ PUSH MPY Set CNT=MPY ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ MCND│ MPY │ 0 │ │ PUSH MCND Load MCND ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X JMP IN ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ -1 │ CNT │ MCND│ CNT │ PROD│ │LOOP ┌─>PUSH #-1 ────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ SP X │ ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │CNT-1│ MCND│ CNT │ PROD│ │ │ ADD
────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─NEW─┬─────┬─────┐ │ │ │ MCND│ CNT │ PROD│ │ │ ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ MCND│ MCND│ CNT │ PROD│ │ │ ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ PROD│ MCND│ MCND│ CNT │ PROD│ │ │ ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ PROD│ MCND│ CNT │ PROD│ │ │ ────┴─────┴+MCND┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─────┬─NEW─┬─────┐ │ │ │ MCND│ CNT │ PROD│ │ │ ────┴─────┴─────┴─────┴─────┴─────┴─────┘ SP X │ ────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ CNT │ MCND│ CNT │ PROD│ │ IN │ ────┴─────┴─────┴─────┴─────┴─────┴─────┘ └──BNE LOOP
│ │ POP SCNT(X) │ │ PUSHT │
Store CNT
Copia MCND
│ PUSH SPROD(X) │ │ ADD │ │ POP │
SPROD(X)
│ PUSH │
SCNT(X)
Load CNT
⌡ 6.4.4 - Altri criteri di classificazione Vari altri possono essere i criteri di classificazione dei processori. Tra i criteri che possono orientare una diversa classificazione sono: A) Il tipo di indirizzamento (prevalente), per cui possiamo distinguere: Tipo istruzione Simbolo Categoria ────────────────────────────────────────────────────────────────── Accumulatore A:=F(A,Mem) 1 indirizzo Memoria-->Registro Reg:=F(Reg,Mem) 1+½ indirizzo Memoria<--Registro Mem:=F(Mem,Reg) 1+½ indirizzo Memoria<-->Memoria Mem2:=F(Mem2,Mem1) 2 indirizzi Stack PUSH(F(POP,POP)) 0 indirizzi (impliciti) Esempi: H6809 appartiene alla prima categoria; H11 appartiene all'ultima categoria; B) Il set di registri operativi: alcuni processori hanno due set di registri (es. MCS-48), altri hanno 16 registri in memoria collocabili ovunque con un apposito registro puntatore (es.TI9900). C) Il numero di bit trattati simultaneamente nelle operazioni (esempi: M6809=8 bit; Intel 8086=16 bit; M68000=32 bit) D) Altri elementi caratterizzanti come: il costo, le dimensioni, ecc. !----------------------------------------------------------------------CAPITOLO 7 - Sottoprogrammi e passaggio dei parametri ══════════
Riassunto: In questo capitolo vengono descritte le modalita' di funzionamento dei sottoprogrammi e le regole che occorre tenere presenti nel passaggio dei parametri con il programma chiamante. ⌡ 7.1 - Tipi di sottoprogrammi: Procedure e Function Quando l'esecuzione di un'elaborazione e' richiesta in piu' punti di un programma, conviene organizzare le istruzioni che la eseguono in un sottoprogramma (subroutine). Cio' non solo per evitare di inserire nel programma principale piu' volte lo stesso insieme di istruzioni, ma anche per semplicita' di programmazione, poiche' un'eventuale modifica o correzione dovra' essere apportata una sola volta (nella subroutine). Inoltre, una volta collaudatone il funzionamento, si puo' evitare di ricompilare tutte le volte i sottoprogrammi e si possono anche creare delle biblioteche personali (library) con tutti i sottoprogrammi precompilati che il Link Editor usera' al pari delle biblioteche di sistema. Questo e' vero sia nei linguaggi ad alto livello (C, Basic, Fortran, Pascal, ecc.) che a basso livello (Assembly). Normalmente i sottoprogrammi hanno bisogno dei dati su cui eseguire le elaborazioni. Questi sono forniti dal programma chiamante, che riceve poi dal sottoprogramma i risultati dell'elaborazione. Questi dati passati dal programma chiamante al sottoprogramma e viceversa sono i parametri del sottoprogramma. In Pascal (ma anche in Fortran, Basic, ecc.) esistono due tipi di sottoprogrammi: la Procedure e la Function. La Procedure esegue delle elaborazioni per compiere delle azioni (come la stampa) o modificare delle variabili, mentre le Function calcolano il valore di una variabile e possono quindi essere usate nelle formule, al pari delle funzioni di biblioteca log(x), cos(x), ecc. Il passaggio dei parametri, come gia' visto nel ⌡2.1.5, puo' avvenire in due modi: con parametri di tipo 'valore' o di tipo 'variabile'. Nel primo caso il parametro e' passato al sottoprogramma copiandone il valore in una distinta locazione di memoria, in cui lavora il sottoprogramma (Procedura o Function che sia). In questo caso le eventuali alterazioni che questo parametro subisce all'interno del sottoprogramma non risultano nel programma chiamante. Nel secondo caso (passaggio dei parametri di tipo 'variabile', come nell'esempio della Procedure Swap) invece viene passato al sottoprogramma anziche' il valore, l'indirizzo della memoria in cui si trova la variabile passata. In questo modo il sottoprogramma lavora direttamente sulla stessa variabile usata dal programma chiamante e conseguentemente, a differenza dal caso precedente, ogni variazione apportata dal sottoprogramma alle variabili passare risultera' anche nel programma chiamante. Esempio di una Procedura che usa parametri di tipo 'variabile': PROGRAM Swapping (input,output); VAR a,b: integer; <---- a,b=variabili globali PROCEDURE Swap (VAR x,y: integer); <-- x,y=param.tipo variabile VAR t:integer; <---- t=variabile locale BEGIN t := x; x := y; y := t; END; BEGIN <---------- Inizio del programma principale a := 1; b := 2; Swap (a,b); write (a,b); <----a,b risultano variati da Swap END. perche' passati di tipo variabile
Esempio di una Function che usa parametri di tipo 'valore': PROGRAM Esempio (input,output); VAR n: integer; x: real; <---- n,x=variabili globali FUNCTION Fact (i: integer): real; <---- i=parametro di tipo valore VAR prod: real; <---- prod=variabile locale BEGIN prod := 1; WHILE i>1 DO BEGIN prod:=prod*i; i:=i-1 END; Fact := prod; END; BEGIN <---------- Inizio del programma principale read (n); WHILE n>0 DO BEGIN x := 2*sqrt(Fact(n))-1; writeln(n,Fact(n),n*n,x); <--- Function usato in una formula read(n); <-----------> END; n non risulta END. alterato perche' passato di tipo valore ⌡ 7.2 - Passaggio di parametri ai sottoprogrammi - Allocazione dinamica Un sottoprogramma ha bisogno di spazio in memoria, oltre che per il codice delle istruzioni, per i parametri passati in ingresso ed uscita, per le variabili locali e per l'indirizzo di ritorno. Questo spazio puo' essere allocato in modo statico o dinamico. Nel modo statico vengono riservate locazioni di memoria per uso esclusivo del sottoprogramma nella sua area di memoria (allocazione unica) o nelle locazioni immediatamente seguenti l'istruzione chiamante (allocazione multiplata: l'accesso ai dati e' fatto con indirizzamento indiretto a partire dall'indirizzo di ritorno memorizzato nello stack). Questo secondo modo puo' essere utile quando i dati passati sono predefiniti in modo diverso e statico per ogni chiamata, per esempio al momento della compilazione (messaggi di errore, ecc.). Nel modo dinamico lo spazio viene allocato al momento della chiamata e rilasciato al momento dell'uscita dal sottoprogramma. Questa allocazione dinamica si puo' ottenere usando lo stack come area di memoria per la memorizzazione dei parametri passati in ingresso ed uscita e per le variabili locali, oltre che per l'indirizzo di ritorno. Infatti all'uscita dal sottoprogramma spostando lo 'stack pointer' le locazioni impegnate durante l'esecuzione del sottoprogramma vengono virtualmente rilasciate (sono cioe' disponibili per gli altri successivi usi dello stack). Questo viene fatto dai compilatori di linguaggi strutturati ad alto livello come il Pascal. Lo schema seguente mostra l'evoluzione dello stack in tempi successivi, nel caso di due chiamate annidate a due sottoprogrammi (relativi agli indici 1 e 2): │ ├ │ │ ├ │ │
│ ┤ │ │ ┤ │ │
│ ├ │ │ ├ │ │
│ ┤ │ │ ┤ │ │
│ ├ │ │ ├ │ │
│ │ │ ┤ ├──────┤ │SP->│Local │ │ │Var.2 │ ┤ ├──────┤ │FP->│ Old │ │ │ FP2 │
│ ├ │ │ ├ │ │
│ ┤ │ │ ┤ │ │
│ ├ │ │ ├ │ │
│ ┤ │ │ ┤ │ │
├ ┤ ├ ┤ ├ ┤ │ │ │ │ │ │ │ │ │ │ │ │ ├ ┤ ├ ┤ ├──────┤ │ │ │ │SP->│Input │ │ │ │ │ │Param2│ ├ ┤ ├ ┤ ├──────┤ │ │ │ │ │Output│ │ │ │ │ │Param2│ ├ ┤ ├──────┤ ├══════┤ │ │SP->│Local │ │Local │ │ │ │Var.1 │ │Var.1 │ ├ ┤ ├──────┤ ├──────┤ │ │FP->│ Old │FP->│ Old │ │ │ │ FP1 │ │ FP1 │ ├──────┤ ├──────┤ ├──────┤ SP->│Return│ │Return│ │Return│ │Addr.1│ │Addr.1│ │Addr.1│ ├──────┤ ├──────┤ ├──────┤ │Input │ │Input │ │Input │ │Param1│ │Param1│ │Param1│ ├──────┤ ├──────┤ ├──────┤ ├──────┤ │Output│ │Output│ │Output│ │Param1│ │Param1│ │Param1│ ├──────┤ ├──────┤ ├──────┤ ├──────┤ FP─>│ │ │ │ │ │ (a) (b) (c)
├──────┤ ├ ┤ ├ ┤ │Return│ │ │ │ │ │Addr.2│ │ │ │ │ ├──────┤ ├ ┤ ├ ┤ │Input │ │ │ │ │ │Param2│ │ │ │ │ ├──────┤ ├──────┤ ├ ┤ │Output│SP->│Output│ │ │ │Param2│ │Param2│ │ │ ├══════┤ ├══════┤ ├ ┤ │Local │ │Local │ │ │ │Var.1 │ │Var.1 │ │ │ ├──────┤ ├──────┤ ├ ┤ │ Old │FP->│ Old │ │ │ │ FP1 │ │ FP1 │ │ │ ├──────┤ ├──────┤ ├ ┤ │Return│ │Return│ │ │ │Addr.1│ │Addr.1│ │ │ ├──────┤ ├──────┤ ├ ┤ │Input │ │Input │ │ │ │Param1│ │Param1│ │ │ ├──────┤ ├──────┤ │Output│ │Output│SP->│Output│ │Param1│ │Param1│ │Param1│ ├──────┤ ├──────┤ │ (d)
│
│ (e)
│FP->│ (f)
SP = Stack pointer FP = Frame pointer (inizio delle locazioni impegnate dal sottoprogramma) (a): Viene eseguita la chiamata al sottoprogramma 1, riservando nello stack le locazioni per i parametri e l'indirizzo di ritorno (b): Si memorizza l'ampiezza del blocco di stack impegnato dalla chiamata salvando FP1 e si assegna spazio per le variabili locali usate dal sottoprogramma 1 (che verranno poi rilasciate) (c): Viene eseguita la chiamata al sottoprogramma 2, riservando ulteriore spazio nello stack (per i parametri e l'indirizzo di ritorno) (d): Si memorizza l'ampiezza del blocco di stack impegnato nella seconda chiamata salvando FP2 e si assegna spazio per le variabili locali usate dal sottoprogramma 2 (che verranno poi rilasciate) (e,f): Si esegue il ritorno dai due sottoprogrammi rilasciando lo spazio dello stack, spostando lo stack pointer con le posizioni FP salvate prima nello stesso stack. ⌡ 7.2 - Ricorsione Una Procedure o Function che chiama se stessa si dice ricorsiva. Esempio: FUNCTION Fact(i: integer): real; BEGIN IF i <= 1 THEN Fact:=1 ELSE Fact:=i*Fact(i-1); END; La ricorsione puo' anche essere indiretta, quando la Procedura o Function ne chiama altre che a loro volta la richiamano (rientranza). Per esempio: PROCEDURE Proc1 (x,y: integer);
│
BEGIN ..... Proc2(a); ..... END; PROCEDURE Proc2 (z: integer); BEGIN ..... Proc1(b,c); ..... END; Per permettere la compilazione in un solo passo (TurboPascal), e' necessario che una procedura sia definita prima di essere richiamata e questo non sarebbe possibile nella ricorsione indiretta. Per permettere questi costrutti garantendo la correttezza sintattica viene posta la seguente dichiarazione (pseudoistruzione) nella giusta posizione, dove il compilatore dovrebbe trovare Proc2, avvertendolo cosi' che Proc2 non e' mancante, ma viene dopo. PROCEDURE Proc2 (z:integer); forward; Il codice ricorsivo dev'essere rientrante (read-only) quindi per la memorizzazione dei parametri passati e dei dati locali non si devono usare locazioni assolute della memoria ma strutture a stack (ad ogni rientro lo stack si approfondisce memorizzando cosi' in locazioni diverse). Pascal memorizza i parametri passati nello stack e quindi consente la ricorsione; il Fortran invece, memorizzandoli locazioni assolute della memoria, non la consente. ⌡ 7.3 - Coroutines I sottoprogrammi Pascal (Procedure e Function) non conservano le variabili locali tra due successive chiamate, poiche' le memorizzano nello stack e quindi le rilasciano all'uscita dal sottoprogramma. Esistono pero' applicazioni in cui si vuole uscire da un sottoprogramma e poi rientrarci avendo preservato i valori delle variabili locali. Un esempio di applicazioni di questo tipo sono quelle di filtraggio di dati provenienti da un dispositivo di input, che devono essere analizzati e riscritti poi su un'altro dispositivo di output: ┌───────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │ Input ├────>┤Coroutine├──>┤Coroutine├──>┤Coroutine├────>┤ Output │ │ Device│ │ Lettura ├<──┤ Analisi ├<──┤Scrittura│ │ Device │ └───────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘ Per includere la possibilita' di programmare le coroutines il compilatore Pascal viene ampliato (Pascal Esteso) con l'inserimento delle parole riservate COROUTINE, che permette di dichiarare un sottoprogramma come coroutine, e RESUME, che permette di uscire e rientrare dalla coroutine salvaguardando i valori delle variabili locali. Il loro uso e' mostrato nell'esempio seguente con due coroutines (i numeri indicano la sequenza temporale dei salti): COROUTINE Cor1; BEGIN COROUTINE Cor2; ..... 1 BEGIN RESUME Cor2; ──2──────────────> ..... ..... <─────────────3──── RESUME Cor1; RESUME Cor2; ──4───────────────> ..... ..... <─────────────5──── RESUME Cor1; RESUME Cor2; ──6───────────────> ..... ..... <─────────────7──── RESUME Cor1;
END;
END;
Per programmare le coroutines in Assembly 6809 bisogna salvare gli indirizzi di ritorno quando si eseguono i salti corrispondenti ai RESUME. Questo puo' essere fatto come nell'esempio seguente (RES1 e RES2 sono le locazioni dove vengono salvati gli indirizzi di ritorno, che vengono poi usati per i salti indiretti JMP@, corrispondenti ai RESUME): COR1 COR2 RES1 RES2
PULS Y STY RES2 JMP @RES1 PULS Y STY RES1 JMP @RES2 RMB 2 RMB 2
!--------------------------------------------------------------------CAPITOLO 8 - Programmazione Assembly ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di descrivere come le istruzioni binarie del linguaggio macchina possano essere sostituite da opportuni codici mnemonici (linguaggio di programmazione Assembly); per fare questo occorre introdurre il necessario software di base per la traduzione automatica dei codici mnemonici del linguaggio Assembly (piu' facili da usare) nelle corrispondenti istruzioni del linguaggio macchina e per il caricamento automatico del programma eseguibile. ⌡ 8.1.1 - Compilatore Assembler Abbiamo gia' visto come l'introduzione di codici mnemonici al posto dei codici binari delle istruzioni ci aiuti ad una comprensione piu' immediata delle istruzioni di macchina e quindi alla stesura di un programma. Naturalmente poi ci troviamo nella necessita' di tradurre questo codice mnemonico, che chiameremo Assembly, nel codice binario caricabile nella memoria della macchina per l'esecuzione. Essendovi a questo livello una corrispondenza 'uno a uno' tra le istruzioni assembly ed i relativi codici binari, non costituisce un serio problema la traduzione dei codici delle istruzioni (OPCODE), al punto che possiamo addirittura predisporre un programma (compilatore assembly ovvero ASSEMBLER) che la esegua automaticamente. Durante il processo di traduzione in linguaggio macchina pero' il compilatore ha bisogno anche di conoscere i valori e gli indirizzi corrispondenti ai vari simboli usati dal programmatore (variabili, labels, ecc.), per poterli inserire come operandi nelle istruzioni macchina. Siccome alcuni di questi simboli possono risultare definiti DOPO le istruzioni che li usano ('forward references', per esempio potrebbero essere stati collocati tutti alla fine del programma, per averli raggruppati tutti insieme), si esegue generalmente una compilazione in due passi. Nel primo passo il programma compilatore legge il codice sorgente (assembly) ed esegue le seguenti operazioni: - riconosce i simboli mnemonici delle istruzioni e ne controlla la correttezza sintattica; - dopo aver riconosciuto le istruzioni ricava da un'apposita tabella, oltre al codice macchina dell'istruzione, anche il numero di bytes
della sua occupazione di memoria ed incrementa quindi il contatore PLC (Program Line Counter), che indica l'indirizzo corrente di caricamento dell'istruzione appena tradotta in codice di macchina; - quando incontra dei simboli alfanumerici al posto degli operandi, come per le variabili (esempio LDA DATO), o per le label (=etichette, esempio JMP LOOP), crea la 'Symbol Table' (tabella dei simboli), che potra' essere poi usata nel passo due per ricavare gli indirizzi dei simboli necessari al completamento delle istruzioni macchina. Naturalmente in questo primo passo si possono incontrare vari tipi di errori, come per esempio (li indichiamo con i termini inglesi come indicati dal compilatore, poiche' riteniamo si spieghino da soli): - Multiple Symbol Definition; - Undefined Symbol; - Syntax Error; - Illegal Opcode; - Addressing Error; Nel secondo passo quindi il compilatore, rileggendo da capo il codice sorgente, e' in grado di generare il codice in linguaggio macchina ('codice oggetto'), completato con gli indirizzi che corrispondono ai vari simboli. Un esempio che mostra il risultato di questo processo di compilazione e' nel listato seguente (programma che esegue la somma dei primi 15 numeri interi): N 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18
PLC
CONTENTS
LABEL
OPCODE
OPERAND
COMMENTS
0000 * Riga di commento 0000 ORG 2000H Indirizzo di Origine 2000 4F START CLRA Azzera SUM 2001 B7 20 1B STA SUM 2004 B6 20 1C LDA ICNT Inizializza CNT 2007 B7 20 1A ALOOP STA CNT 200A BB 20 1B ADDA SUM 200D B7 20 1B STA SUM SUM=SUM+CNT 2010 B6 20 1A LDA CNT 2013 8B FF ADDA #-1 Incrementa CNT 2015 26 F0 BNE ALOOP 2017 7E 10 00 JMP SYS Esce tornando al sistema 201A SYS EQU 1000H Entry point del sist.op. 201A CNT RMB 1 201B SUM RMB 1 201C IVAL EQU 15 201C 0F ICNT FCB IVAL 201D END START (START=entry point) <--------> <-----------------------------------------------> Object Code Source Program (scritto dal programmatore) (risultato della compilazione)
SYMBOL TABLE: SYMBOL START SUM
VALUE 2000 201B
SYMBOL ALOOP IVAL
VALUE 2007 000F
SYMBOL SYS ICNT
VALUE 1000 201C
SYMBOL CNT
VALUE 201A
Il codice oggetto costituisce il programma direttamente caricabile in memoria ed eseguibile a partire dall'indirizzo 'Start Addr'. Esso puo' essere scritto su un supporto magnetico con un formato come quello ora indicato, in cui tutto il file risulta segmentato in N records seguiti dal record finale in cui viene registrato l'indirizzo di partenza
(entry point): ┌──────────┬──────────┬──────────┬──────────┬──────────────┐ │ Record 1 │ Record 2 │ ....... │ Record N │ Record finale│ └──────────┴──────────┴──────────┴──────────┴──────────────┘ ┌────┬────────────┬────────────┬─────┐ Records 1...N: │Addr│ Byte Count │ Data bytes │CKSUM│ └────┴────────────┴────────────┴─────┘ ┌──────────┬─────┬─────┐ Record finale: │Start Addr│ 0 │CKSUM│ (N.B: Byte Count=0 identifica └──────────┴─────┴─────┘ questo come il record finale) Conviene infine sistemare in modo completo la soluzione del problema della compilazione di programmi mnemonici assembly, prevedendo anche un altro programma (LOADER) che provveda, sempre automaticamente, al caricamento nella memoria del programma binario, risultante dalla compilazione e gia' pronto per la successiva esecuzione. Naturalmente per la preparazione della sequenza delle righe che costituiscono il codice mnemonico ci si e' anche serviti di un programma EDITOR, per cui l'intero processo di programmazione in linguaggio Assembly puo' essere rappresentato nello schema seguente: ┌─────────┐ ┌──────────┐ │Operatore│── ── ── ── ── ── ── ── ───>│(DEBUGGER)├ ── ──>┐ Risultati └┬────────┘ └──────────┘ │ ─────┬─── ║ │ ║ ║ ┌──────┐Programma┌─────────┐Modulo ┌──────┐ Core ┌───┴────┐ ║ ║ │ Text │ sorgente│ │Oggetto│ │ Image │ │ ║ ╚═>│EDITOR│════════>│ASSEMBLER│══════>│LOADER│══════>│COMPUTER│══>╝ │ │ .ASM │ │ .OBJ │ │ .EXE │ │ └──────┘ └─────────┘ └──────┘ └────────┘ Questo costituisce il primo embrione del software di base che costruiremo per il nostro elaboratore. ⌡ 8.1.2 - Linking Loader Un ulteriore passo avanti viene fatto introducendo la possibilita' di compilazioni separate per i sottoprogrammi. Infatti non e' conveniente, per programmi di una certa dimensione, ricompilare ogni volta tutto il programma compresi i sottoprogrammi che possono essere numerosi e ormai gia' collaudati (si pensi per esempio alla cosiddetta 'library' ovvero la biblioteca dei programmi di utilita'). Eseguendo compilazioni separate per i vari sottoprogrammi nascono pero' due ordini di problemi: 1) i sottoprogrammi devono avere delle referenze esterne (indirizzi e dati) per il passaggio dei parametri e per le reciproche chiamate; queste referenze non possono essere completate con i giusti indirizzi in fase di compilazione separata e quindi, oltre a tenere nota della loro presenza nel modulo oggetto prodotto dall'assembler, con idonee direttive (ENT,EXT) per evitare la segnalazione di errore 'Undefined Symbol' in fase di compilazione, occorre un ulteriore programma (LINK EDITOR) che correli le referenze esterne dei vari moduli oggetto, concatenandoli tra loro come dovuto. Esempio di uso delle direttive ENT ed EXT: * Programma principale * Routine PTIME * Routine TIMER * (usa PTIME) * (display orario) * (aggiorna orario) EXT PTIME <────┐ EXT HOUR,MINUTE <─────> ENT HOUR,MINUTE MAIN ... └───> ENT PTIME TIMER LDA MINUTE
JSR PTIME ... ecc.
PTIME LDA HOUR INCA ... ... LDA MINUTE LDA HOUR ... ... RTS RTS HOUR RMB 1 MINUTE RMB 1 2) il programma caricatore LOADER deve permettere la traslazione automatica dei vari moduli in memoria all'atto del caricamento, per permettere l'ampliamento di un sottoprogramma, senza che il maggiore spazio di memoria richiesto obblighi a dover ricompilare tutti gli altri sottoprogrammi ad esso seguenti. Questa traslazione viene chiamata 'Rilocazione' ed il Loader dotato di questa funzionalita' si chiama RELOCATING LOADER. Spesso questi due moduli del software di base (LINK EDITOR e RELOCATING LOADER) vengono eseguiti in sequenza automatica e si parla allora di LINKING LOADER: ╔═ ║ LINK EDITOR (risolve ext.ref. e concatena moduli) LINKING LOADER: ║ ║ RELOCATING LOADER (carica in memoria rilocando) ╚═ Il listato seguente mostra il risultato di una compilazione rilocabile (dove la presenza del carattere 'r' indica indirizzo da rilocare in fase di caricamento dal 'Relocating Loader'): N 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18
PLC
CONTENTS
LABEL
OPCODE
OPERAND
COMMENTS
0000 * Riga di commento 0000 RORG 0 Origine rilocabile <-- N.B. 0000 4F START CLRA Azzera SUM 0001 B7 00 1Br STA SUM 0004 B6 00 1Cr LDA ICNT Inizializza CNT 0007 B7 00 1Ar ALOOP STA CNT 000A BB 00 1Br ADDA SUM 000D B7 00 1Br STA SUM SUM=SUM+CNT 0010 B6 00 1Ar LDA CNT 0013 8B FF ADDA #-1 Incrementa CNT 0015 26 F0 BNE ALOOP 0017 7E 10 00 JMP SYS Esce tornando al sistema 001A SYS EQU 1000H Entry point del sist.op. 001A CNT RMB 1 001B SUM RMB 1 001C IVAL EQU 15 001C 0F ICNT FCB IVAL 001D END START (START=entry point) <--------> <-----------------------------------------------> Object Code Source Program
SYMBOL TABLE: SYMBOL START SUM
VALUE 2000r 201Br
SYMBOL ALOOP IVAL
VALUE 2007r 000F
SYMBOL SYS ICNT
VALUE 1000 201Cr
SYMBOL CNT
VALUE 201Ar
In fase di caricamento in memoria il 'Relocating Loader' provvedera' ad incrementare dell'indirizzo di caricamento iniziale di quel modulo tutti gli indirizzi con il carattere 'r' (rilocazione).
⌡ 8.1.3 - Pseudo-istruzioni Per usare questi programmi di compilazione e caricamento automatico delle istruzioni occorre anche prevedere un modo di fornire loro delle direttive sul lavoro che vogliamo fare (per esempio l'indirizzo in cui vogliamo che un programma sia caricato in memoria). La cosa piu' naturale e' di attribuire ad ognuna di queste direttive un codice mnemonico, inserito dal programmatore nel punto appropriato del programma, che sia adeguatamente riconosciuto dal compilatore, il quale si comportera' poi in conformita' alla direttiva ricevuta. Cosi' facendo, ampliamo l'insieme dei codici mnemonici assembly con queste che, non corrispondendo ad istruzioni di macchina, chiameremo pseudo-istruzioni assembly. Esempi di pseudo-istruzioni Assembly sono le seguenti: Nome Formato Esempio ─────────────────────────────────────────────────────────────── ORG=Origine Label ORG espressione ORG 2000H EQU=Eguaglianza Label EQU espressione VAL EQU 15 FCB=Form Const.Byte Label FCB espressione CNT FCB VAL FCW=Form Const.Word Label FCW espressione SUM FCW 10 RMB=Reserve Mem.Bytes Label RMB espressione VET RMB 100 RMW=Reserve Mem.Words Label RMW espressione NUM RMW 1 END=End assembly Label END espressione END START Come espressioni si possono usare combinazioni con + e - di costanti, simboli definiti precedentemente o PLC (Program Line Counter, spesso indicato con *). ⌡ 8.1.4 - Macroistruzioni Osservando i programmi Assembly si nota spesso che certe sequenze di istruzioni si ripetono inalterate in vari punti del programma; esse corrispondono in genere a particolari operazioni che in quella applicazione sono ripetitive. Conviene allora attribuire un particolare simbolo a quella sequenza abilitando il compilatore Assembler a riconoscere quel simbolo, da noi appositamente definito in quel programma (con una 'Macro'), traducendolo sempre con la giusta sequenza di istruzioni cui esso corrisponde. Esempio: capita spesso di dover caricare il numero contenuto in A (di 8 bit) nel registro X (di 16 bit). La sequenza di istruzioni che serve allo scopo e' la seguente: STA TEMPX+1 Carica A in LSB di TEMPX CLRA STA TEMPX Azzera MSB di TEMPX LDX TEMPX Carica i 16 bit in X Si puo' allora definire la seguente macroistruzione: TFRAX MACRO Definisce le istruzioni seguenti come macro STA TEMPX+1 CLRA STA TEMPX LDX TEMPX ENDM Fine della macro in modo da richiamare le 4 istruzioni piu' semplicemente con il mnemonico della macro: LDA J TFRAX Un ulteriore perfezionamento del metodo puo' raggiungersi permettendo
il passaggio di parametri alla macro, come nell'esempio seguente: TFRVX MACRO VAR (VAR=variabile passata alla macro) LDA VAR STA TEMPX+1 CLRA STA TEMPX LDX TEMPX ENDM Fine della macro In questo caso la chiamata della macro si fara' semplicemente con: TFRVX
J
Ovviamente il metodo si puo' ulteriormente estendere con il passaggio di piu' parametri (separati da virgole) e consentendo l'annidamento (nesting) delle macros una dentro l'altra. CAPITOLO 9 - Sistema di Input/Output ══════════ Riassunto: Scopo essenziale di questo capitolo e' quello di descrivere il sistema di Input/Output di un calcolatore elettronico, mostrando come dall'introduzione del sistema di interruzione si arrivi alla programmazione concorrente, che apre le porte alla possibilita' di impiego di un solo processore per la elaborazione simultanea di piu' programmi, anche di utenti diversi (multitasking e multiprogrammazione). ⌡ 9.1.1 - Realizzazione hardware dell'Input/Output Una macchina di Von Neumann richiede un'unita' centrale di elaborazione (CPU) ed un sistema di memoria contenente le sequenza di istruzioni da eseguire, collegati tra loro da un'interconnessione (bus di memoria) usabile per lo scambio di indirizzi e dati. Una tale macchina servirebbe pero' a poco se non fosse dotata della possibilita' di inserimento dei problemi (input) e di estrazione dei risultati dell'elaborazione (output). Tali funzionalita' possono essere introdotte inserendo un nuovo sistema d'interconnessione tra la CPU ed il sistema di input/output (I/O), costituito dall'insieme dei dispositivi di I/O che vogliamo collegare alla CPU (bus di I/O): ┌─────┐ Bus di I/O ┌─────┐ Bus di memoria ┌─────────┐ │ I/O │<──────────────>│ CPU │<──────────────>│ MEMORIA │ └─────┘ └─────┘ └─────────┘ Alternativamente, disponendo gia' dell'interconnessione della CPU al sistema di memoria (bus di memoria), si puo' pensare di usare questo stesso bus anche per il collegamento del sistema di input/output alla CPU (I/O memory mapped), come gia' anticipato nel ⌡6.1.1: ┌─────────┐ │ │ BUS DI COMUNICAZIONE │ C P U │═══════╦════════╦═════════════╦══════ │ │ ═════╦│═══════╦│════════════╦│══════ │ (Master)│ ════╦││══════╦││═══════════╦││══════ │ │ │││ │││ │││ └─────────┘ ┌──┴┴┴──┐ ┌─┴┴┴─┐ ┌─┴┴┴─┐ │MEMORIA│ │ I/F │ ..... │ I/F │ Interfacce
Indirizzi Dati Controllo
└───────┘ └─┬┬┬─┘ └─┬┬┬─┘ ┌─┴┴┴─┐ ┌─┴┴┴─┐ │ I/O │ ..... │ I/O │ Dispositivi └─────┘ └─────┘ <--------- ( Slave ) --------> N.B. Le interfacce I/F servono a collegare elettricamente dispositivi diversi, fatti da costruttori diversi, allo stesso bus di comunicazione (del computer usato) In entrambi i casi la selezione del dispositivo (I/O device), con cui la CPU vuole comunicare, avviene mediante l'invio sul bus dell'indirizzo del dispositivo (device code). La prima soluzione (bus di I/O dedicato) presenta una maggiore complessita' costruttiva sia per la presenza di un secondo bus per l'I/O, distinto dal bus di memoria, che per l'esistenza di apposite istruzioni di I/O distinte da quelle di accesso alla memoria. Nonostante cio' essa puo' risultare preferibile nei casi in cui vengano considerate prevalenti le prestazioni dell'I/O, in quanto presenta i seguenti pregi: - le specifiche istruzioni di I/O (INA,OUTA,...) possono essere ottimizzate e quindi risultare piu' efficienti che non le istruzioni di accesso alla memoria; - basta il numero minimo di linee nel bus di indirizzamento per selezionare il dispositivo di I/O (nel secondo caso l'interfaccia dovra' decodificare un maggiore numero di linee, tante quante sono presenti nel bus di indirizzamento della memoria); I vantaggi essenziali nel secondo caso (I/O bus memory mapped) sono: - maggiore semplicita' costruttiva dell'interconnessione, dovendosi realizzare un solo bus anziche' due (di memoria e di I/O); - maggiore semplicita' costruttiva della CPU, poiche' si accede all'I/O con le stesse istruzioni (LDA,STA,...) con cui si accede alla memoria e quindi non e' necessario prevedere apposite istruzioni di I/O con la relativa elettronica di decodifica e di esecuzione; - tutte le istruzioni di accesso alla memoria (e non solo LDA e STA) sono usabili sugli indirizzi di I/O; - un maggiore campo di indirizzamento per l'I/O. ⌡ 9.2 - I/O port Vediamo ora di capire come puo' essere organizzato l'I/O di dati da parte della CPU su uno specifico dispositivo. L'operazione dovra' essere guidata dalla CPU che svolge il ruolo di master nella transazione, pilotata dalle istruzioni appositamente predisposte dal programmatore. Ovviamente la CPU dovra' poter verificare se il dispositivo e' libero od occupato (in genere i dispositivi di I/O sono piu' lenti della CPU) prima di iniziare il trasferimento dei dati. L'operazione si svolgera' quindi in piu' passi, nei quali il bus e' usato per trasferire tra i registri dell'interfaccia (EC,ED=External Control, External Data), usati dal dispositivo di I/O, ed i registri operativi della CPU (A,B in figura) prima i bit di controllo e poi i dati (vedi freccia tratteggiata). ╔═══╗ <-------> B u s I n t e r n o ╔═══╗ Bus ║ ╠═══╦════╦═════╦═════╦═══════╦═══════╦════╣ ║ Adapter ╚═╦═╝ ║ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌──┴─┐ ┌──┴─┐ ╚═╦═╝
┌───┐ ┌───┐ │ I ├────┐ ║ │ / │ ED ├──╣ I │ O ├────┘ ║ / │ │ ║ O
║ ║ ║ ║
│ A │ │ B │ │ T │ │ IR │ │ PC │ M║ ┌───┤ M │ └─┬─┘ └─┬─┘ └─┬─┘ └──┬─┘ └────┘ e╠───┤ MD│ E │ └─┐ ┌─┘ │ ┌────┴────┐ m║ └───┤ M │ ┌─┴─┴─┐ ┌───┘ │ CONTROL │ o║ │ O │
│ P ├────┐ ║ ║ │ MUX │ │ │ UNIT │ r║ ┌───┤ R │ │ o │ EC ├──╣ B ║ └──┬──┘ │ └────┬────┘ y╠───┤MAR│ I │ │ r ├────┘ ║ u ║ ┌──┴────┴─┐ ┌─┴─┐ ║ └───┤ A │ │ t │ ║ s ║ │ ALU ├───────┤ F │ B║ └───┘ └───┘ ║ └────┬────┘ └─┬─┘ u║ EC=Controllo ╚═════════╩══════════════╝ s║ ED=Dato <--------> <-------------------------------------------> <----------> Sistema Unita' centrale Sistema di I/O (CPU) memoria La connessione dell'interfaccia (di un dispositivo) contenente i registri EC,ED all'I/O bus, si chiama I/O port. Il registro ED viene usato per il dato mentre il registro EC viene usato per l'I/O dei bit di controllo (READY, GO, ...): 7 6 ┌───┬───┬───┬───┬───┬───┬───┬────┐ ┌──────────────────┐ EC: │RDY│IEN│ERR│...non usati...│ GO │ ED: │ D A T O │ └───┴───┴───┴───┴───┴───┴───┴────┘ └──────────────────┘ <------------------- Registri dell'I/O port --------------------> ( RDY=Ready
IEN=Interrupt Enable
ERR=Errore
GO=Avvia I/O )
L'interfaccia contiene la logica di decodifica dell'indirizzo dell'I/O port presente sull'I/O bus, in modo da riconoscere due indirizzi che fa corrispondere ai due registri EC,ED dell'I/O port. Cosi' piu' dispositivi possono essere collegati alle linee dell'I/O bus, ma uno solo partecipera' alla transazione con la CPU: quello che riconoscera' come proprio l'indirizzo presente sull'I/O bus (messo dalla CPU che pilota la transazione sotto controllo del programma). ⌡ 9.2.1 - Programmazione dell'I/O La transazione dovra' essere diversa secondo che si esegue una operazione di input o di output: A) Procedura di output (esempio: stampante di caratteri collegata all'I/O port con indirizzi esadecimali C100H per ED e C101H per EC): CHROUT TST C101H 1) Attende BPL CHROUT cioe' che STA C100H 2) Carica il LDA #1 STA C101H 3) Carica il RTS
che la stampante sia disponibile (READY=1), abbia completato l'operazione precedente; dato in uscita dalla CPU nel registro ED: bit di GO nel registro di controllo EC
Il risultato e' quello di trasferire il dato dall'accumulatore A (della CPU) al registro ED (interfaccia della stampante), risolvendo il problema della sincronizzazione tra il dispositivo stampante (lento) e la CPU (veloce). B) Procedura di input programmato (es. lettura tastiera). Le istruzioni dovranno far compiere al computer le seguenti 3 operazioni (KBCS e KBDATA sono gli indirizzi dell'I/O port della tastiera): KBIN LDA #1 1) Set GO=1 per avviare il device (es. avvia il motore STA KBCS di trascinamento di un lettore a nastro di carta); WAIT TST KBCS 2) Attende che il dato sia immesso nel registro ED BPL WAIT (cioe' RDY=1); LDA KBDATA 3) Legge il dato, trasferendolo da ED al registro
ANDA #7FH RTS
interno della CPU (mascherando a 0 l'ottavo bit).
Il risultato e' quello di trasferire il dato dal registro ED (interfaccia della tastiera) all'accumulatore A (della CPU), risolvendo il problema della sincronizzazione tra il dispositivo tastiera (lento) e la CPU (veloce). Siccome la differenza di velocita' tra la CPU ed i dispositivi di I/O in genere e' molto grande, queste operazioni di I/O programmato comportano delle pause (per l'attesa del READY in EC) nell'attivita' della CPU, durante le quali la potenzialita' elaborativa della CPU viene totalmente persa nel loop di attesa. Da ora in poi ci adopereremo per introdurre vari accorgimenti mirati a ridurre l'inefficienza dovuta a questi loop di attesa (I/O con overlap, DMA, sistema d'interruzione, controller di I/O, canali e front-end computer). C) Procedura di input programmato in sovrapposizione con l'elaborazione: Se ci riferiamo all'esempio della routine KBIN, osserviamo che se, come sempre accade, la routine viene richiamata piu' volte per l'input di piu' bytes, l'avviamento del device potrebbe essere fatto, anziche' all'inizio della chiamata N alla routine, alla fine della chiamata precedente N-1, cosicche' il moto del motore puo' andare in sovrapposizione temporale con l'elaborazione che la CPU fa dopo la chiamata N-1. In tal modo all'inizio della chiamata N il motore e' gia' avanzato per il tempo T dell'elaborazione intercorsa tra le due chiamate consecutive N-1 ed N, e l'attesa del READY sara' quindi inferiore di un tempo T. Il costo da pagare per questo miglioramento e' solo una modifica delle istruzioni nel modo seguente: KBIN TST KBCS 1) Attende che il dato sia immesso nel registro ED BPL KBIN (cioe' RDY=1); LDA KBDATA 2) Legge il dato, trasferendolo da ED al registro ANDA #7FH interno della CPU (mascherando a 0 l'ottavo bit). LDA #1 3) Set GO=1 per avviare il device (es. avvia il motore STA KBCS di trascinamento di un lettore a nastro di carta) RTS per il successivo ciclo di lettura. Ovviamente questo metodo richiede l'inizializzazione iniziale all'avvio del programma principale con le istruzioni: LDA #1 STA KBCS
3) Set GO=1 per avviare il device (es. avvia il motore di trascinamento di un lettore a nastro di carta)
altrimenti la routine KBIN rimarrebbe bloccata nel loop iniziale di attesa del READY, che non arriverebbe mai perche' non e' stato dato il GO al device. ⌡ 9.3 - I/O Driver DRIVER DI CARATTERE: le routines KBIN e CHROUT sono due esempi di I/O drivers di carattere. Si chiama driver (software) una routine creata appositamente per pilotare l'I/O da uno specifico dispositivo. Naturalmente, essendo i vari dispositivi tutti diversi uno dall'altro, occorrono specifici drivers per i vari dispositivi. Nei casi piu' complessi i drivers possono essere ben piu' complicate dei due KBIN e CHROUT visti sopra. Per semplificare il problema agli utenti (e quindi vendere piu' facilmente i loro dispositivi) i vari costruttori si preoccupano di fornire anche i drivers necessari per usare piu' facilmente un dispositivo. Uso piu' facile significa che l'utente per stampare un carattere CHAR dovra'
semplicemente scrivere: LDA CHAR JSR CHROUT mentre per leggere un carattere bastera' scrivere: JSR CHRIN STA .... La complessita' insita nel pilotaggio della periferica e' tutta contenuta nei driver CHROUT, CHRIN per I/O di carattere, forniti dal costruttore. DRIVER DI LINEA: Oltre ai driver per I/O di un carattere si possono fornire anche i driver per I/O di una linea di caratteri. In tal caso l'utente dovra' fornire al driver l'indirizzo dove si trova memorizzata la stringa di caratteri da stampare (si pensi per esempio ad un messaggio di errore, memorizzato all'indirizzo MSG): LDX #MSG JSR LINEOUT ... ..... Il corrispondente driver di linea potra' essere come il seguente (dove vengono scaricati in output con la routine CHROUT vista prima, tutti i caratteri a partire da MSG in poi finche' non incontra il codice ASC 0 (null), che viene usato convenzionalmente come terminatore poiche' non puo' comparire in un testo di caratteri stampabili: LINEOUT LDA @X BEQ EXIT JSR CHROUT ADDX #1 BRA LINEOUT EXIT RTS
In memoria:
MSG FCC 'Errore 9' FCB 0 ...
Anche nel caso di I/O di una linea di caratteri l'utente dovra' fornire al driver l'indirizzo INBUF, dove andra' memorizzata la stringa di caratteri acquisiti dal dispositivo di input (per esempio dalla tastiera): LDX #INBUF JSR LINEIN ... ..... Il corrispondente driver di linea potra' essere come il seguente (dove il carattere ASC=13 (ritorno carrello) e' usato come terminatore di linea, secondo le usuali convenzioni dell'input da tastiera): LINEIN TFR X,Y ADDY #4 GETCHR JSR CHRIN STA @Y CMPA #13 BEQ EXIT CMPY @X BHS GETCHR ADDY #1 BRA GETCHR EXIT STY 2(X) LDA #10 JSR CHROUT
Trasferisce X-->Y In memoria: INBUF FCW INBUF+82 Y=inizio buffer RMB 82 Legge carattere ┌──────────┐ Store in buffer INBUF │End Buffer│<-- (X) e' Carriage Return? ├──────────┤ se si, termina input │Last Input│<--2(X) Buffer overflow? ├──────────┤ Si, non incrementa Y │ Buffer │<--4(X) No, incrementa Y │ ...... │ continua input (finche' =CR) │ │ Store puntatore Last Input └──────────┘ invia Line Feed
RTS I driver, essendo legati alle periferiche istallate nel sistema, fanno parte della configurazione del software di base della macchina e quindi vengono caricati automaticamente all'avvio del sistema (in CONFIG.SYS nei sistemi PC-IBM). ⌡ 9.4 - Direct Memory Access (DMA) Nel caso di periferiche particolarmente veloci l'I/O programmato puo' non essere adeguato per i seguenti motivi: - il tempo di esecuzione delle istruzioni del driver, se pur ottimizzate, puo' risultare troppo lungo (limiterebbe quindi le prestazioni di velocita' della periferica (per esempio un nastro magnetico); - anche se il tempo di esecuzione delle istruzioni del driver fosse adeguato, il grande volume di I/O della periferica occuperebbe una frazione troppo grande del tempo di CPU per operazioni 'stupide', come il semplice trasferimento di blocchi di dati tra CPU e periferica. E' allora possibile accoppiare alla CPU, dispositivo complesso in grado di eseguire operazioni sofisticate, un altro dispositivo piu' semplice, progettato appositamente per eseguire, in maniera ottimizzata e quindi piu' veloce della CPU, soltanto trasferimenti di blocchi di dati tra una periferica veloce e la CPU, in entrambe le direzioni (IN/OUT). Si tratta quindi di un dispositivo hardware aggiunto nel sistema, che consente all'interfaccia di una periferica di trasferire direttamente (senza intervento della CPU) dati in memoria ad alta velocita' (block transfer). Esso realizza un canale di trasferimento di dati che e' in grado di operare indipendentemente dalla CPU, cioe' mentre la CPU sta eseguendo altre elaborazioni. Si raggiungono cosi' entrambi gli obiettivi: - e' assicurata una velocita' di trasferimento adeguata a periferiche molto veloci; - la CPU non spende una frazione di tempo apprezzabile per il trasferimento dei dati. Lo schema di funzionamento e' il seguente: ╔═══╗ B u s I n t e r n o C P U ╔═══╗ Bus ║ ╠═══╦════╦═════╦═════╦═══════╦═══════╦════╣ ║ Adapter ╚═╦═╝ ║ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌──┴─┐ ┌──┴─┐ ╚═╦═╝
┌───┐ ┌───┐ │ I ├────┐ ║ ║ │ A │ │ B │ │ T │ │ IR │ │ PC │ M║ ┌───┤ M │ │ / │ ED ├──╣ I ║ └─┬─┘ └─┬─┘ └─┬─┘ └──┬─┘ └────┘ e╠───┤ MD│ E │ │ O ├────┘ ║ / ║ └─┐ ┌─┘ │ ┌────┴────┐ m║ └───┤ M │ │ │ ║ O ║ ┌─┴─┴─┐ ┌───┘ │ CONTROL │ o║ │ O │ │ P ├────┐ ║ ║ │ MUX │ │ │ UNIT │ r║ ┌───┤ R │ │ o │ EC ├──╣ B ║ └──┬──┘ │ └────┬────┘ y╠───┤MAR│ I │ │ r ├────┘ ║ u ║ ┌──┴────┴─┐ ┌─┴─┐ ║ └───┤ A │ │ t │ ║ s ║ │ ALU ├───────┤ F │ B║ └───┘ └───┘ ║ ║ └────┬────┘ └─┬─┘ u║ Sistema di Sistema ║ ╚═════════╩══════════════╝ s║ memoria di I/O ║ ║ ╔═╩═╗ Direct Memory Access ╔═╩═╗ ║ ╠══════╦═════════╦═════════╦═════════╦════╣ ║ ╚═══╝ ┌───┴───┐ ┌───┴───┐ ┌───┴────┐ │ ╚═══╝ │Memory │ │ Word │ │Function│ │ Registri DMA -->│Address│ │ Count │ │ Status │ │ Il DMA accede └───┬───┘ └───┬───┘ └───┬────┘ │ ai bus di I/O
└────┐ │ ┌────┘ │ e di memoria ┌──────┴────┴────┴────────┐ │ come Master │ D M A Controller ├─────┘ (come la CPU) └─────────────────────────┘ Essendo la memoria 'single port', DMA e CPU possono accedere uno per volta. Quindi l'accesso alla memoria del DMA puo' 'rubare' un ciclo alla CPU, ma la percentuale di rallentamento dell'elaborazione della CPU dovuta ad una concorrente attivita' del DMA controller e' piccola perche' la frequenza degli accessi del DMA e' limitata dal massimo flusso di dati sopportabile dalla periferica ('throughput'), che generalmente e' abbastanza inferiore alla frequenza di accesso della CPU (per esempio un'unita' magnetica che puo' trasferire 1 byte ogni 8 microsecondi rallenterebbe una CPU con ciclo di 1 microsecondo al massimo del 12.5%; un tale risultato non sarebbe ottenibile con I/O senza DMA, cioe' pilotato dalla CPU). PROGRAMMAZIONE DEL DMA: Da programma si carica l'indirizzo di memoria ed in word/byte count nei relativi registri del DMA (accedendovi come per gli altri registri degli I/O port). Poi si carica il registro di Function con la funzione di trasferimento (lettura/scrittura) e con il bit di avvio (GO). In quel momento parte l'attivita' di DMA mentre la CPU rimane libera di eseguire le successive elaborazioni (che non necessitino di dati in input via DMA, ovviamente), mentre il trasferimento viene eseguito dall'elettronica del DMA. Al termine dell'attivita' del DMA viene messo a 1 un apposito bit (DONE) nel terzo registro che in questa fase viene usato come registro di Status anziche' di Function. Questo bit puo' essere letto dalla CPU programmando un 'polling' periodico per controllare lo stato del DMA o meglio ancora puo' richiedere un'interruzione dell'attivita' corrente della CPU, che la forzera' (con le modalita' che vedremo tra poco) ad eseguire le istruzioni di 'Fine attivita' DMA'. Le istruzioni da eseguire alla fine dell'attivita' DMA sono essenzialmente due: 1) Lettura del registro di Status e verifica che il bit di errore sia 0; se e' 1 gli altri bit forniscono il codice di errore (per esempio fine del nastro magnetico) e si adotteranno le azioni conseguenti, precedentemente programmate; 2) Programmazione dei registri per il trasferimento del blocco di dati successivo o reset del registro di Status se il trasferimento e' completato. Un'applicazione particolare, degna di menzione, e' l'impiego del DMA per il display delle immagini 'memory mapped' sul monitor di un PC (l'immagine e' costruita in memoria e un canale DMA si occupa del 'refreshing' sul video). ⌡ 9.5 - Sistemi di I/O intelligenti Quando le operazioni di I/O sono piu' complesse di un semplice trasferimento come quello visto nel paragrafo precedente, puo' essere opportuno introdurre un dispositivo analogo al DMA, nel senso che puo' eseguire I/O come impostato dalla CPU ed indipendentemente da essa, ma piu' complesso, per esempio dotandolo di un microprocessore con proprio software (controller intelligente). Procedendo su questa strada si puo' arrivare a sostituire il controller dotato di microprocessore con un minicomputer o addirittura con un front-end computer, che nei centri di calcolo piu' grandi puo' anche essere un computer di medie dimensioni. Anche un questi casi la strategia di progetto e' quella di svincolare l'unita' centrale principale di calcolo da quelle operazioni piu' 'stupide', relative all'I/O, che
possono essere efficientemente eseguite da computer di piu' basso costo. In sintesi i vari sistemi di I/O possono essere elencati nello schema seguente: ┌─────────┐ Bus di memoria ┌──────────────┐ Bus di I/O ╔══════════╗ │ MEMORIA │<──────────────>│I/O Controller│<──────────────>║I/O Device║ └─────────┘ └──────────────┘ ╚══════════╝ ┌─────────┐ ╔═╗ │ ├────╩═╝ Trasferimento di dati eseguito ┌───┤ C P U ├─── ... dalla CPU in modo programmato │ │ ├────╦═╗ con o senza interruzioni │ └─────────┘ ╚═╝ │ │ ┌─────────┐ ╔═╗ Trasferimento a blocchi di dati ├───┤ D M A ├────║ ║ (non interrompibili) per unita' │ └─────────┘ ╚═╝ veloci │ │ ┌─────────┐ ╔═╗ │ │ CANALE │ ┌─╩═╝ Trasferimento a blocchi (come il ├───┤MULTIPLEX├──┤ ... DMA) da piu' periferiche non veloci │ │ │ └─╦═╗ in modo intelligente (conversioni, ┌─────────┐ │ └─────────┘ ╚═╝ gestione errori, packing,...) │ │ │ │ MEMORIA ├───┤ │ │ │ ┌─────────┐ ╔═╗ └─────────┘ │ │ CANALE ├────╩═╝ Idem per periferiche veloci ├───┤SELECTOR ├─── ... (dischi, nastri magnetici); si │ │ ├────╦═╗ connette ad una sola periferica │ └─────────┘ ╚═╝ durante il trasferimento │ │ ┌─────────┐ ╔═╗ │ │FRONT-END│ ┌─╩═╝ Computer separato, funzionante ├───┤PROCESSOR├──┤ ... come un canale, ma ancora piu' │ │ │ └─╦═╗ flessibile (es. minicomputer) │ └────┬────┘ ╚═╝ : │ ╔═╗ : │ ┌──────┐ ┌─╩═╝ : └──────┤CANALE├──┤ ... : └──────┘ └─╦═╗ ╚═╝ ⌡ 9.6 - Sottosistemi a dischi Un esempio di sistema di I/O intelligente e' il sottosistema a dischi dove attraverso un'interfaccia non intelligente (Host Adapter) si collegano all'host computer fino a 8 controller intelligenti (cioe' dotati di microprocessore comandato da un apposito programma su ROM (firmware) che gli fa eseguire una serie di comandi di accesso ai dischi, che l'utente invia dall'host computer in forma codificata (come per le istruzioni di macchina). Gli N<=8 controller, con 2 hard disk drive ciascuno, sono collegabili in configurazione 'daisy chain'. Attraverso apposite linee viene indirizzato il controller ed il drive, in un tempo tipico di 50ns, che attiva il BUSY per tutta la durata della operazione: Indirizzo SEL
──────────┐ ┌─────────── └──────────────────────────┘ ──────────┐ ┌───────────
BUSY
└──────────────────────────┘ ─────────────────────┐<-- no limit --> <--50ns-->└───────────────────────────
╔═══════╗ ┌──────────┐ ┌───────┐ ╔════════════╗ ║Drive 0║ │ HOST │ Computer │ Host │ SASI Bus ║ Disk ╠────╩═══════╝ │ COMPUTER │<────────>│Adapter│<────┬────>║Controller 0╠────╦═══════╗ └──────────┘ Bus └───────┘ │ ╚════════════╝ ║Drive 1║ │ ╚═══════╝ │ │ ╔═══════╗ │ ╔════════════╗ ║Drive 0║ │ ║ Disk ╠────╩═══════╝ ├────>║Controller 1╠────╦═══════╗ │ ╚════════════╝ ║Drive 1║ : ╚═══════╝ : : .......... ...... │ ┌───────┴──────┐ │Bus Terminator│ └──────────────┘ La codifica dei comandi riconosciuti dal controller avviene secondo il seguente modello: 7 6 5 4 3 2 1 0 ┌──┬──┬──┬──┬──┬──┬──┬───┐ ┐ Byte 0 │ Class │ O P. C O D E │ │ └──┴──┴──┴──┴──┴──┴──┴───┘ │ C ┌──┬──┬──┬──┬──┬──┬──┬───┐ │ Byte 1 │N.Unita'│ H I G H │ │ O └──┴──┴──┴──┴──┴──┴──┴───┘ │ ┌──┬──┬──┬──┬──┬──┬──┬───┐ │ M Byte 2 │ MIDDLE A D D R E S S │ │ └──┴──┴──┴──┴──┴──┴──┴───┘ │ M <--- from Host Adapter ┌──┬──┬──┬──┬──┬──┬──┬───┐ │ Byte 3 │ L O W │ │ A └──┴──┴──┴──┴──┴──┴──┴───┘ │ ┌──┬──┬──┬──┬──┬──┬──┬───┐ │ N Byte 4 │ INTERLEAVE/BLOCK COUNT │ │ └──┴──┴──┴──┴──┴──┴──┴───┘ │ D ┌──┬──┬──┬──┬──┬──┬──┬───┐ │ Byte 5 │ DRIVE O P T I O N S │ │ └──┴──┴──┴──┴──┴──┴──┴───┘ ┘ 7 6 5 4 3 2 1 0 ┌──┬──┬──┬──┬──┬──┬──┬───┐ STATUS │N.Unita'│ 0 0 0 Err 0 │ <---- from controller └──┴──┴──┴──┴──┴──┴──┴───┘ L'indirizzo di 21 bit inviato dall'host computer e' un indirizzo logico che corrisponde all'indirizzo fisico di un settore del disco, che contiene il blocco di dati da trasferire. Il trasferimento avviene per il numero di blocchi (consecutivi) indicato dal block count (byte 4), analogamente a quanto avveniva con il DMA. Il trasferimento e' pilotato dal microprocessore presente nel controller, che risponde al comando ricevuto con i 6 bytes dall'host adapter. Al termine dell'operazione si puo' leggere il registro di STATUS per verificare che non ci siano stati errori (analogamente a quanto avveniva con il DMA controller).
Un disco 'vede' una superficie magnetica per ogni testina di lettura/scrittura (HEAD); ogni superficie magnetica viene suddivisa in tante tracce quante sono le possibili posizioni (discrete) della testina. Per ognuna di queste posizioni (tracce), tenendo conto che ci sono piu' testine, definisce un 'cilindro', fatto di tante tracce allineate nello spazio sulla superficie di un cilindro, quante sono le testine. Ogni traccia viene poi suddivisa in settori, che costituiscono il blocco elementare trasferibile. Da cio' deriva la definizione di indirizzo logico di un blocco di dati e la corrispondenza all'indirizzo fisico e' fissata dalla formula seguente (tra parentesi sono riportati dati esemplificativi per un hard disk da 40Mb, con 512 bytes/settore): Indirizzo Logico = ( CYADR * HDCYL + HDADR ) * SETRK + SEADR │ │ │ │ │ Cylinder Address ───┘ │ Head Address │ └─ Sector Address (0..610) N.Heads (0..3) N.Sectors (0..31) per cilindro per traccia (4) (32) Nel modello di programmazione sono previsti anche: a) l'interleave, che permette di ottimizzare la velocita' di trasferimento, definendo come indirizzi fisici contigui quelli contigui (interleave=0) o a settori alterni (interleave=1), ecc.; b) le 'drive options' che permettono di montare nello stesso sottosistema a dischi dei drives di costruttori diversi, con caratteristiche diverse. Comandi tipici inviabili al controller sono: Read, Write, Seek, Recalibrate, Format track, Format bad track, Sense status, Diagnostic, ecc. (l'elenco completo e le spiegazioni dettagliate si trovano nel manuale del controller). ⌡ 9.7 - Sistema d'interruzione Un progresso sostanziale nel sistema di I/O si e' avuto con l'introduzione del sistema di interruzione. L'I/O programmato con sovrapposizione riduce ma non elimina le pause nella attivita' della CPU, in attesa che arrivi il READY da parte del dispositivo di I/O. Questa attesa con dispositivi lenti, come per esempio le stampanti, comporta uno spreco rilevante della capacita' di elaborazione della CPU. Sarebbe desiderabile poter impiegare quel tempo di attesa per fare qualche elaborazione utile, anziche' eseguire soltanto le istruzioni del loop di attesa del READY. Cio' e' possibile con l'introduzione del sistema d'interruzione. Questo, nel caso di un output su stampante, per esempio, consente alla CPU di eseguire altre elaborazioni durante l'attesa del READY, seguente all'output del primo carattere. Queste elaborazioni saranno poi interrotte temporaneamente all'arrivo del READY: la CPU eseguira' automaticamente un salto ad un'apposita routine, predisposta dall'utente, che inviera' il carattere successivo alla stampante e poi ritornera', con un altro salto (eseguito con un'apposita istruzione RTI) al punto dove l'elaborazione era stata interrotta e continuera' come se l'interruzione non ci fosse stata. Cosi' si evitano le numerosissime esecuzioni delle istruzione nei loop di attesa del READY e la CPU risulta lavorare in modo decisamente piu' efficiente. Per realizzare tutto questo e' necessario pero' apportare delle aggiunte e delle modifiche all'hardware del semplice sistema di elaborazione H6809 finora da noi introdotto. In particolare il complesso dei circuiti che consentono di interrompere l'attivita' di elaborazione in corso al verificarsi di un evento prestabilito (INTERRUPT), forzando il computer ad eseguire una o piu' routines
prefissate (routines di gestione dell'interruzione). L'HARDWARE per poter funzionare (essendo l'interruzione e quindi il salto, cioe' la modifica del PC, eseguito automaticamente via hardware), deve assicurare: - il salvataggio automatico del PC (Program Counter), per poter ritornare all'elaborazione interrotta al termine della routine di servizio; - la disabilitazione del sistema d'interruzione (altrimenti la CPU entrerebbe in un loop infinito, interrompendosi di nuovo alla prima istruzione seguente); - un metodo per il riconoscimento della sorgente dell'interruzione (per scegliere la routine di servizio appropriata, tra quelle disponibili). Il SOFTWARE predisposto dall'utente dovra' poi provvedere a: - salvare in apposite locazioni di memoria (meglio se in uno stack) quella parte dello stato della macchina che verra' usato durante il servizio dell'interruzione (registri, maschere d'interruzione, registri di memory mapping, registri dei controller di I/O, ecc. purche' usati nella routine di servizio dell'interruzione, altrimenti non serve salvarli, perche' restano come inalterati durante l'interruzione); - identificare e servire l'interruzione (con la routine di servizio apposita; - ripristinare lo stato della macchina e riprendere l'elaborazione interrotta. ⌡ 9.7.1 - Gestione delle priorita' nel sistema d'interruzione Durante l'acquisizione dei dati in un esperimento di fisica delle alte energie, non e' ammissibile che il sistema di computer venga interrotto per operazioni meno importanti come l'invio di dati ad una stampante (nulla di male se la stampa e' un po' ritardata per privilegiare l'acquisizione dei dati sperimentali, perche' questo riduce i tempi morti dello apparato sperimentale). Per permettere questo e' necessario attribuire alle varie sorgenti di interruzione differenti priorita', nel senso che una sorgente di priorita' superiore potra' interrompere l'esecuzione di una routine di servizio di un'interruzione di livello inferiore, ma non viceversa. Uno dei modi in cui questo schema di priorita' puo' realizzarsi e' mostrato nella figura seguente: C P U ( generica ) ┌───────────────────────────┐ │ │ │ M A S K B I T S │ │ ┌─────┬─────┬─────┬──── │ │ │ 1 │ 0 │ 0 │.... │ <══ Mask! │ └──┬──┴──┬──┴──┬──┴──── │ ┌────┐ │ │ │ │ │ 1│ ├─────────────────────┐ │ │ ┌────────────────────<──Level 1───┤ OR ├───────────┐ │ │ │ │ │ │ │ │ ├─┐ │ │ │ │ │ │ ┌──────────────<──Level 2...└────┘ │ │ │ │ │ │ │ │ │ │ │0 │0 │1 │ │ │ │ │ │ ┌────────<──Level 3... ╔═══│═══╗ ╔═══│═══╗ ╔═══│═══╗ │ │ │ │ │ │ │ │ ║ ┌─┴─┐ ║ ║ ┌─┴─┐ ║ ║ ┌─┴─┐ ║ │ │ │ │ │ │ │ ........ ecc..... ║ │AND│ ║ ║ │AND│ ║ ║ │AND│ ║ │ ┌┴─┴┐ ┌┴─┴┐ ┌┴─┴┐ │ ║ └┬─┬┘ ║ ║ └┬─┬┘ ║ ║ └┬─┬┘ ║ │ │AND│ │AND│ │AND│ .....│ ║ ┌┴┬┴┬─║ ║ ┌┴┬┴┬─║ ║ ┌┴┬┴┬─║
│ └─┬─┘ └─┬─┘ └─┬─┘ │ Mask! ══> ║ │0│1│ ║ ║ │1│0│ ║ ║ │1│1│ ║ │ 1└────┐│┌────┘0 │ ║ └┬┴┬┴─║ ║ └─┴─┴─║ ║ └─┴─┴─║ │ ┌┴┴┴┐ │ ║RDY IEN║ ║RDY IEN║ ║RDY IEN║ │ │ OR│ │ ╚═══════╝ ╚═══════╝ ╚═══════╝ │ └─┬─┘ │ Device #1 Device #2 Device #3 │ 1│ ┌─<──IEN CPU │ <══ Mask! │ ┌┴─┴┐ │ │ │AND│ │ │ └─┬─┘ │ │ │ │ │ Segnale d'interruzione │ │ ====================== │ └───────────────────────────┘ In questo schema sono previste 3 possibilita' di mascheramento: ============================== 1) delle singole sorgenti d'interruzione, mettendo a zero il bit IEN (Interrupt Enable) nel registro di controllo dell'I/O port del device; 2) dei singoli livelli di priorita', a cui sono collegati tutti i device con la stessa priorita': per questo basta mettere a zero il mask bit corrispondente nel registro di mascheramento delle interruzioni, accessibile con un suo specifico indirizzo (device code o memoria nei sistemi con I/O memory mapped). 3) dell'intero sistema d'interruzione, mettendo a zero il bit IEN CPU, da parte della CPU, accessibile con un suo specifico indirizzo (device code o memoria nei sistemi con I/O memory mapped). Sotto controllo del software i Mask Bits permettono di abilitare o disabilitare i vari LIVELLI di priorita'. All'interno di un livello le interruzioni possono essere gestite con priorita' fisse (obbedendo ad una data gerarchia) od omogenee (schema 'Round Robin'). In entrambi i casi l'identificazione del device da servire puo' avvenire mediante uno dei modi seguenti: - POLLING, cioe' mediante scansione dello stato dei devices: in questo caso al termine dell'esecuzione dell'istruzione corrente, la Control Unit, dopo aver salvato PC e PSW, produce un salto ad una locazione prestabilita, dove saranno state caricate dall'utente apposite istruzioni di test sui registri di controllo degli I/O port che possono essere responsabili dell'interruzione; queste saranno le prime istruzioni da eseguire dopo l'interruzione e determineranno il salto alla giusta routine di servizio. In caso di interruzioni simultanee da piu' devices la priorita' sara' determinata dall'ordine in cui e' eseguito il polling. - interruzioni VETTORIZZATE: in questo caso al termine dell'esecuzione dell'istruzione corrente, la Control Unit, dopo aver salvato PC e PSW, produce un salto ad una locazione diversa per ogni livello d'interruzione. Quindi viene eseguito direttamente il salto alla giusta routine di servizio, senza la perdita di tempo del polling. Per esempio il DEC PDP11 aveva priorita' fissate 'geograficamente', cioe' in base alla prossimita' sul bus alla CPU, e interruzioni vettorizzate sulle prime 64 double words, contenenti i nuovi valori di PC e PSW, Program Counter e Processor Status Word (analogo al Control Code CC del M6809). Il M6809 e' un esempio di sistema con interruzioni vettorizzate su tre livelli di priorita', mostrati nello schema seguente (analogo a quello precedente):
C P U M6809 ┌───────────────────────────┐ MASK BITS: I=IRQ Interrupt Mask │ Control Code CC │ ========= F=FIRQ Fast Interrupt Mask │ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │ (0=enabled) │ │E │F │H │I │N │Z │V │C │ │ │ └──┴┬─┴──┴┬─┴──┴──┴──┴──┘ │ ┌────┐ │ │ │ │ 1│ ├─────────────────────┐ │ │ ┌────────────────────<── IRQ ─────┤ OR ├───────────┐ │ │ │ │ │ │ │ ├─┐ │ │ │ │ │ │ ┌──────────────<── FIRQ ... └────┘ │ │ │ │ │ │ │ │ │ │0 │0 │1 │ │ │ │ │ ┌─────────<── NMI .... ╔═══│═══╗ ╔═══│═══╗ ╔═══│═══╗ │ │ │ │ │ │ │ ║ ┌─┴─┐ ║ ║ ┌─┴─┐ ║ ║ ┌─┴─┐ ║ │ │ │ │ │ │ │ ║ │AND│ ║ ║ │AND│ ║ ║ │AND│ ║ │ ┌┴─┴┐ ┌┴─┴┐ │ │ ║ └┬─┬┘ ║ ║ └┬─┬┘ ║ ║ └┬─┬┘ ║ │ │AND│ │AND│ │ │ ║ ┌┴┬┴┬─║ ║ ┌┴┬┴┬─║ ║ ┌┴┬┴┬─║ │ └─┬─┘ └─┬─┘ │ │ ║ │0│1│ ║ ║ │1│0│ ║ ║ │1│1│ ║ │ └────┐│┌────┘ │ ║ └┬┴┬┴─║ ║ └─┴─┴─║ ║ └─┴─┴─║ │ │││ │ ║RDY IEN║ ║RDY IEN║ ║RDY IEN║ │ │││ │ ╚═══════╝ ╚═══════╝ ╚═══════╝ │ │││ │ Device #1 Device #2 Device #3 │ │││┌─────────────<── Reset │ ││││┌────────────<── SWI,SWI2,SWI3 (Software interrupts) │ ┌┴┴┴┴┴┐ │ │ │ OR │ │ │ └──┬──┘ │ │ │ │ │ Segnale d'interruzione │ │ ====================== │ └───────────────────────────┘ Le 1) 2) 3)
operazioni eseguite AUTOMATICAMENTE all'interruzione IRQ sono: Salva tutti i registri (escluso SP) nello stack; Disabilita il corrispondente mask bit (I=1); Pone E=1 (Entire state on stack, cioe' l'intero stato della macchina e' stato salvato nello stack; 4) Esegue JMP (FFF8H), cioe' salto indiretto al vettore di IRQ. In FFF8..FFF9H dev'essere contenuto l'indirizzo della routine di servizio (interruzione vettorizzata). Al termine della routine di servizio dell'interruzione IRQ, eseguendo un'apposita istruzione RTI (Return from Interrupt), vengono eseguite AUTOMATICAMENTE le operazioni inverse, ripristinando lo stato della macchina e riprendendo l'elaborazione interrotta. ╔═══════════════════════════════════════════════════════════════╗ ║ INTERRUZIONI HARDWARE NEL M6809 (inclusi Traps) ║ ║───────────────────────────────────────────────────────────────║ ║ Interrupt Vettor Address Mask Bit Registri salvati (Stack)║ ║───────────────────────────────────────────────────────────────║ ║ IRQ FFF8H I PC,U,Y,X,DPR,B,A,CC ║ ║ FIRQ FFF6H F PC,CC ║ ║ NMI FFFCH nessuno PC,U,Y,X,DPR,B,A,CC ║ ║ Reset FFFEH nessuno nessuno ║ ╠═══════════════════════════════════════════════════════════════╣ ║ INTERRUZIONI SOFTWARE NEL M6809 (System Calls) ║
║───────────────────────────────────────────────────────────────║ ║ Interrupt Vettor Address Mask Bit Registri salvati (Stack)║ ║───────────────────────────────────────────────────────────────║ ║ SWI FFFAH nessuno PC,U,Y,X,DPR,B,A,CC ║ ║ SWI2 FFF4H nessuno PC,U,Y,X,DPR,B,A,CC ║ ║ SWI3 FFF2H nessuno PC,U,Y,X,DPR,B,A,CC ║ ╚═══════════════════════════════════════════════════════════════╝ Come vengono salvati i registri nello stack al verificarsi di un'interrupt IRQ e' mostrato nello schema seguente: PRIMA DELL'INTERRUPT:
DOPO L'INTERRUPT:
Indir. Memoria ├──────┤ 1234 │ LDB# │ ├──────┤ 1235 │ 81 │ ├──────┤ ┌──────┐ 1236 │ LDA# │<──── │ 1236 │ PC ├──────┤ └──────┘ 1237 │ 17 │ ├──────┤ 1238 │ STA │ ├──────┤ 1239 │ 34 │ ├──────┤ 123A │ 56 │ ├──────┤ : : : : ├──────┤ ┐ 2345 │ LDA │ │ ├──────┤ │ 2346 │ FF │ │ Routine ├──────┤ │ 2347 │ 01 │ │ di ├──────┤ │ : : │ Servizio : : │ ├──────┤ │ 2381 │ RTI │ │ ├──────┤ ┘ : :
Indir. Memoria ├──────┤ 1234 │ LDB# │ ├──────┤ 1235 │ 81 │ ├──────┤ 1236 │ LDA# │ ├──────┤ 1237 │ 17 │ ├──────┤ 1238 │ STA │ ├──────┤ 1239 │ 34 │ ├──────┤ 123A │ 56 │ ├──────┤ : : : : ├──────┤ ╔══════╗ 2345 │ LDA │<──── ║ 2345 ║ PC ├──────┤ ╚══════╝ 2346 │ FF │ ├──────┤ 2347 │ 01 │ ├──────┤ : :
N.B. EFF3..EFFFH=Area di stack : : ============= ├──────┤ ┐ EFF3 │ │ │ ├──────┤ │ EFF4 │ │ │ ┌────┐ ├──────┤ │ │ 08 │ CC EFF5 │ │ │ └────┘ ├──────┤ │ ┌────┐ EFF6 │ │ │ │ FF │ A ├──────┤ │ └────┘ EFF7 │ │ │ ┌────┐ ├──────┤ │ │ 81 │ B EFF8 │ │ │ └────┘ ├──────┤ │ ┌────┐
N.B. Variati PC,S,CC ! ===============
: ╔══════╗ ├──────┤ <──── ║ EFF3 ║ S EFF3 │ 88 │ CC ╚══════╝ ├──────┤ EFF4 │ FF │ A ╔════╗ ├──────┤ ║ 98 ║ CC EFF5 │ 81 │ B ╚════╝ ├──────┤ ┌────┐ EFF6 │ 00 │ DPR │ FF │ A ├──────┤ └────┘ EFF7 │ 24 │ ┌────┐ ├──────┤ X │ 81 │ B EFF8 │ 68 │ └────┘ ├──────┤ ┌────┐ :
EFF9 │ │ │ │ 00 │ DPR ├──────┤ │ └────┘ EFFA │ │ │ ┌──────┐ ├──────┤ │ │ 2468 │ X EFFB │ │ │ └──────┘ ├──────┤ │ ┌──────┐ EFFC │ │ │ │ 369C │ Y ├──────┤ │ └──────┘ EFFD │ │ │ ┌──────┐ ├──────┤ │ │ 3A21 │ U EFFE │ │ │ └──────┘ ├──────┤ │ ┌──────┐ EFFF │ 3A │<────│ EFFF │ S ├──────┤ ┘ └──────┘ : : : : ├──────┤ ┐ FFF8 │ 23 │ │ Interrupt ├──────┤ │ Vector FFF9 │ 45 │ │ ├──────┤ ┘ : :
EFF9 │
36 │ │ 00 │ DPR ├──────┤ Y └────┘ EFFA │ 9C │ ┌──────┐ ├──────┤ │ 2468 │ X EFFB │ 3A │ └──────┘ ├──────┤ U ┌──────┐ EFFC │ 21 │ │ 369C │ Y ├──────┤ └──────┘ EFFD │ 12 │ ┌──────┐ ├──────┤ PC │ 3A21 │ U EFFE │ 36 │ └──────┘ ├──────┤ EFFF │ 3A │ ├──────┤ : : : : ├──────┤ FFF8 │ 23 │ ├──────┤ FFF9 │ 45 │ ├──────┤ : :
L'utente programmatore ha predisposto la routine di servizio della interruzione IRQ in un indirizzo di memoria (2345H) e poi ha caricato questo indirizzo in FFF8H (vettore d'interruzione): all'arrivo della interruzione IRQ la control unit provvede al salvataggio dell'intero stato della macchina nello stack come indicato nello schema e poi esegue il JMP indiretto a questo indirizzo FFF8H (cioe' salta alla routine di servizio dell'interruzione, caricando il vettore di interruzione 2345H in PC). Nei casi in cui la routine di servizio usa solo 1 o 2 soli registri e' conveniente usare l'interruzione FIRQ che salva solo PC e CC (che non si puo' fare a meno di salvare), salvando con apposite istruzioni messe dal programmatore i registri usati dalla routine di servizio (e quindi alterati). In questo modo si puo' ottenere una maggiore velocita' di servizio dell'interruzione: per questo si chiama Fast Interrupt Request (FIRQ). ⌡ 9.7.2 - Programmazione delle interruzioni L'uso del sistema di interruzione sara' fondamentale per l'introduzione dei sistemi operativi multitask e multiutente. Per capire meglio come funzionano e' bene comprendere le modalita' di funzionamento della CPU in ambiente di interruzioni multiple. Per questo mostreremo come si deve impostare il programma che esegue I/O con interruzioni. Le modalita' di I/O sotto interruzione sono sostanzialmente diverse da quelle viste con l'I/O programmato senza interruzioni nel ⌡ 9.3 (I/O Driver). Ora descriveremo la sequenza delle operazioni e poi mostreremo la sequenza di istruzioni che occorre programmare. Le operazioni da eseguire sono diverse per output e per input. Percio' esamineremo ora separatamente prima l'output e poi l'input. Le operazioni del computer andranno organizzate nel modo che ora descriveremo. OUTPUT Esempio: Stampa di caratteri (con priorita' N) ====== (riferirsi all'I/O driver seguente) 1) Durante l'elaborazione, con delle chiamate ad un'apposita routine
(driver di stampa), si caricano i dati di output in un'apposita OUTBUF area di memoria OUTBUF (buffer di output), in modo analogo a quanto visto nel ⌡9.3 (I/O Driver), avendo cura di memorizzare anche la fine del buffer (per evitare di riempirlo oltre il limite massimo) ed il puntatore all'ultima locazione riempita in scrittura (per sapere fin dove va scaricato sul dispositivo di output); questo puntatore potrebbe essere sostituito da un dato convenzionale scritto alla fine (per es. 0='tappo'). 2) Finito il riempimento del buffer si accerta la disponibilita' (READY=1) del dispositivo di output e si avvia l'output del primo carattere, con attivazione della relativa interruzione (IEN=1 sull'I/O port e Mask Bit=1), e s'inizializza sia un puntatore del dato inviato in output (serve per prelevare il dato successivo ad ogni interruzione), che un flag software BUSY=1, che segnala al driver che l'output buffer e' impegnato. Poi si continua l'elaborazione. 3) Al completamento dell'output, la stampante rimette READY=1 nel registro di controllo dell'I/O port e questo genera un'interruzione, essendo IEN=1, purche' il relativo Mask Bit sia =1 (risulta =0 se il READY=1 della stampante arriva durante l'esecuzione della routine di servizio di una interruzione di priorita' > N ) 4) L'interruzione viene ricevuta dalla CPU, che interrompe l'attivita' di elaborazione corrente (salvando quanto dovuto) e salta alla routine di servizio dell'interruzione, che per prima cosa posiziona i Mask Bits in modo da lasciare disabilitate le interruzioni di priorita'<=N, ma abilitate le interruzioni di priorita'>N (da questo momento i dispositivi collegati alle priorita' d'interruzione>N potranno interrompere la routine di servizio della stampante). Poi si preleva dal buffer OUTBUF il carattere seguente da stampare, si incrementa il relativo puntatore e lo si invia all'I/O port della stampante. Se invece l'ultimo carattere presente nel buffer era quello inviato alla stampante precedentemente, provvede ad azzerare il flag software BUSY=0 e disattivale IEN dell'I/O port.
┌───────────┐ │End Buffer │ ├───────────┤ │Last Output│ ├───────────┤ │ Buffer │ │ ...... │ │ │ └───────────┘
5) Infine si esegue l'istruzione RTI (Return from Interrupt) che ripristina i registri precedentemente salvati nello stack, e si riprende l'elaborazione interrotta. Esempio di programmazione dell'I/O driver con interruzioni * --- ESEMPIO DI PROGRAMMA PRINCIPALE --MAIN .... CLRA STA BUSY Set software flag LOOP .... LDX# MSG1 JSR OUTA Output messaggio 1 .... LDX# MSG2 JSR OUTA Output messaggio 2 .... JMP LOOP * AREA DATI: MSG1 FCC 'END OF PAPER' FCB 0 TAPPO MSG2 FCC 'ANALISI COMPLETATA' FCB 0 TAPPO END MAIN * * --- INIZIO OUTPUT DRIVER CON INTERRUZIONI --ORG FFF8H Interruzione IRQ FCW SERVO Vettore interruzione ORG 1000H Locazione di caricamento dell'I/O driver CADDR EQU FF02H I/O Port registers DADDR EQU FF03H BUFPNT RMW 1 Variabili usate da driver BUSY RMB 1 * * --- SUBROUTINE DI AVVIO DEL PROCESSO DI OUTPUT (E ATTIVA INTERRUZIONE) --OUTA TST BUSY Test software flag <---- Entry point BNE OUTA Attesa che output precedente sia terminato STX BUFPNT LDA# FFH STA BUSY Set software flag (output in corso) LDA@ X STA DADDR LDA# 41H Set IEN=1, GO=1 STA CADDR RTS * * --- ROUTINE DI SERVIZIO INTERRUZIONE --SERVO LDX BUFPNT Tutti registri gia' salvati (IRQ) ADDX #1 STX BUFPNT LDA@ X BEQ TAPPO STA DADDR Carica registro DATI dell'I/O port LDA# 41H Set IEN=1, GO=1 STA CADDR Carica registro CONTROLLO dell'I/O port RTI Ritorno dall'interruzione * * --- TERMINA PROCESSO DI OUTPUT (E DISATTIVA INTERRUZIONE) --TAPPO CLRA Clear IEN STA CADDR Carica registro CONTROLLO dell'I/O port
STA RTI
BUSY
Clear software flag (output terminato) Ritorno dall'interruzione
Descriviamo ora il caso dell'input: INPUT: Esempio: Input di caratteri da tastiera (priorita' N) ====== (riferirsi all'I/O driver seguente) 1) Si riserva un'area di memoria INBUF come buffer di memorizzazione temporanea dei dati in arrivo dalla tastiera, in modo analogo a quanto fatto nel caso dell'output; in essa la routine di servizio dell'interruzione trasferira' i caratteri ricevuti da tastiera; ┌───────────┐ si avra' anche cura di memorizzare la INBUF │End Buffer │ fine del buffer 'End Buffer' (per evi├───────────┤ tare di riempirlo oltre il limite mas│Last input │ simo); ├───────────┤ 2) Durante l'elaborazione, con una │Destination│ chiamata ad un'apposita routine INA ├───────────┤ (driver di input), analoga alla OUTA │ Buffer │ vista nell'esempio precedente (la A │ ...... │ finale sta per alfanumerico; ci potreb│ │ bero essere, per esempio, anche i driver └───────────┘ OUTD, OUTH e IND, INH per gli input decimali ed esadecimali), si eseguono le seguenti operazioni: - Si verifica prima che non sia BUSY=1, cioe' che sia gia' attivo un precedente processo di input (in tal caso attende che diventi BUSY=0); - si inizializza quindi il processo di l'input: a) caricando nell'apposita area di memoria INBUF (buffer di input) l'indirizzo di destinazione, in cui la routine di servizio trasferira' i dati all'arrivo del terminatore dell'input, solitamente un CR=Carriage Return (oltre a disattivare l'interruzione ed azzerare il software flag BUSY, che indica all'input driver che il processo di input non e' piu' in corso); b) si inizializza il puntatore Last input con l'indirizzo della locazione immediatamente precedente la prima locazione del buffer; c) si avvia l'input mettendo IEN=1 e GO=1 nel registro di controllo del'l'I/O port della tastiera e si termina la subroutine INA con un RTS (Return from Subroutine). 3) A questo punto l'elaborazione procede mentre il sistema d'interruzione colloca in INBUF tutti i caratteri che arrivano da tastiera. 4) Il processo di input termina quando da tastiera arriva il carattere terminatore
(Carriage Return); se il driver di input e' IND o INH (input decimale o esadecimale) viene eseguita la conversione dei caratteri in INBUF nei 2 o 4 bytes che rappresentano il numero, prima di trasferirlo all'indirizzo di destinazione (o segnala errore se la conversione fallisce per input errato). ⌡ 9.7.3 - Condivisione di istruzioni e dati La tecnica di interrompere l'elaborazione corrente in punti non prestabiliti a priori (perche' determinati dal momento di arrivo dell'interruzione da parte del dispositivi di I/O) pone dei problemi specifici, cui e' opportuno far fronte in maniera sistematica e non episodica. Ci riferiamo in particolare ai seguenti problemi: 1) l'uso nella routine di servizio dell'interruzione di subroutines di uso generale, quindi usabili potenzialmente anche nel programma principale (interrompibile) introduce il problema della 'rientranza' di un segmento di programma. Si tratta della necessita' che, se durante l'esecuzione di una subroutine arriva un'interruzione, durante il servizio della quale viene fatta una chiamata a quella stessa (rientranza) subroutine interrotta, questa elaborazione non comprometta l'integrita' dei dati e delle istruzioni, come erano prima dell'interruzione. Questo pericolo infatti e' reale poiche' se nella prima chiamata alcuni dati erano stati salvati in apposite locazioni di memoria, queste verrebbero sovrascritte nella seconda chiamata, cosicche' al ritorno dall'interruzione il programma principale si verrebbe a trovare dati errati in quelle locazioni. Questo problema si puo' risolvere, per esempio, salvando i nuovi dati (se sono pochi) in registri, che sono salvati durante la interruzione o in uno stack, dove con successivi PUSH si salvano in diverse locazioni di memoria i nuovi dati, senza distruggere quelli salvati precedentemente, poiche' il registro Stack Pointer viene salvato durante l'interruzione. 2) Dato che anche le istruzioni del programma potrebbero essere modificate (si pensi agli operandi) durante l'esecuzione, per evitare i problemi sopra discussi e scrivere un programma che goda del pregio della 'rientranza', occorre che esso sia scritto con metodi appropriati ed istruzioni (metodi di indirizzamento) non automodificantesi durante l'esecuzione. Riprenderemo comunque queste problematiche nel prossimo capitolo, quando parleremo delle elaborazioni 'in concorrenza' tra loro. ⌡ 9.7.4 - Interruzioni interne Le interruzioni interne sono quelle prodotte dalla stessa CPU, anziche' da un dispositivo di I/O. Distinguiamole secondo che siano generate dall'hardware o dal software, cioe' dall'esecuzione di apposite istruzioni. A) Traps (generate via hardware): al verificarsi di eventi eccezionali quali: - Overflow o underflow durante l'elaborazione; - Errori software che intendono eseguire istruzioni proibite (non esistenti, fuori memoria, violazione di protezioni, salto
nell'area occupata dal sistema operativo, ecc.); - Errori hardware (errori di parita'. power fail, ecc.). - System Reset attivato dall'operatore. Il riconoscimento del tipo di evento e' consentito dai differenti trap vectors e/o dal salvataggio nello stack di un apposito identificatore. B) System calls o Software interrupts (generati via software): Molti processori possono generare interruzioni su se stessi mediante apposite istruzioni (esempi sono: SWI, SWI2 E SWI3 disponibili nel M6809). Esse mettono a disposizione un comodo modo (l'unico in sistemi con Memory Mapping and Management perche' un salto nell'area occupata dal sistema operativo genererebbe un Trap, ecc.) di accedere alle utilities del sistema operativo (di qui il nome di System calls. Salvando nello stack un indice usabile in una tabella di indirizzi di routines di utilita' si puo' accedere a molte routines di servizio. !----------------------------------------------------------------------FINE MODULO A