LudoProgrammazione su 6502/6510 (Parte #04 – PacText il distruttore)

(di Emanuele Bonin)

Versione PDF
Sorgente ASM

In questo quarto articolo dedicato alla programmazione in assembly, giocheremo con un nostro vecchio amico che sfido chiunque a non riconoscere (qualche hanno fa ha fatto pure un film): Pac-man in formato testo. Egli verrà a trovarci nel nostro schermo e come si muoverà, farà terra bruciata di tutto ciò che incontrerà, come è nella sua natura d’altronde.

Chi è Pactext

Pactext è semplicemente un Pac-man che invece di gironzolare attraverso labirinti in alta risoluzione, si diverte a distruggere parte del nostro schermo, mangiando tutti i caratteri che incontra. Egli ha delle dimensioni ragguardevoli (sicuramente rispetto ad un carattere), infatti ha un’area totale di 9 caratteri (3×3) ed ha una fame invidiabile. Inoltre, nonostante l’ambiente low-res, egli ha una forma invidiabile (un cerchio, la forma perfetta per eccellenza) e riesce a muoversi con una certa fluidità.

Ridefinizione dei caratteri

Dovendo far muovere il Pactext nel nostro video a caratteri, anch’egli dovrà essere formato da caratteri, ma non avendone a disposizione caratteri con i quali comporre una forma circolare della grandezza desiderata (3 per 3), non ci resta che costruirceli.
Nel Commodore 64 (ma questo è vero anche per altri computer ad 8 bit) la forma dei caratteri che noi utilizziamo è definita all’interno della ROM (Read Only Memory), quindi non possiamo certo andarli a modificare in quella posizione. I progettisti comunque hanno pensato bene di permettere a chi ne avesse necessità di poter trasferire la parte di ROM dedicata alla definizione dei caratteri nella RAM (Random Access Memory), dove possiamo metterci le mani, e poi di far leggere al C64 la definizione dei caratteri da questa nuova collocazione.
Il responsabile della visualizzazione dei caratteri, e non solo, è il chip VIC-II, il quale può leggere la definizione dei caratteri da ROM (come all’accensione) oppure dalla RAM, nel momento in cui si vada ad agire su alcuni bit in un particolare indirizzo di memoria. Il Vic-II può “vedere” 16 Kilobytes (16384 bytes) alla volta, inizialmente punta alla ROM, ma agendo sui bit 1,2,3 all’indirizzo \$D018 (che corrisponde al 24° registro dedicato al VIC-II) possiamo indicare al Vic quale banco RAM utilizzare per leggere i nostri caratteri ridefiniti. Non voglio ammorbarvi elencando tutte le possibili combinazioni, per ora ci è sufficiente sapere che settando i bit 2 e 3 e resettando il bit 1, la definizione dei caratteri verrà letta a partire dalla locazione $3000, combinazione che andremo ad utilizzare per creare il nostro Pactext.
Ora non ci resta che andare a prendere i bytes che definiscono i caratteri e spostarli dalla ROM alla RAM. Come abbiamo visto il Vic legge normalmente il contenuto dei bytes di definizione dei caratteri dalla ROM, ma la CPU, quindi il nostro programma assembly, non può leggere quelle locazioni di memoria della ROM, fintantoché non viene resettato il bit 2 del registro di I/O della cpu che si trova all’indirizzo \$01, in seguito a questa operazione potremo far eseguire la nostra routine che copia la definizione dei caratteri dalla ROM alla RAM.
C’è però un “problemone” da risolvere prima di procedere con qualsiasi cosa, quando “abbassiamo” la ROM dei caratteri sulla RAM, a causa della sovrapposizione degli indirizzi, non viene più vista la porzione di RAM per la gestione degli I/O e del SID e abbiamo il problema che, come minimo, sessanta volte al secondo viene fatta una operazione di I/O, cioè la scansione della tastiera. Per ovviare a questo problema sarà sufficiente disabilitare le interruzioni di I/O resettando il bit 0 all’indirizzo \$DC0E il tempo necessario per eseguire le nostre operazioni di copia.

Prima di dare uno sguardo al codice utilizzato vediamo come funziona la ridefinizione dei caratteri.
Ogni carattere è composto da 8×8=64 pixel, dobbiamo pensare ad ognuna delle otto righe che compongono il carattere come ad un byte, la riga in alto corrisponde al primo byte e via via a scendere. Come valorizzare questi bytes ? La cosa risulta alquanto semplice, basta armarsi di una penna, di un righello e di un foglio di carta quadrettato e tracciamo sul foglio un quadrato 8 x 8. ora immaginate che ogni quadretto all’interno del “quandratone” sia un pixel che può essere spento o acceso. Il nostro carattere, così come è ora, risulta tutto spento, non ci resta che accendere i pixels che ci interessano per formare visivamente il nostro carattere, colorando i pixel che ci interessano. Ora, in corrispondenza di ogni riga di 8 caselle, scriviamo, su altri 8 quadratini, a destra del nostro quadrato, un numero binario con gli zeri in corrispondenza dei pixels spenti, e gli “uni” in corrispondenza di quelli accesi. Bene, le cifre binarie ottenute, lette dall’alto al basso, sono la sequenza di valori (bytes) da inserire nelle opportuni locazioni di memoria al fine di ottenere il nostro carattere.

Nella figura c’è un l’esempio del primo dei caratteri ridefiniti per formare il Pactext. Fortunatamente esistono tool che ci permettono di lavorare visivamente con i pixels e di generare la sequenza di bytes da utilizzare nella nostra mappa dei dei caratteri in RAM, senza usare la penna ed il righello.
Nel C64 abbiamo a disposizione due set di 256 caratteri da usare alternativamente (per passare dall’uno all’altro con la tastiera sarà sufficiente premere i tasti shift+ tasto Commodore), per un totale di 256*2*8 = 4096 bytes (4 KB) per la definizione completa di tutti i caratteri. Come accennato in precedenza non è necessario ridefinirli comunque tutti, ci limiteremo a ridefinire quelli che ci servono, e una volta effettuata la copia delle definizioni standard dalla ROM, dovremo individuare la nuova posizione nella RAM di ogni carattere ridefinito e quindi copiarvi dentro gli 8 bytes necessari.

Riporto sotto la routine di copia dei caratteri con le sole parti salienti:

CopiaCaratteriDaROMaRAM
                            
        lda \$DC0E           ; "Spegnimento" della scansione della tastiera (Interrupt) 
        and #\$FE            ; poneno a 0 il bit 0 di \$DC0E => A
        sta \$DC0E           ; A => \$DC0E
        lda \$01             ; Pongo a 0 il bit 2 all'indirizzo $01, in tal modo
        and #\$FB            ; rendo inacessibile l'area di I/O (ora disabilitato)
        sta \$01             ; e al suo posto posso accedere alla ROM, dove andrò
                            ; a leggere i bytes di definizione dei caratteri che
                            ; che normalmente vengono utilizzati in $D000-$DFFF
    ...
    ; ... cicli di copia dei 4096 bytes di definizione standard
    ; a partire dall'indizzo \$D000 nell'indirizzo \$3000 
    ...                         
       
   lda $01             ; Eseguo le operazioni inverse rispetto a quelle effetuate
        ora #\$04            ; all'inizio, quindi metto a 1 il bit 2 all'indirizzo $01
        sta \$01             ; con una operazione di or sull'accumulatore
        lda \$DC0E           ; e metto a 1 il bit 0 di $DC0E
        ora #\$01            ; sempre con una or Accumulator
        sta \$DC0E           ;
        lda \$D018           ; Abilito la lettura dei caratteri
        and #\$F0            ; a partire da \$3000
        ora #\$0C            ; settando i bit 2 e 3
        sta \$D018           ; all'indirizzo \$D018

        ; -------- Fine copia caratteri standard

        ; -------- Inizio copia dei bytes di redefinione caratteri

                            ; I caratteri ridefiniti da copiare partono da CHRDEF
                            ; ho ridefinito 69 caratteri a partire dai ':'
                            ; quindi mi limiterò a coiare:
                            ; 69*8  = 552 bytes = 2 * 256 + 40


        lda #<CHRDEF        ; Preparo in \$FB/\$FC
        sta \$FB             ; i valori LSB/MSB
        lda #>CHRDEF        ; dell'indirizzo da dove prelevare
        sta \$FC             ; i valori dei bytes di definizione dei caratteri
        lda #\$D0            ; Preparo in $FD/$FE l'indirizzo di arrivo
        sta \$FD             ; mi limito a copiare i 69 caratteri ridefiniti 
        lda #\$31            ; a partire dai ':' (58° carattere - \$3A)
        sta \$FE             ; quindi 58 * 8 = 464 (\$01D0) + \$3000 = $31D0
                            ; quindi Inizializzo $FD/$FE = $31D0

    ...                     
    ; ... Ciclo di copia dei 552 bytes (69 caratteri * 8 bytes)
    ; a partire da dallindirizzo CHRDEF in  \$31D0              
    ...
        rts

Nelle prime tre righe di codice andiamo a disabilitare l’interrupt tramite una operazione di AND con la maschera ‘11111110’ (\$FE) che lascerà inalterati i bit in corrispondenza dei bit a 1 della maschera mentre resetterà i bit in cui la maschera, in corrispondenza, ha uno 0. Con una operazione simile vado a lavorare all’indirizzo $01 per permettere alla CPU di accedere alla parte di ROM con la definizione dei caratteri. Segue il codice di mera copia dalla ROM alla RAM delle definizioni e successivamente il codice che ripristina il normale accesso alla ROM da parte della CPU, la ripresa delle interruzioni. Queste operazioni vengono fatte con la stessa tecnica delle maschere di bit fatta all’inizio, solamente che al posto di un AND usiamo l’operazione di OR con la opportuna maschera che lascerà inalterati i bit in cui corrisponde nella maschera uno zero e setta quelli in cui è presente un uno. Alla fine della copia della ROM attiviamo la lettura da parte di Vic della RAM a partire da \$3000, senza quest’ultima operazione sarebbe stato tutto inutile.

Ora inizia la copia dei caratteri da noi ridefiniti. Prima di tutto bisogna determinare gli indirizzi della ridefinizione in RAM su cui sovrascrivere i nostri valori appena calcolati. Siccome abbiamo ridefinito solamente 69 caratteri contigui a partire da cinquantanovesimo del primo set di caratteri a partire dal carattere 58 (\$3A), i bytes di ridefinizione andranno scritti dopo 58 * 8 = 464 (\$01D0) bytes a partire da $3000, quindi dall’indirizzo \$31D0.
Infatti all’inizio della copia delle nostre ridefinizioni dei caratteri andiamo ad inizializzare i nostri puntatori di pagina zero usati per l’indirizzo target con \$31D0, i puntatori dell’indirizzo sorgente sono stati inizializzati all’indirizzo CHRDEF, che non è altro che la label che è stata assegnata a quella porzione di memoria dedicata dal programma per inserire in successione tutti i bytes di ridefinizione in attesa di essere copiati.

CHRDEF
        BYTE \$00,\$03,\$0E,\$18,\$30,\$20,\$60,\$40 ; CHARACTER 58
        BYTE \$FF,\$81,\$00,\$00,\$00,\$00,\$00,\$00 ; CHARACTER 59
    ... 

Il Movimento

La successione dei frames da Sinistra a destra e dall’alto verso il basso

Ora che sappiamo come ridefinire un singolo carattere, possiamo ripetere la ridefinizione su altri caratteri in modo che disponendone 9 in una griglia 3×3 otterremo la forma del nostro compagno di gioco. Ma come ben sappiamo egli è famelico e quindi dovrà poter aprire e chiudere la bocca e nel mentre deve anche procedere in avanti. Quindi non basteranno 9 caratteri ridefiniti, d’ora in poi un frame, ce ne vorranno altri con in quali il nostro Pactext apparirà a con la bocca sempre più aperta per poi richiuderla fino a serrare le potenti ganasce sullo sfortunato carattere che verrà tritato ed ingoiato. Ora che abbiamo abbozzato l’idea dei frame da utilizzare dobbiamo pensare alla movimentazione orizzontale (nel programma dovrà poter procedere da sinistra a destra), se ci limitassimo a spostarlo di un intero carattere alla volta il movimento non verrebbe un granché, non ci sarebbe quella fluidità di cui ho scritto nella presentazione. Per fare in modo che non venga a mancare una certa fluidità nei movimenti bisogna riconsiderare la forma dei vari frame che dovranno susseguirsi, basterà fare in modo che al susseguirsi dei frame il PacText faccia un passettino di un pixel invece che di un carattere, in tal modo daremo l’impressione che Pactext non sia intrappolato nella rigidità dei caratteri.
Per gestire il tutto ho fatto in modo di creare 8 frame di movimento 7 dei quali composti da 9 caratteri e uno (il sesto) composto da soli 6 caratteri per un totale di 69 caratteri. Con questa serie di frame il programma dovrà solamente preoccuparsi di visualizzare i primi 5 frame sulla stessa posizione, l’effetto di avanzamento verrà prodotto dai caratteri stessi, al sesto frame Pactext effettuerà un effettivo avanzamento, e nei successivi frame comincerà a riserrare le ganasce fino a mordere il povero carattere.

Alcune considerazioni si potrebbero fare sul fatto che avrei potuto risparmiarmi la ridefinizione dei tre spazi presenti nel terzo, quarto e quinto frame, ma per gestire comodamente la successione dei frame è meglio avere tutti i caratteri ridefiniti in successione.
La gestione del morso del carattere inizia al settimo frame, dove il carattere, innanzi alle fauci di Pactext, è ancora intero, ma all’ottavo frame il programma non visualizzerà la bocca socchiusa dell’ottavo frame, ma visualizzerà il merging, quindi una ridefinizione al volo, tra questa e il carattere che si trovava davanti alle fauci nel frame precedente, mettendo il risultato sul carattere \$7F (127), in questa maniera il carattere apparirà effettivamente morsicato.

Effetto morso

Nell’estratto della routine che segue, volevo mettere in evidenza il metodo utilizzato per eseguire la crasi di due caratteri, nello specifico i caratteri che produrranno l’effetto della morsicata da parte di Pactext:

Change127
    ...
    ...
        lda Eated   ; Metto nell'accumulatore il carattere morso ...           
        sta Temp    ; ... lo salvo su una word di comodo (LSB)
        lda #\$00    ; inizializzo       
        sta Temp+1  ; l'MSB della variabile di comodo
                    
        asl Temp    ; Temp * 2 LSB, asl shifta a sinistra mettendo nel carry
                    ; il bit più a sinistra e nel bit 0 mette 0
        rol Temp+1  ; Temp * 2 MSB, rol shifta a sinistra facendo rientrare 
                    ; il carry nel bit più a destra a destra   
                    ; mentre il bit 7 lo mette nel carry
        asl Temp    ; Temp * 2 LSB
        rol Temp+1  ; Temp * 2 MSB
        asl Temp    ; Temp * 2 LSB
        rol Temp+1  ; Temp * 2 MSB

                    ; Ora sommo \$30 all'MSB di Temp in modo
                    ; da avere in Temp LSB/MSB l'indirizzo
                    ; da dove pescare la definizione del carattere mangiato
                    ; che sarà in realtà un carattere non ridefinito
        lda Temp+1  ; Carico MSB in A
        clc         ; pulizia carry
        adc #\$30    ; somma diretta \$30
        sta Temp+1  ; rimetto il risultato nell'MSB di Temp

        lda Temp    ; Inizializzo \$A7/\$A8 con l'indirizzo della redefinizione
        sta \$A7     ; del carattere morso
        lda Temp+1  ; 
        sta \$A8     ;        

        lda First   ; First contiene il carattere della bocca semichiusa

        sta Temp    ; .... Moltiplio * 8        
        lda #\$00            
        sta Temp+1          
        asl Temp            
        rol Temp+1          
    ...
    ...
            ... Sommo \$3000

        lda Temp    ; Salvo il risultato in \$A9/\$AA          
        sta \$A9             
        lda Temp+1          
        sta \$AA             

        lda #\$F8    ; Metto in \$FD/\$FE il puntatore        
        sta \$FD     ; alla ridefinizione del carattere \$7F (127)
        lda #\$33    ; dove ridefinirò un carattere al volo
        sta \$FE     ; composto dalla bocca semichiusa e il carattere morso

        ldy #\$07    ; Inizializzazione Ciclo di merging
LoopOr
        lda (\$A7),y ; Carico il Y-esimo byte del carattere morso
        ora (\$A9),y ; ... lo metto in OR con l'Y-esimo byte della bocca
        sta (\$FD),y ; ... deposito il risultato nell'Y-esimo byte del carattere \$7F
        dey         ; decremento il contatore
        bpl LoopOr  ; Y>=0 ? Si ? esegui l'OR dei prossimi byte di definizione caratteri
    ...
    ...
        rts         ; Lavoro compiuto, torna al chiamante

Esempio di moltiplicazione * 2 di una word. La successione delle operazioni va letta da destra a sinistra.

La routine è alquanto semplice, nelle prime righe reperiamo il carattere che verrà morso, depositato dal programma all’indirizzo Eated, utlizziamo una word Temp, la inizializziamo al valore del carattere (che è praticamente un indice della posizione del carattere) per eseguire dei calcoli, alla fine dei quali otterremo l’indirizzo dal quale il carattere morso ha la sua definizione in RAM. Per prima cosa Temp viene moltiplicato per otto, ed essendo otto una potenza di due ce la caviamo con tre shift a sinistra dei bit (ricordo che lo shift a sinistra di un bit equivale a moltiplicarne * 2 il valore), dovendo eseguire l’operazione su una word, dapprima di esegue una ASL (Arithmetic Shift Left) nell’LSB, spostando di una posizione a sinistra tutti i bit con l’inserimento di un bit a 0 a destra e la copia nel carry del valore del bit uscente più a sinistra, in seguito viene eseguita l’operazione di ROL (ROtate Left) sull’MSB, questa operazione sposta anch’essa i bit di una posizione a sinistra ma prima inserisce nel bit 0 (estremo bit a destra, cioè il bit meno significativo) il valore del carry (che ora contiene il bit 7 dell’LSB) e poi rimette nel carry il valore del bit 7 (bit all’estrema sinistra, cioè il bit più significativo) dell’MSB, in modo da potere ripetere un ulteriore ROL se per esempio dovessimo moltiplicare per 2 un valore rappresentato su 3 bytes. Per ottenere l’indirizzo d’inizio definizione ora non ci resta che sommare il valore \$3000, operazione che già sappiamo fare. Poniamo il risultato negli immancabili indirizzi della pagina zero ed eseguiamo le medesime operazioni per il carattere della bocca socchiusa (First), salvando il risultato in altri indirizzi della pagina zero. Ora gli indirizzi per le varie definizioni dei caratteri sono in \$A7/\$A8 per il carattere da mordere e in \$A9/\$AA il carattere delle bocca nell’atto di mordere, ci manca da inizializzare una terza coppia di indirizzo della pagina zero (\$FD/\$FE) dove mettere l’indirizzo di definizione del carattere \$7F, che sarà il carattere ridefinito al “volo”, per questo non abbiamo certo bisogno di calcoli a run-time.
La parte di merging vero e proprio non farà altro che prendere uno a uno gli otto bytes della definizione di ogni uno dei due caratteri da mergiare assieme, compiere una operazione di OR (ORA (\$A9),Y) e salvare il risultato sul corrispondente byte della definizione del carattere \$7F.
Dopo la chiamata alla routine sopra descritta potremo utilizzare il carattere \$7F per visualizzare il carattere tra le ganasce di Pactext.

Routine Principale

La routine principale del programma, essenzialmente esegue un ciclo continuo sulla label CurrFrame, che tiene conto del frame da visualizzare, avendo l’accortezza di visualizzare i primi 5 sempre nella stessa posizione, per poi avanzare di un intero carattere e visualizzare gli ultimi 3. Questo fintantoché Pactext non raggiunge il bordo destro dello schermo. Dopodiché ripartirà dal bordo sinistro, due righe più sotto per eseguire la seconda, e ultima passeggiata. Il listato è bene commentato, inoltre le tecniche in esso utilizzate sono già state affrontate in precedenza, per cui non dovrebbe essere difficile comprenderlo.

Migliorie ?

La risposta è Ovvio! Ci sono un sacco di migliorie e ottimizzazioni che possono essere fatte, dal creare frame intermedi e rendere ancora più fluido il movimento a fare in modo che al passaggio di Pactext, venga lasciata una scia di detriti o una scritta o quello che più vi aggrada. Provate a modificare il programma, anche con piccoli aggiustamenti, questo sicuramente, se non siete già esperti, vi aiuterà nella comprensione del linguaggio Assembly.

Have your say