Obsah
1. Základy tvorby her pro herní konzoli NES: mikroprocesor 6502 a assembler ca65
4. Realizace obsluhy přerušení pro všechny tři typy přerušení procesoru 6502
5. Programátorský model mikroprocesoru MOS 6502
6. Registry a příznakové bity mikroprocesoru MOS 6502
7. Adresovací režimy, využití registrů X a Y pro adresování
8. Aritmetické a logické instrukce
11. Manipulace s příznakovými bity
13. Kde si lze možnosti 6502 otestovat bez nutnosti pochopit strukturu NESu?
14. Druhý pohled na kostru programu
15. Nastavení stavu mikroprocesoru po resetu
16. Nastavení řídicích registrů
17. Trik pro čekání na zobrazení dalšího snímku
18. Trik pro vymazání obsahu RAM
19. Repositář s demonstračními příklady
1. Základy tvorby her pro herní konzoli NES: mikroprocesor 6502 a assembler ca65
Na úvodní článek o vývoji her nebo grafických a zvukových dem pro slavnou osmibitovou herní konzoli NES dnes navážeme. Podrobněji si popíšeme dvě technologie, které je nutné alespoň do určité míry znát (prozatím nám však budou stačit skutečně jen základní znalosti). Konkrétně se jedná o osmibitový mikroprocesor MOS 6502 a taktéž o assembler, v našem případě konkrétně o assembler nazvaný ca65. Připomeňme si, že pro vývoj pro NES nám bude postačovat jen základní sada nástrojů (a nijak rozsáhlých či komplikovaných):
- Libovolný programátorský textový editor
- Assembler ca65
- Linker cl65
- Libovolný emulátor NESu (podporující formát INES – což dnes umí každý emulátor)
V závěru úvodního článku jsme si ukázali, jak vypadá kostra programu určeného pro NES zapsaná v assembleru. Jedná se o nepatrně delší kód, a to z toho prostého důvodu, že NES neobsahuje žádný operační systém ani BIOS, takže se programátor musí nejprve zabývat inicializací hardware – tedy především čipu PPU (grafika).
Celý kód, který jehož význam si postupně vysvětlíme (a vylepšíme), vypadá následovně:
; --------------------------------------------------------------------- ; Definice hlavičky obrazu ROM ; --------------------------------------------------------------------- ; Size of PRG in units of 16 KiB. prg_npage = 1 ; Size of CHR in units of 8 KiB. chr_npage = 1 ; INES mapper number. mapper = 0 ; Mirroring (0 = horizontal, 1 = vertical) mirroring = 1 .segment "INES" .byte $4e, $45, $53, $1a .byte prg_npage .byte chr_npage .byte ((mapper & $0f) << 4) | (mirroring & 1) .byte mapper & $f0 .code ; --------------------------------------------------------------------- ; Blok paměti s definicí dlaždic 8x8 pixelů ; --------------------------------------------------------------------- .segment "CHR0a" .segment "CHR0b" ; --------------------------------------------------------------------- ; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU ; ; viz též https://www.pagetable.com/?p=410 ; --------------------------------------------------------------------- ; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank) .proc nmi rti ; návrat z přerušení .endproc ; Obslužná rutina pro IRQ (maskovatelné přerušení) .proc irq rti ; návrat z přerušení .endproc ; Obslužná rutina pro RESET .proc reset ; nastavení stavu CPU sei ; zákaz přerušení cld ; vypnutí dekadického režimu (není podporován) ldx #$ff txs ; vrchol zásobníku nastaven na 0xff (první stránka) ; nastavení řídicích registrů ldx #$00 stx $2000 ; nastavení PPUCTRL = 0 stx $2001 ; nastavení PPUMASK = 0 stx $4015 ; nastavení APUSTATUS = 0 ; čekání na vnitřní inicializaci PPU (dva snímky) wait1: bit $2002 ; test obsahu registru PPUSTATUS bpl wait1 ; skok, pokud je příznak N nulový wait2: bit $2002 ; test obsahu registru PPUSTATUS bpl wait2 ; skok, pokud je příznak N nulový ; vymazání obsahu RAM lda #$00 ; vynulování registru A loop: sta $000, x ; vynulování X-tého bajtu v nulté stránce sta $100, x sta $200, x sta $300, x sta $400, x sta $500, x sta $600, x sta $700, x ; vynulování X-tého bajtu v sedmé stránce inx ; přechod na další bajt bne loop ; po přetečení 0xff -> 0x00 konec smyčky ; čekání na dokončení dalšího snímku, potom může začít herní smyčka wait3: bit $2002 ; test obsahu registru PPUSTATUS bpl wait3 ; skok, pokud je příznak N nulový ; vlastní herní smyčka je prozatím prázdná game_loop: jmp game_loop ; nekonečná smyčka (později rozšíříme) .endproc ; --------------------------------------------------------------------- ; Tabulka vektorů CPU ; --------------------------------------------------------------------- .segment "VECTOR" .addr nmi .addr reset .addr irq ; --------------------------------------------------------------------- ; Finito ; ---------------------------------------------------------------------
Důležitá je i konfigurace linkeru, tedy (zjednodušeně řečeno) mapování logických jmen segmentů na fyzické adresy a určení, které segmenty jsou zapisovatelné a které nikoli (tedy zda se jedná o RAM či o ROM):
MEMORY { ZP: start = $0000, size = $0100, type = rw; RAM: start = $0300, size = $0400, type = rw; HEADER: start = $0000, size = $0010, type = rw, file = %O, fill = yes; PRG0: start = $8000, size = $4000, type = ro, file = %O, fill = yes; CHR0a: start = $0000, size = $1000, type = ro, file = %O, fill = yes; CHR0b: start = $1000, size = $1000, type = ro, file = %O, fill = yes; } SEGMENTS { ZEROPAGE: load = ZP, type = zp; BSS: load = RAM, type = bss; HEADER: load = HEADER, type = ro, align = $10; CODE: load = PRG0, type = ro; VECTORS: load = PRG0, type = ro, start = $BFFA; CHR0a: load = CHR0a, type = ro; CHR0b: load = CHR0b, type = ro; }
2. Hlavička obrazu ROM
Teoreticky je možné, aby assembler vygeneroval přesnou kopii paměti NESu. Tj. výsledkem by byl binární soubor o velikosti přesně 65536 bajtů obsahující jak RAM a ROM, tak i všechny řídicí registry (a opakující se bloky paměti). Ve skutečnosti se však dnes používá poměrně flexibilní formát INES, který obsahuje libovolné bloky paměti a jeho velikost je tak proměnlivá. Navíc podporuje mapování dalších paměťových bloků pro hry s větší ROM. Důležité pro nás je, že tento formát obsahuje hlavičku s důležitými informacemi, kterou musíme naplnit. Tato hlavička bude v assembleru reprezentována obsahem segmentu nazvaného „INES“ a obsahuje velikost ROM pro program, velikost ROM pro dlaždice (grafiku) atd.:
; --------------------------------------------------------------------- ; Definice hlavičky obrazu ROM ; --------------------------------------------------------------------- ; Size of PRG in units of 16 KiB. prg_npage = 1 ; Size of CHR in units of 8 KiB. chr_npage = 1 ; INES mapper number. mapper = 0 ; Mirroring (0 = horizontal, 1 = vertical) mirroring = 1 .segment "INES" .byte $4e, $45, $53, $1a .byte prg_npage .byte chr_npage .byte ((mapper & $0f) << 4) | (mirroring & 1) .byte mapper & $f0 .code
Po překladu příkladu (viz úvodní článek) získáme binární soubor nazvaný ctnes.nes, jehož obsah si můžeme prohlédnout libovolným hexa prohlížečem. Standardním hexa prohlížečem na Linuxu je od (nenechte se zmást jeho jménem, které klame – jedná se i o hexa prohlížeč):
$ od -t x1 ctnes.nes
Důležitých je prvních šestnáct bajtů, které skutečně obsahují kýženou hlavičku:
0000000 4e 45 53 1a 01 01 01 00 00 00 00 00 00 00 00 00 0000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 * 0040000 00 00 00 00 00 00 00 00 00 00 00 10 02 10 01 10 0040020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 * 0050020 40 40 78 d8 a2 ff 9a a2 00 8e 00 20 8e 01 20 8e 0050040 15 40 2c 02 20 10 fb 2c 02 20 10 fb a9 00 95 00 0050060 9d 00 01 9d 00 02 9d 00 03 9d 00 04 9d 00 05 9d 0050100 00 06 9d 00 07 e8 d0 e6 2c 02 20 10 fb 4c 3d 10 0050120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 *
3. Mapa paměti NESu
Pro vývoj pro NES je vhodné seznámit s mapou paměti, přesněji řečeno obsazení adres tak, jak je vidí mikroprocesor:
+-----------------------+ $FFFF | Vektory NMI/RESET/IRQ | +-----------------------+ $FFFA | | | | | Cartridge ROM (PRG) | | 32kB | | | | | +-----------------------+ $8000 | | | Cartridge RAM (WRAM) | | 8kB (+expansion ROM) | | | +-----------------------+ $4020 | Řídicí registry APU | | 20B (jen 20 bajtů) | +-----------------------+ $4000 | Řídicí registry PPU | | 8B (jen osm bajtů) | +-----------------------+ $2000 | | | | | (zrcadlená RAM) | | | | | +-----------------------+ $0800 | Interní paměť RAM | | 2kB | |.......................| $0200 | Stack 256B | |.......................| $0100 | Zero Page 256B | +-----------------------+ $0000
V nejvyšších šesti bajtech jsou uloženy vektory (adresy) přerušovacích subrutin volaných automaticky při detekci přerušení či při resetu (inicializaci) CPU:
$FFFA-$FFFB = NMI vector $FFFC-$FFFD = Reset vector $FFFE-$FFFF = IRQ/BRK vector
4. Realizace obsluhy přerušení pro všechny tři typy přerušení procesoru 6502
Mikroprocesor MOS 6502 podporuje tři typy přerušení:
- NMI (nemaskovatelné hardwarové přerušení)
- RESET (reset mikroprocesoru)
- IRQ (maskovatelné přerušení nebo instrukce BRK)
Vzhledem k tomu, že v NESu není žádný operační systém ani BIOS, musíme obsluhu přerušení realizovat vlastními prostředky. Vektory přerušení, tj. adresy subrutin (podprogramů) pro obsluhu přerušení, jsou uloženy na šesti nejvyšších adresách paměti, což pro nás v assembleru znamená obsah (mini)segmentu „VECTOR“:
; --------------------------------------------------------------------- ; Tabulka vektorů CPU ; --------------------------------------------------------------------- .segment "VECTOR" .addr nmi .addr reset .addr irq
Tento segment skutečně obsahuje jen trojici adres, konkrétně adres subrutin. Subrutina pro NMI bude prozatím obsahovat jen instrukci RTI znamenající návrat z přerušení:
; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank) .proc nmi rti ; návrat z přerušení .endproc
Podobně jednoduchá bude obsluha IRQ – opět se vyvolá jen jediná instrukce RTI:
; Obslužná rutina pro IRQ (maskovatelné přerušení) .proc irq rti ; návrat z přerušení .endproc
Ovšem složitější již bude obslužná rutina přerušení RESET. Proč tomu tak je? Tuto rutinu zavolá mikroprocesor po skutečném RESETu, ovšem i po zapnutí herní konzole. Je to tedy vstupní bod do naší hry či dema a můžeme v něm realizovat všechny inicializace a dokonce i spustit hlavní herní smyčku (viz další text):
; Obslužná rutina pro RESET .proc reset ... ... ... ; vlastní herní smyčka je prozatím prázdná game_loop: jmp game_loop ; nekonečná smyčka (později rozšíříme) .endproc
5. Programátorský model mikroprocesoru MOS 6502
Z programátorského hlediska se mikroprocesor MOS 6502 dosti podstatným způsobem odlišuje jak od konkurenčního Intelu 8080 (i od později vydaného Zilogu Z80), tak i od čipu RCA-1802. Zatímco procesor Intel 8080 obsahoval poměrně rozsáhlou sadu obecně použitelných osmibitových registrů (A, B, C, D, E, H a L), které se u některých instrukcí kombinovaly do 16bitových registrových párů, měl 6502 pouze jeden osmibitový akumulátor (registr A) a dva taktéž osmibitové index-registry pojmenované X a Y. Oba zmíněné typy procesorů samozřejmě obsahovaly další speciální registry, jako ukazatel na vrchol zásobníku (SP), programový čítač (PC) a příznakový registr (F).
Na první pohled by se mohlo zdát, že počet registrů mikroprocesoru MOS 6502 je zcela nedostatečný pro provádění většiny aritmetických či logických operací. Ve skutečnosti tomu tak není, protože tento procesor podporuje načtení druhého operandu z operační paměti (rychlost RAM nebyla tak limitujícím faktorem, jako je tomu dnes – ve skutečnosti byl přístup do RAM dvojnásobně rychlý v porovnání s mikroprocesorem). U mnoha instrukcí je podporován větší počet adresovacích režimů, celkově je možné operandy strojových instrukcí adresovat třinácti navzájem odlišnými způsoby. Při adresování se často používají oba index-registry, které je možné inkrementovat a dekrementovat – tím je umožněno provádění blokových přenosů dat, mazání souvislé oblasti paměti atd.
Mikroprocesor MOS 6502 také zavádí pojem takzvané nulté stránky paměti, která byla důsledně využita v instrukční sadě. Jedná se o prvních 256 bytů operační paměti, kterou je možné adresovat zjednodušeným způsobem. Adresa libovolné buňky z nulté stránky paměti je totiž uložena na jednom byte v operačním kódu instrukce, takže celá instrukce může být kratší (typicky pouze dva byte). Současně je i provádění instrukcí adresujících nultou stránku paměti rychlejší než při šestnáctibitovém adresování (například se provádí pouze osmibitové sčítání atd.). Z tohoto důvodu se můžeme na nultou stránku paměti dívat jako na pole 256 registrů, resp. alternativně na 128 plnohodnotně využitelných 16bitových ukazatelů (musíme si opět uvědomit, že operační paměti byly v té době stejně rychlé jako procesor, takže čtení či zápis dat do paměti byla záležitost jednoho či dvou cyklů). Myšlenka nulté stránky paměti byla dále rozšířena v procesoru Motorola 6809, kde se však tato stránka dala v adresovatelné paměti posouvat na libovolné místo, podobně jako v pokračovateli 6502 – 16bitovém čipu 65816 (použit například v herní konzoli SNES).
6. Registry a příznakové bity mikroprocesoru MOS 6502
„The 65×x processors are not ruined with too many registers.“
V předchozí kapitole jsme si řekli, že MOS 6502 obsahoval pouze minimální, ovšem ještě prakticky použitelný počet registrů. Všechny tyto registry, a to jak registry pracovní, tak i speciální, jsou vypsány v následující tabulce:
# | Registr | Šířka | Význam |
---|---|---|---|
1 | A | 8 bitů | akumulátor (pracovní registr) |
2 | X | 8 bitů | index registr |
3 | Y | 8 bitů | index registr |
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 |
Většina aritmetických a logických operací používala jako jeden z operandů akumulátor, tedy registr A. Druhý operand byl typicky načítán z operační paměti. Přitom se pro adresování velmi často používaly index registry X a Y. Ukazatel na vrchol zásobníku SP (či jen S) dokázal adresovat zásobník v rozsahu $0100 až $01FF, tedy 256 bajtů (takzvanou první stránku paměti). A příznakový registr P měl obsazen jen sedm bitů:
Bit | Zkratka | Označení | Význam |
---|---|---|---|
7 | N | Negative | záporný výsledek předchozí operace (popř. nastavený sedmý bit) |
6 | V | oVerflow | přetečení do sedmého bitu (popř. nastavený šestý bit) |
5 | – | – | neobsazeno |
4 | B | Break | rozlišení přerušení od instrukce BRK či PHP |
3 | D | Decimal | režim výpočtů: binární versus BCD (decimální) |
2 | I | Interrupt | zákaz přerušení |
1 | Z | Zero | nulový výsledek předchozí operace |
0 | C | Carry | přenos při předchozí operaci |
7. Adresovací režimy, využití registrů X a Y pro adresování
Adresovací režimy odlišují osmibitový mikroprocesor MOS 6502 od naprosté většiny ostatních mikroprocesorů a umožňují použít (zcela) odlišný styl programování založený na efektivním použití nulté stránky paměti a obou index registrů X i Y. Existuje celkem třináct adresovacích režimů, ovšem již na tomto místě je nutné podotknout, že žádná instrukce nevyužívá všechny tyto režimy. Některé adresovací režimy jsou určeny pouze pro skoky, další pro implicitní operandy atd.:
# | Zápis | Název | Assembler | Stručný popis |
---|---|---|---|---|
1 | A | accumulator | INS A | operandem je přímo akumulátor (instrukce tedy nemá žádný explicitně zapsaný operand) |
2 | abs | absolute | INS $HHLL | za instrukcí následuje šestnáctibitová adresa, na níž je operand uložen |
3 | abs,X | absolute, X-indexed | INS $HHLL,X | za instrukcí následuje šestnáctibitová adresa, která je přičtena k X |
4 | abs,Y | absolute, Y-indexed | INS $HHLL,Y | za instrukcí následuje šestnáctibitová adresa, která je přičtena k Y |
5 | # | immediate | INS #$BB | za instrukcí následuje bajt s osmibitovou konstantou |
6 | impl | implied | INS | operand je odvozen přímo z instrukce, například INX |
7 | ind | indirect | INS ($HHLL) | nepřímá adresace přes adresu uloženou za instrukcí (ta je ukazatelem), nepřímý skok |
8 | X,ind | X-indexed, indirect | INS ($LL,X) | efektivní adresa je spočtena z hodnoty uložené na (LL+X) |
9 | 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 |
10 | rel | relative | INS $BB | použito u relativních skoků; za instrukcí je jeden bajt reprezentující offset se znaménkem |
11 | zpg | zeropage | INS $LL | operand je uložen na nulté stránce na adrese LL |
12 | zpg,X | zeropage, X-indexed | INS $LL,X | operand je uložen na nulté stránce na adrese LL+X |
13 | zpg,Y | zeropage, Y-indexed | INS $LL,Y | operand je uložen na nulté stránce na adrese LL+Y |
Ukažme si nyní zajímavé použití již výše zmíněného režimu číslo 9. Prozatím ovšem nemáme znalosti HW NESu, takže si ukažme, jakým způsobem se na osmibitových počítačích Atari vypíše znak „A“ do levého horního rohu. To je zdánlivě snadné – v textovém režimu prostě na první adresu video paměti zapíšeme kód znaku „A“, což je v ATASCII hodnota 33. Ovšem jak zjistit adresu video paměti, která se odlišuje podle použitého počítače (kapacita RAM může být 16kB, 48kB či 64kB), stavu cartridge atd.? Systém zajišťuje, že tato adresa je uložena v nulté stránce paměti, konkrétně na adresách $88 a $89. Na adresu uloženou na těchto adresách tedy musíme zapsat hodnotu 33. Jedno z možných (plně funkčních) řešení vypadá takto (registr Y je nutné použít, ovšem jeho hodnotu ponecháme na nule, takže adresu neovlivní – Y použijeme při tisku celého řetězce):
.include "atari.inc" .CODE .proc main lda #33 ; ATASCII hodnota znaku "A" ldy #0 ; vynulovat registr Y sta (88),y ; tisk znaku "A" na první místo na obrazovce ; (adresa Video RAM je na adresách 88 a 89) loop: jmp loop end: .endproc .segment "EXEHDR" .word $ffff .word main .word main::end - 1 .segment "AUTOSTRT" .word RUNAD ; definováno v atari.h .word RUNAD+1 .word main
Obrázek 1: Předchozí program po spuštění.
8. Aritmetické a logické instrukce
Mikroprocesor MOS 6502 obsahuje pouze 56 instrukcí, přičemž mnoho instrukcí podporuje větší množství adresovacích režimů a tudíž i více variant (i tak však zdaleka není obsazeno všech 256 možných kombinací – ty byly postupně obsazovány v dalších procesorech, popř. na původním MOS 6502 měly sice oficiálně nedokumentovanou, ovšem logickou/očekávanou funkci – například načtení registru A i X jedinou instrukcí atd.). Nejprve si popíšeme aritmetické a logické instrukce mikroprocesoru MOS 6502. Většina dále popsaných instrukcí jako svůj první operand akceptuje akumulátor a druhým operandem může být konstanta, popř. hodnota načtená z operační paměti s využitím výše popsaných adresovacích režimů. Výjimkou jsou instrukce s jediným operandem, v nichž nemusí vystupovat akumulátor, popř. instrukce, v nichž je přímo operand vyjádřen názvem instrukce (INX atd.):
# | 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á operace AND s akumulátorem |
4 | ORA | or with accumulator | logická operace OR s akumulátorem |
5 | EOR | exclusive or with accumulator | logická operace XOR s akumulátorem |
6 | INC | increment | zvýšení hodnoty o 1 (kupodivu nelze provést přímo 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 provést 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 |
9. Skoky a rozvětvení
Následuje další skupina instrukcí. Konkrétně se jedná o instrukce skoku, popř. skoku a výskoku ze subrutiny (podprogramu). Skákat je možné v rámci celé adresovatelné RAM, tedy v rozsahu plných 64kB:
# | Instrukce | Plné jméno | Popis |
---|---|---|---|
20 | JMP | jump | skok (existuje několik adresovacích režimů této instrukce) |
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 (již jsme tuto instrukci potkali) |
Relativní skoky v rámci rozsahu –128 až 127 jsou provedeny na základě vyhodnocení nějaké podmínky, konkrétně testování zvoleného příznakového bitu. Oproti Motorole 6800, ze které 6502 ideově vychází, byl počet podmíněných skoků snížen na polovinu, takže některé kombinace podmínek neexistují (včetně BRA a BRN):
# | Instrukce | Plné jméno | Popis |
---|---|---|---|
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 |
10. Instrukce pro přesuny dat
Další skupinou instrukcí jsou instrukce určené pro přesuny dat mezi operační pamětí a registry, popř. mezi registry navzájem. Jedná se o poměrně velké množství instrukcí, které jsou zvláštní tím, že instrukce pro načtení dat modifikují příznakové bity N a Z, takže již pouhé načtení hodnoty zjistí, zda se jedná o hodnotu zápornou nebo nulovou (resp. nezápornou či nenulovou):
# | Instrukce | Plné jméno | Popis |
---|---|---|---|
32 | LDA | load accumulator | načtení bajtu do akumulátoru |
33 | LDX | load X | načtení bajtu do registru X |
34 | LDY | load Y | načtení bajtu do registru Y |
35 | STA | store accumulator | uložení hodnoty akumulátoru |
36 | STX | store X | uložení hodnoty registru X |
37 | STY | store Y | uložení hodnoty registru Y |
38 | TAX | transfer accumulator to X | přesun X=A |
39 | TAY | transfer accumulator to Y | přesun Y=A |
40 | TSX | transfer stack pointer to X | přesun X=SP |
41 | TXA | transfer X to accumulator | přesun A=X |
42 | TXS | transfer X to stack pointer | přesun SP=X |
43 | TYA | transfer Y to accumulator | přesun A=Y |
44 | PHA | push accumulator | uložení akumulátoru na zásobník |
45 | PHP | push processor status (SR) | uložení příznaků na zásobník |
46 | PLA | pull accumulator | obnovení akumulátoru ze zásobníku |
47 | PLP | pull processor status (SR) | obnovení příznaků ze zásobníku |
LDA immediate LDA #oper zeropage LDA oper zeropage,X LDA oper,X absolute LDA oper absolute,X LDA oper,X absolute,Y LDA oper,Y (indirect,X) LDA (oper,X) (indirect),Y LDA (oper),Y LDX immediate LDX #oper zeropage LDX oper zeropage,Y LDX oper,Y absolute LDX oper absolute,Y LDX oper,Y LDY immediate LDY #oper zeropage LDY oper zeropage,X LDY oper,X absolute LDY oper absolute,X LDY oper,X
11. Manipulace s příznakovými bity
Několik instrukcí manipuluje přímo s příznakovými bity. Jedná se o následující instrukce:
# | Instrukce | Plné jméno | Operace provedená instrukcí |
---|---|---|---|
48 | CLC | clear carry | nastavení C=0 |
49 | CLD | clear decimal | nastavení D=0 |
50 | CLI | clear interrupt disable | nastavení I=0 |
51 | CLV | clear overflow | nastavení V=0 |
52 | SEC | set carry | nastavení C=1 |
53 | SED | set decimal | nastavení D=1 |
54 | SEI | set interrupt disable | nastavení I=1 |
Mnemotechnika pojmenování těchto instrukcí je jednoduchá – CL znamená clear kdežto SE znamená set.
12. Zbývající instrukce
Zbývá nám popsat už jen dvě instrukce nezařazené do žádné výše uvedené skupiny. Jedná se o tyto instrukce:
# | Instrukce | Plné jméno | Popis |
---|---|---|---|
55 | NOP | no operation | přechod na další instrukci |
56 | BRK | break / interrupt | uložení PC a SR na zásobník, zastavení (návrat do monitoru) |
Instrukce BRK je velmi zvláštní. Samotný operační kód má délku jednoho bajtu, ovšem pokud procesor tuto instrukci vykoná, zapíše na zásobník adresu o další bajt (jedničku) zvětšenou. To znamená, že při návratu z obsluhy přerušení (protože BRK je vlastně softwarové přerušení) se zdá, jakoby BRK měla délku dva bajty. Tento bajt navíc lze použít například pro rozlišení, o které přerušení se jedná atd. A navíc je operační kód instrukce BRK roven $00. Pokud je paměť (až na program) vynulována a provede se skok kamkoli mimo program, dojde ihned k jeho zastavení – což je opět užitečné, zejména pokud je 6502 použit v kritických aplikacích. Mnohé jiné architektury mají operační kód $00 vyhrazen pro NOP, což znamená, že při špatném skoku bude procesor interpretovat tyto NOPy až narazí na nějakou náhodnou instrukci/instrukce, které vykoná (což nechceme).
13. Kde si lze možnosti 6502 otestovat bez nutnosti pochopit strukturu NESu?
Instrukční sadu mikroprocesoru MOS 6502 si lze dokonce odzkoušet na prakticky jakémkoli současném počítači či tabletu, a to bez nutnosti instalace assembleru a/nebo simulátoru (popř. emulátoru některého osmibitového domácího mikropočítače nebo herní konzole). Na stránce http://6502asm.com/ se totiž nachází vydařený simulátor virtuálního počítače vybaveného jednoduchým displejem, klávesnicí a v neposlední řadě právě mikroprocesorem MOS 6502. Tento simulátor, jenž byl naprogramovaný Stianem Sorengem, obsahuje editor (ve skutečnosti se v současné verzi jedná o pouhé textové pole umístěné na HTML stránce), do něhož je možné zapsat program v jazyku symbolických instrukcí a následně tento program přeložit vestavěným assemblerem a poté i spustit. Na adrese $fe se nachází generátor náhodných číel, na adrese $ff pak kód stisknuté klávesy. Jediným výstupním médiem je rastrový obrázek 32×32 pixelů uložený od adresy $200 do $5ff (1024 bajtů), ovšem jen spodní bity obsahují barvu.
Příklad vykreslení svislých pruhů na „obrazovku“:
LDA #0 ; zapisovaná hodnota LDX #0 ; počitadlo a současně i adresa pro zápis LOOP: STA $200,X ; vykreslení do první čtvrtiny obrazovky STA $300,X STA $400,X STA $500,X ; vykreslení do čtvrté čtvrtiny obrazovky ADC #1 ; index vykreslované barvy INX ; přechod na další pixel BNE LOOP ; skonči po zápisu 256 pixelů
14. Druhý pohled na kostru programu
Nyní již máme dostatek informací pro pochopení kostry programu, kterou jsme si uvedli minule. Připomeňme si, že se jedná o plně funkční kód, který ihned po spuštění herní konzole (nebo jejího emulátoru) provede inicializaci mikroprocesoru, následně inicializaci PPU a APU (grafika a zvuky), vymaže operační paměť a spustí nekonečnou (prozatím prázdnou) herní smyčku. Jednotlivé části tohoto kódu jsou vysvětleny v navazujících kapitolách:
; Obslužná rutina pro RESET .proc reset ; nastavení stavu CPU sei ; zákaz přerušení cld ; vypnutí dekadického režimu (není podporován) ldx #$ff txs ; vrchol zásobníku nastaven na 0xff (první stránka) ; nastavení řídicích registrů ldx #$00 stx $2000 ; nastavení PPUCTRL = 0 stx $2001 ; nastavení PPUMASK = 0 stx $4015 ; nastavení APUSTATUS = 0 ; čekání na vnitřní inicializaci PPU (dva snímky) wait1: bit $2002 ; test obsahu registru PPUSTATUS bpl wait1 ; skok, pokud je příznak N nulový wait2: bit $2002 ; test obsahu registru PPUSTATUS bpl wait2 ; skok, pokud je příznak N nulový ; vymazání obsahu RAM lda #$00 ; vynulování registru A loop: sta $000, x ; vynulování X-tého bajtu v nulté stránce sta $100, x sta $200, x sta $300, x sta $400, x sta $500, x sta $600, x sta $700, x ; vynulování X-tého bajtu v sedmé stránce inx ; přechod na další bajt bne loop ; po přetečení 0xff -> 0x00 konec smyčky ; čekání na dokončení dalšího snímku, potom může začít herní smyčka wait3: bit $2002 ; test obsahu registru PPUSTATUS bpl wait3 ; skok, pokud je příznak N nulový ; vlastní herní smyčka je prozatím prázdná game_loop: jmp game_loop ; nekonečná smyčka (později rozšíříme) .endproc
15. Nastavení stavu mikroprocesoru po resetu
Víme již, že jak nastavení hardware, tak i vlastního mikroprocesoru po resetu (a tedy i po spuštění konzole) musíme jako vývojáři zajistit sami. Nejprve se podívejme na způsob nastavení stavu mikroprocesoru. Typicky ihned na začátku zakážeme maskovatelné přerušení instrukcí SEI (Set Interrupt Disable Status) a následně pro jistotu vypneme dekadický režim výpočtů (BCD), jenž stejně není čipem Ricoh 2A03 podporován (a tedy jeho povolení by byla nedefinovaná operace):
; nastavení stavu CPU sei ; zákaz přerušení cld ; vypnutí dekadického režimu (není podporován)
Následuje malý trik, kterým se vlastně vymaže celý zásobník. Nastavíme totiž adresu vrcholu zásobníku na adresu $1ff, přičemž prefix $100 je řešen samotným procesorem (nastavujeme jen spodních osm bitů). Takto ušetříme několik bajtů na zásobníku, protože vlastně zahodíme jak výplňový bajt, tak i adresu návratu ze subrutiny reset (ta končí nekonečnou smyčkou, tudíž z ní nikdy nevyskočíme):
ldx #$ff txs ; vrchol zásobníku nastaven na 0x1ff (první stránka)
16. Nastavení řídicích registrů
Před spuštěním herní smyčky je nutné nastavit i některé řídicí registry PPU (grafika) i APU (hudba a zvuky). Zejména je nutné vynulovat registr PPUCTRL. Zápisem nuly mj. zajistíme, že se nebude generovat NMI (nemaskovatelné přerušení) po každém zobrazeném snímku. Další registr, který vynulujeme, se jmenuje PPUMASK. Zápisem nuly mj. zakážeme zobrazení všech grafických objektů, tj. jak pozadí, tak i spritů. Nulu zapíšeme i do řídicího registru nazvaného SND_CHN neboli plným jménem „Sound channels enable and status“. Ten je (z pohledu mikroprocesoru) namapován na adresu $4015 a zápisem nuly vypneme zvuky:
; nastavení řídicích registrů ldx #$00 stx $2000 ; nastavení PPUCTRL = 0 stx $2001 ; nastavení PPUMASK = 0 stx $4015 ; nastavení APUSTATUS = 0
17. Trik pro čekání na zobrazení dalšího snímku
Před provedením dalších operací je nutné počkat na stabilizaci grafického čipu. Typicky je tato operace vyřešena počkáním na vykreslení dvou snímků. Jak je však možné toto čekání realizovat v praxi? Realizace je založena na opakovaném čtení registru PPUSTATUS, který je mapován na adresu $2002. Při čtení nás bude zajímat nejvyšší (sedmý) bit, jenž je nastaven na jedničku ve chvíli, kdy probíhá takzvaný vertical blank neboli vertikální zatemnění obrazu po vykreslení snímku. Zajímavé je, že po přečtení stavového registru je tento bit automaticky smazán. Algoritmus čekání na snímek tedy bude vypadat takto:
- Přečti obsah sedmého bitu registru PPUSTATUS
- Pokud je obsah nulový, skok na bod 1
- …jsme ve vertikálním zatemnění – snímek byl vykreslen
A při realizaci kroku 1 a 2 využijeme elegantní trik procesoru 6502. Použijeme totiž instrukci BIT, která načte obsah paměti na specifikované adrese a (kromě dalších operací) zkopíruje obsah sedmého bitu z načtené hodnoty do příznakového bitu N (negative). Test na nulovost tedy v tomto případě zajistí instrukce BPL (branch if plus):
wait1: bit $2002 ; test obsahu registru PPUSTATUS bpl wait1 ; skok, pokud je příznak N nulový
18. Trik pro vymazání obsahu RAM
Po zapnutí herní konzole je obecně obsah RAM náhodný. Minimálně pro ladicí účely je vhodné paměť promazat, už jen z toho důvodu, že hodnota $00 znamená operační kód instrukce BRK. Existuje mnoho možností, jak zajistit přepsání bloku paměti konstantní hodnotou a žádný z těchto způsobů není příliš intuitivní. Je tomu tak z toho důvodu, že indexové registry mají šířku jen osmi bitů a případná přímá změna adresy v kódu instrukce není možná, pokud je tento kód uložen v ROM. Ovšem díky tomu, že kapacita RAM u NES je rovna pouze dvěma kilobajtům, což odpovídá osmi stránkám paměti (po 256 bajtech), můžeme „otrocky“ smazat těchto osm stránek například takto:
; vymazání obsahu RAM lda #$00 ; vynulování registru A loop: sta $000, x ; vynulování X-tého bajtu v nulté stránce sta $100, x sta $200, x sta $300, x sta $400, x sta $500, x sta $600, x sta $700, x ; vynulování X-tého bajtu v sedmé stránce inx ; přechod na další bajt bne loop ; po přetečení 0xff -> 0x00 konec smyčky
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v assembleru, které jsou určené pro překlad pomocí ca65, byly uložen do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
# | Příklad | Stručný popis | Adresa |
---|---|---|---|
1 | example01.asm | zdrojový kód příkladu tvořeného kostrou aplikace pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example01.asm |
2 | example02.asm | použití standardní konfigurace linkeru pro konzoli NES | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example02.asm |
3 | example03.asm | symbolická jména řídicích registrů PPU | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example03.asm |
4 | example04.asm | zjednodušený zápis lokálních smyček | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example04.asm |
5 | link.cfg | konfigurace segmentů pro linker ld65 | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/link.cfg |
6 | Makefile | Makefile pro překlad příkladů | https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/Makefile |
20. Odkazy na Internetu
- NesDev.org
https://www.nesdev.org/ - How to Program an NES game in C
https://nesdoug.com/ - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - „Game Development in Eight Bits“ by Kevin Zurawel
https://www.youtube.com/watch?v=TPbroUDHG0s&list=PLcGKfGEEONaBjSfQaSiU9yQsjPxxDQyV8&index=4 - Game Development for the 8-bit NES: A class by Bob Rost
http://bobrost.com/nes/ - Game Development for the 8-bit NES: Lecture Notes
http://bobrost.com/nes/lectures.php - NES Graphics Explained
https://www.youtube.com/watch?v=7Co_8dC2zb8 - NES GAME PROGRAMMING PART 1
https://rpgmaker.net/tutorials/227/?post=240020 - 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/ - History of video game consoles (second generation): Wikipedia
http://en.wikipedia.org/wiki/History_of_video_game_consoles_(second_generation) - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - bee – The Multi-Console Emulator
http://www.thebeehive.ws/ - Nerdy Nights Mirror
https://nerdy-nights.nes.science/ - NES Development Day 1: Creating a ROM
https://www.moria.us/blog/2018/03/nes-development - How to Start Making NES Games
https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/ - 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 - Nocash NES Specs
http://nocash.emubase.de/everynes.htm - Nintendo Entertainment System
http://cs.wikipedia.org/wiki/NES - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - NesDev
http://nesdev.parodius.com/ - 2A03 technical reference
http://nesdev.parodius.com/2A03%20technical%20reference.txt - NES Dev wiki: 2A03
http://wiki.nesdev.com/w/index.php/2A03 - Ricoh 2A03
http://en.wikipedia.org/wiki/Ricoh_2A03 - 2A03 pinouts
http://nesdev.parodius.com/2A03_pinout.txt - 27c3: Reverse Engineering the MOS 6502 CPU (en)
https://www.youtube.com/watch?v=fWqBmmPQP40 - “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/ - Nintendo Entertainment System (NES)
https://8bitworkshop.com/docs/platforms/nes/ - Question about NES vectors and PPU
https://archive.nes.science/nesdev-forums/f10/t4154.xhtml - How do mapper chips actually work?
https://archive.nes.science/nesdev-forums/f9/t13125.xhtml - INES
https://www.nesdev.org/wiki/INES - NES Basics and Our First Game
http://thevirtualmountain.com/nes/2017/03/08/nes-basics-and-our-first-game.html - Where is the reset vector in a .nes file?
https://archive.nes.science/nesdev-forums/f10/t17413.xhtml - CPU memory map
https://www.nesdev.org/wiki/CPU_memory_map - How to make NES music
http://blog.snugsound.com/2008/08/how-to-make-nes-music.html - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - MIDINES
http://www.wayfar.net/0×f00000_overview.php - FamiTracker
http://famitracker.com/ - nerdTracker II
http://nesdev.parodius.com/nt2/ - How NES Graphics work
http://nesdev.parodius.com/nesgfx.txt - NES Technical/Emulation/Development FAQ
http://nesdev.parodius.com/NESTechFAQ.htm - 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 - INES header format
https://github.com/camsaul/nesasm/blob/master/ines_header_format.txt