Assembly Avanzato con MASM

Capitolo 3: Il PIC 8259 - Programmable Interrupt Controller


Un computer è un sistema complesso costituito da una Unità Centrale di Elaborazione (CPU) e da un insieme più o meno numeroso di dispositivi periferici chiamati, semplicemente, periferiche; tra la CPU ed una qualsiasi periferica si deve necessariamente stabilire un sistema di comunicazione che consiste, in sostanza, in una richiesta di I/O da parte della periferica stessa.
Si pone allora il problema fondamentale di come far dialogare la CPU con le periferiche nel modo più efficiente possibile; per risolvere un tale problema esistono due metodi principali denominati polling (sondaggio) e interrupts (interruzioni).

Il metodo del polling consiste nel fatto che la CPU, ad intervalli di tempo regolari, "sonda" a rotazione ciascuna delle periferiche per sapere se c'è una eventuale richiesta di I/O; non ci vuole molto a capire che si tratta di un metodo altamente inefficiente in quanto provoca un enorme rallentamento generale del sistema.
Una periferica che impiega molto tempo per rispondere al sondaggio, tiene inutilmente occupata la CPU e finisce anche per creare una lunga coda di richieste di I/O da parte di altre periferiche costrette ad attendere il loro turno; possiamo affermare quindi che il metodo del polling, grazie anche ad una notevole semplicità circuitale, diventa vantaggioso solo nel caso in cui siano presenti poche periferiche, tutte molto veloci nel rispondere al sondaggio effettuato dalla CPU!

Il metodo delle interrupts comporta una complessità circuitale nettamente superiore, ma garantisce una enorme efficienza generale del sistema; proprio per questo motivo, si tratta di un metodo largamente utilizzato sui PC e su molte altre piattaforme hardware.
Il metodo delle interruzioni prevede che tutte le richieste di I/O vengano intercettate da un apposito dispositivo; lo scopo di tale dispositivo è quello di creare una coda di attesa dove le varie richieste di I/O vengono ordinate in base alla priorità assegnata a ciascuna di esse.
Al momento opportuno, il dispositivo invia alla CPU una richiesta di dialogo da parte della periferica alla quale è stata assegnata la priorità maggiore; solo in quel momento, la CPU interrompe il programma in esecuzione (da cui la denominazione di "interrupt") e soddisfa la richiesta della periferica.
In sostanza, grazie a questo metodo, la CPU viene "disturbata" solo quando è strettamente necessario; il programma in esecuzione viene quindi interrotto per il minor tempo possibile!

In base ad uno standard imposto dalla IBM, per la gestione delle richieste di I/O nei PC è stato scelto un dispositivo denominato PIC 8259; l'acronimo PIC sta per Programmable Interrupt Controller (controllore programmabile delle interruzioni).

3.1 Classificazione delle interruzioni

Possiamo suddividere le interruzioni in quattro categorie fondamentali analizzate nel seguito.

3.1.1 Interruzioni hardware

Le interruzioni hardware sono quelle provocate dalle periferiche; in tal caso si parla di IRQ o Interrupt Request (richiesta di interruzione).
In base a quanto detto in precedenza, tutte le IRQ vengono inviate ad uno o più PIC (dipende da quante IRQ differenti vogliamo gestire); il PIC provvede a disporre le varie IRQ in ordine di priorità e le invia, una alla volta, alla CPU.

Appare evidente il fatto che le IRQ sono "eventi asincroni"; in altre parole, una IRQ può arrivare in qualsiasi momento, anche mentre la CPU sta eseguendo una istruzione.

3.1.2 Interruzioni software

Le interruzioni software sono quelle provocate direttamente dai programmi; come sappiamo, per generare una interruzione software bisogna servirsi dell'istruzione INT n, dove n è un valore intero senza segno a 8 bit compreso tra 0 e 255 (tra 00h e FFh).

Appare evidente il fatto che le interruzioni software sono "eventi sincroni"; in altre parole, una interruzione software viene generata da un programma e quindi la sua gestione è sincronizzata con l'esecuzione del programma stesso.

3.1.3 Eccezioni della CPU

In particolari circostanze, anche le CPU possono generare automaticamente vere e proprie interruzioni software; in tal caso si parla di CPU exceptions (eccezioni della CPU).
Il termine "exception" indica il fatto che queste particolari interruzioni vengono generate dalla CPU quando si verificano casi eccezionali; la Figura 3.1 illustra alcune delle principali eccezioni. Alcune delle eccezioni illustrate in Figura 3.1 sono state già analizzate nella sezione Assembly Base; altre numerosissime eccezioni, come la 0Dh e la 0Eh, si verificano solo quando la CPU opera in modalità protetta e saranno analizzate nella apposita sezione di questo sito.

3.1.4 NMI - Non Maskable Interrupt

Osservando la Figura 9.3 e la Figura 9.6 del Capitolo 9, nella sezione Assembly Base, si può notare che le CPU della famiglia 80x86 sono dotate di un pin di input indicato con NMI; attraverso tale pin arriva un apposito segnale per indicare che si è verificato un evento particolarmente grave!
NMI è l'acronimo di Non Maskable Interrupt (interruzione non mascherabile); tale definizione indica il fatto che la NMI è di vitale importanza per il corretto funzionamento del sistema e non può essere quindi mascherata (interdetta) dall'utente attraverso i metodi illustrati nel seguito del capitolo.

Tra i casi che provocano una NMI si possono citare, i cali di tensione tali da impedire il corretto funzionamento del sistema, un errore di parità in memoria, un trasferimento dati che non è stato portato a termine nel tempo stabilito, etc; l'utente quindi non può che augurarsi che una NMI non arrivi mai all'apposito pin della CPU!

3.2 Gestione delle interruzioni da parte della CPU

Come è stato abbondantemente spiegato nel precedente capitolo e nella sezione Assembly Base, per convenzione i primi 1024 byte della RAM (compresi quindi tra gli indirizzi fisici 00000h e 003FFh) sono riservati a particolari informazioni le quali, nel loro insieme, formano la cosiddetta IVT o Interrupts Vector Table (tabella dei vettori di interruzione); questi 1024 byte vengono suddivisi in 256 locazioni da 4 byte ciascuna (256*4=1024).
Ogni locazione da 4 byte contiene un indirizzo logico Seg:Offset di tipo FAR che prende il nome di Interrupt Vector (vettore di interruzione); tale indirizzo logico punta ad una procedura che deve trovarsi nel primo MiB della RAM (modalità operativa reale).
I 256 vettori di interruzione vengono rappresentati con un indice compreso tra 0 e 255 (tra 00h e FFh); tale indice prende il nome di Interrupt Type (tipo di interruzione).

Ad ogni richiesta di interruzione viene associato un ben preciso Interrupt Type che possiamo rappresentare attraverso il relativo indice n; la CPU soddisfa una richiesta di interruzione chiamando la procedura associata all'Interrupt Vector di indice n nella IVT.
La CPU compie i seguenti passi: La procedura appena chiamata dalla CPU ha il compito di soddisfare la richiesta di interruzione; proprio per questo motivo, una tale procedura prende il nome di ISR o Interrupt Service Routine (procedura di servizio per le interruzioni). Notiamo che la CPU, prima di chiamare una ISR, pone a zero il Trap Flag TF (per evitare l'eccezione INT 01h ad ogni istruzione eseguita) e l'Interrupt Enable Flag IF (per mettere in stato di attesa eventuali altre richieste di interruzione); tutte le interruzioni che possono essere bloccate (mascherate) con IF=0 prendono il nome di Maskable Interrupts (interruzioni mascherabili).
Come si può facilmente immaginare, le NMI sono chiamate "non mascherabili" proprio perché non vengono bloccate da IF=0; anche le interruzioni software non possono essere mascherate in quanto vengono imposte dal programma in esecuzione, indipendentemente dallo stato di IF.

Una ISR deve rigorosamente terminare con una istruzione IRET; in presenza di tale istruzione, la CPU esegue i seguenti passi: Si noti che il ripristino del registro FLAGS comporta anche il ripristino di TF e IF; come sappiamo, normalmente si ha TF=0 e IF=1.
Il Trap Flag deve essere tenuto, possibilmente, sempre a 0 per evitare che la CPU generi una eccezione (INT 01h) ad ogni istruzione eseguita; tale caratteristica viene messa a disposizione dei debuggers, ma chiaramente provoca un sensibile rallentamento generale del sistema!
L'Interrupt Enable Flag deve essere tenuto, possibilmente, sempre a 1 per fare in modo che la CPU elabori tutte le interruzioni mascherabili; se IF=0, le interruzioni mascherabili vengono bloccate e la CPU non può dialogare con le periferiche! In base alle considerazioni esposte in precedenza, possiamo affermare che la gestione, da parte della CPU, delle interruzioni software e delle eccezioni, si svolge in modo molto semplice in quanto viene specificato in modo diretto anche l'indice n nella IVT; la CPU quindi non deve fare altro che accedere alla posizione n*4 nella IVT, leggere l'indirizzo Seg:Offset associato, caricarlo in CS:IP e saltare a CS:IP.
Nel caso, invece, delle interruzioni hardware, è necessario analizzare il meccanismo che permette di associare una IRQ ad un indice n nella IVT; come si può intuire, il compito di effettuare tale associazione spetta al PIC.

3.3 Funzionamento del PIC 8259

La Figura 3.2 illustra lo schema semplificato di un PIC 8259. Come possiamo notare, sono presenti 8 ingressi, indicati con IR0, IR1, etc, sino a IR7; attraverso questi 8 ingressi possiamo gestire le IRQ provenienti da 8 periferiche diverse.

Non appena una determinata IRQ giunge all'ingresso IR a cui è collegata, il PIC modifica un apposito registro a 8 bit denominato Interrupt Request Register o IRR; in pratica, il bit di IRR la cui posizione corrisponde al numero della IRQ viene posto a livello logico 1 per indicare che la relativa richiesta di I/O è in attesa di elaborazione.

Un secondo registro a 8 bit, denominato Interrupt Mask Register o IMR, permette al PIC di sapere se la IRQ è mascherata o meno; una IRQ è mascherata quando il bit di IMR la cui posizione corrisponde al numero della IRQ stessa viene posto a livello logico 1.
Se il bit mask è a 1, il PIC blocca l'elaborazione della IRQ associata; in caso contrario, la IRQ viene inviata ad un dispositivo del PIC denominato Priority Resolver o PR.

Come si intuisce dal nome, il PR ordina le varie IRQ in base alla loro priorità, rappresentata da un numero compreso tra 0 e 7 (interamente riprogrammabile dall'utente); per convenzione, 0 è la priorità più alta, mentre 7 è la più bassa.

La IRQ con priorità più alta viene inviata ad un ulteriore registro a 8 bit denominato In-Service Register o ISR (da non confondere con le ISR); il PIC ora invia un impulso alla CPU attraverso la linea INT collegata al Control Bus (CB).
Tale impulso arriva all'omonimo pin INT (o INTR) della CPU (Figura 9.3 e Figura 9.6 del Capitolo 9, sezione Assembly Base); la CPU porta a termine l'eventuale istruzione che stava eseguendo e invia due impulsi (separati da un piccolo intervallo di tempo) attraverso il proprio pin INTA (interrupt acknowledge) collegato al Control Bus.
I due impulsi giungono in successione all'omonimo ingresso INTA del PIC.

Quando arriva il primo impulso, il PIC pone a 0 il bit che in IRR occupa la posizione corrispondente al numero della IRQ da elaborare; analogamente, il PIC pone a 1 il bit che in ISR occupa la posizione corrispondente al numero della IRQ da elaborare.
Quando arriva il secondo impulso, il PIC utilizza il Data Bus per inviare alla CPU l'Interrupt Type, cioè l'indice n nella IVT in cui si trova l'indirizzo Seg:Offset della ISR da chiamare; come vedremo più avanti, il valore n viene stabilito in fase di inizializzazione del PIC.

A questo punto, il controllo passa alla CPU la quale procede come descritto nel paragrafo 3.2; in particolare, la CPU provvede a porre IF=0 nel registro FLAGS prima di chiamare la ISR.
Porre IF=0 equivale ad eseguire una istruzione CLI (clear interrupt enable flag); l'effetto che si ottiene è il mascheramento di tutte le IRQ che giungono al PIC (in sostanza, tutti gli 8 bit dell'IMR vengono posti a 1)!
Questo comportamento è necessario per evitare che l'elaborazione di una ISR venga interrotta dall'arrivo di un'altra IRQ; nel caso più semplice, quindi, il PIC rimane in attesa finché non termina l'elaborazione di una ISR.
Appare evidente, però, che una tale situazione può portare a gravi latenze dovute, ad esempio, ad una ISR piuttosto lenta; per evitare questo problema, il programmatore può inserire all'inizio della stessa ISR una istruzione STI (set interrupt enable flag) permettendo così al PIC di riprendere subito a funzionare.
In un caso del genere, l'arrivo di una IRQ avente una determinata priorità, può interrompere l'esecuzione di una ISR associata ad un'altra IRQ di priorità strettamente inferiore; si parla allora di nested interrupts (interruzioni innestate)!

3.3.1 Collegamento dei PIC in cascata

Lo schema di Figura 3.2 è tipico dei primissimi PC di classe XT, comparsi sul mercato all'inizio degli anni 80; sin da allora, però, ci si è resi subito conto che 8 sole periferiche gestibili erano veramente poche.
Fortunatamente, il PIC 8259 presenta la caratteristica di potersi collegare in "cascata" ad altri PIC 8259; a tale proposito, è necessario servirsi dell'apposito Cascade Bus costituito dalle tre linee CAS0, CAS1 e CAS2.
Per capire il funzionamento dei PIC in cascata, è necessario partire dal fatto che le CPU della famiglia 80x86 sono dotate di un unico pin INT; solamente uno dei PIC in cascata può quindi inviare gli impulsi verso la CPU!
Per risolvere questo problema, è stato adottato lo schema di Figura 3.3 che si riferisce al caso molto diffuso di due soli PIC in cascata. Osserviamo subito che l'uscita INT del PIC inferiore è collegata ad uno degli ingressi IR del PIC superiore; in base a questa configurazione, il PIC superiore viene definito Master (letteralmente, "padrone") mentre il PIC inferiore viene definito Slave (letteralmente, "schiavo").
Come vedremo più avanti, durante la fase di inizializzazione possiamo informare il PIC Master sul fatto che uno dei suoi ingressi è collegato, non ad una periferica, bensì ad un PIC Slave; in questo modo, il PIC Master è in grado di sapere se una determinata IRQ è arrivata direttamente allo stesso PIC Master o indirettamente da un PIC Slave.

Se una IRQ arriva direttamente al PIC Master, l'elaborazione procede come già descritto in precedenza; in tal caso, il PIC Master invia l'impulso INT e riceve i due impulsi INTA provvedendo poi a fornire l'Interrupt Type alla CPU.

Se una IRQ arriva ad uno dei PIC Slave, lo stesso PIC Slave invia l'impulso INT il quale, però, raggiunge il PIC Master; lo stesso PIC Master è stato programmato in modo da poter determinare quale PIC Slave ha inviato l'impulso.
Il PIC Master invia l'impulso INT alla CPU e poi, attraverso il Cascade Bus, seleziona il PIC Slave che ha ricevuto la IRQ; di conseguenza, i due impulsi INTA inviati dalla CPU raggiungono il PIC Slave selezionato, il quale può così fornire l'Interrupt Type per l'elaborazione.

Osserviamo che il Cascade Bus è formato da 3 linee attraverso le quali il PIC Master può selezionare sino a 23=8 PIC Slave differenti; ciascuno dei PIC Slave può gestire 8 periferiche, per un totale quindi di 8*8=64 periferiche!

Dalle considerazioni appena esposte risulta evidente che solo uno dei PIC può svolgere il ruolo di Master; tutti gli altri possono svolgere solamente il ruolo di Slave e quindi, le loro uscite INT devono essere collegate ai vari ingressi IR del PIC Master.

3.4 Programmazione del PIC 8259

I PIC possono essere totalmente inizializzati e configurati in base alle esigenze del programmatore; è chiaro però che la riprogrammazione completa dei PIC ha senso solamente in circostanze del tutto particolari (ad esempio, quando si intende scrivere un proprio SO)!

I PC meno vecchi della famiglia 80x86 sono dotati di due PIC 8259 collegati in cascata; in fase di avvio del computer, il BIOS provvede ad effettuare tutto il lavoro di diagnosi, inizializzazione e configurazione dei due PIC.
Lo schema adottato è proprio quello di Figura 3.3. L'uscita INT del PIC Slave è collegata all'ingresso IR2 del PIC Master; la IRQ2 viene "dirottata" all'ingresso IR1 del PIC Slave (e si comporta quindi come una IRQ9).

A sua volta, anche il SO può provvedere a riprogrammare i PIC in base alle proprie esigenze; in genere, tale lavoro consiste nel redistribuire le IRQ alle varie periferiche.

Per l'accesso a ciascun PIC sono disponibili due sole porte hardware denominate, simbolicamente, P0 e P1; gli indirizzi di tali porte (come di qualsiasi altra porta hardware) sono stati scelti in base a precise convenzioni.
La Figura 3.4 indica le convenzioni legate ai PC della famiglia 80x86; il PIC Master viene indicato con MPIC, mentre il PIC Slave viene indicato con SPIC. Nella sezione Assembly Base abbiamo visto che la Control Logic (CL) permette alla CPU di distinguere tra indirizzi appartenenti alla memoria RAM e indirizzi appartenenti alle porte hardware delle periferiche; a tale proposito, la CL non fa altro che analizzare il tipo di indirizzamento specificato in una istruzione.
In presenza di istruzioni del tipo IN (Input From Port) e OUT (Output To Port), la CL capisce che vogliamo effettuare una operazione di I/O che coinvolge una periferica; di conseguenza, la stessa CL provvede a disabilitare la RAM e a mettere in comunicazione la CPU con la periferica stessa!

3.4.1 Comandi di inizializzazione del PIC

Per l'inizializzazione dei PIC sono disponibili 4 comandi a 8 bit denominati ICW o Initialization Command Word; questi comandi devono essere specificati in perfetto ordine: ICW1, ICW2 e, se richiesto, ICW3 e ICW4.

Un aspetto fondamentale riguarda il fatto che la fase di inizializzazione, per motivi abbastanza ovvi, deve svolgersi con tutte le interruzioni mascherate. Prima di dare il via a tale fase, è necessaria quindi una istruzioni CLI; terminata l'inizializzazione, sarà necessaria una istruzione STI per ripristinare l'elaborazione delle interruzioni mascherabili.

La Figura 3.5 illustra la struttura del comando ICW1; tale comando deve essere scritto nella porta 20h del PIC Master e, se necessario (PIC in cascata), anche nella porta A0h del PIC Slave. Non appena viene raggiunto da un comando ICW1, individuato dal bit 4 che deve valere 1, il PIC si resetta completamente e resta in attesa, come minimo, di un successivo comando ICW2; se il bit 0 di ICW1 vale 1, allora il PIC attende anche i due ulteriori comandi ICW3 e ICW4.
Se i vari comandi non vengono impartiti in perfetto ordine, l'inizializzazione fallisce; in particolare, se dopo una sequenza di ICW si invia nuovamente un ICW1, il PIC fa ripartire da zero la fase di inizializzazione.

Il bit in posizione 1 indica se è presente un unico PIC o se sono presenti più PIC in cascata; nel caso di Figura 3.3, ad esempio, questo bit deve valere 0.

Il bit in posizione 2 indica la dimensione in byte di ogni Interrupt Vector; nella modalità reale 80x86 tale dimensione è di 4 byte, per cui questo bit deve valere 0.

Il bit in posizione 3 indica la modalità di rilevamento di una IRQ da parte del PIC; le due modalità disponibili sono edge-triggered e level-triggered.
In modalità edge-triggered, la IRQ viene rilevata quando il relativo ingresso IR del PIC si trova nella fase di transizione da livello logico 0 a livello logico 1; in modalità level-triggered, la IRQ viene rilevata quando il relativo ingresso IR del PIC passa da livello logico 0 a livello logico 1 stabile.
Le CPU della famiglia 80x86 utilizzano la modalità edge-triggered, per cui il bit di ICW1 in posizione 3 deve valere 0; la modalità level-triggered viene impiegata nelle architetture IBM PS/2.

I bit in posizione 5, 6 e 7 vengono utilizzati solo con le CPU della famiglia MCS; per le CPU della famiglia 80x86 tali bit devono valere 000b.

Dopo aver ricevuto ICW1, il PIC si aspetta un ICW2; la Figura 3.6 illustra la struttura di tale comando che deve essere scritto nella porta 21h del PIC Master e, se necessario (PIC in cascata), anche nella porta A1h del PIC Slave. Il comando ICW2 serve per associare un Interrupt Type ad una IRQ; in questo modo, il PIC può ricavare il valore n necessario alla CPU per sapere quale ISR chiamare (INT n).
Come si nota in Figura 3.6, ICW2 deve specificare solamente i 5 bit più significativi di n; questi 5 bit, nel loro insieme, formano il cosiddetto BASE_TYPE. I 3 bit meno significativi di n vengono ricavati dal numero che identifica la IRQ da elaborare.
Supponiamo, ad esempio, di aver assegnato al PIC Master un BASE_TYPE=01010000b=50h; di conseguenza, alla IRQ0 sarà associato n=50h+00h=50h, alla IRQ1 sarà associato n=50h+01h=51h e così via, sino alla IRQ7 alla quale sarà associato n=50h+07h=57h .

Durante la fase di inizializzazione svolta dal BIOS, al PIC Master viene assegnato un BASE_TYPE=00001000b=08h; al PIC Slave, invece, viene assegnato un BASE_TYPE=01110000b=70h.

Se il bit 0 di ICW1 vale 1, allora il PIC attende anche i due ulteriori comandi ICW3 e ICW4; in particolare, il comando ICW3 ha lo scopo di impostare il collegamento in cascata tra due o più PIC.
La Figura 3.7 illustra la struttura del comando ICW3 che deve essere scritto esclusivamente nella porta 21h del PIC Master. In sostanza, se il bit in posizione k (con k compreso tra 0 e 7) vale 0, allora alla linea IRk del PIC Master arriva direttamente una IRQk; se, invece, il bit in posizione k vale 1, allora alla linea IRk del PIC Master arriva un impulso INT da un PIC Slave identificato da CAS=k.

A questo punto dobbiamo programmare opportunamente anche i vari PIC Slave; a tale proposito, a ciascun PIC Slave dobbiamo inviare un ICW3 che contiene il valore (CAS) corrispondente all'ingresso IR del PIC Master a cui lo stesso PIC Slave è collegato.
La Figura 3.8 illustra la struttura del comando ICW3 che deve essere scritto esclusivamente nella porta A1h di ogni PIC Slave; si tratta, in sostanza, del valore che il PIC Master inserisce nel Cascade Bus per attivare il PIC Slave che ha ricevuto la IRQ da elaborare. Nel caso di Figura 3.3, ad esempio, dobbiamo inviare il valore 00000100b=04h alla porta 21h del PIC Master e il valore 00000010b=02h alla porta A1h del PIC Slave; in questo modo il PIC Master, quando riceve un impulso INT sull'input IR2, inserisce nel Cascade Bus il valore 010b=2 per attivare il PIC Slave destinatario della IRQ da elaborare.

L'ultimo comando di inizializzazione che dobbiamo esaminare è ICW4; tale comando deve essere scritto nella porta 21h del PIC Master e, se necessario (PIC in cascata), anche nella porta A1h del PIC Slave. Il bit in posizione 0 deve valere sempre 1; il valore 0 viene usato solo con le CPU della famiglia MCS.

Il bit in posizione 1 è molto importante in quanto specifica la modalità secondo la quale viene segnalato un End Of Interrupt al PIC che ha rilevato la IRQ appena elaborata; come sappiamo, l'EOI serve per riportare a zero l'opportuno bit del registro ISR (In Service Register) del PIC.
Il "modo normale" (0) è quello convenzionalmente usato con le CPU della famiglia 80x86; in tale modalità, è compito della ISR inviare l'impulso EOI al PIC (vedere il comando OCW2, più avanti).
Il "modo automatico" (1) lascia al PIC stesso il compito di gestire l'EOI; in tale modalità, dopo aver ricevuto il secondo impulso INTA dalla CPU, il PIC riporta a 0 l'opportuno bit del registro ISR e invia l'Interrupt Type attraverso il Data Bus.

I bit in posizione 2 e 3 permettono di attivare o disattivare la bufferizzazione delle informazioni da inviare attraverso il Data Bus. La bufferizzazione viene usata su sistemi complessi comprendenti numerosi PIC collegati in cascata; nel caso dei PC con due soli PIC, la bufferizzazione è disabilitata, per cui questi due bit vengono inizializzati dal BIOS a 00b.

Il bit in posizione 4 indica il metodo seguito dal PIC per la gestione delle varie IRQ in attesa di elaborazione; la modalità standard prevede una gestione di tipo sequenziale (0). In sostanza, il PIC attende la fine dell'elaborazione di una IRQ prima di inviare la successiva richiesta alla CPU; come sappiamo, una ISR può anche servirsi della istruzione STI per abilitare le IRQ innestate (in tal caso, una ISR può essere interrotta dall'arrivo di una IRQ con priorità più alta).
Se il bit in posizione 4 vale 1, viene utilizzata la modalità SFNM (Special Fully Nested Mode) che permette complessi livelli di innesto per le IRQ; per maggiori dettagli su questo delicato argomento, si consiglia di leggere la documentazione tecnica citata nella Bibliografia.

Per riassumere tutti i concetti appena esposti, possiamo analizzare un esempio pratico di inizializzazione; bisogna ribadire, comunque, che normalmente tale lavoro è di competenza del BIOS e, se necessario, del SO.
Come sappiamo, gli indirizzi di porta devono essere specificati attraverso il registro DX; se l'indirizzo occupa solo 8 bit, può essere specificato anche attraverso un Imm8. In seguito all'inizializzazione standard effettuata dal BIOS, risultano definite le associazioni tra periferiche (IRQ) e Interrupt Type (n); la Figura 3.10 illustra la situazione più diffusa per il PIC Master. La Figura 3.11 illustra la situazione più diffusa per il PIC Slave. Gli utenti DOS possono verificare l'assegnamento delle IRQ attraverso programmi come MSD (Microsoft Diagnostics); in ambiente Windows si può utilizzare il Microsoft System Information.
In ambiente Linux sono disponibili programmi come Centro Informazioni di KDE/Plasma; in alternativa, da un terminale si può impartire il comando:
cat /proc/interrupts
Cosa succede quando arriva una IRQ2?
La periferica che invia una IRQ2 installa in memoria una ISR associata alla INT 0Ah; come si vede, però, in Figura 3.3, la IRQ2 viene dirottata all'ingresso IR1 del PIC Slave e diventa quindi una IRQ9, associata ad una INT 71h.
Per ovviare a questo problema, la ISR associata alla INT 71h deve contenere sempre una istruzione INT 0Ah; sui moderni PC, la IRQ2 (dirottata alla IRQ9) è spesso associata alla gestione dell'ACPI (Advanced Control Power Interface).

3.4.2 Comandi operazionali del PIC

Dopo aver ricevuto l'ultimo comando ICW, il PIC tratta eventuali altri comandi come OCW o Operational Command Word; si tratta di tre comandi a 8 bit (OCW1, OCW2 e OCW3), destinati a modificare la modalità operativa del PIC.
I tre comandi OCW possono essere inviati in qualsiasi momento e in qualsiasi ordine al PIC; la Figura 3.12 illustra il comando OCW1 che deve essere letto/scritto attraverso la porta 21h del PIC Master o A1h del PIC Slave. Come si può notare, OCW1 permette l'accesso al registro IMR attraverso il quale possiamo mascherare o "smascherare" determinate IRQ; il PIC riconosce questo comando in quanto lo riceve attraverso la porta 21h (o A1h) dopo che la fase di inizializzazione era già stata completata.
OCW1 è l'unico comando accessibile, sia in lettura, sia in scrittura; l'accesso in lettura è necessario per dare al programmatore la possibilità di modificare solo determinati bit dell'IMR, lasciando inalterati tutti gli altri.
Supponiamo, ad esempio, di voler mascherare la IRQ1 (PIC Master); a tale proposito possiamo scrivere: Analogamente, per riabilitare la IRQ1 (PIC Master) possiamo scrivere: La Figura 3.13 illustra il comando OCW2 che deve essere scritto nella porta 20h del PIC Master o A0h del PIC Slave. Il comando OCW2 viene riconosciuto dal fatto che i bit in posizione 3 e 4 valgono 00b; inoltre, tale comando deve essere inviato alla porta 20h del PIC Master o alla porta A0h del PIC Slave.

La struttura di OCW2 è piuttosto contorta per cui necessita di una analisi attenta; attraverso questo comando possiamo inviare impulsi EOI e/o modificare le priorità predefinite assegnate alle IRQ.
Come è stato già anticipato, a ciascuno degli 8 ingressi IR di un PIC viene assegnata una priorità distinta, rappresentata da un numero compreso tra 0 (max) e 7 (min); l'inizializzazione predefinita del PIC prevede che venga assegnata la priorità più alta (0) all'ingresso IR0 e la priorità più bassa (7) all'ingresso IR7.
Il programmatore ha la possibilità di alterare completamente questa situazione attraverso il comando OCW2; in particolare, è possibile richiedere una rotazione di 1 delle priorità, oppure una rotazione di un certo numero di posti.

Il bit 7 di OCW2 indica se è richiesta (1) o meno (0) una rotazione delle priorità; se questo bit vale 1, allora il livello di rotazione viene specificato dal bit in posizione 6.

Se il bit 7 vale 1 e il bit 6 vale 0 tutte le priorità vengono ruotate di 1 posto verso sinistra; partendo allora dalla situazione predefinita (IR0=max e IR7=min), il nuovo ordine diventa: IR7=max, IR6=min. Si ottiene cioè la situazione seguente: Se il bit 7 vale 1 e il bit 6 vale 1, l'ingresso con priorità minima diventa quello specificato dai bit in posizione 0, 1, 2 (livello di rotazione); ovviamente, questi tre bit permettono di specificare un valore compreso tra 0 (IR0) e 7 (IR7).
Partendo allora dalla situazione predefinita (IR0=max e IR7=min) e supponendo che il livello di rotazione sia 4 (IR4), il nuovo ordine diventa: IR5=max, IR4=min. Si ottiene cioè la situazione seguente: La tecnica appena descritta viene utilizzata per evitare che una IRQ con elevata priorità, possa interrompere continuamente le richieste di I/O da parte di periferiche con priorità inferiore; in sostanza, la IRQ con elevata priorità viene "servita" e poi le viene assegnata la priorità più bassa in modo da lasciare spazio anche alle altre richieste di I/O con priorità inferiore.

Sicuramente, il bit più utilizzato di OCW2 è quello in posizione 5; se tale bit vale 1, viene inviato un segnale EOI al PIC.
Come è stato già spiegato in precedenza, il segnale EOI informa il PIC sul fatto che l'elaborazione di una IRQ è terminata; in conseguenza dell'EOI, il PIC pone a zero il bit che nel registro ISR rappresenta la IRQ stessa.
Nel caso più frequente, quindi, il comando OCW2 assume il valore 00010000b=20h (EOI senza alcuna rotazione delle priorità); si parla allora di "Specific EOI", cioè EOI relativo alla specifica IRQ individuata dal corrispondente bit del registro ISR.

Il BIOS inizializza a 0 il bit 1 di ICW4 e questo significa che, normalmente, il compito di inviare il segnale EOI attraverso OCW2 spetta alla ISR associata alla IRQ da elaborare; è importante tenere presente che il procedimento da seguire dipende dalla eventualità che la IRQ sia arrivata al PIC Master o al PIC Slave.
Se la IRQ è arrivata al PIC Master, il segnale EOI deve essere inviato solo allo stesso PIC Master; dobbiamo scrivere, quindi: Se la IRQ è arrivata al PIC Slave, il segnale EOI deve essere inviato, prima allo stesso PIC Slave e subito dopo al PIC Master; dobbiamo scrivere, quindi: La Figura 3.14 illustra il comando OCW3 che deve essere scritto nella porta 20h del PIC Master o A0h del PIC Slave. Attraverso i bit in posizione 0 e 1 possiamo effettuare la lettura dei registri IRR e ISR del PIC; a tale proposito, dobbiamo utilizzare i due valori 10b e 11b, mentre 00b e 01b sono riservati.
Se scriviamo OCW3=00001010b nel PIC, una successiva lettura della porta P0 ci fornisce il contenuto del registro IRR; se scriviamo OCW3=00001011b nel PIC, una successiva lettura della porta P0 ci fornisce il contenuto del registro ISR.

Il bit in posizione 2 permette di attivare (1) o disattivare (0) il polling delle IRQ; quando la modalità di polling è attiva, la CPU può "ordinare" al PIC di inviare la prossima IRQ da elaborare!
I computer che utilizzano la tecnica del polling delle IRQ non hanno ovviamente bisogno della linea INT che mette in collegamento il PIC Master con la CPU; a tale proposito, il pin INT della stessa CPU viene lasciato scollegato.
Se scriviamo OCW3=00001100b nel PIC, una successiva lettura della porta P0 ci fornisce un valore a 8 bit che assume il seguente aspetto: Se il bit IN vale 1, allora una nuova IRQ è in attesa di elaborazione; in tal caso, i tre bit W0, W1 e W2 indicano a quale ingresso IR è arrivata la IRQ con priorità maggiore.
In questo modo, conoscendo il BASE_TYPE, possiamo ricavarci il valore n da passare all'istruzione INT; da parte sua, il PIC provvede ad auto inviarsi un EOI che notifica l'avvenuta elaborazione della IRQ.

Se il bit in posizione 6 vale 1, allora il bit in posizione 5 permette di attivare (1) o disattivare (0) la modalità speciale di mascheramento; se questa modalità è attiva, una ISR che sta elaborando una IRQ può essere interrotta dall'arrivo di un'altra IRQ avente priorità strettamente inferiore o strettamente superiore!

3.5 Un esempio pratico: interfaccia con la tastiera

Ogni volta che premiamo un tasto, l'hardware della tastiera genera un codice che prende il nome di scan code (codice di scansione); tale codice è standard (per tutte le tastiere compatibili) e non ha niente a che vedere con il simbolo stampato sul tasto premuto.
Nel caso più semplice, lo scan code di un tasto premuto ha una ampiezza di 8 bit, con il bit più significativo che vale zero; i 7 bit meno significativi permettono quindi di rappresentare un totale di 27=128 scan codes differenti.
Quando un tasto viene rilasciato, la tastiera genera l'analogo scan code dello stesso tasto premuto; la differenza fondamentale sta nel fatto che il bit più significativo dello scan code questa volta vale 1.
Ad esempio, lo scan code del tasto [A] premuto vale 00011110b=1Eh; lo scan code del tasto [A] rilasciato vale 10011110b=9Eh.

Diversi tasti della tastiera, quando vengono premuti, generano uno scan code formato da due codici a 8 bit, con il primo codice che vale sempre E0h e il secondo codice che ha il bit più significativo che vale 0; questi particolari tasti prendono il nome di extended keys (tasti estesi).
Quando un tasto esteso viene rilasciato, l'hardware della tastiera genera nuovamente due codici, con il primo che vale ugualmente E0h; il secondo codice, come al solito, presenta il bit più significativo che vale 1.
Ad esempio, lo scan code del tasto [Ctrl Right] premuto vale 11100000b 00011101b = E0h 1Dh; lo scan code del tasto [Ctrl Right] rilasciato vale 11100000b 10011101b = E0h 9Dh.

La Figura 3.15 illustra gli scan codes (tasti premuti) che caratterizzano le tastiere IBM compatibili, utilizzate dai PC della famiglia hardware 80x86; gli stessi scan codes sono disponibili anche nella apposita tabella. Osserviamo i due casi particolari rappresentati dai due tasti [Print] e [Pause]; la pressione del tasto [Print] produce lo scan code E0h 2Ah E0h 37h, mentre la pressione del tasto [Pause] produce lo scan code E1h 1Dh 45h E1h 9Dh C5h!

Ogni singolo codice a 8 bit generato dall'hardware della tastiera, viene inserito in un apposito buffer dati, accessibile attraverso la porta 60h del dispositivo (controller) che permette al sistema di interfacciarsi con la tastiera stessa; subito dopo, il controller genera una richiesta di I/O che viene inviata ad un PIC.
Come si nota in Figura 3.10, tale richiesta di I/O giunge alla linea IR1 del PIC Master, per cui si tratta di una IRQ1; di conseguenza, lo stesso PIC Master associa la IRQ1 all'Interrupt Type 09h.

Nel caso particolare dei tasti estesi, i due codici a 8 bit generati dalla tastiera risultano disponibili attraverso due IRQ1 consecutive; questo perché, come è stato appena spiegato, viene generata una IRQ1 per ogni singolo codice a 8 bit (stesso discorso per i 4 codici del tasto [Print] e per i 6 codici del tasto [Pause])!

La CPU soddisfa la IRQ1 attraverso l'istruzione INT 09h; il compito della ISR, chiamata da questa istruzione, è quello di leggere il prossimo codice a 8 bit dalla porta 60h e di metterlo a disposizione dei programmi dopo averlo sottoposto alle opportune elaborazioni.
Uno dei compiti più importanti svolto dalla ISR è quello di convertire lo scan code nel codice ASCII del simbolo stampato sul tasto premuto; questa tecnica permette la cosiddetta "internazionalizzazione delle tastiere", nel senso che, in base alla nazione a cui la tastiera è destinata, basta cambiare gli opportuni simboli sui tasti senza apportare alcuna modifica all'hardware!
Un compito piuttosto complesso, svolto dalla ISR, è quello di gestire adeguatamente anche il caso in cui l'utente prema più tasti contemporaneamente (ad esempio, [Alt Left] + [F1]); si tenga presente, infatti, che anche in tali situazioni la tastiera genera separatamente gli scan codes dei singoli tasti!

Se vogliamo analizzare in pratica le considerazioni appena esposte, non dobbiamo fare altro che intercettare la INT 09h; a tale proposito, come è stato già spiegato nella sezione Assembly Base, le fasi da svolgere sono le seguenti: Questa volta la novità è data dal fatto che stiamo intercettando una richiesta di interruzione che proviene da una periferica; di conseguenza, la nostra ISR dovrà anche procedere all'invio dell'EOI al PIC che ha ricevuto la IRQ!
La Figura 3.16 illustra un semplice esempio che si serve della libreria COMLIB; la nuova ISR installata dal programma KEYBOARD.COM si limita a visualizzare sullo schermo i vari scan codes letti dalla porta hardware 60h. Osserviamo il metodo seguito nel listato di Figura 3.16 per attivare/disattivare le interruzioni mascherabili; anziché utilizzare le istruzioni CLI e STI, ci serviamo di una tecnica più sofisticata che permette di agire solamente sulla linea IR che ci interessa.
Per disabilitare temporaneamente la linea IR1 del PIC Master, possiamo scrivere: Analogamente, per riabilitare la linea IR1 del PIC Master, possiamo scrivere: La nuova ISR installata dal programma, verifica se è stato premuto un tasto "normale" o "esteso"; nel primo caso, viene visualizzato direttamente il relativo scan code rappresentato da un unico codice a 8 bit. Nel secondo caso, viene visualizzato il primo codice E0h; subito dopo, si attende la successiva IRQ1 per poter leggere e visualizzare il secondo codice da 8 bit.
La ISR è anche in grado di visualizzare correttamente lo scan code da 6 byte del tasto [Pause]; per il tasto [Print], invece, vengono mostrati solo gli ultimi due codici.

Si noti che nella ISR, prima di leggere il prossimo codice dalla porta 60h, viene effettuato un loop il cui scopo è quello di attendere il "via libera" per la lettura dalla tastiera; più avanti vengono illustrati maggiori dettagli su questo importante aspetto.

Se proviamo a commentare le istruzioni della ISR che inviano il segnale EOI, possiamo constatare che il programma non risponde più ai nostri comandi; infatti, il bit 1 del registro ISR nel PIC Master rimane settato a 1 bloccando l'elaborazione di ulteriori IRQ1. In un caso del genere, se si sta lavorando in ambiente DOS puro, è necessario spegnere il computer in quanto non è ovviamente possibile riavviare con la sequenza di tasti [Ctrl]+[Alt]+[Canc]!

Se, al termine del programma, dimentichiamo di ripristinare il vecchio vettore 09h, non saremo più in grado di usare la tastiera; come al solito, se un problema del genere si verifica in ambiente DOS puro, è necessario spegnere il computer!

3.5.1 Considerazioni sulla programmazione della tastiera

Nel corso degli anni, l'hardware della tastiera ha subito notevoli evoluzioni, spesso legate a particolari modelli di PC; proprio per questo motivo, la programmazione di tale periferica risulta spesso difficoltosa.

Inizialmente, ai tempi dei primi PC di classe XT, la IBM impose una tastiera standard denominata, appunto, "XT keyboard"; si tratta di un modello di tastiera riconoscibile dal fatto che sono presenti su di essa solamente 84 tasti.
In seguito, con l'avvento dei PC di classe AT, la IBM ha aggiornato anche la tastiera imponendo un modello standard denominato, appunto, "AT keyboard"; in questo caso, il numero dei tasti è salito a 101 o a 102.
Una ulteriore evoluzione è arrivata con l'avvento dell'architettura PS/2, imposta dalla IBM per i PC; questa nuova classe di PC è stata affiancata da un nuovo modello di tastiera denominato, appunto, PS/2 keyboard.
Le tastiere PS/2 sono totalmente compatibili con quelle AT, per cui vengono largamente impiegate anche sui PC che non utilizzano l'architettura PS/2; il successo ottenuto da questo standard è legato anche al fatto che l'hardware (8042 controller) preposto alla gestione della tastiera PS/2, permette di controllare anche un mouse il quale, proprio per questo motivo, prende il nome di PS/2 mouse (riconoscibile dal classico connettore tondo).

A complicare ulteriormente la situazione sono poi arrivati numerosi modelli di tastiere personalizzate; questi nuovi modelli, pur mantenendo la piena compatibilità con gli standard AT e PS/2, hanno introdotto una serie di tasti speciali destinati al particolare modello di PC a cui è associata la tastiera (e spesso anche al particolare SO installato sul PC).
Si possono citare, ad esempio, i tasti ("play", "pause", "eject", ...) per la gestione dei CD Audio, i tasti per la connessione ad Internet, i tasti personalizzati per Windows, etc; in genere, questi tasti speciali possono essere rimappati in modo da poterli usare anche con altri SO.

In riferimento ai modelli più recenti di tastiere (compatibili con gli standard AT e PS/2), è necessario sottolineare che l'interfacciamento con il PC è gestito attraverso un doppio controller denominato, genericamente, 8042; uno dei controller è installato sulla tastiera e prende il nome di keyboard controller (KBC), mentre l'altro è installato sul PC e prende il nome di on-board controller (OBC).
Lo scopo di questi due controller è quello di gestire le comunicazioni bidirezionali tra PC e tastiera; infatti, contrariamente a quanto molti pensano, la tastiera oltre a inviare dati al PC può anche ricevere una serie di appositi comandi!

La gestione delle comunicazioni tra tastiera e PC è di competenza del BIOS e del SO; il programmatore, a meno che non stia scrivendo un proprio SO, deve quindi rigorosamente evitare di modificare lo stato operativo della tastiera stessa.
Tanto per citare un aspetto emblematico, tutte le operazioni (pressione e rilascio dei vari tasti) compiute dall'utente sulla tastiera, vengono registrate in dettaglio dal BIOS e memorizzate in una apposita area della BDA; la Figura 3.17, ad esempio, mostra le informazioni presenti all'indirizzo 0040h:0017h della stessa BDA. Appare evidente quindi che qualunque modifica apportata dal programmatore alla configurazione della tastiera, necessita del conseguente aggiornamento delle informazioni presenti nella BDA; in caso contrario, si impedisce agli altri programmi di funzionare correttamente (a causa di incongruenze tra il contenuto della BDA e lo stato hardware della tastiera)!

Le operazioni di I/O con l'OBC avvengono attraverso la porta 64h; tale porta permette di scrivere comandi o di leggere lo stato della tastiera; la Figura 3.18 illustra le informazioni che si ottengono in seguito alla lettura della porta 64h. Lo Status Byte è molto utile in quanto ci permette di sapere in quale esatto momento possiamo dare inizio alle comunicazioni con la tastiera; ad esempio, prima di inviare un comando alla tastiera dobbiamo attendere che il bit 1 dello Status Byte si sia portato a 0 (input buffer vuoto).

Le operazioni di I/O con il KBC avvengono attraverso le porte 60h e 64h; dopo aver verificato lo Status Byte attraverso la porta 64h, possiamo dare il via alla lettura/scrittura di dati o comandi attraverso la porta 60h.

Ad esempio, prima di leggere un dato dalla porta 60h, dobbiamo attendere che il bit 0 dello Status Byte si sia portato a 1 (output buffer pieno); a tale proposito, possiamo servirci del seguente codice: Osserviamo che CX viene inizializzato a 0; di conseguenza, alla prima iterazione si ottiene:
CX = 0 - 1 = FFFFh = 65535
Si tratta del classico trucco che, sfruttando il wrap around, ci permette di ripetere un loop sino a 65536 volte, anche se il registro contatore (CX) è a 16 bit!
Il loop viene ripetuto solo se CX è maggiore di zero e se, contemporaneamente, l'istruzione TEST produce ZF=1; tenendo conto dei tempi di risposta dell'hardware della tastiera, abbiamo la certezza che durante le 65536 iterazioni il bit 0 dello Status Byte si porterà sicuramente a 1!

Vediamo un semplice esempio pratico che mostra come gestire i tre diodi LED posizionati sulla tastiera in alto a destra; come molti sanno, tali "spie luminose" indicano lo stato delle opzioni "Caps Lock", "Numeric Lock" e "Scroll Lock".
Questo esempio funziona solo quando si opera in ambiente DOS puro; è chiaro, infatti, che i SO come Windows e Linux impediscono l'accesso diretto all'hardware del computer!
I LED della tastiera possono essere pilotati attraverso il comando EDh (set/reset mode indicators); tale comando deve essere inviato attraverso la porta 60h.
Dopo aver ricevuto il comando EDh, la tastiera resta in attesa di un secondo comando contenente le impostazioni per i tre diodi led; la struttura del secondo comando, da scrivere sempre nella porta 60h, è illustrata in Figura 3.19. Prima di tutto creiamoci una apposita procedura il cui compito è quello di scrivere nella porta 60h dopo aver atteso il via libera (input buffer vuoto) tramite la porta 64h. A questo punto, per accendere tutti i tre LED della tastiera possiamo scrivere: Dopo aver eseguito queste istruzioni, è importante riportare i LED allo stato precedente, in modo che non ci siano incongruenze con le informazioni presenti nella BDA; a tale proposito, è necessario evitare la pressione dei tre appositi tasti.
La cosa migliore da fare consiste nel rieseguire il precedente codice caricando l'opportuno valore in AL; ad esempio, se in origine era accesa la sola spia "Numeric Lock", dobbiamo porre AL=00000010b.

Bibliografia

Intel - Interfacing the 82C59A to Intel 186 Family Processors
(27282201.pdf)

Intel - Understanding the Interrupt Control Unit of the 80C186EC/80C188EC Processor
(27282301.pdf)

Intel - 80C186EB/80C188EB Microprocessor User's Manual (Capitolo 8)
(27083003.pdf)

Intel - 82093AA I/O ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (IOAPIC)
(29056601.pdf)

IBM Technical Reference Manual - 8042 Keyboard Controller
(8042.pdf)