Vývoj pro osmibitovou herní konzoli NES s využitím překladače jazyka C

10. 1. 2023
Doba čtení: 49 minut

Sdílet

 Autor: Depositphotos
V dalším článku o konzoli NES si ukážeme, jak namísto assembleru (nebo společně s assemblerem) využít i programovací jazyk C. Konkrétně si ukážeme použití překladače CC65 společně s knihovnou NESlib.

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

5. Knihovna neslib

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

20. Odkazy na Internetu

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“).

Poznámka: ze stejného důvodu se na NESu nepoužívají interpretované jazyky typu BASIC, i když několik pokusů o využití interpretovaných jazyků existovalo.

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();
}
Poznámka: tento zdrojový kód naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-cc65/01_Intro/intro.c.

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:

  1. Zákaz vykreslování obrazu čipem PPU
  2. Nastavení barvové palety pozadí (background)
  3. Povolení vykreslování obrazu čipem PPU
  4. 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) {
    }
}
Poznámka: pokud by tato smyčka nebyla zavolána, pokračoval by mikroprocesor v interpretaci instrukcí uvedených až za kódem programu, což by u klasického NESu způsobilo „zatuhnutí“ nebo reset herní konzole, v případě emulátoru pak většinou detekci nekorektní instrukce.

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"
Poznámka: pro porovnání se podívejme na to, jak jsme inicializaci NESu prováděli přímo v assembleru. Některé sekvence instrukcí jsou zcela totožné:
; 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);
Poznámka: pochopitelně zde (kvůli omezenému množství ROM na cartridge) nenalezneme například obdobu sprintf ani žádné funkce pro práci s hodnotami s plovoucí řádovou tečkou atd.

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
Poznámka: obecně platí, že právě neslib.s může být vhodným zdrojem jak pro studium assembleru pro MOS 6502, tak i způsobu ovládání herní konzole NES.

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
Poznámka: konstany 0×20 a 0×00 jsou vypočteny makrem NTADR_A.

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);
}
Poznámka: používáme zde novou funkci nazvanou vram_fill, která dokáže vyplnit část PPU RAM (a tabulka atributů leží v této oblasti) zvolenou konstantou. Výsledný kód by měl být kratší, než explicitně zapsaná programová smyčka.

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.

Poznámka: jak uvidíme dále, zobrazujeme vlastně dlaždice spritů a nikoli dlaždice pozadí. Tento problém lze napravit jediným řádkem v programovém kódu.

Žá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ě.

ict ve školství 24

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_ver­t.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_ver­t.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ář:

# Příklad Stručný popis Adresa
1 example01.asm zdrojový kód příkladu tvořeného kostrou aplikace pro NES https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example01.asm
2 example02.asm použití standardní konfigurace linkeru pro konzoli NES https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example02.asm
3 example03.asm symbolická jména řídicích registrů PPU https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example03.asm
4 example04.asm zjednodušený zápis lokálních smyček v assembleru https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example04.asm
5 example05.asm zvukový výstup s využitím prvního „square“ kanálu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example05.asm
6 example06.asm použití maker bez parametrů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example06.asm
       
7 example07.asm nastavení barvové palety, zvýšení intenzity zvolené barvové složky https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example07.asm
8 example08.asm využití operátorů < a > https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example08.asm
9 example09.asm vymazání barvové palety realizované makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example09.asm
10 example10.asm vymazání barvové palety realizované podprogramem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example10.asm
11 example11.asm nastavení barvové palety pozadí i spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example11.asm
12 example12.asm refaktoring předchozího příkladu makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example12.asm
       
13 example13.asm zobrazení spritů tvořících Maria https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example13.asm
14 example14.asm posun spritů, aby se zdůraznila jejich nezávislost https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example14.asm
15 example15.asm větší množství spritů na obrazovce rozdělených do řádků https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example15.asm
16 example16.asm větší množství spritů na obrazovce na jediném řádku https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example16.asm
17 example17.asm pohyb jednoho spritu pomocí ovladače https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example17.asm
18 example18.asm odvozeno z předchozího příkladu, symbolická jména adres https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example18.asm
19 example19.asm odvozeno z předchozího příkladu, pomocná makra pro pohyb spritu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example19.asm
20 example20.asm pohyb spritu je založen na instrukcích INCDEC https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example20.asm
21 example21.asm přesun celého Maria (8 spritů) https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example21.asm
22 example22.asm (rychlá) změna barvové palety spritů tlačítkem A https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example22.asm
23 example23.asm realizace čítače pro snížení frekvence změn barvové palety https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example23.asm
24 example24.asm horizontální a vertikální zrcadlení spritů řízené hráčem stiskem tlačítek https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example24.asm
25 example25.asm definice pozadí a zobrazení pozadí https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example25.asm
26 example26.asm zobrazení pozadí i spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example26.asm
27 example27.asm pohyb spritu řízený ovladačem, změna atributů spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example27.asm
28 example28.asm definice pozadí přes téměř celou obrazovku https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example28.asm
29 example29.asm scrolling pozadí s využitím ovladače https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example29.asm
30 example30.asm zobrazení hodnot offsetů pro pozadí https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example30.asm
       
31 link.cfg konfigurace segmentů pro linker ld65 https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/link.cfg
32 Makefile Makefile pro překlad a slinkování všech příkladů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/Makefile
Poznámka: pro slinkování a spuštění většiny těchto demonstračních příkladů potřebujete i soubor mario.chr. Ten je stažen automaticky po zadání make example16make example30.

20. Odkazy na Internetu

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

Autor článku

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