Sei sulla pagina 1di 336

Sommario

Capitolo 1 Introduzione ................................................................................................................ 4


Capitolo 2 Il BIOS - Basic Input Output System ......................................................... 6
2.1 Il POST ......................................................................................................................................... 6
2.2 Installazione delle ISR del BIOS ........................................................................... 11
2.3 La BDA - BIOS Data Area ................................................................................................ 15
2.4 La memoria CMOS.................................................................................................................... 16
2.5 La sequenza di boot .......................................................................................................... 18
Bibliografia .................................................................................................................................... 31
Capitolo 3 Il PIC 8259 - Programmable Interrupt Controller .......................... 33
3.1 Classificazione delle interruzioni ...................................................................... 33
3.2 Gestione delle interruzioni da parte della CPU ......................................... 35
3.3 Funzionamento del PIC 8259 ......................................................................................... 37
3.4 Programmazione del PIC 8259 ....................................................................................... 41
3.5 Un esempio pratico: interfaccia con la tastiera ....................................... 51
Bibliografia .................................................................................................................................... 61
Capitolo 4 La memoria CMOS e il Real Time Clock..................................................... 62
4.1 Funzionamento del dispositivo RTC + RAM .......................................................... 62
4.2 Struttura dell'area User Buffer della RAM-CMOS ......................................... 65
4.3 Struttura dell'area Storage Registers della RAM-CMOS ........................... 71
4.4 Programmazione del dispositivo RTC + RAM ........................................................ 75
4.5 Un esempio pratico: orologio/calendario + allarme .................................. 75
Bibliografia .................................................................................................................................... 84
Capitolo 5 Il PIT 8254 - Programmable Interval Timer ......................................... 85
5.1 Funzionamento del dispositivo PIT 8254............................................................. 87
5.2 Modalità operative dei timer..................................................................................... 93
5.3 Impiego dei PIT 8254 sui PC ..................................................................................... 103
5.4 Programmazione dei PIT 8254 ..................................................................................... 106
5.5 Esempi pratici .................................................................................................................... 108
Bibliografia .................................................................................................................................. 122
Capitolo 6 La memoria base del PC .................................................................................... 123
6.1 Organizzazione della memoria base sotto DOS ............................................... 124
6.2 Suddivisione della memoria base sotto DOS ................................................... 128
6.3 Granularità della memoria base sotto DOS ...................................................... 129
6.4 La catena dei Memory Control Blocks .................................................................. 130
6.5 Servizi DOS per la gestione dei blocchi di memoria .............................. 132

1
6.6 Restituzione della memoria allocata in eccesso per un programma 135
6.7 Strategie di allocazione della memoria........................................................... 138
6.8 Esempi pratici .................................................................................................................... 142
Bibliografia .................................................................................................................................. 147
Capitolo 7 Lo standard XMS - eXtended Memory Specifications ...................... 148
7.1 La HMA - High Memory Area .......................................................................................... 150
7.2 La memoria estesa e lo standard XMS .................................................................. 152
7.3 Interfaccia di programmazione XMS....................................................................... 154
7.4 Servizi XMS ........................................................................................................................... 156
7.5 Libreria XMSLIB.................................................................................................................. 168
7.6 Esempi pratici .................................................................................................................... 169
Bibliografia .................................................................................................................................. 186
Capitolo 8 Lo standard EMS - Expanded Memory Specifications ...................... 187
8.1 Lo standard LIM-EMS 4.0 .............................................................................................. 189
8.2 Interfaccia di programmazione EMS....................................................................... 193
8.3 Servizi EMS ........................................................................................................................... 195
8.4 Libreria EMSLIB.................................................................................................................. 208
8.5 Esempi pratici .................................................................................................................... 209
Bibliografia .................................................................................................................................. 216
Capitolo 9 La memoria video in modalità testo standard .................................. 217
9.1 Breve storia degli adattatori video standard ............................................ 218
9.2 Caratteristiche generali delle modalità video testo standard ...... 221
9.3 Principali servizi forniti dalla INT 10h per la modalità testo . 225
9.4 Esempi pratici .................................................................................................................... 232
Bibliografia .................................................................................................................................. 239
Capitolo 10 La memoria video in modalità grafica standard ........................... 240
10.1 Principio di funzionamento di un monitor per PC ................................... 240
10.2 Supporto grafico fornito dagli adattatori CGA ....................................... 244
10.3 Supporto grafico fornito dagli adattatori EGA e VGA ......................... 248
10.5 Supporto grafico fornito dagli adattatori MCGA ..................................... 252
10.6 La tavolozza dei colori ............................................................................................ 254
10.7 Esempi pratici.................................................................................................................. 255
Bibliografia .................................................................................................................................. 257
Capitolo 11 La memoria video in modalità grafica VESA .................................... 258
11.1 Le specifiche VESA VGA BIOS Extension........................................................... 259
11.2 Accesso alla VRAM in modalità reale attraverso i servizi VBE.... 262
11.3 Servizi VBE 3.0 ............................................................................................................... 263
11.4 Supporto VESA per le vecchie schede video SVGA ..................................... 279
2
11.5 Il bank switching .......................................................................................................... 280
11.6 Codifica dei colori nelle modalità video VESA/SVGA............................ 282
11.7 Esempi pratici.................................................................................................................. 287
Bibliografia .................................................................................................................................. 318
Capitolo 12 Servizi DOS per l'I/O su file ................................................................. 320
12.1 Il filesystem .................................................................................................................... 320
12.2 Le handle.............................................................................................................................. 321
12.3 Servizi DOS per la gestione del filesystem ............................................... 321
12.4 Libreria FILELIB ............................................................................................................. 330
12.5 Esempi pratici.................................................................................................................. 330
Bibliografia .................................................................................................................................. 336

3
Capitolo 1 Introduzione
Servendoci dei concetti fondamentali acquisiti nella sezione Assembly
Base, possiamo ora dedicarci allo studio di una numerosa serie di
argomenti importanti, che richiedono tecniche di programmazione
avanzate; un tale obiettivo può essere raggiunto in modo
relativamente semplice grazie al fatto che la conoscenza del
linguaggio Assembly, permette al programmatore di affrontare con
competenza qualsiasi aspetto legato al mondo dei PC.
In effetti, come vedremo chiaramente nei capitoli successivi, buona
parte della documentazione tecnica sui PC richiede al programmatore
una adeguata padronanza proprio dell'Assembly; in ogni caso, è
consigliabile anche una infarinatura generale sul linguaggio C,
notoriamente utilizzato come "linguaggio di sistema".

Gli argomenti trattati in questo tutorial hanno anche l'importante


finalità di facilitare la migrazione verso la cosiddetta modalità
operativa protetta, largamente utilizzata dalle CPU della famiglia
80x86; a tale proposito, bisogna ribadire che la conoscenza della
modalità operativa reale si rivela determinante per capire la
particolare struttura interna delle moderne CPU 80x86, le quali
mantengono tutte la "compatibilità verso il basso".

L'assembler di riferimento nella sezione Assembly Avanzato è il NASM;


il perché di questa scelta è spiegabile con dei ragionamenti che si
basano sulla realtà dei fatti.

Il Borland TASM è un vero e proprio mito tra i programmatori


Assembly, ma l'ultima versione risale ormai al 1996; proprio per
questo motivo, il TASM fornisce un supporto praticamente nullo del
set di istruzioni delle CPU di classe 80586 o superiore (sono
supportate solo pochissime istruzioni appartenenti al set delle prime
CPU di classe Pentium I).
Come se non bastasse, la Borland ha deciso di inserire il TASM nella
categoria dei cosiddetti classic products; si tratta di una categoria
che comprende i prodotti Borland per i quali non sono previste nuove
versioni!

Il Microsoft MASM è relativamente più recente, ma presenta ancora


parecchi problemi di affidabilità; inoltre, corre voce che la stessa
Microsoft stia seriamente pensando di abbandonare lo sviluppo delle
nuove versioni del proprio assembler!

Il sospetto evidente è che la filosofia closed source stia


perseguendo l'obiettivo di nascondere la maggior quantità possibile
di conoscenze ai programmatori; un modo efficace per ottenere un tale
risultato è proprio quello di eliminare gli assembler, considerati
strumenti troppo potenti e quindi pericolosi!

Fortunatamente, la filosofia open source sta seguendo una strada del


tutto opposta; un esempio pratico è rappresentato proprio dal NASM le
cui ultime versioni hanno raggiunto ormai una maturità ed una
affidabilità persino superiori rispetto ai prodotti commerciali!

4
Il NASM è un progetto in continua evoluzione, che solo recentemente è
entrato nel mondo del software libero per evitare una morte
prematura, legata al fatto che i suoi creatori avevano ormai deciso
di abbandonarne lo sviluppo; grazie a questa scelta, numerosi nuovi
sviluppatori hanno salvato un enorme lavoro frutto di anni ed anni di
fatica.
I risultati sono piuttosto evidenti, soprattutto se si considera che
le attuali versioni del NASM supportano ormai l'intero set di
istruzioni delle CPU 80x86 a 16, 32 e 64 bit!
Bisogna anche aggiungere che l'assembler NASM è in grado di generare
codice oggetto destinato a tutti i principali SO; come si può leggere
anche sul manuale d'uso, è possibile chiedere al NASM la generazione
di object file per il DOS (16 e 32 bit), per Windows, Linux e BSD!

Tutti i programmi presentati nella sezione Assembly Avanzato girano


sotto DOS o sotto qualsiasi emulatore DOS per Windows; come al
solito, gli utenti Linux possono servirsi di appositi emulatori come
DOSEmu (raccomandato), DOSBox, Wine, etc.

5
Capitolo 2 Il BIOS - Basic Input Output
System
Non appena si fornisce l'alimentazione elettrica ad un computer
appartenente alla famiglia hardware dei PC 80x86, viene attivato un
procedimento standard il cui scopo è quello di svolgere le seguenti
importantissime fasi:

1) autodiagnosi e inizializzazione dell'hardware;


2) installazione delle ISR per l'interfaccia a basso livello con
l'hardware;
3) lancio del sistema operativo.

Tutto questo lavoro viene svolto da un software (ovviamente scritto


in Assembly) che risiede su una apposita memoria ROM denominata ROM
BIOS e disposta fisicamente in un chip del computer; la sigla BIOS è
l'acronimo di Basic Input Output System (sistema primario per le
operazioni di I/O con l'hardware).
Gli aspetti relativi al BIOS assumono quindi una notevole importanza
generale, non solo per i cosiddetti "programmatori di sistema" che si
dedicano alla scrittura dei SO; analizziamo quindi in dettaglio le
tre fasi elencate in precedenza.

2.1 Il POST

La delicatissima fase di autodiagnosi e inizializzazione


dell'hardware assume, come è facile intuire, una importanza vitale
per il computer; il software del BIOS che svolge tale fase, prende il
nome di POST che è l'acronimo di Power On Self Test (autodiagnosi
all'accensione del PC).

Prima di analizzare le fasi che caratterizzano il POST, dobbiamo


occuparci di un aspetto molto interessante che ci permette di capire
la tecnica attraverso la quale il BIOS prende il controllo
all'accensione del computer; a tale proposito, è necessario
premettere che esistono alcune differenze tra vecchi e nuovi modelli
di CPU (e tra vecchie e nuove architetture dei PC).

Il primo aspetto da considerare riguarda il fatto che una qualsiasi


CPU della famiglia 80x86, viene sempre inizializzata in modalità
reale; come sappiamo, in tale modalità le CPU utilizzano un address
bus a 20 linee attraverso il quale possono indirizzare un massimo di
220 byte, pari ad 1 Mb di RAM (cioè, tutti gli indirizzi fisici
compresi tra 00000h e FFFFFh).
Affinché i programmi che girano in modalità reale possano accedere ai
servizi del BIOS, è necessario quindi che il BIOS stesso venga
mappato in un'area della RAM compresa tra 00000h e FFFFFh; la
posizione di tale area è stata stabilita attraverso una convenzione
con i produttori dei BIOS.
Nei vecchi PC di categoria XT, equipaggiati con CPU di classe 8088,
8086 e 80186, al BIOS viene riservata un'area da 8 Kb posizionata
proprio alla fine del primo Mb di RAM tra gli indirizzi fisici FE000h

6
e FFFFFh; a tali indirizzi fisici possiamo associare, ad esempio, gli
indirizzi logici compresi tra FE00h:0000h e FE00h:1FFFh.
A partire dai PC di categoria AT, equipaggiati con CPU di classe
80286 o superiore, al BIOS viene riservata un'area da 64 Kb
posizionata proprio alla fine del primo Mb di RAM tra gli indirizzi
fisici F0000h e FFFFFh (la cosiddetta regione F); a tali indirizzi
fisici possiamo associare, ad esempio, gli indirizzi logici compresi
tra F000h:0000h e F000h:FFFFh.
In sostanza, la parte della RAM riservata al BIOS si comporta, in
realtà, come una vera e propria ROM; per i programmi che intendono
usufruire dei servizi del BIOS, tale area risulta quindi accessibile
solamente in lettura!
La Figura 1, già presentata nella sezione Assembly Base, mostra
graficamente la posizione della ROM BIOS nella RAM (PC di classe AT).

Figura 1 - Mappa della memoria RAM


(PC IBM compatibili)

Un'altra convenzione relativa alla famiglia hardware dei PC 80x86 ha


stabilito che la prima istruzione in assoluto che la CPU esegue
all'accensione (o al riavvio) del computer, deve trovarsi
rigorosamente all'inizio dell'ultimo paragrafo di memoria
indirizzabile dalla CPU stessa; tutto dipende, quindi, dall'ampiezza
dell'address bus.

7
Una CPU (come la 8086) con address bus a 20 linee, può indirizzare un
massimo di 1 Mb di RAM compresa tra gli indirizzi fisici 00000h e
FFFFFh; in tal caso, l'ultimo paragrafo di memoria è quello compreso
tra gli indirizzi fisici FFFF0h e FFFFFh.
La prima istruzione eseguita dalla CPU all'accensione (o al riavvio)
del computer si viene a trovare quindi all'indirizzo fisico FFFF0h; a
tale indirizzo fisico possiamo associare, ad esempio, l'indirizzo
logico FFFFh:0000h.
Per soddisfare la convenzione appena enunciata, i registri di
"indirizzamento" delle CPU con address bus a 20 linee si
"autoinizializzano" in questo modo:

CS = FFFFh, IP = 0000h, SS = 0000h, SP = 0000h, DS = 0000h, ES = 0000h

Di conseguenza, all'accensione (o al riavvio) del computer, la CPU


tenta di eseguire l'istruzione che si trova all'indirizzo logico
CS:IP=FFFFh:0000h; se vogliamo sapere quale istruzione è presente a
tale indirizzo possiamo servirci, ad esempio, del programma DEBUG
disponibile in ambiente DOS. La Figura 2 illustra l'output prodotto
dal comando u (unassemble) di DEBUG su un vecchio PC dotato di CPU
8088.

Figura 2 - Dump ASM di FFFFh:0000h


-u FFFF:0000

FFFF:0000 EA5BE000F0 JMP F000:E05B


FFFF:0005 3035 XOR [DI], DH
FFFF:0007 2F DAS
FFFF:0008 3239 XOR BH, [BX+DI]
FFFF:000A 2F DAS
FFFF:000B 3836FFFE CMP [FEFF], DH
FFFF:000F EF OUT DX, AX
FFFF:0010 1A5735 SBB DL, [BX+35]
FFFF:0013 025C07 ADD BL, [SI+07]
FFFF:0016 7000 JO 0018
FFFF:0018 C0 DB C0
FFFF:0019 F9 STC
FFFF:001A 00F0 ADD AL, DH
FFFF:001C 5C POP SP
FFFF:001D 07 POP ES
FFFF:001E 7000 JO 0020

Come possiamo notare, all'indirizzo logico FFFFh:0000h è presente


l'istruzione:

JMP F000:E05B

Si tratta quindi di un FAR jump all'indirizzo logico F000h:E05Bh;


tale indirizzo logico può essere scritto anche come FE00h:005Bh e
quindi appartiene proprio all'area di memoria da 8 Kb riservata alla
ROM BIOS dei PC di classe XT!
Come si può intuire, all'indirizzo logico FE00h:005Bh inizia il
codice del BIOS relativo al POST; da questo momento parte quindi la
fase di autodiagnosi e inizializzazione dell'hardware.

8
Una CPU (come la 80386, 80486, 80586) con address bus a 32 linee, può
indirizzare un massimo di 232=4 Gb di RAM compresa tra gli indirizzi
fisici 00000000h e FFFFFFFFh; in tal caso, l'ultimo paragrafo di
memoria è quello compreso tra gli indirizzi fisici FFFFFFF0h e
FFFFFFFFh.
Questa situazione fa nascere subito un problema legato al fatto che
difficilmente un normale PC con address bus a 32 linee dispone di ben
4 Gb di RAM; come è possibile allora che la CPU possa eseguire una
istruzione che si trova all'indirizzo fisico FFFFFFF0h?
Il problema è stato risolto facendo in modo che tale indirizzo venga
mappato, non in RAM, bensì su una memoria EPROM; all'interno della
EPROM, il produttore del PC può inserire tutte le necessarie
istruzioni di inizializzazione.

Per poter accedere all'indirizzo fisico FFFFFFF0h, la CPU si


"autoinizializza" in una particolare modalità chiamata big real mode;
in pratica, la CPU lavora in modalità reale, ma può gestire
componenti Offset a 32 bit in modo da poter indirizzare sino a 4 Gb
di memoria fisica!
Come vedremo nella apposita sezione di questo sito, in big real mode
o in protected mode, un registro di segmento non contiene una
componente Seg, bensì un cosiddetto selettore di segmento la cui
struttura è illustrata in Figura 3.

Figura 3 - Selettore di segmento

Per il momento ci interessa sapere che i 13 bit del campo INDICE


contengono l'indice di uno tra i possibili 213=8192 elementi di una
apposita tabella presente in memoria; ciascun elemento prende il nome
di descrittore di segmento e, come si intuisce dal nome, contiene la
descrizione completa del segmento di programma a cui il selettore di
segmento fa riferimento.
Una delle informazioni contenute nel descrittore di segmento è il
cosiddetto base address che specifica l'indirizzo fisico a 32 bit da
cui inizia il relativo segmento di programma; i vari indirizzi fisici
a cui la CPU deve accedere si ottengono sommando il base address al
contenuto a 32 bit del registro puntatore utilizzato. Nel caso, ad
esempio, dell'indirizzo specificato dalla coppia DS:ESI, la CPU
accede al descrittore di segmento associato a DS e somma il relativo
base address con il contenuto di ESI.

Premesso che per indicare il base address associato ad un determinato


SegReg la Intel utilizza una sintassi del tipo CS.BASE, DS.BASE, etc,
in fase di accensione (o riavvio) del computer i registri di
"indirizzamento" di una CPU con address bus a 32 linee si
"autoinizializzano" in questo modo:

CS = F000h, CS.BASE = FFFF0000h, EIP = 0000FFF0h


SS = 0000h, SS.BASE = 00000000h, ESP = 00000000h
DS = 0000h, DS.BASE = 00000000h
9
ES = 0000h, ES.BASE = 00000000h
FS = 0000h, FS.BASE = 00000000h
GS = 0000h, GS.BASE = 00000000h

In sostanza, CS contiene il selettore F000h che punta ad un segmento


di programma con base address pari a FFFF0000h, mentre EIP vale
0000FFF0h; di conseguenza, all'accensione (o al riavvio) del
computer, la CPU tenta di eseguire l'istruzione che si trova
all'indirizzo logico:

CS.BASE + EIP = FFFF0000h + 0000FFF0h = FFFFFFF0h

Come è stato spiegato in precedenza, tale indirizzo è mappato in una


EPROM la quale contiene le istruzioni di inizializzazione della CPU;
in particolare, tali istruzioni stabiliscono anche se la CPU debba
poi passare in modalità reale o protetta.

2.1.1 La sequenza del POST

La prima istruzione eseguita dalla CPU cede quindi il controllo al


POST; a questo punto inizia la fase di autodiagnosi e
inizializzazione dell'hardware. Questa fase comporta un numero
piuttosto lungo di controlli e verifiche e non può essere certo
dettagliatamente descritta in un tutorial; del resto, il dovere
primario di un vero programmatore Assembly è quello di studiare tutta
la documentazione tecnica relativa agli argomenti che ha intenzione
di approfondire.
In particolare, se vogliamo avere una panoramica dettagliata su tutto
ciò che accade durante il POST, possiamo fare riferimento agli
appositi manuali tecnici forniti dai produttori dei BIOS. Nella
sezione Links Utili di questo sito è presente un link alla Home Page
della Phoenix, noto produttore dei BIOS che equipaggiano quasi tutti
i PC; dal sito della Phoenix si consiglia di effettuare il download
dei documenti biospostcode.pdf (PhoenixBIOS - POST Tasks and Beep
Codes) e biosawardpostcode.pdf (AwardBIOS - Post Codes & Error
Messages).

Eventuali problemi hardware rilevati dal POST, vengono adeguatamente


segnalati all'utente attraverso diversi metodi; in generale,
qualunque problema viene segnalato sul monitor attraverso un apposito
messaggio di errore. Se il problema riguarda la parte video, allora
il POST si serve di appositi segnali acustici inviati
all'altoparlante di sistema; i due documenti descritti in precedenza,
illustrano tutte le informazioni relative a questi aspetti.

Ad ogni controllo svolto dal POST, corrisponde un ben preciso codice


di errore da 1 byte; prima di dare inizio ad un nuovo controllo, il
POST invia il relativo codice di errore ad un dispositivo di I/O
raggiungibile attraverso la porta hardware 80h (chiamata
Manufacturing Diagnostic Port).
In caso di errore con conseguente blocco del computer, la porta 80h
contiene quindi il codice del test che ha rilevato il problema; in
genere, queste informazioni sono riservate ai centri di assistenza
tecnica, i quali dispongono di apparecchiature capaci di visualizzare
10
(attraverso un display) l'ultimo codice prodotto dal POST.
Se tutto procede correttamente, l'ultimo codice di errore inviato dal
POST è quello relativo alla fase di boot (lancio del SO); a seconda
del tipo di BIOS installato sul proprio computer, tale codice può
assumere valori del tipo 00h, F7h, FFh.
Se vogliamo conoscere il valore esatto, possiamo servirci, ad
esempio, del comando:

i 80

(input from port) fornito dal programma DEBUG; si tenga presente


comunque che, se si lavora con un emulatore DOS, tale valore potrebbe
essere privo di senso.

2.2 Installazione delle ISR del BIOS

Terminata la diagnostica e l'inizializzazione dell'hardware, parte la


seconda fase del BIOS che consiste nella installazione in memoria di
una numerosa serie di procedure Assembly; lo scopo di tali procedure
è quello di permettere ai programmi di interfacciarsi a basso livello
con l'hardware del computer.
Si tratta di procedure di utilità generale, che diventano
indispensabili soprattutto quando si opera in condizioni estreme; un
caso del genere si presenta, ad esempio, quando si deve scrivere un
cosiddetto bootloader (il programma che carica in memoria il SO).

Per capire il procedimento utilizzato dal BIOS per l'installazione di


queste procedure, è necessario premettere che, durante il POST, viene
anche effettuato il test e l'inizializzazione di un particolare
dispositivo chiamato PIC o Programmable Interrupt Controller
(controllore programmabile delle interruzioni); come è stato spiegato
nella sezione Assembly Base e come vedremo in dettaglio nel prossimo
capitolo, il compito del PIC è quello di raccogliere e smistare tutte
le richieste che arrivano dalle periferiche che vogliono dialogare
con la CPU.
Ciascuna richiesta che arriva da una periferica, viene chiamata
Interrupt Request (richiesta di interruzione) o IRQ; al momento
opportuno, la CPU interrompe il programma in esecuzione (da cui il
nome "interruzione") e soddisfa la richiesta attraverso la chiamata
di una apposita procedura che, proprio per questo motivo, viene
definita Interrupt Service Routine (procedura di servizio per le
interruzioni) o ISR.
La situazione appena descritta si riferisce alle cosiddette
interruzioni hardware; si tratta cioè di interruzioni dovute a IRQ
provenienti dalle periferiche. Esistono però anche le cosiddette
interruzioni software, così chiamate in quanto provocate dai
programmi attraverso l'istruzione INT (analizzata nella sezione
Assembly Base); anche in questo caso, la CPU soddisfa la richiesta
attraverso la chiamata di apposite ISR.
Molte delle ISR, vengono installate proprio dal BIOS prima della fase
di boot; altre ISR possono essere in seguito installate dal SO o dai
programmi.

L'installazione delle ISR, avviene attraverso un procedimento che


11
risponde a precise convenzioni; in particolare, l'indirizzo logico
Seg:Offset di una qualsiasi ISR deve essere posizionato in un'area
della RAM compresa tra gli indirizzi fisici 00000h e 003FFh (a cui
possiamo associare, ad esempio, gli indirizzi logici compresi tra
0000h:0000h e 0000h:03FFh).
Come si nota in Figura 1, tale area viene denominata Interrupt
Vectors Area (area riservata ai vettori di interruzione); tale nome
deriva dal fatto che ciascun indirizzo logico Seg:Offset di una ISR
viene indicato con il termine Interrupt Vector (vettore di
interruzione).

Ciascun vettore di interruzione punta quindi ad una ISR che si trova


nella RAM; molti dei vettori di interruzione installati dal BIOS
puntano a delle ISR che si trovano nell'area della RAM riservata alla
ROM BIOS del computer!

Tornando alla Figura 1 notiamo che l'area riservata ai vettori di


interruzione occupa complessivamente:

003FFh - 00000h + 1 = 400h byte = 1024 byte

Ogni vettore di interruzione (cioè, ogni indirizzo logico Seg:Offset)


occupa 4 byte, per cui in questi 1024 byte trovano posto:

1024 / 4 = 256 = FFh vettori di interruzione

Per convenzione, i vari vettori di interruzione sono numerati,


nell'ordine, 00h, 01h, 02h e così via, sino al n. FFh; come già
sappiamo, molti dei vettore di interruzione possono essere chiamati
dai programmi (interruzioni software) attraverso l'istruzione INT
(che esegue una FAR call alla relativa ISR).

Come si può facilmente immaginare, alcuni dei vettori di interruzione


sono riservati rigorosamente al BIOS; altri vettori di interruzione
sono riservati al DOS, mentre altri ancora sono disponibili per i
programmi.
Se si vuole conoscere l'elenco completo dei 256 vettori di
interruzione, si può fare riferimento alla apposita tabella
disponibile in questo sito; per quanto riguarda i vettori di
interruzione riservati al BIOS, si consiglia di scaricare, dal sito
della Phoenix, il documento userman.pdf (PhoenixBIOS User's Manual).
Per una descrizione estremamente dettagliata di tutti i vettori di
interruzione e dei servizi ad essi associati, si consiglia di
consultare la celebre Ralf Brown's Interrupt List; a tale proposito,
si veda la sezione Links Utili di questo sito.

2.2.1 Esempio pratico per le ISR del BIOS

Consultando il manuale utente del proprio BIOS, si possono ricavare


informazioni sulle varie ISR disponibili; attraverso tali ISR si
possono scrivere una enorme quantità di procedure estremamente
compatte ed efficienti.
Vediamo un esempio pratico che si riferisce all'output di una stringa
sul video (e che ci tornerà molto utile nel seguito); a tale
12
proposito, ci serviamo di una ISR fornita dalla INT 10h, denominata
Video BIOS Services (servizi BIOS per il video).

Il servizio n. 13h della INT 10h prende il nome di Write string e


permette di visualizzare una stringa sullo schermo; tale servizio è
descritto dalla Figura 4.

Figura 4 - Servizio n. 13h della INT 10h


INT 10h - Servizio n. 13h - Write string:
visualizza una stringa sullo schermo.

Argomenti richiesti:
AH = 13h (servizio Write string)
ES:BP = indirizzo logico Seg:Offset della stringa
CX = lunghezza della stringa
DH = riga di output
DL = colonna di output
BH = pagina video
BL = attributi video
AL = modalità di scrittura
0 = solo caratteri
1 = solo caratteri + aggiornamento posizione cursore
2 = caratteri + attributi
3 = caratteri + attributi + aggiornamento posizione cursore

La Figura 4 ci permette di osservare che ciascuna ISR può fornire


numerosi servizi; tali servizi possono essere selezionati attraverso
un apposito valore passato, in genere, nel registro AH.
Le ISR spesso richiedono uno o più argomenti (entry arguments) che
devono essere passati attraverso i registri generali; gli stessi
registri generali vengono utilizzati dalle ISR per contenere
eventuali valori di ritorno (exit arguments).

Prima di vedere un esempio pratico, analizziamo il concetto di


attributo video; con tale termine si indica il colore di sfondo
(background) e di primo piano (foreground) del testo da stampare.
Il BIOS inizializza la scheda video in modalità testo (o
alfanumerica); in tale modalità, lo schermo viene suddiviso in una
matrice di celle disposte su 25 righe (numerate da 0 a 24) e 80
colonne (numerate da 0 a 79).
Ad ogni cella vengono riservati 2 byte di memoria; il primo byte
contiene il codice ASCII del carattere da stampare, mentre il secondo
byte contiene, appunto, gli attributi video del carattere stesso.
La Figura 5 illustra la struttura del byte degli attributi video.

Figura 5 - Attributi video

Sia per lo sfondo, sia per il primo piano, possiamo ottenere


differenti colori attivando (1) o disattivando (0) le tre componenti
13
fondamentali Red (rosso), Green (verde) e Blue (blu), per un totale
di 23=8 colori; per ciascun colore, il bit IN (intensità) permette di
selezionare una intensità alta (1) o bassa (0), per cui i colori
complessivi diventano 2*8=16.
Su alcuni BIOS (soprattutto quelli meno recenti) il bit in posizione
7 viene definito BL o blinking (lampeggiamento); tale bit permette di
attivare (1) o disattivare (0) il lampeggiamento dello sfondo.

Tornando alla Figura 4, nel registro AL bisogna inserire un valore


che rappresenta la modalità di scrittura.
I valori 0 e 1 indicano che la stringa è composta da soli caratteri,
mentre gli attributi video (uguali per tutti i caratteri) vengono
specificati dal registro BL; la posizione del cursore viene
aggiornata solo per AL=1.
I valori 2 e 3 indicano che la stringa è composta da una sequenza di
coppie (carattere, attributo) con la possibilità quindi di
specificare un attributo video differente per ogni carattere (il
valore in BL viene ignorato); la posizione del cursore viene
aggiornata solo per AL=3.

Fatte queste premesse, supponiamo di avere un blocco dati


referenziato da DS e contenente le seguenti informazioni:

MyString db "Stringa da visualizzare con il servizio BIOS n. 13h della


INT 10h"
strLen equ $ - MyString

A questo punto, nel blocco codice del programma possiamo richiedere


la visualizzazione della stringa MyString con le seguenti istruzioni:

push ds ; copia DS
pop es ; in ES
mov bp, MyString ; ES:BP punta a MyString
mov cx, strLen ; CX = lunghezza stringa
mov dh, 6 ; riga 6
mov dl, 8 ; colonna 8
mov bl, 00011110b ; giallo su blu
mov bh, 0 ; pagina video zero
mov al, 00h ; solo caratteri
mov ah, 13h ; servizio n. 13h
int 10h ; chiama la ISR

Volendo creare una stringa formata da coppie (carattere, attributo),


possiamo scrivere, ad esempio:

MyString db 'T', 1Eh, 'e', 1Fh, 's', 04h, 't', 01h

In tal caso, bisogna ricordare che la lunghezza della stringa


comprende i soli caratteri, per cui dobbiamo scrivere:

strLen equ ($ - MyString) / 2

Nel seguito del capitolo e nei capitoli successivi vedremo altri


esempi relativi ai servizi del BIOS e sarà anche chiarito il concetto
di pagina video.
14
2.3 La BDA - BIOS Data Area

Numerosissime informazioni ricavate dal BIOS durante il POST, vengono


memorizzate in una apposita area della RAM accessibile a tutti i
programmi; tale area prende il nome di BDA o BIOS Data Area (area
dati del BIOS) e, come si vede in Figura 1, per convenzione deve
essere compresa tra gli indirizzi fisici 00400h e 004FFh (a cui
possiamo associare, ad esempio, gli indirizzi logici compresi tra
0040h:0000h e 0040h:00FFh).

La Ralf Brown's Interrupt List contiene una descrizione dettagliata


della BDA nel file memory.lst; si veda la sezione Links Utili di
questo sito.

Per leggere il contenuto della BDA possiamo utilizzare un metodo


diretto che consiste nel caricare il valore 0040h in un registro di
segmento (ad esempio, ES) in modo da poter scrivere poi istruzioni
del tipo:

mov ax, [es:OffsetBDA]

Vediamo un esempio pratico basato sul fatto che all'offset 0010h


della BDA è presente una WORD contenente una serie di informazioni di
sistema relative all'hardware installato sul computer; come si ricava
anche dal manuale utente del BIOS, il significato di tale WORD è
illustrato in Figura 6.

Figura 6 - BDA Equipment Information


Bit Significato
0 Riservato
1 Coprocessore matematico presente
2 Mouse PS/2 presente
3 Riservato
4 Modalità video iniziale:
5 (00b = EGA/VGA, 01b = 40x25 CGA, 10b = 80x25 CGA, 11b = Monocromatico)
6 Numero floppy disk drives:
7 (00b = 1, 01b = 2, 10b = 3, 11b = 4)
8 Riservato
9
Numero porte seriali:
10
(001b = 1, 010b = 2, 011b = 3, 100b = 4, etc)
11
12 Porta giochi installata
13 Riservato
14 Numero porte parallele:
15 (01b = 1, 10b = 2, etc)

In base a quanto esposto in Figura 6, l'istruzione:

mov ax, [es:O010h]


15
carica in AX la Equipment Information WORD della BDA (ovviamente,
ES=0040h)!

In alternativa al metodo appena illustrato, possiamo utilizzare la


INT 11h del BIOS; la chiamata di questo vettore di interruzione
restituisce in AX le identiche informazioni visibili in Figura 6.

Nota importante.
Chi utilizza DOSEmu deve ricordarsi di attivare e configurare
le varie periferiche attraverso l'apposito file
$HOME/.dosemurc; tale file contiene anche le istruzioni sulla
sintassi da utilizzare. Ad esempio:

$_cpu = "80586"
$_mathco = (on)
$_mouse_internal = (on)
$_mouse = "imps2"
$_mouse_dev = "/dev/psaux"
$_joy_device = "/dev/js0"
$_com3 = "/dev/ttyS2 irq 4"
$_com4 = "/dev/ttyS3 irq 3"

e così via.

2.4 La memoria CMOS

Prima che inizi la fase di boot per il lancio del SO, l'utente ha la
possibilità di premere un apposito tasto che gli permette di accedere
al menu di configurazione del BIOS; a seconda del modello di PC, il
tasto da premere può essere [Del] o [Canc] (assemblati), [F10]
(HP/Compaq), [F2] (tasto standard) o un altro tasto spesso
evidenziato mediante un messaggio sul monitor.

Il menu di configurazione del BIOS, presente solo sui PC di classe AT


o superiore, è riservato ad utenti piuttosto esperti; infatti,
attraverso tale menu è possibile modificare le impostazioni hardware
del proprio PC!

Un esempio pratico riguarda la cosiddetta "sequenza di boot"; si


tratta della sequenza di memorie di massa (floppy disk, hard disk,
CD, DVD) analizzate dal BIOS alla ricerca del codice per il lancio
del SO. L'utente ha la possibilità di indicare al BIOS l'ordine
esatto di scansione delle varie memorie di massa; più avanti vedremo
un esempio dettagliato relativo a questo importante aspetto.

Quando usciamo dal menu di configurazione, il BIOS ci chiede se


vogliamo mantenere o meno le nuove impostazioni che abbiamo
eventualmente selezionato; in caso di risposta affermativa, la nuova
configurazione verrà salvata in una apposita memoria RAM denominata
CMOS.
Questa particolare memoria è presente solo a partire dai PC di classe
AT; il suo nome deriva dal fatto che si tratta di una memoria RAM
16
realizzata con i flip flop in tecnologia CMOS.
Come abbiamo visto nella sezione Assembly Base, tale tecnologia viene
impiegata per la realizzazione di piccole memorie RAM ad alta
velocità di accesso; in particolare, la memoria CMOS occupava appena
64 byte sui primi PC di classe AT ed è stata portata a 128 byte sui
PC successivi.

La Figura 7 illustra lo schema a blocchi semplificato del circuito


comprendente la memoria CMOS e l'orologio in tempo reale o Real Time
Clock (RTC) del computer; le informazioni relative alla
configurazione hardware vengono memorizzate nella parte colorata in
rosso.

Figura 7 - Memoria RAM CMOS e RTC

Ad ogni riavvio del computer, il BIOS legge proprio le informazioni


contenute nella CMOS e le utilizza per la configurazione
dell'hardware; a maggior ragione quindi, è necessario evitare
l'inserimento di informazioni errate in questa importante memoria!

Il circuito di Figura 7 ha anche l'importante compito di aggiornare


continuamente l'orologio/calendario del computer; questo lavoro è
svolto dal dispositivo chiamato RTC che memorizza le informazioni
nella apposita area della CMOS.
Ai pin +3V e Gnd viene collegata una batteria ricaricabile il cui
scopo è quello di permettere la conservazione delle informazioni
anche a computer spento; è molto importante quindi che tale batteria
venga mantenuta permanentemente in stato di carica (ciò si ottiene
evitando che il PC rimanga spento per lunghissimi periodi di tempo).

Il BIOS fornisce numerosi servizi finalizzati a leggere in modo


sicuro le informazioni presenti nella CMOS; a tale proposito, si veda
il manuale utente citato in precedenza.
Nei capitoli successivi, il circuito di Figura 7 verrà analizzato in
dettaglio.

17
Nota importante.
Impostando l'hardware in modo non corretto, si può provocare
il malfunzionamento del computer; nei casi meno gravi, la
situazione può essere risolta riavviando il PC e selezionando
le impostazioni di fabbrica nel menu di configurazione del
BIOS.
Bisogna ribadire quindi che solamente gli utenti esperti
possono accedere a tale menu; se, a causa di impostazioni
errate, si riscontrano problemi persistenti sul proprio
computer, si può rendere necessario il ricorso ad un centro
di assistenza tecnica!

2.5 La sequenza di boot

L'ultima fase fondamentale svolta dal BIOS durante l'avvio del


computer, consiste in una chiamata alla INT 19h (System - Bootstrap
loader) che esegue la scansione delle varie memorie di massa (floppy
disk, hard disk, CD, DVD, etc) alla ricerca del codice per il lancio
del SO; l'ordine seguito dal BIOS per effettuare tale scansione,
prende il nome di sequenza di boot.
Sui vecchi PC, la sequenza comprende prima di tutto la scansione dei
floppy disk; se non viene trovato il codice relativo al lancio del
SO, si passa alla scansione degli hard disk. Se anche la scansione
degli hard disk dà esito negativo, il BIOS mostra un messaggio di
errore sullo schermo!
Sui nuovi PC, alla sequenza appena descritta è stata aggiunta anche
la scansione di eventuali CD/DVD e persino di dispositivi Iomega Zip
e schede di rete; come è stato spiegato in precedenza, la sequenza di
boot può essere alterata dall'utente attraverso il menu di
configurazione del BIOS. In questo modo si può chiedere al BIOS di
iniziare la scansione dai dispositivi CD/DVD; ciò rende possibile il
boot direttamente da CD/DVD, come accade, ad esempio, quando si deve
installare Windows XP o quando si vuole eseguire una distribuzione
"live" di Linux!

Per capire il metodo seguito nella scansione delle varie memorie di


massa, analizziamo il caso dei floppy disk e degli hard disk; per i
dettagli relativi ai CD/DVD avviabili, si può consultare il documento
specs-cdrom.pdf (Phoenix IBM - "El Torito" Bootable CD-ROM Format
Specification Version 1.0) scaricabile dal sito della Phoenix.
In riferimento quindi ai floppy disk e agli hard disk, in seguito
alla cosiddetta formattazione, il BIOS suddivide fisicamente ogni
superficie di un disco in tanti cerchi concentrici denominati tracce;
le varie tracce vengono rappresentate con gli indici 0, 1, 2, etc, a
partire da quella più esterna.
Ogni traccia viene suddivisa in tante parti denominate settori; i
vari settori vengono rappresentati con gli indici 0, 1, 2, etc. Il
settore 0, come vedremo più avanti, è riservato; i settori
disponibili per la lettura/scrittura dei file sono quindi quelli con
indici 1, 2, 3, etc.

In base alle considerazioni appena esposte, la superficie di ogni


disco risulta organizzata secondo lo schema mostrato in Figura 8.
18
Figura 8 - Tracce e settori di un disco

Le varie coppie (traccia, settore) rappresentano una sorta di


coordinate cartesiane che permettono di accedere in modo casuale a
qualsiasi settore del disco; come sappiamo, il termine "accesso
casuale" indica il fatto che il tempo di accesso ad un qualsiasi
settore scelto a caso, è costante e quindi indipendente dalla
posizione in cui si trova il settore stesso.

La lettura/scrittura di un disco avviene attraverso apposite testine


magnetiche denominate heads e rappresentate con gli indici 0, 1, 2,
etc; ogni faccia di un disco viene acceduta attraverso una delle
testine magnetiche presenti.
Nel caso dei floppy disk a doppia faccia (nel senso che entrambe le
facce possono memorizzare informazioni), sono presenti due testine,
una per ogni faccia; quella superiore viene denominata head 0, mentre
quella inferiore viene denominata head 1.
Gli hard disk sono costituiti da uno o più dischi a doppia faccia,
sovrapposti tra loro in modo da formare una pila; le varie testine
magnetiche, una per ogni faccia, vengono quindi denominate head 0,
head 1, head 2, etc.
In un hard disk costituito da una pila di dischi a doppia faccia, le
tracce aventi lo stesso indice formano un cosiddetto cilindro; ad
esempio, tutte le tracce 0 dei vari dischi che formano un hard disk,
rappresentano il cilindro 0 (ciò vale anche per i floppy disk dove,
ad esempio, il cilindro 0 è formato dalla traccia 0 della faccia A e
dalla traccia 0 della faccia B).

I SO suddividono logicamente la struttura di Figura 8 in modo da


ottenere un cosiddetto file system; grazie al file system i programmi
vedono un disco, come quello di Figura 8, sotto forma di vettore
lineare di celle.
Ogni cella può essere costituita da un numero di byte che, in genere,
è multiplo di 512; ciò permette di velocizzare notevolmente le
operazioni di I/O su disco.

Il settore 0 relativo alla traccia 0, cilindro 0, head 0, faccia A di


un disco (il primo disco, nel caso degli hard disk), prende il nome
di Master Boot Record (settore principale di boot) o MBR e assume una
importanza particolare; infatti, durante la sequenza di boot, il BIOS
19
controlla il MBR di ogni disco alla ricerca di un eventuale
bootloader.
Con il termine bootloader si indica un piccolo programma da 512 byte
che contiene il codice necessario per il lancio del SO; per
identificare un bootloader, il BIOS utilizza un meccanismo molto
semplice che consiste nel verificare se gli ultimi 2 byte contenuti
in un MBR (da 512 byte) assumono il valore AA55h!
In caso affermativo, il BIOS legge questo blocco da 512 byte e lo
carica in memoria all'indirizzo fisico 07C00h a cui possiamo
associare, ad esempio, l'indirizzo logico 0000h:7C00h; a questo punto
lo stesso BIOS carica l'indirizzo logico 0000h:7C00h in CS:IP e cede
il controllo alla CPU.
La CPU esegue quindi le istruzioni contenute nel bootloader; tali
istruzioni, ovviamente, svolgono tutto il lavoro necessario per il
caricamento in memoria del SO!

Nota importante.
È fondamentale tenere presente che il BIOS utilizza appositi
codici per identificare i vari floppy disk drive e gli hard
disk; in particolare:

i floppy disk drive sono indicati, nell'ordine, dai codici


00h, 01h, 02h, etc;

gli hard disk sono indicati, nell'ordine, dai codici 80h,


81h, 82h, etc.

Il codice del dispositivo da cui è avvenuto il boot viene


caricato dal BIOS nel registro DL; tale codice è quindi a
disposizione del bootloader!

Dalle considerazioni appena esposte, si intuisce che la scrittura di


un piccolo bootloader "didattico" non presenta alcuna difficoltà; in
effetti, si tratta di una esperienza molto interessante che ogni
programmatore Assembly non può fare a meno di provare.

Nota importante.
L'unico aspetto da gestire con molta cautela, riguarda il
metodo da seguire per installare un bootloader "personale"
nel MBR di un disco; si tratta chiaramente di una operazione
molto delicata in quanto il MBR viene anche utilizzato dai SO
per contenere informazioni relative al tipo di file system
presente sul disco!
Tutto ciò significa che dopo l'inserimento del nostro
bootloader nel MBR di un disco, il disco stesso risulta
illeggibile da parte del SO; proprio per questo motivo, si
raccomanda vivamente di effettuare questo tipo di
esperimenti, servendosi di un floppy disk e NON dell'hard
disk del proprio computer!

2.5.1 Recupero di un MBR danneggiato

Indipendentemente dalle considerazioni svolte in questo capitolo, può


20
capitare che un utente possa danneggiare il MBR del proprio hard disk
(ad esempio, a causa di un blackout mentre si stava partizionando il
disco); in una situazione del genere, il danno può essere facilmente
riparato purché l'utente abbia avuto l'accortezza di premunirsi per
tempo.
Il metodo da seguire consiste semplicemente nel dotarsi
preventivamente di un apposito disco di emergenza; vediamo allora
come si crea questo disco in base al SO utilizzato.

Ambiente DOS puro

In ambiente DOS puro si utilizza un file system di vecchio tipo FAT12


o di nuovo tipo FAT16; in tal caso è sufficiente inserire un floppy
disk nel drive e impartire il comando:

format a: /s

L'opzione /s fa in modo che, dopo la formattazione, la parte


essenziale del kernel del DOS venga trasferita sul disco,
trasformandolo in un "disco DOS avviabile"; a questo punto, dobbiamo
anche copiare sul disco il programma FDISK.EXE che, generalmente, si
trova nella directory C:\DOS.
Nel caso in cui il MBR del nostro hard disk abbia subito danni,
dobbiamo inserire il floppy di emergenza e riavviare il computer;
dopo il riavvio, dobbiamo impartire il comando:

fdisk /mbr

Questo comando ripristina il MBR dell'hard disk permettendoci di


rientrare in possesso di tutti i dati in esso contenuti!

Ambiente Windows 9x/Me

In ambiente Windows 9x/Me si utilizza un file system di tipo FAT32;


il metodo da seguire è identico a quello appena descritto, con
l'unica differenza che il comando:

format a: /s

deve essere impartito dalla DOS Box di Windows (o dall'apposito


programma di Windows per la formattazione dei floppy disk).

Ambiente Windows NT, 2000, XP

In ambiente Windows NT, 2000, Xp si può utilizzare un file system di


tipo FAT32 o NTFS; le considerazioni che seguono si riferiscono al
caso di un disco NTFS (in tal caso, non è ovviamente possibile
utilizzare FDISK).
Il metodo da seguire consiste nel servirsi del CD/DVD di
installazione del SO; a tale proposito, bisogna inserire il CD/DVD e
riavviare il computer in modo da far partire la fase di installazione
di Windows.
Dopo aver caricato in memoria tutti gli strumenti necessari, Windows
chiede all'utente se si vuole effettuare una reinstallazione o un
21
ripristino del sistema; naturalmente, bisogna selezionare la seconda
opzione.
A questo punto viene mostrato l'elenco di installazioni disponibili;
generalmente, la lista che appare è del tipo:

1: C:\WINDOWS

Selezionando il numero dell'installazione da ripristinare, si entra


nella cosiddetta console di ripristino; dalla console si può
visualizzare l'elenco delle partizioni attraverso il comando:

map

Le varie partizioni vengono mostrate con una sintassi del tipo:

\Device\Harddisk0\Partition1

Supponendo che Harddisk0 contenga il MBR da riparare, dobbiamo allora


impartire il comando:

fixmbr \Device\HardDisk0

Ora possiamo impartire il comando:

exit

in modo da uscire dalla console e riavviare il computer (dopo aver


tolto il CD/DVD di installazione).

Si tenga presente che, sia FDISK, sia FIXMBR, eliminano un eventuale


bootloader (come Lilo) installato da Linux; per ripristinare il
bootloader di Linux e per maggiori dettagli su questi importanti
aspetti, si consiglia di consultare le numerose informazioni presenti
su Internet.

2.5.2 Esempio di bootloader

Passiamo finalmente alla scrittura di un piccolo bootloader


"didattico"; lo scopo di questo bootloader è semplicemente quello di
ricevere il controllo dal BIOS, mostrare alcune informazioni
diagnostiche e chiedere all'utente di riavviare il computer.

Il primo aspetto da affrontare riguarda il fatto che, come è stato


spiegato in precedenza, il BIOS carica i 512 byte del bootloader in
memoria, all'indirizzo logico 0000h:7C00h; a questo punto, lo stesso
BIOS cede il controllo al nostro bootloader e ci lascia "soli con noi
stessi"!
Non bisogna dimenticare, infatti, che in questa fase non esiste
nessun SO capace di fornirci i suoi servizi; una prima conseguenza di
questo aspetto, è che gli eventuali strumenti di cui possiamo aver
bisogno, devono essere creati attraverso i servizi del BIOS.
Supponiamo, ad esempio, di voler visualizzare sullo schermo il
contenuto esadecimale di un registro a 16 bit; a tale proposito,
dobbiamo prima convertire il valore esadecimale in una stringa.

22
Infatti, come abbiamo visto in precedenza, la scheda video viene
inizializzata in modalità testo 80x25, per cui sullo schermo possiamo
visualizzare solamente simboli appartenenti al set dei codici ASCII!

Una stringa formata esclusivamente da codici ASCII di cifre numeriche


prende il nome di stringa numerica; si tratta in sostanza di una
stringa formata da una sequenza di caratteri del tipo '0', '1', '2',
etc.
Per convertire un numero esadecimale in una stringa numerica, si può
utilizzare un metodo semplicissimo; a tale proposito, creiamoci prima
le seguenti stringhe:

RegStr db "XXXXh"
HexStr db "0123456789ABCDEF"

Consideriamo ora il valore esadecimale 8CE9h contenuto in AX; copiamo


tale valore in BX e isoliamo il nibble meno significativo con
l'istruzione:

and bx, 000Fh

In questo modo si ottiene, ovviamente, BX=0009h; osserviamo ora che:

byte [HexStr + bx] = byte [HexStr + 9] = '9'

Il carattere '9' viene copiato in [RegStr+3].

Facciamo scorrere ora il contenuto di AX di 4 bit verso destra; in


questo modo otteniamo AX=08CEh; copiamo tale valore in BX e isoliamo
il nibble meno significativo con l'istruzione:

and bx, 000Fh

In questo modo si ottiene, ovviamente, BX=000Eh; osserviamo ora che:

byte [HexStr + bx] = byte [HexStr + Eh] = byte [HexStr + 14] = 'E'

Il carattere 'E' viene copiato in [RegStr+2].

A questo punto appare evidente che, con due ulteriori passi, il


numero esadecimale 8CE9h viene facilmente convertito nella stringa
numerica "8CE9h"; la procedura che esegue questo lavoro, presuppone
che AX contenga il numero da convertire e assume il seguente aspetto:

Hex16toStr:

mov si, 3 ; offset 3 in RegStr


mov cx, 4 ; 4 loop (4 cifre esadecimali)

hex_loop:
mov bx, ax ; bx = prossimo nibble
and bx, 000Fh ; isola i 4 bit meno significativi
mov dl, [HexStr + bx] ; converte in ASCII
mov [RegStr + si], dl ; salva in RegStr
dec si ; prossimo carattere di RegStr
23
shr ax, 4 ; prossimo nibble da esaminare
loop hex_loop ; controllo loop

retn ; near return

Una volta ottenuta la stringa numerica, possiamo visualizzarla


attraverso il servizio BIOS n.13h della INT 10h; in particolare, il
programma presentato più avanti visualizza la coppia CS:IP e il
contenuto del registro DL che rappresenta il codice del disco dal
quale è avvenuto il boot.

Un altro importante aspetto da affrontare, riguarda l'indirizzamento


dei vari dati del nostro programma; a tale proposito, partiamo dal
fatto che il BIOS carica il bootloader in memoria a partire
dall'indirizzo logico 0000h:7C00h, pone poi CS:IP=0000h:7C00h e
infine cede il controllo alla CPU.
Per l'indirizzamento del codice non c'è quindi nessun problema in
quanto il BIOS ha provveduto ad inizializzare correttamente CS:IP; il
problema si pone, invece, per l'indirizzamento dei dati.
Ciò accade in quanto, in assenza del SO, non viene effettuata nessuna
rilocazione della componente Seg che carichiamo in DS (o in ES) per
accedere ai dati; questo delicato lavoro spetta dunque al
programmatore!

Assumiamo allora che il nostro bootloader sia contenuto in un


eseguibile in formato COM; come sappiamo, in tal caso il file
eseguibile contiene esclusivamente il codice macchina del programma
(ed è proprio ciò che vogliamo). L'unico segmento di programma
presente, sarà quindi caricato in memoria a partire dall'indirizzo
logico 0000h:7C00h; tale indirizzo logico corrisponde all'indirizzo
fisico, multiplo di 16:

0000h * 10h + 7C00h = 00000h + 7C00h = 07C00h

Come sappiamo, in ambiente DOS i primi 256 byte del segmento unico di
un programma COM devono essere riservati al PSP; nel nostro caso, non
esiste nessun DOS, per cui possiamo posizionare l'entry point dove
vogliamo!

Una prima soluzione consiste allora nell'aprire il segmento unico di


programma con la direttiva:

org 7C00h

In questo caso, sappiamo che l'assembler genera un eseguibile dove


tutte le componenti offset dei dati risulteranno sommate a 7C00h; a
questo punto, per accedere correttamente ai dati stessi, non dobbiamo
fare altro che caricare 0000h in DS.

La seconda soluzione parte dal presupposto che l'indirizzo fisico


07C00h corrisponde all'indirizzo logico normalizzato 07C0h:0000h; ciò
significa che l'indirizzo logico 07C0h:0000h è perfettamente
equivalente all'indirizzo logico 0000h:7C00h!
Possiamo aprire allora il segmento unico di programma con la
24
direttiva:

org 0000h

In questo caso, sappiamo che l'assembler genera un eseguibile dove


tutte le componenti offset dei dati risulteranno sommate a 0000h; a
questo punto, per accedere correttamente ai dati stessi, non dobbiamo
fare altro che caricare 07C0h in DS.

Un ulteriore aspetto interessante riguarda l'eventualità di voler


sapere se il valore assunto da IP all'entry point sia veramente
7C00h; a tale proposito, possiamo servirci del seguente codice:

000Eh E8h 0000h call near get_ip


0011h get_ip:
0011h 58h pop ax
0012h 2Dh 0011h sub ax, (($ - $$) - 1)

Osserviamo che nell'esempio, la CALL (diretta intrasegmento) si trova


all'offset 000Eh; questa istruzione salva nello stack l'indirizzo di
ritorno 0011h e salta a CS:0011h. Ma l'indirizzo di ritorno 0011h non
è altro che il valore da caricare in IP per la prossima istruzione da
eseguire; tale valore viene quindi estratto dallo stack e salvato in
AX (ovviamente, la POP ha anche lo scopo di sostituire l'istruzione
RETN).
Ad AX dobbiamo ora sottrarre l'offset corrente ($-$$) che, per
l'istruzione SUB, è 0012h; siccome però la POP ha un codice macchina
da 1 byte, dobbiamo sottrarre anche 1 a 0012h ottenendo così 0011h.
Quando il programma è in fase di esecuzione, la CALL provoca un salto
a:

CS:IP = 0000h:(0011h + 7C00h) = 0000h:7C11h

L'indirizzo di ritorno salvato nello stack è quindi 7C11h; di


conseguenza, l'istruzione SUB produce:

AX = 7C11h - 0011h = 7C00h

Analizziamo infine gli aspetti relativi all'uscita dal programma; la


soluzione più semplice consiste nel chiedere all'utente di togliere
il floppy disk e riavviare il computer con la sequenza di tasti:

[Ctrl] + [Alt] + [Canc]

Esiste però una soluzione più elegante che consiste in un riavvio


automatico attraverso un salto FAR a FFFFh:0000h. Come sappiamo, nei
vecchi PC a tale indirizzo è presente un salto FAR alla prima
istruzione eseguibile del POST che provoca il reset della CPU con
conseguente riavvio (reboot) del computer; nei moderni PC, per
compatibilità, in FFFFh:0000h si trovano le istruzioni che provocano
il riavvio o lo spegnimento del computer.
Nel caso generale, prima di effettuare il salto FAR, i SO inseriscono
un apposito codice a 16 bit (POST reset flag) in un'area della BDA

25
che si trova all'indirizzo logico 0040h:0072h; sono disponibili i
seguenti codici:

Figura 9 - BDA POST Reset Flags


Valore Effetto
0000h Cold boot - Riavvio totale del computer
0064h Burn-in mode
1234h Warm boot - Riavvio senza pulizia della RAM
4321h (Solo PS/2) - Riavvio senza pulizia della RAM
5678h (Solo PC Convertible) System suspended
9ABCh (Solo PC Convertible) Manufacturing test mode
ABCDh (Solo PC Convertible) POST loop mode

Nel caso del nostro bootloader, utilizziamo il codice 1234h per un


warm reboot (che equivale alla pressione dei tasti
[Ctrl]+[Alt]+[Canc]); il codice 0000h (cold boot) permette, invece,
di spegnere il computer (ovviamente, solo per i PC che supportano lo
spegnimento via software).

A questo punto abbiamo chiarito tutti i dettagli relativi al nostro


bootloader; il conseguente listato è illustrato in Figura 10.

Figura 10 - File BOOTLOAD.ASM


BOOTLOAD.ASM
file bootload.asm
; copyright (C) Ra.M. Software
; esempio di semplice bootloader
; nasm -f bin bootload.asm -o bootload.bin

;--------------- direttive per l'assembler ---------------------

BITS 16 ; al boot la CPU è in real mode

;---------- dichiarazioni macro e costanti simboliche ----------

; WRITE_STRING offsStr, lenStr, x, y, attrib

%macro WRITE_STRING 5

mov bp, %1
mov cx, %2
mov dh, %3
mov dl, %4
mov bl, %5
mov bh, 0
mov al, 01h
mov ah, 13h
int 10h

%endmacro

; WAIT_CHAR

%macro WAIT_CHAR 0

26
xor ah, ah
int 16h

%endmacro

;------------------------- segmento unico ------------------------

section .text

org 0000h ; nessuno spiazzamento iniziale

boot_entry: ; CS:IP = 0000h:7C00h

cli ; disabilita le INT mascherabili


mov ax, 07C0h ; trasferisce il paragrafo 07C0h
mov ds, ax ; in DS
mov es, ax ; in ES
mov ss, ax ; e in SS
mov sp, 0FFFEh ; TOS iniziale
sti ; riabilita le INT mascherabili

; determinazione di IP iniziale (boot_entry)

call near get_ip ; chiamata NEAR a get_ip


get_ip: ; [SS:SP] = IP corrente
pop ax ; ax = IP corrente
sub ax, (($ - $$) - 1) ; sottrae l'offset corrente - 1
mov [regIP], ax ; salva in regIP
mov [regDX], dl ; salva DL (= supporto di boot)

; visualizza il titolo del programma

WRITE_STRING BootStr0, BootStr0Len, 0, 11, 00001011b


WRITE_STRING BootStr1, BootStr1Len, 4, 0, 00001001b

; visualizza CS

WRITE_STRING BootStr2, BootStr2Len, 6, 0, 00001010b


mov ax, cs
call Hex16toStr
WRITE_STRING RegStr, RegStrLen, 6, 5, 00001100b

; visualizza IP

WRITE_STRING BootStr3, BootStr3Len, 7, 0, 00001010b


mov ax, [regIP]
call Hex16toStr
WRITE_STRING RegStr, RegStrLen, 7, 5, 00001100b

; visualizza DX (DL)

WRITE_STRING BootStr4, BootStr4Len, 8, 0, 00001010b


mov ax, [regDX]
call Hex16toStr
WRITE_STRING RegStr, RegStrLen, 8, 5, 00001100b

; visualizza la stringa di uscita e attende la pressione di un tasto

WRITE_STRING BootStr5, BootStr5Len, 10, 0, 00001110b


WAIT_CHAR

; riavvio del computer


27
mov ax, 0040h ; paragrafo 0040h (BDA)
mov es, ax ; in ES
mov word [es:0072h], 1234h ; warm reboot
jmp far [reboot] ; salto FAR a FFFFh:0000h

;------------------------- area dati --------------------------

reboot dd 0FFFF0000h
regIP dw 0
regDX dw 0
BootStr0 db "Tutorial Assembly Avanzato - Copyright (C) Ra.M. Software."
BootStr0Len equ $ - BootStr0
BootStr1 db "Bootloader caricato in memoria!"
BootStr1Len equ $ - BootStr1
BootStr2 db "CS ="
BootStr2Len equ $ - BootStr2
BootStr3 db "IP ="
BootStr3Len equ $ - BootStr3
BootStr4 db "DX ="
BootStr4Len equ $ - BootStr4
BootStr5 db "Rimuovere il floppy disk e premere un tasto ..."
BootStr5Len equ $ - BootStr5
RegStr db "XXXXh"
RegStrLen equ $ - RegStr
HexStr db "0123456789ABCDEF"

;------------------------ area procedure -------------------------

; Hex16toStr(ax) ; converte un Hex16 (AX) in una stringa "XXXXh"

Hex16toStr:

mov si, 3 ; SI = offset 3 in RegStr


mov cx, 4 ; 4 loop (4 cifre esadecimali)

hex_loop:
mov bx, ax ; BX = prossimo nibble
and bx, 000Fh ; isola i 4 bit meno significativi
mov dl, [HexStr + bx] ; converte in ASCII
mov [RegStr + si], dl ; salva in RegStr
dec si ; prossimo carattere di RegStr
shr ax, 4 ; prossimo nibble da esaminare
loop hex_loop ; controllo loop

retn ; near return

;------------------------- boot signature ------------------------------

times 510-($-$$) db 00h ; riempie con 00h sino a 07C0h:01FDh


dw 0AA55h ; codice di boot

;-------------------------------------------------
Come si vede in Figura 10, per l'accesso ai dati è stato scelto
l'indirizzo logico di riferimento 07C0h:0000h (perfettamente
equivalente a 0000h:7C00h); di conseguenza, la direttiva ORG deve
specificare il parametro 0000h (nessuna traslazione in avanti degli
offset).
Il paragrafo 07C0h viene quindi caricato in DS, ES e SS (NEARSTACK);
il registro SP viene inizializzato con il valore massimo possibile
FFFEh (nessuno ce lo impedisce).
28
La macro WRITE_STRING usa il servizio BIOS n.13h della INT 10h per
visualizzare una stringa; la macro WAIT_CHAR attende la pressione di
un tasto grazie al seguente servizio BIOS n.00h della INT 16h
(Keyboard services):

Figura 11 - Servizio n. 00h della INT 16h


INT 16h - Servizio n. 00h - Read keyboard input:
attende la pressione di un tasto.

Argomenti richiesti:
AH = 00h (servizio Read keyboard input)

Valori restituiti:
AL = codice ASCII del tasto premuto
AH = codice di scansione del tasto premuto

Analizzando il listing file del programma di Figura 10, si può notare


che siamo al limite dei 512 byte; aggiungendo poche altre istruzioni,
tale limite viene superato. In un caso del genere NASM produce un
messaggio di errore per indicare che la sottrazione 510-($-$$) ha
prodotto un valore negativo; in sostanza, siamo andati oltre l'offset
massimo 510 (dopo il quale c'è il codice AA55h)!
Proprio per questo motivo, i moderni bootloader non fanno altro che
caricare in memoria un ulteriore programma di dimensioni più
consistenti; tale programma, non avendo problemi di spazio, può così
effettuare tutte le necessarie inizializzazioni del SO.

2.5.3 Generazione dell'eseguibile

Per l'eseguibile di un bootloader conviene decisamente orientarsi sul


formato COM; infatti, sappiamo che per tale formato viene generato il
solo codice macchina, senza nessun inutile header che finirebbe anche
per alterare le dimensioni (512 byte) del file eseguibile.
Nel caso di NASM possiamo evitare l'uso del linker e ottenere
direttamente l'eseguibile finale (formato binario) attraverso il
comando:

nasm -f bin bootload.asm -o bootload.bin

Osservando il file BOOTLOAD.BIN così ottenuto possiamo notare che le


sue dimensioni sono pari esattamente a 512 byte!

Naturalmente, è anche possibile adattare il listato di Figura 10 in


modo da poter generare un eseguibile in formato COM classico; più
avanti verranno dati dei chiarimenti per chi usa MASM e TASM.

2.5.4 Installazione del bootloader

Come è stato ampiamente precisato in precedenza, il bootloader


presentato in Figura 10 è destinato ad essere installato nel MBR di
un floppy disk; chi intende servirsi del proprio hard disk, si assume
tutte le responsabilità sulle possibili conseguenze!

29
Per gli utenti DOS/Windows, esiste un metodo semplicissimo che
permette di effettuare l'installazione del bootloader; a tale
proposito, bisogna servirsi del comando W (write) nel programma DEBUG
(già illustrato nel Capitolo 14 della sezione Assembly Base).
La sintassi del comando W è la seguente:

w address drive sector number

address indica l'indirizzo da cui leggere il blocco sorgente;


drive indica l'unità su cui scrivere i dati;
sector indica il settore iniziale di scrittura;
number indica il numero di blocchi da 512 byte da scrivere.

Per quanto riguarda il drive, abbiamo visto in precedenza che il BIOS


assegna il codice 00h al primo lettore floppy disk e 01h al secondo
lettore floppy disk; analogamente, il codice 80h indica il primo hard
disk e 81h indica il secondo hard disk.
Il settore iniziale di scrittura è ovviamente il n.0; il numero di
blocchi da 512 byte che dobbiamo scrivere è 1.
In relazione all'indirizzo da cui leggere il blocco sorgente, bisogna
ricordare che DEBUG carica i programmi in formato COM a partire
dall'indirizzo CS:0100h; il valore da caricare in CS viene scelto
dallo stesso DEBUG e non deve essere assolutamente alterato!

Posizionandoci allora nella directory di lavoro (dove si trova


BOOTLOAD.BIN), impartiamo il comando:

debug bootload.bin

Come verifica possiamo impartire il comando u (unassemble) che ci


mostrerà il disassemblato delle prime istruzioni del nostro
programma; in questo modo si può anche constatare che il programma
stesso è stato caricato in memoria a partire dall'indirizzo logico
CS:0100h.
A questo punto, inseriamo un floppy disk e da DEBUG impartiamo il
comando:

w 100 0 0 1 (per il primo floppy disk)

Il nostro bootloader è ora installato nel MBR del primo floppy disk;
infatti, riavviando il computer si può constatare che il BIOS cede il
controllo al programma BOOTLOAD.BIN!

Gli utenti Linux possono seguire l'identico metodo appena illustrato;


a tale proposito, bisogna eseguire il clone di DEBUG da DOSEmu.
Esiste però un altro metodo molto più semplice e sicuro, che consiste
nel servirsi del potente comando dd per la copia di dati binari
grezzi da una sorgente ad una destinazione; la sintassi generica di
questo comando è:

dd if=input_file of=output_file bs=block_size count=num_blocks

input_file indica la sorgente da cui leggere i dati;


output_file indica la destinazione in cui scrivere i dati;
30
bs indica la dimensione di ogni singolo blocco da trasferire;
count indica il numero di blocchi da trasferire.

Naturalmente, il comando dd deve essere eseguito da una console Linux


e NON da DOSEmu!

Ricordando che in Linux "tutto è un file", avremo if=bootload.bin e


of=/dev/fd0 (primo lettore floppy disk); aprendo allora una console e
posizionandoci nella directory di lavoro in cui si trova
BOOTLOAD.BIN, inseriamo un floppy disk e impartiamo il comando:

dd if=bootload.bin of=/dev/fd0 bs=512 count=1

Tenendo conto del fatto che il valore predefinito per bs è 512,


mentre per count è 1, possiamo anche scrivere semplicemente:

dd if=bootload.bin of=/dev/fd0

Nota importante.
Si faccia molta attenzione a non utilizzare disinvoltamente
il comando dd; ciò vale, in particolare, quando si specifica
l'hard disk come destinazione!
Prima di utilizzare il comando dd è vivamente consigliabile
la lettura delle relative "pagine man" (man dd).

2.5.5 Conversione di BOOTLOAD.ASM in versione MASM/TASM

Se si intende convertire il listato di Figura 9 in versione


MASM/TASM, è necessario tenere presente che il significato della
direttiva ORG varia da assembler ad assembler.
Nel caso di NASM, una direttiva del tipo:

ORG 150

sposta il location counter in avanti di 150 byte a partire


dall'offset corrente (in cui si trova la stessa direttiva ORG); nel
caso di MASM e TASM, invece, la precedente direttiva posiziona il
location counter all'offset 150 del segmento di programma corrente!
Quindi, con MASM e TASM, la parte finale del nostro bootloader può
essere scritta in questo modo:

org 510
dw 0AA55h

Bisogna anche ricordare che TASM non permette la creazione di un


eseguibile in formato COM con entry point ad un offset diverso da
0100h; la riscrittura di BOOTLOAD.ASM in versione TASM rappresenta un
valido esercizio per chi vuole verificare la propria conoscenza
dell'Assembly!

Bibliografia

31
IA-32 Intel Architecture Software Developer's Manual - Volume 3:
System Programming Guide
(24547212.pdf)

PhoenixBIOS 4.0 Revision 6 User's Manual


(userman.pdf)

PhoenixBIOS 4.0 Release 6.0 POST Tasks and Beep Codes


(biospostcode.pdf)

Phoenix Technologies, Ltd. AwardBIOS Version 4.51PG - Post Codes &


Error Messages
(biosawardpostcode.pdf)

Phoenix IBM - "El Torito" Bootable CD-ROM Format Specification


Version 1.0
(specs-cdrom.pdf)

Ralf Brown's Interrupt List

32
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


33
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


INT Eccezione
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à


34
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.
35
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!

36
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
37
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
38
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

39
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
40
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


Porta Indirizzo
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
41
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)


Bit Significato
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
6
(000b per le CPU 80x86)
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
42
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)


Bit Significato
0
1 Non usati (ignorati)
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


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


Bit Significato
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)


Bit Significato
0
1 CAS (cascade)
2
3
4
5 Riservati (devono valere 00000b)
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.

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


Bit Significato
0 Modalità: 0 = MCS-80/85, 1 = 80x86
1 EOI: 0 = normal mode, 1 = auto mode
2 Bufferizzazione dati:
3 00b e 01b = no, 10b = si (Slave), 11b = si (Master)
4 Gestione IRQ: 0 = sequential mode, 1 = SFNM
5
6 Riservati (devono valere 000b)
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
45
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)


IRQ Assegnato a INT
0 PIT 8254 - Programmable Interval Timer 08h
1 Keyboard controller 09h
2 IRQ da 8 a 15 in cascata dal PIC Slave 0Ah
3 Serial port COM2 (o COM4) 0Bh
46
Figura 10 - Associazioni IRQ - INT n (PIC Master)
IRQ Assegnato a INT
4 Serial port COM1 (o COM3) 0Ch
5 Parallel port LPT2 0Dh
6 Floppy Disk controller 0Eh
7 Parallel port LPT1 0Fh

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

Figura 11 - Associazioni IRQ - INT n (PIC Slave)


IRQ Assegnato a INT
8 CMOS-RTC - Real Time Clock 70h
9 VGA/LAN/ACPI (IRQ2 Dirottata dal PIC Master) 71h
10 Riservato (schede video) 72h
11 Riservato (schede audio) 73h
12 Mouse PS/2 74h
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.

47
Figura 12 - Comando OCW1 (Read/Write - 21h/A1h)
Bit Significato
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)


Bit Significato
0
1 Livello di rotazione priorità IRQ
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

48
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 IR0 IR1 IR2 IR3 IR4 IR5 IR6 IR7


Priorità 1 2 3 4 5 6 7 0

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 IR0 IR1 IR2 IR3 IR4 IR5 IR6 IR7


Priorità 3 4 5 6 7 0 1 2

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


Bit Significato
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
50
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 -- -- -- -- W2 W1 W0
Bit 7 6 5 4 3 2 1 0

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

52
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


KEYBOARD.ASM
;--------------------------------------------------;
; file keyboard.asm ;
; copyright (C) Ra.M. Software ;
; intercetta la IRQ1 - INT 09h keyboard controller ;
;--------------------------------------------------;
; nasm -f obj keyboard.asm ;
; tlink /t keyboard.obj + comlib.obj ;
; (oppure link /tiny keyboard.obj + comlib.obj) ;
;--------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "comlib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign INT09h 09h ; INT 09h - keyboard ISR


%assign KBD_BUFF 60h ; porta buffer dati tastiera
%assign KBD_COMM 64h ; porta comandi tastiera
%assign MPICP0 20h ; porta P0 PIC Master
%assign MPICP1 21h ; porta P1 PIC Master

%assign IRQ1_MASK 00000010b ; mask-on per la IRQ1


%assign IRQ1_UNMASK 11111101b ; mask-off per la IRQ1

;################ segmento unico ##################

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

resb 0100h ; libera 256 byte per il PSP

..start: ; entry point

;------- inizio blocco principale istruzioni ------

call hideCursor ; nasconde il cursore


call clearScreen ; pulisce lo schermo

53
; visualizza il titolo del programma

mov di, title_str ; DS:DI punta a title_str


mov dx, 000Ah ; riga, colonna di output
call writeString ; visualizza la stringa

; installazione nuova ISR per la INT 09h

in al, MPICP1 ; AL = IMR PIC Master


or al, IRQ1_MASK ; pone a 1 il bit 1 (mask)
out MPICP1, al ; scrive OCW1 nel PIC Master

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [es:(INT09h * 4)] ; legge il vecchio vettore 09h
mov [old_int09h], eax ; e lo salva in old_int09h

mov ax, cs ; AX = Seg(new_int09h)


shl eax, 16 ; sposta nella WORD alta di EAX
mov ax, new_int09h ; AX = Offset(new_int09h)
mov [es:(INT09h * 4)], eax ; installa il nuovo vettore 09h

in al, MPICP1 ; AL = IMR PIC Master


and al, IRQ1_UNMASK ; pone a 0 il bit 1 (unmask)
out MPICP1, al ; scrive OCW1 nel PIC Master

mov dx, 0400h ; inizializza riga, colonna

; loop principale del programma

keyboard_loop:

mov al, [key_buffer] ; nuovo scan code letto dalla ISR


cmp al, 01h ; tasto [Esc] premuto?
jne keyboard_loop ; controllo loop

; ripristino vecchia ISR per la INT 09h

in al, MPICP1 ; AL = IMR PIC Master


or al, IRQ1_MASK ; pone a 1 il bit 1 (mask)
out MPICP1, al ; scrive OCW1 nel PIC Master

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [old_int09h] ; EAX = indirizzo vecchio vettore 09h
mov [es:(INT09h * 4)], eax ; ripristina il vecchio vettore 09h

in al, MPICP1 ; AL = IMR PIC Master


and al, IRQ1_UNMASK ; pone a 0 il bit 1 (unmask)
out MPICP1, al ; scrive OCW1 nel PIC Master

call showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;----- inizio definizione variabili statiche ------

54
align 4, db 0 ; allinea alla DWORD

old_int09h dd 0 ; indirizzo vecchia ISR


key_buffer db 0 ; scan code tasto premuto
irq_counter db 0 ; contatore IRQ

title_str db "CODICI DI SCANSIONE DELLA TASTIERA"


db " (premere [Esc] per uscire)", 0
clear_str db " ", 0

;------- fine definizione variabili statiche ------

;---------- inizio definizione procedure ----------

; new_int09h: nuova ISR per la INT 09h


; una ISR deve sempre preservare tutti i registri che utilizza!

new_int09h:

push ax ; preserva AX
push bx ; preserva BX
push cx ; preserva CX
push di ; preserva DI

cmp byte [irq_counter], 0 ; fine scan code ?


ja next_scancode ; se "no" salta a next_scancode
mov di, clear_str ; DS:DI punta a clear_str
mov dx, 0400h ; riga 4, colonna 0
call writeString ; visualizza la stringa

xor cx, cx ; CX = 0
wait1:
in al, KBD_COMM ; legge lo Status Byte
test al, 00000010b ; buffer pronto per la lettura ?
loopnz wait1 ; controllo loop

in al, KBD_BUFF ; legge il prossimo codice


mov [key_buffer], al ; e lo salva in key_buffer
call writeHex8 ; visualizza il codice

mov byte [irq_counter], 0 ; assume "normal key" (1 codice)

test_for_E0h:
cmp al, 0E0h ; codice == E0h ?
jne test_for_E1h ; se "no" salta a test_for_E1h
mov byte [irq_counter], 1 ; "extended key" da 2 codici
add dl, 4 ; incremento colonna
jmp short send_eoi ; salta a send_eoi

test_for_E1h:
cmp al, 0E1h ; codice == E1h ?
jne send_eoi ; se "no" salta a send_eoi
mov byte [irq_counter], 5 ; "extended key" da 6 codici
add dl, 4 ; incremento colonna
jmp short send_eoi ; salta a send_eoi

next_scancode:
xor cx, cx ; CX = 0
wait2:
in al, KBD_COMM ; legge lo Status Byte
test al, 00000010b ; buffer pronto per la lettura ?
loopnz wait2 ; controllo loop
55
in al, KBD_BUFF ; legge il prossimo codice
mov [key_buffer], al ; e lo salva in key_buffer
call writeHex8 ; visualizza il codice
add dl, 4 ; incremento colonna
dec byte [irq_counter] ; decremento contatore

send_eoi:
mov al, 20h ; OCW3 = specific EOI
out MPICP0, al ; scrive OCW3 nel PIC Master

pop di ; ripristina DI
pop cx ; ripristina CX
pop bx ; ripristina BX
pop ax ; ripristina AX
iret ; return from interrupt

;----------- fine definizione procedure -----------

;##################################################

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
56
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 è
57
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


Bit Significato
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)


Bit Significato
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

58
Figura 18 - OBC Status Byte (Read - 64h)
Bit Significato
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.
59
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)


Bit Significato
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
4
5 Riservati (devono valere 00000b)
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
60
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)

61
Capitolo 4 La memoria CMOS e il Real Time
Clock
Quando accendiamo un vecchio PC di classe XT, possiamo notare che
l'orologio/calendario viene fatto partire dalle ore 00:00:00 del
01/01/1980; tale inizializzazione viene effettuata dal DOS ed è
basata su un preciso standard seguito dal SO.
Ogni SO utilizza, infatti, una data di riferimento che prende il nome
di epoch time; una qualunque data precedente a quella di riferimento
viene considerata non valida dallo stesso SO.
La Figura 1 illustra l'epoch time di alcuni SO.

Figura 1 - Epoch time di alcuni SO


Epoch time
SO
Ora Data
DOS 00:00:00 01/01/1980
UNIX 00:00:00 01/01/1970
MacOS 00:00:00 01/01/1904

Sui PC di classe XT possiamo aggiornare l'orologio/calendario ai


valori correnti attraverso i due comandi TIME e DATE forniti dal DOS;
provando, però, a riavviare il PC, possiamo constatare che le nostre
impostazioni vengono perse e l'orologio/calendario riparte nuovamente
dalle 00:00:00 del 01/01/1980!
Ciò accade in quanto, sui PC di classe XT, l'orologio/calendario non
è un vero dispositivo hardware capace di aggiornare la data e l'ora
anche a computer spento; si tratta di una semplice simulazione
software, gestita dal DOS, che costringe l'utente ad aggiornare
manualmente le impostazioni standard ad ogni riavvio!

Questo problema è stato risolto con l'avvento dei PC di classe AT; a


tale proposito, la IBM ha scelto un dispositivo che, in origine, era
rappresentato dal famoso chip Motorola MC146818A RTC + RAM.
Attualmente vengono utilizzati anche dei chip alternativi, del tutto
compatibili con il MC146818A; si possono citare, ad esempio, il Texas
Instruments bq4285 e il Dallas Semiconductors DS12885.

4.1 Funzionamento del dispositivo RTC + RAM

Nel seguito del capitolo, con il termine RTC + RAM verrà indicato uno
dei tanti dispositivi dell'ultima generazione che sostituiscono il
MC146818A; quando sarà necessario, verranno illustrate le eventuali
differenze con lo stesso MC146818A.

La Figura 2 illustra lo schema a blocchi semplificato di un


dispositivo RTC + RAM.

Figura 2 - Dispositivo RTC + RAM

62
Agli ingressi X1 e X2 viene collegato un quarzo che "vibra" alla
frequenza di 32768 kHz; da questa vibrazione, il dispositivo
Oscillatore ricava una frequenza perfettamente stabile, che verrà
utilizzata come segnale periodico di riferimento.

Il dispositivo Divisore di frequenza, permette di dividere la


frequenza del segnale periodico di riferimento; in questo modo, si
ottengono segnali periodici a frequenze sottomultiple di quella di
riferimento, utilizzabili per gestire le funzionalità SQW, INT e
Data/Ora del dispositivo RTC + RAM.
Il dispositivo Generatore di onda quadra permette di generare segnali
ad onda quadra (sull'uscita SQW) la cui frequenza può essere scelta
dal programmatore; generalmente, l'uscita SQW rimane inutilizzata sui
PC.
Il dispositivo Generatore di interrupt permette la generazione di
interruzioni hardware sull'uscita INT; è possibile programmare le
interruzioni in modo che si verifichino in seguito ad un preciso
evento, oppure ad intervalli regolari di tempo.
Il dispositivo Aggiornamento data/ora riceve dal Divisore di
frequenza un segnale periodico a 1 Hz (1 ciclo al secondo) attraverso
il quale viene continuamente aggiornato l'orologio/calendario
interno; in sostanza, ad intervalli di 1 secondo, viene effettuato
l'aggiornamento di svariate informazioni che comprendono: ore,
minuti, secondi, giorno, mese, anno, etc.

Tutte le informazioni relative all'orologio/calendario, vengono


sistemate in una apposita memoria RAM realizzata in tecnologia CMOS;
in Figura 2, l'area riservata all'orologio/calendario è quella
colorata in verde (User Buffer) ed ha una capienza di 14 byte.
Sono presenti anche ulteriori 114 byte (Storage Registers) destinati
alla memorizzazione di informazioni relative alla configurazione
hardware del PC (area colorata in rosso in Figura 2); a tale
proposito, si veda anche il paragrafo 2.4 del Capitolo 2, sezione
Assembly Avanzato.

Complessivamente, la RAM-CMOS del dispositivo di Figura 2 ha una


capienza pari a 14+114=128 byte; nel dispositivo originario
MC146818A, invece, la RAM-CMOS aveva una capienza pari a 14+50=64
63
byte.

A computer spento, la conservazione delle informazioni descritte in


precedenza e l'aggiornamento dell'orologio/calendario, vengono
garantiti da una batteria ricaricabile che alimenta continuamente il
dispositivo RTC + RAM; tale batteria risulta collegata ai pin +3V e
Gnd di Figura 2.

Nota importante.
La batteria deve essere tenuta sempre in stato di massima
efficienza e ciò si ottiene evitando di lasciare spento il
computer per lunghissimi periodi di tempo (mesi o anni); in
caso contrario, la batteria comincia a scaricarsi sino a
provocare la perdita totale delle informazioni contenute
nella RAM-CMOS!

In una situazione del genere, al riavvio del computer ci si


può trovare davanti a dei messaggi di errore generati dal
BIOS in fase di POST; tali messaggi indicano che il BIOS non
ha potuto caricare la configurazione hardware dalla CMOS.
Un utente esperto può affrontare questa situazione accedendo
al menu di configurazione del BIOS per effettuare la
reimpostazione dell'hardware; tutte le nuove impostazioni
verrano poi salvate dal BIOS nella CMOS.
Tuttavia, finché la batteria non raggiunge un adeguato stato
di carica (possono essere necessari anche parecchi giorni), è
probabile che spegnendo il PC vengano nuovamente perse le
informazioni presenti nella CMOS; in tal caso si è costretti
a ripetere la procedura in attesa che la batteria si
ricarichi a dovere.
Se l'utente non è in grado di reimpostare l'hardware, può
chiedere al BIOS di utilizzare una configurazione
predefinita; tale configurazione, entro certi limiti,
garantisce il corretto funzionamento del computer.

Nota importante.
In alcuni rari casi, la perdita delle informazioni presenti
nel dispositivo RTC + RAM può essere sfruttata per risolvere
situazioni di emergenza; un caso del genere si verifica, ad
esempio, quando scordiamo la "password hardware" impostata
attraverso il BIOS!
Tale password viene memorizzata proprio nella CMOS e nel caso
in cui l'utente l'abbia scordata, non c'è assolutamente verso
di avviare il PC; una tale situazione può essere risolta
facendo scaricare la batteria in modo che tutte le
informazioni della CMOS vengano perse!

Molti dispositivi RTC + RAM sono appositamente dotati di un


"ponticello" togliendo il quale si provoca la disconnessione
della batteria, con conseguente perdita delle informazioni in
tempi relativamente rapidi; per maggiori dettagli su questo
delicato argomento si consiglia di consultare la
documentazione tecnica del proprio PC (spesso disponibile sul
sito del produttore).
64
4.2 Struttura dell'area User Buffer della RAM-CMOS

L'area User Buffer della RAM-CMOS ha una capienza di 14 byte e


risulta suddivisa in 14 locazioni da 1 byte ciascuna; la Figura 3
illustra le informazioni contenute in queste 14 locazioni.

Figura 3 - User Buffer


Offset Campo Blocco
00h Secondi
01h Allarme Secondi
02h Minuti
03h Allarme Minuti
04h Ore
Orologio Calendario
05h Allarme Ore
06h Giorno della Settimana
07h Giorno del Mese
08h Mese
09h Anno
0Ah Registro A
0Bh Registro B Registri di Stato
0Ch Registro C e di Controllo
0Dh Registro D

Ad intervalli di 1 secondo, il dispositivo Aggiornamento data/ora


incrementa di 1 il contenuto del campo Secondi; se il campo Secondi
raggiunge il valore 60, viene azzerato e viene incrementato di 1 il
campo Minuti.
Se il campo Minuti raggiunge il valore 60, viene azzerato e viene
incrementato di 1 il campo Ore; se il campo Ore raggiunge il valore
24, viene azzerato e vengono incrementati di 1 i campi Giorno della
Settimana e Giorno del Mese ... e così via.

Il campo Giorno della Settimana varia da 1 a 7 con il valore 1 che


indica la Domenica; il campo Giorno del Mese varia da 1 a 31 (il
dispositivo RTC + RAM gestisce automaticamente le diverse lunghezze
dei mesi e l'aggiunta di un giorno al mese di Febbraio negli anni
bisestili).
Il campo Mese varia da 1 a 12; il campo Anno varia da 00 a 99 e
indica solamente le due cifre meno significative del valore a 4 cifre
che rappresenta l'anno.

In Figura 3 possiamo notare anche la presenza dei tre campi Allarme


Secondi, Allarme Minuti e Allarme Ore; come vedremo più avanti,
questi tre campi permettono di generare un evento (allarme) non
appena si verifica la condizione:

Ore:Minuti:Secondi = Allarme Ore:Allarme Minuti:Allarme Secondi

65
Gli ultimi 4 byte dell'area User Buffer sono riservati ai cosiddetti
Control/Status Registers (registri di stato e di controllo); come si
intuisce dal nome, questi 4 registri sono molto importanti in quanto
permettono all'utente di controllare (programmare) il dispositivo RTC
+ RAM. Gli stessi registri forniscono anche informazioni complete su
tutto ciò che accade nel dispositivo; analizziamoli quindi in
dettaglio.

4.2.1 Registro A

La Figura 4 illustra la struttura assunta dal Registro A.

Figura 4 - Registro A
Bit 7 6 5 4 3 2 1 0
Contenuto UIP DV2 DV1 DV0 RS3 RS2 RS1 RS0

I bit RS0, RS1, RS2 e RS3 sono accessibili in lettura/scrittura e


permettono di impostare, sia la frequenza del segnale ad onda quadra
generato sull'uscita SQW, sia l'intervallo di tempo che intercorre
tra una interruzione e quella successiva generate sull'uscita INT
(per un tipo particolare di "interruzione periodica", illustrata più
avanti); con 4 bit possiamo gestire sino a 24=16 impostazioni
differenti le cui caratteristiche vengono illustrate in Figura 5.

Figura 5 - Frequenze SQW e intervalli INT


RS3 RS2 RS1 RS0 Freq. SQW Interv. INT
0 0 0 0 - -
0 0 0 1 256 Hz 3,90625 ms
0 0 1 0 128 Hz 7,8125 ms
0 0 1 1 8,192 kHz 122,070 µs
0 1 0 0 4,096 kHz 244,141 µs
0 1 0 1 2,048 kHz 488,281 µs
0 1 1 0 1,024 kHz 976,5625 µs
0 1 1 1 512 Hz 1,95315 ms
1 0 0 0 256 Hz 3,90625 ms
1 0 0 1 128 Hz 7,8125 ms
1 0 1 0 64 Hz 15,625 ms
1 0 1 1 32 Hz 31,25 ms
1 1 0 0 16 Hz 62,5 ms
1 1 0 1 8 Hz 125 ms
1 1 1 0 4 Hz 250 ms
1 1 1 1 2 Hz 500 ms

In fase di avvio del PC, il BIOS imposta questi 4 bit al valore


0110b; il valore 0000b disabilita l'output del Divisore di frequenza.

I bit DV0, DV1 e DV2 sono accessibili in lettura/scrittura e


66
permettono di abilitare/disabilitare l'Oscillatore e il Divisore di
frequenza; in fase di avvio del PC, il BIOS imposta questi 3 bit al
valore 010b con la conseguenza che l'Oscillatore risulta abilitato e
l'orologio/calendario viene aggiornato alla corretta frequenza di 1
Hz.
Tutti gli altri valori provocano risultati anomali o, in generale,
differenti da quelli predefiniti (ad esempio, l'orologio/calendario
non viene aggiornato, oppure viene aggiornato ad una velocità
sbagliata); si raccomanda quindi di non modificare l'impostazione
predefinita 010b per i bit DV0, DV1 e DV2!

Il bit UIP (Update cycle In Progress) è accessibile in sola lettura e


indica se è in atto o meno la fase di aggiornamento
dell'orologio/calendario; se UIP=1, l'orologio/calendario è in fase
di aggiornamento, mentre se UIP=0, tale fase è terminata.
Se vogliamo accedere in lettura/scrittura all'orologio/calendario,
dobbiamo prima attendere che si abbia UIP=0; in caso contrario,
potremmo anche ottenere risultati errati!

4.2.2 Registro B

La Figura 6 illustra la struttura assunta dal Registro B.

Figura 6 - Registro B
Bit 7 6 5 4 3 2 1 0
Contenuto UTI PIE AIE UIE SQWE DF HF DSE

Il bit DSE (Daylight Saving Enable) permette di abilitare (1) o


disabilitare (0) il passaggio automatico dall'ora solare all'ora
legale e viceversa; se DSE viene posto a 1, durante l'anno si
verificano i seguenti due eventi:

* la prima Domenica di Aprile, durante il ciclo di aggiornamento


successivo alle ore 01:59:59, l'orologio passa automaticamente alle
ore 03:00:00 (incremento di un'ora);
* l'ultima Domenica di Ottobre, durante il ciclo di aggiornamento
successivo alle ore 01:59:59, l'orologio passa automaticamente alle
ore 01:00:00 (decremento di un'ora).

Le convenzioni relative all'ora solare/legale non sono uguali per


tutti i Paesi; purtroppo, però, le impostazioni del dispositivo RTC +
RAM non sono modificabili. Proprio per questo motivo, la gestione del
cambiamento dell'ora solare/legale viene lasciata al SO.

L'impostazione predefinita è DSE=0 (nessun cambio automatico


dell'ora).

Il bit HF (Hour Format) permette di impostare l'orologio di 12 ore o


di 24 ore; se HF=0, il campo Ore varia da 0 a 11 (AM e PM), mentre se
HF=1, il campo Ore varia da 0 a 23.

L'impostazione predefinita è HF=1 (orologio di 24 ore).

67
Il bit DF (Data Format) permette di impostare il formato numerico
usato per la rappresentazione della data e dell'ora; se DF=1,
l'orologio/calendario viene rappresentato con numeri in formato
binario (ad esempio, le 05:28:49 del 12/07/01), mentre se DF=0,
l'orologio/calendario viene rappresentato con numeri in formato BCD
(ad esempio, le 05h:28h:49h del 12h/07h/01h).

L'impostazione predefinita è DF=0 (numeri in formato BCD).

Il bit SQWE (SQuare Wave Enable) permette di abilitare (1) o


disabilitare (0) l'uscita SQW del Generatore di onda quadra; come
abbiamo visto in precedenza, la frequenza del segnale ad onda quadra
può essere impostata attraverso i bit RS0, RS1, RS2 e RS3 del
Registro A.
La Figura 7 illustra la forma di un tipico segnale ad onda quadra.

Figura 7 - Segnale ad onda quadra

L'impostazione predefinita è SQWE=0 (uscita SQW disabilitata).

Il bit UIE (Update cycle Interrupt Enable) permette di abilitare (1)


o disabilitare (0) la generazione di un impulso INT al termine di
ogni ciclo di aggiornamento dell'orologio/calendario; ricordando che
la frequenza di aggiornamento dell'orologio/calendario è pari a 1 Hz,
possiamo affermare che ponendo UIE=1 otteniamo la generazione di un
impulso INT ogni secondo.
Il bit UIE si rivela molto utile per sapere quando possiamo accedere
in lettura/scrittura all'orologio/calendario senza trovarci nel bel
mezzo di un ciclo di aggiornamento; a tale proposito, dobbiamo porre
UIE=1 e installare una nostra ISR che intercetta l'impulso INT.
Quando la ISR riceve il controllo, siamo sicuri che la fase di
aggiornamento dell'orologio/calendario è appena terminata; da quel
momento, abbiamo a disposizione al massimo 999 ms per accedere in
sicurezza alle informazioni relative all'orologio/calendario.

L'impostazione predefinita è UIE=0 (nessun impulso INT al termine del


ciclo di aggiornamento dell'orologio/calendario).

Il bit AIE (Alarm Interrupt Enable) permette di abilitare (1) o


disabilitare (0) la generazione di un impulso INT nel momento esatto
in cui si verifica la condizione:

Ore:Minuti:Secondi = Allarme Ore:Allarme Minuti:Allarme Secondi

Il bit AIE ci permette quindi di svolgere un determinato compito allo


scoccare di una precisa ora del giorno; a tale proposito, dobbiamo
porre AIE=1, impostare i campi Allarme Ore, Allarme Minuti, Allarme
Secondi e installare una nostra ISR che intercetta l'impulso INT.
68
L'impostazione predefinita è AIE=0 (nessun impulso INT al verificarsi
della condizione di allarme).

Il bit PIE (Periodic Interrupt Enable) permette di abilitare (1) o


disabilitare (0) la generazione di un impulso INT ad intervalli
regolari di tempo (da cui il nome "periodic interrupt"); se PIE=1,
viene generata una sequenza continua di impulsi INT ad intervalli di
tempo che possiamo impostare attraverso i bit RS0, RS1, RS2, RS3 del
Registro A.

L'impostazione predefinita è PIE=0 (nessun impulso INT periodico).

Il bit UTI (Update Transfer Inhibit) permette di abilitare (0) o


disabilitare (1) la memorizzazione nella CMOS degli aggiornamenti
relativi all'orologio/calendario; in sostanza, se poniamo UTI=1,
l'orologio/calendario viene regolarmente aggiornato una volta al
secondo, ma tali aggiornamenti non vengono memorizzati nell'area User
Buffer della CMOS.
Ponendo UTI=1 (aggiornamento inibito), si ottiene automaticamente
UIE=0 (nessun impulso INT automatico al termine del ciclo di
aggiornamento).

L'impostazione predefinita è UTI=0 (aggiornamenti salvati nella


CMOS).

4.2.3 Registro C

La Figura 8 illustra la struttura assunta dal Registro C.

Figura 8 - Registro C
Bit 7 6 5 4 3 2 1 0
Contenuto INTF PF AF UF 0 0 0 0

I 4 bit meno significativi del Registro C sono riservati e devono


valere 0.

Il bit UF (Update event Flag) è accessibile in sola lettura e


permette di sapere se il ciclo di aggiornamento
dell'orologio/calendario è terminato; infatti, questo bit vale
normalmente 0 e viene posto automaticamente a 1 al termine del ciclo
di aggiornamento.
UF fornisce quindi un metodo alternativo (a UIE) per sapere quando
possiamo accedere in sicurezza alle informazioni relative
all'orologio/calendario; a tale proposito, non dobbiamo fare altro
che monitorare continuamente il valore assunto dallo stesso UF.
La modifica di UF è automatica ed è indipendente dal valore assunto
dal bit UIE (Update cycle Interrupt Enable); inoltre, una operazione
di lettura del Registro C provoca l'azzeramento del bit UF.

Il bit AF (Alarm event Flag) è accessibile in sola lettura e permette


di sapere se si è verificata la condizione:

69
Ore:Minuti:Secondi = Allarme Ore:Allarme Minuti:Allarme Secondi

Infatti, questo bit vale normalmente 0 e viene posto automaticamente


a 1 quando si verifica la suddetta condizione.
La modifica di AF è automatica ed è indipendente dal valore assunto
dal bit AIE (Alarm Interrupt Enable); inoltre, una operazione di
lettura del Registro C provoca l'azzeramento del bit AF.

Il bit PF (Periodic event Flag) è accessibile in sola lettura e


permette di sapere se è trascorso l'intervallo di tempo necessario
per la generazione di una interruzione periodica (in base alle
impostazioni illustrate in Figura 5); infatti, questo bit vale
normalmente 0 e viene posto automaticamente a 1 ogni volta che
trascorre il suddetto intervallo di tempo.
La modifica di PF è automatica ed è indipendente dal valore assunto
dal bit PIE (Periodic Interrupt Enable); inoltre, una operazione di
lettura del Registro C provoca l'azzeramento del bit PF.

In base a quanto è stato appena esposto, i tre bit UF, AF e PF


vengono modificati automaticamente senza nessuna relazione con lo
stato dei tre bit UIE, AIE e PIE; se, invece, siamo interessati
proprio a tale relazione, possiamo servirci del bit INTF.
Il bit INTF (INTerrupt request Flag) è accessibile in sola lettura e
viene posto automaticamente a 1 solamente quando si verifica una
delle seguenti condizioni:

(UIE = 1) AND (UF = 1)


(AIE = 1) AND (AF = 1)
(PIE = 1) AND (PF = 1)

Supponiamo, ad esempio, di aver posto UIE=1 (Update cycle Interrupt


Enable); in questo modo, stiamo richiedendo la generazione di un
impulso INT ogni volta che termina il ciclo di aggiornamento
dell'orologio/calendario.
Non appena tale ciclo termina, si ottiene automaticamente UF=1; solo
in questo caso si ha anche INTF=1. Se, invece, avessimo posto UIE=0,
al termine del ciclo di aggiornamento avremmo ottenuto UF=1 e INTF=0!
La modifica di INTF è automatica; inoltre, una operazione di lettura
del Registro C provoca l'azzeramento del bit INTF.

4.2.4 Registro D

La Figura 9 illustra la struttura assunta dal Registro D.

Figura 9 - Registro D
Bit 7 6 5 4 3 2 1 0
Contenuto VRT 0 0 0 0 0 0 0

I 7 bit meno significativi del Registro D sono riservati e devono


valere 0.

Il bit VRT (Valid Ram and Time) è accessibile in sola lettura e


permette di sapere se la batteria di alimentazione ha una carica
70
sufficiente per garantire la correttezza delle informazioni presenti
nel dispositivo RTC + RAM; solamente quando VRT=1 (batteria
sufficientemente carica) tali informazioni possono essere considerate
attendibili.

4.2.5 Gestione dei diversi tipi di interruzione

Dalle considerazioni appena esposte risulta che il dispositivo RTC +


RAM è in grado di generare tre tipi differenti di interruzione e
cioè:

Update Cycle Interrupt (al termine di ogni ciclo di aggiornamento


dell'orologio/calendario);
Alarm Interrupt (quando si verifica la condizione di allarme);
Periodic Interrupt (ogni volta che trascorre un intervallo di tempo
prestabilito).

Per gestire ciascun tipo di interrupt abbiamo a disposizione due


metodi; il primo metodo è quello classico della ISR, mentre il
secondo consiste nel "polling" del Registro C.

Il metodo della ISR è più impegnativo ma garantisce la massima


efficienza possibile; per sfruttare tale metodo dobbiamo installare
una nostra ISR in memoria, effettuare eventuali impostazioni sul
dispositivo RTC + RAM e abilitare l'opportuno bit (UIE, AIE o PIE)
nel Registro B.
Supponiamo, ad esempio, di voler richiedere la generazione di una
interruzione periodica ogni 125 ms; a tale proposito, dopo aver
installato in memoria la nostra ISR dobbiamo impostare il valore
1101b nei bit RS del Registro A e porre infine PIE=1 nel Registro B.

La ISR, ogni volta che riceve il controllo, deve svolgere un compito


importantissimo che consiste nell'accedere in lettura al Registro C;
come è stato spiegato in precedenza, tale operazione permette di
azzerare tutti i bit del Registro C. Se non si svolge questo lavoro,
le interruzioni rimangono disabilitate!

Il metodo del polling è più facile da applicare in quanto richiede un


semplice sondaggio continuo sullo stato dell'opportuno bit (UF, AF o
PF) del Registro C; l'unico difetto di questo metodo è rappresentato
dalla scarsa efficienza dovuta alle continue operazioni di lettura
del Registro C che impegnano pesantemente la CPU.

4.3 Struttura dell'area Storage Registers della RAM-CMOS

I 114 byte della CMOS, successivi ai 14 byte dell'area User Buffer,


sono riservati alla memorizzazione di numerose informazioni relative
alla configurazione hardware del PC; tali informazioni sono di
competenza esclusiva del BIOS e non devono essere assolutamente
modificate dal programmatore attraverso l'accesso diretto in
scrittura alla CMOS stessa!

Si tenga anche presente che l'ordine con il quale risultano


memorizzate le varie informazioni nella CMOS può differire in base
71
alla marca del BIOS e/o al modello di PC; la Figura 10 illustra
alcuni campi rilevanti che si possono trovare nella CMOS di un tipico
PC "IBM compatibile" (ovviamente, gli offset partono da 0Eh in quanto
sono successivi all'area User Buffer).

Figura 10 - Storage Registers


Offset Campo
0Eh IBM PS/2 - Diagnostic status byte
0Fh IBM - Reset code
10h IBM - Floppy disk type
11h IBM PS/2 - First fixed disk type
12h IBM - Hard disk data
14h IBM - Equipment byte
15h IBM - Base memory in Kb (low byte)
16h IBM - Base memory in Kb (high byte)
17h IBM - Extended memory in Kb (low byte)
18h IBM - Extended memory in Kb (high byte)
19h IBM - First extended hard disk drive type
2Eh IBM - Standard CMOS checksum (high byte)
2Fh IBM - Standard CMOS checksum (low byte)
30h IBM - Extended memory in Kb (low byte)
31h IBM - Extended memory in Kb (high byte)
32h IBM - Century byte (formato BCD)
33h IBM - Information flag

Se si ha la necessità di accedere in lettura a queste informazioni,


si può ricorrere ad un metodo sicuro che consiste nel servirsi dei
numerosi servizi offerti dal BIOS; a tale proposito, si consiglia di
consultare il manuale utente del proprio BIOS.

Osservando la Figura 10 possiamo notare, all'offset 32h della CMOS,


la presenza di un "curioso" byte denominato Century (secolo); tale
byte indica il secolo corrente e viene unito al campo Anno di Figura
3 per ottenere la rappresentazione a 4 cifre dell'anno corrente.

Ovviamente, nasce subito una domanda: cosa ci fa il campo Century in


quella strana posizione?

La risposta a questa domanda non può essere riassunta in poche parole


in quanto comporta implicazioni di vario genere; come vedremo più
avanti, una di queste implicazioni chiama addirittura in causa il
famoso (o fantomatico) millennium bug!

Per chiarire la situazione bisogna partire dal fatto che, all'avvio


del PC, il BIOS sfrutta il dispositivo RTC + RAM per inizializzare un
proprio sistema autonomo di gestione della data e dell'ora; anche i
SO utilizzano un proprio sistema autonomo di gestione della data e
dell'ora, che viene inizializzato attraverso la lettura delle
informazioni fornite dal BIOS o dallo stesso dispositivo RTC + RAM.
72
Il perché di questa situazione è legato a motivi prestazionali; se il
BIOS e il SO dovessero aggiornare continuamente il loro
orologio/calendario attraverso la lettura del dispositivo RTC + RAM,
si andrebbe incontro ad un serio rallentamento generale del sistema!
Il problema viene allora risolto facendo in modo che il dispositivo
RTC + RAM, il BIOS e il SO aggiornino separatamente e autonomamente i
rispettivi orologi/calendari; ciò giustifica il fatto che si possa
riscontrare una leggera divergenza tra i vari orologi (soprattutto
per quando riguarda il campo Secondi)!

Come sappiamo, il dispositivo originario RTC + RAM usato per


equipaggiare i PC dei primi anni 80 fu il Motorola MC146818A; stiamo
parlando quindi di vicende relative al XIX secolo.
Per gestire l'anno corrente i progettisti della Motorola misero a
disposizione il campo Anno; come è stato già spiegato, tale campo è
destinato a contenere solamente le 2 cifre meno significative
dell'anno corrente.
Nelle intenzioni di quei progettisti, i SO dell'epoca avrebbero
potuto ricavare in modo semplicissimo la rappresentazione a 4 cifre
dell'anno corrente; a tale proposito, non dovevano fare altro che
leggere le 2 cifre (ad esempio, 87) del campo Anno e metterci un 19
davanti in modo da ottenere 1987.
Inoltre, nessuno riteneva, all'epoca, che quei PC potessero
raggiungere e superare la soglia del XX secolo; si pensava cioè che
nel frattempo tali PC sarebbero stati sostituiti da hardware più
evoluto, capace di affrontare in modo adeguato anche eventuali
problemi legati all'orologio/calendario.
In ogni caso, la IBM pensò di cautelarsi sistemando il byte Century
nella CMOS; non essendoci spazio disponibile nell'area User Buffer,
venne scelto arbitrariamente l'offset 32h. In tale posizione venne
messo il valore 19h (secolo in formato BCD) in attesa di "ulteriori
sviluppi".

Contrariamente alle previsioni, una enorme quantità di vecchi


computer, dotati di dispositivo RTC + RAM, ha raggiunto e superato la
soglia del XX secolo; ciò ha determinato un problema generalmente
conosciuto con il nome di millennium bug!

4.3.1 Il millennium bug

Cosa succede quando un generico orologio/calendario supera le ore


23:59:59 del 31/12/1999?

Al successivo ciclo di aggiornamento dovrebbero scoccare le ore


00:00:00 del 01/01/2000; con i vecchi dispositivi RTC + RAM le cose
non vanno però così!

Alle ore 23:59:59 del 31/12/1999, il campo Anno contiene il valore


99; al successivo ciclo di aggiornamento, si passa alle ore 00:00:00
del 01/01/2000, con il campo Anno che diventa quindi 00.
Il campo Century non fa parte dell'orologio/calendario e quindi non
subisce nessun aggiornamento automatico (da 19 a 20); si tratta,
infatti, di una semplice locazione da 1 byte inserita arbitrariamente
dalla IBM all'offset 32h della CMOS.
73
A questo punto, tutto passa nelle mani del SO; più precisamente,
tutto dipende dalla consapevolezza che il SO ha del problema appena
illustrato.

I vecchi SO, come il DOS, per inizializzare il proprio sistema di


gestione della data e dell'ora, leggono le 2 cifre del campo Anno e
continuano a metterci davanti un 19; nel caso dell'anno 2000, quindi,
mettendo un 19 davanti a 00 si ottiene 1900!
Il DOS non accetta questa data e la adegua al valore minimo possibile
(legato all'epoch time) pari a 1980; come se non bastasse, lo stesso
DOS non si preoccupa di salvare le nuove impostazioni nel dispositivo
RTC + RAM il quale continua quindi ad indicare Anno=00h e
Century=19h!
La conseguenza è che al successivo riavvio del PC si verifica
nuovamente il problema appena illustrato; tutto ciò va avanti (se non
si provvede ad aggiornare il dispositivo RTC + RAM) per 80 anni,
finché cioè il campo Anno non raggiunge il valore 80!

Purtroppo, esistono anche particolari BIOS o SO che si rifiutano di


avviarsi in presenza di una data non valida; questa situazione
diventa particolarmente pericolosa nel caso di computer destinati a
gestire situazioni molto delicate (ad esempio, apparati militari,
strumenti elettromedicali, etc).

Diversi dispositivi RTC + RAM successivi al MC146818A (ad esempio, il


Dallas Semiconductors DS12885) affrontano il problema della data
provvedendo ad aggiornare automaticamente anche il campo Century; in
sostanza, il campo Anno viene azzerato ogni volta che supera il
valore 99 e viene incrementato di 1 il campo Century.
Naturalmente, è fondamentale che il SO venga progettato in modo da
ricavare la rappresentazione a 4 cifre dell'anno corrente attraverso
i campi Century e Anno del dispositivo RTC + RAM; i SO dell'ultima
generazione seguono proprio questa strada.

Nota importante.
Tutta la storia dei computer è costellata da un numero
impressionante di problemi legati alla gestione delle date;
molti di tali problemi sono derivati persino da erronee
interpretazioni delle differenti convenzioni sul calcolo del
tempo!

Per citare un esempio pratico, tutti i sistemi UNIX (che sono


scritti in C) utilizzano un tipo di dato time_t definito come
signed long int; in base alle convenzioni C, sulle
architetture a 32 bit un signed long int ha una ampiezza di
32 bit e ci permette quindi di rappresentare tutti i numeri
interi con segno compresi tra -2147483648 e +2147483647.
Il tipo time_t viene impiegato anche per contenere il numero
di secondi trascorsi a partire dall'epoch time che, per i
sistemi UNIX, è rappresentato dalle ore 00:00:00 del
01/01/1970 (tempo 0); il problema che si presenta è dato dal
fatto che alle ore 03:14:07 del 19/01/2038, il conteggio
raggiunge il valore +2147483647. Al successivo ciclo di
aggiornamento il contatore va quindi in overflow e passa al
74
valore negativo -2147483648!
Un problema di questo genere può essere risolto passando ad
un computer dotato di architettura a 64 bit; su tali
architetture, infatti, in base alle convenzioni C il tipo
signed long int assume un'ampiezza pari a 64 bit.

Un altro esempio è rappresentato dal dispositivo RTC + RAM


che è atteso da un nuovo bug alle ore 23:59:59 del
31/12/9999; infatti, durante il successivo ciclo di
aggiornamento si verifica l'overflow del campo Century con la
necessità di passare agli anni a 5 cifre!
Molto probabilmente, però, entro tale data sarà già stata
trovata una soluzione.
Chi vivrà vedrà!

Per maggiori dettagli sui problemi legati alla gestione delle


date sul computer, si possono consultare i numerosi documenti
disponibili in Rete; un'ottima fonte di consultazione può
essere anche Wikipedia.

4.4 Programmazione del dispositivo RTC + RAM

La programmazione del dispositivo RTC + RAM avviene in modo


semplicissimo; infatti, ciascuna operazione di I/O richiede le
seguenti due fasi:

1) indicazione dell'offset a cui si vuole accedere;


2) accesso in I/O all'offset specificato nella fase 1.

Per svolgere queste due fasi sono disponibili due apposite porte; la
Figura 11 illustra tutti i dettagli.

Figura 11 - Porte hardware del dispositivo RTC + RAM


Indirizzo Funzione
70h Address port
71h Data port

In sostanza, attraverso la porta 70h specifichiamo a quale offset


vogliamo accedere (vedere Figura 3 e Figura 10); attraverso poi la
porta 71h effettuiamo l'accesso, in lettura o in scrittura,
all'offset specificato in precedenza.

Naturalmente, il programmatore deve prestare molta attenzione al


fatto che, come abbiamo già visto, alcune locazioni sono accessibili
in sola lettura; inoltre, è necessario ribadire che è pericolosissimo
modificare le informazioni presenti nell'area Storage Registers della
CMOS!

4.5 Un esempio pratico: orologio/calendario + allarme

75
Il dispositivo RTC + RAM può essere interamente programmato
attraverso i servizi offerti dalla INT 1Ah del BIOS; a tale
proposito, si consiglia di consultare il manuale utente del proprio
BIOS.

Nel nostro caso, anziché ricorrere al BIOS, seguiamo la strada


dell'accesso diretto al dispositivo RTC + RAM scrivendo un programma
che mostra un orologio/calendario completo di ore, minuti, secondi,
giorno della settimana, giorno del mese, mese e anno; inoltre,
impostiamo un allarme da attivare attraverso l'alarm interrupt.
Nella Figura 11 del Capitolo 3 possiamo notare che il dispositivo RTC
+ RAM invia le proprie IRQ alla linea IR0 del PIC Slave; si tratta
quindi di una IRQ8 a cui il PIC associa una INT 70h. Il nostro
programma deve quindi installare una propria ISR destinata ad
intercettare la INT 70h; la Figura 12 mostra il listato Assembly.

Figura 12 - File RTCALARM.ASM


RTCALARM.ASM
;----------------------------------------------------;
; file rtcalarm.asm ;
; copyright (C) Ra.M. Software ;
; orologio/calendario + allarme ;
;----------------------------------------------------;
; nasm -f obj rtcalarm.asm ;
; tlink /t rtcalarm.obj + comlib.obj ;
; (oppure link /map /tiny rtcalarm.obj + comlib.obj) ;
;----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "comlib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign CMOS_ADDR 0070h ; address port CMOS


%assign CMOS_IO 0071h ; I/O port CMOS
%assign CMOS_AIEON 00100000b ; mask-on bit AIE (alarm INT)
%assign CMOS_AIEOFF 11011111b ; mask-off bit AIE (alarm INT)
%assign CMOS_UIP 10000000b ; posizione bit UIP Registro A

%assign INT70h 70h ; INT 70h - RTC ISR


%assign MPICP0 0020h ; porta P0 PIC Master
%assign SPICP0 00A0h ; porta P0 PIC Slave
%assign SPICP1 00A1h ; porta P1 PIC Slave
%assign IRQ8_MASK 00000001b ; mask-on per la IRQ8
%assign IRQ8_UNMASK 11111110b ; mask-off per la IRQ8

%assign ALL_ORE 19h ; allarme ore


%assign ALL_MIN 50h ; allarme minuti
%assign ALL_SEC 00h ; allarme secondi

;################ segmento unico ##################

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

resb 0100h ; libera 256 byte per il PSP

..start: ; entry point


76
;------- inizio blocco principale istruzioni ------

call hideCursor ; nasconde il cursore


call clearScreen ; pulisce lo schermo

mov di, str_title ; DI punta a str_title


mov dx, 0010h ; riga 0, colonna 16
call writeString ; visualizza la stringa

mov di, str_clock ; DI punta a str_clock


mov dx, 0400h ; riga 4, colonna 0
call writeString ; visualizza la stringa

; installazione nuova ISR per INT 70h, IRQ8 (PIC Slave)

in al, SPICP1 ; AL = IMR PIC Slave


or al, IRQ8_MASK ; pone a 1 il bit 0 (mask)
out SPICP1, al ; scrive OCW1 nel PIC Slave

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [es:(INT70h * 4)] ; legge il vecchio vettore 70h
mov [old_int70h], eax ; e lo salva in old_int70h

mov ax, cs ; AX = Seg(new_int70h)


shl eax, 16 ; sposta nella WORD alta di EAX
mov ax, new_int70h ; AX = Offset(new_int70h)
mov [es:(INT70h * 4)], eax ; installa il nuovo vettore 70h

in al, SPICP1 ; AL = IMR PIC Slave


and al, IRQ8_UNMASK ; pone a 0 il bit 0 (unmask)
out SPICP1, al ; scrive OCW1 nel PIC Slave

; impostazione allarme

call wait_uip0 ; attesa per UIP = 0

mov al, 05h ; AL = offset allarme ore


out CMOS_ADDR, al ; seleziona l'offset
mov al, ALL_ORE ; AL = allarme ore
out CMOS_IO, al ; scrive nella CMOS

mov al, 03h ; AL = offset allarme minuti


out CMOS_ADDR, al ; seleziona l'offset
mov al, ALL_MIN ; AL = allarme minuti
out CMOS_IO, al ; scrive nella CMOS

mov al, 01h ; AL = offset allarme secondi


out CMOS_ADDR, al ; seleziona l'offset
mov al, ALL_SEC ; AL = allarme secondi
out CMOS_IO, al ; scrive nella CMOS

; attivazione bit AIE (Registro B)

mov al, 0Bh ; AL = offset Registro B


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il Registro B dalla CMOS

mov bl, al ; copia AL in BL


or bl, CMOS_AIEON ; pone a 1 il bit 5 di BL

77
mov al, 0Bh ; AL = offset Registro B
out CMOS_ADDR, al ; seleziona l'offset
mov al, bl ; copia BL in AL
out CMOS_IO, al ; scrive nella CMOS (AIE = 1)

; loop principale del programma (orologio/calendario)

rtc_loop:

call wait_uip0 ; attesa per UIP = 0

mov al, 04h ; AL = offset ore


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge le ore dalla CMOS

mov dx, 0400h ; riga 4, colonna 0


call writeHex8 ; visualizza le ore

mov al, 02h ; AL = offset minuti


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge i minuti dalla CMOS

mov dx, 0404h ; riga 4, colonna 4


call writeHex8 ; visualizza i minuti

mov al, 00h ; AL = offset secondi


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge i secondi dalla CMOS

mov dx, 0408h ; riga 4, colonna 8


call writeHex8 ; visualizza i secondi

mov al, 06h ; AL = offset giorno della settimana


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il giorno della sett. dalla CMOS

xor bh, bh ; BH = 0
mov bl, al ; copia AL in BL
shl bx, 1 ; BX = BX * 2
mov di, [vpt_giorni + bx] ; DI punta al nome del giorno
mov dx, 040Eh ; riga 4, colonna 14
call writeString ; visualizza il nome del giorno

mov al, 07h ; AL = offset giorno del mese


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il giorno del mese dalla CMOS

mov dx, 0418h ; riga 4, colonna 24


call writeHex8 ; visualizza il giorno del mese

mov al, 08h ; AL = offset mese


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il mese dalla CMOS

xor bh, bh ; BH = 0
mov bl, al ; copia AL in BL
shl bx, 1 ; BX = BX * 2
mov di, [vpt_mesi + bx] ; DI punta al nome del mese
mov dx, 041Ch ; riga 4, colonna 28
call writeString ; visualizza il nome del mese

mov al, 32h ; AL = offset century


78
out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il century dalla CMOS
shl ax, 8 ; e lo sposta nel BYTE alto di AX

mov al, 09h ; AL = offset anno


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge l'anno dalla CMOS

mov dx, 0426h ; riga 4, colonna 38


call writeHex16 ; visualizza l'anno

mov ax, [alarm_flag] ; AX = alarm_flag


test ax, ax ; alarm_flag == 1 ?
jz rtc_loop ; controllo loop

; ripristino vecchia ISR per INT 70h, IRQ8 (PIC Slave)

in al, SPICP1 ; AL = IMR PIC Slave


or al, IRQ8_MASK ; pone a 1 il bit 0 (mask)
out SPICP1, al ; scrive OCW1 nel PIC Slave

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [old_int70h] ; EAX = indirizzo vecchio vettore 70h
mov [es:(INT70h * 4)], eax ; ripristina il vecchio vettore 70h

in al, SPICP1 ; AL = IMR PIC Slave


and al, IRQ8_UNMASK ; pone a 0 il bit 0 (unmask)
out SPICP1, al ; scrive OCW1 nel PIC Slave

call showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;----- inizio definizione variabili statiche ------

align 4, db 0 ; allinea alla DWORD

old_int70h dd 0 ; indirizzo vecchia ISR per INT 70h


alarm_flag dw 0 ; segnalazione allarme

str_title db "REAL TIME CLOCK - OROLOGIO/CALENDARIO + ALLARME", 0


str_clock db "XXH:XXH:XXH -", 0
str_alarm db "IRQ8 - INT 70h - Alarm INT", 0

align 4, db 0 ; allinea alla DWORD

; stringa dei giorni della settimana

str_giorni db "Domenica ", 0, "Lunedi ", 0, "Martedi ", 0


db "Mercoledi", 0, "Giovedi ", 0, "Venerdi ", 0
db "Sabato ", 0

align 4, db 0 ; allinea alla DWORD

; stringa dei mesi

str_mesi db "Gennaio ", 0, "Febbraio ", 0, "Marzo ", 0


79
db "Aprile ", 0, "Maggio ", 0, "Giugno ", 0
db "Luglio ", 0, "Agosto ", 0, "Settembre", 0
db "Ottobre ", 0, "Novembre ", 0, "Dicembre ", 0

align 4, db 0 ; allinea alla DWORD

; vettore di puntatori a str_giorni

vpt_giorni dw 0, str_giorni, str_giorni + 10, str_giorni + 20


dw str_giorni + 30, str_giorni + 40, str_giorni + 50
dw str_giorni + 60

; vettore di puntatori a str_mesi

vpt_mesi dw 0, str_mesi, str_mesi + 10, str_mesi + 20, str_mesi + 30


dw str_mesi + 40, str_mesi + 50, str_mesi + 60, str_mesi + 70
dw str_mesi + 80, str_mesi + 90, str_mesi + 100, str_mesi + 110

;------- fine definizione variabili statiche ------

;---------- inizio definizione procedure ----------

align 4, db 0 ; allinea alla DWORD

; new_int70h: nuova ISR per la INT 70h


; una ISR deve sempre preservare tutti i registri che utilizza!

new_int70h:

push ax ; preserva AX
push bx ; preserva BX
push dx ; preserva DX
push di ; preserva DI

mov word [alarm_flag], 1 ; segnala che l'allarme è scattato

mov di, str_alarm ; DI punta a str_alarm


mov dx, 0800h ; riga 8, colonna 0
call writeString ; visualizza la stringa

; disattivazione bit AIE (Registro B)

mov al, 0Bh ; AL = offset Registro B


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il Registro B dalla CMOS

mov bl, al ; copia AL in BL


and bl, CMOS_AIEOFF ; pone a 0 il bit 5 di BL

mov al, 0Bh ; AL = offset Registro B


out CMOS_ADDR, al ; seleziona l'offset
mov al, bl ; copia BL in AL
out CMOS_IO, al ; scrive nella CMOS (AIE = 0)

; lettura Registro C

mov al, 0Ch ; AL = offset Registro C


out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il Registro C dalla CMOS

; invio EOI al PIC Slave e al PIC Master

80
mov al, 20h ; OCW3 = specific EOI
out SPICP0, al ; scrive OCW3 nel PIC Slave
out MPICP0, al ; scrive OCW3 nel PIC Master

pop di ; ripristina DI
pop dx ; ripristina DX
pop bx ; ripristina BX
pop ax ; ripristina AX
iret ; return from interrupt

align 4, db 0 ; allinea alla DWORD

; wait_uip0: attende che UIP = 0

wait_uip0:

uip_loop:
mov al, 0Ah ; AL = offset Registro A
out CMOS_ADDR, al ; seleziona l'offset
in al, CMOS_IO ; legge il Registro A dalla CMOS

test al, CMOS_UIP ; UIP == 0 ?


jnz uip_loop ; controllo loop

ret ; NEAR return

;----------- fine definizione procedure -----------

;##################################################
Il primo compito svolto dal programma consiste nell'installazione
della nuova ISR per la INT 70h; il procedimento da seguire è già
stato abbondantemente descritto nel precedente capitolo.

Dopo l'installazione della ISR, procediamo con l'impostazione dei


campi Allarme Ore, Allarme Minuti e Allarme Secondi; a tale
proposito, il programmatore deve servirsi delle costanti simboliche
ALL_ORE, ALL_MIN e ALL_SEC (ovviamente, in fase di "collaudo" del
programma è consigliabile impostare un'ora di allarme molto vicina
all'ora corrente in modo da non dover attendere per troppo tempo).
Si noti che prima di inizializzare i campi relativi all'allarme,
viene chiamata la procedura wait_uip0 per attendere che il bit UIP
(Update cycle In Progress) si sia portato a zero.

Una volta che l'ora di allarme è stata impostata, dobbiamo attivare


il bit AIE (Alarm Interrupt Enable) del Registro B; questo passo è
necessario in quanto abbiamo deciso di utilizzare il classico metodo
della ISR.

A questo punto parte il loop principale del programma; al suo interno


sono presenti le istruzioni necessarie per mostrare tutte le
informazioni relative all'orologio/calendario.
All'inizio di ogni loop attendiamo, come al solito, che il bit UIP si
sia portato a zero; è importante ribadire che se non si segue questo
procedimento, c'è il serio rischio di ottenere risultati privi di
senso (a titolo di curiosità, si può provare a commentare la chiamata
a wait_uip0 per vedere quello che succede)!

Mentre il loop è in esecuzione, si verifica la condizione di allarme;


81
a quel punto, il dispositivo RTC + RAM genera un Alarm Interrupt che
raggiunge il PIC Slave. Lo stesso PIC Slave informa la CPU che
bisogna chiamare la INT 70h.
Tale chiamata viene intercettata dalla nostra ISR la quale provvede
ad assegnare il valore 1 alla variabile alarm_flag; si tratta della
condizione che provoca la terminazione del loop principale.
La ISR svolge anche un compito importante che consiste nell'accesso
in lettura al Registro C; come sappiamo, tale accesso è necessario
per ripristinare lo stato dei vari flags presenti nello stesso
registro.
Prima di terminare, la ISR invia il segnale EOI; come abbiamo visto
nel precedente capitolo, per le IRQ che arrivano al PIC Slave bisogna
mandare un EOI allo stesso PIC Slave e un EOI al PIC Master!

Una volta che il loop principale del programma è terminato, si


procede al ripristino della vecchia ISR relativa alla INT 70h; in
questo modo, il nostro programma termina riportando lo stato del
computer alla situazione originaria.

4.5.1 Vettori di puntatori

L'esempio di Figura 12 ci offre l'opportunità di analizzare il caso


dei "vettori di puntatori"; si tratta comunque di un argomento già
affrontato in dettaglio nella sezione Assembly Base.

Un vettore di puntatori o "vettore di indirizzi" è un vettore i cui


elementi sono tutti degli indirizzi; ogni elemento del vettore
contiene quindi l'indirizzo di una qualche informazione in memoria.
Consideriamo, ad esempio, la stringa str_giorni; questa stringa
contiene una sequenza di stringhe C, consecutive e contigue. A sua
volta, ciascuna stringa C contiene il nome di uno dei giorni della
settimana; come si vede allora nel listato di Figura 12, possiamo
affermare che:

str_giorni+00 è l'offset da cui inizia in memoria la stringa


"Domenica ", 0
str_giorni+10 è l'offset da cui inizia in memoria la stringa
"Lunedi ", 0
str_giorni+20 è l'offset da cui inizia in memoria la stringa
"Martedi ", 0

A questo punto, definiamo il vettore vpt_giorni il cui scopo è


contenere gli indirizzi (offset) di ciascuna delle precedenti
stringhe C; si noti che il primo elemento di vpt_giorni è 0 e non
viene utilizzato in quanto, nel dispositivo RTC + RAM, i giorni della
settimana partono dall'indice 1.
In sostanza, la variabile vpt_giorni è un vettore di WORD in quanto
ogni suo elemento contiene un offset a 16 bit. Di conseguenza,
l'istruzione:

mov di, vpt_giorni

trasferisce in DI l'indirizzo di memoria della prima WORD di


vpt_giorni; invece:
82
mov di, [vpt_giorni]

trasferisce in DI il contenuto della prima WORD di vpt_giorni e cioè,


0000h.

L'istruzione:

mov di, vpt_giorni+2

trasferisce in DI l'indirizzo di memoria della seconda WORD di


vpt_giorni; invece:

mov di, [vpt_giorni+2]

trasferisce in DI il contenuto della seconda WORD di vpt_giorni e


cioè, str_giorni+0; si tratta quindi dell'indirizzo di memoria da cui
inizia la stringa "Domenica ", 0.
Ne consegue che:

mov al, [di] (che equivale a mov al, [str_giorni+0])

trasferisce in AL il contenuto del primo BYTE di str_giorni+0 e cioè,


'D'.

L'istruzione:

mov di, vpt_giorni+4

trasferisce in DI l'indirizzo di memoria della terza WORD di


vpt_giorni; invece:

mov di, [vpt_giorni+4]

trasferisce in DI il contenuto della terza WORD di vpt_giorni e cioè,


str_giorni+10; si tratta quindi dell'indirizzo di memoria da cui
inizia la stringa "Lunedi ", 0.
Ne consegue che:

mov al, [di] (che equivale a mov al, [str_giorni+10])

trasferisce in AL il contenuto del primo BYTE di str_giorni+10 e


cioè, 'L'.

Analoghe considerazioni per tutte le altre WORD di vpt_giorni.

Si consiglia di scrivere un programma per verificare in pratica i


concetti appena esposti.

La lettura del campo Giorno della Settimana dal dispositivo RTC + RAM
ci fornisce un valore compreso tra 1 e 7; tale valore viene caricato
in BX e moltiplicato per 2 in quanto ogni elemento di vpt_giorni
occupa 2 byte. A questo punto, [vpt_giorni+BX] contiene l'offset da
cui inizia in memoria la stringa C con il nome del giorno corrente.

83
Naturalmente, le considerazioni appena esposte si applicano anche al
caso del vettore vpt_mesi.

Nota importante.
Se, eseguendo il programma di Figura 12, vengono visualizzati
caratteri senza senso al posto delle stringhe relative al
giorno della settimana e al mese, significa che i
corrispondenti campi del dispositivo RTC + RAM non sono
impostati correttamente; ciò è plausibile in quanto il
dispositivo non si preoccupa minimamente della correttezza
delle impostazioni, limitandosi solo ad incrementare i vari
campi ad intervalli di 1 secondo!
Il caso più diffuso è relativo alla impostazione non corretta
del campo Giorno della Settimana; questa situazione non è
infrequente in quanto tale campo viene ignorato da molti SO.

Per ovviare ai problemi appena illustrati, l'utente può


servirsi di appositi comandi attraverso i quali i SO
permettono di reimpostare la data e l'ora; generalmente i SO
meno vecchi salvano le nuove impostazioni nel dispositivo RTC
+ RAM (ciò accade anche con le ultime versioni del DOS).
In alternativa, si può scrivere un apposito programma
destinato a correggere le informazioni presenti
nell'orologio/calendario; a tale proposito, è necessario
operare attraverso un SO che permetta l'accesso diretto al
dispositivo RTC + RAM.
Sotto DOS non ci sono problemi in quanto tale SO, come
sappiamo, permette di accedere direttamente a tutto
l'hardware del computer; proprio per questo motivo, il DOS
rappresenta l'ambiente ideale per "sperimentare" i programmi
Assembly.
Il discorso cambia radicalmente nel caso degli emulatori DOS
utilizzati sotto SO come Windows, Linux, BSD, etc; tali SO
possono anche imporre delle limitazioni agli utenti privi dei
privilegi dell'amministratore di sistema.
Si tenga anche presente che alcuni di tali emulatori
potrebbero offrire un supporto limitato alle funzionalità dei
vari dispositivi hardware presenti nel computer; per maggiori
dettagli si consiglia di leggere la documentazione tecnica
relativa all'emulatore che si sta utilizzando.

Bibliografia

Motorola Semiconductors - MC146818A Real Time Clock plus RAM


(501896_DS.pdf)

Dallas Semiconductors - Real Time Clock


(DS12885-DS12C887A.pdf)
Texas Instruments - bq4285 Real Time Clock with NVRAM Control
(bq4285.pdf)

84
Capitolo 5 Il PIT 8254 - Programmable
Interval Timer
Quando si programma un computer si presenta spesso la necessità di
dover gestire dei conteggi del tempo (cronometraggi); si pensi, ad
esempio, ad un programma che deve svolgere un certo compito ad
intervalli di tempo regolari.
In molti casi di questo genere, capita poi che venga richiesta anche
una notevole precisione nel cronometraggio; è necessario quindi
trovare il modo per affrontare e risolvere queste situazioni con la
massima efficienza possibile.

Una prima soluzione potrebbe essere quella di ricorrere ad appositi


servizi di conteggio del tempo, forniti dal BIOS o dal SO; la Figura
1, ad esempio, mostra le caratteristiche del servizio Wait (attesa)
della INT 15h del BIOS.

Figura 1 - Servizio n. 86h della INT 15h


INT 15h - Servizio n. 86h - Wait:
attende che trascorra un intervallo di tempo in microsecondi.

Argomenti richiesti:
AH = 86h (servizio Wait)
CX = microsecondi (WORD alta)
DX = microsecondi (WORD bassa)

Si tenga presente che:

1 microsecondo (µs) = 1/1000000 s

Supponiamo ora di voler scrivere un loop per visualizzare sullo


schermo una sequenza di 20 asterischi; per ogni asterisco appena
visualizzato, attendiamo che trascorra un intervallo di tempo pari a
1 secondo, per cui l'intero loop dovrebbe svolgersi in circa 20
secondi.
Definiamo innanzi tutto la seguente stringa:

asterisco db "*", 0

A questo punto possiamo scrivere:

mov bx, 20 ; 20 iterazioni


mov di, asterisco ; di punta a asterisco
mov dx, 0400h ; riga 4, colonna 0

delay_loop:
call writeString ; visualizza un asterisco
push dx ; salva riga, colonna
mov cx, 000Fh ; microsecondi (WORD alta)
mov dx, 4240h ; microsecondi (WORD bassa)
mov ah, 86h ; servizio n. 86h (Wait)
int 15h ; chiama il BIOS
pop dx ; ripristina riga, colonna
inc dl ; incremento colonna
85
dec bx ; decremento contatore
jnz delay_loop ; controllo loop

(1 s = 1000000 µs = 000F4240h µs)

In termini di efficienza, il metodo appena illustrato è un vero


disastro!
L'aspetto più grave è dato dal fatto che il servizio Wait del BIOS si
impossessa del controllo e non lo restituisce finché non è trascorso
l'intervallo di tempo che avevamo richiesto; in tale intervallo di
tempo, il nostro programma rimane letteralmente bloccato e non può
svolgere altri compiti.
Come se non bastasse, il tempo di esecuzione del loop può essere
pesantemente influenzato da eventi esterni come interruzioni
hardware, movimenti rapidi del mouse, etc; provando, ad esempio, a
trascinare velocemente la finestra DOS durante la fase di esecuzione
del loop, si può constatare che la visualizzazione dei 20 asterischi
può richiedere un tempo sensibilmente superiore ai 20 secondi.

Una seconda soluzione potrebbe essere quella di sfruttare il Real


Time Clock o RTC (orologio in tempo reale) analizzato nel precedente
capitolo; abbiamo visto, infatti, che tale dispositivo è in grado di
generare delle interruzioni periodiche ad intervalli di tempo
regolari (e totalmente programmabili dall'utente).
In base allora a quanto è stato esposto nel precedente capitolo,
prima di tutto impostiamo una nostra ISR destinata ad intercettare la
INT 70h; ogni volta che tale ISR viene chiamata, provvede a stampare
un asterisco e a svolgere altre eventuali operazioni (incremento
colonna, conteggio del numero di asterischi stampati, etc).
Selezioniamo ora l'opportuno intervallo di tempo per la periodic
interrupt attraverso i bit RS0, RS1, RS2 e RS3 del Registro A;
infine, abilitiamo la periodic interrupt attraverso il bit PIE del
Registro B.

L'evidente vantaggio di questo secondo metodo è rappresentato dalla


notevole efficienza; infatti, la ISR viene chiamata solo al momento
opportuno, mentre tra una chiamata e l'altra la CPU rimane a
disposizione del nostro programma (e del SO) permettendo lo
svolgimento di altre operazioni. L'intervallo di tempo della periodic
interrupt viene calcolato via hardware dal RTC; ne consegue che tale
intervallo è molto preciso in quanto le interferenze esterne
producono effetti trascurabili sul calcolo stesso.
Il RTC dispone di un numero limitato di intervalli programmabili, per
cui dobbiamo prendere le opportune precauzioni; ad esempio, se
abbiamo bisogno di un intervallo da 1 s (1000 ms), possiamo
selezionare 500 ms nel RTC facendo poi in modo che la ISR stampi un
asterisco una volta si e una no (in questo modo, infatti, verrà
stampato un asterisco ogni 500 + 500 = 1000 ms).

Il problema che si presenta è dato dal fatto che il RTC è stato


concepito principalmente per l'aggiornamento continuo
dell'orologio/calendario; il suo impiego in compiti di cronometraggio
è possibile quindi in modo molto limitato da parte dei programmi,
anche perché i SO potrebbero avere la necessità di utilizzare
86
intensivamente i pochi servizi forniti dallo stesso RTC.
Proprio per ovviare a tale problema, si è deciso di dotare i PC di un
numero adeguato di timer destinati esplicitamente al calcolo di
temporizzazioni molto precise; a tale proposito, la IBM ha scelto un
dispositivo rappresentato in origine dal chip PIT 8253, a cui ha
fatto seguito il PIT 8254. L'acronimo PIT sta per Programmable
Interval Timer (generatore di intervalli di tempo programmabili).

5.1 Funzionamento del dispositivo PIT 8254

Il PIT 8254 incorpora tutte le caratteristiche del vecchio PIT 8253


garantendo in tal modo la piena compatibilità verso il basso; la
Figura 2 illustra lo schema a blocchi semplificato del dispositivo.

Figura 2 - Dispositivo PIT 8254

Come si può notare, un PIT 8254 contiene al suo interno tre timer
che, per convenzione, vengono denominati Timer0, Timer1 e Timer2; i
tre timer sono del tutto identici e ciascuno di essi funziona in modo
totalmente indipendente dagli altri.

Attraverso l'address bus è possibile selezionare uno qualsiasi dei


tre timer, oppure un registro denominato control word; tale registro
è accessibile in sola scrittura e viene utilizzato per programmare i
timer.
La logica di controllo permette l'accesso in lettura al PIT
abilitando la linea RD, mentre l'accesso in scrittura è reso
possibile abilitando la linea WR; attraverso la linea CS (chip
select) viene abilitato o disabilitato l'intero dispositivo.
Ovviamente, tutte le operazioni di I/O si svolgono tramite il data
bus.
Fondamentalmente, la programmazione di un timer consiste nel
caricamento di un valore iniziale a 16 bit che verrà utilizzato per
effettuare un conto alla rovescia; per ogni ciclo di clock che arriva
attraverso la linea CLK del timer, il relativo contatore viene
87
decrementato di 1.
Il risultato che si ottiene sulla linea OUT dipende dalla modalità di
funzionamento con la quale è stato programmato il timer; in totale,
risultano disponibili sei modalità operative che verranno analizzate
nel seguito.
Il segnale che arriva dalla linea GATE permette di abilitare (1) o
disabilitare (0) il conteggio; l'effetto di tale segnale sullo stato
della linea OUT dipende dalla modalità operativa del timer.

5.1.1 Struttura interna di un timer

Visto e considerato che i tre timer sono assolutamente identici,


possiamo limitarci ad analizzare il funzionamento di uno solo di
essi; la Figura 3 mostra la struttura interna di un singolo timer (il
control word register non fa parte del timer ed è stato inserito
nello schema solo per comodità di esposizione).

Figura 3 - Struttura di un timer

Il blocco CE (Count Element) contiene fisicamente il contatore; come


è stato già anticipato, si tratta di una locazione da 16 bit
destinata a gestire un numero intero senza segno compreso tra 0 e
65535.
Il blocco CE non può essere acceduto in modo diretto dal
programmatore (anche perché, in tal caso, si otterrebbero risultati
privi di attendibilità); tutti gli accessi in lettura e in scrittura
avvengono attraverso i due registri a 16 bit denominati OL e CR.
Il registro CR (Count Register) è suddiviso in due half registers a 8
bit denominati CRM (most significant byte) e CRL (least significant
byte); il suo scopo è quello di scrivere un nuovo valore nel blocco
CE. Il programmatore può decidere se gestire il nuovo valore in un
unico blocco da 16 bit o in due blocchi da 8 bit ciascuno che
rappresentano il byte basso e il byte alto del contatore; non appena
il registro CR è stato caricato, il suo contenuto viene trasferito

88
automaticamente in CE e lo stesso registro CR viene azzerato.
Il registro OL (Output Latch) è suddiviso in due half registers a 8
bit denominati OLM (most significant byte) e OLL (least significant
byte); questo registro viene costantemente aggiornato con il valore
corrente del conteggio gestito da CE. Il programmatore può utilizzare
OL per conoscere proprio il valore raggiunto da tale conteggio in un
determinato istante; a tale proposito, la lettura può coinvolgere gli
interi 16 bit del contatore o due valori a 8 bit che rappresentano il
byte basso e il byte alto del contatore stesso. Durante la fase di
lettura, l'aggiornamento di OL viene temporaneamente bloccato
(latched); una volta che la lettura è terminata, il registro viene
sbloccato e il suo contenuto viene nuovamente sottoposto ad un
continuo aggiornamento con il valore letto da CE.

Come è stato già anticipato, il valore caricato in CE viene


utilizzato per effettuare un conto alla rovescia; tale valore viene
decrementato di 1 ad ogni ciclo di clock del segnale che arriva
attraverso la linea CLK del timer selezionato. Il clock è
rappresentato dal classico segnale ad onda quadra come quello
descritto in Figura 4.

Figura 4 - Segnale ad onda quadra

Il decremento del contatore avviene durante ogni "fronte di discesa"


(falling edge) del segnale di clock; il fronte di discesa rappresenta
la fase durante la quale, un segnale come quello di Figura 4, passa
da livello logico 1 a livello logico 0 (la fase opposta prende il
nome di "fronte di salita" o rising edge).

Nota importante.
Cosa succede se si inizializza il contatore con il valore
0000h?
In tal caso, al primo fronte di discesa del segnale di clock,
il contatore (che è una locazione da 16 bit) viene
decrementato di 1 e assume quindi il valore:

0000h - 1 = 0000h + (-1) = 0000h + FFFFh = FFFFh = 65535

In sostanza, inizializzare il contatore a 0000h equivale ad


inizializzarlo con il valore 65536!

Lo status register di Figura 3 (compreso lo status latch) è


concettualmente simile al registro OL; infatti, lo status register
viene costantemente aggiornato con il contenuto letto dal control
word register. Lo status register può essere quindi utilizzato dal
programmatore per leggere il contenuto corrente del control word
register.
Durante la fase di lettura, l'aggiornamento dello status register
89
viene temporaneamente bloccato (latched); una volta che la lettura è
terminata, il registro viene sbloccato e il suo contenuto viene
nuovamente sottoposto ad un continuo aggiornamento con il valore
letto dal control word register.

5.1.2 Il control word register

L'address bus che connette il PIT 8254 con la CPU permette di


selezionare uno dei tre timer, oppure il cosiddetto control word
register; si tratta di un registro a 8 bit la cui struttura è
illustrata in Figura 5.

Figura 5 - Control word register


Bit 7 6 5 4 3 2 1 0
Contenuto SC1 SC0 RW1 RW0 M2 M1 M0 BCD

Il bit BCD permette di stabilire se il conteggio deve essere espresso


in formato binario (BCD=0) o in binary coded decimal (BCD=1); nel
primo caso, il contatore può gestire un numero intero senza segno
compreso tra 0 e 65535, mentre nel secondo caso gli estremi sono
0000h e 9999h (ovviamente, in formato BCD).

Attraverso i tre bit M0, M1 e M2 possiamo selezionare sei modalità


operative differenti per ciascuno dei tre timer; tali modalità
vengono illustrate più avanti.
Sono considerati validi solamente i sei valori 000b, 001b, 010b,
011b, 100b e 101b.

Attraverso i due bit RW0 e RW1 possiamo selezionare la modalità di


lettura/scrittura del contatore; il significato dei quattro possibili
valori assunti da questi due bit è illustrato in Figura 6.

Figura 6 - Read/Write modes


RW1 RW0 Significato
0 0 Counter latch command
0 1 Leggi/scrivi solo il LSB
1 0 Leggi/scrivi solo il MSB
1 1 Leggi/scrivi prima il LSB e poi il MSB

Se vogliamo effettuare una operazione di scrittura, dopo aver


selezionato il timer da programmare attraverso i due bit SC0 e SC1
illustrati più avanti, possiamo procedere con il caricamento del
valore iniziale per il contatore; la modalità di caricamento viene
stabilita proprio dai due bit RW0 e RW1.
Come si nota in Figura 6, possiamo richiedere il caricamento
dell'intero contatore a 16 bit spezzandolo in due valori a 8 bit
oppure possiamo caricare solamente il LSB o il MSB; ricordiamoci che
il registro CR (in cui viene pre-caricato il contatore) viene
automaticamente azzerato ogni volta che il suo contenuto viene
trasferito nel blocco CE, per cui se richiediamo solamente il
caricamento del LSB o del MSB, allora i restanti 8 bit del contatore

90
valgono implicitamente 0 (ad esempio, se carichiamo solo il MSB del
contatore, allora LSB=0).
Nel caso particolare in cui si vogliano caricare gli interi 16 bit
del contatore, è necessario assicurarsi che il caricamento del MSB
segua immediatamente il caricamento del LSB; in caso contrario si
ottengono risultati imprevedibili!

Il due bit RW0 e RW1 permettono anche di richiedere una operazione di


lettura del valore corrente del contatore; sono disponibili tre
distinte modalità di lettura.
La prima modalità è la più semplice e consiste nel seguire lo stesso
procedimento appena illustrato per la scrittura; con i due bit SC0 e
SC1 selezioniamo il timer da leggere e con i due bit RW0 e RW1
stabiliamo se leggere gli interi 16 bit (11b), solamente il MSB (10b)
o solamente il LSB (01b).
Nel caso particolare in cui si vogliano leggere gli interi 16 bit del
contatore, è necessario assicurarsi che la lettura del MSB segua
immediatamente la lettura del LSB; in caso contrario si ottengono
risultati imprevedibili!
Il metodo appena descritto, pur essendo molto semplice, richiede che
la lettura venga effettuata solamente dopo che il conteggio è stato
sospeso; infatti, se si effettua una lettura mentre il conteggio è in
corso, si ottengono valori privi di attendibilità. Per sospendere
temporaneamente il conteggio si può usare, ad esempio, la linea GATE
del timer che vogliamo leggere.

Il secondo metodo di lettura (counter latch) consiste nell'assegnare


il valore 00b ai due bit RW0 e RW1; in una situazione del genere, il
timer selezionato (mediante i due bit SC0 e SC1) si predispone per
una operazione di lettura del contatore attraverso il registro OL.
Quando si imposta il comando counter latch, i primi 4 bit del control
word register vengono ignorati; per garantire la piena compatibilità
con le future versioni del PIT, si raccomanda di porre questi 4 bit a
0!
Una volta che il comando counter latch è stato inviato,
l'aggiornamento continuo di OL viene temporaneamente bloccato
(latched) in attesa che il suo contenuto venga letto; solamente dopo
la lettura, il registro OL viene sbloccato e il suo contenuto viene
nuovamente sottoposto ad un continuo aggiornamento con il valore
letto da CE.
La lettura deve rispecchiare rigorosamente la modalità con la quale è
stato caricato il valore iniziale del contatore (e ciò vale anche per
il comando read-back illustrato più avanti); se, ad esempio, abbiamo
caricato il contatore sotto forma di LSB+MSB, allora anche la lettura
deve svolgersi sotto forma di LSB+MSB!
Il fatto che i tre timer siano indipendenti, ci permette di leggerli
in tutte le combinazioni possibili; possiamo decidere di leggerne
solo uno di essi o anche tutti e tre. In quest'ultimo caso, ciascuno
dei tre registri OL rimane bloccato finché non viene letto.

Il terzo metodo di lettura (read back) è ancora più sofisticato e può


essere richiesto attraverso i due bit SC0 e SC1 che ci permettono
anche di selezionare uno dei tre timer da leggere/scrivere; il

91
significato dei quattro possibili valori assegnabili alla coppia SC0,
SC1 è illustrato in Figura 7.

Figura 7 - Select counter modes


SC1 SC0 Significato
0 0 Seleziona il Timer0
0 1 Seleziona il Timer1
1 0 Seleziona il Timer2
1 1 Read-back command

Come è stato già spiegato, assegnando alla coppia SC0, SC1 uno dei
tre possibili valori 00b, 01b, 10b, otteniamo la selezione di uno dei
tre timer presenti nel PIT; a questo punto, attraverso la coppia RW0,
RW1, possiamo selezionare la modalità di lettura/scrittura semplice,
oppure il comando counter latch.
Se, invece, carichiamo in SC0, SC1 il valore 11b, allora stiamo
richiedendo esplicitamente l'esecuzione del comando read back; grazie
a questo comando, è possibile richiedere al PIT una serie di
informazioni comprendenti lo stato corrente del contatore, la
modalità operativa, lo stato dell'uscita OUT e la condizione null
count (descritta più avanti) di un qualsiasi timer.
Quando si assegna il valore 11b alla coppia SC0, SC1, il control word
register muta la sua struttura secondo lo schema illustrato in Figura
8.

Figura 8 - Control word register (read back)


Bit 7 6 5 4 3 2 1 0
Contenuto 1 1 CNT STAT T2 T1 T0 0

Il bit in posizione 0 deve valere rigorosamente 0; il suo utilizzo è


riservato per le future versioni del PIT.
I tre bit in posizione 1, 2 e 3 permettono di selezionare (1) o
deselezionare (0) il timer (o i timer) da leggere; è possibile quindi
leggere tutti i tre timer contemporaneamente.
Il bit (complementato) in posizione 4, quando vale 0, permette di
richiedere la lettura dello "status" relativo ai timer selezionati;
le informazioni restituite dal PIT comprendono la modalità operativa,
lo stato della linea OUT e la condizione null count.
Dopo la ricezione di un comando read back con STAT=0, le suddette
informazioni risultano disponibili attraverso lo status register;
tale registro, assume la configurazione illustrata in Figura 9.

Figura 9 - Status register (read back)


Bit 7 6 5 4 3 2 1 0
Contenuto OUT NULL RW1 RW0 M2 M1 M0 BCD

I sei bit compresi tra la posizione 0 e la posizione 5, restituiscono


informazioni che rispecchiano la configurazione con la quale abbiamo
inizializzato un determinato timer; il significato di questi bit è
quindi lo stesso già descritto in relazione alla Figura 5.

92
Il bit in posizione 6 indica se si è verificata o meno la condizione
null count per un determinato timer; tale bit vale 0 quanto il
contatore è pronto per una eventuale operazione di lettura, mentre
vale 1 quando il contatore si trova in CR e non è stato ancora
trasferito in CE.
Il bit in posizione 7 indica lo stato corrente della linea OUT.

Come al solito, ciascuno degli status register selezionati, rimane


bloccato finché il programmatore non esegue l'operazione di lettura.

Tornando alla Figura 8, il bit (complementato) in posizione 5, quando


vale 0, permette di richiedere la lettura del contatore relativo ai
timer selezionati; tale informazione, come già sappiamo, viene
restituita nel registro OL di ciascuno dei timer coinvolti
nell'operazione di read back.
Anche in questo caso, ciascuno dei registri OL selezionati, rimane
bloccato finché il programmatore non esegue l'operazione di lettura.

5.2 Modalità operative dei timer

Quando si inizializza un qualsiasi timer, i tre bit M0, M1 e M2 del


control word register, illustrato in Figura 5, permettono di
selezionare la modalità operativa del timer stesso; analizziamo in
dettaglio le sei modalità disponibili.

5.2.1 Modo 0: Interrupt on terminal count

Non appena si carica una control word (CW) che specifica il Modo 0,
l'uscita OUT del timer selezionato assume immediatamente il livello
logico 0; a questo punto, lo stesso timer attende che venga caricato
il valore iniziale (che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR per essere poi trasferito in CE al successivo fronte
di discesa del segnale CLK; tale fronte di discesa non effettua
nessun decremento (di 1) dello stesso valore N, mentre l'uscita OUT
continua a permanere sul livello logico 0.
A questo punto, per ogni successivo fronte di discesa del segnale
CLK, il contatore viene decrementato di 1; l'uscita OUT si porta a
livello logico 1 solamente quando il conteggio raggiunge il valore 0.
Possiamo affermare quindi che, una volta caricato il nuovo valore del
contatore, l'uscita OUT si porterà a livello logico 1 esattamente
dopo N+1 cicli di clock.
Un ulteriore fronte di discesa del segnale CLK provoca il wrap around
del contatore il quale ricomincia a contare da 65535; il segnale OUT
non viene influenzato e continua a permanere sul livello logico 1
finché il timer non viene nuovamente programmato.

La Figura 10 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 10 - Modo 0

93
Il segnale WR viene gestito dalla logica di controllo e permette di
abilitare una operazione di scrittura; come si nota in Figura 10,
tale segnale è complementato e quindi la scrittura viene abilitata
quando WR=0.
Nell'esempio di Figura 10 si assume che il timer si trovi
inizialmente in una modalità operativa indefinita (situazione che si
verifica tipicamente dopo l'accensione del computer); anche il
contatore assume quindi un valore iniziale indefinito (compreso tra 0
e 65535) che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 0 (000b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00010000b = 10h

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 0.

La seconda operazione di scrittura consiste nel caricamento del nuovo


valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=5.
A questo punto, al successivo fronte di discesa del segnale CLK, il
nuovo valore del contatore (5) viene caricato in CE (con MSB=0); come
è stato già spiegato, durante tale operazione il contatore stesso non
subisce nessun decremento di 1.

Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il


decremento di 1 del contatore; come si nota in Figura 10, il
conteggio assume quindi in sequenza i valori 5, 4, 3, 2, 1, 0.
Solamente a questo punto, il segnale OUT si porta a livello logico 1;
ciò accade quindi esattamente dopo 5+1=6 cicli di clock!

Un nuovo fronte di discesa del segnale CLK provoca il wrap around del
contatore il quale ricomincia a contare da 65535; il segnale OUT
continua a valere 1 finché il timer non viene nuovamente programmato.

Cosa succede se GATE viene portato a 0 mentre è in già corso il conto


94
alla rovescia?
In tal caso, lo stesso conto alla rovescia viene sospeso in attesa
che GATE si riporti a 1; ad esempio, se GATE diventa 0 quando il
conteggio è arrivato a 2, ad ogni successivo fronte di discesa del
segnale CLK il contatore continua a valere 2 in attesa che lo stesso
GATE si riporti a 1. A sua volta, il segnale OUT continua a valere 0
in attesa che il conto alla rovescia raggiunga lo 0.

5.2.2 Modo 1: Hardware retriggerable one-shot

Non appena si carica una CW che specifica il Modo 1, l'uscita OUT del
timer selezionato assume immediatamente il livello logico 1; a questo
punto, lo stesso timer attende che venga caricato il valore iniziale
(che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR; l'uscita OUT continua a permanere sul livello logico
1 in attesa che arrivi un "impulso di innesco" (trigger) attraverso
la linea GATE.
Subito dopo l'arrivo del trigger, al successivo fronte di discesa del
segnale CLK l'uscita OUT assume immediatamente il livello logico 0 e
il nuovo valore del contatore, memorizzato in CR, viene trasferito in
CE; ogni ulteriore fronte di discesa del segnale CLK decrementa di 1
il valore del contatore stesso.
Quando il conto alla rovescia raggiunge il valore 0, l'uscita OUT si
porta a livello logico 1 e permane in questo stato finché non arriva
un nuovo trigger. Nel caso in cui arrivi un nuovo trigger, a partire
dal successivo fronte di discesa del segnale CLK il segnale OUT si
riporta a livello logico 0 e viene ripetuto il conto alla rovescia
sempre con valore iniziale N; se non arriva nessun nuovo trigger, il
contatore subisce il wrap around e ricomincia a contare da 65535,
mentre la linea OUT continua a mantenersi sul livello logico 1.
Possiamo affermare quindi che, dopo l'arrivo di un trigger, l'uscita
OUT si porterà a livello logico 1 esattamente dopo N cicli di clock.

La Figura 11 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 11 - Modo 1

Nell'esempio di Figura 11 si assume che il timer si trovi


95
inizialmente in una modalità operativa indefinita; anche il contatore
assume quindi un valore iniziale indefinito (compreso tra 0 e 65535)
che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 1 (001b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00010010b = 12h

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 1.

La seconda operazione di scrittura consiste nel caricamento in CR del


nuovo valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=4.
A questo punto, il timer resta in attesa di un trigger il quale,
arrivando dalla linea GATE, innesca una serie di precise azioni; in
particolare, al successivo fronte di discesa del segnale CLK il nuovo
valore del contatore, memorizzato in CR, viene trasferito in CE e la
linea OUT si porta immediatamente a livello logico 0.

Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il


decremento di 1 del contatore; come si nota in Figura 11, il
conteggio assume quindi in sequenza i valori 4, 3, 2, 1, 0.
Solamente a questo punto, il segnale OUT si porta a livello logico 1;
ciò accade quindi esattamente dopo 4 cicli di clock!

Un nuovo fronte di discesa del segnale CLK provoca il wrap around del
contatore il quale ricomincia a contare da 65535; il segnale OUT
continua a valere 1 finché il timer non viene nuovamente raggiunto da
un trigger il quale determina l'inizio di un nuovo conto alla
rovescia sempre a partire da 4.

Cosa succede se dal GATE arriva un nuovo trigger mentre è in già


corso il conto alla rovescia?
In tal caso, il conto alla rovescia ricomincia dal valore iniziale
(che nell'esempio di Figura 11 è 4); questa situazione non influisce
sulla linea OUT la quale continua a valere 0 in attesa che il conto
alla rovescia raggiunga lo 0.

5.2.3 Modo 2: Rate generator

Non appena si carica una CW che specifica il Modo 2, l'uscita OUT del
timer selezionato assume immediatamente il livello logico 1; a questo
punto, lo stesso timer attende che venga caricato il valore iniziale
(che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR; il trasferimento di N, da CR a CE, avviene al
successivo fronte di discesa del segnale CLK.
Ogni ulteriore fronte di discesa del segnale CLK decrementa di 1 il
valore del contatore; una volta che il contatore stesso ha raggiunto
il valore 1, l'uscita OUT si porta a livello logico 0 per un solo
96
ciclo di clock. Subito dopo, l'uscita OUT torna a livello logico 1;
il contatore viene nuovamente inizializzato con il valore N e tutta
la fase appena descritta si ripete indefinitamente.
Possiamo affermare quindi che nel Modo 2, il timer si comporta come
un "generatore di eventi" con frequenza pari a N cicli di clock;
infatti, l'uscita OUT si porta a livello logico 0 (per un solo ciclo
di clock) esattamente ogni N cicli di clock.
Dalle considerazioni appena svolte risulta, inoltre, che nel Modo 2
non è ammesso inizializzare il contatore con un valore inferiore a 2!

La Figura 12 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 12 - Modo 2

Nell'esempio di Figura 12 si assume che il timer si trovi


inizialmente in una modalità operativa indefinita; anche il contatore
assume quindi un valore iniziale indefinito (compreso tra 0 e 65535)
che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 2 (010b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00010100b = 14h

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 1.

La seconda operazione di scrittura consiste nel caricamento in CR del


nuovo valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=4.
A questo punto, al successivo fronte di discesa del segnale CLK il
nuovo valore del contatore, memorizzato in CR, viene trasferito in
CE; ogni ulteriore fronte di discesa del segnale CLK provoca il
decremento di 1 del contatore.
Non appena il conteggio raggiunge il valore 1, la linea OUT si porta
a livello logico 0 per un solo ciclo di clock; subito dopo, il
contatore viene nuovamente inizializzato con il valore 4 e tutta la
97
sequenza appena descritta viene ripetuta indefinitamente.
In sostanza, il conto alla rovescia consiste in una ripetizione
all'infinito della sequenza 4, 3, 2, 1.

Cosa succede se GATE viene portato a 0 mentre è in già corso il conto


alla rovescia?
In tal caso, lo stesso conto alla rovescia viene sospeso in attesa
che GATE si riporti a 1; ad esempio, se GATE diventa 0 quando il
conteggio è arrivato a 2, ad ogni successivo fronte di discesa del
segnale CLK il contatore continua a valere 2 in attesa che lo stesso
GATE si riporti a 1.
Se, attraverso GATE, viene generato un trigger 1 - 0 - 1 proprio
mentre OUT vale 0, allora lo stesso OUT si riporta immediatamente a
1; al successivo ciclo di clock il contatore viene ricaricato con il
valore iniziale e il conto alla rovescia viene ripetuto.

5.2.4 Modo 3: Square wave mode

Non appena si carica una CW che specifica il Modo 3, l'uscita OUT del
timer selezionato assume immediatamente il livello logico 1; a questo
punto, lo stesso timer attende che venga caricato il valore iniziale
(che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR; il trasferimento di N, da CR a CE, avviene al
successivo fronte di discesa del segnale CLK.
Ogni ulteriore fronte di discesa del segnale CLK decrementa di 2 il
valore del contatore; una volta che il conteggio ha raggiunto il
valore 0 (quindi, dopo N/2 cicli di clock), l'uscita OUT si porta a
livello logico 0 e il contatore viene nuovamente inizializzato con il
valore N e decrementato di 2 ad ogni ciclo di clock con l'uscita OUT
che permane sul livello logico 0.
Non appena il conteggio raggiunge il valore 0 (quindi, dopo N/2 cicli
di clock), la linea OUT si riporta a livello logico 1 e il contatore
viene nuovamente inizializzato con il valore N e decrementato di 2 ad
ogni ciclo di clock con la linea OUT che permane sul livello logico
1; tutta la sequenza appena descritta viene ripetuta indefinitamente.
Possiamo affermare quindi che nel Modo 3, il timer si comporta come
un "generatore di onda quadra" con frequenza pari a N cicli di clock;
infatti, l'uscita OUT si alterna indefinitamente tra il livello
logico 1 (semionda positiva) per N/2 cicli di clock e il livello
logico 0 (semionda negativa) per N/2 cicli di clock.

Le considerazioni appena svolte si riferiscono al caso in cui N sia


un numero intero positivo pari; se N è dispari, la semionda positiva
dura (N+1)/2 cicli di clock, mentre la semionda negativa dura (N-1)/2
cicli di clock.

La Figura 13 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 13 - Modo 3

98
Nell'esempio di Figura 13 si assume che il timer si trovi
inizialmente in una modalità operativa indefinita; anche il contatore
assume quindi un valore iniziale indefinito (compreso tra 0 e 65535)
che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 3 (011b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00010110b = 16h

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 1.

La seconda operazione di scrittura consiste nel caricamento in CR del


nuovo valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=4.
A questo punto, al successivo fronte di discesa del segnale CLK il
nuovo valore del contatore, memorizzato in CR, viene trasferito in
CE; ogni ulteriore fronte di discesa del segnale CLK provoca il
decremento di 2 del contatore.
Non appena il conteggio raggiunge il valore 0 (dopo 4/2=2 cicli di
clock), la linea OUT si porta a livello logico 0; subito dopo, il
contatore viene nuovamente inizializzato con il valore 4 e
decrementato di 2 ad ogni ciclo di clock con la linea OUT che permane
sul livello logico 0.
Non appena il conteggio raggiunge il valore 0 (dopo 4/2=2 cicli di
clock), la linea OUT si riporta a livello logico 1 e il contatore
viene nuovamente inizializzato con il valore 4 e decrementato di 2 ad
ogni ciclo di clock con la linea OUT che permane sul livello logico
1; tutta la sequenza appena descritta viene ripetuta indefinitamente.
In sostanza, il conto alla rovescia consiste in una ripetizione
all'infinito della sequenza 4, 2 per la semionda positiva e 4, 2 per
la semionda negativa.
Se avessimo assegnato al contatore il valore iniziale 5, avremmo
ottenuto un'onda quadra con semionda positiva di durata pari a
(5+1)/2=3 cicli di clock e con semionda negativa di durata pari a (5-
1)/2=2 cicli di clock; in tal caso, la sequenza ripetuta all'infinito
99
sarebbe stata 6, 4, 2 per la semionda positiva e 4, 2 per la semionda
negativa.

Cosa succede se GATE viene portato a 0 mentre è in già corso il conto


alla rovescia?
In tal caso, lo stesso conto alla rovescia viene sospeso in attesa
che GATE si riporti a 1; ad esempio, se GATE diventa 0 quando il
conteggio è arrivato a 2, ad ogni successivo fronte di discesa del
segnale CLK il contatore continua a valere 2 in attesa che lo stesso
GATE si riporti a 1.
Se, attraverso GATE, viene generato un trigger 1 - 0 - 1 proprio
mentre OUT vale 0, allora lo stesso OUT si riporta immediatamente a
1; al successivo ciclo di clock il contatore viene ricaricato con il
valore iniziale e il conto alla rovescia viene ripetuto.

5.2.5 Modo 4: Software triggered strobe

Non appena si carica una control word (CW) che specifica il Modo 4,
l'uscita OUT del timer selezionato assume immediatamente il livello
logico 1; a questo punto, lo stesso timer attende che venga caricato
il valore iniziale (che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR per essere poi trasferito in CE al successivo fronte
di discesa del segnale CLK; tale fronte di discesa non effettua
nessun decremento (di 1) dello stesso valore N, mentre l'uscita OUT
continua a permanere sul livello logico 1.
A questo punto, per ogni successivo fronte di discesa del segnale
CLK, il contatore viene decrementato di 1; l'uscita OUT si porta a
livello logico 0, per un unico ciclo di clock, solamente quando il
conteggio raggiunge il valore 0.
Possiamo affermare quindi che, una volta caricato il nuovo valore del
contatore, l'uscita OUT si porterà a livello logico 0 esattamente
dopo N+1 cicli di clock; il caricamento del nuovo valore del
contatore viene trattato come un trigger che innesca il conto alla
rovescia e, proprio per questo motivo, si parla di "innesco
software".
Un ulteriore fronte di discesa del segnale CLK provoca il wrap around
del contatore il quale ricomincia a contare da 65535; il segnale OUT
non viene influenzato e continua a permanere sul livello logico 1
finché il timer non viene nuovamente programmato.

La Figura 14 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 14 - Modo 4

100
Nell'esempio di Figura 14 si assume che il timer si trovi
inizialmente in una modalità operativa indefinita; anche il contatore
assume quindi un valore iniziale indefinito (compreso tra 0 e 65535)
che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 4 (100b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00011000b = 18h

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 1.

La seconda operazione di scrittura consiste nel caricamento del nuovo


valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=4.
A questo punto, al successivo fronte di discesa del segnale CLK, il
nuovo valore del contatore (4) viene caricato in CE (con MSB=0); come
è stato già spiegato, durante tale operazione il contatore stesso non
subisce nessun decremento di 1.

Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il


decremento di 1 del contatore; come si nota in Figura 14, il
conteggio assume quindi in sequenza i valori 4, 3, 2, 1, 0.
Solamente a questo punto, il segnale OUT si porta a livello logico 0
per un solo ciclo di clock; ciò accade quindi esattamente dopo 4+1=5
cicli di clock!

Un nuovo fronte di discesa del segnale CLK provoca il wrap around del
contatore il quale ricomincia a contare da 65535; il segnale OUT
continua a valere 1 finché il timer non viene nuovamente programmato.

Cosa succede se GATE viene portato a 0 mentre è in già corso il conto


alla rovescia?
In tal caso, lo stesso conto alla rovescia viene sospeso in attesa
che GATE si riporti a 1; ad esempio, se GATE diventa 0 quando il
conteggio è arrivato a 2, ad ogni successivo fronte di discesa del
101
segnale CLK il contatore continua a valere 2 in attesa che lo stesso
GATE si riporti a 1. A sua volta, il segnale OUT continua a valere 1
in attesa che il conto alla rovescia raggiunga lo 0.

5.2.6 Modo 5: Hardware triggered strobe

Non appena si carica una CW che specifica il Modo 5, l'uscita OUT del
timer selezionato assume immediatamente il livello logico 1; a questo
punto, lo stesso timer attende che venga caricato il valore iniziale
(che possiamo indicare con N) del conteggio.
Il nuovo valore N, specificato dal programmatore, viene immagazzinato
nel registro CR; l'uscita OUT continua a permanere sul livello logico
1 in attesa che arrivi un trigger attraverso la linea GATE (proprio
per questo motivo, si parla di "innesco hardware").
Subito dopo l'arrivo del trigger, al successivo fronte di discesa del
segnale CLK l'uscita OUT permane sul livello logico 1 e il nuovo
valore del contatore, memorizzato in CR, viene trasferito in CE senza
subire nessun decremento di 1; ogni ulteriore fronte di discesa del
segnale CLK decrementa di 1 il valore del contatore stesso.
Quando il conto alla rovescia raggiunge il valore 0, l'uscita OUT si
porta a livello logico 0 per un solo ciclo di clock; in assenza di
altri eventi, al successivo fronte di discesa del segnale CLK il
contatore subisce il wrap around e ricomincia a contare da 65535,
mentre la linea OUT continua a mantenersi sul livello logico 1.
L'arrivo di un nuovo trigger provoca il caricamento in CE del valore
iniziale N e la conseguente ripetizione del conto alla rovescia
appena descritto.

Possiamo affermare quindi che, dopo l'arrivo di un trigger, l'uscita


OUT si porterà a livello logico 0 esattamente dopo N+1 cicli di
clock.

La Figura 15 illustra un esempio pratico che riassume tutti i


concetti appena esposti.

Figura 15 - Modo 5

Nell'esempio di Figura 15 si assume che il timer si trovi


inizialmente in una modalità operativa indefinita; anche il contatore
assume quindi un valore iniziale indefinito (compreso tra 0 e 65535)
102
che indichiamo con N.

La prima operazione di scrittura consiste nel caricamento della CW;


stiamo richiedendo (Figura 5) la modalità di conteggio binaria (0b),
la modalità operativa 5 (101b), la scrittura del solo LSB del
contatore (01b) e la selezione del Timer0 (00b) per cui otteniamo:

CW = 00011010b = 1Ah

Non appena la scrittura della nuova CW è stata portata a termine, il


segnale OUT si porta subito a livello logico 1.

La seconda operazione di scrittura consiste nel caricamento in CR del


nuovo valore del contatore; nel nostro esempio, il nuovo valore è
rappresentato dal solo LSB=4.
A questo punto, il timer resta in attesa di un trigger il quale,
arrivando dalla linea GATE, innesca una serie di precise azioni; in
particolare, al successivo fronte di discesa del segnale CLK il nuovo
valore del contatore, memorizzato in CR, viene trasferito in CE senza
subire nessun decremento di 1, mentre la linea OUT permane sul
livello logico 1.

Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il


decremento di 1 del contatore; come si nota in Figura 15, il
conteggio assume quindi in sequenza i valori 4, 3, 2, 1, 0.
Solamente a questo punto, il segnale OUT si porta a livello logico 0
per un solo ciclo di clock; ciò accade quindi esattamente dopo 4+1=5
cicli di clock!

Un nuovo fronte di discesa del segnale CLK provoca il wrap around del
contatore il quale ricomincia a contare da 65535; il segnale OUT
continua a valere 1 finché il timer non viene nuovamente raggiunto da
un trigger il quale determina l'inizio di un nuovo conto alla
rovescia sempre a partire da 4.

Cosa succede se dal GATE arriva un nuovo trigger mentre è in già


corso il conto alla rovescia?
In tal caso, il conto alla rovescia ricomincia dal valore iniziale
(che nell'esempio di Figura 15 è 4); questa situazione non influisce
sulla linea OUT la quale continua a valere 1 in attesa che il conto
alla rovescia raggiunga lo 0.

5.3 Impiego dei PIT 8254 sui PC

Sui PC della famiglia hardware 80x86 è presente almeno un PIT


denominato, convenzionalmente, PIT 1; i tre relativi timer presentano
una caratteristica comune legata al fatto che all'ingresso CLK di
ciascuno di essi arriva un segnale ad onda quadra di frequenza pari a
1193181 Hz (o, in esadecimale, 1234DDh Hz).

In base agli standard imposti a suo tempo dalla IBM, i tre timer
presenti nel PIT 1 sono tutti rigorosamente impegnati nello
svolgimento di precisi compiti.

103
5.3.1 Timer 0 - PIT 1

Il Timer 0 del PIT 1 assume una importanza enorme nel mondo dei PC in
quanto il suo scopo è quello di generare un vero e proprio "battito
cardiaco" del computer; tale battito viene utilizzato dal BIOS e dai
SO per svolgere una serie di operazioni ad intervalli di tempo
prestabiliti.

Durante l'avvio del computer, il BIOS programma il Timer 0 nel Modo 2


(Rate generator); il contatore viene inizializzato con il valore 0
(che, come sappiamo, equivale a 65536), per cui sull'uscita OUT si
ottiene una sequenza indefinita di impulsi (Figura 12) alla
frequenza:

f = 1193181 / 65536 = 18.2 Hz

In sostanza, ogni secondo vengono generati 18.2 impulsi ciascuno dei


quali prende il nome di timer tick o, semplicemente, tick; possiamo
affermare quindi che il tick è una vera e propria unità di misura del
tempo sul PC.
Ricordando che T=1/f, si deduce che i ticks si susseguono ad
intervalli regolari di tempo pari a:

T = 1 / f = 1 / 18.2 = 0.0549 s = 54.9 ms

L'uscita OUT del Timer 0 è collegata all'ingresso IR0 del PIC Master;
ne consegue che ogni tick rappresenta una IRQ0 la quale (Figura 10
del Capitolo 3, sezione Assembly Avanzato) viene associata alla INT
08h (timer interrupt).
La ISR associata alla INT 08h permette al BIOS e al SO di svolgere
una serie di compiti di vitale importanza per il computer; tanto per
citare alcuni esempi emblematici, grazie a tale ISR viene
continuamente aggiornato l'orologio "software" del BIOS e del SO e
viene stabilito quando è il momento di spegnere i motori dei lettori
di floppy, CD e DVD (lo spegnimento viene deciso quando trascorre un
certo intervallo di tempo durante il quale l'utente non svolge
nessuna operazione di I/O su tali supporti)!

Prima di terminare, la ISR associata alla INT 08h esegue una INT 1Ch;
i programmi sono liberi di intercettare la INT 1Ch per gestire
ulteriori eventi temporizzati.

La linea GATE del Timer 0 è inaccessibile; tale linea viene tenuta


costantemente a livello logico 1 in modo che il conteggio sia
abilitato in modo permanente.

5.3.2 Timer 1 - PIT 1

In termini di importanza, il Timer 1 del PIT 1 non è certo inferiore


al Timer 0; infatti, il suo scopo è quello di scandire il ritmo di
lavoro del dispositivo che provvede al continuo refresh delle memorie
RAM dinamiche (capitolo 8, paragrafo 8.4.2, sezione Assembly Base)!

Durante l'avvio del computer, il BIOS programma il Timer 1 nel Modo 2


104
(Rate generator); il contatore viene inizializzato con il valore 18,
per cui sull'uscita OUT si ottiene una sequenza indefinita di impulsi
(Figura 12) alla frequenza:

f = 1193181 / 18 = 66287.8 Hz = 66.2878 kHz

In sostanza, ogni secondo vengono generati 66287.8 impulsi;


ricordando che T=1/f, si deduce che i gli impulsi si susseguono ad
intervalli regolari di tempo pari a:

T = 1 / f = 1 / 66287.8 = 1.5*10-5 s = 15 µs

L'uscita OUT del Timer 1 è collegata ad un dispositivo che provvede


al refresh delle RAM dinamiche; l'operazione di refresh viene quindi
eseguita 66287.8 volte al secondo!

La linea GATE del Timer 1 è inaccessibile; tale linea viene tenuta


costantemente a livello logico 1 in modo che il refresh delle RAM
dinamiche sia abilitato in modo permanente.

5.3.3 Timer 2 - PIT 1

Il Timer 2 è totalmente a disposizione del programmatore e risulta


connesso allo Speaker (altoparlante di sistema) del PC secondo lo
schema di Figura 16.

Figura 16 - Connessione Timer 2 - Speaker

Nelle intenzioni della IBM, il circuito di Figura 16 doveva servire


per la generazione di suoni attraverso l'altoparlante del PC; a tale
proposito, come sarà chiarito nel seguito del capitolo, il Timer 2
viene programmato nel Modo 3 (square wave mode).

Lo speaker risulta accessibile attraverso la porta hardware 61h


(keyboard controller - port B); di tale porta ci interessano
principalmente i due bit in posizione 0 e 1 (i restanti bit non
devono essere modificati).
Attraverso il bit 0 possiamo controllare il GATE del Timer 2; come
sappiamo, in tal modo è possibile generare dei trigger che fanno
ripartire il conteggio dal valore iniziale.
Attraverso il bit 1 possiamo controllare la porta AND del circuito di
Figura 16; in tal modo possiamo permettere o inibire il transito
dell'onda quadra che arriva dalla linea OUT del Timer 2.

L'amplificatore provvede ad aumentare adeguatamente l'ampiezza del


segnale che deve pilotare lo speaker; il filtro "passa basso" blocca
i suoni a frequenza troppo alta evitando così che lo speaker
105
(generalmente di qualità piuttosto scadente) possa subire dei danni.

5.3.4 Il PIT 2

Sui PC meno vecchi è presente anche un secondo PIT 8254 denominato,


per convenzione, PIT 2; anche i tre timer presenti nel PIT 2 sono
tutti destinati a svolgere compiti ben determinati.

Il Timer 0 del PIT 2 è denominato Watchdog Timer e viene utilizzato,


soprattutto nei SO multitasking, per evitare che un task (cioè, uno
dei programmi in esecuzione) troppo lento (o in crash) possa bloccare
l'intero sistema; il Timer 0 viene quindi programmato in modo che al
termine dell'intervallo di tempo prestabilito, il controllo venga
tolto al task in esecuzione e passato ad un altro in attesa.

Il Timer 1 del PIT 2 risulta inaccessibile via software.

Il Timer 2 del PIT 2 è denominato Slowdown Timer; il suo impiego è


legato alla possibilità di variare la frequenza di lavoro delle
moderne CPU.

5.4 Programmazione dei PIT 8254

Vediamo subito quali indirizzi sono stati assegnati alle porte


hardware del PIT 1 e del PIT 2 sui PC della famiglia 80x86; la Figura
17 illustra tutti i dettagli.

Figura 17 - Porte hardware dei PIT


1 e 2
PIT 1
Nome porta Indirizzo Accesso
Timer 0 40h Read/Write
Timer 1 41h Read/Write
Timer 2 42h Read/Write
Control Word
43h Write Only
Reg.
PIT 2
Nome porta Indirizzo Accesso
Timer 0 48h Read/Write
Timer 1 49h Inaccessibile
Timer 2 4Ah Read/Write
Control Word
4Bh Write Only
Reg.

Sulla base delle considerazioni svolte in questo capitolo, la


programmazione dei timer risulta molto semplice; prima di tutto
scriviamo nel control word register l'operazione da svolgere e poi
effettuiamo la conseguente lettura o scrittura nel timer selezionato.

Bisogna premettere un aspetto importantissimo legato al fatto che la


106
programmazione dei timer si deve sempre svolgere con le interruzioni
mascherabili disabilitate; ciò è vero non solo per il Timer 0 del PIT
1 (la cui linea OUT è collegata alla linea IR0 del PIC Master), ma
anche per gli altri timer, soprattutto quando si devono leggere o
scrivere gli interi 16 bit del valore del contatore (infatti, come è
stato spiegato in precedenza, la lettura/scrittura del LSB deve
essere immediatamente seguita dalla lettura/scrittura del MSB
evitando che tra le due operazioni si interponga, in particolare, una
qualsiasi interruzione hardware)!

Supponiamo, ad esempio, di voler programmare nel Modo 3 (square wave


mode) il Timer 2 del PIT 1 in modo che venga generata un'onda quadra
alla frequenza di 1200 Hz; posiamo procedere quindi con la
determinazione della CW per la quale otteniamo (Figura 5):

CW = 10110110b = B6h

Infatti, stiamo richiedendo il conteggio binario (0b), il modo


operativo 3 (011b), la scrittura del contatore sotto forma di LSB+MSB
(11b) e il Timer 2 (10b).

Per generare un'onda quadra alla frequenza f=1200 Hz bisogna


ricordare che all'ingresso CLK di ciascuno dei tre timer del PIT 1
arriva un segnale di clock con frequenza 1193181 Hz; nel nostro caso
vogliamo ottenere:

f = 1193181 / N = 1200 Hz

Il contatore dovrà quindi essere inizializzato al valore:

N = 1193181 / 1200 = 994.3175

Il valore 994.3175 può essere arrotondato a 994 che in esadecimale si


scrive 03E2h; ricaviamo quindi LSB=E2h e MSB=03h.
Convertendo il tutto in Assembly otteniamo il seguente codice:

%assign T2P1_PORT 42h ; porta Timer 2 - PIT 1


%assign CWP1_PORT 43h ; porta CWR PIT 1

cli ; disabilita le INT masch.

mov al, 10110110b ; Control Word


out CWP1_PORT, al ; scrive nel PIT 1

mov al, 0E2h ; LSB contatore


out T2P1_PORT, al ; scrive nel PIT 1
mov al, 03h ; MSB contatore
out T2P1_PORT, al ; scrive nel PIT 1

sti ; riabilita le INT masch.

Supponiamo, invece, di voler leggere lo status byte corrente del


Timer 0 - PIT 1 con il metodo read-back; in questo caso sappiamo che
il control word register assume l'aspetto mostrato in Figura 8 per
cui si ottiene:
107
CW = 11100010b = E2h

Infatti, stiamo richiedendo la lettura del Timer 0 (001b), la lettura


del byte di stato (CNT=1, STAT=0) e il comando read-back (11b); si
ricordi, inoltre, che il bit in posizione 0 deve valere 0.

Convertendo il tutto in Assembly otteniamo il seguente codice:

%assign T0P1_PORT 40h ; porta Timer 0 - PIT 1


%assign CWP1_PORT 43h ; porta CWR PIT 1

cli ; disabilita le INT masch.

mov al, 11100010b ; Control Word


out CWP1_PORT, al ; scrive nel PIT 1

in al, T0P1_PORT ; lettura Status Byte

sti ; riabilita le INT masch.

Una volta ottenuto il byte di stato, siamo in grado di sapere con


quale metodo è stato inizializzato il contatore del timer (ad
esempio, LSB+MSB); tale informazione è fondamentale per il corretto
svolgimento di una eventuale operazione di lettura del valore
corrente del contatore stesso.

Il fatto che la gestione dei PIT sia relativamente semplice, non


autorizza il programmatore ad utilizzare i timer in modo disinvolto;
infatti, come è stato spiegato in precedenza, tutti i timer sono
impegnati nello svolgimento di compiti particolarmente importanti e,
in alcuni casi, anche vitali per il computer.
Se ad esempio, intercettiamo la INT 08h associata alla IRQ0 del Timer
0 - PIT 1, stiamo impedendo l'aggiornamento dell'orologio del SO e lo
svolgimento di altre importantissime operazioni temporizzate;
analoghe considerazioni per il caso, ancora più delicato, del Timer 1
- PIT 1 utilizzato per il refresh delle RAM dinamiche!
Nel seguito del capitolo vengono presentati una serie di esempi che
illustrano anche come bisogna procedere nel caso in cui si debba per
forza riprogrammare un timer già impegnato in altre operazioni.

5.5 Esempi pratici

Analizziamo ora una serie di esempi pratici che si concentrano


sull'impiego dei timer 0 e 2 del PIT 1.

5.5.1 Visualizzare l'orologio del BIOS

All'offset 006Ch della BDA (e cioè, all'indirizzo logico 0040h:006Ch)


è presente una DWORD nella quale il BIOS memorizza il numero di ticks
generati dal Timer 0 - PIT 1 a partire dalla mezzanotte; tale
informazione viene continuamente aggiornata grazie alla ISR associata
alla INT 08h.

108
Con l'ausilio della libreria COMLIB possiamo allora visualizzare
facilmente l'orologio del BIOS in questo modo:

mov ax, 0040h ; trasferisce il Seg(0040h)


mov es, ax ; in ES
mov dx, 0400h ; riga 4, colonna 0
call hideCursor ; nasconde il cursore

biostime_loop:
mov eax, [es:006Ch] ; lettura orologio
call writeUdec32 ; visualizzazione lettura

mov ah, 01 ; servizio Return keyboard status


int 16h ; chiama il BIOS
jz biostime_loop ; controllo loop

call showCursor ; ripristina il cursore

La gestione del loop è affidata al servizio BIOS n.01h il quale


verifica se l'utente ha premuto o meno un tasto; le caratteristiche
di tale servizio vengono illustrate in Figura 18

Figura 18 - Servizio n. 01h della INT 16h


INT 16h - Servizio n. 01h - Return keyboard status:
verifica se l'utente ha premuto un tasto.

Argomenti richiesti:
AH = 01h (servizio Return keyboard status)

Valori restituiti:
ZF = 0 se l'utente ha premuto un tasto
AL = codice ASCII del tasto premuto
AH = codice di scansione del tasto premuto
ZF = 1 se l'utente non ha premuto un tasto

Ricordando che il Timer 0 genera 18.2 ticks ogni secondo, una volta
letto il contenuto corrente (che possiamo chiamare NTICKS)
dell'orologio del BIOS possiamo ricavare il numero di secondi
trascorsi dalla mezzanotte attraverso la divisione NTICS/18.2; a
questo punto diventa semplicissimo ricavare l'ora corrente.

Le stesse informazioni che abbiamo ricavato con l'accesso diretto


all'indirizzo logico 0040h:006Ch, possono essere ottenute attraverso
il servizio BIOS n.00h (Read current time) della INT 1Ah; tale
servizio restituisce in CX:DX il valore corrente letto dall'indirizzo
logico 0040h:006Ch.

5.5.2 Visualizzare i ticks del Timer 0 - PIT 1

Per visualizzare i ticks generati dal Timer 0 - PIT 1 esiste un


metodo molto semplice; a tale proposito, analizziamo il meccanismo
con il quale viene gestita la IRQ0:

1) Mentre un programma è in esecuzione, il Timer 0 genera una IRQ0;


2) il PIC Master invia alla CPU una richiesta di chiamata della INT
109
08h;
3) la CPU interrompe il programma in esecuzione e chiama la INT 08h;
4) la ISR della INT 08h esegue i propri compiti e poi chiama una INT
1Ch;
5) la ISR della INT 1Ch svolge il proprio lavoro e restituisce il
controllo alla ISR della INT 08h;
6) la ISR della INT 08h invia un EOI al PIC Master e restituisce il
controllo;
7) la CPU riavvia il programma precedentemente interrotto.

Ciò che dobbiamo fare consiste allora nell'intercettare la INT 1Ch;


tale interruzione è a completa disposizione dei programmi i quali
possono così servirsi dei ticks del Timer 0 (alla frequenza fissa di
18.2 Hz) per eseguire operazioni temporizzate.

Il programma di Figura 19 intercetta, appunto, la INT 1Ch attraverso


una ISR che si occupa di visualizzare i ticks prodotti dal Timer 0; a
tale proposito, viene costantemente aggiornato un apposito contatore
dei ticks.

Figura 19 - File INT1CH.ASM


;--------------------------------------------------;
; file int1ch.asm ;
; Visualizz. ticks Timer 0 - PIT 1 via INT 1Ch ;
;--------------------------------------------------;
; nasm -f obj int1ch.asm ;
; tlink /t int1ch.obj + comlib.obj ;
; (oppure link /map /tiny int1ch.obj + comlib.obj) ;
;--------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "comlib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign INT_1Ch 1Ch ; INT 1Ch vector

;################ segmento unico ##################

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

resb 0100h ; libera 256 byte per il PSP

..start: ; entry point

;------- inizio blocco principale istruzioni ------

call hideCursor ; nasconde il cursore


call clearScreen ; pulisce lo schermo

; installazione nuova ISR per INT 1Ch

cli ; disabilita le int. mascherabili

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h

110
mov eax, [es:(INT_1Ch * 4)] ; legge il vecchio vettore 1Ch
mov [old_int1ch], eax ; e lo salva in old_int1ch

mov ax, cs ; AX = Seg(new_int1ch)


shl eax, 16 ; sposta nella WORD alta di EAX
mov ax, new_int1ch ; AX = Offset(new_int1ch)
mov [es:(INT_1Ch * 4)], eax ; installa il nuovo vettore 1Ch

sti ; riabilita le int. mascherabili

; loop principale del programma

mov dx, 0400h ; riga 4, colonna 0

timerticks_loop:
mov ax, [counter] ; AX = contatore
call writeUdec16 ; visualizza il contatore

mov ah, 01h ; servizio Return keyboard status


int 16h ; chiama il BIOS
jz timerticks_loop ; controllo loop

; ripristino vecchia ISR per INT 1Ch

cli ; disabilita le int. mascherabili

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [old_int1ch] ; EAX = indirizzo vecchio vettore 1Ch
mov [es:(INT_1Ch * 4)], eax ; ripristina il vecchio vettore 1Ch

sti ; riabilita le int. mascherabili

call showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;----- inizio definizione variabili statiche ------

align 4, db 0 ; allinea alla DWORD

old_int1ch dd 0 ; indirizzo vecchio vettore 1Ch


counter dw 0 ; contatore dei ticks

;------- fine definizione variabili statiche ------

;---------- inizio definizione procedure ----------

; nuova ISR per INT 1Ch

new_int1ch:

push ds ; preserva il registro DS

push cs ; copia CS
pop ds ; in DS

inc word [counter] ; incremento contatore ad ogni tick


111
pop ds ; ripristina DS
iret ; return from interrupt

;----------- fine definizione procedure -----------

;##################################################
Cronometrando l'output di questo programma si può constatare che,
effettivamente, vengono generati 18.2 ticks ogni secondo; per
maggiore semplicità conviene provare ad effettuare un cronometraggio
di 10 secondi che dovrebbe produrre circa 18.2*10=182 ticks.

Un aspetto fondamentale da considerare riguarda il fatto che la INT


1Ch viene chiamata dalla ISR della INT 08h; è molto probabile allora
che, quando la procedura new_int1ch riceve il controllo, DS non stia
puntando a COMSEGM. Ne consegue che, se vogliamo accedere ai dati del
nostro programma dall'interno di new_int1ch, dobbiamo prendere le
opportune precauzioni; nell'esempio di Figura 19 la soluzione
adottata è:

push cs ; copia CS
pop ds ; in DS

Questa tecnica funziona in quanto new_int1ch è stata definita


chiaramente all'interno del blocco COMSEGM; di conseguenza, quando la
CPU chiama new_int1ch abbiamo sicuramente CS=COMSEGM e quindi le
precedenti due istruzioni pongono anche DS=COMSEGM.

Una ulteriore considerazione riguarda il fatto che, come si può


facilmente intuire, la nostra ISR che intercetta la INT 1Ch deve
svolgere il proprio lavoro nel minor tempo possibile; in questo modo
evitiamo di rallentare l'esecuzione della ISR associata alla INT 08h.

5.5.3 Riprogrammare il Timer 0 - PIT 1

Nei limiti del possibile, si raccomanda vivamente di sfruttare la INT


1Ch per lo svolgimento di operazioni temporizzate da parte dei propri
programmi; in questo modo, si evita di dover intercettare la INT 08h,
cosa che impedirebbe al BIOS e al SO di gestire compiti
delicatissimi.
Può capitare però che un programma abbia la necessità di svolgere
determinate operazioni ad una frequenza diversa da 18.2 ticks al
secondo; in tal caso, si può anche optare per la riprogrammazione del
Timer 0 - PIT 1 prendendo però tutte le opportune precauzioni.
La tecnica da adottare consiste nel fare in modo che la nostra ISR,
dopo aver intercettato direttamente la INT 08h, provveda essa stessa
a chiamare al momento opportuno la vecchia ISR; ovviamente, la
chiamata della vecchia ISR deve avvenire approssimativamente 18.2
volte al secondo in modo da garantire che tutte le operazioni
temporizzate, gestite dal BIOS e dal SO, si svolgano in modo corretto
(e, soprattutto, nei tempi prestabiliti)!

Supponiamo, ad esempio, di voler scrivere un programma che visualizza


50 asterischi al secondo sullo schermo; a tale proposito, possiamo
riprogrammare il Timer 0 - PIT 1 in modo che lavori nel Modo 2 ad una
112
frequenza di 50 Hz.
In sostanza, vogliamo ottenere:

f = 1193181 / N = 50 Hz

Il contatore dovrà quindi essere inizializzato al valore:

N = 1193181 / 50 = 23863.62

Questo valore può essere arrotondato a 23864 che in esadecimale si


scrive 5D38h.

Osserviamo ora che:

50 / 18.2 = 2.75

La frequenza di 50 Hz è quindi circa il triplo di 18.2 Hz; ciò


significa che la nostra ISR, ogni tre chiamate, deve provvedere a
chiamare a sua volta la vecchia ISR!

Il programma di Figura 20 traduce in pratica tutte le considerazioni


appena esposte.

Figura 20 - File INT08H.ASM


;--------------------------------------------------;
; file int08h.asm ;
; Riprogrammazione Timer 0 - PIT 1 a 50 Hz ;
;--------------------------------------------------;
; nasm -f obj int08h.asm ;
; tlink /t int08h.obj + comlib.obj ;
; (oppure link /map /tiny int08h.obj + comlib.obj) ;
;--------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "comlib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign INT_08h 08h ; INT 08h vector

%assign MPICP0 20h ; porta P0 PIC Master


%assign MPICP1 21h ; porta P1 PIC Master
%assign IRQ0_MASK 00000001b ; mask-on per la IRQ0
%assign IRQ0_UNMASK 11111110b ; mask-off per la IRQ0

%assign T0P1_PORT 40h ; porta Timer 0 - PIT 1


%assign CWP1_PORT 43h ; porta CWR PIT 1

;################ segmento unico ##################

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

resb 0100h ; libera 256 byte per il PSP

..start: ; entry point

113
;------- inizio blocco principale istruzioni ------

call hideCursor ; nasconde il cursore


call clearScreen ; pulisce lo schermo

; riprogrammazione Timer 0 e installazione nuova ISR per INT 08h

in al, MPICP1 ; AL = IMR PIC Master


or al, IRQ0_MASK ; pone a 1 il bit 0 (mask)
out MPICP1, al ; scrive OCW1 nel PIC Master

mov al, 00110100b ; CW = Timer 0, Modo 2, LSB+MSB


out CWP1_PORT, al ; scrive la Control Word nel PIT 1

mov al, 38h ; LSB contatore


out T0P1_PORT, al ; scrive LSB nel Timer 0
mov al, 5Dh ; MSB contatore
out T0P1_PORT, al ; scrive MSB nel timer 0

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [es:(INT_08h * 4)] ; legge il vecchio vettore 08h
mov [old_int08h], eax ; e lo salva in old_int08h

mov ax, cs ; AX = Seg(new_int08h)


shl eax, 16 ; sposta nella WORD alta di EAX
mov ax, new_int08h ; AX = Offset(new_int08h)
mov [es:(INT_08h * 4)], eax ; installa il nuovo vettore 08h

in al, MPICP1 ; AL = IMR PIC Master


and al, IRQ0_UNMASK ; pone a 0 il bit 0 (unmask)
out MPICP1, al ; scrive OCW1 nel PIC Master

; loop principale del programma

asterisk_loop:

mov ah, 01h ; servizio Return keyboard status


int 16h ; chiama il BIOS
jz asterisk_loop ; controllo loop

; riprogrammazione Timer 0 e ripristino vecchia ISR per INT 08h

in al, MPICP1 ; AL = IMR PIC Master


or al, IRQ0_MASK ; pone a 1 il bit 0 (mask)
out MPICP1, al ; scrive OCW1 nel PIC Master

mov al, 00110100b ; CW = Timer 0, Modo 2, LSB+MSB


out CWP1_PORT, al ; scrive la Control Word nel PIT 1

xor al, al ; LSB = MSB = 0


out T0P1_PORT, al ; scrive LSB nel Timer 0
out T0P1_PORT, al ; scrive MSB nel timer 0

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [old_int08h] ; EAX = indirizzo vecchio vettore 08h
mov [es:(INT_08h * 4)], eax ; ripristina il vecchio vettore 08h

in al, MPICP1 ; AL = IMR PIC Master


and al, IRQ0_UNMASK ; pone a 0 il bit 0 (unmask)
out MPICP1, al ; scrive OCW1 nel PIC Master
114
call showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;----- inizio definizione variabili statiche ------

align 4, db 0 ; allinea alla DWORD

old_int08h dd 0 ; indirizzo vecchio vettore 08h


counter dw 0 ; contatore dei ticks
asterisco db "*$" ; stringa asterisco

;------- fine definizione variabili statiche ------

;---------- inizio definizione procedure ----------

; nuova ISR per INT 08h

new_int08h:

push ax ; preserva AX
push dx ; preserva DX
push ds ; preserva DS

push cs ; copia CS
pop ds ; in DS

mov dx, asterisco ; DS:DX punta al messaggio


mov ah, 09h ; servizio DOS Display String
int 21h ; visualizza la stringa

inc word [counter] ; incremento contatore ad ogni tick


cmp word [counter], 2 ; il contatore ha raggiunto 2 ?
jb send_eoi ; no
mov word [counter], 0 ; azzera il contatore
pushf ; salva FLAGS nello stack
call far [old_int08h] ; chiama la vecchia ISR
jmp short end_isr08h ; salta l'invio dell'EOI
send_eoi:
mov al, 20h ; OCW3 = specific EOI
out MPICP0, al ; scrive OCW3 nel PIC Master
end_isr08h:

pop ds ; ripristina DS
pop dx ; ripristina DX
pop ax ; ripristina AX
iret ; return from interrupt

;----------- fine definizione procedure -----------

;##################################################
Notiamo subito che al posto delle istruzioni CLI e STI si ricorre
all'accesso diretto all'interrupt mask register del PIC Master; in
questo modo si agisce solamente sulla linea riservata alla IRQ0 (in
ogni caso, le istruzioni CLI e STI vanno ugualmente bene).

115
Per sapere quando chiamare la vecchia ISR, si utilizza un contatore
(counter) che viene inizializzato a 0; quando counter raggiunge il
valore 2, viene azzerato e si procede alla chiamata con le classiche
istruzioni (che simulano il procedimento seguito dalla CPU per la
chiamata di una ISR):

pushf ; salva FLAGS nello stack


call far [old_int08h] ; chiama la vecchia ISR

In base al fatto che questa volta stiamo gestendo direttamente una


interruzione hardware, se la vecchia ISR deve essere chiamata, viene
lasciato ad essa anche il compito di mandare un EOI al PIC Master; se
la vecchia ISR non deve essere chiamata, tale compito deve essere
svolto dalla nostra ISR!

All'interno di new_int08h, per il registro DS valgono le analoghe


considerazioni fatte per l'esempio di Figura 19; nel caso di un
eseguibile in formato EXE, per accedere ai dati presenti in un
segmento chiamato, ad esempio, DATASEGM, avremmo potuto scrivere:

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS

In un eseguibile in formato COM, invece, non possiamo fare


riferimento a segmenti rilocabili; dobbiamo quindi necessariamente
seguire la tecnica illustrata in Figura 19 e in Figura 20.

5.5.3 Gestione dello speaker attraverso il Timer 2 - PIT 1

La possibilità di generare suoni attraverso il circuito di Figura 16


è legata al fatto che in fisica si definisce suono un fenomeno di
propagazione di onde meccaniche all'interno di un mezzo elastico
come, ad esempio, l'aria; ogni volta che una persona parla, esercita
una compressione sulle molecole che si trovano davanti alla bocca.
Queste molecole, a loro volta, comprimono le molecole adiacenti e
generano così una reazione a catena che si propaga nell'aria; la
reazione a catena è costituita da un susseguirsi di compressioni e
rarefazioni assimilabile ad un segnale periodico avente una
determinata frequenza (un suono non riconducibile ad un segnale
periodico viene definito rumore).
L'orecchio umano è una vera e propria antenna in grado di percepire
tutti i suoni la cui frequenza è compresa tra 10 Hz e 20000 Hz (tali
estremi possono differire da persona a persona e variano anche in
funzione dell'età); l'apparato uditivo converte il tutto in un
segnale elettrico il quale, giungendo al cervello, provoca la
sensazione del suono.
Nel gergo degli audiofili lo spettro delle frequenze "udibili" viene
suddiviso in tre principali categorie definite: bassi, medi e acuti;
con questa terminologia si indica la cosiddetta tonalità del suono.
Più rigorosamente, l'insieme delle tonalità viene suddiviso in ottave
rappresentate dagli indici 0, 1, 2, etc; l'ottava 0 viene definita
ottava base, mentre le ottave successive vengono definite prima
ottava, seconda ottava e così via.
Ogni ottava comprende 12 cosiddetti semitoni rappresentati dai
116
seguenti simboli (il simbolo # si legge diesis):

DO, DO#, RE, RE#, MI, FA, FA#, SOL, SOL#, LA, LA#, SI

I 7 semitoni principali (DO, RE, MI, FA, SOL, LA, SI) vengono
definiti note musicali; gli altri semitoni hanno frequenze intermedie
tra quelle dei semitoni adiacenti.
Si possono calcolare facilmente le frequenze di qualsiasi semitono
considerando le seguenti convenzioni:

* la nota di riferimento è il LA della terza ottava ed ha una


frequenza pari a 440 Hz;
* il rapporto tra la frequenza di un semitono e la frequenza del
semitono precedente è pari alla radice dodicesima di 2;
* il rapporto tra la frequenza di un semitono e la frequenza
dell'omonimo semitono dell'ottava precedente è pari a 2.

Sulla base delle considerazioni appena esposte, possiamo intuire che


un metodo semplicissimo per la generazione di suoni attraverso lo
speaker del PC consiste nell'inviare a tale dispositivo un segnale
periodico la cui frequenza deve appartenere allo spettro
dell'udibile; in un caso del genere, il nucleo dell'elettrocalamita
posta al centro dello speaker comincia a muoversi trascinando con se
la "membrana" le cui vibrazioni innescano un fenomeno di propagazione
di onde meccaniche nell'aria.
Un caso molto importante di segnale periodico "sonoro" è
rappresentato dalla cosiddetta sinusoide di Figura 21, così chiamata
in quanto la si ottiene graficamente dalla funzione y=sin(x); i suoni
di questo genere vengono definiti puri.

Figura 21 - Grafico di y = sin(x)

Assegnando l'opportuna frequenza al segnale di Figura 21, otteniamo


il semitono desiderato; variando continuamente la frequenza del
segnale, possiamo ottenere una sequenza di semitoni che, nel loro
insieme, creano un effetto musicale.

Esaminando il circuito di Figura 16 si può notare che l'unico segnale


periodico a nostra disposizione, assimilabile a quello di Figura 21,
è costituito dall'onda quadra ottenibile dal Timer 2 - PIT 1
programmato nel Modo 3 (square wave mode); chiaramente, come è facile
intuire, i risultati ottenibili con tale onda quadra sono piuttosto

117
grossolani.

Vediamo subito un esempio pratico che, in ogni caso, ha una notevole


importanza didattica; infatti, lo scopo principale dell'esempio è
quello di mostrare come sia possibile far collaborare tra loro
diversi timer (in questo caso si tratta del Timer 0 e del Timer 2).
La Figura 22 mostra il listato Assembly del programma; questa volta
ci serviamo del formato EXE in modo da rendere più evidente la
separazione tra codice, dati e stack.

Figura 22 - File TIMER2.ASM


;--------------------------------------------------;
; file timer2.asm ;
; gestione speaker attraverso il Timer 2 - PIT 1 ;
;--------------------------------------------------;
; nasm -f obj timer2.asm ;
; tlink timer2.obj + exelib.obj ;
; (oppure link timer2.obj + exelib.obj) ;
;--------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%define STACK_SIZE 0400h ; 1024 byte per lo stack

%assign INT_1Ch 1Ch ; INT 1Ch vector


%assign PIT_FREQ 1193181 ; frequenza di CLK del PIT 1
%assign SPK_PORT 61h ; porta speaker
%assign SPK_ON 00000011b ; bit mask speaker ON
%assign SPK_OFF 11111100b ; bit mask speaker OFF
%assign T2P1_PORT 42h ; porta Timer 2 - PIT 1
%assign CWP1_PORT 43h ; porta CWR PIT 1

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

old_int1ch dd 0 ; indirizzo vecchio vettore 1Ch

; vettore note formato da coppie (durata in ticks, frequenza in Hz)

note dw 30, 480, 50, 1200, 40, 3000, 30, 2500


dw 40, 8000, 40, 7500, 40, 6000, 40, 5500
dw 40, 4000, 40, 3500, 40, 2000, 40, 1500
dw 40, 900, 40, 800, 40, 700, 40, 600
dw 40, 500, 40, 400, 40, 300, 40, 200
dw 40, 100, 20, 300, 20, 500, 20, 700
dw 20, 900, 20, 1100, 20, 1300, 20, 1500
dw 20, 1700, 20, 1900, 20, 2100, 20, 2300

MAX_NOTE equ ($ - note) / 4 ; numero totale note

index_nota dw 0 ; indice nota corrente


durata dw 0 ; durata nota corrente
118
save_port61h db 0 ; stato originale porta 61h
str_info db "Nota n. XXXXX, f = XXXXX Hz, durata = XXXXX ticks", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

; salvataggio stato originale porta 61h

in al, SPK_PORT ; legge la porta 61h


mov [save_port61h], al ; e salva lo stato originale

; installazione nuova ISR per INT 1Ch

cli ; disabilita le INT mascherabili

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [es:(INT_1Ch * 4)] ; legge il vecchio vettore 1Ch
mov [old_int1ch], eax ; e lo salva in old_int1ch

mov ax, cs ; AX = Seg(new_int1ch)


shl eax, 16 ; sposta nella WORD alta di EAX
mov ax, new_int1ch ; AX = Offset(new_int1ch)
mov [es:(INT_1Ch * 4)], eax ; installa il nuovo vettore 1Ch

sti ; riabilita le INt mascherabili

push ds ; copia DS
pop es ; in ES (per writeString)

; ATTENZIONE! Se si ha la necessita' di modificare ES, bisogna preservarne


; il vecchio contenuto; la procedura writeString presuppone, infatti, che
; ES contenga la componente Seg dell'indirizzo della stringa da visualizzare!

; visualizza una stringa di informazioni

mov di, str_info ; es:di punta a str_info


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa

; loop principale del programma

music_loop:

cmp word [index_nota], MAX_NOTE ; altre note da suonare ?


jb music_loop ; controllo loop

; ripristino stato originale porta 61h


119
mov al, [save_port61h] ; AL = stato originale porta 61h
and al, SPK_OFF ; pone a 0 i bit 0 e 1 di AL
out SPK_PORT, al ; scrive nella porta 61h

; ripristino vecchia ISR per INT 1Ch

cli ; disabilita le INT mascherabili

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov eax, [old_int1ch] ; EAX = indirizzo vecchio vettore 1Ch
mov [es:(INT_1Ch * 4)], eax ; ripristina il vecchio vettore 1Ch

sti ; riabilita le INT mascherabili

call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

; nuova ISR per INT 1Ch

new_int1ch:

push eax ; preserva EAX


push ebx ; preserva EBX
push ecx ; preserva ECX
push edx ; preserva EDX
push ds ; preserva DS
push es ; preserva ES

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS
mov es, ax ; e in ES

cmp word [durata], 0 ; la nota dura ancora ?


ja exit_isr1ch ; si

mov al, [save_port61h] ; AL = stato originale porta 61h


and al, SPK_OFF ; pone a 0 i bit 0 e 1 di AL
out SPK_PORT, al ; scrive nella porta 61h

mov eax, PIT_FREQ ; EAX = frequenza di CLK del PIT 1


xor edx, edx ; prepara EDX per la divisione
mov bx, [index_nota] ; indice vettore note
shl bx, 2 ; offset durata vettore note
mov cx, [note+bx] ; CX = durata prossima nota
mov [durata], cx ; salva in durata
add bx, 2 ; offset frequenza vettore note
movzx ecx, word [note+bx] ; ECX = frequenza prossima nota
div ecx ; valore iniziale contatore (N)
mov cx, ax ; salva in CX

mov al, 10110110b ; CW = Timer 2, Modo 3, LSB+MSB


out CWP1_PORT, al ; scrive la Control Word nel PIT 1

120
mov al, cl ; LSB contatore
out T2P1_PORT, al ; scrive LSB nel Timer 2
mov al, ch ; MSB contatore
out T2P1_PORT, al ; scrive MSB nel timer 2

mov al, [save_port61h] ; AL = stato originale porta 61h


or al, SPK_ON ; pone a 1 i bit 0 e 1 di AL
out SPK_PORT, al ; scrive nella porta 61h

mov ax, [index_nota] ; AX = indice nota corrente


mov bx, ax ; salva in BX
mov dx, 0408h ; riga 4, colonna 8
call far writeUdec16 ; mostra l'indice

shl bx, 2 ; prossima coppia vettore note


mov ax, [note+bx+2] ; AX = frequenza nota corrente
mov dx, 0413h ; riga 4, colonna 19
call far writeUdec16 ; mostra la frequenza

inc word [index_nota] ; incremento indice vettore note

exit_isr1ch:
mov ax, [durata] ; AX = durata nota corrente
mov dx, 0426h ; riga 4, colonna 38
call far writeUdec16 ; mostra la durata

dec word [durata] ; decremento durata ad ogni tick

pop es ; ripristina ES
pop ds ; ripristina DS
pop edx ; ripristina EDX
pop ecx ; ripristina ECX
pop ebx ; ripristina EBX
pop eax ; ripristina EAX
iret ; return from interrupt

;-------------- fine blocco procedure -------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Il funzionamento del programma è molto semplice e, allo stesso tempo,
molto efficiente; come si può notare, il brano da eseguire è
memorizzato in un vettore note costituito da coppie (durata della
nota in ticks, frequenza della nota in Hz).
La ISR della INT 1Ch (legata alla IRQ0 del Timer 0) viene chiamata,
come sappiamo, 18.2 volte al secondo; al suo interno è presente il
vero "motore" del programma.
Questa ISR, al momento opportuno, riprogramma il Timer 2 nel Modo 3
in modo che venga generata un'onda quadra alla frequenza della
prossima nota da suonare; subito dopo viene "aperta" la connessione
con la porta 61h in modo che il segnale giunga allo speaker.
Per inizializzare il contatore del Timer 2 viene usato il solito
metodo; supponendo, ad esempio, di voler generare un'onda quadra alla
frequenza di 1200 Hz, abbiamo:

121
f = 1193181 / N = 1200 Hz

Il contatore dovrà quindi essere inizializzato al valore:

N = 1193181 / 1200 = 994.3175 (approssimabile a 994).

Ad ogni chiamata la ISR decrementa di 1 la durata della nota corrente


in modo che la nota stessa venga eseguita per un certo intervallo di
tempo (in ticks); la variabile durata viene quindi decrementata 18.2
volte al secondo e quando raggiunge il valore 0 indica alla ISR che è
il momento di riprogrammare il Timer 2 con una nuova frequenza.
La stessa ISR si occupa anche di visualizzare tutte le informazioni
relative all'indice della nota corrente, la frequenza in Hz e la
durata in ticks della nota stessa; nell'esempio, vengono eseguite 32
note.

L'aspetto notevole del programma di Figura 22 è che tutta la parte


relativa alla temporizzazione e all'esecuzione del suono viene
gestita "in background" dall'hardware del PC (cioè, dal PIT 1);
questa tecnica è piuttosto efficiente e permette anche al nostro
programma di eseguire altri compiti senza interferire con la gestione
del suono!

Nota importante.
Purtroppo, il programma di Figura 22 produce suoni udibili
solo sui PC relativamente vecchi; ciò accade in quanto, sui
nuovi PC dotati di potenti schede audio, lo speaker esiste
ancora solo per compatibilità con gli standard imposti dalla
IBM ed è costituito da un dispositivo miniaturizzato
assolutamente incapace di emettere qualsiasi suono!
Per ovviare a tale inconveniente, il programma di Figura 22
mostra anche visivamente ciò che sta accadendo; in questo
modo si ha almeno la certezza che tutto stia funzionando a
dovere.

Sui PC dotati di speaker udibile, si può osservare che i


segnali a frequenze relativamente alte (oltre gli 8000 Hz)
producono suoni che si riescono a sentire con una certa
difficoltà; ciò è causato dalla scarsa qualità dello speaker
il quale, per evitare danni dovuti a frequenze troppo alte,
viene protetto con il filtro passa basso visibile in Figura
16 (tale filtro limita, appunto, la frequenza al valore
massimo di circa 10000 Hz).

Bibliografia

Intel - 82C54 CHMOS PROGRAMMABLE INTERVAL TIMER


(23124406.pdf)

Intel - 80C186EB/80C188EB Microprocessor User's Manual (Capitolo 9)


(27083003.pdf)

122
Capitolo 6 La memoria base del PC
Con il termine base memory (memoria base) si indica l'unico Mb di RAM
direttamente indirizzabile attraverso l'address bus a 20 linee delle
vecchie CPU 80x86; tra i vari modelli di tali CPU si possono citare,
in particolare, la 8086, la 80186 e la 8088. Nel seguito del capitolo
si utilizzerà la sigla 8086 per rappresentare una qualunque CPU con
address bus a 20 linee.
Come sappiamo, con un address bus a 20 linee possiamo accedere ad un
massimo di 220=1048576 locazioni di memoria, ciascuna delle quali
occupa 1 byte (sulla piattaforma hardware 80x86); a tali 1048576 byte
possiamo assegnare tutti gli indirizzi fisici compresi tra 00000h e
FFFFFh.
Nella sezione Assembly Base abbiamo visto che per rendere possibile
l'accesso a tutti i 1048576 byte della RAM attraverso i registri a 16
bit di cui erano dotate le vecchie CPU, si è deciso di ricorrere alla
cosiddetta segmentazione della memoria; a tale proposito, i 1048576
byte vengono suddivisi in blocchi da 16 byte ciascuno, chiamati
paragrafi.
Complessivamente, abbiamo quindi un totale di:

1048576 / 16 = 220 / 24 = 220 - 4 = 216 = 65536 paragrafi

L'accesso ai 1048576 indirizzi fisici avviene, via software,


attraverso i cosiddetti indirizzi logici rappresentati da coppie
Seg:Offset; entrambe le componenti, Seg e Offset, hanno un'ampiezza
di 16 bit.
La componente Seg rappresenta uno dei 65536 possibili paragrafi (da
0000h a FFFFh); la componente Offset rappresenta uno spiazzamento,
compreso tra 0000h e FFFFh, relativo al paragrafo specificato da Seg.

La CPU utilizza un metodo semplicissimo per convertire un indirizzo


logico in un indirizzo fisico; infatti, osservando che la componente
Seg coincide con il numero di paragrafo e che la componente Offset è
uno spiazzamento relativo a Seg, si ottiene:

indirizzo_fisico = (Seg * 16) + Offset

Da quanto detto segue immediatamente che ogni paragrafo segna


l'inizio di un blocco di memoria da 65536 byte; tale blocco prende il
nome di segmento di memoria. Gli ultimi 4095 segmenti di memoria
vengono definiti incompleti in quanto hanno una dimensione inferiore
a 65536 byte; ciò è dovuto al fatto che il primo Mb di RAM termina
all'indirizzo fisico FFFFFh per cui, ad esempio, il segmento di
memoria n. FFFFh ha una lunghezza che viene troncata a 16 byte.
Infatti:

(FFFFh * 16) + 16 = 1048575 = FFFFFh

Un'altra conseguenza evidente delle cose appena esposte è che i


segmenti di memoria, essendo allineati al paragrafo, hanno indirizzi
fisici iniziali del tipo XXXX0h (multipli interi di 16);
normalizzando tali tipi di indirizzo fisico otteniamo XXXXh:0000h.
Possiamo affermare allora che il valore contenuto nella componente
123
Seg dell'indirizzo logico normalizzato da cui inizia un segmento di
memoria, coincide con il numero di paragrafo che identifica il
segmento stesso; ad esempio, il segmento di memoria n.3FC8h è
identificato dal paragrafo n.3FC8h e inizia dall'indirizzo logico
normalizzato 3FC8h:0000h.

Per maggiori dettagli sui concetti appena esposti si consiglia la


rilettura del Capitolo 9, sezione Assembly Base.

6.1 Organizzazione della memoria base sotto DOS

La tecnica appena descritta permette di accedere ad uno qualunque dei


1048576 byte presenti nell'unico Mb indirizzabile direttamente dalla
8086; in sostanza, attraverso gli indirizzi logici possiamo accedere
a tutti gli indirizzi fisici "realmente" presenti nella RAM.
Questa situazione giustifica la definizione di modalità reale per
indicare la modalità operativa con la quale i programmi che girano su
una 8086 accedono alla RAM attraverso le coppie Seg:Offset; come
sappiamo, il SO che ha dominato la modalità reale è stato il DOS.

La Figura 1, che già conosciamo, illustra il modo con il quale il DOS


organizza la memoria base.

Figura 1 - Memoria base sotto DOS

124
Ricapitoliamo brevemente il significato dei vari blocchi presenti in
Figura 1; per la delimitazione dei blocchi stessi ci serviamo degli
indirizzi fisici a 20 bit.

6.1.1 Vettori di interruzione - da 00000h a 003FFh

In seguito ad una serie di convenzioni stabilite dai produttori di


hardware e di SO, è stato deciso che i PC debbano riservare un'area
della RAM sufficiente a contenere 256 vettori di interruzione;
ciascun vettore è un indirizzo logico Seg:Offset che richiede
16+16=32 bit (4 byte) e quindi, l'area complessiva della RAM
riservata ai vettori di interruzione è pari a:

256 * 4 = 1024 byte = 400h byte

Quest'area deve essere rigorosamente posizionata nella parte iniziale


della RAM compresa tra gli indirizzi fisici 00000h e 003FFh; ciascun
vettore di interruzione viene identificato da un indice compreso tra
0 e 255 (in esadecimale, tra 00h e FFh).

Bisogna ribadire un aspetto molto importante relativo al fatto che,


in modalità reale, i vettori di interruzione rappresentano il metodo
attraverso il quale i programmi comunicano con il DOS e con il BIOS;
un programma che abbia la necessità di richiedere un servizio del DOS
o del BIOS, non deve fare altro che chiamare l'opportuno vettore di
interruzione!

Nel seguito del capitolo esamineremo, in particolare, il vettore di


interruzione n. 21h attraverso il quale si possono ottenere numerosi
servizi offerti dal DOS; proprio per questo motivo, tale vettore
viene definito vettore dei servizi DOS.

6.1.2 Area dati ROM-BIOS - da 00400h a 004FFh

Nella sezione Assembly Base è stato spiegato che quando si accende il


computer, il controllo passa ad una serie di programmi memorizzati
nella ROM del PC; questi programmi comprendono, in particolare, il
POST che esegue l'autodiagnosi relativa a tutto l'hardware del
computer.
Uno dei compiti fondamentali svolti dai programmi della ROM consiste
nella raccolta di una serie di importanti informazioni relative alle
caratteristiche generali dell'hardware del PC; queste informazioni
vengono messe a disposizione dei programmi e comprendono, ad esempio,
gli indirizzi delle porte seriali e parallele, lo stato corrente
dell'hard disk e del floppy disk, la modalità video corrente, etc.
Tutte queste informazioni vengono inserite in un'area della RAM
chiamata BDA (Bios Data Area) e formata da 256 byte (100h byte); per
convenzione quest'area deve essere rigorosamente compresa tra gli
indirizzi fisici 00400h e 004FFh.

6.1.3 Area comunicazioni del DOS - da 00500h a 006FFh

Come già sappiamo, l'ultimo compito svolto dai programmi della ROM in
125
fase di avvio del computer, consiste nel cercare ed eventualmente
eseguire un programma chiamato boot loader; il boot loader ha
l'importante compito di caricare in memoria il SO che può essere il
DOS, Windows, Linux, BSD, etc.
Nel caso particolare del DOS, una volta che tale SO ha ricevuto il
controllo, esegue a sua volta una serie di inizializzazioni; in
questa fase il DOS raccoglie anche una serie di importanti
informazioni globali relative al SO.
Tutte queste informazioni vengono messe a disposizione dei programmi
in un'area della RAM formata da 512 byte (200h byte); per convenzione
quest'area deve essere rigorosamente compresa tra gli indirizzi
fisici 00500h e 006FFh.

6.1.4 Kernel del DOS, Device Driver, etc - da 00700h a ?????h

In quest'area viene caricato, in particolare, il kernel del DOS


formato dai due file IO.SYS e MSDOS.SYS; sempre in quest'area trovano
posto svariati device driver usati dal DOS e alcuni buffer interni
(aree usate dal DOS per depositare informazioni temporanee).
Come si può notare in Figura 1, quest'area parte rigorosamente
dall'indirizzo fisico 00700h e assume una dimensione variabile; ciò è
dovuto al fatto che, a seconda della configurazione del proprio
computer, è possibile ridurre le dimensioni di quest'area spostando
porzioni del DOS in memoria alta o nella memoria superiore e
liberando così spazio nella memoria convenzionale. Sui computer
dotati di un vero DOS (o di un emulatore come DOSEmu) è presente in
C:\ un file chiamato CONFIG.SYS; all'interno di questo file si
possono trovare i comandi che permettono, appunto, di liberare spazio
nella memoria convenzionale.
Ad esempio, il comando:

DOS=UMB

se è presente permette di spostare parti del DOS in memoria


superiore; la sigla UMB sta per Upper Memory Blocks (blocchi di
memoria superiore).

Il comando:

DOS=HIGH

se è presente permette di spostare parti del DOS in memoria alta.


In generale, l'area della RAM destinata ad ospitare il kernel del
DOS, i device driver e i buffer interni occupa alcune decine di Kb.

6.1.5 Disponibile per i programmi DOS - da ?????h a 9FFFFh

Tutta l'area rimanente nella memoria convenzionale è disponibile per


i programmi DOS; quest'area inizia quindi dalla fine del blocco
precedentemente descritto e termina all'indirizzo fisico 9FFFFh.
L'indirizzo fisico successivo è A0000h che tradotto in base 10
corrisponde a 655360 (640*1024) e rappresenta la tristemente famosa
barriera dei 640 Kb; un qualunque programma DOS, per poter essere
caricato in memoria ed eseguito, deve avere dimensioni non superiori
126
a 640 Kb.
In realtà un programma da 640 Kb è troppo grande in quanto abbiamo
visto che i primi Kb della RAM sono riservati ai vettori di
interruzione, all'area dati della ROM BIOS, etc; tolte queste aree
riservate restano a disposizione per i programmi circa 600 Kb
effettivi!

6.1.6 Buffer video per la modalità grafica - da A0000h a AFFFFh

L'indirizzo fisico A0000h segna l'inizio della memoria superiore;


teoricamente la memoria superiore è un'area riservata, non
utilizzabile quindi in modo diretto dai programmi DOS.
Nella sezione Assembly Base abbiamo visto che una CPU che opera in
modalità reale 8086 è in grado di indirizzare in modo diretto solo 1
Mb di RAM (memoria base); questo significa che qualsiasi altra
memoria esterna, per poter essere accessibile, deve essere mappata in
qualche zona della memoria base (I/O Memory Mapped). Lo scopo
principale della memoria superiore è proprio quello di contenere
svariati buffer nei quali vengono mappate le memorie esterne come la
memoria video, la ROM BIOS, etc; questi buffer sono aree di scambio
attraverso le quali un programma DOS può comunicare con le memorie
esterne.
L'area compresa tra gli indirizzi fisici A0000h e AFFFFh viene
utilizzata per mappare la memoria video in modalità grafica;
quest'area ha una dimensione pari a:

AFFFFh - A0000h + 1h = 10000h byte = 65536 byte = 64 Kb

Attraverso quest'area un programma DOS può quindi leggere o scrivere


in un blocco da 64 Kb della memoria video grafica; tutto ciò ci fa
intuire che, com'era prevedibile, la segmentazione a 64 Kb della
modalità reale si ripercuote anche sulle memorie esterne.
Un buffer per una memoria esterna può essere paragonato ad un
fotogramma di una pellicola cinematografica; il proiettore fa
scorrere la pellicola mostrando agli spettatori la sequenza dei vari
fotogrammi. Allo stesso modo il programmatore può richiedere la
mappatura nel buffer video della porzione desiderata della memoria
video; questa tecnica permette di accedere (dal buffer video) a tutta
la memoria video disponibile. Proprio per questo motivo, un buffer
come quello descritto prende anche il nome di frame window (finestra
fotogramma) o frame buffer (buffer fotogramma).

6.1.7 Buffer video per la modalità testo in b/n - da B0000h a B7FFFh

L'area della RAM compresa tra gli indirizzi fisici B0000h e B7FFFh
contiene il buffer per l'I/O con la memoria video in modalità testo
per i vecchi monitor in bianco e nero; si tratta quindi di un buffer
da 8000h byte, cioè 32768 byte (32 Kb).

6.1.8 Buffer video per la modalità testo a colori - da B8000h a


BFFFFh

L'area della RAM compresa tra gli indirizzi fisici B8000h e BFFFFh
contiene il buffer per l'I/O con la memoria video in modalità testo a
127
colori; si tratta anche in questo caso di un buffer da 8000h byte,
cioè 32768 byte (32 Kb).

6.1.9 Video ROM BIOS - da C0000h a CFFFFh

In quest'area da 64 Kb viene mappata la ROM BIOS delle schede video


che contiene una numerosa serie di procedure per l'accesso a basso
livello all'hardware di tali dispositivi; le schede video sono ben
presto diventate talmente potenti ed evolute da richiedere un loro
specifico BIOS che permette ai SO di gestire al meglio queste
periferiche.

6.1.10 Disponibile per altri BIOS e buffer - da D0000h a EFFFFh

Quest'area da 128 Kb è disponibile per ospitare ulteriori buffer per


il collegamento con altre memorie esterne; in particolare, in
quest'area viene creata la frame window per l'accesso alle espansioni
di memoria che si utilizzavano ai tempi delle CPU 8086.
Può capitare frequentemente che in quest'area rimangano dei blocchi
liberi di memoria; questi blocchi liberi possono essere utilizzati
dai programmi DOS e in certi casi è persino possibile far girare
programmi DOS in memoria superiore. A tale proposito è necessario
disporre di una CPU 80386 o superiore e di un cosiddetto Memory
Manager (gestore della memoria) che permetta di indirizzare in
modalità reale queste aree riservate; nel mondo del DOS il memory
manager più famoso è senz'altro EMM386.EXE che viene fornito insieme
allo stesso SO.

6.1.11 PC ROM BIOS - da F0000h a FFFFFh

In quest'area da 64 Kb viene mappata la ROM BIOS principale del PC;


in questo modo i programmi DOS possono usufruire di numerosi servizi
offerti dal BIOS per accedere a basso livello all'hardware del
computer.
Come si può notare, quest'area comprende anche i famosi 4095 segmenti
di memoria incompleti; riservando questi segmenti al BIOS, si evita
che i normali programmi DOS possano incappare nel wrap around.

6.2 Suddivisione della memoria base sotto DOS

Dalle considerazioni appena esposte, risulta che la memoria base del


PC viene suddivisa in due aree delimitate dall'indirizzo fisico
A0000h; tali due aree vengono definite:

conventional memory (memoria convenzionale) - da 00000h a 9FFFFh;


upper memory (memoria superiore) - da A0000h a FFFFFh.

Per una precisa scelta dei progettisti del DOS, i programmi che
girano in modalità reale possono accedere in modo diretto solamente
alla memoria convenzionale; in assenza di diverse indicazioni da
parte del programmatore, tutte le operazioni di allocazione e
deallocazione della memoria si riferiscono quindi ai primi 655360
byte di RAM compresi tra gli indirizzi fisici 00000h e 9FFFFh!

128
6.3 Granularità della memoria base sotto DOS

Per motivi di efficienza, il DOS maneggia la memoria base


suddividendola in paragrafi che, come sappiamo, occupano ciascuno 16
byte; la dimensione di 16 byte rappresenta quindi la cosiddetta
granularità della memoria DOS e cioè, l'unità di misura che il DOS
utilizza per i blocchi di memoria allocati dai programmi.
In sostanza, un qualsiasi blocco di memoria DOS ha sempre una
dimensione multipla intera di 16 byte; non è possibile quindi avere
blocchi di memoria DOS che occupano, ad esempio, 11 byte, 351 byte,
etc.

L'unico svantaggio di questa tecnica è rappresentato da un piccolo


spreco di memoria (ad esempio, se ci servono solamente 18 byte,
dobbiamo richiederne per forza 2*16=32); i vantaggi, invece, sono
notevoli e possono essere facilmente dedotti da tutto ciò che abbiamo
visto nella sezione Assembly Base.
In particolare, osserviamo che grazie alla granularità pari a 16
byte, un qualsiasi blocco di memoria DOS parte sempre da un indirizzo
fisico del tipo XXXX0h (multiplo intero di 16) che possiamo associare
all'indirizzo logico normalizzato XXXXh:0000h; la conseguenza è che
l'indirizzo logico normalizzato iniziale di un blocco di memoria DOS
coincide con l'indirizzo logico normalizzato iniziale di un segmento
di memoria e quindi ha sempre la componente Offset che vale 0000h!

Le considerazioni appena esposte permettono al DOS di identificare un


qualsiasi blocco di memoria attraverso la sola componente Seg
dell'indirizzo logico iniziale; infatti, la componente Offset di tale
indirizzo vale implicitamente 0000h.

Quando richiediamo allora al DOS un blocco di memoria, ci viene


restituito un valore a 16 bit che rappresenta proprio la componente
Seg dell'indirizzo logico iniziale del blocco stesso; il DOS non ha
bisogno di specificare anche la componente Offset in quanto essa
vale, implicitamente, 0000h (e ciò deve essere ben chiaro al
programmatore al quale il DOS delega l'importante compito di
rispettare tale convenzione).

Supponiamo, ad esempio, di richiedere al DOS un blocco da 1500


paragrafi di memoria, pari a:

1500 x 16 = 24000 byte = 5DC0h byte

Il DOS risponde affermativamente e ci restituisce, ad esempio, il


valore 3FC8h; ciò significa che il blocco di memoria da noi richiesto
parte dall'indirizzo logico 3FC8h:0000h ed occupa in totale 5DC0h
byte.
Il limite inferiore del nostro blocco è quindi 3FC8h:0000h; il limite
superiore è, invece, 3FC8h:5DBFh (cioè, 3FC8h:(5DC0h-1)). Se non
rispettiamo questi limiti, il nostro programma va a sovrascrivere
aree di memoria riservate ad altri programmi o, peggio ancora, al SO;
nella gran parte dei casi la conseguenza di tutto ciò è un classico
crash!

129
6.4 La catena dei Memory Control Blocks

Per tenere traccia dei vari blocchi di memoria presenti nella RAM, il
DOS assegna a ciascuno di essi una intestazione la quale precede
immediatamente il blocco stesso; ogni intestazione, denominata Memory
Control Block o MCB, occupa 16 byte ed assume la struttura mostrata
in Figura 2 (in versione NASM).

Figura 2 - Memory Control Block


STRUC MemoryControlBlock

IndicatoreBlocco resb 1 ; 'M' = in mezzo, 'Z' = ultimo blocco


ProprietarioBlocco resw 1 ; PSP del proprietario del blocco
DimensioneBlocco resw 1 ; dimensione del blocco in paragrafi
resb 3 ; riservato
NomeBlocco resb 8 ; nome del proprietario del blocco

ENDSTRUC

In sostanza, immediatamente prima di ogni blocco di memoria DOS, è


presente un MCB che contiene la descrizione completa del blocco
stesso; ogni MCB è allineato al paragrafo ed essendo la sua
dimensione pari a 16 byte (1 paragrafo), viene anche garantito il
corretto allineamento al paragrafo del relativo blocco di memoria.

Nel loro insieme, i MCB formano una vera e propria catena; il membro
IndicatoreBlocco della struttura di Figura 2 indica se ci troviamo in
mezzo alla catena o alla fine (ultimo blocco della catena).
Se tale membro contiene il codice ASCII della lettera 'M', significa
che ci troviamo in uno dei possibili blocchi che precedono l'ultimo;
il codice ASCII della lettera 'Z' indica, invece, che ci troviamo
nell'ultimo blocco della catena.
Qualsiasi altro valore indica che la catena dei MCB è stata corrotta!

Il membro ProprietarioBlocco è una WORD che contiene l'eventuale PSP


del programma che ha allocato il relativo blocco di memoria; in tal
caso, come sappiamo, il programma stesso ha a disposizione un program
segment che inizia in memoria dall'indirizzo fisico PSP:0000h. Se,
invece, il blocco di memoria è libero, il membro ProprietarioBlocco
vale 0000h.

Il membro DimensioneBlocco contiene la dimensione in paragrafi del


relativo blocco di memoria (escluso il paragrafo occupato dal MCB);
di conseguenza, la dimensione in byte del blocco di memoria sarà:

DimensioneBlocco * 16

Il membro DimensioneBlocco è molto importante in quanto ci permette


di percorrere l'intera catena dei MCB; infatti, il MCB immediatamente
successivo a quello corrente si troverà al paragrafo:

Paragrafo_MCB_corrente + DimensioneBlocco + 1

Il +1 è necessario per tenere conto del fatto che DimensioneBlocco


130
non comprende il paragrafo occupato dall'intestazione.

Il membro NomeBlocco contiene il nome dell'eventuale programma che ha


allocato il relativo blocco di memoria (si ricordi che nel DOS il
nome di un programma non può essere più lungo di 8 caratteri
eventualmente seguiti da una estensione di 3 caratteri); tale membro
è disponibile solo con le versioni più recenti del DOS.

Dalle considerazioni appena esposte risulta chiaramente che la catena


dei MCB forma una classica lista lineare di elementi; infatti, le
informazioni presenti in ogni MCB ci permettono di scandire
facilmente tutta la catena.
A tale proposito, ci manca solamente una informazione fondamentale
rappresentata dall'indirizzo del primo MCB; tale informazione può
essere ricavata dal servizio n.52h della INT 21h visibile in Figura
3.

Figura 3 - Servizio n. 52h della INT 21h


INT 21h - Servizio n. 52h - Get List of Lists:
restituisce l'indirizzo della List of Lists.

Argomenti richiesti:
AH = 52h (servizio Get List of Lists)

Valori restituiti:
in caso di successo
CF = 0
ES:BX = indirizzo logico Seg:Offset della List of Lists
in caso di insuccesso
CF = 1
AX = codice di errore

Questo servizio restituisce in ES:BX l'indirizzo logico di una


tabella, chiamata List of Lists, contenente una serie di importanti
variabili interne del DOS; per maggiori dettagli sul contenuto di
tale tabella si può fare riferimento, ad esempio, alla Interrupts
list di Ralf Brown.
Un aspetto particolare dell'indirizzo restituito in ES:BX è che esso
non è quello iniziale della List of Lists; infatti, la tabella
comprende anche ben 48 byte di informazioni disposte agli indirizzi
che precedono ES:BX.
In particolare, all'indirizzo ES:(BX-2) è presente una WORD che
rappresenta la componente Seg dell'indirizzo logico iniziale del
primo MCB della catena; ricordando che tutti i MCB sono allineati al
paragrafo, la componente Offset implicita dell'indirizzo logico
iniziale del primo MCB (come di qualsiasi altro MCB) sarà quindi
0000h.

Utilizzando tutte queste informazioni, siamo già in grado di scrivere


un semplice programma che percorre tutti i MCB della catena; ad
esempio, servendoci della libreria COMLIB, possiamo scrivere:

mov ah, 52h ; servizio: Get List of Lists


int 21h ; chiama il DOS

131
mov ax, [es:bx-2] ; AX = Seg del primo MCB
mov es, ax ; copia AX in ES
mov dx, 0400h ; riga 4, colonna 0

mcb_loop:
cmp byte [es:0000h], 'M' ; Indicatore Blocco == 'M' ?
jne exit_loop ; no (fine catena o catena corrotta)

call writeHex16 ; visualizza AX = Seg MCB corrente

add ax, [es:0003h] ; AX = Seg + DimensioneBlocco


inc ax ; AX = AX + 1
mov es, ax ; ES = Seg prossimo MCB
inc dh ; incremento riga
jmp short mcb_loop ; ripete il loop

exit_loop:
call writeHex16 ; visualizza AX = Seg ultimo MCB

Analizzando l'output prodotto da queste istruzioni si può notare che


i vari MCB occupano paragrafi inferiori a A000h, appartenenti quindi
alla conventional memory; nel seguito del capitolo vedremo che, in
realtà, la catena prosegue anche nella upper memory!

6.5 Servizi DOS per la gestione dei blocchi di memoria

I principali servizi offerti dal DOS per la gestione della memoria


base, permettono di allocare, deallocare o ridimensionare un blocco
di memoria; analizziamo tali servizi in dettaglio.

6.5.1 Allocazione di un blocco di memoria

Se un programma ha bisogno di allocare uno o più blocchi di memoria,


può ricorrere al servizio n.48h della INT 21h; le caratteristiche di
tale servizio sono illustrate in Figura 4.

Figura 4 - Servizio n. 48h della INT 21h


INT 21h - Servizio n. 48h - Allocate Memory:
restituisce l'indirizzo del blocco di memoria allocato.

Argomenti richiesti:
AH = 48h (servizio Allocate Memory)
BX = Numero di paragrafi richiesti

Valori restituiti:
in caso di successo
CF = 0
AX = Seg del blocco di memoria allocato
in caso di insuccesso
CF = 1
AX = codice di errore (07h o 08h)
BX = dimensione in paragrafi del più grande blocco libero

Chiamando quindi la INT 21h con AH=48h, stiamo chiedendo al DOS di


riservarci un blocco di memoria la cui dimensione in paragrafi deve

132
essere specificata in BX; se la nostra richiesta viene accolta, si
ottiene CF=0.
In tal caso, AX contiene la componente Seg dell'indirizzo logico da
cui parte il nostro blocco; come sappiamo, la componente Offset di
tale indirizzo vale, implicitamente, 0000h.
In caso di insuccesso si ottiene CF=1, mentre in AX viene restituito
un codice di errore il cui significato viene illustrato più avanti;
il registro BX, invece, contiene la dimensione in paragrafi del più
grande blocco di memoria libero disponibile.

Osserviamo che, se il blocco di memoria che abbiamo allocato ha una


dimensione non superiore a 64 Kb, la sua gestione è molto semplice;
infatti, attraverso la componente Offset a 16 bit, possiamo spaziare
da 0000h a FFFFh, coprendo l'intera estensione del blocco stesso.

Se, però, il blocco di memoria supera i 64 Kb, la sua gestione


diventa più impegnativa a causa della segmentazione a 64 Kb della
memoria; in un caso del genere, infatti, con la componente Offset non
possiamo andare oltre FFFFh in quanto si verificherebbe il wrap
around!
La soluzione consiste allora nel modificare la componente Seg a
seconda delle necessità; a tale proposito, supponiamo di avere
ottenuto dal DOS un blocco di memoria da 128 Kb (2*64 Kb) che parte
dall'indirizzo logico normalizzato 3CF2h:0000h (quindi, il DOS ci ha
restituito Seg=3CF2h).
In tal caso, ricordando che in 64 Kb ci sono 4096 paragrafi, per
gestire i primi 64 Kb del blocco possiamo usare Seg=3CF2h e un Offset
compreso tra 0000h e FFFFh; invece, per gestire la seconda metà del
blocco (cioè, i successivi 64 Kb), possiamo usare Seg=3CF2h+4096 e un
Offset compreso tra 0000h e FFFFh!

6.5.2 Deallocazione di un blocco di memoria

Se un programma ha bisogno di restituire al DOS un blocco di memoria


precedentemente allocato, può ricorrere al servizio n.49h della INT
21h; le caratteristiche di tale servizio sono illustrate in Figura 5.

Figura 5 - Servizio n. 49h della INT 21h


INT 21h - Servizio n. 49h - Free Memory:
libera un blocco di memoria precedentemente allocato.

Argomenti richiesti:
AH = 49h (servizio Free Memory)
ES = Seg del blocco di memoria da liberare

Valori restituiti:
in caso di successo
CF = 0
in caso di insuccesso
CF = 1
AX = codice di errore (07h o 09h)

Chiamando quindi la INT 21h con AH=49h, stiamo chiedendo al DOS di


liberare un blocco di memoria precedentemente allocato dal nostro

133
programma; il registro ES deve contenere la stessa componente Seg
usata dal DOS per identificare il blocco da liberare.
In caso di successo si ottiene CF=0, mentre CF=1 indica che si è
verificato un problema; in tal caso, in AX viene restituito un codice
di errore il cui significato viene illustrato più avanti.

6.5.3 Ridimensionamento di un blocco di memoria già allocato

Se un programma ha bisogno di ridimensionare un blocco di memoria già


allocato in precedenza, può ricorrere al servizio n.4Ah della INT
21h; le caratteristiche di tale servizio sono illustrate in Figura 6.

Figura 6 - Servizio n. 4Ah della INT 21h


INT 21h - Servizio n. 4Ah - Resize Memory Block:
ridimensiona un blocco di memoria precedentemente allocato.

Argomenti richiesti:
AH = 4Ah (servizio Resize Memory Block)
BX = nuova dimensione in paragrafi
ES = Seg del blocco di memoria da ridimensionare

Valori restituiti:
in caso di successo
CF = 0
in caso di insuccesso
CF = 1
AX = codice di errore (07h, 08h o 09h)
BX = numero massimo di paragrafi disponibili

Chiamando quindi la INT 21h con AH=4Ah, stiamo chiedendo al DOS di


ridimensionare un blocco di memoria precedentemente allocato dal
nostro programma; il registro BX deve contenere la nuova dimensione
in paragrafi, mentre il registro ES deve contenere la stessa
componente Seg usata dal DOS per identificare il blocco da
ridimensionare.
In caso di successo si ottiene CF=0, mentre CF=1 indica che si è
verificato un problema; in tal caso, in AX viene restituito un codice
di errore il cui significato viene illustrato più avanti, mentre in
BX viene restituito il numero massimo di paragrafi disponibili per
l'eventuale ingrandimento di un blocco di memoria.

6.5.4 Codici di errore

La Figura 7 illustra il significato dei codici di errore restituiti


dai servizi analizzati in precedenza.

Figura 7 - Codici di errore


Codice Significato
07h MCB danneggiato
08h Memoria insufficiente
09h Indirizzo del blocco di memoria non valido

Ad esempio, se abbiamo a disposizione un blocco di memoria da 26


134
paragrafi e vogliamo ingrandirlo sino a 40 paragrafi con il servizio
n.4Ah della INT 21h, può capitare che la memoria libera disponibile
non sia sufficiente; in tal caso il DOS ci restituisce CF=1 e AX=08h,
mentre in BX viene restituito il massimo numero di paragrafi
disponibile per ingrandire il nostro blocco di memoria.

6.6 Restituzione della memoria allocata in eccesso per un


programma

Analizzando la Figura 4 possiamo notare che il servizio n.48h della


INT 21h ci mette a disposizione un metodo per conoscere la dimensione
in paragrafi del più grande blocco libero disponibile; il trucco da
adottare consiste nel chiamare tale servizio ponendo in BX un valore
"esagerato".
Proviamo, ad esempio, a richiedere un blocco di memoria da BX=FFFFh
paragrafi, pari a:

FFFFh * 16 = 65535 * 16 = 1048560 byte

In un caso del genere siamo praticamente certi del fatto che il DOS
rifiuterà la nostra richiesta restituendoci CF=1 e AX=08h (memoria
insufficiente); ma la cosa più importante è che in BX ci viene
restituita la dimensione in paragrafi del più grande blocco libero
disponibile.

Vediamo allora quello che succede eseguendo il seguente codice:

mov ah, 48h ; servizio: Allocate Memory


mov bx, 0FFFFh ; richiesta di FFFFh paragrafi
int 21h ; chiama il DOS
mov ax, bx ; copia BX in AX
mov dx, 0400h ; riga 4, colonna 0
call writeHex16 ; visualizza AX = max. paragrafi

Il risultato può variare da computer a computer ma, nel caso


generale, si può constatare che il più grande blocco libero
disponibile è costituito da un numero veramente irrisorio di
paragrafi!
Come è possibile una cosa del genere?

La risposta a questa domanda è molto semplice in quanto basta fare


riferimento al metodo adottato dal DOS per ottimizzare l'uso della
memoria; la regola fondamentale che viene seguita consiste nel fatto
che: non possono esistere due blocchi liberi consecutivi e contigui!
Se si verifica una situazione del genere, il DOS unisce i due blocchi
liberi ottenendo un unico blocco libero; in questo modo viene ridotto
al minimo il rischio rappresentato dalla cosiddetta frammentazione
della memoria!

La conseguenza di tutto ciò è che, inizialmente, risultano presenti


alcuni blocchi allocati dal DOS, alcuni piccoli blocchi liberi (non
contigui) e un enorme blocco libero principale da circa 600 Kb; nel
momento in cui richiediamo l'esecuzione di un nostro programma, il
DOS va alla ricerca di un blocco per l'environment segment e un
135
blocco per il program segment.
Per l'environment segment è in genere sufficiente uno dei piccoli
blocchi liberi, mentre per il program segment il DOS va alla ricerca
di un blocco di dimensioni adeguate e trova proprio il suddetto
enorme blocco libero principale; in sostanza, al nostro programma
viene assegnata quasi tutta la memoria convenzionale libera e ciò
spiega il perché della strana situazione descritta in precedenza.

L'unico modo per risolvere questo problema consiste nel ricorrere al


servizio n.4Ah della INT 21h per ridurre al minimo indispensabile le
dimensioni del program segment; a tale proposito, dobbiamo conoscere
l'indirizzo logico iniziale del nostro programma e quello finale, in
modo da poter calcolare la dimensione in paragrafi del programma
stesso.
Per l'indirizzo logico iniziale il discorso è semplice in quanto esso
è rappresentato, come sappiamo, da PSP:0000h; per l'indirizzo logico
finale, invece, il discorso è ben più complesso in quanto può
capitare che parte del programma si trovi in una o più librerie di
cui non disponiamo del codice sorgente!

Per evitare calcoli contorti, la cosa migliore da fare consiste nel


generare il map file del nostro programma da cui possiamo ricavare la
dimensione complessiva in paragrafi; analizziamo allora come ci si
deve comportare nel caso degli eseguibili COM e EXE.

6.6.1 Riduzione del program segment di un eseguibile COM

In questo caso bisogna ricordare che è presente un unico segmento di


programma che contiene codice, dati e stack; appena il programma
viene caricato in memoria, tutti i registri di segmento, CS, DS, ES e
SS, contengono il PSP, mentre ad SP viene assegnato il valore FFFEh.

Il map file di un eseguibile COM specifica le dimensioni complessive


in byte dell'unico segmento di programma presente; tali dimensioni
non comprendono lo stack (che è a carico del SO).
Supponiamo allora che il map file ci dica che il nostro programma
occupa circa 4580 byte; ricaviamo quindi:

4580 / 16 = 286.25 paragrafi

Il risultato che otteniamo deve essere sempre approssimato per


eccesso; nel nostro caso, un valore di sicurezza può essere 300
paragrafi.
A questi 300 paragrafi dobbiamo poi sommare lo stack che si trova
sempre agli indirizzi più alti; supponendo di avere bisogno di circa
800 byte di stack, pari a 50 paragrafi, otteniamo la dimensione
totale del program segment che è pari a:

300 + 50 = 350 paragrafi = 350 * 16 = 5600 byte

Subito dopo il ridimensionamento del program segment, dobbiamo


procedere alla modifica di SP; in base ai calcoli appena effettuati,
dobbiamo ovviamente porre SP=5600.

136
In definitiva, immediatamente dopo l'entry point (con ES=PSP)
dobbiamo inserire il seguente codice:

cli ; disabilita le INT mascherabili


mov ah, 4Ah ; servizio: Resize Memory Block
mov bx, 350 ; nuova dimensione in paragrafi
int 21h ; chiama il DOS
mov sp, 5600 ; riposiziona SP
sti ; riabilita le INT mascherabili

Richiamando ora il servizio n.48h della INT 21h con BX=FFFFh, il DOS
ci informa che non può allocare un blocco da quasi 1 Mb in quanto il
più grande blocco libero disponibile ha una dimensione di circa 600
Kb; ciò dimostra quindi che abbiamo liberato quasi tutta la memoria
che era stata assegnata in eccesso al nostro programma!

6.6.2 Riduzione del program segment di un eseguibile EXE

In questo caso bisogna ricordare che possono essere presenti anche


numerosi segmenti di programma nei quali viene distribuito il codice,
i dati e lo stack; appena il programma viene caricato in memoria, i
soli registri di segmento, DS e ES, contengono il PSP.

Il map file di un eseguibile EXE specifica le dimensioni complessive


in byte dei vari segmenti di programma presenti, compreso il segmento
di stack; bisogna prestare molta attenzione al fatto che tra i vari
segmenti possono essere presenti dei buchi di memoria (necessari per
l'allineamento) e quindi non si deve commettere l'errore di calcolare
semplicemente la somma delle dimensioni dei segmenti stessi!
Consideriamo, ad esempio, il map file del programma TIMER2.EXE
presentato nel precedente capitolo; la Figura 8 illustra tutti i
dettagli.

Figura 8 - Map File di TIMER2.EXE


Start Stop Length Name Class
00000h 000BAh 000BBh DATASEGM DATA
000C0h 0047Bh 003BCh LIBIODATA DATA
00480h 0057Ah 000FBh CODESEGM CODE
00580h 0115Ah 00BDBh LIBIOCODE CODE
01160h 0155Fh 00400h STACKSEGM STACK

Program entry point at 0048h:0000h

L'aspetto che ci interessa è che, tenendo conto dei vari buchi di


memoria, il primo segmento in assoluto (DATASEGM) parte
dall'indirizzo fisico 00000h, mentre l'ultimo segmento in assoluto
(STACKSEGM) termina all'indirizzo fisico 0155Fh; otteniamo quindi:

155Fh byte = 5471 byte = 5471 / 16 = 341.9375 paragrafi

Il risultato che otteniamo deve essere sempre approssimato per


eccesso; nel nostro caso, un valore di sicurezza può essere 350
paragrafi. In definitiva, immediatamente dopo l'entry point (con
ES=PSP) dobbiamo inserire il seguente codice:
137
mov ah, 4Ah ; servizio: Resize Memory Block
mov bx, 350 ; nuova dimensione in paragrafi
int 21h ; chiama il DOS

6.7 Strategie di allocazione della memoria

Quando un programma richiede l'allocazione di un blocco di memoria


attraverso il servizio n.48h della INT 21h, il DOS ricorre ad una
delle seguenti tre strategie:

* First Fit (il primo adatto);


* Best Fit (il più adatto);
* Last Fit (l'ultimo adatto).

La prima strategia è quella predefinita e consiste nel fatto che il


DOS ci mette a disposizione il primo blocco di memoria, di adeguate
dimensioni, che incontra nella catena dei MCB; ad esempio, se
richiediamo un blocco da 400 paragrafi, il DOS comincia a percorrere
in avanti la catena dei MCB finché non incontra un blocco da almeno
400 paragrafi.

La seconda strategia consiste nel fatto che il DOS ci mette a


disposizione il primo blocco di memoria le cui dimensioni sono più
vicine a quelle richieste; ad esempio, se richiediamo un blocco da
180 paragrafi, il DOS comincia a percorrere in avanti la catena dei
MCB finché non incontra un blocco le cui dimensioni siano le più
vicine (per eccesso) a 180 paragrafi.

La terza strategia è una sorta di prima strategia al contrario in


quanto il DOS effettua la ricerca partendo dall'ultimo MCB e
percorrendo la catena a ritroso; ad esempio, se richiediamo un blocco
da 280 paragrafi, il DOS comincia a percorrere a ritroso la catena
dei MCB finché non incontra un blocco da almeno 280 paragrafi.

Per conoscere la strategia di allocazione corrente, è necessario


utilizzare il servizio n.58h, sottoservizio n.00h, della INT 21h; la
Figura 9 illustra tutti i dettagli.

Figura 9 - Servizio n. 5800h della INT 21h


INT 21h - Servizio n. 58h - Memory Allocation Strategy
Sottoservizio n. 00h - Get Memory Allocation Strategy:
restituisce la strategia di allocazione corrente.

Argomenti richiesti:
AH = 58h (servizio Memory Allocation Strategy)
AL = 00h (sottoservizio Get Memory Allocation Strategy)

Valori restituiti:
in caso di successo
CF = 0
AL = strategia di allocazione
in caso di insuccesso
CF = 1

138
AX = codice di errore (01h = function number invalid)

Per impostare una nuova strategia di allocazione è necessario


utilizzare il servizio n.58h, sottoservizio n.01h, della INT 21h; la
Figura 10 illustra tutti i dettagli.

Figura 10 - Servizio n. 5801h della INT 21h


INT 21h - Servizio n. 58h - Memory Allocation Strategy
Sottoservizio n. 01h - Set Memory Allocation Strategy:
imposta la nuova strategia di allocazione.

Argomenti richiesti:
AH = 58h (servizio Memory Allocation Strategy)
AL = 01h (sottoservizio Set Memory Allocation Strategy)
BL = nuova strategia di allocazione

Valori restituiti:
in caso di successo
CF = 0
in caso di insuccesso
CF = 1
AX = codice di errore (01h = function number invalid)

La strategia di allocazione viene rappresentata attraverso una


apposita codifica; con le versioni più recenti del DOS sono
disponibili le codifiche illustrate in Figura 11.

Figura 11 - Strategie di allocazione


Codice Strategia
00h First Fit Conventional (default)
01h Best Fit Conventional
02h Last Fit Conventional
40h First Fit Upper Only
41h Best Fit Upper Only
42h Last Fit Upper Only
80h First Fit Upper
81h Best Fit Upper
82h Last Fit Upper

Le vecchie versioni del DOS, che si utilizzavano ai tempi delle CPU


con architettura a 16 bit, rendevano disponibili solo le prime tre
strategie (00h, 01h e 02h); tali strategie operavano esclusivamente
sulla memoria convenzionale del PC.
Bisogna ribadire, infatti, che la memoria superiore era stata
concepita come un'area riservata, invisibile ai normali programmi che
giravano in modalità reale (nella memoria convenzionale); la
situazione, però, cambiò radicalmente con l'avvento delle CPU 80386 e
superiori.
Le caratteristiche di tali CPU resero possibile la realizzazione di
particolari software denominati memory manager (gestori della
memoria); lo scopo di un memory manager è quello di rendere visibile
139
ai normali programmi DOS l'intera memoria fisicamente presente sul
computer, compresa la memoria superiore e persino la memoria estesa
disposta oltre il primo Mb!
Nel DOS vero e proprio il memory manager più famoso è stato
EMM386.EXE, fornito insieme allo stesso SO; a loro volta, i moderni
emulatori DOS sono in grado di supportare tutti i servizi offerti dai
memory manager più evoluti.

Avendo a disposizione una CPU 80386 o superiore, siamo in grado di


sfruttare al massimo tutte le strategie di allocazione fornite dalle
versioni più recenti del DOS; in questo capitolo analizzeremo
l'accesso alla memoria superiore, mentre nel capitolo successivo
parleremo dell'accesso alla memoria estesa.

Il comportamento predefinito del DOS consiste nel fare in modo che i


programmi vedano solo la memoria convenzionale; in sostanza, la
catena dei MCB inizia e termina all'interno dei primi 640 Kb di RAM.
In realtà, come è stato già anticipato, tale catena continua anche
nella memoria superiore; la connessione tra l'ultimo MCB della
memoria convenzionale e il primo MCB della memoria superiore, prende
il nome di Upper Memory Link o UML (collegamento alla memoria
superiore).
Nelle condizioni specificate in precedenza (presenza di una CPU 80386
o superiore, presenza di un memory manager, etc) il programmatore ha
la possibilità di richiedere al DOS l'attivazione o la disattivazione
dell'UML; la situazione predefinita prevede che l'UML sia
disattivato.

Per conoscere la condizione corrente dell'UML è necessario utilizzare


il servizio n.58h, sottoservizio n.02h, della INT 21h; la Figura 12
illustra tutti i dettagli.

Figura 12 - Servizio n. 5802h della INT 21h


INT 21h - Servizio n. 58h - Memory Allocation Strategy
Sottoservizio n. 02h - Get Upper Memory Link State:
restituisce lo stato dell'Upper Memory Link

Argomenti richiesti:
AH = 58h (servizio Memory Allocation Strategy)
AL = 02h (sottoservizio Get Upper Memory Link State)

Valori restituiti:
in caso di successo
CF = 0
AL = stato corrente dell'UML
in caso di insuccesso
CF = 1
AX = codice di errore (01h = function number invalid)

Per impostare lo stato dell'UML è necessario utilizzare il servizio


n.58h, sottoservizio n.03h, della INT 21h; la Figura 13 illustra
tutti i dettagli.

Figura 13 - Servizio n. 5803h della INT 21h


140
INT 21h - Servizio n. 58h - Memory Allocation Strategy
Sottoservizio n. 03h - Set Upper Memory Link State:
imposta lo stato dell'Upper Memory Link.

Argomenti richiesti:
AH = 58h (servizio Memory Allocation Strategy)
AL = 03h (sottoservizio Set Upper Memory Link State)
BX = nuovo stato dell'UML

Valori restituiti:
in caso di successo
CF = 0
in caso di insuccesso
CF = 1
AX = codice di errore (01h = function number invalid)

Lo stato dell'UML viene rappresentato attraverso una apposita


codifica illustrata in Figura 14.

Figura 14 - Stato dell'UML


Codice Stato
0000h Disattivato (default)
0001h Attivo

Se attiviamo l'UML e proviamo a rieseguire il programma di esempio


(presentato in precedenza) che percorre tutta la catena dei MCB,
possiamo notare che verranno visualizzati anche paragrafi che si
trovano oltre i primi 640 Kb; si tenga presente che lo stato dell'UML
deve essere rigorosamente preservato da un programma che lo intenda
modificare!

Tornando ora alla Figura 11 e assumendo che l'UML sia attivo, avremo
a disposizione la catena completa dei MCB che si sviluppa per tutta
la memoria base (compresa quindi la memoria superiore); in tal caso,
il DOS ricercherà un blocco libero di memoria secondo i seguenti
criteri:

strategie 00h, 01h, 02h - la ricerca parte dalla memoria


convenzionale e prosegue, se necessario, nella memoria superiore;
strategie 40h, 41h, 42h - la ricerca si svolge esclusivamente nella
memoria superiore;
strategie 80h, 81h, 82h - la ricerca parte dalla memoria superiore e
prosegue, se necessario, nella memoria convenzionale.

Nota importante.
Il DOS vero e proprio e anche alcuni emulatori (come DOSEmu)
permettono l'accesso alla memoria superiore solo quando, nel
file C:\CONFIG.SYS, viene inserito l'apposito comando:

DOS=UMB

Naturalmente, resta fondamentale la presenza di una CPU 80386


o superiore e di un gestore della memoria!

141
Altri emulatori potrebbero anche offrire il pieno supporto
della memoria superiore senza la necessità di ricorrere a
comandi particolari.

6.8 Esempi pratici

Come esempio pratico, scriviamo un programma che, dopo aver attivato


l'UML, scandisce tutta la catena dei MCB visualizzando informazioni
dettagliate sui relativi blocchi di memoria; per ogni blocco viene
mostrato: il paragrafo iniziale, la dimensione in byte, la posizione
in memoria (convenzionale o superiore), lo stato (libero o allocato),
il proprietario e la lettera ('M' o 'Z') presente all'offset 0000h
del MCB.

L'unico aspetto degno di nota riguarda l'individuazione del


proprietario del blocco di memoria; a tale proposito, facciamo
riferimento a quanto esposto in questo capitolo e nel Capitolo 13
della sezione Assembly Base.

Ricordiamo che il membro ProprietarioBlocco di Figura 2 contiene


l'eventuale PSP del proprietario del blocco di memoria; se il blocco
è libero, tale membro vale 0000h.
Quindi, se all'offset 0001h del MCB che stiamo esaminando troviamo il
valore 0000h, siamo sicuri che il relativo blocco è libero; se tale
valore è diverso da 0000h, verifichiamo se il blocco appartiene al
DOS.

Ciò può essere accertato grazie al fatto che, per convenzione, i


blocchi appartenenti al DOS sono caratterizzati da
ProprietarioBlocco=0008h; se non troviamo il valore 0008h,
verifichiamo se il blocco è un vero program segment di un programma.

Ciò può essere accertato osservando che, in tal caso, il contenuto


del membro ProprietarioBlocco coincide con il paragrafo da cui inizia
il relativo blocco di memoria; infatti, il program segment di un
programma inizia sempre da PSP:0000h e il contenuto del membro
ProprietarioBlocco è proprio il PSP del programma stesso!

Se la verifica fornisce esito negativo, il blocco potrebbe essere un


environment segment; in tal caso, basta ricordare che all'offset
002Ch del PSP è presente proprio il paragrafo da cui inizia
l'environment segment del relativo programma.
In sostanza, dobbiamo verificare se il contenuto di PSP:002Ch
coincide con il paragrafo da cui inizia il blocco di memoria che
stiamo esaminando; se la verifica fornisce esito negativo, per
esclusione possiamo concludere che abbiamo a che fare con un normale
blocco dati allocato dinamicamente da un programma in esecuzione!

Raccogliendo tutti i concetti appena esposti, otteniamo il listato


illustrato in Figura 15.

Figura 15 - File MCBLIST.ASM

142
;----------------------------------------------------;
; file mcblist.asm ;
; Percorre la catena dei Memory Control Blocks ;
;----------------------------------------------------;
; nasm -f obj mcblist.asm ;
; tlink /t mcblist.obj + comlib.obj ;
; (oppure link /map /tiny mcblist.obj + comlib.obj) ;
;----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "comlib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign UPPER_LIMIT 0A000h ; limite inferiore Upper Mem.

;################ segmento unico ##################

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

resb 0100h ; libera 256 byte per il PSP

..start: ; entry point

;------- inizio blocco principale istruzioni ------

; restituzione memoria in eccesso

cli ; disabilita le INT mascherabili


mov bx, 350 ; nuova dimensione in paragrafi
mov ah, 4Ah ; servizio Resize Memory Block
int 21h ; chiama il DOS
mov sp, 5600 ; riposiziona SP
sti ; riabilita le INT mascherabili

; allocazione di un blocco di memoria convenzionale da 250 paragrafi

mov bx, 250 ; paragrafi da allocare


mov ah, 48h ; servizio Allocate Memory
int 21h ; chiama il DOS

; allocazione di un blocco di memoria convenzionale da 180 paragrafi

mov bx, 180 ; paragrafi da allocare


mov ah, 48h ; servizio Allocate Memory
int 21h ; chiama il DOS

; attivazione Upper Memory Link

mov ax, 5802h ; servizio Get UML State


int 21h ; chiama il DOS
mov [uml_state], al ; salva lo stato dell'UML

mov ax, 5803h ; servizio Set UML State


mov bx, 0001h ; attiva l'UML
int 21h ; chiama il DOS

; inizializzazioni varie

call set80x50 ; modo video testo 80x50


143
call hideCursor ; nasconde il cursore
call clearScreen ; pulisce lo schermo
call show_strings ; mostra le stringhe del programma

; calcolo indirizzo primo MCB

mov ah, 52h ; servizio: Get List of Lists


int 21h ; chiama il DOS
mov ax, [es:bx-2] ; AX = Seg primo MCB
mov es, ax ; copia AX in ES

mov dx, 0500h ; riga 5, colonna 0

; loop principale del programma

mcb_loop:

; visualizza il paragrafo iniziale del blocco di memoria

inc ax ; AX = Seg associato al MCB


call writeHex16 ; visualizza Seg

; visualizza la dimensione in byte del blocco di memoria

push ax ; preserva AX
xor eax, eax ; EAX = 0
mov ax, [es:0003h] ; AX = dim. blocco in paragrafi
shl eax, 4 ; EAX = dim. blocco in byte
add dl, 9 ; colonna + 9
call writeUdec32 ; visualizza dim. blocco in byte
pop ax ; ripristina AX

; visualizza la posizione (convenz./superiore) del blocco di memoria

mov di, convent_str ; DI punta a convent_str


cmp ax, UPPER_LIMIT ; paragrafo minore di A000h ?
jbe write_memtype ; si (mem. convenzionale)
mov di, upper_str ; no (mem. superiore)
write_memtype:
add dl, 14 ; colonna + 14
call writeString ; visualizza la stringa

; visualizza lo stato (libero/allocato) del blocco di memoria

push ax ; preserva AX
mov ax, [es:0001h] ; AX = proprietario blocco
mov di, free_str ; DI punta a free_str
test ax, ax ; proprietario blocco == 0000h ?
jz write_memstate ; si (blocco libero)
mov di, reserved_str ; no (blocco allocato)
write_memstate:
add dl, 17 ; colonna + 17
call writeString ; visualizza la stringa
pop ax ; ripristina AX

; visualizza il nome del proprietario del blocco di memoria

push ax ; preserva AX
mov bx, ax ; BX = AX = Seg associato al MCB
mov ax, [es:0001h] ; AX = proprietario blocco
test_free: ; test blocco libero
mov di, noname_str ; DI punta a noname_str
144
test ax, ax ; proprietario blocco == 0000h ?
jz write_progname ; si (blocco libero)
test_dos: ; no - test blocco allocato dal DOS
mov di, dos_str ; DI punta a dos_str
cmp ax, 0008h ; proprietario blocco == 0008h ?
je write_progname ; si (blocco allocato dal DOS)
test_prog: ; no - test program segment
mov di, prog_str ; DI punta a prog_str
mov ecx, [es:0008h] ; ECX = prima DWORD del nome
mov [di+0], ecx ; copia nella prima DWORD di prog_str
mov ecx, [es:000Ch] ; ECX = seconda DWORD del nome
mov [di+4], ecx ; copia nella seconda DWORD di prog_str
cmp ax, bx ; proprietario blocco == Seg blocco ?
je write_progname ; si (program segment)
test_env: ; no - test environment segment
mov di, env_str ; DI punta a env_str
push es ; preserva ES
mov es, ax ; ES = PSP proprietario blocco
cmp bx, [es:002Ch] ; Seg blocco == [PSP:002Ch] ?
pop es ; ripristina ES (senza modificare FLAGS)
je write_progname ; si (environment segment)
test_dati: ; no - per esclusione e' un blocco dati
mov di, data_str ; DI punta a data_str
write_progname:
add dl, 12 ; colonna + 12
call writeString ; visualizza il proprietario del blocco
pop ax ; ripristina AX

; visualizza l'identificatore ('M'/'Z') del blocco di memoria

push ax ; preserva AX
mov al, [es:0000h] ; AL = indicatore blocco ('M' o 'Z')
mov [id_str], al ; copia in id_str
mov di, id_str ; DI punta a id_str
add dl, 17 ; colonna + 17
call writeString ; visualizza la stringa id_Str
pop ax ; ripristina AX

cmp byte [es:0000h], 'Z' ; IndicatoreBlocco == 'Z' ?


je exit_loop ; si: fine catena

add ax, [es:0003h] ; AX = Seg blocco + Dim blocco


mov es, ax ; ES = paragrafo prossimo MCB

xor dl, dl ; azzera la colonna di output


inc dh ; incrementa la riga di output
jmp mcb_loop ; ripete il loop

exit_loop:
call waitChar ; attende la pressione di un tasto

; ripristino Upper Memory Link

mov ax, 5803h ; servizio Set UML State


mov bx, [uml_state] ; BX = stato originale dell'UML
int 21h ; chiama il DOS

call set80x25 ; modo video testo 80x25


call clearScreen ; pulisce lo schermo
call showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------


145
mov ah, 4ch ; servizio Terminate Program
mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;----- inizio definizione variabili statiche ------

align 4, db 0 ; allinea alla DWORD

uml_state dw 0
title_str db "MCB List - Copyright (C) Ra.M. Software", 0
info_str db "Seg Dim. (byte) Posizione Stato"
db " Proprietario Id", 0
separ_str db "========================================"
db "========================================", 0
keypress_str db "Premere un tasto per continuare", 0
convent_str db "Convenzionale", 0
upper_str db "Superiore", 0
free_str db "Libero", 0
reserved_str db "Allocato", 0
noname_str db "--------", 0
dos_str db "DOS", 0
prog_str db " ", 0
env_str db "[Environment]", 0
data_str db "[Dati]", 0
id_str db " ", 0

;------- fine definizione variabili statiche ------

;---------- inizio definizione procedure ----------

; show_strings: mostra le stringhe del programma

show_strings:

mov di, title_str ; DI punta a title_str


mov dx, 0014h ; riga 0, colonna 20
call writeString ; mostra la stringa

mov di, info_str ; DI punta a info_str


mov dx, 0200h ; riga 2, colonna 0
call writeString ; mostra la stringa

mov di, separ_str ; DI punta a separ_str


mov dx, 0300h ; riga 3, colonna 0
call writeString ; mostra la stringa

mov di, keypress_str ; DI punta a keypress_str


mov dx, 3100h ; riga 49, colonna 0
call writeString ; mostra la stringa

retn ; near return

;----------- fine definizione procedure -----------

;##################################################

Prima di tutto, il programma restituisce tutta la memoria allocata in


eccesso; dal map file risulta che il programma richiede circa 276
paragrafi a cui aggiungiamo 74 paragrafi di stack per un totale di
350 paragrafi (5600 byte).

146
Subito dopo, il programma alloca un blocco da 250 paragrafi (4000
byte) e uno da 180 paragrafi (2880 byte); in fase di esecuzione, tali
blocchi appaiono come aree dati allocate dal nostro programma (si
noti che, non essendo ancora attivo l'UML, i due blocchi vengono
allocati nella memoria convenzionale).

Il passo successivo consiste nell'attivare l'UML; è importante


ricordare che ogni programma che intenda modificare lo stato
dell'UML, deve preservare le impostazioni originali.

A questo punto viene eseguito il loop principale del programma che


visualizza tutti i dettagli relativi a ciascuno dei blocchi di
memoria incontrati nella catena dei MCB; il loop viene ripetuto
finché il programma non incontra un MCB identificato dalla lettera
'Z'.

Come si può notare in Figura 15, il programma termina senza


restituire al DOS i due blocchi di memoria allocati dinamicamente in
fase di esecuzione; ciò è possibile in quanto, con le versioni più
recenti del DOS, tale lavoro viene svolto in modo automatico!
Si tenga anche presente che il programma di Figura 15 è totalmente
privo del controllo sugli errori; tale aspetto è di fondamentale
importanza e non può essere certo trascurato nella progettazione di
programmi "seri".

Bibliografia

Cristopher, Feigenbaum, Saliga - MS-DOS MANUALE DI PROGRAMMAZIONE -


Mc Graw Hill

147
Capitolo 7 Lo standard XMS - eXtended Memory
Specifications
Intorno al 1982, la Intel mette in commercio una nuova CPU della
famiglia 80x86, denominata 80286; tale CPU presenta, proprio come la
8086, una normalissima architettura a 16 bit, ma la vera novità è
rappresentata dalla presenza di un address bus a 24 linee!
Ciò significa che mentre la 8086, con il suo address bus a 20 linee,
può indirizzare 1 Mb di RAM, la 80286 è in grado di accedere a ben:

224 = 16777216 byte = 16 Mb di memoria fisica!

Per rendere possibile l'indirizzamento di tutti i 16 Mb di memoria


fisica, la 80286 introduce una nuova modalità operativa denominata
modalità protetta; anche in tale modalità continuano ad esistere gli
indirizzi logici Seg:Offset da 16+16 bit, ma il loro significato è
completamente differente rispetto al caso della modalità reale.
Nella modalità protetta della 80286, la componente Seg a 16 bit di un
indirizzo logico, contiene un indice relativo ad una tabella di
cosiddetti descrittori di segmento; come si intuisce dal nome,
ciascun descrittore contiene, appunto, la descrizione completa di un
segmento di programma. In particolare, ogni descrittore contiene
l'indirizzo a 24 bit da cui inizia in memoria il relativo segmento di
programma; tale indirizzo prende il nome di base address (indirizzo
base).
La componente Offset a 16 bit di un indirizzo logico permette allora
di specificare uno spiazzamento, compreso tra 0000h e FFFFh, relativo
al corrispondente base address; con questo meccanismo, è possibile
indirizzare tutti i 16 Mb gestibili da una CPU 80286!

Nel progettare la 80286, la Intel deve necessariamente tenere conto


del fatto che il mondo dei PC è ormai invaso da una enorme quantità
di software destinato alla modalità reale della 8086; a tale
proposito, la 80286 mantiene una struttura interna totalmente
compatibile con l'architettura della 8086 e può quindi eseguire i
programmi scritti originariamente per la modalità reale.

Nello sforzo teso a garantire la massima compatibilità verso il basso


della 80286, la Intel si trova ad affrontare un curioso aspetto
legato proprio all'address bus a 24 linee della nuova CPU; per capire
tale aspetto, è necessario ricordare il fenomeno del wrap around
tipico della modalità reale delle CPU con address bus a 20 linee.
Consideriamo l'indirizzo logico normalizzato FFFFh:000Fh; in
sostanza, abbiamo a che fare con uno spiazzamento 000Fh relativo al
segmento di memoria n.FFFFh (ultimo segmento disponibile). Tale
indirizzo logico corrisponde all'indirizzo fisico a 20 bit:

(FFFFh * 16) + Fh = (65535 * 16) + 15 = 1048575 = FFFFFh =


11111111111111111111b

Si tratta chiaramente dell'indirizzo fisico relativo all'ultimo byte


dell'unico Mb di RAM gestibile da una CPU con address bus a 20 linee!

148
Incrementando ora di 1 la componente Offset del precedente indirizzo
logico normalizzato, otteniamo FFFFh:0010h; tale indirizzo logico
corrisponde all'indirizzo fisico a 21 bit:

(FFFFh * 16) + 10h = (65535 * 16) + 16 = 1048576 = 100000h =


100000000000000000000b

Una CPU 8086, avendo a disposizione solo 20 linee di indirizzamento,


tronca il bit più significativo (il ventunesimo bit) del precedente
indirizzo fisico e ottiene 00000000000000000000b; in sostanza, se
tentiamo di accedere all'indirizzo logico FFFFh:0010h, ci ritroviamo
all'indirizzo fisico 00000h!

Come è facile constatare, la situazione è del tutto analoga anche per


gli offset successivi a 0010h, relativi al segmento di memoria
n.FFFFh; quindi, riassumendo:

* se tentiamo di accedere a FFFFh:0010h, ci ritroviamo all'indirizzo


fisico 00000h;
* se tentiamo di accedere a FFFFh:0011h, ci ritroviamo all'indirizzo
fisico 00001h;
* se tentiamo di accedere a FFFFh:0012h, ci ritroviamo all'indirizzo
fisico 00002h;
* se tentiamo di accedere a FFFFh:0013h, ci ritroviamo all'indirizzo
fisico 00003h
e così via.

Il discorso cambia radicalmente nel caso di un programma, scritto per


la modalità reale, che viene fatto girare su una 80286; infatti,
grazie all'address bus a 24 linee, tutti i precedenti indirizzi
logici sono perfettamente validi!
Non essendoci nessun troncamento del bit più significativo, risulta
che:

* se tentiamo di accedere a FFFFh:0010h, ci ritroviamo all'indirizzo


fisico 100000h;
* se tentiamo di accedere a FFFFh:0011h, ci ritroviamo all'indirizzo
fisico 100001h;
* se tentiamo di accedere a FFFFh:0012h, ci ritroviamo all'indirizzo
fisico 100002h;
* se tentiamo di accedere a FFFFh:0013h, ci ritroviamo all'indirizzo
fisico 100003h
e così via.

In pratica, un programma in modalità reale che gira su una 80286 è in


grado di scavalcare il primo Mb di RAM attraverso tutti gli indirizzi
logici compresi tra FFFFh:0010h e FFFFh:FFFFh; complessivamente si
tratta di un numero di byte pari a:

FFFFh - 0010h + 1 = 65535 - 16 + 1 = 65535 - 15 = 65520

Stiamo parlando quindi di un blocco di memoria la cui dimensione in


byte è inferiore di 1 paragrafo a quella di un segmento di memoria;
questo blocco da 65520 byte, che si trova immediatamente dopo il
149
primo Mb, è stato chiamato High Memory Area o HMA (area di memoria
alta).

Per garantire l'assoluta compatibilità verso la modalità reale 8086,


la Intel ha allora fatto in modo che sulla 80286 sia possibile
disabilitare via software tutte le linee di indirizzo dalla A20 alla
A23; in questo modo, la 80286 si trova ad operare con un address bus
a 20 linee e può quindi eseguire senza nessun problema, programmi
destinati alla modalità reale 8086.

Tutte le considerazioni appena esposte si applicano anche alle CPU


successive alla 80286; in generale quindi, una qualsiasi CPU 80286 o
superiore, può operare in modalità reale attraverso la
disabilitazione di tutte le linee di indirizzo dalla A20 in su.
Nel gergo tecnico si semplifica tutto il discorso attraverso il
riferimento alla sola linea A20. Quando si parla quindi di "linea A20
disabilitata", si intende dire che la CPU opera con le sole prime 20
linee dell'address bus (da A0 ad A19); l'espressione "linea A20
abilitata" indica, invece, che la CPU opera con tutte le linee che
compongono il suo address bus (ad esempio, su una 80486 con linea A20
abilitata risultano disponibili tutte le 32 linee di indirizzo, da A0
ad A31).

7.1 La HMA - High Memory Area

Nel precedente capitolo è stato spiegato che, avendo a disposizione


una CPU 80386 o superiore e un memory manager, è possibile accedere
alla Upper Memory da un normale programma che gira in modalità reale;
un discorso analogo può essere fatto anche per la HMA!

Tutto è iniziato quando alcuni hackers osservarono che il DOS 5.x era
in grado di spostare parte del kernel proprio nella HMA attraverso il
comando del file CONFIF.SYS:

DOS=HIGH

Dopo un intenso lavoro di reverse engineering, quegli stessi hackers


si accorsero che il DOS 5.x non faceva altro che sfruttare la
possibilità di scavalcare, in modalità reale, la barriera del primo
Mb di RAM sulle CPU 80286 o superiori; in sostanza, il DOS 5.x era
stato dotato di funzioni nascoste capaci di abilitare la linea A20 in
modo da poter accedere a tutti i 65520 byte della HMA attraverso gli
indirizzi logici, da FFFFh:0010h a FFFFh:FFFFh, tipici della modalità
reale!

Il reverse engineering ha portato a scoprire diversi servizi non


documentati, usati dal DOS per la determinazione della quantità di
HMA libera, per l'allocazione e la deallocazione di blocchi in HMA,
etc; un aspetto importante è che i vari servizi (tutti basati sulla
INT 2Fh, denominata multiplex interrupt) presuppongono che il DOS
abbia caricato parte del kernel in HMA.

Se un programma ha bisogno di conoscere la quantità di HMA libera,

150
può ricorrere al servizio n.4Ah, sottoservizio n.01h, della INT 2Fh;
le caratteristiche di tale servizio sono illustrate in Figura 1.

Figura 1 - Servizio n. 4A01h della INT 2Fh


INT 2Fh - Servizio n. 4Ah - Miscellaneous Services
Sottoservizio n. 01h - Query Free HMA Space:
restituisce la quantità di HMA libera.

Argomenti richiesti:
AH = 4Ah (Miscellaneous Services)
AL = 01h (Query Free HMA Space)

Valori restituiti:
BX = numero di byte liberi in HMA
ES:DI = indirizzo del primo byte libero

Come è stato già spiegato, i vari servizi presuppongono che il DOS


abbia caricato parte del kernel in HMA; in caso contrario, il
servizio 4A01h della INT 2Fh restituisce BX=0000h e
ES:DI=FFFFh:FFFFh!

In analogia a quanto abbiamo visto per la upper memory, anche per


l'accesso alla HMA è richiesta una CPU 80286 o superiore, un memory
manager e un apposito comando nel file CONFIG.SYS rappresentato dalla
linea:

DOS=HIGH

Come vedremo in seguito, le versioni più recenti del DOS hanno reso
disponibile un memory manager denominato HIMEM.SYS; il suo scopo è
quello di fornire una serie completa di servizi destinati alla
gestione della upper memory, della high memory e della extended
memory.

Nell'ipotesi quindi che siano verificati tutti i requisiti appena


esposti, possiamo conoscere la quantità di HMA libera sul nostro
computer con il seguente codice:

mov ax, 4A01h ; servizio: Query Free HMA Space


int 2Fh ; multiplex INT
mov dx, 0400h ; riga 4, colonna 0
mov ax, bx ; copia BX in AX
call writeUdec16 ; mostra i byte liberi
inc dh ; incremento riga
mov ax, es ; copia ES in AX
call writeHex16 ; mostra il Seg del primo byte libero
add dl, 6 ; incremento colonna
mov ax, di ; copia DI in AX
call writeHex16 ; mostra l'Offset del primo byte libero

Generalmente, il risultato prodotto da questo codice mostra la


presenza di uno o due Kb di HMA liberi; un esempio di output può
essere il seguente (da notare come l'indirizzo del primo byte libero
faccia riferimento ad un'area della RAM oltre il primo Mb):

151
10367
FFFFh D780h

Gli altri servizi della INT 2Fh, relativi alla HMA, permettono di
allocare/deallocare blocchi di memoria e di ottenere l'indirizzo
iniziale della catena dei MCB in HMA; a tale proposito si può
consultare, ad esempio, la interrupts list di Ralf Brown.

Nota importante.
Appare evidente che i servizi non documentati per la HMA sono
riservati al SO e non hanno nulla a che vedere con i servizi
standard del DOS; il loro utilizzo da parte dei normali
programmi può quindi provocare seri problemi.
C'è anche da considerare un altro aspetto legato al fatto che
una grossa porzione della HMA viene occupata da parte del
kernel DOS liberando in questo modo diverse decine di Kb in
memoria convenzionale; questa situazione rende del tutto
insignificanti i vantaggi che si ottengono facendo accedere i
normali programmi alla HMA!

In ogni caso, con la definizione dello standard XMS, sono


stati resi disponibili diversi servizi ufficiali per lo
sfruttamento della HMA da parte dei normali programmi che
girano in modalità reale; eventualmente, è vivamente
raccomandato quindi l'uso di tali servizi al posto di quelli
non documentati del DOS.

7.2 La memoria estesa e lo standard XMS

Con il passare degli anni, si è venuta a creare una notevole


confusione in relazione alla possibilità, offerta ai programmi in
modalità reale, di accedere ai vari blocchi della upper memory, della
high memory e della extended memory; in particolare, i programmatori
si sono trovati davanti ad un un proliferare di tecniche di
programmazione che molto spesso risultavano incompatibili tra loro.
Una delle prime tecniche venne introdotta dalla Microsoft e si basava
sull'uso di un driver denominato VDISK.SYS; il driver creava un disco
virtuale in memoria, attraverso il quale era possibile accedere alla
RAM oltre i 640 Kb convenzionali.
Un'altra tecnica consisteva nel ricorrere ai servizi della INT 15h
(Big Memory Services) del BIOS; attraverso tale interruzione, il BIOS
permette ai programmi di accedere anche alla memoria oltre il primo
Mb.

Proprio per porre rimedio a questa situazione confusionaria, sul


finire degli anni 80 un gruppo di aziende del settore hardware e
software si sono riunite per definire uno standard denominato XMS o
eXtended Memory Specifications (specifiche per la memoria estesa);
nel seguito del capitolo faremo riferimento allo standard XMS 3.0 del
1991, definito da un consorzio di cui facevano parte la Intel
Corporation, la Lotus Development Corporation, la AST Research Inc. e
la Microsoft Corporation.

152
Lo standard XMS definisce tutte le specifiche necessarie per la
progettazione dei driver (memory manager) attraverso i quali i
programmi che girano in modalità reale possono accedere alla upper
memory, alla high memory e alla extended memory; in questo modo,
l'accesso alle varie aree di memoria avviene attraverso una
interfaccia standard che garantisce l'assoluta compatibilità tra i
programmi.
Come vedremo nel seguito, ogni driver deve fornire obbligatoriamente
una determinata serie di servizi; è anche previsto un certo numero di
ulteriori servizi opzionali.

7.2.1 Definizioni convenzionali

La Figura 2 illustra le definizioni convenzionali previste dallo


standard XMS per le varie aree di memoria; per delimitare le aree
stesse, vengono usati gli indirizzi fisici a 32 bit (ovviamente, nel
caso della 80286 si deve fare riferimento ai corrispondenti indirizzi
a 24 bit).

Figura 2 - Definizioni per le aree di memoria


Area di memoria Intervallo
Conventional Memory da 00000000h a 0009FFFFh
Upper Memory Blocks (UMB) da 000A0000h a 000FFFFFh
Extended Memory da 00100000h in su
High Memory Area (HMA) da 00100000h a 0010FFEFh
Extended Memory Blocks (EMB) da 0010FFF0h in su

Quindi, il termine extended memory rappresenta tutta la memoria


(estesa) che si trova oltre il primo Mb; lo standard XMS suddivide
tale area in HMA (primi 65520 byte di memoria estesa) e EMB (memoria
estesa successiva alla HMA)!

Proseguendo con le definizioni convenzionali, il termine A20 indica


la ventunesima linea dell'address bus di una CPU 80286 o superiore;
abilitando la linea A20 è possibile accedere alla memoria oltre il
primo Mb.

Il termine XMM sta per eXtended Memory Manager e indica un qualsiasi


driver necessario per l'accesso alla memoria superiore, alta e estesa
in modalità reale; in effetti, come è stato già spiegato, lo scopo
fondamentale dello standard XMS è quello di fornire una interfaccia
standard unificata per tali tre aree di memoria.
Il memory manager più famoso è sicuramente HIMEM.SYS in quanto viene
fornito insieme al DOS; esistono anche altri XMM abbastanza
conosciuti come, ad esempio, 386MAX, QEMM, NETROOM, etc.

7.2.2 Installazione dell'eXtended Memory Manager

Come è stato appena spiegato, il compito di un XMM è quello di far


"apparire" la memoria superiore, alta e estesa ai programmi che
girano in modalità reale; se si ha la necessità di accedere a tali
153
aree di memoria, è necessario quindi installare un XMM.
In un ambiente DOS puro, aprendo con un editor il file C:\CONFIG.SYS,
si può notare la presenza di una linea del tipo:

DEVICE=C:\DOS\HIMEM.SYS

Tale linea provvede, appunto, ad installare il driver HIMEM.SYS in


fase di avvio del DOS; lo stesso driver accetta anche dei parametri
da linea di comando, alcuni dei quali verranno illustrati nel seguito
(per un elenco dettagliato di tali parametri e del loro significato,
si consiglia di consultare un manuale del DOS).

Nota importante.
Molti moderni emulatori DOS provvedono autonomamente al
supporto della memoria superiore, alta e estesa; tali
emulatori non hanno bisogno quindi di un driver come
HIMEM.SYS.
Eventualmente, all'utente viene data la possibilità di
modificare le impostazioni predefinite; nel caso di Windows,
ad esempio, le impostazioni relative alle varie aree di
memoria sono modificabili attraverso il menu "Proprietà" del
programma DOS da eseguire.
Nel caso di DOSEmu, invece, l'utente può editare il file
$HOME/.dosemurc e apportare le modifiche desiderate nella
apposita sezione Memory settings.

In ogni caso, la quantità di memoria estesa specificata


dall'utente non può essere superiore a quella fisicamente
disponibile sul computer in uso; si tenga anche presente che
solamente in ambiente DOS puro l'XMM permette di accedere a
tutta la memoria estesa fisicamente disponibile. I moderni SO
che girano in modalità protetta, sfruttano pesantemente la
memoria estesa; di conseguenza, solamente una quantità
limitata di tale memoria viene resa disponibile per i
programmi DOS!

7.3 Interfaccia di programmazione XMS

Tutti i servizi forniti da un XMM risultano accessibili attraverso


una chiamata alla cosiddetta XMS driver's control function (funzione
di controllo del driver XMS); ciascuno di tali servizi viene
identificato attraverso un codice a 8 bit da caricare in AH.
Nel caso generale, quindi, l'accesso ad un servizio XMS, fornito da
un XMM, avviene nel seguente modo:

mov ah, codice servizio ; AH = codice del servizio richiesto


call control function ; chiamata della funzione di controllo

Un programma che intenda accedere ai servizi offerti dallo standard


XMS deve innanzi tutto verificare che sia installato un XMM in
memoria; a tale proposito, è necessario porre AL=00h, AH=43h e
chiamare la INT 2Fh.
Se un XMM è installato in memoria, la precedente chiamata alla INT
154
2Fh restituisce AL=80h; qualunque altro valore restituito in AL
indica che non è presente nessun XMM.
Il codice da eseguire assume quindi il seguente aspetto:

mov ax, 4300h ; verifica la presenza di un XMM


int 2Fh ; multiplex INT
cmp al, 80h ; driver installato ?
je XMM_presente ; if (AL == 80h) salta a XMM_presente
........... ; else mostra un messaggio di errore
jmp exit_program ; e termina il programma
XMM_presente: ; continua l'esecuzione

Una volta accertata la presenza di un XMM installato in memoria,


dobbiamo procedere con la determinazione dell'indirizzo della
funzione di controllo; a tale proposito, dobbiamo porre AL=10h,
AH=43h e chiamare la INT 2Fh.
La funzione di controllo è di tipo FAR per cui il suo indirizzo è una
coppia Seg:Offset da 16+16 bit che viene restituita in ES:BX; la cosa
migliore da fare consiste nel memorizzare tale indirizzo in una
DWORD.
Nell'ipotesi allora di aver definito una variabile a 32 bit
denominata XMSControlFunc, possiamo scrivere il seguente codice:

mov ax, 4310h ; richiesta indirizzo control function


int 2Fh ; multiplex INT
mov [XMSControlFunc+0], bx ; salva l'Offset
mov [XMSControlFunc+2], es ; salva il Seg

Ricordiamoci di rispettare sempre la convenzione Intel per la


memorizzazione degli indirizzi logici Seg:Offset; in base a tale
convenzione, la componente Offset deve sempre precedere la componente
Seg!

A questo punto, avendo a disposizione l'indirizzo della funzione di


controllo, possiamo richiedere i vari servizi XMS con le seguenti
istruzioni:

mov ah, codice servizio ; AH = codice del servizio richiesto


call far [XMSControlFunc] ; chiamata della funzione di controllo

Nel caso di MASM e TASM, la chiamata della funzione di controllo


diventa:

call dword ptr XMSControlFunc

Se la richiesta di un servizio XMS ha successo, la funzione di


controllo restituisce AX=0001h più eventuali informazioni addizionali
in BX e DX; se, invece, la richiesta fallisce, la funzione di
controllo restituisce AX=0000h più un codice di errore in BL.
I codici di errore validi hanno sempre il bit più significativo che
vale 1; la Figura 3 illustra l'elenco completo dei codici di errore.

Figura 3 - Codici di errore XMS


BL Tipo di errore
155
Figura 3 - Codici di errore XMS
BL Tipo di errore
80h servizio non implementato
81h è stato individuato un driver VDISK in memoria
82h errore relativo alla linea A20
8Eh errore generale del driver XMM
8Fh errore fatale del driver XMM (riavviare il PC)
90h la HMA non esiste
91h la HMA è già in uso
92h valore di DX minore del parametro /HMAMIN
93h allocazione HMA fallita
94h disabilitazione della linea A20 fallita
A0h memoria estesa esaurita
A1h handles degli EMB esauriti
A2h handle non valido per un EMB
A3h valore SourceHandle non valido
A4h valore SourceOffset non valido
A5h valore DestHandle non valido
A6h valore DestOffset non valido
A7h valore Length non valido
A8h trasferimento dati con sovrapposizione non valida
A9h errore di parità in memoria
AAh l'EMB non è bloccato
ABh l'EMB è bloccato
ACh contatore di blocco in overflow per un EMB
ADh bloccaggio fallito per un EMB
B0h è disponibile un UMB più piccolo
B1h nessun UMB disponibile
B2h segmento non valido per un UMB

Il significato dei vari termini presenti in Figura 3 sarà chiarito


nel seguito del capitolo.

7.4 Servizi XMS

Lo standard XMS comprende una serie di servizi che possono essere


suddivisi nei gruppi illustrati in Figura 4.

Figura 4 - Servizi XMS suddivisi per gruppi


Gruppo Codici (AH)
Servizi di informazione sul driver 00h
Servizi di gestione della HMA da 01h a 02h
Servizi di gestione della linea A20 da 03h a 07h
Servizi di gestione degli EMB da 08h a 0Fh

156
Figura 4 - Servizi XMS suddivisi per gruppi
Gruppo Codici (AH)
Servizi di gestione degli UMB da 10h a 12h

Un XMM viene considerato pienamente aderente allo standard XMS 3.0


quando implementa tutti i servizi dei primi quattro gruppi; quindi, i
servizi relativi al quinto gruppo (gestione della upper memory) sono
considerati facoltativi.
In generale, un determinato driver potrebbe anche non implementare
tutti i sevizi obbligatori, previsti dallo standard XMS 3.0; proprio
per questo motivo, è importante che il programmatore verifichi sempre
l'eventuale codice di errore restituito in BL dalla funzione di
controllo, con particolare riferimento al codice 80h (servizio non
implementato)!

Analizziamo ora l'elenco completo dei vari servizi XMS e le


caratteristiche di ciascuno di essi; per maggiori dettagli si
consiglia di scaricare il file xms30.tar.bz2 presente nella sezione
Downloads di questo sito.

7.4.1 Servizio n.00h: Get XMS Version Number

Questo servizio restituisce il numero di versione dello standard XMS


supportato dal driver XMM.

XMS - Servizio n. 00h - Get XMS Version Number

Argomenti richiesti:
AH = 00h (Get XMS Version Number)

Valori restituiti:
AX = numero di versione standard XMS (in BCD)
BX = numero di revisione interno del driver
DX = 0001h se la HMA esiste, 0000h in caso contrario

Il valore restituito in AX è in formato BCD; ad esempio, AX=0250h


significa che il driver supporta lo standard XMS versione 2.50.

Il valore restituito in BX si riferisce al numero di versione del


driver XMM e non al numero di versione dello standard XMS.

Si presti attenzione al fatto che DX=0001h indica che la HMA esiste


ma non è detto che sia disponibile; in sostanza, può capitare che la
HMA, pur essendo presente, sia già in uso da parte di un altro
programma!

7.4.2 Servizio n.01h: Request High Memory Area

Questo servizio tenta di allocare per un programma i 65520 byte della


HMA.

XMS - Servizio n. 01h - Request High Memory Area

157
Argomenti richiesti:
AH = 01h (Request High Memory Area)
Se il richiedente è un TSR o un device driver
DX = spazio in byte richiesto dal TSR
Se il richiedente è un normale programma
DX = FFFFh

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 90h, 91h, 92h)

Un TSR o Terminate and stay resident è un programma che, una volta


lanciato, si installa in memoria e resta attivo, in genere, sino allo
spegnimento del computer; il suo scopo è quello di fornire dei
servizi richiesti da altri programmi.

Lo standard XMS tratta la HMA come un blocco unico di memoria, non


condivisibile tra due o più programmi; tale blocco viene quindi
assegnato in esclusiva al primo programma che ne faccia richiesta,
purché tale richiesta sia compatibile con il valore specificato
attraverso il parametro HMAMIN del driver HIMEM.SYS.
Il parametro HMAMIN specifica la quantità minima (in Kb) di HMA che
un programma deve richiedere per avere in esclusiva l'accesso a tale
area della RAM; ad esempio, il comando:

DEVICE=C:\DOS\HIMEM.SYS /HMAMIN=32

installa in memoria il driver HIMEM.SYS e stabilisce che la HMA venga


assegnata al primo programma che ne richieda un blocco da almeno 32
Kb.
Il valore specificato da HMAMIN (che deve essere compreso tra 0 e 63)
è significativo solo per i TSR e per i device driver; i normali
programmi dovrebbero specificare sempre DX=FFFFh quando richiedono il
servizio n.01h.
Si presti anche attenzione al fatto che HMAMIN specifica un valore in
Kb, mentre in DX bisogna inserire un valore in byte!

L'allocazione della HMA da parte di un programma ha senso solo quando


tale area di memoria non è utilizzata dal DOS; se si ha intenzione di
sfruttare la HMA dai propri programmi, è necessario quindi togliere
il comando DOS=HIGH dal CONFIG.SYS (o sostituirlo con il comando
DOS=LOW).
Per sicurezza, conviene comunque verificare sempre se il DOS sta
usando o meno la HMA; a tale proposito, si può ricorrere al servizio
33h, sottoservizio 06h della INT 21h, che restituisce nel bit 4 di
DH, il valore 0 se il DOS non occupa la HMA, il valore 1 se il DOS
occupa la HMA.

7.4.3 Servizio n.02h: Release High Memory Area

Questo servizio restituisce la HMA precedentemente allocata da un


programma.
158
XMS - Servizio n. 02h - Release High Memory Area

Argomenti richiesti:
AH = 02h (Release High Memory Area)

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 90h, 93h)

A differenza di quanto accade per i normali blocchi di memoria DOS,


la HMA non viene automaticamente deallocata quando il programma che
l'aveva in uso termina; tale compito deve essere svolto rigorosamente
dal programma stesso!

7.4.4 Servizio n.03h: Global Enable A20

Questo servizio tenta di abilitare la linea A20 in modo da rendere


possibile l'accesso alla memoria oltre il primo Mb.

XMS - Servizio n. 03h - Global Enable A20

Argomenti richiesti:
AH = 03h (Global Enable A20)

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 82h)

Questo servizio deve essere usato esclusivamente da un programma


intenzionato a richiedere il controllo diretto della HMA; in ogni
caso (cioè, anche se l'allocazione della HMA fallisce), il programma
stesso deve provvedere a disabilitare la linea A20 prima di
terminare!

7.4.5 Servizio n.04h: Global Disable A20

Questo servizio tenta di disabilitare la linea A20.

XMS - Servizio n. 04h - Global Disable A20

Argomenti richiesti:
AH = 04h (Global Disable A20)

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 82h, 94h)

159
Questo servizio deve essere usato esclusivamente da un programma che
ha ottenuto il controllo diretto della HMA; in sostanza, il programma
che ha l'accesso esclusivo alla HMA deve provvedere rigorosamente a
restituire tale area di memoria e a disabilitare la linea A20 prima
di terminare!

7.4.6 Servizio n.05h: Local Enable A20

Questo servizio tenta di abilitare la linea A20 in modo da rendere


possibile l'accesso alla memoria oltre il primo Mb.

XMS - Servizio n. 05h - Local Enable A20

Argomenti richiesti:
AH = 05h (Local Enable A20)

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 82h)

Questo servizio deve essere usato esclusivamente da un programma


intenzionato ad accedere in I/O alla HMA già allocata da un altro
programma che ne ha il controllo diretto; lo stesso programma che
richiede il servizio n.05h deve poi provvedere a disabilitare la
linea A20 con il servizio n. 06h!

7.4.7 Servizio n.06h: Local Disable A20

Questo servizio tenta di disabilitare la linea A20.

XMS - Servizio n. 06h - Local Disable A20

Argomenti richiesti:
AH = 06h (Local Disable A20)

Valori restituiti:
In caso di successo:
AX = 00001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 82h, 94h)

Questo servizio deve essere usato esclusivamente da un programma


intenzionato ad accedere in I/O alla HMA già allocata da un altro
programma che ne ha il controllo diretto; prima di terminare, il
programma che ha eseguito l'accesso in I/O alla HMA deve provvedere
rigorosamente a disabilitare localmente la linea A20 con il servizio
n.06h!

7.4.8 Servizio n.07h: Query A20

160
Questo servizio verifica lo stato della linea A20.

XMS - Servizio n. 07h - Query A20

Argomenti richiesti:
AH = 07h (Query A20)

Valori restituiti:
In caso di successo:
AX = 00001h se la linea A20 è abilitata
AX = 00000h se la linea A20 è disabilitata
In caso di insuccesso:
BL = Codice di errore (80h, 81h)

Questo servizio verifica se la linea A20 è abilitata o meno; a tale


proposito, viene tentato l'accesso all'indirizzo logico FFFFh:0010h
per verificare se si ottiene (linea A20 disabilitata) o meno (linea
A20 abilitata) un wrap around!

In generale, si tenga presente che su molti computer, tutte le


operazioni che hanno a che fare con la linea A20 possono risultare
relativamente lente; proprio per questo motivo, i programmi DOS che
fanno un uso intensivo dei servizi XMS soffrono di prestazioni non
particolarmente brillanti!

7.4.9 Servizio n.08h: Query Free Extended Memory

Questo servizio restituisce la dimensione del più grande EMB


disponibile.

XMS - Servizio n. 08h - Query Free Extended Memory

Argomenti richiesti:
AH = 08h (Query Free Extended Memory)

Valori restituiti:
In caso di successo:
AX = dimensione in Kb del più grande EMB
disponibile
DX = dimensione totale in Kb della memoria estesa
In caso di insuccesso:
BL = Codice di errore (80h, 81h, A0h)

Questo servizio restituisce informazioni relative alla dimensione


totale in Kb della memoria estesa presente sul computer (DX) e alla
dimensione in Kb del più grande EMB libero; tali informazioni non
comprendono i 65520 byte della HMA.

7.4.10 Servizio n.09h: Allocate Extended Memory Block

Questo servizio tenta di allocare un EMB per un programma.

XMS - Servizio n. 09h - Allocate Extended Memory Block

161
Argomenti richiesti:
AH = 09h (Allocate Extended Memory Block)
DX = dimensioni in Kb dell'EMB richiesto

Valori restituiti:
In caso di successo:
AX = 0001h
DX = handle a 16 bit dell'EMB
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A0h, A1h)

Questo servizio tenta di allocare un blocco di memoria estesa (EMB)


per un programma; la dimensione richiesta deve essere espressa in Kb.

In caso di successo, viene restituito in DX un cosiddetto handle


(maniglia) e cioè, un valore a 16 bit che identifica univocamente
l'EMB appena allocato; l'handle viene utilizzato in tutte le
operazioni che coinvolgono l'EMB ad esso associato.
Gli handle sono disponibili in quantità relativamente limitata; in
assenza di altre indicazioni, la quantità predefinita è 32. Se
necessario, si può incrementare tale valore attraverso il parametro
NUMHANDLES che permette di specificare un numero compreso tra 1 e
128; possiamo scrivere, ad esempio:

DEVICE=C:\DOS\HIMEM.SYS /NUMHANDLES=64

7.4.11 Servizio n.0Ah: Free Extended Memory Block

Questo servizio tenta di deallocare un EMB precedentemente allocato


da un programma.

XMS - Servizio n. 0Ah - Free Extended Memory Block

Argomenti richiesti:
AH = 0Ah (Free Extended Memory Block)
DX = handle dell'EMB da deallocare

Valori restituiti:
In caso di successo:
AX = 0001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A2h, ABh)

A differenza di quanto accade per i normali blocchi di memoria DOS,


un EMB non viene automaticamente deallocato quando il programma che
lo aveva in uso termina; tale compito deve essere svolto
rigorosamente dal programma stesso!
L'handle dell'EMB appena deallocato non è più valido e quindi non
deve essere utilizzato per ulteriori operazioni di I/O con l'EMB
stesso!

7.4.12 Servizio n.0Bh: Move Extended Memory Block

162
Questo servizio tenta di effettuare un trasferimento dati tra una
sorgente e una destinazione.

XMS - Servizio n. 0Bh - Move Extended Memory Block

Argomenti richiesti:
AH = 0Bh (Move Extended Memory Block)
DS:SI = puntatore alla Extended Memory Move Structure

Valori restituiti:
In caso di successo:
AX = 0001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, 82h, A3h, A4h,
A5h, A6h, A7h, A8h, A9h)

Questo servizio tenta di effettuare un trasferimento dati tra un


blocco sorgente e un blocco destinazione; il servizio opera
principalmente sugli EMB, ma è permesso anche l'utilizzo di normali
blocchi DOS che si trovano nella memoria base (convenzionale o
superiore). In sostanza, possiamo effettuare trasferimenti di dati
tra memoria base e memoria base, tra memoria base e memoria estesa,
tra memoria estesa e memoria base, tra memoria estesa e memoria
estesa!

Tutte le informazioni relative al trasferimento dati, devono trovarsi


in una apposita struttura denominata Extended Memory Move Structure
(EMMS); tale struttura assume l'aspetto mostrato in Figura 5.

Figura 5 - Extended Memory Move Structure


STRUC ExtMemMoveStruct

Length resd 1 ; numero di byte da trasferire


SourceHandle resw 1 ; handle del blocco sorgente
SourceOffset resd 1 ; offset a 32 bit nel blocco sorgente
DestHandle resw 1 ; handle del blocco destinazione
DestOffset resd 1 ; offset a 32 bit nel blocco
destinazione

ENDSTRUC

Il valore contenuto in Lenght deve essere un numero pari; anche se


non è obbligatorio, si raccomanda di allineare i blocchi da
trasferire, alla WORD sulle CPU a 16 bit (80286) e alla DWORD sulle
CPU a 32 bit.

Se il blocco sorgente si trova nella memoria base, allora


SourceHandle deve valere 0000h, mentre SourceOffset viene
interpretato come un normale indirizzo logico Seg:Offset a 16+16 bit;
come al solito, si faccia attenzione a rispettare la convenzione
Intel per la disposizione in memoria degli indirizzi logici!
Se il blocco sorgente è un EMB, allora SourceOffset indica un Offset
a 32 bit relativo all'indirizzo iniziale dello stesso blocco
sorgente; ad esempio, 00000000h indica uno spiazzamento nullo
163
relativo all'indirizzo iniziale del blocco sorgente, mentre 0008FB42h
indica uno spiazzamento pari a 588610 byte relativo all'indirizzo
iniziale del blocco sorgente.
Considerazioni del tutto identiche valgono anche per DestHandle e
DestOffset.

Se il blocco sorgente e il blocco destinazione sono parzialmente o


totalmente sovrapposti, è obbligatorio che l'indirizzo iniziale del
blocco sorgente sia minore dell'indirizzo iniziale del blocco
destinazione; in caso contrario, si ottiene il codice di errore
BL=A8h!

Come si nota in Figura 5, il membro Lenght ha un'ampiezza di 32 bit


per cui, teoricamente, potremmo effettuare trasferimenti di dati tra
blocchi grandi sino a 4 Gb; inoltre, grazie a questa caratteristica,
il servizio 0Bh può essere sfruttato per aggirare la segmentazione a
64 Kb relativa ai trasferimenti di dati che coinvolgono la memoria
base!

Nel caso di trasferimenti di dati tra blocchi particolarmente grandi,


il servizio 0Bh provvede a garantire un adeguato numero di "finestre
temporali" durante le quali la CPU può elaborare eventuali
interruzioni hardware in attesa; in questo modo si evitano gravi
rallentamenti generali del sistema.

I programmi che richiedono servizi XMS relativi agli EMB, non devono
assolutamente modificare lo stato della linea A20 (ad esempio, non è
necessario abilitare la linea A20 per accedere ad un EMB); infatti, a
differenza di quanto accade per la HMA, quando si opera sugli EMB
tale lavoro viene svolto in automatico dagli stessi servizi XMS!

7.4.13 Servizio n.0Ch: Lock Extended Memory Block

Questo servizio tenta di bloccare la posizione di un blocco di


memoria già allocato da un programma.

XMS - Servizio n. 0Ch - Lock Extended Memory Block

Argomenti richiesti:
AH = 0Ch (Lock Extended Memory Block)
DX = handle dell'EMB da bloccare

Valori restituiti:
In caso di successo:
AX = 0001h
DX:BX = indirizzo fisico a 32 bit dell'EMB appena
bloccato
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A2h, ACh, ADh)

Le varie operazioni di allocazione e deallocazione degli EMB compiute


dai programmi, possono portare al classico fenomeno della
frammentazione; per ovviare a questo problema, un XMM potrebbe
decidere ad un certo punto di riorganizzare la disposizione in
164
memoria degli stessi EMB.
Una tale situazione può creare seri problemi a quei programmi che, in
quel momento, stanno operando su EMB presupponendo che essi si
trovino in posizione fissa; per evitare sorprese, un programma può
allora utilizzare il servizio 0Ch attraverso il quale è possibile
richiedere il bloccaggio temporaneo della posizione di un EMB già
allocato dal programma stesso.

In caso di successo, il servizio 0Ch restituisce in DX:BX l'indirizzo


fisico a 32 bit dell'EMB appena bloccato; il programma che ha
richiesto il servizio può quindi utilizzare tale indirizzo per
accedere in modo diretto all'EMB stesso. Come è facile intuire, si
tratta di un servizio destinato principalmente ai SO che operano in
modalità protetta.

Un EMB deve essere tenuto bloccato da un programma per il minor tempo


possibile.
Il servizio 0Ch non deve essere usato per gestire il trasferimento
dati tra blocchi di memoria (servizio 0Bh).
L'XMM è dotato di un lock counter (contatore dei bloccaggi) per
tenere traccia dello stato (bloccato/sbloccato) dei vari EMB.

7.4.14 Servizio n.0Dh: Unlock Extended Memory Block

Questo servizio tenta di sbloccare la posizione di un blocco di


memoria precedentemente bloccato da un programma.

XMS - Servizio n. 0Dh - Unlock Extended Memory Block

Argomenti richiesti:
AH = 0Dh (Unlock Extended Memory Block)
DX = handle dell'EMB da sbloccare

Valori restituiti:
In caso di successo:
AX = 0001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A2h, AAh)

Un programma che ha precedentemente bloccato un EMB, deve provvedere


a sbloccarlo al più presto con il servizio 0Dh; subito dopo lo
sbloccaggio, l'indirizzo a 32 bit dell'EMB (ottenuto dal servizio
0Ch) potrebbe non essere più valido e quindi non deve essere
utilizzato per altre operazioni di I/O!

7.4.15 Servizio n.0Eh: Get EMB Handle Information

Questo servizio tenta di ottenere informazioni addizionali relative


ad un EMB allocato da un programma.

XMS - Servizio n. 0Eh - Get EMB Handle Information

Argomenti richiesti:

165
AH = 0Eh (Get EMB Handle Information)
DX = handle dell'EMB

Valori restituiti:
In caso di successo:
AX = 0001h
BH = valore del contatore di blocco per l'EMB
BL = numero di handles liberi
DX = dimensione in Kb dell'EMB
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A2h)

Il servizio 0Eh restituisce una serie di informazioni addizionali


relative ad un EMB allocato da un programma; se si desidera conoscere
anche l'indirizzo fisico iniziale a 32 bit dell'EMB stesso, bisogna
ricorrere al servizio 0Ch.

7.4.16 Servizio n.0Fh: Reallocate Extended Memory Block

Questo servizio tenta di ridimensionare un EMB allocato da un


programma.

XMS - Servizio n. 0Fh - Reallocate Extended memory Block

Argomenti richiesti:
AH = 0Fh (Reallocate Extended memory Block)
BX = nuova dimensione in Kb dell'EMB
DX = handle dell'EMB

Valori restituiti:
In caso di successo:
AX = 0001h
In caso di insuccesso:
AX = 0000h
BL = Codice di errore (80h, 81h, A0h, A1h, A2h,
ABh)

Il servizio 0Fh tenta di ridimensionare un EMB allocato da un


programma; l'EMB può essere ridimensionato solo se è stato
precedentemente sbloccato.
Se la nuova dimensione è minore di quella vecchia, tutti i dati che
si trovavano nella parte finale (eccedente) del vecchio EMB vengono
persi!

7.4.17 Servizio n.10h: Request Upper Memory Block

Questo servizio tenta di allocare un UMB per un programma.

XMS - Servizio n. 10h - Request Upper Memory Block

Argomenti richiesti:
AH = 10h (Request Upper Memory Block)
DX = dimensione in paragrafi dell'UMB

166
Valori restituiti:
In caso di successo:
AX = 0001h
BX componente Seg dell'UMB (Offset = 0000h)
DX dimensione in paragrafi dell'UMB
In caso di insuccesso:
AX = 0000h
DX = dimensione in paragrafi del più grande UMB
disponibile
BL = Codice di errore (80h, B0h, B1h)

Il servizio 10h tenta di allocare un UMB per un programma;


ovviamente, trovandosi la upper memory al di sotto del primo Mb, non
è richiesta nessuna abilitazione della linea A20!

Come già sappiamo, gli UMB sono paragraph aligned per cui, in caso di
successo, il servizio 10h restituisce in BX la sola componente Seg
dell'indirizzo logico iniziale dell'UMB stesso; la componente Offset
è implicitamente 0000h.

Analogamente a quanto abbiamo visto nel precedente capitolo, se


vogliamo conoscere la dimensione in paragrafi del più grande UMB
disponibile dobbiamo chiamare il servizio 10h ponendo DX=FFFFh.

7.4.18 Servizio n.11h: Release Upper Memory Block

Questo servizio tenta di deallocare un UMB precedentemente allocato


da un programma.

XMS - Servizio n. 11h - Release Upper Memory Block

Argomenti richiesti:
AH = 11h (Release Upper Memory Block)
DX = componente Seg dell'UMB

Valori restituiti:
In caso di successo:
AX = 0001h
DX componente Seg dell'UMB
In caso di insuccesso:
AX = 0000h
DX = dimensione in paragrafi del più grande UMB
disponibile
BL = Codice di errore (80h, B2h)

Il servizio 11h tenta di deallocare un UMB precedentemente allocato


da un programma; dopo la deallocazione, tutte le informazioni
contenute nel blocco non sono più valide!

7.4.19 Servizio n.12h: Reallocate Upper Memory Block

Questo servizio tenta di ridimensionare un UMB allocato da un


programma.

XMS - Servizio n. 12h - Reallocate Upper Memory Block


167
Argomenti richiesti:
AH = 12h (Reallocate Upper Memory Block)
BX = nuova dimensione in paragrafi dell'UMB
DX = componente Seg dell'UMB

Valori restituiti:
In caso di successo:
AX = 0001h
In caso di insuccesso:
AX = 0000h
DX = dimensione in paragrafi del più grande UMB
disponibile
BL = Codice di errore (80h, B0h, B2h)

Il servizio 12h tenta di ridimensionare un UMB precedentemente


allocato da un programma; se la nuova dimensione è minore di quella
vecchia, tutti i dati che si trovavano nella parte finale (eccedente)
del vecchio UMB vengono persi!

7.5 Libreria XMSLIB

Visto e considerato che i servizi XMS sono standard, la cosa migliore


da fare consiste nello scriversi una apposita libreria linkabile a
tutti i programmi che ne hanno bisogno; nella sezione Downloads è
presente una libreria, denominata XMSLIB, che può essere linkata ai
programmi destinati alla generazione di eseguibili in formato EXE.

All'interno del pacchetto xmslibexe.tar.bz2 è presente la


documentazione, la libreria vera e propria XMSLIB.ASM, l'include file
XMSLIB.INC per il NASM e l'header file XMSLIB.H per eventuali
programmi scritti in linguaggio C (purché destinati sempre alla
modalità reale).

Per la creazione dell'object file di XMSLIB.ASM è richiesta la


presenza della libreria di macro LIBPROC.MAC (Capitolo 29 della
sezione Assembly Base con NASM); l'assemblaggio consiste nel semplice
comando:

nasm -f obj xmslib.asm

Per esercizio si può anche provare a convertire XMSLIB.ASM in formato


COM; a tale proposito, nella eventualità di volersi interfacciare
anche all'altra libreria COMLIB.OBJ, bisogna ricordarsi di utilizzare
un segmento unico avente le seguenti caratteristiche:

SEGMENT COMSEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

La possibilità di linkare la libreria ai programmi scritti in C è


legata al fatto che le varie procedure definite in XMSLIB seguono le
convenzioni C per il passaggio degli argomenti e per il valore di
ritorno; da notare, inoltre, la presenza degli underscore all'inizio
di ogni identificatore globale definito nella libreria stessa. Più
avanti vedremo un esempio pratico.

168
Analizzando il codice sorgente della libreria XMSLIB.ASM, notiamo
subito il metodo di chiamata alla funzione di controllo:

call far [cs:XMSControlFunc] (chiamata indiretta intersegmento).

La variabile XMSControlFunc a 32 bit è definita all'interno del


blocco codice XMSLIBCODE ed è destinata a contenere l'indirizzo
Seg:Offset della control function; quando l'esecuzione del programma
salta al codice definito in tale blocco, abbiamo sicuramente
CS=XMSLIBCODE per cui, in quel momento:

cs:XMSControlFunc rappresenta l'indirizzo logico Seg:Offset della


variabile XMSControlFunc,

mentre:

[cs:XMSControlFunc] rappresenta il contenuto della variabile


XMSControlFunc!

Senza il segment override, la variabile XMSControlFunc verrebbe


associata a DS il quale, in quel momento, sta puntando a DATASEGM!

Nel caso di MASM e TASM avremmo dovuto usare la sintassi:

call dword ptr cs:XMSControlFunc

Diverse procedure della libreria richiedono argomenti passati per


indirizzo; tale indirizzo deve essere sempre di tipo FAR!
Ricordando le convenzioni C per il passaggio degli argomenti (da
destra verso sinistra), dobbiamo stare attenti quindi a mettere nella
lista di chiamata, prima la componente Offset e poi la componente Seg
della variabile da passare per indirizzo; in questo modo, la
componente Offset si troverà a precedere la componente Seg in
memoria, nel rispetto della convenzione Intel (con il Pascal avremmo
dovuto disporre le due componenti in ordine inverso).
All'interno delle procedure, le variabili passate per indirizzo
vengono gestite con la coppia DS:SI inizializzata con l'istruzione
LDS; come sappiamo, tale istruzione lavora in modo corretto solo
quando la coppia Seg:Offset, da caricare in DS:SI, si trova disposta
in memoria (in questo caso, nello stack) secondo la convenzione
Intel!

7.6 Esempi pratici

Prima di procedere con alcuni esempi pratici, è necessario


predisporre l'ambiente operativo in modo che la memoria estesa e la
HMA vengano attivate e rese utilizzabili; a tale proposito, si deve
procedere diversamente a seconda del SO che si sta usando.

Nel caso del DOS vero e proprio o della DOS Box di Windows9x, è
necessario editare il file C:\CONFIG.SYS; al suo interno dovrebbe
essere presente una linea del tipo:

169
DEVICE=C:\DOS\HIMEM.SYS oppure, nel caso di Windows,
DEVICE=C:\WINDOWS\HIMEM.SYS

Se questa linea non è presente deve essere aggiunta dall'utente; in


tal caso, è anche necessario riavviare il computer.
Come sappiamo, HIMEM.SYS è il driver più diffuso per il supporto
dell'XMS; bisogna ricordare comunque che esistono anche altri XMM
altrettanto validi come, ad esempio, 386MAX, QEMM e NETROOM.
Su alcune versioni del DOS o di Windows il driver potrebbe anche
avere nomi del tipo HIMEM.COM o HIMEM.EXE; inoltre, abbiamo anche
visto che il driver stesso accetta diversi parametri come, ad
esempio, /HMAMIN=n e /NUMHANDLES=m.
Se abbiamo intenzione di accedere alla HMA via XMS, dobbiamo
ricordarci di commentare, sempre in C:\CONFIG.SYS, la linea DOS=HIGH;
nel caso in cui siano presenti altri parametri (ad esempio,
DOS=HIGH,UMB,AUTO) basta togliere solamente HIGH o sostituirlo con
LOW. Analoghe considerazioni per l'impostazione DOS=UMB, necessaria
nel caso in cui si voglia accedere alla memoria superiore; anche tali
modifiche diventano attive solo dopo il riavvio del computer.
Le impostazioni di memoria relative ad un programma DOS da eseguire
sotto qualunque versione di Windows a 32 bit (compresi quindi Windows
XP e versioni superiori), vengono effettuate attraverso il menu
Proprietà del programma stesso; a tale proposito, basta cliccare con
il tasto destro del mouse sull'icona del programma eseguibile e
selezionare Proprietà - Memoria.

Per quanto riguarda WindowsXP e versioni superiori, tutte le


impostazioni per la configurazione dell'ambiente operativo della DOS
Box si svolgono attraverso i due file AUTOEXEC.NT e CONFIG.NT che si
trovano, generalmente, nella directory C:\WINDOWS\SYSTEM32; tali file
contengono anche una serie di informazioni che spiegano come
effettuare le varie impostazioni.
Una volta che l'utente ha apportato eventuali modifiche ai due file
AUTOEXEC.NT e CONFIG.NT, deve semplicemente riavviare la DOS Box; non
è necessario quindi il riavvio del computer.

Nota importante.
Su alcune vecchie versioni di Windows XP (in particolare, XP
Professional) la DOS Box potrebbe non funzionare in presenza
del parametro DOS=LOW nel file CONFIG.NT; spesso, tale
problema può essere risolto installando appositi SoftPack di
aggiornamento.

Anche sotto Linux i vari emulatori DOS possono essere configurati


attraverso appositi file senza la necessità di riavviare il computer.
Nel caso di DOSEmu, ad esempio, i file di avvio della finestra DOS
sono i soliti AUTOEXEC.BAT e CONFIG.SYS (che si trovano nella
directory scelta come radice C:\ dell'emulatore); per quanto riguarda
le impostazioni della memoria, basta editare il file $HOME/.dosemurc,
apportare le modifiche desiderate nella sezione Memory settings e
riavviare l'emulatore.

7.6.1 Accesso alla HMA via XMS

170
Partiamo con un esempio il cui scopo è quello di permettere ad un
programma DOS di richiedere la disponibilità della HMA; la Figura 6
illustra il relativo listato.

Figura 6 - File HMATEST.ASM

;-----------------------------------------------------;
; file hmatest.asm ;
; accesso alla HMA via XMS (libreria XMSLIB) ;
;-----------------------------------------------------;
; nasm -f obj hmatest.asm ;
; tlink hmatest.obj + xmslib.obj + exelib.obj ;
; (oppure link hmatest.obj + xmslib.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O
%include "xmslib.inc" ; inclusione libreria XMS 3.0
%include "libproc.mac" ; inclusione macro per le procedure

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

%assign FALSE 0 ; variabile booleana FALSE


%assign TRUE 1 ; variabile booleana TRUE

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

XMSControlFunc dd 0 ; indirizzo XMS control func.


XMS_version dw 0 ; versione XMS
XMM_version dw 0 ; versione XMM
Error_code dw 0 ; codice di errore
HMA_exist dw 0 ; esistenza HMA

str_title db "ACCESSO ALLA HMA VIA XMS (LIBRERIA XMSLIB)", 0


str_keypress db "Premere un tasto per continuare ... ", 0
str_xmmtestok db "Il driver XMM e' installato!", 0
str_xmmtestko db "Errore! Il driver XMM non e' installato!", 0
str_xmscfaddr db "XMS Control Function all'indirizzo XXXXh:XXXXh", 0
str_xmsversion db "Standard XMS versione XXh.XXh", 0
str_xmmversion db "Driver XMM versione XXh.XXh", 0
str_hmaexist db "Esistenza HMA XXXXh", 0
str_hmafree db "La HMA e' stata allocata con successo!", 0
str_hmaerror db "Errore HMA! Codice di errore XXh", 0
str_a20enable db "Attivazione linea A20 e trasferimento dati in HMA", 0
str_a20error db "Errore! Attivazione linea A20 fallita!", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point


171
mov ax, DATASEGM ; trasferisce DATASEGM
mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 0013h ; riga 0, colonna 19
call far writeString ; mostra la stringa

; verifica la presenza di un XMM

mov di, str_xmmtestok ; assume XMM presente


callproc LANGC, FARPROC, _test_xmmdriver
cmp ax, TRUE ; XMM presente ?
je xmm_ok ; si
mov di, str_xmmtestko ; no
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; Errore! Fine programma!
xmm_ok:
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

; determina l'indirizzo della control function

mov di, str_xmscfaddr ; stringa da mostrare


mov dx, 0300h ; riga 3, colonna 0
call far writeString ; mostra la stringa
callproc LANGC, FARPROC, _get_xmsctrlfuncaddr
mov [XMSControlFunc+0], ax ; salva l'Offset
mov [XMSControlFunc+2], dx ; salva il Seg

mov ax, [XMSControlFunc+2] ; AX = Seg


mov dx, 0323h ; riga 3, colonna 35
call far writeHex16 ; mostra il Seg
mov ax, [XMSControlFunc+0] ; AX = Offset
mov dx, 0329h ; riga 3, colonna 41
call far writeHex16 ; mostra l'Offset

; determina versione XMS, versione XMM e esistenza HMA

callproc LANGC, FARPROC, _get_xmsversion, XMS_version, seg XMS_version, \


XMM_version, seg XMM_version, \
HMA_exist, seg HMA_exist
mov di, str_xmsversion ; stringa da mostrare
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
mov al, [XMS_version+1] ; AL = major number versione XMS
mov dx, 0416h ; riga 4, colonna 22
call far writeHex8 ; mostra il major number
mov al, [XMS_version+0] ; AL = minor number versione XMS
mov dx, 041Ah ; riga 4, colonna 26
call far writeHex8 ; mostra il minor number
172
mov di, str_xmmversion ; stringa da mostrare
mov dx, 0500h ; riga 5, colonna 0
call far writeString ; mostra la stringa
mov al, [XMM_version+1] ; AL = major number versione XMM
mov dx, 0514h ; riga 5, colonna 20
call far writeHex8 ; mostra il major number
mov al, [XMM_version+0] ; AL = minor number versione XMM
mov dx, 0518h ; riga 5, colonna 24
call far writeHex8 ; mostra il minor number

mov di, str_hmaexist ; stringa da mostrare


mov dx, 0600h ; riga 6, colonna 0
call far writeString ; mostra la stringa
mov ax, [HMA_exist] ; AX = esistenza HMA (1 o 0)
mov dx, 060Eh ; riga 6, colonna 14
call far writeHex16 ; mostra esistenza HMA

cmp word [HMA_exist], TRUE ; HMA disponibile ?


je hma_presente ; si
jmp exit_on_error ; no
hma_presente:

; tenta di allocare la HMA

mov di, str_hmafree ; assume HMA libera


callproc LANGC, FARPROC, _request_HMA, Error_code, seg Error_code, \
word 0FFFFh
cmp ax, TRUE ; HMA allocata ?
je hma_ok ; si
mov di, str_hmaerror ; no
mov dx, 0800h ; riga 8, colonna 0
call far writeString ; mostra la stringa
mov ax, [Error_code] ; AX = codice di errore
mov dx, 081Dh ; riga 8, colonna 29
call far writeHex8 ; mostra il codice di errore
jmp exit_on_error ; Errore! Fine programma!
hma_ok:
mov dx, 0800h ; riga 8, colonna 0
call far writeString ; mostra la stringa

mov di, str_a20enable ; stringa da mostrare


mov dx, 0A00h ; riga 10, colonna 0
call far writeString ; mostra la stringa

mov di, str_keypress ; stringa da mostrare


mov dx, 0C00h ; riga 12, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; tenta di abilitare la linea A20

mov di, str_a20error ; assume A20 error


callproc LANGC, FARPROC, _global_enableA20, Error_code, seg Error_code
cmp ax, TRUE ; linea A20 abilitata ?
je A20_ok ; si
mov dx, 0E00h ; riga 14, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_A20error ; Errore! Fine programma!
A20_ok:

; riempie la HMA con 80 x 24 = 1920 lettere 'A' + un '$' finale


173
push es ; preserva ES
mov ax, 0FFFFh ; AX = Seg inizio HMA
mov es, ax ; copia AX in ES
mov di, 0010h ; DI = Offset inizio HMA
mov eax, "AAAA" ; EAX = 'A', 'A', 'A', 'A'
mov cx, 480 ; 480 dword da trasferire
cld ; clear direction flag
rep stosd ; copia 480 dword in HMA
mov byte [es:di], "$" ; aggiunge un '$' finale
pop es ; ripristina ES

; visualizza la stringa memorizzata in HMA

push ds ; preserva DS
mov ax, 0FFFFh ; AX = Seg inizio HMA
mov ds, ax ; copia AX in DS
mov dx, 0010h ; DX = Offset inizio HMA
mov ah, 09h ; servizio DOS Display String
int 21h ; visualizza la stringa
pop ds ; ripristina DS

; tenta di disabilitare la linea A20

callproc LANGC, FARPROC, _global_disableA20, Error_code, seg Error_code

exit_on_A20error:

; tenta di deallocare la HMA

callproc LANGC, FARPROC, _release_HMA, Error_code, seg Error_code

exit_on_error:
mov di, str_keypress ; stringa da mostrare
mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa

call far waitChar ; attende la pressione di un tasto


call far clearScreen ; pulisce lo schermo
call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Prima di tutto, il programma effettua una serie di test diagnostici
per verificare la presenza del driver XMM, la disponibilità della
HMA, etc; per ogni test viene visualizzato un apposito messaggio

174
informativo.
Successivamente, il programma tenta di allocare la HMA e di abilitare
la linea A20; se l'operazione riesce, viene effettuato un
trasferimento dati che consiste nel sistemare in HMA una stringa
formata da 80x24=1920 elementi di tipo BYTE, ciascuno dei quali
contiene il codice ASCII della lettera 'A'.
Per il trasferimento dati viene usata l'istruzione REP STOSD con
CX=480; infatti, un vettore di 1920 elementi di tipo BYTE può essere
trasferito molto più velocemente quanto viene gestito come un vettore
di 480 elementi di tipo DWORD.
Come si può notare, l'operando destinazione di STOSD è ES:DI che,
inizialmente, punta a FFFFh:0010h (indirizzo logico iniziale della
HMA); ciò dimostra che il programma di Figura 6, pur operando in
modalità reale, riesce a scrivere nel blocco da 65520 byte che si
trova disposto in memoria subito dopo il primo Mb di RAM!

Come sappiamo, l'istruzione CLD pone a zero il flag DF facendo in


modo che REP STOSD si occupi anche dell'incremento automatico del
puntatore DI; nel nostro caso, terminato il trasferimento dati, DI
punta alla fine della stringa per cui l'istruzione successiva
aggiunge alla stringa stessa un carattere '$' richiesto dal servizio
Display String della INT 21h.
Subito dopo, la stringa viene fatta puntare da DS:DX=FFFFh:0010h per
essere visualizzata, appunto, con il servizio 09h della INT 21h;
infine, il programma termina dopo aver rigorosamente disabilitato la
linea A20 e restituito la HMA!

7.6.2 Conversione in linguaggio C del programma HMATEST.ASM

Come è stato già anticipato, la libreria XMSLIB può essere linkata


anche ai programmi scritti in linguaggio C (purché destinati alla
modalità reale); la Figura 7 mostra la traduzione in linguaggio C
(Borland C) del precedente esempio HMATEST.ASM.

Figura 7 - File HMATEST2.C


/*******************************************************/
/* file hmatest2.c */
/* accesso alla HMA via XMS (libreria XMSLIB) */
/*******************************************************/
/* nasm -f obj xmslib.asm */
/* bcc -ml -3 hmatest2.c xmslib.obj */
/*******************************************************/

/************* direttive per il compilatore ************/

#include <stdio.h> /* libreria standard I/O */


#include <conio.h> /* libreria console I/O */
#include <stdlib.h> /* libreria standard */
#include <dos.h> /* servizi DOS */
#include "xmslib.h" /* libreria XMS 3.0 */

/***************** variabili globali ******************/

Ulong XMSControlFunc = 0; /* indirizzo XMS control func. */


Uint XMS_version = 0; /* versione XMS */
Uint XMM_version = 0; /* versione XMM */

175
Uint Error_code = 0; /* codice di errore */
Uint HMA_exist = 0; /* esistenza HMA */

char str_title[] = "ACCESSO ALLA HMA VIA XMS (LIBRERIA XMSLIB)";


char str_keypress[] = "Premere un tasto per continuare ... ";
char str_xmmtestok[] = "Il driver XMM e' installato!";
char str_xmmtestko[] = "Errore! Il driver XMM non e' installato!";
char str_xmscfaddr[] = "XMS Control Function all'indirizzo";
char str_xmsversion[] = "Standard XMS versione";
char str_xmmversion[] = "Driver XMM versione";
char str_hmaexist[] = "Esistenza HMA";
char str_hmafree[] = "La HMA e' stata allocata con successo!";
char str_hmaerror[] = "Errore HMA! Codice di errore";
char str_a20enable[] = "Attivazione linea A20 e trasferimento dati in HMA";
char str_a20error[] = "Errore! Attivazione linea A20 fallita! \
Codice di errore";

/********************* entry point *********************/

int main(void)
{
int i;
Uchar *HMA_pointer = (Uchar *)MK_FP(0xFFFF, 0x0010);

clrscr(); /* pulisce lo schermo */

/* visualizza il titolo del programma */

printf(" %s\n", str_title);

/* verifica la presenza di un XMM */

if (test_xmmdriver())
printf("%s\n", str_xmmtestok);
else
{
printf("%s\n", str_xmmtestko);
exit(1); /* exit value = 1 (errore XMM) */
}

/* determina l'indirizzo della control function */

XMSControlFunc = get_xmsctrlfuncaddr();
printf("%s %4.4Xh:%4.4Xh\n", str_xmscfaddr, (Uint)(XMSControlFunc >> 16), \
(Uint)XMSControlFunc);

/* determina versione XMS, versione XMM e esistenza HMA */

get_xmsversion(&XMS_version, &XMM_version, &HMA_exist);


printf("%s %X.%X\n", str_xmsversion, (Uchar)(XMS_version >> 8), \
(Uchar)XMS_version);
printf("%s %X.%X\n", str_xmmversion, (Uchar)(XMM_version >> 8), \
(Uchar)XMM_version);
printf("%s %s\n\n", str_hmaexist, (HMA_exist ? "SI" : "NO"));

if (!HMA_exist)
exit(2); /* exit value = 2 (errore HMA) */

/* tenta di allocare la HMA */

if (request_HMA(&Error_code, (Uint)0xFFFF))
printf("%s\n\n", str_hmafree);
176
else
{
printf("%s %2.2Xh\n\n", str_hmaerror, (Uchar)Error_code);
exit(3); /* exit value = 3 (errore HMA) */
}

printf("%s\n\n", str_a20enable); /* stringa da mostrare */


printf("%s\n", str_keypress); /* stringa da mostrare */
getch(); /* attende la pressione di un tasto */

/* tenta di abilitare la linea A20 */

if (!global_enableA20(&Error_code))
{
printf("%s %2.2Xh\n\n", str_a20error, (Uchar)Error_code);
release_HMA(&Error_code); /* libera la HMA */
exit(4); /* exit value = 4 (errore A20) */
}

/* riempie la HMA con 80 x 24 = 1920 lettere 'A' (41h) + uno 0 finale */

for (i = 0; i < 1920; i += 4)


*(Ulong *)(HMA_pointer + i) = 0x41414141;

*(HMA_pointer + i) = '\0'; /* zero finale (C string) */

/* visualizza la stringa memorizzata in HMA */

printf("%s", HMA_pointer);

/* tenta di disabilitare la linea A20 */

global_disableA20(&Error_code);

/* tenta di deallocare la HMA */

release_HMA(&Error_code);

printf("%s", str_keypress); /* stringa da mostrare */


getch(); /* attende la pressione di un tasto */
clrscr(); /* pulisce lo schermo */

return 0; /* exit value = 0 */


}
Per la generazione dell'eseguibile, è necessaria la presenza
dell'header file XMSLIB.H (disponibile nel pacchetto
xmslibexe.tar.bz2) e dell'object file XMSLIB.OBJ; a tale proposito,
dobbiamo impartire il comando:

nasm -f obj xmslib.asm

A questo punto possiamo chiamare il compilatore Borland C con il


comando:

bcc -ml -3 hmatest2.c xmslib.obj

Osserviamo, in particolare, il parametro -ml passato al compilatore;


come sappiamo, tale parametro dice al Borland C che vogliamo usare il
modello di memoria large.
Questo aspetto è fondamentale in quanto tutte le procedure definite
177
in XMSLIB e tutti gli argomenti che esse richiedono per indirizzo,
sono i tipo FAR; in presenza del parametro -ml, anche il compilatore
tratta, implicitamente, tutti i puntatori e tutte le funzioni esterne
come se fossero di tipo FAR (a meno che il programmatore non
specifichi indicazioni differenti attraverso gli operatori di
distanza NEAR o FAR).

Analizzando il codice sorgente di Figura 7 notiamo, all'inizio della


funzione main, la definizione:

Uchar *HMA_pointer = (Uchar *)MK_FP(0xFFFF, 0x0010);

MK_FP (Make Far Pointer) è una semplicissima macro del Borland C che
crea un puntatore FAR generico (void *) a partire da una coppia
Seg:Offset; è necessario quindi usare un cast per indicare a MK_FP
che la variabile HMA_Pointer è un puntatore a Uchar (intero senza
segno a 8 bit).
Nel nostro caso, HMA_pointer viene inizializzato con l'indirizzo
logico FFFFh:0010h; come sappiamo, si tratta dell'indirizzo logico da
cui inizia la HMA.

Osserviamo ora il codice che trasferisce 1920 byte (ASCII('A')=41h)


in HMA; siccome HMA_pointer è un puntatore a Uchar, avremmo dovuto
scrivere:

for (i = 0; i < 1920; i++)


*(HMA_pointer + i) = 0x41;

Se, però, vogliamo trasferire 4 byte alla volta, dobbiamo usare il


cast (Ulong *) per indicare al compilatore che, in questo caso
particolare, vogliamo che HMA_pointer venga trattato come puntatore a
Ulong (intero senza segno a 32 bit); possiamo quindi scrivere:

for (i = 0; i < 1920; i += 4)


*(Ulong *)(HMA_pointer + i) = 0x41414141;

Infine, consideriamo la seguente istruzione che visualizza la stringa


memorizzata in HMA:

printf("%s", HMA_pointer);

In questo caso, printf ha bisogno di un puntatore FAR alla stringa da


visualizzare; e infatti, HMA_pointer è proprio un puntatore FAR alla
nostra stringa che si trova all'indirizzo logico FFFFh:0010h.
Utilizzando una sintassi ridondante, avrebbe quindi senso scrivere
anche:

printf("%s", *(&HMA_pointer));

Il simbolo *(&HMA_pointer) rappresenta, infatti, il contenuto


(FFFFh:0010h) dell'indirizzo in cui si trova memorizzata la variabile
HMA_pointer!

Le considerazioni appena esposte dimostrano ulteriormente per quale


178
motivo il linguaggio C venga definito un Assembly di alto livello;
inoltre, appare anche evidente il fatto che la conoscenza
dell'Assembly permette di sfruttare al massimo i potenti strumenti
offerti dai linguaggi di alto livello!

7.6.3 Accesso alla memoria estesa via XMS

La Figura 8 illustra un altro esempio del tutto analogo a quello di


Figura 6; l'unica differenza è data dal fatto che al posto della HMA
utilizziamo un EMB.

Figura 8 - File XMSTEST.ASM


;-----------------------------------------------------;
; file xmstest.asm ;
; accesso agli EMB via XMS (libreria XMSLIB) ;
;-----------------------------------------------------;
; nasm -f obj xmstest.asm ;
; tlink xmstest.obj + xmslib.obj + exelib.obj ;
; (oppure link xmstest.obj + xmslib.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O
%include "xmslib.inc" ; inclusione libreria XMS 3.0
%include "libproc.mac" ; inclusione macro per le procedure

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

%assign FALSE 0 ; variabile booleana FALSE


%assign TRUE 1 ; variabile booleana TRUE

; struttura per lo scambio di dati UMB - EMB

STRUC ExtMemMoveStruct

Length resd 1 ; numero di byte da trasferire


SourceHandle resw 1 ; handle del blocco sorgente
SourceOffset resd 1 ; offset a 32 bit nel blocco sorgente
DestHandle resw 1 ; handle del blocco destinazione
DestOffset resd 1 ; offset a 32 bit nel blocco dest.

ENDSTRUC

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

move_struct ISTRUC ExtMemMoveStruct

at Length, dd 0 ; numero di byte da trasferire


at SourceHandle, dw 0 ; handle del blocco sorgente
at SourceOffset, dd 0 ; offset a 32 bit nel blocco sorgente
at DestHandle, dw 0 ; handle del blocco destinazione
at DestOffset, dd 0 ; offset a 32 bit nel blocco dest.
179
IEND

XMSControlFunc dd 0 ; indirizzo XMS control func.


XMS_version dw 0 ; versione XMS
XMM_version dw 0 ; versione XMM
Error_code dw 0 ; codice di errore
HMA_exist dw 0 ; esistenza HMA
EMB_total dw 0 ; memoria estesa totale in Kb
EMB_max dw 0 ; max. EMB libero in Kb
DOS_strategy dw 0 ; strategia di allocazione orig.
UML_state dw 0 ; stato originale dell'UML
UMB_seg dw 0 ; componente Seg di un UMB
EMB_handle dw 0 ; handle dell'EMB allocato

str_title db "ACCESSO AGLI EMB VIA XMS (LIBRERIA XMSLIB)", 0


str_keypress db "Premere un tasto per continuare ... ", 0
str_xmmtestok db "Il driver XMM e' installato!", 0
str_xmmtestko db "Errore! Il driver XMM non e' installato!", 0
str_xmscfaddr db "XMS Control Function all'indirizzo XXXXh:XXXXh", 0
str_xmsversion db "Standard XMS versione XXh.XXh", 0
str_xmmversion db "Driver XMM versione XXh.XXh", 0
str_embtotal db "Memoria estesa totale XXXXX Kb", 0
str_embmax db "Dimensione del piu' grande EMB libero XXXXX Kb", 0
str_umbinfo db "UMB da 121 para. allocato all'indirizzo XXXXH:0000H", 0
str_embinfo db "Allocazione di un EMB da 2 Kb - Handle 0000H", 0
str_error1 db "Errore 1! Lettura stato UML fallita!", 0
str_error2 db "Errore 2! Modifica stato UML fallita!", 0
str_error3 db "Errore 3! Lettura strategia DOS fallita!", 0
str_error4 db "Errore 4! Modifica strategia DOS fallita!", 0
str_error5 db "Errore 5! Allocazione UMB fallita!", 0
str_error6 db "Errore 6! Allocazione EMB fallita!", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

; restituzione memoria in eccesso

mov bx, 480 ; nuova dimensione in paragrafi


mov ah, 4Ah ; servizio Resize Memory Block
int 21h ; chiama il DOS

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 0013h ; riga 0, colonna 19
180
call far writeString ; mostra la stringa

; verifica la presenza di un XMM

mov di, str_xmmtestok ; assume XMM presente


callproc LANGC, FARPROC, _test_xmmdriver
cmp ax, TRUE ; XMM presente ?
je xmm_ok ; si
mov di, str_xmmtestko ; no
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; Errore! Fine programma!
xmm_ok:
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

; determina l'indirizzo della control function

mov di, str_xmscfaddr ; stringa da mostrare


mov dx, 0300h ; riga 3, colonna 0
call far writeString ; mostra la stringa
callproc LANGC, FARPROC, _get_xmsctrlfuncaddr
mov [XMSControlFunc+0], ax ; salva l'Offset
mov [XMSControlFunc+2], dx ; salva il Seg

mov ax, [XMSControlFunc+2] ; AX = Seg


mov dx, 0323h ; riga 3, colonna 35
call far writeHex16 ; mostra il Seg
mov ax, [XMSControlFunc+0] ; AX = Offset
mov dx, 0329h ; riga 3, colonna 41
call far writeHex16 ; mostra l'Offset

; determina versione XMS, versione XMM e esistenza HMA

callproc LANGC, FARPROC, _get_xmsversion, XMS_version, seg XMS_version, \


XMM_version, seg XMM_version, \
HMA_exist, seg HMA_exist
mov di, str_xmsversion ; stringa da mostrare
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
mov al, [XMS_version+1] ; AL = major number versione XMS
mov dx, 0416h ; riga 4, colonna 22
call far writeHex8 ; mostra il major number
mov al, [XMS_version+0] ; AL = minor number versione XMS
mov dx, 041Ah ; riga 4, colonna 26
call far writeHex8 ; mostra il minor number

mov di, str_xmmversion ; stringa da mostrare


mov dx, 0500h ; riga 5, colonna 0
call far writeString ; mostra la stringa
mov al, [XMM_version+1] ; AL = major number versione XMM
mov dx, 0514h ; riga 5, colonna 20
call far writeHex8 ; mostra il major number
mov al, [XMM_version+0] ; AL = minor number versione XMM
mov dx, 0518h ; riga 5, colonna 24
call far writeHex8 ; mostra il minor number

; verifica memoria espansa totale e max EMB libero

callproc LANGC, FARPROC, _query_EMB, Error_code, seg Error_code, \


EMB_total, seg EMB_total
mov [EMB_max], ax ; salva la dim. max. EMB libero
181
mov di, str_embtotal ; stringa da mostrare
mov dx, 0600h ; riga 6, colonna 0
call far writeString ; mostra la stringa
mov ax, [EMB_total] ; AX = memoria estesa totale
mov dx, 0616h ; riga 6, colonna 22
call far writeUdec16 ; mostra EMB totale

mov di, str_embmax ; stringa da mostrare


mov dx, 0700h ; riga 7, colonna 0
call far writeString ; mostra la stringa
mov ax, [EMB_max] ; AX = max. EMB libero
mov dx, 0726h ; riga 7, colonna 38
call far writeUdec16 ; mostra max. EMB libero

cmp word [EMB_max], 2 ; max EMB maggiore di 2 Kb ?


ja continua ; si
jmp exit_on_error ; no
continua:

; salva lo stato corrente dell'Upper Memory Link

mov ax, 5802h ; servizio Get UML state


int 21h ; chiama il DOS
mov [UML_state], al ; salva l'informazione
jnc continua1 ; ok
mov di, str_error1 ; stringa di errore 1
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; uscita per errore 1
continua1:

; imposta il nuovo stato dell'UML (abilita la memoria superiore)

mov ax, 5803h ; servizio Set UML state


mov bx, 0001h ; UML attivato
int 21h ; chiama il DOS
jnc continua2 ; ok
mov di, str_error2 ; stringa di errore 2
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; uscita per errore 2
continua2:

; salva la strategia di allocazione originale del DOS

mov ax, 5800h ; servizio Get memory alloc. strategy


int 21h ; chiama il DOS
mov [DOS_strategy], al ; salva l'informazione
jnc continua3 ; ok
mov di, str_error3 ; stringa di errore 3
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; uscita per errore 3
continua3:

; imposta la nuova strategia di allocazione del DOS (first fit - upper only)

mov ax, 5801h ; servizio Set memory alloc. strategy


mov bl, 40h ; first fit - upper only
int 21h ; chiama il DOS
jnc continua4 ; ok
182
mov di, str_error4 ; stringa di errore 4
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; uscita per errore 4
continua4:

; allocazione di un blocco di memoria superiore da 121 paragrafi

mov ah, 48h ; servizio Allocate Memory


mov bx, 121 ; paragrafi da allocare
int 21h ; chiama il DOS
jnc continua5 ; ok
mov di, str_error5 ; stringa di errore 5
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_UMB_error ; uscita per errore UMB
continua5:
mov [UMB_seg], ax ; salva il Seg del blocco allocato

mov di, str_umbinfo ; stringa da mostrare


mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
mov ax, [UMB_seg] ; AX = Seg UMB allocato
mov dx, 0928h ; riga 9, colonna 40
call far writeHex16 ; mostra Seg UMB allocato

; allocazione di un EMB da 2 Kb

callproc LANGC, FARPROC, _allocate_EMB, Error_code, seg Error_code, \


EMB_handle, seg EMB_handle, \
word 2

test ax, ax ; errore ?


jnz continua6 ; no (AX = 0001h)
mov di, str_error6 ; stringa di errore 6
mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_EMB_error ; uscita per errore EMB
continua6:
mov di, str_embinfo ; stringa da mostrare
mov dx, 0A00h ; riga 10, colonna 0
call far writeString ; mostra la stringa
mov ax, [EMB_handle] ; AX = Handle EMB allocato
mov dx, 0A27h ; riga 10, colonna 39
call far writeHex16 ; mostra Handle EMB allocato

mov di, str_keypress ; stringa da mostrare


mov dx, 0C00h ; riga 12, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; riempie l'UMB con 80 x 24 = 1920 lettere 'X' + un '$' finale

push es ; preserva ES
mov es, [UMB_seg] ; ES = Seg UMB
xor di, di ; DI = Offset UMB (0000h)
mov eax, "XXXX" ; EAX = 'X', 'X', 'X', 'X'
mov cx, 480 ; 480 dword da trasferire
cld ; clear direction flag
rep stosd ; copia 480 dword in HMA
mov byte [es:di], "$" ; aggiunge un '$' finale
pop es ; ripristina ES
183
; trasferisce 121 paragrafi (1936 byte) da UMB a EMB

mov dword [move_struct + Length], 1936


mov word [move_struct + SourceHandle], 0000h
mov ax, [UMB_seg]
mov [move_struct + SourceOffset + 2], ax
mov word [move_struct + SourceOffset + 0], 0000h
mov ax, [EMB_handle]
mov [move_struct + DestHandle], ax
mov dword [move_struct + DestOffset], 00000000h

callproc LANGC, FARPROC, _move_EMB, Error_code, seg Error_code, \


move_struct, seg move_struct

; pulizia UMB con 80 x 24 = 1920 spazi

push es ; preserva ES
mov es, [UMB_seg] ; ES = Seg UMB
xor di, di ; DI = Offset UMB (0000h)
mov eax, " " ; EAX = ' ', ' ', ' ', ' '
mov cx, 480 ; 480 dword da trasferire
cld ; clear direction flag
rep stosd ; copia 480 dword in HMA
pop es ; ripristina ES

; trasferisce 121 paragrafi (1936 byte) da EMB a UMB

mov dword [move_struct + Length], 1936


mov ax, [EMB_handle]
mov [move_struct + SourceHandle], ax
mov dword [move_struct + SourceOffset], 00000000h
mov word [move_struct + DestHandle], 0000h
mov ax, [UMB_seg]
mov [move_struct + DestOffset + 2], ax
mov word [move_struct + DestOffset + 0], 0000h

callproc LANGC, FARPROC, _move_EMB, Error_code, seg Error_code, \


move_struct, seg move_struct

; visualizza la stringa memorizzata in UMB

push ds ; preserva DS
mov ax, [UMB_seg] ; AX = Seg UMB
mov ds, ax ; copia AX in DS
xor dx, dx ; DX = Offset UMB (0000h)
mov ah, 09h ; servizio DOS Display String
int 21h ; visualizza la stringa
pop ds ; ripristina DS

; deallocazione dell'EMB da 2 Kb

callproc LANGC, FARPROC, _free_EMB, Error_code, seg Error_code, \


word [EMB_handle]

exit_on_EMB_error:

; deallocazione del blocco di memoria superiore da 121 paragrafi

push es ; preserva ES
mov ah, 49h ; servizio Free Memory
mov es, [UMB_seg] ; Seg. del blocco da deallocare
184
int 21h ; chiama il DOS
pop es ; ripristina ES

exit_on_UMB_error:

; ripristina la strategia originale di allocazione del DOS

mov ax, 5801h ; servizio Set memory alloc. strategy


mov bl, [DOS_strategy] ; strategia originale
int 21h ; chiama il DOS

; ripristina lo stato originale dell'UML

mov ax, 5803h ; servizio Set UML state


mov bx, [UML_state] ; UML originale
int 21h ; chiama il DOS

exit_on_error:

mov di, str_keypress ; stringa da mostrare


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa

call far waitChar ; attende la pressione di un tasto


call far clearScreen ; pulisce lo schermo
call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################

Inizialmente il programma mostra i soliti messaggi diagnostici; in


questo caso, viene mostrata anche la quantità totale di memoria
estesa e la dimensione del più grande EMB libero.
In seguito, il programma alloca un UMB da 121 paragrafi (1936 byte) e
un EMB da 2 Kb (2048 byte); questi due blocchi vengono utilizzati per
lo scambio di dati tra memoria base e memoria estesa. Si noti che per
poter allocare blocchi di memoria DOS, il programma deve prima
liberare la memoria che occupa in eccesso; in base alle informazioni
ricavate dal map file, la dimensione del program segment viene
ridotta a 480 paragrafi.
Come sappiamo, prima di richiedere un UMB dobbiamo attivare l'UML e
modificare opportunamente la strategia di allocazione del DOS; questi
due passaggi devono essere svolti in ordine rigoroso (prima
l'attivazione dell'UML e poi la modifica della strategia di
allocazione).
185
A questo punto, l'UMB viene riempito con una stringa di 1920 lettere
'X' più un '$' finale, per un totale di 1921 byte; tale stringa viene
poi trasferita nell'EMB come se fosse un blocco unico da 1936 byte o
121 paragrafi (si ricordi che i trasferimenti di dati che coinvolgono
la memoria estesa devono avere una lunghezza in byte multipla di 2).
Successivamente, l'UMB viene ripulito attraverso 1920 spazi (' ') e
la stringa precedente viene ritrasferita dall'EMB all'UMB stesso;
l'ultimo passo consiste nel visualizzare la stringa appena
trasferita.

Per l'allocazione e la deallocazione dell'UMB il programma di Figura


8 si serve dei normali servizi DOS; questa è una pratica molto comune
anche perché, come sappiamo, lo standard XMS 3.0 considera
facoltativo il supporto dei servizi per la gestione della memoria
superiore!

Bibliografia

eXtended Memory Specifications (XMS), ver. 3.0


(disponibile nella sezione Downloads - Documentazione -
xms30.tar.bz2)

186
Capitolo 8 Lo standard EMS - Expanded Memory
Specifications
Sin dai tempi dei vecchi PC basati sulle CPU 8086, ci si era già resi
conto del fatto che l'unico Mb indirizzabile con l'address bus a 20
linee si sarebbe rivelato ben presto insufficiente a soddisfare i
requisiti di memoria, sempre più esorbitanti, richiesti dai nuovi
programmi; nell'attesa che uscissero nuove CPU dotate di un maggior
numero di linee di indirizzo, venne adottata una soluzione largamente
sfruttata nel mondo della modalità reale.

La Figura 1 illustra il concetto fondamentale su cui si basa tale


soluzione.

Figura 1 - Tecnica del frame-buffer

Bisogna partire quindi dal presupposto che la 8086 può accedere in


modo diretto ad un solo Mb di RAM, compreso tra gli indirizzi fisici
00000h e FFFFFh; qualunque altra memoria supplementare (memoria
esterna) che, in genere, si trova su una scheda separata dalla
cosiddetta RAM centrale, risulta quindi inaccessibile, in modo
diretto, alla CPU.
Per aggirare questo ostacolo, viene utilizzata la tecnica di Figura
1; in pratica, nel momento in cui si vuole accedere in I/O ad una
determinata area della memoria esterna, si fa in modo che l'area
stessa "appaia" in un preciso punto della RAM centrale.

Non si sta parlando quindi di un trasferimento dati da memoria


esterna a RAM centrale e viceversa (come nel caso, analizzato nel
precedente capitolo, in relazione alla memoria estesa); si tratta,
invece, di un vero e proprio procedimento hardware grazie al quale la
porzione desiderata di memoria esterna viene "mappata" fisicamente in
una determinata area della RAM centrale!
A questo punto, l'area della memoria esterna appena mappata nella RAM
187
centrale si viene a trovare all'interno dello spazio di
indirizzamento della CPU e risulta quindi accessibile in modo diretto
(e in modalità reale); tutte le operazioni di I/O svolte all'interno
dell'area mappata nella RAM centrale, si ripercuotono immediatamente
sulla corrispondente area della memoria esterna, senza la necessità
di un trasferimento dati.

La tecnica appena descritta prende il nome di I/O Memory Mapped


(input/output su un'area di memoria esterna mappata nella memoria
centrale); l'area della RAM centrale nella quale viene mappata la
porzione di memoria esterna prende il nome di frame buffer (buffer
fotogramma).

Il compito di mappare la memoria esterna nel frame buffer spetta


all'hardware della scheda su cui è installata la stessa memoria
esterna; il programmatore quindi non deve fare altro che specificare
(all'apposito driver) quale area della memoria esterna deve essere
mappata nel frame buffer.

Il termine frame buffer fa chiaramente riferimento all'analogia con


la pellicola cinematografica (la memoria esterna) la quale, come
sappiamo, è composta da una lunga sequenza di fotogrammi; il
proiettore (l'hardware della memoria esterna) fa scorrere la
pellicola in modo che i vari fotogrammi compaiano uno alla volta
sullo schermo (il frame buffer) risultando così visibili al pubblico
(la CPU).

Come è stato già spiegato in precedenza, la tecnica descritta in


Figura 1 viene largamente impiegata nel mondo della modalità reale
proprio con lo scopo di permettere alla CPU l'accesso diretto alle
memorie che si trovano al di fuori del suo spazio di indirizzamento;
tra gli esempi più emblematici si possono citare: il BIOS del PC, il
BIOS della scheda video e la VRAM (Video RAM) della stessa scheda
video.
Nel caso particolare in cui la memoria esterna sia una scheda
contenente RAM supplementare che va ad aggiungersi alla RAM centrale,
si utilizza la definizione di espansione di memoria; la RAM
supplementare presente in tale scheda viene definita memoria espansa.

Come si può facilmente intuire, la segmentazione a 64 Kb che


caratterizza la gestione della memoria in modalità reale con le CPU
80x86, si ripercuote anche sulla tecnica illustrata in Figura 1; in
sostanza, il frame buffer è una sorta di "collo di bottiglia"
attraverso il quale possiamo "vedere" la memoria esterna suddivisa in
tante porzioni ciascuna delle quali non può superare la dimensione
massima di 64 Kb!

Ai tempi delle CPU 8086, se si aveva la necessità di ulteriore


memoria RAM, l'unica strada da seguire consisteva nell'installazione
di una espansione di memoria la cui caratteristica principale era il
prezzo proibitivo; l'utente doveva anche installare un apposito
driver il cui compito era quello di pilotare l'hardware della stessa
espansione di memoria.
Ci si potrebbe chiedere allora che senso abbia oggi parlare di
188
memoria espansa, vista la disponibilità delle potenti CPU 80386 e
superiori, capaci di indirizzare direttamente diversi Gb di RAM
(anche in modalità reale grazie ai servizi XMS descritti nel
precedente capitolo).
La risposta a questa domanda è legata ad una geniale intuizione avuta
dagli sviluppatori di 386MAX (il famoso XMM descritto nel precedente
capitolo); quegli sviluppatori si accorsero ad un certo punto che
avendo a disposizione una CPU 80386 o superiore ed un XMM, sarebbe
stato possibile simulare la memoria espansa via software, facendola
apparire nella memoria estesa!
L'aspetto interessante è che la gestione della memoria espansa
(simulata) avviene in modo nettamente più veloce ed efficiente
rispetto al caso della memoria estesa (grazie al fatto che non
abbiamo a che fare con le lentissime operazioni di attivazione e
disattivazione della linea A20, necessarie per poter svolgere
trasferimenti di dati); ma un altro aspetto ben più interessante per
i programmi che operano in modalità reale è dato dal fatto che, a
differenza di quanto accade con la memoria estesa, utilizzabile solo
per operazioni di I/O, la memoria espansa può essere utilizzata anche
per implementare SO capaci di eseguire uno o più programmi
(multitasking)!
Il driver della memoria espansa fornisce anche tutti i meccanismi di
protezione necessari per evitare che due o più programmi in
esecuzione possano interferire tra loro; la vecchia versione 2.0 di
Windows venne completamente riscritta proprio per poter beneficiare
di tutte quelle novità che, all'epoca, erano sicuramente
rivoluzionarie.

8.1 Lo standard LIM-EMS 4.0

In analogia a quanto è successo per la memoria estesa, anche


l'utilizzo della memoria espansa da parte dei programmi che operano
in modalità reale è stato regolamentato da un apposito standard
definito sul finire degli anni 80 da un gruppo di aziende composto
dalla Lotus Development Corporation, dalla Intel Corporation e dalla
Microsoft Corporation; le iniziali dei nomi di queste tre aziende
hanno originato l'acronimo LIM, mentre per indicare lo standard viene
usato l'acronimo EMS che sta per Expanded Memory Specifications.
Nel seguito del capitolo faremo riferimento allo standard LIM-EMS 4.0
definito nel 1987.

8.1.1 Definizioni convenzionali

Con la definizione di expanded memory (memoria espansa) si indica


tutta la RAM che si trova disposta oltre i primi 640 Kb (cioè, subito
dopo la memoria convenzionale); come sappiamo, una CPU che opera in
modalità reale, oltre i primi 640 Kb può accedere in modo diretto
solamente alla upper memory.
Lo standard LIM-EMS 4.0 prevede, appunto, che proprio nella upper
memory venga creato il frame buffer, attraverso il quale è possibile
accedere a tutta la memoria espansa disponibile; è chiaro quindi che
la frase: "tutta la RAM che si trova disposta oltre i primi 640 Kb"
deve essere interpretata come: "tutta la upper memory, compresa la
expanded memory visibile attraverso il frame buffer"!
189
Si ricordi che, come è stato spiegato nei precedenti capitoli, in
assenza di appositi accorgimenti tutta la RAM compresa tra gli
indirizzi fisici A0000h e FFFFFh (cioè, la upper memory) risulta
invisibile ai normali programmi DOS; uno di questi accorgimenti è
rappresentato, appunto, dallo standard LIM-EMS.

Il termine EMM sta per Expanded Memory Manager e indica un qualsiasi


driver capace di fornire tutti i servizi necessari per l'accesso alla
memoria espansa; l'EMM più famoso è sicuramente EMM386 in quanto
viene fornito insieme al DOS.
Compito fondamentale dell'EMM è anche quello di creare il frame
buffer nella upper memory; a tale proposito, si veda la Figura 1 del
Capitolo 6, sezione Assembly Avanzato.

L'EMM suddivide la memoria espansa in tanti blocchi, consecutivi e


contigui, ciascuno dei quali ha una dimensione pari a 16 Kb; ogni
blocco da 16 Kb rappresenta una cosiddetta logical page (pagina
logica).
A sua volta, il frame buffer viene suddiviso in tanti blocchi,
consecutivi e contigui, ciascuno dei quali ha una dimensione pari a
16 Kb; ogni blocco da 16 Kb rappresenta una cosiddetta physical page
(pagina fisica).

In ciascuna pagina fisica del frame buffer può essere mappata una
delle eventuali pagine logiche appositamente allocate da un programma
nella memoria espansa; si viene a creare quindi una situazione come
quella descritta in Figura 2.

Figura 2 - Pagine logiche e fisiche

190
Nell'esempio di Figura 2 notiamo che l'EMM ha creato il frame buffer
in upper memory a partire dall'indirizzo fisico D0000h a cui possiamo
associare l'indirizzo logico normalizzato D000h:0000h; risultano
disponibili quattro pagine fisiche, convenzionalmente numerate da 0 a
3, per un totale di 4*16=64 Kb.
Sempre nell'esempio di Figura 2, un programma in esecuzione ha
allocato dodici pagine logiche, convenzionalmente numerate da 0 a 11;
complessivamente, il programma stesso sta usando 12*16=192 Kb di
memoria espansa.

Una volta creato lo schema di Figura 2, possiamo gestire la memoria


espansa nel modo che riteniamo più opportuno, anche senza rispettare
l'ordine stabilito dalla numerazione; in pratica (come si vede in
Figura 2), una qualunque delle dodici pagine logiche può essere
mappata in una qualunque delle quattro pagine fisiche.
Appare evidente però che per ottenere il massimo risultato in termini
di efficienza, conviene gestire le pagine logiche a gruppi di
quattro, in modo da operare su 64 Kb di memoria espansa per volta;
per lo stesso motivo, è anche chiaro il vantaggio che si ottiene
facendo in modo che ciascun gruppo contenga quattro pagine logiche
consecutive e contigue (ad esempio, gruppo(0, 1, 2, 3), gruppo(4, 5,
6, 7), etc)!

8.1.2 Installazione dell'Expanded Memory Manager

Nel seguito del capitolo, ci occuperemo della simulazione della


memoria espansa attraverso la memoria estesa; di conseguenza, viene
data per scontata la presenza di una CPU 80386 o superiore e di una
adeguata quantità di memoria estesa.

Se vogliamo simulare la memoria espansa attraverso la memoria estesa,


prima di tutto dobbiamo procedere, ovviamente, all'installazione di
un XMM; a tale proposito, valgono tutte le considerazioni esposte nel
precedente capitolo.

A questo punto possiamo passare all'installazione dell'EMM; come al


solito, il procedimento può variare a seconda del SO che si sta
impiegando.

In un ambiente DOS puro o con la DOS Box di Windows 9x, aprendo con
un editor il file C:\CONFIG.SYS, si può notare la presenza di una
linea del tipo:

DEVICE=C:\DOS\EMM386.EXE o, nel caso di Windows,


DEVICE=C:\WINDOWS\EMM386.EXE

Tale linea deve essere successiva al comando di installazione


dell'XMM e provvede, a sua volta, ad installare l'EMM predefinito
(EMM386.EXE) in fase di avvio del SO; lo stesso driver accetta anche
numerosi parametri da linea di comando, alcuni dei quali verranno
illustrati nel seguito (per un elenco dettagliato di tali parametri e
del loro significato, si consiglia di consultare un manuale del DOS).

191
Si presti attenzione al fatto che il precedente comando potrebbe
anche assumere il seguente aspetto:

DEVICE=C:\DOS\EMM386.EXE NOEMS

Il parametro NOEMS indica che vogliamo la disponibilità della memoria


superiore, ma non della memoria espansa; in un caso del genere, per
rendere disponibile la memoria espansa dobbiamo eliminare il
parametro NOEMS e riavviare il computer!

In relazione alla DOS Box di Windows XP, la disponibilità della


memoria espansa può essere richiesta direttamente dai programmi (come
viene spiegato più avanti); eventualmente, l'utente può impostare
alcuni dettagli attraverso il comando EMM presente nel file
C:\WINDOWS\SYSTEM32\CONFIG.NT (lo stesso file contiene tutte le
spiegazioni in proposito).
Dopo aver effettuato le modifiche richieste, basta riavviare la DOS
Box; non è necessario quindi il riavvio del computer.

Nel caso degli emulatori come DOSEmu, in genere, non è necessario


apportare alcuna modifica in quanto nel file CONFIG.SYS è già
presente il comando:

devicehigh=c:\dosemu\ems.sys

Nota importante.
Sulla base di ciò che è stato esposto in questo e nei
precedenti due capitoli, si rende necessario un chiarimento
in relazione alla gestione delle varie aree di memoria da
parte dei programmi che operano in modalità reale; vediamo
allora di fare un riassunto.

Normalmente, il DOS è in grado di vedere esclusivamente i


primi 640 Kb di RAM (memoria convenzionale); tutti i servizi
di gestione della memoria forniti dal DOS e basati sulla INT
21h operano, implicitamente, su tale area della RAM.
Attraverso alcuni servizi non documentati, il DOS permette
anche di accedere alla HMA; tali servizi sono destinati al SO
e il loro uso da parte dei programmi è vivamente
sconsigliato.

Lo standard XMS definisce le linee guida per la realizzazione


dei driver XMM i quali forniscono ai programmi DOS una serie
completa di servizi per la gestione della upper memory, della
HMA e della extended memory; in presenza di un driver XMM,
quindi, un programma DOS è in grado di vedere la upper memory
e può accedervi attraverso i servizi XMS ma non attraverso i
servizi ordinari della INT 21h.

Lo standard EMS sfrutta la presenza di un driver XMM per


rendere possibile la simulazione via software della expanded
memory nella extended memory; a tale proposito, è presente un
driver EMM il quale crea in upper memory un frame buffer da
utilizzare come "finestra" di accesso alla memoria espansa.
192
Il driver EMM rende direttamente visibile la upper memory ai
programmi DOS, ma non fornisce nessun servizio per la
gestione degli UMB; se si ha la necessità di accedere agli
UMB (in presenza di un driver EMM), bisogna quindi utilizzare
i servizi XMS o, in alternativa, quelli ordinari del DOS
basati sulla INT 21h.

Si tenga presente che le considerazioni appena esposte si


riferiscono al caso dei driver predefiniti (HIMEM e EMM386)
forniti dal DOS; esistono anche dei driver alternativi (ad
esempio, 386MAX) i quali sono in grado di fornire da soli il
supporto per qualsiasi tipo di memoria, compresa quella
estesa ed espansa.

8.2 Interfaccia di programmazione EMS

Tutti i servizi forniti da un EMM risultano accessibili attraverso


una chiamata della INT 67h (denominata LIM-EMS interrupt); ciascuno
di tali servizi viene identificato attraverso un codice a 8 bit da
caricare in AH.
Nel caso generale, quindi, l'accesso ad un servizio EMS, fornito da
un EMM, avviene nel seguente modo:

mov ah, codice servizio ; AH = codice del servizio richiesto


int 67h ; chiamata della LIM-EMS interrupt

Un programma che intenda accedere ai servizi offerti dallo standard


EMS deve innanzi tutto verificare che sia installato (e che sia
operativo) un EMM in memoria; a tale proposito, come vedremo anche
più avanti, è necessario porre AH=40h (Get EMM Status) e chiamare la
INT 67h. L'aspetto paradossale, però, è che tale servizio può essere
richiesto solo se un EMM è già installato in memoria!
Come se non bastasse, in assenza di un EMM in memoria, la chiamata
alla INT 67h potrebbe anche provocare un crash del computer; ciò
accade, ad esempio, quando il BIOS (durante il boot) non inizializza
correttamente la ISR associata al vettore di interruzione n.67h
(generalmente, le ISR associate ai vettori di interruzione
inutilizzati, vengono inizializzate dal BIOS con il codice macchina
CFh dell'istruzione IRET).

Per ovviare a questo problema, possiamo seguire un procedimento


alternativo basato sul fatto che, se un EMM è installato in memoria,
allora all'indirizzo logico XXXXh:000Ah, dove XXXXh è la componente
Seg della ISR associata alla INT 67h, è presente la stringa
"EMMXXXX0"; tale stringa rappresenta il nome in codice che
identifica, appunto, un EMM.
Possiamo verificare questo aspetto anche attraverso il programma
DEBUG (solo in un ambiente DOS puro o con un emulatore come DOSEmu);
a tale proposito, tenendo presente che 67h*4=019Ch, la coppia
Seg:Offset della relativa ISR sarà memorizzata nel vettore delle
interruzioni all'indirizzo logico 0000h:019Ch. A questo punto,
supponendo che a tale indirizzo sia presente la coppia C302h:0091h,

193
non dobbiamo fare altro che richiedere il "dump" della memoria con il
comando:

-d c302:000a

In base alle considerazioni appena esposte, per sapere se il driver


EMM è installato in memoria possiamo procedere nel modo seguente:

xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov es, [es:(67h * 4) + 2] ; ES = Seg ISR
mov di, 000Ah ; DI = 10

mov cx, 8 ; CX = lunghezza stringa


cld ; clear direction flag
repz cmpsb ; comparazione stringhe
jz EMM_presente ; if (ZF == 1) comparazione OK
........... ; else mostra un messaggio di errore
jmp exit_program ; e termina il programma
EMM_presente: ; continua l'esecuzione

Queste istruzioni presuppongono che la coppia DS:SI stia puntando


alla stringa "EMMXXXX0"; bisogna ricordare, infatti, che l'istruzione
CMPSB compara il BYTE puntato da DS:SI (stringa sorgente) con il BYTE
puntato da ES:DI (stringa destinazione).
Dopo ogni comparazione CMPSB pone ZF=1 se i due BYTE sono uguali e
ZF=0 se i due BYTE sono diversi; l'istruzione CLD pone DF=0
determinando così l'incremento automatico dei puntatori SI e DI dopo
l'esecuzione di CMPSB.
A sua volta, il prefisso REPZ ripete il loop per un massimo di CX=8
volte decrementando lo stesso registro CX ad ogni iterazione (senza
alterare il registro FLAGS); il loop termina in modo regolare quando
CX=0 o in modo prematuro quando ZF=0.
Se il loop termina in modo regolare si ha quindi CX=0 e ZF=1; in tal
caso, siamo sicuri che le due stringhe comparate sono uguali.
Nel caso in cui la comparazione abbia dato esito positivo, possiamo
procedere con la chiamata del servizio AH=40h della INT 67h per
verificare lo stato operativo dell'EMM; tale servizio viene descritto
in dettaglio più avanti.

Se la richiesta di un servizio EMS ha successo, la chiamata alla INT


67h restituisce AH=00h più eventuali informazioni addizionali in
altri registri; se, invece, la richiesta fallisce, la chiamata alla
INT 67h restituisce un codice di errore sempre in AH.
I codici di errore validi hanno sempre il bit più significativo che
vale 1; la Figura 3 illustra l'elenco completo dei codici di errore.

Figura 3 - Codici di errore EMS


AH Tipo di errore
80h l'EMM ha segnalato un malfunzionamento software
81h l'EMM ha segnalato un malfunzionamento hardware
82h l'EMM è occupato
83h handle non trovato

194
Figura 3 - Codici di errore EMS
AH Tipo di errore
84h il registro AH specifica un servizio inesistente
85h non ci sono più handle disponibili
86h errore di ripristino del mapping context
87h numero insufficiente di pagine logiche totali
88h numero insufficiente di pagine logiche libere
89h è proibito allocare zero pagine logiche
8Ah indice fuori limite per la pagina logica specificata
8Bh indice fuori limite per la pagina fisica specificata
8Ch spazio esaurito per il salvataggio del mapping context
8Dh tentativo di salvataggio multiplo dello stesso mapping context
8Eh tentativo di ripristino di un mapping context inesistente
8Fh il registro AL specifica un sottoservizio inesistente
90h tipo di attributo non definito
91h attributo "non-volatility" non supportato dal sistema
92h trasferimento dati con sovrapposizione non valida
93h la dimensione specificata eccede quella allocata
94h aree sovrapposte di memoria espansa e memoria convenzionale
95h offset fuori limite per una pagina logica
96h allocazione di un'area più grande di 1 Mb
97h l'area sorgente e l'area destinazione hanno lo stesso handle
98h area sorgente e/o area destinazione non definite
9Ah "alternate map register set" non supportato
9Bh tutti gli "alternate map/DMA register sets" sono già allocati
9Ch "alternate map/DMA register sets" non supportati
9Dh "alternate map/DMA register set" non definito
9Eh "dedicated DMA channels" non supportati
9Fh "dedicated DMA channel" non definito
A0h il nome dell'handle specificato non corrisponde a nessun valore valido
A1h il nome dell'handle specificato esiste già
A2h il trasferimento dati ha provocato un tentativo di wrap around
A3h struttura dati corrotta
A4h accesso negato dal Sistema Operativo

Il significato dei vari termini presenti in Figura 3 sarà chiarito


nel seguito del capitolo.

8.3 Servizi EMS

Lo standard LIM-EMS 4.0 mette a disposizione una numerosa serie di


servizi destinati ai normali programmi DOS; tali servizi hanno lo
scopo di permettere ai programmi che girano in modalità reale di
accedere in I/O alla memoria espansa.
Sono anche presenti diversi potenti servizi destinati alla
195
implementazione di veri e propri SO capaci di eseguire uno o più
programmi contemporaneamente; i SO implementati con lo standard LIM-
EMS 4.0 hanno a disposizione sino a 24 pagine fisiche da 16 Kb in
memoria convenzionale e sino a 12 pagine fisiche da 16 Kb in memoria
superiore.
Nel seguito del capitolo faremo riferimento solo ai servizi standard
destinati ai normali programmi DOS; ci occuperemo quindi di semplici
operazioni di I/O nella memoria espansa attraverso un frame buffer in
upper memory costituito da 4 pagine fisiche.

In generale, un determinato EMM potrebbe anche non implementare tutti


i servizi previsti dallo standard LIM-EMS 4.0; proprio per questo
motivo, è importante che il programmatore verifichi sempre
l'eventuale codice di errore restituito in AH dalla INT 67h, con
particolare riferimento ai codici 84h (servizio inesistente) e 8Fh
(sottoservizio inesistente)!

Analizziamo ora l'elenco dei principali servizi EMS e le


caratteristiche di ciascuno di essi; per maggiori dettagli si
consiglia di scaricare il file ems40.tar.bz2 presente nella sezione
Downloads di questo sito.

8.3.1 Servizio n.40h: Get EMM Status

Questo servizio restituisce un codice per indicare se un EMM è


installato in memoria e se il relativo hardware sta funzionando
correttamente.

EMS - Servizio n. 40h - Get EMM Status

Argomenti richiesti:
AH = 40h (Get EMM Status)

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

Il valore 00h restituito in AH indica che il driver è installato in


memoria e il relativo hardware di gestione della expanded memory
funziona in modo corretto; ogni altro valore restituito in AH indica,
invece, una condizione di errore!

8.3.2 Servizio n.41h: Get Page Frame Address

Questo servizio restituisce l'indirizzo in upper memory del frame


buffer.

EMS - Servizio n. 41h - Get Page Frame Address

Argomenti richiesti:
AH = 41h (Get Page Frame Address)

196
Valori restituiti:
In caso di successo:
AH = 00h
BX = componente Seg del Frame Buffer
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

Bisogna ricordare che tutti i blocchi di memoria DOS (compresi gli


UMB) sono sempre allineati al paragrafo per cui il loro indirizzo
logico iniziale Seg:Offset ha la componente Offset che vale 0000h;
proprio per questo motivo, il servizio n.41h restituisce solo la
componente Seg dell'indirizzo logico iniziale del frame buffer.

8.3.3 Servizio n.42h: Get Unallocated Page Count

Questo servizio restituisce il numero di pagine logiche libere e il


numero totale di pagine logiche disponibili.

EMS - Servizio n. 42h - Get Unallocated Page Count

Argomenti richiesti:
AH = 42h (Get Unallocated Page Count)

Valori restituiti:
In caso di successo:
AH = 00h
BX = numero di pagine logiche libere
DX = numero totale di pagine logiche disponibili
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

In caso di successo, questo servizio restituisce in BX il numero di


pagine logiche libere, allocabili dai programmi che ne hanno bisogno;
in DX viene, invece, restituito il numero di pagine logiche
complessive (libere o già allocate).
Lo standard LIM-EMS 4.0 permette di gestire un massimo di 2048 pagine
logiche da 16 Kb ciascuna per un totale di 32 Mb di memoria espansa.

8.3.4 Servizio n.43h: Allocate Pages

Questo servizio tenta di allocare il numero di pagine logiche


richiesto dal programmatore.

EMS - Servizio n. 43h - Allocate Pages

Argomenti richiesti:
AH = 43h (Allocate Pages)
BX = numero di pagine logiche da allocare

Valori restituiti:
In caso di successo:
AH = 00h
DX = handle che identifica le pagine allocate
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h, 85h, 87h,

197
88h, 89h)

In caso di successo, questo servizio restituisce in DX un cosiddetto


handle rappresentato da un numero intero il cui scopo è quello di
identificare in modo univoco il blocco di pagine logiche allocato da
un programma; gli handle per i programmi hanno sempre un valore
compreso tra 1 e 254 in quanto il valore 0 è riservato ad un
eventuale SO.
Si tenga presente che è proibito allocare 0 pagine logiche!

Il DOS non ha nulla a che vedere con la gestione della memoria


espansa e quindi non è in grado di deallocarla automaticamente quando
un programma termina; proprio per questo motivo, i programmi che
allocano memoria espansa devono rigorosamente restituirla prima di
terminare!

8.3.5 Servizio n.44h: Map/Unmap Handle Pages

Questo servizio ha lo scopo di abilitare o disabilitare la mappatura


delle pagine logiche nel frame buffer.

EMS - Servizio n. 44h - Map/Unmap Handle Pages

Argomenti richiesti:
AH = 44h (Map/Unmap Handle Pages)
AL = indice pagina fisica
BX = indice pagina logica
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 8Ah,
8Bh)

Questo servizio permette di mappare pagine logiche (allocate da un


programma) nelle pagine fisiche del frame buffer; il programmatore
deve specificare in AL l'indice della pagina fisica in cui verrà
mappata la pagina logica il cui indice è contenuto in BX.
Come si può notare, il servizio 44h permette di effettuare un solo
accoppiamento (pagina logica - pagina fisica) per volta e ciò può
chiaramente comportare problemi di efficienza nel caso di frequenti
operazioni di mappatura; proprio per questo motivo, viene reso
disponibile un altro servizio (descritto più avanti) attraverso il
quale si possono effettuare mappature multiple.

Lo stesso servizio 44h permette anche di disabilitare la mappatura


corrente di una pagina logica in una pagina fisica; a tale proposito,
bisogna porre BX=FFFFh.
In una situazione del genere, la pagina logica che era stata
precedentemente mappata nella pagina fisica specificata in AL diventa
inaccessibile per le operazioni di I/O!

198
8.3.6 Servizio n.45h: Deallocate Pages

Questo servizio tenta di deallocare le pagine logiche precedentemente


allocate da un programma.

EMS - Servizio n. 45h - Deallocate Pages

Argomenti richiesti:
AH = 45h (Deallocate Pages)
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 86h)

Come è stato spiegato in precedenza, il DOS non ha nessun ruolo nella


gestione della memoria espansa per cui non è in grado di sapere se un
programma, prima di terminare, ha proceduto alla deallocazione di
eventuali pagine logiche che stava usando; di conseguenza, il compito
di effettuare tale deallocazione spetta rigorosamente al programma
stesso.
Se non si tiene conto di questo aspetto, tutte le pagine logiche
associate ai vari handle non restituiti al sistema risulteranno
inutilizzabili da altri programmi; lo stesso problema riguarda quindi
anche i programmi che terminano in modo anomalo (ad esempio, per un
crash) dopo aver allocato memoria espansa!

8.3.7 Servizio n.46h: Get Version

Questo servizio restituisce la versione dello standard LIM-EMS


supportata dall'EMM in uso.

EMS - Servizio n. 46h - Get Version

Argomenti richiesti:
AH = 46h (Get Version)

Valori restituiti:
In caso di successo:
AH = 00h
AL = Versione LIM-EMS in BCD
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

Il valore restituito in AL è espresso in formato BCD; il nibble alto


di AL contiene il major number, mentre il nibble basso di AL contiene
il minor number.
Ricordando quanto è stato esposto nella sezione Assembly Base, se
vogliamo scompattare i due nibble di AL nella coppia AH:AL dobbiamo
utilizzare l'istruzione AAM con divisore 16; possiamo scrivere
allora:

db 0D4h, 16
199
8.3.8 Servizio n.47h: Save Page Map

Questo servizio ha lo scopo di salvare lo stato corrente del frame


buffer.

EMS - Servizio n. 47h - Save Page Map

Argomenti richiesti:
AH = 47h (Save Page Map)
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 8Ch,
8Dh)

Supponiamo di avere un programma che, dopo aver allocato un certo


numero di pagine logiche, mappa 4 di esse nelle 4 pagine fisiche del
frame buffer; la situazione che si viene a creare nel frame buffer
(cioè, l'associazione tra pagine fisiche e pagine logiche)
rappresenta il cosiddetto mapping context corrente.
Nel caso in cui, in quel momento, arrivi una interruzione hardware,
come sappiamo, la CPU interrompe il programma in esecuzione e chiama
una apposita ISR; se questa ISR deve fare uso della memoria espansa,
con conseguente modifica del mapping context corrente, si viene a
creare una situazione piuttosto pericolosa.
Infatti, al termine della ISR, il programma precedentemente
interrotto riprende l'esecuzione e si ritrova con un mapping context
completamente alterato; come è facile immaginare, la conseguenza di
tutto ciò è un sicuro crash!

Per ovviare a questo problema, un programma che ne interrompe un


altro e fa poi uso della memoria espansa, deve rigorosamente
preservare un eventuale mapping context già esistente; a tale
proposito, può essere utilizzato il servizio 47h il quale salva tutte
le necessarie informazioni in una apposita area dati riservata.

Un aspetto importantissimo riguarda il fatto che tale servizio deve


essere utilizzato DOPO aver ottenuto un handle e PRIMA di procedere
con la mappatura nel frame buffer!

Tutto ciò è necessario in quanto il servizio 47h richiede


esplicitamente un handle; se il programmatore ha bisogno di salvare
semplicemente il mapping context corrente senza specificare nessun
handle, può utilizzare un altro servizio descritto più avanti.

8.3.9 Servizio n.48h: Restore Page Map

Questo servizio ha lo scopo di ripristinare un mapping context


salvato in precedenza con il servizio 47h.

200
EMS - Servizio n. 48h - Restore Page Map

Argomenti richiesti:
AH = 48h (Restore Page Map)
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 8Eh)

Un programma che ha precedentemente salvato un mapping context già


esistente, prima di terminare deve rigorosamente ripristinarlo; a
tale proposito, può essere utilizzato il servizio 48h.

Un aspetto importantissimo riguarda il fatto che tale servizio deve


essere utilizzato PRIMA di restituire l'handle delle pagine logiche
al gestore della memoria espansa!

Tutto ciò è necessario in quanto il servizio 48h richiede


esplicitamente un handle; se il programmatore ha bisogno di
ripristinare semplicemente un mapping context senza specificare
nessun handle, può utilizzare un altro servizio descritto più avanti.

8.3.10 Servizio n.4Bh: Get Handle Count

Questo servizio restituisce il numero totale di handle correntemente


aperti.

EMS - Servizio n. 4Bh - Get Handle Count

Argomenti richiesti:
AH = 4Bh (Get Handle Count)

Valori restituiti:
In caso di successo:
AH = 00h
BX = numero totale di handle aperti
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

Il servizio 4Bh restituisce il numero totale di handle correntemente


aperti dai vari programmi che stanno utilizzando la memoria espansa;
il valore restituito da questo servizio comprende anche l'handle
numero 0000h riservato al SO.

8.3.11 Servizio n.4Ch: Get Handle Pages

Questo servizio restituisce il numero di pagine logiche associate ad


un handle.

EMS - Servizio n. 4Ch - Get Handle Pages

Argomenti richiesti:
201
AH = 4Ch (Get Handle Pages)
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
BX = numero pagine associate all'handle
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h)

Il servizio 4Ch restituisce il numero di pagine logiche associate


all'handle specificato nel registro DX; il valore massimo possibile
restituito da tale servizio è 2048 in quanto, come sappiamo, lo
standard LIM-EMS 4.0 può gestire sino a 32 Mb di memoria espansa
suddivisa in pagine da 16 Kb ciascuna.

8.3.12 Servizio n.4Dh: Get All Handle Pages

Questo servizio restituisce l'elenco completo degli handle aperti e


il numero delle pagine logiche associate a ciascuno di essi.

EMS - Servizio n. 4Dh - Get All Handle Pages

Argomenti richiesti:
AH = 4Dh (Get All Handle Pages)
ES:DI = puntatore al vettore delle coppie [handle,
pagine]

Valori restituiti:
In caso di successo:
AH = 00h
BX = numero totale di handle aperti
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h)

Il servizio 4Dh riempie un vettore, puntato da ES:DI, con l'elenco


completo delle coppie [handle, pagine]; ogni coppia contiene quindi
uno degli handle aperti e il numero delle pagine logiche ad esso
associate.
Nel registro BX viene restituito il numero totale di coppie contenute
nel vettore e cioè, il numero totale di handle aperti (compreso il
n.0000h riservato al SO).

Lo spazio per il vettore puntato da ES:DI deve essere allocato dal


programma che richiede il servizio 4Dh; a tale proposito, si tenga
presente che lo standard EMS-LIM 4.0 prevede un numero massimo di
handle pari a 255 (compreso il n.0000h riservato al SO).
Ogni elemento del vettore è una struttura il cui aspetto è illustrato
in Figura 4.

Figura 4 - Handle Page Structure


STRUC handle_page_struct

emm_handle resw 1 ; codice handle aperto

202
logical_pages resw 1 ; pagine associate all'handle

ENDSTRUC

In definitiva, il programmatore predispone un vettore capace di


contenere un massimo di 255 strutture handle_page_struct; tale
vettore, puntato da ES:DI, viene riempito dal servizio 4Dh con tutte
le coppie [handle, pagine] che, in quel momento, sono in uso ai vari
programmi che accedono alla memoria espansa.

8.3.13 Servizio n.4E00h: Get Page Map

Questo servizio ha lo scopo di salvare tutti i mapping context attivi


senza la necessità di specificare un handle.

EMS - Servizio n. 4E00h - Get Page Map

Argomenti richiesti:
AX = 4E00h (Get Page Map)
ES:DI = puntatore al vettore delle informazioni da
salvare

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h, 8Fh)

Il servizio 4Eh, sottoservizio 00h, salva tutti i mapping context


correnti senza che il programmatore debba specificare un handle;
ovviamente, nel caso di un semplice programma DOS che accede in I/O
alla memoria espansa, è presente un solo eventuale mapping context
relativo all'unico frame buffer allocato nella upper memory.
Questo servizio deve essere utilizzato al posto del n.47h quando non
vogliamo (o non possiamo) fare riferimento ad uno specifico handle.

Tutte le necessarie informazioni, la cui struttura viene stabilita


dall'EMM, vengono salvate in un vettore puntato da ES:DI e
predisposto dal programmatore; le dimensioni di tale vettore possono
essere stabilite attraverso il servizio 4E03h descritto più avanti.

8.3.14 Servizio n.4E01h: Set Page Map

Questo servizio ha lo scopo di ripristinare tutti i mapping context


salvati in precedenza attraverso il servizio 4E00h.

EMS - Servizio n. 4E01h - Set Page Map

Argomenti richiesti:
AX = 4E01h (Set Page Map)
DS:SI = puntatore al vettore delle informazioni da
ripristinare

Valori restituiti:

203
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h, 8Fh, A3h)

Il servizio 4Eh, sottoservizio 01h, ripristina tutti i mapping


context salvati in precedenza con il servizio 4Eh, sottoservizio 00h;
tale servizio deve essere quindi utilizzato al posto del n.48h quando
non vogliamo (o non possiamo) fare riferimento ad uno specifico
handle.

Tutte le necessarie informazioni, la cui struttura viene stabilita


dall'EMM, vengono lette da un vettore puntato da DS:SI e predisposto
dal programmatore; le dimensioni di tale vettore possono essere
stabilite attraverso il servizio 4E03h descritto più avanti.

8.3.15 Servizio n.4E02h: Get & Set Page Map

Questo servizio ha lo scopo di salvare tutti i mapping context attivi


e di ripristinare tutti i mapping context salvati in precedenza senza
la necessità di specificare un handle.

EMS - Servizio n. 4E02h - Get & Set Page Map

Argomenti richiesti:
AX = 4E02h (Get & Set Page Map)
DS:SI = puntatore al vettore delle informazioni da
ripristinare
ES:DI = puntatore al vettore delle informazioni da
salvare

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h, 8Fh, A3h)

Il servizio 4Eh, sottoservizio 02h, salva tutti i mapping context


correnti e ripristina tutti quelli precedentemente salvati senza che
il programmatore debba specificare un handle; tale servizio deve
essere quindi utilizzato al posto dei n.47h e 48h quando non vogliamo
(o non possiamo) fare riferimento ad uno specifico handle.

Prima di tutto, il servizio 4E02h salva tutti i mapping context


correnti in un vettore puntato da ES:DI e successivamente ripristina
tutti i mapping context salvati in precedenza leggendo le
informazioni da un vettore puntato da DS:SI; entrambi i vettori
devono essere predisposti dal programmatore.

8.3.16 Servizio n.4E03h: Get Size of Page Map Save Array

Questo servizio restituisce le dimensioni dei vettori richiesti dai


precedenti tre sottoservizi.

204
EMS - Servizio n. 4E03h - Get Size of Page Map Save
Array

Argomenti richiesti:
AX = 4E03h (Get Size of Page Map Save Array)

Valori restituiti:
In caso di successo:
AH = 00h
AL = dimensione in byte del vettore
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 84h, 8Fh)

Prima di predisporre i vettori richiesti dai precedenti tre servizi,


il programmatore può determinarne le dimensioni attraverso il
servizio 4E03h; in questo modo è possibile allocare la quantità
opportuna di memoria necessaria per i vettori stessi.

8.3.17 Servizio n.5000h: Map/Unmap Multiple Handle Pages -


Logical/Physical Method

Questo servizio permette di attivare o disattivare la mappatura di


pagine multiple nel frame buffer.

EMS - Servizio n. 5000h - Map/Unmap Multiple Handle


Pages

Argomenti richiesti:
AX = 5000h (Map/Unmap Multiple Handle Pages)
DS:SI = puntatore al vettore delle coppie [logical,
physical]
CX = numero di coppie nel vettore
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 8Ah,
8Bh, 8Fh)

Il servizio 50h, sottoservizio 00h, permette di mappare


simultaneamente una serie di pagine logiche in una serie di pagine
fisiche; si tratta quindi di un servizio da utilizzare, al posto del
n.44h, quando un programma deve gestire una grande quantità di
operazioni di mappatura.
Nei casi che stiamo esaminando (programmi DOS che accedono alla
memoria espansa per semplici operazioni di I/O), abbiamo a
disposizione 4 pagine fisiche rappresentate dagli indici 0, 1, 2, 3;
possiamo mappare quindi simultaneamente sino a 4 pagine logiche nelle
4 pagine fisiche disponibili.

Il metodo logical/physical prevede che, sia le pagine logiche, sia le


pagine fisiche, vengano identificate mediante i rispettivi indici

205
numerici; tali indici devono essere inseriti in un vettore i cui
elementi sono rappresentati dalla struttura illustrata in Figura 5.

Figura 5 - Logical to Physical Structure


STRUC log2phys_struct

logical_index resw 1 ; indice pagina logica


physical_index resw 1 ; indice pagina fisica

ENDSTRUC

Il vettore, predisposto dal programmatore, deve essere puntato dalla


coppia DS:SI; il numero di strutture presenti nel vettore deve essere
indicato nel registro CX.

Questo stesso servizio può essere utilizzato per impedire l'accesso


in I/O ad una serie di pagine logiche associate ad una serie di
pagine fisiche; a tale proposito, nel membro logical_index relativo
al membro physical_index che ci interessa, dobbiamo inserire il
valore FFFFh.
Il risultato che si ottiene è che tutte le pagine fisiche associate a
pagine logiche identificate dal valore FFFFh inibiscono qualunque
tentativo di accesso in I/O; si tratta quindi di una funzionalità
molto utile quando, ad esempio, un programma lancia un secondo
programma al quale vuole impedire l'accesso a determinate pagine
logiche.

8.3.18 Servizio n.5001h: Map/Unmap Multiple Handle Pages -


Logical/Segment Method

Questo servizio permette di attivare o disattivare la mappatura di


pagine multiple nel frame buffer.

EMS - Servizio n. 5001h - Map/Unmap Multiple Handle


Pages

Argomenti richiesti:
AX = 5001h (Map/Unmap Multiple Handle Pages)
DS:SI = puntatore al vettore delle coppie [logical,
segment]
CX = numero di coppie nel vettore
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 8Ah,
8Bh, 8Fh)

Il servizio 50h, sottoservizio 01h, è del tutto simile al caso


trattato in precedenza; l'unica differenza è che le pagine fisiche
vengono individuate dal loro indirizzo Seg:Offset anziché da un
indice.
Per sapere a quali indirizzi Seg:Offset si trovano le 4 pagine
206
fisiche disponibili dobbiamo ricordare innanzi tutto che qualunque
blocco di memoria DOS (e quindi anche il frame buffer) è sempre
allineato al paragrafo; supponiamo allora che il servizio 41h (Get
Page Frame Address) ci abbia restituito il valore D000h.
Possiamo dire quindi che il nostro frame buffer si trova in upper
memory a partire dall'indirizzo logico D000h:0000h; osservando ora
che ogni pagina fisica occupa 16 Kb (cioè, 4000h byte), possiamo
affermare che:

Indirizzo logico Pagina Fisica 0 = D000h:0000h

Indirizzo logico Pagina Fisica 1 = D000h:4000h

Indirizzo logico Pagina Fisica 2 = D000h:8000h

Indirizzo logico Pagina Fisica 3 = D000h:C000h

Normalizzando questi 4 indirizzi logici otteniamo:

Indirizzo logico normalizzato Pagina Fisica 0 = D000h:0000h

Indirizzo logico normalizzato Pagina Fisica 1 = D400h:0000h

Indirizzo logico normalizzato Pagina Fisica 2 = D800h:0000h

Indirizzo logico normalizzato Pagina Fisica 3 = DC00h:0000h

Le 4 componenti Seg appena ottenute sono proprio quelle che dobbiamo


utilizzare per identificare le corrispondenti 4 pagine fisiche;
ovviamente, le relative componenti Offset valgono, implicitamente,
0000h.

Tutte le informazioni richieste dal metodo logical/segment devono


essere inserite in un vettore i cui elementi sono rappresentati dalla
struttura illustrata in Figura 6.

Figura 6 - Logical to Segment Structure


STRUC log2seg_struct

logical_index resw 1 ; indice pagina logica


physical_seg resw 1 ; Seg pagina fisica

ENDSTRUC

Il vettore, predisposto dal programmatore, deve essere puntato dalla


coppia DS:SI; il numero di strutture presenti nel vettore deve essere
indicato nel registro CX.

Questo stesso servizio può essere utilizzato per impedire l'accesso


in I/O ad una serie di pagine logiche associate ad una serie di
pagine fisiche; a tale proposito, valgono le identiche considerazioni
svolte per il servizio 5000h.

8.3.19 Servizio n.51h: Reallocate Pages

207
Questo servizio permette di modificare il numero di pagine associate
ad un handle.

EMS - Servizio n. 51h - Reallocate Pages

Argomenti richiesti:
AH = 51h (Reallocate Pages)
BX = nuovo numero di pagine logiche
DX = handle delle pagine logiche

Valori restituiti:
In caso di successo:
AH = 00h
BX = numero di pagine effettivamente allocate
In caso di insuccesso:
AH = Codice di errore (80h, 81h, 83h, 84h, 87h,
88h)

Con questo servizio è possibile modificare il numero di pagine


associate ad un handle già assegnato ad un programma; si possono
presentare i seguenti 4 casi:

1) BX = 0
Se BX specifica il valore 0, l'handle rimane assegnato al programma
ma tutte le pagine logiche associate vengono restituite al sistema;
questa tecnica può essere utilizzata per impedire che un determinato
handle, pur non essendo associato a nessuna pagina logica, venga
utilizzato da altri programmi.

2) BX specifica lo stesso numero di pagine già in uso all'handle


In questo caso la situazione rimane immutata; all'handle restano
associate le stesse pagine in uso prima della chiamata del servizio
51h.

3) BX specifica un numero di pagine maggiore di quello in uso


all'handle
L'EMM tenta di aggiungere ulteriori pagine logiche all'handle, sino a
raggiungere il valore specificato in BX; gli indici delle nuove
pagine logiche sono consecutivi a quelli già in uso all'handle.

4) BX specifica un numero di pagine minore di quello in uso


all'handle
L'EMM tenta di sottrarre pagine logiche all'handle, sino a
raggiungere il valore specificato in BX; gli indici delle pagine
sottratte sono quelli posti alla fine della sequenza precedentemente
in uso all'handle.

Se la richiesta del servizio 51h fallisce, il registro BX restituisce


lo stesso numero di pagine logiche precedentemente in uso all'handle;
si presti però attenzione al fatto che il caso 2) non rappresenta una
condizione di errore e produce quindi sempre la restituzione di AH=0.

8.4 Libreria EMSLIB


208
In analogia al caso della memoria estesa, trattato nel precedente
capitolo, anche per la gestione dello standard LIM-EMS 4.0 conviene
scriversi una apposita libreria linkabile a tutti i programmi che ne
hanno bisogno; nella sezione Downloads è presente una libreria,
denominata EMSLIB, che può essere linkata ai programmi destinati alla
generazione di eseguibili in formato EXE.

All'interno del pacchetto emslibexe.tar.bz2 è presente la


documentazione, la libreria vera e propria EMSLIB.ASM, l'include file
EMSLIB.INC per il NASM e l'header file EMSLIB.H per eventuali
programmi scritti in linguaggio C (purché destinati sempre alla
modalità reale).

Per la creazione dell'object file di EMSLIB.ASM è richiesta la


presenza della libreria di macro LIBPROC.MAC (Capitolo 29 della
sezione Assembly Base con NASM); l'assemblaggio consiste nel semplice
comando:

nasm -f obj emslib.asm

La possibilità di linkare la libreria ai programmi scritti in C è


legata al fatto che le varie procedure definite in EMSLIB seguono le
convenzioni C per il passaggio degli argomenti e per il valore di
ritorno; da notare, inoltre, la presenza degli underscore all'inizio
di ogni identificatore globale definito nella libreria stessa.

Diverse procedure della libreria richiedono argomenti passati per


indirizzo; tale indirizzo deve essere sempre di tipo FAR!
Ricordando le convenzioni C per il passaggio degli argomenti (da
destra verso sinistra), dobbiamo stare attenti quindi a mettere nella
lista di chiamata, prima la componente Offset e poi la componente Seg
della variabile da passare per indirizzo; in questo modo, la
componente Offset si troverà a precedere la componente Seg in
memoria, nel rispetto della convenzione Intel (con il Pascal avremmo
dovuto disporre le due componenti in ordine inverso).
All'interno delle procedure, le variabili passate per indirizzo
vengono gestite con le coppie DS:SI e ES:DI inizializzate con le
istruzioni LDS e LES; come sappiamo, tali istruzioni lavorano in modo
corretto solo quando la coppia Seg:Offset, da caricare in DS:SI o
ES:DI, si trova disposta in memoria (in questo caso, nello stack)
secondo la convenzione Intel!

8.5 Esempi pratici

Prima di procedere con alcuni esempi pratici, è necessario


predisporre l'ambiente operativo in modo che la memoria espansa venga
attivate e resa utilizzabile; a tale proposito, si deve procedere
diversamente a seconda del SO che si sta usando.

Nel caso del DOS vero e proprio o della DOS Box di Windows9x, è
necessario editare il file C:\CONFIG.SYS; al suo interno dovrebbe
essere presente una linea del tipo:

DEVICE=C:\DOS\EMM386.EXE oppure, nel caso di Windows,


209
DEVICE=C:\WINDOWS\EMM386.EXE

Se questa linea non è presente deve essere aggiunta dall'utente


(bisogna anche ricordarsi di togliere l'eventuale parametro NOEMS);
in tal caso, è anche necessario riavviare il computer.
Come sappiamo, EMM386.EXE è il driver più diffuso per il supporto
dell'EMS; bisogna ricordare comunque che esistono anche altri EMM
altrettanto validi come, ad esempio, 386MAX che è capace di svolgere
contemporaneamente il ruolo di XMM e di EMM.
Le impostazioni di memoria relative ad un programma DOS da eseguire
sotto qualunque versione di Windows a 32 bit (compresi quindi Windows
XP e versioni superiori), vengono effettuate attraverso il menu
Proprietà del programma stesso; a tale proposito, basta cliccare con
il tasto destro del mouse sull'icona del programma eseguibile e
selezionare Proprietà - Memoria.
Si consiglia di selezionare 600 Kb di memoria convenzionale, almeno 5
Mb di memoria estesa e almeno 2 Mb di memoria espansa.

Per quanto riguarda WindowsXP e versioni superiori, tutte le


impostazioni per la configurazione dell'ambiente operativo della DOS
Box si svolgono attraverso i due file AUTOEXEC.NT e CONFIG.NT che si
trovano, generalmente, nella directory C:\WINDOWS\SYSTEM32; tali file
contengono anche una serie di informazioni che spiegano come
effettuare le varie impostazioni.
Una volta che l'utente ha apportato eventuali modifiche ai due file
AUTOEXEC.NT e CONFIG.NT, deve semplicemente riavviare la DOS Box; non
è necessario quindi il riavvio del computer.

Anche sotto Linux i vari emulatori DOS possono essere configurati


attraverso appositi file senza la necessità di riavviare il computer.
Nel caso di DOSEmu, ad esempio, i file di avvio della finestra DOS
sono i soliti AUTOEXEC.BAT e CONFIG.SYS (che si trovano nella
directory scelta come radice C:\ dell'emulatore); per quanto riguarda
le impostazioni della memoria, basta editare il file $HOME/.dosemurc,
apportare le modifiche desiderate nella sezione Memory settings e
riavviare l'emulatore.

8.5.1 Trasferimento di stringhe in memoria espansa

Analizziamo ora un esempio molto simile a quelli presentati nel


precedente capitolo; la Figura 7 mostra un programma che memorizza 16
stringhe in 16 pagine logiche differenti e poi le visualizza sullo
schermo.

Figura 7 - File EMSTEST.ASM


;-----------------------------------------------------;
; file emstest.asm ;
; accesso ai servizi EMS (libreria EMSLIB) ;
;-----------------------------------------------------;
; nasm -f obj emstest.asm ;
; tlink emstest.obj + emslib.obj + exelib.obj ;
; (oppure link emstest.obj + emslib.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############


210
CPU 386 ; set di istruzioni a 32 bit
%include "exelib.inc" ; inclusione libreria di I/O
%include "emslib.inc" ; inclusione libreria EMS 4.0
%include "libproc.mac" ; inclusione macro per le procedure

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack


%assign MAX_PAGES 16 ; max. numero pagine logiche

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

EMS_version dw 0 ; versione EMS


total_pages dw 0 ; pagine logiche totali
free_pages dw 0 ; pagine logiche libere
EMM_fbseg dw 0 ; Seg frame buffer
EMM_handle dw 0 ; handle pagine logiche
char_data dd 0 ; dati da trasferire
page_index dw 0 ; contatore pagine logiche
EMM_string db "EMMXXXX0" ; stringa di identificazione EMM

str_title db "ACCESSO ALLA MEMORIA ESPANSA VIA EMS (LIBRERIA EMSLIB)", 0


str_keypress db "Premere un tasto per continuare ... ", 0
str_emmtestok db "Il driver EMM e' installato e operativo!", 0
str_emmtestko1 db "Errore! Il driver EMM non e' installato!", 0
str_emmtestko2 db "Errore! Il driver EMM non e' operativo!", 0
str_emsversion db "Standard EMS versione XXh.XXh", 0
str_emstotal db "Memoria espansa totale XXXXX Pagine Logiche", 0
str_emsfree db "Memoria espansa libera XXXXX Pagine Logiche", 0
str_fbseg db "Frame Buffer all'indirizzo XXXXH:0000H", 0
str_allocinfo db "Allocazione di 16 Pagine Logiche - Handle 0000H", 0
str_allocerror db "Errore! Allocazione fallita!", 0
str_transfer db "Trasferimento dati in memoria espansa", 0
str_freeinfo db "Deallocazione di 16 Pagine Logiche", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


211
mov dx, 000Eh ; riga 0, colonna 14
call far writeString ; mostra la stringa

; verifica la presenza e lo stato operativo di un EMM

push es ; preserva ES
xor ax, ax ; AX = 0
mov es, ax ; ES = paragrafo 0000h
mov es, [es:(67h * 4) + 2] ; ES = Seg ISR
mov di, 000Ah ; DI = 10
mov si, EMM_string ; DS:SI punta a EMM_string
mov cx, 8 ; CX = lunghezza stringa
cld ; clear direction flag
repz cmpsb ; comparazione stringhe
pop es ; ripristina ES
jz EMM_presente ; CX = 0, ZF = 1, comparazione OK
mov di, str_emmtestko1 ; stringa EMM assente
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; Errore! Fine programma!

EMM_presente: ; continua l'esecuzione


callproc LANGC, FARPROC, _get_emmstatus
test ax, ax ; AX == 0 ?
jz EMM_operativo ; si, EMM operativo
mov di, str_emmtestko2 ; no, EMM non operativo
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; Errore! Fine programma!

EMM_operativo:
mov di, str_emmtestok ; EMM presente e operativo
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

; determina la versione EMS

callproc LANGC, FARPROC, _get_emsversion, EMS_version, seg EMS_version


mov di, str_emsversion ; stringa da mostrare
mov dx, 0300h ; riga 3, colonna 0
call far writeString ; mostra la stringa
mov al, [EMS_version+1] ; AL = major number versione EMS
mov dx, 0316h ; riga 3, colonna 22
call far writeHex8 ; mostra il major number
mov al, [EMS_version+0] ; AL = minor number versione EMS
mov dx, 031Ah ; riga 3, colonna 26
call far writeHex8 ; mostra il minor number

; determina il numero di pagine logiche totali e libere

callproc LANGC, FARPROC, _get_pagecount, free_pages, seg free_pages, \


total_pages, seg total_pages
mov di, str_emstotal ; stringa da mostrare
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
mov ax, [total_pages] ; AX = numero di pagine totali
mov dx, 0417h ; riga 4, colonna 23
call far writeUdec16 ; mostra le pagine totali
mov di, str_emsfree ; stringa da mostrare
mov dx, 0500h ; riga 5, colonna 0
call far writeString ; mostra la stringa
mov ax, [free_pages] ; AX = numero di pagine libere
212
mov dx, 0517h ; riga 5, colonna 23
call far writeUdec16 ; mostra le pagine libere

cmp word [free_pages], MAX_PAGES ; ci sono almeno 16 pagine libere ?


ja continua1 ; si
jmp exit_on_error ; no
continua1:

; determina l'indirizzo del frame buffer

callproc LANGC, FARPROC, _get_framebuffer, EMM_fbseg, seg EMM_fbseg


mov di, str_fbseg ; stringa da mostrare
mov dx, 0600h ; riga 6, colonna 0
call far writeString ; mostra la stringa
mov ax, [EMM_fbseg] ; AX = Seg frame buffer
mov dx, 061Bh ; riga 6, colonna 27
call far writeHex16 ; mostra Seg frame buffer

; tenta di allocare 16 pagine logiche

callproc LANGC, FARPROC, _allocate_pages, word MAX_PAGES, EMM_handle, seg


EMM_handle
test ax, ax ; AX == 0 ?
jz continua2 ; si
mov di, str_allocerror ; no, allocazione fallita
mov dx, 0800h ; riga 8, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; Errore! Fine programma!
continua2:
mov di, str_allocinfo ; stringa da mostrare
mov dx, 0800h ; riga 8, colonna 0
call far writeString ; mostra la stringa
mov ax, [EMM_handle] ; AX = Handle pagine logiche
mov dx, 082Ah ; riga 8, colonna 42
call far writeHex16 ; mostra Handle pagine logiche

; determina il numero di pagine logiche totali e libere

callproc LANGC, FARPROC, _get_pagecount, free_pages, seg free_pages, \


total_pages, seg total_pages
mov di, str_emsfree ; stringa da mostrare
mov dx, 0A00h ; riga 10, colonna 0
call far writeString ; mostra la stringa
mov ax, [free_pages] ; AX = numero di pagine libere
mov dx, 0A17h ; riga 10, colonna 23
call far writeUdec16 ; mostra le pagine libere

mov di, str_transfer ; stringa da mostrare


mov dx, 0C00h ; riga 12, colonna 0
call far writeString ; mostra la stringa

mov di, str_keypress ; stringa da mostrare


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; trasferimento stringhe in memoria espansa

mov dword [char_data], "AAAA" ; dati da trasferire


mov word [page_index], 0 ; pagina logica iniziale

read_loop:
213
; mappatura pagina logica page_index in pagina fisica 0

callproc LANGC, FARPROC, _mapunmap_pages, word 0, word [page_index], word


[EMM_handle]

; riempie la pagina logica page_index con 80 x 24 = 1920 lettere 'char_data' +


un '$' finale

push es ; preserva ES
mov es, [EMM_fbseg] ; ES = Seg frame buffer
xor di, di ; DI = Offset frame buffer (0000h)
mov eax, [char_data] ; EAX = 'X', 'X', 'X', 'X'
mov cx, 480 ; 480 dword da trasferire
cld ; clear direction flag
rep stosd ; copia 480 dword in memoria espansa
mov byte [es:di], "$" ; aggiunge un '$' finale
pop es ; ripristina ES

add dword [char_data], 01010101h ; prossima lettera


inc word [page_index] ; incremento page_index
cmp word [page_index], MAX_PAGES ; page_index == 16 ?
jb read_loop ; controllo loop

; visualizzazione stringhe

mov word [page_index], 0 ; pagina logica iniziale

write_loop:

; mappatura pagina logica page_index in pagina fisica 0

callproc LANGC, FARPROC, _mapunmap_pages, word 0, word [page_index], word


[EMM_handle]

; visualizza la prossima stringa

push ds ; preserva DS
mov ds, [EMM_fbseg] ; DS = Seg frame buffer
xor dx, dx ; DX = Offset frame buffer (0000h)
mov ah, 09h ; servizio DOS Display String
int 21h ; visualizza la stringa
pop ds ; ripristina DS

mov di, str_keypress ; stringa da mostrare


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

inc word [page_index] ; incremento page_index


cmp word [page_index], MAX_PAGES ; page_index == 16 ?
jb write_loop ; controllo loop

call far clearScreen ; pulisce lo schermo

; tenta di liberare 16 pagine logiche

callproc LANGC, FARPROC, _deallocate_pages, word [EMM_handle]


mov di, str_freeinfo ; stringa da mostrare
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

214
; determina il numero di pagine logiche totali e libere

callproc LANGC, FARPROC, _get_pagecount, free_pages, seg free_pages, \


total_pages, seg total_pages
mov di, str_emsfree ; stringa da mostrare
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
mov ax, [free_pages] ; AX = numero di pagine libere
mov dx, 0417h ; riga 4, colonna 23
call far writeUdec16 ; mostra le pagine libere

exit_on_error:

mov di, str_keypress ; stringa da mostrare


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa

call far waitChar ; attende la pressione di un tasto


call far clearScreen ; pulisce lo schermo
call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################

Innanzi tutto il programma effettua una serie di test diagnostici


mostrando dei messaggi relativi alla presenza e all'operatività di un
EMM, al numero di versione del driver, al numero di pagine logiche
totali e libere, etc; è molto importante che i programmi che accedono
alla memoria espansa facciano tutte le verifiche necessarie per
evitare sorprese.
Successivamente, il programma tenta di allocare 16 pagine logiche; se
l'allocazione riesce, viene effettuato un trasferimento dati che
consiste nel sistemare in ciascuna delle 16 pagine logiche una
stringa formata da 80x24=1920 elementi di tipo BYTE più un '$' finale
(necessario per il servizio Display String della INT 21h).
Ogni stringa è formata da una sequenza di lettere maiuscole tutte
uguali; la prima stringa contiene una sequenza di lettere 'A' (codice
ASCII 41h), la seconda stringa contiene una sequenza di lettere 'B'
(codice ASCII 42h) e così via.

Il trasferimento delle 16 stringhe nelle 16 pagine logiche avviene,


per semplicità, tramite la sola pagina fisica 0 del frame buffer; a
tale proposito, viene eseguito un loop durante il quale le stringhe
stesse vengono create in modo automatico.
215
Si osservi, infatti, che se EAX="AAAA", allora:

EAX + 01010101h = "AAAA" + 01010101h = 41414141h + 01010101h = 42424242h =


"BBBB"

In seguito, viene attivato un secondo loop il cui scopo è quello di


visualizzare le 16 stringhe in 16 schermate successive; a tale
proposito, ciascuna stringa viene mappata nella pagine fisica 0 del
frame buffer e poi visualizzata attraverso il servizio Display String
della INT 21h.

Da notare ancora una volta l'importanza della parametrizzazione dei


programmi; se si ha bisogno di allocare un numero di pagine logiche
diverso da 16, nel listato di Figura 7 basta semplicemente modificare
il valore associato alla costante simbolica MAX_PAGES.

Bibliografia

Expanded Memory Specifications (EMS), ver. 4.0


(disponibile nella sezione Downloads - Documentazione -
ems40.tar.bz2)

216
Capitolo 9 La memoria video in modalità
testo standard
In seguito ad una serie di convenzioni stabilite a suo tempo dalla
IBM, tutti i PC della famiglia 80x86 sono tenuti a fornire il
supporto per le cosiddette modalità video standard; a tale proposito,
è presente apposito hardware che, nel suo insieme, costituisce il
video adapter (adattatore video) del PC.

Nei primi modelli di PC tutto l'hardware relativo al video adapter


risulta integrato nella scheda madre e comprende, tra l'altro, una
apposita memoria dedicata; per distinguere tale memoria dalla RAM
centrale si utilizza la definizione di Video RAM o VRAM.
Nei moderni PC, tutto l'hardware relativo al supporto video risulta
ormai disposto in apposite schede periferiche; in particolare, molte
di tali schede sono dotate di enormi quantità di VRAM e di una o più
GPU (Graphics Processing Unit) il cui scopo è quello di gestire
autonomamente le elaborazioni grafiche le quali, altrimenti,
finirebbero per gravare pesantemente sulla CPU.

Trattandosi di una memoria esterna, posta al di fuori dello spazio di


indirizzamento della CPU, la VRAM viene resa accessibile attraverso
un apposito frame buffer, esattamente come accade per la expanded
memory o per il BIOS; in modalità reale, l'indirizzo del frame buffer
e la relativa dimensione in byte variano a seconda della modalità
video e assumono i valori convenzionali illustrati in Figura 1
(vedere anche la Figura 1 del Capitolo 6).

Figura 1 - Indirizzi frame buffer VRAM


Indirizzo fisico Indirizzo logico Dimensione
Modalità video
frame buffer frame buffer frame buffer (byte)
Testo in bianco e nero B0000h B000h:0000h 32768
Testo a colori B8000h B800h:0000h 32768
Grafica A0000h A000h:0000h 65536

Come si può vedere, la dimensione di ogni frame buffer non supera i


65536 byte (64 Kb); ancora una volta quindi si nota che, in modalità
reale, la segmentazione a 64 Kb della RAM si ripercuote anche sulle
memorie esterne (come la VRAM).

Per attivare le varie modalità video standard, il metodo preferito


consiste nel servirsi della INT 10h del BIOS; tale vettore di
interruzione fornisce anche una numerosa serie di servizi che saranno
analizzati in questo e nei successivi capitoli (per ulteriori
dettagli si consiglia di consultare i manuali disponibili sui siti
dei vari produttori di BIOS).
La modalità video standard desiderata viene selezionata ponendo AH=0
(Set Video Mode) e caricando in AL un apposito codice a 8 bit; il
relativo codice Assembly assume quindi un aspetto del tipo:

mov ah, 00h ; AH = servizio Set Video Mode

217
mov al, codice_modo_video ; AL = modo video richiesto
int 10h ; chiama i servizi video del BIOS

Nel corso degli anni la IBM ha sviluppato una numerosa serie di


adattatori video dotati di caratteristiche sempre più evolute; può
essere interessante quindi ripercorrere brevemente l'evoluzione di
questo particolare dispositivo hardware.

9.1 Breve storia degli adattatori video standard

Nell'estate del 1981 la IBM mette in commercio il primo PC di classe


XT, bastato su CPU 8086 e 8088; il relativo adattatore video prende
il nome di MDA o Monochrome Dispaly Adapter (adattatore video
monocromatico).
Attraverso tale adattatore, lo schermo viene suddiviso in una matrice
di 720x350 punti, denominati pixel; il prodotto 720x350 (pixel)
indica la cosiddetta risoluzione dello schermo e, per convenzione,
prevede che sia sempre specificato nel primo fattore il numero di
colonne (larghezza dello schermo in pixel) e nel secondo fattore il
numero di righe (altezza dello schermo in pixel).
A sua volta, la matrice 720x350 risulta suddivisa in 80x25 celle
(cioè, 80 celle in larghezza per 25 celle in altezza) ciascuna delle
quali sarà quindi formata da 720/80=9 pixel in orizzontale e
350/25=14 pixel in verticale; in ciascuna cella l'utente può
visualizzare un qualunque simbolo appartenente al set di codici
ASCII.
Una simile modalità video viene denominata alfanumerica (o modalità
testo) proprio per indicare il fatto che l'utente può visualizzare
sullo schermo solamente lettere dell'alfabeto, segni di
punteggiatura, cifre numeriche e altri simboli del codice ASCII.
Il MDA non fornisce nessuna modalità video di tipo grafico e,
inoltre, permette la visualizzazione del testo attraverso un unico
colore su sfondo nero (per un totale di 2 colori); i PC dell'epoca,
di conseguenza, vengono abbinati ai cosiddetti monitor monocromatici
suddivisi in modelli a fosfori bianchi (bianco su nero), a fosfori
verdi (verde su nero) e a fosfori ambra (ambra su nero).

La Figura 2 illustra le caratteristiche generali del supporto video


offerto dal MDA (A/N = alfanumerico = modo testo); in particolare, la
figura mostra il codice del BIOS da caricare in AL, l'indirizzo
fisico del frame buffer, la memoria video totale fornita
dall'adattatore e il numero totale di colori disponibili.

Figura 2 - Caratteristiche standard dell'adattatore MDA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
07h B0000h 720x350 80x25 1 A/N 4 2

Poco tempo dopo l'uscita dell'adattatore MDA, una azienda di nome


Hercules mette in commercio un nuovo adattatore video denominato HGC
(Hercules Graphics Card); tale adattatore è del tutto analogo al MDA
con la differenza però che, oltre alla modalità testo monocromatica
da 80x25 celle, è anche presente una modalità grafica monocromatica
218
con risoluzione di 720x350 pixel.
La IBM non sta a guardare e rende subito disponibile un adattatore
denominato CGA o Color Graphics Adapter (adattatore grafico a
colori); tale adattatore fornisce differenti modalità testo e grafica
con la possibilità di utilizzare sino a 16 colori differenti.
Nelle modalità grafiche, ogni pixel risulta indirizzabile dai
programmi e, proprio per questo motivo, tali modalità vengono
indicate con l'acronimo APA che sta per All Points Addressable (tutti
i pixel sono indirizzabili); la Figura 3 illustra le caratteristiche
generali del supporto video offerto dal CGA.

Figura 3 - Caratteristiche standard dell'adattatore CGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
00h/01h B8000h 640x200 40x25 8 A/N 64 16
02h/03h B8000h 640x200 80x25 8 A/N 64 16
04h/05h B8000h 320x200 40x25 1 APA 64 4
06h B8000h 640x200 80x25 1 APA 64 2

(Si noti che, ai tempi dell'adattatore CGA, il frame buffer per


l'accesso alla VRAM si trovava sempre all'indirizzo fisico B8000h,
sia per la modalità testo, sia per la modalità grafica).

Nel 1984, con l'avvento dei primi PC di classe AT, la IBM introduce
un nuovo adattatore video denominato EGA o Enhanced Graphics Adapter
(adattatore grafico avanzato); l'EGA apre una nuova era in quanto
viene prodotto anche sotto forma di scheda periferica e comprende,
tra le altre cose, la possibilità di espandere la VRAM.
Uno degli scopi principali dell'EGA è anche quello di garantire la
compatibilità tra vecchie e nuove modalità video standard testo e
grafiche; la Figura 4 illustra le caratteristiche generali del
supporto video offerto dall'EGA.

Figura 4 - Caratteristiche standard dell'adattatore EGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
00h/01h B8000h 320x350 40x25 8 A/N 64 16
02h/03h B8000h 640x350 80x25 8 A/N 64 16
07h B0000h 720x350 80x25 1 A/N 4 2
0Dh A0000h 320x200 40x25 8 APA 64 16
0Eh A0000h 640x200 80x25 4 APA 64 16
0Fh A0000h 640x350 80x25 1 APA 64 2
0Fh A0000h 640x350 80x25 2 APA 128 2
10h A0000h 640x350 80x25 1 APA 64 4
10h A0000h 640x350 80x25 1 APA 128 16
10h A0000h 640x350 80x25 2 APA 256 16

Dopo una sfortunata parentesi rappresentata dallo scarso successo


ottenuto dal costosissimo adattatore PGA (Professional Dispaly
219
Adapter), nel 1987 la IBM introduce nel mondo dei PC un nuovo
standard denominato PS/2; questa nuova classe di PC si caratterizza
per la presenza di differenti adattatori video tra i quali il 8514/A
Display Adapter e il MCGA o MultiColor Graphics Adapter (adattatore
grafico multicolore).
Il 8514/A Display Adapter è destinato esclusivamente all'architettura
Micro Channel dei computer di classe PS/2 e fornisce diverse modalità
grafiche con risoluzioni sino alla 1024x768 che, per l'epoca, erano
considerate altissime; il MCGA, invece, è compatibile anche con
l'architettura dei generici PC della famiglia 80x86.
In analogia all'EGA, anche il MCGA introduce nuove modalità video e
garantisce la compatibilità con gli adattatori precedenti; la Figura
5 illustra le caratteristiche generali del supporto video offerto dal
MCGA.

Figura 5 - Caratteristiche standard dell'adattatore MCGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
00h/01h B8000h 320x400 40x25 8 A/N 64 16
02h/03h B8000h 640x400 80x25 8 A/N 64 16
04h/05h B8000h 320x200 40x25 1 A/N 64 4
06h B8000h 640x200 80x25 1 A/N 64 2
11h A0000h 640x480 80x30 1 APA 256 2
13h A0000h 320x200 40x25 1 APA 256 256

Sempre nel 1987 la IBM annuncia che, oltre all'adattatore MCGA a


bassa risoluzione, per i PC di classe PS/2 è disponibile anche un
nuovo adattatore ad alta risoluzione denominato VGA o Video Graphics
Array (matrice video grafica); questo nuovo adattatore ottiene un
successo enorme ma, come vedremo nei capitoli successivi, segna anche
la fine del dominio IBM nel settore degli adattatori video.
La caratteristica rivoluzionaria della VGA è la presenza di una
interfaccia di programmazione incorporata nel Video ROM BIOS; i
programmi che utilizzano tale interfaccia possono così garantire la
totale indipendenza dall'hardware della scheda video.
Come nei casi precedenti, anche la VGA introduce nuove modalità video
e garantisce la compatibilità con gli adattatori precedenti; la
Figura 6 illustra le caratteristiche generali del supporto video
offerto dalla VGA.

Figura 6 - Caratteristiche standard dell'adattatore VGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
00h/01h B8000h 360x400 40x25 8 A/N 64 16
02h/03h B8000h 720x400 80x25 8 A/N 64 16
04h/05h B8000h 320x200 40x25 1 A/N 64 4
06h B8000h 640x200 80x25 1 A/N 64 2
07h B0000h 720x400 80x25 1 A/N 4 2
0Dh A0000h 320x200 40x25 8 APA 64 16

220
0Eh A0000h 640x200 80x25 4 APA 64 16
0Fh A0000h 640x350 80x25 1 APA 64 2
0Fh A0000h 640x350 80x25 2 APA 128 2
10h A0000h 640x350 80x25 1 APA 64 4
10h A0000h 640x350 80x25 1 APA 128 16
10h A0000h 640x350 80x25 2 APA 256 16
11h A0000h 640x480 80x30 1 APA 256 2
12h A0000h 640x480 80x30 1 APA 256 16
13h A0000h 320x200 40x25 1 APA 256 256

In questo capitolo ci occuperemo delle modalità video testo standard;


nei capitoli successivi, invece, parleremo delle modalità video
grafiche standard.

9.2 Caratteristiche generali delle modalità video testo


standard

Come sappiamo, all'accensione del PC il BIOS avvia una fase di


diagnostica e inizializzazione dell'hardware; tra le varie
inizializzazioni, una riguarda proprio la modalità video predefinita.
Nel caso più generale, tale modalità predefinita è quella indicata in
Figura 6 dal codice 03h per gli adattatori video a colori e 07h per
gli adattatori video monocromatici; si tratta quindi di una modalità
video testo (o modalità alfanumerica) che, come è stato già spiegato,
è così chiamata per indicare il fatto che sullo schermo è possibile
rappresentare solo lettere dell'alfabeto, segni di punteggiatura,
cifre numeriche e altri simboli appartenenti al set di codici ASCII.

9.2.1 Bitmap dei simboli per la modalità testo

Indipendentemente dall'aspetto esteriore, le modalità video sono in


realtà tutte di tipo grafico; lo schermo viene cioè gestito in ogni
caso sotto forma di matrice composta da m x n pixel.
La modalità testo viene simulata suddividendo la matrice di m x n
pixel in tante celle, ciascuna delle quali è a sua volta una matrice
di p x q pixel; nel caso della modalità 03h fornita dalla VGA, ad
esempio, lo schermo è una matrice di 720x400 pixel suddivisa in 80x25
celle (cioè, 80 celle in larghezza per 25 celle in altezza).
Ogni cella viene identificata da un numero di colonna e un numero di
riga; per la modalità 03h della VGA ricaviamo quindi le seguenti
dimensioni p x q in pixel di ogni cella:

p = 720 / 80 = 9 pixel (larghezza)

q = 400 / 25 = 16 pixel (altezza)

La Figura 7a illustra la situazione appena descritta.

Figura 7 - Celle 9x16 della modalità testo 03h (VGA)

221
Come è stato già spiegato, in ogni cella è possibile visualizzare uno
dei 256 simboli appartenenti al set di codici ASCII; tali simboli si
trovano memorizzati in apposite aree della PC ROM BIOS e della Video
ROM BIOS (vedere la Figura 1 del Capitolo 6).
Nei vecchi PC i 256 simboli visualizzabili erano suddivisi in due
gruppi o mappe: la mappa 1 con i simboli da 0 a 127 e la mappa 2 con
i simboli da 128 a 256; successivamente, si è passati definitivamente
ad una mappa unica contenente tutti i 256 simboli.
Ogni simbolo è memorizzato sotto forma di bitmap (mappa di bit);
all'interno della mappa, un bit di valore 0 rappresenta un pixel
spento sullo schermo, mentre un bit di valore 1 rappresenta un pixel
acceso sullo schermo.
La Figura 7b mostra, ad esempio, una cella 9x16 riempita con la
bitmap relativa alla lettera 'a' minuscola; i pixel bianchi
rappresentano bit di valore 0, mentre i pixel rossi rappresentano bit
di valore 1.
Le bitmap memorizzate nella ROM BIOS possono anche avere dimensioni
differenti da quelle delle celle presenti sullo schermo;
generalmente, i formati più usati sono quelli da 8x8, 8x14 e 8x16
pixel.
Nel caso, ad esempio, del formato 8x16, risulta quindi che ogni
bitmap associata ad un simbolo ASCII occupa 16 byte; il vettore che
contiene tutte le 256 bitmap occupa allora:

16 * 256 = 4096 byte

In modalità testo, il programmatore non deve assolutamente


preoccuparsi di questi aspetti in quanto tutto viene gestito
automaticamente via hardware; eventualmente (come vedremo in
seguito), è possibile installare un proprio vettore di bitmap che
sarà utilizzato al posto di quello predefinito.

Per i vecchi adattatori video monocromatici (con monitor a fosfori


bianchi, verdi o ambra), la modalità video predefinita è la 07h; si
tratta di una modalità testo che per le schede VGA prevede una
matrice video di 720x400 pixel suddivisa in 80x25 celle da 9x16 pixel
ciascuna.
222
9.2.2 Il BYTE degli attributi

Anche se lo schermo appare organizzato in forma matriciale, in realtà


la VRAM (come la RAM) è gestita sotto forma di vettore di BYTE; in
modalità testo, ad ogni cella come quella di Figura 7 vengono
assegnati 2 byte: il primo byte contiene il codice ASCII del simbolo
da visualizzare, mentre il secondo byte contiene il cosiddetto byte
degli attributi.
Indicando allora con S il byte relativo al simbolo da visualizzare in
una cella e con A il byte degli attributi della cella stessa, il
vettore della VRAM in modo testo risulterà strutturato in questo
modo:

S, A, S, A, S, A, S, A, S, A, ...

Nel caso degli adattatori monocromatici, il byte degli attributi


permette di abilitare o disabilitare le funzionalità descritte in
Figura 8; se un determinato bit vale 1, la relativa funzione risulta
abilitata, mentre se lo stesso bit vale 0, la relativa funzione
risulta disabilitata.

Figura 8 - Byte degli attributi


(modo video testo 07h monocromatico)
bit Funzione
1 Testo sottolineato
3 Testo ad alta intensità
7 Testo lampeggiante

Ad esempio, se il bit 1 nel byte degli attributi di una cella vale 1,


il simbolo visualizzato nella cella stessa risulta sottolineato;
viceversa, se tale bit vale 0, il simbolo visualizzato nella cella
stessa risulta privo di sottolineatura.

Esistono anche altre particolari combinazioni del byte degli


attributi per gli adattatori monocromatici; la Figura 9 illustra
tutti i dettagli riferiti ad un monitor a fosfori bianchi (bianco su
nero).

Figura 9 - Byte degli attributi


(modo video testo 07h monocromatico)
Combinazione Funzione
00000000b = 00h Spazio vuoto nero
00001000b = 08h Spazio vuoto nero
10000000b = 80h Spazio vuoto nero
10001000b = 88h Spazio vuoto nero
01110000b = 70h Nero su bianco (Colori invertiti)
01111000b = 78h Grigio su bianco
11110000b = F0h Come 70h ma lampeggiante
11111000b = F8h Come 78h ma lampeggiante
223
Per gli adattatori a colori, il byte degli attributi permette di
selezionare, principalmente, il colore di primo piano e il colore di
sfondo; la Figura 10 illustra tutti i dettagli relativi alla modalità
video 03h a 16 colori.

Figura 10 - Byte degli attributi


(modo video testo 03h a colori)
bit Nome Funzione
0 B
1 G Colore di primo piano
2 R (foreground)
3 INT
4 B
5 G Colore di sfondo
6 R (background)
7 INT

Sia per il colore di sfondo, sia per il colore di primo piano,


risultano disponibili 4 bit; uno dei bit permette di definire
l'intensità (INT) di colore (0 = bassa, 1 = alta), mentre gli altri
tre definiscono una cosiddetta terna RGB (da Red, Green, Blue =
Rosso, Verde, Blu).
Come si sa dalla fisica ottica, i tre colori Rosso, Verde e Blu
vengono definiti primari in quanto a partire da essi si può ottenere
un qualsiasi altro colore; a tale proposito, basta assegnare i valori
desiderati di intensità al Rosso, al Verde e al Blu e poi miscelare
le tre componenti di colore così ottenute.
Nel caso della modalità testo 03h, ad ogni terna RGB possiamo
assegnare solamente due valori dell'intensità (0 = bassa, 1 = alta);
quindi, con i tre bit RGB possiamo formare 23=8 colori, ciascuno dei
quali può avere una intensità alta o bassa, per un totale di 2*8=16
colori (sia per lo sfondo, sia per il primo piano).
Su alcuni BIOS (soprattutto quelli meno recenti) il bit in posizione
7 viene definito blinking bit (bit di lampeggiamento) in quanto
permette di attivare (1) o disattivare (0) il lampeggiamento dello
sfondo; in tal caso, per lo stesso sfondo si hanno a disposizione
solo 23=8 colori a bassa intensità.

9.2.3 Le pagine video

Ad ogni modalità video viene riservata una apposita quantità di VRAM


accessibile attraverso il frame buffer; ovviamente, tale quantità di
VRAM deve essere sufficiente a contenere almeno una cosiddetta
schermata (l'area visibile attraverso lo schermo).
Nel caso della modalità testo 07h, ad esempio, abbiamo visto che una
schermata è formata da una matrice di 80x25=2000 celle; come
sappiamo, a ciascuna cella vengono riservati 2 byte per cui sarà
necessaria una VRAM formata da almeno 2*2000=4000 byte.
In effetti, come si vede in Figura 6, alla modalità testo 07h viene
riservata una VRAM da 4096 byte (4 Kb) accessibile attraverso un
224
frame buffer da 32 Kb che parte dall'indirizzo logico B000h:0000h;
tale blocco da 4096 byte prende il nome di pagina video.
Le pagine video vengono numerate con gli indici 0, 1, 2, ... e, in un
determinato momento, solo una di esse può essere attiva (cioè,
visibile attraverso lo schermo); in Figura 6 si nota che, nel caso
della modalità testo 07h, esiste una sola pagina video identificata
dall'indice 0 (esistono anche varianti del MDA che forniscono una
quantità maggiore di VRAM suddivisa in 2 o più pagine video da 4096
byte ciascuna).

Per la modalità testo 03h, che è la più diffusa, abbiamo a


disposizione sino a 64 Kb di VRAM accessibili attraverso un frame
buffer da 32 Kb che parte dall'indirizzo logico B800h:0000h; anche la
modalità 03h richiede 4000 byte per ogni schermata 80x25.
Inizialmente, sullo schermo si trova visualizzata la pagina video 0,
ma sono disponibili in totale sino a 8 pagine video, ciascuna delle
quali occupa 4096 byte; osservando che 4096 in esadecimale si scrive
1000h, possiamo affermare che la pagina video 0 parte dall'indirizzo
logico B800h:0000h, la pagina video 1 parte dall'indirizzo logico
B800h:1000h, la pagina video 2 parte dall'indirizzo logico
B800h:2000h e così via.

Se, in un determinato momento, risulta attiva la pagina video di


indice m, allora tutto l'output inviato ad una pagina video di indice
n (non attiva) risulterà invisibile sullo schermo; questa situazione
permane finché il programmatore non effettua una commutazione
rendendo la pagina video n attiva (con la pagina video m che diventa
quindi invisibile)!

9.3 Principali servizi forniti dalla INT 10h per la


modalità testo

Come è stato già anticipato, il vettore di interruzione n.10h del


BIOS fornisce una numerosa serie di servizi destinati alla gestione
del supporto video; analizziamo allora i principali di tali servizi
che si rivolgono espressamente alle modalità testo.

9.3.1 Servizio n.00h: Set Video Mode

Questo servizio permette di selezionare la modalità video desiderata.

Video BIOS - Servizio n. 00h - Set Video Mode

Argomenti richiesti:
AH = 00h (Set Video Mode)
AL = codice modo video

Valori restituiti:
AL = video mode flag

Il servizio n.00h permette di selezionare la modalità video


desiderata; a tale proposito, bisogna caricare in AL uno dei codici
visibili nelle figure dalla 2 alla 6.
Dopo la chiamata della INT 10h, nel registro AL viene restituito un
225
codice che indica sommariamente la modalità video corrente; i valori
possibili sono: 20h (indica che la modalità video selezionata è
maggiore di 07h), 30h (indica che la modalità video selezionata è una
tra 00h, 01h, 02h, 03h, 04h, 05h, 07h), 3Fh (indica che la modalità
video selezionata è la 06h).
Quando si attiva una qualunque modalità video, l'intero contenuto
dello schermo viene cancellato; se l'utente non vuole che ciò
avvenga, deve porre a 1 il bit 7 di AL prima di chiamare il servizio
n.00h.

9.3.2 Servizio n.01h: Set Cursor Size

Questo servizio permette di impostare lo spessore del cursore sullo


schermo.

Video BIOS - Servizio n. 01h - Set Cursor Size

Argomenti richiesti:
AH = 01h (Set Cursor Size)
CH = top scan line + modo di lampeggiamento
CL = bottom scan line

Il servizio 01h permette di impostare l'altezza in pixel del cursore


sullo schermo; a tale proposito, bisogna tenere presente che ogni
cella è suddivisa in tante linee orizzontali (da 1 pixel di spessore
ciascuna) denominate scan lines (linee di scansione) e numerate con
gli indici 0, 1, 2, ... a partire da quella più in alto.
Nel caso, ad esempio, della modalità video 03h, abbiamo visto che
ogni cella ha un'altezza in pixel pari a 16, per cui la cella stessa
risulterà composta da 16 linee di scansione numerate da 0 (quella più
in alto) a 15; il programmatore può impostare lo spessore in pixel
del cursore specificando la scan line iniziale (top scan line) e
quella finale (bottom scan line) tra le quali il cursore stesso
risulterà compreso.
La top scan line deve essere specificata nei bit 0, 1, 2, 3, 4 di CH,
mentre la bottom scan line deve essere specificata nei bit 0, 1, 2,
3, 4 di CL; i bit 5, 6, 7 di CL vengono ignorati.
Il bit 7 di CH deve valere 0, mentre i bit 5, 6 di CH permettono di
impostare la modalità di lampeggiamento del cursore; i valori
possibili sono: 00h (normale), 01h (invisibile), 10h (erratico), 11h
(lento).

9.3.3 Servizio n.02h: Set Cursor Position

Questo servizio permette di modificare la posizione del cursore sullo


schermo.

Video BIOS - Servizio n. 02h - Set Cursor Position

Argomenti richiesti:
AH = 02h (Set Cursor Position)
BH = indice pagina video
DH = riga (00h = prima riga in alto)
DL = colonna (00h = prima colonna a sinistra)

226
Attraverso questo servizio il programmatore può posizionare il
cursore lampeggiante in una qualsiasi cella dello schermo; a tale
proposito, le nuove coordinate devono comprendere la riga e la
colonna della cella in cui il cursore deve essere posizionato.
Il registro DH indica la nuova riga, mentre il registro DL indica la
nuova colonna; ad esempio, nella modalità video 03h da 80x25 celle,
le righe sono comprese tra 0 (quella più in alto) e 24, mentre le
colonne sono comprese tra 0 (quella più a sinistra) e 79.
Il registro BH indica la pagina video in cui visualizzare il cursore;
per le modalità con una sola pagina video, BH deve valere sempre 00h.

9.3.4 Servizio n.03h: Get Cursor Position and Size

Questo servizio restituisce una serie di informazioni sul cursore


lampeggiante.

Video BIOS - Servizio n. 03h - Get Cursor Position and


Size

Argomenti richiesti:
AH = 03h (Get Cursor Position and Size)
BH = indice pagina video

Valori restituiti:
AX = 0000h
CH = top scan line
CL = bottom scan line
DH = riga corrente del cursore
DL = colonna corrente del cursore

Questo servizio restituisce una serie di informazioni relative allo


stato corrente del cursore lampeggiante; tali informazioni
comprendono lo spessore del cursore in pixel e la sua posizione
(coordinate riga, colonna) sullo schermo.

9.3.5 Servizio n.05h: Select Active Display Page

Questo servizio permette di impostare la pagina video corrente.

Video BIOS - Servizio n. 05h - Select Active Display


Page

Argomenti richiesti:
AH = 05h (Select Active Display Page)
AL = indice pagina video

Attraverso questo servizio, il programmatore può impostare la nuova


pagina video attiva (cioè, visibile sullo schermo); subito dopo
l'esecuzione di tale servizio, la precedente pagina video diventa
invisibile.

9.3.6 Servizio n.08h: Read Character and Attribute at Cursor Position

227
Questo servizio permette di ottenere il simbolo e l'attributo
relativo alla cella in cui si trova il cursore.

Video BIOS - Servizio n. 08h - Read Char. and Attr. at


Cursor Position

Argomenti richiesti:
AH = 08h (Read Character and Attribute at Cursor
Position)
BH = indice pagina video

Valori restituiti:
AH = attributo della cella
AL = codice ASCII del simbolo presente nella cella

Chiamando questo servizio, possiamo ottenere l'attributo e il codice


ASCII relativi alla cella in cui si trova in quel momento il cursore
dello schermo; è anche possibile ottenere le stesse informazioni
accedendo direttamente alla memoria video.

9.3.7 Servizio n.09h: Write Character and Attribute at Cursor


Position

Questo servizio permette di modificare il simbolo e l'attributo


presenti nella cella in cui si trova il cursore.

Video BIOS - Servizio n. 09h - Write Char. and Attr. at


Cursor Position

Argomenti richiesti:
AH = 09h (Write Character and Attribute at Cursor
Position)
AL = codice ASCII del simbolo da visualizzare
BH = indice pagina video
BL = attributo della cella
CX = numero di ripetizioni del simbolo

Utilizzando questo servizio è possibile impostare dei nuovi valori


per il codice ASCII e l'attributo relativi alla cella in cui si trova
in quel momento il cursore dello schermo; il simbolo da visualizzare
viene ripetuto per un numero di volte specificato in CX.
Si può ottenere lo stesso risultato accedendo direttamente alla
memoria video.

9.3.8 Servizio n.0Fh: Get Current Video Mode

Questo servizio restituisce informazioni relative alla modalità video


correntemente selezionata.

Video BIOS - Servizio n. 0Fh - Get Current Video Mode

Argomenti richiesti:
AH = 0Fh (Get Current Video Mode)

Valori restituiti:
228
AH = numero di celle in orizzontale (colonne)
AL = modalità video corrente
BH = indice pagina video attiva

Attraverso questo servizio il programmatore può ottenere tutte le


informazioni relative alla modalità video correntemente selezionata;
il valore restituito in AL è uno di quelli visibili nelle figure
dalla 2 alla 6.

9.3.9 Servizio n.11h: Text Mode Character Generator

Questo servizio permette di selezionare un vettore di bitmap per i


simboli da visualizzare sullo schermo.

Video BIOS - Servizio n. 11h - Text Mode Character


Generator

Argomenti richiesti:
AH = 11h (Text Mode Character Generator)
AL = tipo di bitmap da caricare
BL = blocco da caricare

Attraverso questo servizio, possiamo impostare il vettore contenente


le bitmap dei simboli da visualizzare sullo schermo; è possibile
selezionare uno dei vettori memorizzati nella ROM, oppure un vettore
di bitmap personalizzate.

Se AL vale 00h o 10h, allora questo servizio permette di caricare un


vettore di bitmap personalizzate predisposto dal programmatore; in
tal caso, la coppia ES:BP deve puntare al vettore, CX deve contenere
il numero di elementi del vettore, BH deve contenere l'ampiezza in
byte di ogni bitmap, BL deve contenere il blocco da caricare nella
mappa 2, DX deve contenere l'offset delle bitmap all'interno del
blocco nella mappa 2.
I valori da caricare in DX e BL possono essere ricavati dal servizio
1Bh illustrato più avanti; i valori che ci interessano si trovano
agli offset 2Bh e 2Ch della tabella di informazioni restituita dal
servizio stesso.

Se AL contiene un valore diverso da 00h o 10h, allora questo servizio


permette di selezionare uno dei vettori di bitmap presenti nella ROM;
in particolare:

* se AL=01h o AL=11h, viene selezionato un vettore di bitmap 8x14 per


adattatori monocromatici;
* se AL=02h o AL=12h, viene selezionato un vettore di bitmap 8x8;
* se AL=04h o AL=14h, viene selezionato un vettore di bitmap 8x16 per
adattatori VGA.

In tutti questi casi BL deve valere 00h.

9.3.10 Servizio n.12h: Select Vertical Resolution (VGA)

229
Questo servizio permette di impostare il numero di linee di scansione
per la modalità testo degli adattatori VGA.

Video BIOS - Servizio n. 12h - Select Vertical


Resolution

Argomenti richiesti:
AH = 12h (Select Vertical Resolution)
AL = numero linee di scansione
BL = 30h (codice servizio)

Valori restituiti:
AL = 12h se la funzione è supportata

Attraverso questo servizio, possiamo modificare l'altezza in pixel


dello schermo (numero di linee di scansione dello schermo) in
modalità testo; ciò è possibile solo in presenza di un adattatore
VGA.
Il nuovo numero di line di scansione deve essere specificato
attraverso il registro AL; i valori permessi sono: 00h (200 linee di
scansione), 01h (350 linee di scansione), 02h (400 linee di
scansione).

Sfruttando questo servizio e quello n.11h presentato in precedenza,


si può ottenere un interessante effetto che consiste nell'impostare
una modalità testo 03h da 80x50 celle in combinazione con un vettore
di bitmap da 8x8 pixel ciascuna; il procedimento da utilizzare è il
seguente:

; selezione vertical resolution

mov ah, 12h ; AH = servizio Select Vertical


Resolution
mov al, 02h ; AL = 400 scan lines
mov bl, 30h ; BL = codice servizio
int 10h ; chiama i servizi video del BIOS

; selezione modalità video 03h da 80x50 celle a 16 colori

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 03h ; AL = modo testo 80x50 a 16 colori
int 10h ; chiama i servizi video del BIOS

; caricamento font 8x8 dalla ROM

mov ah, 11h ; AH = servizio Text Mode Character


Generator
mov al, 12h ; AL = load ROM 8x8 font
mov bl, 00h ; BL = blocco 0
int 10h ; chiama i servizi video del BIOS

In pratica, selezioniamo una normale modalità video testo 03h con


risoluzione di schermo da 720x400 pixel (400 scan lines); l'aspetto
particolare sta nel fatto che ci serviamo di font da 8x8 pixel che ci
permettono di sfruttare 400/8=50 righe di celle (al posto delle
400/16=25 righe di celle che si hanno con l'utilizzo dei normali font
230
da 8x16 pixel).

Per tornare alla normale visualizzazione da 80x25 celle, basta


semplicemente eseguire il codice:

; selezione modalità video 03h da 80x25 celle a 16 colori

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 03h ; AL = modo testo 80x25 a 16 colori
int 10h ; chiama i servizi video del BIOS

9.3.11 Servizio n.13h: Write String

Questo servizio permette di visualizzare una stringa sullo schermo.

Video BIOS - Servizio n. 13h - Write String

Argomenti richiesti:
AH = 13h (Write String)
AL = modo di scrittura
BH = indice pagina video
BL = attributo
CX = numero di simboli nella stringa
DH = riga iniziale di output
DL = colonna iniziale di output
ES:BP = puntatore alla stringa da visualizzare

Il servizio 13h permette di visualizzare una stringa sullo schermo a


partire dalla cella che si trova alle coordinate DH (riga), DL
(colonna); la stringa deve essere puntata dalla coppia ES:BP e deve
terminare con il simbolo '$'.
Il registro AL permette di selezionare alcune opzioni supplementari
attraverso i bit in posizione 0 e 1 (i bit dal 2 al 7 sono
riservati); il bit 0 permette di stabilire se la posizione del
cursore deve essere aggiornata (1) o meno (0) dopo la scrittura,
mentre il bit 1 indica se la stringa è formata solo da simboli (0) o
da una alternanza di simboli e attributi (1).
Il registro BL permette di impostare l'attributo comune di tutte le
celle occupate dalla stringa; ovviamente, BL viene preso in
considerazione solo se il bit 1 di AL vale 0.

9.3.12 Servizio n.1Bh: Functionality/State Information

Questo servizio permette di ottenere una dettagliata lista di


informazioni sull'adattatore video presente nel computer.

Video BIOS - Servizio n. 1Bh - Functionality/State


Information

Argomenti richiesti:
AH = 1Bh (Functionality/State Information)
BX = 0000h (codice servizio)
ES:DI = puntatore al buffer informazioni da 64 byte

231
Valori restituiti:
AL = 1Bh se la funzione è supportata
ES:DI = punt. al buffer riempito con 64 byte di
informazioni

Il servizio 1Bh riempie un vettore da 64 byte, puntato da ES:DI, con


un elenco dettagliato di informazioni sull'adattatore video presente
nel computer; il blocco da 64 byte necessario per contenere il
vettore deve essere allocato dal programmatore.
Per maggiori dettagli su questo servizio si può consultare la
documentazione presente nella sezione Downloads (vedere la
bibliografia in fondo alla pagina).

9.4 Esempi pratici

I numerosi servizi offerti dalla INT 10h, pur essendo molto semplici
e comodi da usare, presentano la caratteristica negativa di una
relativa lentezza; come sappiamo, ciò è dovuto al fatto che spesso le
ISR eseguono prudenzialmente una notevole serie di controlli che
finiscono per appesantire notevolmente il codice.
Questa situazione diventa particolarmente problematica quando si ha a
che fare con dispositivi (come la memoria video) che comportano
trasferimenti di grosse quantità di dati alla massima velocità
possibile; proprio per questo motivo, gli esempi che seguono fanno
ricorso ad un metodo molto più efficiente che consiste nell'accesso
diretto alla VRAM.

9.4.1 Accesso alle 8 pagine video della modalità 03h da 80x25 celle

Partiamo subito con un programma che mostra come gestire le 8 pagine


video disponibili nella modalità standard 03h da 80x25 celle; la
Figura 11 illustra il listato del programma VIDPAGES.ASM.

Figura 11 - File VIDPAGES.ASM


;-----------------------------------------------------;
; file vidpages.asm ;
; gestione pagine video nella modalita' testo 03h ;
;-----------------------------------------------------;
; nasm -f obj vidpages.asm ;
; tlink vidpages.obj + exelib.obj ;
; (oppure link vidpages.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

232
;----- inizio definizione variabili statiche ------

str_title db "GESTIONE DELLE 8 PAGINE VIDEO NELLA MODALITA' "


db "TESTO 03h 80x25", 0
str_keypress db "Premere un tasto per continuare ... ", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 000Ah ; riga 0, colonna 10
call far writeString ; mostra la stringa

mov di, str_keypress ; stringa da mostrare


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa

call far waitChar ; attende la pressione di un tasto

mov ax, 0B800h ; AX = Seg frame buffer


mov es, ax ; ES = AX
xor di, di ; DI = 0000h (Offset frame buffer)
mov eax, 12411241h ; EAX = 'A', 12h, 'A', 12h
mov cx, 8 ; 8 pagine video da riempire
cld ; DF = 0 (incremento puntatori)

fill_vpages:
push cx ; preserva CX
mov cx, 1000 ; 1000 DWORD da scrivere
rep stosd ; scrive nel buffer video
pop cx ; ripristina CX

add eax, 12011201h ; incrementa simbolo e attributo


add di, 96 ; DI = Offset prossima pagina video
loop fill_vpages ; controllo loop

call far waitChar ; attende la pressione di un tasto

mov bl, 1 ; indice prossima pagina video


mov cx, 7 ; altre 7 pagine video da mostrare

show_vpages:
mov ah, 05h ; AH = Select Active Display Page
mov al, bl ; AL = indice pagina video
233
int 10h ; chiama i servizi Video BIOS

inc bl ; indice prossima pagina video


call far waitChar ; attende la pressione di un tasto
loop show_vpages ; controllo loop

mov ah, 05h ; AH = Select Active Display Page


mov al, 00h ; AL = pagina video 0
int 10h ; chiama i servizi Video BIOS

call far clearScreen ; pulisce lo schermo


call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Questo programma riempie le 8 pagine video disponibili nella modalità
testo standard 03h da 80x25 celle; successivamente, le informazioni
memorizzate in VRAM vengono mostrate in sequenza sullo schermo.

La pagina video corrente è la numero 0 e viene riempita con


80x25=4000 coppie simbolo, attributo; il simbolo iniziale è il codice
ASCII (41h) della lettera 'A', mentre l'attributo iniziale è
12h=00010010b e cioè: sfondo blu scuro e testo verde scuro.
Per accelerare al massimo il trasferimento dati nella VRAM, mettiamo
due di queste coppie (12411241h) in EAX; a questo punto trasferiamo
2000 coppie (4000 byte) in VRAM attraverso REP STOSD.
Siccome stiamo scrivendo nella pagina video corrente (la numero 0),
tutti i dati appena trasferiti in VRAM saranno visualizzati
istantaneamente sul monitor.

Ad ogni loop incrementiamo di 01h il codice ASCII del simbolo da


visualizzare, mentre il byte degli attributi viene incrementato di
12h (per influenzare lo sfondo e il primo piano); nel caso, ad
esempio, del primo loop, otteniamo:

EAX = EAX + 12011201h = 12411241h + 12011201h = 24422442h

Osserviamo che dopo l'esecuzione dell'istruzione REP STOSD con


CX=1000, il registro DI risulterà contenere il valore 4000; per far
puntare DI all'offset della successiva pagina video (da 4096 byte)
dobbiamo quindi incrementare lo stesso DI di 96 byte.

Il loop delimitato dall'etichetta show_vpages non fa altro che


234
visualizzare in sequenza le altre 7 pagine video che abbiamo appena
riempito (la n.0 è già visibile); a tale proposito, utilizziamo il
servizio n.05h della INT 10h.

Prima di terminare, il programma provvede a ripristinare lo stato


dell'adattatore video in modo che la pagina video n.0 torni ad essere
quella attiva.

Nota importante.
Si ricordi che le 8 pagine video sono disponibili solo nella
modalità testo standard 03h da 80x25 celle; se intendiamo
attivare la modalità testo estesa 03h da 80x50 celle,
dobbiamo tenere presente che le pagine video diventano 4 da
8192 byte ciascuna.
Se non rispettiamo queste regole, un tentativo di scrittura
nella VRAM potrebbe sconfinare nell'area di memoria
successiva; in casi del genere la conseguenza più probabile è
un crash del programma!

Come si può facilmente intuire, la struttura del programma di Figura


11 si presta perfettamente per l'uso dei segmenti di programma con
attributo ABSOLUTE; in base allora a quanto è stato esposto nel
Capitolo 12 della sezione Assembly Base, possiamo definire un
segmento di programma del tipo:

SEGMENT VIDBUFFER ABSOLUTE=0B800h

In questo modo, NASM crea un segmento di programma che parte


dell'indirizzo logico assoluto B800h:0000h, coincidente proprio con
l'inizio del frame buffer per la modalità video testo a colori;
ponendo ora ES=VIDBUFFER, possiamo facilmente effettuare operazioni
di I/O sulla VRAM utilizzando un registro puntatore (ad esempio, DI)
in combinazione con lo stesso ES!

9.4.2 Accesso ad una qualunque cella dello schermo

Se vogliamo accedere in I/O ad una qualunque cella dello schermo, non


dobbiamo fare altro che applicare semplicissime formule di geometria;
a tale proposito, basta ricordare che lo schermo ci appare sotto
forma di matrice di celle, mentre la VRAM è, in realtà, un vettore di
BYTE.
Nel caso, ad esempio, della modalità testo standard 03h da 80x25
celle, abbiamo 80 colonne numerate da 0 a 79 e 25 righe numerate da 0
a 24; ad ogni cella viene assegnata una WORD per cui, se vogliamo
sapere a quale WORD della VRAM corrisponde la cella di coordinate x,
y (colonna, riga) della pagina video di indice i, basta calcolare:

(i * 4096) + (y * (2 * 80)) + (x * 2)

Ad esempio, la cella di coordinate 13, 8 della pagina video 0 risulta


associata alla WORD di indice:

(0 * 4096) + (8 * (2 * 80)) + (13 * 2) = 0 + 1280 + 26 = 1306 = 051Ah

235
del vettore della VRAM; di conseguenza, il simbolo da visualizzare
nella cella si trova all'indirizzo B800h:051Ah, mentre l'attributo si
trova all'indirizzo B800h:051Bh.

Per velocizzare le moltiplicazioni si può notare che 4096=212;


possiamo affermare allora che, simbolicamente:

(i * 4096) = i * 212 = SHL i, 12

Analogamente:

(x * 2) = x * 21 = SHL x, 1

Il valore 2 * 80 = 160, invece, non può essere espresso sotto forma


di potenza intera di 2 o somma di potenze intere di 2; in un caso del
genere siamo costretti a svolgere la moltiplicazione ordinaria.

9.4.3 Visualizzazione di numeri interi sullo schermo

In base a quanto è stato esposto in questo capitolo, abbiamo appurato


che sullo schermo è possibile visualizzare in modo diretto solamente
stringhe; ciò vale, sia per la modalità testo, sia per la modalità
grafica.
Tutto ciò significa che non è possibile visualizzare numeri
direttamente sullo schermo; per fare una cosa del genere, è
necessario prima convertire i numeri stessi in quelle che, nella
sezione Assembly Base, abbiamo definito stringhe numeriche.

Partiamo dal caso più semplice rappresentato dai numeri binari che,
come sappiamo, sono composti da sequenze dei due soli simboli '0' e
'1'; nell'ipotesi di voler operare sui numeri binari a 8 bit,
definiamoci allora le due stringhe:

str_cifre db "01"
str_bin8 db "00000000b", 0

Utilizzando DS per gestire il segmento dati contenente le due


stringhe, possiamo scrivere:

mov di, str_bin8 + 7 ; DS:DI punta a str_bin8 + 7


xor bh, bh ; BH = 0
mov al, [numero_binario] ; AL = numero binario da convertire
mov cx, 8 ; 8 cifre da gestire

bin8_to_str:
mov bl, al ; BL = AL
and bl, 00000001b ; isola il bit meno significativo di BL
mov dl, [str_cifre+bx] ; DL = codice ASCII cifra binaria
mov [di], dl ; copia DL in str_bin8

shr al, 1 ; prossimo bit da convertire


dec di ; decremento puntatore DI
loop bin8_to_str ; controllo loop

Il registro AL contiene il numero binario a 8 bit da convertire in


236
una stringa; il registro DI contiene l'offset dell'ottavo BYTE (da
sinistra) di str_bin8 (ricordiamoci che le stringhe vengono
visualizzate da sinistra a destra mentre, nel sistema posizionale, il
peso delle cifre di un numero cresce procedendo da destra verso
sinistra).
Ripetiamo ora per 8 volte il loop delimitato dall'etichetta
bin8_to_str; ad ogni iterazione, trasferiamo in BL il contenuto di AL
ed isoliamo il bit meno significativo dello stesso BL.
A questo punto, tenendo presente che BH=0, si vede subito che, se
BL=0, allora str_cifre+BX punta al simbolo '0'; analogamente, se
BL=1, allora str_cifre+BX punta al simbolo '1'.
Tale simbolo, che rappresenta il codice ASCII di '0' o di '1', viene
trasferito in DL; a sua volta, il contenuto di DL viene trasferito in
str_bin8 attraverso il puntatore DI.
Prima di ripetere il loop spostiamo di un posto verso destra i bit di
AL in modo che il prossimo bit da convertire si venga a trovare in
posizione 0 nello stesso registro AL; il registro DI viene
decrementato di 1 in modo che punti ad un nuovo BYTE di str_bin8.
Terminate le 8 iterazioni, abbiamo ottenuto in str_bin8 una stringa
che riproduce simbolicamente gli 8 bit del valore numero_binario.

Nel caso di numeri binari a 16 o più bit, il procedimento è


assolutamente analogo; a tale proposito, basta modificare
opportunamente il numero di iterazioni da effettuare ricordandosi
anche di applicare SHR ad AX per i numeri a 16 bit e ad EAX per i
numeri a 32 bit.

Passiamo ora al caso altrettanto semplice dei numeri esadecimali che,


come sappiamo, sono composti da sequenze dei simboli da '0' a '9' e
da 'A' a 'F'; anche in questo caso, il procedimento è del tutto
analogo a quello seguito per i numeri binari.
Osserviamo subito che ogni cifra esadecimale è composta da 4 bit e
rappresenta un valore compreso tra 0 e 15; nell'ipotesi allora di
voler operare sui numeri esadecimali a 16 bit, definiamoci le due
stringhe:

str_cifre db "0123456789ABCDEF"
str_hex16 db "0000h", 0

Utilizzando DS per gestire il segmento dati contenente le due


stringhe, possiamo scrivere:

mov di, str_hex16 + 3 ; DS:DI punta a str_hex16 + 3


xor bh, bh ; BH = 0
mov ax, [numero_esadec] ; AX = numero esadecimale da convertire
mov cx, 4 ; 4 cifre da gestire

hex16_to_str:
mov bl, al ; BL = AL
and bl, 00001111b ; isola il nibble meno significativo di
BL
mov dl, [str_cifre+bx] ; DL = codice ASCII cifra esadecimale
mov [di], dl ; copia DL in str_hex16

shr ax, 4 ; prossimo nibble da convertire


237
dec di ; decremento puntatore DI
loop hex16_to_str ; controllo loop

Come si può notare, l'unica differenza rispetto ai numeri binari sta


nel fatto che operiamo sui nibble anziché sui singoli bit; tenendo
presente che BH=0, ogni nibble isolato in BL è tale che BX conterrà
un valore compreso tra 0 e 15 per cui str_cifre+BX punterà al
corrispondente simbolo esadecimale.

Il caso più impegnativo è sicuramente quello dei numeri interi in


base 10; come sappiamo, le difficoltà sono legate al fatto che 10 non
può essere espresso sotto forma di potenza intera di 2.
Consideriamo, ad esempio, il numero 27952; questo numero richiede
almeno 16 bit, ma non avrebbe alcun senso pensare di suddividerlo in
gruppi di bit in modo da ricavare una delle cifre decimali da
ciascuno di tali gruppi!

Un altro aspetto di notevole importanza è dato dal fatto che in


presenza di numeri interi relativi in base 10, dobbiamo occuparci
anche della gestione del segno; questo problema non si pone per i
numeri relativi binari o esadecimali in quanto il segno è codificato
nel numero stesso (bit di segno).
La gestione del segno avviene in modo molto semplice sfruttando il
concetto di complemento a 2 illustrato nel Capitolo 4 della sezione
Assembly Base; per analizzare in pratica il metodo da seguire,
supponiamo di operare sui numeri interi relativi a 16 bit.
Come sappiamo, in questo caso si parla di "codifica dei numeri interi
relativi in complemento a 2 modulo 65536; il nostro insieme sarà
allora rappresentato da tutti i numeri interi relativi compresi tra -
32768 e +32767.

Per convertire un numero intero decimale in una stringa numerica, si


può utilizzare un procedimento che già conosciamo e che consiste nel
sottoporre il numero stesso ad una serie di divisioni per 10 (cioè,
per la base); in questo modo si ottiene una sequenza di resti che,
nel loro insieme, rappresentano le cifre del nostro numero (a partire
da quella meno significativa).
Vediamo un esempio relativo al numero 23272; dividendo ripetutamente
per 10 otteniamo:

23272 / 10 = 2327 con resto 2

2327 / 10 = 232 con resto 7

232 / 10 = 23 con resto 2

23 / 10 = 2 con resto 3

2 / 10 = 0 con resto 2

Come si può notare, il procedimento termina quando si ottiene un


quoziente pari a 0; unendo tutti i resti ottenuti si ricava proprio
23272.

238
Nell'ipotesi allora di voler operare sui numeri interi relativi a 16
bit, definiamoci le due stringhe:

str_cifre db "0123456789"
str_dec16 db "+00000", 0

Utilizzando DS per gestire il segmento dati contenente le due


stringhe, possiamo scrivere:

mov [str_dec16], byte '+' ; assume che il numero sia positivo


mov ax, [numero_decimale] ; AX = numero decimale da convertire
test ah, 80h ; test sul bit di segno
jz numero_positivo ; bit di segno = 0 (numero positivo)
mov [str_dec16], byte '-' ; mette il segno meno davanti al numero
neg ax ; e nega il numero stesso

numero positivo:
mov di, str_dec16 + 5 ; DS:DI punta a str_dec16 + 5
mov bx, 10 ; BX = divisore
mov cx, 5 ; 5 cifre da convertire

dec16_to_str:
xor dx, dx ; DX = 0 (per la divisione)
div bx ; AX = quoziente, DX = resto
mov si, dx ; SI = cifra da convertire
mov dl, [str_cifre+si] ; DL = codice ASCII cifra decimale
mov [di], dl ; copia DL in str_dec16

dec di ; decremento puntatore DI


loop dec16_to_str ; controllo loop

Prima di tutto, se il numero è positivo lo lasciamo così com'è; se,


invece, è negativo, gli mettiamo un segno - davanti e lo neghiamo
(cioè, lo convertiamo nel suo equivalente positivo).
A questo punto possiamo applicare il metodo delle divisioni
successive; ogni cifra ottenuta (dal resto) viene usata per leggere
il corrispondente simbolo da str_cifre.

Bibliografia

Cristopher, Feigenbaum, Saliga - MS-DOS MANUALE DI PROGRAMMAZIONE -


Mc Graw Hill

VGABIOS.TXT - Elenco servizi Video BIOS della INT 10h


(disponibile nella sezione Downloads - Documentazione -
vgabios.tar.bz2)

239
Capitolo 10 La memoria video in modalità
grafica standard
Nel precedente capitolo abbiamo analizzato le caratteristiche
generali delle modalità video testo standard; in questo capitolo,
invece, parleremo dell'evoluzione del supporto grafico attraverso i
vari adattatori video standard imposti nel corso degli anni dalla
IBM.

Nota importante.
I programmi di esempio presentati in questo capitolo,
funzionano perfettamente sugli emulatori DOS come, ad
esempio, DOSEmu; tali emulatori utilizzano una semplice
finestra alla quale viene inviato l'output prodotto dalle
varie modalità video grafiche.
La DOS Box fornita dalle versioni più recenti di Windows
(come, XP, Vista, etc) potrebbe, invece, impedire all'utente
di abilitare determinate modalità grafiche; ciò è vero, in
particolare, per le modalità grafiche non standard
(soprattutto le cosiddette modalità Super VGA)!

10.1 Principio di funzionamento di un monitor per PC

Per comprendere meglio alcuni concetti esposti in questo capitolo,


analizziamo sommariamente il principio di funzionamento delle due più
diffuse famiglie di monitor per PC.

10.1.1 Monitor CRT

Il monitor CRT o Cathode Ray Tube (tubo a raggi catodici) è così


chiamato in quanto al suo interno è presente un dispositivo
denominato cannone elettronico il cui scopo è quello di "sparare" un
raggio di elettroni (raggio catodico) verso la parte interna dello
schermo che è interamente ricoperta da piccolissime particelle di
fosforo; una importante caratteristica del fosforo è quella di
emettere luce quando viene colpito da una qualche fonte di energia
(come quella legata allo stesso raggio catodico).
Nel caso più semplice, rappresentato dai monitor monocromatici,
vengono usate particelle di fosforo capaci di emettere luce di un
unico colore (in genere, bianco, verde o ambra); in un caso del
genere, per colpire i fosfori viene usato un singolo raggio catodico.
L'hardware della scheda video legge il contenuto "visibile" della
VRAM e lo trasferisce sullo schermo secondo lo schema mostrato in
Figura 1; in tale figura, l'andamento del raggio catodico è stato
volutamente esagerato per motivi di chiarezza.

Figura 1 - Refresh dello schermo

240
Attraverso apposite placche di deflessione, il raggio elettronico può
essere deviato, sia verticalmente, sia orizzontalmente; il
riempimento (o ritraccia) dello schermo viene effettuato posizionando
inizialmente il raggio in alto a destra.
Il raggio viene spostato in orizzontale sino all'estremità sinistra
dello schermo in modo da tracciare la prima linea di pixel (linea di
scansione); i pixel che vengono colpiti emettono luce, mentre gli
altri restano spenti (colore nero). Lo stesso raggio viene poi
riportato a destra in modo da procedere con la ritraccia della
successiva linea di scansione.
Naturalmente, questo movimento a zig-zag del raggio catodico si
ripete finché non viene tracciato l'intero schermo; a questo punto,
il raggio viene riportato in alto a destra in modo da dare inizio
alla ritraccia di una nuova schermata.

I fosfori colpiti dal raggio catodico emettono luce per un intervallo


di tempo estremamente breve (una frazione di secondo); ciò rende
necessaria la ritraccia dello schermo per numerose volte al secondo.
Il numero di ritracce al secondo dell'intero schermo prende il nome
di vertical refresh (rinfresco verticale); per evitare fastidiosi
fenomeni di sfarfallio, dovuti alla persistenza delle immagini nella
retina dell'occhio umano, il vertical refresh deve avvenire diverse
decine di volte al secondo (cioè, la frequenza del vertical refresh
deve essere di diverse decine di Hz).
I monitor di qualità sono capaci di supportare vertical refresh
abbondantemente superiori a 60 Hz anche con risoluzioni grafiche
molto alte; maggiore è la frequenza del vertical refresh, minore sarà
l'affaticamento degli occhi (in quanto l'immagine sullo schermo
risulterà più stabile).
Ovviamente, il vertical refresh è necessario, non solo per sopperire
alla "decadenza luminosa" dei fosfori, ma anche per il fatto che il
contenuto dello schermo può variare molto rapidamente necessitando
quindi di un continuo aggiornamento; questo aspetto diventa
particolarmente importante, ad esempio, nel caso dei videogiochi.

Nei monitor a colori, ogni particella di fosforo monocromatico viene


sostituita con una terna di particelle che comprende: un fosforo a
luce rossa, un fosforo a luce verde e un fosforo a luce blu; il
241
singolo raggio catodico viene sostituito con tre raggi, ciascuno dei
quali colpisce una delle particelle della terna producendo così un
classico colore RGB.
Al variare dell'intensità di ogni raggio varia anche l'intensità
della luce emessa dal fosforo colpito; in questo modo, si possono
ottenere teoricamente infiniti colori!

10.1.2 Monitor LCD

Il monitor LCD o Liquid Crystall Display (visore a cristalli liquidi)


basa il suo funzionamento sulle proprietà chimico-fisiche di
particolari composti; tali proprietà furono scoperte nel 1888 dal
botanico austriaco Friedrich Reinitzer relativamente al benzoato di
colesterile.
In particolare, Reinitzer si accorse che questa sostanza sembrava
possedere due specifiche temperature di fusione; a 145 °C diventava
un liquido opaco, per poi tornare trasparente a 179 °C.
In seguito, il professore di fisica tedesco Otto Lehmann studiò il
fenomeno in modo più approfondito scoprendo che, effettivamente, tra
le due temperature di fusione il benzoato di colesterile si comporta
come un liquido che però mantiene la struttura cristallina tipica dei
solidi; per evidenziare tale aspetto, nel 1889 lo stesso Lehmann
coniò il termine cristallo liquido (CL).

La caratteristica fondamentale dei CL è che le loro molecole hanno


una forma piuttosto allungata e, allo stato liquido, risultano molto
vicine le une alle altre; tali molecole sono costrette quindi a
disporsi (allinearsi) secondo una direzione preferenziale lungo un
asse detto direttore.
Un'altra caratteristica importantissima dei CL è che le loro molecole
sono soggette ai campi elettrici; infatti, applicando un campo
elettrico ad un CL, le sue molecole si orientano tutte parallelamente
al campo stesso.

Tutte queste caratteristiche dei CL sono state sfruttate, a partire


dal 1968, per la realizzazione dei visori a cristalli liquidi (LCD);
la Figura 2 illustra il principio di funzionamento di una cella LCD.

242
Figura 2 - Cella LCD

In Figura 2a vediamo quello che succede nella cella in assenza di


campo elettrico; in tal caso le molecole di CL tendono ad orientarsi
secondo una direzione che possiamo supporre casuale. Per alterare
questa situazione vengono utilizzati due strati di allineamento che,
oltre a permettere il passaggio della luce, presentano pure una
superficie interna aderente per le stesse molecole di CL.
La superficie interna dello strato di allineamento superiore è
microscopicamente corrugata in direzione nord sud, mentre la
superficie interna dello strato di allineamento inferiore è
microscopicamente corrugata in direzione est ovest; di conseguenza,
le molecole che stanno in alto si dispongono in direzione nord sud,
quelle che stanno in basso si dispongono in direzione est ovest,
mentre quelle che stanno in mezzo si dispongono secondo direzioni
intermedie tra nord sud e est ovest.
Una sorgente di luce che arriva dall'alto (dall'interno dello
schermo) incontra un filtro polarizzatore progettato per polarizzare
la luce stessa secondo la direzione nord sud; la luce così
polarizzata penetra nella cella e, a causa della disposizione
elicoidale delle molecole, subisce una rotazione di 90°
(polarizzazione in direzione est ovest).
Il secondo filtro polarizzatore in basso, può essere attraversato
solo dalla luce polarizzata in direzione est ovest; questo è proprio
il caso di Figura 2a, per cui l'utente che osserva davanti al monitor
vedrà un puntino illuminato!

In Figura 2b vediamo quello che succede in presenza di un campo


elettrico; in tal caso, come è stato già anticipato, le molecole di
243
CL si dispongono parallelamente alle linee di forza del campo stesso.
La sorgente di luce che arriva dall'alto, polarizzata in direzione
nord sud, questa volta non subisce nessuna rotazione e non può quindi
attraversare il filtro polarizzatore inferiore; l'utente che osserva
davanti al monitor vedrà un puntino nero!

Nei monitor LCD monocromatici si varia l'intensità della sorgente di


luce bianca in modo da ottenere le varie tonalità di grigio; nei
monitor LCD a colori si utilizzano dei filtri che scompongono la luce
bianca nelle tre componenti primarie RGB.

Considerando il fatto che durante il normale funzionamento i pixel


dello schermo sono quasi tutti accesi, si può facilmente intuire per
quale motivo sia stato associato il caso di Figura 2a all'assenza di
campo elettrico; infatti, in questo modo si ottiene un enorme
risparmio di energia elettrica (e ciò spiega anche il bassissimo
consumo di energia elettrica dei monitor LCD rispetto ai monitor
CRT)!

Il concetto di vertical refresh rimane valido anche per i monitor


LCD; infatti, anche se in questo caso non esiste il problema della
"decadenza luminosa" dei fosfori, bisogna ricordare che il contenuto
dello schermo può variare molto rapidamente (ad esempio, nei
videogiochi) per cui è necessario un continuo aggiornamento.

10.2 Supporto grafico fornito dagli adattatori CGA

Il CGA è stato il primo adattatore video standard della IBM capace di


fornire il supporto per tre modalità grafiche differenti; la Figura 3
mostra tutti i dettagli.

Figura 3 - Supporto grafico dell'adattatore CGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
04h/05h B8000h 320x200 40x25 1 APA 64 4
06h B8000h 640x200 80x25 1 APA 64 2

Queste tre modalità grafiche vengono definite interlacciate; tale


nome è dovuto al fatto che l'hardware della scheda video, nel
disegnare una schermata, traccia per prime tutte le scan line di
indice pari e subito dopo quelle di indice dispari.
Lo scopo di questa tecnica è quello di ridurre il fenomeno dello
sfarfallio causato dalla lentezza, da parte dell'hardware, nel
ridisegnare lo schermo; in effetti, la modalità grafica CGA viene
ricordata principalmente per tale serio problema.
Tutte le scan line di indice pari sono accessibili a partire
dall'indirizzo logico B800h:0000h, mentre quelle di indice dispari
sono accessibili a partire dall'indirizzo logico BA00h:0000h; come si
può notare, ai tempi del CGA non si utilizzava ancora il frame buffer
all'indirizzo A000h:0000h dedicato alle modalità grafiche.

Nelle modalità 04h e 05h a 4 colori, vengono utilizzati 2 bit per

244
ogni pixel (con 2 bit possiamo rappresentare 22=4 colori); ogni byte
del vettore della VRAM memorizza quindi le informazioni relative a
8/2=4 pixel.
Nella modalità 06h a 2 colori, viene utilizzato 1 bit per ogni pixel
(con 1 bit possiamo rappresentare 21=2 colori); ogni byte del vettore
della VRAM memorizza quindi le informazioni relative a 8/1=8 pixel.

La Figura 4 mostra un esempio relativo ai primi due byte della VRAM


in modalità 320x200 a 4 colori; in questo caso supponiamo che i
colori siano rappresentati come: 00b nero, 01b rosso, 10b verde, 11b
blu.

Figura 4 - Accesso ai pixel nel modo CGA 320 x 200 a 4 colori

Come si può notare, la figura mostra l'angolo in alto a sinistra


dello schermo e cioè, la parte iniziale della scan line di indice 0
accessibile a partire dall'indirizzo logico B800h:0000h; in base a
quanto è stato spiegato in precedenza, le informazioni relative ai
primi 8 pixel si trovano memorizzate nei primi 8/4=2 byte della VRAM.
Trattandosi di una risoluzione grafica 320x200, ogni scan line è
formata da 320 pixel ed occupa quindi nella VRAM un blocco da
320/4=80 byte (0050h byte); le varie scan line di indice pari (0, 2,
4, 6, 8, ...) partiranno quindi dagli indirizzi logici B800h:0000h,
B800h:0050h, B800h:00A0h, B800h:00F0h, ...
Per le scan line di indice dispari (1, 3, 5, 7, ...) il discorso è
del tutto analogo; tali scan line partiranno quindi dagli indirizzi
logici BA00h:0000h, BA00h:0050h, BA00h:00A0h, BA00h:00F0h, ...

In base a quanto è stato appena esposto, se volessimo riempire tutto


lo schermo con il colore 10b per le 100 scan line di indice pari e
con il colore 01b per le 100 scan line di indice dispari, il
procedimento da seguire sarebbe molto semplice; osservando che un
blocco da 100 scan line è formato da 320x100=32000 pixel, pari a
32000/4=8000 byte, possiamo scrivere:

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 04h ; AL = video grafico 320x200 a 4 colori
int 10h ; chiama i servizi video del BIOS
245
mov ax, 0B800h ; AX = Seg frame buffer scan line pari
mov es, ax ; copia in ES
xor di, di ; DI = Offset frame buffer
mov al, 10101010b ; AL = 4 pixel di colore 10b
mov cx, 8000 ; CX = 8000 byte da scrivere
cld ; DF = 0 (incremento puntatori)
rep stosb ; trasferisce in VRAM

mov ax, 0BA00h ; AX = Seg frame buffer scan line


dispari
mov es, ax ; copia in ES
xor di, di ; DI = Offset frame buffer
mov al, 01010101b ; AL = 4 pixel di colore 01b
mov cx, 8000 ; CX = 8000 byte da scrivere
cld ; DF = 0 (incremento puntatori)
rep stosb ; trasferisce in VRAM

call waitChar ; attende la pressione di un tasto

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 03h ; AL = video testo 80x25 a 16 colori
int 10h ; chiama i servizi video del BIOS

Ovviamente, per velocizzare le operazioni possiamo osservare che 8000


byte equivalgono a 2000 doubleword per cui, dopo aver caricato i
colori dei pixel in EAX, possiamo porre CX=2000 e sostituire STOSB
con STOSD.

Ben diverso è il discorso nel momento in cui vogliamo accedere in I/O


ad un determinato pixel dello schermo; come risulta evidente dalla
Figura 4, il procedimento da seguire è piuttosto contorto e ciò porta
ad inevitabili ripercussioni sulle prestazioni dei programmi.

Prima di tutto dobbiamo individuare il BYTE della VRAM contenente le


informazioni relative al pixel che ci interessa; a tale proposito,
osserviamo subito che, se ogni pixel occupasse 1 byte nella VRAM,
potremmo calcolare facilmente la posizione (byte_index) del BYTE in
cui si trova il pixel di coordinate x, y (colonna, riga) con la
formula:

byte_index = (y * 320) + x

In realtà sappiamo che ogni BYTE della VRAM contiene le informazioni


relative a 4 pixel per cui, sia il valore 320, sia la coordinata x,
devono essere divisi per 4; possiamo porre allora 320/4=80 e xb=x/4
(divisioni intere).
Analogamente, le 200 scan line sono divise in due gruppi, per cui la
coordinata y deve essere divisa per 2; possiamo porre allora yb=y/2
(divisione intera).
A questo punto possiamo scrivere:

byte_index = (yb * 80) + xb = ((y / 2) * 80) + (x / 4)

La divisione intera x/4 è molto importante in quanto il suo resto è


sempre un valore intero compreso tra 0 e 3 (ricordiamo che il resto è
246
sempre inferiore al divisore); tale valore rappresenta la posizione,
all'interno del BYTE di indice byte_index, della coppia di bit
relativi al pixel a cui vogliamo accedere.
Nel caso, ad esempio, del pixel di coordinate x=6, y=2, si ottiene
6/4=1 con resto 2; quindi, le informazioni relative al nostro pixel
si trovano nella terza coppia (indice 2) di bit del BYTE di indice
byte_index della VRAM.
Una volta individuate queste informazioni, dobbiamo isolare la coppia
di bit a cui vogliamo accedere in I/O; questa operazione, ovviamente,
deve essere tale da non influenzare i pixel adiacenti.
Supponendo di voler accedere in scrittura ad un determinato pixel
dello schermo, possiamo scrivere allora la seguente procedura in
stile C:

; void put_pixel(int x, int y, int color)

put_pixel:

%define x [bp+4] ; colonna pixel


%define y [bp+6] ; riga pixel
%define color [bp+8] ; colore pixel

push bp ; preserva il vecchio bp


mov bp, sp ; ss:bp = ss:sp

mov ax, y ; AX = riga pixel


shr ax, 1 ; divide y per 2
mov bl, 80 ; 80 byte per scan line
mul bl ; AX = AL * BL
mov di, ax ; salva AX in DI
mov ax, x ; AX = colonna pixel
xor dx, dx ; DX = 0 per la divisione
mov bx, 4 ; BX = divisore
div bx ; AX = quoziente, DX = resto
add di, ax ; DI = ((y/2)*80)+(x/4)
mov ax, color ; AX = colore pixel
mov cl, 3 ; CL = max resto di x/4
sub cl, dl ; CL = posizione coppia di bit
shl cl, 1 ; moltiplica CL per 2
shl al, cl ; posizionamento coppia di bit
mov dl, 11111100b ; DL = maschera di bit
rol dl, cl ; posizionamento coppia 00b
mov bx, 0B800h ; assume y pari
test byte y, 00000001b ; verifica se y è pari
jz scrivi_pixel ; y è pari
mov bx, 0BA00h ; y è dispari
scrivi_pixel:
mov es, bx ; ES = Seg frame buffer
and [es:di], dl ; azzera i 2 bit da modificare
or [es:di], al ; scrive il pixel

pop bp ; ripristino vecchio bp


retn ; NEAR return

Dopo aver calcolato il valore ((y/2)*80)+(x/4), utilizziamo il resto


di x/4 per determinare la posizione, all'interno del BYTE di indice
byte_index, della coppia di bit che rappresenta il pixel che ci
247
interessa; tale resto deve essere quindi moltiplicato per 2 in modo
da ottenere la posizione effettiva dei due bit da modificare.
Osservando la Figura 4 possiamo notare che nei byte della VRAM le
varie coppie di bit sono ordinate da destra verso sinistra, mentre
sullo schermo i pixel corrispondenti risultano disposti da sinistra
verso destra; per tenere conto di tale aspetto basta sottrarre il
resto di x/4 da 3 (massimo valore possibile per il resto). Il
risultato così ottenuto viene memorizzato in CL; lo stesso CL viene
usato in combinazione con SHL per posizionare correttamente la coppia
di bit del parametro colore memorizzato in AL.
Prima di procedere con la modifica del pixel dobbiamo stabilire se ci
troviamo su una scan line pari o dispari; a tale proposito, basta
controllare se il bit meno significativo della coordinata y vale 0
(scan line pari) o 1 (scan line dispari).
La modifica del pixel è preceduta dall'azzeramento dei due suoi bit;
a tale proposito, utilizziamo l'istruzione ROL in combinazione con CL
sul valore DL=11111100b. Lo stesso DL può essere così usato con
l'istruzione AND per azzerare i soli due bit che ci interessano; a
questo punto, l'istruzione OR scrive il colore (AL) del pixel da
modificare.

Per velocizzare le operazioni possiamo subito osservare che la


divisione x/4 consiste nel far scorrere di due posti verso destra i
bit di x; come sappiamo, i due bit che traboccano da destra
rappresentano il resto della stessa divisione x/4. Prima di applicare
l'istruzione SHR a x dobbiamo quindi memorizzare il relativo resto; a
tale proposito, basta copiare in una apposita locazione i due bit
meno significativi di x.
Per quanto riguarda il prodotto yb*80 possiamo notare che 80=24+26 per
cui possiamo scrivere simbolicamente:

yb * 80 = yb * (24 + 26) = (yb * 24) + (yb * 26) = (SHL yb, 4) + (SHL yb, 6)

Tutte le considerazioni appena esposte si applicano in modo molto


simile per la modalità video CGA 06h; a tale proposito, bisogna
ricordare che questa volta viene utilizzato 1 solo bit per ogni pixel
per cui ciascun byte della VRAM memorizza le informazioni relative a
8/1=8 pixel.

10.3 Supporto grafico fornito dagli adattatori EGA e VGA

Successivamente al CGA, la IBM ha introdotto gli adattatori EGA e


VGA; la Figura 5 mostra tutti i dettagli.

Figura 5 - Supporto grafico degli adattatori EGA e VGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
0Dh A0000h 320x200 40x25 8 APA 64 16
0Eh A0000h 640x200 80x25 4 APA 64 16
0Fh A0000h 640x350 80x25 1 APA 64 2
0Fh A0000h 640x350 80x25 2 APA 128 2
10h A0000h 640x350 80x25 1 APA 64 4
248
10h A0000h 640x350 80x25 1 APA 128 16
10h A0000h 640x350 80x25 2 APA 256 16
11h A0000h 640x480 80x30 1 APA 256 2
12h A0000h 640x480 80x30 1 APA 256 16

La modalità video principale introdotta dall'adattatore EGA è la 10h


caratterizzata da una risoluzione di 640x350 pixel a 16 colori;
analogamente, la modalità video principale introdotta dall'adattatore
VGA è la 12h caratterizzata da una risoluzione di 640x480 pixel a 16
colori. Nel seguito ci occuperemo quindi di queste due modalità.

Gli adattatori EGA e VGA presentano, da un punto di vista tecnico,


caratteristiche molto simili; in particolare, la principale analogia
riguarda l'utilizzo dei cosiddetti bit planes (piani di bit) per la
memorizzazione dei colori dei pixel.
Per capire il perché dell'utilizzo di tale tecnica osserviamo che, ad
esempio, nel caso della modalità 10h abbiamo una risoluzione di
640x350=224000 pixel, ciascuno dei quali può assumere uno tra 16
possibili colori; per rappresentare ogni pixel sono quindi necessari
4 bit (24=16) per cui ogni byte della VRAM contiene le informazioni
relative a 8/4=2 pixel.
Se venisse utilizzata allora la tecnica di Figura 4, ogni schermata
640x350 richiederebbe 224000/2=112000 byte di memoria; questo valore
è nettamente maggiore della dimensione di un segmento di memoria e
ciò costringerebbe l'utente a destreggiarsi tra diversi blocchi da 64
Kb per gestire una singola schermata.
Il discorso appena svolto vale, a maggior ragione, per la modalità
12h con risoluzione di 640x480=307200 pixel; per risolvere il
problema è stata allora adottata la tecnica illustrata in Figura 6.

Figura 6 - Accesso ai pixel nei modi EGA e VGA a 16 colori

In sostanza, una schermata risulta distribuita su 4 piani


sovrapposti, indicizzati con i valori 0, 1, 2, 3; il colore del pixel
249
di coordinate x, y sullo schermo viene ottenuto combinando i 4 bit
che si trovano alle coordinate x, y di ciascun piano.
Supponendo di assegnare al colore verde il valore 0010b, possiamo
osservare in Figura 6 che al pixel di coordinate 3, 0 sullo schermo
corrisponde il bit (0) di coordinate 3, 0 sul Piano 0, il bit (1) di
coordinate 3, 0 sul Piano 1, il bit (0) di coordinate 3, 0 sul Piano
2 e il bit (0) di coordinate 3, 0 sul Piano 3; unendo questi 4 bit si
ottiene, appunto, 0010b.
Nel caso del modo VGA, ciascuno dei piani sovrapposti è costituito da
640x480=307200 bit ed occupa quindi 307200/8=38400 byte di memoria;
in questo modo si evita che una singola schermata si trovi
distribuita su due o più blocchi da 64 Kb, cosa che costringerebbe
l'utente a dover saltare da un blocco all'altro!

I 4 piani di bit, essendo sovrapposti, risultano tutti accessibili a


partire dallo stesso indirizzo logico; per gli adattatori EGA e VGA
tale indirizzo è A0000h:0000h.
In Figura 6 vediamo (in alto a sinistra) la parte iniziale della VRAM
con il primo elemento del vettore che è costituito da 4 byte
sovrapposti per la memorizzazione di 8 pixel; si tratta chiaramente
dei primi 8 pixel visualizzati nell'angolo in alto a sinistra dello
schermo.
Il programmatore può selezionare il bit plane desiderato attraverso
apposite porte hardware dell'adattatore; per illustrare il
procedimento generale da seguire, analizziamo un esempio pratico
riferito alla modalità 12h da 640x480 pixel.
Osserviamo subito che, in questo caso, ognuna delle 480 scan line è
formata da 640 pixel e quindi, ogni bit plane risulterà a sua volta
composto da 480 righe da 640 bit ciascuna, pari a 640/8=80 byte; per
ottenere l'indice (byte_index) del byte della VRAM contenente il
pixel di coordinate x, y dobbiamo allora calcolare:

byte_index = (y * 80) + (x / 8)

In analogia al caso dell'adattatore CGA, il resto della divisione


intera x/8 (che è sempre un numero intero compreso tra 0 e 7) ci
fornisce la posizione, all'interno del byte di indice byte_index, del
pixel a cui vogliamo accedere; come al solito, tale posizione è
intesa a partire dal bit più significativo a causa del fatto che,
come si vede in Figura 6, i bit nella VRAM sono ordinati da destra
verso sinistra, mentre i corrispondenti pixel sullo schermo sono
ordinati da sinistra verso destra.
Una volta ottenute queste informazioni, possiamo procedere con
l'accesso alla VRAM; a tale proposito, come è stato già anticipato,
dobbiamo programmare apposite porte hardware dell'adattatore.
Prima di tutto dobbiamo accedere alla porta 03CEh denominata Graphics
Address Register (o GAR); in tale porta dobbiamo scrivere l'indice 8
che ci permette di selezionare il Bit Mask Register (o BMR).
Dopo accediamo alla porta 03CFh denominata BMR Data Area per
specificare in quale posizione (nel byte di indice byte_index) si
trova il pixel a cui vogliamo accedere; ad esempio, se vogliamo
accedere al bit in posizione 3, dobbiamo scrivere in tale porta il
valore 00001000b.
In seguito accediamo alla porta 03C4h denominata Sequencer Address
250
Register (o SAR); in tale porta dobbiamo scrivere l'indice 2 per
selezionare il Map Mask Register (o MMR).
A questo punto, attraverso il MMR, la cui porta è 03C5h, selezioniamo
i bit plane da abilitare; ad esempio, per abilitare tutti i 4 bit
plane dobbiamo scrivere in tale porta il valore 00001111b (ciascuno
dei primi 4 bit rappresenta uno dei bit plane a partire da quello di
indice 0).
Terminata questa fase dobbiamo obbligatoriamente leggere il byte di
indice byte_index della VRAM; questa lettura è molto importante in
quanto obbliga l'adattatore a bloccare (to latch) il byte contenente
il pixel a cui vogliamo accedere.
Il byte appena bloccato deve essere "pulito" scrivendo in esso il
valore 00000000b; subito dopo possiamo finalmente scrivere, nella
porta 03C5h, il nuovo colore a 4 bit.
L'ultimo passaggio consiste nello scrivere il valore 11111111b nel
byte di indice byte_index; tale operazione prende il nome di write
back e permette di sbloccare il byte che abbiamo appena modificato.

Supponendo di voler accedere in scrittura ad un determinato pixel


dello schermo, possiamo scrivere allora la seguente procedura in
stile C:

; void put_pixel(int x, int y, int color)

put_pixel:

%define x [bp+4] ; colonna pixel


%define y [bp+6] ; riga pixel
%define color [bp+8] ; colore pixel

push bp ; preserva il vecchio bp


mov bp, sp ; ss:bp = ss:sp

mov ax, y ; AX = y
shl ax, 4 ; AX = y * 2^4
mov di, ax ; DI = y * 2^4
shl ax, 2 ; AX = y * 2^6
add di, ax ; DI = (y * 2^6) + (y * 2^4)
mov ax, x ; AX = x
shr ax, 3 ; AX = x / 8
add di, ax ; DI = (y * 80) + (x / 8)
mov cx, x ; CX = x
and cl, 00000111b ; CL = resto di (x / 8)

mov dx, 03CEh ; DX = Graphics Address Register


mov al, 08h ; AL = Bit Mask Register
out dx, al ; output to port

mov dx, 03CFh ; DX = BMR Data Area


mov al, 10000000b ; AL = bitmask
shr al, cl ; posiziona il bit di valore 1
out dx, al ; output to port

mov dx, 03C4h ; DX = Sequencer Address Register


mov al, 02h ; AL = Map Mask Register
out dx, al ; output to port

251
mov dx, 03C5h ; DX = Map Mask Register
mov al, 00001111b ; AL = abilita tutti i piani
out dx, al ; output to port

mov ax, 0A000h ; AX = Seg framebuffer


mov es, ax ; copia in ES
mov al, [es:di] ; latch video byte
mov [es:di], byte 0 ; pulizia pixel
mov al, color ; AL = colore pixel
out dx, al ; output to port
mov [es:di], byte 11111111b ; write back

pop bp ; ripristino vecchio bp


retn ; NEAR return

In questo esempio utilizziamo vari accorgimenti per velocizzare i


calcoli del tipo y*80, x/8 e così via; si può anche notare che, per
calcolare il resto di x/8, carichiamo lo stesso x in CX e isoliamo i
3 bit meno significativi.

Nota importante.
Gli adattatori EGA e VGA dispongono di numerose altre porte
hardware attraverso le quali è possibile gestire diverse
funzionalità come, ad esempio, l'ampiezza orizzontale delle
scan line, il numero di scan line, la polarità orizzontale e
verticale dello schermo, etc; è chiaro quindi che un uso
scorretto di tali porte può anche provocare danni
all'hardware, soprattutto nel caso di vecchi monitor privi di
adeguati circuiti di protezione!

10.5 Supporto grafico fornito dagli adattatori MCGA

Dopo l'uscita dell'EGA e prima dell'avvento del VGA, la IBM ha


introdotto un particolare modello di adattatore denominato MCGA; la
Figura 7 mostra tutti i dettagli relativi alle modalità grafiche
supportate da tale adattatore.

Figura 7 - Supporto grafico dell'adattatore MCGA


Codice Frame Risoluz. Risoluz. Pagine Mem. tot. Numero
Tipo Modo
(AL) Buffer (pixel) (celle) video (Kb) colori
11h A0000h 640x480 80x30 1 APA 256 2
13h A0000h 320x200 40x25 1 APA 256 256

La modalità video principale introdotta dall'adattatore MCGA è la 13h


caratterizzata da una risoluzione di 320x200 pixel a 256 colori; nel
seguito ci occuperemo quindi di tale modalità.

La caratteristica fondamentale dell'adattatore MCGA è l'estrema


semplicità del metodo adottato per la memorizzazione dei pixel nella
VRAM; tale semplicità è legata al fatto che ciascun pixel può
assumere uno tra 256 differenti colori e richiede quindi per la sua
rappresentazione 8 bit (28=256), pari a 1 byte.

252
La conseguenza è che ogni byte della VRAM rappresenta 1 pixel sullo
schermo; i 320x200=64000 pixel di una schermata MCGA vengono gestiti
allora nella VRAM sotto forma di vettore lineare da 64000 byte.
Tale dimensione è inferiore ai 64 Kb di un segmento di memoria e
comporta quindi una gestione estremamente semplice ed efficiente;
proprio questo aspetto ha dato alla modalità 13h una enorme
popolarità ai tempi del DOS tra i programmatori di videogiochi.

La Figura 8 illustra i concetti appena esposti; in tale figura


vediamo i primi 3 byte della VRAM associati ai primi 3 pixel
visualizzati nell'angolo in alto a sinistra dello schermo.

Figura 8 - Accesso ai pixel nel modo MCGA a 256 colori

Come possiamo notare, il primo pixel si trova all'indirizzo


A000h:0000h della VRAM, il secondo pixel si trova all'indirizzo
A000h:0001h della VRAM e così via; tenuto conto allora della
risoluzione 320x200, per accedere al byte della VRAM contenente il
pixel di coordinate x, y basta calcolare semplicemente:

byte_index = (y * 320) + x

Considerando poi il fatto che 320=26+28, possiamo anche scrivere:

byte_index = (y * (26 + 28)) + x = ((y * 26) + (y * 28)) + x = ((SHL y, 6) +


(SHL y, 8)) + x

La procedura per la scrittura di un pixel si semplifica quindi in


questo modo:

; void put_pixel(int x, int y, int color)

put_pixel:

%define x [bp+4] ; colonna pixel


%define y [bp+6] ; riga pixel
%define color [bp+8] ; colore pixel

253
push bp ; preserva il vecchio bp
mov bp, sp ; ss:bp = ss:sp

mov ax, y ; AX = y
shl ax, 6 ; AX = y * 2^6
mov di, ax ; DI = y * 2^6
shl ax, 2 ; AX = y * 2^8
add di, ax ; DI = (y * 2^6) + (y * 2^8)
add di, x ; DI = (y * 320) + x

mov ax, 0A000h ; AX = Seg framebuffer


mov es, ax ; copia in ES
mov al, color ; AL = colore pixel
mov [es:di], al ; scrittura pixel

pop bp ; ripristino vecchio bp


retn ; NEAR return

La situazione è altrettanto semplice nel momento in cui vogliamo


riempire una intera schermata con un determinato colore (ad esempio,
10001111b); in tal caso, dopo aver caricato 4 byte di colore in EAX e
dopo aver posto CX=64000/4 (doubleword) e ES:DI=A000h:0000h, possiamo
eseguire l'istruzione:

rep stosd

10.6 La tavolozza dei colori

In base a quanto è stato esposto in questo capitolo, si potrebbe


supporre che i gruppi di bit della VRAM assegnati a ciascun pixel
rappresentino in modo diretto il colore del pixel stesso; in molti
casi (come vedremo nel capitolo successivo) ciò può essere vero ma,
per certi adattatori video, tali bit codificano in realtà l'indice
relativo all'elemento di un vettore che contiene l'effettivo colore
del pixel.
Un caso emblematico è quello dell'adattatore VGA dove ogni gruppo di
bit della VRAM contiene un indice relativo ad uno dei 256 elementi di
un vettore di colori; ciascun elemento assume la struttura mostrata
in Figura 9.

Figura 9 - Colore RGB dell'adattatore VGA

I 3 byte sono ordinati da sinistra verso destra; di conseguenza, RED


è il primo byte, GREEN è il secondo, BLUE è il terzo.
Come si può notare, solo i primi 6 bit di ogni byte vengono usati per
codificare l'intensità delle componenti primarie Red, Green, Blue;
con questo sistema è possibile rappresentare un totale di:

26 * 26 * 26 = 26 + 6 + 6 = 218 = 262144 colori

254
Questi 262144 colori rappresentano la cosiddetta palette (tavolozza
dei colori); come abbiamo visto in precedenza, a seconda del modo
video che si sta utilizzando, risultano disponibili tutti o parte dei
256 elementi del vettore dei colori.
Nel modo 12h sono disponibili solo i primi 16 elementi del vettore
dei colori; nel modo 13h sono disponibili tutti i 256 elementi del
vettore dei colori.

Tale vettore risulta accessibile attraverso le porte hardware 03C7h,


03C8h e 03C9h; la porta 03C7h (PEL Read Register) permette di
specificare l'indice dell'elemento da leggere, la porta 03C8h (PEL
Write Register) permette di specificare l'indice dell'elemento da
scrivere, la porta 03C9h (PEL Data Register) permette di effettuare
materialmente la lettura o la scrittura dei dati (la sigla PEL sta
per Picture ELement).
Ogni volta che vengono letti o scritti i 3 byte di un PEL attraverso
la porta 03C9h, si verifica automaticamente l'incremento di 3 (byte)
del puntatore al vettore dei colori; ciò ci permette di leggere o
scrivere l'intero vettore con un'unica istruzione Assembly.

10.7 Esempi pratici

Analizziamo ora alcuni esempi pratici riferiti al modo 13h


(risoluzione 320x200 a 256 colori); come è stato spiegato in
precedenza, questo modo video è estremamente semplice da programmare
e si presta quindi in modo particolare per esperimenti di tipo
grafico.

10.7.1 Rotazione dei colori nel modo 13h

Il primo esempio mostra come visualizzare tutti i 256 colori della


modalità 13h e come scambiarli di posto in modo da ottenere un
effetto "rotazione"; la Figura 10 mostra il listato del programma
ROTCOLOR.ASM.

Figura 10 - File ROTCOLOR.ASM

Questo programma visualizza 256 rettangoli distribuiti su 8 righe e


32 colonne; ciascuno dei rettangoli viene poi riempito con uno dei
256 colori disponibili.
Successivamente, viene attivato un loop all'interno del quale i 256
colori dell'adattatore vengono ruotati di una posizione verso destra
ad ogni iterazione; a tale proposito, si utilizza un vettore di 256
terne RGB che viene continuamente sottoposto ad una sorta di ROR.

Nei veri ambienti DOS (ma anche nella DOS Box di Windows) il
programma di Figura 10 può produrre qualche effetto indesiderato; in
particolare, si può notare un effetto di alterazione dei colori.
Come è stato spiegato nella sezione Assembly Base, ciò è dovuto
all'uso massiccio di istruzioni INS e OUTS in combinazione con il
prefisso REP; questa situazione è problematica in quanto molte porte
hardware richiedono che, tra una operazione di I/O e quella
successiva, debba trascorrere un opportuno intervallo di tempo.
255
Per risolvere questo problema conviene posizionare le istruzioni INS
e OUTS all'interno di un normale loop; eventualmente, nello stesso
loop si possono inserire anche istruzioni destinate a creare dei
ritardi di tempo.

Un altro problema del programma di Figura 10 può essere riscontrato


sui monitor CRT collegati a PC poco potenti; in tal caso si nota un
evidente fenomeno di sfarfallio dello schermo.
Per risolvere tale problema si può ricorrere alla porta 03DAh
denominata Input Status Register; il bit 3 del byte letto da tale
porta vale 1 solo quando è in atto un vertical retrace.
Di conseguenza, possiamo sincronizzarci facilmente con lo stesso
vertical retrace attraverso il seguente codice che deve essere
posizionato prima dell'istruzione OUTSB che aggiorna i colori appena
ruotati:

mov dx, 03DAh ; DX = Input Status Register


wait_vr:
in al, dx ; lettura porta
test al, 00001000b ; test sul bit 3
jnz wait_vr ; attesa fine vertical retrace
wait_hr:
in al, dx ; lettura porta
test al, 00001000b ; test sul bit 3
jz wait_hr ; attesa inizio vertical retrace

In sostanza, se il bit 3 vale 1 significa che è in atto un vertical


retrace; in questo caso ci conviene aspettare che il vertical retrace
abbia termine.
Se il bit 3 vale 0 significa che il vertical retrace è appena
terminato e il pennello elettronico si sta riposizionando nell'angolo
in alto a sinistra (rispetto all'utente); in tal caso ci conviene
attendere l'inizio del successivo vertical retrace in modo da poter
sincronizzare il nostro output con il ridisegno dello schermo.

10.7.2 Visualizzazione di stringhe in modalità grafica

Nel precedente capitolo è stato spiegato che, in modalità testo,


sullo schermo possiamo visualizzare esclusivamente simboli
appartenenti al set di codici ASCII; di conseguenza, eventuali
informazioni numeriche da mostrare sul monitor devono essere prima
convertite in forma di stringhe.
Nel caso poi della modalità grafica, siamo anche costretti a crearci
da zero i simboli (font) necessari per visualizzare le stringhe; a
tale proposito, il metodo più semplice consiste nello sfruttare
appositi font di tipo bitmap memorizzati nella ROM BIOS della scheda
video.
Analizziamo, in particolare, il seguente servizio della INT 10h
riservato agli adattatori EGA, MCGA e VGA:

Video BIOS - Servizio n. 1130h - Get Font Information

Argomenti richiesti:
AX = 1130h (Get Font Information)

256
BH = pointer specifier
00h = INT 1Fh pointer
01h = INT 43h pointer
02h = ROM 8x14 character font pointer
03h = ROM 8x8 double dot font pointer (da 0 a 127)
04h = ROM 8x8 double dot font pointer (da 128 a
255)
05h = ROM alpha alternate 9x14 pointer
06h = ROM 8x16 font (MCGA, VGA)
07h = ROM alternate 8x16 font (VGA)

Valori restituiti:
ES:BP = puntatore al vettore delle bitmap
CX = numero di byte per carattere
DL = numero di (righe - 1) sullo schermo

Selezionando, ad esempio, BH=06h, otteniamo in ES:BP l'indirizzo di


un vettore di bitmap 8x16 che possiamo utilizzare per visualizzare
caratteri in modo grafico; la Figura 11 mostra un esempio pratico che
stampa sullo schermo la lettera 'R' maiuscola (o uno qualunque dei
256 simboli a scelta del programmatore).

Figura 11 - File FONTBMP.ASM

Osserviamo che il codice ASCII della lettera 'R' è 82; quindi, la


bitmap relativa alla lettera 'R' sarà quella di indice 82 nel vettore
puntato da ES:BP.
Ogni bitmap 8x16 occupa 16 byte per cui, l'offset all'interno del
vettore sarà: 82*16=1312; la bitmap che ci interessa si viene a
trovare quindi all'indirizzo logico ES:BP+1312.

La visualizzazione della bitmap avviene con il solito loop interno


innestato in un loop esterno; ad ogni iterazione del loop interno
viene stampata una linea della bitmap (per l'esattezza, vengono
stampati solo i pixel in corrispondenza dei bit che valgono 1).

Bibliografia

Cristopher, Feigenbaum, Saliga - MS-DOS MANUALE DI PROGRAMMAZIONE -


Mc Graw Hill

VGABIOS.TXT - Elenco servizi Video BIOS della INT 10h


(disponibile nella sezione Downloads - Documentazione -
vgabios.tar.bz2)

VGAREGS.TXT - Programmazione dei registri VGA


(disponibile nella sezione Downloads - Documentazione -
vgaregs.tar.bz2)

257
Capitolo 11 La memoria video in modalità
grafica VESA
Nel 1987, mentre la IBM studiava le possibili evoluzioni
dell'adattatore VGA, una azienda di nome NEC Home Electronics suscitò
grande clamore mettendo in commercio una potente scheda video per PC
capace di supportare una risoluzione grafica da 800x600 pixel a 256
colori; per sottolineare il notevole balzo in avanti fatto rispetto
al modo VGA, questa nuova modalità video venne denominata Super VGA o
SVGA.
La IBM cercò di replicare a questa mossa imprevista e realizzò
l'adattatore 8514/A capace di supportare risoluzioni grafiche sino a
1024x768 pixel a 256 colori; forse però a causa della quasi totale
assenza di adeguata documentazione sulle specifiche hardware, questo
nuovo adattatore ottenne uno scarso successo.
Stessa sorte toccò nel 1990 ad un ancora più potente adattatore
denominato eXtended Graphics Array o XGA; questo adattatore era
capace di supportare risoluzioni grafiche da 1024x768 pixel a 256
colori e da 640x480 pixel a ben 65536 colori!

Nel frattempo, l'iniziativa della NEC era stata seguita rapidamente


da diversi altri produttori di hardware i quali misero in commercio
schede video di classe SVGA con caratteristiche sempre più evolute;
la fine del dominio IBM nel settore degli adattatori grafici era
ormai iniziata in modo irreversibile!

Analizzando ciò che è diventato oggi il mondo delle schede video,


possiamo affermare che la fine del dominio IBM ha avuto enormi
conseguenze positive in quanto i produttori di hardware si sono
potuti svincolare da standard spesso troppo restrittivi; in questo
modo è stato possibile dare libero sfogo alla creatività dei
progettisti determinando così una evoluzione vertiginosa del settore
della grafica computerizzata.
In questa fase di evoluzione, una tappa fondamentale è stata la
nascita dell'azienda 3Dfx, fondata nel 1994 da vari colossi della
computer grafica come Digital Equipment Corporation, Silicon
Graphics, MIPS Computer Systems e Pellucid; l'obiettivo era quello di
portare anche nel mondo dei PC il concetto di accelerazione grafica,
sino ad allora riservato solo ad ambienti professionali.
Per raggiungere tale obiettivo la 3Dfx mise in commercio schede video
dotate di una novità rivoluzionaria rappresentata dalla GPU o
Graphics Processing Unit; la GPU determina un enorme aumento delle
prestazioni generali del sistema in quanto si fa carico di tutto il
pesantissimo lavoro di elaborazione grafica che altrimenti andrebbe a
gravare sulla CPU.

L'effetto negativo legato alla fine del dominio IBM è stato


sicuramente il caos che si è venuto a creare in relazione alle
specifiche hardware delle nuove schede video; in particolare, i vari
produttori hanno abbandonato ogni convenzione in relazione alla
codifica delle modalità video e degli indirizzi hardware dei
registri.
Questa situazione ha avuto pesanti ripercussioni sui programmatori i
258
quali si sono trovati costretti a destreggiarsi tra una miriade di
codici e indirizzi che differivano da una scheda video all'altra; a
titolo di esempio, la Figura 1 illustra i differenti codici
utilizzati dai vari produttori di schede video per indicare la
modalità 1024x768 a 16 colori.

Figura 1 - Codifiche modo 1024x768 a 16 colori


Scheda video Chipset Codice
ATI VGA Wonder ATI Old 65h
ATI VGA Wonder+ ATI New 55h
Diamond 24x Paradise 5Dh
Genoa 5400 Tseng 3000 37h
Realtek RTVGA Realtek 21h
Primus P2000 GA Primus 30h
OAK OAK 56h
Hi Res 512 Zymos 5Fh

Da un lato, non avrebbe avuto senso imporre delle convenzioni


univoche su aspetti legati all'hardware in quanto così facendo si
sarebbe tornati indietro ai tempi del dominio IBM; d'altra parte,
bisognava necessariamente trovare una qualche soluzione per venire
incontro alle esigenze dei programmatori senza limitare in alcun modo
la creatività dei progettisti.
Fortunatamente, gli stessi produttori di schede video si resero conto
della situazione e si riunirono per dare vita ad un consorzio
denominato Video Electronics Standards Association o VESA; lo scopo
che si prefiggeva tale consorzio era proprio quello di mettere a
disposizione degli sviluppatori una interfaccia di programmazione
standard capace di semplificare al massimo l'accesso a tutte le
caratteristiche di qualsiasi scheda video dotata di supporto VESA.

11.1 Le specifiche VESA VGA BIOS Extension

Una delle attività più importanti svolte dal comitato VESA è la


definizione e l'aggiornamento continuo delle specifiche VGA BIOS
Extension o VBE; scopo di tali specifiche è quello di estendere i
servizi video forniti dal BIOS attraverso la INT 10h in modo da
supportare tutte le modalità SVGA disponibili con le schede video
aderenti allo standard VESA. Nel seguito del capitolo ci occuperemo
delle specifiche VBE versione 3.0 pubblicate nel 1998 e destinate
alla modalità reale.

Una scheda video VESA compatibile supporta quindi tutti i servizi


standard imposti a suo tempo dalla IBM e, in più, una ulteriore serie
di servizi definiti dalle specifiche VBE; il programmatore accede a
tutti questi servizi attraverso la normalissima procedura di chiamata
della INT 10h.
I vecchi programmi grafici possono continuare a girare senza
modifiche richiedendo i normali servizi standard IBM e ignorando
completamente le specifiche VBE; i nuovi programmi grafici possono
accedere a tutti i servizi VBE abbandonando del tutto i servizi
259
standard IBM.
Le specifiche VBE includono anche i due vecchi servizi 00h (Set Video
Mode) e 0Fh (Read Current Video State); in ogni caso, per i nuovi
programmi che vogliono avvalersi delle specifiche VBE è raccomandato
l'uso dei servizi alternativi 02h (Set Super VGA Video Mode) e 03h
(Read Current Super VGA Video State).

Per distinguere i servizi VBE da quelli ordinari, viene utilizzato il


valore 4Fh che deve essere caricato in AH; la chiamata di un servizio
VBE assume quindi il seguente aspetto generale:

mov ah, 4Fh ; AH = supporto VBE


mov al, codice_servizio ; AL = codice del servizio richiesto
int 10h ; chiama i servizi video del BIOS

Tutte le informazioni di stato (VBE Return Status) relative al


servizio richiesto con il codice precedente vengono restituite in AL
e AH; il contenuto di tali registri assume il seguente significato
espresso con gli operatori booleani del linguaggio C:

AL == 4Fh il servizio richiesto è supportato;


AL != 4Fh il servizio richiesto non è supportato;
AH == 00h il servizio richiesto è stato eseguito correttamente;
AH == 01h il servizio richiesto non è stato eseguito correttamente;
AH == 02h il servizio richiesto non è supportato nella configurazione
hardware corrente;
AH == 03h il servizio richiesto non è supportato nella modalità video
corrente.

Le specifiche VBE raccomandano che un qualunque valore diverso da


zero in AH venga trattato dal programmatore come una condizione
generale di errore; è anche importante testare il valore restituito
in AL in quanto determinati servizi potrebbero essere disponibili
solo con le versioni più recenti delle specifiche VBE.

Per convenzione, tutte le modalità video VESA devono essere


rappresentate da un codice a 16 bit il cui bit di posto 8 deve valere
1; ne consegue che tali codici partono dal valore minimo 0100h.
La modalità video è rappresentata quindi dai primi 9 bit del codice a
16 bit; i restanti 7 bit vengono utilizzati per abilitare o
disabilitare determinate funzionalità.
I bit nelle posizioni 9 e 10 sono riservati e devono valere 0.
Il bit in posizione 11 serve per selezionare la modalità di controllo
della frequenza di scansione verticale del monitor; se questo bit
vale 0 si abilita la modalità predefinita controllata dal BIOS,
mentre se questo bit vale 1 si lascia al programmatore il compito di
effettuare le necessarie impostazioni.
I bit nelle posizioni 12 e 13 sono riservati e devono valere 0.
Il bit in posizione 14 serve per abilitare o disabilitare l'uso del
frame buffer lineare per l'accesso alla VRAM; se questo bit vale 0
viene usato un normale frame buffer che permette di vedere la VRAM
suddivisa in una serie di blocchi di dimensione predefinita
(segmentazione della modalità reale), mentre se questo bit vale 1
viene usato un frame buffer lineare che permette di vedere la VRAM
260
come un unico blocco (modalità protetta).
Il bit in posizione 15 serve per abilitare o disabilitare la pulizia
della VRAM quando viene attivata una nuova modalità video VESA; se
questo bit vale 0 il vecchio contenuto della VRAM viene cancellato,
mentre se questo bit vale 1 il vecchio contenuto della VRAM viene
preservato.

La Figura 2 illustra l'elenco delle modalità video grafiche ufficiali


aggiornate allo standard VESA versione 3.0; la risoluzione è espressa
in pixel.

Figura 2 - Codici modi video grafici VBE 3.0


Codice Risoluzione Colori
0100h 640x400 256
0101h 640x480 256
0102h 800x600 16
0103h 800x600 256
0104h 1024x768 16
0105h 1024x768 256
0106h 1280x1024 16
0107h 1280x1024 256
010Dh 320x200 32768
010Eh 320x200 65536
010Fh 320x200 16777216
0110h 640x480 32768
0111h 640x480 65536
0112h 640x480 16777216
0113h 800x600 32768
0114h 800x600 65536
0115h 800x600 16777216
0116h 1024x768 32768
0117h 1024x768 65536
0118h 1024x768 16777216
0119h 1280x1024 32768
011Ah 1280x1024 65536
011Bh 1280x1024 16777216

La Figura 3 illustra l'elenco delle modalità video testo ufficiali


aggiornate allo standard VESA versione 3.0; la risoluzione è espressa
in celle.

Figura 3 - Codici modi video testo VBE 3.0


Codice Risoluzione Colori
0108h 80x60 256
0109h 132x25 256
010Ah 132x43 16

261
010Bh 132x50 256
010Ch 132x60 16

11.2 Accesso alla VRAM in modalità reale attraverso i


servizi VBE

In modalità reale, l'accesso alla VRAM deve avvenire necessariamente


in modo segmentato; come sappiamo, ciò è dovuto al fatto che in tale
modalità la CPU vede qualunque tipo di memoria suddivisa in tanti
blocchi (segmenti) ciascuno dei quali non può superare la dimensione
di 65536 byte (in quanto i registri puntatori a 16 bit permettono di
esprimere un offset compreso tra 0 e 65535).
Per venire incontro a questa situazione, le specifiche VBE
implementano l'accesso alla VRAM in modalità reale attraverso una
tecnica del tutto simile a quella illustrata nel capitolo dedicato
allo standard EMS; la Figura 4 illustra tutti i dettagli.

Figura 4 - Supporto VBE per l'accesso alla VRAM in modalità reale

Osserviamo subito che la VRAM viene suddivisa in tanti blocchi


consecutivi e contigui denominati memory banks (banchi di memoria),
indicizzati a partire da 0; la dimensione fissa di ogni banco
rappresenta la granularità della VRAM.
Il valore più usato per la granularità è pari a 64 Kb, ma non è raro
trovare schede video con granularità 8 Kb, 16 Kb o 32 Kb;
nell'esempio di Figura 4 la granularità è pari a 16 Kb.

Per l'accesso ai vari banchi di memoria il programmatore ha a


disposizione due apposite finestre (in genere da 64 Kb ciascuna)
denominate Window A e Window B, posizionabili a piacere in qualunque
area della VRAM; la presenza di due finestre (anziché del singolo
frame buffer dello standard EMS) permette di ottimizzare al massimo
le operazioni di I/O con la VRAM grazie al fatto che si può usare, ad

262
esempio, la Window A per la lettura dei dati e la Window B per la
scrittura dei dati.
Questo però è un caso ideale in quanto la maggior parte delle schede
video supportano una singola finestra da utilizzare quindi, sia per
la lettura, sia per la scrittura dei dati; ovviamente, è compito dei
programmi individuare l'esatta configurazione disponibile
(granularità della VRAM, numero di finestre di I/O, dimensione delle
finestre, etc).

Come è stato già spiegato, ogni finestra di I/O può essere


posizionata a piacere in qualunque area della VRAM (cioè, in
corrispondenza di un qualunque banco di memoria); in Figura 4, ad
esempio, vediamo che la Window A (da 64 Kb) è posizionata a partire
dal banco 2 in modo da permettere la lettura diretta dei dati dai
banchi 2, 3, 4, 5, mentre la Window B (da 64 Kb) è posizionata a
partire da banco 9 in modo da permettere la scrittura diretta dei
dati nei banchi 9, 10, 11, 12.
Per essere precisi, ciascuna delle due finestre ha, in realtà, un
indirizzo fisso di partenza; come accade con lo standard EMS, il
programmatore non deve fare altro che mappare in ogni finestra l'area
desiderata della VRAM.

11.3 Servizi VBE 3.0

Analizziamo ora i principali servizi messi a disposizione dalle


specifiche VBE versione 3.0; come è stato già anticipato, prenderemo
in considerazione solamente i servizi destinati alla modalità reale.

11.3.1 Servizio n.4F00h: Return VBE Controller Information

Questo servizio restituisce una numerosa serie di informazioni


relative alla versione VBE supportata dal Video BIOS (VBIOS) e alle
caratteristiche hardware dell'adattatore video.

VBE - Servizio n. 4F00h - Return VBE Controller


Information

Argomenti richiesti:
AX = 4F00h (Return VBE Controller Information)
ES:DI = Puntatore al buffer delle informazioni

Valori restituiti:
AX = VBE Return Status

Un programma che intende accedere correttamente ai servizi VBE


supportati dal BIOS della scheda video, deve prima raccogliere una
serie completa di informazioni relative alle capacità hardware
dell'adattatore; a tale proposito, è disponibile proprio il servizio
4F00h.
Questo servizio richiede che la coppia ES:DI stia puntando ad un
blocco di memoria appositamente allocato dal programmatore; tale
blocco viene riempito dal servizio 4F00h con una numerosa serie di
informazioni che, necessariamente, possono variare a seconda della
versione VBE supportata dal VBIOS.
263
Tutte le informazioni vengono restituite dal servizio 4F00h in una
struttura che prende il nome di VbeInfoBlock; tale struttura assume
le caratteristiche illustrate in Figura 5.

Figura 5 - VbeInfoBlock Structure


STRUC VbeInfoBlock

VbeSignature resb 4 ; stringa di identificazione "VESA"


VbeVersion resw 1 ; versione VBE
OemStringPtr resd 1 ; puntatore FAR alla OEM String
Capabilities resb 4 ; caratteristiche hardware
dell'adattatore
VideoModePtr resd 1 ; puntatore FAR alla lista dei modi
video
TotalMemory resw 1 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
OemSoftwareRev resw 1 ; codice revisione software VBE
OemVendorNamePtr resd 1 ; puntatore FAR alla stringa del
fabbricante
OemProductNamePtr resd 1 ; puntatore FAR alla stringa del
prodotto
OemProductRevPtr resd 1 ; puntatore FAR alla stringa di
revisione
Reserved resb 222 ; riservato VBE
OemData resb 256 ; area dati per le OEM Strings

ENDSTRUC

Il membro VbeSignature viene riempito dal servizio 4F00h con la


stringa "VESA"; lo stesso membro può essere usato dal programmatore
per richiedere esplicitamente informazioni avanzate disponibili solo
a partire dalla versione 2.0 delle specifiche VBE.
Se questo membro non viene inizializzato dal programmatore con
l'apposita stringa "VBE2", il servizio 4F00h riempie solo i primi 256
byte della struttura VbeInfoBlock (come previsto dalla versione 1.0
delle specifiche VBE); se questo membro viene inizializzato dal
programmatore con l'apposita stringa "VBE2", il servizio 4F00h
riempie tutti i 512 byte della struttura VbeInfoBlock (come previsto
dalla versione 2.0 o superiore delle specifiche VBE).

Il membro VbeVersion indica la versione VBE, in formato BCD,


implementata nel VBIOS; ad esempio, il valore 0102h indica la
versione 1.2, dove 1 è il major number, mentre 2 è il minor number.

Il membro OemStringPtr è un puntatore FAR ad una stringa C contenente


informazioni relative all'Original Equipment Manufacturer o OEM
(fabbricante dell'hardware); questa stringa ha una lunghezza massima
raccomandata di 256 byte e può trovarsi memorizzata nella RAM o nella
ROM della scheda video (a partire dalle specifiche VBE 3.0 la stringa
si trova nel membro OemData della struttura VbeInfoBlock).

Il membro Capabilities fornisce informazioni su particolari


caratteristiche grafiche supportate dall'adattatore video; si tratta
di un valore a 32 bit che assume la struttura illustrata in Figura 6.

264
Figura 6 - Struttura del membro Capabilities
Bit Significato
0 Caratteristiche del DAC
1 Compatibilità VGA dell'adattatore
2 Modalità operativa del DAC
3 Supporto hardware dello "stereoscopic signaling"
4 Informazioni sullo "stereoscopic signaling"
5-31 Riservati

Il bit 0 indica la struttura del DAC per la rappresentazione dei


colori primari RGB; la sigla DAC sta per Digital to Analog Converter
ed indica il dispositivo hardware che converte una terna RGB
(digitale) in un segnale analogico da inviare al monitor.
Se il bit 0 vale 0, il DAC usa 6 bit per ogni colore primario; se il
bit 0 vale 1, il DAC può essere programmato per usare 8 (o più) bit
per ogni colore primario.

Il bit 1 indica la compatibilità VGA dell'adattatore video;


controllando tale bit il programmatore può sapere se, anche quando si
opera in modalità VBE, possono essere utilizzate le funzionalità
tipiche degli adattatori VGA (indirizzi delle porte hardware,
tavolozze dei colori, etc).
Se il bit 1 vale 0, l'adattatore è VGA compatibile; se il bit 1 vale
1, l'adattatore non è VGA compatibile (cioè, supporta solo le
funzionalità standard VBE, ma non quelle VGA).

Il bit 2 indica la modalità operativa del DAC; se si eccettua il caso


delle vecchie schede video, normalmente tale bit viene posto a 0 per
indicare la modalità operativa predefinita.
Le vecchie schede video, per evitare il fastidioso fenomeno dello
sfarfallio, richiedevano che la programmazione del DAC avvenisse in
modo sincrono con il refresh verticale; per queste vecchie schede
video, il bit 2 deve essere posto a 1.

Il bit 3 indica se l'adattatore video supporta via hardware l'effetto


"stereoscopico"; si tratta di una tecnica che consiste nel ricorso ad
un particolare sistema di suddivisione e visualizzazione delle
immagini in modo da creare un effetto stereoscopico tridimensionale.
Se il bit 3 vale 0, l'adattatore non supporta l'effetto
stereoscopico; se il bit 3 vale 1, l'adattatore possiede l'hardware
necessario per il supporto dell'effetto stereoscopico.

Se il servizio 4F00h pone a 1 il bit 3, allora il bit 4 indica se il


segnale di sincronizzazione stereoscopica arriva attraverso un
apposito connettore denominato EVC; se il bit 4 vale 1, è presente un
connettore EVC, mentre in caso contrario è presente un generico
connettore VESA.

Tornando alla struttura VbeInfoBlock di Figura 5, il membro


VideoModePtr contiene un puntatore FAR ad una lista di modalità video
VESA supportate dall'adattatore; tali modalità sono indicate dai
265
valori a 16 bit di Figura 2 e Figura 3, mentre la fine della lista è
individuata dal valore FFFFh.

Nota importante.
Si tenga presente che la lista puntata da VideoModePtr
rappresenta l'elenco delle modalità video supportate
dall'adattatore; non è detto però che tutte queste modalità
video siano realmente attivabili.
Soprattutto le modalità video ad elevata risoluzione e numero
di colori, richiedono una notevole quantità di VRAM che
potrebbe anche non essere disponibile; inoltre (e questo è
l'aspetto più importante), determinate modalità video fornite
dall'adattatore potrebbero non essere supportate dal monitor.

Le specifiche VBE indicano chiaramente che è compito dei


programmi verificare che la particolare modalità video che si
vuole attivare sia effettivamente supportata dal monitor; i
moderni sistemi operativi si servono delle specifiche del
monitor stesso per rendere disponibili solamente le modalità
video effettivamente attivabili.

Il membro TotalMemory indica la quantità totale di VRAM (espressa in


numero di blocchi da 64 Kb ciascuno) fisicamente disponibile e
indirizzabile attraverso il frame buffer; si tenga presente che, a
seconda della modalità video selezionata, potrebbe risultare
disponibile tutta o solo un parte della VRAM totale.
Quando si utilizza un emulatore DOS, il membro TotalMemory fornisce
in genere una quantità totale di VRAM inferiore a quella
effettivamente presente sulla scheda video; ovviamente, ciò è dovuto
al fatto che una parte consistente della VRAM è riservata all'uso
esclusivo del SO.

Tutti i successivi membri della struttura VbeInfoBlock sono


disponibili solo in presenza del supporto VBE 2.0 o superiore e
contengono informazioni che gli OEM utilizzano per identificare
l'hardware da essi prodotto; tali informazioni sono rappresentate da
numeri in formato BCD e da stringhe C.
A partire dalle specifiche VBE 2.0, le informazioni relative all'OEM
possono trovarsi inserite, eventualmente, nei 256 byte del membro
OemData.

In base alle considerazioni appena esposte, possiamo affermare quindi


che il servizio 4F00h può essere usato innanzi tutto per verificare
se un determinato VBIOS supporta i servizi VBE; ciò avviene quando lo
stesso servizio 4F00h restituisce AL=4Fh e AH=00h.
Se in AL e AH si ottengono codici di errore, bisogna ovviamente
terminare il programma segnalando, attraverso un apposito messaggio,
il mancato supporto VBE; se, invece, il supporto VBE è presente, si
può usare il contenuto della struttura VbeInfoBlock per ricavare
tutte le informazioni sull'adattatore video.

Si ricordi che, per ottenere le informazioni estese sul supporto VBE,


bisogna chiamare il servizio 4F00h dopo aver inizializzato il membro
266
VbeSignature con la stringa "VBE2"; come sappiamo, tali informazioni
sono disponibili solo in presenza di un VBIOS che supporta le
specifiche VBE 2.0 o superiori (cosa che può essere verificata
attraverso il membro VbeVersion). In ogni caso, per evitare
spiacevoli sorprese conviene sempre allocare 512 byte di memoria da
destinare alla struttura VbeInfoBlock!

11.3.2 Servizio n.4F01h: Return VBE Mode Information

Questo servizio restituisce una numerosa serie di informazioni


relative ad una specifica modalità video supportata dall'adattatore.

VBE - Servizio n. 4F01h - Return VBE Mode Information

Argomenti richiesti:
AX = 4F01h (Return VBE Mode Information)
CX = Codice modalità video
ES:DI = Puntatore al buffer delle informazioni

Valori restituiti:
AX = VBE Return Status

Per ciascuna delle modalità video restituite nella lista puntata da


VideoModePtr, è possibile richiedere una serie di ulteriori
informazioni estremamente dettagliate; tali informazioni vengono
restituite dal servizio 4F01h in una struttura denominata
ModeInfoBlock.
La struttura ModeInfoBlock ha una dimensione pari a 256 byte che
devono essere allocati dal programma che richiede il servizio 4F01h;
la Figura 7 illustra tutti i dettagli.

Figura 7 - ModeInfoBlock Structure


STRUC ModeInfoBlock

ModeAttributes resw 1 ; attributi del modo video


WinAAttributes resb 1 ; attributi finestra A
WinBAttributes resb 1 ; attributi finestra B
WinGranularity resw 1 ; granularità finestre
WinSize resw 1 ; dimensione finestre
WinASegment resw 1 ; segmento di inizio finestra A
WinBSegment resw 1 ; segmento di inizio finestra B
WinFuncPtr resd 1 ; puntatore FAR alla window function
BytesPerScanLine resw 1 ; lunghezza in byte di una scan line
XResolution resw 1 ; risoluzione orizzontale
YResolution resw 1 ; risoluzione verticale
XCharSize resb 1 ; larghezza in pixel cella carattere
YCharSize resb 1 ; altezza in pixel cella carattere
NumberOfPlanes resb 1 ; numero di piani di bit
BitsPerPixel resb 1 ; bit di colore per ogni pixel
NumberOfBanks resb 1 ; numero banchi di memoria video
MemoryModel resb 1 ; modello di memoria
BankSize resb 1 ; dimensione banco di memoria in Kb
NumberOfImgPages resb 1 ; numero di pagine immagine
Reserved1 resb 1 ; riservato (viene settato a 1)
RedMaskSize resb 1 ; dimensione in bit della Red Mask
RedFieldPos resb 1 ; posizione LSB nella Red Mask
267
GreenMaskSize resb 1 ; dimensione in bit della Green Mask
GreenFieldPos resb 1 ; posizione LSB nella Green Mask
BlueMaskSize resb 1 ; dimensione in bit della Blue Mask
BlueFieldPos resb 1 ; posizione LSB nella Blue Mask
RsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
RsvdFieldPos resb 1 ; posizione LSB nella Reserved Mask
DirectColModeInfo resb 1 ; attributi direct color mode
PhysBasePtr resd 1 ; indirizzo framebuffer (modo lineare)
Reserved2 resd 1 ; riservato (viene settato a 0)
Reserved3 resw 1 ; riservato (viene settato a 0)
LinByteScanLine resw 1 ; byte per scan line (modo lineare)
BnkNumOfImgPages resb 1 ; numero pagine immagine (modo reale)
LinNumOfImgPages resb 1 ; numero pagine immagine (modo lineare)
LinRedMaskSize resb 1 ; dimensione in bit della Red Mask (modo
lineare)
LinRedFieldPos resb 1 ; posizione LSB nella Red Mask (modo
lineare)
LinGreenMaskSize resb 1 ; dimensione in bit della Green Mask
(modo lineare)
LinGreenFieldPos resb 1 ; posizione LSB nella Green Mask (modo
lineare)
LinBlueMaskSize resb 1 ; dimensione in bit della Blue Mask
(modo lineare)
LinBlueFieldPos resb 1 ; posizione LSB nella Blue Mask (modo
lineare)
LinRsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
(linear)
LinRsvFieldPos resb 1 ; posizione LSB nella Reserved Mask
(linear)
MaxPixelClock resd 1 ; max. pixel clock in Hz per il modo
grafico
Reserved4 resb 190 ; rende la struttura da 256 byte

ENDSTRUC

Il membro ModeAttributes è un valore a 16 bit che fornisce importanti


caratteristiche hardware della modalità video selezionata; il
significato dei vari bit è illustrato dalla Figura 8.

Figura 8 - Struttura del membro ModeAttributes


Valore
Bit Significato
0 1
0 Supporto hardware del modo video NO SI
1 Riservato - -
2 Supporto BIOS per l'output in modo testo (TTY) NO SI
3 Modalità colore o monocromatica Mono. Colore
4 Modalità testo o grafica Testo Grafica
5 Compatibilità VGA del modo video SI NO
6 Accesso segmentato alla memoria video SI NO
7 Accesso lineare alla memoria video NO SI
8 Disponibilità della modalità "double scan" NO SI
9 Disponibilità della modalità interlacciata NO SI
10 Supporto hardware del "triple buffering" NO SI
268
11 Supporto hardware dello "stereoscopic display" NO SI
12 Supporto del "dual display start address" NO SI
13-15 Riservati - -

Il bit 0 è molto importante in quanto ci permette di sapere se la


modalità video VESA che abbiamo specificato è effettivamente
attivabile sul computer in uso; come è stato spiegato in precedenza,
può capitare che una determinata modalità video VESA, pur essendo
supportata dalla scheda video, non possa essere attivata a causa
delle limitazioni del monitor e/o della VRAM insufficiente.
Il bit 2 indica se il BIOS supporta le funzioni di emulazione del
terminale; in caso affermativo, il programmatore può chiamare
eventualmente la INT 10h per usufruire dei servizi standard 01h, 02h,
06h, 07h, 09h, 0Ah, 0Eh (vedere il documento VGABIOS.TXT disponibile
nella sezione Downloads).
Il bit 3 indica se la modalità video selezionata è monocromatica o a
colori; analogamente, il bit 4 indica se la modalità video
selezionata è testuale o grafica.
Il bit 5 indica se la modalità video selezionata è "VGA compatibile";
in tal caso, il programmatore può eventualmente accedere a tutte le
caratteristiche hardware standard degli adattatori VGA (porte di I/O,
frame buffer, etc).
Il bit 6 (che deve essere combinato con il bit 7) indica se la
modalità video selezionata permette l'accesso alla VRAM attraverso le
apposite finestre (Window A e Window B) di I/O (accesso segmentato in
modalità reale); in caso affermativo, come è stato già spiegato, la
VRAM risulta suddivisa in tanti blocchi (generalmente da 64 Kb
ciascuno) denominati memory banks (banchi di memoria).
Il bit 7 indica se la modalità video selezionata permette l'accesso
alla VRAM attraverso un frame buffer lineare (modalità protetta); il
bit 7 si combina con il bit 6 per formare un codice a 2 bit per il
quale sono permessi solamente i tre valori 00b (solo accesso
segmentato), 10b (accesso segmentato o lineare), 11b (solo accesso
lineare).
Il bit 8 indica se la modalità video selezionata supporta il "double
scan"; nelle modalità video "double scan" (con risoluzioni standard
da 320x200, 320x240, 400x300) ogni scan line viene scandita due volte
durante il refresh verticale.
Il bit 9 indica se la modalità video selezionata supporta la
scansione interlacciata delle scan line; in caso affermativo, durante
il vertical refresh, vengono scandite prima le scan line di indice
pari e poi quelle di indice dispari.
Il bit 10 indica se la modalità video selezionata supporta il "triple
buffering"; in caso affermativo i programmi possono usufruire di tre
appositi buffer attraverso i quali si possono velocizzare
notevolmente le operazioni di rendering sullo schermo.
Il bit 11 indica se la modalità video selezionata supporta lo
"stereoscopic display"; come è stato già spiegato, si tratta di una
tecnica che consiste nel ricorso ad un particolare sistema di
suddivisione e visualizzazione delle immagini in modo da creare un
effetto stereoscopico tridimensionale.
Il bit 12 indica se la modalità video selezionata supporta
l'indirizzamento del "dual display"; in caso affermativo, l'effetto
269
stereoscopico può essere creato con l'ausilio di due monitor.

Tornando alla struttura di Figura 7, i membri WinAAttributes e


WinBAttributes forniscono le caratteristiche delle finestre per
l'accesso segmentato alla VRAM; ciascuno dei due valori a 8 bit
assume la struttura mostrata in Figura 9.

Figura 9 - Attributi finestra


Valore
Bit Significato
0 1
0 Finestra supportata NO SI
1 Finestra leggibile NO SI
2 Finestra scrivibile NO SI
3-7 Riservati - -

Nella gran parte dei casi, risulta disponibile unicamente la Window A


la quale sarà quindi leggibile e scrivibile; se nessuna delle due
finestre è supportata, il programmatore può accedere alla VRAM solo
attraverso un frame buffer lineare (modalità protetta).

Il membro WinGranularity indica la dimensione in Kb di ciascuno dei


banchi di memoria visibili in Figura 4; si tratta quindi
dell'incremento (decremento) in Kb necessario per posizionare la
finestra di I/O sul banco di memoria successivo (precedente) a quello
in cui ci troviamo.

Il membro WinSize indica la dimensione in Kb di ciascuna delle due


finestre di I/O; il valore raccomandato dal comitato VESA è pari a 64
Kb (pari cioè alla dimensione di un segmento di memoria della
modalità reale) e quindi, i programmi possono sempre assumere
WinSize=64.

I membri WinASegment e WinBSegment hanno senso solo per la modalità


reale e contengono l'indirizzo di memoria da cui parte la relativa
finestra di I/O; come al solito, si tratta di un indirizzo allineato
al paragrafo per cui viene fornita la sola componente Seg a 16 bit
(la componente Offset vale implicitamente 0000h).
Nel caso più frequente è presente una sola finestra che parte
all'indirizzo A000h:0000h per le modalità grafiche, B800h:0000h per
le modalità testo a colori e B000h:0000h per le modalità testo in
bianco e nero; se una determinata finestra non è supportata, il
relativo membro vale 0000h.

Il membro WinFuncPtr è un puntatore FAR da utilizzare per la chiamata


di una apposita funzione attraverso la quale si può posizionare una
finestra di I/O nell'area desiderata della VRAM; se tale funzione non
è disponibile, il relativo membro vale 0000h:0000h (puntatore a
NULL).
L'utilizzo della "window function" permette di velocizzare
notevolmente le operazioni di posizionamento delle finestre di I/O;
in assenza di tale funzione, il programmatore deve necessariamente
ricorrere al meno efficiente servizio 4F05h della INT 10h (vedere più
270
avanti).

Il membro BytesPerScanLine indica la dimensione in byte di ogni linea


di scansione dello schermo; come abbiamo visto nel precedente
capitolo, tale valore coincide con la larghezza in pixel dello
schermo solo nelle modalità grafiche che utilizzano 1 byte per
identificare il colore di ogni pixel.

I membri XResolution e YResolution indicano le dimensioni (larghezza


e altezza) dello schermo nella modalità video selezionata; per le
modalità grafiche si tratta di valori in pixel, mentre per le
modalità testuali si tratta di valori in celle di carattere.

I membri XCharSize e YCharSize indicano le dimensioni in pixel di


ogni cella di carattere nella modalità video selezionata.

Il membro NumberOfPlanes indica il numero di piani di bit utilizzati


dalla modalità video che si sta usando; come abbiamo visto nel
precedente capitolo, alcune particolari modalità video rappresentano
la VRAM sotto forma di piani di bit sovrapposti.

Il membro BitsPerPixel indica il numero di bit utilizzato per


rappresentare il colore di ciascun pixel nella modalità video che si
sta usando; come sappiamo, spesso tale valore rappresenta un indice
relativo agli elementi di una apposita tavolozza di colori.

Il membro MemoryModel codifica l'organizzazione della VRAM nella


modalità video selezionata; si tratta di un codice a 8 bit che può
assumere i valori illustrati in Figura 10.

Figura 10 - Modello di memoria


Valore Significato
00h Modo testo
01h Modo grafico CGA
02h Modo grafico Hercules
03h Planare
04h Packed pixel
05h Non-chain 4, 256 colori
06h Direct color
07h YUV
08h-0Fh Riservato VESA
10h-FFh Riservato OEM

Il membro NumberOfBanks indica il numero di banchi di memoria nei


quali si trovano raggruppate le scan line relative alla modalità
grafica selezionata; per le modalità video che non utilizzano il
raggruppamento in banchi delle scan line, il valore di questo membro
è 1.
Dividendo l'indice n di una scan line per il valore NumberOfBanks si
ottiene un quoziente che rappresenta l'indice b del banco che
contiene la scan line n; il resto della divisione rappresenta,
271
invece, l'indice i della scan line n relativo al banco b.

Il membro BankSize rappresenta la dimensione in Kb di ciascun banco


utilizzato per raggruppare le scan line nella modalità video
selezionata; per le modalità video che non utilizzano il
raggruppamento in banchi delle scan line, il valore di questo membro
è 0.

Il membro NumberOfImgPages rappresenta il numero totale, meno uno, di


schermate complete che la VRAM è in grado di contenere
contemporaneamente; se questo valore è maggiore di uno, è possibile
gestire nella VRAM più schermate, ciascuna delle quali può essere poi
trasferita velocemente sullo schermo.

I membri RedMaskSize, GreenMaskSize, BlueMaskSize e RsvdMaskSize


rappresentano le dimensioni in bit delle tre componenti RGB (più una
componente riservata) utilizzate per codificare i colori nelle
modalità video che supportano tale caratteristica (Modello di memoria
Direct Color o YUV); per le modalità video che non supportano le
componenti di colore, questi quattro membri valgono 0.

I membri RedFieldPos, GreenFieldPos, BlueFieldPos e RsvdFieldPos


rappresentano le posizioni del bit meno significativo di ciascuna
delle tre componenti di colore (più una componente riservata)
all'interno di una terna RGB; ad esempio, se le tre precedenti
MaskSize sono 8:8:8, allora questi tre membri indicano le posizioni
16:8:0.
Per le modalità video che non supportano le componenti di colore,
questi quattro membri valgono 0.

Il membro DirectColModeInfo contiene importanti informazioni relative


alle modalità video che supportano le componenti di colore RGB; si
tratta di un valore a 8 bit di cui solamente i primi due bit hanno
significato.
Il bit in posizione 0 indica se la tavolozza dei colori è fissa (0) o
programmabile (1); se tale bit vale 1, il programmatore può
utilizzare il servizio 4F09h per caricare una tavolozza
personalizzata.
Il bit in posizione 1 indica se i membri RsvdMaskSize e RsvdFieldPos
sono usabili (1) o no (0); se tale bit vale 1, i colori RGB
utilizzano anche una quarta componente (ad esempio, alcuni SO si
servono di questa quarta componente per definire il grado di
trasparenza del colore RGB di un pixel.

Tutti gli altri membri della struttura ModeInfoBlock hanno


significato solo per la modalità protetta.

11.3.3 Servizio n.4F02h: Set VBE Mode

Questo servizio permette di abilitare una modalità video VESA


supportata dall'adattatore video.

VBE - Servizio n. 4F02h - Set VBE Mode

272
Argomenti richiesti:
AX = 4F02h (Set VBE Mode)
BX = Codice modalità video
ES:DI = Puntatore alla struttura CRTCInfoBlock

Valori restituiti:
AX = VBE Return Status

Il servizio 4F02h permette di abilitare una delle modalità video VESA


supportate dall'hardware del computer che si sta utilizzando; il
registro BX deve contenere l'opportuno codice di Figura 2.
Come è stato già spiegato in precedenza, il bit in posizione 11 di
tale codice serve per selezionare la modalità di controllo della
frequenza di scansione verticale del monitor; se questo bit vale 0
vengono utilizzate le impostazioni predefinite del BIOS, mentre se
questo bit vale 1 vengono utilizzate le impostazioni personalizzate
che il programmatore deve specificare in una struttura CRTCInfoBlock
puntata da ES:DI.
A meno che si sappia esattamente ciò che si sta facendo, si consiglia
vivamente di lasciare a 0 il bit in posizione 11; per maggiori
dettagli sulla struttura CRTCInfoBlock si può consultare il documento
VBE3.PDF disponibile nella sezione Downloads di questo sito.

Un altro aspetto molto importante da ricordare è che una data


modalità video, pur essendo supportata dal proprio adattatore,
potrebbe non essere attivabile a causa della scarsità di VRAM o a
causa delle limitazioni del monitor; proprio per questo motivo, prima
di abilitare la modalità video desiderata conviene sempre consultare
il bit in posizione 0 del membro ModeAttributes della struttura
ModeInfoBlock.

11.3.4 Servizio n.4F03h: Return Current VBE Mode

Questo servizio permette di ottenere informazioni sulla modalità


video VESA attualmente abilitata.

VBE - Servizio n. 4F03h - Return Current VBE Mode

Argomenti richiesti:
AX = 4F03h (Return Current VBE Mode)

Valori restituiti:
AX = VBE Return Status
BX = Informazioni sul modo video corrente

Il servizio 4F03h restituisce alcune sommarie informazioni sulla


modalità video VESA corrente; tali informazioni risultano disponibili
nel registro BX.
I bit nelle posizioni dalla 0 alla 13 contengono il codice della
modalità video corrente (Figura 2). Il bit in posizione 14 indica se
il modo video corrente supporta il frame buffer a finestre (0) o il
frame buffer lineare (1); il bit in posizione 15 indica se, quando è
stata attivata la modalità video corrente, la VRAM è stata cancellata
(0) oppure no (1).
273
Si tenga presente che il servizio 4F03h restituisce le informazioni
appena descritte solo quando la modalità video corrente è stata
attivata con il servizio 4F02h; ciò può non accadere quando la
modalità video corrente è stata attivata con il servizio standard 00h
(Set Video Mode) della INT 10h.

11.3.5 Servizio n.4F04h: Save/Restore Video State

Questo servizio permette di salvare o ripristinare lo stato hardware


dell'adattatore video.

VBE - Servizio n. 4F04h - Save/Restore Video State

Argomenti richiesti:
AX = 4F04h (Save/Restore Video State)
DL = Operazione richiesta
CX = Stato da salvare/ripristinare
ES:BX = Puntatore al buffer (se DL != 0)

Valori restituiti:
AX = VBE Return Status
BX = Dimensione del buffer (se DL == 0)

Attraverso il servizio 4F04h è possibile salvare lo stato hardware


corrente o ripristinare lo stato hardware precedente dell'adattatore
video; la specifica operazione da eseguire deve essere indicata
attraverso un codice nel registro DL.
Se DL=00h, stiamo richiedendo la dimensione del buffer da utilizzare
per contenere le informazioni da salvare o ripristinare; tale
dimensione viene restituita nel registro BX ed è espressa in blocchi
da 64 byte.
Se DL=01h stiamo richiedendo il salvataggio dello stato hardware
corrente dell'adattatore video; le informazioni vengono salvate
nell'area di memoria puntata da ES:BX (allocata dal programmatore in
base al valore calcolato in precedenza con DL=00h).
Se DL=02h stiamo richiedendo il ripristino dello stato hardware
precedente dell'adattatore video; le informazioni vengono lette
dall'area di memoria puntata da ES:BX.

Il registro CX, attraverso i suoi 4 bit meno significativi, permette


di specificare quale stato hardware si deve salvare (DL=01h) o
ripristinare (DL=02h); il bit in posizione 0 si riferisce allo stato
hardware del controller, il bit in posizione 1 si riferisce allo
stato dei dati del BIOS, il bit in posizione 2 si riferisce allo
stato hardware del DAC, il bit in posizione 3 si riferisce allo stato
dei registri.
Naturalmente, è anche possibile combinare questi 4 bit; ponendo, ad
esempio, CX=000Fh, si indica che l'operazione di salvataggio o
ripristino coinvolge tutte le quattro voci elencate in precedenza.

11.3.6 Servizio n.4F05h: Display Window Control

274
Questo servizio permette di gestire l'accesso alla VRAM attraverso il
frame buffer.

VBE - Servizio n. 4F05h - Display Window Control

Argomenti richiesti:
AX = 4F05h (Display Window Control)
BH = Operazione richiesta
BL = Numero finestra
DX = Posizione finestra (se BH == 0)

Valori restituiti:
AX = VBE Return Status
DX = Posizione finestra (se BH == 1)

Attraverso il servizio 4F05h è possibile controllare il


posizionamento nella VRAM del frame buffer a finestre; l'operazione
da svolgere viene specificata attraverso il registro BH, mentre la
finestra coinvolta (A o B) viene specificata attraverso il registro
BL.
Se BH=0, stiamo richiedendo il posizionamento della finestra in VRAM;
in tal caso, il registro DX deve specificare la posizione in unità di
misura pari alla granularità (vedere la Figura 4).
Se BH=1, stiamo richiedendo la posizione attuale della finestra in
VRAM; in tal caso, l'informazione richiesta viene restituita nel
registro DX ed è espressa in unità di misura pari alla granularità.

Il registro BL indica la finestra alla quale ci stiamo riferendo; il


valore 0 indica la Window A, mentre il valore 1 indica la Window B.

Molti programmi fanno un uso piuttosto pesante del bank switching


(commutazione di banco); in casi del genere, la chiamata del servizio
4F05h attraverso la INT 10h può provocare un sensibile calo delle
prestazioni.
Per ovviare a questo inconveniente, il programmatore può anche
servirsi della window function il cui indirizzo FAR, come già
sappiamo, viene restituito nel membro WinFuncPtr della struttura
ModeInfoBlock; è importante ricordare che, se tale funzione non è
supportata, il membro WinFuncPtr restituisce l'indirizzo 0000h:0000h
(puntatore FAR a NULL).

Se la window function è supportata, il suo utilizzo è vivamente


raccomandato al posto della INT 10h; a tale proposito, dopo aver
inizializzato i registri BX e DX richiesti dal servizio 4F05h, basta
effettuare una chiamata FAR all'indirizzo WinFuncPtr. Ad esempio, se
abbiamo creato una istanza mode_info della struttura ModeInfoBlock,
possiamo scrivere:

mov bx, 0000h ; posizionamento Window A


mov dx, 20 ; banco 20 della VRAM
call far [mode_info + WinFuncPtr] ; chiamata window function

Ovviamente, se la window function non è supportata, dobbiamo


sostituire la precedente chiamata FAR con una istruzione INT 10h

275
(dopo aver posto AX=4F05h)!

11.3.7 Servizio n.4F06h: Set/Get Logical Scan Line Length

Questo servizio permette di specificare la larghezza virtuale dello


schermo.

VBE - Servizio n. 4F06h - Set/Get Logical Scan Line


Length

Argomenti richiesti:
AX = 4F06h (Set/Get Logical Scan Line Length)
BL = Operazione richiesta
CX = Larghezza scan line

Valori restituiti:
AX = VBE Return Status
BX = Larghezza scan line in byte
CX = Larghezza scan line in pixel
DX = Massimo numero di scan line

Attraverso il servizio 4F06h il programmatore può impostare una


larghezza "virtuale" dello schermo maggiore di quella fisicamente
visibile sul monitor; ad esempio, dopo aver attivato una modalità
video da 640x480 pixel si può impostare una larghezza virtuale pari a
3*640=1920 pixel.

Il registro BL permette di specificare l'operazione richiesta; i


valori disponibili sono: 00h (imposta la larghezza in pixel della
scan line), 01h (restituisce la larghezza corrente della scan line),
02h (imposta la larghezza in byte della scan line), 03h (restituisce
la massima larghezza possibile per una scan line).

Il registro CX permette di specificare la larghezza desiderata di una


scan line; se BL=00h allora CX indica una larghezza in pixel, mentre
se BL=02h allora CX indica una larghezza in byte (il contenuto di CX
viene ignorato quando BL vale 01h o 03h).

In risposta alla chiamata del servizio 4F06h, vengono restituite una


serie di informazioni nei registri BX, CX e DX; il registro BX
restituisce la larghezza in byte della scan line, il registro CX
restituisce la larghezza in pixel della scan line, mentre il registro
DX restituisce il massimo numero di scan line disponibili.

Si presti molta attenzione al fatto che la richiesta di questo


servizio può restituire un errore; come è stato spiegato in
precedenza, ciò è dovuto ad eventuali limitazioni hardware (in questo
caso, VRAM insufficiente).

11.3.8 Servizio n.4F07h: Set/Get Display Start

Questo servizio permette di specificare quale pixel dello schermo


virtuale verrà visualizzato nell'angolo in alto a sinistra del
monitor.

276
VBE - Servizio n. 4F07h - Set/Get Display Start

Argomenti richiesti:
AX = 4F07h (Set/Get Display Start)
BH = 00h (riservato)
BL = Operazione richiesta
CX = Primo pixel da mostrare nella prima scan line
DX = Prima scan line da mostrare

Valori restituiti:
AX = VBE Return Status
BH = 00h
CX = Primo pixel da mostrare nella prima scan line
DX = Prima scan line da mostrare

Attraverso il servizio 4F07h il programmatore può specificare la


porzione dello schermo "virtuale" che verrà visualizzata sul monitor
(cioè, il pixel dello schermo virtuale che comparirà nell'angolo in
alto a sinistra del monitor); in tal modo è possibile creare un
effetto di scorrimento dello schermo virtuale sul monitor.

Per effettuare tutte le necessarie impostazioni bisogna porre BL=00h;


in tal caso, DX deve contenere l'indice della prima scan line da
visualizzare, mentre CX deve contenere l'indice del pixel, di quella
stessa scan line, da visualizzare per primo .
Per richiedere le impostazioni correnti bisogna porre BL=01h; in tal
caso in DX viene restituito l'indice della prima scan line
visualizzata, mentre in CX viene restituito il primo pixel
visualizzato nella stessa prima scan line.

Lo standard VBE 3.0 introduce diverse nuove funzionalità per il


servizio 4F07h; a tale proposito, si consiglia di consultare il
documento VBE3.PDF disponibile nella sezione Downloads di questo
sito.

11.3.9 Servizio n.4F08h: Set/Get DAC Palette Format

Questo servizio permette di specificare il numero di bit da


utilizzare per rappresentare ciascuna delle 3 componenti RGB di un
colore (Direct Color).

VBE - Servizio n. 4F08h - Set/Get DAC Palette Format

Argomenti richiesti:
AX = 4F08h (Set/Get DAC Palette Format)
BL = Operazione richiesta
BH = Numero di bit per ogni componente RGB

Valori restituiti:
AX = VBE Return Status
BH = Numero di bit per ogni componente RGB

Attraverso il servizio 4F08h il programmatore può specificare il


numero di bit da utilizzare per rappresentare ciascuna delle 3
componenti primarie di un colore RGB (modello di memoria Direct
277
Color); ciò è possibile solo quando il bit meno significativo del
membro Capabilities della struttura VbeInfoBlock vale 1.

Per impostare il numero di bit per componente primaria bisogna porre


BL=00h; in ogni caso (e quindi, anche in caso di errore), il registro
BH restituisce il numero effettivo di bit assegnato ad ogni
componente primaria.
Per richiedere le impostazioni correnti bisogna porre BL=01h; in tal
caso in BH viene restituito il numero corrente di bit assegnato ad
ogni componente primaria.

11.3.10 Servizio n.4F09h: Set/Get DAC Palette Data

Questo servizio permette di impostare una tavolozza di colori RGB da


utilizzare nelle modalità video con modello di memoria Direct Color.

VBE - Servizio n. 4F09h - Set/Get DAC Palette Data

Argomenti richiesti:
AX = 4F09h (Set/Get DAC Palette Data)
BL = Operazione richiesta
CX = Numero di elementi RGB da aggiornare
DX = Indice primo elemento RGB da aggiornare
ES:DI = Puntatore alla nuova tavolozza

Valori restituiti:
AX = VBE Return Status

Attraverso il servizio 4F09h il programmatore può specificare una


nuova tavolozza da utilizzare nelle modalità video con modello di
memoria Direct Color; questa funzionalità deve essere impiegata solo
con gli adattatori video che non supportano l'accesso diretto alla
tavolozza attraverso i servizi standard VGA (vedere la Figura 6).

Per impostare una nuova tavolozza bisogna porre BL=00h; in tal caso,
il registro CX deve specificare il numero di elementi RGB da
aggiornare nella tavolozza, il registro DX deve specificare l'indice
del primo elemento RGB da aggiornare nella tavolozza, mentre ES:DI
deve puntare alla nuova tavolozza.

La tavolozza è un vettore di elementi RGB, ciascuno dei quali assume


l'aspetto illustrato in Figura 11.

Figura 11 - Elemento RGB


STRUC PaletteEntry

Blue resb 1 ; valore componente Blue


Green resb 1 ; valore componente Green
Red resb 1 ; valore componente Red
Alignment resb 1 ; non usato (DWORD ALIGN)

ENDSTRUC

Usualmente, il DAC utilizza 6 bit per ogni componente primaria di

278
colore; per sapere se il DAC supporta le componenti a 8 bit è
necessario consultare il bit in posizione 1 del membro Capabilities
(vedere figura 6).

11.4 Supporto VESA per le vecchie schede video SVGA

Può capitare di imbattersi in vecchi PC dotati di schede video le


quali, pur supportando diverse modalità SVGA, risultano prive delle
estensioni VBE nel VBIOS essendo state fabbricate in un periodo
precedente alla nascita dello standard VESA; molto spesso, questo
problema può essere efficacemente risolto attraverso appositi
programmi TSR (Terminate and Stay Resident) reperibili su Internet.
Una volta che il programmatore ha reperito il TSR adatto alla propria
scheda video, procede alla relativa installazione ed attivazione al
boot attraverso il file AUTOEXEC.BAT; ad esempio, nel caso di una
scheda video Tseng Labs ET4000, il TSR prende il nome di TLIVESA.COM
e può essere installato con la riga:

LH C:\TLIVESA.COM

(il comando LH significa Load High e permette di installare un


programma nella upper memory).

Al riavvio del PC, il TSR si installa permanentemente in memoria ed


effettua un lavoro molto importante che consiste nell'intercettare
tutte le chiamate alla INT 10h; se il TSR si accorge che AH=4Fh,
provvede a fornire il servizio VBE richiesto, mentre in caso
contrario trasferisce la chiamata alla ISR predefinita per la INT
10h.

La Figura 12 illustra un elenco, in ordine alfabetico, dei principali


produttori di (vecchie) schede video e dei relativi TSR reperibili su
Internet; generalmente, ogni TSR è accompagnato da un documento che
illustra le modalità di utilizzo e la versione VBE supportata.

Figura 12 - TSR per il supporto VBE


Produttore TSR
ACER VESA.EXE
APPIAN APVESA.EXE
ATI VESA.COM
DIAMOND VMODE.COM
EVEREX EVRVESA.COM
GENOA VESA.COM
ORCHID ORCHDVSA.COM
PARADISE VESA.EXE
SIGMA SIGVESA.COM
TSENG TLIVESA.COM
VIDEO7 V7VESA.COM

Naturalmente, è del tutto superfluo sottolineare che il programmatore


279
deve munirsi del TSR adeguato per la propria scheda video SVGA; in
caso contrario, si possono ottenere risultati del tutto
imprevedibili!

11.5 Il bank switching

Come è stato ampiamente spiegato in precedenza, quando si programma


in modalità VESA, uno degli aspetti fondamentali riguarda il
cosiddetto bank switching (commutazione di banco); con questo termine
si indica l'operazione di posizionamento della finestra (o delle
finestre) nell'area desiderata della VRAM.
Per svolgere correttamente tale lavoro, il programmatore deve prima
raccogliere una serie completa di informazioni come quelle restituite
dai servizi 4F00h e 4F01h; in particolare, le informazioni più
importanti riguardano: la disponibilità del frame buffer a finestre,
la dimensione delle finestre e la granularità della VRAM.
Tutte queste informazioni sono necessarie in quanto l'operazione di
bank switching comporta lo svolgimento di una serie di calcoli; per
chiarire questo aspetto, vediamo un esempio pratico illustrato dalla
Figura 13.

Figura 13 - Esempio di bank switching

Nell'esempio di Figura 13 supponiamo di aver attivato una ipotetica


modalità video 512x512 a 256 colori; come sappiamo, nelle modalità
grafiche a 256 colori vengono utilizzati 8 bit per codificare il
colore di ogni pixel.
La struttura ModeInfoBlock ci restituisce quindi le informazioni
BitsPerPixel=8 e BytesPerScanLine=512; supponiamo, inoltre, che sia
presente il supporto per la sola WindowA con WinASegment=A000h,
WinSize=64 Kb e WinGranularity=16 Kb.
La conseguenza di tutto ciò è che una schermata 512x512 (pari a
262144 byte) risulterà suddivisa in 16 banchi di memoria (indicizzati
in Figura 13 da 0 a 15) da 16 Kb ciascuno; la stessa Figura 13 mostra
280
anche i 16 banchi di memoria suddivisi in 4 gruppi da 64 Kb ciascuno
(e indicizzati da 0 a 3).
Un'altra conseguenza è che ogni banco di memoria risulterà contenere
32 scan line complete; come vedremo in seguito, questa è una
situazione ideale che non sempre si verifica nella realtà.

Nell'esempio di Figura 13 risulta semplicissimo il calcolo


dell'offset relativo ad un pixel di coordinate x, y; la formula
generale è, infatti:

pixel_address = (y * BytesPerScanLine) + x

Nel nostro caso, con x=158 e y=300, otteniamo:

pixel_address = (300 * 512) + 158 = 153758

L'aspetto importantissimo da notare è che il risultato ottenuto può


anche richiedere più di 16 bit per la sua rappresentazione; è
fondamentale quindi utilizzare sempre ampiezze a 32 bit per questi
particolari calcoli.

A questo punto dobbiamo determinare l'indice (win_bank) del blocco da


65536 byte che contiene il pixel P (assumiamo quindi che, come
specificato dallo standard VESA, il valore WinSize sia sempre pari a
64 Kb); questo calcolo è semplicissimo in quanto basta dividere
pixel_address per 65536.
Per rendere più efficiente tale operazione basta osservare che
65536=216 per cui, indicando l'operazione di shift logico a destra con
>> (sintassi C), si ha:

win_bank = pixel_address / 65536 = pixel_address >> 16

Nel nostro caso otteniamo:

win_bank = 153758 >> 16 = 2

Per determinare ora il banco di memoria in cui posizionare la


WindowA, basta osservare che ogni win_bank è formato da 4 banchi da
16 Kb; non dobbiamo fare altro quindi che moltiplicare win_bank per
4.
Per rendere più efficiente tale operazione basta osservare che 4=22
per cui, indicando l'operazione di shift logico a sinistra con <<
(sintassi C), si ha:

bank = win_bank * 4 = win_bank << 2

Nel nostro caso otteniamo:

bank = 2 << 2 = 8

Dopo aver posizionato la WindowA nel banco 8, dobbiamo effettuare un


ultimo calcolo che riguarda la determinazione dell'offset (a 16 bit)
del pixel P all'interno della stessa WindowA; anche questo calcolo è
molto semplice in quanto basta sottrarre a pixel_address il valore

281
win_bank*WinSize.
Considerando ancora il fatto che il valore WinSize è sempre 65536
(10000h), si può rendere tale operazione molto più efficiente
effettuando un AND (a 32 bit) tra pixel_address e FFFFh; nel nostro
caso otteniamo:

pixel_offset = pixel_address AND FFFFh = 153758 AND FFFFh = 0002589Eh AND


0000FFFFh = 589Eh = 22686

In definitiva, il nostro pixel P si troverà all'indirizzo logico


A000h:589Eh della WindowA la quale, a sua volta, si trova già
posizionata nel banco 8 della VRAM.

Un aspetto degno di nota riguarda il metodo da seguire per calcolare


il numero di shift a sinistra a cui sottoporre win_bank in modo da
ottenere l'indice del banco di memoria in cui posizionare la
finestra; tale calcolo (il cui risultato viene memorizzato in una
variabile denominata bank_shift) si basa ancora una volta sul fatto
che WinSize vale sempre 64 Kb.
Indicando allora con mode_info una istanza della struttura
ModeInfoBlock, possiamo determinare il valore bank_shift con il
seguente codice:

mov byte [bank_shift], 0 ; num. shift per win_bank


numshift_loop:
mov al, 64 ; AL = WinSize in Kb
mov cl, [bank_shift] ; CL = numero di shift a
destra
shr al, cl ; AL = AL / (2^bank_shift)
cmp al, [mode_info + WinGranularity] ; AL == WinGranularity ?
je exit_loop ; si
inc byte [bank_shift] ; incrementa bank_shift
jmp numshift_loop ; ripeti il loop
exit_loop:

La variabile bank_shift viene inizializzata a 0 e indica il numero di


posti verso destra di cui bisogna far scorrere i bit di AL=64; se il
risultato è diverso da WinGranularity, la stessa variabile bank_shift
viene incrementata di 1 e il loop viene ripetuto.
Come si può notare, assegnando a bank_shift i valori 0, 1, 2, etc, il
risultato di SHR sarà 64, 32, 16, etc (e cioè, tutti i valori
possibili per WinGranularity); quando il risultato di SHR eguaglia
WinGranularity, il loop termina e in bank_shift si ottiene il numero
di shift a sinistra da applicare a win_bank.

11.6 Codifica dei colori nelle modalità video VESA/SVGA

Nel precedente capitolo, parlando delle varie modalità video grafiche


standard, abbiamo visto che in certi casi i bit della VRAM assegnati
ad ogni pixel contengono un valore che non rappresenta direttamente
il colore del pixel stesso, bensì l'indice relativo ad un vettore di
colori; l'elemento associato a quell'indice codifica l'effettivo
colore del pixel.
Questa stessa situazione si presenta anche quando si programmano le

282
modalità SVGA attraverso lo standard VESA; la differenza fondamentale
è data dal fatto che, nelle modalità SVGA, si ha a che fare con un
numero di colori che può andare da 2 (modalità monocromatiche) a
diversi milioni, per cui si rendono necessarie anche altre tecniche
di codifica!

Abbiamo anche visto che, quando si lavora con le modalità video


grafiche standard a bassa risoluzione (CGA, EGA, VGA e MCGA), le
varie tecniche di codifica dei colori permettono di gestire in
memoria una intera schermata senza mai sconfinare dal limite dei 64
Kb; in questo modo, il programmatore può lavorare sulla VRAM in modo
estremamente efficiente.
Nella gestione della VRAM in modalità SVGA, attraverso lo standard
VESA, si ha spesso a che fare con risoluzioni grafiche piuttosto alte
che comportano la suddivisione di una schermata in numerosi blocchi
da 64 Kb; ciò significa che, quando si deve accedere in I/O alla
VRAM, si può rendere necessario un pesante lavoro di bank switching.

Per chiarire tutti questi aspetti, analizziamo i vari sistemi di


codifica dei colori nelle modalità video grafiche VESA/SVGA.

11.6.1 Modalità video grafiche a 2 e a 4 colori

Nel precedente capitolo abbiamo visto che, nelle modalità video


grafiche monocromatiche, ad ogni pixel viene assegnato un solo bit
della VRAM; infatti, con 1 bit possiamo codificare la condizione 0
(pixel spento = colore nero) o 1 (pixel acceso).
Ogni byte della VRAM contiene le informazioni relative a 8/1=8 pixel;
possiamo affermare quindi che il valore BytesPerScanLine è pari a
XResolution/8.
Nelle modalità video grafiche a 4 colori, ad ogni pixel vengono
assegnati 2 bit della VRAM; infatti, con 2 bit possiamo codificare
22=4 colori differenti.
Ogni byte della VRAM contiene le informazioni relative a 8/2=4 pixel;
possiamo affermare quindi che il valore BytesPerScanLine è pari a
XResolution/4.
La Figura 14 illustra quest'ultima situazione.

Figura 14 - Accesso ai pixel nel modo grafico a 4 colori

283
Le specifiche VESA non prevedono nessun supporto per le modalità
video grafiche a 2 o 4 colori; il programmatore deve quindi ricorrere
ai servizi standard del BIOS illustrati nel precedente capitolo.

11.6.2 Modalità video grafiche a 16 colori

Nel precedente capitolo abbiamo visto che, nelle modalità video


grafiche a 16 colori, ad ogni pixel vengono assegnati 4 bit della
VRAM; infatti, con 4 bit possiamo codificare 24=16 colori differenti.
I 4 bit di ogni pixel risultano disposti in posizioni corrispondenti
su 4 piani sovrapposti; la Figura 15 illustra questa situazione.

Figura 15 - Accesso ai pixel nel modo grafico a 16 colori

Ogni piano di bit risulta composto da XResolution*YResolution bit;


possiamo affermare quindi che il valore BytesPerScanLine è pari a
XResolution/8.

284
Le specifiche VESA forniscono il supporto per le modalità video
grafiche a 16 colori con risoluzione maggiore o uguale a 800x600; le
risoluzioni inferiori (ad esempio, 640x480) possono essere
programmate con i servizi standard del BIOS illustrati nel precedente
capitolo.
A seconda della risoluzione grafica (ad esempio, 1024x768) la
quantità di memoria richiesta per gestire una schermata supera il
limite dei 64 Kb (per ogni piano di bit) per cui entra in gioco anche
il bank switching; ovviamente, questo aspetto vale per ciascuno dei 4
piani di bit.

Come sappiamo, nelle modalità video grafiche a 16 colori, i 4 bit


assegnati ad ogni pixel rappresentano l'indice relativo ai primi 16
elementi di un vettore di 256 colori; ogni elemento è formato da 3
byte (componenti RGB) di ciascuno dei quali vengono utilizzati solo i
primi 6 bit in modo da ottenere sino a 218=262144 colori.
Se il proprio adattatore video è dotato di compatibilità VGA (Figura
6 e Figura 8), tutti i registri relativi alla gestione dei piani di
bit e della tavolozza dei colori possono essere programmati
attraverso le tecniche illustrate nel precedente capitolo; in caso
contrario, si veda il servizio 4F09h descritto in questo capitolo.

11.6.3 Modalità video grafiche a 256 colori

Nel precedente capitolo abbiamo visto che, nelle modalità video


grafiche a 256 colori, ad ogni pixel vengono assegnati 8 bit della
VRAM; infatti, con 8 bit possiamo codificare 28=256 colori differenti.
I vari gruppi di 8 bit risultano disposti in sequenza nella VRAM per
cui la programmazione di queste modalità grafiche risulta
semplicissima; la Figura 16 illustra questa situazione.

Figura 16 - Accesso ai pixel nel modo grafico a 256 colori

L'unico piano di bit presente risulta composto da


XResolution*YResolution byte; possiamo affermare quindi che il valore
BytesPerScanLine è pari a XResolution.

285
Come risulta dalla Figura 2, le specifiche VESA 3.0 forniscono il
supporto per cinque differenti modalità video grafiche a 256 colori;
le risoluzioni vanno dalla 640x400 alla 1280x1024.
A seconda della risoluzione grafica, la quantità di memoria richiesta
per gestire una schermata supera il limite dei 64 Kb per cui entra in
gioco anche il bank switching; ad esempio, una schermata da 640x480
pixel richiede 640x480=307200 byte di VRAM suddivisi in quasi 5
blocchi da 64 Kb.

Come sappiamo, nelle modalità video grafiche a 256 colori, gli 8 bit
assegnati ad ogni pixel rappresentano l'indice relativo agli elementi
di un vettore di 256 colori; ogni elemento è formato da 3 byte
(componenti RGB) di ciascuno dei quali vengono utilizzati solo i
primi 6 bit in modo da ottenere sino a 218=262144 colori.
Se il proprio adattatore video è dotato di compatibilità VGA (Figura
6 e Figura 8), tutti i registri relativi alla gestione della
tavolozza dei colori possono essere programmati attraverso le
tecniche illustrate nel precedente capitolo; in caso contrario, si
veda il servizio 4F09h descritto in questo capitolo.

11.6.4 Modalità video grafiche High Color

La continua evoluzione tecnologica degli adattatori video ha portato


anche ad un notevole aumento della quantità disponibile di VRAM e ciò
ha reso possibile il supporto di modalità video grafiche capaci di
gestire decine di migliaia di colori contemporaneamente; tali
modalità video sono state definite High Color.

La codifica dei colori nelle modalità High Color si basa sempre sul
concetto di terna RGB; a tale proposito, vengono utilizzati 2 byte (1
word) per ogni pixel secondo lo schema illustrato in Figura 17.

Figura 17 - Rappresentazione di un pixel nel modo High Color

In Figura 17a vediamo la tecnica di codifica di tipo 5:5:5 che


consiste nell'utilizzare 5 bit per ogni componente primaria di
colore; in questo modo, complessivamente, possiamo rappresentare:

25 * 25 * 25 = 25 + 5 + 5 = 215 = 32768 colori differenti.

Osservando che, nella WORD associata ad ogni pixel, rimane 1 bit


libero, si può pensare ad una seconda tecnica di codifica di tipo
5:6:5 come descritto in Figura 17b; in questo modo, complessivamente,
possiamo rappresentare:

286
25 * 26 * 25 = 25 + 6 + 5 = 216 = 65536 colori differenti.

La notevole quantità di VRAM presente negli adattatori video con


supporto High Color permette di memorizzare i colori effettivi
direttamente nella stessa VRAM; in sostanza, la memoria video nelle
modalità High Color è strutturata come un vettore di WORD, ciascuna
delle quali contiene direttamente il colore del relativo pixel.
Ogni schermata High Color risulta composta da
(XResolution*2)*YResolution byte; possiamo affermare quindi che il
valore BytesPerScanLine è pari a XResolution*2.

11.6.5 Modalità video grafiche True Color

L'ulteriore evoluzione tecnologica degli adattatori video ha reso


possibile il supporto di modalità video grafiche capaci di gestire
milioni di colori contemporaneamente; tali modalità video sono state
definite True Color.

La codifica dei colori nelle modalità True Color si basa sempre sul
concetto di terna RGB; a tale proposito, vengono utilizzati 3 byte
per ogni pixel secondo lo schema illustrato in Figura 18.

Figura 18 - Rappresentazione di un pixel nel modo True Color

La tecnica di codifica è di tipo 8:8:8 e consiste nell'utilizzare 8


bit per ogni componente primaria di colore; in questo modo,
complessivamente, possiamo rappresentare:

28 * 28 * 28 = 28 + 8 + 8 = 224 = 16777216 colori differenti.

La notevole quantità di VRAM presente negli adattatori video con


supporto True Color permette di memorizzare i colori effettivi
direttamente nella stessa VRAM; in sostanza, la memoria video nelle
modalità True Color è strutturata come un vettore di terne di BYTE,
ciascuna delle quali contiene direttamente il colore del relativo
pixel.
Ogni schermata True Color risulta composta da
(XResolution*3)*YResolution byte; possiamo affermare quindi che il
valore BytesPerScanLine è pari a XResolution*3.

Esistono anche modalità True Color che prevedono la rappresentazione


del colore di un pixel attraverso 4 byte di cui i primi tre
contengono la solita terna RGB di tipo 8:8:8, mentre il quarto byte è
disponibile per altri usi; in questo caso, ogni schermata risulta
composta da (XResolution*4)*YResolution byte con il valore
BytesPerScanLine che è pari a XResolution*4.

11.7 Esempi pratici

287
Prima di analizzare alcuni esempi pratici è necessario sottolineare
un aspetto importante relativo all'uso degli emulatori DOS; un
emulatore DOS fornisce un proprio supporto VESA che potrebbe anche
risultare differente da quello effettivamente presente nel VBIOS.
Generalmente, la differenza principale riguarda la versione VBE; non
è raro allora che un emulatore DOS fornisca caratteristiche hardware
"virtuali" superiori a quelle effettivamente disponibili.

11.7.1 Visualizzazione di informazioni dettagliate sul supporto VBE

Per programmare una scheda video in modo corretto ed efficiente


attraverso le specifiche VESA è necessario raccogliere innanzi tutto
una serie dettagliata di informazioni relative all'hardware
supportato; il programma di Figura 19 si prefigge proprio di eseguire
tale compito.

Figura 19 - File VESAINFO.ASM


;-----------------------------------------------------;
; file vesainfo.asm ;
; verifica del supporto VESA/VBE da parte del VBIOS ;
;-----------------------------------------------------;
; nasm -f obj vesainfo.asm ;
; tlink vesainfo.obj + exelib.obj ;
; (oppure link vesainfo.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

STRUC VbeInfoBlock

VbeSignature resb 4 ; stringa di identificazione "VESA"


VbeVersion resw 1 ; versione VBE
OemStringPtr resd 1 ; puntatore FAR alla OEM String
Capabilities resb 4 ; caratteristiche hardware
dell'adattatore
VideoModePtr resd 1 ; puntatore FAR alla lista dei modi video
TotalMemory resw 1 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
OemSoftwareRev resw 1 ; codice revisione software VBE
OemVendorNamePtr resd 1 ; puntatore FAR alla stringa del
fabbricante
OemProductNamePtr resd 1 ; puntatore FAR alla stringa del prodotto
OemProductRevPtr resd 1 ; puntatore FAR alla stringa di revisione
Reserved resb 222 ; riservato VBE
OemData resb 256 ; area dati per le OEM Strings

ENDSTRUC

STRUC ModeInfoBlock

ModeAttributes resw 1 ; attributi del modo video


WinAAttributes resb 1 ; attributi finestra A
288
WinBAttributes resb 1 ; attributi finestra B
WinGranularity resw 1 ; granularita' finestre
WinSize resw 1 ; dimensione finestre
WinASegment resw 1 ; segmento di inizio finestra A
WinBSegment resw 1 ; segmento di inizio finestra B
WinFuncPtr resd 1 ; puntatore FAR alla window function
BytesPerScanLine resw 1 ; lunghezza in byte di una scan line
XResolution resw 1 ; risoluzione orizzontale
YResolution resw 1 ; risoluzione verticale
XCharSize resb 1 ; larghezza in pixel cella carattere
YCharSize resb 1 ; altezza in pixel cella carattere
NumberOfPlanes resb 1 ; numero di piani di bit
BitsPerPixel resb 1 ; bit di colore per ogni pixel
NumberOfBanks resb 1 ; numero banchi di memoria video
MemoryModel resb 1 ; modello di memoria
BankSize resb 1 ; dimensione banco di memoria in Kb
NumberOfImgPages resb 1 ; numero di pagine immagine
Reserved1 resb 1 ; riservato (viene settato a 1)
RedMaskSize resb 1 ; dimensione in bit della Red Mask
RedFieldPos resb 1 ; posizione LSB nella Red Mask
GreenMaskSize resb 1 ; dimensione in bit della Green Mask
GreenFieldPos resb 1 ; posizione LSB nella Green Mask
BlueMaskSize resb 1 ; dimensione in bit della Blue Mask
BlueFieldPos resb 1 ; posizione LSB nella Blue Mask
RsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
RsvdFieldPos resb 1 ; posizione LSB nella Reserved Mask
DirectColModeInfo resb 1 ; attributi direct color mode
PhysBasePtr resd 1 ; indirizzo framebuffer (modo lineare)
Reserved2 resd 1 ; riservato (viene settato a 0)
Reserved3 resw 1 ; riservato (viene settato a 0)
LinByteScanLine resw 1 ; byte per scan line (modo lineare)
BnkNumOfImgPages resb 1 ; numero pagine immagine (modo reale)
LinNumOfImgPages resb 1 ; numero pagine immagine (modo lineare)
LinRedMaskSize resb 1 ; dimensione in bit della Red Mask (modo
lineare)
LinRedFieldPos resb 1 ; posizione LSB nella Red Mask (modo
lineare)
LinGreenMaskSize resb 1 ; dimensione in bit della Green Mask
(modo lineare)
LinGreenFieldPos resb 1 ; posizione LSB nella Green Mask (modo
lineare)
LinBlueMaskSize resb 1 ; dimensione in bit della Blue Mask (modo
lineare)
LinBlueFieldPos resb 1 ; posizione LSB nella Blue Mask (modo
lineare)
LinRsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
(linear)
LinRsvFieldPos resb 1 ; posizione LSB nella Reserved Mask
(linear)
MaxPixelClock resd 1 ; max. pixel clock in Hz per il modo
grafico
Reserved4 resb 190 ; rende la struttura da 256 byte

ENDSTRUC

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

info_block ISTRUC VbeInfoBlock


289
at VbeSignature, resb 4 ; stringa di identificazione "VESA"
at VbeVersion, dw 0 ; versione VBE
at OemStringPtr, dd 0 ; puntatore FAR alla OEM String
at Capabilities, resb 4 ; caratteristiche hardware
dell'adattatore
at VideoModePtr, dd 0 ; puntatore FAR alla lista dei modi video
at TotalMemory, dw 0 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
at OemSoftwareRev, dw 0 ; codice revisione software VBE
at OemVendorNamePtr, dd 0 ; puntatore FAR alla stringa del
fabbricante
at OemProductNamePtr,dd 0 ; puntatore FAR alla stringa del prodotto
at OemProductRevPtr, dd 0 ; puntatore FAR alla stringa di revisione
at Reserved, resb 222 ; riservato VBE
at OemData, resb 256 ; area dati per le OEM Strings

IEND

mode_info ISTRUC ModeInfoBlock

at ModeAttributes, dw 0 ; attributi del modo video


at WinAAttributes, db 0 ; attributi finestra A
at WinBAttributes, db 0 ; attributi finestra B
at WinGranularity, dw 0 ; granularita' finestre
at WinSize, dw 0 ; dimensione finestre
at WinASegment, dw 0 ; segmento di inizio finestra A
at WinBSegment, dw 0 ; segmento di inizio finestra B
at WinFuncPtr, dd 0 ; puntatore FAR alla window function
at BytesPerScanLine, dw 0 ; lunghezza in byte di una scan line
at XResolution, dw 0 ; risoluzione orizzontale
at YResolution, dw 0 ; risoluzione verticale
at XCharSize, db 0 ; larghezza in pixel cella carattere
at YCharSize, db 0 ; altezza in pixel cella carattere
at NumberOfPlanes, db 0 ; numero di piani di bit
at BitsPerPixel, db 0 ; bit di colore per ogni pixel
at NumberOfBanks, db 0 ; numero banchi di memoria video
at MemoryModel, db 0 ; modello di memoria
at BankSize, db 0 ; dimensione banco di memoria in Kb
at NumberOfImgPages, db 0 ; numero di pagine immagine
at Reserved1, db 1 ; riservato (viene settato a 1)
at RedMaskSize, db 0 ; dimensione in bit della Red Mask
at RedFieldPos, db 0 ; posizione LSB nella Red Mask
at GreenMaskSize, db 0 ; dimensione in bit della Green Mask
at GreenFieldPos, db 0 ; posizione LSB nella Green Mask
at BlueMaskSize, db 0 ; dimensione in bit della Blue Mask
at BlueFieldPos, db 0 ; posizione LSB nella Blue Mask
at RsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
at RsvdFieldPos, db 0 ; posizione LSB nella Reserved Mask
at DirectColModeInfo,db 0 ; attributi direct color mode
at PhysBasePtr, dd 0 ; indirizzo framebuffer (modo lineare)
at Reserved2, dd 0 ; riservato (viene settato a 0)
at Reserved3, dw 0 ; riservato (viene settato a 0)
at LinByteScanLine, dw 0 ; byte per scan line (modo lineare)
at BnkNumOfImgPages, db 0 ; numero pagine immagine (modo reale)
at LinNumOfImgPages, db 0 ; numero pagine immagine (modo lineare)
at LinRedMaskSize, db 0 ; dimensione in bit della Red Mask (modo
lineare)
at LinRedFieldPos, db 0 ; posizione LSB nella Red Mask (modo
lineare)
at LinGreenMaskSize, db 0 ; dimensione in bit della Green Mask
(modo lineare)
290
at LinGreenFieldPos, db 0 ; posizione LSB nella Green Mask (modo
lineare)
at LinBlueMaskSize, db 0 ; dimensione in bit della Blue Mask (modo
lineare)
at LinBlueFieldPos, db 0 ; posizione LSB nella Blue Mask (modo
lineare)
at LinRsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
(linear)
at LinRsvFieldPos, db 0 ; posizione LSB nella Reserved Mask
(linear)
at MaxPixelClock, dd 0 ; max. pixel clock in Hz per il modo
grafico
at Reserved4, resb 190 ; rende la struttura da 256 byte

IEND

str_title db "VERIFICA PRESENZA SUPPORTO VESA/VBE NEL VBIOS", 0


str_vesaok db "Il VBIOS supporta le specifiche VESA/VBE versione
XXh:XXh", 0
str_vesako db "Errore! Supporto VESA/VBE assente!", 0
str_vesaerr db "VBE Status Error!", 0
str_vbeoem db "Stringa OEM:", 0
str_capabil db "Capabilities:", 0
str_totmem db "VRAM totale: 00000 * 64 Kb:", 0
str_waitkey db "Premere un tasto per continuare ... ", 0
str_vmode db "Modalita' video XXXXh", 0
str_mattrib db "Attributi modo video:", 0
str_waattrib db "Attributi Window A:", 0
str_wbattrib db "Attributi Window B:", 0
str_wgran db "Granularita' finestra:", 0
str_wsize db "Dimensione finestra:", 0
str_waseg db "Segmento Window A:", 0
str_wbseg db "Segmento Window B:", 0
str_wfuncptr db "Indirizzo win func:", 0
str_bps db "Byte per scan line:", 0
str_xresol db "Risoluzione asse X:", 0
str_yresol db "Risoluzione asse Y:", 0
str_xchar db "Larghezza char cell:", 0
str_ychar db "Altezza char cell:", 0
str_numplanes db "Num. piani di bit:", 0
str_bitpixel db "Bit per pixel:", 0
str_memmodel db "Modello di memoria:", 0
str_numbanks db "Num. di banchi:", 0
str_banksize db "Dim. di un banco (Kb):", 0
str_numimgpag db "Num. schermate fb:", 0
str_rgbmask db "RGB mask size:", 0
str_rgbfield db "RGB field pos:", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


291
call far clearScreen ; pulisce lo schermo

; ES = DS per il corretto accesso alle strutture info_block e mode_info

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 0010h ; riga 0, colonna 16
call far writeString ; mostra la stringa

; verifica se le specifiche VESA/VBE sono supportate

mov ax, 4F00h ; servizio Return VBE Controller Info.


mov di, info_block ; ES:DI punta a info_block
mov dx, 0200h ; riga 2, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vesa_ok ; supporto VESA/VBE presente
mov di, str_vesako ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_program ; termina il programma

vesa_ok:
mov di, str_vesaok ; ES:DI punta alla stringa
call far writeString ; mostra la stringa

; visualizza la versione VBE

mov al, [info_block + VbeVersion + 1]


mov dx, 0232h ; riga 2, colonna 50
call far writeHex8 ; mostra VbeVersion major num.
mov al, [info_block + VbeVersion + 0]
mov dx, 0236h ; riga 2, colonna 54
call far writeHex8 ; mostra VbeVersion minor num.

; visualizza la OEM String

mov di, str_vbeoem ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa

les di, [info_block + OemStringPtr]


mov dx, 040Eh ; riga 4, colonna 14
call far writeString ; mostra la OEM String

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza le caratteristiche grafiche supportate

mov di, str_capabil ; ES:DI punta alla stringa


mov dx, 0500h ; riga 5, colonna 0
call far writeString ; mostra la stringa

mov eax, [info_block + Capabilities]


mov dx, 050Eh ; riga 5, colonna 14
call far writeBin32 ; mostra Capabilities

; visualizza la VRAM totale in blocchi da 64 Kb


292
mov di, str_totmem ; ES:DI punta alla stringa
mov dx, 0600h ; riga 6, colonna 0
call far writeString ; mostra la stringa

mov ax, [info_block + TotalMemory]


mov dx, 060Eh ; riga 6, colonna 14
call far writeUdec16 ; mostra TotalMemory

push ds ; copia DS
pop es ; in ES (per writeString)

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; loop caratteristiche modalita' video - FS:SI punta a VideoModePtr

lfs si, [info_block + VideoModePtr]

modeinfo_loop:

call far clearScreen ; pulisce lo schermo

mov ax, 4F01h ; servizio Return VBE Mode Info.


mov cx, [fs:si] ; CX = codice modo video
mov di, mode_info ; ES:DI punta a mode_info
mov dx, 0200h ; riga 2, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vbestatus_ok ; VBE Status OK
mov di, str_vesaerr ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto
jmp exit_program ; termina il programma

vbestatus_ok:

mov di, str_vmode ; ES:DI punta alla stringa


mov dx, 0000h ; riga 0, colonna 0
call far writeString ; mostra la stringa

mov ax, [fs:si] ; AX = codice modo video


mov dx, 0010h ; riga 0, colonna 15
call far writeHex16 ; mostra codice modo video

mov di, str_mattrib ; ES:DI punta alla stringa


mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + ModeAttributes]


mov dx, 0218h ; riga 2, colonna 24
call far writeBin16 ; mostra ModeAttributes

mov di, str_waattrib ; ES:DI punta alla stringa


mov dx, 0300h ; riga 3, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + WinAAttributes]


mov dx, 0318h ; riga 3, colonna 24
call far writeBin8 ; mostra WinAAttributes
293
mov di, str_wbattrib ; ES:DI punta alla stringa
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + WinBAttributes]


mov dx, 0418h ; riga 3, colonna 24
call far writeBin8 ; mostra WinBAttributes

mov di, str_wgran ; ES:DI punta alla stringa


mov dx, 0500h ; riga 5, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + WinGranularity]


mov dx, 0518h ; riga 5, colonna 24
call far writeUdec16 ; mostra WinGranularity

mov di, str_wsize ; ES:DI punta alla stringa


mov dx, 0600h ; riga 6, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + WinSize]


mov dx, 0618h ; riga 6, colonna 24
call far writeUdec16 ; mostra WinSize

mov di, str_waseg ; ES:DI punta alla stringa


mov dx, 0700h ; riga 7, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + WinASegment]


mov dx, 0718h ; riga 7, colonna 24
call far writeHex16 ; mostra WinASegment

mov di, str_wbseg ; ES:DI punta alla stringa


mov dx, 0800h ; riga 8, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + WinBSegment]


mov dx, 0818h ; riga 8, colonna 24
call far writeHex16 ; mostra WinBSegment

mov di, str_wfuncptr ; ES:DI punta alla stringa


mov dx, 0900h ; riga 9, colonna 0
call far writeString ; mostra la stringa

mov eax, [mode_info + WinFuncPtr]


mov dx, 0918h ; riga 9, colonna 24
call far writeHex32 ; mostra WinFuncPtr

mov di, str_bps ; ES:DI punta alla stringa


mov dx, 0A00h ; riga 10, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + BytesPerScanLine]


mov dx, 0A18h ; riga 10, colonna 24
call far writeUdec16 ; mostra BytesPerScanLine

mov di, str_xresol ; ES:DI punta alla stringa


mov dx, 0B00h ; riga 11, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + XResolution]


294
mov dx, 0B18h ; riga 11, colonna 24
call far writeUdec16 ; mostra XResolution

mov di, str_yresol ; ES:DI punta alla stringa


mov dx, 0C00h ; riga 12, colonna 0
call far writeString ; mostra la stringa

mov ax, [mode_info + YResolution]


mov dx, 0C18h ; riga 12, colonna 24
call far writeUdec16 ; mostra YResolution

mov di, str_xchar ; ES:DI punta alla stringa


mov dx, 0D00h ; riga 13, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + XCharSize]


mov dx, 0D18h ; riga 13, colonna 24
call far writeUdec8 ; mostra XCharSize

mov di, str_ychar ; ES:DI punta alla stringa


mov dx, 0E00h ; riga 14, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + YCharSize]


mov dx, 0E18h ; riga 14, colonna 24
call far writeUdec8 ; mostra YCharSize

mov di, str_numplanes ; ES:DI punta alla stringa


mov dx, 0F00h ; riga 15, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + NumberOfPlanes]


mov dx, 0F18h ; riga 15, colonna 24
call far writeUdec8 ; mostra NumberOfPlanes

mov di, str_bitpixel ; ES:DI punta alla stringa


mov dx, 1000h ; riga 16, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + BitsPerPixel]


mov dx, 1018h ; riga 16, colonna 24
call far writeUdec8 ; mostra BitsperPixel

mov di, str_memmodel ; ES:DI punta alla stringa


mov dx, 1100h ; riga 17, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + MemoryModel]


mov dx, 1118h ; riga 17, colonna 24
call far writeHex8 ; mostra MemoryModel

mov di, str_numbanks ; ES:DI punta alla stringa


mov dx, 1200h ; riga 18, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + NumberOfBanks]


mov dx, 1218h ; riga 18, colonna 24
call far writeUdec8 ; mostra NumberOfBanks

mov di, str_banksize ; ES:DI punta alla stringa


mov dx, 1300h ; riga 19, colonna 0
call far writeString ; mostra la stringa
295
mov al, [mode_info + BankSize]
mov dx, 1318h ; riga 19, colonna 24
call far writeUdec8 ; mostra BankSize

mov di, str_numimgpag ; ES:DI punta alla stringa


mov dx, 1400h ; riga 20, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + NumberOfImgPages]


mov dx, 1418h ; riga 20, colonna 24
call far writeUdec8 ; mostra NumberOfImgPages

mov di, str_rgbmask ; ES:DI punta alla stringa


mov dx, 1500h ; riga 21, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + RedMaskSize]


mov dx, 1518h ; riga 21, colonna 24
call far writeUdec8 ; mostra RedMaskSize
mov al, [mode_info + GreenMaskSize]
mov dx, 151Ch ; riga 21, colonna 28
call far writeUdec8 ; mostra GreenMaskSize
mov al, [mode_info + BlueMaskSize]
mov dx, 1520h ; riga 21, colonna 32
call far writeUdec8 ; mostra BlueMaskSize

mov di, str_rgbfield ; ES:DI punta alla stringa


mov dx, 1600h ; riga 22, colonna 0
call far writeString ; mostra la stringa

mov al, [mode_info + RedFieldPos]


mov dx, 1618h ; riga 22, colonna 24
call far writeUdec8 ; mostra RedFieldPos
mov al, [mode_info + GreenFieldPos]
mov dx, 161Ch ; riga 22, colonna 28
call far writeUdec8 ; mostra GreenFieldPos
mov al, [mode_info + BlueFieldPos]
mov dx, 1620h ; riga 22, colonna 32
call far writeUdec8 ; mostra BlueFieldPos

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

add si, 2 ; incremento puntatore


cmp word [fs:si], 0FFFFh ; fine lista ?
jne modeinfo_loop

exit_program:

call far clearScreen ; pulisce lo schermo


call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------


296
;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################

Il programma inizia chiamando il servizio 4F00h per riempire la


struttura VbeInfoBlock; se la chiamata non restituisce un codice di
errore, vengono visualizzate le principali informazioni contenute
nella struttura stessa.

Successivamente, il programma si serve del puntatore VideoModePtr per


attivare un loop dal quale si esce quando viene incontrato il valore
FFFFh (fine lista delle modalità video); per ogni modalità video,
vengono mostrate tutte le informazioni principali restituite nella
struttura ModeInfoBlock dal servizio 4F01h.

11.7.2 Trasferimento dati in VRAM nei modi video a 256 colori

Vediamo ora un programma che utilizza REP STOSD per riempire con
differenti colori i vari blocchi da 64 Kb che compongono una
schermata grafica a 256 colori (modalità video standard con
BitsPerPixel=8); la Figura 20 mostra il relativo listato.

Figura 20 - File VESAWIN.ASM


;-----------------------------------------------------;
; file vesawin.asm ;
; riempimento dei blocchi da 64 Kb nei modi video ;
; a 256 colori (8 bit per pixel) ;
;-----------------------------------------------------;
; nasm -f obj vesawin.asm ;
; tlink vesawin.obj + exelib.obj ;
; (oppure link vesawin.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

%assign VESA_MODE 0101h ; modo video richiesto


%assign WSIZE_DWORD 16384 ; dimensione finestra in DWORD
%assign WIN_FUNC 1 ; utilizzo win func

STRUC VbeInfoBlock

VbeSignature resb 4 ; stringa di identificazione "VESA"


VbeVersion resw 1 ; versione VBE
OemStringPtr resd 1 ; puntatore FAR alla OEM String

297
Capabilities resb 4 ; caratteristiche hardware
dell'adattatore
VideoModePtr resd 1 ; puntatore FAR alla lista dei modi video
TotalMemory resw 1 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
OemSoftwareRev resw 1 ; codice revisione software VBE
OemVendorNamePtr resd 1 ; puntatore FAR alla stringa del
fabbricante
OemProductNamePtr resd 1 ; puntatore FAR alla stringa del prodotto
OemProductRevPtr resd 1 ; puntatore FAR alla stringa di revisione
Reserved resb 222 ; riservato VBE
OemData resb 256 ; area dati per le OEM Strings

ENDSTRUC

STRUC ModeInfoBlock

ModeAttributes resw 1 ; attributi del modo video


WinAAttributes resb 1 ; attributi finestra A
WinBAttributes resb 1 ; attributi finestra B
WinGranularity resw 1 ; granularita' finestre
WinSize resw 1 ; dimensione finestre
WinASegment resw 1 ; segmento di inizio finestra A
WinBSegment resw 1 ; segmento di inizio finestra B
WinFuncPtr resd 1 ; puntatore FAR alla window function
BytesPerScanLine resw 1 ; lunghezza in byte di una scan line
XResolution resw 1 ; risoluzione orizzontale
YResolution resw 1 ; risoluzione verticale
XCharSize resb 1 ; larghezza in pixel cella carattere
YCharSize resb 1 ; altezza in pixel cella carattere
NumberOfPlanes resb 1 ; numero di piani di bit
BitsPerPixel resb 1 ; bit di colore per ogni pixel
NumberOfBanks resb 1 ; numero banchi di memoria video
MemoryModel resb 1 ; modello di memoria
BankSize resb 1 ; dimensione banco di memoria in Kb
NumberOfImgPages resb 1 ; numero di pagine immagine
Reserved1 resb 1 ; riservato (viene settato a 1)
RedMaskSize resb 1 ; dimensione in bit della Red Mask
RedFieldPos resb 1 ; posizione LSB nella Red Mask
GreenMaskSize resb 1 ; dimensione in bit della Green Mask
GreenFieldPos resb 1 ; posizione LSB nella Green Mask
BlueMaskSize resb 1 ; dimensione in bit della Blue Mask
BlueFieldPos resb 1 ; posizione LSB nella Blue Mask
RsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
RsvdFieldPos resb 1 ; posizione LSB nella Reserved Mask
DirectColModeInfo resb 1 ; attributi direct color mode
PhysBasePtr resd 1 ; indirizzo framebuffer (modo lineare)
Reserved2 resd 1 ; riservato (viene settato a 0)
Reserved3 resw 1 ; riservato (viene settato a 0)
LinByteScanLine resw 1 ; byte per scan line (modo lineare)
BnkNumOfImgPages resb 1 ; numero pagine immagine (modo reale)
LinNumOfImgPages resb 1 ; numero pagine immagine (modo lineare)
LinRedMaskSize resb 1 ; dimensione in bit della Red Mask (modo
lineare)
LinRedFieldPos resb 1 ; posizione LSB nella Red Mask (modo
lineare)
LinGreenMaskSize resb 1 ; dimensione in bit della Green Mask
(modo lineare)
LinGreenFieldPos resb 1 ; posizione LSB nella Green Mask (modo
lineare)
LinBlueMaskSize resb 1 ; dimensione in bit della Blue Mask (modo
lineare)
298
LinBlueFieldPos resb 1 ; posizione LSB nella Blue Mask (modo
lineare)
LinRsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
(linear)
LinRsvFieldPos resb 1 ; posizione LSB nella Reserved Mask
(linear)
MaxPixelClock resd 1 ; max. pixel clock in Hz per il modo
grafico
Reserved4 resb 190 ; rende la struttura da 256 byte

ENDSTRUC

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

info_block ISTRUC VbeInfoBlock

at VbeSignature, resb 4 ; stringa di identificazione "VESA"


at VbeVersion, dw 0 ; versione VBE
at OemStringPtr, dd 0 ; puntatore FAR alla OEM String
at Capabilities, resb 4 ; caratteristiche hardware
dell'adattatore
at VideoModePtr, dd 0 ; puntatore FAR alla lista dei modi video
at TotalMemory, dw 0 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
at OemSoftwareRev, dw 0 ; codice revisione software VBE
at OemVendorNamePtr, dd 0 ; puntatore FAR alla stringa del
fabbricante
at OemProductNamePtr,dd 0 ; puntatore FAR alla stringa del prodotto
at OemProductRevPtr, dd 0 ; puntatore FAR alla stringa di revisione
at Reserved, resb 222 ; riservato VBE
at OemData, resb 256 ; area dati per le OEM Strings

IEND

mode_info ISTRUC ModeInfoBlock

at ModeAttributes, dw 0 ; attributi del modo video


at WinAAttributes, db 0 ; attributi finestra A
at WinBAttributes, db 0 ; attributi finestra B
at WinGranularity, dw 0 ; granularita' finestre
at WinSize, dw 0 ; dimensione finestre
at WinASegment, dw 0 ; segmento di inizio finestra A
at WinBSegment, dw 0 ; segmento di inizio finestra B
at WinFuncPtr, dd 0 ; puntatore FAR alla window function
at BytesPerScanLine, dw 0 ; lunghezza in byte di una scan line
at XResolution, dw 0 ; risoluzione orizzontale
at YResolution, dw 0 ; risoluzione verticale
at XCharSize, db 0 ; larghezza in pixel cella carattere
at YCharSize, db 0 ; altezza in pixel cella carattere
at NumberOfPlanes, db 0 ; numero di piani di bit
at BitsPerPixel, db 0 ; bit di colore per ogni pixel
at NumberOfBanks, db 0 ; numero banchi di memoria video
at MemoryModel, db 0 ; modello di memoria
at BankSize, db 0 ; dimensione banco di memoria in Kb
at NumberOfImgPages, db 0 ; numero di pagine immagine
at Reserved1, db 1 ; riservato (viene settato a 1)
at RedMaskSize, db 0 ; dimensione in bit della Red Mask
at RedFieldPos, db 0 ; posizione LSB nella Red Mask
299
at GreenMaskSize, db 0 ; dimensione in bit della Green Mask
at GreenFieldPos, db 0 ; posizione LSB nella Green Mask
at BlueMaskSize, db 0 ; dimensione in bit della Blue Mask
at BlueFieldPos, db 0 ; posizione LSB nella Blue Mask
at RsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
at RsvdFieldPos, db 0 ; posizione LSB nella Reserved Mask
at DirectColModeInfo,db 0 ; attributi direct color mode
at PhysBasePtr, dd 0 ; indirizzo framebuffer (modo lineare)
at Reserved2, dd 0 ; riservato (viene settato a 0)
at Reserved3, dw 0 ; riservato (viene settato a 0)
at LinByteScanLine, dw 0 ; byte per scan line (modo lineare)
at BnkNumOfImgPages, db 0 ; numero pagine immagine (modo reale)
at LinNumOfImgPages, db 0 ; numero pagine immagine (modo lineare)
at LinRedMaskSize, db 0 ; dimensione in bit della Red Mask (modo
lineare)
at LinRedFieldPos, db 0 ; posizione LSB nella Red Mask (modo
lineare)
at LinGreenMaskSize, db 0 ; dimensione in bit della Green Mask
(modo lineare)
at LinGreenFieldPos, db 0 ; posizione LSB nella Green Mask (modo
lineare)
at LinBlueMaskSize, db 0 ; dimensione in bit della Blue Mask (modo
lineare)
at LinBlueFieldPos, db 0 ; posizione LSB nella Blue Mask (modo
lineare)
at LinRsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
(linear)
at LinRsvFieldPos, db 0 ; posizione LSB nella Reserved Mask
(linear)
at MaxPixelClock, dd 0 ; max. pixel clock in Hz per il modo
grafico
at Reserved4, resb 190 ; rende la struttura da 256 byte

IEND

win_color dd 04040404h ; colore iniziale


write_segm dw 0 ; Seg writeable Window
last_winsize dw 0 ; dimensione ultimo blocco
num_windows dw 0 ; numero finestre in una schermata
num_winbank dw 0 ; numero banchi in una finestra
bank_shift dw 0 ; numero di shift per il bank switching
curr_bank dw 0 ; banco corrente
bank_num dw 0 ; banco da commutare

str_title db "RIEMPIMENTO DEI BLOCCHI DA 64 Kb NEL MODO XXXXh", 0


str_waitkey db "Premere un tasto per continuare ... ", 0
str_vesaok db "Il VBIOS supporta le specifiche VESA/VBE versione
XXh:XXh", 0
str_vesako db "Errore! Supporto VESA/VBE assente!", 0
str_vesaerr db "VBE Status Error!", 0
str_modeko db "Errore! Modo video non supportato!", 0
str_modesupko db "Errore! Modo video supportato ma non attivabile!", 0
str_winko db "Errore! Il modo video non supporta le finestre!", 0
str_wfuncko db "Errore! Window Function non supportata!", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point


300
mov ax, DATASEGM ; trasferisce DATASEGM
mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

; ES = DS per il corretto accesso alle strutture info_block e mode_info

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 0010h ; riga 0, colonna 16
call far writeString ; mostra la stringa
mov ax, VESA_MODE ; AX = codice modo video
mov dx, 003Ah ; riga 0, colonna 58
call far writeHex16 ; mostra il modo video

; verifica se le specifiche VESA/VBE sono supportate

mov ax, 4F00h ; servizio Return VBE Controller Info.


mov di, info_block ; ES:DI punta a info_block
mov dx, 0200h ; riga 2, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vesa_ok ; supporto VESA/VBE presente
mov di, str_vesako ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vesa_ok:
mov di, str_vesaok ; ES:DI punta alla stringa
call far writeString ; mostra la stringa

; visualizza la versione VBE

mov al, [info_block + VbeVersion + 1]


mov dx, 0232h ; riga 2, colonna 50
call far writeHex8 ; mostra VbeVersion major num.
mov al, [info_block + VbeVersion + 0]
mov dx, 0236h ; riga 2, colonna 54
call far writeHex8 ; mostra VbeVersion minor num.

; verifica se il modo video VESA_MODE e' supportato (FS:SI punta a VideoModePtr)

lfs si, [info_block + VideoModePtr]

testmode_loop:
mov ax, [fs:si] ; AX = codice modo video
cmp ax, VESA_MODE ; AX == VESA_MODE ?
je vesamode_ok ; si
add si, 2 ; incremento puntatore
cmp ax, 0FFFFh ; fine lista ?
jne testmode_loop ; no

mov di, str_modeko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
301
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vesamode_ok:

; verifica supporto hardware (modo video, bit per pixel e finestre)

mov ax, 4F01h ; servizio Return VBE Mode Info.


mov cx, VESA_MODE ; CX = codice modo video
mov di, mode_info ; ES:DI punta a mode_info
mov dx, 0400h ; riga 4, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vbestatus_ok ; VBE Status OK
mov di, str_vesaerr ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vbestatus_ok:

; verifica supporto hardware modo video

mov ax, [mode_info + ModeAttributes]


test al, 01h ; supporto hardware del modo video ?
jnz test_bitperpixel ; si

mov di, str_modesupko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

test_bitperpixel:

; verifica se BitsPerPixel = 8

mov al, [mode_info + BitsPerPixel]


cmp al, 8 ; 8 bit per pixel ?
je test_window ; si

mov di, str_bpsko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

test_window:

; verifica supporto finestre

mov al, [mode_info + WinAAttributes]


or al, [mode_info + WinBAttributes]
test al, 01h ; finestre supportate ?
jnz window_ok ; si

mov di, str_winko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

window_ok:

; determina la writeable window

302
mov ax, [mode_info + WinASegment]
mov [write_segm], ax ; assume WinA scrivibile
mov al, [mode_info + WinAAttributes]
test al, 00000001b ; finestra A supportata ?
jz test_winb ; no
test al, 00000100b ; finestra A scrivibile ?
jnz end_wintest ; si
test_winb:
mov ax, [mode_info + WinBSegment]
mov [write_segm], ax ; assume WinB scrivibile

end_wintest:

; calcola il numero di banchi in una finestra da 64 Kb

xor dx, dx ; DX = 0 per la divisione


mov ax, [mode_info + WinSize]
mov bx, [mode_info + WinGranularity]
div bx ; AX = AX / BX
mov [num_winbank], ax ; salva il risultato

; calcola il numero di bank shift

mov word [bank_shift], 0 ; num. shift per win_bank


numshift_loop:
mov ax, 64 ; AX = WinSize (in Kb)
mov cl, [bank_shift] ; CL = numero di shift a destra
shr ax, cl ; AX = AX / (2^bank_shift)
cmp ax, [mode_info + WinGranularity] ; AX == WinGranularity ?
je exit_shiftloop
inc word [bank_shift] ; incrementa bank_shift
jmp numshift_loop ; ripeti il loop

exit_shiftloop:

; calcola il numero di finestre in una schermata

movzx eax, word [mode_info + XResolution]


movzx ebx, word [mode_info + YResolution]
mul ebx ; EAX = EAX * EBX
mov ecx, eax ; salva in ECX
shr ecx, 16 ; ECX = ECX / 65536 = num win
mov [num_windows], cx ; salva il risultato
and eax, 0000FFFFh ; dimensione ultimo blocco
mov [last_winsize], ax ; salva il risultato

; verifica se la Window Function è supportata


; Questa porzione di codice viene assemblata solo se WIN_FUNC e' stata
dichiarata.
; Commentare la dichiarazione di WIN_FUNC per usare la INT 10h.

%IFDEF WIN_FUNC

mov eax, [mode_info + WinFuncPtr]


test eax, eax ; puntatore a NULL ?
jnz winfunc_ok ; no
mov di, str_wfuncko ; ES:DI punta alla stringa
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

winfunc_ok:
303
%ENDIF

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; tenta di attivare la modalita' video richiesta

mov ax, 4F02h ; servizio Set VBE Mode


mov bx, VESA_MODE ; BX = codice modo video
mov dx, 0400h ; riga 4, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vbesetmode_ok ; VBE Status OK
mov di, str_vesaerr ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vbesetmode_ok:

mov es, [write_segm] ; ES = Seg write window


cld ; DF = 0 (incremento puntatori)

fillwindow_loop:

; bank switching

mov dx, [curr_bank] ; DX = banco corrente


mov cl, [bank_shift] ; fattore di shifting
shl dx, cl ; DX = banco da commutare
mov [bank_num], dx ; salva il risultato
%IFDEF WIN_FUNC
mov bx, 0000h ; posizionamento Window A
mov dx, [bank_num] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
mov bx, 0001h ; posizionamento Window B
mov dx, [bank_num] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
%ELSE
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0000h ; posizionamento Window A
mov dx, [bank_num] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0001h ; posizionamento Window B
mov dx, [bank_num] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
%ENDIF

mov ax, [num_winbank] ; AX = dim. finestra in banchi


add [curr_bank], ax ; aggiorna il banco corrente

xor di, di ; ES:DI punta all'inizio della finestra


mov eax, [win_color] ; colore da usare (4 pixel)
mov cx, WSIZE_DWORD ; numero di loop (in DWORD)
rep stosd ; trasferisce EAX in VRAM

add dword [win_color], 01010101h ; nuovo colore


dec word [num_windows] ; decremento contatore
jnz fillwindow_loop ; controllo loop
304
; riempimento ultima finestra (minore o uguale a 64 Kb)

mov dx, [curr_bank] ; DX = banco corrente


mov cl, [bank_shift] ; fattore di shifting
shl dx, cl ; DX = banco da commutare
mov [bank_num], dx ; salva il risultato
%IFDEF WIN_FUNC
mov bx, 0000h ; posizionamento Window A
mov dx, [bank_num] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
mov bx, 0001h ; posizionamento Window B
mov dx, [bank_num] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
%ELSE
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0000h ; posizionamento Window A
mov dx, [bank_num] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0001h ; posizionamento Window B
mov dx, [bank_num] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
%ENDIF

xor di, di ; ES:DI punta all'inizio della finestra


mov al, [win_color] ; colore da usare (1 pixel)
mov cx, [last_winsize] ; numero di loop (in BYTE)
rep stosb ; trasferisce AL in VRAM

call far waitChar ; attende la pressione di un tasto

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 03h ; AL = modo testo 80x25 a 16 colori
int 10h ; chiama i servizi video del BIOS

jmp exit_program ; terminazione normale del programma

exit_on_error:

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

exit_program:

call far clearScreen ; pulisce lo schermo


call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

305
SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Inizialmente il programma effettua una serie di controlli per
verificare che tutto sia a posto; in particolare, viene verificata la
presenza del supporto VESA, il supporto hardware del modo video
richiesto e la presenza del frame buffer a finestre.
Se tutti i controlli forniscono esito positivo, si procede con il
calcolo di una serie di parametri da utilizzare nel seguito del
programma; in particolare, viene determinato il numero di banchi che
compongono una finestra, il numero di finestre in una schermata e il
valore bank_shift di cui si è parlato in precedenza.
Successivamente, viene attivata la modalità video richiesta e si
avvia un loop attraverso il quale si procede al riempimento dei vari
blocchi da 64 Kb con differenti colori; si noti che, in generale,
l'ultimo blocco ha una dimensione che è sempre minore o uguale a 64
Kb.

Osservando l'output che questo programma produce con certe


risoluzioni grafiche, si nota chiaramente che ogni finestra da 64 Kb
contiene un numero non intero di scan line; ciò accade quando la
XResolution non è esprimibile come potenza intera di 2.
Ad esempio, per la risoluzione 640x480, ogni scan line occupa 640
byte, per cui il numero totale di scan line contenuto in una finestra
da 64 Kb sarà:

65536 / 640 = 102.4

Un altro aspetto da notare nel listato del programma è la presenza di


direttive %IFDEF, %ELSE e %ENDIF; grazie a tali direttive è possibile
assemblare determinate porzioni di programma al posto di altre
(assemblaggio condizionale).
In particolare, lo scopo di queste direttive nel listato di Figura 20
è legato alla necessità di impiegare preferibilmente la window
function al posto della INT 10h per tutte le operazioni di bank
switching (evitando l'uso pesante di istruzioni per il controllo del
flusso); nel caso di hardware grafico che non supporta la window
function, basta commentare la dichiarazione della costante simbolica
WIN_FUNC all'inizio del programma per fare in modo che venga usata
automaticamente la INT 10h.

Nota importante.
Il programma di Figura 20 è interamente parametrizzato per
cui funziona perfettamente anche con altre modalità video
grafiche a 256 colori; ad esempio, oltre alla modalità 0101h
si possono anche testare le modalità 0103h, 0105h e 0107h.
Si tenga presente però che, come è stato già sottolineato,
certe modalità grafiche supportate dalla propria scheda video
potrebbero non essere attivabili a causa, soprattutto, delle
limitazioni del monitor; in particolare, l'attivazione di
determinate modalità grafiche ad alta risoluzione spesso
richiede l'impostazione di valori personalizzati per i
parametri hardware utilizzati dal monitor (frequenza di
306
refresh, interlacciamento, sincronizzazione orizzontale e
verticale, etc).
Si tratta di aspetti molto complessi che vengono gestiti
direttamente dai SO; si raccomanda quindi di evitare
impostazioni azzardate a meno che si abbiano le idee chiare
su ciò che si intende fare (il documento VBE3.PDF,
disponibile nella sezione Downloads, illustra numerosi
dettagli su questo aspetto).

Molti moderni monitor sono in grado di adattarsi


automaticamente alla modalità grafica impostata dall'utente
(nel senso che il monitor imposta automaticamente tutti i
parametri hardware descritti in precedenza); se il monitor
non riesce ad adattarsi alla nuova modalità grafica
selezionata, mostra una finestra di errore con un messaggio
del tipo out of synch!

11.7.3 Accesso ai pixel nelle modalità grafiche True Color

Vediamo ora un programma che ha lo scopo di evidenziare un importante


aspetto relativo alle modalità video grafiche True Color con
BitsPerPixel=24; la Figura 21 mostra il relativo listato.

Figura 21 - File VESAPIX.ASM


;-----------------------------------------------------;
; file vesapix.asm ;
; accesso ai pixel nelle modalita' grafiche ;
; True Color ;
;-----------------------------------------------------;
; nasm -f obj vesapix.asm ;
; tlink vesapix.obj + exelib.obj ;
; (oppure link vesapix.obj + exelib.obj) ;
;-----------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

%assign VESA_MODE 0142h ; modo video richiesto


%assign WIN_FUNC 1 ; utilizzo win func

STRUC VbeInfoBlock

VbeSignature resb 4 ; stringa di identificazione "VESA"


VbeVersion resw 1 ; versione VBE
OemStringPtr resd 1 ; puntatore FAR alla OEM String
Capabilities resb 4 ; caratteristiche hardware
dell'adattatore
VideoModePtr resd 1 ; puntatore FAR alla lista dei modi video
TotalMemory resw 1 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
OemSoftwareRev resw 1 ; codice revisione software VBE
307
OemVendorNamePtr resd 1 ; puntatore FAR alla stringa del
fabbricante
OemProductNamePtr resd 1 ; puntatore FAR alla stringa del prodotto
OemProductRevPtr resd 1 ; puntatore FAR alla stringa di revisione
Reserved resb 222 ; riservato VBE
OemData resb 256 ; area dati per le OEM Strings

ENDSTRUC

STRUC ModeInfoBlock

ModeAttributes resw 1 ; attributi del modo video


WinAAttributes resb 1 ; attributi finestra A
WinBAttributes resb 1 ; attributi finestra B
WinGranularity resw 1 ; granularita' finestre
WinSize resw 1 ; dimensione finestre
WinASegment resw 1 ; segmento di inizio finestra A
WinBSegment resw 1 ; segmento di inizio finestra B
WinFuncPtr resd 1 ; puntatore FAR alla window function
BytesPerScanLine resw 1 ; lunghezza in byte di una scan line
XResolution resw 1 ; risoluzione orizzontale
YResolution resw 1 ; risoluzione verticale
XCharSize resb 1 ; larghezza in pixel cella carattere
YCharSize resb 1 ; altezza in pixel cella carattere
NumberOfPlanes resb 1 ; numero di piani di bit
BitsPerPixel resb 1 ; bit di colore per ogni pixel
NumberOfBanks resb 1 ; numero banchi di memoria video
MemoryModel resb 1 ; modello di memoria
BankSize resb 1 ; dimensione banco di memoria in Kb
NumberOfImgPages resb 1 ; numero di pagine immagine
Reserved1 resb 1 ; riservato (viene settato a 1)
RedMaskSize resb 1 ; dimensione in bit della Red Mask
RedFieldPos resb 1 ; posizione LSB nella Red Mask
GreenMaskSize resb 1 ; dimensione in bit della Green Mask
GreenFieldPos resb 1 ; posizione LSB nella Green Mask
BlueMaskSize resb 1 ; dimensione in bit della Blue Mask
BlueFieldPos resb 1 ; posizione LSB nella Blue Mask
RsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
RsvdFieldPos resb 1 ; posizione LSB nella Reserved Mask
DirectColModeInfo resb 1 ; attributi direct color mode
PhysBasePtr resd 1 ; indirizzo framebuffer (modo lineare)
Reserved2 resd 1 ; riservato (viene settato a 0)
Reserved3 resw 1 ; riservato (viene settato a 0)
LinByteScanLine resw 1 ; byte per scan line (modo lineare)
BnkNumOfImgPages resb 1 ; numero pagine immagine (modo reale)
LinNumOfImgPages resb 1 ; numero pagine immagine (modo lineare)
LinRedMaskSize resb 1 ; dimensione in bit della Red Mask (modo
lineare)
LinRedFieldPos resb 1 ; posizione LSB nella Red Mask (modo
lineare)
LinGreenMaskSize resb 1 ; dimensione in bit della Green Mask
(modo lineare)
LinGreenFieldPos resb 1 ; posizione LSB nella Green Mask (modo
lineare)
LinBlueMaskSize resb 1 ; dimensione in bit della Blue Mask (modo
lineare)
LinBlueFieldPos resb 1 ; posizione LSB nella Blue Mask (modo
lineare)
LinRsvdMaskSize resb 1 ; dimensione in bit della Reserved Mask
(linear)
LinRsvFieldPos resb 1 ; posizione LSB nella Reserved Mask
(linear)
308
MaxPixelClock resd 1 ; max. pixel clock in Hz per il modo
grafico
Reserved4 resb 190 ; rende la struttura da 256 byte

ENDSTRUC

STRUC PaletteEntry

Blue resb 1 ; componente blu


Green resb 1 ; componente verde
Red resb 1 ; componente rossa
Alignment resb 1 ; allineamento alla DWORD

ENDSTRUC

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

info_block ISTRUC VbeInfoBlock

at VbeSignature, resb 4 ; stringa di identificazione "VESA"


at VbeVersion, dw 0 ; versione VBE
at OemStringPtr, dd 0 ; puntatore FAR alla OEM String
at Capabilities, resb 4 ; caratteristiche hardware
dell'adattatore
at VideoModePtr, dd 0 ; puntatore FAR alla lista dei modi video
at TotalMemory, dw 0 ; numero blocchi da 64 Kb di VRAM (VBE
2.0+)
at OemSoftwareRev, dw 0 ; codice revisione software VBE
at OemVendorNamePtr, dd 0 ; puntatore FAR alla stringa del
fabbricante
at OemProductNamePtr,dd 0 ; puntatore FAR alla stringa del prodotto
at OemProductRevPtr, dd 0 ; puntatore FAR alla stringa di revisione
at Reserved, resb 222 ; riservato VBE
at OemData, resb 256 ; area dati per le OEM Strings

IEND

mode_info ISTRUC ModeInfoBlock

at ModeAttributes, dw 0 ; attributi del modo video


at WinAAttributes, db 0 ; attributi finestra A
at WinBAttributes, db 0 ; attributi finestra B
at WinGranularity, dw 0 ; granularita' finestre
at WinSize, dw 0 ; dimensione finestre
at WinASegment, dw 0 ; segmento di inizio finestra A
at WinBSegment, dw 0 ; segmento di inizio finestra B
at WinFuncPtr, dd 0 ; puntatore FAR alla window function
at BytesPerScanLine, dw 0 ; lunghezza in byte di una scan line
at XResolution, dw 0 ; risoluzione orizzontale
at YResolution, dw 0 ; risoluzione verticale
at XCharSize, db 0 ; larghezza in pixel cella carattere
at YCharSize, db 0 ; altezza in pixel cella carattere
at NumberOfPlanes, db 0 ; numero di piani di bit
at BitsPerPixel, db 0 ; bit di colore per ogni pixel
at NumberOfBanks, db 0 ; numero banchi di memoria video
at MemoryModel, db 0 ; modello di memoria
at BankSize, db 0 ; dimensione banco di memoria in Kb
at NumberOfImgPages, db 0 ; numero di pagine immagine
309
at Reserved1, db 1 ; riservato (viene settato a 1)
at RedMaskSize, db 0 ; dimensione in bit della Red Mask
at RedFieldPos, db 0 ; posizione LSB nella Red Mask
at GreenMaskSize, db 0 ; dimensione in bit della Green Mask
at GreenFieldPos, db 0 ; posizione LSB nella Green Mask
at BlueMaskSize, db 0 ; dimensione in bit della Blue Mask
at BlueFieldPos, db 0 ; posizione LSB nella Blue Mask
at RsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
at RsvdFieldPos, db 0 ; posizione LSB nella Reserved Mask
at DirectColModeInfo,db 0 ; attributi direct color mode
at PhysBasePtr, dd 0 ; indirizzo framebuffer (modo lineare)
at Reserved2, dd 0 ; riservato (viene settato a 0)
at Reserved3, dw 0 ; riservato (viene settato a 0)
at LinByteScanLine, dw 0 ; byte per scan line (modo lineare)
at BnkNumOfImgPages, db 0 ; numero pagine immagine (modo reale)
at LinNumOfImgPages, db 0 ; numero pagine immagine (modo lineare)
at LinRedMaskSize, db 0 ; dimensione in bit della Red Mask (modo
lineare)
at LinRedFieldPos, db 0 ; posizione LSB nella Red Mask (modo
lineare)
at LinGreenMaskSize, db 0 ; dimensione in bit della Green Mask
(modo lineare)
at LinGreenFieldPos, db 0 ; posizione LSB nella Green Mask (modo
lineare)
at LinBlueMaskSize, db 0 ; dimensione in bit della Blue Mask (modo
lineare)
at LinBlueFieldPos, db 0 ; posizione LSB nella Blue Mask (modo
lineare)
at LinRsvdMaskSize, db 0 ; dimensione in bit della Reserved Mask
(linear)
at LinRsvFieldPos, db 0 ; posizione LSB nella Reserved Mask
(linear)
at MaxPixelClock, dd 0 ; max. pixel clock in Hz per il modo
grafico
at Reserved4, resb 190 ; rende la struttura da 256 byte

IEND

rgb_color ISTRUC PaletteEntry

at Blue, db 46h ; componente blu


at Green, db 0D6h ; componente verde
at Red, db 0C0h ; componente rossa
at Alignment, db 0 ; allineamento alla DWORD

IEND

write_segm dw 0 ; Seg writeable Window


bank_shift dw 0 ; numero di shift per il bank switching
curr_bank dw -1 ; banco corrente
xpos dw 0 ; ascissa pixel
ypos dw 0 ; ordinata pixel

str_title db "ACCESSO AI PIXEL NEL MODO GRAFICO TRUE COLOR", 0


str_waitkey db "Premere un tasto per continuare ... ", 0
str_vesaok db "Il VBIOS supporta le specifiche VESA/VBE versione
XXh:XXh", 0
str_vesako db "Errore! Supporto VESA/VBE assente!", 0
str_vesaerr db "VBE Status Error!", 0
str_modeko db "Errore! Modo video non supportato!", 0
str_modesupko db "Errore! Modo video supportato ma non attivabile!", 0
str_bpsko db "Errore! Valore BitsPerPixel incompatibile!", 0
310
str_winko db "Errore! Il modo video non supporta le finestre!", 0
str_wfuncko db "Errore! Window Function non supportata!", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

; ES = DS per il corretto accesso alle strutture info_block e mode_info

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 0010h ; riga 0, colonna 16
call far writeString ; mostra la stringa

; verifica se le specifiche VESA/VBE sono supportate

mov ax, 4F00h ; servizio Return VBE Controller Info.


mov di, info_block ; ES:DI punta a info_block
mov dx, 0200h ; riga 2, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vesa_ok ; supporto VESA/VBE presente
mov di, str_vesako ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vesa_ok:
mov di, str_vesaok ; ES:DI punta alla stringa
call far writeString ; mostra la stringa

; visualizza la versione VBE

mov al, [info_block + VbeVersion + 1]


mov dx, 0232h ; riga 2, colonna 50
call far writeHex8 ; mostra VbeVersion major num.
mov al, [info_block + VbeVersion + 0]
mov dx, 0236h ; riga 2, colonna 54
call far writeHex8 ; mostra VbeVersion minor num.

; verifica se il modo video VESA_MODE e' supportato (FS:SI punta a VideoModePtr)

lfs si, [info_block + VideoModePtr]

testmode_loop:
mov ax, [fs:si] ; AX = codice modo video
cmp ax, VESA_MODE ; AX == VESA_MODE ?
311
je vesamode_ok ; si
add si, 2 ; incremento puntatore
cmp ax, 0FFFFh ; fine lista ?
jne testmode_loop ; no

mov di, str_modeko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vesamode_ok:

; verifica supporto hardware (modo video, bit per pixel e finestre)

mov ax, 4F01h ; servizio Return VBE Mode Info.


mov cx, VESA_MODE ; CX = codice modo video
mov di, mode_info ; ES:DI punta a mode_info
mov dx, 0400h ; riga 4, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vbestatus_ok ; VBE Status OK
mov di, str_vesaerr ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vbestatus_ok:

; verifica supporto hardware modo video

mov ax, [mode_info + ModeAttributes]


test al, 01h ; supporto hardware del modo video ?
jnz test_bitperpixel ; si

mov di, str_modesupko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

test_bitperpixel:

; verifica se BitsPerPixel = 24

mov al, [mode_info + BitsPerPixel]


cmp al, 24 ; 24 bit per pixel ?
je test_window ; si

mov di, str_bpsko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

test_window:

; verifica supporto finestre

mov al, [mode_info + WinAAttributes]


or al, [mode_info + WinBAttributes]
test al, 01h ; finestre supportate ?
jnz window_ok ; si

mov di, str_winko ; ES:DI punta alla stringa


mov dx, 0400h ; riga 4, colonna 0
312
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

window_ok:

; determina la writeable window

mov ax, [mode_info + WinASegment]


mov [write_segm], ax ; assume WinA scrivibile
mov al, [mode_info + WinAAttributes]
test al, 00000001b ; finestra A supportata ?
jz test_winb ; no
test al, 00000100b ; finestra A scrivibile ?
jnz end_wintest ; si
test_winb:
mov ax, [mode_info + WinBSegment]
mov [write_segm], ax ; assume WinB scrivibile

end_wintest:

; calcola il numero di bank shift

mov word [bank_shift], 0 ; num. shift per win_bank


numshift_loop:
mov ax, 64 ; AX = WinSize (in Kb)
mov cl, [bank_shift] ; CL = numero di shift a destra
shr ax, cl ; AX = AX / (2^bank_shift)
cmp ax, [mode_info + WinGranularity] ; AX == WinGranularity ?
je exit_shiftloop
inc word [bank_shift] ; incrementa bank_shift
jmp numshift_loop ; ripeti il loop

exit_shiftloop:

; verifica se la Window Function è supportata


; Questa porzione di codice viene assemblata solo se WIN_FUNC e' stata
dichiarata.
; Commentare la dichiarazione di WIN_FUNC per usare la INT 10h.

%IFDEF WIN_FUNC

mov eax, [mode_info + WinFuncPtr]


test eax, eax ; puntatore a NULL ?
jnz winfunc_ok ; no
mov di, str_wfuncko ; ES:DI punta alla stringa
mov dx, 0400h ; riga 4, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

winfunc_ok:

%ENDIF

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; tenta di attivare la modalita' video richiesta

mov ax, 4F02h ; servizio Set VBE Mode


mov bx, VESA_MODE ; BX = codice modo video
313
mov dx, 0400h ; riga 4, colonna 0
int 10h ; chiama i servizi Video BIOS
cmp ax, 004Fh ; test sul VBE Return Status
je vbesetmode_ok ; VBE Status OK
mov di, str_vesaerr ; ES:DI punta alla stringa di errore
call far writeString ; mostra la stringa
jmp exit_on_error ; termina il programma

vbesetmode_ok:

; loop per la visualizzazione di XResolution * YResolution pixel

external_loop:
mov word [xpos], 0 ; azzeramento xpos

internal_loop:

push dword [rgb_color] ; colore RGB


push word [ypos] ; ordinata pixel
push word [xpos] ; ascissa pixel
call put_pixel ; chiama put_pixel
add sp, 8 ; pulizia stack

inc word [xpos] ; aggiorna l'ascissa


mov ax, [xpos] ; AX = xpos
cmp ax, [mode_info + XResolution]
jb internal_loop ; controllo loop

inc word [ypos] ; aggiorna l'ordinata


mov ax, [ypos] ; AX = ypos
cmp ax, [mode_info + YResolution]
jb external_loop ; controllo loop

call far waitChar ; attende la pressione di un tasto

mov ah, 00h ; AH = servizio Set Video Mode


mov al, 03h ; AL = modo testo 80x25 a 16 colori
int 10h ; chiama i servizi video del BIOS

jmp exit_program ; terminazione normale del programma

exit_on_error:

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

exit_program:

call far clearScreen ; pulisce lo schermo


call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

; void bank_switch(void)
314
bank_switch:

push eax ; preserva EAX

%IFDEF WIN_FUNC
mov bx, 0000h ; posizionamento Window A
mov dx, [curr_bank] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
mov bx, 0001h ; posizionamento Window B
mov dx, [curr_bank] ; DX = banco da commutare
call far [mode_info + WinFuncPtr]
%ELSE
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0000h ; posizionamento Window A
mov dx, [curr_bank] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
mov ax, 4F05h ; servizio Display Window Control
mov bx, 0001h ; posizionamento Window B
mov dx, [curr_bank] ; DX = banco da commutare
int 10h ; chiama i servizi video del BIOS
%ENDIF

pop eax ; ripristina EAX


retn ; NEAR return

; void put_pixel(int x1, int y1, unsigned long color)

put_pixel:

%define x1 [bp+4] ; ascissa pixel


%define y1 [bp+6] ; ordinata pixel
%define color [bp+8] ; colore RGB pixel

push bp ; preserva il vecchio BP


mov bp, sp ; SS:BP = SS:SP
push es ; preserva ES

; calcolo pixel_address

movzx eax, word y1 ; EAX = y1


movzx ebx, word [mode_info + BytesPerScanLine]
mul ebx ; EAX = y1 * BytesPerScanLine
movzx ebx, word x1 ; EBX = x1
add bx, x1 ; EBX = 2 * x1
add bx, x1 ; EBX = 3 * x1
add eax, ebx ; EAX = pixel_address
push eax ; preserva pixel address

; calcolo indice blocco da 64 Kb

shr eax, 16 ; win_bank = pixel_address / 65536

; calcolo indice banco di memoria

mov cl, [bank_shift] ; CL = bank_shift


shl ax, cl ; AX = bank

; bank switching

cmp ax, [curr_bank] ; AX == banco corrente ?


je show_pixel ; si
315
mov [curr_bank], ax ; salva il nuovo banco
call bank_switch ; chiama bank_switch

show_pixel:

mov es, [write_segm] ; ES = Seg framebuffer


mov eax, color ; EAX = colore RGB + align
cld ; DF = 0 (incremento puntatori)
pop edi ; EDI = pixel address (32 bit)
and edi, 0000FFFFh ; DI = pixel offset (16 bit)
cmp di, 0FFFDh ; test sull'offset
jbe show_pixel_3_0 ; minore o uguale a 65532 ?
cmp di, 0FFFEh ; test sull'offset
je show_pixel_2_1 ; uguale a 65534 ?

; byte 0 nel banco corrente e byte 1 e 2 nel banco successivo

stosb ; copia il byte 0 in VRAM


inc word [curr_bank] ; aggiorna curr_bank
call bank_switch ; chiama bank_switch
shr eax, 8 ; posiziona i 2 byte successivi
stosw ; copia i byte 1 e 2 in VRAM
jmp short exit_put_pixel

; byte 0 e 1 nel banco corrente e byte 2 nel banco successivo

show_pixel_2_1:

stosw ; copia i byte 0 e 1 in VRAM


inc word [curr_bank] ; aggiorna curr_bank
call bank_switch ; chiama bank_switch
shr eax, 16 ; posiziona il byte successivo
stosb ; copia il byte 2 in VRAM
jmp short exit_put_pixel

; byte 0, 1 e 2 nel banco corrente

show_pixel_3_0:

stosw ; copia i byte 0 e 1 in VRAM


shr eax, 16 ; posiziona il byte successivo
stosb ; copia il byte 2 in VRAM

exit_put_pixel:

pop es ; ripristina ES
pop bp ; ripristino vecchio BP
retn ; NEAR return

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Il compito di questo programma è apparentemente molto semplice; si
tratta, infatti, di riempire una intera schermata con
XResolution*YResolution pixel.

316
Per ogni pixel di coordinate x,y dobbiamo calcolare innanzi tutto il
valore pixel_address; osservando che, nelle modalità True Color, ogni
pixel richiede 3 byte in VRAM, la formula generale sarà:

pixel_address = (y * BytesPerScanLine) + (x * 3)

Il blocco da 64 Kb in cui si trova il pixel si ottiene dalla formula:

win_bank = pixel_address >> 16

Dopo aver calcolato nel solito modo il valore bank_shift, possiamo


determinare il banco in cui posizionare la finestra con la formula:

bank = win_bank << bank_shift

Dopo aver posizionato la finestra, calcoliamo l'offset del pixel con


la formula:

pixel_offset = pixel_address AND FFFFh

A questo punto non dobbiamo fare altro che trasferire 3 byte di


colore all'indirizzo appena calcolato; se però proviamo ad usare
queste formule nel programma di Figura 21 otteniamo un sicuro crash!
Il perché di questo crash è legato proprio al fatto che, nelle
modalità True Color, il colore di ogni pixel viene rappresentato con
3 byte, ma la dimensione di un banco di memoria è sempre multipla
intera di 2 e non di 3; ciò significa che la terna di BYTE relativa a
particolari pixel potrebbe trovarsi a cavallo tra un banco di memoria
e quello successivo!
Questo problema non si presenta nelle modalità High Color dove ogni
pixel occupa 2 byte in VRAM; questa coppia di BYTE relativa ad ogni
pixel verrà quindi sempre a trovarsi all'interno dello stesso banco
di memoria.

Il programma di Figura 21 mostra come affrontare la situazione nelle


modalità True Color; in pratica, si distinguono i seguenti tre casi:

1) l'offset del pixel è al massimo FFFDh per cui i relativi 3 byte


devono essere disposti tutti all'interno dello stesso banco di
memoria;
2) l'offset del pixel è pari a FFFEh per cui i relativi 3 byte devono
essere disposti: i primi due in un banco di memoria e il terzo nel
banco successivo;
2) l'offset del pixel è pari a FFFFh per cui i relativi 3 byte devono
essere disposti: il primo in un banco di memoria e gli altri due nel
banco successivo.

Da notare l'importantissimo ruolo svolto dalla variabile curr_bank


che memorizza l'indice del banco di memoria correntemente attivo;
consultando tale variabile possiamo ridurre al minimo indispensabile
le pesanti operazioni di bank switching!

L'esempio di Figura 21 si riferisce alla modalità VESA standard 0142h

317
(risoluzione 640x480 e colori a 24 bit 8:8:8); il programma è
totalmente parametrizzato per cui può essere testato anche con le
altre modalità standard True Color (24 bit) tenendo però presente ciò
che è stato spiegato per l'esempio di Figura 20.
Lo stesso esempio di Figura 21, inoltre, può essere facilmente
adattato al caso BitsPerPixel=32 (dove i primi tre BYTE rappresentano
una terna RGB di tipo 8:8:8, mentre il quarto BYTE è disponibile per
altri usi); in tal caso la situazione si semplifica notevolmente in
quanto WinGranularity è anche un multiplo intero di 4, per cui è
impossibile che la quaterna di BYTE relativa ad un qualunque pixel
venga a trovarsi a cavallo tra due banchi di memoria adiacenti.
Per esercizio si può provare a realizzare proprio l'adattamento
appena descritto; in tal caso, si tenga presente che:

pixel_address = (y * BytesPerScanLine) + (x * 4)

Nota importante.
Come accade purtroppo molto frequentemente, persino la
documentazione ufficiale sulle varie specifiche standard
hardware e software può contenere numerosi e gravi errori di
stampa; a questo problema non si sottrae nemmeno la
documentazione ufficiale sulle specifiche VBE.
In particolare, il documento sulle specifiche VBE 3.0
contiene un grave errore relativo alla struttura
ModeInfoBlock; l'ultimo membro viene riportato come:

Reserved4 resb 189

Il valore 189 rende però la dimensione della struttura pari a


255 byte mentre le specifiche standard prevedono 256 byte!
Il valore corretto deve essere quindi:

Reserved4 resb 190

In assenza di questa correzione, si può andare incontro a


seri problemi; ad esempio, una eventuale variabile definita
subito dopo una istanza della struttura ModeInfoBlock
verrebbe sovrascritta da una chiamata al servizio 4F01h (che
riempie la struttura stessa)!

Anche i documenti ufficiali relativi alle specifiche VBE


versione 1.x e 2.x contengono numerosi errori di stampa;
alcuni di tali errori coinvolgono incredibilmente sempre la
solita struttura ModeInfoBlock!

Bibliografia

VESASP12.TXT - Super VGA BIOS Extension - Standard #VS911022 - VBE


Version 1.2
(disponibile nella sezione Downloads - Documentazione -
vesasp12.tar.bz2)

318
VBE3.PDF - VESA BIOS EXTENSION - Core Functions Standard
(disponibile nella sezione Downloads - Documentazione - vbe3.tar.bz2)

319
Capitolo 12 Servizi DOS per l'I/O su file
In questo capitolo analizzeremo brevemente i principali servizi
offerti dal DOS per le operazioni di I/O su file; come spesso accade,
tali servizi vengono ottenuti attraverso una chiamata alla INT 21h.
Gli emulatori DOS forniscono i servizi per i file sfruttando quelli
del SO sotto il quale stanno girando; così, la DOS Box sfrutta i
servizi Windows, mentre DOSEmu sfrutta i servizi Linux.

12.1 Il filesystem

La possibilità per i programmi di accedere in I/O alle memorie di


massa (hard disk, floppy disk, pen drive, memory card, CD, DVD, etc)
è legata alla presenza su tali supporti di un cosiddetto filesystem;
grazie al filesystem il SO fornisce ai programmi una serie di
procedure standard per le operazioni di creazione, lettura,
scrittura, modifica, cancellazione di file.
In assenza di un filesystem, l'accesso in I/O alle memorie di massa
comporta il ricorso a tecniche per la gestione a basso livello dei
dispositivi; tali tecniche vengono utilizzate, ad esempio, dai
programmi diagnostici, dai programmi per il recupero di file
danneggiati, etc.

Il filesystem del DOS (ma anche di altri SO) distingue innanzi tutto
tra dispositivi a caratteri (character devices) e dispositivi a
blocchi (block devices).

I dispositivi a caratteri sono periferiche identificate attraverso un


nome riservato come, ad esempio, CON: (l'insieme della tastiera e del
monitor), LPT1: (la stampante collegata alla prima porta parallela),
etc; attraverso questi nomi è possibile svolgere operazioni dirette
di I/O senza la necessità di dover indicare percorsi specifici.

I dispositivi a blocchi sono periferiche (generalmente, memorie di


massa) identificate attraverso una lettera dell'alfabeto (A:, B:, C:,
etc) che rappresenta la cosiddetta radice del dispositivo stesso; a
partire dalla radice è possibile accedere in I/O ai dati che si
trovano memorizzati nella periferica sotto forma di file.
I file possono trovarsi direttamente nella radice o anche raggruppati
in appositi contenitori denominati directory; a loro volta, le
directory possono contenere anche altre directory denominate
subdirectory.

Nei dispositivi a blocchi le directory assumono quindi una


caratteristica struttura ad albero; oltre alla radice, le directory e
le subdirectory rappresentano i rami, mentre i file rappresentano le
foglie.
I file e le directory vengono identificati attraverso un nome lungo
al massimo 8 caratteri e una estensione opzionale lunga al massimo 3
caratteri (ad esempio, MIOFILE.TXT); il nome e l'eventuale estensione
devono essere separati da un punto ('.').
In base alla struttura ad albero appena descritta, per specificare in
modo completo e univoco un determinato file, è necessario indicare il
320
cosiddetto percorso (path) da seguire lungo l'albero stesso e cioè,
la sequenza dei nomi formata dalla radice, dalle eventuali directory
e subdirectory e dal file; tutti i nomi devono essere separati dal
carattere '\' (backslash).
Per un ipotetico file MIOFILE.TXT possiamo avere, ad esempio:

C:\UTENTE1\ARCHIVIO\DOCUM.TXT\MIOFILE.TXT

Il DOS (così come Unix/Linux) definisce anche due nomi riservati per
le directory e cioè, '.' e '..'; il simbolo '.' indica la directory
corrente (cioè, la directory in cui ci troviamo), mentre il simbolo
'..' indica la directory superiore (cioè, la directory che contiene
quella corrente).

12.2 Le handle

Le versioni meno vecchie del DOS hanno introdotto il concetto di


handle (maniglia) per la gestione dei file; ogni volta che un
programma chiede al DOS l'autorizzazione per l'accesso in I/O ad un
file, riceve dal SO una handle che identifica in modo univoco il file
stesso.

L'uso delle handle è molto comodo in quanto permette di delegare al


DOS tutte le operazioni preliminari (allocazione della memoria per
contenere le strutture dati destinate al controllo del file); nelle
prime versioni del DOS si utilizzavano i FCB (File Control Blocks) e
tutte le operazioni preliminari erano a carico del programma che
intendeva accedere al filesystem.

Grazie alle handle, il DOS può effettuare tutti i necessari controlli


di sicurezza sui file in modo da evitare che vengano compiute
operazioni non lecite; lo stesso DOS può, ad esempio, chiudere tutti
i file lasciati aperti da un programma che ha terminato l'esecuzione
in modo normale o forzato (ad esempio, per un crash).

12.3 Servizi DOS per la gestione del filesystem

Tutti i servizi DOS per la gestione del filesystem vengono forniti


dalla INT 21h; il codice principale del servizio viene passato
attraverso il registro AH. La tipica chiamata di un servizio DOS per
i file assume quindi il seguente aspetto:

mov ah, codice servizio ; AH = codice del servizio richiesto


int 21h ; chiama i servizi DOS

In caso di successo, si ottiene CF=0, mentre in caso di insuccesso si


ottiene CF=1; se CF=1, il registro AX contiene uno dei codici di
errore illustrati dalla Figura 1.

Figura 1 - Codici di errore della INT 21h


AX Tipo di errore
00h nessun errore

321
Figura 1 - Codici di errore della INT 21h
AX Tipo di errore
01h servizio inesistente
02h file non trovato
03h percorso non trovato
04h troppi file aperti (handle esaurite)
05h accesso negato
06h handle non valida
0Ch codice di accesso errato
0Fh drive specificato non valido
10h tentativo di rimuovere la directory corrente
11h il dispositivo è cambiato
12h non ci sono più file
13h dischetto protetto in scrittura
14h unità sconosciuta
15h drive non pronto
17h errore CRC sul dischetto
1Ah supporto di tipo sconosciuto
1Bh settore non trovato
1Dh errore di scrittura
1Eh errore di lettura
22h cambio inaspettato del disco
26h impossibile completare l'operazione
50h il file esiste già
52h impossibile scrivere nella directory
56h password non valida
5Bh fine del file in lettura
5Ch spazio su disco esaurito

Analizziamo ora l'elenco completo dei servizi che presuppongono il


riferimento ai file tramite il metodo delle handle.

12.3.1 Servizio n.39h: Create Subdirectory (MKDIR)

Questo servizio permette di creare una nuova subdirectory.

INT 21h - Servizio n. 39h - Create Subdirectory

Argomenti richiesti:
AH = 39h (Create Subdirectory)
DS:DX = puntatore FAR al nome della subdirectory

Valori restituiti:
AX = codice di errore se CF = 1 (03h, 05h)

Questo servizio permette di creare una nuova subdirectory in una


directory già esistente o nella radice del dispositivo corrente; il
322
nome della subdirectory deve essere contenuto in una stringa C
puntata da DS:DX.
Se il programmatore indica un singolo nome, la nuova subdirectory
verrà creata nella directory corrente; in alternativa, è possibile
specificare il percorso completo.

12.3.2 Servizio n.3Ah: Remove Subdirectory (RMDIR)

Questo servizio permette di rimuovere una subdirectory esistente.

INT 21h - Servizio n. 3Ah - Remove Subdirectory

Argomenti richiesti:
AH = 3Ah (Remove Subdirectory)
DS:DX = puntatore FAR al nome della subdirectory

Valori restituiti:
AX = codice di errore se CF = 1 (03h, 05h, 06h, 10h)

Questo servizio permette di eliminare una subdirectory esistente con


tutti gli eventuali file in essa presenti; non è possibile eliminare
una subdirectory che, al suo interno, contiene anche altre
subdirectory e non è neanche possibile eliminare la directory
corrente.
Il nome della subdirectory da eliminare deve essere contenuto in una
stringa C puntata da DS:DX. Se il programmatore indica un singolo
nome, verrà eliminata la relativa subdirectory contenuta nella
directory corrente; in alternativa, è possibile specificare il
percorso completo.

12.3.3 Servizio n.3Bh: Set Current Directory (CHDIR)

Questo servizio permette di impostare la nuova directory corrente.

INT 21h - Servizio n. 3Bh - Set Current Directory

Argomenti richiesti:
AH = 3Bh (Set Current Directory)
DS:DX = puntatore FAR al nome della subdirectory

Valori restituiti:
AX = codice di errore se CF = 1 (03h)

Questo servizio permette di entrare in una directory la quale, di


conseguenza, diventerà la nuova directory corrente; il nome della
directory in cui spostarsi deve essere contenuto in una stringa C
puntata da DS:DX
Se il programmatore indica un singolo nome, sta dando per scontato
che la directory in cui spostarsi è contenuta nella directory
corrente; in alternativa, è possibile specificare il percorso
completo.

12.3.4 Servizio n.3Ch: Create or Truncate File (CREAT)

323
Questo servizio permette di creare un nuovo file.

INT 21h - Servizio n. 3Ch - Create or Truncate File

Argomenti richiesti:
AH = 3Ch (Create or Truncate File)
CX = attributi del file
DS:DX = puntatore FAR al nome del file

Valori restituiti:
AX = file handle se CF = 0
AX = codice di errore se CF = 1 (03h, 04h, 05h)

Questo servizio permette di creare un nuovo file; in caso di


successo, il file appena creato viene automaticamente aperto per le
operazioni di I/O a partire dalla posizione iniziale del file stesso.
Il nome del file da creare deve essere contenuto in una stringa C
puntata da DS:DX; se il nome esiste già, il relativo file viene
troncato alla dimensione di 0 byte e tutti gli eventuali dati in esso
presenti vengono persi!
Se il programmatore indica un singolo nome, sta dando per scontato
che il file deve essere creato nella directory corrente; in
alternativa, è possibile specificare il percorso completo.

Chiamando questo servizio, il programmatore può anche specificare gli


attributi da assegnare al file; tali attributi possono essere
successivamente cambiati con il servizio 43h.
Gli attributi devono essere specificati attraverso i bit del registro
CX in base a quanto riportato in Figura 2; se il bit vale 0 il
corrispondente attributo viene disattivato, mentre se il bit vale 1
il corrispondente attributo viene attivato (ovviamente, è anche
possibile combinare tra loro due o più attributi).

Figura 2 - Attributi per i file


Bit Significato
0 file a sola lettura
1 file nascosto
2 file di sistema
3 etichetta di volume
4 riservato (deve valere 0)
5 file di archivio
6-15 riservati

12.3.5 Servizio n.3Dh: Open Existing File (OPEN)

Questo servizio permette di aprire un file esistente.

INT 21h - Servizio n. 3Dh - Open Existing File

Argomenti richiesti:
324
AH = 3Dh (Open Existing File)
AL = modalità di accesso
DS:DX = puntatore FAR al nome del file

Valori restituiti:
AX = file handle se CF = 0
AX = codice di errore se CF = 1 (01h, 02h, 03h, 04h,
05h, 0Ch, 56h)

Questo servizio permette di aprire un file che deve essere già


presente nel filesystem; in caso di successo, il file appena aperto
risulta disponibile per le operazioni di I/O a partire dalla
posizione iniziale del file stesso e secondo la modalità di accesso
specificata attraverso il registro AL.
Il nome del file da aprire deve essere contenuto in una stringa C
puntata da DS:DX; il programmatore può indicare un singolo nome (file
relativo alla directory corrente) o, in alternativa, il percorso
completo.

Chiamando questo servizio, il programmatore può anche specificare la


modalità con la quale avverrà l'accesso al file; a tale proposito, è
necessario utilizzare il registro AL i cui bit assumono il
significato mostrato in Figura 3.

Figura 3 - Modalità di accesso (AL)


Bit Significato
0
1 Codice di accesso
2
3 riservato (deve valere 0)
4
5 Modo di condivisione
6
7 Eredità della handle

I bit in posizione 0, 1, 2 possono assumere i soli valori: 000b


(accesso in sola lettura), 001b (accesso in sola scrittura), 010b
(accesso in lettura e scrittura); il bit in posizione 3 è riservato e
deve valere 0.
I bit in posizione 4, 5, 6 esprimono le modalità con le quali il file
può essere condiviso tra più applicazioni e/o tra più computer
connessi in rete; i soli valori possibili sono: 000b (COMPATIBLE - il
file è accessibile da qualunque applicazione purché residente sul
computer in cui si trova il file stesso), 001b (DENYALL - l'accesso
in lettura e scrittura è permesso solamente all'applicazione che ha
aperto il file), 010b (DENYWRITE - l'accesso in scrittura è permesso
solamente all'applicazione che ha aperto il file), 011b (DENYREAD -
l'accesso in lettura è permesso solamente all'applicazione che ha
aperto il file), 100b (DENYNONE - nessun divieto).

12.3.6 Servizio n.3Eh: Close File (CLOSE)

325
Questo servizio permette di chiudere un file già aperto in
precedenza.

INT 21h - Servizio n. 3Eh - Close File

Argomenti richiesti:
AH = 3Eh (Close File)
BX = handle del file

Valori restituiti:
AX = codice di errore se CF = 1 (06h)

Questo servizio permette di chiudere un file precedentemente aperto


da una applicazione; si tratta di un servizio molto importante in
quanto la sua chiamata provoca la scrittura nel file di eventuali
dati ancora nel buffer e la restituzione della handle al SO.

12.3.7 Servizio n.3Fh: Read from File or Device (READ)

Questo servizio permette di leggere da un file già aperto in


precedenza.

INT 21h - Servizio n. 3Fh - Read from File or Device

Argomenti richiesti:
AH = 3Fh (Read from File or Device)
BX = handle del file
CX = numero di byte da leggere
DS:DX = puntatore FAR al buffer di lettura

Valori restituiti:
AX = numero di byte letti se CF = 0
AX = codice di errore se CF = 1 (05h, 06h)

Questo servizio permette di leggere dati da un file precedentemente


aperto da una applicazione; la lettura inizia a partire dalla
posizione corrente nel file stesso (se si desidera leggere da una
posizione differente si veda più avanti il servizio 42h).
Il numero di byte effettivamente letti, restituito in AX, potrebbe
essere differente dal numero di byte richiesto (ad esempio, dalla
console si possono leggere al massimo 128 caratteri per riga); se in
AX viene restituito il valore 0 significa che in fase di lettura è
stata raggiunta la fine del file (EOF o End Of File).

Tutti i dati letti dal file vengono copiati in un buffer puntato da


DS:DX; la memoria per tale buffer deve essere allocata dal
programmatore.

12.3.8 Servizio n.40h: Write to File or Device (WRITE)

Questo servizio permette di scrivere in un file già aperto in


precedenza.

326
INT 21h - Servizio n. 40h - Write to File or Device

Argomenti richiesti:
AH = 40h (Write to File or Device)
BX = handle del file
CX = numero di byte da scrivere
DS:DX = puntatore FAR al buffer di scrittura

Valori restituiti:
AX = numero di byte scritti se CF = 0
AX = codice di errore se CF = 1 (05h, 06h)

Questo servizio permette di scrivere dati in un file precedentemente


aperto da una applicazione; la scrittura inizia a partire dalla
posizione corrente nel file stesso (se si desidera scrivere in una
posizione differente si veda più avanti il servizio 42h).

Tutti i dati da scrivere nel file vengono copiati da un buffer


puntato da DS:DX; ovviamente, tale buffer deve essere appositamente
predisposto dal programmatore.

12.3.9 Servizio n.41h: Delete File (UNLINK)

Questo servizio permette di cancellare un file esistente.

INT 21h - Servizio n. 41h - Delete File

Argomenti richiesti:
AH = 41h (Delete File)
DS:DX = puntatore FAR al nome del file

Valori restituiti:
AX = codice di errore se CF = 1 (02h, 03h, 05h)

Questo servizio permette di rimuovere fisicamente un file esistente;


è importante assicurarsi che il file venga chiuso prima della sua
cancellazione.

Il nome del file da cancellare deve essere contenuto in una stringa C


puntata da DS:DX; il programmatore può indicare un singolo nome (file
relativo alla directory corrente) o, in alternativa, il percorso
completo.

12.3.10 Servizio n.42h: Set Current File Position (SEEK)

Questo servizio permette di spostare il puntatore di I/O all'interno


di un file esistente.

INT 21h - Servizio n. 42h - Set Current File Position

Argomenti richiesti:
AH = 42h (Set Current File Position)
AL = modo di posizionamento
BX = handle del file
CX:DX = incremento della posizione
327
Valori restituiti:
DX:AX = nuova posizione se CF = 0
AX = codice di errore se CF = 1 (01h, 06h)

Questo servizio permette di spostare il puntatore che indica la


posizione corrente nel file; come è stato spiegato in precedenza,
tutte le operazioni di I/O relative ad un file si svolgono a partire
dalla posizione corrente.
Il modo di posizionamento deve essere specificato nel registro AL;
gli unici tre valori permessi sono: 00h (incremento puntatore a
partire dall'inizio del file), 01h (incremento puntatore a partire
dalla posizione corrente), 02h (incremento puntatore a partire dalla
fine del file).
L'incremento da effettuare deve essere specificato nella coppia
CX:DX; si tratta di un intero con segno a 32 bit che ci permette di
esprimere un incremento compreso tra -2147483648 e +2147483647.

Se l'operazione ha successo, la nuova posizione viene restituita


nella coppia DX:AX; si tratta di un numero intero senza segno che
indica la posizione corrente a partire dall'inizio del file.

Sfruttando un semplice espediente, possiamo usare questo servizio per


ottenere in DX:AX la lunghezza in byte di un file; a tale proposito,
basta porre AL=02h (posizionamento relativo alla fine del file) e
CX:DX=00000000h (incremento nullo).

12.3.11 Servizio n.4300h: Get File Attributes (ATTRIB)

Questo servizio permette di conoscere gli attributi assegnati ad un


file esistente.

INT 21h - Servizio n. 4300h - Get File Attributes

Argomenti richiesti:
AX = 4300h (Get File Attributes)
DS:DX = puntatore FAR al nome del file

Valori restituiti:
CX = attributi del file se CF = 0
AX = codice di errore se CF = 1 (01h, 02h, 03h, 05h)

Questo servizio permette di conoscere gli attributi che


caratterizzano un file esistente; in caso di successo, le
informazioni richieste vengono restituite nel registro CX i cui bit
assumono lo stesso significato già illustrato in Figura 2.

Il nome del file che ci interessa deve essere contenuto in una


stringa C puntata da DS:DX; il programmatore può indicare un singolo
nome (file relativo alla directory corrente) o, in alternativa, il
percorso completo.

12.3.12 Servizio n.4301h: Set File Attributes (CHMOD)

328
Questo servizio permette di modificare gli attributi assegnati ad un
file esistente.

INT 21h - Servizio n. 4301h - Set File Attributes

Argomenti richiesti:
AX = 4301h (Set File Attributes)
CX = nuovi attributi
DS:DX = puntatore FAR al nome del file

Valori restituiti:
AX = codice di errore se CF = 1 (01h, 02h, 03h, 05h)

Questo servizio permette di modificare gli attributi precedentemente


assegnati ad un file esistente; i nuovi attributi devono essere
specificati nel registro CX secondo lo schema già illustrato in
Figura 2.

Il nome del file che ci interessa deve essere contenuto in una


stringa C puntata da DS:DX; il programmatore può indicare un singolo
nome (file relativo alla directory corrente) o, in alternativa, il
percorso completo.

12.3.13 Servizio n.56h: Rename/Move File or Directory (RENAME)

Questo servizio permette di cambiare il nome di un file o di una


directory; lo stesso servizio può essere usato per spostare un file
da una directory ad un'altra.

INT 21h - Servizio n. 56h - Rename/Move File or


Directory

Argomenti richiesti:
AH = 56h (Rename/Move File or Directory)
DS:DX = puntatore FAR al nome del vecchio file
ES:DI = puntatore FAR al nome del nuovo file

Valori restituiti:
AX = codice di errore se CF = 1 (02h, 03h, 05h,
11h)

Lo scopo principale di questo servizio è la modifica del nome di un


file o di una directory.
Il vecchio nome del file/directory da modificare deve essere
contenuto in una stringa C puntata da DS:DX e non può avere caratteri
jolly (* e ?); il nuovo nome del file/directory da modificare deve
essere contenuto in una stringa C puntata da ES:DI e può avere
caratteri jolly.
Il programmatore può indicare un singolo nome (file/directory
relativo alla directory corrente) o, in alternativa, il percorso
completo.

Il servizio 56h può essere usato anche per spostare un file da una
directory ad un'altra; a tale proposito, basta indicare nel nuovo

329
nome un percorso differente da quello vecchio. Non è consentito usare
questo servizio per spostare una directory.

12.4 Libreria FILELIB

In analogia a quanto visto nei precedenti capitoli, anche per la


gestione dell'I/O su file conviene scriversi una apposita libreria
linkabile a tutti i programmi che ne hanno bisogno; nella sezione
Downloads è presente una libreria, denominata FILELIB, che può essere
linkata ai programmi destinati alla generazione di eseguibili in
formato EXE.

All'interno del pacchetto filelibexe.tar.bz2 è presente la


documentazione, la libreria vera e propria FILELIB.ASM, l'include
file FILELIB.INC per il NASM e l'header file FILELIB.H per eventuali
programmi scritti in linguaggio C (purché destinati sempre alla
modalità reale).

Per la creazione dell'object file di FILELIB.ASM è richiesta la


presenza della libreria di macro LIBPROC.MAC (Capitolo 29 della
sezione Assembly Base con NASM); l'assemblaggio consiste nel semplice
comando:

nasm -f obj filelib.asm

La possibilità di linkare la libreria ai programmi scritti in C è


legata al fatto che le varie procedure definite in FILELIB seguono le
convenzioni C per il passaggio degli argomenti e per il valore di
ritorno; da notare, inoltre, la presenza degli underscore all'inizio
di ogni identificatore globale definito nella libreria stessa.

Diverse procedure della libreria richiedono argomenti passati per


indirizzo; tale indirizzo deve essere sempre di tipo FAR!
Ricordando le convenzioni C per il passaggio degli argomenti (da
destra verso sinistra), dobbiamo stare attenti quindi a mettere nella
lista di chiamata, prima la componente Offset e poi la componente Seg
della variabile da passare per indirizzo; in questo modo, la
componente Offset si troverà a precedere la componente Seg in
memoria, nel rispetto della convenzione Intel (con il Pascal avremmo
dovuto disporre le due componenti in ordine inverso).
All'interno delle procedure, le variabili passate per indirizzo
vengono gestite con le coppie DS:SI e ES:DI inizializzate con le
istruzioni LDS e LES; come sappiamo, tali istruzioni lavorano in modo
corretto solo quando la coppia Seg:Offset, da caricare in DS:SI o
ES:DI, si trova disposta in memoria (in questo caso, nello stack)
secondo la convenzione Intel!

12.5 Esempi pratici

Vediamo un semplice esempio pratico rappresentato da un programma che


crea un nuovo file nella directory corrente, lo riempie con 80x24
lettere 'X' in verde chiaro su sfondo blu scuro e, infine,

330
trasferisce tutti questi dati nella memoria video in modo testo; la
Figura 4 mostra il relativo listato.

Figura 4 - File FILETEST.ASM


;-------------------------------------------------------;
; file filetest.asm ;
; test della libreria FILELIB per l'I/O su file ;
;-------------------------------------------------------;
; nasm -f obj filetest.asm ;
; tlink filetest.obj + filelib.obj + exelib.obj ;
; (oppure link filetest.obj + filelib.obj + exelib.obj) ;
;-------------------------------------------------------;

;########### direttive per l'assembler ############

CPU 386 ; set di istruzioni a 32 bit


%include "exelib.inc" ; inclusione libreria di I/O
%include "filelib.inc" ; inclusione libreria FILELIB
%include "libproc.mac" ; inclusione macro per le procedure

;######### dichiarazione tipi e costanti ##########

%assign STACK_SIZE 0400h ; 1024 byte per lo stack

%assign FILE_ATTR FAT_ARCHIVE ; file di archivio accessibile in I/O


%assign BUFF_SIZE 3840 ; dimensione buffer in byte
%assign SEG_VTMEM 0B800h ; Seg memoria video testo

;################ segmento dati ###################

SEGMENT DATASEGM ALIGN=16 PUBLIC USE16 CLASS=DATA

;----- inizio definizione variabili statiche ------

buff_seg dw 0 ; Seg buffer di memoria


file_handle dw 0 ; handle del file

str_title db "TEST DELLA LIBRERIA FILELIB", 0


str_waitkey db "Premere un tasto per continuare ... ", 0

str_filename db "FILETEST.DAT", 0
str_memerror db "Errore! Allocazione memoria DOS fallita!", 0
str_ferror1 db "Errore! Creazione del file fallita!", 0
str_ferror2 db "Errore! Scrittura del file fallita!", 0
str_ferror3 db "Errore! Lettura del file fallita!", 0
str_ferror4 db "Errore! Posizionamento nel file fallito!", 0
str_ferror5 db "Errore! Chiusura del file fallita!", 0

;------- fine definizione variabili statiche ------

;############### segmento codice ##################

SEGMENT CODESEGM ALIGN=16 PUBLIC USE16 CLASS=CODE

..start: ; entry point

mov ax, DATASEGM ; trasferisce DATASEGM


mov ds, ax ; in DS attraverso AX

; restituzione memoria in eccesso

331
mov bx, 400 ; nuova dimensione in paragrafi
mov ah, 4Ah ; servizio Resise Memory Block
int 21h ; chiama il DOS

;------- inizio blocco principale istruzioni ------

call far hideCursor ; nasconde il cursore


call far clearScreen ; pulisce lo schermo

push ds ; copia DS
pop es ; in ES (per writeString)

; visualizza il titolo del programma

mov di, str_title ; ES:DI punta alla stringa


mov dx, 001Ah ; riga 0, colonna 26
call far writeString ; mostra la stringa

; allocazione di un blocco di memoria da 80x24x2=3840 byte (240 paragrafi)

mov ah, 48h ; servizio Allocate Memory


mov bx, BUFF_SIZE / 16 ; 240 paragrafi da allocare
int 21h ; chiama il DOS
jnc continua1 ; ok
mov di, str_memerror ; stringa di errore
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp exit_on_error ; uscita per errore DOS
continua1:
mov [buff_seg], ax ; salva il Seg del blocco allocato

; riempie il buffer con 80x24=1920 lettere 'X' + sfondo blu scuro e testo verde
chiaro

push es ; preserva ES
mov es, [buff_seg] ; ES = Seg buffer
xor di, di ; DI = Offset buffer (0000h)
mov eax, 1A581A58h ; EAX = 'X', 1Ah, 'X', 1Ah
mov cx, BUFF_SIZE / 4 ; 960 dword da trasferire
cld ; clear direction flag
rep stosd ; copia 960 dword nel buffer
pop es ; ripristina ES

; crea un file FILETEST.DAT nella directory corrente

callproc LANGC, FARPROC, _create_file, str_filename, seg str_filename, word


FILE_ATTR

jnc continua2 ; ok
mov di, str_ferror1 ; stringa di errore 1
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp deallocate_mem ; uscita per errore DOS
continua2:
mov [file_handle], ax ; salva la handle del file

; copia nel file il contenuto del buffer

callproc LANGC, FARPROC, _write_file, word 0000h, word [buff_seg], word


BUFF_SIZE, word [file_handle]

jnc continua3 ; ok
332
mov di, str_ferror2 ; stringa di errore 2
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp close_file ; uscita per errore DOS
continua3:

; riposizionamento puntatore all'inizio del file (offset 0 a partire dall'inizio


del file)

callproc LANGC, FARPROC, _seek_file, dword 00000000h, word FSEEK_SET, word


[file_handle]

jnc continua4 ; ok
mov di, str_ferror3 ; stringa di errore 3
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa
jmp close_file ; uscita per errore DOS
continua4:

; attende la pressione di un tasto

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
call far waitChar ; attende la pressione di un tasto

; copia sullo schermo il contenuto del file FILETEST.DAT

callproc LANGC, FARPROC, _read_file, word 0000h, word SEG_VTMEM, word


BUFF_SIZE, word [file_handle]

jnc close_file ; ok
mov di, str_ferror4 ; stringa di errore 4
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

close_file:

; chiude il file FILELIST.DAT

callproc LANGC, FARPROC, _close_file, word [file_handle]

jnc deallocate_mem ; ok
mov di, str_ferror5 ; stringa di errore 5
mov dx, 0200h ; riga 2, colonna 0
call far writeString ; mostra la stringa

deallocate_mem:

; deallocazione del blocco di memoria da 240 paragrafi

push es ; preserva ES
mov ah, 49h ; servizio Free Memory
mov es, [buff_seg] ; Seg. del blocco da deallocare
int 21h ; chiama il DOS
pop es ; ripristina ES

exit_on_error:

mov di, str_waitkey ; ES:DI punta alla stringa


mov dx, 1800h ; riga 24, colonna 0
call far writeString ; mostra la stringa
333
call far waitChar ; attende la pressione di un tasto

exit_program:

call far clearScreen ; pulisce lo schermo


call far showCursor ; ripristina il cursore

;-------- fine blocco principale istruzioni -------

mov ah, 4ch ; servizio Terminate Program


mov al, 00h ; exit code = 0
int 21h ; chiama i servizi DOS

;------------ inizio blocco procedure -------------

;------------- fine blocco procedure --------------

;################# segmento stack #################

SEGMENT STACKSEGM ALIGN=16 STACK USE16 CLASS=STACK

resb STACK_SIZE ; 1024 byte per lo stack

;##################################################
Prima di tutto, il programma libera tutta la memoria convenzionale in
eccesso; osservando il map file possiamo notare che il programma
stesso ha bisogno di circa 370 paragrafi i quali, per sicurezza,
vengono estesi a 400.
Lo scopo del passaggio appena descritto è legato al fatto che, invece
di scrivere i singoli byte direttamente nel file, utilizziamo un
apposito buffer di memoria (allocato attraverso il servizio 48h della
INT 21h) in modo da velocizzare notevolmente le operazioni; anche la
fase di lettura del file viene svolta in modo molto efficiente grazie
al fatto che tutte le informazioni vengono copiate in un colpo solo
in memoria video.
Le dimensioni del buffer sono pari a 80x24x2=3840 byte (240
paragrafi) in quanto, come sappiamo, la memoria video in modo testo
assegna ad ogni cella 1 byte per il codice ASCII del carattere da
stampare e 1 byte per gli attributi video; nel nostro esempio, il
carattere da stampare è 58h='X', mentre gli attributi video sono
rappresentati dal valore 1Ah (background blu scuro e foreground verde
chiaro).

Una volta creato il buffer di memoria, lo riempiamo con tutti i 3840


byte di informazioni; in seguito trasferiamo in un colpo solo tutti
questi 3840 byte in un file FILETEST.DAT appositamente creato con la
procedura _create_file della libreria FILELIB.
Subito dopo la scrittura, dobbiamo tenere presente che il puntatore
al file si trova posizionato all'offset 3840 di FILETEST.DAT; proprio
per questo motivo, prima di effettuare l'operazione di lettura,
dobbiamo utilizzare la procedura _seek_file per riposizionarci
all'inizio dello stesso FILETEST.DAT.

La procedura _read_file copia in un colpo solo tutti i 3840 byte,


dalla sorgente FILETEST.DAT, alla destinazione B800h:0000h (indirizzo
iniziale della VRAM in modo testo); se tutto si è svolto
correttamente, vedremo sullo schermo 1920 lettere 'X' in verde chiaro
334
su sfondo blu scuro.

Si osservi che il file FILETEST.DAT viene trattato implicitamente


come binario per cui, aprendolo con un editor di testo, si vedrebbero
delle 'X' intervallate con dei simboli strani dovuti al fatto che il
valore 1Ah viene erroneamente interpretato come codice del carattere
di controllo (non stampabile) SUB!

Un altro aspetto importante è dato dal fatto che i servizi legati


alle procedure _read_file e _write_file possono anche restituire CF=0
nonostante un numero di byte, letti o scritti, diverso da quello
richiesto dal programmatore; proprio per questo motivo, è importante
controllare sempre il valore restituito in AX anche quando si ottiene
CF=0!

12.5.1 Versione C di FILETEST.ASM

Come è stato spiegato in precedenza, la libreria FILELIB può essere


interfacciata anche con programmi scritti in C per la modalità reale;
la Figura 5 illustra una versione C semplificata di FILETEST.ASM.

Figura 5 - File FILETEST.C


/*************************************************/
/* file filetest.c */
/* test della libreria FILELIB per l'I/O su file */
/*************************************************/
/* bcc -ml -3 filetest.c filelib.obj */
/*************************************************/

#include "filelib.h" /* inclusione libreria FILELIB */


#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

/*######### dichiarazione tipi e costanti ##########*/

#define FILE_ATTR FAT_ARCHIVE /* file di archivio accessibile in I/O */


#define BUFF_SIZE 3840 /* dimensione buffer in byte */
#define SEG_VTMEM 0xB800 /* Seg memoria video testo */

/*############### variabili globali ################*/

Uint buff_seg; /* Seg buffer di memoria */


Uint file_handle; /* handle del file */

char str_title[] = "TEST DELLA LIBRERIA FILELIB";


char str_waitkey[] = "Premere un tasto per continuare ... ";

char str_filename[] = "FILETEST.DAT";


char str_memerror[] = "Errore! Allocazione memoria DOS fallita!";
char str_ferror1[] = "Errore! Creazione del file fallita!";
char str_ferror2[] = "Errore! Scrittura del file fallita!";
char str_ferror3[] = "Errore! Lettura del file fallita!";
char str_ferror4[] = "Errore! Posizionamento nel file fallito!";
char str_ferror5[] = "Errore! Chiusura del file fallita!";

/*############### main function ##################*/

335
int main(void)
{
int i, ch;
Uchar far *ptvmem;
Ulong far *ptbuff;

printf(