Lingue disponibili:

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.

Architettura del Mini Piano

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.