Obsah
1. Barvy použité při zobrazení spritů
2. Uložení indexu barvové palety v atributech spritů
3. Realizace změny barvové palety spritů při stisku tlačítka A
4. Úplný zdrojový kód prvního demonstračního příkladu
5. Zpomalení změny barvové palety spritů při stisku tlačítka
6. Realizace jednoduchého čítače
7. Úplný zdrojový kód druhého demonstračního příkladu
8. Horizontální a vertikální zrcadlení spritů
9. Makro pro inverzi vybraného bitu či bitů v paměťovém bloku
10. Realizace zrcadlení spritů řízených hráčem
11. Úplný zdrojový kód třetího demonstračního příkladu
12. Celková velikost vygenerovaného strojového kódu
13. Repositář s demonstračními příklady
1. Barvy použité při zobrazení spritů
V první polovině dnešního článku si ukážeme, jakým způsobem je možné modifikovat barvy spritů zobrazených na displeji řízeného osmibitovou herní konzolí NES. Jedná se o relativně komplikovanou techniku, protože barvy všech objektů ve scéně nejsou určeny přímo (například s využitím RGB), ale je zde zvolen odlišný způsob – mapování barev s využitím barvové palety (color table). Ovšem ve skutečnosti je situace ještě nepatrně složitější, protože se nepracuje přímo s indexy do jedné barvové palety, ale je prováděno dvojí mapování, což je sice z pohledu vývojáře složitější řešení, ale počet operací s pamětí se poměrně radikálním způsobem snižuje, stejně jako celkové nároky na kapacitu RAM i ROM (ROM je přitom instalována na cartridgi, jejíž celková cena se nepřímo promítá do ceny každé hry).
Připomeňme si, že barvová paleta používaná v daném okamžiku je uložena v operační paměti od adresy $3f00, tedy konkrétně na konci třetí stránky paměti (každá stránka má 256 bajtů):
PALETTE = $3f00
Celková délka palety je rovna 32 bajtům, přičemž nejhrubší rozdělení je na šestnáct barev pozadí (background) a šestnáct barev spritů:
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
Hodnoty zde uložené jsou indexy do této barvové škály:
Obrázek 1: Barvová paleta používaná herní konzolí NES.
Ve skutečnosti je oněch 32 bajtů rozděleno nikoli do pouhých dvou oblastí, ale do devíti bloků:
| Od | Do | Význam |
|---|---|---|
| 0×3f00 | (jediný bajt) | globální barva pozadí |
| 0×3f01 | 0×3f03 | paleta pozadí #0 |
| 0×3f05 | 0×3f07 | paleta pozadí #1 |
| 0×3f09 | 0×3f0b | paleta pozadí #2 |
| 0×3f0d | 0×3f0f | paleta pozadí #3 |
| 0×3f11 | 0×3f13 | paleta spritů #0 |
| 0×3f15 | 0×3f17 | paleta spritů #1 |
| 0×3f19 | 0×3f1b | paleta spritů #2 |
| 0×3f1d | 0×3f1f | paleta spritů #3 |
Zaměřme se nyní na barvy pixelů v zobrazených spritech. Skutečná barva je vybrána z výše uvedené palety (32 kódů barev), ovšem index do této palety se počítá složitějším způsobem. Konkrétní paleta #0 až #3 je uložena v atributu spritu, konkrétně v dolních dvou bitech atributového bajtu (viz další kapitolu) – to znamená, že můžeme barvovou paletu snadno modifikovat zápisem jediného bajtu do RAM. A barva v rámci této palety je získána ze dvou bitů bitmapy, která popisuje vlastní tvar spritu. Přitom platí, že barva číslo 0 je průhledná, takže sprite v pixelech s touto barvou není vykreslen a prosvítá zde buď jiný sprite nebo pozadí.
Obrázek 2: V tomto editoru spritů je patrné, jak jsou bitmapy spritů uloženy v ROM. Každý pixel může nabývat jedné ze čtyř barev (první barva je přitom při zobrazení na displeji průhledná) a jedná se o nepravé barvy. Konkrétní barva spritu je získána až výběrem určité palety čtyř barev atributovým bajtem (resp. dvěma bity atributového bajtu).
2. Uložení indexu barvové palety v atributech spritů
Ještě jednou se podívejme na to, jaké metainformace o spritech jsou uloženy v operační paměti dostupné mikroprocesoru MOS 6502. Připomeňme si, že pro uložení těchto informací máme rezervovanou celou druhou stránku operační paměti, tj. paměťové buňky s adresami $0200 až $02ff. Celkem je možné do těchto 256 bajtů uložit metainformace o 64 spritech, protože pro každý sprite jsou vyhrazeny čtyři bajty. Nás nyní bude nejvíce zajímat třetí bajt s atributy spritů. Prozatím zobrazujeme jen osm spritů, takže se celkově bude jednat o 8×4=32 bajtů:
; data pro větší množství spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
Třetí bajt z celé čtyřbajtové struktury s metainformacemi o spritu obsahuje jedno bitové pole a tři samostatné bity, které řídí způsob zobrazení daného spritu (a to zcela nezávisle na ostatních spritech):
7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | +-+- Index barvové palety | | | | | | | | | +-+-+----- Nepoužito | | | | | +----------- Priorita (0: před pozadím; 1: za pozadím) | | | +------------- Horizontální zrcadlení spritu | +--------------- Vertikální zrcadlení spritu
Prozatím nás budou zajímat nejnižší dva bity, které určují index do barvové palety – viz též úvodní kapitolu s podrobnějšími informacemi.
Obrázek 3: Sprity zobrazené s využitím první barvové palety.
Obrázek 4: Sprity zobrazené s využitím druhé barvové palety.
Obrázek 5: Sprity zobrazené s využitím třetí barvové palety.
Obrázek 6: Sprity zobrazené s využitím čtvrté barvové palety.
3. Realizace změny barvové palety spritů při stisku tlačítka A
V této kapitole si ukážeme, jakým způsobem lze realizovat změnu barvové palety spritů zobrazených na obrazovce (konkrétně se jedná o osm spritů tvořících figurku Maria), a to konkrétně (opakovaným) stiskem tlačítka A na prvním herním ovladači.
Nejprve je nutné načíst stav všech osmi tlačítek do záchytného registru. To je technika, s níž jsme se již seznámili minule a spočívá v poslání signálu latch (záchyt hodnot) do řídicího registru $4016, jenž je v našich zdrojových kódech pojmenován JOYPAD:
lda #$01
sta JOYPAD1 ; načtení stavu všech osmi tlačítek do záchytného registru
lda #$00
sta JOYPAD1 ; začátek načítání jednotlivých bitů se stavy tlačítek v tomto pořadí:
;
; 1) A
; 2) B
; 3) Select
; 4) Start
; 5) Up
; 6) Down
; 7) Left
; 8) Right
Po provedení této operace je nutné osmkrát přečíst obsah řídicího registru JOYPAD1 a získat tak postupně stav všech osmi tlačítek ovladače. Nás dnes bude zajímat jen přečtení stavu tlačítka A. V případě, že je toto tlačítko stlačeno, provede se kód zapsaný v makru increment_block_mask:
ATTRS = $0202 ; adresa buňky paměti s atributy spritu
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
...
...
...
Makro increment_block_mask zvýší obsah buněk ve vybraném paměťovém bloku, přičemž se po zvýšení hodnoty dané paměťové buňky provede maskování hodnoty bitovou maskou uloženou v parametru mask (my použijeme hodnotu 3, což znamená, že se nastaví jen dva nejnižší bity). Jak již dobře víme z minulého článku, nebudeme pracovat s blokem paměťových buněk uložených ihned za sebou, ale naopak s buňkami, mezi nimiž se nachází tři další bajty, které měnit nechceme. Adresa další paměťové buňky je tedy zvyšována nikoli o jedničku, ale o hodnotu specifikovanou v parametru gap (což je konkrétně hodnota 4):
.macro increment_block_mask address, count, gap, mask
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
lda address, x ; maskování hodnoty
and #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
4. Úplný zdrojový kód prvního demonstračního příkladu
Úplný zdrojový kód dnešního prvního demonstračního příkladu (v pořadí již dvacátého druhého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example22.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example21.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice spritu a zobrazení spritů s rozloženým Mariem.
; Pohyb celého Maria.
; Využití symbolických jmen adres.
; Pomocná makra pro pohyb spritu.
; Změna dalších vlastností spritů s využitím tlačítek A a B
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
; Ovladače
JOYPAD1 = $4016
JOYPAD2 = $4017
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; 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)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: 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 :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
.macro increment_block address, count, gap
ldx #0 ; inicializace okonkrétní ffsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro decrement_block address, count, gap
ldx #0 ; inicializace offsetu
:
dec address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro increment_block_mask address, count, gap, mask
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
lda address, x ; maskování hodnoty
and #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro read_button
lda JOYPAD1 ; stav tlačítka
and #%00000001 ; maskovat všechny bity kromě prvního
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; 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
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
lda #$01
sta JOYPAD1 ; načtení stavu všech osmi tlačítek do záchytného registru
lda #$00
sta JOYPAD1 ; začátek načítání jednotlivých bitů se stavy tlačítek v tomto pořadí:
;
; 1) A
; 2) B
; 3) Select
; 4) Start
; 5) Up
; 6) Down
; 7) Left
; 8) Right
XPOS = $0203 ; adresa buňky paměti s x-ovou souřadnicí spritu
YPOS = $0200 ; adresa buňky paměti y x-ovou souřadnicí spritu
ATTRS = $0202 ; adresa buňky paměti s atributy spritu
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
read_button ; stav tlačítka B jen načteme a ingorujeme
read_button ; stav tlačítka Select jen načteme a ingorujeme
read_button ; stav tlačítka Start jen načteme a ingorujeme
read_button ; stav tlačítka Up
beq up_not_pressed ; není stisknuto? => skok
decrement_block YPOS, 8, 4
up_not_pressed:
read_button ; stav tlačítka Down
beq down_not_pressed ; není stisknuto? => skok
increment_block YPOS, 8, 4
down_not_pressed:
read_button ; stav tlačítka Left
beq left_not_pressed ; není stisknuto? => skok
decrement_block XPOS, 8, 4
left_not_pressed:
read_button ; stav tlačítka Right
beq right_not_pressed ; není stisknuto? => skok
increment_block XPOS, 8, 4
right_not_pressed:
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
setup_cpu
; nastavení řídicích registrů
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; nastavení spritů
jsr load_sprites ; zavolání subrutiny
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování barvové palety
.proc clear_palette
ppu_data_palette_address
ldx #$20 ; počitadlo barev v paletě: 16+16
lda #$00 ; vynulování každé barvy
:
sta PPUDATA ; zápis barvy
dex ; snížení hodnoty počitadla
bne :-
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení spritů
.proc load_sprites
ldx #0
:
lda spritedata,X ; budeme přesouvat data z této oblasti
sta $0200,X ; uložení do paměti spritů
inx ; zvýšení hodnoty počitadla
cpx #32 ; každý sprite má 4 bajty: y-coord, tile, attributy, y-coord * 8 spritů = 32
bne :-
cli ; vynulování bitu I - povolení přerušení
lda #%10000000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00010000 ; povolení zobrazení spritů
sta PPUMASK
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; data pro větší množství spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
5. Zpomalení změny barvové palety spritů při stisku tlačítka
Pokud jste si předchozí demonstrační příklad přeložili a spustili, pravděpodobně jste již přišli na jednu jeho nepříjemnou vlastnost – změna barvové palety spritů totiž probíhá (pokud je pochopitelně tlačítko A stisknuto) velmi rychle. Je tomu tak z toho jednoduchého důvodu, že se test stisku tlačítka a případná změna palety provádí v subrutině VBLANK volané automaticky 50× až 60× za sekundu. To znamená, že i změna vybrané barvové palety probíhá s touto relativně vysokou frekvencí. Toto chování nijak nevadilo ve chvíli, kdy jsme zajišťovali pohyb spritu, protože změna pozice spritu 50×/60× za sekundu znamená, že sprite přejede přes celou obrazovku za přibližně pět sekundu (256/50 resp. 256/60 – lze si snadno a dokonce i relativně přesně ověřit stopkami). Nicméně se vraťme k problematice změny barvové palety. Bylo by ideální, kdyby tato změna probíhala s menší frekvencí, řekněme jen několikrát za sekundu. Toho lze relativně snadno dosáhnout s využitím čítače, který je postupně (s frekvencí 50 resp. 60 Hz) snižován na nulu a teprve při dosažení nuly se provede příslušná změna atributu spritů (a hodnota čítače je obnovena na původní hodnotu).
To znamená, že frekvence změny barvové palety bude přibližně rovna:
50/počáteční_hodnota_čítače
nebo:
60/počáteční_hodnota_čítače
6. Realizace jednoduchého čítače
Nyní se podívejme na způsob realizace čítače zmíněného v předchozí kapitole. Hodnota čítače bude maximálně osmibitová a čítač uložíme do nulté stránky paměti, z níž je možné data číst či zapisovat efektivněji – všechny instrukce pracující s nultou stránkou paměti jsou totiž kratší o jeden bajt a taktéž rychlejší (typicky o jeden strojový cyklus – viz například https://www.masswerk.at/6502/6502_instruction_set.html#LDA):
addressing assembler opc bytes cycles immediate LDA #oper A9 2 2 zeropage LDA oper A5 2 3 zeropage,X LDA oper,X B5 2 4 absolute LDA oper AD 3 4 absolute,X LDA oper,X BD 3 4* absolute,Y LDA oper,Y B9 3 4* (indirect,X) LDA (oper,X) A1 2 6 (indirect),Y LDA (oper),Y B1 2 5*
Definujme tedy adresu, na níž je čítač uložen (poslední bajt nulté stránky):
; Čítač COUNTER = $00ff
Čítač nastavíme na výchozí hodnotu v rutině RESET, která je zavolána automaticky při inicializaci herní konzole:
; Obslužná rutina pro RESET
.proc reset
...
...
...
lda #10 ; inicializace čítače
sta COUNTER
...
...
...
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
A v obslužné rutině VBLANK provedeme tento pseudokód:
if stisknuto(tlačítko_a) {
čítač -= 1
if čítač == 0 {
čítač = výchozí_hodnota
změn_atributy(sprite0..7)
}
}
Výše uvedených sedm řádků pseudokódu lze zapsat do pouhých osmi řádků v assembleru:
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
dec COUNTER ; snížení hodnoty čítače a test na nulu
bne button_a_not_pressed ; čítač != 0? => skok
lda #10 ; nastavení výchozí hodnoty čítače
sta COUNTER
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
7. Úplný zdrojový kód druhého demonstračního příkladu
Úplný zdrojový kód dnešního druhého demonstračního příkladu (v pořadí již dvacátého třetího příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example23.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example23.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice spritu a zobrazení spritů s rozloženým Mariem.
; Pohyb celého Maria.
; Využití symbolických jmen adres.
; Pomocná makra pro pohyb spritu.
; Změna dalších vlastností spritů s využitím tlačítek A a B
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
; Ovladače
JOYPAD1 = $4016
JOYPAD2 = $4017
; Čítač
COUNTER = $00ff
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; 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)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: 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 :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
.macro increment_block address, count, gap
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro decrement_block address, count, gap
ldx #0 ; inicializace offsetu
:
dec address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro increment_block_mask address, count, gap, mask
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
lda address, x ; maskování hodnoty
and #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro read_button
lda JOYPAD1 ; stav tlačítka
and #%00000001 ; maskovat všechny bity kromě prvního
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; 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
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
lda #$01
sta JOYPAD1 ; načtení stavu všech osmi tlačítek do záchytného registru
lda #$00
sta JOYPAD1 ; začátek načítání jednotlivých bitů se stavy tlačítek v tomto pořadí:
;
; 1) A
; 2) B
; 3) Select
; 4) Start
; 5) Up
; 6) Down
; 7) Left
; 8) Right
XPOS = $0203 ; adresa buňky paměti s x-ovou souřadnicí spritu
YPOS = $0200 ; adresa buňky paměti y x-ovou souřadnicí spritu
ATTRS = $0202 ; adresa buňky paměti s atributy spritu
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
dec COUNTER
bne button_a_not_pressed
lda #10
sta COUNTER
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
read_button ; stav tlačítka B jen načteme a ingorujeme
read_button ; stav tlačítka Select jen načteme a ingorujeme
read_button ; stav tlačítka Start jen načteme a ingorujeme
read_button ; stav tlačítka Up
beq up_not_pressed ; není stisknuto? => skok
decrement_block YPOS, 8, 4
up_not_pressed:
read_button ; stav tlačítka Down
beq down_not_pressed ; není stisknuto? => skok
increment_block YPOS, 8, 4
down_not_pressed:
read_button ; stav tlačítka Left
beq left_not_pressed ; není stisknuto? => skok
decrement_block XPOS, 8, 4
left_not_pressed:
read_button ; stav tlačítka Right
beq right_not_pressed ; není stisknuto? => skok
increment_block XPOS, 8, 4
right_not_pressed:
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
setup_cpu
; nastavení řídicích registrů
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; nastavení spritů
jsr load_sprites ; zavolání subrutiny
lda #10 ; inicializace čítače
sta COUNTER
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování barvové palety
.proc clear_palette
ppu_data_palette_address
ldx #$20 ; počitadlo barev v paletě: 16+16
lda #$00 ; vynulování každé barvy
:
sta PPUDATA ; zápis barvy
dex ; snížení hodnoty počitadla
bne :-
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení spritů
.proc load_sprites
ldx #0
:
lda spritedata,X ; budeme přesouvat data z této oblasti
sta $0200,X ; uložení do paměti spritů
inx ; zvýšení hodnoty počitadla
cpx #32 ; každý sprite má 4 bajty: y-coord, tile, attributy, y-coord * 8 spritů = 32
bne :-
cli ; vynulování bitu I - povolení přerušení
lda #%10000000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00010000 ; povolení zobrazení spritů
sta PPUMASK
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; data pro větší množství spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
8. Horizontální a vertikální zrcadlení spritů
Vraťme se nyní k obsahu bajtu, který pro každý sprite zvlášť určuje, jakým způsobem se má sprite zobrazit. Již víme, jakou roli hrají dva nejnižší bity – určují index barvové palety. Další tři bity jsou nevyužity a nejvyšší tři bity určují prioritu (ukážeme si příště), horizontální zrcadlení spritu a vertikální zrcadlení spritu:
7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | +-+- Index barvové palety | | | | | | | | | +-+-+----- Nepoužito | | | | | +----------- Priorita (0: před pozadím; 1: za pozadím) | | | +------------- Horizontální zrcadlení spritu | +--------------- Vertikální zrcadlení spritu
Díky možnosti individuálního zrcadlení spritů je možné, aby se Mario (či další postavy ve hrách) pohybovaly doprava i doleva, a to bez nutnosti mít pro každý směr rezervovány další sprity (tedy „obličej doprava“ i „obličej doleva“). Zrcadlení je možné využít i k dalším trikům, k nimž se vrátíme později. V každém případě se jedná o nenápadnou, ale o to důležitější součást grafického subsystému NESu.
Obrázek 7: Nezrcadlené sprity.
Obrázek 8: Vertikální zrcadlení.
Obrázek 9: Horizontální zrcadlení.
Obrázek 10: Horizontální i vertikální zrcadlení.
9. Makro pro inverzi vybraného bitu či bitů v paměťovém bloku
Pokud budeme chtít zrcadlit (ať již vertikálně či horizontálně) všech osm spritů tvořících postavičku Maria, bude nutné projít všemi osmi atributovými bajty a nastavit nebo invertovat buď sedmý bit nebo bit šestý. K tomuto účelu lze použít instrukci nazvanou EOR neboli exclusive or (známá na jiných platformách jako XOR). Inverzi sedmého (nejvyššího) bitu tak můžeme provést takto:
lda address, x ; maskování hodnoty
eor #%10000000
sta address, x
podobně inverze šestého bitu se provede následovně:
lda address, x ; maskování hodnoty
eor #%01000000
sta address, x
přičemž v address je uložena počáteční adresa metainformací o spritech (tedy konkrétně hodnota $0200) a v registru x offset atributového bajtu.
Můžeme tedy velmi snadno upravit již existující makro increment_block_mask tak, aby se namísto pouhého zvýšení obsahu atributového bajtu (s následným maskováním) jen invertoval jediný bit tohoto atributu. Maska pro inverzi je předána v parametru mask:
.macro flip_bit_block address, count, gap, mask
ldx #0 ; inicializace offsetu
:
lda address, x ; maskování hodnoty
eor #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
Příklad aplikace tohoto makra pro inverzi šestého bitu:
flip_bit_block ATTRS, 8, 4, %01000000
Pro úplnost dodejme seznam parametrů makra:
| Parametr | Význam |
|---|---|
| ATTRS | adresa buňky paměti s atributy prvního spritu ($0202) |
| 8 | celkový počet spritů, jejichž atributy se mají měnit |
| 4 | offset mezi dvěma sousedními atributovými bajty |
| %01000000 | vlastní bitová maska (zapsána binárně) |
10. Realizace zrcadlení spritů řízených hráčem
Vlastní realizace zrcadlení spritů bude jednoduchá. Zrcadlení jsou realizována stiskem tlačítek B a Select. Stisk každého z těchto tlačítek vede k zahájení odpočítávání čítače a při dosažení nuly se neguje nejvyšší bit nebo šestý bit atributového bajtu všech prvních osmi spritů:
read_button ; stisk tlačítka B bude sloužit pro přepínání atributů spritů
beq button_b_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_b_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %01000000
button_b_not_pressed:
read_button ; stisk tlačítka Select bude sloužit pro přepínání atributů spritů
beq button_select_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_select_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %10000000
button_select_not_pressed:
11. Úplný zdrojový kód třetího demonstračního příkladu
Úplný zdrojový kód dnešního třetího demonstračního příkladu (v pořadí již dvacátého čtvrtého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example24.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example24.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice spritu a zobrazení spritů s rozloženým Mariem.
; Pohyb celého Maria.
; Využití symbolických jmen adres.
; Pomocná makra pro pohyb spritu.
; Změna dalších vlastností spritů s využitím tlačítek A a B
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
; Ovladače
JOYPAD1 = $4016
JOYPAD2 = $4017
; Čítače
COUNTER1 = $00fe
COUNTER2 = $00ff
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; 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)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: 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 :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
.macro increment_block address, count, gap
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro decrement_block address, count, gap
ldx #0 ; inicializace offsetu
:
dec address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro increment_block_mask address, count, gap, mask
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
lda address, x ; maskování hodnoty
and #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro flip_bit_block address, count, gap, mask
ldx #0 ; inicializace offsetu
:
lda address, x ; maskování hodnoty
eor #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro read_button
lda JOYPAD1 ; stav tlačítka
and #%00000001 ; maskovat všechny bity kromě prvního
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; 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
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
lda #$01
sta JOYPAD1 ; načtení stavu všech osmi tlačítek do záchytného registru
lda #$00
sta JOYPAD1 ; začátek načítání jednotlivých bitů se stavy tlačítek v tomto pořadí:
;
; 1) A
; 2) B
; 3) Select
; 4) Start
; 5) Up
; 6) Down
; 7) Left
; 8) Right
XPOS = $0203 ; adresa buňky paměti s x-ovou souřadnicí spritu
YPOS = $0200 ; adresa buňky paměti y x-ovou souřadnicí spritu
ATTRS = $0202 ; adresa buňky paměti s atributy spritu
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
dec COUNTER1
bne button_a_not_pressed
lda #10
sta COUNTER1
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
read_button ; stisk tlačítka B bude sloužit pro přepínání atributů spritů
beq button_b_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_b_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %01000000
button_b_not_pressed:
read_button ; stisk tlačítka Select bude sloužit pro přepínání atributů spritů
beq button_select_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_select_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %10000000
button_select_not_pressed:
read_button ; stav tlačítka Start jen načteme a ingorujeme
read_button ; stav tlačítka Up
beq up_not_pressed ; není stisknuto? => skok
decrement_block YPOS, 8, 4
up_not_pressed:
read_button ; stav tlačítka Down
beq down_not_pressed ; není stisknuto? => skok
increment_block YPOS, 8, 4
down_not_pressed:
read_button ; stav tlačítka Left
beq left_not_pressed ; není stisknuto? => skok
decrement_block XPOS, 8, 4
left_not_pressed:
read_button ; stav tlačítka Right
beq right_not_pressed ; není stisknuto? => skok
increment_block XPOS, 8, 4
right_not_pressed:
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
setup_cpu
; nastavení řídicích registrů
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; nastavení spritů
jsr load_sprites ; zavolání subrutiny
lda #10 ; inicializace čítačů
sta COUNTER1
sta COUNTER2
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování barvové palety
.proc clear_palette
ppu_data_palette_address
ldx #$20 ; počitadlo barev v paletě: 16+16
lda #$00 ; vynulování každé barvy
:
sta PPUDATA ; zápis barvy
dex ; snížení hodnoty počitadla
bne :-
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení spritů
.proc load_sprites
ldx #0
:
lda spritedata,X ; budeme přesouvat data z této oblasti
sta $0200,X ; uložení do paměti spritů
inx ; zvýšení hodnoty počitadla
cpx #32 ; každý sprite má 4 bajty: y-coord, tile, attributy, y-coord * 8 spritů = 32
bne :-
cli ; vynulování bitu I - povolení přerušení
lda #%10000000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00010000 ; povolení zobrazení spritů
sta PPUMASK
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; data pro větší množství spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
12. Celková velikost vygenerovaného strojového kódu
Zdrojový kód demonstračních příkladů již překročil 400 řádků, takže (pokud by se jednalo o vyšší programovací jazyk) by bylo vhodné zjistit, kolik ROM jsme vlastně prozatím obsadili (v případě RAM to víme – čtyři paměťové stránky, takže 1kB, z toho naprostá většina místa je prozatím nevyužita). ROM je rozdělena do jednotlivých segmentů, přičemž velikosti segmentů je možné v případě assembleru ca65 zjistit tak, že se při překladu dvakrát použije přepínač -v -v (druhou možností je vyčtení stejných údajů z listingu):
$ ca65 example24.asm -o example24.o -v -v
Assembler v tomto případě vypíše obsah jednotlivých buněk ROM, což nás ovšem nyní nezajímá. Důležitější jsou pro nás informace na řádcích End PC, které udávají adresu poslední zapisované buňky a tedy i velikost segmentu:
New segment: CODE Literal: A9 02 8D 14 40 A9 01 8D 16 40 A9 00 8D 16 40 AD 16 40 Literal: 29 01 F0 1E C6 FE D0 1A A9 0A 85 FE A2 00 FE 02 02 ... ... ... Literal: FB A9 00 95 00 9D 00 01 9D 00 02 9D 00 03 9D 00 04 Literal: 9D 00 05 9D 00 06 9D 00 07 E8 D0 E6 2C 02 20 10 FB Literal: 20 Expression (2): SYM( SEC $013A +) ... ... ... Literal: 17 0F 22 16 27 18 22 1A 30 27 22 16 30 27 22 0F 36 Literal: 17 10 00 00 08 10 01 00 10 18 02 00 08 18 03 00 10 Literal: 20 04 00 08 20 05 00 10 28 06 00 08 28 07 00 10 End PC = $01AE New segment: HEADER Literal: 4E 45 53 1A 02 01 01 00 End PC = $0008 New segment: VECTORS Expression (2): SYM( SEC) Expression (2): SYM( SEC $00D3 +) Expression (2): SYM( SEC $00D2 +) End PC = $0006 New segment: CHARS Literal: 03 0F 1F 1F 1C 24 26 66 00 00 00 00 1F 3F 3F 7F E0 C0 80 FC 80 C0 00 20 Literal: 00 20 60 00 F0 FC FE FE 60 70 18 07 0F 1F 3F 7F 7F 7F 1F 07 00 1E 3F 7F Literal: FC 7C 00 00 E0 F0 F8 F8 FC FC F8 C0 C2 67 2F 37 7F 7F FF FF 07 07 0F 0F ... ... ... Literal: 00 58 52 46 58 52 C6 9C F7 FF C6 F6 FE C6 F6 7A 00 7B 42 02 7A 42 02 7A Literal: 38 BC F6 F6 F6 F6 FE 5C 00 18 52 52 52 52 46 5C 00 FF FF FF FF FF FF FF Literal: FF FF FF FF FF FF FF FF End PC = $2000
Shrňme si tedy zjištěné informace do tabulky:
| Segment | Velikost (hex) | Velikost (dec) | Poznámka |
|---|---|---|---|
| CODE | $01AE | 430 | veškerý náš programový kód po překladu |
| HEADER | $0008 | 8 | hlavička ROM má pevně zadanou strukturu i délku 8 bajtů |
| VECTORS | $0006 | 6 | tři vektory pro RESET, IRQ a NMI: 3×2=6 bajtů |
| CHARS | $2000 | 8192 | dlaždice 8×8 pixelů s 2bpp (16 bajtů). 256 dlaždic pozadí + 256 dlaždic spritů |
Strojový kód nám tedy poněkud narostl, a to mj. i kvůli barvové paletě (32 bajtů+obslužná rutina), datům spritů (32 bajtů) a expanzi maker. Nicméně s dalším rozšiřováním programové logiky bude velikost kódu narůstat velmi pomalu – a necelých 0,5kB je stále dobrá hodnota. V případě potřeby často volaná makra předěláme na běžné podprogramy (subrutiny).
13. Repositář s demonstračními příklady
Demonstrační příklady napsané v assembleru, které jsou určené pro překlad pomocí assembleru ca65 (jenž je součástí cc65), byly uložen do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační 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ář:
14. Odkazy na Internetu
- The Thirty Million Line Problem
https://www.youtube.com/watch?v=kZRE7HIO3vk - NesDev.org
https://www.nesdev.org/ - The Sprite Attribute Byte
https://www.patater.com/nes-asm-tutorials/day-17/ - How to Program an NES game in C
https://nesdoug.com/ - Cycle reference chart
https://www.nesdev.org/wiki/Cycle_reference_chart - 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 (Nintendo Entertainment System) controller pinout
https://pinoutguide.com/Game/NES_controller_pinout.shtml - NES Controller Shift Register
https://www.allaboutcircuits.com/uploads/articles/nes-controller-arduino.png?v=1469416980041 - „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/ - The Nerdy Nights ca65 Remix
https://github.com/ddribin/nerdy-nights - 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 - Day 1 – Beginning NES Assembly
https://www.patater.com/nes-asm-tutorials/day-1/ - Day 2 – A Source Code File's Structure
https://www.patater.com/nes-asm-tutorials/day-2/ - Assembly Language Misconceptions
https://www.youtube.com/watch?v=8_0tbkbSGRE - How Machine Language Works
https://www.youtube.com/watch?v=HWpi9n2H3kE