Základy tvorby her pro herní konzoli NES: triky nabízené assemblerem, tvorba zvuků a grafiky

16. 6. 2022
Doba čtení: 35 minut

Sdílet

Autor: Depositphotos
Ve třetí části seriálu o tvorbě her pro NES se nejprve seznámíme s některými triky nabízenými assemblerem ca65 a pak si ukážeme tvorbu zvuků. Taktéž si (prozatím bez příkladu) řekneme, jak se na NESu pracuje s grafikou.

Obsah

1. Kostra programu, kterou budeme v dalších kapitolách rozšiřovat

2. Standardní nastavení segmentů definované v souborech ca65 (cc65)

3. Zdrojový kód druhého demonstračního příkladu

4. Definice symbolů pro pojmenování řídicích registrů PPU i APU

5. Zdrojový kód třetího demonstračního příkladu

6. Podpora lokálních automaticky generovaných návěští v ca65

7. Zdrojový kód čtvrtého demonstračního příkladu

8. Vygenerování jednoduchého zvuku – základní možnosti APU

9. Zdrojový kód pátého demonstračního příkladu

10. Makroassemblery

11. Makra bez parametrů v ca65

12. Praktické použití maker bez parametrů

13. Zobrazení kódu generovaného ca65 společně s původním zdrojovým kódem

14. Zdrojový kód šestého demonstračního příkladu

15. Grafický subsystém NESu

16. Datové struktury použité pro vytvoření pozadí

17. Sprity

18. Editory grafických objektů NESu

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Kostra programu, kterou budeme v dalších kapitolách rozšiřovat

V navazujících kapitolách se budeme odkazovat na následující kostru programu, který provede inicializaci herní konzole NES (tedy inicializaci CPU, PPU i APU – mikroprocesoru, grafického řadiče a zvukového čipu). Tento příklad postupně upravíme do čitelnější a lépe pochopitelně a měnitelné podoby:

; ---------------------------------------------------------------------
; Neoptimalizovaná kostra programu pro herní konzoli NES
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; ---------------------------------------------------------------------
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "INES"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
.code
 
 
 
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
 
.segment "CHR0a"
.segment "CHR0b"
 
 
 
; ---------------------------------------------------------------------
; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU
;
; viz též https://www.pagetable.com/?p=410
; ---------------------------------------------------------------------
 
; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
 
.proc nmi
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro IRQ (maskovatelné přerušení)
 
.proc irq
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        sei                     ; zákaz přerušení
        cld                     ; vypnutí dekadického režimu (není podporován)
 
        ldx #$ff
        txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
 
        ; nastavení řídicích registrů
        ldx #$00
        stx $2000               ; nastavení PPUCTRL = 0
        stx $2001               ; nastavení PPUMASK = 0
        stx $4015               ; nastavení APUSTATUS = 0
 
        ; čekání na vnitřní inicializaci PPU (dva snímky)
wait1:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait1               ; skok, pokud je příznak N nulový
wait2:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait2               ; skok, pokud je příznak N nulový
 
        ; vymazání obsahu RAM
        lda #$00                ; vynulování registru A
loop:   sta $000, x             ; vynulování X-tého bajtu v nulté stránce
        sta $100, x
        sta $200, x
        sta $300, x
        sta $400, x
        sta $500, x
        sta $600, x
        sta $700, x             ; vynulování X-tého bajtu v sedmé stránce
        inx                     ; přechod na další bajt
        bne loop                ; po přetečení 0xff -> 0x00 konec smyčky
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
wait3:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait3               ; skok, pokud je příznak N nulový
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTOR"
.addr nmi
.addr reset
.addr irq
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

2. Standardní nastavení segmentů definované v souborech ca65 (cc65)

Při instalaci cc65 a ca65 se mj. nainstalovaly i konfigurační soubory pro linker cl65, a to pro každou podporovanou platformu jeden unikátní soubor (protože každý počítač či herní konzole založená na MOS 6502 má zcela odlišnou strukturu/mapu paměti). Konkrétně pro osmibitovou herní konzoli NES se jedná o tento soubor:

/usr/share/cc65/cfg/nes.cfg
Poznámka: pochopitelně na Windows/Mac bude umístění odlišné a autorovi textu neznámé :-)

Obsah tohoto souboru vypadá na mém systému následovně:

SYMBOLS {
    __STACKSIZE__: type = weak, value = $0300; # 3 pages stack
}
MEMORY {
    ZP:     file = "", start = $0002, size = $001A, type = rw, define = yes;
 
    # INES Cartridge Header
    HEADER: file = %O, start = $0000, size = $0010, fill = yes;
 
    # 2 16K ROM Banks
    # - startup
    # - code
    # - rodata
    # - data (load)
    ROM0:   file = %O, start = $8000, size = $7FFA, fill = yes, define = yes;
 
    # Hardware Vectors at End of 2nd 8K ROM
    ROMV:   file = %O, start = $FFFA, size = $0006, fill = yes;
 
    # 1 8k CHR Bank
    ROM2:   file = %O, start = $0000, size = $2000, fill = yes;
 
    # standard 2k SRAM (-zeropage)
    # $0100-$0200 cpu stack
    # $0200-$0500 3 pages for ppu memory write buffer
    # $0500-$0800 3 pages for cc65 parameter stack
    SRAM:   file = "", start = $0500, size = __STACKSIZE__, define = yes;
 
    # additional 8K SRAM Bank
    # - data (run)
    # - bss
    # - heap
    RAM:    file = "", start = $6000, size = $2000, define = yes;
}
SEGMENTS {
    ZEROPAGE: load = ZP,              type = zp;
    HEADER:   load = HEADER,          type = ro;
    STARTUP:  load = ROM0,            type = ro,  define   = yes;
    LOWCODE:  load = ROM0,            type = ro,  optional = yes;
    ONCE:     load = ROM0,            type = ro,  optional = yes;
    CODE:     load = ROM0,            type = ro,  define   = yes;
    RODATA:   load = ROM0,            type = ro,  define   = yes;
    DATA:     load = ROM0, run = RAM, type = rw,  define   = yes;
    VECTORS:  load = ROMV,            type = rw;
    CHARS:    load = ROM2,            type = rw;
    BSS:      load = RAM,             type = bss, define   = yes;
}
FEATURES {
    CONDES: type    = constructor,
            label   = __CONSTRUCTOR_TABLE__,
            count   = __CONSTRUCTOR_COUNT__,
            segment = ONCE;
    CONDES: type    = destructor,
            label   = __DESTRUCTOR_TABLE__,
            count   = __DESTRUCTOR_COUNT__,
            segment = RODATA;
    CONDES: type    = interruptor,
            label   = __INTERRUPTOR_TABLE__,
            count   = __INTERRUPTOR_COUNT__,
            segment = RODATA,
            import  = __CALLIRQ__;
}
Poznámka: povšimněte si, že některé segmenty jsou pojmenovány mírně odlišně, ovšem ty hlavní – HEADER, ZEROPAGE, VECTORS a CODE můžeme ihned použít ve zdrojových kódech psaných v assembleru:
--- example01.asm       2022-06-12 16:36:17.851609148 +0200
+++ example02.asm       2022-06-12 16:36:25.591641357 +0200
@@ -1,5 +1,6 @@
 ; ---------------------------------------------------------------------
 ; Neoptimalizovaná kostra programu pro herní konzoli NES
+; Použití standardního nastavení segmentů podle ca65 (cc65)
 ;
 ; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
 ; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
@@ -33,8 +34,8 @@
 ; Blok paměti s definicí dlaždic 8x8 pixelů
 ; ---------------------------------------------------------------------
 
-.segment "CHR0a"
-.segment "CHR0b"
+.segment "CHARS"
+
 
 
 .code
@@ -118,6 +119,8 @@
 
 
 
+.segment "STARTUP"
+
 ; ---------------------------------------------------------------------
 ; Finito
 ; ---------------------------------------------------------------------
Poznámka: jedná se o výstup příkazu diff -u, tedy „unifikovaný diff“.

3. Zdrojový kód druhého demonstračního příkladu

Zdrojový kód dnešního druhého demonstračního příkladu popsaného v předchozí kapitole je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example02.asm (pro překlad a slinkování tohoto demonstračního příkladu použijte tento Makefile):

; ---------------------------------------------------------------------
; Neoptimalizovaná kostra programu pro herní konzoli NES
; Použití standardního nastavení segmentů podle ca65 (cc65)
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; ---------------------------------------------------------------------
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "HEADER"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
 
 
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
 
.segment "CHARS"
 
 
 
.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
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro IRQ (maskovatelné přerušení)
 
.proc irq
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        sei                     ; zákaz přerušení
        cld                     ; vypnutí dekadického režimu (není podporován)
 
        ldx #$ff
        txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
 
        ; nastavení řídicích registrů
        ldx #$00
        stx $2000               ; nastavení PPUCTRL = 0
        stx $2001               ; nastavení PPUMASK = 0
        stx $4015               ; nastavení APUSTATUS = 0
 
        ; čekání na vnitřní inicializaci PPU (dva snímky)
wait1:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait1               ; skok, pokud je příznak N nulový
wait2:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait2               ; skok, pokud je příznak N nulový
 
        ; vymazání obsahu RAM
        lda #$00                ; vynulování registru A
loop:   sta $000, x             ; vynulování X-tého bajtu v nulté stránce
        sta $100, x
        sta $200, x
        sta $300, x
        sta $400, x
        sta $500, x
        sta $600, x
        sta $700, x             ; vynulování X-tého bajtu v sedmé stránce
        inx                     ; přechod na další bajt
        bne loop                ; po přetečení 0xff -> 0x00 konec smyčky
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
wait3:  bit $2002               ; test obsahu registru PPUSTATUS 
        bpl wait3               ; skok, pokud je příznak N nulový
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
.segment "STARTUP"
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

4. Definice symbolů pro pojmenování řídicích registrů PPU i APU

Pokud se podíváme na následující úryvek zdrojového kódu s vymazanými poznámkami, můžeme vidět, že se nejedná o příliš čitelný kód, protože se v něm objevují přímo adresy řídicích registrů a nikoli jejich jména:

        ldx #$00
        stx $2000
        stx $2001
        stx $4015
 
wait1:  bit $2002
        bpl wait1
Poznámka: tento kód není nečitelný kvůli použití assembleru, ale právě numerických adres.

Assembler však umožňuje (stejně jako jakýkoli vyšší programovací jazyk) použít symbolická jména pro jakékoli hodnoty. Definice takových symbolických jmen je v assembleru triviální:

; Jména řídicích registrů použitých v kódu
PPUCTRL         = $2000
PPUMASK         = $2001
PPUSTATUS       = $2002
 
; Řídicí registry APU
APUSTATUS       = $4015

Předchozí kód se díky symbolickým jménům konstant stane mnohem čitelnější, a to i bez použití poznámek:

        ldx #$00
        stx PPUCTRL
        stx PPUMASK
        stx APUSTATUS
 
wait1:  bit PPUSTATUS
        bpl wait1

Symbolická jména nyní použijeme v celém zdrojovém kódu. V následujícím výpisu jsou zvýrazněny rozdíly mezi oběma verzemi – kupodivu těchto změn není příliš mnoho:

--- example01.asm       2022-06-12 16:36:17.851609148 +0200
+++ example03.asm       2022-06-12 16:36:32.259669073 +0200
@@ -1,10 +1,20 @@
 ; ---------------------------------------------------------------------
-; Neoptimalizovaná kostra programu pro herní konzoli NES
+; Upravená kostra programu pro herní konzoli NES
+; Pojmenování všech řídicích registrů
 ;
 ; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
 ; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
 ; ---------------------------------------------------------------------
 
+; Jména řídicích registrů použitých v kódu
+PPUCTRL         = $2000
+PPUMASK         = $2001
+PPUSTATUS       = $2002
+
+;;; Other IO registers.
+APUSTATUS       = $4015
+
+
 ; ---------------------------------------------------------------------
 ; Definice hlavičky obrazu ROM
 ; ---------------------------------------------------------------------
@@ -37,6 +47,7 @@
 .segment "CHR0b"
 
 
+
 .code
 
 ; ---------------------------------------------------------------------
@@ -73,14 +84,14 @@
 
         ; nastavení řídicích registrů
         ldx #$00
-        stx $2000               ; nastavení PPUCTRL = 0
-        stx $2001               ; nastavení PPUMASK = 0
-        stx $4015               ; nastavení APUSTATUS = 0
+        stx PPUCTRL             ; nastavení PPUCTRL = 0
+        stx PPUMASK             ; nastavení PPUMASK = 0
+        stx APUSTATUS           ; nastavení APUSTATUS = 0
 
         ; čekání na vnitřní inicializaci PPU (dva snímky)
-wait1:  bit $2002               ; test obsahu registru PPUSTATUS 
+wait1:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
         bpl wait1               ; skok, pokud je příznak N nulový
-wait2:  bit $2002               ; test obsahu registru PPUSTATUS 
+wait2:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
         bpl wait2               ; skok, pokud je příznak N nulový
 
         ; vymazání obsahu RAM
@@ -97,7 +108,7 @@
         bne loop                ; po přetečení 0xff -> 0x00 konec smyčky
 
         ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
-wait3:  bit $2002               ; test obsahu registru PPUSTATUS 
+wait3:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
         bpl wait3               ; skok, pokud je příznak N nulový
 
         ; vlastní herní smyčka je prozatím prázdná
Poznámka: opět se jedná o výstup příkazu diff -u, tedy „unifikovaný diff“.

5. Zdrojový kód třetího demonstračního příkladu

Zdrojový kód dnešního třetího demonstračního příkladu popsaného v předchozí kapitole je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example03.asm (pro překlad a slinkování tohoto demonstračního příkladu použijte tento Makefile):

; ---------------------------------------------------------------------
; Upravená kostra programu pro herní konzoli NES
; Pojmenování všech řídicích registrů
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; ---------------------------------------------------------------------
 
; Jména řídicích registrů použitých v kódu
PPUCTRL         = $2000
PPUMASK         = $2001
PPUSTATUS       = $2002
 
;;; Other IO registers.
APUSTATUS       = $4015
 
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "HEADER"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
 
 
; ---------------------------------------------------------------------
; 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
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro IRQ (maskovatelné přerušení)
 
.proc irq
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        sei                     ; zákaz přerušení
        cld                     ; vypnutí dekadického režimu (není podporován)
 
        ldx #$ff
        txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
 
        ; nastavení řídicích registrů
        ldx #$00
        stx PPUCTRL             ; nastavení PPUCTRL = 0
        stx PPUMASK             ; nastavení PPUMASK = 0
        stx APUSTATUS           ; nastavení APUSTATUS = 0
 
        ; čekání na vnitřní inicializaci PPU (dva snímky)
wait1:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait1               ; skok, pokud je příznak N nulový
wait2:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait2               ; skok, pokud je příznak N nulový
 
        ; vymazání obsahu RAM
        lda #$00                ; vynulování registru A
loop:   sta $000, x             ; vynulování X-tého bajtu v nulté stránce
        sta $100, x
        sta $200, x
        sta $300, x
        sta $400, x
        sta $500, x
        sta $600, x
        sta $700, x             ; vynulování X-tého bajtu v sedmé stránce
        inx                     ; přechod na další bajt
        bne loop                ; po přetečení 0xff -> 0x00 konec smyčky
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
wait3:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait3               ; skok, pokud je příznak N nulový
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

6. Podpora lokálních automaticky generovaných návěští v ca65

Další velmi užitečnou (jak uvidíme dále) vlastností assembleru ca65 je podpora lokálních návěští, které jsou automaticky generovány. Co to vlastně znamená? Podívejme se na následující kód získaný z našeho demonstračního příkladu. Tento kód obsahuje trojici prakticky stejných smyček, ovšem pro každou smyčku musíme vygenerovat unikátní návěští – cíl skoku:

wait1:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait1               ; skok, pokud je příznak N nulový
wait2:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait2               ; skok, pokud je příznak N nulový
 
        ...
        ...
        ...
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
wait3:  bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl wait3               ; skok, pokud je příznak N nulový

Takto strukturovaný kód se nejenom těžko udržuje, ale navíc je nutné si pořád vymýšlet nová a nová unikátní jména návěští. Vzhledem k tomu, že se v tomto případě jedná o lokální návěští (tedy ne například o adresy procedur), můžeme využít malého triku:

        ; čekání na vnitřní inicializaci PPU (dva snímky)
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový

Zde můžeme vidět, že návěští nemá jméno (druhý řádek) a že je referencováno pomocí zápisu „:-“. Ten v assembleru ca65 znamená „první lokální návěští, které nalezneš průchodem kódem směrem nahoru“. Pokud by se například použila dvojice vnořených smyček, vypadal by skok na konci vnější smyčky „:--“, což znamená „druhé lokální návěští, které nalezneš průchodem kódem směrem nahoru“

Poznámka: navíc je možné použít i pojmenované „cheap local labels“, které použijeme příště.

7. Zdrojový kód čtvrtého demonstračního příkladu

Zdrojový kód dnešního čtvrtého demonstračního příkladu popsaného v předchozí kapitole je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example04.asm (pro překlad a slinkování tohoto demonstračního příkladu použijte tento Makefile):

; ---------------------------------------------------------------------
; Upravená kostra programu pro herní konzoli NES
; Lokální automaticky generovaná návěští
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; ---------------------------------------------------------------------
 
; Jména řídicích registrů použitých v kódu
PPUCTRL         = $2000
PPUMASK         = $2001
PPUSTATUS       = $2002
 
;;; Other IO registers.
APUSTATUS       = $4015
 
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "HEADER"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
 
 
; ---------------------------------------------------------------------
; 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
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro IRQ (maskovatelné přerušení)
 
.proc irq
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        sei                     ; zákaz přerušení
        cld                     ; vypnutí dekadického režimu (není podporován)
 
        ldx #$ff
        txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
 
        ; nastavení řídicích registrů
        ldx #$00
        stx PPUCTRL             ; nastavení PPUCTRL = 0
        stx PPUMASK             ; nastavení PPUMASK = 0
        stx APUSTATUS           ; nastavení APUSTATUS = 0
 
        ; čekání na vnitřní inicializaci PPU (dva snímky)
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový
 
        ; vymazání obsahu 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
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
:       bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl :-                  ; skok, pokud je příznak N nulový
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

8. Vygenerování jednoduchého zvuku – základní možnosti APU

Nyní se konečně začneme zabývat praktičtějšími problémy. Nejdříve si ukážeme, jak lze vygenerovat jednoduchý zvuk. Použijeme přitom pochopitelně modul APU jenž je ovládán s využitím řídicích registrů, které jsme si ve stručnosti popsali již v prvním článku. Prozatím si vystačíme s prvním kanálem – generátorem obdélníkového signálu. Budou nám tedy postačovat čtyři řídicí registry:

; vybrané řídicí registry APU
APUSTATUS       = $4015
SQ1_VOL         = $4000
SQ1_LO          = $4002
SQ1_HI          = $4003

První zvukový kanál povolíme nastavením nejnižšího bitu registru APUSTATUS, tedy následovně:

lda #$01                ; povolení zvukového kanálu square1
sta APUSTATUS

Následně nastavíme periodu signálu. Jedná se o jedenáctibitovou hodnotu uloženou v registru SQ1_LO a ve spodních třech bitech registru SQ1_HI:

lda #$08                ; perioda signálu
sta SQ1_LO
lda #$02
sta SQ1_HI

Uložená hodnota je tedy rovna 2×256+8=520. Frekvence zvuku se vypočte jako:

f = 1789773 / (16 * (520 + 1))

což zhruba odpovídá 215 Hz (blízko noty E3).

To ovšem není vše, protože musíme nastavit i registr SQ1_VOL. Samotná hlasitost je uložena ve spodních čtyřech bitech. První dva bity nastavují střídu (duty cycle), další bit zakáže automatické ukončení zvuku po určité době (řízeno čítačem) a pátý bit zleva pak zakáže automatické snižování hlasitosti zvuku (decay):

Bity Stručný popis
7–6 střída (duty cycle)
5 vypnutí automatického ukončení zvuku (length counter)
4 vypnutí snižování hlasitosti zvuku (decay)
0–3 hlasitost

V našem konkrétním případě:

lda #%00111111          ; příznaky + hlasitost signálu
sta SQ1_VOL
Poznámka: povšimněte si, že assembler ca65 podporuje zápis konstant v dvojkovém kódu.

9. Zdrojový kód pátého demonstračního příkladu

Zdrojový kód dnešního pátého demonstračního příkladu popsaného v předchozí kapitole je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example05.asm (pro překlad a slinkování tohoto demonstračního příkladu použijte tento Makefile):

; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Zvukový výstup z generátoru obdélníkových signálů
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; 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
 
; vybrané řídicí registry APU
APUSTATUS       = $4015
SQ1_VOL         = $4000
SQ1_LO          = $4002
SQ1_HI          = $4003
 
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "HEADER"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
 
 
 
; ---------------------------------------------------------------------
; 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
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro IRQ (maskovatelné přerušení)
 
.proc irq
        rti                     ; návrat z přerušení
.endproc
 
 
 
; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        sei                     ; zákaz přerušení
        cld                     ; vypnutí dekadického režimu (není podporován)
 
        ldx #$ff
        txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
 
        ; nastavení řídicích registrů
        ldx #$00
        stx PPUCTRL             ; nastavení PPUCTRL = 0
        stx PPUMASK             ; nastavení PPUMASK = 0
        stx APUSTATUS           ; nastavení APUSTATUS = 0
 
        ; čekání na vnitřní inicializaci PPU (dva snímky)
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový
 
        ; vymazání obsahu 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
 
        ; čekání na dokončení dalšího snímku, potom může začít herní smyčka
:       bit PPUSTATUS           ; test obsahu registru PPUSTATUS 
        bpl :-                  ; skok, pokud je příznak N nulový
 
        ; zvukový výstup
        lda #$01                ; povolení zvukového kanálu square1
        sta APUSTATUS
        lda #$08                ; perioda signálu
        sta SQ1_LO
        lda #$02
        sta SQ1_HI
        lda #%00111111          ; příznaky + hlasitost signálu
        sta SQ1_VOL
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

10. Makroassemblery

Nástroje typu „assembler“ (mezi něž patří i ca65) je možné podle principu jejich práce rozdělit do několika kategorií. Do první kategorie spadají assemblery interaktivní, které uživateli nabízejí poměrně komfortní vývojové prostředí, v němž je v případě potřeby možné zapisovat jednotlivé instrukce, spouštět programy, krokovat je, vypisovat obsahy pracovních registrů mikroprocesoru, prohlížet si obsah operační paměti, zásobníku atd. Výhodou byla nezávislost těchto assemblerů na rychlém externím paměťovém médiu, proto jsme se s nimi mohli setkat například na osmibitových domácích mikropočítačích či dnes na různých zařízeních typu IoT (i když zde úlohu pouhého interaktivního assembleru mnohdy přebírá interaktivní debugger). Ovšem pro NES nejsou vyvíjeny. Druhý typ assemblerů je široce používán dodnes – jedná se vlastně o běžné překladače, kterým se na vstupu předloží zdrojový kód a po překladu se výsledný nativní kód taktéž uloží na paměťové médium (odkud ho lze přímo spustit, což se dělo například v operačním systému DOS, popř. ho ještě před spuštěním slinkovat, což je případ Linuxu a dalších moderních operačních systémů, ovšem i NESu).

Assemblery spadající do druhé kategorie jsou mnohdy vybaveny více či méně dokonalým systémem maker; odtud ostatně pochází i jejich často používané označení macroassembler. Makra, která se většinou aplikují na zdrojový kód v první fázi překladu, je možné použít pro různé činnosti, ať již se jedná o zjednodušení zápisu kódu či o jeho zkrácení a zpřehlednění. Existují například sady poměrně složitých maker, které do assembleru přidávají některé konstrukce známé z vyšších programovacích jazyků – rozvětvení, programové smyčky, deklaraci objektů atd. Všechny moderní assemblery práci s makry podporují, i když se způsob zápisu maker i jejich základní vlastnosti od sebe odlišují. Z tohoto důvodu se v dnešním článku budeme věnovat (prozatím) pouze makrům v ca65.

Poznámka: i přes podporu maker je ca65 jednoprůchodovým assemblerem.

11. Makra bez parametrů v ca65

Makra v assembleru, tedy i v ca65, provádí textové substituce, což mj. znamená, že expanze maker je vykonána v první fázi překladu. V ca65 deklarace makra začíná direktivou .macro a končí direktivou .endmacro (obě direktivy se zapisují včetně teček na začátku). Za direktivou .macro musí následovat jméno makra a popř. i jeho parametry. Na dalších řádcích je pak vlastní text makra:

.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

Použití makra je ještě jednodušší než jeho deklarace – kdekoli se prostě uvede jméno makra s případnými parametry. Jakmile assembler zjistí, že se ve zdrojovém kódu nachází jméno makra, provede jeho expanzi, takže se vlastně případné instrukce, ze kterých se text makra skládá, přímo vloží do kódu na místo volání makra:

.proc reset
        ; nastavení stavu CPU
        setup_cpu
.endproc
Poznámka: existují i standardní makra, které z pohledu programátora rozšiřují instrukční soubor mikroprocesoru 6502 (interně se však makro expanduje obecně na větší množství instrukcí):
; add - Add without carry
.macro  add     Arg1, Arg2
        clc
        .if .paramcount = 2
                adc     Arg1, Arg2
        .else
                adc     Arg1
        .endif
.endmacro
 
; sub - subtract without borrow
.macro  sub     Arg1, Arg2
        sec
        .if .paramcount = 2
                sbc     Arg1, Arg2
        .else
                sbc     Arg1
        .endif
.endmacro
 
; bgt - jump if unsigned greater
.macro  bgt     Arg
        .local  L
        beq     L
        bcs     Arg
L:
.endmacro

12. Praktické použití maker bez parametrů

Podívejme se nyní na praktické použití maker podporovaných assemblerem ca65 při zjednodušení předchozího demonstračního příkladu. Velmi jednoduché bude makro zajišťující inicializaci CPU po resetu:

.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

Poněkud složitější bude makro pro čekání na zobrazení snímku, a to z toho důvodu, že obsahuje skok, resp. programovou smyčku. Právě v tomto případě využijeme anonymní návěští, které zajistí, že nedojde k pokusu o znovupoužití pojmenovaného návěští:

.macro wait_for_frame
:       bit PPUSTATUS            ; test obsahu registru PPUSTATUS 
        bpl :-                   ; skok, pokud je příznak N nulový
.endmacro

Podobným způsobem lze realizovat makro pro vymazání operační paměti:

.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

Makra se použijí následovně:

; Obslužná rutina pro RESET
 
.proc reset
        ; nastavení stavu CPU
        setup_cpu
 
        ...
        ...
        ...
 
        ; č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
 
        ...
        ...
        ...
 
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc

13. Zobrazení kódu generovaného ca65 společně s původním zdrojovým kódem

Assembler ca65 umožňuje (samozřejmě kromě vlastního překladu) vytvoření souboru obsahujícího mix mezi původním zdrojovým kódem a vygenerovanou sekvencí bajtů (tedy jak dat, tak i operačních kódů instrukcí). Pro tento účel se používá přepínač -l, který je ovšem vhodné doplnit přepínačem –list-bytes větší_hodnota, aby se kromě jména makra vypsaly i všechny operační kódy instrukcí (což v našem případě platí zejména po delší makro pro smazání operační paměti):

$ ca65 example06.asm -o example06.o -l test.list --list-bytes 100

Výsledkem této operace bude mj. i soubor „test.list“ s následujícím obsahem:

ca65 V2.18 - Ubuntu 2.18-1
Main file   : example06.asm
Current file: example06.asm
 
000000r 1               ; ---------------------------------------------------------------------
000000r 1               ; Kostra programu pro herní konzoli NES
000000r 1               ; Zvukový výstup z generátoru obdélníkových signálů
000000r 1               ;
000000r 1               ; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
000000r 1               ; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
000000r 1               ; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
000000r 1               ; ---------------------------------------------------------------------
000000r 1
000000r 1               ; Jména řídicích registrů použitých v kódu
000000r 1               PPUCTRL         = $2000
000000r 1               PPUMASK         = $2001
000000r 1               PPUSTATUS       = $2002
000000r 1
000000r 1               ; vybrané řídicí registry APU
000000r 1               APUSTATUS       = $4015
000000r 1               SQ1_VOL         = $4000
000000r 1               SQ1_LO          = $4002
000000r 1               SQ1_HI          = $4003
000000r 1
000000r 1
000000r 1               ; ---------------------------------------------------------------------
000000r 1               ; Definice maker
000000r 1               ; ---------------------------------------------------------------------
000000r 1
000000r 1               .macro setup_cpu
000000r 1                       ; nastavení stavu CPU
000000r 1                       sei                     ; zákaz přerušení
000000r 1                       cld                     ; vypnutí dekadického režimu (není podporován)
000000r 1
000000r 1                       ldx #$ff
000000r 1                       txs                     ; vrchol zásobníku nastaven na 0xff (první stránka)
000000r 1               .endmacro
000000r 1
000000r 1               .macro wait_for_frame
000000r 1               :       bit PPUSTATUS            ; test obsahu registru PPUSTATUS
000000r 1                       bpl :-                   ; skok, pokud je příznak N nulový
000000r 1               .endmacro
000000r 1
000000r 1               .macro clear_ram
000000r 1                       lda #$00                ; vynulování registru A
000000r 1               :       sta $000, x             ; vynulování X-tého bajtu v nulté stránce
000000r 1                       sta $100, x
000000r 1                       sta $200, x
000000r 1                       sta $300, x
000000r 1                       sta $400, x
000000r 1                       sta $500, x
000000r 1                       sta $600, x
000000r 1                       sta $700, x             ; vynulování X-tého bajtu v sedmé stránce
000000r 1                       inx                     ; přechod na další bajt
000000r 1                       bne :-                  ; po přetečení 0xff -> 0x00 konec smyčky
000000r 1               .endmacro
000000r 1
000000r 1
000000r 1
000000r 1               ; ---------------------------------------------------------------------
000000r 1               ; Definice hlavičky obrazu ROM
000000r 1               ; ---------------------------------------------------------------------
000000r 1
000000r 1               ; Size of PRG in units of 16 KiB.
000000r 1               prg_npage = 1
000000r 1
000000r 1               ; Size of CHR in units of 8 KiB.
000000r 1               chr_npage = 1
000000r 1
000000r 1               ; INES mapper number.
000000r 1               mapper = 0
000000r 1
000000r 1               ; Mirroring (0 = horizontal, 1 = vertical)
000000r 1               mirroring = 1
000000r 1
000000r 1               .segment "HEADER"
000000r 1  4E 45 53 1A          .byte $4e, $45, $53, $1a
000004r 1  01                   .byte prg_npage
000005r 1  01                   .byte chr_npage
000006r 1  01                   .byte ((mapper & $0f) << 4) | (mirroring & 1)
000007r 1  00                   .byte mapper & $f0
000008r 1
000008r 1
000008r 1
000008r 1               ; ---------------------------------------------------------------------
000008r 1               ; Blok paměti s definicí dlaždic 8x8 pixelů
000008r 1               ; ---------------------------------------------------------------------
000008r 1
000008r 1               .segment "CHR0a"
000000r 1               .segment "CHR0b"
000000r 1
000000r 1
000000r 1               .code
000000r 1
000000r 1               ; ---------------------------------------------------------------------
000000r 1               ; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU
000000r 1               ;
000000r 1               ; viz též https://www.pagetable.com/?p=410
000000r 1               ; ---------------------------------------------------------------------
000000r 1
000000r 1               ; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
000000r 1
000000r 1               .proc nmi
000000r 1  40                   rti                     ; návrat z přerušení
000001r 1               .endproc
000001r 1
000001r 1
000001r 1
000001r 1               ; Obslužná rutina pro IRQ (maskovatelné přerušení)
000001r 1
000001r 1               .proc irq
000001r 1  40                   rti                     ; návrat z přerušení
000002r 1               .endproc
000002r 1
000002r 1
000002r 1
000002r 1               ; Obslužná rutina pro RESET
000002r 1
000002r 1               .proc reset
000002r 1                       ; nastavení stavu CPU
000002r 1  78 D8 A2 FF          setup_cpu
000006r 1  9A
000007r 1
000007r 1                       ; nastavení řídicích registrů
000007r 1  A2 00                ldx #$00
000009r 1  8E 00 20             stx PPUCTRL             ; nastavení PPUCTRL = 0
00000Cr 1  8E 01 20             stx PPUMASK             ; nastavení PPUMASK = 0
00000Fr 1  8E 15 40             stx APUSTATUS           ; nastavení APUSTATUS = 0
000012r 1
000012r 1                       ; čekání na vnitřní inicializaci PPU (dva snímky)
000012r 1  2C 02 20 10          wait_for_frame
000016r 1  FB
000017r 1  2C 02 20 10          wait_for_frame
00001Br 1  FB
00001Cr 1
00001Cr 1                       ; vymazání obsahu RAM
00001Cr 1  A9 00 95 00          clear_ram
000020r 1  9D 00 01 9D
000024r 1  00 02 9D 00
000028r 1  03 9D 00 04
00002Cr 1  9D 00 05 9D
000030r 1  00 06 9D 00
000034r 1  07 E8 D0 E6
000038r 1
000038r 1                       ; čekání na další snímek
000038r 1  2C 02 20 10          wait_for_frame
00003Cr 1  FB
00003Dr 1
00003Dr 1                       ; zvukový výstup
00003Dr 1  A9 01                lda #$01                ; povolení zvukového kanálu square1
00003Fr 1  8D 15 40             sta APUSTATUS
000042r 1  A9 08                lda #$08                ; perioda signálu
000044r 1  8D 02 40             sta SQ1_LO
000047r 1  A9 02                lda #$02
000049r 1  8D 03 40             sta SQ1_HI
00004Cr 1  A9 3F                lda #%00111111          ; příznaky + hlasitost signálu
00004Er 1  8D 00 40             sta SQ1_VOL
000051r 1
000051r 1                       ; vlastní herní smyčka je prozatím prázdná
000051r 1               game_loop:
000051r 1  4C rr rr             jmp game_loop           ; nekonečná smyčka (později rozšíříme)
000054r 1               .endproc
000054r 1
000054r 1
000054r 1
000054r 1               ; ---------------------------------------------------------------------
000054r 1               ; Tabulka vektorů CPU
000054r 1               ; ---------------------------------------------------------------------
000054r 1
000054r 1               .segment "VECTORS"
000000r 1  rr rr        .addr nmi
000002r 1  rr rr        .addr reset
000004r 1  rr rr        .addr irq
000006r 1
000006r 1
000006r 1
000006r 1               ; ---------------------------------------------------------------------
000006r 1               ; Finito
000006r 1               ; ---------------------------------------------------------------------
000006r 1

Příklad zobrazení makra – ve druhém sloupci jsou zobrazeny operační kódy instrukcí, ve třetím sloupci pak opis zdrojového kódu, tedy volání makra:

000017r 1  2C 02 20 10          wait_for_frame
00001Br 1  FB

Nejdelší je expanze makra clear_ram:

00001Cr 1  A9 00 95 00          clear_ram
000020r 1  9D 00 01 9D
000024r 1  00 02 9D 00
000028r 1  03 9D 00 04
00002Cr 1  9D 00 05 9D
000030r 1  00 06 9D 00
000034r 1  07 E8 D0 E6

14. Zdrojový kód šestého demonstračního příkladu

Zdrojový kód dnešního šestého demonstračního příkladu popsaného v předchozí kapitole je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example06.asm (pro překlad a slinkování tohoto demonstračního příkladu opět použijte tento Makefile):

; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Zvukový výstup z generátoru obdélníkových signálů
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; 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
 
; vybrané řídicí registry APU
APUSTATUS       = $4015
SQ1_VOL         = $4000
SQ1_LO          = $4002
SQ1_HI          = $4003
 
 
; ---------------------------------------------------------------------
; 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
 
 
 
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
 
; Size of PRG in units of 16 KiB.
prg_npage = 1
 
; Size of CHR in units of 8 KiB.
chr_npage = 1
 
; INES mapper number.
mapper = 0
 
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
 
.segment "HEADER"
        .byte $4e, $45, $53, $1a
        .byte prg_npage
        .byte chr_npage
        .byte ((mapper & $0f) << 4) | (mirroring & 1)
        .byte mapper & $f0
 
 
 
; ---------------------------------------------------------------------
; 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
        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
        stx PPUMASK             ; nastavení PPUMASK = 0
        stx APUSTATUS           ; nastavení APUSTATUS = 0
 
        ; č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
 
        ; zvukový výstup
        lda #$01                ; povolení zvukového kanálu square1
        sta APUSTATUS
        lda #$08                ; perioda signálu
        sta SQ1_LO
        lda #$02
        sta SQ1_HI
        lda #%00111111          ; příznaky + hlasitost signálu
        sta SQ1_VOL
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

15. Grafický subsystém NESu

Grafický subsystém použitý v herní konzoli Nintendo Entertainment System byl v mnoha ohledech ještě zajímavější než její zvukový subsystém. Víme již, že herní konzole NES existovala ve variantě pro televizní normu NTSC i pro normu PAL. Konzole určené pro televizní normu PAL obsahovaly mikroprocesor 2A07 s hodinovou frekvencí 1,66 MHz, zatímco pro normu NTSC byly určeny konzole s mikroprocesorem 2A03 používající hodinovou frekvenci 1,79 MHz. I grafické čipy se lišily podle toho, pro jakou televizní normu byly určeny. Pro normu NTSC se používal čip RP2C02 se vstupní hodinovou frekvencí 5,37 MHz, zatímco pro normu PAL byl použit čip RP2C07 s frekvencí 5,32 MHz. Tyto čipy, označované taktéž zkratkou PPU, obsahovaly 256 interní paměti využívané systémem pro zobrazení spritů. Kromě toho přistupoval PPU k samostatnému čipu RAM o kapacitě dva kilobajty. V této paměti bylo uloženo větší množství datových struktur nesoucích informace o pozadí scény, o barvové paletě i o tvarech spritů. Ve skutečnosti však mohl PPU přistupovat i k paměti ROM umístěné na paměťovém modulu se hrou.

Obrázek 1: Barvová paleta používaná herní konzolí NES.

16. Datové struktury použité pro vytvoření pozadí

U herní konzole NES se obraz posílaný na televizor skládal ze dvou částí: pozadí a pohyblivých spritů. Nejprve si stručně popíšeme, jakým způsobem se vytvářelo pozadí. Při použití televizní normy PAL bylo rozlišení obrazu rovno 256×240 pixelům, zatímco u normy SECAM bylo horních osm řádků a spodních osm řádků zatemněných, tj. rozlišení bylo sníženo na 256×224 pixelů. Teoreticky sice bylo možné vytvořit klasický framebuffer, v němž by bylo celé pozadí uloženo, ale při šestnáctibarevném obrazu, tj. při použití čtyř bitů na pixel, by musela být kapacita framebufferu poměrně velká: 28 kilobajtů (navíc je 16 „globálních“ barev relativně malé množství). Konstruktéři čipu PPU tedy využili technologii, s níž jsme se seznámili i u dalších typů herních konzolí: namísto framebufferu byly v obrazové paměti uloženy vzorky o velikosti 8×8 pixelů, které byly skládány do mřížky 32×30 dlaždic, což přesně odpovídá již zmíněnému rozlišení 256×240 pixelů (32×8=256, 30×8=240).

Obrázek 2: Screenshot ze hry Kirby's Adventure.

Základní datovou strukturou byla struktura nazvaná Name Table o velikosti 960 bajtů, která obsahovala indexy všech tvarů tvořících dlaždicovitý obraz 32×30 dlaždic. Mohlo by se tedy zdát, že jedinou další potřebnou strukturou je tabulka všech vzorků (bitmap), z nichž každá má velikost 8×8 pixelů. Situace je však poněkud složitější, protože na pozadí lze vykreslit až šestnáct různých barev. V tabulce vzorků (Pattern Table) jsou pro každý pixel vyhrazeny dva bity, tj. lze rozlišit čtyři možnosti/barvy. Kromě toho existuje ještě tabulka atributů (Attribute Table) o velikosti 64 bajtů, která obsahuje horní dva bity pro oblast o velikosti 4×4 dlaždice, tj. 32×32 pixelů. Díky existenci této druhé tabulky je skutečně možné – i když s mnoha omezeními – použít na pozadí šestnáct různých barev – a to při minimálních paměťových nárocích. Důležité přitom je, že tabulka vzorků může být přemapována do ROM na paměťovém modulu, takže je možné poměrně jednoduchým způsobem například animovat celou scénu pouhou změnou „ukazatele“ na tuto tabulku (což mnohé hry skutečně dělaly).

Obrázek 3: Další screenshot ze hry Kirby's Adventure.

17. Sprity

Způsob vytváření pozadí, o němž jsme se zmínili v předchozí kapitole, je poměrně složitý (i když výhody spočívající v relativně malém zatížení procesoru, většinou převažovaly). Se sprity je však situace poněkud jednodušší. Základem při vykreslování spritů je opět tabulka vzorků, ta je ovšem doplněna pomocnou pamětí o kapacitě 256 bajtů, která je umístěna přímo na čipu PPU. Programátor měl k této paměti přístup buď přes řídicí registry PPU, alternativně pak přes DMA. Ve zmíněných 256 bajtech jsou umístěny informace o 64 spritech, tj. pro každý sprite jsou vyhrazeny čtyři bajty. V těchto bajtech se nachází horizontální pozice spritu, vertikální pozice spritu, horní dva bity barvy (spodní bity jsou přímo v tabulce vzorků), index do tabulky vzorků (ukazuje na tvar spritu) a konečně taktéž bitové příznaky: horizontální zrcadlení, vertikální zrcadlení a priorita spritu (před/za pozadím).

Obrázek 4: Screenshot ze hry Donkey Kong.

Kvůli dalším technologickým omezením čipu PPU mohlo být na jednom řádku (tj. vedle sebe) zobrazeno pouze omezené množství spritů, tj. nebylo například možné všechny sprity umístit vedle sebe. Taktéž počet celkově zobrazovaných barev nedosáhl hodnoty 32 (16 pro pozadí, 16 pro sprity), ale pouze 25, přičemž barvová paleta obsahovala 48 barev a pět odstínů šedi (konkrétní způsob zobrazení barev byl na obou televizních normách poněkud odlišný).

Poznámka: na originální konzoli lze zobrazit jen osm spritů na obrazovém řádku, ovšem mnohé emulátory toto omezení neobsahují.

Obrázek 5: Další screenshot ze hry Donkey Kong.

18. Editory grafických objektů NESu

V současnosti existuje několik editorů grafických objektů pro NES. Tyto editory většinou dokážou provádět export dat přesně v takovém formátu, jak jsou uloženy v reálném cartridge (ROM). Tato data dokáže ca65 (resp. přesněji řečeno cl65, tedy linker) slinkovat se strojovým kódem do obrazu cartridge. Příště si ukážeme práci s editorem nazvaným Tilemolester:

Obrázek 6: Tilemolester – data pozadí (background).

Obrázek 7: Tilemolester – data spritů.

19. 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. 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 link.cfg konfigurace segmentů pro linker ld65 https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/link.cfg
8 Makefile Makefile pro překlad a slinkování všech příkladů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/Makefile

20. Odkazy na Internetu

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

Autor článku

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