Per mezzo dei salti si possono realizzare le selezioni, cioè dei percorsi di esecuzione alternativi all'interno di un programma in base al risultato di una certa condizione. Nei linguaggi di programmazione ad alto livello le selezioni sono realizzate per mezzo di istruzioni come l'if...else dell'esempio seguente (in linguaggio C):
La selezione precedente potrebbe essere scritta nel linguaggio assembly dello Z80 nel seguente modo:
Un ciclo è una sequenza di istruzioni del programma che dev'essere ripetuta o meno in dipendenza di una certa condizione. Si consideri per esempio il ciclo seguente in C, il quale copia gli elementi del vettore vet1 nel vettore vet2 finché non viene incontrato un elemento di valore zero:
i = INIZIO1; // costante col valore iniziale indice primo vettore
j = INIZIO2; // costante col valore iniziale indice secondo vettore
while (vet1[i]!=0)
{
vet2[j]=vet1[i];
i++;
j++;
}
In assembly Z80 potremmo realizzare qualcosa di simile nel seguente modo:
LD HL,1000h | ; inizializza HL con l'indirizzo di memoria 1000h | |
LD BC, 2000h | ; inizializza BC con l'indirizzo di memoria 2000h | |
inizio: | ; inizio del ciclo | |
LD A,(HL) | ; carica in A il valore contenuto all'indirizzo HL | |
CP 0 | ; confronta A con zero | |
JP Z, fine | ; se A==0 salta a fine | |
LD (BC),A | ; copia A nell'indirizzo contenuto in BC | |
INC HL | ; incrementa HL | |
INC BC | ; incrementa BC | |
JP inizio | ; torna all'inizio del ciclo (fine del ciclo) | |
fine: | ; istruzioni successive nel programma |
Si osservi che quello che in C è un vettore in assembly è un'area di memoria RAM a partire da un certo indirizzo.
Consideriamo ora la realizzazione di una moltiplicazione in assembly Z80. Come già abbiamo detto l'assembly Z80 non possiede istruzioni di moltiplicazione e dunque la moltiplicazione deve essere implementata via software. Qualcosa di simile viene realizzato da questo programma in C++:
Nell'eseguire la moltiplicazione in assembly dobbiamo tenere conto del fatto che i registri interni dello Z80 sono a 8 bit (max valore 28-1 = 255), ma il prodotto di due numeri a 8 bit ha bisogno di 16 bit per poter essere rappresentato (infatti 255*255 = 65025). Pertanto usiamo come variabile di accumulo una coppia di registri HL, in modo da avere 16 bit a disposizione per il risultato. Siccome l'operazione ADD non consente la somma di valori a 8 bit con valori a 16, usiamo anche una coppia di registri DE per uno dei due fattori (di questi 16 bit però ne useremo sempre solo 8: quelli appartenenti al registro E). Abbiamo dunque:
Esaminiamo ora una importante applicazione dei salti condizionati: i cosiddetti cicli di ritardo. Molto frequentemente, un sistema a µP deve generare una sequenza di controlli e attivazioni, lasciando trascorrere precisi intervalli di tempo tra un'azione e l'altra. Si pensi ad esempio ad un sistema antifurto: prima di attivare l'allarme, si darà tempo al legittimo proprietario di disinnestare la protezione (con un'attesa di qualche secondo). Per un µP, lasciare trascorrere il tempo senza fare apparentemente nulla significa generalmente eseguire un ciclo di ritardo. Qui ne vediamo un esempio semplice:
In questo esempio di ciclo a contatore:
a) viene inizializzato un contatore (il registro C) prima di entrare nel ciclo;
b) all'interno del ciclo, il contatore viene decrementato di uno, ad ogni "giro";
c) sempre ad ogni "giro", il contatore viene controllato per sapere se il conteggio è terminato: se non lo è, si salta indietro a LOOP, altrimenti si prosegue.
Il test viene effettuato osservando il flag di zero dopo l'operazione di decremento: se tale operazione ha azzerato il registro, allora il flag è stato attivato. Così, saltiamo indietro a LOOP solo se il risultato della precedente operazione non e` zero. L'unico effetto significativo prodotto da questo pezzo di programma è di lasciar trascorrere del tempo, ossia di generare un ritardo.
Proviamo a calcolare quanto ritardo genera questo esempio. Supponiamo che il nostro microprocessore lavori con una frequenza di clock di 10 MHz: ogni ciclo corrisponde a 100 nS. Dalla tabella delle istruzioni risulta che le nostre istruzioni impiegano, per essere eseguite, rispettivamente:
Dato che C = FFh all'inizio, il ciclo viene eseguito 255 volte. Il tempo risultante sarà:
(7 + 255 x (4+10)) x 100 nS = 3577 x 100 nS = 0,3577 mS
Questo tempo può essere facilmente calibrato con precisione, sia modificando il valore iniziale di C, sia aggiungendo, eventualmente, altre istruzioni all'interno del ciclo, al solo fine di aumentarne il tempo di esecuzione. Ad esempio, inserendo due NOP e riducendo il valore iniziale del contatore:
Otteniamo così un tempo di circa mezzo millisecondo:
(7 + 227 x (4+4+4+10)) x 100 nS = 5001 x 100 nS = 0,5001 mS
E` importante farsi un idea dell'ordine di grandezza dei tempi ottenibili (con il nostro microprocessore con il clock a 10 MHz):
a) ordine del microsecondo o meno per l'esecuzione di una istruzione;
b) ordine del millisecondo per un semplice ciclo su contatore ad 8 bit
Nel caso in cui si vogliano ottenere ritardi più lunghi, dovremo ricorrere a tecniche un poco più complesse per aumentare il numero di ripetizioni, come ad esempio quella dei cicli annidati (nested loop):
Un'altra possibilità per ottenere cicli più lunghi consiste nell'usare come contatori coppie di registri a 16 bit:
Si noti che nel precedente spezzone di programma è stato necessario testare separatamente il valore 0 sul registro B e poi sul registro C: questo è necessario poiché nello Z80 le istruzioni di decremento a 16 bit (come la DEC BC) non cambiano i valori dei flag e quindi non è possibile effettuare un JP condizionato subito dopo.
Ovviamente è possibile abbinare le due tecniche precedenti, realizzando cicli nested con contatori a 16 bit (in questo modo i tempi di ritardo si allungano ulteriormente).
Consideriamo ora un sistema a µP che conta il numero di pressioni su un pulsante, mostrando il conteggio su una coppia di display di uscita:
Si noti che il pulsante è stato collegato con tutti i bit della porta di ingresso IA: quando è premuto il valore letto dalla porta sarà FFh; quando è rilasciato sarà 00h. Data la velocità di esecuzione del ciclo di conteggio, occorre aspettare che il pulsante venga premuto e poi successivamente rilasciato, prima di procedere a incrementare il contatore. In caso contrario, mantenendo il pulsante premuto, il programma continuerebbe a incrementare il conteggio.
Il programma in assembly corrispondente è questo:
LD B,0 attesa1: IN A,(0) ; legge lo stato del pulsante di ingresso CP 0 JP Z, attesa1 attesa2: IN A,(0) ; legge lo stato del pulsante di ingresso CP 0 JP NZ, attesa2 INC B LD A,B OUT (0),A ; visualizza il conteggio parziale sui display JP attesa1
Si osservino in particolare i due cicli di attesa. Il primo (etichetta attesa1) è necessario per attendere finché l'utente preme il pulsante. Il secondo (etichetta attesa2) serve invece per aspettare che l'utente rilasci il pulsante (in caso contrario il programma potrebbe acquisire più volte lo stesso numero nel tempo in cui l'utente tiene premuto il pulsante di inserimento).
Sito realizzato in base al
template offerto da
http://www.graphixmania.it