Programmare come se fosse il 1982. Parte VII – Progetto: Mini Piano
Nei post precedenti di questa serie abbiamo smontato il C64 pezzo per pezzo. Registri, memoria, istruzioni, controllo di flusso e i chip hardware. Ora rimettiamo tutto insieme in qualcosa che funziona.
Il progetto è un mini piano a cinque note. I tasti Z X C V B suonano Do Re Mi Fa Sol. Ogni nota cambia il colore del bordo dello schermo, riproduce un suono nel SID e visualizza la lettera della nota al centro dello schermo. Abbastanza semplice da stare in un unico post, abbastanza complesso da usare tutti e tre i chip insieme.
Il ruolo di ogni chip è diretto. Il CIA 1 legge la tastiera, il SID genera il suono e il VIC-II gestisce la parte visiva. Il loop principale è il direttore che coordina i tre.
Lo stub BASIC e il punto di ingresso
I programmi assembly sul C64 hanno bisogno di un piccolo stub BASIC all’inizio perché RUN funzioni. Si trova a $0801, l’indirizzo dove l’interprete BASIC cerca il primo programma, e contiene una singola riga che dice al processore di saltare al codice assembly subito dopo.
Prima di leggere questo codice, c’è una cosa da notare. Non tutto in un file sorgente assembly è un’istruzione del processore. Alcune righe sono direttive dell’assembler, comandi per l’assembler stesso e non per il processore. *=$0801 dice all’assembler di posizionare i byte seguenti a partire dall’indirizzo $0801. .WORD $080B gli dice di memorizzare il valore $080B come due byte in little-endian. .BYTE $9E memorizza un singolo byte. Il processore non legge mai queste direttive. Quando esegue il programma, l’assembler ha già fatto il suo lavoro e in memoria restano solo i byte grezzi.
*=$0801
.WORD $080B ; puntatore alla riga BASIC successiva
.WORD $000A ; numero di riga: 10
.BYTE $9E ; token SYS
.BYTE "2064" ; 2064 = $0810 in decimale
.BYTE $00 ; fine riga
.WORD $0000 ; fine del programma BASIC
*=$0810 ; il codice inizia qui
Gli assembler moderni come KickAssembler e ACME gestiscono tutto questo automaticamente, ma vale la pena capire cosa sta succedendo. $9E è il token BASIC per SYS, e i byte seguenti sono il numero 2064 come testo, l’indirizzo dove inizia l’assembly. Quando digiti RUN, BASIC esegue SYS 2064 e il controllo passa al nostro codice.
Inizializzazione
Prima di entrare nel loop, configuriamo il SID e il VIC-II a uno stato noto. Il SID in particolare ha bisogno di attenzione: all’accensione, i suoi registri possono avere valori casuali e generare rumore.
INIT:
; Silenzia il SID e configura la voce 1
LDA #$00
STA $D418 ; volume master = 0 (silenzio totale)
STA $D404 ; spegni gate e waveform della voce 1
LDA #$08 ; attack=0 (istantaneo), decay=8 (medio)
STA $D405
LDA #$F4 ; sustain=F (massimo), release=4 (medio)
STA $D406
LDA #$0F
STA $D418 ; volume master = 15 (massimo)
; Colori iniziali: schermo nero
LDA #$00
STA $D020 ; bordo: nero
STA $D021 ; sfondo: nero
; Pulisci lo schermo via KERNAL
LDA #$93 ; PETSCII: CLR (pulisci schermo e torna all'inizio)
JSR $FFD2 ; CHROUT -- stampa il carattere di controllo
RTS
Il valore $93 non è un carattere visibile. È un codice di controllo PETSCII che il chip video interpreta come “pulire lo schermo”. Inviarlo al CHROUT del KERNAL è il modo più semplice per iniziare con uno schermo pulito senza scrivere direttamente nei 1.000 byte della screen RAM.
L’inviluppo del SID merita una nota. $D405 e $D406 memorizzano i quattro parametri ADSR in nibble da 4 bit ciascuno. Attack=0 significa che la nota raggiunge il volume massimo istantaneamente, ideale per un piano. Release=4 dà un decay morbido quando la nota finisce, invece di tagliarla bruscamente.
Il loop principale e la lettura della tastiera
Il CIA 1 espone la tastiera come una matrice 8×8 bit accessibile tramite registri a $DC00 e $DC01. Scorrere l’intera matrice manualmente è tedioso, quindi per questo progetto usiamo la routine GETIN del KERNAL ($FFE4), che fa già quel lavoro e restituisce in A il codice PETSCII dell’ultimo tasto premuto, o zero se nessun tasto è stato premuto.
MAIN:
JSR INIT
LOOP:
JSR $FFE4 ; GETIN: A = codice tasto (0 = nessuno)
BEQ LOOP ; A=0, nessun tasto -- torna all'inizio
CMP #$5A ; 'Z' in PETSCII = 90 = $5A
BEQ SUONA_DO
CMP #$58 ; 'X' = 88 = $58
BEQ SUONA_RE
CMP #$43 ; 'C' = 67 = $43
BEQ SUONA_MI
CMP #$56 ; 'V' = 86 = $56
BEQ SUONA_FA
CMP #$42 ; 'B' = 66 = $42
BEQ SUONA_SOL
JMP LOOP ; tasto sconosciuto, ignora
Ogni CMP confronta il valore in A con il codice PETSCII del tasto. Quando trova una corrispondenza, BEQ salta al blocco di quella nota. Se nessuno dei confronti corrisponde, JMP LOOP scarta il tasto e ricomincia dall’inizio.
Impostare e suonare ogni nota
Ogni blocco di nota fa tre cose. Imposta la frequenza nel SID, cambia il colore del bordo nel VIC-II e scrive la lettera della nota sullo schermo.
Le frequenze del SID si calcolano con la formula Fn = Fout × 16.777.216 / Fclk. Sul C64 PAL, Fclk è circa 985.248 Hz. Per Do centrale (261 Hz), il risultato è 4457 = $1169. Ogni nota occupa due byte in little-endian, byte basso prima, byte alto dopo.
SUONA_DO:
LDA #$69
STA $D400 ; freq low -- $1169 ≈ C4 (261 Hz)
LDA #$11
STA $D401 ; freq high
LDA #$02
STA $D020 ; bordo: rosso (colore 2)
LDA #$03
STA $05F4 ; 'C' sullo schermo -- screen code 3, riga 12 col 20
LDA #$01
STA $D9F4 ; colore char: bianco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
SUONA_RE:
LDA #$89
STA $D400 ; $1389 ≈ D4 (293 Hz)
LDA #$13
STA $D401
LDA #$05
STA $D020 ; bordo: verde
LDA #$04
STA $05F4 ; 'D' -- screen code 4
LDA #$01
STA $D9F4 ; colore char: bianco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
SUONA_MI:
LDA #$F0
STA $D400 ; $15F0 ≈ E4 (329 Hz)
LDA #$15
STA $D401
LDA #$06
STA $D020 ; bordo: blu
LDA #$05
STA $05F4 ; 'E' -- screen code 5
LDA #$01
STA $D9F4 ; colore char: bianco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
SUONA_FA:
LDA #$3E
STA $D400 ; $173E ≈ F4 (349 Hz)
LDA #$17
STA $D401
LDA #$07
STA $D020 ; bordo: giallo
LDA #$06
STA $05F4 ; 'F' -- screen code 6
LDA #$01
STA $D9F4 ; colore char: bianco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
SUONA_SOL:
LDA #$18
STA $D400 ; $1A18 ≈ G4 (392 Hz)
LDA #$1A
STA $D401
LDA #$04
STA $D020 ; bordo: viola
LDA #$07
STA $05F4 ; 'G' -- screen code 7
LDA #$01
STA $D9F4 ; colore char: bianco (color RAM = screen RAM + $D400)
JSR PLAY
JMP LOOP
L’indirizzo $05F4 è la posizione centrale dello schermo ($0400 + 12 × 40 + 20). La screen RAM del C64 usa screen codes, non PETSCII. A vale 1, B vale 2, C vale 3, e così via. L’indirizzo corrispondente nella color RAM è $D9F4 ($D800 + $01F4), dove scriviamo il colore del carattere.
La subroutine PLAY
PLAY viene chiamata da ogni blocco di nota. Attiva la voce 1 con waveform triangolare, aspetta un tempo fisso e poi spegne il gate, avviando la fase di release dell’inviluppo.
PLAY:
LDA #$11 ; waveform triangolare ($10) + gate ON ($01)
STA $D404 ; la nota inizia qui
; Delay di ~0,33 secondi
; Calcolo: 255 est × 255 int × ~5 cicli = ~326.000 cicli
; A 985.248 Hz (PAL): ~0,33 secondi
LDX #$FF
WAIT1: LDY #$FF
WAIT2: DEY
BNE WAIT2 ; ciclo interno: 255 × 5 cicli ≈ 1.275 cicli
DEX
BNE WAIT1 ; ciclo esterno: ripete 255 volte
LDA #$10 ; mantieni waveform triangolare, gate OFF
STA $D404 ; avvia il release -- la nota si spegne gradualmente
LDA #$00
STA $D020 ; ripristina bordo a nero
RTS
Il delay usa due cicli annidati che contano cicli di clock, il modo standard per creare pause precise nell’assembly del 6510. Per note più lunghe, aumenta il valore iniziale di LDX. Per note più corte, diminuiscilo. Ogni unità di LDX equivale a circa 1,3 millisecondi.
Come eseguirlo
Salva il codice in un file .asm, assemblalo con KickAssembler o ACME e apri il .prg generato nell’emulatore VICE. All’interno dell’emulatore, LOAD "PIANO",8,1 seguito da RUN avvierà il programma. Premi Z, X, C, V o B.
Se vuoi sperimentare direttamente senza assemblare, VICE ha un monitor machine language accessibile dal menu Debug. Puoi digitare i byte direttamente in memoria a partire da $0810 e poi G $0810 per eseguire, esattamente come si faceva negli anni 80.
Cosa puoi provare adesso
La struttura è pronta. Alcune estensioni naturali:
Più note. Un’ottava completa ha 12 note. Calcola le frequenze con la formula Fn = Fout × 16.777.216 / 985.248, convertile in esadecimale e aggiungi altri blocchi seguendo lo stesso schema.
Waveform diverse. Il valore $11 in $D404 seleziona l’onda triangolare. Sostituiscilo con $21 (dente di sega), $41 (quadra) o $81 (rumore). Ognuna ha un timbro completamente diverso. Il rumore, per esempio, diventa percussione.
Sustain mentre il tasto è premuto. GETIN rileva solo il momento in cui il tasto entra nel buffer. Per un sustain reale, leggi direttamente la matrice del CIA 1 a $DC01 all’interno di un ciclo che controlla se il tasto è ancora premuto prima di spegnere il gate.
Due voci contemporaneamente. Il SID ha tre voci indipendenti, ognuna con i propri registri di frequenza, waveform e inviluppo. Configurare la voce 1 e la voce 2 con frequenze diverse dà due note simultanee.
Con questo si chiude la serie. Siamo partiti dalla Parte I senza sapere cosa fosse un registro e finiamo qui: tre chip coordinati da un ciclo di qualche decina di istruzioni, che fanno qualcosa che puoi ascoltare e vedere. Non c’è nessuna magia nel percorso tra i due. Solo byte, indirizzi e il processore che fa quello per cui è stato progettato.