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
IPIP+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
XX-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
Xvalore 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.