av Nick Morgan
översatt till svenska av Mikael O. Bonnier
I denna lilla bok jag ska visa dig hur du kommer igång att skriva 6502-assembler. 6502-processorn var massivt populär på sjuttio- och åttiotalet, och drev kända datorer som BBC Micro, Atari 2600, VIC-20, Commodore 64, Apple II, och Nintendo Entertainment System (NES). Bender i Futurama har en 6502-processor som hjärna. Även Terminator programmerades i 6502.
Så, varför skulle du vilja lära dig 6502? Det är ett dött språk, eller hur? Tja, det är latin också. Och de lär fortfarande ut det. Q.E.D.
(Faktiskt, jag har fått tillförlitlig information om att 6502-processorer fortfarande produceras av Western Design Center (WDC), så uppenbarligen är 6502 inte ett dött språk! Vem kunde tro det?)
Allvarligt talat, jag tycker det är värdefullt att ha en förståelse för assembler. Assembler är den lägsta nivån av abstraktion i datorer - den punkt där koden fortfarande är läsbar. Assembler översätter direkt till byte som sedan utförs av datorns processor. Om du förstår hur det fungerar, har du i princip blivit en datortrollkarl.
Varför 6502? Varför inte ett användbart assemblerspråk, som x86? Tja, jag tror inte att lära x86 är användbart. Jag tror inte att du någonsin kommer att behöva skriva assembler i ditt vanliga jobb - det är enbart en akademisk övning, något att utöka ditt sinne och ditt tänkande med. 6502 skrevs ursprungligen i en annan tid, en tid då de flesta av utvecklarna skrev assembler direkt, i stället för i dessa nymodiga högnivåprogrammeringsspråk. Så de var konstruerade för att skrivas av människor. Mer moderna assemblerspråk är tänkta att skrivas av kompilatorer, så låt oss lämna det till dem. Dessutom är 6502 kul. Ingen har någonsin kallat x86 kul.
Låt oss dyka i! Den saken här nedan är en liten JavaScript 6502-assemblator och -simulator/emulator som jag anpassade för denna bok. Klicka Assemble och sedan Run att assemblera och köra snutten med assemblerkod.
Förhoppningsvis har det svarta området till höger nu tre färgade “pixlar” uppe till vänster. (Om detta inte fungerar, behöver du förmodligen uppgradera din webbläsare till en mer modern, som Chrome eller Firefox.)
Alltså, vad gör det här programmet egentligen? Låt oss gå igenom den med
debuggern. Klicka Reset, kryssa sedan i Debugger-kryssrutan för att starta
debuggern. Klicka Step en gång. Om du tittade noga, bör du ha
märkt att A=
ändras från $00
till $01
och PC=
ändras från $0600
till
$0602
.
Alla tal med prefixet $
i 6502-assembler (och, i förlängningen, i
denna bok) är i hexadecimalt (hex) format. Om du inte är bekant med hex-tal, rekommenderar jag att du läser Wikipedia
artikeln. Allt som har #
som prefix
är verkliga, konkreta tal (#
är i detta sammanhang den amerikanska mostvarigheten till förkortningen, no, d.v.s. numero). Alla andra tal (d.v.s. utan #
) hänvisar till en minnesplats (d.v.s. en adress).
Utrustad med den kunskapen, bör du kunna inse att instruktionen
LDA #$01
laddar hextalet $01
i register A
(LoaD A med talet $01
). Jag ska gå in mer
i detalj på register i nästa avsnitt.
Klicka Step igen för att exekvera (d.v.s. utföra) den andra instruktionen. Den övre vänstra pixeln i
simulatorns display bör nu vara vit. Denna simulator använder minnesadresserna $0200
till $05ff
för att rita pixlar på sin skärm. Värdena $00
till
$0f
representerar 16 olika färger ($00
är svart och $01
är vit). Att lagra värdet $01
på adress $0200
ritar en vit pixel i
övre vänstra hörnet. Detta är enklare än hur en verklig dator skulle lagra data i grafikminnet, men det får duga tills vidare.
Alltså, instruktionen STA $0200
lagrar värdet för A
-registret i minnesaddressen
$0200
(STore A i adressen $0200
). Klicka Step ytterligare fyra gånger för att utföra resten av
instruktionerna, och håll ett öga på A
-registret, ty det ändrar sig.
$05ff
).Vi har redan tittat lite på processorstatusavdelningen (avdelningen med
A
, PC
m.fl.), men vad betyder allt detta?
Den första raden visar A
-, X
- och Y
-registren (A
kallas ofta
“ackumulatorn”). Varje register innehåller en enda byte. De flesta operationerna arbetar på
innehållet i dessa register.
‘SP’ är stackpekaren. Jag kommer inte gå igenom stacken ännu, men i grund och botten minskas detta registrer varje gång en byte läggs på stacken, och ökas när en byte plockas upp från stacken.
PC
är programräknaren - det är så processorn vet vid vilken punkt i
programmet den är just nu. Det är som den aktuella raden i ett körande
skript. I JavaScript-simulatorn assembleras koden med början på minnesadress $0600
, så PC
börjar alltid där.
Den sista delen visar processorflaggor. Varje flagga är en bit, så alla sju flaggor lever i en enda byte. Flaggorna ställs in av processorn för att ge information om den föregående instruktionen. Mer om det senare. Läs mer om register och flaggor här.
Instruktionerna i assembler är som en liten uppsättning fördefinierade funktioner. Alla instruktioner tar noll eller ett argument. Här är några kommenterade källkoder för att introducera några olika instruktioner:
Assemblera koden, slå sedan på debuggern och stega igenom koden, titta
då på A
- och X
-registret. Något lite underligt händer på raden ADC #$c4
.
Du förväntade dig kanske att lägga till $c4
till $c0
skulle ge $184
, men denna
processor ger resultatet som $84
. Vad pågår här?
Problemet är, $184
är för stort för att passa i en enda byte (max är $ff
),
och registren kan endast lagra en enda byte. Det är dock OK; processorn
är faktiskt inte dum. Om du tittade tillräckligt noga, märkte du
att carry-flaggan (d.v.s. minnessiffran) sattes till 1
efter denna operation. Så det är så du
vet.
I simulatorn nedan skriv in (inte klistra in) följande kod:
LDA #$80
STA $01
ADC $01
En viktig sak att notera här är skillnaden mellan ADC #$01
och
ADC $01
. Den första adderar värdet $01
till A
-registret, men den
andra adderar värdet lagrat på minnesplats $01
till A
-registret.
Assemblera, kryssa i Monitor-kryssrutan, stega sedan igenom dessa tre
instruktioner. Monitorn visar en del av minnet, och kan vara till hjälp för att
visualisera exekveringen av programmen. STA $01
lagrar värdet på A
-registret
på minnesplats $01
och ADC $01
adderar värdet lagrat i
minnesplats $01
till A
-registret. $80 + $80
skall motsvara $100
, men
eftersom det är större än en byte, sätts A
-registret till $00
och
carry-flaggan sätts. Förutom detta sätts dock zero-flaggan (d.v.s. nollflaggan). Zero-flaggan
sätts av alla instruktioner där resultatet är noll.
En fullständig lista över 6502 instruktionsuppsättning är tillgängliga här och här (jag brukar hänvisa till bägge sidorna enär de har sina styrkor och svagheter). Dessa sidor anger argument till varje instruktion, vilka register de använder, och vilka flaggor som de sätter. De är din bibel.
TAX
Du kan nog gissa vad TAY
, TXA
och TYA
gör,
men skriv lite kod för att testa dina antaganden.Y
-registret i stället för
X
-registeret.ADC
är SBC
(SuBtrahera med Carry (d.v.s. lån)). Skriv ett program som
använder denna instruktion.Hittills har vi bara kunnat skriva grundläggande program utan villkorliga hopp. Låt oss ändra på det.
6502 assembler har en massa hoppinstruktioner, som alla
hoppar baserat på om vissa flaggor är satta eller inte. I det här exemplet kommer vi att
titta på BNE
: “Hoppa om inte lika” (en. “Branch on Not Equal”).
Först laddar vi värdet $08
i X
-registret. Nästa rad är en etikett.
Etiketter markerar bara vissa punkter i ett program så att vi kan gå tillbaka till dem senare.
Efter etiketten minskar (dekrementerar) vi X
med 1, lagrar det i $0200
(övre, vänstra pixeln), och
jämför det sedan med värdet $03
.
CPX
jämför
värde i X
-registret med ett annat värde. Om de två värdena är lika,
sätts Z
-flaggan till 1
, annars sätts den till 0
.
Nästa rad, BNE minska
, flyttar exekveringen till minska-etiketten om
den så kallade Z
-flaggan är satt till 0
(vilket betyder att de två värdena i CPX
-jämförelsen
inte var lika), annars gör den ingenting och vi lagrar X
i $0201
, och sen
avslutar vi programmet.
I assembler brukar man använda etiketter tillsammans med hoppinstruktioner. När en sådan assemblerats så omvandlas etiketten till en en-byte relativ förflyttning (ett antal byte att hoppa bakåt eller framåt från nästa instruktion) så hoppinstruktioner som börjar på B (en. Branch, d.v.s. sv. förgrena) kan bara gå framåt och tillbaka i ett intervall på 256 byte. Detta betyder att de endast kan användas för att förflytta sig omkring i lokal kod. För att förflytta längre måste du använda långhoppinstruktionerna som börjar på J (en. Jump, sv. hoppa).
BNE
är BEQ
. Försök att skriva ett program som använder BEQ
.BCC
och BCS
(“hoppa om minnessiffran är nollställd” (en. “branch on carry clear”) och
“hoppa om minnessiffran är satt” (en. “branch on carry set”)) används
för att hoppa baserat på carry-flaggan. Skriv ett program som använder en av dessa två.6502 använder en 16-bitars adressbuss, vilket innebär att det finns 65536 bytes
minne tillgängligt för processorn. Kom ihåg att en byte representeras av två
hex-tecken, så minnesadresserna anges i allmänhet som $0000 - $ffff
. Det finns
olika sätt att hänvisa till dessa minnesadresser, vilket beskrivs i detalj nedan.
Tillsammans med alla dessa exempel kan du tycka att det underlättar att använda minnesmonitorn för
att se när minnet ändras. Monitorn tar en utgångsadress och antalet byte som
visas från den adressen. Båda dessa är hex-värden.
Till exempel, för att visa 16 byte minne från $c000
, ange c000
och 10
i Start respektive Length.
$c000
Med absolut adressering, används den fullständiga minnesadressen som argument till instruktionen. Till exempel:
STA $c000 ;Lagra värdet i ackumulatorn på minnesadress $c000
$c0
Alla instruktioner som stödjer absolut adressering (med undantag för långhoppinstruktionerna) har också möjlighet att ta en en-byte adress. Denna typ av adressering kallas “noll-sida” - bara den första sidan (de första 256 byte) av minnet är tillgängligt. Detta är snabbare, eftersom endast en byte behöver slås upp, och tar även upp mindre plats i den assemblerade koden (d.v.s. maskinkoden).
$c0,X
Det är här som adressering blir intressant. I detta läge är en nollsidesadress given, och sedan läggs värdet för X
-registret till. Här är ett exempel:
LDX #$01 ;X är $01
LDA #$aa ;A är $aa
STA $a0,X ;Lagra värdet av A i minnesadress $a1
INX ;Öka X med 1
STA $a0,X ;Lagra värdet av A i minnesadress $a2
Om resultatet av additionen är större än en enda byte, så går adressen runt. Till exempel:
LDX #$05
STA $ff,X ;Lagra värdet av A i minnesadress $04
$c0,Y
Detta motsvarar noll-sida,X, men kan bara användas med LDX
och STX
.
$c000,X
och $c000,Y
Dessa är absolutadresseringsversionerna av noll-sida,X och noll-sida,Y. Till exempel:
LDX #$01
STA $0200,X ;Lagra värdet av A på minnesadress $0201
#$c0
Omedelbar adressering handlar inte direkt om minnesadresser - detta är det
adresseringssätt där faktiska värden används. Till exempel, LDX #$01
laddar värdet
$01
i X
-registret. Det är väldigt annorlunda jämfört med noll-sideinstruktionen
LDX $01
som läser in värdet från minnesadress $01
i X
-registret.
$c0
(eller etikett)Relativ adressering används för hoppinstruktioner. Dessa instruktioner tar en enda byte, som används som en förskjutning från den följande instruktionen.
Assemblera följande kod, klicka sedan på Hexdump-knappen för att se den assemblerade koden.
Hexkoden bör se ut ungefär så här:
a9 01 c9 02 d0 02 85 22 00
a9
och c9
är processor-opkoder för omedelbart adresserade LDA
respektive CMP
.
01
och 02
är argumenten till dessa instruktioner. d0
är
opkoden för BNE
, och dess argument är 02
. Det innebär att “hoppa över nästa
två byte” (85 22
, den assemblerade versionen av STA $22
). Försök att redigera koden
så att STA
tar en två-byte absolut adress i stället för en enda byte noll-sideadress
(t.ex. ändra STA $22
till STA $2222
). Assemblera om koden och titta på
hexdumpen igen (klicka på Hexdump) - argumentet till BNE
ska nu vara 03
, eftersom
instruktionen som processorn skall hoppa över nu är tre byte lång.
Vissa instruktioner hanterar inte minnesadresser (t.ex. INX
- öka (d.v.s. inkrementera)
X
-registret). Dessa sägs ha implicit (eller underförstådd) adressering - argumentet
är inbyggt i instruktionen.
($c000)
Indirekt adressering använder en absolut adress för att slå upp en annan adress. Den första adressen ger den minst signifikanta byten i adressen, och den följande byten ger den mest signifikanta byten. Detta kan vara svårt att förstå, så här är ett exempel:
I detta exempel innehåller $f0
värdet $01
och $f1
innehåller värdet
$cc
. Instruktionen JMP ($00f0)
får processorn att slå upp de två
byten på $f0
och $f1
($01
och $cc
) och sätta ihop dem för att bilda
adressen $cc01
, som blir den nya programräknaren. Assemblera och stega
igenom programmet ovan för att se vad som händer. Jag ska prata mer om JMP
i
avsnittet om Långa hopp.
($c0,X)
Den här är ganska konstig. Det är som en korsning mellan noll-sida,X och indirekt.
I grund och botten tar du noll-sidans adress, lägger till värdet av X
-registret,
använder sedan summan för att slå upp en två-byte adress. Till exempel:
Minnesadresserna $01
och $02
innehåller värdena $05
respektive $06
.
Tänk på ($00,X)
som ($00+X)
. I detta fall är X
lika med $01
, så
detta förenklas till ($01)
. Härifrån fortgår det som vid normal indirekt
adressering - de två byten vid $01
och $02
($05
och $06
) slås upp
för att bilda adressen $0605
. Detta är den adress som Y
-registret lagrades
i i den tidigare instruktionen, så A
-registret får samma
värde som Y
, om än genom en omfattande omväg. Du kommer inte att se denna
ofta.
($c0),Y
Indirekt indexerad är som indexerad indirekt men mindre galen. I stället för att addera
X
-registret till adressen innan avreferering, så avrefereras noll-sidans adress,
och Y
-registret adderas till den resulterande adressen.
I detta fall, ($01)
slår upp de två byten i $01
och $02
: $03
och
$07
. Dessa utgör adressen $0703
. Värdet på Y
-registret läggs
till den här adressen för att ge den slutliga adressen $0704
.
Stacken i en 6502-processor är precis som alla andra stackar (d.v.s. travar/högar) - värden läggs/trycks
(“pushas”) på den och lyfts/dras (“poppas”, eller “pullas” i 6502-språkbruk) av den. Det aktuella djupet för
stacken mäts av stackpekaren, ett särskilt register. Stacken bor i
minnet mellan $0100
och $01ff
. Stackpekaren är initialt $ff
, vilket
pekar på minnesadress $01ff
. När en byte skjuts på stacken, så blir
stackpekaren $fe
, eller minnesadress $01fe
, och så vidare.
Två av stackinstruktionerna är PHA
och PLA
, “PusH Ackumulator” och “PulL
Ackumulator”. Nedan är ett exempel på dessa två i aktion.
X
lagrar pixelfärgen, och Y
lagrar positionen för den aktuella pixeln.
Den första loopen ritar den aktuella färgen som en pixel (via A
-registret),
pushar färgen till stacken, ökar sedan färgen och positionen med ett. Den
andra loopen poppar stacken, ritar den poppade färgen som en bildpunkt, och ökar
sedan positionen. Som man kan förvänta skapar detta ett speglat mönster.
Långa hopp är som villkorliga hopp men med två huvudsakliga skillnader. För det första, långa hopp utförs inte villkorligt, för det andra, de tar en två-byte absolut adress. För små program, är denna andra detalj inte särskilt viktig, eftersom du oftast använder etiketter och assembleraren räknar ut rätt minnesadress med hjälp av etiketten. För större program är dock långa hopp det enda sättet att flytta körningen från en del av koden till en annan.
JMP
är ett ovillkorligt långt hopp (en. JuMP). Här är ett mycket enkelt exempel för att visa det i aktion:
JSR
och RTS
(“hopp (en. Jump) till SubRutin” och “ReTur från Subrutin/underprogram”) är en
dynamisk duo som man brukar se tillsammans. JSR
används för att hoppa från
den aktuella adressen till en annan del av koden. RTS
återgår till föregående
position. Detta är i princip som att anropa en funktion och återvända/returnera.
Processorn vet vart den skall återvända eftersom JSR
lägger/pushar adressen minus
ett för nästa instruktion på stacken innan den hoppar till den givna
adressen. RTS
lyfter/poppar av denna adress, adderar ett till den, och hoppar till den adressen.
Ett exempel:
Den första instruktionen gör så att körningen hoppar till init
-etiketten. Detta sätter
X
, och sedan återvänder vi till nästa instruktion, JSR loopa
. Denna hoppar till loopa
-etiketten,
som ökar X
med ett tills det är lika med $05
. Efter detta återvänder vi till
nästa instruktion, JSR avsluta
, som hoppar till slutet av filen. Detta
illustrerar hur JSR
och RTS
kan användas tillsammans för att skapa modulär kod.
Låt oss nu se till att all denna kunskap kommer till nytta, och göra ett spel! Vi ska göra en riktigt enkel version av det klassiska spelet “Masken”/”Ormen” (en. “Snake”).
Simulator-fönstret nedan innehåller hela källkoden till spelet. Jag ska förklara hur det fungerar i de följande avsnitten. I detta program är etiketterna på originalspråket: engelska.
Willem van der Jagt har gjort en fullständigt kommenterad gist av denna källkod (på engelska), så följ med i den för fler detaljförklaringar.
Efter det första blocket med kommentarer (rader som börjar med semikolon), så är de första två raderna:
jsr init
jsr loop
init
och loop
är bägge subrutiner. init
initierar speltillståndet, och
loop
är den viktiga spelslingan.
Själva loop
-subrutinen anropar bara ett antal subrutiner sekventiellt,
innan den loopar tillbaka till sig själv:
loop:
jsr readkeys
jsr checkCollision
jsr updateSnake
jsr drawApple
jsr drawSnake
jsr spinwheels
jmp loop
Först kontrollerar readkeys
om en av riktningsknapparna (W, A, S, D)
har tryckts ner, och om så är fallet, så ställs riktningen på masken/ormen in i enlighet därmed. Sedan
kontrollerar checkCollision
om ormen kolliderade med sig själv eller med äpplet.
updateSnake
uppdaterar den interna representationen av ormen, baserat på dess
riktning. Därefter ritas äpple och orm. Slutligen får spinWheels
processorn att göra en del fördröjningsarbete, för att hindra spelet från att köra för fort. Tänk
på det som ett sov-kommando. Spelet fortsätter att köra tills ormen kolliderar
med väggen eller med sig själv.
Minnets nollsida används för att lagra ett antal speltillståndsvariabler, som
noteras i kommentarssektionen överst i spelet. Allt i $00
, $01
och $10
och uppåt är par av bytes som representerar en två-byte minnesadress
som kommer att slås upp med hjälp av indirekt adressering. Dessa minnesadresser kommer
alla att vara mellan $0200
och $05ff
- den del av minnet som motsvarar
simulatorns skärm. Till exempel, om $00
och $01
innehöll värdena $01
och $02
, skulle de hänvisa till den andra bildpunkten på skärmen ($0201
De första två byten lagrar placeringen av äpplet. Denna uppdateras varje gång
ormen äter äpplet. Byte $02
innehåller den aktuella riktningen. 1
betyder
upp, 2
höger, 4
ner och 8
vänster. Resonemanget bakom dessa siffror kommer
att framgå senare.
Slutligen, byte $03
innehåller den aktuella längden på ormen, i form av antal byte
i minnet vid adressen $10
(så att en längd på 4 betyder 2 bildpunkter).
init
-subrutinen anropar två subrutiner: initSnake
och
generateApplePosition
. initSnake
ställer in ormens riktning, längd och
laddar därefter de ursprungliga minnesadresserna för ormens huvud och kropp. Byte-paret vid
$10
innehåller huvudets skärmposition, och paret på $12
innehåller
positionen av det enda kroppssegmentet, och $14
innehåller positionen av
svansen (svansen är det sista segmentet av kroppen och ritas i svart för att hålla
ormen i rörelse). Detta händer i följande kod:
lda #$11
sta $10
lda #$10
sta $12
lda #$0f
sta $14
lda #$04
sta $11
sta $13
sta $15
Detta laddar värdet $11
i minnesadress $10
, värdet $10
i
$12
och $0f
i $14
. Den laddar sedan värdet $04
in $11
, $13
och $15
. Detta leder till detta i minnet:
0010: 11 04 10 04 0f 04
som representerar de indirekt-adresserade minnesadresserna $0411
, $0410
och
$040f
(tre pixlar i mitten av displayen). Jag betonar kanske denna sak överdrivet,
men det är viktigt att till fullo greppa hur indirekt adressering fungerar.
Nästa subrutin, generateApplePosition
ställer in äpplets läge till en
slumpmässig position på skärmen. Först laddar det en slumpmässig byte till
ackumulatorn ($fe
är en slumpgenerator i denna simulator). Detta
lagras i $00
. Därefter laddas en annan slumpmässig byte in i
ackumulatorn, som sedan AND
-as (d.v.s. bitvis och) med värdet $03
. Detta avsnitt behöver nu en
liten avstickare.
Hexvärdet $07
representeras i binär form som 00000111
. AND
opkoden
genomför en bitvis och (en. and) av argumentet med ackumulatorn. Till exempel, om
ackumulatorn innehåller det binära talet 01010101
, då blir resultatet av AND
med 00000111
blir 00000101
.
Resultatet av AND #$03
är att maska de två minst signifikanta bitararna i
ackumulatorn, och sätta de andra till noll. Detta konverterar ett tal i intervallet
0–255 till ett tal i intervallet 0–3.
Efter detta adderas värdet 2
till ackumulatorn, för att skapa ett slutligt, slumpvist
tal i området 2–5.
Resultatet av denna subrutin är att ladda en slumpmässig byte i $00
, och ett slumpmässigt
tal mellan 2 och 5 i $01
. Eftersom den minst signifikanta byten kommer
först med indirekt adressering, leder detta till en minnesadress mellan
$0200
och $05ff
: det exakta intervallet som används för att rita på skärmen.
Nästan alla spel har i sin kärna en spel-loop. Alla spel-loopar har samma grundläggande form: acceptera användarinmatning, uppdatera speltillståndet, och rita speltillståndet. Denna loop är inte annorlunda.
Den första subrutinen, readKeys
, tar på sig jobbet att acceptera användarinmatningen.
Minnesadressen $ff
lagrar ASCII-koden för den senaste knapptryckningen i denna
simulator. Värdet laddas in i ackumulatorn, jämförs sedan med $77
(hex-koden för W), $64
(D), $73
(S) och $61
. Om någon av dessa
jämförelser är framgångsrika, hoppar programmet till det lämpliga avsnittet.
Varje avsnitt (upKey
, rightKey
o.s.v.) kontrollerar först för att se om den aktuella
riktningen är motsatsen till den nya riktningen. Detta kräver en annan liten avstickare.
Som nämnts tidigare, representeras de fyra riktningarna internt av talen
1, 2, 4 och 8. Vart och ett av dessa tal är en potens av 2, och därmed är de representerade
av ett binärt tal med en enda 1
:
1 => 0001 (upp)
2 => 0010 (höger)
4 => 0100 (ner)
8 => 1000 (vänster)
BIT
-opkoden liknar AND
, men beräkningen används endast för att sätta
nollflaggan - det faktiska resultatet kastas. Nollflaggan sätts endast om
resultatet av att AND-a ackumulatorn med argumentet är noll. När vi tittar på
potenser av två, kommer nollflaggan endast ställas in om de två talen inte är
desamma. Till exempel är 0001 AND 0001
inte noll, men 0001 AND 0010
är noll.
Vi undersöker fallet upKey
. Om den aktuella riktningen är nedåt (4), kommer bittestet
vara noll. BNE
betyder “hoppa om nollflaggan är nollställd”, så i detta fall vi kommer
hoppa till illegalMove
, som bara återvänder från subrutinen. Annars
lagras den nya riktningen (1 i detta fall) på den avsedda minnesadressen.
Nästa subrutin, checkCollision
, anropar checkAppleCollision
och
checkSnakeCollision
. checkAppleCollision
bara kontrollerar om de två
byte som lagrar positionen av äpplet matchar de två byte som lagrar
positionen av huvudet. Om de gör det, ökas längden och en ny äppleposition genereras.
checkSnakeCollision
loopar igenom ormens kroppssegment och kontrollerar varje
byte-par mot huvudparet. Om det finns en matchning, då är spelet över.
Efter kollisionsdetektering, uppdaterar vi ormens position. Detta görs på en hög nivå som så här: För det första, flytta varje byte-par av kroppen upp en position i minnet. För det andra, uppdatera huvuded enligt den aktuella riktningen. Slutligen, om huvudet är utanför ramen, hantera det som en kollision. Jag ska illustrera detta med lite ASCII-konst. Varje par av hakparenteser innehåller en x,y-koordinat snarare än ett byte-par för enkelhets skull.
0 1 2 3 4
Huvud Svans
[1,5][1,4][1,3][1,2][2,2] Utgångsläge
[1,5][1,4][1,3][1,2][1,2] Värdet av (3) kopieras in i (4)
[1,5][1,4][1,3][1,3][1,2] Värdet av (2) kopieras in i (3)
[1,5][1,4][1,4][1,3][1,2] Värdet av (1) kopieras in i (2)
[1,5][1,5][1,4][1,3][1,2] Värdet av (0) kopieras in i (1)
[0,5][1,5][1,4][1,3][1,2] Värdet av (0) uppdateras utifrån riktning
På en låg nivå, är denna subrutin något mer komplex. Först laddas längden
in i X
-registret, som sedan minskas. Strängen nedan
visar utgångsminnet för ormen.
Minnesadress: $10 $11 $12 $13 $14 $15
Värde: $11 $04 $10 $04 $0f $04
Längden initieras till 4
, så X
börjar som 3
. LDA $10,x
laddar
värdet i $13
in i A
, sedan STA $12,x
lagrar detta värde i $15
. X
minskas, och vi loopar. Nu är X
lika med 2
, så vi laddar $12
och sparar det i
$14
. Detta loopar så länge X
är positivt (BPL
betyder “hoppa om positivt”).
När värdena väl har skiftats ormen ned, måste vi räkna ut vad som ska
göras med huvudet. Riktningen är laddas först in i A
. LSR
betyder “logisk
skift höger”, eller “flytta alla bitar ett steg åt höger”. Den minst
signifikanta biten skiftas in i carry-flaggan. Så om ackumulatorn är 1
,
efter LSR
är den 0
, med carry-flaggan satt (d.v.s. ettställd).
För att testa om riktningen är 1
, 2
, 4
eller 8
, skiftar koden ständigt
höger tills carry är satt. En LSR
betyder “upp”, två betyder “höger” o.s.v.
Nästa bit uppdaterar ormens huvud beroende på riktning. Detta är förmodligen den mest komplicerade delen av koden, och det beror helt på hur minnesaddresser motsvarar skärmkoordinater. Så låt oss titta på det mer i detalj.
Du kan tänka på skärmen som fyra horisontella band av 32 × 8 pixlar.
Dessa remsor motsvarar $0200-$02ff
, $0300-$03ff
, $0400-$04ff
och $0500-$05ff
.
De första raderna av bildpunkter är $0200-$021f
, $0220-$023f
, $0240-$025f
o.s.v.
Så länge man rör sig inom ett av dessa horisontella band, är saker
enkla. Till exempel, för att flytta höger, öka bara den minst signifikanta byten
(t.ex. $0200
blir $0201
). För att gå ner, lägg till $20
(t.ex. $0200
blir
$0220
). Vänster och uppåt är det omvända.
Att gå mellan remsorna är mer komplicerat, eftersom vi även måste ta hänsyn till den
mest signifikanta byten. Till exempel, att gå ner från $02e1
bör leda
till $0301
. Lyckligtvis är det ganska lätt att åstadkomma. Att addera $20
till $e1
resulterar i $01
och sätter carry-biten. Om carry-biten blev satt, vet vi att vi
också måste öka den mest signifikanta byten.
Efter ett steg i varje riktning, måste vi också kontrollera om huvudet
skulle hamna utanför ramen. Detta hanteras på olika sätt för varje riktning. För
vänster och höger, kan vi kontrollera om huvudet i praktiken har “gått
runt”. Att gå höger från $021f
genom att öka den minst signifikanta byten
skulle leda till $0220
, men detta innebär faktiskt att hoppa från den sista pixeln i den
första raden till den första pixeln i den andra raden. Så varje gång vi går åt höger,
måste vi kontrollera om den nya minst signifikanta byten är en multipel av $20
. Detta
görs med hjälp av en bitkontroll mot masken $1f
. Förhoppningsvis ska illustrationen
nedan visa hur maskering av de lägsta 5 bitarna avslöja om ett tal
är en multipel av $20
eller inte.
$20: 0010 0000
$40: 0100 0000
$60: 0110 0000
$1f: 0001 1111
Jag kommer inte att förklara ingående hur varje riktning fungerar, men förklaringen ovan bör ge dig tillräckligt för att kunna reda ut det med lite iakttagelser.
Eftersom speltillståndet lagras i termer av pixeladresser, är uppritningen av
spelet mycket enkel. Den första subrutinen, drawApple
, är extremt
enkel. Den sätter Y
till noll, laddar en slumpmässig färg i ackumulatorn, sen
lagrar den detta värde i ($00),y
. $00
är där äpplets adress
lagras, så ($00),y
avrefereras till denna minnesadress. Läs avsnittet “Indirekt
indexerad” i Adresseringssätt för mer information.
Härnäst kommer drawSnake
. Denna är också ganska enkel. X
sätts till noll och A
till ett. Vi lagrar sedan A
i ($10,x)
. $10
lagrar huvudets två byte-adress,
så det ritar en vit pixel på den aktuella huvudpositionen. Därefter laddar vi
$03
i X
. $03
lagrar längden på ormen, så ($10,x)
kommer i detta
fall att vara läget för svansen. Eftersom A
är noll nu, ritar detta en
svart pixel över svansen. Eftersom bara huvudet och svansen på ormen flyttas,
är detta tillräckligt för att hålla ormen i rörelse.
Den sista subrutinen, spinWheels
, är bara där för att spelet skulle gå för
snabbt annars. Allt spinWheels
gör är att räkna ner X
från noll tills den blir
noll igen. Den första dex
slår runt, vilket gör X
lika med #$ff
.
Hela detta avsnitt är författat av översättaren. Här finns ett labyrintspel för Easy 6502 på engelska och här finns ännu fler program som fungerar i Easy 6502. 6502 är en tidig, populär processor från 70-talet som också är liten: ca 3500 transistorer (Z80 har ca 8500; i386, ca 275000; ARMv7-A, ca 26000000). Man kan lätt få in en 6502 i en FPGA-krets, men jag vet inte om den är den bästa processorn i sin storleksklass för FPGA.
Resurserna i detta stycke är på engelska. Hur som helst, anledningen till att jag valde att göra en resurs som lär ut 6502 var att jag hittade webbplatsen som jag översatte och att processorn är väl utforskad på t.ex. http://www.visual6502.org/JSSim/ (några som löste upp en 6502 i syra). Det finns även andra inlärningsresurser för 6502, t.ex. http://jbit.sourceforge.net/ som finns för mobiler med J2ME/Java ME, datorer med Linux och Windows, m.fl. system, se https://github.com/efornara/jbit/wiki. Dessutom kan man köpa en begagnad Commodore 64 (C64/VIC64) inklusive bandspelare för endast ca 200 kr på auktionswebbplatser. Man kan sedan använda en kassettadapter för bilstereo för att överföra programmen från en PC:s hörlursutgång till C64:an.
Man kan lära sig mer om 6502-assembler för specifikt (emulerad) C64 men på engelska, se http://digitalerr0r.wordpress.com/category/commodore-64/. Börja med inlägget längst ner. Kursen är visserligen för Windows men jag har även kunnat köra den i Linux för x86 och den fungerar troligen även på Mac OS X för x86. Det går även att utveckla spel till 8-bitars NES, se Game Development for the 8-bit NES.
Det finns också en online-kurs i programspråket C för C64 som använder Linux men på tyska, se C Lernen mit cc65 und C64.
Det finns böcker på svenska om 6502-programmering. Nyligen auktionerades en barnbok
om 6502- och Z80-programmering ut på Tradera:
“Maskinkod och Assembler” av Lisa Watts och Mike Wharton.
Jag har läst den och då lånade jag den från Malmö stadsbibliotek.
Andra böcker om 6502-assembler på svenska:
“Programmera 6502” av Rodnay Zaks.
“Mastercod för VIC-64” av David Lawrence och Mark England.
Jag har länkat till universitetsbibliotek men böckerna kan även finnas på folkbibliotek.
Det finns ett forum för programmering av Commodore 64 på svenska och där kan man också få hjälp med sina program i 6502-assembler eller C, se Commodore 64 ‹ Programmering/prog.-verktyg.