Scarica la versione PDF

Premessa

Con questa serie di articoli mi propongo di descrivere alcuni piccoli e semplici programmi scritti in Assembly (il linguaggio mnemonico per creare il codice macchina o assembler), per Commodore 64, che scrissi qualche tempo fa, provando molto divertimento nel crearli prima e nel vederli funzionare poi. Si tratta di programmi che non hanno nessuna pretesa, non hanno alcuna funzione in se, se non quella accademica di permettere di scoprire la leggerezza di programmare con il solo scopo ludico ed e di apprendimento. Se qualcuno avrà la pazienza di continuare a leggere e magari di provare a buttare giù due righe da una propria idea scommetto che potrà divertirsi non poco.

Linguaggio Macchina, Assembler ed Assembly

L’assembler (o linguaggio macchina) è l’unico linguaggio che una cpu possa eseguire, qualsiasi altro linguaggio più ad alto livello deve essere per forza “tradotto” in assembler. A seguito delle istruzioni dell’Assembler vengono compiute operazioni semplicissime, del tipo leggi/scrivi una cella di memoria, incrementa, somma, confronta, salta e così via. Nonostante la semplicità delle operazioni possibili, programmare in assembler non è assolutamente difficile e può dare grosse soddisfazioni. Se guardassimo un listato scritto in assembler vedremmo una lista di numeri e nulla più, fortunatamente, in fase di scrittura dei programmi, ci viene in aiuto l’assembly. L’assembly ci evita di scrivere direttamente i numeri interpretati dalla CPU come comandi, infatti al loro posto scriveremo delle sigle (codici mnemonici) più adatte ad essere memorizzate dal programmatore, che verranno tradotte opportunamente dal compilatore. L’assembly ci agevolerà anche in altri modi durante la stesura del listato, ma li vedremo al momento opportuno.

Ambiente di sviluppo

L’assembler a cui farò riferimento è quello del 6502/6510 (i due processori a livello di programmazione base sono praticamente identici) ma molti concetti sono condivisi anche su altri processori a 8 bit. Per programmare in assembly ad 8 bit ci sono vari tool di sviluppo per varie piattaforme, personalmente uso CBM prg Studio un IDE gratuito per windows che trovo abbastanza completo e semplice da utilizzare, ma questa è solo una scelta personale.

Si comincia

Il programma da realizzare è una versione modificata della classica pallina che rimbalza sullo schermo in modalità testo. La novità sarà che i rimbalzi non avverranno solamente quando la pallina (che è semplicemente un carattere PETSCII) toccherà i bordi dello schermo, ma anche quando incontrerà un carattere diverso dallo spazio. In seguito faremo in modo che il moto della pallina non impedisca all’utente di continuare il proprio lavoro.

La posizione sul video

Nel C64, la memoria video, in poche parole, non è altro che una porzione della memoria RAM che gode della proprietà di essere continuamente visualizzata sul monitor. Tant’è che tale porzione di memoria può essere modificata in base alle esigenze del programma che si vuole realizzare, nel nostro caso l’impostazione all’accensione andrà benissimo. Il primo carattere in alto a sinistra corrisponde, di default, ala locazione $0400 in esadecimale (1024 in decimale), la $0401 corrisponde alla cella accanto a destra, l’ultimo carattere della prima riga corrisponderà quindi alla cella $0427 e il primo carattere a sinistra della seconda riga alla cella $0428 … e così via fino all’ultimo carattere in basso a destra che corrispondenza $07E7 (2023 in decimale).

Per comodità dovremmo identificare la posizione che di volta in volta avrà il nostro pallino come se fosse su un piano cartesiano in cui il punto (0,0) è il carattere in alto a sinistra ($0400), all’aumentare delle X ci si sposterà sulla destra di un carattere, mentre all’aumentare delle Y ci si sposterà verso il basso sempre di un carattere. Quindi il primo problema che possiamo affrontare è costruire una routine che ci permetta di identificare la cella video avendo a disposizione due coordinate X e Y che andremo di volta in volta a modificare per identificare la nuova posizione.
La formula da utilizzare per calcolare la cella avendo le due coordinate X e Y è:

IndirizzoCellaVideo = $0400 + Y * 40 + X

Ricordo che il C64 ha 40 colonne per 25 righe con la X che può variare da 0 a 39 ($27) e la Y da 0 a 24 ($18).

Le coordinate X e Y nell’assembler, in buona sostanza, saranno celle di memoria identificate da un numero che non è altro che il loro indirizzo. In assembly ci possiamo facilitare la vita semplicemente dando loro un nome o meglio una label (cioè le etichettiamo). Ogni cella di memoria (nel 6510) è un byte quindi conterrà un valore intero tra 0 e 255 ($00 – $ff), quindi per ognuna delle coordinate sarà sufficiente una byte, mentre per contenere l’indirizzo di memoria di una cella video ci serviranno 2 byte appaiati (o word).

Le label necessarie per il nostro calcolo sono:

  • Factor1 word $0000 ; Primo fattore per un’operazione
 Factor2 word $0000 ; Secondo fattore per un’operazione
  • Result word $0000 ; Risultato delle operazioni
  • PosXY byte $00,$00 ; x, y Posizione attuale

Per ogni riga sopra scritta abbiamo:

<label> <tipo> <valore iniziale>{,<valore iniziale>}

dove:

  • <label> è il nome che vogliamo dare al primo dei byte che anremo ad usare come memoria di lavoro
  • <tipo> indica come devono essere cosiderate le celle di memoria che andremo a inizializzare
  • <valore iniziale>{,<valore iniziale>} indica che andremo a specificare i valori iniziali delle celle di memoria da riservare, possiamo specificare più di un valore diviso da virgola.

Per esempio Factor1 è l’indirizzo di memoria che punta ad una cella che considereremo parte di una word (2 byte consecutivi), mentre PosXY identificherà la posizione del primo di due byte distinti. Da notare che in entrambi i casi verranno riservati 2 byte in totale, ciò che cambia è il modo in cui l’assembly andrà a considerarle.

Tutto ciò che è scritto dopo il ; sono dei semplici commenti e in quanto tali verranno ignorati in fase di bulding del programma. In ultima analisi volevo far notare che avremmo potuto identificare due label distinte per la coppia X e Y, scrivendo per esempio:

PosX byte $00 ; Posizione X
PosY byte $00 ; Posizione Y

ma ho optato, per utilizzare una sola label per gestirle entrambe.

Routine di calcolo della posizione a video

Purtroppo sono costretto a spiegare un po’ di teoria ancora prima di passare a mostrare un po’ di codice.
 Normalmente se volessimo indicare un certo numero a sedici bit (2 byte) utilizzando la notazione esadecimale, magari per indicare l’indirizzo di memoria del secondo carattere in alto a destra del video, scriveremo $0401, se tale valore lo considerassimo come due byte appaiati potremmo (ma in realtà dobbiamo) dire che $04 è il byte più significativo (Most Significant Byte o MSB) mentre $01 è il byte meno significativo (Least Significant Byte o LSB). Nell’Assembler del 6510 molto spesso troverete invertiti questi due byte quindi in memoria troverete prima l’LSB e successivamente l’MSB, soprattutto quando si tratterà di indicare indirizzi di memoria, quindi per tornare all’esempio precedente il valore $0401 lo troveremmo scritto come $0104.

Ecco a voi il primo assaggio di routine in assembly:

XytoSCPos ; Calcola la posizione dello schermo = $0400 + xpos + ypos * 40
lda PosXY+1 ; copio il contenuto di YPos su Factor1
 sta factor1 ; Byte meno significativo (LSB)
lda #$28 ; Scrivo 40 ($28) in Factor2 (LSB)
sta factor2
lda #$0 ; Azzero i byte più ignificativi (MSB)
sta factor1+1 ; su Factor1
sta factor2+1 ; su Factor2
jsr Moltiplica ; Chiamata alla Routine Moltiplica 
; eseguirà Result = Factor1 * Factor2
; cioè Result = YPos * 40
lda Result ; Copio LSB di Result su Factor1
sta Factor1 ; LSB
lda Result+1 ; Copio MSB di Result su Factor1
sta Factor1+1 ; MSB
lda PosXY ; Copio LSB di XPos in 
sta Factor2 ; LSB di Factor2 (MSB è $00)
jsr Sum16 ; Chiamata alla routine di somma a 16 bit
 ; Factor1 + Result
; cioè XPos + YPos * 40 
; metterà il risultato della somma in Result
lda Result ; Copio Result LSB/MSB in Factor1
sta Factor1 ; copia prima LSB
lda Result+1 ; ... poi
sta Factor1+1 ; ... MSB
lda #$00 ; Copio la LSB di $0400 (inizio memoria video) 
sta Factor2 ; in Factor2 (LSB)
lda #$04 ; Copio la MSB di $0400 (inizio memoria video) 
sta Factor2+1 ; ... MSB
jsr Sum16 ; Chiamata alla routine di somma a 16 bit
; Result = Factor1 + Factor2 = (XPos + YPos * 40) + $0400
lda Result ; Finalmente copio Result a partire da $FB
sta $FB ; ... manco a dirlo prima LSB su una Locazione libera della Pagina 0
lda Result+1 ; ... poi
sta $FC ; ... MSB
ldy #$00 ; Azzero il registro indice y 
lda ($FB),y ; carico nell'accumulatore il valore che si trova
; all’ indirizzo che trovo a a partire dell'indirizzo
; $FB (LSB/MSB) in pagina 0, cioè il valore calcolato
rts ; ritorno a chiamante

La routine proposta prende i valori delle coordinate memorizzate a partire da PosXY e calcola il valore dell’indirizzo di memoria video corrispondente utilizzando la formula sopra descritta.

I registri A,X,Y

Prima di procedere oltre, ancora un po’ di teoria. In assembler 6510 non è possibile con un solo comando copiare il contenuto di una cella di memoria in un altro, per fare questa operazione bisogna utilizzare quelli che vengono chiamati registri.

I registri possono essere considerati alla stregua di celle di memoria speciali, infatti per accedervi la cpu ci mette meno tempo rispetto all’accesso a qualsiasi altra cella di memoria RAM. Inoltre non hanno un indirizzo fisico, risiedono all’interno del microprocessore e ci si riferisce a loro con un nome.

I tre registri del 6502/6510 che ora a noi interessano sono A, X, Y e hanno ampiezza di un byte, ve n’è qualcun altro ma per ora non voglio mettere troppa carne al fuoco. Il registro A, detto anche accumulatore, è il più utilizzato (anche se non ho le statistiche alla mano penso di non poter essere smentito) i registri X e Y possono anch’essi essere utilizzati, per molti aspetti come il registro A, ma hanno delle funzionalità di indicizzazione molto potenti quando vengono utilizzati in coppia con il registro A.

Per caricare con un valore il registro A si utilizza il comando LDA (LoaD Accumualtor) seguito da un argomento, quest’ultimo può essere un valore a 16 o 8 bit, a seconda di come intendiamo reperire il valore da assegnare poi all’accumulatore. Ogni tipo di caricamento si distingue per quella che si chiama indicizzazione, che può essere di vari tipi.

Immediato

Viene caricato un valore esplicito, l’argomento inizierà con un # e seguirà un valore o una label:

lda #$2A ; Carica l’accumulatore con il valore 42 decimale

Pagina Zero

Se l’argomento è un valore di un byte (valore o label), viene eseguita la lettura da quella che viene definita pagina zero di memoria (da $0000 a $00FF), sono stati riservati codici assembler apposta per l’indirizzamento a questa pagina in quanto il tempo di accesso ad essa è inferiore rispetto ad altre pagine, essendo sempre l’MSB a zero, la CPU risparmierà cicli di lettura, purtroppo, a meno di trucchetti, questa pagina è quasi tutta usata per il BASIC e la gestione del sistema, ci rimangono solo pochi byte a nostra disposizione, ma questo non ha mai fermato nessuno:

lda $2A ; Carica L’accumulatore col il valore della cella che si trova all’indirizzo $002A

Pagina Zero X-indicizzata

In questo caso entra in gioco anche il registro X, l’accumulatore verrà caricato con la cella di memoria identificata dal calcolo Argomento+X:

ldx #$01 ; caricamento immediato del registro X con il valore 1
lda $29,X ;
Carica l’Accumuatore col il valore della cella che si trova all’indirizzo $002A = $0029 + $01

Assoluto/Assoluto-X/Assoluto-Y

Con questa modalità, l’argomento son sarà più un byte, bensì un valore di 2 byte che indicheranno il valore di una cella di memoria di riferimento dalla quale prelevare il valore se in modalità Assoluta o utilizzando X o Y come indice per spostarsi rispetto alla cella indicata:


ldx #$01 ; caricamento immediato del registro X con il valore 1

ldy #$02 ;caricamento immediato del registro Y con il valore 2
lda $29,X ; Carica l’Accumulatore col il valore della cella che si trova all’indirizzo $002A = $0029 + $01

lda $29,Y ; Carica l’Accumulatore col il valore della cella che si trova all’indirizzo $002B = $0029 + $02

Indiretto Indicizzato o Indiretto-Y

Questo metodo di caricamento dell’Accumulatore è molto potente e spesso utilizzato. L’argomento, lungo un byte, punterà ad un indirizzo di memoria della pagina zero che assieme al byte successivo a quell’indirizzo formeranno un nuovo indirizzo di memoria al quale verrà sommato il registro Y, il risultato sarà l’indirizzo di memoria dal quale prelevare il valore e portarlo nell’accumulatore, come precedentemente indicato i due byte che verranno utilizzati per comporre l’indirizzo di memoria dovranno essere nela forma LSB-MSB, cioè prima il meno significativo e poi il più significativo:
 supponendo di avere i seguenti valori nella memoria (il numero a sinistra è l’indirizzo fisco quello a destra il valore al suo interno);:

$00FB $01

$00FC $C0

….
….

$C001 $20

$C002 $2A

Possiamo utilizzare i seguenti codici mnemonci per caricare l’accumulatore:

ldy #$01 ; carico nel registro Y il valore 1

lda ($FB),Y; nell’accumulatore verrà inserito il valore $2A che è contenuto nella cella $C001 + $01 = $C002

Indicizzato Indiretto o Indiretto-X

Anche in questo caso viene utilizzata la pagina zero, viene utilizzato il registro X come indice di offset, ma in questa modalità viene prima sommato l’argomento alla X e poi viene composto l’indirizzo da cui prendere il valore da caricare nell’accumulatore:
considerando la stessa configurazione di memoria dell’esempio precedente:


ldx #$02 ; X=$02

lda ($F0, X) ; carico l’accumulatore con il valore presente nella cella di memoria $C001 cioè $20

; infatti $F0 + $02 = $FB indirizzo contente $01 (LSB) che assieme al valore $C0 contenuto in $FC (MSB)

; formato l’indirizzo $C001

Come avrete capito, leggendo alcuni esempi, anche i registri X e Y possono essere caricati tramite i comandi LDX e LDY. Questi due registri hanno comunque un numero minore di metodi di indirizzamento.

Tornando all’analisi della routine, nella prima riga abbiamo la label XYtoSCPos che “marca” l’inizio della routine stessa, d’ora in poi se volessimo “chiamare” la nostra routine useremo il comando opportuno seguito da questa label.

Nelle 7 righe successive si predispongono i valori delle label affinché venga eseguito il calcolo coordinata y * 40, quindi viene reperito il valore della coordinata y e copiata in Factor1 mentre in Factor2 dovrà essere memorizzato il valore 40 ($28) . Abbiamo visto come reperire un valore tramite il comando LDA, per fare l’opposto, cioè scrivere in memoria il valore di un registro, si utilizzano i comandi STA, STX e STY (Store A,X,Y), i quali hanno anche loro vari metodi di indirizzamento (tranne ovviamente l’immediato).

Nella nostra routine carichiamo innanzitutto il valore della nostra y del piano cartesiano (che non ha nulla a che fare con il registro Y) la quale si trova alla posizione PosXY+1. Fortunatamente l’assembly ci permette di eseguire alcune operazioni sulle label (ATTENZIONE non è un nuovo modo di indirizzamento, è semplicemente l’assembly che calcolerà il valore opportuno prima del building del programma) quindi con il comando LDA PosXY+1 non facciamo altro che dire all’Assembly che la locazione di memoria da indirizzare sarà il byte successivo a quello puntato dalla label PosXY, eseguendo un caricamento con indicizzazione assoluta.
Piccola annotazione i fattori Factor1 e Factor2 sono delle word per cui i valori cartesiani della x e y, costituiti ognuno da un byte, dovranno essere memorizzati nell’LSB dei rispettivi fattori mentre gli MSB dei fattori dovranno essere azzerati.
Il prossimo comando è JSR Moltiplica, il comando JSR (Jump to SubRoutine) esegue il codice all’indirizzo indicato nell’argomento (la JSR permette il solo indirizzamento assoluto, quindi va specificato sempre un valore di 2 byte), una volta terminato il codice chiamato dalla JSR il nostro programma ripartirà dall’istruzione successiva alla JSR.

Nel nostro caso, senza entrare nei dettagli del codice chiamato, basti sapere che la chiamata alla SubRoutine Moltiplica non farà altro che moltiplicare i due fattori che abbiamo caricato e il risultato (una word) ce lo ritroveremo dall’indirizzo Result sempre nella forma LSB/MSB.

Eh già!, forse non ve lo aspettavate, ma il 6510 non ha un comando mnemonico per seguire le moltiplicazioni, tocca rimboccarsi le maniche, e costruirsi la propria routine.

Se avete letto fin qui, ammesso e non concesso di essere stato chiaro, avrete le conoscenze sufficienti per interpretare il codice fin’ora scritto, per ora basti sapere che la label Sum16 è una Subroutime che mette in Result il valore di Factor1+Factor2.

Alla fine della Routine di calcolo, il comando RTS (ReTurn from Subroutine) farà in modo che il programma prosegua a partire dall’istruzione successiva all’ultima chiamata fatta alla nostra routine tramite JSR, infatti anche la nostra routine (come per Moltiplica e Sum16) verrà chiamata tramite il comando JSR.

Nella penultima riga c’è un comando di caricamento dell’accumulatore indiretto indicizzato, riuscite a capire il significato del valore caricato nell’accumulatore? 
Si, proprio così, prima di ritornare al chiamante viene fatto in modo che nel registro A venga caricato il valore contenuto all’indirizzo video puntato dalle nostre coordinate PosX e PosY, questo ci servirà per capire (una volta usciti dalla routine) se in quella posizione c’è un carattere diverso dallo spazio, controllo essenziale al fine del del nostro programma.

Dapprima le somme e le sottrazioni …

Come abbiamo visto ogni cella di memoria può contenere valori interi da 0 a 255, per sommare tra loro due valori (che siano essi presi dalla memoria o valori immediati) bisogna passare per l’accumulatore (il suo nome è proprio dovuto a questo), vediamo subito un esempio:

lda #$2A  ; carico in A il valore $2A
clc     ; azzero il riporto (carry)

adc #$02    ; A = A + $02 => A = $2C

La prima riga ci dovrebbe essere già familiare, tralasciamo per un attimo la seconda riga e passiamo alla terza, dove incontriamo l’istruzione ADC (ADd with Carry – somma con il riporto) che è la responsabile della somma tra l’accumulatore, il valore determinato dall’argomento ed il riporto (carry) rimettendo il risultato nell’accumulatore. Non esistendo un comando che esegua la somma senza sommare anche il carry siamo costretti, in questo caso, a “pulire” il carry con il comando CLC (CLear Carry), altrimenti avremmo rischiato di trovarci nell’ammontare della somma anche un riporto dovuto ad operazioni precedenti.

In effetti il carry non è altro che un bit-flag che quindi può assumere il valore 0 o 1 e il cui significato può variare a seconda delle operazioni che stiamo eseguendo. Il carry in realtà ha molteplici utilizzi, dipende dal contesto in cui stiamo operando, proprio per questo può venire modificato anche da operazioni che con la somma esplicita non hanno nulla a che fare.

Vediamo ora come sommare due valori a 16 bit analizzando la routine Sum16:

clc   ; Mi assicuro che non vi sia un riporto (Carry)
lda  Factor1    ; A = LSB(Factor1)
adc  Factor2    ; A = A + LSB(Factor2) + Carry
sta  Result ; LSB(Result) = A  e viene impostato il carry
lda  Factor1+1  ; A = MSB(Factor1)
adc  Factor2+1  ; A = A + MSB(Factor2) + Carry
sta  Result+1   ; MSB(Result) = A  e viene impostato il carry
rts ; Ritorno al chiamante

Innanzitutto, nelle prime tre righe di codice, sommiamo i due byte meno significativi dei due addendi (Factor1 e Factor2) con la tecnica spiegata sopra, tale somma potrebbe avere un riporto (1), tale riporto quindi andrà sommato nel byte + significativo, ma di questo non dobbiamo preoccuparci più di tanto in quanto la nostra ADC lo prende già in considerazione. Infatti nelle righe successive, dopo esserci salvati il registro sull’LSB di Result, non facciamo altro che ripetere le operazioni precedenti (escluso il clc, ora il carry è importante che venga effettivamente sommato) sugli MSB dei Fattori e del Risultato, questo è tutto. Alla fine, al ritorno dalla nostra subroutine (RTS alla fine) nei due byte a partire dalla label/indirizzo Risultato avremo la somma di factor1 + Factor2. Se avessimo avuto la necessita di eseguire una sottrazione avremmo dovuto utilizzare il codice mnemonico SBC (SuBtract with Carry) , con la differenza che il carry in questo caso funziona come un prestito invertito, quindi viene sottratta la sua negazione, il che rende necessario settare il bit di carry con SEC (Set Carry) prima di eseguire la prima (se la differenza va fatta su più byte di seguito) delle differenze.

Carry e i suoi fratelli

Fin’ora abbiamo visto un utilizzo del carry, è venuto il momento di approfondire da dove viene ma soprattutto andiamo a fare la conoscenza degli altri flag che assieme al carry formano un altro importante registro del processore 6502: Il registro di stato del processore (per gli amici P). Questi non è altro che un byte in cui vengono immagazzinate le informazioni sullo stato del processore. A parte uno, gli altri bit di P indicano la condizione in cui si trova una ben determinata proprietà. I valori questi bit (0 o 1) vengono modificati implicitamente da alcune operazioni, ma possono essere modificate esplicitamente tramite opportuni istruzioni, vedi CLC e SEC ad esempio.

I significati di ogni flag di P sono nell’ordine:

  • 7: N Negative, questo flag è a 1 se l’ultima istruzione ha prodotto un risultato negaitvo
  • 6: O OverFlow, nel caso di operazioni con segno viene settato a 1 se il valore è al di fuori del range (-128 – 127)
  • 5: – Non utilizzato
  • 4: B Break Command, 1 indica che a richiesta di interrupt è stata fatta tramite l’istruzione BRK
  •  3: D Decimal mode, 1 Indica che il processore è nella modalità decimale
  • 2: I IRQ disabled, 1 indica che le richieste di interrupt sono disabilitate
  • 1: Z Zero, 1 indica che l’ultima operazione ha prodotto 0 come risultato

La spiegazione dettagiata del significato di ogni flag esula dall’intento che ha questo articolo, quindi per ora prenderemo in considerazione il carry (di cui ne abbiamo già visto un utilizzo) e il flag Z, che come descritto sopra indica quando l’ultima istruzione che ne modifica il valore ha avuto come risultato esattamente zero.

… quindi le moltiplicazioni

Come dei bravi scolaretti, a questo punto, è arrivato il momento di eseguire le moltiplicazioni, con i nostri byte. Genericamente, la moltiplicazione di due numeri N * M, avendo a disposizione la sola somma come operazione, si ottiene sommando M volte N (o viceversa). Sin d’ora vorrei chiarire che c’è almeno un altro algoritmo da poter utilizzare per fare le moltiplicazioni con l’ assembler 6502, che risulta sicuramente più performante, ma per i nostri fini va benissimo il metodo “classico”.

Scriviamo la nostra routine di moltiplicazione:

Moltiplica                ; Moltiplica LSB(Factor1) * LSB(Factor2)
        lda  #$00           ; 
        sta  Result         ;  
        sta  Result+1       ; Risultato = 0
        ldx  Factor2        ; x = Factor2
        cpx  #$00           ; x == 0 ?
        beq  FineMoltiplica     ; Se x==0 fine moltiplicazione (Result = 0) 
        lda  Factor1        ; a = Factor1
        cmp  #$00           ; a == 0 ?
        beq  FineMoltiplica     ; se a == 0 vai a fine moltiplicazione (Result = 0)
        sta  Result         ; altrimenti Result = Factor1
LoopMoltiplica 
        dex                 ; x=x-1 (x inizialmente conteneva LSB(Factor2))
        beq  FineMoltiplica     ; Se x==0 vai a fine Moltiplicazione
        clc                 ; Pulisci il Carry
        lda  Result         ; -----------------
        adc  Factor1        ; Somma Result con Factor1
        sta  Result         ; metti il risultato su Result
        lda  #$00   ; Caricamento immediato (più veloce)
        adc  Result+1       ; al posto di Factor+1 che so essere a 0
        sta  Result+1       ; ----------------
        jmp  LoopMoltiplica ; Vai a LoopMoltiplica
FineMoltiplica
        rts                 ; Torna al chiamante

La routine di moltiplicazione eseguirà la moltiplicazione di Factor1 e Factor2 intesi come singoli byte, infatti prima della chiamata a questa subroutine, il nostro programma (vedi il listato più sopra) inserirà in queste word le coordinate dello schermo che ci stanno comodamente su di un byte, quindi per ottimizzare le cose (e renderle più semplici) viene preso in considerazione solo l’LSB dei due fattori.

Ciò che volevo mettere in evidenza con questa routine erano i comandi di confronto e di salto condizionato. 
Nelle prime tre istruzioni non si fa altro che azzerare Result (il nostro contenitore del risultato finale) successivamente incontriamo l’istruzione CPX (ComPare X, per inciso esiste anche CPY per il registro Y), la quale esegue una comparazione tra il registro X (che è stato caricato col valore di Factor2) e l’argomento (che può essere di tipo immediato, pagina zero o assoluto).

Ci riferiamo a CMP come una comparazione, ma possiamo pensarla come una semplice operazione di differenza tra X e l’argomento, la quale va a modificati alcuni dei flag di P e per la precisione verranno modificati i flag di Segno, Zero e Carry.

L’istruzione successiva BEQ (Branch on Equal tradotta malamente sarebbe “Salta se uguale”) esegue il controllo del Flag Z, e se Z=1 (nel nostro caso se la differenza Registrox-$00 = 0 il che implica che Registro-x sia $00) allora la CPU andrà ad eseguire le istruzioni che si trovano a partire dall’indirizzo indicato con l’argomento.
 Oltre a BEQ esistono anche altre istruzioni di salto condizionato che eseguono controlli sugli altri flag del registro di stato P, per ora li tralascio, sebbene siano importantissimi, altrimenti questo articolo diverrebbe un libro.

Riprendendo il filo del nostro discorso, con l’istruzione BEQ FineMoltiplica quindi si indica al programma di andare ad eseguire l’istruzione RTS e quindi tornare al chiamante con Result = 0 (qualsiasi numero moltiplicato per 0 dà come risultato 0).

Nelle istruzioni successive eseguiamo lo stesso controllo su Factor1, solo che stavolta la comparazione la facciamo utilizzando l’accumulatore e quindi l’istruzione CMP (CoMPare accumulator, la quale ha più tipi di indirizzamento rispetto a CPY e CPX).
 Se entrambi i fattori sono diversi da 0 (in questo caso li consideriamo sempre come valori positivi) allora procediamo l’algoritmo andando a sommare Factor1 per Factor2 volte.

Innanzitutto Inizializziamo Result con il valore di Factor1, poi per tenere il conteggio del numero di volte in cui dovremmo la somma viene tenuto dal registro X (inizialmente eguagliato a Factor2) decrementandolo di una unità tramite l’istruzione DEX (Decrement X), quando X raggiungerà il valore 0, la successiva istruzione BEQ prenderà la decisione di terminare il Loop, se ciò non avvenisse (X > 0) allora il numero di sommatorie da effettuare non è ancora sufficiente a raggiungere lo scopo, quindi eseguiremo la sommatoria di Factor1 con Result e riporteremo il risultato su Result (qui si mi preoccupo di usare LSB e MSB di Result in quanto la moltiplicazione potrebbe raggiungere valori non contenibili in un singolo byte) e poi, per chiudere il ciclo, verrà eseguita l’istruzione JMP (JuMP) a LoopMoltiplica, da dove si si eseguirà nuovamente il decremento di X e il controllo, tutto questo finché X non raggiungerà il valore di 0.

Questa tecnica di eseguire i loop “al contrario”, cioè decrementare un contatore piuttosto che incrementarlo, è molto usata in assembler; oltre a DEX, esistono le istruzioni DEY e DEC che vanno ad agire rispettivamente sul registro Y e sull’accumulatore.

Routine principale

La routine principale del nostro programma si occuperà di tre cose essenzialmente, porre dei valori iniziali alle coordinate x e y ed eseguire un loop (per ora infinito) che cancelli il pallino alla posizione precedente e lo stampi in quella attuale. A questo punto dovrà essere dichiarata un’altra label che punterà a due byte su cui memorizzare le coordinate “future” (NewXY) del pallino, il loro utilizzo sarà chiaro più avanti, e una Label di un byte (Delay) il cui scopo è semplicemente quello di farci perdere tempo … nel senso che ferma per un po’ il ciclo del programma, altrimenti il pallino sarebbe talmente veloce da non poter essere visto.

Oltre alle nuove label, ora sfruttiamo un altro vantaggio offerto dalla programmazione in assembly, cioè la definizione di alcuna costanti da poter utilizzare al posto di un valore esplicito all’interno del listato. In particolare ci servirà definire una variabile SCPos da utilizzare al posto del valore $FB (che, se avete fatto attenzione, abbiamo incontrato nel primo listato dell’articolo assieme a $FC).

La dichiarazione è semplice:

SCPos = $FB ; Locazione libera alla pagina zero assieme a $FC conterrà il valore della cella video
                         ; calcolata dalla subroutine XYToSCPos

Nel Commodore 64, come accennato in precedenza ci sono poche locazioni libere da sfruttare per caricare e salvare tramite l’indirizzamento Indiretto indicizzato, $FB e $FC sono moto utilizzate essendo 2 tra le 5 libere che io conosco (naturalmente questo è vero se non si libera la pagina 0 dal BASIC).

Questo il listato della routine principale nella versione con Loop Infinito:

Start
  lda  #$13     ; mi posiziono al centro dello schermo
  sta  PosXY        ;
  sta  NewXY
  lda  #$09      
  sta  PosXY+1
  sta  NewXY+1
  jsr  XYtoSCPos    ; calcolo la cella dello schermo relativa a x,y
        ; quindi ne metto l'indirizzo in SCPos ($00FB/$00FC)
LoopSenzaFine
  dec  Delay        ; Contatore di ritardo (già inizializzato nella sua dichiarazione)
  bne  SkipNewPos   ; Se diverso da 0 Non fare nulla
  ldy  #$05     ; Reinizializza il numero di cicli di ritardo
  sty  Delay        ; in Delay
  ldy  #$00
  lda  #$20     ; Metto in A il valore screen code di spazio
  sta  (SCPos),Y    ; Indiretto Indicizzato Pagina zero visualizzo lo spazio
        ; alla posizione appena calcolata e posta in SCPOS
  jsr  IncDec       ; Calcolo la prossima  coppia coord. x,y
  jsr  XYtoSCPos    ; Calcolo Cella in funzione di x,y
  lda  #$51     ; A = Pallino
  sta  (SCPos),Y    ; Stampa il pallino nella cella puntata da SCPos
SkipNewPos
  jsr  Wait     ; Attendi
  jmp  LoopSenzaFine    ; torna all'inizio del ciclo

Possiamo tranquillamente saltare alla riga di LoopSenzaFine dove iniziamo subito a decrementare il contatore Delay per poi andarne a testare il valore tramite l’istruzione BNE (Brach on Not Equal), la quale esegue il controllo esattamente opposto a BEQ, cioè continuerà l’esecuzione partendo dalla linea SkipNewPos nel caso in cui il flag Z (Zero) sia uguale a 0, cioè quando Delay è diversa da 0. Se fosse uguale a zero procederebbe reinizializzando Delay a 5 (per ricominciare a saltare il calcolo della nuova posizione al prossimo ciclo).

Arrivati a Questo punto non ci resta che analizzare la subroutine IncDec e Wait.

Subroutine decisionale IncDec

La subroutine IncDec, pur essendo corposa no ha quasi nulla che non sia stato affrontato nell’articolo tranne unica istruzione non ancora incontrata, l’istruzione EOR (Exclusive OR) la quale esegue un’operazione logica di OR esclusivo bit a bit tra l’argomento indicato e l’accumulatore mettendo il risultato nell’accumulatore. Nel caso qualcuno di voi fosse a digiuno di algebra binaria, basti sapere che l’or esclusivo tra due bit, restituisce 1 se i due bit confrontati sono diversi altrimenti restituisce 0. Nello specifico, il punto in cui viene usato (che è in realtà è una subroutine di IncDec) viene sfruttato per eseguire l’operazione di negazione bit a bit di una particolare locazione di memoria (infatti non esiste un’istruzione che lo faccia di suo). Infatti eseguendo EOR tra un Byte (es: 00110010) con l’accumulatore = $FF (11111111) ci ritroveremmo sempre nell’accumulatore un valore che è la negazione bit a bit del valore iniziale (nell’ es:11001101).

Detto questo mi limiterò a descrivere a grandi linee il semplice algoritmo implementato nella subroutine IncDec.
 Il pallino oltre ad essere ad avere una posizione identificata tramite due coordinate x y (poi tradotte in una cella video) ha un’altra proprietà essenziale per capire la direzione dello stesso, tramite la label DirXY identifichiamo due byte che indicano se stiamo “avanzando” o “indietreggiando” sull’asse delle x e delle y (si lo ammetto sono uno sprecone, sarebbero bastati due bit); anche in questo caso, come noterete, la scelta è quella di usare un’unica label per identificare le due proprietà.

Questa scelta di usare un’unica label (come per PosXY, NewXY e altre ancora che noterete nel listato completo) è dovuta al fatto che in questa maniera potrò gestire le due coordinate separatamente pur senza scrivere lo stesso codice due volte. Infatti, la gestione di una coordinata piuttosto che l’altra è demandata al valore del Registro X, il quale farà da indice nelle varie istruzioni all’interno della subroutine.

Inizialmente il pallino avanzerà in entrambe le direzioni x e y (quindi verrà incrementata di uno la posizione ad ogni passaggio) percorrendo la diagonale verso il basso a destra. IncDec, ad ogni ciclo, testerà (per ognuna delle coordinate) se il pallino “sforerà” il bordo sia superiore o inferiore mantenendo quella direzione, in tal caso viene cambiata la direzione, label DirXY. Ed è proprio qui che interverrà l’operazione EOR sopra descritta, infatti ho convenuto che per l’avanzamento, il byte direzione dovesse essere $00, mentre per l’arretramento sarà $FF, due valori facilmente convertibili l’uno nell’altro. Da notare che se lo sforamento potenziale avvenisse nel bordo inferiore, dovremmo testare che la coordinata vada a -1, ma non è necessario impelagarsi troppo con le varie operazioni di complemento (in questo caso), in quanto possiamo semplicemente testare che x o y non diventi $FF.

Questo perché in un byte con valore $00, se andassimo a sottrarre il valore $01, ci ritroveremmo con il valore $FF (e i vari bit di stato modificati di conseguenza). Altre verifiche che verranno fatte riguardano il contenuto delle celle video dove il pallino si ritroverebbe, anche in questo caso Cambierà direzione qualora incontrasse un carattere diverso da spazio, con in aggiunta il controllo del tocco su uno degli spigoli oppure nel caso si ritrovasse il un “angolo” formato dalle lettere, in entrambi i casi dovrà invertire la direzione sia su x che y.

Subroutine Wait

La subroutine Wait, il cui scopo è semplicemente tenere in sospeso il programma per rendere più fluida la visualizzazione del pallino, sfrutta il tempo che il Commodore 64 ci mette a “ridisegnare” una riga di pixel dello schermo, detta raster line. Senza addentrarci troppo nello specifico, il Commodore 64 ha un paio di celle di memoria ($D011/$D012) in cui viene scritta su quale raster line sta eseguendo in refresh. Per precisione il numero di righe varia da 0 a 319, l’LSB di tale valore è D012, mentre nel $D011 si deve tenere conto solo del bit meno significativo come MSB.

Il corpo di Wait è semplicissimo:

Wait
        lda  $d012      ; Carico in A l'attuale linea di raster LSB
WaitLoop
        clc             ; pulizia carry
        adc  #$FE       ; A = A + $FE
        cmp  $D012      ; l'attuale raster line = A
        bne  WaitLoop   ; No, attendi ancora
        rts             ; torna al chiamante

La routine non fa altro che leggere il valore dell’attuale raster line quindi ci somma $FE, lo mette nell’accumulatore e continua a leggere il valore dell’attuale raster line finché non è uguale a quella dell’accumulatore (CMP $D012/BNE WaitLoop). L’intento era quello di aspettare poco meno di 1/50 di secondo, il tempo che il Commodore 64 ci mette a refreshare tutto il video, la routine comunque non è stata fatta come se stessimo progettando lo Space Shuttle, ma per quello che serve a noi svolge il suo compito più che bene.

Interagiamo con il pallino

Come da progetto iniziale ora ci occuperemo di come fare in modo che il nostro pallino non ci impedisca di svolgere i nostri lavori.
 Questo è possibile grazie ad una particolare coppia di bytes, l’interrupt vector ($0314/$0315), i quali, in condizione di normalità, contengono l’indirizzo di memoria (nella forma LSB/MSB) $31 e $EA, richiamato 60 volte al secondo. Queste chiamate sono essenziali al sistema affinchè possa per esempio ricevere input da tastiera o semplicemente far lampeggiare il cursore.

Il trucchetto è quello di andare a modificare questi 2 bytes inserendo la locazione del corpo principale della nostra routine, assicurandosi che alla fine di essa si vada a fare un JMP alla locazione originaria $EA31. In questa maniera la nostra routine verrà eseguita 60 volte al secondo.

Per fare tutto ciò dobbiamo introdurre una routine di inizializzazione che vada a modificare l’indirizzo dell’interrupt vector, con l’accortezza di disabilitare momentaneamente proprio le richieste di interruzione e riabilitarle quando abbiamo finito.

Start   
  sei       ; disabilitazione dell'interrupt request    
  lda  #$00 ; inizializzo alla posizione 0,0 schermo
  sta  PosXY    ; le varie coordinate
  sta  NewXY
  sta  PosXY+1
  sta  NewXY+1
  jsr  XytoSCPos    ; salvo su SCPos il valore della corrispondente cella video
      ; Mofico l'intrerrupt vector in modo
      ; che punti alla mia routine di visualizzazione del
      ; pallino (che ho chiamato IRQ)
  lda  #<IRQ     ; indirizzamento immediato LSB     
  ldx  #>IRQ     ; indirizzamento immediato MSB
  sta  $314     ; Indirizzi interrupt vector
  stx  $315     ;
  cli           ; ripristino l'interrupt
  rts           ; ritorno al chiamante (BASIC)

La disabilitazione dell’interrupt avviene con l’istruzione SEI (Set Interrupt) che va a modificare il relativo bit nel registro di stato del processore, la riabilitazione degli interrupt avviene con l’istruzione contraira CLI (Clear Interrupt). IRQ è la label che ho dato alla routine principale da eseguire 60 volte al secondo. La nomenclatura “<IRQ” o “>IRQ” è una istruzione assembly che indica semplicemente di sostituire quei caratteri con LSB (<) e MSB (>) della label che segue.

Siccome la nostra routine principale ora verrà eseguita all’interno di un processo più ampio di cui non conosciamo (o comunque possiamo non conoscere) i dettagli e la nostra routine principale va ad utilizzare i registri A X Y senza remora alcuna, potremmo andare in conflitto con quelle routine che seguiranno la nostra (a partire da $EA31). Per non saper ne leggere ne scrivere bisogna quindi salvare tali valori per poi ripristinarli prima del salto finale.
Per fare ciò ci viene in aiuto lo Stack. Con questo termine si identifica un’area di memoria che viene gestita tramite alcune apposite istruzioni mnemoniche implicitamente o esplicitamente e sostanzialmente permette di salvare e riprendere dei valori in un ben determinato ordine e cioè facendo in modo che l’ultimo valore che è stato chiesto di salvare (operazione di Push) sia anche il primo ritornato quanto viene richiesto di tornare un valore (operazione di POP). Le istruzioni implicite che vanno ad utilizzare lo stack sono JSR e RTS, infatti quando viene eseguita l’istruzione JSR, prima di saltare alla subroutine viene salvato l’indirizzo successivo a quello di JSR (quindi il push di 2 bytes), nel momento in cui la subroutine chiamata esegue un RTS, il sistema prende dallo stack l’indirizzo salvato e riparte ad eseguire il codice da quell’indirizzo. Se vi fossero subroutine annidate in questo modo il gioco JSR/RTS potrà sempre funzionare.

Nel C64 allo stack è riservata l’area di memoria che va da $0100 a $01FF, quindi risulta abbastanza intoccabile dai programmi, altrimenti vi è il rischio di bloccare tutto. Per tenere traccia di dove si trovi l’ultimo dato inserito, il sistema utilizza un ulteriore registro detto Stack Pointer, il quale si incrementa o decrementa in base alle operazione di Push o di Pop effettuate. Le istruzioni esplicite che vanno a lavorare sullo stack sono PHA (PusH Accumulator ), PLP (PuLl Accumulator), che permettono il Push e il Pop dell’accumulatore, PHP (PusH Processor status) e PLP (PuLl Processor status).

Come possiamo vedere non ci sono operazioni dirette sui registri X e Y, quindi dovremmo passare sempre per l’accumulatore nel momento in cui vogliamo farne il Push/Pop. Per traferire il registro X in A e viceversa esiste la coppia di istruzioni TXA e TAX (Tranfer X to A e Transfer A to X) in questo modo riusciamo a salvare tutti i registri senza problemi. Dobbiamo comunque avere l’accortezza di eseguire le istruzioni di Pop in ordine inverso alle istruzioni di Push.

La nostra routine di IRQ è quindi:

IRQ
      ; Salvataggio sullo stack dei registri
  php               ; Processor status
  pha               ; Salvo l''Accumulatore
  txa               ; A=X
  pha               ; salvo X (tramite il salvataggio dell'accumulatore)
  tya               ; A=Y
  pha               ; Salvo Y
  dec  Delay        ; Ritardo per rendere un pò persistente
  bne  SkipIrq      ; la visualizzazione del pallino
  ldy  #$03
  sty  Delay
  lda  #$20         ; A=Spazio Code screen
  ldy  #$00         ; pongo a 0 l'indice per
  sta  (SCPos),y    ; pongo A in SCPos con l'indirizzamento Indiretto indicizzato
  jsr  IncDec       ; calcolo prossima possizione PosXY 
  jsr  XYtoSCPos    ; pongo in SCPos l'indirizzo della cella video 
  lda  #$51         ; A = Pallino
  sta  (SCPos),y    ; pongo A in SCPos con l'indirizzamento Indiretto indicizzato
SkipIrq 
  pla               ; prendo il quarto valore salvato nello stack e lo metto in A
  tay               ; Y=A
  pla               ; prendo il terzo valore salvato nello stack e lo metto in A
  tax               ; X=A
  pla               ; prendo il secondo valore salvato nello stack e lo metto in A
  plp               ; prendo il primo valore salvato e lo rimetto nel registro di stato
  jmp  $ea31        ; Salto alla normale gestione dell'interrupt

Arrivati a questo punto basta mettere il tutto all’interno di un progetto assembly, non prima di aver deciso da quale indirizzo far partire il nostro programma in assembler. Nel C64 ci sono varie opzioni su dove far iniziare un programma in assembler senza far danni, ogni uno ha pregi e difetti, ma in definitiva la scelta dell’entry point dipende da fattori tipo l’ampiezza del programma stesso ma soprattutto dipende da quello per cui è stato progettato. Per i nostri scopi possiamo sfruttare lo spazio libero definito come santo graal per il C64, tale spazio inizia dall’indirizzo $C000 (49152 in decimale).
L’indicazione dell’indirizzo (nel CBM studio ma anche in altri programmi di assembly) si effettua ponendo la direttiva *=$C000

Una volta assemblato il tutto e caricato in memoria, da BASIC possiamo scrivere SYS 49152 (che altri non è che l’istruzione JSR $C000) per far eseguire il nostro programma.

Il programma presentato ha margini di ottimizzazione e soprattutto ha dei problemini per quanto riguarda per esempio l’incontro del pallino con il cursore oppure il problema di proliferazione dei pallini (statici stavolta) nel caso in cui avvenisse uno scroll del video.

Note

(1) Per capire meglio come funziona il riporto con ADC prendiamo ad esempio la somma di due “grandi” numeri (non enormi, ma in un byte ci stanno giusti), 254 + 254. Il “giochetto” del riporto si vede bene se il 254 lo scriviamo in binario e facciamo le somme dei singoli bit in colonna:

Ripassiamo giusto come si esegue la somma di bit:
0 + 0 = 0 (nessun riporto)
0 + 1 = 1 (nessun riporto)
1 + 1 = 0 (con riporto di 1)
(1+1) + 1 = 1 (con riporto di 1)

Somma di 254 + 254:

1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 +
1 1 1 1 1 1 1 0 =
1 1 1 1 1 1 1 0 0

In rosso sono evidenziati i riporti, della somma della colonna alla loro immediata destra. Il riporto più a sinistra è il famoso carry-bit.

Sorgente Completo

Screen  = $0400     ; Indirizzo prima cella video
SCPos   = $FB       ; indirizzo della pagina 0 libero per i nostri scopi $fb e $fe 
Sid     = $0D400    ; registro SID     (Bonus)                                            


                *=$C000
;------------- versione con utilizzo dell'IRQ
                        ; Routine Iniziale per incastonare
                        ; la visualizzazione del pallino
                        ; tramite interrupt
Start   sei             ; disabilitazione dell'interrupt request

        lda  #$00       ; inizializzo alla posizione 0,0 schermo
        sta  PosXY      ; le varie coordinate
        sta  NewXY
        sta  PosXY+1
        sta  NewXY+1
        jsr  XYtoSCPos  ; salvo su SCPos il valore della corrispondente cella video
                        ; Mofico l'intrerrupt vector in modo
                        ; che punti alla mia routine di visualizzazione del
                        ; pallino (che ho chiamato IRQ)
        lda  #<IRQ      ; indirizzamento immediato LSB     
        ldx  #>IRQ      ; indirizzamento immediato MSB
        sta  $314       ; Indirizzi interrupt vector
        stx  $315       ;
        cli             ; ripristino l'interrupt
        rts             ; ritorno al chiamante

IRQ
                        ; Salvataggio sullo stack dei registri
        php             ; Processor status
        pha             ; Salvo l''Accumulatore
        txa             ; A=X
        pha             ; salvo X (tramite il salvataggio dell'accumulatore)
        tya             ; A=Y
        pha             ; Salvo Y
        dec  Delay      ; Ritardo per rendere un pò persistente
        bne  SkipIrq    ; la visualizzazione del pallino
        ldy  #$03
        sty  Delay
        lda  #$20       ; A=Spazio Code screen
        ldy  #$00       ; pongo a 0 l'indice per
        sta  (SCPos),y  ; pongo A in SCPos con l'indirizzamento Indiretto indicizzato
        jsr  IncDec     ; calcolo prossima possizione PosXY 
        jsr  XYtoSCPos  ; pongo in SCPos l'indirizzo della cella video 
        lda  #$51       ; A = Pallino
        sta  (SCPos),y  ; pongo A in SCPos con l'indirizzamento Indiretto indicizzato
SkipIrq
        pla             ; prendo il quarto valore salvato nello stack e lo metto in A
        tay             ; Y=A
        pla             ; prendo il terzo valore salvato nello stack e lo metto in A
        tax             ; X=A
        pla             ; prendo il secondo valore salvato nello stack e lo metto in A
        plp             ; prendo il primo valore salvato e lo rimetto nel registro di stato
        jmp  $ea31      ; Salto alla normale gestione dell'interrupt
;------------Fine versione con IRQ

; ------ Versione senza interazione
;Start
;        lda  #$13       ; mi posiziono al centro dello schermo
;        sta  PosXY      ;
;        sta  NewXY
;        lda  #$09      
;        sta  PosXY+1
;        sta  NewXY+1
;        jsr  XYtoSCPos  ; calcolo la cella dello schermo relativa a x,y
;                        ; quindi ne metto l'indirizzo in SCPos ($00FB/$00FC)
;LoopSenzaFine
;        dec  Delay      ; Contatore di ritardo
;        bne  SkipNewPos ; Se diverso da 0 Non fare nulla
;        ldy  #$05       ; Reinizializza il numero di cicli di ritardo
;        sty  Delay      ; in Delay
;        ldy  #$00
;        lda  #$20       ; Metto in A il valore screen code di spazio
;        sta  (SCPos),Y  ; Indiretto Indicizzato Pagina zero visualizzo lo spazio
;                        ; alla posizione appena calcolata e posta in SCPOS
;        jsr  IncDec     ; Calcolo la prossima  coppia coord. x,y
;        jsr  XYtoSCPos  ; Calcolo Cella in funzione di x,y
;        lda  #$51       ; A = Pallino
;        sta  (SCPos),Y  ; Stampa il pallino nella cella puntata da SCPos
;SkipNewPos
;        jsr  Wait       ; Attendi
;        jmp  LoopSenzaFine ; torna all'inizio del ciclo
; ------ Fine versione senza irq



                       
XYtoSCPos               ; Calcola la posizione dello schermo = $0400 + xpos + ypos * 40
        lda  PosXY+1    ; copio il contenuto di YPos su Factor1
        sta  factor1    ; Byte meno significativo (LSB)
        lda  #$28       ; Scrivo 40 ($28) in Factor2 (LSB)
        sta  factor2    
        lda  #$00       ; Azzero i byte più significativi (MSB)
        sta  factor1+1  ; su Factor1
        sta  factor2+1  ; su Factor2
        jsr  Moltiplica ; Chiamata alla Routine Moltiplica 
                        ; eseguirà Result = Factor1 * Factor2
                        ; cioè Result = YPos * 40
        lda  Result     ; Copio LSB di Result su Factor1
        sta  Factor1    ; LSB
        lda  Result+1   ; Copio MSB di Result su Factor1
        sta  Factor1+1  ; MSB
        lda  PosXY      ; Copio LSB di XPos in 
        sta  Factor2    ; LSB di Factor2 (MSB è $00)
        jsr  Sum16      ; Chiamata alla routine di somma a 16 bit
                        ; Factor1 + Result
                        ; cioè XPos + YPos * 40 
                        ; metterà il risultato della somma in Result
        lda  Result     ; Copio Result LSB/MSB in Factor1
        sta  Factor1    ; copia prima LSB
        lda  Result+1   ; ... poi
        sta  Factor1+1  ; ... MSB
        lda  #<Screen   ; Copio la costante screen Screen (LSB/MSB) 
        sta  Factor2    ; in Factor2 (LSB)
        lda  #>Screen   ; ... poi
        sta  Factor2+1  ; ... MSB
        jsr  Sum16      ; Chiamata alla routine di somma a 16 bit
                        ; Result = Result + Screen
        lda  Result     ; Finalmente copio Result in SCPos
        sta  SCPos      ; ... manco a dirlo prima LSB 
        lda  Result+1   ; ... poi
        sta  SCPos+1    ; ... MSB
        ldy  #$00       ; Azzero il registro indice y 
        lda  (SCPos),y  ; carico nell'accumulatore il valore che si trova
                        ; allindirizzo che trovo a a partire dall'indirizzo
                        ; SCPos, cioè il valore calcolato
        rts             ; ritorno a chiamante
Sum16
        clc             ; Mi assicuro che non vi sia un riporto (Carry)
        lda  Factor1    ; A = LSB(Factor1)
        adc  Factor2    ; A = A + LSB(Factor2) + Carry  
        sta  Result     ; LSB(Result) = A  e viene impostato il carry
        lda  Factor1+1  ; A = MSB(Factor1)
        adc  Factor2+1  ; A = A + MSB(Factor2) + Carry
        sta  Result+1   ; MSB(Result) = A  e viene impostato il carry
        rts             ; Ritorno al chiamante


Moltiplica              ; Moltiplica LSB(Factor1) * LSB(Factor2)
        lda  #$00       ; 
        sta  Result     ;
        sta  Result+1   ; Risultato = 0
        ldx  Factor2    ; x = Factor2
        cpx  #$00       ; x == 0 ?
        beq  FineMoltiplica ; Se x==0 fine moltiplicazione (Result = 0) 
        lda  Factor1    ; a = LSB(Factor1)
        cmp  #$00       ; a == 0 ?
        beq  FineMoltiplica ; se a == 0 vai a fine moltiplicazione (Result = 0)
        sta  Result     ; altrimenti Result = Factor1
LoopMoltiplica 
        dex             ; x=x-1 (x inizialmente conteneva LSB(Factor2))
        beq  FineMoltiplica ; Se x==0 vai a fine Moltiplicazione
        clc             ; Pulisci il Carry
        lda  Result     ; -----------------
        adc  Factor1    ; Somma Result con Factor1
        sta  Result     ; metti il risultato su Result
        lda  #$00       ; Caricamento immediato 00 (più veloce)
        adc  Result+1   ; Sommo il solo carry
        sta  Result+1   ; ----------------
        jmp  LoopMoltiplica     ; Vai a LoopMoltiplica
FineMoltiplica
        rts             ; Torna al chiamante

                       
                       
IncDec                  ; incr./decr. (in base al verso) coordinate x o y
        lda  PosXY      ; Salvo la coordinata x 
        sta  BakXY      ; su posizione di Backup
                        ; giro di test su x per vedere se andanado
                        ; avanti per la direzione x attuale incontrerò
                        ; un carattere diverso da spazio
                        ; la routine IncDecXY tiene conto di eventuali bordi
                        ; dello schermo raggiunti
        ldx  #$00       ; registro X indica a IndDecXY se lavorare con x o y
        jsr  IncDecXY   ; registro X=0 => lavora su coordinata x
        lda  NewXY      ; assegna la nuova coordinata x (calcolata da IncDecXY)
        sta  PosXY      ; alla coord. x attuale
        jsr  XYtoSCPos  ; calc.cella schermo e mette in A il carattere 
        ldy  BakXY      ; ripristino la coordinata x
        sty  PosXY      ; al valore prima della routine per test
        cmp  #$20       ; in A ho il carattere delle coordinate calcolate
        beq  TestYDir   ; se spazio allora proseguo col vedere la stessa cosa su Y 
        jsr  RestoreXY  ; prima di proseguire ritorno nei miei passi in x e cambio direzione x
TestYDir
        lda  PosXY+1    ; Salvo y attuale
        sta  BakXY+1    ;
        ldx  #$01       ; indico di lavorare su y
        jsr  IncDecXY   ; Decrementa o incrementa y in base alla direzione y attuale
        lda  NewXY+1    
        sta  PosXY+1
        jsr  XYtoSCPos  ; A = Carattere in prossimità seguendo nuova y
        ldy  BakXY+1    ; rispristino la y prima del test
        sty  PosXY+1
        cmp  #$20       ; A contiene uo spazio ?
        beq  TestDiagonale ; se contiene uno spazio test carattere diagonale in direzione 
        jsr  RestoreXY  ; ritorno sui miei passi per y
TestDiagonale           ; controllo del carattere che si trova nella diagonale
                        ; calcolata rispetto alla direzione attuale
        lda  NewXY      ; Copio x e y possibili calcolate rispetto al test sui caratteri 
        sta  PosXY      ; prossimi a x e y attuali ...
        lda  NewXY+1
        sta  PosXY+1    
        jsr  XYtoSCPos  ; ... calcolo nuova cella schermo A=car.sulla cella
        cmp  #$20       ; A contiene uno spazio ?
        beq  IncDecExit ; Si, tutto a posto le coordinate nuove vanno bene
        ldx  #$00       ; No
        jsr  ChgDirXY   ; Cambio Direzione delle x
        ldx  #$01       ; 
        jsr  ChgDirXY   ; e delle y (rimbalzo)
        lda  BakXY      ; e ripristino le "vecchie" coordinate x y
        sta  PosXY
        lda  BakXY+1
        sta  PosXY+1
IncDecExit
        rts             ; ritorno al chiamante

IncDecXY
        lda  DirXY,X    ; carico in A x o y in base all'indice Registro X
        cmp  #$00       ; coordinata == 0 ?
        beq  IncTestCoord ; Si, Incremento la coordinata di 1 e check sforamento bordi
        jmp  DecTestCoord ; No, Decremento la coordinata di 1 e check sforamento bordi

IncTestCoord
        inc  NewXY,X    ; Incremento la coordinata puntata dal registro X
        lda  NewXY,X    ; la carico in A
        cmp  MaxXY,X    ; controllo col numero di colonne
                        ; coordinata parte da 0 quindi se = numero colonna ho sforato
        beq  ChgDirDec  ; se ho "sforato" il bordo massimo inverto direzione
                        ; su quella specifica coordinata e la decremento
        rts             ; ritorno

DecTestCoord
        dec  NewXY,X    ; Decremento la coordinata puntata dal registro X
        lda  NewXY,X    ; Carico in A
        cmp  #$FF       ; ho sforato il bordo minimo ($00-$01 = $FF) ?
        beq  ChgDirInc  ; Cambia direzione e incrementa la coordinata
        rts             ; ritorno


ChgDirInc
        jsr  ChgDirXY   ; Cambia direzione rispetto alla coordinata
        inc  NewXY,X    ; incrementa la coordinata
        rts             ; ritorno

ChgDirDec
        jsr  ChgDirXY   ; Cambio direzione
        dec  NewXY,X    ; Decremento la coordinata in accordo con il registro X
        rts             ; ritorno


RestoreXY               ; ritorno sulla coordinata temporalmente precedente
        lda  PosXY,X    ; copio la "vecchia" coordinata
        sta  NewXY,X    ; su quella nuova
ChgDirXY
        lda  #$FF       ; A = $FF
        eor  DirXY,X    ; inverto i bit della direzione della coordinata
                        ; se era $00 diverrà $FF e viceversa
        sta  DirXY,X    ; aggiorno la direzione della coordinata
        jsr  Boing      ; effetto sonoro
        rts             ; ritorno

Boing                   ; Piccolo effetto sonoro
        php             ; Salvataggio dei vari registri
        pha
        txa
        pha
        ldx  #$1C
        lda  #$00
BoingLoop
        sta  SID,x
        dex
        bne  BoingLoop
        sta  SID
        lda  #$0f
        sta  SID + 24
        lda  #$14
        sta  SID + 1
        lda  #$00      ; 0*16+0
        sta  SID + 5
        lda  #$f9      ; 15*16+9 (249)
        sta  SID + 6
        lda  #$11      ; 1+16
        sta  SID + 4
        lda  #$10      ; 16
        sta  SID + 4
        pla             ; Ripristino dei registri
        tax
        pla
        plp
        rts

                        ; Attesa senza far niente 
                        ; sfruttando la raster line
Wait
        lda  $d012      ; Carico in A l'attuale linea di raster LSB
WaitLoop
        clc             ; pulizia carry
        adc  #$FE       ; A = A + $FE
        cmp  $D012      ; l'attuale raster line = A
        bne  WaitLoop   ; No, attendi ancora
        rts             ; torna al chiamante

Delay   byte $01        ; Indicatore del numero di cicli Wait a vuoto pe rla persistenza a video
Factor1 word $0000      ; Primo fattore per la moltiplicazione
Factor2 word $0000      ; Secondo fattore per la moltiplicazione
Result  word $0000      ; Risultato delle operazioni
PosXY   byte $00,$00    ; x, y Posizione attuale 
NewXY   byte $00,$00    ; x, y Poszione futura
MaxXY   byte $28,$19    ; col/row number 40, 25 (remember lsb &msb are inverted)
DirXY   byte $00,$00    ; x, y directions 00 forward ff backward
BakXY   byte $00,$00    ; Backup dei valori XY

 

Have your say