LA GESTIONE DELL’INPUT/OUTPUT Introduzione al colloquio di I/O Lo schema di principio di un qualsiasi sistema a microprocessore è il seguente: ROM MEMORIA CPU controllo dati RAM indirizzi interfacce Periferiche I/O L’architettura proposta è fondamentalmente quella di Von Neumann; la differenza sostanziale tra la rappresentazione tradizionale della macchina di Von Neumann e quella raffigurata sta nel modo in cui i diversi dispositivi sono tra loro connessi: nel primo caso ci troviamo di fronte ad un collegamento “punto a punto”, in quanto ogni unità è collegata direttamente alle altre. Nella figura si vede invece che i sistemi a microprocessore attualmente in diffusione prevedono la presenza di tre bus a cui tutti i dispositivi sono collegati in parallelo. I bus si distinguono in indirizzi, dati e controllo a seconda delle informazioni che trasportano; nei sistemi a microprocessore lo scambio di informazioni tra tutti i dispositivi avviene tramite questi canali. Architetture di questo tipo si dicono pertanto bus oriented. Oltre a presentare una circuiteria semplice e razionale, le strutture orientate a bus offrono l’importante vantaggio dell’espandibilità del sistema: sfruttando infatti la compatibilità strutturale dei diversi dispositivi di una stessa famiglia, è possibile ampliare il sistema, a seconda delle esigenze, aggiungendo i moduli più idonei alla gestione delle funzioni per cui il sistema è stato concepito. E’ così possibile personalizzare le caratteristiche di un elaboratore espandendo la memoria, inserendo convertitori A/D o prevedendo la presenza di dispositivi programmabili quali Floppy Disk Controller, CRT Controller, interfacce seriali e parallele e numerosi altri integrati di controllo che forniscono le funzioni fondamentali del sistema, a cominciare dal circuito 8253 Timer che fornisce il clock di sistema. Poiché tutti i dispositivi sono collegati in parallelo al bus e poiché solitamente è solo la CPU l’unico modulo a poter gestire lo scambio di informazioni all’interno del sistema, ciò che non deve assolutamente verificarsi è che durante il colloquio tra la CPU e un dispositivo, altri dispositivi non coinvolti possano inserirsi nella comunicazione interferendo sulle informazioni presenti sul bus. Il primo problema è dunque quello di consentire al microprocessore di poter selezionare il dispositivo di memoria o di I/O con il quale vuole colloquiare e, contemporaneamente, di interdire gli altri da qualsiasi possibile interferenza. Alcuni microprocessori (dal 6800, al 6502, al 6809, al 68000) lasciano dello spazio di memoria disponibile per l’I/O. In questo caso si parla di I/O mappato in memoria. Nella famiglia Intel, il sistema I/O ha il proprio spazio di indirizzamento e tutto lo spazio di memoria del sistema può essere usato per memoria. Il bus di controllo del sistema definisce il tipo di comunicazione che ha luogo. In altre parole, ci sono linee di controllo separate per il sistema di I/O e per il sistema memoria. Il sistema di memoria usa le linee di controllo etichettate ”lettura in memoria” e “scrittura in memoria”, mentre il sistema di I/O usa “lettura in input” e “scrittura in output”. Ciò consente di utilizzare le stesse linee di indirizzo e può esistere un indirizzo 00F4H sia di memoria che di I/O. A ciascun dispositivo di I/O (cioè a qualsiasi componente hardware controllata dal sistema) viene assegnato un insieme di indirizzi di I/O, ognuno dei quali individua una porta di I/O. Una porta di I/O è un posto univocamente identificato che svolge una funzione analoga ad una locazione di memoria, cioè permette di leggere e scrivere dati. Le porte di I/O funzionano da interfacce con l’esterno del sistema. Per accedere alla porte si utilizzano istruzioni di lettura e scrittura apposite: IN registro, porta OUT porta, registro dove: porta è l’indirizzo di I/O della porta e può essere espresso da una costante o dal contenuto del registro DX registro deve essere il registro AL per porte a 8 bit e il registro AX per porte a 16 bit. Esiste un secondo problema, che riguarda il colloquio tra la CPU e le periferiche esterne interfacciate al sistema tramite le porte di I/O e che può essere visto sostanzialmente come un problema di sincronizzazione. L’esecuzione di un programma in linguaggio macchina si basa su uno schema di interpretazione hardware che governa l’attività della CPU, indicato come ciclo macchina di base, che può essere schematizzato come segue: ripeti fetch {cioè MAR (Memory Address Register) IP (Istruction Pointer) individuazione della cella di memoria con indirizzo = IP deposito del suo contenuto in MDR (Memory Data Register) IR (Istruction Register) MDR IPIP+1} decode dell’istruzione contenuta in IR execute finché la macchina si ferma Se però il programma in esecuzione non si limita a operare su dati residenti in memoria centrale o nei registri della CPU, ma effettua delle operazioni di I/O1, il ciclo di controllo descritto non è più sufficiente. Infatti, mentre il colloquio con la memoria centrale è sincrono, cioè la CPU può accedere in qualunque momento per le operazioni di lettura e scrittura, l’accesso alle periferiche esterne per operazioni di ingresso uscita può avvenire solamente se il dispositivo periferico interessato è pronto a ricevere o inviare dati e non si può prevedere il tempo necessario al completamento di un’operazione di I/O: si pensi ad esempio al caso di un’operazione di input da tastiera che richieda la digitazione di una stringa. Si deve attendere che la periferica concluda il proprio lavoro. In altri termini diventa indispensabile che la CPU, che sta eseguendo il programma, e la periferica, che sta eseguendo l’operazione di I/O, si sincronizzino. La sincronizzazione tra periferica e CPU può essere realizzata via software, facendo in modo che la CPU, dopo aver inviato il comando di I/O, continui a interrogare la periferica, o meglio un flag che rappresenta lo stato della periferica, sino a quando quest’ultimo non diventa 1, cioè ... comanda alla periferica l’operazione di I/O 1 Si pensi a tutti i programmi scritti con i tradizionali linguaggi imperativi, dal Pascal al C++ che per accedere all'I/O (ad esempio la tastiera e la stampante) utilizzano funzioni tipo readln(), scanf(), cin() ed altre mentre flag di stato = 0 NOP (Not Operation) finementre ... La soluzione presentata, nota come attesa attiva (busy waiting), comporta un notevole spreco di tempo di CPU. Infatti le periferiche sono notevolmente più lente della CPU e inoltre la loro attività può essere condizionata da ulteriori eventi nel mondo esterno (si pensi alla velocità di risposta dell’operatore, etc). Pertanto, nell’intervallo di tempo che intercorre tra l’inizio e la fine dell’operazione di I/O, la CPU potrebbe eseguire centinaia di altre istruzioni. L'attesa del dato determina inevitabilmente una grave inefficienza. Il solo modo per superare tale problema consiste nel far sì che la CPU, invece che aspettare la fine dell’operazione di I/O, passi all’esecuzione di un altro programma e, solo quando l’operazione di I/O è terminata, riprenda l’esecuzione del programma interrotto2. Un tale tipo di gestione delle operazioni di I/O e, più in generale, degli eventi asincroni, è noto come meccanismo di interrupt e si basa sul fatto che il sistema sia in grado di svolgere le seguenti operazioni: 1. controllare, a livello di ciclo di fetch and execute, se è presente un segnale 2. interrompere il programma in esecuzione e cedere la CPU ad un altro programma, facendo però in modo che il programma interrotto possa essere ripreso in un momento successivo senza che ciò comporti alcuna differenza rispetto ad una sua esecuzione strettamente sequenziale. Salvataggio e ripristino del contesto Un programma in esecuzione è normalmente un insieme di istruzioni eseguite una dopo l'altra. Quando arriva il momento di interrompere il programma in esecuzione, la CPU deve mandarne in esecuzione un altro per gestire il colloquio con la periferica che necessita dei suoi comandi. Affinché il meccanismo dell'interruzione funzioni correttamente, è necessario che tutte le azioni svolte dalla routine di gestione del dispositivo siano trasparenti rispetto al programma principale: al termine della routine deve essere ripristinato tutto come era prima della sospensione; in termini informatici, si richiede quindi che la routine sia perfettamente rientrante. Per fare ciò bisogna che la CPU, prima di mandare in esecuzione la routine, salvi tutto quello che stava facendo (cioè il suo contesto attuale) ed, alla fine della routine, lo ripristini com'era. La CPU non può perdere tempo per salvare tutto il contesto attuale (variabili, stato del programma, l'immagine sullo schermo, ...) ma solo quello che effettivamente verrà modificato dalla routine. D'altra parte la CPU non sa esattamente che cosa modificherà la routine e quindi non può conoscere a priori che cosa salvare. Chiamiamo: contesto minimo quello che viene modificato sicuramente ad ogni esecuzione di una qualsiasi routine contesto specifico quello che viene modificato dall'esecuzione di una particolare routine (cioè quali registri della CPU usa, ad esempio) contesto invariante tutto quello che fa parte del contesto attuale ma che non viene modificato dall'esecuzione della routine (ad esempio le variabili del programma precedentemente in esecuzione) Il contesto minimo dipende dal tipo di CPU in uso e, nel caso della famiglia INTEL, è costituito dai registri CS, IP e FLAG. CS (Code Segment) ed IP (Instruction Pointer) rappresentano l'indirizzo della prossima istruzione da eseguire e quindi sicuramente il lancio della routine li modifica. Meno intuitivo è il salvataggio del registro FLAG ma supponiamo, ad esempio, che il programma interrotto riparta da un’istruzione di salto condizionata dal contenuto della flag Zero. Affinché il salto sia eseguito correttamente è necessario che il Flag in questione (probabilmente modificato dalla routine di servizio) assuma lo stesso valore che aveva prima dell’interruzione... La CPU salva automaticamente il contesto minimo nello stack prima di passare il controllo alla routine e lo ripristina quando la routine è terminata, per effetto dell'esecuzione dell'istruzione IRET (Interrupt RETurn). 2 Si pensi al microprocessore come al solito cuoco che esegue ricette: un’operazione di I/O può corrispondere alla fase di cottura delegata ad un forno. L’esecutore può continuamente controllare l’avanzamento della cottura o, più efficientemente, passare ad eseguire un’altra ricetta. Per scoprire quando il cibo è cotto, può controllare ad intervalli (sospendendo periodicamente la nuova attività intrapresa) o essere avvisato dal trillo del timer del forno. Il contesto specifico dipende dalla particolare routine e dovrà essere il programmatore che scrive la stessa ad includervi all'inizio le istruzioni per salvarlo ed, alla fine, prima dell'IRET, a mettere le rispettive istruzioni per ripristinarlo. Poiché il contesto specifico verrà salvato nello stack, normalmente una routine di servizio di un dispositivo periferico comincia con una o più istruzioni di tipo PUSH e finisce con le corrispondenti POP, in ordine inverso. Il contesto invariante non viene influenzato e quindi può essere tranquillamente trascurato. Tecniche di colloquio Esistono tre modi sostanzialmente diversi per risolvere il problema dell’ottimizzazione dei tempi di CPU. Il primo modo è noto come POLLING e consiste nel far sì che la CPU interrompa il proprio lavoro a intervalli regolari, interroghi sequenzialmente lo stato di tutte le periferiche, provvedendo a gestire eventuali situazioni in cui l’operazione di I/O sia conclusa e riprenda il programma interrotto. Il secondo modo è detto semplicemente delle INTERRUZIONI (o device interrupt); con questa tecnica non è più la CPU ad interrompere se stessa in maniera “arbitraria”, ma è la periferica stessa che, al momento opportuno e cioè quando ha terminato l’operazione di I/O, invia un segnale di interrupt alla CPU. Il terzo modo è detto DMA (Direct Memory Access) e si utilizza quando la CPU non è coinvolta come sorgente o destinazione di dati scambiati nel colloquio con le periferiche. In questo caso la tecnica DMA permette il passaggio diretto dei dati da periferica a memoria centrale (o viceversa), escludendo la CPU dal processo di comunicazione . Il Polling Il primo problema da affrontare consiste nel decidere la lunghezza del periodo, cioè ogni quanto tempo o, meglio, ogni quanti cicli di fetch and execute, la CPU provveda alla scansione delle periferiche. Da un punto di vista software, l’orologio che governa l’intero meccanismo si può rappresentare con un particolare registro X, che viene decrementato ad ogni ciclo di fetch and execute e il cui valore viene testato prima della fase di fetch della successiva istruzione del programma. Se tale valore vale zero, la CPU non effettua il fetch dell’istruzione successiva del programma, ma dopo aver salvato il contesto minimo passa ad eseguire una particolare routine. In termini più precisi: ripeti se X= 0 allora salva contesto minimo CS:IP indirizzo della routine di polling finese XX-1 fetch decode and execute dell’istruzione contenuta in IR finché la macchina si ferma Per prima cosa la routine deve salvare i contenuti dei registri utilizzati dal programma precedentemente in esecuzione. In un secondo tempo passa in rassegna tutte le periferiche Ad ogni periferica è associato un Flag di Stato (solitamente un registro) che contiene le informazioni relative allo stato in cui si trova il dispositivo: libero, occupato, in attesa... Schema di principio della tecnica di colloquio POLLING CPU BUS DATI Flag1 Flag2 FlagN Interfaccia 1 Interfaccia 2 Interfaccia N Periferica 1 Periferica 2 Periferica N La CPU testa questo Flag e, se il dispositivo è libero, invoca un’altra routine di gestione specifica del dispositivo. Al termine della scansione ripristina lo stato della CPU relativo al programma interrotto, riassegna il valore iniziale al registro X e torna al programma, cioè salva il contesto specifico del programma interrotto se Flag1 (stato della prima periferica)=1 allora routine1 finese se Flag2 (stato della seconda periferica)=1 allora routine2 finese ... se FlagN (stato della enne-sima periferica)=1 allora routineN finese Xvalore iniziale ripristina contesto specifico del programma interrotto ritorna La tecnica del polling offre il vantaggio di poter gestire più periferiche con una struttura hw e sw piuttosto semplice. Esistono, però, delle circostanze per cui è assolutamente impossibile adottare la tecnica del polling. Poichè l’intervallo di tempo tra due successive interrogazioni di uno stesso Flag non è costante, ma dipende strettamente dallo stato delle altre periferiche, questa tecnica non può essere adottata nel caso in cui la trasmissione tra microprocessore e periferiche debba avvenire a scadenze fisse. Inoltre, ci si può trovare nella situazione in cui una periferica abbia una richiesta di intervento straordinaria, ad esempio una routine di risposta ad un allarme, o comunque un intervento in tempo reale. Gestendo le periferiche in polling, è possibile che la richiesta possa essere presa in considerazione solamente dopo aver concluso il colloquio, magari con tutte le altre periferiche, e quindi con un ritardo inaccettabile. Il limite maggiore di questa tecnica, comunque, sta nel fatto che la CPU è costretta a spendere il suo tempo per interrogare i Flag anche quando tutte le periferiche sono occupate e non risulta possibile avviare nessuna routine di gestione specifica3. 3 L’esempio banale è quello di un telefono privo di suoneria presso il quale si attende una telefonata nella prossima mezz’ora: si deve periodicamente sollevare il ricevitore per accertare se qualcuno è in linea, sottraendo tempo ad altre attività Le Interruzioni L'alternativa è ricorrere a linguaggi orientati ad eventi o, più semplicemente, utilizzare gli interrupt. Quando si deve acquisire un dato, si può proseguire con un altro programma e lasciare che sia la periferica ad avvisare quando il dato è disponibile. In quel momento si accantonerà momentaneamente il programma in corso e si leggerà il dato. In tal modo l'efficienza ritorna a livelli accettabili. Per quanto abbiamo detto fino ad ora, il concetto di interrupt è strettamente collegato a quello di gestione di un dato, ma vi è anche un'altra possibilità. Gli interrupt infatti si possono genericamente dividere in due categorie: interrupt esterni o hardware, sono quegli interrupt generati da dispositivi esterni alla CPU, che hanno il compito di comunicare il verificarsi di eventi esterni (tipo quelli di cui abbiamo parlato fino ad ora) interrupt interne: software: sono delle istruzioni (INT XX) che possono essere assimilate alle chiamate di sottoprogrammi (CALL XX) ma che sfruttano il meccanismo delle interruzioni per passare il controllo dal programma chiamante a quello chiamato e viceversa; vengono utilizzati per accedere direttamente alle risorse del Sistema Operativo eccezioni: sono generate da anomalie che si verificano nel corso dell’esecuzione di un programma Forniamo la definizione formale di interrupt hardware: un interrupt è un segnale o un messaggio, generalmente di natura asincrona, che arriva alla CPU per avvisarla del verificarsi di un certo evento Le interruzioni In un PC vi sono molte fonti di generazione di interruzioni: esterne: ogni volta che si preme un tasto della tastiera, l'integrato che la gestisce emette un interrupt ogni volta che si sposta il mouse o che se ne preme un pulsante, il driver del mouse genera un interrupt l'integrato timer 8253 contenuto nella scheda madre genera un interrupt 18,2 volte al secondo ogni volta che la stampante ha terminato di stampare un carattere, o svuotato il suo buffer interno, emette un interrupt interne: se tentate di dividere per zero (in linguaggio macchina) la CPU genera un interrupt non mascherabile per accedere alle risorse del calcolatore si usano normalmente delle chiamate a servizi offerti dal Sistema Operativo (DOS, API, ...) richiamabili via interrupt software Per quanto riguarda la ricezione del segnale di interrupt, in ogni CPU sono presenti solitamente due ingressi, denominati INTR e NMI (Non-Maskable Interrupt), che vengono abilitati proprio dal segnale di interruzione. Gli interrupt hardware presentati negli esempi fanno capo all'unica linea INTR della CPU. Questi interrupt sono mascherabili, cioè possono essere nascosti e quindi ignorati dalla CPU Sorgono quindi alcuni problemi: 1. se più dispositivi condividono l'unico accesso, come fa la CPU a sapere chi ha lanciato l'interrupt? (problema del riconoscimento) 2. se arrivano contemporaneamente due interrupt, chi dei due viene soddisfatto per primo? (problema delle richieste multiple e priorità) 3. mentre è in esecuzione una ISR (Interrupt Service Routine), che cosa succede se arrivano altri interrupt? (problema delle interruzioni nidificate) Esistono sostanzialmente tre modi per risolvere i problemi. Il primo è molto semplice e squisitamente software. Le periferiche sono collegate tutte assieme, tramite una porta logica OR, all’ingresso INTR. All’arrivo del segnale, la CPU salva il contesto e lancia una routine di gestione simile a quella del metodo di polling: interroga tutte le Flag di stato delle periferiche, in un ordine preciso e avvia le ISR appropriate. Si noti che ciò risolve anche il problema delle priorità perchè nel caso di richieste multiple, l’ordine con cui vengono servite è quello di scansione. Non risolve invece il problema della nidificazione di richieste perchè un dispositivo lento come la tastiera può interrompere il trattamento di un accesso a disco... Le Interruzioni con tecnica sw di riconoscimento e attribuzione priorità (in polling) CPU BUS DATI Flag1 Flag2 INTR Interfaccia 1 Interfaccia 2 OR Periferica 1 Periferica 2 FlagN NN Interfaccia N Periferica N Il secondo metodo è sostanzialmente hardware e si basa sull’utilizzo di un circuito integrato, detto Priority Encoder, con n ingressi di diversa priorità ed un numero di uscite sufficienti a codificare in binario il numero di ingressi, più un’uscita, collegata al piedino INTR della CPU, corrispondente ad un OR logico effettuato sugli ingressi. Gli n ingressi sono collegati ognuno ad un diverso dispositivo; all’attivazione di uno degli ingressi, sulle uscite comparirà la codifica binaria corrispondente al numero della linea abilitata. Se risultano attivi più ingressi, le uscite riporteranno il numero della linea con priorità maggiore. Quando la CPU troverà attivo l’ingresso INTR, invierà il segnale di accettazione INTA (da INTerrupt Acknowledge) all’Encoder, per poi provvedere alla lettura della uscite dello stesso per riconoscere il dispositivo che ha inviato l’interrupt. Tramite il Priority Encoder vengono risolti agevolmente i problemi legati alle richieste multiple e viene stabilita una rigida gerarchia tra i dispositivi periferici interfacciati al sistema. Il limite della soluzione è proprio nella rigidità del sistema hardware: una volta decise le priorità delle periferiche non sarà più possibile cambiarle. Le Interruzioni con meccanismo hw di riconoscimento e attribuzione priorità mediante Priority Encoder BUS DATI Periferica 1 Periferica 2 Periferica 3 ... ... ... ... Periferica N Priority Encoder CPU INTA INTR Il terzo metodo è una soluzione più flessibile perchè offre la possibilità di modificare la gerarchia delle periferiche a seconda delle esigenze e si basa su un integrato apposito, inizialmente presente nelle schede madre dei PC ed ora inglobato nel chip set che accompagna la CPU, denominato 8259A Programmable Interrupt Controller, o più semplicemente PIC. Nei primissimi PC ve ne era un solo esemplare, ma si sentì subito la necessità di ampliare il numero di interrupt gestiti fisicamente, e quindi si passò a due PIC collegati in cascata, il cui schema funzionale è riportato qui sotto. PIC in cascata IRQ8 IRQ9 IRQ10 IRQ11 IRQ12 IRQ13 IRQ14 IRQ15 PIC 2 OUT 2 IRQ0 IRQ1 IRQ3 IRQ4 IRQ5 IRQ6 IRQ7 PIC 1 OUT 1 alla linea INTR della CPU L'integrato PIC 1 possiede, tra le altre, 8 linee di ingresso (dette IRQ da Interrupt ReQuest) a cui pervengono le richieste di interruzione di altrettanti dispositivi esterni ed una linea d'uscita con cui controlla direttamente il segnale INTR della CPU. L'integrato PIC 2 è collegato in cascata, cioè ha altre 8 linee per altrettante richieste di interruzione e gestisce l'uscita come il PIC 1, ma ora è collegata alla linea IRQ2 del PIC 1. Analogamente a quanto accade nel Priority Encoder, le linee d’ingresso hanno una priorità rigidamente stabilita, massima per il dispositivo collegato alla linea IRQ0, minima per IRQ7. Per semplicità analizziamo il funzionamento di un solo PIC, in quanto il funzionamento del secondo PIC è del tutto simile al primo. Quando una linea IRQ viene attivata dalla periferica esterna, se la linea è abilitata (cioè, non si è mascherato l’ingresso con le apposite istruzioni software per la programmazione del dispositivo) e se non sono pendenti altre richieste a priorità superiore, la richiesta viene inoltrata alla CPU tramite la linea INTR4. La CPU risponde mediante la linea INTA per avvisare il PIC di inoltrare il codice di identificazione di interrupt. Il PIC emette un byte, detto codice di interruzione, associato alla linea IRQ durante la fase di inizializzazione. Successivamente il PIC rimane bloccato fino a quando non viene avvisato che la richiesta è stata soddisfatta e può inoltrare eventuali richieste pendenti o successive.5. A questo punto, la CPU, letto il codice di interruzione, conosce la periferica che ha inviato la richiesta e deve determinare l'indirizzo della routine ISR associata alla gestione della periferica. Per fare ciò, la CPU ricorre ad una tabella di sistema, detta Tabella dei Vettori, allocata a partire dall'indirizzo di memoria 0000:0000. Ogni elemento della tabella, detto vettore, è costituito da 4 byte che rappresentano l’indirizzo di partenza completo (due byte per il segmento e due byte per l'offset) della ISR (Interrupt Service Routine). Nella tabella, ciascun vettore occupa la posizione logica individuata dal codice che identifica l’interrupt; la posizione fisica (cioè l’offset del vettore) si calcola moltiplicando il codice d’interruzione per la dimensione dell’elemento della tabella (cioè 4 byte). Le interruzioni con riconoscimento e attribuzione priorità (mascherabili) mediante PIC (interrupt vettorizzato) 4 Per il funzionamento e la programmazione del dispositivo PIC, si veda il capitolo del Sistema hardware 5 Ciò deve essere effettuata dal programmatore emettendo l'opportuno messaggio EOI all'interno della sua ISR. BUS DATI 4. Periferica 1 Periferica 2 Periferica 3 ... 1. ... ... ... Periferica N periferica 2 periferica 3 6. Codice di interruzi PICone 3. CPU INTA 0000:0000 indirizzo partenza ISR 0 0000:0004 indirizzo partenza ISR 1 0000:0008 .......... INTR RAM 2. 5. 0000:codice x 4 BUS INDIRIZZI periferica n 1. Una periferica segnala la richiesta di servizio al PIC 2. Il PIC, se l’ingresso di quella periferica non è mascherato e non vi sono altre richieste di priorità superiore pendenti, inoltra la richiesta sulla linea INTR 3. La CPU, terminata l’esecuzione dell’istruzione in corso, se la linea INTR è abilitata (cioè, se la priorità non è stata cambiata via software), invia il segnale INTA 4. Il PIC invia il codice di interruzione 5. La CPU costruisce l’indirizzo completo (segmento = 0000 e offset = codice x 4) e legge quattro byte che rappresentano l’indirizzo di partenza della ISR specifica per l’interrupt ricevuto e memorizza tali valori nei registri CS e IP Questa tecnica è nota come interrupt vettorizzato. La tabella dei vettori viene inizializzata al momento dell’accensione e, poiché contiene gli indirizzi di tutte le ISR relative agli interrupt riconosciuti dal sistema in uso, è diversa per ciascuna “famiglia” di microprocessori. Indirizzo Codice di interruzione 0 1 254 255 IP CS IP CS … … … …. IP CS IP CS … … Memoria 0000h 0004h 0008h Tabella dei vettori di interrupt 03F8h 03FCh 0400h Risulta evidente che questa tecnica risolve in modo efficiente il problema del riconoscimento ed è sufficientemente flessibile nell’attribuzione delle priorità poiché consente di nascondere o mascherare alcune IRQ, programmando opportunamente il PIC. Anche il problema delle interruzioni nidificate, cioè della richiesta di una periferica proprio mentre è in esecuzione la routine di servizio di un altro interrupt, è parzialmente risolto perchè il PIC è disabilitato fino a quando la CPU segnala di avere terminato di soddisfare la richiesta inoltrata precedentemente. Diciamo “parzialmente” perché esistono situazioni in cui è meglio introdurre anche altri meccanismi. E’ preferibile ricorrere ad una gestione software delle priorità se, ad esempio, si desiderano mascherare tutte le linee di ingresso del PIC (cioè l’ingresso INTR) per tutta la durata dell’esecuzione di una routine o anche per l’esecuzione di solo una particolare sequenza di istruzioni. In questi casi si sfrutta il bit IF (Interrupt enable Flag) del registro delle Flag che viene testato in AND con l’ingresso INTR. Per modificare il bit IF si utilizzano due istruzioni assembly: STI (SeT Interrupt enable flag) che abilita l'inoltro di interrupt mascherabili, ponendo IF=1 CLI (CLear Interrupt enable flag) che li disabilita, ponendo IF=0. Nel caso invece si voglia fare in modo che la richiesta abbia priorità massima (cioè non possa essere in alcun modo disattesa) bisogna semplicemente collegare la periferica interessata all’altro ingresso per gli interrupt esterni: NMI (Not Maskable Interrupt). Una richiesta di questo tipo non è sensibile a disabilitazioni software ed ha priorità maggiore delle richieste della linea INTR. Le interruzioni software Le differenze tra un interrupt software (INT N) e una chiamata ad un sottoprogramma (CALL XXX) sono relativamente poche, ma molto importanti. L’esecuzione di una INT comporta (nell’ordine) il salvataggio del registro delle Flag, la disabilitazione della linea INTR con l’istruzione CLI, il salvataggio dei valori contenuti in CS e IP e l’inizializzazione degli stessi registri con l’indirizzo di partenza di ISR mentre l’esecuzione di CALL XXX comporta solo il salvataggio e l’inizializzazione IP ed eventualmente anche di CS se la procedura è FAR. Analogamente, l’esecuzione di una IRET comporta i passaggi inversi di una INT (senza però riabilitare la linea INTR, visto che si ripristina il registro delle flag dell’ambiente di chiamata della INT) e differisce dall’esecuzione una RET che comporta i passaggi inversi della CALL. Negli interrupt software il meccanismo di selezione della procedura da eseguire è abbastanza macchinoso ma è in realtà l'artefice principale della portabilità del software; gli interrupt software assicurano che un programma scritto in un PC possa essere eseguito anche in un altro PC con dotazione hardware diversa, purché dotato dello stesso sistema operativo. Infatti quando si deve accedere ad una risorsa, ad esempio il video, il programmatore corretto non accede direttamente ai registri della scheda video, ma utilizza le chiamate di sistema che hanno validità generale e sono implementate via interrupt software. Il programma lancia un INT N prestabilito, con N in funzione di quale risorsa si vuole utilizzare, e questo in tutto il software standard. La tabella dei vettori di interruzione è invece diversa in ogni PC e richiama la routine specifica per l'hardware installato in quel momento. Se si fa riferimento alle risorse di base standard l'insieme delle routine che vi accedono a basso livello prende il nome di BIOS mentre, se vi si accede a livello più astratto, è costituito dalle cosiddette chiamate di sistema operativo (funzioni del DOS o API in Windows). Se invece si fa riferimento a risorse particolari, come la stampante o lo scanner, gli stessi produttori delle periferiche producono e distribuiscono i driver (insiemi di routine da richiamare per ottenere certe funzioni dalle specifiche risorse) che il possessore del PC dovrà installare (cioè farle caricare in forma residente e agganciarle agli opportuni interrupt in maniera automatica all'atto dell'accensione). Gestione delle interruzioni nell'architettura INTEL L'INTEL, dal processore 80486 in poi, distingue tra interruzioni (interrupt) ed eccezioni (exception): le prime vengono utilizzate per comunicare alla CPU il verificarsi di certi eventi esterni, mentre le seconde servono a gestire il malfunzionamento di istruzioni. Gli interrupt software generati dalle istruzioni INT xx vengono gestiti dalla CPU come se fossero eccezioni. interruzioni esterne o hardware interrupt mascherabili interrupt non mascherabili interne interrupt software eccezioni faults traps abort Gli interrupt hardware sono ulteriormente classificabili in interrupt mascherabili o non mascherabili. Gli interrupt mascherabili sono quelli che fanno capo al piedino INTR e, normalmente, vengono utilizzati dai vari dispositivi esterni. L'interrupt non mascherabile ha ovviamente una priorità maggiore dei precedenti, in quanto non può essere mai disabilitato. Viene attivato portando a livello alto la linea NMI (Non-Maskable Interrupt); la CPU termina l'istruzione in corso e poi genera un interrupt di tipo 2, associato per default all'interrupt NMI. Non è possibile annidare interrupt NMI e viene automaticamente disabilitato il bit IF per evitare che sopraggiungano interrupt mascherabili. Si noti che esiste un’ulteriore interruzione esterna, non contemplata nella tabella dei vettori, che ha un proprio ingresso, la linea di RESET. Viene attivata sia via hardware (accendendo il computer o premendo l’apposito pulsante) sia via software (premendo contemporaneamente i tasti CTRL, ALT e DEL). Il registro CS viene settato a FFFFh e IP a 0000h e il BIOS ottiene il controllo del sistema. Gli interrupt software vengono determinati dall'esecuzione di istruzioni INT N ed in tal caso viene attivata il corrispondente programma predisposto ad hoc dal programmatore per quel particolare interrupt, detto ISR il cui indirizzo è all'N-esimo posto della tabella dei descrittori di interruzione. Le eccezioni si possono suddividere in: fault, anomalie di funzionamento rilevate e gestite immediatamente prima dell'esecuzione dell'istruzione che le genera: ad esempio nella gestione della memoria virtuale, quando il gestore genera un miss, cioè quel blocco di memoria non è presente in cache; trap, anomalie di funzionamento rilevate e gestite immediatamente dopo l'esecuzione dell'istruzione che le genera: ad esempio un interrupt software; abort, anomalie di funzionamento che non permettono di individuare esattamente l'istruzione che le hanno determinate: ad esempio un malfunzionamento segnalato da una periferica. Rivediamo ora in dettaglio che cosa succede all'interno di una CPU 486 o successive: Appena la CPU termina un'istruzione: 1. verifica che l'istruzione appena terminata non abbia prodotto una eccezione di tipo TRAP 2. verifica che la prossima istruzione da eseguire non produca una eccezione di tipo FAULT 3. verifica se è arrivato un interrupt hardware non mascherabile sulla linea NMI 4. verifica se sono arrivati interrupt hardware mascherabili sulla linea INTR e se il bit di flag IF è abilitato 5. se in modalità protetta, verifica che la prossima istruzione da eseguire non produca eccezioni di tipo FAULT Nella tabella dei vettori delle interruzioni sono riportati i 256 possibili tipi di interruzioni nelle CPU x86. codice di interrupt 0 indirizzo (descrittore) 0000h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16.. 255 0004h 0008h 000Ch 0010h 0014h 0018h 001Ch 0020h 0024h 0028h 002Ch 0030h 0034h 0038h 003Ch 0040h.. 03FCh classificazione origine fault divisione per zero o quoziente che eccede la capacità del registro destinazione single step (bit TF =1) interrupt non mascherabile istruzione INT o interrupt sw overflow istruzione BOUND codice operativo non valido dispositivo non disponibile timer di sistema tastiera ridirezione IRQ8-15 serial COM2-COM4 serial COM1-COM3 LPT2, Sound Card, ... Floppy Disk Controller parallel LPT1 interruzioni gestite da ISR trap NMI trap trap trap fault fault hw IRQ0 hw IRQ1 hw IRQ2 hw IRQ3 hw IRQ4 hw IRQ5 hw IRQ6 hw IRQ7 … Riassumendo, in forma algoritmica: ripeti se Interruzione interna (codice = 0, 3, 4 o istruzione INT codice) allora salva contesto minimo CS:IP 0000: (codice x 4)h altrimenti se NMI = 1 allora salva contesto minimo CS:IP 0000: 0008h altrimenti se INTR =1 and IF =1 allora IF=0; TF=0 salva contesto minimo legge codice CS:IP 0000: (codice x 4)h altrimenti se TF =1 allora salva contesto minimo CS:IP 0000: 0004h finese finese finese finese fetch decode and execute dell’istruzione contenuta in IR finché la macchina si ferma. Se è stato rilevato un interrupt, la CPU deve per prima cosa salvare il contesto minimo nello stack, e poi stabilire dove è allocata la ISR relativa. Se è arrivato un interrupt hardware non mascherabile il tipo di default vale 2. Se è arrivato un interrupt hardware mascherabile legge il valore ad 8 bit immesso nel Data Bus dalla periferica, che identifica il tipo di interruzione richiesta. Se l'istruzione da eseguire è un interrupt software, il tipo è contenuto direttamente nell'istruzione stessa. Se l'interruzione è un'eccezione, il tipo viene attribuito per default. I tipi di interrupt disponibili sono in tutto 256. Prima di mandare in esecuzione l'ISR relativa, in caso di interrupt hardware mascherabile e non, il bit IF viene azzerato, disabilitando l'arrivo di ulteriori interrupt mascherabili. Il programmatore, se necessario, può riabilitarlo manualmente (con l'istruzione STI) consentendo di eseguire un interrupt durante l'esecuzione di una ISR. Quando la ISR termina, viene ripristinato il registro di flag pre-esistente, con il relativo valore del bit IF. La tabella dei vettori occupa 1 Kbyte (256 vettori di 4 byte ciascuno). Ovviamente gli indirizzi sono allocati in maniera sequenziale. Se ad esempio la memoria contiene queste informazioni: 0000:0000 12 34 56 78 01 02 03 04 - 05 06 07 08 ...6 significa che la routine ISR associata all'interrupt di tipo 0 è allocata a partire dall'indirizzo 7856:3412, mentre quella associata all'interrupt di tipo 1 è allocata a partire dall'indirizzo 0403:0201. Realizzazione di una Interrupt Service Routine Come indicazione generale, per scrivere una routine di gestione di interrupt possiamo adottare la seguente scaletta operativa: 1. Scegliere il tipo di interrupt a cui agganciare l'ISR 2. Scrivere il corpo della routine 3. Curare l'accesso alle variabili 4. Aggiungere le istruzioni per il salvataggio ed il recupero del contesto specifico 5. Allocare in maniera stabile e sicura la routine ISR 6. Aggiornare la tabella dei vettori di interruzione con il nuovo indirizzo 7. Considerare il problema della rientranza Per rendere più comprensibile la trattazione, scriviamo una ISR di esempio che visualizzi costantemente sull'angolo in alto a destra dello schermo una cifra (dallo 0 al 9) che si incrementi ad ogni secondo (circa). Scegliere il tipo di interrupt a cui agganciare l'ISR: per una corretta implementazione del problema avremmo bisogno di un generatore di impulsi della frequenza di 1 Hz da collegare ad una linea IRQ ancora libera dei due PIC del nostro calcolatore. Tuttavia la Microsoft, ancora quando produsse il primo MS-DOS ed in previsione di futuri sviluppi multitasking, predispose un meccanismo di interruzione basato sull'INT 1Ch, a cui il programmatore esperto può collegarsi. Nell'esempio che sviluppiamo ci conviene seguire la strada più semplice e collegarci all'INT 1C. Scrivere il corpo della routine: oramai tutti i linguaggi (escluso il Java per scelta metodologica inderogabile, leggi sicurezza) permettono di accedere alle risorse fisiche del calcolatore e quindi forniscono strumenti per scrivere routine ISR. Tuttavia i linguaggi di basso livello sono sicuramente i più indicati a questo scopo. Per questo motivo si propone come linguaggio principale l'Assembler e se ne prevede anche una versione alternativa in linguaggio C. Nella stesura di una ISR bisogna ricordare che in generale non è possibile utilizzare le funzioni del DOS in quanto sono ISR non rientranti. Ciò significa che per accedere alle risorse del PC, tipo lo schermo e la tastiera, è necessario scriversi le proprie routine! Il problema è talmente semplice che se ne propone direttamente la soluzione, opportunamente commentata: POSIZ EQU 159 SegVideo EQU 0B800h Cifra DB '0' Tempo DB 18 ; posizione del carattere sullo schermo ; indirizzo inizio memoria video (a colori) ; carattere ASCII della cifra da visualizzare ; numero di interrupt da aspettare per ... ; ... sapere se è passato un secondo 6 Si ricordi che di una parola viene memorizzato prima il byte meno significativo e poi il byte più significativo... routine: mostra: avanza: esci: DEC tempo ; controllo se è passato un secondo JNZ mostra ; se no, salta a visualizzare la cifra MOV tempo,18 ; se si ripristina il valore e ... MOV AX,SegVideo ;aggiusta il valore del segmento ES MOV ES,AX MOV ES:POSIZ,Cifra ; scrivi la cifra sul video INC Cifra ; valuta la prossima cifra CMP Cifra,'9'+1 JNZ esci ; se minore di 10 va bene MOV Cifra,'0' ; altrimenti riportala a 0 (in ASCII) ... Curare l'accesso alle variabili: in linguaggio macchina nelle CPU x86 l'accesso alla memoria avviene (anche se non solo) attraverso un meccanismo di segmentazione. Se nella routine ISR si fa riferimento a variabili, queste devono essere rese accessibili al programma: se siamo in linguaggio macchina occorre ricordarsi di inizializzare correttamente il registro DS (Data Segment) mentre se utilizziamo il linguaggio C bisogna sincerarsi che le variabili manipolate dalla ISR siano di tipo globale, cioè allocate in forma statica. In linguaggio macchina non è facile passare il valore del registro DS alla routine ISR, quindi si preferisce ricorrere ad una scorciatoia, ove possibile: se la routine ha dimensioni contenute, meno di 64 KByte, la sua creazione può essere inglobata in un modello .COM, in cui per definizione i valori dei quattro registri di segmento coincidono. In tal modo prima di accedere ai dati all'interno della ISR sarà sufficiente attribuire al registro DS il valore del registro CS, che viene già inizializzato dalla stessa CPU al lancio della ISR. Questa la versione corretta del corpo della routine di interruzione d'esempio: POSIZ EQU 159 ; posizione del carattere sullo schermo SegVideo EQU 0B800h ; indirizzo inizio memoria video (a colori) Cifra DB '0' ; carattere ASCII della cifra da visualizzare Tempo DB 18 ; numero di interrupt da aspettare per ... ; ... sapere se è passato un secondo routine: mostra: avanza: esci: PUSH CS ; copio il valore del registro CS ... POP DS ; ... nel registro DS, per accedere ai dati DEC tempo ; controllo se è passato un secondo JNZ mostra ; se no, salta a visualizzare la cifra MOV tempo,18 ; se si ripristina il valore e ... MOV AX,SegVideo ;aggiusta il valore del segmento ES MOV ES,AX MOV ES:POSIZ,Cifra ; scrivi la cifra sul video INC Cifra ; valuta la prossima cifra CMP Cifra,'9'+1 JNZ esci ; se minore di 10 va bene MOV Cifra,'0' ; altrimenti riportala a 0 (in ASCII) ... Aggiungere le istruzioni per il salvataggio ed il recupero del contesto specifico: in linguaggio macchina questa operazione deve essere fatta direttamente dal programmatore, in base ai registri effettivamente utilizzati, mentre in linguaggio C, se la funzione viene dichiarata di tipo interrupt, queste istruzioni vengono aggiunte automaticamente. Anzi, per non sbagliare, il compilatore normalmente salva tutti i registri della CPU, indipendentemente dal fatto che questi vengano realmente modificati all'interno della funzione ISR. Questa la versione aggiornata del corpo della routine di interruzione d'esempio: POSIZ EQU 159 SegVideo EQU 0B800h Cifra DB '0' Tempo DB 18 ; posizione del carattere sullo schermo ; indirizzo inizio memoria video (a colori) ; carattere ASCII della cifra da visualizzare ; numero di interrupt da aspettare per ... ; ... sapere se è passato un secondo MioInt: routine: mostra: avanza: esci: PUSH AX ; salvataggio del contesto specifico PUSH ES PUSH DS PUSH CS ; copio il valore del registro CS ... POP DS ;... nel registro DS, per accedere ai dati DEC tempo ; controllo se è passato un secondo JNZ mostra ; se no, salta a visualizzare la cifra MOV tempo,18 ; se si ripristina il valore e ... MOV AX,SegVideo ; aggiusta il valore del segmento ES MOV ES,AX MOV ES:POSIZ,Cifra ; scrivi la cifra sul video INC Cifra ; valuta la prossima cifra CMP Cifra,'9'+1 JNZ esci ; se minore di 10 va bene MOV Cifra,'0' ; altrimenti riportala a 0 (in ASCII) POP DS ; ripristino del contesto specifico POP ES ; si noti l'inversione dell'ordine ... POP AX ; ... per effetto dello stack IRET ; termine della routine ISR Allocare in maniera stabile e sicura la routine ISR: ad una routine ISR viene di solito attribuito il significato di processo in background, cioè viene mandata in esecuzione e poi continua a funzionare mentre la CPU esegue un altro processo in foreground. In DOS, quando carichiamo un programma, questo va ad occupare la memoria a disposizione a partire dalla prima cella libera. Se dopo ne carichiamo un altro, quest'ultimo andrà ad occupare la memoria a partire dallo stesso indirizzo iniziale. Per evitare questo, possiamo: 1. caricare il programma in background (tipicamente la ISR) 2. spostare l'indirizzo della prima cella libera di memoria alla fine del programma appena caricato 3. caricare il programma in foreground. Esiste una funzione DOS che svolge proprio questi compiti, si chiama TSR da Terminate but Stay Resident e produce l'effetto di terminare il programma in corso e di mantenerlo residente spostando il puntatore della memoria libera di una quantità stabilita dal programmatore, definita in multipli di blocchi da 16 Byte, detti paragrafi. Questo un esempio di programma .COM scritto in Assembler con direttive semplificate, che rimane residente: DOSSEG .MODEL tiny ; modalita` .COM .CODE org 0100h ; locazione iniziale standard Start: JMP Inizio ; salto la routine ISR ; ............. area dati ............. TSR EQU 31h ; funzione TSR del DOS DOS EQU 21h ; codice interrupt DOS ; ... MioInt: ; ........ area contenente la ISR ..... ; ... Inizio: MOV AH,TSR ; funzione TSR MOV DX, ((Inizio-Start)/16)+17 INT DOS END Start Ricordarsi di compilare il file .ASM con modalità .COM: ad esempio con TASM digitare 'tasm file' e poi 'tlink file /t'. Si noti il calcolo dei paragrafi da mantenere residenti: si fa uso delle etichette Inizio e Start per far riferimento alle celle di memoria associate e calcolare la lunghezza del blocco di celle tra Start e Inizio. La quantità 17d (11h) viene sommata per arrotondare per eccesso il risultato della divisione. Aggiornare la tabella dei vettori di interruzione con il nuovo indirizzo: si potrebbe accedere direttamente alle celle di memoria della tabella, ma in tal caso si rischia di essere interrotti nel bel mezzo della scrittura da un interrupt, avendo salvato ad esempio solo la parte bassa dell'indirizzo. Verrebbe quindi passato un indirizzo errato in CS:IP, con l'ovvia conseguenza di bloccare il sistema. Una soluzione possibile sarebbe quella di racchiudere le istruzioni di scrittura dell'indirizzo tra due comandi di disabilitazione e riabilitazione degli interrupt. La soluzione migliore è invece quella di ricorrere ad una specifica funzione del DOS. Questa funzione richiede che • venga caricato nel registro AH il valore 25h che la identifica • venga caricato nel registro AL il tipo identificativo dell'interrupt di cui si vuole cambiare l'ISR • venga caricato nei registri DS:DX l'indirizzo segmento:offset dove è allocata la routine ISR Inglobando le soluzioni proposte in questi due ultimi punti possiamo scrivere il seguente programma, detto loader, che ha solo il compito di caricare in memoria in maniera permanente la routine ISR di nome MioInt e di aggiornare di conseguenza la tabellla dei vettori delle interruzioni: DOSSEG .MODEL tiny .CODE org 0100h Start: JMP Inizio ; ............. area dati ............. TSR EQU 31h ; funzione TSR del DOS DOS EQU 21h ` ; codice interrupt DOS SETVECT EQU 25h ; funzione SetVect del DOS MioInt: ; routine ISR personalizzata ; ... Inizio: MOV AH,SETVECT ; funzione Set Vector MOV AL,1Ch ; tipo di interrupt MOV DX,Offset MioInt ; salvo l'offset ; il segmento non serve perché CS e DS si equivalgono nei file .COM INT DOS ; aggiorna la tabella MOV AH,TSR ; funzione TSR MOV DX, ((Inizio-Start)/16)+17 INT DOS END Start Considerare il problema della rientranza: può succedere, anche se spesso non è auspicabile, che mentre è in esecuzione una routine ISR arrivi un altro interrupt che richiami la stessa ISR, in un meccanismo che richiama il concetto di ricorsione. Se la routine è ancora in grado di funzionare correttamente, la ISR di dice rientrante. I problemi nascono dal fatto che una ISR può utilizzare una risorsa a cui ha diritto di accesso, ma che è contraddistinta da molteplicità unaria: l'accesso legittimo ed in qualche modo concorrente ad una risorsa da parte di due istanze diverse dello stesso processo porta inevitabilmente a situazioni di incongruenza ben note nella programmazione dei nuclei di sistemi operativi multitasking. La soluzione è relativamente semplice: si devono individuare le zone critiche, in cui si accede a risorse condivise, e delimitarle con istruzioni CLI... STI che garantiscono la mutua esclusione dall'ingresso in queste aree. Molte funzioni del DOS non sono rientranti e questo ci obbliga per precauzione a non utilizzarle quando siamo in una routine ISR. Il DMA Nei processi di comunicazione fino ad ora descritti, la CPU ha sempre la funzione di master mentre ogni altro dispositivo si comporta come slave. Ciò significa che è la CPU a gestire l’intero sistema controllando completamente il flusso di dati sul bus. La modesta perdita di tempo introdotta per accantonare momentaneamente il programma e poi riprenderlo (scambio di contesto) determina il limite dell'utilizzo degli interrupt: se la frequenza dei dati di I/O gestiti è elevata, i tempi di scambio del contesto non sono più trascurabili rispetto alla gestione del dato, e quindi anche la tecnica dell'interrupt diventa inefficiente. In tal caso si potrebbe ricorrere alla tecnica del DMA (Direct Memory Access)7. Scambio di dati tramite CPU BUS DATI MEMORIA CPU Interfaccia I/O Periferica I/O Accesso Diretto alla Memoria BUS DATI MEMORIA CPU Interfaccia I/O Periferica I/O Infatti, per memorizzare un dato proveniente da una porta di I/O è necessario che questo prima sia letto dalla CPU, quindi memorizzato nell’accumulatore ed infine trasferito in memoria. Analogamente le stesse operazioni devono essere compiute in senso contrario per inviare ad una periferica un dato che si trova in memoria. Questo tipo di soluzione risulta piuttosto dispendioso dal punto di vista del tempo di esecuzione, in quanto la CPU è costretta , per effettuare un semplice trasferimento di dati, ad eseguire un considerevole numero di microistruzioni con altrettanti accessi in memoria. Inoltre, microprocessore e periferica dialogano ad interrupt. Tra la richiesta di interrupt e la sua accettazione da parte della CPU intercorre un intervallo di tempo che non è fisso, ma è determinato esclusivamente dal tempo che la CPU impiega per concludere 7 Si pensi ancora all’esempio del telefono: sicuramente l’installazione della suoneria ha migliorato la situazione rispetto al dover controllare periodicamente se vi era qualcuno in linea, ma se il telefono squillasse in continuazione, ad esempio ogni 5 minuti, come si potrebbe lavorare? Le perdite di tempo maggiori si avrebbero soprattutto per le chiamate che non fossero dirette a noi, ma ad un altro ufficio, a cui dovremmo successivamente riferire il contenuto della telefonata. Meglio pensare a mettere direttamente in comunicazione... l’istruzione in corso: in una situazione di questo tipo non è allora possibile il trasferimento da e per una periferica il cui funzionamento sia in grado di inviare o ricevere dati solamente ad intervalli di tempo fissi e limitati. Tali inconvenienti possono essere superati escludendo la CPU dal processo di comunicazione: la periferica interessata può accedere alla memoria per le operazioni di lettura/scrittura senza dover impiegare la CPU. Vale a dire che la periferica deve essere in grado di richiedere alla CPU la concessione del bus per poterlo gestire autonomamente. La realizzazione di questa tecnica prevede la presenza di un dispositivo “intelligente” che, all’occasione, possa trasformarsi in master per sostituire il microprocessore nelle attività descritte. Tale dispositivo programmabile è il DMA Controller (DMAC). In particolare, un trasferimento dati prevede i seguenti passi: 1. la periferica segnala la propria disponibilità alle operazioni di trasferimento al DMAC e/o alla CPU 2. il DMAC, tramite un opportuno segnale (HOLD), invia alla CPU la richiesta di cessione del bus; 3. la CPU, quando trova attivo l’ingresso HOLD, inizializza alcuni registri del DMAC con a) l’indirizzo della periferica selezionata b) il tipo di operazione da compiere (il senso del trasferimento) c) l’indirizzo della prima locazione di memoria interessata al trasferimento d) il numero di byte da trasferire 4. la CPU si preoccupa di porre tutti i suoi collegamenti al bus in alta impedenza, scollegandosi logicamente dal resto del sistema (si sottolinea che la richiesta di cessione del bus ha una priorità maggiore delle richieste di interruzione) La segnalazione dell’avvenuta cessione del bus è inviata al DMAC mediante il segnale di HOLDA (HOLD Acknowledge): a questo punto il DMA Controller assume la funzione di master, controllando il flusso di dati sul bus. Finchè il DMAC mantiene attivo il segnale HOLD il bus è sotto il suo controllo e la CPU mantiene i suoi bus ad un livello di alta impedenza ed esegue ripetutamente solo l’operazione di controllo dello stato del segnale HOLD: questa situazione permane finchè il segnale di HOLD non viene disattivato da parte del DMAC quando la comunicazione è terminata. A questo punto la CPU disattiva il segnale HOLDA per riprendere la sua funzione di master. I tempi di inattività della CPU costituiscono l’inconveniente maggiore di questa tecnica: come al solito è opportuno valutare attentamente tutte le esigenze prima di effettuare una scelta. Un altro caso in cui può essere utile disporre di un dispositivo DMAC riguarda il trasferimento di dati da una zona all’altra della memoria stessa. Se la mole di dati è rilevante, il passaggio attraverso i registri della CPU di tutti i dati rende lunghissima l’operazione, per cui, previa impostazione di alcuni registri del DMAC al valore degli indirizzi dell’area sorgente, dell’area destinazione e del numero di byte da trasferire, l’uso del DMAC è una buona alternativa. Tecniche di trasferimento E’ possibile classificare le tecniche di trasferimento utilizzate nei diversi DMA Controller in due categorie: sequenziale e simultaneo. Nel trasferimento sequenziale, il DMAC, dopo aver assunto il controllo del bus, si comporta come la CPU e cioè, in un’operazione di input ad esempio, legge il dato dalla periferica lo memorizza temporaneamente in un suo registro interno per poi scriverlo, in un secondo tempo, nella locazione di memoria desiderata. Analogamente si comporta in un’operazione di output. Nel trasferimento simultaneo, il DMAC, dopo aver assunto il controllo del bus, in un’operazione di output, effettua la lettura della memoria. A questo punto il dato è posto direttamente sul bus. Il DMAC avvisa la periferica che il dato è disponibile, cosicché quest’ultima può prelevarlo. Non viene effettuata nessuna memorizzazione temporanea. Indipendentemente dalla tecnica utilizzata, i dati possono essere trasferiti: un byte alla volta, a pacchetti di byte, in modo continuo. Nel primo caso (byte by byte), il DMAC, una volta ottenuto il controllo del bus, si preoccupa di trasferire un solo byte per poi rilasciare immediatamente il bus alla CPU. Nel secondo caso (burst), dopo aver trasferito un byte, il DMAC (senza rilasciare il bus) si accerta se sia disponibile il byte successivo: se lo è avviene il trasferimento, altrimenti restituisce il controllo del bus alla CPU. Infine, nel modo continuo, il bus viene rilasciato solo quando finito il trasferimento del blocco di dati programmato. Ovviamente, i tre modi di trasferimento appena descritti sono in ordine crescente di velocità di lavoro: ciò che può convincere a non utilizzare sempre il terzo modo, il più veloce, è che in questo caso si costringe la CPU a lunghe attese, in modo incondizionato, indipendentemente dall’importanza del lavoro che è rimasto in sospeso. Nel terzo modo, se la produzione dei dati è lenta, oppure se la mole di dati da trasferire è rilevante, l’attesa a cui è costretta la CPU può essere inaccettabile. Per questo motivo, il secondo modo, che prevede la prosecuzione fintanto che sono pronti i dati, può essere una interessante alternativa tra il primo e il terzo metodo. In breve, non è sempre vero che l’ottimizzazione delle operazioni di trasferimento comporti un miglioramento nelle prestazioni complessive del sistema di elaborazione. Dispositivi di supporto Il sistema fornisce un meccanismo di DMA nella forma del circuito integrato 8237 DMA Controller (a causa della sua complessità, il meccanismo di DMA non è stato mostrato nella figura iniziale). Per usare il DMA, il processore programma questo integrato con un conteggio iterativo e con un indirizzo di inizio della memoria. Il dispositivo di I/O è quindi comandato per cominciare l'operazione di lettura (o scrittura). Per ogni byte che deve essere letto (o scritto), il dispositivo di I/O manda un segnale di richiesta dati al controllore del DMA. Il controllore DMA quindi prende il controllo dei bus degli indirizzi e dei dati al posto del processore centrale e li usa per effettuare il trasferimento dati. Questo meccanismo permette al processore di partire con un'operazione di I/O e quindi di fare qualcos'altro mentre l'operazione ha luogo. Il circuito integrato 8237 fornisce quattro canali8 di DMA; cioè, quattro operazioni DMA indipendenti possono essere programmate e possono aver luogo nel medesimo tempo. Uno di questi canali è usato costantemente per provvedere al rinfresco (refresh) di memoria dei circuiti integrati RAM. Il rinfresco della memoria è necessario perché i circuiti RAM dinamici possono mantenere dati soltanto per un breve tempo prima di "dimenticarli". Il canale DMA è programmato per leggere da ciascuna locazione di memoria e quindi riscrivere nella stessa in continuazione, senza una fine. In questo modo, ogni locazione di memoria è rinfrescata prima che possa perdere il suo contenuto. Gli altri tre canali DMA sono disponibili per specifici dispositivi di I/O. Uno di questi è usato dall'interfaccia per i dischetti. In aggiunta all'8237, il sistema utilizza parecchi altri integrati di controllo che forniscono le funzioni fondamentali del sistema. I più importanti di questi sono l'integrato 8259A Programmable Interrupt Controller (Controllore di interruzione programmabile o PIC), l'integrato 8255 Programmable Peripheral Interface (PPI o Interfaccia di periferiche programmabile; indicato anche come PIO, Programmable I/O) ed il circuito 8253 Timer. La figura iniziale mostra che ognuno di questi integrati è connesso al sistema come un dispositivo di I/O (le connessioni tra questi dispositivi ed il bus dati sono state omesse per chiarezza). Il processore è quindi in grado di accedere ad essi e di controllarli attraverso le sue porte di I/O. La tabella successiva riassume gli effettivi indirizzi delle porte di I/O per ogni integrato di controllo. 20H 21H A0H A1H 8259A Programmable Interrupt Controller Registro di comando interruzione PIC 1 Registro di maschera interruzione PIC 1 Registro di comando interruzione PIC 2 Registro di maschera interruzione PIC 2 8 Un canale (o processore di I/O) è un particolare processore dedicato alle operazioni di I/O, introdotto per superare i problemi legati alla disparità di velocità a cui lavorano CPU e periferiche. 40H 41H 42H 43H 60H 61H 62H 63H 8253 Timer Porta accesso registro Latch Canale 0 Timer Porta accesso registro Latch Canale 1 Timer Porta accesso registro Latch Canale 2 Timer Registro Comando Timer 8255 Program mable Peripheral Interface Input porta "PA" Input/Output porta "PB" Input porta "PC" Registro di comando 8255 (Settato a 99H) Si noti che per ogni integrato di controllo è allocata più di una porta di I/O. Ognuno di questi integrati è in effetti un microprocessore nel vero senso della parola. A differenza di quello centrale, comunque, questi microprocessori sono progettati per realizzare compiti molto specifici. L'8253, per esempio, è in grado di contare e/o tempificare eventi che possono essere segnalati sia dall'hardware che dal software. Esso può anche essere usato per mantenere memoria del tempo e quindi servire come "clock". Sebbene le loro possibilità siano limitate e specifiche, questi integrati sono non di meno programmabili. Ognuno contiene uno o più registri interni. Noi possiamo programmare un integrato di controllo scrivendo dei valori nei suoi registri (attraverso le istruzioni di OUT all’indirizzo della porta appropriata). Similmente, lo stato di un integrato può essere ottenuto leggendo il contenuto dei suoi registri (con un'istruzione IN ). Armati della conoscenza di come operano questi integrati, noi saremo in grado di controllarli dai nostri programmi in linguaggio Assembly. Controllore Programmabile di Interruzioni (PIC) 8259 Il controllore d'interruzione 8259 fornisce un servizio di supporto vitale per il processore centrale. Si è già visto come eventi esterni possano essere segnalati al processore centrale attraverso il meccanismo d'interruzione. In un tipico sistema personal computer, tali segnali d'interruzione possono avere origine da parecchi posti differenti (per esempio dalla tastiera, dai dischi, ecc.). Schema a blocchi dell’8259 ____ INTA D7-D0 Data Bus Buffer INTR Control Logic Internal Bus __ RD __ WR A0 __ CS Read/ Write Logic In Service Reg. (ISR) Priority Resolver Interrupt Request Reg. (IRR) IRQ0 IRQ1 IRQ2 IRQ3 IRQ4 IRQ5 IRQ6 IRQ7 CAS0 CAS1 CAS2 Cascade Buffer Comparator Interrupt Mask Reg. (IMR) __ __ SP/EN La programmazione del PIC avviene mediante un meccanismo abbastanza complesso accedendo a due indirizzi mappati sull'integrato: per il PIC 1 questi due indirizzi sono relativi alle porte di I/O 20h e 21h, per il PIC 2 sono 0A0h ed 0A1h. Il PIC contiene al suo interno 3 registri ad 8 bit: IMR (Interrupt Mask Register), serve per abilitare o meno le singole linee di richiesta di interruzione; ponendo un bit 1 nella relativa posizione del registro si disabilita l'IRQ associato ISR (In Service Register, sola lettura), serve per sapere quali interrupt siano attualmente in gestione dalle rispettive routine. Se un bit di questo registro è a 1 significa che la relativa richiesta è in esecuzione IRR (Interrupt Request Register, sola lettura), serve per sapere quali richieste IRQ siano state attivate dai dispositivi esterni. Se un bit di questo registro è a 1 significa che è arrivata una richiesta nella rispettiva linea IRQ. Il PIC, prima di essere utilizzato, va inizializzato inviando 3 o 4 parole di inizializzazione (dette ICW1ICW4 da Inizialization Command Word): questa fase viene fatta una tantum all'accensione del sistema, ed è bene non modificarla per non bloccare il regolare funzionamento del PIC. Quando invece il PIC opera regolarmente, si possono inviare fino a 3 parole di controllo (dette OCW1OCW4 da Operational Command Word). Con la OCW1 all'indirizzo 021h per il PIC1 e 0A1h per il PIC2, si può accedere direttamente ai rispettivi registri IMR, e quindi abilitare o disabilitare l'accettazione delle richieste. Con la OCW2 all'indirizzo 020h per PIC1 e 0A0h per PIC2 possiamo emettere un comando EOI (da End Of Interrupt) per avvisare il rispettivo PIC che la richiesta di INT è stata soddisfatta completamente. Con la OCW3 sempre all'indirizzo 020h/0A0h possiamo leggere i valori dei registri ISR ed IRR. Ecco le semplici istruzioni in Assembler necessarie per poter accedere direttamente al PIC ed alle sue più semplici possibilità di programmazione. Programmazione del PIC in Assembly abilitare l’inoltro di una richiesta ad una linea (ad esempio IRQ3) IN AL, 21h AND AL, 0F7h ; leggo il registro IMR ; 11110111 azzero il bit 3 OUT 21h, AL disabilitare l’inoltro di una richiesta IRQ (ad esempio IRQ4) IN AL, 21h OR AL, 10h OUT 21h, AL ; parola di controllo OCW2 ; leggere il registro ISR MOV AL, 0Bh OUT 20h, AL IN AL, 20h ; leggo il registro IMR ; 00010000 setto il bit 4 ; disabilito IRQ4 emettere un EOI MOV AL, 20h OUT 20h, AL ; abilito IRQ3 ; parola di controllo 0CW3 per leggere ISR ; ; leggo il valore di ISR leggere il registro IRR MOV AL, 0Ah OUT 20h, AL IN AL, 20h ; parola di controllo 0CW3 per leggere IRR ; ; leggo il valore di IRR Nei PC alle richieste IRQ0-IRQ7 vengono associati i codici (o vettori) 09-0F, mentre alle richieste IRQ8IRQ15 vengono associati i codici 70-7F. Quando una linea IRQ viene attivata dalla periferica esterna, il bit relativo del registro IRR viene posto a 1, se il corrispondente bit del registro IMR è a livello basso, cioè se quella IRQ è abilitata. Se non sono pendenti altre richieste a priorità superiore, viene settato il bit relativo del registro ISR e la richiesta viene inoltrata alla CPU tramite la linea INTR. La CPU è ora in grado di soddisfare la richiesta, ma il PIC rimane bloccato fino a che non viene avvisato che la richiesta è stata soddisfatta; questa operazione deve essere effettuata dal programmatore emettendo l'opportuno messaggio EOI all'interno della sua ISR; a questo punto il PIC può inoltrare eventuali richieste pendenti o successive. Timer 8253 II circuito integrato Timer 82539 può realizzare una quantità di tempificazioni differenti e/o funzioni di conteggio. All'interno del circuito ci sono tre contatori indipendenti, numerati 0, 1 e 2. Ognuno di questi tre canali di tempificazione può essere programmato per operare in sei modi differenti, a cui ci si riferisce come da modo 0 a modo 5. Una volta che sono stati programmati, tutti i canali possono realizzare simultaneamente le operazioni designate di conteggio o tempificazione. Come si può immaginare, con questo dispositivo si possono realizzare operazioni molto sofisticate. 9 L’integrato 8253 è attualmente superato dall’ 8254 (standard industriale HMOS) e dall’82C54, (versione avanzata del precedente, con tecnologia CHMOS). Sono entrambi compatibili con tutti i processori Intel e hanno prestazioni maggiori con consumi più bassi.