Sei sulla pagina 1di 21

1.

Dichiarazione delle variabili (e delle costanti)

La dichiarazione delle variabili è generalmente la prima cosa da fare quando si vuole scrivere la
parte software di un compito d’esame.

Se si vuole specificare un valore specifico o una costante, quale può essere l’indirizzo a cui si
trova, per esempio, una certa risorsa di I/O o il PIC, sarà necessario fare uso della parola EQU:
es. Display EQU 80H
Questo fa comodo in quanto, all’interno del codice, diventerà facile ed espressivo effettuare
operazioni coi comandi IN e OUT:
es. OUT Display AX
(manda la word presente in AX verso BufferOUT, cioè verso 80H)
Per quale tipo di dispositivi bisogna specificare l’indirizzo in questo modo?
• Per i dispositivi di I/O coinvolti nelle varie operazioni (ad es. una centralina ABS1, un
pulsante, un componente 244, un componente 373);
• per il PIC (che dovrà essere anche programmato a parte);
• per tutti quei dispositivi, insomma, che sono attaccati al bus dati e che hanno necessità di
essere attivati/disattivati (o comunque interfacciati).
NOTA: In genere si utilizza EQU per indicare l’indirizzo o la locazione di un dispositivo di I/O,
ma non il dato che tale dispositivo può fornire o l’informazione che esso stesso deve gestire: per
quello bisognerà definire una variabile (cioè un tipo “dato”, v. di seguito).
EQU può anche utilizzato anche per definire le parole di comando da dare al PIC (le OCW e le
ICW):
es. MASKING EQU FFH ; tutte le interruzioni sono mascherate
8259_OCW1 EQU C1H ; alla locazione C1H è possibile “scrivere” OCW1
….
MOV AL, FFH ; scriviamo in OCW1 passando per un registro
OUT 8259_OCW1, AL

Bisognerà invece utilizzare una sintassi del genere


es. BUF_IN DB 1024
LedFlag DB 1 (il numero che segue DB è una lunghezza in byte2)
per le variabili o i dati, ad es.:
• un flag di fine routine;
• un dato caratteristico del problema o, comunque, un dato comunicato da (o da comunicare
a) un dispositivo di I/O (ad es. la velocità angolare di una ruota1, il valore di un display3);
• un buffer o un vettore che dev’essere riempito (ad es. in una comunicazione seriale4);
• un puntatore che può cambiare valore nel corso dell’esecuzione del programma;
• un indice (che di sua natura è qualcosa di variabile).

Si noti che quando nel programma principale si dovranno richiamare le costanti (definite con
EQU) o le variabili (definite con DB, DW, etc…):

1 Vedi compito del 7 aprile 2007.


2 Dovremo scegliere 1 se abbiamo un byte, 2 se abbiamo una word (in alternativa si poteva scrivere WORD DW 1, in quanto DW
ritaglia uno spazio di memoria in word), 1024 se abbiamo un buffer di 1 KB, etc… Se dobbiamo specificare N kilobytes conviene
utilizzare la notazione N*1024 dopo DB.
3 Vedi compito del 7 dicembre 2006.

4 Vedi compito dell’11 gennaio 2007.


• dovremo richiudere fra parentesi quadre [ … ] le variabili,
ad es. Index DB 1

MOV [Index], SI

• scrivere così come sono (senza parentesi) le costanti,
ad es. InterfacciaParallelo EQU 80H

IN AL, InterfacciaParallelo

Par superfluo ricordare che, se dobbiamo allocare spazio per una variabile, dobbiamo essere sicuri
che tale spazio sia sufficiente. Ad es. se vogliamo memorizzare da qualche parte l’informazione
riguardate i secondi da visualizzare sul display di un orologio, un byte sarà sufficiente (i secondi
vanno da 0 a 59 e con 8 bit riusciamo a fare tutto).

2. Il PIC (8259)

Il PIC, nome in codice 8259, è un componente della famiglia Intel che si


occupa di gestire il meccanismo delle interruzioni: esso è in grado di
gestire fino a 8 richieste di interruzione, corrispondenti ai piedini (IR0…
IR7). Ad essi andranno a finire i segnali di quei dispositivi (pulsanti,
clock, etc..) che devono essere gestiti ad interrupt.
Alcune caratteristiche:
• può essere utilizzato singolarmente o in cascata (soluzione
avanzata che non tratteremo): se preso da solo gestisce fino a 8
interruzioni mentre nella configurazione più complicata può gestirne
fino a 64;
• si tratta di un dispositivo programmabile: possiamo cioè variarne opportunamente il
comportamento;
• si interfaccia sia col bus dati (8 bit) che con quello degli indirizzi (1 bit);
• è un dispositivo bifronte (rivolto sia verso il processore che verso i dispositivi periferici);
• il nocciolo del dispositivo ruota attorno ad alcuni registri ad 8 bit:
o IMR (Interrupt Mask Register): permette di mascherare le richieste di interruzione
provenienti da uno o più dispositivi (0 = non mascherato, 1 = mascherato);
o IRR (Interrupt Request
Register) tiene traccia
dei dispositivi che
hanno fatto una
richiesta e che non
sono riusciti ancora ad
averne riscontro;
o ISR (Interrupt Service
Register): tiene traccia di quali sono le procedure di servizio in corso (se la richiesta i-
esima viene servita viene resettato il bit corrispondente in IRR e settato quello in
ISR).
SEQUENZA DI INTERRUZIONE:
1. una richiesta arriva all’8259;
2. viene posto ad 1 il corrispondente bit del registro IRR per segnalare che c’è stata una
richiesta;
3. si valuta se la richiesta dev’essere eseguita o meno controllando l’IMR;
4. viene avanzata la richiesta di interruzione al processore (attraverso INT);
5. se IF vale 1 il processore conferma la ricezione della richiesta e invia un primo
segnale di INTA;
6. l’8259 fa la cernita di quali sono i dispositivi in attesa di essere serviti e seleziona
quello con priorità più alta;
7. forza ad 1 il corrisponde bit in ISR;
8. forza a 0 il corrispondente bit in IIR;
9. il processore invia un secondo impulso su INTA;
10. l’8259 scrive sul data bus il codice del dispositivo che ha fatto richiesta di
interruzione;
11. il processore riceve il messaggio e attiva la procedura di servizio dell’interruzione.
L’8259 è in grado di riconoscere, se adeguatamente programmato, quali siano le richieste più
urgenti (cioè più prioritarie): di default, le richieste che giungono su IR0 sono le più prioritarie e
quelle su IR7 le meno prioritarie (maggiore è i e minore è la priorità). Nella modalità di
funzionamento detta Fully nested mode, un interrupt più prioritario può sempre interrompere
l’esecuzione della procedura di servizio di un interrupt meno prioritario, mentre una richiesta su
una linea IRi a minore priorità non viene servita fino al termine delle procedure di servizio
associate alle precedenti (e più prioritarie) richieste.
Il riconoscimento delle richieste agli IRi (cioè ai piedini) può avvenire a livello oppure sul fronte di
salita:
• se rileviamo a fronte l’interrupt viene generato a seguito di una transizione di un ingresso
da 0 a 1. Viene quindi generato un interrupt in corrispondenza di ogni fronte positivo di un
segnale d’ingresso IRi;
• se rileviamo a livello l’interrupt viene riconosciuto se viene generato un livello logico alto
di un ingresso Iri. A differenza della gestione a fronte, se il valore di IRi non si porta a
valore logico basso prima dell’invio del comando EOI verrà generato un altro interrupt.
Si noti che, affinché l’interrupt sia riconosciuto come tale, occorre che IRi permanga al valore
logico 1 almeno fino al primo fronte negativo del segnale INTA*: al primo abbassamento di
quest’ultimo, infatti, il PIC congela lo stato delle richieste e non ne accetta più; sul secondo
abbassamento, invece, viene emesso l’interrupt type associato alla richiesta più prioritaria attiva.

3. Programmazione del PIC

La programmazione del PIC passa attraverso la definizione di alcuni comandi di inizializzazione


inviati dal processore. In particolare, vengono spedite:
• le ICWs (Initialization Command Words): sono inviate all’inizializzazione. L’ordine delle ICW
è fisso e c’è una ben precisa sequenza con la quale devono essere inviate (l’ordine è
prestabilito e immutabile).
o ICW1 5 ha alcuni bit notevoli: D3 (LTIM) specifica se il PIC dev’essere sensibile a
fronte [= 0] o a livello [=1]; D1 (SNGL) specifica se il dispositivo è in cascata con
dispositivi analoghi [=0] o meno [=1]; D0 (IC4) serve per far capire all’8259 se ci sarà
una ICW4.

5 L’8259 ha un unico bit d’indirizzo A0; la sequenza ICW1 è riconoscibile rispetto a tutte le altre perché ha D4 = 1.
o ICW2 riguarda la necessità per il processore di comunicare all’8259 le informazioni
necessarie per poter poi generare i codici dei dispositivi che hanno fatto richiesta di
interruzione. Dopo che l’8259 ha avanzato una richiesta di interruzione al
processore, questo - attraverso i due impulsi di INTA - vorrà ricevere un codice
(associato al dispositivo che ha fatto la richiesta) che gli permetta di sapere chi ha
fatto l’interruzione. Chi dice all’8259 quali siano questi codici è la ICW2 secondo un
meccanismo che prevede alcuni vincoli su quali siano questi codici: i codici devono
essere contigui fra di loro (numerati in sequenza) e devono iniziare ad un valore
multiplo di 8. La parola ICW2 contiene i 5 bit più significativi, comuni agli 8 codici; i
3 bit meno significativi corrispondono all’indice del
piedino IR0...7 a cui è connesso il dispositivo.
Es: all’inizializzazione del PIC passiamo la stringa
ADDRESS EQU 1FH = 0001 1111
Con questa riga di codice diciamo all’8259 che i cinque
bit più significativi sono a 11111. La parola ICW2 avrà
quindi valore 1111 1XXX = F8H e gli interrupt types
(specificati dai tre bit a X) andranno da F8H a FFH.
o ICW3 viene usata solo quando l’8259 è connesso in
cascata.
o ICW4 non è obbligatoria da inviare: se non viene
inviata, tuttavia, alcune funzionalità non vengono attivate. Bit notevoli: D4 (SFNM
 Special Fully Nested Mode), D1 (AEOI  Automatic End of Interrupt).
• le OCWs (Operation Command Words): possono essere inviate in qualsiasi momento per
modificare la modalità di funzionamento del dispositivo. Possono essere inviate in
qualunque ordine.
o OCW1 permette al programmatore di caricare un valore nel registro di maschera: in
questo modo si possono mascherare (ovvero ignorare) le richieste di interruzione
provenienti dai dispositivi. La OCW1 viene caricata direttamente in IMR: se l’i-esimo
bit di OCW1 vale 1 si forza infatti ad 1 il bit corrispondente di IMR (e si maschera
perciò la rispettiva richiesta).
o OCW2 permette di programmare in maniera diversa attività o aspetti legati alle
priorità e all’EOI (End Of Interrupt). Ogni OCW2 permette di fare un’operazione su
un canale (ovvero su uno dei piedini IR0..7 su cui arrivano le richieste di
interruzione). Possiamo ad esempio voler segnalare all’8259 che è terminata la
procedura di servizio della richiesta di interruzione associata ad un certo piedino:
nel caso in cui il PIC sia stato spettato con il bit di AOEI = 0 (v. ICW4), è infatti a
carico della CPU provvedere a resettare il bit del registro ISR corrispondente
all’interrupt appena servito. Per avvisare il PIC mandiamo quindi una OCW2
apposita.
o OCW3 permette di eseguire una serie di operazioni che permettono sostanzialmente
di leggere i registri del processore.
Quando in un esercizio è necessario programmare il PIC, bisogna scrivere la seguente sequenza, in
cui andiamo a ripescare il caso del compito del 28 marzo 2007.

; Indirizzi
[Questa parte non richiede sforzo di ragionamento, basta aggiungere le quantità tra parentesi
all’indirizzo esadecimale (“base”) cui è mappato il PIC (nell’esempio 90H)]
ICW1 EQU 90H Base (+ 0)
ICW2 EQU 91H Base (+ 1)
ICW4 EQU 91H Base (+ 1)
OCW1 EQU 91H Base (+ 1)
OCW2 EQU 90H Base (+ 0)

; Parole di comando
RESET EQU 13H ; questo è fisso, serve a segnalare che l’interrupt viene
; rilevato sul fronte
ADDRESS EQU 1FH ; qui vanno messi i bit più significativi dell’interrupt type
; 1F = 11111  ICW2 1111 1000 = F8H
EN_AEOI EQU 1H ; Servirà per definire l’AEOI disabilitato (3H = abilitato)
MASK EQU F8H ; Questa parte va scelta con attenzione: in genere si sceglie
; di mascherare i bit relativi ai piedini non utilizzati:
; nell’esempio usiamo 3 piedini (IR0, IR1, IR2) e
; mascheriamo tutti gli altri 1111 1000 = F8H
EOI EQU 20H ; da inviare per segnalare la fine della routine

; Inizializzazione
CLI
OUT ICW1, RESET ; mettiamo in ICW1 0001 0011
OUT ICW2, ADDRESS ; mettiamo in ICW2 0001 1111
OUT ICW4, EN_AEOI ; mettiamo in ICW4 0000 0001 (no AEOI)
OUT OCW1, MASK ; mettiamo in OCW1 1111 1000 (maschera)
STI

4. Interrupt vs. polling

Tutti i sistemi a microprocessore eseguono operazioni di input/output: questo fa scaturire una


questione relativa a quando e con che cadenza il processore debba andare a prendere un nuovo
dato da un dispositivo I/O (ad es. da una porta seriale). Il problema della sincronizzazione fra le
varie periferiche è quindi suscettibile di due possibili soluzioni:
• polling: il processore legge periodicamente il registro di stato del dispositivo tramite un
ciclo software, il quale scandisce di continuo il registro di stato e quant’altro serva a capire
quale sia lo stato del dispositivo periferico.
Es. secsGoesOn: CMP [Setting],1 ; è premuto il pulsante set?
JE exit
JMP secsGoesOn
Questa operazione culmina nel momento in cui il dispositivo richiede il servizio (nell’es.
[Setting] va ad 1).
Es(2). MOV DX, prSTAT ; prSTAT = stato della periferica (stampante)
pr_n_ready: IN AL, DX ; legge lo stato della stampante
TEST AL, sERR
JZ pr_error ; errore
TEST AL, sBUSY
JZ pr_n_ready ; busy: acquisizione periodica dello stato della
stampante
VANTAGGI: meccanismo semplice, non si richiede particolare supporto hardware, basta
che il dispositivo abbia un suo registro di stato attraverso il quale comunicare col
processore:
SVANTAGGIO: il processore spreca tantissimi cicli per fare l’operazione di controllo,
inutile dal punto di vista della computazione. Se abbiamo più elementi connessi a polling il
processore deve scandire tutti gli elementi e spreca tantissimi cicli;
questo implica inoltre che un dispositivo debba attendere che il
processore interroghi il suo registro di stato prima di essere servito,
ma se i dispositivi sono molto può essere che debba aspettare a
lungo (elevata latenza).
La tipica configurazione di un dispositivo gestito a polling, ad es.
un pulsante, è quella a fianco (il pulsante SET può scrivere sul bus
quando viene abilitato da CS_SET# e IORDC#). Si noti che il nostro
pulsante comunica direttamente col bus dati (mentre un dispositivo
gestito ad interrupt dev’essere collegato al PIC 8259).
• interrupt: il processore non interroga i dispositivi di I/O, ma sono questi ultimi a contattare
il processore tramite una richiesta di interruzione (interrupt). Esiste infatti un meccanismo
supportato in hardware attraverso il quale le periferiche fanno sapere al processore che
sono pronte ad essere servite (v. meccanismo delle interruzioni).
VANTAGGI: il meccanismo dell’interrupt permette al processore di gestire liberamente il
suo tempo tra un’interruzione e l’altra. Questa soluzione è quindi più performante, in
quanto non fa sprecare al microprocessore neanche un ciclo per fare le insistenti e spesso
inconcluse richieste a polling. Inoltre, possiamo stabilire criteri di mascheramento e priorità
tramite un’opportuna programmazione del PIC (v. relativo paragrafo). La gestione ad
interrupt è particolarmente adatta ai sistemi multitasking.
SVANTAGGI: dobbiamo fornire supporto hardware e prevedere un controllore che si faccia
carico della gestione dell’interrupt; la realizzazione è più complessa rispetto al caso polling.
Esempio di flusso di programma che esegue un’operazione di I/O seguendo lo schema ad
interrupt:

NOTA: l’interrupt handler trasferisce un solo dato alla volta! Spesso negli esercizi capita di risolvere
problemi di “riempimento buffer”6, i quali devono essere risolti trasferendo - nel caso specifico -
un byte alla volta.

6 Compito dell’11 gennaio 2007.


5. Meccanismo delle interruzioni

Quando il processore decide di servire una richiesta d’interruzione ciò che fa è scatenare una
procedura di servizio di interruzione che blocca il flusso del programma in esecuzione. Al termine
della procedura (a causa della quale abbiamo lanciato l’interrupt) il programma viene riattivato da
punto esatto in cui era stato interrotto, ossia in corrispondenza dell’istruzione successiva a quella
eseguita per ultima. Nel generico sistema a microprocessore se abbiamo n dispositivi periferici,
allora n sarà il numero degli “attori” in grado di poter avanzare una richiesta di interruzione; il
processore però ha solo un piedino, quindi ci sarà bisogno di un controllore delle interruzioni che
faccia da controllore e da “schedulatore” delle richieste di interrupt. Grazie a questo controllore (è
il PIC 8259) vi sarà la possibilità di decidere se alcune richieste debbano essere ignorate, o se vi
siano diversi stadi di priorità. Il processore si interfaccia quindi col controllore e non direttamente
con i dispositivi di interrupt.
Nei sistemi Intel il processore dispone di due segnali:
• INT: viene portato alto per segnalare
al processore che un dispositivo ha
fatto richiesta di interruzione;
• INTA: segnale di uscita del processore,
il quale segnala di aver percepito la
richiesta e richiede il codice del dispositivo.
Si ha anche un flag IF (Interrupt Flag), il quale permette di abilitare/disabilitare le richieste di
interruzioni: questo bit può essere modificato via software attraverso le istruzioni CLI
(interdiciamo le interruzioni) e STI (riabilitiamo le interruzioni). L’IF viene automaticamente posto
a 0 all’attivazione di una procedura di servizio dell’interruzione.

Problemi importanti:
• Come fa il processore a capire chi ha avanzato una richiesta di interrupt?
La CPU richiede al controllore chi è stato attraverso un ciclo di bus apposito; come risposta,
il controllore passa al microprocessore un certo codice che permette di individuare
univocamente chi ha fatto la
richiesta.
• Una volta individuato il
dispositivo, come fa il sapere
quale procedura di servizio
dev’essere attivata?
La soluzione passa attraverso
una tabella (IVR, Interrupt
Vector Table, v. figura a destra),
che mette in corrispondenza i
codici dei vari dispositivi con
le relative procedure. La IVR
risiede in memoria principale e associa a ciascun codice di interruzione l’indirizzo della
corrispondente procedura di servizio. Ogni entry di questa tabella viene detta interrupt type;
un interrupt type è un numero naturale a 8 bit, quindi esistono 2 8 (256) possibili tipi di
interruzione (0… 255). Ogni elemento della tabella occupa 4 byte, per cui la tabella avrà
dimensione pari a 1 KB (M[0…3FFH]).
SCHEMA DEL PROTOCOLLO DI INTERRUZIONE
1. Un dispositivo richiede un servizio attraverso una richiesta di interruzione (mandata al PIC
e non al BUS dati come avviene nel caso polling);
2. il PIC decide se quella richiesta ha da essere servita o meno, e in caso positivo inoltra quella
(sola) richiesta al processore;
3. il processore riceve la richiesta e, al termine dell’istruzione che sta eseguendo in quel
momento, la rileva. Se la richiesta ha le caratteristiche per essere servita, il programma in
esecuzione viene interrotto;
4. viene interrogato il controllo
delle interruzioni per sapere il
codice del dispositivo che ha
avanzato la richiesta (si passa
attraverso il piedino INTA, che
sta per INTerrupt Acknowledge);
5. il PIC risponde passando al
microprocessore il tipo di
interruzione che dev’essere servita;
6. il processore accede alla IVT e legge l’indirizzo della procedura d i servizio che dev’essere
attivata;
7. parte la procedura di servizio riguardante il dispositivo che ha fatto la richiesta;
8. terminata la procedura, il processore ritorna al punto in cui era stato interrotto.

LO STESSO SCHEMA DAL PUNTO DI VISTA DEL PROCESSORE (INTEL)


1. Un dispositivo esterno invia una richiesta di interruzione sul pin INTR;
2. durante l’ultimo periodo di clock di un’istruzione il processore rileva la presenza di un
valore 1 su INT;
3. il processore invia un impulso su INTA per segnalare che la richiesta è stata rilevata;
4. il processore invia un secondo impulso di INTA per chiedere di leggere sul data bus il
codice del dispositivo;
5. il processore legge dal data bus il codice n del device che ha richiesto l’interrupt (1 byte);
6. il processore salva nello stack il valore del registro dei flag e l’indirizzo di ritorno (CS e IP);
7. … azzera IF;
8. … accede all’interrupt vector table per prelevare l’indirizzo della procedura di servizio;
9. la procedura va in esecuzione;
10. al termine della procedura il processore ripristina dallo stack il registro dei flag e l’indirizzo
di ritorno, tornando ad eseguire il programma interrotto.

6. Interfacce di I/O

Il sottosistema di I/O consente la


comunicazione fra il calcolatore ed il
mondo esterno. Fanno parte del
sottosistema i dispositivi (unità di
I/O) per la comunicazione
uomo/macchina (video, stampanti,
terminali) e quelli utilizzati per la
memorizzazione permanente delle
informazioni (unità a disco, nastri magnetici), nonché la rete. Tutte le unità di I/O sono collegate al
bus di sistema mediante dispositivi detti interfacce di I/O.
Il generico schema di
un’interfaccia di I/O è quello
visibile a fianco: grazie ad esso,
l’interfaccia svolge una funzione
di adattamento tra la modalità di
trasferimento dei dati utilizzata
all’interno del sistema (cicli di
bus) e quella utilizzata dall’unità
di I/O.

Negli esercizi vi sono due principali modi per interfacciare un dispositivo di I/O:
• tramite un 244: un 244 è un driver 3-state (schematizzabile come una “batteria di buffer”) a 8
bit strutturato in 2 gruppi da 4 bit, abilitati da EN1* ed EN2*.
Esempio pratico - Interfacciamento di 8 interruttori (switch)
Quando dobbiamo lavorare con gli interruttori (dai quali ad un certo punto è necessario
rilevare il valore) è necessario usare un 244, prestando attenzione al fatto che il primo gruppo
di bit è regolato da EN1* e il
secondo da EN2* (nell’esempio
non vi è bisogno di fare
distinzione, ma in un compito
d’esame tutto può succedere).
Nell’esempio vediamo come
sono stati interfacciati gli 8
interruttori mappati a 80 H; il
software riportato nel riquadro
si occupa di leggere lo stato
degli interruttori tramite il
comando IN.
Altri casi in cui è stato usato un
244: trasduttore di velocità in
input (non c’è necessità alcuna
di memorizzare, bisogna
semplicemente leggere il dato)
e in generale in quasi tutti i
dispositivi che comunicano in
input.
• attraverso un 373: questo
componente è un latch a 8 bit
con uscite 3-state; campiona
sul fronte negativo del clock (il
valore viene mantenuto fino a
quando CK non torna a valore
logico alto) e quando CK = 1
l’uscita riproduce l’ingresso. I latch hanno il pregio di mantenere in memoria il dato.
Esempio pratico - Interfacciamento di un display a 7 segmenti
Un display a sette segmenti (v. figura pagina precedente) può essere visto ai morsetti come
costituito da 7 led. L’attivazione di un segmento corrisponde all’accensione del led relativo.
Risulta quindi sbagliato utilizzare un buffer 3-state (244) per interfacciare il display al
sistema. In questo caso, infatti, il valore non viene mantenuto (memorizzato).
Per fare ciò occorre necessariamente usare un registro o un latch.
Altri casi in cui è stato utilizzato un 373: in generale nei dispositivi che comunicano in
output.

Alcuni aspetti importanti sulle interfacce di I/O:


• generalmente un’interfaccia di I/O dispone di un insieme di registri interni a cui la CPU
deve accedere. Una volta selezionato il chip (chip select CS* basso) i segnali di indirizzo
(A[0.. n-1], v. figura in alto pagina precedente) consentono la selezione fra i suoi differenti
registri interni (analogamente a quanto già visto per i chip di memoria). Per accedere ai
registri interni è necessario scrivere una certa configurazione binaria (control word7) sul
registro di controllo. Generalmente si usa la direttiva EQU per definire degli identificatori
associati alle costanti che rappresentano gli indirizzi dei registri e la control word:
es. ControlWord EQU D4H
BufferInRegister EQU 80H
• le interfacce di I/O devono poter funzionare secondo un certo insieme di modalità
differenti (ad es. un’interfaccia per comunicazioni seriali asincrone RS232 deve poter
scambiare dati con un modem impiegando trame e frequenze differenti).
Conseguentemente le interfacce di I/O sono tipicamente programmabili: sono presenti cioè
al loro interno un certo numero di registri di controllo che vengono scritti dalla CPU all’atto
dell’inizializzazione del dispositivo per impostare la modalità di funzionamento
desiderata. Ad esempio, questa potrebbe essere una porzione di programma in grado di
occuparsi di inizializzare tutte le periferiche programmabili presenti nel sistema:
MOV AL, ControlWord
OUT ControlRegister, AL (ControlRegister è l’indirizzo del relativo spazio di I/O)
• strettamente associata alla
funzionalità di adattamento
fra calcolatore e l’unità di I/O
è la presenza all’interno
dell’interfaccia di registri di
appoggio (“buffer”) utilizzati
nei trasferimenti dei dati da
CPU ad unità di I/O e viceversa. Per inviare un dato all’unità di I/O la CPU effettua una
scrittura del dato su un buffer dell’interfaccia, da cui poi quest’ultima si occupa di trasferire
il dato all’unità di I/O. Per prelevare un dato dall’unità di I/O la CPU effettua una lettura da
un buffer dell’interfaccia, su cui quest’ultima ha precedentemente appoggiato il dato
proveniente dall’unità di I/O. Esempi:
o Trasferimento dati: INPUT
IN AL, BufferIn (AL  contenuto del registro mappato a “BufferIn”)
MOV DatiIn[SI], AL (casella indice SI vettore DatiIn  AL)
INC SI (incrementiamo l’indice del vettore)

7 Una parola di controllo è un byte che viene trasmesso alla porta di controllo attraverso il bus dati.
o Trasferimento dati: OUTPUT
MOV AL, DatiOut[SI] (AL  casella indice SI vettore DatiOut)
OUT BufferOut, AL (AL  contenuto del registro mappato a “BufferOut”)
INC SI (incrementiamo l’indice del vettore)
Si noti che in entrambi gli esempi le interfacce trasferiscono un dato alla volta!
• di norma le unità di I/O lavorano in modo asincrono rispetto al funzionamento della CPU e
sono molto più lente di quest’ultima. Si rende quindi necessario introdurre all’interno
dell’interfaccia di I/O un qualche meccanismo di sincronizzazione fra l’attività svolta dalla
CPU e quella svolta dall’unità di I/O. Per maggiori delucidazioni si rimanda ai paragrafi
introduttivi sulle politiche di interrupt e polling, nonché a quello successivo (con esempi
software);
• tipicamente le interfacce di I/O dispongono di registri di stato (es. bufferFull, ledOn,
deviceReady…) tramite cui vengono rese disponibili alla CPU tutte le informazioni
necessarie per la sincronizzazione con l’unità di I/O. Ogni qual volta il programma ha
necessità di leggere lo stato della periferica (ad es. per controllare se ci sono stati errori), il
programmatore userà un istruzione del tipo:
IN AL, StatusRegister (caricamento in AL del contenuto del registro mappato dove
indica StatusRegister)
• in aggiunta a ciò i registri di stato situati all’interno di un’interfaccia di I/O sono spesso
utilizzati per segnalare alla CPU il verificarsi di eventuali condizioni di malfunzionamento
o errore (es. errore di parità nelle comunicazioni seriali).

7. Interrupt vs. polling: qualche esempio software

Esempio 1 (polling)

In questo esempio viene mostrato il


flusso di un programma che esegue
una’operazione di input a polling: si
nota infatti che viene consultato il
registro Status per vedere se il
buffer è stato completamente
riempito (BIF = 1).
Solo al riempimento totale del
buffer si inizia a leggere il buffer e a
portare in memoria i dati (in DatiIn),
sempre un elemento alla volta.
Soltanto al termine dell’operazione
di I/O il ciclo terminerà di effettuare
il suo trasferimento.

Esempio 2 (polling)

Il problema che esaminiamo consiste nel:


• trasferire i byte provenienti da un’interfaccia d’ingresso (mappata a 80H) verso una d’uscita
(mappata a 100H);
• memorizzare i dati ricevuti in un vettore BUF_IN di 1 KB;
• chiamare la procedura CALCOLA quando BUF_IN è pieno.
Vi sono due registri importanti:
• BIF (Buffer In Full)  posizione 0;
• BOE (Buffer Out Empty)  posizione 1.
E disponiamo di quattro registri interni:
• 00: Buffer IN
• 01: Buffer OUT
• 10: Status
• 11: Control

PUNTO 1: mappiamo le variabili e le costanti


Abbiamo tantissime costanti, visto che ci fa comodo dare nomi simbolici a tutti gli indirizzi dei
vari registri.

IN_BufferIN EQU 80H ---- ----


---- ---- OUT_BufferOUT EQU 101H
IN_Status EQU 82H OUT_Status EQU 102H
IN_Control EQU 83H OUT_Control EQU 103H

IN_ControlWord EQU 33H OUT_ControlWord EQU 62H

MASK_BIT0 EQU 1H MASK_BIT1 EQU 2H

BUF_IN DB 1024

Risoluzione del problema a polling:

Main: MOV AL, IN_ControlWord ; programmazione interfacce:


OUT IN_Control, AL ; mandiamo al dispositivo le direttive per
MOV AL, OUT_ControlWord ; effettuare l’input/output
OUT OUT_Control, AL
MOV SI, 0 ; inizializzazione indice buffer in memoria
Poll_IN: IN AL, IN_Status ; polling sull’interfaccia d’ingresso
AND AL, MASK_BIT0 ; si fa l’AND con 1: in questo modo
; rispettiamo la maschera che abilita solo IR0
CMP AL, 0 ; se ancora non si ha niente
JE Poll_IN ; ricomincia il ciclo
; altrimenti…
IN AL, IN_BufferIN ; leggiamo un nuovo dato
MOV BL, AL ; copiamo AL in BL
MOV BUF_IN[SI], BL ; e scriviamo il dato in memoria
INC SI ; incrementiamo l’indice
CMP SI, 1024 ; letto il KB?
JNE Poll_OUT ; se non l’abbiamo ancora letto, passiamo a
; Poll_OUT (trasferimento dati in uscita)
MOV SI, 0 ; sennò abbiamo finito di trasferire i dati:
; si resetta l’indice SI per il prossimo giro
CALL CALCOLA ; chiamiamo la procedura CALCOLA

Poll_OUT: IN AL, OUT_Status ; polling sull’interfaccia d’uscita


AND AL, MASK_BIT1
CMP AL, 0
JE Poll_OUT
OUT OUT_BufferOUT, BL ; invio in uscita del dato ricevuto
JMP Poll_IN ; ho inviato: pronto a ricevere

Schematicamente:
MAIN:
Main: MOV AL, IN_ControlWord ; programmazione interfacce:
OUT IN_Control, AL ; mandiamo al dispositivo le direttive per
MOV AL, OUT_ControlWord ; effettuare l’input/output
OUT OUT_Control, AL
MOV SI, 0 ; inizializzazione indice buffer in memoria

• programmiamo le interfacce e spediamo le relative control word con due comandi di OUT.
In questo modo diamo le direttive necessarie affinché tutto funzioni come voluto. Quasi
sempre nel main capita di dover inizializzare delle variabili, oppure qualche indice che
servirà per scorrere il buffer.

POLL_IN:
Poll_IN: IN AL, IN_Status ; polling sull’interfaccia d’ingresso
AND AL, MASK_BIT0 ; si fa l’AND con 1: in questo modo
; rispettiamo la maschera che abilita solo IR0
CMP AL, 0 ; se ancora non si ha niente
JE Poll_IN ; ricomincia il ciclo
; altrimenti…
IN AL, IN_BufferIN ; leggiamo un nuovo dato
MOV BL, AL ; copiamo AL in BL
MOV BUF_IN[SI], BL ; e scriviamo il dato in memoria
INC SI ; incrementiamo l’indice
CMP SI, 1024 ; letto il KB?
JNE Poll_OUT ; se non l’abbiamo ancora letto, passiamo a
; Poll_OUT (trasferimento dati in uscita)
MOV SI, 0 ; sennò abbiamo finito di trasferire i dati:
; si resetta l’indice SI per il prossimo giro
CALL CALCOLA ; chiamiamo la procedura CALCOLA

• in questa parte compaiono le tipiche istruzioni per il trasferimento dati (input) e si


effettuano alcuni controlli: quello tipico del polling (si ha qualcosa da ricevere? se sì
procedi, sennò ricontrolla) e quello di fine lettura del KB (se abbiamo finito di leggere
allora abbiamo anche finito di trasferire quindi non saltiamo a Poll_OUT ma procediamo
con CALCOLA).

POLL_OUT:
Poll_OUT: IN AL, OUT_Status ; polling sull’interfaccia d’uscita
AND AL, MASK_BIT1
CMP AL, 0
JE Poll_OUT
OUT OUT_BufferOUT, BL ; invio in uscita del dato ricevuto
JMP Poll_IN ; ho inviato: pronto a ricevere

• queste righe di codice sono dedicate al trasferimento del dato in uscita: anche qui è
presente un’operazione di polling (questa volta sull’interfaccia d’uscita). Si fa uso di BL, in
precedenza creato come duplicato di AL, perché quest’ultimo registro viene modificato e
poi fatto passare attraverso l’operazione logica di AND.

Esempio 3 (interrupt)

Possiamo complicare a piacimento la situazione rispetto a quella trattata nell’esempio 2,


supponendo che il sistema debba contemporaneamente gestire un orologio.
Lo scopo è quello di mostrare quanto
tempo è trascorso dall’avvio del
sistema; è inoltre disponibile un’onda
quadra con frequenza 1 Hz.
Casi come questo sono tipicamente
trattati ad interrupt; si sa che lo scorrere
dei secondi avviene con una cadenza
tale che rende inutile far continuamente
chiedere al processore (magari ogni
microsecondo): “Scusa, ma è scattato il
secondo?”. Si perderebbero inutilmente tantissimi cicli di clock che possono essere molto meglio
impiegati. Ecco allora come si agisce:
• si collega all’ingresso INT della CPU il segnale a 1 Hz, in modo che si abbia
un’interruzione al secondo;
• si memorizzano ore minuti e secondi in delle variabili in memoria:
Ore DB 0
Minuti DB 0
Secondi DB 0
(NOTA: con questa sintassi si inizializzano delle variabili di 1 byte inizialmente poste a
zero)
• si interfaccia il nostro orologio (mappato a 200H), il quale dispone di tre registri contenenti
ore, minuti e secondi correnti:
CLOCK_H EQU 200H
CLOCK_M EQU 201H
CLOCK_S EQU 202H

Programmazione ad interrupt per l’orologio

CLOCK_INT : PUSH AX ; all’inizio di ogni routine di interrupt bisogna


; effettuare un “prologo”, durante il quale andiamo
; a collocare sullo stack le variabili di contesto che
; dovranno essere ripristinate al termine nella fase
; detta “epilogo” (il termine della procedura)
INC [Secondi] ; facciamo andare avanti i secondi (abbiamo avuto
; un interrupt dal segnale di onda con periodo 1 s
CMP [Secondi], 60 ; abbiamo fatto un minuto?
JNE End ; se no, vai a End, altrimenti…
MOV [Secondi], 0 ; bisogna resettare i secondi e
INC [Minuti] ; aumentare i minuti di uno!
CMP [Minuti], 60 ; abbiamo fatto un’ora?
JNE End ; se no, vai a End, altrimenti…
MOV [Minuti], 0 ; bisogna resettare i minuti e
INC [Ore] ; aumentare le ore di uno!
CMP [Ore], 24 ; abbiamo fatto un giorno?
JNE End ; se no, vai a End, altrimenti…
MOV [Ore], 0 ; resetta le ore!
End: MOV AL, [Secondi] ; dobbiamo aggiornare il display: … secondi….
OUT CLOCK_S, AL ;
MOV AL, [Minuti] ; dobbiamo aggiornare il display: … minuti….
OUT CLOCK_M, AL ;
MOV AL, [ore] ; dobbiamo aggiornare il display: … ore!
OUT CLOCK_H, AL
POP AX ; ripristino del contesto (faccio il pop di tutto ciò
; che avevamo push-ato nel prologo): questa parte
; viene detta “epilogo”
IRET ; con questo comando usciamo dalla routine di
; interrupt

Programmazione ad interrupt per l’interfaccia

Main: CLI ; Disabilitazione interrupt (all’inizializzazione


; non devono disturbare!)
MOV AL, IN_ControlWord ; Programmazione interfacce tramite parole di
; comando
OUT IN_Control, AL ; spediamo la prima parola
MOV AL, OUT_ControlWord
OUT OUT_Control, AL ; spediamo la seconda parola
MOV [1k_done], 0 ; Inizializzazione variabili: la variabile
; [1k_done] indica al main l’avvenuta
; ricezione di 1 KB
MOV [Index], 0 ; indice del vettore a 0
STI ; Abilitazione interrupt

NOTA: spesso il main viene racchiuso da CLI - STI perché contiene l’inizializzazione del programma, che non
dev’essere interrotta per nessun motivo.

Loop: CMP [1k_done], 1 ; Sincronizzazione: hai finito di trasferire?


JNE Utile ; Se no, torna al lavoro utile…
MOV [1k_done], 0 ; altrimenti resetta la variabile 1k_done
CALL CALCOLA ; e chiama la procedura “CALCOLA”
Utile: … ; Il main in attesa che le operazioni di I/O
… ; vengano completate può eseguire delle
… ; operazioni utili…
JMP Loop



RX_INT: PUSH AX ; PROLOGO


PUSH SI
MOV SI, [Index] ; Mettiamo l’indice corrente nel registro SI
IN AL, IN_BufferIN ; Lettura del nuovo dato
MOV BUF_IN[SI], AL ; Scrittura in BUF_IN (memoria) del dato ricevuto
INC [Index] ; incremento dell’indice
CMP [Index], 1024 ; Letti 1K dati?
JNE send_OUT ; se no, bisogna spedire
MOV [Index], 0 ; altrimenti resettiamo l’indice
MOV [1k_done], 1 ; e segnaliamo di aver terminato il KB di dati
send_OUT: OUT OUT_BufferOUT, AL ; Invio in uscita del dato ricevuto
POP SI ; EPILOGO
POP AX
RETI

Si noti che questa routine di interrupt (ma si tenga presente che non viene conteggiato il main) è
più snella della sua controparte polling.

8. Comunicazioni seriali

Esistono due modalità diverse di trasferimento dei


dati tra due sistemi, distinte dal numero di segnali
che connettono il trasmettitore col ricevitore:
• comunicazione parallela: maggiore velocità
(ad es. vengono spediti 8 bit alla volta), maggior
costo. Viene adottata quando la distanza tra
ricevitore e trasmettitore è relativamente ridotta;
• comunicazione in serie: minore velocità
(viene spedito un bit alla volta), minor costo, viene
usata laddove la distanza tra il trasmettitore e il
ricevitore è significativa (per es. superiore al metro).

Sincronismo di ricezione
In una trasmissione seriale
il dispositivo ricevente, per
poter decodificare ed
interpretare correttamente i
dati ricevuti, deve sapere:
• quando campionare la linea per identificare il valore di ciascun bit (deve cioè acquisire il
sincronismo di clock)…
• … quando inizia e finisce ciascun gruppo di bit o carattere (sincronismo di carattere),
• … nonché quando inizia e finisce ciascun blocco d’informazione (sincronismo di frame).
Inoltre:
• dev’essere in grado di conoscere il formato dei frames;
• deve lavorare alla stessa frequenza del trasmettitore (e avere uguale bit-rate);

Problema di conversione S/P e P/S


Il mondo dei sistemi a microprocessore è un mondo che lavora in parallelo (v. bus, memorie…),
ma noi dobbiamo interfacciarlo col mondo seriale della linea; se andiamo a vedere cosa succede
quando si realizza una trasmissione seriale osserveremo che il trasmettitore riceverà dal
processore (in parallelo) il dato da inviare, lo serializzerà e poi lo invierà. Lo shift register, ovvero
colui che si incarica di effettuare la conversione P/S, è quindi un componente fondamentale in
questo processo. Questi bit, una volta trasmessi, verranno presi dal ricevitore che dovrà lavorare
nel senso opposto (parallelizzarli per mandarli sul bus). Alla fine di questa operazione il dato sarà
parallelizzato e fornito al sistema ricevente.

Trasmissioni seriali sincrone e asincrone


Possiamo adottare due soluzioni:
• sincrona: trasmettitore e ricevitore condividono uno stesso segnale di clock (avente
frequenza pari al bit-rate) che aiuta loro ad individuare dove stiano temporalmente i bit. Più
precisamente, il trasmettitore e il ricevitore si scambiano i dati su un canale ed il segnale di
temporizzazione su una seconda linea. È la più semplice tra le due modalità (sincrona e
asincrona), ma bisogna prestare attenzione al fatto che il segnale fisico di clock può subire
consistenti ritardi e/o sfasamento se i dispositivi in collegamento sono lontani: per questo si
usa collegare in questo modo due dispositivi vicini (spesso sulla stessa scheda). Durante i
periodi di inattività il trasmettitore invia dei caratteri, detti di sincronismo; il loro scopo è
infatti quello di mantenere il sincronismo di carattere fra i due dispositivi che, grazie a
questa strategia, continuano a sapere dove finisce un carattere e inizia quello successivo,
rendendo superflua la ri-sincronizzazione. Proprio perché sono a tutti gli effetti occupati
dalla trasmissione di carattere, tali periodi di inattività hanno una durata multipla di quella
necessaria per la trasmissione di un singolo carattere;
• asincrona: trasmettitore e ricevitore non condividono alcun clock8 e quindi c’è bisogno di
sincronizzarli in qualche altro modo: in particolare, il trasmettitore genererà e invierà al
ricevitore un unico segnale contenente sia le informazioni di temporizzazione che i dati; il
ricevitore, di contro, utilizzerà questo segnale sia per rifasare il proprio clock tramite un
circuito PLL (Phase Lock Loop) sia per estrarre i dati. Inoltre, nel caso asincrono, la
sincronizzazione è in termini più di fase che di frequenza, visto che i dispositivi ricevono i
segnali di clock alla stessa frequenza nominale. Questo è possibile dal momento che essi
ricevono (in fase di programmazione) una costante k (detta fattore di scalamento),
fondamentale perché attraverso di essa i due dispositivi acquisiscono l’informazione sulla
frequenza cui avviene la trasmissione: in particolare, un bit viene trasmesso ogni k colpi di
clock. NOTA: nella trasmissione asincrona, se il trasmettitore non trasmette nulla, la linea è
vuota (contrariamente a ciò che avviene nella trasmissione sincrona).

Caso esemplificativo di
comunicazione seriale9
1. la linea è inattiva;
2. start bit (è uno soltanto e
va al valore logico basso):
serve per far capire quando
inizia la trama;
3. data bits: qui è contenuto il
dato vero e proprio;
4. parity bit: fornisce una
indicazione sul verificarsi o

8 ATTENZIONE - Non è che non ci sia il clock: il clock - anzi, i clock, uno per componente - ci sono eccome… il problema è

condividerli visto che non sono già in partenza sincronizzati fra i componenti.
9 Per maggiori dettagli si faccia riferimento alla figura.
meno di un errore di trasmissione;
5. stop bit: fanno capire al ricevitore che la trama è finita.

Dispositivo 8250 e protocollo RS-232


Il dispositivo 8250 appartiene alla
categoria degli UART (Universal
Asynchronous Receiver and Transmitter).
Trattasi inoltre di un dispositivo
programmabile: l’utente può infatti
specificarne le caratteristiche di
funzionamento attraverso opportune
parole di controllo. Supporta l’RS-232,
protocollo/standard elettrico per le
comunicazioni seriali usato spesso
nelle comunicazioni coi modem, e
supporta la trasmissione asincrona.

In figura a destra vediamo lo


schema dell’interfaccia e possiamo
distinguere alcuni fondamentali
gruppi di bit:
• segnali di indirizzo A[0..2];
• collegamento al bus dati
D[0..7];
• chip select (CS);
• comandi di read e write (per
operazioni di lettura e
scrittura) sui registri interni;
• il segnale INT per
richiamare l’attenzione del
processore.

Abbiamo però anche dei registri nonché dei segnali “nuovi”:


• registri di DATO
o receiver buffer register (RBR): qui va a finire il dato una volta ricevuto dalla linea
seriale. Il bit meno significativo del registro contiene il bit ricevuto per primo;
o transmitter holding register (THR): è il registro in cui il processore scrive il dato da
trasmettere sulla linea, una volta serializzato (è il registro simmetrico del RBR). Il bit
meno significativo del registro contiene il bit che dev’essere trasmesso per primo;
• registri di STATO
o line status register (LSR): è un registro di sola lettura che serve per conoscere lo stato
della linea;
 LSR0: va a 1 quando è stato ricevuto un nuovo carattere ed è disponibile il RBR;
 LSR1: va a 1 in corrispondenza di un errore di overrun, cioè quando in ricezione
l’8250 ha acquisito un carattere, l’ha scritto nel buffer e tuttavia il processore non
è stato sufficientemente veloce a leggere questo dato cosicché ne è arrivato un
altro che l’ha sovrascritto;
 LSR2: bit di parità (è a 1 quando c’è un errore di parità);
 LSR3: server per il frame error, quando l’8250 si aspetta di ricevere un 1 e invece
riceve 0 (non rileva il bit di stop);
 LSR4: va a 1 quando l’8250 non riceve nulla (linea inattiva);
 LSR5: quando è 1 il dato nel registro è pronto;
o interrupt identification
register (IIR): identifica
la richiesta di
interruzione;
• registri di CONTROLLO
o line control register
(LCR): è accessibile in
lettura e serve a capire
lo stato delle richieste
di interruzione nonché
per forzare alcuni
parametri significativi
che caratterizzano il
modo di funzionamento (ad es. formato della trama);
 L0 e L1: determinano numero di bit per carattere;
 L2: definisce il numero di bit di stop (1,5 carattere a 5 bit, 2 se sono più di 5);
 L3: riguarda bit di parità (ed è posto a 1 se si desidera il bit di parità);
o interrupt enable register (IER): grazie ad esso il processore più decidere (in maniera
selettiva) se l’8250 può scatenare
richieste di interruzione; le
interruzioni (ad es. buffer di
ricezione pieno, buffer di
trasmissione vuoto, errore di
ricezione, etc.) vengono gestite
con un certo criterio di priorità:
di default, le richieste di interruzione a priorità più bassa sono
bloccate se pende una richiesta a priorità più alta (la priorità
massima è quella a livello zero);
o division latch register (DLR): è registro a 16 bit strutturato in 2
registri da 8 bit, (DLL e DLM); attraverso questi registri
comunichiamo il parametro k di scalamento e quindi
determiniamo il bit-rate (sia per la trasmissione che per la ricezione). Più
precisamente, il dispositivo ricava il proprio bit rate come:
bit-rate = frequenza di pilotaggio / (16* costante di tempo)
La costante di tempo dice di quante volte dev’essere scalata la frequenza di
pilotaggio: si veda la tabella per determinarla in base al caso (la tabella è stata
calcolata sulla frequenza F = 1,8432 MHz).

Infine, l’8250 ha un insieme di segnali di controllo utilizzabili per interfacciarsi con un modem
secondo le modalità dello standard RS-232.
9. Comunicazioni seriali: un po’ di codici

Anzitutto, ecco due esempi di mapping nello spazio di I/O per le interfacce 8250:
REGISTRO COM1 COM2
RBR,THR,DLL 03F8H 02F8H
IER,DLM 03F9H 02F9H
IIR 03FAH 02FAH
LCR 03FBH 02FBH
MCR 03FCH 02FCH
LSR 03FDH 02FDH
MSR 03FEH 02FEH

Ecco come avviene l’inizializzazione dell’interfaccia COM1:

; La procedura inizializza COM1 : bit-rate 9600, 8 bit per carattere, parità pari, 1 stop bit
ini_com1 PROC FAR
PUSH AX ; prologo
PUSH DX
; DLAB = 1 per accedere a DLM e DLL
MOV DX, LCR ; mettiamo 80H = 1000 0000
MOV AL, 80H ; in LCR in modo da forzare DLAB = 1 (DLAB è il bit LCR8)
OUT DX, AL
; bit rate = 9600 -> DLL = 000CH (diviso in 00 DLM e 0C DLL)
MOV DX, DLM
MOV AL, 00H
OUT DX, AL
MOV DX, DLL
MOV AL, 0CH
OUT DX, AL
; 8 bit per car., 1 stop bit, parità pari, DLAB = 0
;  LCR = 1BH = 0001 1100
MOV DX, LCR
MOV AL, 1BH
OUT DX, AL
; “clear” di IER (= 00H) per disabilitare le interruzioni
MOV DX, IER
MOV AL, 00H
OUT DX, AL
; lettura iniziale per svuotare il buffer di ricezione
MOV DX, RBR
IN AL, DX

POP DX ; epilogo
POP AX
RET
ini_com1 ENDP
Lettura e scrittura su COM1 in modalità “polling”

read_com1 PROC FAR write_com1 PROC FAR


; la procedura legge un carattere da COM1 ; la procedura scrive su COM1 il carattere passato
; e lo restituisce in AL ; in AL

PUSH DX PUSH DX
PUSH AX
; attesa che il buffer di ricezione sia pieno: il bit 0
; del registro LSR fornisce lo stato del buffer di ; attesa che il buffer di trasmissione sia vuoto:
; ricezione ( 1 = pieno, 0 = vuoto ) ; il bit 5 del registro LSR fornisce lo stato del buffer
; di trasmissione (1 = vuoto, 0 = pieno)
MOV DX, LSR MOV DX, LSR
wait_pieno: IN AL, DX
CMP AL, 01H ( 0000 0001) wait_vuoto: IN AL, DX
JZ wait_pieno CMP AL, 20H ( 0010 0000)
JZ wait_vuoto
; lettura del carattere ricevuto
MOV DX, RBR ; scrittura del carattere da trasmettere
IN AL, DX MOV DX, THR
POP DX POP AX
RET OUT DX, AL
read_com1 ENDP POP DX
RET
write_com1 ENDP