Obsah
1. Vývoj pro osmibitovou herní konzoli NES s využitím překladače jazyka C
2. Struktura jednoduchého projektu pro NES založeného na jazyku C
3. Zdrojový kód kostry projektu
4. Soubor crt0.s s inicializačním kódem
6. Překlad a slinkování celého projektu
7. Výsledek překladu ve formě assembleru
8. Spuštění vygenerovaného obrazu cartridge
9. Přístup do paměti PPU – vykreslení pozadí herní scény
10. Výsledek překladu do assembleru
11. Modifikace barvových atributů
12. Výsledek překladu do assembleru
13. Využití definice dlaždic z herního světa Maria
14. Výsledek překladu do assembleru
15. Specifikace, která sada dlaždic se má použít pro vykreslování pozadí
16. Opravený příklad ze třinácté kapitoly
17. Výsledek překladu do assembleru
18. Obsah závěrečné části seriálu
19. Repositář s demonstračními příklady
1. Vývoj pro osmibitovou herní konzoli NES s využitím překladače jazyka C
Jak je z demonstračních příkladů, které jsme si prozatím v seriálu o vývoji pro slavnou osmibitovou herní konzoli NES ukázali ukázali, patrné, je možné s využitím „pouhého“ assembleru napsat i relativně složitý kód. V minulosti se ostatně v assembleru psaly celé hry. Je to umožněno mj. i možností pojmenovat si všechna důležitá paměťová místa i hodnoty, dále možností deklarovat subrutiny (podprogramy) a v neposlední řadě nesmíme zapomenout ani na význam maker, zejména maker s parametry (tato makra jsou v assembleru ca65 plně podporována). I přesto je však tvorba větších aplikací přímo v assembleru komplikovaná.
Z tohoto důvodu si v dnešním článku ukážeme práci s překladačem programovacího jazyka C, jehož varianta určená pro osmibitový mikroprocesor MOS 6502 se jmenuje cc65 (viz též články [1] a [2] věnované konkrétním vlastnostem tohoto céčka a odlišnostmi od ANSI či ISO C). Na rozdíl do překladačů programovacího jazyka C určeného pro moderní rychlé mikroprocesory je však nutné při použití cc65 přemýšlet o tom, jak se konkrétní jazykové konstrukce přeloží do sekvence strojových instrukcí. V opačném případě by výsledný strojový kód byl příliš velký a především pomalý, což není v případě herní konzole NES akceptovatelné (nelze říct „kupte si lepší CPU“).
Překladač cc65 taktéž programátorům nabízí ucelený pohled na celý projekt, a to včetně inicializačního kódu („co je spuštěno před main?“), tvorby základních knihoven prakticky na zelené louce, struktury výsledného obrazu cartridge atd. Tj. může se jednat i o poměrně zajímavou učební pomůcku.
2. Struktura jednoduchého projektu pro NES založeného na jazyku C
Podívejme se nyní na strukturu velmi jednoduchého projektu se zdrojovými kódy pro herní konzoli NES. Tento projekt je sice založen na jazyku C, ale obsahuje i dva soubory zapsané v assembleru – inicializační kód a implementaci knihovny. Celý projekt naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/:
-rw-rw-r-- 1 ptisnovs ptisnovs 8192 Dec 23 16:46 Alpha.chr -rw-rw-r-- 1 ptisnovs ptisnovs 4221 Dec 24 08:47 crt0.s -rw-rw-r-- 1 ptisnovs ptisnovs 635 Dec 24 09:00 intro.c -rw-rw-r-- 1 ptisnovs ptisnovs 418 Dec 24 08:59 Makefile -rw-rw-r-- 1 ptisnovs ptisnovs 8877 Dec 23 16:46 neslib.h -rw-rw-r-- 1 ptisnovs ptisnovs 14481 Dec 24 08:46 neslib.s -rw-rw-r-- 1 ptisnovs ptisnovs 2019 Dec 23 16:46 nrom_32k_vert.cfg
Jaký je však význam jednotlivých souborů? To je uvedeno v další tabulce:
Soubor | Stručný popis souboru | Popsáno v |
---|---|---|
Alpha.chr | binární soubor s definicí dlaždic pozadí i spritů (4kB+4kB) | |
intro.c | vlastní zdrojový kód demonstračního příkladu | třetí kapitola |
crt0.s | inicializační kód spuštěný před main | čtvrtá kapitola |
neslib.h | hlavičkový soubor „standardní“ knihovny pro NES | pátá kapitola |
neslib.s | implementace funkcí ze standardní knihovny (assembler) | pátá kapitola |
Makefile | pravidla pro překlad a slinkování příkladu do formy NES cartridge | šestá kaptiola |
nrom_32k_vert.cfg | konfigurace pro linker ld65 | × |
3. Zdrojový kód kostry projektu
Podívejme se nyní na základ celého projektu; konkrétně na soubor nazvaný intro.c, jenž obsahuje kostru aplikace (dema, hry) určené pro herní konzoli NES. Jedná se o zdrojový kód napsaný v C určený pro překlad céčkovým překladačem cc65. Až na řádek #pragma se vlastně jedná o standardní céčko:
#include "neslib.h" #define BLACK_COLOR 0x0f #define DARK_GRAY_COLOR 0x00 #define LIGHT_GRAY_COLOR 0x10 #define WHITE_COLOR 0x30 #define MARIO_BACKGROUND_COLOR 0x22 #pragma bss-name(push, "ZEROPAGE") const unsigned char palette[] = { MARIO_BACKGROUND_COLOR, DARK_GRAY_COLOR, LIGHT_GRAY_COLOR, WHITE_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR }; void game_loop(void) { while (1) { } } void main(void) { ppu_off(); pal_bg(palette); ppu_on_all(); game_loop(); }
Celý program z pohledu céčkovského programátora začíná funkcí main. Zde se provádí pouhé čtyři operace, které jsme prozatím museli realizovat přímo v assembleru:
- Zákaz vykreslování obrazu čipem PPU
- Nastavení barvové palety pozadí (background)
- Povolení vykreslování obrazu čipem PPU
- Vstup do (nekonečné) herní smyčky
Tyto čtyři operace se v céčku realizují triviálně:
void main(void) { ppu_off(); pal_bg(palette); ppu_on_all(); game_loop(); }
Samozřejmě musíme specifikovat i data pro paletu barev použitou pro pozadí herní scény. Připomeňme si, že barvová paleta pozadí obsahuje osmibitové indexy šestnácti barev, takže pozadí můžeme snadno realizovat polem hodnot typu unsigned char:
#define BLACK_COLOR 0x0f #define DARK_GRAY_COLOR 0x00 #define LIGHT_GRAY_COLOR 0x10 #define WHITE_COLOR 0x30 #define MARIO_BACKGROUND_COLOR 0x22 const unsigned char palette[] = { MARIO_BACKGROUND_COLOR, DARK_GRAY_COLOR, LIGHT_GRAY_COLOR, WHITE_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR };
Obrázek 1: Sada barev používaná herní konzolí NES.
Dále ve zdrojovém kódu nalezneme tento řádek:
#pragma bss-name(push, "ZEROPAGE")
Za tímto řádkem lze zapsat deklarace globálních proměnných, které budou uloženy do nulté stránky paměti, ke které mikroprocesor MOS 6502 přistupuje rychleji a s využitím kratších instrukcí, než k ostatním stránkám.
Zbytek zdrojového kódu je jednoduchý – jedná se o implementaci (nekonečné) herní smyčky, kterou můžeme zapsat například takto:
void game_loop(void) { while (1) { } }
4. Soubor crt0.s s inicializačním kódem
V projektu se kromě samotného zdrojového kódu intro.c nachází i další důležité soubory, o nichž jsme se ve stručnosti zmínili ve druhé kapitole. Jedná se především o soubor pojmenovaný crt0.s, tedy o soubor obsahující zdrojový kód psaný v assembleru (ostatně právě proto má koncovku .s). Jméno crt0 může napovídat, že tento soubor obsahuje inicializační kód, který je spuštěn ještě před zavoláním funkce main, resp. přesněji řečeno před sekvencí instrukcí, které vzniknou překladem zdrojového kódu funkce main. Musíme si totiž uvědomit, že NES neobsahuje žádnou ROM s BIOSem ani operačním systémem, takže i samotná inicializace herní konzole po RESETu je plně ponechána na programátorovi.
A právě tato inicializace mikroprocesoru i všech dalších čipů herní konzole NES (především pak čipu PPU, jenž se stará o zobrazení herní scény) je popsána v souboru crt0.s. Povšimněte si, že se provádí již zmíněná inicializace PPU, smazání video paměti, čekání na dokončení minimálně dvou snímků (tedy na stabilizaci činnosti všech čipů) a až na konci je proveden skok na kód funkce main:
; Startup code for cc65 and Shiru's NES library ; based on code by Groepaz/Hitmen <groepaz@gmx.net>, Ullrich von Bassewitz <uz@cc65.org> FT_BASE_ADR = $0100 ;page in RAM, should be $xx00 FT_DPCM_OFF = $f000 ;$c000..$ffc0, 64-byte steps FT_SFX_STREAMS = 1 ;number of sound effects played at once, 1..4 FT_THREAD = 1 ;undefine if you call sound effects in the same thread as sound update FT_PAL_SUPPORT = 1 ;undefine to exclude PAL support FT_NTSC_SUPPORT = 1 ;undefine to exclude NTSC support FT_DPCM_ENABLE = 0 ;undefine to exclude all DMC code FT_SFX_ENABLE = 1 ;undefine to exclude all sound effects code ;REMOVED initlib ;this called the CONDES function .export _exit,__STARTUP__:absolute=1 .import push0,popa,popax,_main,zerobss,copydata ; Linker generated symbols .import __STACK_START__ ,__STACKSIZE__ ;changed .import __ROM0_START__ ,__ROM0_SIZE__ .import __STARTUP_LOAD__,__STARTUP_RUN__,__STARTUP_SIZE__ .import __CODE_LOAD__ ,__CODE_RUN__ ,__CODE_SIZE__ .import __RODATA_LOAD__ ,__RODATA_RUN__ ,__RODATA_SIZE__ .import NES_MAPPER, NES_PRG_BANKS, NES_CHR_BANKS, NES_MIRRORING .importzp _PAD_STATE, _PAD_STATET ;added .include "zeropage.inc" PPU_CTRL =$2000 PPU_MASK =$2001 PPU_STATUS =$2002 PPU_OAM_ADDR=$2003 PPU_OAM_DATA=$2004 PPU_SCROLL =$2005 PPU_ADDR =$2006 PPU_DATA =$2007 PPU_OAM_DMA =$4014 PPU_FRAMECNT=$4017 DMC_FREQ =$4010 CTRL_PORT1 =$4016 CTRL_PORT2 =$4017 OAM_BUF =$0200 PAL_BUF =$01c0 VRAM_BUF =$0700 .segment "ZEROPAGE" NTSC_MODE: .res 1 FRAME_CNT1: .res 1 FRAME_CNT2: .res 1 VRAM_UPDATE: .res 1 NAME_UPD_ADR: .res 2 NAME_UPD_ENABLE: .res 1 PAL_UPDATE: .res 1 PAL_BG_PTR: .res 2 PAL_SPR_PTR: .res 2 SCROLL_X: .res 1 SCROLL_Y: .res 1 SCROLL_X1: .res 1 SCROLL_Y1: .res 1 PAD_STATE: .res 2 ;one byte per controller PAD_STATEP: .res 2 PAD_STATET: .res 2 PPU_CTRL_VAR: .res 1 PPU_CTRL_VAR1: .res 1 PPU_MASK_VAR: .res 1 RAND_SEED: .res 2 FT_TEMP: .res 3 TEMP: .res 11 SPRID: .res 1 PAD_BUF =TEMP+1 PTR =TEMP ;word LEN =TEMP+2 ;word NEXTSPR =TEMP+4 SCRX =TEMP+5 SCRY =TEMP+6 SRC =TEMP+7 ;word DST =TEMP+9 ;word RLE_LOW =TEMP RLE_HIGH =TEMP+1 RLE_TAG =TEMP+2 RLE_BYTE =TEMP+3 ;nesdoug code requires VRAM_INDEX: .res 1 META_PTR: .res 2 DATA_PTR: .res 2 .segment "HEADER" .byte $4e,$45,$53,$1a .byte <NES_PRG_BANKS .byte <NES_CHR_BANKS .byte <NES_MIRRORING|(<NES_MAPPER<<4) .byte <NES_MAPPER&$f0 .res 8,0 .segment "STARTUP" start: _exit: sei cld ldx #$40 stx CTRL_PORT2 ldx #$ff txs inx stx PPU_MASK stx DMC_FREQ stx PPU_CTRL ;no NMI initPPU: bit PPU_STATUS @1: bit PPU_STATUS bpl @1 @2: bit PPU_STATUS bpl @2 clearPalette: lda #$3f sta PPU_ADDR stx PPU_ADDR lda #$0f ldx #$20 @1: sta PPU_DATA dex bne @1 clearVRAM: txa ldy #$20 sty PPU_ADDR sta PPU_ADDR ldy #$10 @1: sta PPU_DATA inx bne @1 dey bne @1 clearRAM: txa @1: sta $000,x sta $100,x sta $200,x sta $300,x sta $400,x sta $500,x sta $600,x sta $700,x inx bne @1 lda #4 jsr _pal_bright jsr _pal_clear jsr _oam_clear jsr zerobss jsr copydata lda #<(__STACK_START__+__STACKSIZE__) ;changed sta sp lda #>(__STACK_START__+__STACKSIZE__) sta sp+1 ; Set argument stack ptr ; jsr initlib ; removed. this called the CONDES function lda #%10000000 sta <PPU_CTRL_VAR sta PPU_CTRL ;enable NMI lda #%00000110 sta <PPU_MASK_VAR waitSync3: lda <FRAME_CNT1 @1: cmp <FRAME_CNT1 beq @1 detectNTSC: ldx #52 ;blargg's code ldy #24 @1: dex bne @1 dey bne @1 lda PPU_STATUS and #$80 sta <NTSC_MODE jsr _ppu_off lda #0 ldx #0 jsr _set_vram_update lda #$fd sta <RAND_SEED sta <RAND_SEED+1 lda #0 sta PPU_SCROLL sta PPU_SCROLL jmp _main ;no parameters .include "neslib.s" .segment "RODATA" music_data: ; .include "music.s" .if(FT_SFX_ENABLE) sounds_data: ; .include "sounds.s" .endif .segment "SAMPLES" ; .incbin "music_dpcm.bin" .segment "VECTORS" .word nmi ;$fffa vblank nmi .word start ;$fffc reset .word irq ;$fffe irq / brk .segment "CHARS" .incbin "Alpha.chr"
; Obslužná rutina pro RESET .proc reset ; nastavení stavu CPU setup_cpu ; nastavení řídicích registrů ldx #$40 stx $4017 ; zákaz IRQ z APU ldx #$00 stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI) stx PPUMASK ; nastavení PPUMASK = 0 stx DMC_FREQ ; zákaz DMC IRQ ldx #$40 stx $4017 ; interrupt inhibit bit ; čekání na vnitřní inicializaci PPU (dva snímky) wait_for_frame wait_for_frame ; vymazání obsahu RAM clear_ram ; čekání na další snímek wait_for_frame cli ; vynulování bitu I - povolení přerušení lda #%10010000 sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!) .endproc
5. Knihovna neslib
V projektu kromě nezbytného souboru crt.0 nalezneme i soubory nazvané neslib.h a neslib.s. Jedná se o obdobu standardní knihovny céčka, která obsahuje konstanty a funkce používané při ovládání herní konzole NES. Tato knihovna je postupně rozšiřována a optimalizována, což je ostatně patrné i při pohledu na první řádky v hlavičkovém souboru neslib.h:
//NES hardware-dependent functions by Shiru (shiru@mail.ru) //Feel free to do anything you want with this code, consider it Public Domain // for nesdoug version 1.2, 1/1/2022 // changes, removed sprid from oam functions, oam_spr 11% faster, meta 5% faster //Versions history: // 050517 - pad polling code optimized, button bits order reversed // 280215 - fixed palette glitch caused by the active DMC DMA glitch // 030914 - minor fixes in the VRAM update system // 310814 - added vram_flush_update // 120414 - removed adr argument from vram_write and vram_read, // unrle_vram renamed to vram_unrle, with adr argument removed // 060414 - many fixes and improvements, including sequential VRAM updates // previous versions were created since mid-2011, there were many updates
Samotná knihovna NESlib je sice implementována v assembleru, ovšem vzhledem k tomu, že její funkce mají být volány z céčka, obsahuje i hlavičkový soubor neslib.h. Ten v první řadě obsahuje hlavičky funkcí, z nichž prakticky všechny používají modifikátor __fastcall__ používaný u běžných NEvariadických funkcí (tedy u funkcí s pevným počtem parametrů). Díky tomuto modifikátoru je práce překladače zjednodušena, protože se zkrátí kód pro předávání parametrů.
Nalezneme zde například funkce pro práci s barvovou paletou NESu, které využijeme v demonstračních příkladech (samotná paleta je představována polem s prvky typu char, resp. unsigned char):
void __fastcall__ pal_all(const char *data); void __fastcall__ pal_bg(const char *data); void __fastcall__ pal_spr(const char *data); void __fastcall__ pal_col(unsigned char index,unsigned char color); void __fastcall__ pal_clear(void);
Dále využijeme funkce ovládající čip PPU, jenž generuje v NESu grafický obraz:
void __fastcall__ ppu_wait_nmi(void); void __fastcall__ ppu_wait_frame(void); void __fastcall__ ppu_off(void); void __fastcall__ ppu_on_all(void);
Z obecných funkcí jedná především o dvojici užitečných funkcí memcpy a memfil:
void __fastcall__ memcpy(void *dst,void *src,unsigned int len); void __fastcall__ memfill(void *dst,unsigned char value,unsigned int len);
Dále v hlavičkovém souboru nalezneme množství užitečných konstant, například masky příznaků jednotlivých tlačítek ovladačů (D-padů):
#define PAD_A 0x80 #define PAD_B 0x40 #define PAD_SELECT 0x20 #define PAD_START 0x10 #define PAD_UP 0x08 #define PAD_DOWN 0x04 #define PAD_LEFT 0x02 #define PAD_RIGHT 0x01
Další tři konstanty použijeme příště při práci se sprity – jedná se o masky atributů spritů:
#define OAM_FLIP_V 0x80 #define OAM_FLIP_H 0x40 #define OAM_BEHIND 0x20
V souboru neslib.h se taktéž nachází definice několika adres, například začátků paměťových regionů s uloženými dlaždicemi pozadí:
#define NAMETABLE_A 0x2000 #define NAMETABLE_B 0x2400 #define NAMETABLE_C 0x2800 #define NAMETABLE_D 0x2c00
A konečně zde najdeme i několik užitečných maker, zejména makra pro výpočet adresy v paměťových regionech s uloženými dlaždicemi pozadí:
#define NTADR_A(x,y) (NAMETABLE_A|(((y)<<5)|(x))) #define NTADR_B(x,y) (NAMETABLE_B|(((y)<<5)|(x))) #define NTADR_C(x,y) (NAMETABLE_C|(((y)<<5)|(x))) #define NTADR_D(x,y) (NAMETABLE_D|(((y)<<5)|(x)))
Soubor neslib.s je napsán v assembleru. Všechny funkce, jejichž hlavičky jsou uvedeny v souboru neslib.h jsou nejprve exportovány; přesněji řečeno jsou exportovány symboly s adresami těchto funkcí:
.export _pal_all,_pal_bg,_pal_spr,_pal_col,_pal_clear .export _pal_bright,_pal_spr_bright,_pal_bg_bright .export _ppu_off,_ppu_on_all,_ppu_on_bg,_ppu_on_spr,_ppu_mask,_ppu_system .export _oam_clear,_oam_size,_oam_spr,_oam_meta_spr,_oam_hide_rest .export _ppu_wait_frame,_ppu_wait_nmi .export _scroll,_split .export _bank_spr,_bank_bg .export _vram_read,_vram_write .export _sfx_play,_sample_play .export _pad_poll,_pad_trigger,_pad_state .export _rand8,_rand16,_set_rand .export _vram_adr,_vram_put,_vram_fill,_vram_inc,_vram_unrle .export _set_vram_update,_flush_vram_update .export _memcpy,_memfill,_delay .export _flush_vram_update2, _oam_set, _oam_get
Implementace jednotlivých funkcí vždy začínají návěštím (label), jehož adresa je následně použita linkerem. Příkladem mohou být funkce ppu_off a ppu_on_all, jejichž implementace v assembleru vypadá takto:
_ppu_off: lda <PPU_MASK_VAR and #%11100111 sta <PPU_MASK_VAR jmp _ppu_wait_nmi _ppu_on_all: lda <PPU_MASK_VAR ora #%00011000
Implementace funkce pracující s barvovou paletou:
_pal_bg: sta <PTR stx <PTR+1 ldx #$00 lda #$10 bne pal_copy ;bra
Uvnitř některých implementovaných funkcí se používají lokální návěští:
@skipUpd: lda #0 sta PPU_ADDR sta PPU_ADDR lda <SCROLL_X sta PPU_SCROLL lda <SCROLL_Y sta PPU_SCROLL lda <PPU_CTRL_VAR sta PPU_CTRL @skipAll: lda <PPU_MASK_VAR sta PPU_MASK inc <FRAME_CNT1 inc <FRAME_CNT2 lda <FRAME_CNT2 cmp #6 bne @skipNtsc lda #0 sta <FRAME_CNT2
V navazujícím článku využijeme i funkci oam_clear pracující s pamětí pro sprity:
_oam_clear: ldx #0 stx SPRID ; automatically sets sprid to zero lda #$ff @1: sta OAM_BUF,x inx inx inx inx bne @1 rts
A konečně se pro zajímavost podívejme na implementaci funkcí memcpy a memfill, u nichž se předpokládá velká míra optimalizace:
;void __fastcall__ memcpy(void *dst,void *src,unsigned int len); _memcpy: sta <LEN stx <LEN+1 jsr popax sta <SRC stx <SRC+1 jsr popax sta <DST stx <DST+1 ldx #0 @1: lda <LEN+1 beq @2 jsr @3 dec <LEN+1 inc <SRC+1 inc <DST+1 jmp @1 @2: ldx <LEN beq @5 @3: ldy #0 @4: lda (SRC),y sta (DST),y iny dex bne @4 @5: rts ;void __fastcall__ memfill(void *dst,unsigned char value,unsigned int len); _memfill: sta <LEN stx <LEN+1 jsr popa sta <TEMP jsr popax sta <DST stx <DST+1 ldx #0 @1: lda <LEN+1 beq @2 jsr @3 dec <LEN+1 inc <DST+1 jmp @1 @2: ldx <LEN beq @5 @3: ldy #0 lda <TEMP @4: sta (DST),y iny dex bne @4 @5: rts
6. Překlad a slinkování celého projektu
Pro zjednodušení překladu a slinkování celého projektu je určen soubor Makefile, jenž je interpretován standardním nástrojem GNU Make (není tedy součástí instalace ca65 ani cc65). V Makefile jsou zapsána pravidla pro překlad zdrojového kódu v C do assembleru, pro překlad souborů zapsaných v assembleru do objektového kódu i pravidla pro slinkování objektových souborů do výsledného obrazu NESovské cartridge:
CC65 = cc65 CA65 = ca65 LD65 = ld65 NAME = intro CFG = nrom_32k_vert.cfg .PHONY: default clean default: $(NAME).nes $(NAME).nes: $(NAME).o crt0.o $(CFG) $(LD65) -C $(CFG) -o $(NAME).nes crt0.o $(NAME).o nes.lib crt0.o: crt0.s Alpha.chr $(CA65) crt0.s $(NAME).o: $(NAME).s $(CA65) $(NAME).s -g $(NAME).s: $(NAME).c $(CC65) -Oirs $(NAME).c --add-source clean: rm -f $(NAME).nes rm -f *.o
Překlad provedeme snadno, a to příkazem:
$ make
Měly by se postupně provést následující kroky: překlad zdrojového kódu (*.c) překladačem céčka do assembleru (*.s), dále překlad z kódu zapsaného v assembleru (*.s) do objektového kódu (*.o) a následně slinkování do souboru intro.nes, což je obraz cartridge NESu:
cc65 -Oirs intro.c --add-source ca65 intro.s -g ca65 crt0.s ld65 -C nrom_32k_vert.cfg -o intro.nes crt0.o intro.o nes.lib
7. Výsledek překladu ve formě assembleru
Překladač cc65 nevygeneroval přímo objektový kód (tedy soubory s koncovkou .o), ale provedl překlad zdrojových kódů z céčka do assembleru (s tím, že druhá fáze překladu bude provedena assemblerem as65). Výsledkem překladu souboru nazvaného intro.c je tedy soubor se jménem intro.s, jenž obsahuje, jak je ostatně patrné, pouze kód získaný překladem céčka – prozatím se tedy neprovádí spojení s kódem z knihovny NESlib ani s kódem, jenž je zapsán ve výše zmíněném souboru crt0.s.
Nejprve zde kromě konfiguračních voleb nalezneme import symbolů tří funkcí z NESlibu a naopak export symbolů tří funkcí, které zapsal přímo programátor:
.import _pal_bg .import _ppu_off .import _ppu_on_all .export _palette .export _game_loop .export _main
Dále je definována sekce (segment) s neměnnými daty, v nichž je deklarováno pole s barvovou paletou:
.segment "RODATA" _palette: .byte $22 .byte $00 .byte $10 ... ... ... .byte $30 .byte $0F .byte $0F .byte $0F
A konečně ve vygenerovaném kódu nalezneme i funkce původně psané v jazyku C, které byly přeloženy do assembleru do formy procedur/subrutin. Příkladem je funkce game_loop, jejíž exportovaný symbol je změněn na _game_loop, jak je tomu ve světě céčka dobrým zvykem:
; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L001D: jmp L001D .endproc
Takto vypadá celý soubor intro.s (žádná velká „magie“ zde tedy není použita; vše je přímočaré):
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .import _pal_bg .import _ppu_off .import _ppu_on_all .export _palette .export _game_loop .export _main .segment "RODATA" _palette: .byte $22 .byte $00 .byte $10 .byte $30 .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F ; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L001D: jmp L001D .endproc ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; ppu_off(); ; jsr _ppu_off ; ; pal_bg(palette); ; lda #<(_palette) ldx #>(_palette) jsr _pal_bg ; ; ppu_on_all(); ; jsr _ppu_on_all ; ; game_loop(); ; jmp _game_loop .endproc
8. Spuštění vygenerovaného obrazu cartridge
Výsledkem překladu a slinkování dnešního prvního demonstračního příkladu je soubor nazvaný intro.nes. Jedná se o přesný obraz cartridge (resp. paměti v cartridgi) pro reálnou herní konzoli NES. Tento obraz můžeme spustit v prakticky libovolném emulátoru NESu. V mém případě používám emulátor nazvaný Nestopia. Náš příklad se v tomto emulátoru spustí následovně:
$ nestopia intro.nes
Výsledek by měl vypadat následovně:
Obrázek 2: Herní scéna zobrazená po spuštění dnešního prvního demonstračního příkladu v emulátoru herní konzole NES.
9. Přístup do paměti PPU – vykreslení pozadí herní scény
Připomeňme si, že 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 3: Takto vypadají dlaždice pro pozadí zobrazené v k tomu určeném grafickém editoru.
Pole s indexy dlaždic pro zobrazení pozadí se nazývá tabulka jmen. Ta je z pohledu PPU (nikoli CPU!) uložena od adresy $2000 a má délku přesně 960 bajtů (32×30). Ihned za touto tabulkou se nachází tabulka s atributy (použijeme později). Vyplnění této tabulky by v assembleru vypadalo přibližně takto – data se přenáší do PPU a pro jednoduchost přeneseme jen 256 bajtů, tedy jedinou stránku paměti:
; načtení tabulky jmen .proc load_nametable lda PPUSTATUS ; reset záchytného registru lda #>NAME_TABLE_0 ; horní bajt adresy sta PPUADDR lda #<NAME_TABLE_0 ; spodní bajt adresy sta PPUADDR ldx #$00 ; počitadlo : lda nametabledata,X sta PPUDATA ; zápis indexu dlaždice inx cpx #$80 ; chceme přenést 128 bajtů bne :- rts ; návrat ze subrutiny .endproc
V céčku se nemusíme tímto nízkoúrovňovým kódem příliš zabývat, protože postačuje napsat jednoduchou programovou smyčku, v níž dlaždice vyplníme hodnotami 0, 1, …255, 0, 1, … Povšimněte si, že nejdříve musíme nastavit ukazatel zápisu na první dlaždici funkcí vram_adr s makrem NTADR_D a poté se již ve funkci vram_put specifikují pouze indexy dlaždic, nikoli jejich souřadnice:
void fill_in_ppu_ram(void) { vram_adr(NTADR_A(0, 0)); for (i = 0; i < 32 * 30; i++) { vram_put(i); } }
Dále si povšimněte, že čítač předchozí programové smyčky, tedy proměnná i, není deklarována jako proměnná lokální, jak by se slušelo. Takto deklarovaná proměnná by se totiž vytvářela na zásobníku a na tento koncept není procesor MOS 6502 příliš dobře připraven. Z hlediska velikosti a rychlosti výsledného kódu je lepší tuto proměnnou deklarovat jako globální, ovšem uloženou v nulté stránce paměti (s tím, že se proměnná použije i v dalších částech kódu):
#pragma bss-name(push, "ZEROPAGE") int i;
Úplný zdrojový kód dnešního druhého demonstračního příkladu bude vypadat následovně:
#include "neslib.h" #define BLACK_COLOR 0x0f #define DARK_GRAY_COLOR 0x00 #define LIGHT_GRAY_COLOR 0x10 #define WHITE_COLOR 0x30 #define MARIO_BACKGROUND_COLOR 0x22 #pragma bss-name(push, "ZEROPAGE") int i; const unsigned char palette[] = { MARIO_BACKGROUND_COLOR, DARK_GRAY_COLOR, LIGHT_GRAY_COLOR, WHITE_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR, BLACK_COLOR }; void fill_in_ppu_ram(void) { vram_adr(NTADR_A(0, 0)); for (i = 0; i < 32 * 30; i++) { vram_put(i); } } void game_loop(void) { while (1) { } } void main(void) { ppu_off(); pal_bg(palette); fill_in_ppu_ram(); ppu_on_all(); game_loop(); }
Obrázek: 4: Takto vypadá tento demonstrační příklad po překladu a spuštění v emulátoru herní konzole NES.
10. Výsledek překladu do assembleru
Nyní se podívejme na to, jakým způsobem se vlastně přeloží naše nová funkce fill_in_ppu_ram, která mj. obsahuje i programovou smyčku. Tato funkce začíná specifikací adresy, od níž se má do PPU RAM provádět zápis. Tato adresa se zapamatuje v interních strukturách NESlibu:
; ; vram_adr(NTADR_A(0, 0)); ; ldx #$20 lda #$00 jsr _vram_adr
Následuje samotná realizace programové smyčky. Ta je poměrně složitá, protože se pracuje s proměnnou i, což je šestnáctibitová hodnota uložená v nulté stránce paměti. Všechny šestnáctibitové operace se musí rozkládat na dvojici osmibitových operací a navíc je součet (resp. zvýšení hodnoty i o 1) realizován poněkud podivným způsobem – testem přetečení ze spodních osmi bajtů následovaný podmíněným skokem, pokud k přetečení nedošlo (pokud došlo, zvýší se horní bajt proměnné):
lda _i ldx _i+1 clc adc #$01 bcc L0023 inx L0023: sta _i stx _i+1
Celá realizace smyčky tedy není v žádném případě optimalizována:
; ; for (i = 0; i < 32 * 30; i++) { ; lda #$00 sta _i sta _i+1 L001A: lda _i cmp #$C0 lda _i+1 sbc #$03 bvc L0021 eor #$80 L0021: bpl L001B ; ; vram_put(i); ; lda _i jsr _vram_put lda _i ldx _i+1 clc adc #$01 bcc L0023 inx L0023: sta _i stx _i+1 jmp L001A
A pro úplnost: takto vypadá celý soubor ppu_ram.s:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .import _pal_bg .import _ppu_off .import _ppu_on_all .import _vram_adr .import _vram_put .export _i .export _palette .export _fill_in_ppu_ram .export _game_loop .export _main .segment "RODATA" _palette: .byte $22 .byte $00 .byte $10 .byte $30 .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .byte $0F .segment "BSS" .segment "ZEROPAGE" _i: .res 2,$00 ; --------------------------------------------------------------- ; void __near__ fill_in_ppu_ram (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_ppu_ram: near .segment "CODE" ; ; vram_adr(NTADR_A(0, 0)); ; ldx #$20 lda #$00 jsr _vram_adr ; ; for (i = 0; i < 32 * 30; i++) { ; lda #$00 sta _i sta _i+1 L001A: lda _i cmp #$C0 lda _i+1 sbc #$03 bvc L0021 eor #$80 L0021: bpl L001B ; ; vram_put(i); ; lda _i jsr _vram_put ; ; for (i = 0; i < 32 * 30; i++) { ; lda _i ldx _i+1 clc adc #$01 bcc L0023 inx L0023: sta _i stx _i+1 jmp L001A ; ; } ; L001B: rts .endproc ; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L0032: jmp L0032 .endproc ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; ppu_off(); ; jsr _ppu_off ; ; pal_bg(palette); ; lda #<(_palette) ldx #>(_palette) jsr _pal_bg ; ; fill_in_ppu_ram(); ; jsr _fill_in_ppu_ram ; ; ppu_on_all(); ; jsr _ppu_on_all ; ; game_loop(); ; jmp _game_loop .endproc
11. Modifikace barvových atributů
Při definici pozadí herního světa se kromě tabulky s dlaždicemi (name table) o rozměrech 32×30 dlaždic používá i mnohem menší tabulka atributů. Tato tabulka má velikost pouhých 64 bajtů a obsahuje horní dva bity (výběr barvové palety) pro oblast o velikosti 4×4 dlaždice, tj. 32×32 pixelů. I tuto tabulku můžeme v céčku velmi snadno modifikovat, resp. vyplnit, protože nám knihovna NESlib dává k dispozici potřebné funkce. Zkusme tedy vyplnit vždy jednu čtvrtinu této tabulky (tedy šestnáct bajtů) stejnou hodnotou. Výsledkem by mělo být, že na obrazovce se objeví čtyři regiony, každý s odlišnou barvou dlaždic:
void fill_in_attributes(void) { vram_adr(ATTRIBUTE_TABLE); vram_fill(0, 16); vram_fill(0x55, 16); vram_fill(0xAA, 16); vram_fill(0xFF, 16); }
Celý zdrojový kód dnešního třetího demonstračního příkladu naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/attributes.c:
#include "neslib.h" #define ATTRIBUTE_TABLE 0x23c0 #pragma bss-name(push, "ZEROPAGE") int i; int address; const unsigned char palette[16] = { 0x0f, 0x00, 0x10, 0x30, 0x0f, 0x01, 0x21, 0x31, 0x0f, 0x06, 0x26, 0x36, 0x0f, 0x09, 0x29, 0x39 }; void fill_in_ppu_ram(void) { vram_adr(NTADR_A(0, 0)); for (i = 0; i < 32 * 30; i++) { vram_put(i); } } void fill_in_attributes(void) { vram_adr(ATTRIBUTE_TABLE); vram_fill(0, 16); vram_fill(0x55, 16); vram_fill(0xAA, 16); vram_fill(0xFF, 16); } void game_loop(void) { while (1) { } } void main(void) { ppu_off(); pal_bg(palette); fill_in_ppu_ram(); fill_in_attributes(); ppu_on_all(); game_loop(); }
Po spuštění v emulátoru herní konzole je patrné, že se barvové atributy skutečně změnily podle očekávání:
Obrázek 5: Herní scéna vytvořená třetím demonstračním příkladem po spuštění v emulátoru herní konzole NES.
12. Výsledek překladu do assembleru
Opět se podívejme na to, jak bude vypadat výsledek překladu našeho céčkového programu do assembleru. Zajímat nás pochopitelně bude především způsob přeložení funkce fill_in_attributes. Jedná se vlastně „pouze“ o sekvenci volání příslušné subrutiny s předáním parametrů:
; ; vram_adr(ATTRIBUTE_TABLE); ; ldx #$23 lda #$C0 jsr _vram_adr ; ; vram_fill(0, 16); ; lda #$00 jsr pusha tax lda #$10 jsr _vram_fill ; ; vram_fill(0x55, 16); ; lda #$55 jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xAA, 16); ; lda #$AA jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xFF, 16); ; lda #$FF jsr pusha ldx #$00 lda #$10 jmp _vram_fill .endproc
Samotná volaná subrutina je součástí NESlibu a vypadá takto (opět tedy platí, že v assembleru bychom dosáhli kratšího a rychlejšího kódu – to je daň za použití céčka a předpřipravených obecných knihoven):
_vram_fill: sta <LEN stx <LEN+1 jsr popa ldx <LEN+1 beq @2 ldx #0 @1: sta PPU_DATA dex bne @1 dec <LEN+1 bne @1 @2: ldx <LEN beq @4 @3: sta PPU_DATA dex bne @3 @4: rts
Pro úplnost se podívejme na to, jak vlastně dopadl překlad celého kódu:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .import _pal_bg .import _ppu_off .import _ppu_on_all .import _vram_adr .import _vram_put .import _vram_fill .export _i .export _address .export _palette .export _fill_in_ppu_ram .export _fill_in_attributes .export _game_loop .export _main .segment "RODATA" _palette: .byte $0F .byte $00 .byte $10 .byte $30 .byte $0F .byte $01 .byte $21 .byte $31 .byte $0F .byte $06 .byte $26 .byte $36 .byte $0F .byte $09 .byte $29 .byte $39 .segment "BSS" .segment "ZEROPAGE" _i: .res 2,$00 _address: .res 2,$00 ; --------------------------------------------------------------- ; void __near__ fill_in_ppu_ram (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_ppu_ram: near .segment "CODE" ; ; vram_adr(NTADR_A(0, 0)); ; ldx #$20 lda #$00 jsr _vram_adr ; ; for (i = 0; i < 32 * 30; i++) { ; lda #$00 sta _i sta _i+1 L001B: lda _i cmp #$C0 lda _i+1 sbc #$03 bvc L0022 eor #$80 L0022: bpl L001C ; ; vram_put(i); ; lda _i jsr _vram_put ; ; for (i = 0; i < 32 * 30; i++) { ; lda _i ldx _i+1 clc adc #$01 bcc L0024 inx L0024: sta _i stx _i+1 jmp L001B ; ; } ; L001C: rts .endproc ; --------------------------------------------------------------- ; void __near__ fill_in_attributes (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_attributes: near .segment "CODE" ; ; vram_adr(ATTRIBUTE_TABLE); ; ldx #$23 lda #$C0 jsr _vram_adr ; ; vram_fill(0, 16); ; lda #$00 jsr pusha tax lda #$10 jsr _vram_fill ; ; vram_fill(0x55, 16); ; lda #$55 jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xAA, 16); ; lda #$AA jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xFF, 16); ; lda #$FF jsr pusha ldx #$00 lda #$10 jmp _vram_fill .endproc ; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L0043: jmp L0043 .endproc ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; ppu_off(); ; jsr _ppu_off ; ; pal_bg(palette); ; lda #<(_palette) ldx #>(_palette) jsr _pal_bg ; ; fill_in_ppu_ram(); ; jsr _fill_in_ppu_ram ; ; fill_in_attributes(); ; jsr _fill_in_attributes ; ; ppu_on_all(); ; jsr _ppu_on_all ; ; game_loop(); ; jmp _game_loop .endproc
13. Využití definice dlaždic z herního světa Maria
V dnešním čtvrtém demonstračním příkladu, který naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A, jsou provedeny pouze dvě změny, které však mají velký dopad na to, jaké objekty je vlastně možné zobrazit v herním světě. Jedna ze změn spočívá v tom, že barvová paleta obsahuje nejenom definici šestnácti barev pozadí, ale i definici šestnácti barev používaných sprity:
const unsigned char palette[32] = { 0x22, 0x29, 0x1a, 0x0F, 0x22, 0x36, 0x17, 0x0F, 0x22, 0x30, 0x21, 0x0F, 0x22, 0x27, 0x17, 0x0F, // barvy pozadí 0x22, 0x16, 0x27, 0x18, 0x22, 0x1A, 0x30, 0x27, 0x22, 0x16, 0x30, 0x27, 0x22, 0x0F, 0x36, 0x17, // barvy spritů };
Druhou změnou je nahrazení binárního souboru Alpha.chr za soubor mario.chr, s čímž souvisí i nepatrné úpravy souboru Makefile a crt0.s. Připomeňme si, že soubory *.chr mají typicky velikost 8192 bajtů. V těchto souborech je uložena definice 256 dlaždic pozadí a 256 dlaždic spritů. Každá dlaždice má velikost 8×8 pixelů, přičemž každý pixel je definován dvojicí bitů. To znamená, že jedna dlaždice je uložena v šestnácti bitech a tedy celková velikost tohoto binárního souboru je skutečně rovna 2×256×(8×8/4)=8192 bajtům.
Po překladu a spuštění tohoto demonstračního příkladu se zobrazí následující obraz:
Obrázek 6: Všechny dostupné dlaždice zobrazené postupně ve čtyřech kombinacích barev.
Žádné další změny (kromě změny palety zmíněné výše) ve zdrojovém kódu provedeny nejsou:
#include "neslib.h" #define ATTRIBUTE_TABLE 0x23c0 #pragma bss-name(push, "ZEROPAGE") int i; int address; const unsigned char palette[32] = { 0x22, 0x29, 0x1a, 0x0F, 0x22, 0x36, 0x17, 0x0F, 0x22, 0x30, 0x21, 0x0F, 0x22, 0x27, 0x17, 0x0F, // barvy pozadí 0x22, 0x16, 0x27, 0x18, 0x22, 0x1A, 0x30, 0x27, 0x22, 0x16, 0x30, 0x27, 0x22, 0x0F, 0x36, 0x17, // barvy spritů }; void fill_in_ppu_ram(void) { vram_adr(NTADR_A(0, 0)); for (i = 0; i < 32 * 30; i++) { vram_put(i); } } void fill_in_attributes(void) { vram_adr(ATTRIBUTE_TABLE); vram_fill(0, 16); vram_fill(0x55, 16); vram_fill(0xAA, 16); vram_fill(0xFF, 16); } void game_loop(void) { while (1) { } } void main(void) { ppu_off(); pal_bg(palette); fill_in_ppu_ram(); fill_in_attributes(); ppu_on_all(); game_loop(); }
14. Výsledek překladu do assembleru
Bez dalších podrobností si ukažme, jak se zdrojový kód z předchozí kapitoly přeložil do assembleru:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .import _pal_bg .import _ppu_off .import _ppu_on_all .import _vram_adr .import _vram_put .import _vram_fill .export _i .export _address .export _palette .export _fill_in_ppu_ram .export _fill_in_attributes .export _game_loop .export _main .segment "RODATA" _palette: .byte $22 .byte $29 .byte $1A .byte $0F .byte $22 .byte $36 .byte $17 .byte $0F .byte $22 .byte $30 .byte $21 .byte $0F .byte $22 .byte $27 .byte $17 .byte $0F .byte $22 .byte $16 .byte $27 .byte $18 .byte $22 .byte $1A .byte $30 .byte $27 .byte $22 .byte $16 .byte $30 .byte $27 .byte $22 .byte $0F .byte $36 .byte $17 .segment "BSS" .segment "ZEROPAGE" _i: .res 2,$00 _address: .res 2,$00 ; --------------------------------------------------------------- ; void __near__ fill_in_ppu_ram (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_ppu_ram: near .segment "CODE" ; ; vram_adr(NTADR_A(0, 0)); ; ldx #$20 lda #$00 jsr _vram_adr ; ; for (i = 0; i < 32 * 30; i++) { ; lda #$00 sta _i sta _i+1 L002B: lda _i cmp #$C0 lda _i+1 sbc #$03 bvc L0032 eor #$80 L0032: bpl L002C ; ; vram_put(i); ; lda _i jsr _vram_put ; ; for (i = 0; i < 32 * 30; i++) { ; lda _i ldx _i+1 clc adc #$01 bcc L0034 inx L0034: sta _i stx _i+1 jmp L002B ; ; } ; L002C: rts .endproc ; --------------------------------------------------------------- ; void __near__ fill_in_attributes (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_attributes: near .segment "CODE" ; ; vram_adr(ATTRIBUTE_TABLE); ; ldx #$23 lda #$C0 jsr _vram_adr ; ; vram_fill(0, 16); ; lda #$00 jsr pusha tax lda #$10 jsr _vram_fill ; ; vram_fill(0x55, 16); ; lda #$55 jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xAA, 16); ; lda #$AA jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xFF, 16); ; lda #$FF jsr pusha ldx #$00 lda #$10 jmp _vram_fill .endproc ; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L0053: jmp L0053 .endproc ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; ppu_off(); ; jsr _ppu_off ; ; pal_bg(palette); ; lda #<(_palette) ldx #>(_palette) jsr _pal_bg ; ; fill_in_ppu_ram(); ; jsr _fill_in_ppu_ram ; ; fill_in_attributes(); ; jsr _fill_in_attributes ; ; ppu_on_all(); ; jsr _ppu_on_all ; ; game_loop(); ; jmp _game_loop .endproc
15. Specifikace, která sada dlaždic se má použít pro vykreslování pozadí
Na screenshotu uvedeném ve třinácté kapitole je patrné, že se namísto dlaždic pozadí zobrazují dlaždice, které jsou určeny pro zobrazení spritů. Musíme být tedy schopni nějakým způsobem informovat PPU o tom, kterou sadu dlaždic má vlastně pro zobrazení pozadí použít. Tuto vlastnost, kterou jsme v předchozích částech tohoto seriálu nemuseli používat (protože jsme využili vlastní inicializační kód odlišný od crt0.s) lze snadno zajistit zavoláním funkce bank_bg, které se předá číslo „banky“, což je pro běžné cartridge hodnota 0 nebo 1. Dlaždice pozadí jsou v našem obrazu cartridge uloženy (s celkem 512 dlaždicemi uloženými v osmi kilobajtech) v bance číslo 1 a proto bude tělo funkce main, v němž se provádí inicializace, vypadat takto:
ppu_off(); pal_bg(palette); bank_bg(1); fill_in_ppu_ram(); fill_in_attributes(); ppu_on_all(); game_loop();
Samotná funkce bank_bg je realizována v assembleru. Nastavuje se v ní obsah řídicího registru PPU_CTRL. Povšimněte si, že se vstupní číslo banku maskuje hodnotou 1, což znamená, že skutečně má význam použít bank 0 nebo 1. Dále je tato nyní již jednobitová hodnota posunuta na pátý bit a obsah registru je modifikován (u moderních procesorů by se jednalo o jedinou instrukci):
_bank_bg: and #$01 asl a asl a asl a asl a sta <TEMP lda <PPU_CTRL_VAR and #%11101111 ora <TEMP sta <PPU_CTRL_VAR rts
Nyní již bude výsledek korektní a to znamená, že takto opravený demonstrační příklad bude použit jako základ pro příklady, které si uvedeme příště:
Obrázek 7: Všechny dostupné dlaždice zobrazené postupně ve čtyřech kombinacích barev.
16. Opravený příklad ze třinácté kapitoly
Takto vypadá (jen pro úplnost) celý zdrojový kód dnešního posledního demonstračního příkladu. Tento kód naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/mario_world.c:
#include "neslib.h" #define ATTRIBUTE_TABLE 0x23c0 #pragma bss-name(push, "ZEROPAGE") int i; int address; const unsigned char palette[32] = { 0x22, 0x29, 0x1a, 0x0F, 0x22, 0x36, 0x17, 0x0F, 0x22, 0x30, 0x21, 0x0F, 0x22, 0x27, 0x17, 0x0F, // barvy pozadí 0x22, 0x16, 0x27, 0x18, 0x22, 0x1A, 0x30, 0x27, 0x22, 0x16, 0x30, 0x27, 0x22, 0x0F, 0x36, 0x17, // barvy spritů }; void fill_in_ppu_ram(void) { vram_adr(NTADR_A(0, 0)); for (i = 0; i < 32 * 30; i++) { vram_put(i); } } void fill_in_attributes(void) { vram_adr(ATTRIBUTE_TABLE); vram_fill(0, 16); vram_fill(0x55, 16); vram_fill(0xAA, 16); vram_fill(0xFF, 16); } void game_loop(void) { while (1) { } } void main(void) { ppu_off(); pal_bg(palette); bank_bg(1); fill_in_ppu_ram(); fill_in_attributes(); ppu_on_all(); game_loop(); }
17. Výsledek překladu do assembleru
Na závěr si ještě ukažme, jak se kód z předchozí kapitoly uložit do assembleru:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .import _pal_bg .import _ppu_off .import _ppu_on_all .import _bank_bg .import _vram_adr .import _vram_put .import _vram_fill .export _i .export _address .export _palette .export _fill_in_ppu_ram .export _fill_in_attributes .export _game_loop .export _main .segment "RODATA" _palette: .byte $22 .byte $29 .byte $1A .byte $0F .byte $22 .byte $36 .byte $17 .byte $0F .byte $22 .byte $30 .byte $21 .byte $0F .byte $22 .byte $27 .byte $17 .byte $0F .byte $22 .byte $16 .byte $27 .byte $18 .byte $22 .byte $1A .byte $30 .byte $27 .byte $22 .byte $16 .byte $30 .byte $27 .byte $22 .byte $0F .byte $36 .byte $17 .segment "BSS" .segment "ZEROPAGE" _i: .res 2,$00 _address: .res 2,$00 ; --------------------------------------------------------------- ; void __near__ fill_in_ppu_ram (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_ppu_ram: near .segment "CODE" ; ; vram_adr(NTADR_A(0, 0)); ; ldx #$20 lda #$00 jsr _vram_adr ; ; for (i = 0; i < 32 * 30; i++) { ; lda #$00 sta _i sta _i+1 L002B: lda _i cmp #$C0 lda _i+1 sbc #$03 bvc L0032 eor #$80 L0032: bpl L002C ; ; vram_put(i); ; lda _i jsr _vram_put ; ; for (i = 0; i < 32 * 30; i++) { ; lda _i ldx _i+1 clc adc #$01 bcc L0034 inx L0034: sta _i stx _i+1 jmp L002B ; ; } ; L002C: rts .endproc ; --------------------------------------------------------------- ; void __near__ fill_in_attributes (void) ; --------------------------------------------------------------- .segment "CODE" .proc _fill_in_attributes: near .segment "CODE" ; ; vram_adr(ATTRIBUTE_TABLE); ; ldx #$23 lda #$C0 jsr _vram_adr ; ; vram_fill(0, 16); ; lda #$00 jsr pusha tax lda #$10 jsr _vram_fill ; ; vram_fill(0x55, 16); ; lda #$55 jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xAA, 16); ; lda #$AA jsr pusha ldx #$00 lda #$10 jsr _vram_fill ; ; vram_fill(0xFF, 16); ; lda #$FF jsr pusha ldx #$00 lda #$10 jmp _vram_fill .endproc ; --------------------------------------------------------------- ; void __near__ game_loop (void) ; --------------------------------------------------------------- .segment "CODE" .proc _game_loop: near .segment "CODE" ; ; while (1) { ; L0055: jmp L0055 .endproc ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; ppu_off(); ; jsr _ppu_off ; ; pal_bg(palette); ; lda #<(_palette) ldx #>(_palette) jsr _pal_bg ; ; bank_bg(1); ; lda #$01 jsr _bank_bg ; ; fill_in_ppu_ram(); ; jsr _fill_in_ppu_ram ; ; fill_in_attributes(); ; jsr _fill_in_attributes ; ; ppu_on_all(); ; jsr _ppu_on_all ; ; game_loop(); ; jmp _game_loop .endproc
18. Obsah závěrečné části seriálu
V jedenácté a současně i závěrečné části seriálu o vývoji her, popř. multimediálních dem určených pro historickou osmibitovou herní konzoli NES si ukážeme některé další možnosti, které nám kombinace překladače CC65 s knihovnou NESlib poskytují. Vykreslíme si (poněkud upravenou) úvodní obrazovku ze světa Maria, ukážeme si výpis řetězců, řekneme si, jak se pracuje se sprity i s takzvanými multisprity a nezapomeneme ani na čtení stavu ovladačů (D-pad), popř. na realizaci scrollingu celého herního světa. Z demonstračních příkladů bude zřejmé, že naprostou většinu těchto operací je možné v céčku (díky NESlibu) napsat na jediný řádek či na pouhých několik řádků (v porovnání s desítkami řádků, které vyžaduje tatáž operace zapsaná v assembleru).
Obrázek 8: Upravená úvodní obrazovka ze světa Maria vykreslená demonstračním příkladem, který si popíšeme příště.
19. Repositář s demonstračními příklady
V tabulce zobrazené pod tímto odstavcem jsou uvedeny odkazy na všechny demonstrační příklady určené pro překlad a spuštění na osmibitové herní konzoli NES, které jsou psány v céčku a určeny pro překlad pomocí cc65. Vždy se jedná o ucelené a současně i samostatně použitelné projekty, což mj. znamená, že každý příklad obsahuje i svoji lokální verzi souboru crt0.s a neslibu:
konfigurace paměťových regionů herní konzole NES# | Soubor | Stručný popis | Adresa |
---|---|---|---|
1 | 01_Intro/Makefile | Makefile pro překlad a slinkování aplikace | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/Makefile |
2 | 01_Intro/nrom_32k_vert.cfg | konfigurace paměťových regionů herní konzole NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/nrom_32k_vert.cfg |
3 | 01_Intro/Alpha.chr | binární soubor obsahující definice dlaždic pozadí a spritů | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/Alpha.chr |
4 | 01_Intro/crt0.s | inicializační rutiny naprogramované v assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/crt0.s |
5 | 01_Intro/neslib.h | hlavičkový soubor s pomocnými funkcemi pro vývoj v C pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/neslib.h |
6 | 01_Intro/neslib.s | implementace funkcí předběžně definovaných v hlavičkovém souboru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/neslib.s |
7 | 01_Intro/intro.c | zdrojový kód prvního demonstračního příkladu psaný v C | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/intro.c |
8 | 01_Intro/intro.s | demonstrační příklad přeložený do assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/intro.s |
9 | 02_PPU_RAM/Makefile | Makefile pro překlad a slinkování aplikace | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/Makefile |
10 | 02_PPU_RAM/nrom_32k_vert.cfg | konfigurace paměťových regionů herní konzole NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/nrom_32k_vert.cfg |
11 | 02_PPU_RAM/Alpha.chr | binární soubor obsahující definice dlaždic pozadí a spritů | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/Alpha.chr |
12 | 02_PPU_RAM/crt0.s | inicializační rutiny naprogramované v assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/crt0.s |
13 | 02_PPU_RAM/neslib.h | hlavičkový soubor s pomocnými funkcemi pro vývoj v C pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/neslib.h |
14 | 02_PPU_RAM/neslib.s | implementace funkcí předběžně definovaných v hlavičkovém souboru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/neslib.s |
15 | 02_PPU_RAM/ppu_ram.c | zdrojový kód druhého demonstračního příkladu psaný v C | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/ppu_ram.c |
16 | 02_PPU_RAM/ppu_ram.s | demonstrační příklad přeložený do assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/02_PPU_RAM/ppu_ram.s |
17 | 03_Attributes/Makefile | Makefile pro překlad a slinkování aplikace | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/Makefile |
18 | 03_Attributes/nrom_32k_vert.cfg | konfigurace paměťových regionů herní konzole NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/nrom_32k_vert.cfg |
19 | 03_Attributes/Alpha.chr | binární soubor obsahující definice dlaždic pozadí a spritů | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/Alpha.chr |
20 | 03_Attributes/crt0.s | inicializační rutiny naprogramované v assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/crt0.s |
21 | 03_Attributes/neslib.h | hlavičkový soubor s pomocnými funkcemi pro vývoj v C pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/neslib.h |
22 | 03_Attributes/neslib.s | implementace funkcí předběžně definovaných v hlavičkovém souboru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/neslib.s |
23 | 03_Attributes/attributes.c | zdrojový kód třetího demonstračního příkladu psaný v C | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/attributes.c |
24 | 03_Attributes/attributes.s | demonstrační příklad přeložený do assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/03_Attributes/attributes.s |
25 | 04_Mario_world_A/Makefile | Makefile pro překlad a slinkování aplikace | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/Makefile |
26 | 04_Mario_world_A/nrom_32k_vert.cfg | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/nrom_32k_vert.cfg | |
27 | 04_Mario_world_A/mario.chr | binární soubor obsahující definice dlaždic pozadí a spritů | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/mario.chr |
28 | 04_Mario_world_A/crt0.s | inicializační rutiny naprogramované v assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/crt0.s |
29 | 04_Mario_world_A/neslib.h | hlavičkový soubor s pomocnými funkcemi pro vývoj v C pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/neslib.h |
30 | 04_Mario_world_A/neslib.s | implementace funkcí předběžně definovaných v hlavičkovém souboru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/neslib.s |
31 | 04_Mario_world_A/mario_world.c | zdrojový kód čtvrtého demonstračního příkladu psaný v C | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/mario_world.c |
32 | 04_Mario_world_A/mario_world.s | demonstrační příklad přeložený do assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/04_Mario_world_A/mario_world.s |
33 | 05_Mario_world_B/Makefile | Makefile pro překlad a slinkování aplikace | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/Makefile |
34 | 05_Mario_world_B/nrom_32k_vert.cfg | konfigurace paměťových regionů herní konzole NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/nrom_32k_vert.cfg |
35 | 05_Mario_world_B/mario.chr | binární soubor obsahující definice dlaždic pozadí a spritů | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/mario.chr |
36 | 05_Mario_world_B/crt0.s | inicializační rutiny naprogramované v assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/crt0.s |
37 | 05_Mario_world_B/neslib.h | hlavičkový soubor s pomocnými funkcemi pro vývoj v C pro NES | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/neslib.h |
38 | 05_Mario_world_B/neslib.s | implementace funkcí předběžně definovaných v hlavičkovém souboru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/neslib.s |
39 | 05_Mario_world_B/mario_world.c | zdrojový kód pátého demonstračního příkladu psaný v C | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/mario_world.c |
40 | 05_Mario_world_B/mario_world.s | demonstrační příklad přeložený do assembleru | https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/05_Mario_world_B/mario_world.s |
Pro úplnost si ještě uveďme odkazy na demonstrační příklady napsané v assembleru, které jsou určené pro překlad pomocí assembleru ca65 (jenž je součástí cc65), byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
20. Odkazy na Internetu
- Překladače jazyka C pro historické osmibitové mikroprocesory
https://www.root.cz/clanky/prekladace-jazyka-c-pro-historicke-osmibitove-mikroprocesory/ - Překladače programovacího jazyka C pro historické osmibitové mikroprocesory (2)
https://www.root.cz/clanky/prekladace-programovaciho-jazyka-c-pro-historicke-osmibitove-mikroprocesory-2/ - Program a NES game in C
https://learncgames.com/program-a-nes-game-in-c/ - The Thirty Million Line Problem
https://www.youtube.com/watch?v=kZRE7HIO3vk - crt0
https://en.wikipedia.org/wiki/Crt0 - NesDev.org
https://www.nesdev.org/ - The Sprite Attribute Byte
https://www.patater.com/nes-asm-tutorials/day-17/ - How to Program an NES game in C
https://nesdoug.com/ - Cycle reference chart
https://www.nesdev.org/wiki/Cycle_reference_chart - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - NES (Nintendo Entertainment System) controller pinout
https://pinoutguide.com/Game/NES_controller_pinout.shtml - NES Controller Shift Register
https://www.allaboutcircuits.com/uploads/articles/nes-controller-arduino.png?v=1469416980041 - „Game Development in Eight Bits“ by Kevin Zurawel
https://www.youtube.com/watch?v=TPbroUDHG0s&list=PLcGKfGEEONaBjSfQaSiU9yQsjPxxDQyV8&index=4 - Game Development for the 8-bit NES: A class by Bob Rost
http://bobrost.com/nes/ - Game Development for the 8-bit NES: Lecture Notes
http://bobrost.com/nes/lectures.php - NES Graphics Explained
https://www.youtube.com/watch?v=7Co_8dC2zb8 - NES GAME PROGRAMMING PART 1
https://rpgmaker.net/tutorials/227/?post=240020 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - Minimal NES example using ca65
https://github.com/bbbradsmith/NES-ca65-example - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - History of video game consoles (second generation): Wikipedia
http://en.wikipedia.org/wiki/History_of_video_game_consoles_(second_generation) - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - bee – The Multi-Console Emulator
http://www.thebeehive.ws/ - Nerdy Nights Mirror
https://nerdy-nights.nes.science/ - The Nerdy Nights ca65 Remix
https://github.com/ddribin/nerdy-nights - NES Development Day 1: Creating a ROM
https://www.moria.us/blog/2018/03/nes-development - How to Start Making NES Games
https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/ - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - Nocash NES Specs
http://nocash.emubase.de/everynes.htm - Nintendo Entertainment System
http://cs.wikipedia.org/wiki/NES - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - NesDev
http://nesdev.parodius.com/ - 2A03 technical reference
http://nesdev.parodius.com/2A03%20technical%20reference.txt - NES Dev wiki: 2A03
http://wiki.nesdev.com/w/index.php/2A03 - Ricoh 2A03
http://en.wikipedia.org/wiki/Ricoh_2A03 - 2A03 pinouts
http://nesdev.parodius.com/2A03_pinout.txt - 27c3: Reverse Engineering the MOS 6502 CPU (en)
https://www.youtube.com/watch?v=fWqBmmPQP40 - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - Nintendo Entertainment System (NES)
https://8bitworkshop.com/docs/platforms/nes/ - Question about NES vectors and PPU
https://archive.nes.science/nesdev-forums/f10/t4154.xhtml - How do mapper chips actually work?
https://archive.nes.science/nesdev-forums/f9/t13125.xhtml - INES
https://www.nesdev.org/wiki/INES - NES Basics and Our First Game
http://thevirtualmountain.com/nes/2017/03/08/nes-basics-and-our-first-game.html - Where is the reset vector in a .nes file?
https://archive.nes.science/nesdev-forums/f10/t17413.xhtml - CPU memory map
https://www.nesdev.org/wiki/CPU_memory_map - How to make NES music
http://blog.snugsound.com/2008/08/how-to-make-nes-music.html - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - MIDINES
http://www.wayfar.net/0×f00000_overview.php - FamiTracker
http://famitracker.com/ - nerdTracker II
http://nesdev.parodius.com/nt2/ - How NES Graphics work
http://nesdev.parodius.com/nesgfx.txt - NES Technical/Emulation/Development FAQ
http://nesdev.parodius.com/NESTechFAQ.htm - Adventures with ca65
https://atariage.com/forums/topic/312451-adventures-with-ca65/ - example ca65 startup code
https://atariage.com/forums/topic/209776-example-ca65-startup-code/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - Day 1 – Beginning NES Assembly
https://www.patater.com/nes-asm-tutorials/day-1/ - Day 2 – A Source Code File's Structure
https://www.patater.com/nes-asm-tutorials/day-2/ - Assembly Language Misconceptions
https://www.youtube.com/watch?v=8_0tbkbSGRE - How Machine Language Works
https://www.youtube.com/watch?v=HWpi9n2H3kE - Super Mario Bros. (1985) Full Walkthrough NES Gameplay [Nostalgia]
https://www.youtube.com/watch?v=rLl9XBg7wSs - [Longplay] Castlevania (NES) – All Secrets, No Deaths
https://www.youtube.com/watch?v=mOTUVXrAOE8 - Herní série Castlevania
https://www.root.cz/clanky/historie-vyvoje-pocitacovych-her-24-cast-hry-pro-konzoli-nes/#k07