Hlavní navigace

Vývoj her pro herní konzoli NES: atributy spritů, pokročilejší makra, zjištění velikosti generované ROM

23. 8. 2022
Doba čtení: 38 minut

Sdílet

 Autor: Depositphotos
V dalším článku o vývoji pro Nintendo Entertainment System (NES) si podrobněji ukážeme práci s atributy spritů (barva, zrcadlení…), vytvoříme složitější makra a zjistíme, kolik bajtů ROM je obsazeno námi vytvořeným demem.

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

14. Odkazy na Internetu

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
Poznámka: připomeňme si, že se skutečně jedná o makro a tudíž je nutné korektně pracovat se znakem #, který určuje konstanty. Například adc #gap a adc gap jsou zcela odlišné instrukce – první pracuje s konstantou, druhá s obsahem adresy gap.

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
Poznámka: mimochodem – ve hrách, v nichž se sprity pohybují rychleji, tj. přejedou přes celou obrazovku za méně než přibližně pět sekund, je změna pozice spritu prováděna o více než 1 pixel v každém směru (to se týká střel atd.). I přesto v naprosté většině případů není toto „poskakování“ patrné.

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_in­struction_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:
Poznámka: je to zajímavé (a možná i neintuitivní), ale podobné jednoduché rozhodovací a konstrukce bývají v assembleru stejně složité (či naopak jednoduché), jako například v céčku. Situace se ovšem radikálně změní ve chvíli, kdy se volají funkce (s předáním parametrů), nebo se pracuje s poli či záznamy.

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:
Poznámka: stále tedy používáme stejných triků i stejné množiny instrukcí.

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:

root_podpora

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ář:

# 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 v assembleru https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example04.asm
5 example05.asm zvukový výstup s využitím prvního „square“ kanálu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example05.asm
6 example06.asm použití maker bez parametrů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example06.asm
       
7 example07.asm nastavení barvové palety, zvýšení intenzity zvolené barvové složky https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example07.asm
8 example08.asm využití operátorů < a > https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example08.asm
9 example09.asm vymazání barvové palety realizované makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example09.asm
10 example10.asm vymazání barvové palety realizované podprogramem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example10.asm
11 example11.asm nastavení barvové palety pozadí i spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example11.asm
12 example12.asm refaktoring předchozího příkladu makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example12.asm
       
13 example13.asm zobrazení spritů tvořících Maria https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example13.asm
14 example14.asm posun spritů, aby se zdůraznila jejich nezávislost https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example14.asm
15 example15.asm větší množství spritů na obrazovce rozdělených do řádků https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example15.asm
16 example16.asm větší množství spritů na obrazovce na jediném řádku https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example16.asm
17 example17.asm pohyb jednoho spritu pomocí ovladače https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example17.asm
18 example18.asm odvozeno z předchozího příkladu, symbolická jména adres https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example18.asm
19 example19.asm odvozeno z předchozího příkladu, pomocná makra pro pohyb spritu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example19.asm
20 example20.asm pohyb spritu je založen na instrukcích INCDEC https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example20.asm
21 example21.asm přesun celého Maria (8 spritů) https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example21.asm
22 example22.asm (rychlá) změna barvové palety spritů tlačítkem A https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example22.asm
23 example23.asm realizace čítače pro snížení frekvence změn barvové palety https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example23.asm
24 example24.asm horizontální a vertikální zrcadlení spritů řízené hráčem stiskem tlačítek https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example24.asm
       
25 link.cfg konfigurace segmentů pro linker ld65 https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/link.cfg
26 Makefile Makefile pro překlad a slinkování všech příkladů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/Makefile
Poznámka: pro slinkování a spuštění dnešních demonstračních příkladů potřebujete i soubor mario.chr. Ten je stažen automaticky po zadání make example16make example23.

14. Odkazy na Internetu

  1. The Thirty Million Line Problem
    https://www.youtube.com/wat­ch?v=kZRE7HIO3vk
  2. NesDev.org
    https://www.nesdev.org/
  3. The Sprite Attribute Byte
    https://www.patater.com/nes-asm-tutorials/day-17/
  4. How to Program an NES game in C
    https://nesdoug.com/
  5. Cycle reference chart
    https://www.nesdev.org/wi­ki/Cycle_reference_chart
  6. Getting Started Programming in C: Coding a Retro Game with C Part 2
    https://retrogamecoders.com/getting-started-with-c-cc65/
  7. NES game development in 6502 assembly – Part 1
    https://kibrit.tech/en/blog/nes-game-development-part-1
  8. NES (Nintendo Entertainment System) controller pinout
    https://pinoutguide.com/Ga­me/NES_controller_pinout.shtml
  9. NES Controller Shift Register
    https://www.allaboutcircu­its.com/uploads/articles/nes-controller-arduino.png?v=1469416980041
  10. „Game Development in Eight Bits“ by Kevin Zurawel
    https://www.youtube.com/wat­ch?v=TPbroUDHG0s&list=PLcGKfGE­EONaBjSfQaSiU9yQsjPxxDQyV8&in­dex=4
  11. Game Development for the 8-bit NES: A class by Bob Rost
    http://bobrost.com/nes/
  12. Game Development for the 8-bit NES: Lecture Notes
    http://bobrost.com/nes/lectures.php
  13. NES Graphics Explained
    https://www.youtube.com/wat­ch?v=7Co_8dC2zb8
  14. NES GAME PROGRAMMING PART 1
    https://rpgmaker.net/tuto­rials/227/?post=240020
  15. 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/
  16. Minimal NES example using ca65
    https://github.com/bbbradsmith/NES-ca65-example
  17. List of 6502-based Computers and Consoles
    https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/
  18. History of video game consoles (second generation): Wikipedia
    http://en.wikipedia.org/wi­ki/History_of_video_game_con­soles_(second_generation)
  19. 6502 – the first RISC µP
    http://ericclever.com/6500/
  20. 3 Generations of Game Machine Architecture
    http://www.atariarchives.or­g/dev/CGEXPO99.html
  21. bee – The Multi-Console Emulator
    http://www.thebeehive.ws/
  22. Nerdy Nights Mirror
    https://nerdy-nights.nes.science/
  23. The Nerdy Nights ca65 Remix
    https://github.com/ddribin/nerdy-nights
  24. NES Development Day 1: Creating a ROM
    https://www.moria.us/blog/2018/03/nes-development
  25. How to Start Making NES Games
    https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/
  26. ca65 Users Guide
    https://cc65.github.io/doc/ca65.html
  27. cc65 Users Guide
    https://cc65.github.io/doc/cc65.html
  28. ld65 Users Guide
    https://cc65.github.io/doc/ld65.html
  29. da65 Users Guide
    https://cc65.github.io/doc/da65.html
  30. Nocash NES Specs
    http://nocash.emubase.de/everynes.htm
  31. Nintendo Entertainment System
    http://cs.wikipedia.org/wiki/NES
  32. Nintendo Entertainment System Architecture
    http://nesdev.icequake.net/nes.txt
  33. NesDev
    http://nesdev.parodius.com/
  34. 2A03 technical reference
    http://nesdev.parodius.com/2A03%20techni­cal%20reference.txt
  35. NES Dev wiki: 2A03
    http://wiki.nesdev.com/w/in­dex.php/2A03
  36. Ricoh 2A03
    http://en.wikipedia.org/wi­ki/Ricoh_2A03
  37. 2A03 pinouts
    http://nesdev.parodius.com/2A03_pi­nout.txt
  38. 27c3: Reverse Engineering the MOS 6502 CPU (en)
    https://www.youtube.com/wat­ch?v=fWqBmmPQP40
  39. “Hello, world” from scratch on a 6502 — Part 1
    https://www.youtube.com/wat­ch?v=LnzuMJLZRdU
  40. A Tour of 6502 Cross-Assemblers
    https://bumbershootsoft.wor­dpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/
  41. Nintendo Entertainment System (NES)
    https://8bitworkshop.com/doc­s/platforms/nes/
  42. Question about NES vectors and PPU
    https://archive.nes.science/nesdev-forums/f10/t4154.xhtml
  43. How do mapper chips actually work?
    https://archive.nes.science/nesdev-forums/f9/t13125.xhtml
  44. INES
    https://www.nesdev.org/wiki/INES
  45. NES Basics and Our First Game
    http://thevirtualmountain­.com/nes/2017/03/08/nes-basics-and-our-first-game.html
  46. Where is the reset vector in a .nes file?
    https://archive.nes.science/nesdev-forums/f10/t17413.xhtml
  47. CPU memory map
    https://www.nesdev.org/wi­ki/CPU_memory_map
  48. How to make NES music
    http://blog.snugsound.com/2008/08/how-to-make-nes-music.html
  49. Nintendo Entertainment System Architecture
    http://nesdev.icequake.net/nes.txt
  50. MIDINES
    http://www.wayfar.net/0×f00000_o­verview.php
  51. FamiTracker
    http://famitracker.com/
  52. nerdTracker II
    http://nesdev.parodius.com/nt2/
  53. How NES Graphics work
    http://nesdev.parodius.com/nesgfx.txt
  54. NES Technical/Emulation/Development FAQ
    http://nesdev.parodius.com/NES­TechFAQ.htm
  55. Adventures with ca65
    https://atariage.com/forum­s/topic/312451-adventures-with-ca65/
  56. example ca65 startup code
    https://atariage.com/forum­s/topic/209776-example-ca65-startup-code/
  57. 6502 PRIMER: Building your own 6502 computer
    http://wilsonminesco.com/6502primer/
  58. 6502 Instruction Set
    https://www.masswerk.at/6502/6502_in­struction_set.html
  59. Chip Hall of Fame: MOS Technology 6502 Microprocessor
    https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor
  60. Single-board computer
    https://en.wikipedia.org/wiki/Single-board_computer
  61. www.6502.org
    http://www.6502.org/
  62. 6502 PRIMER: Building your own 6502 computer – clock generator
    http://wilsonminesco.com/6502pri­mer/ClkGen.html
  63. Great Microprocessors of the Past and Present (V 13.4.0)
    http://www.cpushack.com/CPU/cpu.html
  64. Jak se zrodil procesor?
    https://www.root.cz/clanky/jak-se-zrodil-procesor/
  65. Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
    https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/
  66. 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/
  67. 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/
  68. 25 Microchips That Shook the World
    https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world
  69. Comparison of instruction set architectures
    https://en.wikipedia.org/wi­ki/Comparison_of_instructi­on_set_architectures
  70. Day 1 – Beginning NES Assembly
    https://www.patater.com/nes-asm-tutorials/day-1/
  71. Day 2 – A Source Code File's Structure
    https://www.patater.com/nes-asm-tutorials/day-2/
  72. Assembly Language Misconceptions
    https://www.youtube.com/wat­ch?v=8_0tbkbSGRE
  73. How Machine Language Works
    https://www.youtube.com/wat­ch?v=HWpi9n2H3kE

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.