Assembly Avanzato

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.

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 1 illustra alcune delle principali eccezioni.

Figura 1 - Principali eccezioni della CPU
INTEccezione
00h Si è verificata una divisione per zero durante una operazione
01h Esecuzione single-step di un programma (debug mode)
03h Breakpoint incontrato in un programma
04h Si è verificato un overflow durante una operazione
05h Bound range exceeded (indice fuori limite in un vettore)
06h Opcode non valido
07h Dispositivo (o estensione della CPU) non disponibile
0Dh General protection fault (protected mode)
0Eh Page fault (protected mode)

Alcune delle eccezioni illustrate in Figura 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 3 e la Figura 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 Mb 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.
I passi compiuti dalla CPU sono i seguenti:

1) la CPU salva il registro FLAGS nello stack;
2) la CPU pone TF=0 (Trap Flag) e IF=0 (Interrupt Enable Flag);
3) la CPU salva l'indirizzo di ritorno completo Seg:Offset nello stack;
4) la CPU legge l'indirizzo Seg:Offset che si trova in posizione n*4 nella IVT;
5) la CPU carica tale indirizzo in CS:IP e salta a CS:IP.

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).

Nota importante.
Prima di chiamare una ISR, è necessario salvare lo stato del programma da interrompere; a tale proposito, la CPU si limita a salvare solamente il contenuto del registro FLAGS.
Il compito importantissimo di salvare il contenuto di eventuali registri, spetta quindi alla ISR; in sostanza, la ISR deve preservare il contenuto di tutti i registri che utilizza.
Se non si rispetta questa regola, al termine della ISR viene riavviato il programma precedentemente interrotto, il quale si trova a lavorare con dei registri il cui contenuto ha subito modifiche; la conseguenza è che, in genere, il programma va in crash!

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:

1) la CPU estrae due WORD dallo stack e le carica in CS:IP;
2) la CPU estrae una WORD dallo stack e la carica nel registro FLAGS;
3) la CPU salta a CS:IP (indirizzo di ritorno).

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!

Nota importante.
Teoricamente, il programmatore ha la possibilità di intercettare una qualsiasi interruzione n, hardware o software; a tale proposito, non deve fare altro che installare in memoria una propria ISR il cui indirizzo deve essere collocato all'indice n nella IVT.
In realtà, è vivamente sconsigliabile intercettare tutte quelle interruzioni (soprattutto hardware) associate a ISR che svolgono compiti molto complessi e delicati; ciò è vero, in particolare, per la INT 02h che viene associata ad una NMI!

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 2 illustra lo schema semplificato di un PIC 8259.

Figura 2 - 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 3 e Figura 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)!

Nota importante.
Al termine di una ISR destinata a soddisfare una richiesta di I/O da parte di una periferica, non è sufficiente eseguire una istruzione IRET (come accade per le interruzioni software o per le eccezioni); il programmatore deve prima provvedere ad inviare al PIC un apposito segnale denominato EOI (End Of Interrupt).
Il compito di tale segnale è quello di riportare a 0 il bit del registro ISR corrispondente alla IRQ appena elaborata; se non viene compiuto questo passo, il PIC non sarà più in grado di elaborare ulteriori IRQ associate a quello stesso bit rimasto a 1 nel registro ISR!


3.3.1 Collegamento dei PIC in cascata

Lo schema di Figura 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 che si riferisce al caso molto diffuso di due soli PIC in cascata.

Figura 3 - PIC 8259 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. 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 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.

Figura 4 - Porte hardware dei PIC
PortaIndirizzo
MPICP0 20h
MPICP1 21h
SPICP0 A0h
SPICP1 A1h

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 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.

Figura 5 - Comando ICW1 (Write - 20h/A0h)
BitSignificato
0 ICW4 richiesto? 0 = no, 1 = si
1 PIC in cascata? 0 = si, 1 = no
2 Dimensione indirizzi IVT: 0 = 4 byte, 1 = 8 byte
3  Rilevamento IRQ: 0 = edge-triggered, 1 = level-triggered
4 Tipo comando: 1 = ICW1
5 Indirizzi ISR per le CPU MCS-80/85
 (000b per le CPU 80x86)
6
7

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, 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 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.

Figura 6 - Comando ICW2 (Write - 21h/A1h)
BitSignificato
0  Non usati (ignorati)
1
2
3 Interrupt Type - bit 3
4 Interrupt Type - bit 4
5 Interrupt Type - bit 5
6 Interrupt Type - bit 6
7 Interrupt Type - bit 7

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 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 7 illustra la struttura del comando ICW3 che deve essere scritto esclusivamente nella porta 21h del PIC Master.

Figura 7 - Comando ICW3 (Write - 21h)
BitSignificato
0  0 = IRQ0, 1 = INT da un PIC Slave (CAS = 000b)
1  0 = IRQ1, 1 = INT da un PIC Slave (CAS = 001b)
2  0 = IRQ2, 1 = INT da un PIC Slave (CAS = 010b)
3  0 = IRQ3, 1 = INT da un PIC Slave (CAS = 011b)
4  0 = IRQ4, 1 = INT da un PIC Slave (CAS = 100b)
5  0 = IRQ5, 1 = INT da un PIC Slave (CAS = 101b)
6  0 = IRQ6, 1 = INT da un PIC Slave (CAS = 110b)
7  0 = IRQ7, 1 = INT da un PIC Slave (CAS = 111b)

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 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.

Figura 8 - Comando ICW3 (Write - A1h)
BitSignificato
0 CAS (cascade)
1
2
3  Riservati (devono valere 00000b)
4
5
6
7

Nel caso di Figura 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.

Figura 9 - Comando ICW4 (Write - 21h/A1h)
BitSignificato
0  Modalità: 0 = MCS-80/85, 1 = 80x86
1 EOI: 0 = normal mode, 1 = auto mode
2 Bufferizzazione dati:
 00b e 01b = no, 10b = si (Slave), 11b = si (Master)
3
4 Gestione IRQ: 0 = sequential mode, 1 = SFNM
5 Riservati (devono valere 000b)
6
7

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.

%assign  MPICP0         20h         ; porta P0 del PIC Master
%assign  MPICP1         21h         ; porta P1 del PIC Master
%assign  SPICP0         0A0h        ; porta P0 del PIC Slave
%assign  SPICP1         0A1h        ; porta P1 del PIC Slave

%assign  MPIC_BASE_TYPE 08h         ; BASE_TYPE del PIC Master
%assign  SPIC_BASE_TYPE 70h         ; BASE_TYPE del PIC Slave

   cli                              ; clear INT enable flag

; inizializzazione PIC Slave

   mov      al, 00010001b           ; ICW1 = ICW4 richiesto, CAS
   out      SPICP0, al              ; scrive ICW1
   mov      al, SPIC_BASE_TYPE      ; ICW2 = 70h
   out      SPICP1, al              ; scrive ICW2
   mov      al, 00000010b           ; ICW3 = Slave connesso a IR2 Master
   out      SPICP1, al              ; scrive ICW3
   mov      al, 00000001b           ; ICW4 = normal EOI, sequential
   out      SPICP1, al              ; scrive ICW4
   
; inizializzazione PIC Master

   mov      al, 00010001b           ; ICW1 = ICW4 richiesto, CAS
   out      MPICP0, al              ; scrive ICW1
   mov      al, MPIC_BASE_TYPE      ; ICW2 = 08h
   out      MPICP1, al              ; scrive ICW2
   mov      al, 00000100b           ; ICW3 = input IR2 da Slave
   out      MPICP1, al              ; scrive ICW3
   mov      al, 00000001b           ; ICW4 = normal EOI, sequential
   out      MPICP1, al              ; scrive ICW4
   
   sti                              ; set INT enable flag

In seguito all'inizializzazione standard effettuata dal BIOS, risultano definite le associazioni tra periferiche (IRQ) e Interrupt Type (n); la Figura 10 illustra la situazione più diffusa per il PIC Master.

Figura 10 - Associazioni IRQ - INT n (PIC Master)
IRQAssegnato aINT
0  PIT 8254 - Programmable Interval Timer08h
1  Keyboard controller09h
2  IRQ da 8 a 15 in cascata dal PIC Slave0Ah
3  Serial port COM2 (o COM4)0Bh
4  Serial port COM1 (o COM3)0Ch
5  Parallel port LPT20Dh
6  Floppy Disk controller0Eh
7  Parallel port LPT10Fh

La Figura 11 illustra la situazione più diffusa per il PIC Slave.

Figura 11 - Associazioni IRQ - INT n (PIC Slave)
IRQAssegnato aINT
8  CMOS-RTC - Real Time Clock70h
9  VGA/LAN/ACPI (IRQ2 Dirottata dal PIC Master)71h
10  Riservato (schede video)72h
11  Riservato (schede audio)73h
12  Mouse PS/274h
13  Eccezioni del coprocessore matematico (FPU)75h
14  Hard Disk controller (IDE0)76h
15  Riservato (IDE1)77h

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 kinfocenter; in alternativa, da una console 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, 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 12 illustra il comando OCW1 che deve essere letto/scritto attraverso la porta 21h del PIC Master o A1h del PIC Slave.

Figura 12 - Comando OCW1 (Read/Write - 21h/A1h)
BitSignificato
0  IMR - IR0: 0 = unmask, 1 = mask
1 IMR - IR1: 0 = unmask, 1 = mask
2 IMR - IR2: 0 = unmask, 1 = mask
3 IMR - IR3: 0 = unmask, 1 = mask
4 IMR - IR4: 0 = unmask, 1 = mask
5 IMR - IR5: 0 = unmask, 1 = mask
6 IMR - IR6: 0 = unmask, 1 = mask
7 IMR - IR7: 0 = unmask, 1 = mask

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:

   in       al, MPICP1              ; legge l'IMR del PIC Master
   or       al, 00000010b           ; pone a 1 il bit 1 di AL
   out      MPICP1, al              ; scrive OCW1 nell'IMR

Analogamente, per riabilitare la IRQ1 (PIC Master) possiamo scrivere:

   in       al, MPICP1              ; legge l'IMR del PIC Master
   and      al, 11111101b           ; pone a 0 il bit 1 di AL
   out      MPICP1, al              ; scrive OCW1 nell'IMR

La Figura 13 illustra il comando OCW2 che deve essere scritto nella porta 20h del PIC Master o A0h del PIC Slave.

Figura 13 - Comando OCW2 (Write - 20h/A0h)
BitSignificato
0  Livello di rotazione priorità IRQ
1
2
3 Il valore 00b identifica OCW2
4
5  End Of Interrupt
6  Livello di rotazione: 0 = ruota di 1, 1 = vedi bit 0, 1, 2
7  Rotazione priorità: 0 = no, 1 = vedi bit 6

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:

Ingresso IR0IR1IR2IR3 IR4IR5IR6IR7
Priorità 12345670

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:

Ingresso IR0IR1IR2IR3 IR4IR5IR6IR7
Priorità 34567012

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 nessuna 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:

   mov      al, 20h                 ; OCW2 = Specific EOI
   out      MPICP0, al              ; scrive OCW2 nel PIC Master

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:

   mov      al, 20h                 ; OCW2 = Specific EOI
   out      SPICP0, al              ; scrive OCW2 nel PIC Slave
   out      MPICP0, al              ; scrive OCW2 nel PIC Master

La Figura 14 illustra il comando OCW3 che deve essere scritto nella porta 20h del PIC Master o A0h del PIC Slave.

Figura 14 - Comando OCW3 (Write - 20h/A0h)
BitSignificato
0  Lettura registri IRR (10b) e ISR (11b)
1
2 Polling mode: 0 = no, 1 = si
3 Il valore 01b identifica OCW3
4
5 Mask mode: 0 = normal, 1 = special
6 Special mask mode: 0 = no, 1 = si
7 Riservato (deve valere 0)

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:

Contenuto IN--------W2W1W0
Bit 76543210

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 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.

Figura 15 - Codici di scansione della tastiera

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; subito dopo, lo stesso hardware della tastiera genera una richiesta di I/O che viene inviata ad un PIC.
Come si nota in Figura 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 nessuna 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:

1) salvare il vecchio vettore di interruzione 09h;
2) installare la nuova ISR;
3) completare l'esecuzione del programma;
4) ripristinare il vecchio vettore di interruzione 09h;
5) terminare il programma.

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 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.

Figura 16 - File KEYBOARD.ASM

Osserviamo il metodo seguito nel listato di Figura 1 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:

   in       al, MPICP1              ; legge l'IMR del PIC Master
   or       al, 00000010b           ; pone a 1 il bit 1 di AL
   out      MPICP1, al              ; scrive OCW1 nell'IMR

Analogamente, per riabilitare la linea IR1 del PIC Master, possiamo scrivere:

   in       al, MPICP1              ; legge l'IMR del PIC Master
   and      al, 11111101b           ; pone a 0 il bit 1 di AL
   out      MPICP1, al              ; scrive OCW1 nell'IMR

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 17, ad esempio, mostra le informazioni presenti all'indirizzo 0040h:0017h della stessa BDA.

Figura 17 - Keyboard Status Flag 1
BitSignificato
0  Stato del tasto [Shift Right]
1 Stato del tasto [Shift Left]
2 Stato del tasto [Ctrl] (Left o Right)
3 Stato del tasto [Alt] (Left o Right)
4 Stato del led "Scroll Lock"
5 Stato del led "Numeric Lock"
6 Stato del led "Caps Lock"
7 Stato del tasto [Ins]

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 18 illustra le informazioni che si ottengono in seguito alla lettura della porta 64h.

Figura 18 - OBC Status Byte (Read - 64h)
BitSignificato
0  Stato del buffer di Output: 0 = vuoto, 1 = pieno
1 Stato del buffer di Input: 0 = vuoto, 1 = pieno
2 Flag di sistema (POST): 0 = fallito, 1 = passato
3 Informazioni in attesa: 0 = dato (60h), 1 = comando (64h)
4 Stato della tastiera: 0 = disabilitata, 1 = abilitata
5 Errore in fase di trasmissione: 0 = no, 1 = si
6 Errore di time-out: 0 = no, 1 = si
7 Errore di parità: 0 = no, 1 = si

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 0 dello Status Byte si sia portato a 0 (tastiera pronta per l'output).

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 1 dello Status Byte si sia portato a 0 (tastiera pronta per l'input); a tale proposito, possiamo servirci del seguente codice:

   xor      cx, cx               ; max 65536 iterazioni
wait_for_input:
   in       al, 64h              ; legge lo Status Byte (OBC)
   test     al, 00000010b        ; bit 1 = 0?
   loopnz   wait_for_input      ; controllo loop
   
   in       al, 60h              ; lettura dati (KBC)

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=0; tenendo conto dei tempi di risposta dell'hardware della tastiera, abbiamo la certezza che durante le 65536 iterazioni il bit 1 dello Status Byte si porterà sicuramente a zero!

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; e 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 19.

Figura 19 - Set/Reset mode indicators (Write - 60h)
BitSignificato
0  Scroll Lock LED: 0 = off, 1 = on
1 Numeric Lock LED: 0 = off, 1 = on
2 Caps Lock LED: 0 = off, 1 = on
3 Riservati (devono valere 00000b)
4
5
6
7

Prima di tutto creiamoci una apposita procedura il cui compito è quello di scrivere nella porta 60h dopo aver atteso il via libera tramite la porta 64h.

; AL = informazione da scrivere

sendData:

   cli                           ; disabilita le INT masch.
   
   push     ax                   ; salva AX
   
   xor      cx, cx               ; max 65536 iterazioni
wait_for_output:
   in       al, 64h              ; legge lo Status Byte (OBC)
   test     al, 00000001b        ; bit 0 = 0?
   loopnz   wait_for_output      ; controllo loop
   
   pop      ax                   ; ripristina AX
   out      60h, al              ; invio dati al KBC
   
   sti                           ; ripristina le INT masch.
   
   retn                          ; NEAR return

A questo punto, per accendere tutti i tre LED della tastiera possiamo scrivere:

   mov      al, 0EDh             ; mode indicators
   call     sendData             ; scrive il comando
   
   mov      al, 00000111b        ; tutti i LED accesi
   call     sendData             ; scrive il comando

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.

Nota importante.
Bisogna ribadire che la gestione dei comandi di configurazione della tastiera è di competenza del BIOS e del SO; è vivamente sconsigliabile quindi affidare questo compito ai propri programmi in quanto si possono provocare malfunzionamenti del computer.
Si tenga anche presente che sui vecchi PC di classe XT, l'invio di comandi casuali alla tastiera poteva provocare anche danni all'hardware; per maggiori dettagli, si consiglia di consultare la documentazione tecnica.


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)