Assembly Avanzato con MASM

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