ELEMANIA
Z80 - Selezioni e cicili
Selezioni con le istruzioni di salto

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

    if (A>5)
        A--;
   else
        A++;
   B = A;

La selezione precedente potrebbe essere scritta nel linguaggio assembly dello Z80 nel seguente modo:

                CP 5 ; confronta il contenuto di A col valore 5
                JP M, nopos ; se il risultato è negativo  o nullo (cioè se A<=5) salta a nopos
                DEC A ; decrementa A
                JP fine ; salta a fine
nopos:       INC A ; incrementa A
fine:          LD B, A ; istruzioni seguenti nel programma

 

Cicli con le istruzioni di salto

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.

Ciclo di moltiplicazione

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

DE = 17;            // primo fattore
B = 4;               // secondo fattore
HL = 0;             // variabile di accumulo per la somma
while (1)
    {
    HL = HL + DE;
    B = B -1;      // decrementa B
    if (B == 0)     // esce dal ciclo se B è zero 
        break;
    }

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:

                LD DE,17        ; primo fattore
                LD B, 4          ; secondo fattore
                LD HL, 0         ; variabile di accumulo per la somma
inizio:        ADD HL,DE      ; HL = HL + DE      
                DEC B            ; B = B -1
                JP NZ, inizio    ; se B è diverso da zero torna all'inizio

 

Cicli di ritardo

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:

            LD C,0FFh     ;inizializza il contatore
LOOP:    DEC C          ;decrementa il contatore
            JP NZ, LOOP  ;salta a LOOP se C è diverso da zero
                              ;altrimenti esce dal ciclo e prosegue

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:

            LD C,0FFh        ; 7 cicli di clock
LOOP:    DEC C             ; 4 cicli di clock
            JP NZ, LOOP     ; 10 cicli di clock

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:

            LD C,0E3h     ; 7 cicli di clock
LOOP:    NOP           ; 4 cicli di clock
            NOP           ; 4 cicli di clock
            DEC C         ; 4 cicli di clock
            JP NZ, LOOP   ; 10 cicli di clock

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

            LD B,0FFh     ;inizializza il contatore B
LOOP2:   LD C,0FFh     ;inizializza il contatore C
LOOP1:   DEC C          ;decrementa il contatore C
            JP NZ, LOOP1  ;salta a LOOP1 se C è diverso da zero
            DEC B          ;decrementa il contatore B
            JP NZ, LOOP2  ;salta a LOOP2 se C è diverso da zero
                              ;altrimenti esce dal ciclo e prosegue

Un'altra possibilità per ottenere cicli più lunghi consiste nell'usare come contatori coppie di registri a 16 bit:

            LD BC,0FFFFh

LOOP:    DEC BC          
            LD A,B
           CP 0    
            JP NZ, LOOP
            LD A,C
           CP 0    
            JP NZ, LOOP  

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

Ciclo di acquisizione pulsante

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:

Contatore con pulsante e Z80

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

 

precedente - successiva

Sito realizzato in base al template offerto da

http://www.graphixmania.it