Programmare come se fosse il 1982. Parte VI – Hardware in Memoria
Nei post precedenti di questa serie abbiamo costruito il modello completo. Registri, memoria, istruzioni, decisioni e cicli. Sai già come un programma si muove attraverso la memoria e come il processore esegue ogni byte che incontra. Quello che manca è la parte più sorprendente della storia del C64, e probabilmente l’idea più elegante dell’intera serie.
Scrivere in memoria era attivare hardware
Nel post sulla mappa di memoria abbiamo visto che certe regioni non erano RAM normale. Il range da $D000 a $DFFF apparteneva ai chip hardware. Il VIC-II, il SID e il CIA. Ma cosa significa in pratica lo si capisce solo quando si vede il codice.
Per cambiare il colore del bordo dello schermo sul C64, scrivevi:
LDA #$02 ; 2 = rosso nella palette del C64
STA $D020 ; scrive nel registro del colore del bordo
Due istruzioni. Nessuna chiamata di sistema. Nessun driver. Nessuna API. Hai scritto un byte in un indirizzo e il bordo è diventato rosso immediatamente, al ciclo di clock successivo. Questo è il memory-mapped I/O: i chip hardware vivevano nello stesso spazio di indirizzi della RAM, e scrivere in quegli indirizzi non salvava dati. Comandava direttamente l’hardware.
Non c’era nessuno strato tra il codice e l’effetto. Nessuno.
VIC-II, video senza scheda grafica
Il VIC-II esponeva i suoi 47 registri nella regione da $D000 a $D02E. Ognuno controllava un aspetto di ciò che appariva sullo schermo. Colori, posizione degli sprite, modalità grafica, scrolling, sincronizzazione con il raggio video.
La palette del C64 era fissa a 16 colori, numerati da 0 a 15. Il colore 0 era nero, 1 bianco, 2 rosso, e così via. Per cambiare lo sfondo:
LDA #$06 ; 6 = blu
STA $D021 ; registro del colore di sfondo
Gli sprite seguivano lo stesso schema. Ogni sprite aveva due registri per la posizione, uno per X e uno per Y, più bit di controllo distribuiti in altri registri. Per abilitare lo sprite 0 e posizionarlo:
LDA #$01
STA $D015 ; bit 0 = sprite 0 abilitato
LDA #$60 ; posizione X = 96
STA $D000 ; registro X dello sprite 0
LDA #$80 ; posizione Y = 128
STA $D001 ; registro Y dello sprite 0
Un dettaglio che mostra bene il livello di controllo disponibile: il VIC-II disegnava lo schermo riga per riga, dall’alto in basso, 50 volte al secondo nello standard PAL. Il registro $D012 comunicava su quale riga si trovava il raggio in quel preciso momento. I programmi che volevano effetti visivi sincronizzati con il raster, come cambiare i colori a metà schermo, dovevano aspettare che il raggio raggiungesse la riga giusta prima di scrivere nel registro. I programmatori esperti facevano questo in cicli che contavano i cicli di clock, sapendo esattamente quante istruzioni stavano in mezzo tra una riga e l’altra.
SID, un sintetizzatore dentro un chip
Il SID aveva tre voci indipendenti. Ognuna poteva suonare una frequenza diversa con la propria forma d’onda e inviluppo di ampiezza. Per suonare una nota, scrivevi la frequenza nei registri della voce, sceglievvi una forma d’onda e attivavi il gate:
LDA #$0F
STA $D418 ; volume master = 15 (massimo)
LDA #$25 ; frequenza byte basso (nota La approssimata)
STA $D400
LDA #$1D ; frequenza byte alto
STA $D401
LDA #$A0 ; attack veloce, decay medio
STA $D405
LDA #$F0 ; sustain alto, release lungo
STA $D406
LDA #$11 ; forma d'onda triangolare + gate ON
STA $D404 ; la nota inizia a suonare qui
Otto scritture in memoria e avevi un oscillatore triangolare che suonava una nota con inviluppo completo. Per silenziarlo, spegni il gate. LDA #$10 / STA $D404. Per suonare una nota diversa, basta cambiare i registri di frequenza mentre il gate è attivo.
L’inviluppo ADSR, Attack, Decay, Sustain, Release, è un concetto della sintesi analogica. Definisce come si comporta il volume del suono nel tempo. Quanto tempo impiega per raggiungere il volume massimo quando una nota inizia, come decade dopo, quale volume mantiene mentre la nota è tenuta premuta e quanto ci vuole per silenziarsi dopo il rilascio. I quattro parametri erano divisi tra i due byte a $D405 e $D406, quattro bit ciascuno.
CIA, leggere il joystick
Il CIA gestiva gli input, e leggere il joystick era una delle operazioni più comuni nei giochi. Il registro $DC00 conteneva lo stato delle due porte di controllo:
LDA $DC00 ; legge la porta del joystick
AND #$10 ; isola il bit del pulsante di fuoco
BNE non_sparato ; se bit = 1, il pulsante NON è premuto
; arrivato qui: il pulsante è premuto
non_sparato:
Il dettaglio controintuitivo è la logica invertita. Bit 0 significa che il pulsante è premuto, bit 1 significa rilasciato. Questo si chiama active-low, comune nell’hardware dell’epoca dove il pin fisico veniva portato basso quando attivo. Ogni direzione del joystick corrispondeva a un bit:
Bit 0 → su
Bit 1 → giù
Bit 2 → sinistra
Bit 3 → destra
Bit 4 → pulsante di fuoco
Verificare se il giocatore stava andando su e a destra allo stesso tempo era solo una questione di controllare entrambi i bit con operazioni logiche. Non c’era nessuna astrazione di “evento di input”. Era lettura hardware, grezza.
Il loop principale di un gioco
Con tutto questo a disposizione, la struttura di un gioco sul C64 era diretta:
game_loop:
JSR leggi_joystick
JSR aggiorna_giocatore
JSR verifica_collisioni
JSR aggiorna_sprite
JSR aspetta_raster ; sincronizza con $D012
JMP game_loop
La subroutine aspetta_raster era il cuore della sincronizzazione. Rimaneva in un ciclo leggendo $D012 finché il raggio non raggiungeva una riga specifica, di solito sotto l’area visibile dello schermo. Solo allora il ciclo avanzava al frame successivo. Questo garantiva che tutti gli aggiornamenti di sprite e colori avvenissero quando il raggio non stava disegnando, evitando il tearing visivo che si verifica quando aggiorni uno sprite mentre il raggio lo sta attraversando.
Questo schema di sincronizzazione manuale con l’hardware video è l’antenato diretto del VSync che conosci nei moderni motori di gioco. La differenza è che sul C64 lo implementavi a mano, sapendo esattamente cosa stavi facendo e perché.
A questo punto conosci i tre chip e sai cosa fa ogni indirizzo. Il VIC-II cambia ciò che appare sullo schermo. Il SID genera suono. Il CIA legge il mondo esterno. Ognuno vive nella mappa di memoria e risponde alle scritture come se fossero comandi.
Quello che manca è vedere tutti e tre funzionare insieme in un programma reale, non in esempi isolati, ma coordinati da un ciclo che legge un tasto, suona una nota e cambia un colore allo stesso tempo. È esattamente quello che faremo nel prossimo post.