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
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
16. Datové struktury použité pro vytvoření pozadí
18. Editory grafických objektů NESu
19. Repositář s demonstračními příklady
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
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__;
}
--- 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 ; ---------------------------------------------------------------------
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
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á
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“
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
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.
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
; 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ý).
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ář:
20. Odkazy na Internetu
- NesDev.org
https://www.nesdev.org/ - How to Program an NES game in C
https://nesdoug.com/ - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - „Game Development in Eight Bits“ by Kevin Zurawel
https://www.youtube.com/watch?v=TPbroUDHG0s&list=PLcGKfGEEONaBjSfQaSiU9yQsjPxxDQyV8&index=4 - Game Development for the 8-bit NES: A class by Bob Rost
http://bobrost.com/nes/ - Game Development for the 8-bit NES: Lecture Notes
http://bobrost.com/nes/lectures.php - NES Graphics Explained
https://www.youtube.com/watch?v=7Co_8dC2zb8 - NES GAME PROGRAMMING PART 1
https://rpgmaker.net/tutorials/227/?post=240020 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - Minimal NES example using ca65
https://github.com/bbbradsmith/NES-ca65-example - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - History of video game consoles (second generation): Wikipedia
http://en.wikipedia.org/wiki/History_of_video_game_consoles_(second_generation) - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - bee – The Multi-Console Emulator
http://www.thebeehive.ws/ - Nerdy Nights Mirror
https://nerdy-nights.nes.science/ - The Nerdy Nights ca65 Remix
https://github.com/ddribin/nerdy-nights - NES Development Day 1: Creating a ROM
https://www.moria.us/blog/2018/03/nes-development - How to Start Making NES Games
https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/ - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - Nocash NES Specs
http://nocash.emubase.de/everynes.htm - Nintendo Entertainment System
http://cs.wikipedia.org/wiki/NES - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - NesDev
http://nesdev.parodius.com/ - 2A03 technical reference
http://nesdev.parodius.com/2A03%20technical%20reference.txt - NES Dev wiki: 2A03
http://wiki.nesdev.com/w/index.php/2A03 - Ricoh 2A03
http://en.wikipedia.org/wiki/Ricoh_2A03 - 2A03 pinouts
http://nesdev.parodius.com/2A03_pinout.txt - 27c3: Reverse Engineering the MOS 6502 CPU (en)
https://www.youtube.com/watch?v=fWqBmmPQP40 - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - Nintendo Entertainment System (NES)
https://8bitworkshop.com/docs/platforms/nes/ - Question about NES vectors and PPU
https://archive.nes.science/nesdev-forums/f10/t4154.xhtml - How do mapper chips actually work?
https://archive.nes.science/nesdev-forums/f9/t13125.xhtml - INES
https://www.nesdev.org/wiki/INES - NES Basics and Our First Game
http://thevirtualmountain.com/nes/2017/03/08/nes-basics-and-our-first-game.html - Where is the reset vector in a .nes file?
https://archive.nes.science/nesdev-forums/f10/t17413.xhtml - CPU memory map
https://www.nesdev.org/wiki/CPU_memory_map - How to make NES music
http://blog.snugsound.com/2008/08/how-to-make-nes-music.html - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - MIDINES
http://www.wayfar.net/0×f00000_overview.php - FamiTracker
http://famitracker.com/ - nerdTracker II
http://nesdev.parodius.com/nt2/ - How NES Graphics work
http://nesdev.parodius.com/nesgfx.txt - NES Technical/Emulation/Development FAQ
http://nesdev.parodius.com/NESTechFAQ.htm - Adventures with ca65
https://atariage.com/forums/topic/312451-adventures-with-ca65/ - example ca65 startup code
https://atariage.com/forums/topic/209776-example-ca65-startup-code/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - Day 1 – Beginning NES Assembly
https://www.patater.com/nes-asm-tutorials/day-1/ - Day 2 – A Source Code File's Structure
https://www.patater.com/nes-asm-tutorials/day-2/ - Assembly Language Misconceptions
https://www.youtube.com/watch?v=8_0tbkbSGRE - How Machine Language Works
https://www.youtube.com/watch?v=HWpi9n2H3kE