LudoProgrammazione su 6502/6510 #03 – Una vita da protista (Yet Another Conway’s Game of Life)

(di Emanuele Bonin)

Versione PDF Articolo
Sorgente ASM

In questo terzo articolo dedicato alla programmazione ludica vorrei presentare una versione per C64 dell’immancabile gioco della vita inventato da John Horton Conway, uno dei più carismatici matematici che l’umanità possa vantare di avere.
Lo schermo dell’amato biscottone (io adoro la forma stondata  delle prime versioni) diverrà un brodo primordiale in cui esseri monocellulari (i protisti appunto) lottano per vivere e riprodursi. Anche questa volta, come nei ludoprogrammi precedenti, avremo la possibilità di intervenire, durante lo svolgersi della vita, al pari di una divinità, posizionando qua e là alcuni esserini che potranno dare così anche loro un contributo all’interno del mondo creato.

Nascere, vivere e morire da protisti

Lo screenshot di un momento dell’evoluzione dei protisti.

Per coloro che non conoscessero Life (presumo veramente pochi) riassumerò brevemente come si svolge. Assumiamo di partire da varie colonie, sparse a casaccio, di cellule bidimensionali e tutte della stessa dimensione, lo spazio occupato da un protista in una cella, non può contemporaneamente contenerne un altro. Essi vivono in un mondo limitato e la loro vita è scandita da un orologio discreto, nel senso che il tempo non è continuo, ma scandito da periodi che possiamo enumerare con numeri naturali.
Ogni protista ha la possibilità di avere attorno a se fino a 8 cellule vive, ad ogni “tic” del nostro orologio possono nascere nuovi protisti (nuova generazione) e possono morirne altri in base a tre semplici regole:

  • se una cellula risultasse viva è avesse meno di due cellule attorno a se, morirà in seguito di solitudine, lasciando vuoto lo spazio occupato;
  • se una cellula risultasse viva e avesse più di tre cellule attorno a se, morirà per soffocamento nella prossima generazione;
  • se una cella risultasse vuota, ma avesse attorno a se esattamente 3 protisti, in quella cella nascerà un nuovo protista, anche se non so dirvi esattamente con quale meccanismo (forse una “mitosi condivisa” tra le tre cellule!?).

A seconda della disposizione iniziale delle varie cellule nel mondo virtuale, si osserveranno colonie che si “allargano” lasciando dei buchi al centro, spostano, si estinguono o rimangono stabili, dando vita a nuove generazioni intuitivamente non prevedili.

Dalla biologia allo schermo

Con la manciata di regole da seguire ed avendo a disposizione il nostro fidato C64 possiamo tranquillamente scrivere un programma in assembly che simuli il susseguirsi delle generazioni di protisti. Inoltre avremo la possibilità di creare al “volo” nuove colonie (già avrete capito che anche stavolta l’interrupt del C64 farà la sua comparsa) e ad ogni protista assegneremo un colore che cambierà ad ogni ciclo, qualora non morisse, praticamente l’indicazione di quanto “vecchio” sia.

Useremo gli onnipresenti indirizzi di memoria video per visualizzare (ma anche per “leggere”) la generazione attuale, mentre per la generazione che verrà dovremo riservarci uno spazio di memoria egualmente ampio dove riporre la rappresentazione della nuova configurazione.
Lo schermo ha un’area di 40×25 caratteri (ogni carattere una cella) pari a 1000 byte, quindi altrettanti byte verranno riservati e “targarti” per il nostro fine:

NWGEN   bytes $001000  ; Celle di memoria riservate per la nuova generazione
La scrittura $00
1000 ci permette di non scrivere per 1000 volte la stringa $00.

NWGEN quindi sarà l’indirizzo dal quale partirà la memoria riservata ai nostri scopi.

Il corpo principale del programma (Start), a grandi linee, dovrà iterare, fino alla fine del mondo, su tre fasi, ognuna delle quali è numerata da 0 a 2 all’interno dell’indirizzo Phase:

  1. inizializzazione delle coordinate dello schermo e dei puntatori;
  2. scansione di ogni cella dello schermo, elaborazione e calcolo del nuovo stato della cella;
  3. visualizzazione della nuova generazione.

Nella prima fase (Phase = $00), andremo ad “resettare” i bytes di conteggio e puntamento, che successivamente utilizzeremo nella seconda fase.

Quest’ultima si occuperà di passare in rassegna i 1000 bytes della memoria video, controllando quante celle adiacenti siano occupate da protisti e quante siano vuote, applicherà le regole del gioco e scriverà nell’omologa cella all’interno della memoria riservata alla nuova generazione (NWGEN) l’indicazione se la cella sarà occupata o meno nella prossima era, inoltre andrà ad indicare il colore che assumerà la cella e evidenzierà con il colore nero le cellule morenti.

Dopo aver completato la scansione, entra in gioco la terza fase che semplicemente copierà i valori dei bytes della nuova generazione nei corrispettivi indirizzi della memoria video. Dopodiché tutto reinizierà daccapo.

Coordinate, indirizzi e routine mutanti

La scansione di tutte e 1000 le celle di memoria video avverrà partendo dalla cella in alto a sinistra all’indirizzo \$0400, proseguirà fino alla cella all’estrema destra della prima riga, quindi ripartirà dalla prima cella della seconda riga e così via, fino alla cella in basso a destra, all’indirizzo \$07E7. Così facendo per avere l’indirizzo di ogni singola cella video, seguendo lo stesso percorso sopra descritto, basterà partire dalla cella video \$0400 incrementandolo di uno ad ogni ciclo di scansione.

Al fine di velocizzare le operazioni di scansione, eviteremo i calcoli per convertire l’indirizzo della cella video “corrente” in coordinate di riga e colonna (necessarie per i controlli di validità dell’intorno della cella), riservando due byte (Row e Column) che verranno opportunamente incrementati parallelamente ed in sincronia con l’incremento dell’indirizzo correntemente controllato.

Quest’ultimo viene inserito in una coppia di byte della pagina zero, le onnipresenti \$FB e \$FC in modo da poter sfruttare la lettura e scrittura della cella tramite l’utilizzo dell’indirizzamento indiretto indicizzato con l’accumulatore. Nello specifico del programma l’indirizzo \$FB è stato etichettato con ScrPos, andremo ad incrementare la word composta dai due byte al fine di raggiungere tutte e 1000 le locazioni di memoria video. Supponiamo che l’indirizzo \$FB contenga il valore $00 mentre il successivo \$FC contenga \$04, con le istruzioni:

ldy #$01
lda ($FB),Y

stiamo chiedendo al nostro processore di caricare nell’accumulatore il valore che si trova all’indirizzo formato dai valori all’interno di $FB e $FC nel formato LSB/MSB, quindi \$0400, incrementato dal valore all’interno del registro Y, cioè \$0400+ \$01 = \$0401.

Quindi basterebbe incrementare Y, lasciando intonsi i due bytes \$FB/\$FC, per raggiungere le celle successive. Ma in Y possiamo avere al massimo il valore $FF che ci permeterebbe di leggere al massimo fino a \$04FF (le prime 256 celle), quindi per raggiungere le celle oltre $FF dovremo necessariamente scrivere in \$FB/\$FC l’indirizzo $0500 e giocare nuovamente con Y per raggiungere altri 256 bytes e così via, oppure, come più spesso accade (e in questo mondo virtuale accade!), lasciamo il registro Y sempre a 0 e incrementiamo di volta in volta di una unità la word \$FB/\$FC.

La pagina zero risulta comunque quasi tutta occupata dal “sistema operativo” del C64, cioè dal basic, ed è per questo che nei programmi (non solo quelli che presento) si troveranno spesso utilizzati sempre gli stessi indirizzi della pagina zero, a meno di non disabilitare il Basic, il che è comunque possibile. Volendo preservare il normale funzionamento del basic e spulciando la mappa di memoria del C64 si possono trovare alcuni indirizzi (l’uso che vogliamo farne impone che siano due indirizzi appaiati) non utilizzati o normalmente utilizzati in casi abbastanza particolari che non non vanno a cozzare con il programma che si sta scrivendo. In questa rivisitazione di Life utilizzo altre 2 coppie di indirizzi della pagina zero, oltre alla succitata ScrPos in \$FB/\$FC, verranno sfruttati gli indirizzi $FD/FE (ScrMod) e \$03/\$04 (ScrMod2). La prima la utilizzo per fare la scansione delle celle adiacenti alla cella “corrente” mentre la seconda la utilizzo per fare la copia della nuova generazione. Sicuramente son stato troppo prodigo, avrei potuto riutilizzare in alcuni punti gli stessi indirizzi della pagina zero, ma ho voluto stare comodo.

A ben guardare l’indirizzamento che sfrutta la pagina zero (indiretto indicizzato e il suo “simile” ma meno utilizzato indicizzato indiretto) è l’unico modo “nativo” per raggiungere una qualsiasi cella di memoria il cui indirizzo si conoscerà solo a run-time. Infatti, negli altri metodi di indirizzamento, l’indirizzo da cui reperire i valori va comunque scritto esplicitamente durante la fase di sviluppo.

Fortunatamente l’architettura del 6502/6510 ci permette di eseguire una sorta di hacking del codice stesso. Infatti questo tipo di processori, nel momento in cui devono eseguire una certa istruzione, leggono l’istruzione direttamente dalla memoria in cui è stata scritta, quindi senza quella che viene definita fase di pre-fetch delle istruzioni, una fase presente nei moderni microprocessori, in cui le istruzioni da eseguire vengono lette in un momento antecedente alla necessità di essere eseguite e depositate in un’altra zona di memoria ad accesso più veloce (la cache di pre-fetch appunto), al fine di velocizzare l’esecuzione dei programmi. Inoltre, altra caratteristica del 6502/6510, è il fatto che non vi sia, a livello hardware, una netta distinzione tra memoria di programma e memoria di dati, quindi possiamo leggere e scrivere indifferentemente sia sull’uno che sull’altro. La mancanza di queste due caratteristiche ci permette di creare programmi auto-mutanti, le istruzioni quindi possono essere modificate o create anche un “istante” prima della loro esecuzione.

Quest’ultima tecnica è stata usata anche nella routine che inizializza lo schermo:

Initialization
        lda #FirstGen    ; inizializzo colore caratteri a $07 (prima generazione)
        sta TxtColor     ; locazione di memoria per settare il colore testo

        lda #>Screen     
        sta ReadChar+2   ; MSB locazione memoria video per lettura
        lda #<Screen     
        sta ReadChar+1   ; LSB locazione memoria video per lettura
        lda #>ColorMap   
        sta SetChrCol+2  ; MSB locazione memoria video per settaggio colore
        lda #<ColorMap   
        sta SetChrCol+1  ; LSB locazione memoria video per settaggio colore

InitLoop
        lda ReadChar+1   ; Copio LSB dell'indirizzo da cui leggere il carattere
        sta ChgChar+1    ; su LSB dell'indirizzo dove verrà eventualmente sovrascritto
        lda ReadChar+2   ; ... lo stesso con l'MSB
        sta ChgChar+2    ; ...

ReadChar                 ; Segnaposto per poter identificare
                         ; le locazioni di memoria dell'argomento
                         ; dell'istruzione lda/sta
        lda $FFFF        ; A=valore dello screen code
        cmp #Space       
        beq InitLoopNext ; se A = spazio procedo oltre
        lda #FirstGen    ; inizializzo il colore a $07 (Prima generazione)
SetChrCol
        sta $FFFF        
        lda #Protista     
ChgChar
        sta $FFFF        ; al posto del carattere immetto un pallino
InitLoopNext
        inc SetChrCol+1  ; incrementa LSB dell'argomento di sta per il ColorMap
                         ; se LSB era $FF sommando 1 diventa $00
                         ; quindi bisogna incrementare di uno anche MSB
        bne InitLoopInc  ; se <> 0 non c'è stato riporto quindi non incremento MSB
        inc SetChrCol+2  ; altrimenti incremento MSB dell'argomento

InitLoopInc

        inc ReadChar+1   ; incrementa LSB dell'argomento
                         ; se LSB era $FF sommando 1 diventa $00
                         ; quindi bisogna incrementare di uno anche MSB
        bne InitLoopTest ; se <> 0 non c'è stato riporto quindi non incremento MSB
        inc ReadChar+2   ; altrimenti incremento MSB dell'argomento
InitLoopTest                 ; controllo di non aver sforato $07E7
                         ; ultima locazione video utilizzabile
        lda ReadChar+2   ; A=MSB dell'argomento, testo se inferiore a $07 non serve altro, la locazione
                         ; è sicuramente nella zona visibile
        cmp #$07         
        bcc InitLoop     ; se MSB < $07 test OK continua pure
InitLoopLSB              ; MSB potrà al massimo essere uguale a $07
                         ; secondo la logica imposta, quindi non servono altri controlli
                         ; su A, ora devo controllare che LSB sia < $E8
        lda ReadChar+1   ; LSB argomento
        cmp #$E8         
        bcc InitLoop     ; se LSB <= $E7 test OK continua pure
InitLoopExit
        rts

Nella routine sopra riportata vengono usate delle costanti definite all’inizio del programma:

BKColor = $06           ; Codice Colore cella vuota (Colore del background all'avvio del C64)
FirstGen= BKColor+1     ; Codice Colore prima generazione
Space   = $20           ; Codice cella vuota
Protista= $51           ; Codice Protista
Screen  = $0400         ; Primo indirizzo memoria video angolo in alto a sinistra
ColorMap= $D800         ; Inizio Mappa colore per il testo a video
TxtColor= $0286         ; Locazione di memoria del C64 per indicare il colore del testo da utilizzare

Già alla terza riga di istruzione vengono inizializzate delle locazioni di memoria che in realtà si riferiscono a indirizzi all’interno del programma stesso (ReadChar e SetChrCol). Più precisamente vanno a scrivere gli argomenti da utilizzare per le istruzioni indicate più avanti (lda \$FFFF e sta \$FFFF).
Gli argomenti, sebbene possano apparire nella forma MSB/LSB nel listato assembly (se scritti esplicitamente, cioè senza l’utilizzo di una label), dobbiamo ricordare che i due bute sono da considerarsi invertiti LSB/MSB.

Ogni istruzione mnemonica, a livello macchina, occuperà un byte, quindi nel momento in cui hackereremo il programma ne dovremo tenere conto, per esempio la parte LSB dell’indirizzo da specificare come argomento nell’istruzione lda \$FFFF si troverà all’indirizzo ReadChar+1 e l’MSB ovviamente in ReadChar+2, in quanto all’indirizzo ReadChar ci sarà il codice macchina dell’istruzione lda con indirizzamento assoluto.

Successivamente, a partire da InitLoopNext, andrò ad incrementare direttamente i valori dei succitati argomenti all’interno del programma e poi a fare le varie considerazioni per le condizioni di uscita dal loop di inizializzazione.

La mappa del colore dei caratteri

Nell’esempio si evidenziano gli indirizzi e i valori coinvolti nella visualizzazione di un pallino bianco nell’angolo in alto a sinistra dello schermo.

Il C64 ci permette di assegnare un colore diverso, scelto tra una tavolozza di 16 colori, ad ogni carattere dello schermo. All’accensione del computer già sappiamo che la memoria video occupa la zona di memoria che va da \$0400 a \$07E7, questi mille byte hanno una corrispondenza 1 a 1 con un’altra zona di memoria in cui viene definito il colore da usare per visualizzare il carattere nell’omologa cella video. La mappa del colore, sempre allo start-up del sistema, si trova dall’indirizzo \$D800 fino a \$DBE7, quindi il colore del carattere in alto a sinistra, definito all’indirizzo \$0400, sarà visualizzato nel colore indicato dal valore del byte all’indirizzo \$D800. Come scritto sopra abbiamo a disposizione 16 colori che vanno da \$0 a \$F, quindi per indicare un colore basta mezzo byte (4 bit), cioè un nibble, ed in effetti di ogni byte della mappa del colore caratteri verrà preso in considerazione il nibble meno significativo (LSN), il nibble più significativo (MSN) può assumere qualsiasi valore, verrà semplicemente ignorato dalla circuiteria che si occuperà della visualizzazione dei caratteri sullo schermo, il famoso video controller VIC II, per gli amici “Vic”. Quindi, ad esempio, utilizzando i valori \$37 o \$B7 all’interno della mappa dei colori otterremo lo stesso colore.

Stack, stack pointer e program counter

In questa sezione volevo approfondire l’utilizzo dello stack (lo avevamo incontrato nel primo articolo) e di conseguenza presentare un registro essenziale per qualsiasi cpu, il program counter, PC. Quest’ultimo, parlando specificatamente del 6502 e 6510, è un registro della cpu a 16 bit che contiene l’indirizzo dell’istruzione che si sta per eseguire, in assenza di istruzioni specifiche (per esempio JSR, JMP e le istruzioni di salto condizionato) una volta che un’istruzione viene eseguita, verrà automaticamente incrementato di un numero che dipende dalla lunghezza dell’intera istruzione stessa (intesa come codice mnemonico ed eventuale argomento) in modo tale da proseguire l’esecuzione con l’istruzione successiva. Ogni codice mnemonico è lungo un byte, mentre l’argomento, se presente, può occupare uno o due bytes, vien da se che gli incrementi possibili per il PC sono 1, 2 o 3.
Ma cosa succede se ad un certo punto il programma esegue una JMP \$C100 ? semplicemente il microprocessore scriverà nel PC il valore \$C100, facendo continuare l’esecuzione del programma a partire da \$C100. Anche con una istruzione di salto condizionato, per esempio BEQ, nel caso in cui la condizione si avverasse, verrà modificato il PC, ma c’è da specificare che i salti condizionati possono avere un’ampiezza limitata rispetto ad un salto incondizionato come JMP o JSR. Infatti quando in assembly scriviamo :

... 
beq $C100   ; occupazione in memmoria 2 bytes CaricoY
ldy #$01     ; occupazione in memmoria bytes
      ;Inizio istruzione dall’indirizzo $C100 
... 

In linguaggio macchina anche la riga con beq \$C100 occuperà solamente 2 bytes (e non 3 come si potrebbe pensare), in quanto al posto dell’intero indirizzo \$C100 (di 2 bytes) , in fase si creazione del programma in LM, verrà messo un solo byte che rappresenterà il numero di bytes (offset) che divide CaricoY da \$C100 , cioè due bytes in tutto:

$C0FC F0 02                 ;BEQ $C100
$C0FE A0 01                 ;LDY #$01
$C100 ...

 

Quindi all’avverarsi della condizione la cpu non farà altro che modificare il program counter usando il valore di offset. Ultima cosa, i salti condizionati possono essere fatti sia in avanti che indietro, quindi il nostro byte potrà variare da -127 a +128 e questo è il range massimo che possiamo ottenere da questo tipo di istruzioni. I numeri negativi, come già visto in precedenza, vengono rappresentati con il complemento a 2.

Illustrati i tre momenti che evidenziano il comportamento dello Stack, Stack Pointer e del Program Counter

Sappiamo che anche l’utilizzatissima istruzione JSR esegue un salto incondizionato, ma in questo caso viene coinvolto lo stack, che permetterà al processore di ricordare da dove è stato effettuato l’ultimo salto con JSR quando incontrerà l’istruzione RTS.
Lo stack è una zona di memoria, riservata alla CPU per effettuare delle operazioni di inserimento (PUSH) e reperimento (POP) di determinati valori, ma seguendo una regola bene precisa, l’ultimo byte che verrà scritto da un’operazione di push, sarà poi anche il primo che verrà letto da una operazione di pop. Lo stack dei processori 6502/6510 si avvale di un registro a 8 bit, lo stack pointer, il quale indica al processore quale è l’indirizzo di memoria libero per la prossima operazione di inserimento. Essendo un registro ad 8 bit, lo stack potrà contenere al massimo 256 bytes, l’indirizzo base dello stack è \$0100, quindi lo stack pointer non è altro che l’LSB da sommare a \$0100 per ottenere l’indirizzo completo della prossima cella libera sullo stack stesso.
Lo stack pointer viene inizializzato col valore \$FF e ad ogni push viene decrementato di 1, mentre ad ogni pop viene incrementato di 1.
L’esecuzione di una istruzione JSR non fa altro che calcolare il valore PC+2, prendere il risultato e scriverlo nello stack tramite due operazioni di push consecutive, prima l’MSB e poi l’LSB. A questo punto mette nel PC il valore dell’indirizzo da cui inizia la nostra routine, che al suo termine avrà l’immancabile RTS, quest’ultimo non farà altro che due operazioni di pop consecutive, reperendo il valore precedentemente salvato nello stack, prima l’LSB e poi l’MSB, incrementa tale valore di uno (per inciso è la lunghezza in bytes dell’istruzione rts) e mette il risultato nel PC, a questo punto quindi il programma continuerà dall’istruzione successiva alla JSR da cui eravamo partiti.
Data la limitata ampiezza dello stack dovremo fare attenzione a non fare più di 128 chiamate JSR l’una all’interno dell’altra senza mai fare RTS, altrimenti incorreremmo in uno “stack overflow error”.

Routine con parametri

La buona pratica di programmazione richiede di scrivere subroutine avente quel codice che viene spesso usato in molti punti del programma in modo da sfruttare l’accoppiata JSR/RTS per richiamarle quando serve, ma soprattutto per andare a modificare in un sol punto del programma stesso qualora vi fosse la necessità di revisionare il codice della subroutine. L’altra buona pratica è quella di scrivere subroutine che possano ricevere parametri e restituire risultati indipendentemente dal programma in cui le si vuole inserire.
Per esempio se scrivessi una subroutine che moltiplica egregiamente le label Fattore1 e Fattore2 e ne mette il risultato nella label Risultato e avessi l’esigenza di usarla in un altro programma totalmente diverso, per poterla utilizzare anche in quest’ultimo dovrei ricordarmi di dichiarare le tre label sopra citate. Praticamente la portabilità della mia subroutine risulta un pochino limitata.
In questa versione di Life ho creato una semplice subroutine che in base ad un parametro leggerà il colore della cella video corrente, ScrPos, oppure lo imposterà con un valore ricavato dal parametro stesso.
I più attenti avranno notato che la routine così spiegata non è completamente portabile, in quanto comunque utilizza almeno una label esterna alla routine (ScrPos) ed hanno assolutamente ragione, ma per la tecnica che vorrei esporre è più che sufficiente questa routine dalla portabilità azzoppata.
Nella routine RdWrCol ho previsto un parametro di lunghezza un byte nel cui nibble più significativo (MSN) posso avere \$0 o un valore diverso da \$0, nel caso in cui l’MSN sia diverso da \$0 è stata richiesta la scrittura del colore della cella video puntata da SrcPos, il valore da scrivere si troverà nell’LSN del parametro, se invece l’MSN dello stesso è uguale a \$0 la routine dovrà leggere il valore dalla mappa dei colori e depositare il colore trovato nell’LSN del parametro passato. Per fornire questo parametro a RdWrCol dovrò semplicemente inserirlo nello stack, tramite una operazione esplicita di push (PHA), successivamente la routine lo preleverà e farà la sue considerazioni. Questo un esempio di chiamata a RdWrCol:

lda #$F7         ; Metto il valore del colore 7 nell’LSN e F nell’MSN
pha              ; push dell'accumulatore sullo stack
jsr RdWrCol      ; routine di lettura/scrittura colore
pla              ; pulisco lo stack dall'argomento

Nelle righe sopra descritte preparo il parametro nell’accumulatore, indicando che voglio venga scritto il valore del colore presente nell’LSN (\$7) dell’accumulatore, quindi eseguo un PHA (PusH Accumulator) e subito dopo richiamo la routine RdWrCol, la quale eseguirà quanto richiesto, lasciando il parametro sullo stack, questo è il motivo per cui bisognerà comunque eseguire una PLA (PuLl to Accumulator) successivamente alla JSR, anche se, come in questo caso, il parametro risultasse solo in ingresso rispetto alla routine chiamata.

Ora veniamo alla “ciccia”:

RdWrCol                 
        txa             ; mi salvo il valore di X
                        ; per poterlo sporcare più avanti senza remore
        pha             ; ... e lo metto nello stack
        tya             ; lo stesso per Y
        pha             ; ... salvo nello Stack
                        
                        ; Calcolo offset, differenza tra cella corrente  e inizio memoria
                        ; video (Screen = $0400)
                        ; Essendo LSB(Screen) = 0, la differenza tra 
                        ; LSB(ScrPos) - LSB(Screen) = LSB(ScrPos)
                        ; mi limito a copiare LSB(ScrPos)
        lda ScrPos      ; A=LSB(ScrPos)
        sta RdWrIstr+1  ; Copio l'accumulatore all'indirizzo del programma
                        ; dove c'è l'LSB dell'indirizzo del colore della cella corrente
        lda ScrPos+1    ; A=MSB(ScrPos)
        sec             ; imposto il carry per prepararmi alla sottrazione degli MSBs
        sbc #>Screen    ; A=MSB(ScrPos) - MSB(Screen) (Offset)
                        ; A questo punto l'Offset è dato dall'Accumulatore MSB(Offset)
                        ; e da RdColByte+2 che contiene LSB(Offset)
                        ; Ora dovrei sommarci l'indirizzo base della mappa del colore (ColorMap = $D800) 
                        ; la quale ha l'LSB=$00 quindi lascio stare RdWrIstr+1 e sommo solo gli MSBs
                        ; A = MSB(Offset) quindi posso già procedere alla somma di MSB(ColorMap)
        clc             ; pulisco il carry e mi preparo alla somma
        adc #>ColorMap  ; A=A + MSB(ColorMap) => MSB(Offset) + MSB(ColorMap)
        sta RdWrIstr+2  ; Copio l'accumulatore all'indirizzo del programma 
                        ; dove c'è l'MSB dell'indirizzo del colore della cella corrente
                        ; così sono pronto alla lettura/scrittura del colore                  
                        ; Lettura parametro dallo Stack 
        tsx             ; metto in x lo stack pointer
                        ; sommo $05 per ragiungere l'indirizzo dello stack dove è stato riservato spazio
                        ; dal chiamante per il risultato
                        ; prima del parametro nello stack ho i byte di y e X e i MSB/LSB 
                        ; a partire dall'indirizzo più basso in su
        txa             ; per aggiungere 5 a X, metto X in A
        clc             ; pulisco il carry per prepararmi all'adc
        adc #$05        ; sommo 5 ad A
        tax             ; rirporto A in X
                        ; ora X contiene il valore che dovrebbe avere lo Stack Pointer
                        ; per puntare alla cella parametro/risultato quindi ...
        lda #$F0        ; carico in A la maschera per testare il MSN
        and Stack,X     ; eseguo l'AND bit a bit del parametro (con indirizzamento assoluto indice X)
                        ; con A, il risultato finirà in A
        beq LoadIstr    ; se A==0 allora è stata richiesta la lettura quindi inserisco
                        ; l'istruzione lda all'indirizzo RdWrIstr ...
        ldy #AssSTA     ; .... altrimenti carico in Y il corrispettivo 
                        ; codice LM per STA ad indirizzamento assoluto
        sty RdWrIstr    ; e quindi vado a modificare al volo il programma per far scrivere
                        ; il valore passato invece che leggerlo
        lda Stack,X     ; carico in A il parametro ...
        and #$0F        ; ... azzero con l'AND immediato in A il MSN
        jmp RdWrIstr
LoadIstr
        ldy #AssLDA     ; carico in Y il corrispettivo 
                        ; codice LM per LDA ad indirizzamento assoluto
        sty RdWrIstr    ; e quindi vado a modificare al volo il programma per far scrivere
                        ; il valore passato invece che leggerlo
                        ; così rimane l'LSN (Colore)
RdWrIstr
        lda $FFFF       ; Eseguo la lettura nell'accumulatore del colore corrente
                        ; ... o la scrittura scrittura del colore passato come parametro
                        ; dipendentemente dal MSN del parametro stesso
                        ; $FFFF è solo un placeholder che è stato riempito dalle
                        ; istruzioni precedenti
        and #$0F        ; azzero con l'AND immediato in A il MSN 
                        ; così rimane l'LSN (Colore) in caso di lettura potrebbe essere 'sporco'
                        ; in quanto nella memoria dei colori vale solamente l'LSN
        sta Stack,X     ; ... con un indicizzazione Assouta con indice X ci metto 
                        ; il valore dell'accumuatore (Colore della cella corrente)
                        ; o parametro passato per la scrittura ripulito del'MSN
        pla             ; recupero il valore della Y
        tay             ; salvata all'inizio della routine
        pla             ; recupero il valore della X
        tax             ; salvata all'inizio della routine
        rts             ; ritorno al chiamante, togliendo implicitamente 2 bytes allo Stack
                        ; che sono l'indirizzo a cui tornare subito dopo la jsr del chiamante
                        ; quindi nello stack c'è il parametro di input/output
                        ; sarà cura del chiamate leggerlo e ripulire per bene lo Stack

Nelle prime quattro istruzioni della routine viene eseguito il push nello stack dei registri X e Y, in modo da poterli riprendere subito prima dell’uscita dalla routine. In questo modo, all’interno della routine potrò utilizzarli senza paura di sovrascrivere eventuali valori degli stessi che il chiamante si potrebbe aspettare non alterati dopo la JSR. Buona norma sarebbe quella di salvare prima di tutto il registro di stato (PHP) e successivamente l’Accumulatore e i due registri X e Y, in questo modo il chiamante si ritroverà tutto come prima della chiamata, ma per i nostri scopi espositivi non è necessario.
Dopo il salvataggio dei registri vi sono un po’ di istruzioni già affrontate in precedenza che ci permettono di calcolare l’indirizzo nella mappa dei colori della cella video puntata da SrcPos, tale valore verrà depositato in RdWrIstr, che come noterete è un indirizzo all’interno della routine stessa, quindi anche RdWrCol è una routine mutante, andando a scrivere a run-tima proprio l’argomento dell’istruzione che poi verrà eseguita.
Subito dopo incontriamo il codice mnemonico TSX (Transfer Stack pointer to X) il quale prende il valore corrente dello stack pointer e lo copia nel registro X, in questo modo potremmo raggiungere, all’interno dello stack, l’argomento passato dal chiamante. Nello stack abbiamo in sequenza (con indirizzi decrescenti):

  • parametro passato alla routine (1 byte) inserito dal chiamante tramite PHA;
  • indirizzo di ritorno (2 bytes) inseriti implicitamente dal chiamante con JSR;
  • valore del registro X salvato dalla routine (1 byte);
  • valore del registro Y salvato dalla routine (1 byte).

Per un totale di 5 bytes, sapendo che lo stack pointer viene diminuito di un byte per ogni byte inserito nello stack, sarà sufficiente aggiungere \$05 al valore dello stack pointer reperito con la TSX. Di questo si occuperanno le istruzioni successive le quali mettono X (che contiene il valore dello stack pointer) in A, aggiungono all’accumulatore lo \$05 e a quindi rimettono l’accumulatore in X con TAX (Transfer Accumulator to X).
A questo punto per leggere il nostro tanto desiderato parametro potrei utilizzare ad esempio l’istruzione lda \$0100,X (ricordo che \$0100 è l’indirizzo più “basso” dello stack) che caricherebbe nell’accumulatore il valore desiderato, puntato da \$0100+X. Ma avendo la necessità di controllare se l’MSN del parametro sia uguale a 0 (per capire se devo leggere o scrivere l‘LSN di A), devo fare in modo di leggere solo l’MSN del parametro, mascherando l’LSN, le istruzioni seguenti assolvono il compito:

     lda #$F0        
     and Stack,X    ; Stack è una costante = $0100

Dapprima carico nell’accumulatore la maschera opportuna (con l’MSN a \$F), dopodiché eseguo l’istruzione AND (bitwse AND with accumulator) la quale esegue un and logico bit a bit tra l’accumulatore e l’argomento (reperito tramite l’indirizzamento assoluto con indice X) dopodiché il risultato verrà riposto nuovamente nell’accumulatore. Avendo, prima dell’AND, nell’MSN dell’accumulatore tutti 1, nella sua rappresentazione binaria, il risultato di un AND preserverà l’MSN dell’argomento, mentre al contrario, avendo tutti 0 nell’LSN di A, verranno portati a 0 anche tutti i bit dell’LSN del risultato. Ora basta testare se l’accumulatore è uguale a 0 o meno, per sapere se l’MSN del parametro passato segue la stessa logica.
Successivamente, se siamo in scrittura (MSN del parametro != 0), verrà letto anche l’LSN, in maniera similare alla modalità sopra esposta.

Ora manca da decidere se leggere o scrivere il colore, se osservate, nel listato vi è solo l’istruzione lda \$FFFF (quindi lettura), sappiamo già che \$FFFF è un segnaposto per l’argomento che verrà fornito a run-time, ma anche ‘lda’ è in realtà un segnaposto, infatti qualche istruzione prima viene deciso quale valore inserire nell’indirizzo dell’operazione (battezzata RdWrIstr).

All’inizio del programma sono state dichiarate le costanti:

AssLDA  = $AD           ; codice LM per LDA con indirizzamento assoluto
AssSTA  = $8D           ; codice LM per STA con indirizzamento assoluto

le quali corrispondo al codice macchina di lda/sta con indirizzamento assoluto, e, ad esempio, con le istruzioni:

ldy #AssSTA
sty RdWrIstr

Andiamo a modificare l’istruzione in RdWrIstr con una sta.
Prima del ritorno al chiamante ci mancano un paio di cose, la prima è quella di riporre nel parametro passato nello stack il valore del colore eventualmente letto (ripulito dall’MSN) per renderlo disponibile al chiamante stesso, l’altro punto è il ripristino dei registri salvati all’inizio della routine, in sequenza inversa al salvataggio degli stessi, tramite una serie di pop dallo stack.
Gestire i parametri sullo stack è ciò che viene normalmente fatto nei linguaggi di programmazione anche più avanzati, soprattutto nel momento in cui si utilizzano variabili con scope locale, cioè quando vengono definite all’interno di una funzione e distrutte quando la funzione termina il proprio lavoro. Inoltre la gestione dei parametri risulta comoda, se non indispensabile, qualora si vogliano gestire routine ricorsive, cioè che richiamano se stesse.

Il ciclo delle generazioni

Come anticipato Life sfrutterà il ciclo di interruzioni che il c64 esegue 60 volte al secondo, lasciandoci liberi di scrivere lettere a casaccio sullo schermo per simulare la nascita forzata di nuovi individui. Questo ci obbliga a scrivere routine “veloci”, al di sotto del sessantesimo di secondo, che ci permettano quindi di usare agevolmente la tastiera per procedere con l’opera di interferenza alla vita.
Delle tre fasi che compongono un ciclo di vita dei protisti, la seconda fase, elaborazione e determinazione della nuova generazione, risulta essere molto pesante e se effettuata tutta in blocco all’interno di una singola chiamata da parte dell’interrupt, ci si ritroverà con una tastiera poco o per nulla responsiva. Per aggirare l’ostacolo ho fatto in modo che in questa fase della vita, ad ogni ciclo di interrupt, venga gestito un numero esiguo di celle, tramite l’utilizzo di un semplice contatore (Counter) che terrà conto del numero di celle gestite all’interno dell’interrupt request e di una costante (CellsPerIRQ), definita all’inizio del programma, che conterrà il numero di celle che vogliamo gestire ad ogni ciclo di interrupt.

Conclusione

All’interno del programma si potranno vedere chiaramente (almeno spero) le varie parti ben distinte, quelle non presentate in questo articolo contengono tecniche di programmazione già affrontate negli articoli precedenti, ad esempio controllo dell’intorno della cella o controllo delle coordinate video rispetto ai limiti dello schermo, quindi ho ritenuto inutile allungare il brodo.
Nella speranza che il programma sia stato di vostro gradimento vi saluto con un arrisentirci alla prossima puntata, in cui cercherò di introdurre delle novità che spero saranno gradite.

 

Have your say