Assembly Avanzato con NASM
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 5.1, ad
esempio, mostra le caratteristiche del servizio Wait (attesa) della INT
15h del BIOS.
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:
(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
di VirtualBox 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
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 5.2 illustra
lo schema a blocchi semplificato del dispositivo.
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 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 5.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).
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 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 5.4.
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 5.4, passa da livello logico 1 a livello
logico 0 (la fase opposta prende il nome di "fronte di salita" o rising
edge).
Lo status register di Figura 5.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 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.5.
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 5.6.
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 5.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 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 significato dei
quattro possibili valori assegnabili alla coppia SC0, SC1 è
illustrato in Figura 5.7.
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 5.8.
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 5.9.
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.5.
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 5.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.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 alcun
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 5.10 illustra un esempio pratico che riassume tutti i concetti appena esposti.
Il segnale WR viene gestito dalla logica di controllo e permette di abilitare
una operazione di scrittura; come si nota in Figura 5.10, tale segnale è complementato
e quindi la scrittura viene abilitata quando WR=0.
Nell'esempio di Figura 5.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.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 alcun
decremento di 1.
Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il
decremento di 1 del contatore; come si nota in Figura 5.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 è già in 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. 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 alcun 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 5.11 illustra un esempio pratico che riassume tutti i concetti appena esposti.
Nell'esempio di Figura 5.11 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.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 5.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 è già in corso il conto
alla rovescia?
In tal caso, il conto alla rovescia ricomincia dal valore iniziale (che nell'esempio
di Figura 5.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 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 5.12 illustra un esempio pratico che riassume tutti i concetti appena
esposti.
Nell'esempio di Figura 5.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.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 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 è già in 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 5.13 illustra un esempio pratico che riassume tutti i concetti appena
esposti.
Nell'esempio di Figura 5.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.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 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 è già in 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 alcun
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 5.14 illustra un esempio pratico che riassume tutti i concetti appena esposti.
Nell'esempio di Figura 5.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.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 alcun
decremento di 1.
Ogni ulteriore fronte di discesa del segnale CLK provoca, invece, il
decremento di 1 del contatore; come si nota in Figura 5.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 è già in 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. 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 alcun 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 5.15 illustra un esempio pratico che riassume tutti i concetti appena esposti.
Nell'esempio di Figura 5.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) che indichiamo con N.
La prima operazione di scrittura consiste nel caricamento della CW; stiamo
richiedendo (Figura 5.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 alcun 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 5.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 è già in corso il conto
alla rovescia?
In tal caso, il conto alla rovescia ricomincia dal valore iniziale (che nell'esempio
di Figura 5.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.
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 5.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 3.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 alcuna 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 (Rate generator); il contatore viene inizializzato con il valore
18, per cui sull'uscita OUT si ottiene una sequenza indefinita di
impulsi (Figura 5.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 5.16.
Nelle intenzioni della IBM, il circuito di Figura 5.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 5.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 (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 5.17 illustra tutti i dettagli.
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 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; possiamo procedere quindi con la determinazione della
CW per la quale otteniamo (Figura 5.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:
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 5.8 per cui si
ottiene:
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:
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.
Con l'ausilio della libreria COMLIB possiamo allora visualizzare
facilmente l'orologio del BIOS in questo modo:
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 5.18
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:
- mentre un programma è in esecuzione, il Timer 0 genera una
IRQ0
- il PIC Master invia alla CPU una richiesta di chiamata della
INT 08h
- la CPU interrompe il programma in esecuzione e chiama la INT
08h
- la ISR della INT 08h esegue i propri compiti e poi chiama
una INT 1Ch
- la ISR della INT 1Ch svolge il proprio lavoro e restituisce
il controllo alla ISR della INT 08h
- la ISR della INT 08h invia un EOI al PIC
Master e restituisce il controllo
- 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 5.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.
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 5.19 la soluzione adottata è:
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 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 5.20 traduce in pratica tutte le considerazioni appena
esposte.
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).
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):
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 5.19; nel caso di un eseguibile in
formato EXE, per accedere ai dati presenti in un segmento chiamato, ad
esempio, DATASEGM, avremmo potuto scrivere:
In un eseguibile in formato COM, invece, non possiamo fare riferimento a
segmenti rilocabili; dobbiamo quindi necessariamente seguire la tecnica illustrata
in Figura 5.19 e in Figura 5.20.
5.5.4 Gestione dello speaker attraverso il Timer 2 - PIT 1
La possibilità di generare suoni attraverso il circuito di Figura 5.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
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 5.21, così chiamata in quanto la si ottiene
graficamente dalla funzione y=sin(x); i suoni di questo genere vengono
definiti puri.
Assegnando l'opportuna frequenza al segnale di Figura 5.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 5.16 si può notare che l'unico segnale periodico
a nostra disposizione, assimilabile a quello di Figura 5.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 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 5.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.
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:
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 5.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!
Bibliografia
Intel - 82C54 CHMOS PROGRAMMABLE INTERVAL TIMER
(23124406.pdf)
Intel - 80C186EB/80C188EB Microprocessor User's Manual (Capitolo 9)
(27083003.pdf)