Programmable Interrupt Controller
Anteprima
ESTRATTO DOCUMENTO
Facoltà di Ingegneria
Corso di Studi in Ingegneria Informatica
Esame di Calcolatori Elettronici II
tesina di fine corso
Implementazione VHDL di un Programmable Interrupt Controller
(PIC) mod. Intel 8259A
Anno Accademico 2007-2008
Docente
Ch.mo prof. Antonino Mazzeo
Studente
Anzivino Antonio – 534/1922
PIC 8259A/simple
L’obiettivo di questo progetto è implementare in linguaggio VHDL un controller interrupt programmabile (PIC)
secondo le specifiche del modello Intel 8259A, uno dei più diffusi chip per la realizzazione di funzionalità di
interruzione.
Lo scopo di un controller interrupt programmabile è la gestione, per conto della CPU, delle richieste di interruzione
provenienti dai dispositivi periferici connessi al calcolatore. L’Intel 8259A è stato a lungo adottato nelle architetture
basate su x86 (Intel e AMD), ma, con l’evolvere dei calcolatori, il dispositivo PIC è stato sostituito da un più avanzato
APIC (Advanced PIC), che non sarà oggetto di trattazione.
La caratteristica di rilievo di un controller PIC è proprio la programmabilità: come si può vedere sul datasheet, l’Intel
8259A possiede diverse modalità di funzionamento, ed è possibile programmare da CPU il modo in cui le interruzioni
dovranno essere gestite. Il PIC, infatti, è in grado di mascherare le interruzioni che non dovranno essere servite in un
dato momento, in quanto il processore sta eseguendo compiti più importanti. La programmazione del PIC è
un’operazione che viene eseguita all’accensione del
calcolatore, mediante BIOS, ma le impostazioni possono
essere sovrascritte dal sistema operativo in fase di
caricamento, in quanto il kernel gira in modalità
Supervisore con libero accesso all’hardware.
Nei calcolatori moderni, il controller PIC è integrato sulla
scheda madre, all’interno del Southbridge, che include
tutti i dispositivi di interfacciamento con i bus di I/O, come
PCI, PCI-Express, IDE, porte seriali e parallele.
Il Southbridge gestisce le operazioni più lente del
calcolatore, come presto vedremo.
Assieme alla piedinatura, il datasheet fornisce dettagliate
indicazioni sui registri che possono essere programmati nel
PIC, e una completa descrizione del protocollo di
interruzione. Tuttavia, per motivi di praticità e di tempo,
non siamo stati in grado di implementare tutte le
funzionalità, e pertanto ci limiteremo ad implementare un
PIC a priorità che implementa unicamente il protocollo di
segnalazione a livelli di priorità.
1. Interruzioni e operazioni di I/O
Iniziamo con una panoramica sul problema delle
interruzioni. Le operazioni di I/O sono l’elemento chiave di
un calcolatore, in quanto esso non è un sistema autistico
ma deve comunicare con l’esterno: tanto con un operatore umano quanto con un altro calcolatore.
Esempi tipici di I/O sono i seguenti:
Visualizzazione a schermo di testo su una console di comando
Visualizzazione a schermo di una interfaccia grafica bidimensionale (GUI)
Rendering tridimensionale (applicazioni CAD e videogiochi)
Lettura e scrittura di dati su memoria di massa
Trasmissione di dati ad un altro calcolatore attraverso una rete di calcolatori, come Internet
Scansione di un documento di testo
Stampa di fotografie
Riproduzione e registrazione di segnali audiovisivi
Interazione utente con tastiera e mouse/trackball o touchscreen (lettura dei comandi impartiti dall’utente
sull’interfaccia attraverso i suddetti dispositivi)
Tali operazioni, così come tutte le altre che non riteniamo di elencare, vengono eseguiti da dispositivi che, anche
se interni al calcolatore, sono esterni alla CPU che esegue il software applicativo. In un tipico scenario,
supponendo che l’utente voglia spedire un messaggio di posta elettronica, i dispositivi di I/O coinvolti sono tanti
e ognuno ha una specifica funzione. Descriviamo quindi lo scenario come segue:
1. L’utente utilizza il mouse per posizionare il puntatore sul comando “Crea messaggio” (input)
2. Mentre l’utente esegue l’azione al punto 1, il puntatore sullo schermo segue i movimenti dell’utente
(output)
3. L’utente fa click sul comando (input)
4. A schermo viene visualizzata una finestra di composizione messaggio (output)
5. L’utente richiede che venga aperta la rubrica per selezionare il destinatario (input)
6. Il sistema legge la rubrica da disco (input)
7. Il sistema visualizza i contatti a schermo sotto forma di una lista (output)
8. L’utente seleziona il destinatario (input)
9. L’utente scrive il testo del messaggio sulla tastiera (input)
10. A schermo vengono visualizzati i caratteri digitati dall’utente, che così può correggere eventuali errori
(output)
11. L’utente fa click sul comando “Invia messaggio” (input)
12. Il sistema trasmette un pacchetto di sincronizzazione TCP verso il server di posta in uscita (output)
13. Il sistema attente un acknowledgement TCP dal server (input)
14. Il messaggio viene trasmesso secondo il protocollo SMTP (output, ma vi sono anche input in realtà)
15. A schermo viene visualizzata una conferma dell’avvenuto invio
In questo semplice scenario, i dispositivi coinvolti nell’interazione sono tastiera, mouse, schermo, disco rigido e
scheda di rete. Ognuno ha il proprio compito, e il processore, guidato dal sistema operativo e dal software
applicativo, deve far sì che il loro utilizzo corretto porti allo scopo preposto, nel caso l’invio di un messaggio email.
Il problema è che le operazioni di I/O sono estremamente lente rispetto alla velocità di esecuzione delle istruzioni di
una CPU. Un dattilografo difficilmente supera la velocità di 5 tasti premuti al secondo, mentre una CPU in grado di
completare un’istruzione al secondo (ciò si può ottenere col pipelining, ad esempio), alla frequenza di soli 100MHz è
in grado di completare 100 milioni di istruzioni al secondo.
È ovvio che, nel caso di un calcolatore che stia eseguendo molti compiti, come la navigazione sul web, la
compilazione di un programma e la masterizzazione di un DVD, è improponibile che il processore si metta in attesa
attiva dell’input utente da tastiera, ossia non esegua alcun compito fin quando l’utente non avrà premuto un tasto.
La prima soluzione, apparentemente, a questo problema, sarebbe quella per la CPU di interrogare (polling) uno a
uno i dispositivi di input per stabilire quale desideri essere servito: nel nostro caso, non sappiamo se in un dato
momento è la tastiera a richiedere che venga letto un carattere o il mouse che venga rilevato il suo movimento. La
soluzione sincrona, però, non si presta all’applicazione reale, in quanto la CPU spreca inutilmente cicli nella fase di
polling.
La soluzione al problema è asincrona: si fa in modo che siano le periferiche stesse a interrompere la CPU quando è
richiesta attenzione. Per la verità, una periferica non può letteralmente interrompere la CPU, ma ciò è solo l’effetto
macroscopico che si vede eseguendo i programmi a velocità CPU. Per implementare le interruzioni, è innanzi tutto
necessario un passo teorico: una piccola modifica al ciclo di Von Neumann, che diviene il seguente:
1. Fetch
2. Operand Assembly
3. Execute
4. IF NOT Interrupt THEN
Serve Interrupt
5. GoTo 1
La CPU, dunque, al termine di ogni istruzione, verifica se vi è una richiesta di interruzione da parte di una periferica, e
in tal caso si prepara a servire tale interruzione.
Fatto ciò, si è subito visto che vi sono servizi da considerarsi time critical, ossia che è importante che vengano serviti
prima di altri tipi di servizi di minore importanza. Ad esempio, in un impianto industriale, l’interruzione proveniente
da un controller termostatico che segnala l’eccessiva temperatura di un altoforno dovrebbe far immediatamente
scattare una routine di emergenza che spenga l’impianto. Ciò dovrebbe avere massima priorità, senz’altro più alta di
quella di un dispositivo che segnala l’avvenuta produzione di un pezzo.
Dunque, nei processori moderni, gli interrupt sono organizzati a priorità. La priorità di un dispositivo è detta IRQ. Per
convenzione, sui dispositivi 8259, ha priorità maggiore il dispositivo con IRQ0.
La convenzione va oltre la specifica delle priorità: la maggior parte dei dispositivi assume un IRQ specifico. Wikipedia
ci riporta una tabella nella quale possiamo osservare quali siano, nell’architettura x86, gli IRQ tipicamente assegnati
a determinate periferiche:
IRQ 0 - System timer. Reserved for the system. Cannot be changed by a user.
IRQ 1 - Keyboard. Reserved for the system. Cannot be altered even if no keyboard is
present or needed.
IRQ 2 - Cascaded signals from IRQs 8-15. A device configured to use IRQ 2 will actually
be using IRQ 9
IRQ 3 - COM2 (Default) and COM4 (User) serial ports
IRQ 4 - COM1 (Default) and COM3 (User) serial ports
IRQ 5 - LPT2 Parallel Port 2 or sound card
IRQ 6 - Floppy disk controller
IRQ 7 - LPT1 Parallel Port 1 or sound card (8-bit Sound Blaster and compatibles)
IRQ 8 - Real time clock
IRQ 9 - Free / Open interrupt / Available / SCSI. Any devices configured to use IRQ 2
will actually be using IRQ 9.
IRQ 10 - Free / Open interrupt / Available / SCSI
IRQ 11 - Free / Open interrupt / Available / SCSI
IRQ 12 - PS/2 connector Mouse. If no PS/2 connector mouse is used, this can be used for
other peripherals
IRQ 13 - ISA / Math Co-Processor
IRQ 14 - Primary IDE. If no Primary IDE this can be changed
IRQ 15 - Secondary IDE
Come possiamo notare, il timer di sistema ha priorità massima, il controller IDE primario ha priorità sul secondario
ma non sul coprocessore matematico né sulle porte seriali COM.
Possiamo verificare ciò empiricamente: nel nostro caso abbiamo constatato, attraverso i tool forniti dal sistema
operativo, l’assegnazione degli IRQ su un processore AMD64 per quanto riguarda il timer di sistema.
Ciò significa che, nel caso in cui sia la porta
parallela LPT1 che la tastiera sollevino un
interrupt, verrà servita prima la tastiera e poi la
porta parallela.
In un tipico protocollo di I/O asincrono, CPU e
periferica comunicano tra di loro per concordare
l’operazione e in seguito scambiare i dati.
Ricordiamo che l’accesso all’I/O è riservato al
sistema operativo, quindi un software (come il
client di posta che vuole caricare la rubrica)
dovrà chiedere al kernel di eseguire l’azione per
suo tramite.
Sebbene la gestione del file system sia
incredibilmente complicata, per ragioni di
robustezza e sicurezza, possiamo semplificare
tutto ciò riducendo il caricamento della rubrica
da disco ad una serie di passi più semplici:
1. Il client prepara i registri del
processore in una configurazione conforme alle
specifiche del sistema operativo. Ad esempio, il
1
registro A0 viene caricato col puntatore alla stringa che contiene nome e percorso del file da caricare,
D0 viene caricato con i flag di sola lettura e A1 con il puntatore al buffer dove dovrà essere caricato il
contenuto del file.
2. Il client effettua dunque una system call, facendo in modo che il processore entri in stato Supervisore ed
esegua le routine di file system del kernel. Da adesso in poi, il controllo è del sistema operativo.
3. Il sistema verifica la validità dei dati e individua il disco dove essi sono presenti, nonché le esatte
coordinate da caricare
4. Il sistema scrive su un apposito registro del controller del disco il settore da caricare e abilita il flag di
lettura
5. A questo punto, il sistema operativo, sapendo di dover attendere diverse decine di millisecondi prima
del caricamento, sospende il thread in esecuzione e passa il controllo ad uno in attesa di essere eseguito
6. Il disco, una volta letti i dati e caricati nel suo buffer interno, segnala alla CPU il completamento
dell’operazione sollevando un interrupt
7. La CPU sospende l’esecuzione e individua che la richiesta proviene dal disco rigido quindi conferma
l’avvenuto riconoscimento dell’interrupt (INT acknowledgement)
8. La CPU esegue una routine del kernel che completa il caricamento dei dati spostandoli dal buffer disco
alla memoria, nella locazione desiderata dal thread richiedente. Il protocollo con il disco termina
9. Il sistema operativo, in base alle proprie regole di schedulingl, decide se restituire il controllo al client di
posta
Finora non abbiamo trattato in adeguato dettaglio la parte hardware della gestione delle interruzioni, soffermandoci
sul software.
Quando si produce un calcolatore, in particolare la sua scheda madre, tutti i dispositivi in essa integrati vengono
fisicamente connessi, in maniera permanente, alle varie linee di controllo e dati che mettono in comunicazione i
componenti del sistema. Un metodo semplice ed economico per gestire le interruzioni a priorità è la daisy chain,
implementata nei sistemi basati su 68000.
1 Faremo sempre riferimento al Motorola 68000
La daisy chain è come una lista concatenata: i dispositivi sono connessi in parallelo al bus dati e la linea INT, ma lo
sono in serie per quanto riguarda la linea INTA, così che ogni dispositivo è connesso al successivo (per INTA).
Per convenzione, diciamo che il dispositivo 0 è quello connesso direttamente alla CPU mentre il dispositivo N-1 è
l’ultimo della catena. Quando un dispositivo I intende sollevare un interrupt, alza la linea INT sulla linea comune,
tipicamente realizzata con tecnologia open drain/open collector. Anziché effettuare il polling, la CPU alza INTA su 0,
e la regola è la seguente: quando un dispositivo J riceve INTA dal precedente, se non ha richiesto interruzioni allora
propaga INTA su J+1, altrimenti prosegue con il protocollo. In questo modo, anche il segnale INTA si propagherà fino
al destinatario.
Quando il dispositivo che ha richiesto INT riceve INTA, capisce di essere in fase di servizio e scrive sul bus dati il suo
identificativo, in modo che la CPU capisca quale dispositivo ha richiesto l’interrupt. Quindi la CPU, letto l’ID, abbassa
INTA e il dispositivo abbassa INT.
In questa daisy chain, come possiamo osservare dall’algoritmo di propagazione di INTA, il dispositivo fisicamente più
vicino alla CPU ha la massima priorità, mentre quello a fine catena la minore. Infatti possiamo affermare che per
servire la INT di un dispositivo K non ci deve essere nessun dispositivo K-1 a richiedere interruzioni
contemporaneamente.
L’uso del PIC, a fronte di ciò, è legato alla possibilità di controllare dinamicamente il comportamento degli interrupt,
senza dover implementare su ogni anello della daisy chain un apposito algoritmo. In questo caso, tutte le linee INT
dei dispositivi sono connesse al PIC in parallelo, e il PIC esce con un’unica linea INT verso la CPU, dalla quale riceve
INTA. La programmazione del PIC fa sì che il criterio di selezione del dispositivo da servire possa cambiare
dinamicamente. Ad esempio, l’ultimo dispositivo servito potrebbe diventare quello dalla minor priorità, oppure si
può usare un algoritmo round robin per le priorità. In questi casi, la convenzione fatta sulla numerazione degli IRQ è
superflua, in quanto, in un dato momento, IRQ(i) potrebbe avere meno priorità di IRQ(i+1).
2. Interfaccia del dispositivo 8259A e indirizzamento
Esaminiamo i connettori dell’8259A come indicati sul datasheet Intel:
Nome pin Input/Output Descrizione
Vcc I Alimentazione 5V
GND I Alimentazione massa
CS’ I Chip select: quando basso, abilita le comunicazioni con la CPU. Viene
collegato ad un comparatore connesso al bus indirizzi
WR’ I Write: quando basso, abilita il PIC a ricevere comandi
RD’ I Read: quando basso, abilita il PIC a inviare dati alla CPU sul bus dati
D7-D0 I/O Bus dati: linee bidirezionali per la comunicazione con la CPU
CAS0-CAS2 I/O Cascade: controllano i PIC in cascata. Sono di output per il master e di
input per lo slave
SP’/EN’ I/O Slave program/Enable buffer: doppia funzionalità. Quando non è in
modalità buffer, designa uno slave, altrimenti comanda i trasmettitori
del buffer
INT O Interrupt: interrompe la CPU
IR0-IR7 I IRQ: le periferiche vengono connesse ad uno dei pin rispettando la
regola del lower-priority
INTA I Interrupt acknowledgement: la CPU conferma che è pronta a servire la
richiesta
A0 I A0: parte del meccanismo di indirizzamento dei registri PIC
In totale ci sono 28 pin. Osserviamo che il PIC è una rete asincrona, in quanto non vi sono segnali di clock in ingresso.
Analizziamo subito la particolare modalità di indirizzamento del PIC: un controller PIC ha due indirizzi, uno pari ed
uno dispari. I bit più significativi dell’indirizzo vengono confrontati con i bit più significativi del bus indirizzi per
determinare se il PIC è stato selezionato, mentre il bit meno significativo (A0) viene inviato direttamente al PIC. Ciò ci
permette di indirizzare due registri mediante il bus indirizzi. In realtà, il PIC contiene al suo interno un particolare
meccanismo di indirizzamento che usa il bit A0 in concomitanza con alcuni bit della parola dati (D7-D0) per
indirizzare più di due registri. Questo perché il PIC ha 4 registri ICW (Instruction Command Word) e 3 OCW
(Operation Command Word) da 8 bit ognuno.
Ad esempio, ICW2 viene indirizzato in scrittura dopo aver scritto su ICW1. La sincronizzazione delle operazioni
avviene attraverso pulsazioni del segnale INTA in logica 0. Programmare il PIC significa, come già illustrato, definirne
la modalità di funzionamento: è possibile impostare la modalità di selezione delle priorità, il tipo di protocollo
(perché si può usare uno stesso 8259A su diverse architetture)o la maschera.
Nella nostra implementazione, apporteremo delle semplificazioni al protocollo di interrupt, e non andremo a
prevedere alcuna programmazione.
In particolare, dal punto di vista comportamentale, ci aspettiamo quanto segue:
1. Quando il PIC riceve un interrupt su una o più linee IRx, determina il dispositivo con maggiore priorità
attraverso la regola semplice dell’ID minore, quindi alza il segnale INT
2. Il processore riceve INT e invia un impulso negativo di INTA al PIC
3. Sul primo impulso INTA, il PIC abbassa INT e scrive sul bus dati l’identificativo del dispositivo nei bit
meno significativi (D2-D0)
4. Il processore legge l’identificativo e si prepara a servire l’interrupt
5. Sul secondo impulso INTA, il PIC disabilita il bus dati e si aspetta che la periferica abbassi la sua linea INT,
quindi si riporta nello stato iniziale
Facciamo l’assunzione che la maschera sia gestita internamente dal PIC: in fase di simulazione, forzeremo la
maschera a particolari valori per testarne le funzionalità. Nel nostro caso, la maschera non è selettiva come nel
protocollo reale, ma maschera soltanto gli interrupt maggiori del valore indicato (o nessuno).
3. Progettazione VHDL di basso livello
Il PIC è stato implementato in VHDL mediante componenti di più basso livello, applicando il principio di granularità di
VHDL: mentre per alcuni dei componenti è stata utilizzata una descrizione comportamentale del funzionamento,
essenzialmente dovuta alla laboriosità della progettazione in logica a due livelli, per altri sono invece state
implementate le effettive architetture fino al livello gate.
Per quanto riguarda la logica, abbiamo ritenuto interessante utilizzare la logica NOR. La scelta delle gate
normalmente è dettata dalla tecnologia su silicio attualmente in uso. Supponendo di lavorare in NMOS, sarebbe più
economico realizzare gate NOR. Le specifiche sulle gate vengono imposte dal produttore dell’hardware e/o dal
committente e non dal progettista, il quale si deve attenere ad esse. Grazie a VHDL, come vedremo, sarà possibile in
maniera molto semplice lo switch da una tecnologia all’altra cambiando semplicemente la dichiarazione delle
architetture nell’istanziazione dei componenti.
Siamo dunque partiti dalla progettazione degli elementi di più basso livello, utili in seguito a implementare i
principali componenti del PIC: abbiamo cioè implementato le gate NOR a due ingressi, e da lì le porte AND, OR e NOT
utilizzando le suddette NOR. Qui va fatta una precisazione doverosa: implementare le tre gate fondamentali per
mezzo di un’altra gate è un passaggio decisamente ridondante, in quanto il progettista ha gli strumenti per
effettuare direttamente la progettazione in una data logica di una rete. Tuttavia, poiché in alcuni punti, come ad
esempio nella maschera di interrupt, la funzionalità di più alto livello da implementare è una AND bit a bit, anziché
implementare tale funzionalità direttamente in NOR, abbiamo preferito aggiungere un layer di astrazione utilizzando
le entità AND2, che a loro volta sono mappate su entità NOR2. Nel caso fosse necessario passare ad una logica
NAND, basterebbe cambiare le istanze delle suddette AND2 verso una implementazione basata su NAND.
Tale comportamento non giustifica, tuttavia, l’utilizzo di tutto il set di gate nella progettazione delle reti più
complesse: per quanto ciò crei la massima astrazione e riuso possibile, una progettazione generica non è in grado di
ottimizzare il circuito logico per l’implementazione, aggiungendo livelli logici e ritardi che si potrebbero evitare
effettuando progettazioni separate per NAND e NOR anziché un’unica progettazione riusabile. Il motivo per cui noi lo
abbiamo fatto è stato però quello di mostrare, a titolo illustrativo, le potenzialità del linguaggio. Ma proprio in virtù
del potenziale di VHDL, qualunque progettista che metta mano ai nostri sorgenti potrà aggiungere, molto
semplicemente, una implementazione basata su NOR2 e/o su NAND2 dei medesimi componenti rispettandone
l’interfaccia e ottenendo un circuito ottimizzato.
Ciò premesso, analizziamo il contenuto del file NOR2.VHD realizzato con PeakVHDL. Questa libreria, realizzata
appositamente per questo progetto, contiene essenzialmente le porte logiche comuni e un registro D a 8 bit
asincrono implementato mediante flip-flop RS a NOR incrociate.
ENTITÀ NOR2
Dato che questa gate è per noi atomica, la implementiamo come behavioural, lasciando al produttore del circuito la
mappatura su transistor. Di seguito il codice sorgente:
entity NOR2 is
generic ( port_delay: time := 5ns );
port (
-- WIZ BEGINPORTS (Entity Wizard command)
x: in std_logic;
y: in std_logic;
o: out std_logic
-- WIZ ENDPORTS (Entity Wizard command)
);
end NOR2;
architecture BEHAVIOR of NOR2 is
-- Note: signals, components and other objects may be declared here if needed.
begin o <= x nor y after port_delay;
end BEHAVIOR;
ENTITÀ INV
Questa è una semplice porta NOT, realizzata implementando una singola gate NOR2 che in ingresso prende la stessa
variabile in entrambe le porte. Di seguito il sorgente:
entity inv is port ( x: in std_logic;
y: out std_logic
);
end inv;
architecture withnor2 of inv is
component n port (x, y: in std_logic; o: out std_logic);
end component;
for all: n use entity work.nor2(behavior);
I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher valeria0186 di informazioni apprese con la frequenza delle lezioni di Calcolatori Elettronici II e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università Napoli Federico II - Unina o del prof Mazzeo Antonino.
Acquista con carta o conto PayPal
Scarica il file tutte le volte che vuoi
Paga con un conto PayPal per usufruire della garanzia Soddisfatto o rimborsato