Salta ai contenuti

UDA 3 — Memoria, stack e sottoprogrammi

6 ore (2 di teoria + 4 di laboratorio) · Settimane 5-6

Al termine di questa unità:

  • organizzi vettori e tabelle in memoria (.db, .dw) e li scorri con HL/IX;
  • copi (e riempi) blocchi di memoria con LDIR;
  • conosci lo stack: PUSH/POP, SP, dove finiscono davvero i byte;
  • scrivi e chiami sottoprogrammi (CALL/RET), anche annidati;
  • passi parametri e ricevi valori di ritorno secondo una convenzione;
  • hai completato gli Esercizi 3 e 4 e sostieni la prova scritta n. 1.

Piano delle due settimane — Incontro 1 (3 h): memoria, vettori, LDIR + Esercizio 4. Incontro 2 (3 h): stack, sottoprogrammi + Esercizio 3 e prova scritta.


La memoria del G80 è fatta di 65536 celle (0x0000–0xFFFF), ognuna da 1 byte. Un “vettore” (o tabella) è semplicemente una zona di memoria con dati in fila:

dati: .db 10, 20, 30, 40, 50 ; 5 byte consecutivi
importi:
.dw 100, 250, 40 ; 3 parole (6 byte): 64 00 FA 00 28 00

Due cose da ricordare:

  • le etichette sono indirizzi: dati+3 è l’indirizzo del 4° byte;
  • le parole .dw sono salvate little-endian: prima il byte basso (100 = 0x0064 → in memoria 64 00).

Un vettore di dati si scorre con un puntatore (HL, IX) e un contatore (B con DJNZ):

ld hl, dati ; puntatore all'inizio
ld b, 5 ; quanti elementi
xor a ; accumulatore a 0
ciclo:
add a, (hl) ; somma l'elemento puntato
inc hl ; avanza al prossimo
djnz ciclo
halt
dati: .db 10, 20, 30, 40, 50

Risultato: somma 10+20+30+40+50 = 150 = 0x96 in A (provalo!).

Per leggere una parola servono due letture (byte basso, poi alto). Con IX come puntatore si legge comodamente con (ix+0) e (ix+1):

ld ix, importi
ld hl, 0 ; totale a 16 bit
ld b, 3
ciclo:
ld e, (ix+0) ; byte basso
ld d, (ix+1) ; byte alto
add hl, de ; totale = totale + importo
inc ix
inc ix ; avanza di 2 (una parola)
djnz ciclo
halt
importi: .dw 100, 250, 40

Risultato: 100 + 250 + 40 = 390 = 0x0186 in HL.

Nota sui flag: ADD HL, DE è una somma a 16 bit e aggiorna solo H, N, C (Z, S, V restano invariati). Per accorgersi di un “riporto” a 16 bit si guarda C.


Spostare dati un byte per volta è noioso: la CPU ha un’istruzione fatta apposta.

ld hl, origine ; da dove
ld de, destinazione ; dove
ld bc, 16 ; quanti byte
ldir ; copia finché BC != 0

LDIR ripete automaticamente questi passi:

  1. copia il byte (HL) in (DE);
  2. incrementa HL e DE; decrementa BC;
  3. se BC ≠ 0 ripete; altrimenti prosegue.

Varianti utili: LDI (una sola iterazione), LDDR (copiando all’indietro, utile se le zone si sovrappongono). Per riempire una zona si usa spesso LDIR con origine fissa… oppure un semplice ciclo di LD (HL), n.


Lo stack è una zona di memoria gestita in modalità LIFO (last in, first out): l’ultimo valore inserito è il primo a uscire. Il registro SP punta alla cima.

IstruzioneCosa succede
push afSP = SP − 1; (SP) = A; SP = SP − 1; (SP) = F
push bccome sopra, con B (alto) e C (basso)
pop afF = (SP); SP = SP + 1; A = (SP); SP = SP + 1
pop bcC = (SP), B = (SP+1), poi SP += 2

Verifica concreta — esegui questo programma e guarda la memoria da 0xFFFB:

ld sp, 0xFFFF ; buona norma: fissa la cima dello stack
ld a, 0x12
ld b, 0x34
push af
push bc
halt

Dopo l’esecuzione: SP = 0xFFFB e nelle celle: 0xFFFB = 00 (C) · 0xFFFC = 34 (B) · 0xFFFD = 00 (F) · 0xFFFE = 12 (A).

Regole d’oro:

  • pareggia sempre i PUSH con i POP: lo stack deve tornare al punto di partenza;
  • lo stack cresce verso il basso (da 0xFFFF verso 0x0000): fissare SP = 0xFFFF all’inizio del programma mette lo stack “in cima” alla memoria;
  • PUSH/POP non modificano i flag (salvarli è proprio il loro mestiere!).

Perché “salvare i flag”? Perché PUSH AF conserva insieme risultato e flag: è il modo standard per non rovinare il lavoro del chiamante (lo vedrai tra un attimo).


Un sottoprogramma (subroutine) è un blocco di codice riusabile, chiamato per nome:

ld a, 0x15
call raddoppia ; chiama la subroutine
halt ; qui A = 0x2A
raddoppia:
add a, a ; A = A * 2
ret ; torna al chiamante

Cosa fanno davvero:

  • CALL nome = PUSH dell’indirizzo di ritorno + JP nome (3 byte, salto a 16 bit);
  • RET = POP dell’indirizzo di ritorno + salto lì.

Ecco perché lo stack serve: a ogni CALL si “impila” il punto da cui si dovrà riprendere. Le chiamate possono annidarsi (una subroutine che ne chiama un’altra): lo stack tiene l’ordine, e i RET risalgono la catena all’indietro.

Un contratto chiaro evita bug difficilissimi. La convenzione che useremo:

RuoloRegola
Parametrisi passano nei registri (es. valore in A, indirizzo in HL)
Valore di ritornoin A (8 bit) o HL (16 bit)
Registri internila subroutine salva con PUSH/POP quelli che usa ma non restituisce
Flagse al chiamante servono, la subroutine li preserva con PUSH AF / POP AF

Esempio di contratto: la pausa attesa dell’Esercizio 3

attesa: ; input: nessuno
push af ; output: nessuno
push bc ; preserva: A, B, C, flag (SP torna com'era)
ld bc, 0x2000
att1:
dec bc
ld a, b
or c
jp nz, att1
pop bc
pop af
ret

Senza push af, la pausa azzererebbe A (xor/or … e il conteggio sui LED si romperebbe!): è l’errore classico dell’Esercizio 3.


Apri esercizi/04-copia-blocco.asm: manca la copia vera e propria.

  • Verifica: 0x0300..0x030F uguali a 0x0200..0x020F. Nel simulatore apri la memoria, vai a 0x0300 e controlla i 16 byte; oppure usa la variante con i dati nel sorgente:
.seg CODE (abs)
.org 0x0200
origine: .db 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
.org 0x0100
ld hl, 0x0200
ld de, 0x0300
ld bc, 16
ldir
halt

Dopo l’esecuzione, in memoria: 0x0300: 01 02 03 … 10.

Consegna: modifica la variante per copiare 8 byte e poi per copiare “all’indietro” (LDDR): in quale caso è necessario copiare all’indietro? (pensa a zone sovrapposte)

Attività B — Esercizio 3 (Contatore con ritardo)

Sezione intitolata “Attività B — Esercizio 3 (Contatore con ritardo)”

Apri esercizi/03-contatore.asm: serve una subroutine attesa che preservi A.

  • Verifica: i LED cambiano circa 2 volte al secondo (regola la costante BC).
  • Contratto da rispettare: input nessuno, output nessuno, preserva A, BC e i flag.
  • Confronta con la soluzione solo dopo aver capito l’effetto di push af / pop af.

Attività C — Contare gli elementi “grandi” di un vettore

Sezione intitolata “Attività C — Contare gli elementi “grandi” di un vettore”

Scrivi un programma che conta quanti valori di un vettore sono ≥ 100:

ld hl, valori
ld b, 6
ld c, 0 ; contatore dei "grandi"
ciclo:
ld a, (hl)
cp 0x64 ; 0x64 = 100
jr c, piccolo ; < 100 ?
inc c ; no: contalo
piccolo:
inc hl
djnz ciclo
ld a, c ; risultato in A
halt
valori: .db 10, 200, 100, 99, 150, 30

Verifica: i valori ≥ 100 sono 200, 100, 150 → A = 3. Prova con altri vettori.

Consegna: adatta il programma per contare i valori dispari (suggerimento: AND 1).


In coda a questa unità è prevista la prima verifica sommativa (1 ora): tracciamento di codice, completamento di frammenti e domande su flag, stack e sottoprogrammi. La trovi nella pagina “UDA 3 — Esempio di prova scritta n. 1” (con soluzioni per il docente): usala come palestra, poi il docente prepara la versione per la classe.


  1. Con HL = 0x1234, dove punta HL+2? E cosa contiene (HL) in una tabella .dw?
  2. Quali passi esegue LDIR a ogni iterazione? Quando si ferma?
  3. Cosa fa esattamente PUSH BC? E dopo POP BC, dov’è SP?
  4. Che differenza c’è tra CALL e JP? Cosa finisce sullo stack?
  5. La tua subroutine usa A e B come registri di lavoro e restituisce il risultato in A: quali registri devi salvare con PUSH/POP, e perché?

📎 I file .asm di esempi ed esercizi si scaricano dalla pagina Materiali.

  • Compito: completa gli Esercizi 3 e 4; prova la variante LDDR; risolvi la prova scritta di esempio senza guardare le soluzioni.
  • Pre-lettura: manuale, sezioni Memoria e Esempi multi-file.
  • Materiali: esercizi 03-contatore.asm e 04-copia-blocco.asm (+ soluzioni); esempio 03-led-contatore.asm per la pausa.
  • Prossima unità: ingressi e uscite — le periferiche di base (UDA 4).