Akumulátor a dva index registry – a není to málo, Antone Pavloviči?
Co se dozvíte v článku
- Akumulátor a dva index registry – a není to málo, Antone Pavloviči?
- Vyplnění bloku paměti bajtem se zvolenou hodnotou
- Řešení využívající jen jediný index registr
- Oprava klasické chyby ±1
- Vyplňování směrem k vyšším adresám
- Porovnání všech čtyř metod z pohledu velikosti kódu a výpočetní rychlosti
- Vyplnění bloku většího než 256 bajtů: koncept nepřímého adresování
- Realizace programu vyplňujícího více než 256 bajtů paměti
- Algoritmus pro blokové přesuny dat
- Grafické subsystémy osmibitových mikropočítačů
- Rozlišení a počet barev
- Grafické čipy v osmibitových počítačích Atari
- ANTIC – generování rastrové grafiky
- Grafické režimy čipu ANTIC
- GTIA – sprity a vícebarevná grafika
- Priorita spritů a detekce kolize spritů
- Příloha A: Seznam všech doposud popsaných instrukcí mikroprocesoru MOS 6502
- Příloha B: Makefile pro překlad všech demonstračních příkladů
- Repositář s demonstračními příklady
- Odkazy na Internetu
Osmibitový mikroprocesor MOS 6502, který je ústředním prvkem osmibitových domácích mikropočítačů Atari, obsahoval pouze minimální, ovšem ještě stále prakticky použitelný počet registrů. Všechny tyto registry jsou pro připomenutí vypsány v následující tabulce:
| # | Registr | Šířka | Význam |
|---|---|---|---|
| 1 | A | 8 bitů | akumulátor |
| 2 | X | 8 bitů | index registr (osmibitová adresa nebo offset adresy) |
| 3 | Y | 8 bitů | index registr (osmibitová adresa nebo offset adresy) |
| 4 | SP | 8 bitů | část ukazatele na vrchol zásobníku (+ $0100) |
| 5 | PC | 16 bitů | čítač instrukcí |
| 6 | P | 7/8 bitů | příznakový a stavový registr |
Poslední tři registry se většinou používají nepřímo. Naopak přímo se pracuje s akumulátorem A a index registry X a Y. Kupodivu i přes tato omezení (připomeňme si, že index registry jsou jen osmibitové) je možné realizovat i sofistikované programy, a to díky pokročilým adresním režimům. Opět si pro připomenutí vypišme ty adresovací režimy, které se používají pro adresování operandů (načítání, ukládání, vyplňování bloků/polí, přenosy bloků atd.). Vynechám režimy používané pro realizaci skoků:
| # | Zápis | Název | Assembler | Stručný popis |
|---|---|---|---|---|
| 1 | impl | implied | INS | operand je odvozen přímo z instrukce, například INX |
| 2 | A | accumulator | INS A | operandem je přímo akumulátor |
| 3 | # | immediate | INS #$BB | za instrukcí následuje bajt s konstantou |
| 4 | zpg | zeropage | INS $LL | operand je uložen na nulté stránce na adrese LL |
| 5 | zpg,X | zeropage, X-indexed | INS $LL,X | operand je uložen na nulté stránce na adrese LL+X |
| 6 | zpg,Y | zeropage, Y-indexed | INS $LL,Y | operand je uložen na nulté stránce na adrese LL+Y |
| 7 | abs | absolute | INS $LLHH | za instrukcí následuje šestnáctibitová adresa, na níž je operand uložen |
| 8 | abs,X | absolute, X-indexed | INS $LLHH,X | za instrukcí následuje šestnáctibitová adresa, která je přičtena k X |
| 9 | abs,Y | absolute, Y-indexed | INS $LLHH,Y | za instrukcí následuje šestnáctibitová adresa, která je přičtena k Y |
| 10 | X,ind | X-indexed, indirect | INS ($LL,X) | efektivní adresa je spočtena z hodnoty uložené na (LL+X) |
| 11 | ind,Y | indirect, Y-indexed | INS ($LL),Y | efektivní adresa je spočtena z hodnoty uložené na LL, k výsledku se přičte Y |
Vyplnění bloku paměti bajtem se zvolenou hodnotou
Nejprve se pokusme vytvořit jednoduchý program, který po svém spuštění vyplní prvních pět textových řádků ve standardním textovém režimu. Každý řádek obsahuje čtyřicet znaků, takže je nutné vyplnit pouze 5×40=200 bajtů. A to je „pěkná“ hodnota, protože je menší než 256, což celý algoritmus zjednoduší.
První varianta programu pro vyplnění pěti textových řádků bude používat index registr Y ve funkci offsetu a index registr X bude počitadlo smyčky. Offset se zvyšuje instrukcí INY (INcrement Y) a adresa pro zápis je vypočtena na základě adresy uložené v buňkách 88 a 89, ke které je připočten právě obsah registru Y. Počitadlo budeme postupně snižovat instrukcí DEX (DEcrement X), která mj. nastavuje i příznakový bit zero. Ten je testován instrukcí BNE (Branch if Not Equal), čímž je realizována celá programová smyčka:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldx #40*5 ; počet zápisů
ldy #0 ; offset
clear:
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
iny ; zvýšit hodnotu offsetu
dex ; zmenšit hodnotu počitadla
bne clear ; skok, pokud X>0
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Po překladu a spuštění tohoto programu by měla mít obrazovka následující obsah:
Obrázek 1: Obrazovka ve standardním textovém režimu; prvních pět řádků je vyplněno jednoduchou programovou smyčkou.
Řešení využívající jen jediný index registr
Program z předchozí kapitoly je sice plně funkční, ovšem je taktéž zbytečně dlouhý a využívá všechny tři pracovní registry. Ve skutečnosti je ovšem možné ho zkrátit a navíc využít jen jediný index registr. Musí se jednat o registr Y, protože jen pro něj existuje adresovací režim indirect, Y-indexed. Registr naplníme koncovým offsetem (hodnotou počitadla) a budeme ho postupně snižovat o jedničku. Smyčka bude dokončena ve chvíli, kdy registr dosáhne nulové hodnoty:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldy #40*5 ; počet zápisů
clear:
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
dey ; zmenšit hodnotu počitadla a offsetu
bne clear ; skok, pokud Y>0
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Po překladu a spuštění tohoto programu ovšem zjistíme, že vyplňuje chybně – podařilo se nám udělat klasickou „off-by-one“ chybu:
Obrázek 2: Vyplnění s chybou typu off-by-one.
Oprava klasické chyby ±1
Vyplnění s posunem o jeden znak je způsobeno tím, že sice programovou smyčku opakujeme správně 40×5=200krát, ovšem samotný zápis je proveden ještě před snížením hodnoty index registru Y. Náprava je ve skutečnosti snadná, protože můžeme zápis provést až po zmenšení počitadla Y. Vše bude funkční, protože instrukce STA (zápis do paměti) je jednou z mála instrukcí mikroprocesoru MOS 6502, která nemění příznakové bity. Korektní varianta programu tedy bude vypadat takto:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldy #40*5 ; počet zápisů
clear:
dey ; zmenšit hodnotu počitadla a offsetu
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
bne clear ; skok, pokud Y>0
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Výsledek bude (opět) korektní:
Obrázek 3: Korektní vyplnění prvních pěti textových řádků.
Vyplňování směrem k vyšším adresám
Samozřejmě se můžeme pokusit o úpravu počítané programové smyčky takovým způsobem, že budeme provádět vyplňování od prvního znaku a nikoli od znaku posledního. Počitadlo a současně i offset reprezentovaný index registrem Y tedy bude začínat hodnotou 0 a bude postupně zvyšováno až do hodnoty 40×5=200. Po dosažení této hodnoty bude počítaná programová smyčka ukončena:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldy #0 ; počitadlo zápisů
clear:
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
iny ; zvětšit hodnotu počitadla a offsetu
cpy #40*5 ; test na koncovou hodnotu počitadla
bne clear ; skok, pokud Y<40*5
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Opět se podívejme na výsledek:
Obrázek 4: Korektní vyplnění prvních pěti textových řádků.
Porovnání všech čtyř metod z pohledu velikosti kódu a výpočetní rychlosti
Všechny čtyři algoritmy, tedy i včetně algoritmu nekorektního, si pochopitelně můžeme porovnat jak s ohledem na velikost strojového kódu, tak i s ohledem na rychlost jedné iterace. Nejprve si uvedeme výpisy strojových kódů jednotlivých variant, ze kterých je možné zjistit velikosti kódů:
000000r 1 A9 21 lda #33 ; kod znaku, ktery se bude tisknout 000002r 1 A2 C8 ldx #40*5 ; počet zápisů 000004r 1 A0 00 ldy #0 ; offset 000006r 1 clear: 000006r 1 91 58 sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce 000008r 1 ; (adresa Video RAM je na adresách 88 a 89) 000008r 1 C8 iny ; zvýšit hodnotu offsetu 000009r 1 CA dex ; zmenšit hodnotu počitadla 00000Ar 1 D0 FA bne clear ; skok, pokud X>0
000000r 1 A9 21 lda #33 ; kod znaku, ktery se bude tisknout 000002r 1 A0 C8 ldy #40*5 ; počet zápisů 000004r 1 clear: 000004r 1 91 58 sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce 000006r 1 ; (adresa Video RAM je na adresách 88 a 89) 000006r 1 88 dey ; zmenšit hodnotu počitadla a offsetu 000007r 1 D0 FB bne clear ; skok, pokud Y>0
000000r 1 A9 21 lda #33 ; kod znaku, ktery se bude tisknout 000002r 1 A0 C8 ldy #40*5 ; počet zápisů 000004r 1 clear: 000004r 1 88 dey ; zmenšit hodnotu počitadla a offsetu 000005r 1 91 58 sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce 000007r 1 ; (adresa Video RAM je na adresách 88 a 89) 000007r 1 D0 FB bne clear ; skok, pokud Y>0
000000r 1 A9 21 lda #33 ; kod znaku, ktery se bude tisknout 000002r 1 A0 00 ldy #0 ; počitadlo zápisů 000004r 1 clear: 000004r 1 91 58 sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce 000006r 1 ; (adresa Video RAM je na adresách 88 a 89) 000006r 1 C8 iny ; zvětšit hodnotu počitadla a offsetu 000007r 1 C0 C8 cpy #40*5 ; test na koncovou hodnotu počitadla 000009r 1 D0 F9 bne clear ; skok, pokud Y<40*5
Můžeme tedy snadno porovnat velikosti jednotlivých implementací:
| Implementace | Velikost (bajtů) |
|---|---|
| dva index registry | 12 |
| výplň směrem dolů, jeden registr | 9 |
| oprava předchozího řešení | 9 |
| výplň směrem nahoru | 11 |
Rychlost jedné iterace vypočteme z tabulky na adrese https://www.nesdev.org/wiki/6502_cycle_times pouhým součtem cyklů jednotlivých instrukcí (instrukce se nepřekrývají):
| Implementace | Jedna iterace (cyklů) |
|---|---|
| dva index registry | 6+2+2+3=13 |
| výplň směrem dolů, jeden registr | 6+2+3=11 |
| oprava předchozího řešení | 2+6+3=11 |
| výplň směrem nahoru | 6+2+2+3=13 |
Porovnáním obou tabulek tedy zjistíme, že realizace třetím algoritmem jasně vyhrává:
000004r 1 clear: 000004r 1 88 dey ; zmenšit hodnotu počitadla a offsetu 000005r 1 91 58 sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce 000007r 1 ; (adresa Video RAM je na adresách 88 a 89) 000007r 1 D0 FB bne clear ; skok, pokud Y>0
Vyplnění bloku většího než 256 bajtů: koncept nepřímého adresování
Komplikovanější situace nastane tehdy, pokud velikost vyplňovaného bloku přesáhne 256 bajtů. V tomto případě již není možné ve funkci offsetu použít index registr Y, ale je nutné nějakým způsobem pracovat se šestnáctibitovou adresou. Ta nemůže být uložena v registru (žádný tak široký registr nemáme!), ale přímo v paměti. Využijeme tedy nepřímé adresování (což již do jisté míry známe) a budeme muset vyřešit i zvyšování této adresy: buď jen spodního bajtu, nebo i bajtu vyššího. Adresu pro zápis uložíme někam do nulté stránky paměti, ke které je rychlejší přístup:
to = $80 ; dva bajty z nulté stránky
Taktéž je ovšem nutné pracovat i se šestnáctibitovým počitadlem, takže se opět použije dvojice bajtů uložená v operační paměti:
sizeh: .byte >800 ; vyšší bajt velikosti sizel: .byte <800 ; nižší bajt velikosti
Zápis je proveden ve třech počítaných smyčkách. Nejprve se ve dvojici vnořených smyček vyplní celé 256bajtové bloky a ve druhé smyčce případný kratší blok na konci. Uveďme si nejprve první blok s vnořenými smyčkami. Vnější smyčka má počitadlo X, původně obsahuje počet vyplňovaných 256bajtových bloků. Vnitřní smyčka má počitadlo Y a provádí výplň celého bloku (což znamená, že se Y automaticky nuluje – jedná se totiž o čistě osmibitový registr):
ldy #0 ; offset pro zápis
ldx sizeh ; vyšší bajt velikosti (počet bloků)
beq fill2 ; je nulový? -> přeneseme jen zbylé bajty
fill1: sta (to), y ; zápis do bloku
iny ; zvýšit offset
bne fill1 ; počítáme až do 256
inc to+1 ; zvýšit adresu (!!!)
dex ; zmenšit počitadlo bloků
bne fill1 ; pokračovat dalším blokem
Přenos zbylých bajtů je realizováno takto (X obsahuje hodnotu získanou sizel, Y byl naopak vynulován, takže nemusíme provádět inicializaci):
fill2: ldx sizel ; nižší bajt velikosti (počet zbývajících bajtů)
beq fill4 ; je nulový? -> vše hotovo!
fill3: sta (to), y ; přenést zbylé bajty (méně než 256)
iny ; zvýšit offset
dex ; zmenšit počitadlo smyčky
bne fill3 ; opakujeme dokud se X nevynuluje
fill4:
Realizace programu vyplňujícího více než 256 bajtů paměti
Podívejme se nyní na praktickou realizaci algoritmu pro vyplnění 800 bajtů textové paměti, tedy celých dvaceti textových řádků:
.include "atari.inc"
.CODE
to = $80 ; dva bajty z nulté stránky
.proc main
lda 88 ; načíst část adresy počátku video RAM
sta to ; uložit do "proměnné"
lda 89 ; načíst část adresy počátku video RAM
sta to+1 ; uložit do "proměnné"
lda #33 ; kod znaku, ktery se bude tisknout
ldy #0 ; offset pro zápis
ldx sizeh ; vyšší bajt velikosti (počet bloků)
beq fill2 ; je nulový? -> přeneseme jen zbylé bajty
fill1: sta (to), y ; zápis do bloku
iny ; zvýšit offset
bne fill1 ; počítáme až do 256
inc to+1 ; zvýšit adresu (!!!)
dex ; zmenšit počitadlo bloků
bne fill1 ; pokračovat dalším blokem
fill2: ldx sizel ; nižší bajt velikosti (počet zbývajících bajtů)
beq fill4 ; je nulový? -> vše hotovo!
fill3: sta (to), y ; přenést zbylé bajty (méně než 256)
iny ; zvýšit offset
dex ; zmenšit počitadlo smyčky
bne fill3 ; opakujeme dokud se X nevynuluje
fill4:
loop: jmp loop
; data
sizeh: .byte >800 ; vyšší bajt velikosti
sizel: .byte <800 ; nižší bajt velikosti
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
O tom, že se vyplní více než 256 bajtů (znaků) se můžeme velmi snadno přesvědčit:
Obrázek 5: Obrazovka, ve které je vyplněno více než 256 bajtů (znaků).
Obrazovka, ve které je vyplněno více než 256 bajtů (znaků).
Pro úplnost se ještě podívejme na výpis vygenerovaného strojového kódu:
000000r 1 .proc main 000000r 1 A5 58 lda 88 ; načíst část adresy počátku video RAM 000002r 1 85 80 sta to ; uložit do "proměnné" 000004r 1 A5 59 lda 89 ; načíst část adresy počátku video RAM 000006r 1 85 81 sta to+1 ; uložit do "proměnné" 000008r 1 000008r 1 A9 21 lda #33 ; kod znaku, ktery se bude tisknout 00000Ar 1 00000Ar 1 A0 00 ldy #0 ; offset pro zápis 00000Cr 1 AE rr rr ldx sizeh ; vyšší bajt velikosti (počet bloků) 00000Fr 1 F0 0A beq fill2 ; je nulový? -> přeneseme jen zbylé bajty 000011r 1 91 80 fill1: sta (to), y ; zápis do bloku 000013r 1 C8 iny ; zvýšit offset 000014r 1 D0 FB bne fill1 ; počítáme až do 256 000016r 1 E6 81 inc to+1 ; zvýšit adresu (!!!) 000018r 1 CA dex ; zmenšit počitadlo bloků 000019r 1 D0 F6 bne fill1 ; pokračovat dalším blokem 00001Br 1 AE rr rr fill2: ldx sizel ; nižší bajt velikosti (počet zbývajících bajtů) 00001Er 1 F0 06 beq fill4 ; je nulový? -> vše hotovo! 000020r 1 91 80 fill3: sta (to), y ; přenést zbylé bajty (méně než 256) 000022r 1 C8 iny ; zvýšit offset 000023r 1 CA dex ; zmenšit počitadlo smyčky 000024r 1 D0 FA bne fill3 ; opakujeme dokud se X nevynuluje 000026r 1 fill4: 000026r 1 000026r 1 4C rr rr loop: jmp loop
Celková délka programu nyní dosahuje úctyhodných 41 bajtů!
Algoritmus pro blokové přesuny dat
Pro úplnost si ještě představme algoritmus pro blokové přesuny dat, konkrétně přesuny bloků, které mohou přesahovat velikost 256 bajtů (pokud jsou bloky menší, jedná se o relativně jednoduchou záležitost). Při blokových přesunech je pochopitelně nutné zjistit, jestli se zdrojový a cílový blok nepřekrývá a pokud tomu tam je, který z bloků začíná na nižší adrese, protože výběr konkrétního způsobu přenosu na těchto okolnostech závisí. Různé realizace přesunu bloků uvedené pod tímto odstavcem byly získány ze stránky http://www.6502.org/source/general/memory_move.html a dokonce nejsou psány v syntaxi CA65 (to je však triviálně řešitelné):
; Move memory down
;
; FROM = source start address
; TO = destination start address
; SIZE = number of bytes to move
;
MOVEDOWN LDY #0
LDX SIZEH
BEQ MD2
MD1 LDA (FROM),Y ; move a page at a time
STA (TO),Y
INY
BNE MD1
INC FROM+1
INC TO+1
DEX
BNE MD1
MD2 LDX SIZEL
BEQ MD4
MD3 LDA (FROM),Y ; move the remaining bytes
STA (TO),Y
INY
DEX
BNE MD3
MD4 RTS
Přesun opačným směrem:
; Move memory up
;
; FROM = source start address
; TO = destination start address
; SIZE = number of bytes to move
;
MOVEUP LDX SIZEH ; the last byte must be moved first
CLC ; start at the final pages of FROM and TO
TXA
ADC FROM+1
STA FROM+1
CLC
TXA
ADC TO+1
STA TO+1
INX ; allows the use of BNE after the DEX below
LDY SIZEL
BEQ MU3
DEY ; move bytes on the last page first
BEQ MU2
MU1 LDA (FROM),Y
STA (TO),Y
DEY
BNE MU1
MU2 LDA (FROM),Y ; handle Y = 0 separately
STA (TO),Y
MU3 DEY
DEC FROM+1 ; move the next page (if any)
DEC TO+1
DEX
BNE MU1
RTS
Jiná alternativa:
; Move memory up
;
; FROM = 1 + source end address
; TO = 1 + destination end address
; SIZE = number of bytes to move
;
MOVEUP LDY #$FF
LDX SIZEH
BEQ MU3
MU1 DEC FROM+1
DEC TO+1
MU2 LDA (FROM),Y ; move a page at a time
STA (TO),Y
DEY
BNE MU2
LDA (FROM),Y ; handle Y = 0 separately
STA (TO),Y
DEY
DEX
BNE MU1
MU3 LDX SIZEL
BEQ MU5
DEC FROM+1
DEC TO+1
MU4 LDA (FROM),Y ; move the remaining bytes
STA (TO),Y
DEY
DEX
BNE MU4
MU5 RTS
Ještě jedna odlišná alternativa:
; Move memory up
;
; FROM = source end address
; TO = destination end address
; SIZE = number of bytes to move
;
MOVEUP LDY #0
LDX SIZEH
BEQ MU3
MU1 LDA (FROM),Y ; handle Y = 0 separately
STA (TO),Y
DEY
DEC FROM+1
DEC TO+1
MU2 LDA (FROM),Y ; move a page at a time
STA (TO),Y
DEY
BNE MU2
DEX
BNE MU1
MU3 LDX SIZEL
BEQ MU5
LDA (FROM),Y ; handle Y = 0 separately
STA (TO),Y
DEY
DEX
BEQ MU5
DEC FROM+1
DEC TO+1
MU4 LDA (FROM),Y ; move the remaining bytes
STA (TO),Y
DEY
DEX
BNE MU4
MU5 RTS
ld hl, SOURCE_ADDRESS ; adresa zdrojového bloku
ld de, TARGET_ADDRESS ; adresa cílového bloku
ld bc, BLOCK_SIZE ; velikost přenášených dat
ldir ; provést blokový přenos
Grafické subsystémy osmibitových mikropočítačů
Ve druhé části dnešního článku se, prozatím alespoň ve stručnosti, seznámíme s grafickým subsystémem osmibitových mikropočítačů Atari. Ten je do značné míry unikátní a do jisté míry vychází z čipů, které obsahovala osmibitová herní konzole Atari 2600.
S nástupem osmibitových domácích počítačů je spojen i prudký rozvoj počítačové grafiky, který byl způsobem zejména vývojem počítačových her. Později byly pro tyto počítače vytvořeny i „seriózní“ programy vyžívající grafické možnosti těchto počítačů, například jednoduché CAD systémy, matematické programy, které dokázaly vykreslovat grafy apod.
Podívejme se však nejprve, jaké vlastnosti měl typický osmibitový počítač. Vzhledem k tomu, že mikroprocesory, kterými byly tyto počítače osazeny (většinou MOS 6502 a jeho varianty či Zilog Z80), měly osmibitovou datovou sběrnici a šestnáctibitovou sběrnici adresovou. Bylo tak možné adresovat až 216B=64KB paměti.
Do tohoto adresového prostoru byla namapována jak klasická operační paměť typu RAM/RWM, tak i paměť ROM a obrazová paměť, která se většinou nacházela přímo v operační paměti, což komplikovalo přístup procesoru a grafického čipu na sběrnici (oba čipy se musely nějakým způsobem o sběrnici dělit, z čehož například plyne rozdíl mezi „pomalým“ a „rychlým“ regionem RAM v ZX Spectru).
Z výše uvedeného také vyplývá, že kapacita obrazové paměti musela být co nejmenší, aby její adresový rozsah neubíral příliš mnoho z již tak omezeného adresového prostoru mikroprocesoru, nehledě na to, že by bylo pomalejší i překreslování snímků ve hrách atd.. Maximální praktická velikost obrazové paměti se z tohoto důvodu pohybovala na hranici osmi kilobytů (výjimkou jsou některé modely BBC Micro).
I na takto malém prostoru však bylo možné vytvořit překvapivě působivé a k tomu i dynamické efekty. Návrháři většinou použili obvody, které mohly pracovat ve více grafických režimech, někdy byly využity i barvové atributy platné pro větší blok pixelů, typicky 8×8. Řešení je v této oblasti mnoho, každé má své výhody a nevýhody.
Dalším požadavkem kladeným na domácí počítače byla samozřejmě i jejich cena. Z důvodu co nejnižšího zatížení peněženky případného kupce se proto tyto počítače konstruovaly tak, že je bylo možné zapojit na běžný televizor a jako externí paměťové médium se používaly kompaktní magnetofonové kazety nebo i klasické magnetofonové cívky (snad s výjimkou českého počítače Maťo, který používal gramofonové desky). Televizní přijímač vykazuje díky šířce pásma a modulaci i demodulaci při zobrazování jemné rastrové grafiky velké množství chyb, které bylo zapotřebí eliminovat (nebo naopak i využít) vhodným návrhem grafického výstupu.
Taktovací frekvence použitých mikroprocesorů se pohybovala v řádu jednotek MHz a proto nebylo vhodné, aby samotný mikroprocesor řešil tvorbu všech grafických efektů, neboť to vyžaduje poměrně rozsáhlé, a tím pádem i pomalé přesuny dat v paměti. Z tohoto důvodu měly některé počítače na grafickém čipu implementovánu podporu pro sprity, pomocí nichž bylo možné zobrazovat pohybující se předměty nezávisle na okolním obraze (sem pochopitelně spadají i osmibitová Atari).
Rozlišení a počet barev
S velikostí obrazové paměti úzce souvisí i rozlišovací schopnost a počet současně zobrazitelných barev. Podle rozlišovací schopnosti můžeme osmibitové počítače zhruba rozdělit do dvou skupin:
- V první skupině se nachází počítače, které zobrazovaly obraz v rozlišení 256×192 pixelů, což v textovém režimu odpovídá textovému oknu 32×24 znaků. Předností tohoto rozlišení je jednoduché adresování (délka řádku je mocninou dvou) a také fakt, že se všechny pixely zobrazily přesně i na běžném televizním přijímači. Nevýhodný je především horizontální počet pixelů při práci v textových režimech, kdy je počet 32 znaků pro mnoho účelů nedostatečný, což se obcházelo zmenšením rastru, ve kterých se znaky zobrazují. Typickým zástupcem těchto počítačů je slavné ZX Spectrum a prakticky všechny jeho klony a nástupci.
- Ve druhé skupině se nachází počítače, jejichž maximální podporované rozlišení bylo 320×192 resp. 320×200 pixelů nebo vyšší. Toto rozlišení se nazývalo „profesionální“, protože bylo možné zobrazit znaky o rozměrech 8×8 pixelů v textovém režimu 40×24 resp. 40×25 znaků podobně jako na prvních PC s grafickou kartou CGA – ostatně násobky 320 najdeme i dnes u mnoha standardizovaných rozlišení. Nevýhodou těchto rozlišení je složitější výpočet pozice pixelu či znaku na obrazovce a také problematické zobrazení na televizním monitoru, kdy při poměrně velkém horizontálním rozlišení docházelo ke vzniku barevných artefaktů u sousedních pixelů, které byly navzájem kontrastní (zejména v NTSC, zatímco norma PAL byla použitelná i pro takto relativně vysoké horizontální rozlišení). Představitelem této skupiny počítačů jsou osmibitová Atari a Commodore C64.
Grafické čipy v osmibitových počítačích Atari
V osmibitových domácích počítačích firmy Atari (například se jedná o modely Atari 800XL, Atari 800XE, Atari 130XE, Atari 1200XL a Atari 65XE) byla pro vytváření grafického výstupu použita dvojice poměrně komplikovaných číslicových obvodů nazvaných ANTIC a GTIA. Tyto dva obvody se navzájem doplňovaly ve svých funkcích a umožňovaly tak precizní nastavení grafického či textového režimu, horizontální i vertikální posun obrazu a na zobrazeném pozadí nezávislé vykreslování spritů.
Na čipech ANTIC a GTIA je nejzajímavější začlenění do systému počítače. Obvod ANTIC byl totiž ve skutečnosti samostatným grafickým procesorem (GPU), jelikož obsahoval vlastní sadu příkazů a dokázal obsluhovat přístup do paměti bez zásahu procesoru (tím byl MOS 6502A). Dnes si popíšeme základní možnosti těchto čipů, příště je už vyžijeme v programech.
ANTIC – generování rastrové grafiky
Na osmibitových počítačích Atari se grafika skládala ze dvou částí: pozadí (hrací pole, playfield) a pohyblivých rastrových obrázků (spritů). Pro nastavení grafického režimu pozadí (playfield) byl určen obvod ANTIC (což je zkratka ze sousloví Alpha-Numeric Television Interface Circuit), který umožnil pro každý vykreslovaný řádek (scanline) programově nastavit jeden ze dvanácti grafických režimů, které se lišily svým rozlišením, počtem současně zobrazitelných barev a velikostí alokované paměti.
Některé z podporovaných režimů zobrazovaly pouze znaky, jiné byly čistě bitmapové a další byly sice textové, ovšem se speciální interpretací rastrů znaků (tyto režimy jsou nejužitečnější při tvorbě her). U každého zobrazovaného řádku bylo možné povolit přerušení DLI (Display-List Interrupt), které nastalo při horizontálním zpětném běhu elektronového paprsku na televizní obrazovce. Pro každý zobrazovaný řádek bylo také možné povolit jemný posuv o maximálně osm pixelů, čímž bylo možné realizovat jednoduchý scrolling obrazovky nebo její části (tj. pouze několika vybraných řádků).
Vzhledem k tomu, že se čip ANTIC řídil vlastní sadou příkazů nazývanou Display List a při vykreslování přebíral od hlavního procesoru řízení sběrnice, se v podstatě jednalo o jeden z prvních široce používaných grafických procesorů (Graphics Processing Unit – GPU). Operační paměť byla mezi hlavním procesorem počítače MOS 6502 a čipem ANTIC sdílená, což vedlo k nutnosti postupného přebírání řízení jedním z procesorů za pomoci operací přerušení.
Díky frekvenci hlavního procesoru a potřebě synchronizace elektronového paprsku bylo maximální podporované rozlišení rovno 352×240 pixelům. Toto rozlišení, které se v literatuře nazývá celkem trefně overscan, se kvůli kompatibilitě s kompozitními monitory a televizními obrazovkami snižovalo na standardních 320×200 pixelů resp. 320×192 pixelů. V režimu overscan totiž paprsek okrajové části obrazu kreslil „za roh“ obrazovky, což bylo krásně viditelné například na elektronkových televizorech Salermo, které měly přední část obrazovky o cca 2cm vysunutou z šasi přístroje.
Grafické režimy čipu ANTIC
V následující tabulce jsou pro ilustraci některých možností čipu ANTIC uvedeny vybrané základní režimy, které je možné nastavit pro každý obrazový řádek zvlášť. V prvním sloupci je číslo (kód) instrukce pro čip ANTIC, ve sloupci druhém pak číslo, které se předává příkazu GRAPHICS v Basicu při nastavování textového či grafického režimu.
| Instrukce ANTIC | GRAPHICS x | Režim | Rozlišení | Počet barev | Poznámka |
|---|---|---|---|---|---|
| 2 | 0 | textový | 40 znaků na řádek | 2 | standardní textový režim |
| 3 | × | textový | 40 znaků na řádek | 2 | textový režim s vyššími znaky |
| 4 | 12 | textový | 40 znaků na řádek | 5 | výška znaku 8 pixelů, 4 barvy/znak |
| 5 | 13 | textový | 40 znaků na řádek | 5 | výška znaku 16 pixelů, 4 barvy/znak |
| 6 | 1 | textový | 20 znaků na řádek | 5 | výška znaku 8 pixelů |
| 7 | 2 | textový | 20 znaků na řádek | 5 | výška znaku 16 pixelů |
| 8 | 3 | grafický | 40 pixelů na řádek | 4 | neznám žádnou aplikaci, která by tento režim používala |
| 9 | 4 | grafický | 80 pixelů na řádek | 2 | |
| A | 5 | grafický | 80 pixelů na řádek | 4 | |
| B | 6 | grafický | 160 pixelů na řádek | 2 | výška pixelu dva obrazové řádky, vertikální rozlišení cca 96 pixelů |
| C | 14 | grafický | 160 pixelů na řádek | 2 | výška pixelu jeden obrazový řádek, vertikální rozlišení cca 192 pixelů |
| D | 7 | grafický | 160 pixelů na řádek | 4 | výška pixelu dva obrazové řádky, vertikální rozlišení cca 96 pixelů |
| E | 15 | grafický | 160 pixelů na řádek | 4 | výška pixelu jeden obrazový řádek, vertikální rozlišení cca 192 pixelů |
| F | 8 | grafický | 320 pixelů na řádek | 2 | nejvyšší možné standardní rozlišení |
Povšimněte si, že textový režim číslo 3 není přímo z Basicu přístupný. Jedná se o režim s vyššími znaky, který lze s výhodou využít například v textových editorech používajících font s „nabodeníčky“. Pokud je tedy požadováno, aby se zobrazil textový režim se 40 znaky na řádek a 24 textovými řádky, musí display-list řídicí čip ANTIC obsahovat mj. i 24 instrukcí s kódem 2 (podrobnosti si řekneme příště). Ve skutečnosti je však display-list poněkud složitější, protože obsahuje i instrukci pro vykreslení horního jednobarevného okraje, instrukci pro nastavení začátku obrazové paměti (adresu prvního znaku na obrazovce) a na samotném konci display-listu taktéž instrukci pro skok na jeho začátek.
Obrázek 6: Hra Amaroute běžící v monochromatickém režimu s nejvyšším rozlišením 320x192 pixelů. Povšimněte si korektního skrývání vzdálených objektů; pravděpodobně se používá optimalizovaná podoba malířova algoritmu.
GTIA – sprity a vícebarevná grafika
S obvodem ANTIC úzce spolupracoval číslicový obvod GTIA (Graphics Television Interface Adapter nebo také George's Television Interface Adapter), který zaváděl podporu pro další tři grafické režimy s horizontálním rozlišením 80 pixelů na řádek a především umožňoval vykreslování spritů (ovšem mluvit o nových grafických režimech je poněkud zavádějící; podrobnější vysvětlení bude opět uvedeno jindy). Současně bylo možné vykreslit čtyři sprity s rozlišením maximálně 8×256 pixelů a další čtyři sprity s rozlišením 2×256 pixelů, které bylo možno spojit do jednoho většího spritu s rozlišením 8×256 pixelů.
Sprity široké 8 pixelů se v literatuře nazývají hráči (players), úzké dvoupixelové sprity se jmenují střely (missiles). Proto se ve světě osmibitových Atari celý koncept spritů nazývá PMG neboli player-missile graphics.
Sprity byly jednobarevné. Více barev bylo možno dosáhnout logickými operacemi nad překrývajícími se sprity (počítače Commodore C64 ovšem nabízely i sprity v režimu multicolor). Každý sprite mohl pomocí jedné instrukce měnit svoji horizontální velikost i horizontální pozici, přičemž polohy spritů byly navzájem nezávislé.
Vertikální pozice spritů se měnila přesunem bitmapy spritu v operační paměti. Bylo také možné definovat priority vykreslování spritů vůči sobě navzájem i vůči pozadí.
Priorita spritů a detekce kolize spritů
Čip GTIA podporoval i změnu priority jednotlivých spritů (jejich překrývání) a bylo také možné detekovat kolizi spritu s jiným spritem popř. s nějakou barvou hracího pole. To stejné samozřejmě platí i pro střely, u nichž byla možná detekce kolize s hráčem či kolize s hracím polem. Při kolizi (do úvahy se samozřejmě braly pouze viditelné pixely spritu, tj. pixely nastavené na logickou jedničku) se nastavil příslušný bit ve stavových registrech, odkud bylo možné kdykoli poté zjistit, zda ke kolizi došlo či nikoli. Díky této funkcionalitě bylo možné velmi snadno otestovat například náraz hráče do stěny, zásah hráče střelou atd.
Obrázek 7: Hra Adventure 2 pro počítače Atari 5200. Jedná se o hry z 21. století vytvořenou v domácích podmínkách, která se snaží zachovat prvky z původní hry Adventure pro Atari 2600 (viz tvar hráče - čtverečku).
Vzhledem k tomu, že sprity byly pouze jednobarevné, museli se vícebarevní hráči sestavovat z několika spritů. Omezení počtu spritů naproti tomu nebylo kritické, neboť jeden sprite mohl být ve skutečnosti použitý pro zobrazení většího množství objektů ve scéně – jediným omezením bylo to, že tyto objekty nesměly ležet na stejném obrazovém řádku.
Obrázek 8: Další screenshot ze hry Adventure 2.
Příloha A: Seznam všech doposud popsaných instrukcí mikroprocesoru MOS 6502
Všechny doposud popsané instrukce mikroprocesoru MOS 6502 jsou vypsány v následující tabulce:
| # | Instrukce | Plné jméno | Popis |
|---|---|---|---|
| 1 | ADC | add with carry | součet hodnoty s akumulátorem (včetně přetečení) |
| 2 | SBC | subtract with carry | odečtení hodnoty od akumulátoru (včetně výpůjčky) |
| 3 | AND | and with accumulator | logické AND s akumulátorem |
| 4 | ORA | or with accumulator | logické OR s akumulátorem |
| 5 | EOR | exclusive or with accumulator | logické XOR s akumulátorem |
| 6 | INC | increment | zvýšení hodnoty o 1 (kupodivu nelze s akumulátorem, ovšem s pamětí ano) |
| 7 | INX | increment X | zvýšení hodnoty index registru X o 1 |
| 8 | INY | increment Y | zvýšení hodnoty index registru Y o 1 |
| 9 | DEC | decrement | snížení hodnoty o 1 (opět nelze s akumulátorem) |
| 10 | DEX | decrement X | snížení hodnoty index registru X o 1 |
| 11 | DEY | decrement Y | snížení hodnoty index registru Y o 1 |
| 12 | CMP | compare with accumulator | odečtení hodnoty od akumulátoru bez zápisu výsledku |
| 13 | CPX | compare with X | odečtení hodnoty od index registru X bez zápisu výsledku |
| 14 | CPY | compare with Y | odečtení hodnoty od index registru Y bez zápisu výsledku |
| 15 | BIT | bit test | logické AND bez uložení výsledků (změní se jen příznakové bity) |
| 16 | ASL | arithmetic shift left | aritmetický posun doleva o jeden bit |
| 17 | LSR | logical shift right | logický posun doprava o jeden bit |
| 18 | ROL | rotate left | rotace doleva o jeden bit |
| 19 | ROR | rotate right | rotace doprava o jeden bit |
| 20 | JMP | jump | skok (existuje několik adresovacích režimů) |
| 21 | JSR | jump to subroutine | skok do podprogramu s uložením návratové adresy na zásobník |
| 22 | RTS | return from subroutine | návrat z podprogramu |
| 23 | RTI | return from interrupt | návrat z prerušovací rutiny |
| 24 | BCC | branch on carry clear | rozvětvení za podmínky C==0 |
| 25 | BCS | branch on carry set | rozvětvení za podmínky C==1 |
| 26 | BEQ | branch on equal (zero set) | rozvětvení za podmínky Z==1 |
| 27 | BMI | branch on minus (negative set) | rozvětvení za podmínky N==1 |
| 28 | BNE | branch on not equal (zero clear) | rozvětvení za podmínky Z==0 |
| 29 | BPL | branch on plus (negative clear) | rozvětvení za podmínky N==0 |
| 30 | BVC | branch on overflow clear | rozvětvení za podmínky O==0 |
| 31 | BVS | branch on overflow set | rozvětvení za podmínky O==1 |
| 32 | PHA | push accumulator on stack | uložení obsahu akumulátoru na zásobník |
| 33 | PLA | pull accumulator from stack | obnovení obsahu akumulátoru ze zásobníku |
Příloha B: Makefile pro překlad všech demonstračních příkladů
Všechny minule i dnes popsané demonstrační příklady, pro jejichž překlad je zapotřebí použít assembler ca65 a linker ld65, je možné přeložit s využitím souboru Makefile, jehož obsah je vypsán pod tímto odstavcem:
execs := dummy.xex print_a.xex \
background_color_1.xex background_color_2.xex \
color_computation_1.xex color_computation_2.xex \
subroutine_1.xex subroutine_2.xex \
hex_number_1.xex hex_number_2.xex \
hex_number_3.xex hex_number_4.xex \
hex_number_5.xex hex_number_6.xex \
hex_number_7.xex hex_number_8.xex \
hex_number_9.xex \
fill_block_1.xex fill_block_2.xex \
fill_block_3.xex fill_block_4.xex \
fill_block_5.xex
all: $(execs)
clean:
rm -f *.o
rm -f *.xex
.PHONY: all clean
%.o: %.asm
ca65 $< -t atari -o $@ -l $(basename $<)_list.asm --list-bytes 100
%.xex: %.o
ld65 -C linker.cfg $< -o $@ -m $(basename $<).map
Výsledkem překladu jsou soubory s koncovkou .xex, které je možné přímo spustit v emulátoru osmibitových počítačů Atari.
Repositář s demonstračními příklady
Všechny demonstrační příklady, s nimiž jsme se v předchozích článcích i v článku dnešním seznámili a které jsou určeny pro překlad s využitím assembleru ca65, jsou dostupné, jak je zvykem, na GitHubu. V tabulce níže jsou uvedeny odkazy na jednotlivé zdrojové kódy příkladů psané v assembleru i „listingy“ vygenerované samotným assemblerem, ze kterých je patrné, jakým způsobem se jednotlivé příklady přeložily do výsledného XEX souboru:
Odkazy na Internetu
- MOS 6502 instruction set
http://www.6502.org/users/obelisk/6502/instructions.html - EXE File Format Description
https://gury.atari8.info/refs/file_formats_exe.php - XEX Filter – A toolkit to analyze and manipulate Atari binary files
https://www.vitoco.cl/atari/xex-filter/index.html - chkxex.py
https://raw.githubusercontent.com/seban-slt/tcx_tools/refs/heads/master/chkxex.py - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - Překladače jazyka C pro historické osmibitové mikroprocesory
https://www.root.cz/clanky/prekladace-jazyka-c-pro-historicke-osmibitove-mikroprocesory/ - Překladače programovacího jazyka C pro historické osmibitové mikroprocesory (2)
https://www.root.cz/clanky/prekladace-programovaciho-jazyka-c-pro-historicke-osmibitove-mikroprocesory-2/ - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - Minimal NES example using ca65
https://github.com/bbbradsmith/NES-ca65-example - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - Adventures with ca65
https://atariage.com/forums/topic/312451-adventures-with-ca65/ - example ca65 startup code
https://atariage.com/forums/topic/209776-example-ca65-startup-code/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - How To Start Learning Atari 8 Bit Assembly For Free
https://forums.atariage.com/topic/300732-how-to-start-learning-atari-8-bit-assembly-for-free/ - WUDSN (Demo Group)
https://www.wudsn.com/ - Machine Language For Beginners
https://www.atariarchives.org/mlb/ - Assembly language: all about I/O
https://www.atarimagazines.com/v3n8/AllAbout_IO.html - Sedmdesátiny assemblerů: lidsky čitelný strojový kód
https://www.root.cz/clanky/sedmdesatiny-assembleru-lidsky-citelny-strojovy-kod/ - Color names
https://atariwiki.org/wiki/Wiki.jsp?page=Color%20names - ATASCII
https://en.wikipedia.org/wiki/ATASCII - Put characters in display ram isn't ATASCII?
https://forums.atariage.com/topic/359973-put-characters-in-display-ram-isnt-atascii/ - ATASCII And Internal Character Code Values
https://www.atariarchives.org/mapping/appendix10.php - Reading ATASCII from the keyboard in assembly
https://forums.atariage.com/topic/361733-reading-atascii-from-the-keyboard-in-assembly/ - Why does the 6502 JSR instruction only increment the return address by 2 bytes?
https://retrocomputing.stackexchange.com/questions/19543/why-does-the-6502-jsr-instruction-only-increment-the-return-address-by-2-bytes - Pushing return address to stack off by 1 byte
https://forums.atariage.com/topic/378206-pushing-return-address-to-stack-off-by-1-byte/ - Intel x86 documentation has more pages than the 6502 has transistors
https://www.righto.com/2013/09/intel-x86-documentation-has-more-pages.html - Clearing a Section of Memory
http://www.6502.org/source/general/clearmem.htm - Practical Memory Move Routines by Bruce Clark
http://www.6502.org/source/general/memory_move.html - 6502 Assembly Programming Guide
https://neumont-gamedev.github.io/posts/retrogamedev-6502-guide/ - Off-by-one error
https://en.wikipedia.org/wiki/Off-by-one_error - 6502 cycle times
https://www.nesdev.org/wiki/6502_cycle_times
