├── .gitignore ├── part01 ├── ex02-asm-example │ ├── anton.chr │ ├── nesfile.ini │ ├── nesdefs.inc │ ├── README.md │ └── test.s ├── README.md └── ex01-c-example │ ├── README.md │ └── hello-nes.c ├── part02 ├── ex01-controller-test │ ├── anton.chr │ ├── anton2.chr │ ├── ram.inc │ ├── rodata.inc │ ├── nesfile.ini │ ├── nesdefs.inc │ ├── README.md │ ├── helpers.inc │ └── test.s └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | *.swp 3 | *~ 4 | *.o 5 | *.lst 6 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/anton.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/HEAD/part01/ex02-asm-example/anton.chr -------------------------------------------------------------------------------- /part02/ex01-controller-test/anton.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/HEAD/part02/ex01-controller-test/anton.chr -------------------------------------------------------------------------------- /part02/ex01-controller-test/anton2.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algofoogle/nes-gamedev-examples/HEAD/part02/ex01-controller-test/anton2.chr -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nes-gamedev-examples 2 | 3 | This repository is to support a series of 4 | articles on NES (Nintendo Entertainment System) game development, to be published at 5 | [anton.maurovic.com](http://anton.maurovic.com/). The initial article can be found at: 6 | 7 | 8 | Expect this repository to grow over time, as the articles in the series are published. 9 | -------------------------------------------------------------------------------- /part02/README.md: -------------------------------------------------------------------------------- 1 | # part02 -- Nintendo (NES) Gamedev, part 2: Basic I/O 2 | 3 | The contents of this `part02` directory are associated with the following article: 4 | 5 | * -- **NOT YET RELEASED** 6 | 7 | This part contains just the following: 8 | 9 | * `ex01-controller-test` -- Built on `part01/ex02-asm-example`, this shows the key parts of input 10 | and output on the NES: Video, audio, and reading the controller. 11 | 12 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/ram.inc: -------------------------------------------------------------------------------- 1 | ; ===== Zero-page RAM ========================================================== 2 | 3 | .segment "ZEROPAGE" 4 | 5 | nmi_counter: .res 1 ; Counts DOWN for each NMI. 6 | msg_ptr: .res 1 ; Points to the next character to fetch from a message. 7 | screen_offset: .res 1 ; Points to the next screen offset to write. 8 | 9 | ; ===== General RAM ============================================================ 10 | 11 | .segment "BSS" 12 | ; Put labels with .res statements here. 13 | 14 | -------------------------------------------------------------------------------- /part01/README.md: -------------------------------------------------------------------------------- 1 | # part01 -- Nintendo (NES) Gamedev, part 1: Setting up 2 | 3 | The contents of this `part01` directory are associated with the following article: 4 | 5 | * 6 | 7 | This part contains just the following: 8 | 9 | * `ex01-c-example` -- Simple example NES program written in C code that can be used to 10 | prove whether your `cc65` compiler platform is working. 11 | 12 | * `ex02-asm-example` -- Slightly more complex example, written in 6502 assembly language, 13 | to prove whether your `ca65` build process is working. 14 | 15 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/rodata.inc: -------------------------------------------------------------------------------- 1 | ; ===== Program data (read-only) =============================================== 2 | 3 | .segment "RODATA" 4 | 5 | palette_data: 6 | ; Colours available in the NES palette are: 7 | ; http://bobrost.com/nes/files/NES_Palette.png 8 | .repeat 2 9 | pal $09, $16, $2A, $12 ; $09 (dark plant green), $16 (red), $2A (green), $12 (blue). 10 | pal $16, $28, $3A ; $16 (red), $28 (yellow), $3A (very light green). 11 | pal $00, $10, $20 ; Grey; light grey; white. 12 | pal $25, $37, $27 ; Pink; light yellow; orange. 13 | .endrepeat 14 | 15 | hello_msg: 16 | ; 01234567890123456789012345678901 17 | .byt " Hello, World! " 18 | .byt " This is a test by " 19 | .byt " anton@maurovic.com " 20 | .byt " - http://anton.maurovic.com", 0 21 | -------------------------------------------------------------------------------- /part01/ex01-c-example/README.md: -------------------------------------------------------------------------------- 1 | # ex01-c-example 2 | 3 | This is the source code for an example NES ROM, written in C, that demonstrates 4 | a simple program that can be compiled using cc65. 5 | 6 | The *original* source was taken from: 7 | 8 | * [NES Game Programming Part 1](http://www.dreamincode.net/forums/topic/152401-nes-game-programming-part-1/), 9 | by [WolfCoder](http://www.dreamincode.net/forums/user/4811-wolfcoder/). 10 | 11 | To compile this example to a `.nes` file (which can be run in, say, 12 | [FCEUX](http://www.fceux.com/web/download.html)): 13 | 14 | cl65 -t nes hello-nes.c -o hello.nes 15 | 16 | This tells the [`cl65` compile-and-link utility](http://www.cc65.org/doc/cl65-2.html) 17 | to use the `nes` target and compile `hello-nes.c` to a NES image called `hello.nes`, 18 | which is a binary with an [INES cartridge header](http://wiki.nesdev.com/w/index.php/INES). 19 | 20 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/nesfile.ini: -------------------------------------------------------------------------------- 1 | # This is a configuration file (as per: http://www.cc65.org/doc/ld65-5.html) that 2 | # tells ld65 how we want to lay out our output file. 3 | 4 | # NOTE: Parts of this file are inspired by "nrom-template-0.02" written by 5 | # Damian Yerrick (tepples). See: http://pics.pineight.com/nes/ 6 | # ...and in particular: http://pics.pineight.com/nes/nrom-template-0.02.zip 7 | 8 | # ----- MEMORY section ------------------------------------------------------------ 9 | 10 | MEMORY { 11 | ZP: start=$10, size=$f0, type=rw; 12 | HEADER: start=0, size=$10, type=ro, file=%O, fill=yes, fillval=$00; 13 | # NES RAM runs from $0000-$07FF... 14 | STACK: start=$0100, size=$0100, type=rw; 15 | # We reserve $0200-$02FF for DMA. 16 | # We just define our 'RAM' as the remaining space from $0300-$07FF: 17 | RAM: start=$0300, size=$0500, type=rw; 18 | # Cartridge RAM, "if present": 19 | EXTRAM: start=$6000, size=$2000, type=rw; 20 | # Our 16KiB of PRG ROM sits at the top of memory, from $C000-$FFFF. 21 | ROM7: start=$C000, size=$4000, type=ro, file=%O, fill=yes, fillval=$FF; 22 | # Our 8KiB CHR ROM sits on the PPU's address bus at $0000-$1FFF. 23 | CHR: start=$0000, size=$2000, type=ro, file=%O, fill=yes, fillval=$CC; 24 | } 25 | 26 | # ----- SEGMENTS section ------------------------------------------------------------ 27 | 28 | SEGMENTS { 29 | INESHDR: load=HEADER, type=ro, align=$10; 30 | ZEROPAGE: load=ZP, type=zp; 31 | # The BSS segment is assumed to be 'uninitialised memory'. 32 | BSS: load=RAM, type=bss, define=yes, align=$100; 33 | # If used, the DMC segment (I think?) holds data that may be used by 34 | # the APU's Delta Modulation Channel 35 | # (see: http://wiki.nesdev.com/w/index.php/APU_DMC). 36 | DMC: load=ROM7, type=ro, align=64, optional=yes; 37 | CODE: load=ROM7, type=ro, align=$100; 38 | # RODATA still appears inside the ROM, after CODE, but is just reserved 39 | # for data that we'd potentially want to reference. 40 | RODATA: load=ROM7, type=ro, align=$100; 41 | VECTORS: load=ROM7, type=ro, start=$FFFA; 42 | # The pattern data is loaded into the CHR-ROM. Can either use 43 | # the full PATTERN segment, or the separate PATTERN0 ('left') and 44 | # PATTERN1 ('right') segments. 45 | PATTERN: load=CHR, type=ro, optional=yes; 46 | PATTERN0: load=CHR, type=ro, optional=yes; # Implicit start at $0000. 47 | PATTERN1: load=CHR, type=ro, optional=yes, start=$1000; 48 | } 49 | 50 | FILES { 51 | %O: format=bin; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/nesfile.ini: -------------------------------------------------------------------------------- 1 | # This is a configuration file (as per: http://www.cc65.org/doc/ld65-5.html) that 2 | # tells ld65 how we want to lay out our output file. 3 | 4 | # NOTE: Parts of this file are inspired by "nrom-template-0.02" written by 5 | # Damian Yerrick (tepples). See: http://pics.pineight.com/nes/ 6 | # ...and in particular: http://pics.pineight.com/nes/nrom-template-0.02.zip 7 | 8 | # ----- MEMORY section ------------------------------------------------------------ 9 | 10 | MEMORY { 11 | ZP: start=$10, size=$f0, type=rw; 12 | HEADER: start=0, size=$10, type=ro, file=%O, fill=yes, fillval=$00; 13 | # NES RAM runs from $0000-$07FF... 14 | STACK: start=$0100, size=$0100, type=rw; 15 | # We reserve $0200-$02FF for DMA. 16 | # We just define our 'RAM' as the remaining space from $0300-$07FF: 17 | RAM: start=$0300, size=$0500, type=rw; 18 | # Cartridge RAM, "if present": 19 | EXTRAM: start=$6000, size=$2000, type=rw; 20 | # Our 16KiB of PRG ROM sits at the top of memory, from $C000-$FFFF. 21 | ROM7: start=$C000, size=$4000, type=ro, file=%O, fill=yes, fillval=$FF; 22 | # Our 8KiB CHR ROM sits on the PPU's address bus at $0000-$1FFF. 23 | CHR: start=$0000, size=$2000, type=ro, file=%O, fill=yes, fillval=$CC; 24 | } 25 | 26 | # ----- SEGMENTS section ------------------------------------------------------------ 27 | 28 | SEGMENTS { 29 | INESHDR: load=HEADER, type=ro, align=$10; 30 | ZEROPAGE: load=ZP, type=zp; 31 | # The BSS segment is assumed to be 'uninitialised memory'. 32 | BSS: load=RAM, type=bss, define=yes, align=$100; 33 | # If used, the DMC segment (I think?) holds data that may be used by 34 | # the APU's Delta Modulation Channel 35 | # (see: http://wiki.nesdev.com/w/index.php/APU_DMC). 36 | DMC: load=ROM7, type=ro, align=64, optional=yes; 37 | CODE: load=ROM7, type=ro, align=$100; 38 | # RODATA still appears inside the ROM, after CODE, but is just reserved 39 | # for data that we'd potentially want to reference. 40 | RODATA: load=ROM7, type=ro, align=$100; 41 | VECTORS: load=ROM7, type=ro, start=$FFFA; 42 | # The pattern data is loaded into the CHR-ROM. Can either use 43 | # the full PATTERN segment, or the separate PATTERN0 ('left') and 44 | # PATTERN1 ('right') segments. 45 | PATTERN: load=CHR, type=ro, optional=yes; 46 | PATTERN0: load=CHR, type=ro, optional=yes; # Implicit start at $0000. 47 | PATTERN1: load=CHR, type=ro, optional=yes, start=$1000; 48 | } 49 | 50 | FILES { 51 | %O: format=bin; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/nesdefs.inc: -------------------------------------------------------------------------------- 1 | ; This file defines a lot of NES registers and bit fields. 2 | 3 | PPU_CTRL = $2000 4 | PPU_MASK = $2001 5 | ;PPU_STATUS = $2002 ; Defined in nes.inc 6 | PPU_OAM_ADDR = $2003 ; OAM = "Object Attribute Memory", for sprites. 7 | PPU_OAM_DATA = $2004 8 | ; NOTE: Use OAM_DMA instead of OAM_DATA. That is, instead of directly latching 9 | ; each byte into the PPU's RAM, we set up a DMA copy driven by hardware. 10 | PPU_SCROLL = $2005 11 | PPU_ADDR = $2006 12 | PPU_DATA = $2007 13 | 14 | APU_NOISE_VOL = $400C 15 | APU_NOISE_FREQ = $400E 16 | APU_NOISE_TIMER = $400F 17 | APU_DMC_CTRL = $4010 18 | APU_CHAN_CTRL = $4015 19 | APU_FRAME = $4017 20 | 21 | ; NOTE: I've put this outside of the PPU & APU, because it is a feature 22 | ; of the APU that is primarily of use to the PPU. 23 | OAM_DMA = $4014 24 | ; OAM local RAM copy goes from $0200-$02FF: 25 | OAM_RAM = $0200 26 | 27 | ; PPU_CTRL bit flags: 28 | ; NOTE: Many of these are expressed in binary, 29 | ; to highlight which bit(s) they pertain to: 30 | NT_0 = %00 ; Use nametable 0 ($2000). 31 | NT_1 = %01 ; Use nametable 1 ($2400). 32 | NT_2 = %10 ; Use nametable 2 ($2800). 33 | NT_3 = %11 ; Use nametable 3 ($2C00). 34 | VRAM_RIGHT = %000 ; Increment nametable address rightwards, after a write. 35 | VRAM_DOWN = %100 ; Increment nametable address downwards, after a write. 36 | SPR_0 = %0000 ; Use sprite pattern table 0. 37 | SPR_1 = %1000 ; Use sprite pattern table 1. 38 | BG_0 = %00000 ; Use background pattern table 0 ($0000). 39 | BG_1 = %10000 ; Use background pattern table 1 ($1000). 40 | SPR_8x8 = %00000 ; Use standard 8x8 sprites. 41 | SPR_8x16 = %100000 ; Use 8x16 sprites, instead of 8x8. 42 | NO_VBLANK_NMI = %00000000 ; Don't generate VBLANK NMIs. 43 | VBLANK_NMI = %10000000 ; DO generate VBLANK NMIs. 44 | 45 | ; PPU_MASK bit flags: 46 | COLOR_NORMAL = %0 47 | COLOR_GRAYSCALE = %1 48 | HIDE_BG_LHS = %00 ; Hide left-most 8 pixels of the background. 49 | SHOW_BG_LHS = %10 ; Show left-most 8 pixels of BG. 50 | HIDE_SPR_LHS = %000 ; Prevent displaying sprites in left-most 8 pixels of screen. 51 | SHOW_SPR_LHS = %100 ; Show sprites in left-most 8 pixels of screen. 52 | BG_OFF = %0000 ; Hide background. 53 | BG_ON = %1000 ; Show background. 54 | SPR_OFF = %00000 ; Hide sprites. 55 | SPR_ON = %10000 ; Show sprites. 56 | 57 | 58 | 59 | ; Load a given 16-bit address into the PPU_ADDR register. 60 | .macro ppu_addr Addr 61 | ldx #>Addr ; High byte first. 62 | stx PPU_ADDR 63 | ldx #Addr ; High byte first. 62 | stx PPU_ADDR 63 | ldx #=M (i.e. X>=32). 105 | bcc :- ; Loop if P.C is clear. 106 | ; NOTE: Trying to load the palette outside of VBLANK may lead to the colours being 107 | ; rendered as pixels on the screen. See: 108 | ; http://wiki.nesdev.com/w/index.php/Palette#The_background_palette_hack 109 | .endmacro 110 | 111 | 112 | .macro clear_vram first_table, num_tables 113 | ; Clear the nametables. 114 | ; The physical VRAM (Video RAM) of the NES is only 2KiB, allowing for two nametables. 115 | ; Each nametable is 1024 bytes of memory, arranged as 32 columns by 30 rows of 116 | ; tile references, for a total of 960 ($3C0) bytes. The remaining 64 bytes are 117 | ; for the attribute table of that nametable. 118 | ; Nametable 0 starts at PPU address $2000, while nametable 1 starts at $2400, 119 | ; nametable 3 at $2800, and nametable 4 at $2c00. 120 | ; For more information, see: http://wiki.nesdev.com/w/index.php/Nametable 121 | ; Because of mirroring, however, in this case implied by the INES header to 122 | ; be "horizontal mirroring", $2000-$23FF is mapped to the same memory 123 | ; as $2400-$27FF, meaning that the next UNIQUE nametable starts at $2800. 124 | ; In order to keep things simple when we clear the video RAM, we just blank out 125 | ; the entire $2000-$2FFF range, which means we're wasting some CPU time, but 126 | ; it isn't noticeable to the user, and saves overcomplicating the code below. 127 | ; NOTE: In order to keep this loop tight (knowing we can only easily count 256 iterations 128 | ; in a single loop), we just have one loop and do multiple writes in it. 129 | ppu_addr $2000+($400*first_table) 130 | ldx #0 131 | txa 132 | ; Write 0 into the PPU Nametable RAM, 16 times, per each of 256 iterations: 133 | : Repeat 4*num_tables, sta PPU_DATA 134 | inx 135 | bne :- 136 | .endmacro 137 | 138 | 139 | .macro enable_vblank_nmi 140 | ; Activate VBLANK NMIs. 141 | lda #VBLANK_NMI 142 | sta PPU_CTRL 143 | .endmacro 144 | 145 | 146 | .macro fill_attribute_table tnum 147 | ; Clear attribute table, for a given nametable: 148 | ; One palette (out of the 4 background palettes available) may be assigned 149 | ; per 2x2 group of tiles. The actual layout of the attribute table is a bit 150 | ; funny. See here for more info: http://wiki.nesdev.com/w/index.php/PPU_attribute_tables 151 | ; Attribute table for Nametable 0, first: 152 | ppu_addr ($23c0+($400*tnum)) 153 | ldx #64 154 | : sta PPU_DATA 155 | dex 156 | bne :- 157 | .endmacro 158 | 159 | 160 | .macro trigger_ppu_dma 161 | ; Trigger DMA to copy from local OAM_RAM ($0200-$02FF) to PPU OAM RAM. 162 | ; For more info on DMA, see: http://wiki.nesdev.com/w/index.php/PPU_OAM#DMA 163 | lda #0 164 | sta PPU_OAM_ADDR ; Specify the target starts at $00 in the PPU's OAM RAM. 165 | lda #>OAM_RAM ; Get upper byte (i.e. page) of source RAM for DMA operation. 166 | sta OAM_DMA ; Trigger the DMA. 167 | ; DMA will halt the CPU while it copies 256 bytes from $0200-$02FF 168 | ; into $00-$FF of the PPU's OAM RAM. 169 | .endmacro 170 | 171 | 172 | .macro init_sprites 173 | ; Move all sprites below line 240, so they're hidden. 174 | ; Here, we PREPARE this by loading $0200-$02FF with data that we will transfer, 175 | ; via DMA, to the NES OAM (Object Attribute Memory) in the PPU. The DMA will take 176 | ; place after we know the PPU is ready (i.e. after 2nd VBLANK). 177 | ; NOTE: OAM RAM contains 64 sprite definitions, each described by 4 bytes: 178 | ; byte 0: Y position of the top of the sprite. 179 | ; byte 1: Tile number. 180 | ; byte 2: Attributes (inc. palette, priority, and flip). 181 | ; byte 3: X position of the left of the sprite. 182 | ldx #0 183 | lda #$FF 184 | : sta OAM_RAM,x ; Each 4th byte in OAM (e.g. $00, $04, $08, etc.) is the Y position. 185 | Repeat 4, inx 186 | bne :- 187 | .endmacro 188 | -------------------------------------------------------------------------------- /part01/ex01-c-example/hello-nes.c: -------------------------------------------------------------------------------- 1 | /* 2 | Hello, NES! 3 | Writes a message to the screen and plays a tone. 4 | 5 | Originally written by WolfCoder (2010). See: 6 | http://www.dreamincode.net/forums/topic/152401-nes-game-programming-part-1/ 7 | 8 | Modified slightly by Anton Maurovic (2013) for: 9 | http://anton.maurovic.com/posts/nintendo-nes-gamedev-part-1-setting-up/ 10 | 11 | Build with cc65 as follows: 12 | cl65 -t nes hello-nes.c -o hello.nes 13 | 14 | This example will use a default CHR ROM that comes with the cc65 15 | target files for NES. 16 | */ 17 | 18 | /* Includes */ 19 | #include 20 | 21 | /* For more information on these PPU registers, see: 22 | * http://wiki.nesdev.com/w/index.php/PPU_registers */ 23 | #define PPU_CTRL2 0x2001 /* Aka PPUMASK: Turns features of the PPU on or off. */ 24 | #define PPU_VRAM_ADDR1 0x2005 /* Aka PPUSCROLL: Used for X/Y scroll */ 25 | #define PPU_VRAM_ADDR2 0x2006 /* Aka PPUADDR: Nametable 'cursor' */ 26 | #define PPU_VRAM_IO 0x2007 /* Aka PPUDATA: Access data pointed to by last PPUADDR write. */ 27 | 28 | /* For more information on these APU registers, see: 29 | * http://wiki.nesdev.com/w/index.php/APU_Status */ 30 | #define APU_STATUS 0x4015 /* Used for activating APU "voices". */ 31 | #define APU_PULSE 0x4000 /* 0x4000-0x4003: Registers for pulse 1. 0x4004-0x4007 for pulse 2. */ 32 | 33 | /* Write a byte to a given address: */ 34 | #define poke(addr, data) (*((unsigned char*)addr) = data) 35 | 36 | /* Write a pair of bytes to the PPU VRAM Address 2. */ 37 | #define ppu_2(a, b) { poke(PPU_VRAM_ADDR2, a); poke(PPU_VRAM_ADDR2, b); } 38 | 39 | /* Set the nametable x/y position. The top-left corner is 0x2000, and each row 40 | * is 32 bytes wide. Hence: 41 | * (0,0) => 0x2000; 42 | * (1,2) => 0x2000 + 2*32 + 1 => 0x2041; 43 | * (20,16) => 0x2000 + 16*32 + 20 => 0x2214; 44 | */ 45 | #define ppu_set_cursor(x, y) ppu_2(0x20+((y)>>3), ((y)<<5)+(x)) 46 | 47 | /* Set the screen scroll offsets: */ 48 | #define ppu_set_scroll(x, y) { poke(PPU_VRAM_ADDR1, x); poke(PPU_VRAM_ADDR1, y); } 49 | 50 | /* Set "foreground colour"... 51 | * i.e. write color 'c' value into VRAM address 0x3F03 52 | * ...which is colour no. 3 in "background palette 0". 53 | * See: http://wiki.nesdev.com/w/index.php/PPU_palettes */ 54 | #define ppu_set_color_text(c) { ppu_2(0x3F, 0x03); ppu_io(c); } 55 | 56 | /* Set "background colour"... 57 | * i.e. write color 'c' value into VRAM address 0x3F00 58 | * ...which is the "Universal background color" palette entry. 59 | * See: http://wiki.nesdev.com/w/index.php/PPU_palettes */ 60 | #define ppu_set_color_back(c) { ppu_2(0x3F, 0x00); ppu_io(c); } 61 | 62 | /* Write to the PPU IO port, e.g. to write a byte at the nametable 'cursor' position: */ 63 | #define ppu_io(c) poke(PPU_VRAM_IO, (c)) 64 | 65 | /* Write to APU_STATUS register: */ 66 | #define apu_status(c) poke(APU_STATUS, (c)) 67 | 68 | /* Write to one of the APU pulse registers. 69 | * Parameter 'ch' is the channel (pulse channel 0 or 1), 70 | * 'r' is the register (0-4), and 'c' is the data to write. */ 71 | #define apu_pulse(ch, r, c) poke(APU_PULSE+((ch)<<2)+(r), (c)) 72 | 73 | /* Writes the string to the screen */ 74 | /* Note how the NES hardware itself automatically moves the position we write to the screen */ 75 | void write_string(char *str) 76 | { 77 | /* Position the cursor at what APPEARS to be (1,1). */ 78 | /* We only need to do this once. */ 79 | /* We start 2 rows down since the first 8 pixels from the top of the screen is hidden */ 80 | ppu_set_cursor(1, 2); 81 | 82 | /* Write the string */ 83 | while(*str) 84 | { 85 | /* Write a letter */ 86 | /* Note that the compiler's lib/nes.lib defines a CHR ROM that 87 | has graphics matching ASCII characters. */ 88 | ppu_io(*str); 89 | /* Advance pointer that reads from the string */ 90 | str++; 91 | } 92 | } 93 | 94 | /* Program entry */ 95 | int main() 96 | { 97 | /* This is just used as a frame counter, so we can synchronise 98 | * things based on the number of frames rendered. PAL NES is 50FPS, 99 | * while NTSC NES is 60FPS. Hence, on an NTSC NES, we know one frame 100 | * lasts approx 16.67ms and after 60 frames, 1 second has elapsed. */ 101 | int frame = 0; 102 | /* We have to wait for VBLANK or we can't even use the PPU */ 103 | waitvblank(); /* This is found in nes.h */ 104 | 105 | /* Now set basic colours which we'll use for foreground and background. 106 | * This is based on the NES palette: 107 | * http://en.wikipedia.org/wiki/List_of_video_game_console_palettes#NES */ 108 | /* Set the background color (0x11 => medium blue): */ 109 | ppu_set_color_back(0x11); 110 | /* Set the text colour: */ 111 | /* Then, we need to set the text color (0x10 => light grey): */ 112 | ppu_set_color_text(0x10); 113 | 114 | /* We must write our message to the screen */ 115 | write_string("Anton says: Hello, World!"); 116 | 117 | /* Reset the screen scroll position: */ 118 | ppu_set_scroll(0, 0); 119 | 120 | /* Enable the screen; 121 | * By default, the screen and sprites were off. 122 | * This turns on only bit 3 of the Control/Mask register, which 123 | * activates backgrounds (i.e. rendering of the nametable). 124 | * See: http://wiki.nesdev.com/w/index.php/PPU_registers#Mask_.28.242001.29_.3E_write */ 125 | poke(PPU_CTRL2, 8); 126 | 127 | /* Start making a noise, by turning on bit 0 of APU_STATUS, 128 | * which activates "pulse channel 1" */ 129 | apu_status(1); 130 | 131 | /* Set the pulse timer for the first pulse channel (0), via registers 132 | * 2 and 3 (i.e. 0x4002 and 0x4003). Set the timer value to 0x208: */ 133 | apu_pulse(0, 2, 0x08); /* Set low 8 bits of pulse timer to 8 (00001000) */ 134 | apu_pulse(0, 3, 0x2); /* Set high 3 bits of pulse timer to 2 (010) */ 135 | /* A timer value of 0x208 means a frequency of about 214.7Hz. */ 136 | 137 | /* Set: 138 | * 10...... = Duty Cycle is 50%; 139 | * ..1..... = Disable length counter; 140 | * ...1.... = Constant volume option; 141 | * ....1111 = Maximum volume level. 142 | */ 143 | apu_pulse(0, 0, 0xBF); 144 | 145 | /* This is an endless loop that alternates a square wave tone between 146 | * two different frequencies, at a rate of 0.5Hz (i.e. a high tone for 147 | * 1 second, then a low tone for 1 second). */ 148 | while (1) 149 | { 150 | /* Wait until the current frame has finished drawing to the screen. */ 151 | waitvblank(); 152 | /* Increment our frame counter: */ 153 | frame++; 154 | /* Check if we've reached a frame we need to take action on: */ 155 | if (frame == 60) 156 | { 157 | /* 60 frames have elapsed (1 second) so switch to a higher frequency. 158 | * 0x193 becomes about 276.9Hz. */ 159 | apu_pulse(0, 2, 0x93); 160 | apu_pulse(0, 3, 0x1); 161 | } 162 | else if (frame == 120) 163 | { 164 | /* Another 60 frames have elapsed, so switch back to lower frequency, 165 | * and reset the frame counter. */ 166 | apu_pulse(0, 2, 0x08); 167 | apu_pulse(0, 3, 0x2); 168 | frame = 0; 169 | } 170 | } 171 | 172 | /* NOTE: Though we never make it here because of the 'while' loop above, 173 | * this is an example of making the program hang at this point. The compiler 174 | * normally loops the main() function if it exits, so we put this in to stop 175 | * it from ever exiting. */ 176 | while(1); 177 | 178 | return 0; 179 | } 180 | 181 | -------------------------------------------------------------------------------- /part02/ex01-controller-test/test.s: -------------------------------------------------------------------------------- 1 | ; This example makes use of nesfile.ini (i.e. a configuration file for ld65). 2 | 3 | ; Build this by running ./bb, which basically does this: 4 | ; # Assemble: 5 | ; ca65 test.s -o output/test.o -l 6 | ; # Link, to create test.nes: 7 | ; ld65 output/test.o -m output/map.txt -o output/test.nes -C nesfile.ini 8 | ; ...and then runs it with FCEUX. 9 | 10 | ; ===== Includes =============================================================== 11 | 12 | .include "nes.inc" ; This is found in cc65's "asminc" dir. 13 | .include "nesdefs.inc" ; This may be better than "nes.inc". 14 | .include "helpers.inc" ; Various helper macros for init, etc, used by Anton's examples. 15 | 16 | ; ===== Local macros =========================================================== 17 | 18 | ; (None) 19 | 20 | 21 | ; ===== iNES header ============================================================ 22 | 23 | .segment "INESHDR" 24 | .byt "NES",$1A 25 | .byt 1 ; 1 x 16kB PRG block. 26 | .byt 1 ; 1 x 8kB CHR block. 27 | ; Rest of iNES header defaults to 0, indicating mapper 0, standard RAM size, etc. 28 | 29 | ; ===== Interrupt vectors ====================================================== 30 | 31 | .segment "VECTORS" 32 | .addr nmi_isr, reset, irq_isr 33 | 34 | ; ===== RAM reservations ======================================================= 35 | 36 | .include "ram.inc" ; Reservations in Zero-page RAM, and General (BSS) WRAM. 37 | 38 | 39 | ; ===== Program data (read-only) =============================================== 40 | 41 | .include "rodata.inc" ; "RODATA" segment; data found in the ROM. 42 | 43 | 44 | ; ===== Main code ============================================================== 45 | 46 | .segment "CODE" 47 | 48 | 49 | ; MAIN PROGRAM START: The 'reset' address. 50 | .proc reset 51 | 52 | basic_init 53 | clear_wram 54 | ack_interrupts 55 | init_apu 56 | ppu_wakeup 57 | 58 | ; We're in VBLANK for a short while, so do video prep now... 59 | 60 | load_palettes palette_data 61 | 62 | ; Clear all 4 nametables (i.e. start at nametable 0, and clear 4 nametables): 63 | clear_vram 0, 4 64 | 65 | ; Fill attribute tables, for nametable 0, with palette %01 66 | ; (for all 4 palettes, hence %01010101 or $55): 67 | lda #$55 ; Select palette %01 (2nd palette) throughout. 68 | fill_attribute_table 0 69 | fill_attribute_table 2 70 | ; These two are done because 0 and 2 are stacked vertically, 71 | ; due to the INES header selecting horizontal mirroring in this case. 72 | 73 | enable_vblank_nmi 74 | 75 | ; Now wait until nmi_counter increments, to indicate the next VBLANK. 76 | wait_for_nmi 77 | ; By this point, we're in the 3rd VBLANK. 78 | 79 | init_sprites 80 | trigger_ppu_dma 81 | 82 | ; Set X & Y scrolling positions (which have ranges of 0-255 and 0-239 respectively): 83 | ppu_scroll 0, 0 84 | 85 | ; Configure PPU parameters/behaviour/table selection: 86 | lda #VBLANK_NMI|BG_0|SPR_0|NT_0|VRAM_RIGHT 87 | sta PPU_CTRL 88 | 89 | ; Turn the screen on, by activating background and sprites: 90 | lda #BG_ON|SPR_ON 91 | sta PPU_MASK 92 | 93 | ; Wait until the screen refreshes. 94 | wait_for_nmi 95 | ; OK, at this point we know the screen is visible, ready, and waiting. 96 | 97 | ; ------ Configure noise channel ------ 98 | 99 | ; Set noise type and period: 100 | ; 0------- Pseudo-random noise (instead of random regular waveform). 101 | ; ----1000 Mid-range period/frequency. 102 | lda #%00001000 103 | sta APU_NOISE_FREQ ; Noise mode & period (frequency). 104 | 105 | ; Set volume control: 106 | ; --0----- Use silencing timer (makes it one-shot). 107 | ; ---0---- Use volume envelope (fade). 108 | ; ----0000 Envelope length (shortest). 109 | lda #%00000000 ; Very short fade, one-shot. 110 | sta APU_NOISE_VOL ; Noise channel volume control. 111 | 112 | ; Set length counter: 113 | ; 11111--- Maximum timer (though other values seem to have no effect?) 114 | lda #%11111000 115 | sta APU_NOISE_TIMER ; Length counter load. 116 | 117 | ; Channel control: 118 | ; ----1--- Enable noise channel. 119 | lda #%00001000 120 | sta APU_CHAN_CTRL ; Channel control. 121 | 122 | 123 | message_loop: 124 | ; Wait 1s (60 frames at 60Hz): 125 | nmi_delay 60 126 | 127 | ; Make a debug click by firing the noise channel one-shot 128 | ; (by loading the length counter from a value selected from 129 | ; a look-up table, specified here by the upper 5 bits). 130 | ; The table is described here: 131 | ; http://wiki.nesdev.com/w/index.php/APU_Length_Counter#Table_structure 132 | ; ...and in this case you can see that $03 is the shortest (2). 133 | lda #%00011000 ; 134 | sta APU_NOISE_TIMER 135 | 136 | ; Clear lines 2-6 of the nametable (i.e. skip first 32*2 tiles, clear next 32*4 tiles): 137 | ppu_addr $2000+(32*2) 138 | lda #0 139 | ldx #(32*4/4) ; NOTE: 4 x "STA" instructions make this loop faster. 140 | : Repeat 4, sta PPU_DATA 141 | dex 142 | bne :- 143 | 144 | ; Now fix the palettes for the above 4 lines (2-6) that we just cleared: 145 | ; NOTE: There are 4 actual rows to a metarow. 1 metarow is 8 bytes across. 146 | ; Hence, setting the palettes for rows 2-6 requires that we rewrite both 147 | ; metarows 0 and 1 (which covers actual rows 0-3, and 4-7, respectively). 148 | ppu_addr $23c0 ; Select 1st metarow (rows 0-3; we'll then do 4-7). 149 | ldx #(16/4) ; Fill two metarows (8 bytes each), which covers 8 actual rows. 150 | lda #%01010101 ; Both the upper rows (bits 0-3) and the lower rows (bits 4-7) get pallete 1 (%01 x 4). 151 | : Repeat 4, sta PPU_DATA 152 | dex 153 | bne :- 154 | 155 | ; Point screen offset counter back to start of line 2: 156 | lda #(32*2) 157 | sta screen_offset 158 | 159 | ; Point back to start of source message: 160 | lda #0 161 | sta msg_ptr 162 | 163 | ; Fix scroll position: 164 | ; NOTE: We have to do this after writing to VRAM, because scroll position seems 165 | ; to automatically track the VRAM target address. For a possible explanation of this, see: 166 | ; http://wiki.nesdev.com/w/index.php/The_skinny_on_NES_scrolling 167 | ppu_scroll 0, 0 168 | 169 | ; Wait 1s: 170 | nmi_delay 60 171 | 172 | char_loop: 173 | ; Fix message screen offset pointer: 174 | lda #$20 ; Hi-byte of $2000 175 | sta PPU_ADDR 176 | lda screen_offset ; Get current screen offset. 177 | inc screen_offset ; Increment screen offset variable, for next time. 178 | sta PPU_ADDR 179 | 180 | ; Fix scroll position: 181 | ppu_scroll 0, 0 182 | 183 | ; Write next character of message: 184 | ldx msg_ptr ; Get message offset. 185 | inc msg_ptr ; Increment message offset source. 186 | lda hello_msg,x ; Get message character. 187 | beq message_done ; A=0 => End of message. 188 | sta PPU_DATA ; Write the character. 189 | 190 | cmp #$20 191 | beq skip_click ; Don't make a click for space characters. 192 | 193 | ; Activate short one-shot noise effect here, by loading length counter: 194 | lda #%00111000 ; Length ID 7 is "6" (?) steps => 6/240 => 25ms?? 195 | sta APU_NOISE_TIMER 196 | 197 | skip_click: 198 | ; Wait for 50ms (3 frames at 60Hz): 199 | nmi_delay 3 200 | jmp char_loop ; Go process the next character. 201 | 202 | message_done: 203 | ; Message is done; wait half a second. 204 | nmi_delay 30 205 | ; Change the text colour of the 5th and 6th rows. 206 | lda #$23 ; Attribute table starts at $23C0. 207 | sta PPU_ADDR 208 | lda #$C8 ; Select 2nd metarow (rows 4, 5, 6, and 7). 209 | sta PPU_ADDR 210 | ldx #8 ; Fill just one metarow. 211 | lda #%01011111 ; Lower 2 rows (bits 4-7) get palette 1 (%01 x 2), upper 2 get palette 3 (%11 x 2). 212 | : sta PPU_DATA 213 | dex 214 | bne :- 215 | 216 | ; Fix scroll position: 217 | ppu_scroll 0, 0 218 | 219 | ; Wait 1 sec: 220 | nmi_delay 60 221 | 222 | ; Scroll off screen: 223 | ldx #0 224 | scroll_loop: 225 | cpx #((7*8)<<1) ; Scroll by 56 scanlines (7 lines), using lower bit to halve the speed. 226 | beq repeat_message_loop ; Reached our target scroll limit. 227 | wait_for_nmi 228 | lda #0 229 | sta PPU_SCROLL ; X scroll is still 0. 230 | txa 231 | lsr a ; Discard lower bit. 232 | sta PPU_SCROLL ; Y scroll is upper 6 bits of X register. 233 | inx ; Increment scroll counter. 234 | jmp scroll_loop 235 | 236 | repeat_message_loop: 237 | jmp message_loop 238 | 239 | .endproc 240 | 241 | 242 | ; NMI ISR. 243 | ; Use of .proc means labels are specific to this scope. 244 | .proc nmi_isr 245 | dec nmi_counter 246 | rti 247 | .endproc 248 | 249 | 250 | ; IRQ/BRK ISR: 251 | .proc irq_isr 252 | ; Handle IRQ/BRK here. 253 | rti 254 | .endproc 255 | 256 | 257 | 258 | 259 | 260 | ; ===== CHR-ROM Pattern Tables ================================================= 261 | 262 | ; ----- Pattern Table 0 -------------------------------------------------------- 263 | 264 | .segment "PATTERN0" 265 | 266 | .incbin "anton2.chr" 267 | 268 | .segment "PATTERN1" 269 | 270 | .repeat $100 271 | .byt %11111111 272 | .byt %10111011 273 | .byt %11010111 274 | .byt %11101111 275 | .byt %11010111 276 | .byt %10111011 277 | .byt %11111111 278 | .byt %11111111 279 | Repeat 8, .byt $FF 280 | .endrepeat 281 | -------------------------------------------------------------------------------- /part01/ex02-asm-example/test.s: -------------------------------------------------------------------------------- 1 | ; This example makes use of nesfile.ini (i.e. a configuration file for ld65). 2 | 3 | ; Build this by running ./bb, which basically does this: 4 | ; # Assemble: 5 | ; ca65 test.s -o output/test.o -l 6 | ; # Link, to create test.nes: 7 | ; ld65 output/test.o -m output/map.txt -o output/test.nes -C nesfile.ini 8 | ; ...and then runs it with FCEUX. 9 | 10 | ; ===== Includes =============================================================== 11 | 12 | .include "nes.inc" ; This is found in cc65's "asminc" dir. 13 | .include "nesdefs.inc" ; This may be better than "nes.inc". 14 | 15 | ; ===== Local macros =========================================================== 16 | 17 | ; This waits for a change in the value of the NMI counter. 18 | ; It destroys the A register. 19 | .macro wait_for_nmi 20 | lda nmi_counter 21 | : cmp nmi_counter 22 | beq :- ; Loop, so long as nmi_counter hasn't changed its value. 23 | .endmacro 24 | 25 | ; This waits for a given no. of NMIs to pass. It destroys the A register. 26 | ; Note that it relies on an NMI counter that decrements, rather than increments. 27 | .macro nmi_delay frames 28 | lda #frames 29 | sta nmi_counter ; Store the desired frame count. 30 | : lda nmi_counter ; In a loop, keep checking the frame count. 31 | bne :- ; Loop until it's decremented to 0. 32 | .endmacro 33 | 34 | 35 | 36 | ; ===== iNES header ============================================================ 37 | 38 | .segment "INESHDR" 39 | .byt "NES",$1A 40 | .byt 1 ; 1 x 16kB PRG block. 41 | .byt 1 ; 1 x 8kB CHR block. 42 | ; Rest of iNES header defaults to 0, indicating mapper 0, standard RAM size, etc. 43 | 44 | ; ===== Interrupt vectors ====================================================== 45 | 46 | .segment "VECTORS" 47 | .addr nmi_isr, reset, irq_isr 48 | 49 | ; ===== Zero-page RAM ========================================================== 50 | 51 | .segment "ZEROPAGE" 52 | 53 | nmi_counter: .res 1 ; Counts DOWN for each NMI. 54 | msg_ptr: .res 1 ; Points to the next character to fetch from a message. 55 | screen_offset: .res 1 ; Points to the next screen offset to write. 56 | 57 | ; ===== General RAM ============================================================ 58 | 59 | .segment "BSS" 60 | ; Put labels with .res statements here. 61 | 62 | ; ===== Program data (read-only) =============================================== 63 | 64 | .segment "RODATA" 65 | 66 | palette_data: 67 | ; Colours available in the NES palette are: 68 | ; http://bobrost.com/nes/files/NES_Palette.png 69 | .repeat 2 70 | pal $09, $16, $2A, $12 ; $09 (dark plant green), $16 (red), $2A (green), $12 (blue). 71 | pal $16, $28, $3A ; $16 (red), $28 (yellow), $3A (very light green). 72 | pal $00, $10, $20 ; Grey; light grey; white. 73 | pal $25, $37, $27 ; Pink; light yellow; orange. 74 | .endrepeat 75 | 76 | hello_msg: 77 | ; 01234567890123456789012345678901 78 | .byt " Hello, World! " 79 | .byt " This is a test by " 80 | .byt " anton@maurovic.com " 81 | .byt " - http://anton.maurovic.com", 0 82 | 83 | ; ===== Main code ============================================================== 84 | 85 | .segment "CODE" 86 | 87 | 88 | ; NMI ISR. 89 | ; Use of .proc means labels are specific to this scope. 90 | .proc nmi_isr 91 | dec nmi_counter 92 | rti 93 | .endproc 94 | 95 | 96 | ; IRQ/BRK ISR: 97 | .proc irq_isr 98 | ; Handle IRQ/BRK here. 99 | rti 100 | .endproc 101 | 102 | 103 | ; MAIN PROGRAM START: The 'reset' address. 104 | .proc reset 105 | 106 | ; Disable interrupts: 107 | sei 108 | 109 | ; Basic init: 110 | ldx #0 111 | stx PPU_CTRL ; General init state; NMIs (bit 7) disabled. 112 | stx PPU_MASK ; Disable rendering, i.e. turn off background & sprites. 113 | stx APU_DMC_CTRL ; Disable DMC IRQ. 114 | 115 | ; Set stack pointer: 116 | dex ; X = $FF 117 | txs ; Stack pointer = $FF 118 | 119 | ; Clear lingering interrupts since before reset: 120 | bit PPU_STATUS ; Ack VBLANK NMI (if one was left over after reset); bit 7. 121 | bit APU_CHAN_CTRL ; Ack DMC IRQ; bit 7 122 | 123 | ; Init APU: 124 | lda #$40 125 | sta APU_FRAME ; Disable APU Frame IRQ 126 | lda #$0F 127 | sta APU_CHAN_CTRL ; Disable DMC, enable/init other channels. 128 | 129 | ; PPU warm-up: Wait 1 full frame for the PPU to become stable, by watching VBLANK. 130 | ; NOTE: There are 2 different ways to wait for VBLANK. This is one, recommended 131 | ; during early startup init. The other is by the NMI being triggered. 132 | ; For more information, see: http://wiki.nesdev.com/w/index.php/NMI#Caveats 133 | : bit PPU_STATUS ; P.V (overflow) <- bit 6 (S0 hit); P.N (negative) <- bit 7 (VBLANK). 134 | bpl :- ; Keep checking until bit 7 (VBLANK) is asserted. 135 | ; First PPU frame has reached VBLANK. 136 | 137 | ; Clear zeropage: 138 | ldx #0 139 | txa 140 | : sta $00,x 141 | inx 142 | bne :- 143 | 144 | ; Disable 'decimal' mode. 145 | cld 146 | 147 | ; Move all sprites below line 240, so they're hidden. 148 | ; Here, we PREPARE this by loading $0200-$02FF with data that we will transfer, 149 | ; via DMA, to the NES OAM (Object Attribute Memory) in the PPU. The DMA will take 150 | ; place after we know the PPU is ready (i.e. after 2nd VBLANK). 151 | ; NOTE: OAM RAM contains 64 sprite definitions, each described by 4 bytes: 152 | ; byte 0: Y position of the top of the sprite. 153 | ; byte 1: Tile number. 154 | ; byte 2: Attributes (inc. palette, priority, and flip). 155 | ; byte 3: X position of the left of the sprite. 156 | ldx #0 157 | lda #$FF 158 | : sta OAM_RAM,x ; Each 4th byte in OAM (e.g. $00, $04, $08, etc.) is the Y position. 159 | Repeat 4, inx 160 | bne :- 161 | ; NOTE our DMA isn't triggered until a bit later on. 162 | 163 | ; Wait for second VBLANK: 164 | : bit PPU_STATUS 165 | bpl :- 166 | ; VLBANK asserted: PPU is now fully stabilised. 167 | 168 | ; --- We're still in VBLANK for a short while, so do video prep now --- 169 | 170 | ; Load the main palette. 171 | ; $3F00-$3F1F in the PPU address space is where palette data is kept, 172 | ; organised as 2 sets (background & sprite sets) of 4 palettes, each 173 | ; being 4 bytes long (but only the upper 3 bytes of each being used). 174 | ; That is 2(sets) x 4(palettes) x 3(colours). $3F00 itself is the 175 | ; "backdrop" colour, or the universal background colour. 176 | ppu_addr $3F00 ; Tell the PPU we want to access address $3F00 in its address space. 177 | ldx #0 178 | : lda palette_data,x 179 | sta PPU_DATA 180 | inx 181 | cpx #32 ; P.C gets set if X>=M (i.e. X>=32). 182 | bcc :- ; Loop if P.C is clear. 183 | ; NOTE: Trying to load the palette outside of VBLANK may lead to the colours being 184 | ; rendered as pixels on the screen. See: 185 | ; http://wiki.nesdev.com/w/index.php/Palette#The_background_palette_hack 186 | 187 | ; Clear the first nametable. 188 | ; Each nametable is 1024 bytes of memory, arranged as 32 columns by 30 rows of 189 | ; tile references, for a total of 960 ($3C0) bytes. The remaining 64 bytes are 190 | ; for the attribute table of that nametable. 191 | ; Nametable 0 starts at PPU address $2000. 192 | ; For more information, see: http://wiki.nesdev.com/w/index.php/Nametable 193 | ; NOTE: In order to keep this loop tight (knowing we can only count up to 194 | ; 255 in a single loop, rather than 960), we just have one loop and do 195 | ; multiple writes in it. 196 | ppu_addr $2000 197 | lda #0 198 | ldx #32*30/4 ; Only need to repeat a quarter of the time, since the loop writes 4 times. 199 | : Repeat 4, sta PPU_DATA 200 | dex 201 | bne :- 202 | 203 | ; Clear attribute table. 204 | ; One palette (out of the 4 background palettes available) may be assigned 205 | ; per 2x2 group of tiles. The actual layout of the attribute table is a bit 206 | ; funny. See here for more info: http://wiki.nesdev.com/w/index.php/PPU_attribute_tables 207 | ; See also, the bottom of: http://www.thetechnickel.com/video-games/nes-development-intro 208 | ldx #64 209 | lda #$55 ; Select palette 1 (2nd palette) throughout. 210 | : sta PPU_DATA 211 | dex 212 | bne :- 213 | 214 | ; Activate VBLANK NMIs. 215 | lda #VBLANK_NMI 216 | sta PPU_CTRL 217 | 218 | ; Now wait until nmi_counter increments, to indicate the next VBLANK. 219 | wait_for_nmi 220 | ; By this point, we're in the 3rd VBLANK. 221 | 222 | ; Trigger DMA to copy from local OAM_RAM ($0200-$02FF) to PPU OAM RAM. 223 | ; For more info on DMA, see: http://wiki.nesdev.com/w/index.php/PPU_OAM#DMA 224 | lda #0 225 | sta PPU_OAM_ADDR ; Specify the target starts at $00 in the PPU's OAM RAM. 226 | lda #>OAM_RAM ; Get upper byte (i.e. page) of source RAM for DMA operation. 227 | sta OAM_DMA ; Trigger the DMA. 228 | ; DMA will halt the CPU while it copies 256 bytes from $0200-$02FF 229 | ; into $00-$FF of the PPU's OAM RAM. 230 | 231 | ; Set X & Y scrolling positions (which have ranges of 0-255 and 0-239 respectively): 232 | lda #0 233 | sta PPU_SCROLL ; Write X position first. 234 | sta PPU_SCROLL ; Then write Y position. 235 | 236 | ; Configure PPU parameters/behaviour/table selection: 237 | lda #VBLANK_NMI|BG_0|SPR_0|NT_0|VRAM_RIGHT 238 | sta PPU_CTRL 239 | 240 | ; Turn the screen on, by activating background and sprites: 241 | lda #BG_ON|SPR_ON 242 | sta PPU_MASK 243 | 244 | ; Wait until the screen refreshes. 245 | wait_for_nmi 246 | ; OK, at this point we know the screen is visible, ready, and waiting. 247 | 248 | ; ------ Configure noise channel ------ 249 | 250 | ; Set noise type and period: 251 | ; 0------- Pseudo-random noise (instead of random regular waveform). 252 | ; ----1000 Mid-range period/frequency. 253 | lda #%00001000 254 | sta APU_NOISE_FREQ ; Noise mode & period (frequency). 255 | 256 | ; Set volume control: 257 | ; --0----- Use silencing timer (makes it one-shot). 258 | ; ---0---- Use volume envelope (fade). 259 | ; ----0000 Envelope length (shortest). 260 | lda #%00000000 ; Very short fade, one-shot. 261 | sta APU_NOISE_VOL ; Noise channel volume control. 262 | 263 | ; Set length counter: 264 | ; 11111--- Maximum timer (though other values seem to have no effect?) 265 | lda #%11111000 266 | sta APU_NOISE_TIMER ; Length counter load. 267 | 268 | ; Channel control: 269 | ; ----1--- Enable noise channel. 270 | lda #%00001000 271 | sta APU_CHAN_CTRL ; Channel control. 272 | 273 | 274 | message_loop: 275 | ; Wait 1s (60 frames at 60Hz): 276 | nmi_delay 60 277 | 278 | ; Make a debug click by firing the noise channel one-shot 279 | ; (by loading the length counter from a value selected from 280 | ; a look-up table, specified here by the upper 5 bits). 281 | ; The table is described here: 282 | ; http://wiki.nesdev.com/w/index.php/APU_Length_Counter#Table_structure 283 | ; ...and in this case you can see that $03 is the shortest (2). 284 | lda #%00011000 ; 285 | sta APU_NOISE_TIMER 286 | 287 | ; Clear the first 8 lines of the nametable: 288 | ppu_addr $2000 289 | lda #0 290 | ldx #(32*8/4) 291 | : Repeat 4, sta PPU_DATA 292 | dex 293 | bne :- 294 | 295 | ; Now fix the palettes for those 8 lines: 296 | lda #$23 297 | sta PPU_ADDR 298 | lda #$C0 ; Select 1st metarow (rows 0-3; we'll then do 4-7). 299 | sta PPU_ADDR 300 | ldx #16 ; Fill two metarows (8 bytes each) 301 | lda #%01010101 ; Both the upper rows (bits 0-3) and the lower rows (bits 4-7) get pallete 1 (%01 x 4). 302 | : sta PPU_DATA 303 | dex 304 | bne :- 305 | 306 | ; Point screen offset counter back to start of line 2: 307 | lda #(32*2) 308 | sta screen_offset 309 | 310 | ; Point back to start of source message: 311 | lda #0 312 | sta msg_ptr 313 | 314 | ; Fix scroll position: 315 | lda #0 316 | sta PPU_SCROLL ; Write X position first. 317 | sta PPU_SCROLL ; Then write Y position. 318 | 319 | ; Wait 1s: 320 | nmi_delay 60 321 | 322 | char_loop: 323 | ; Fix message screen offset pointer: 324 | lda #$20 ; Hi-byte of $2000 325 | sta PPU_ADDR 326 | lda screen_offset ; Get current screen offset. 327 | inc screen_offset ; Increment screen offset variable, for next time. 328 | sta PPU_ADDR 329 | 330 | ; Fix scroll position: 331 | lda #0 332 | sta PPU_SCROLL ; Write X position first. 333 | sta PPU_SCROLL ; Then write Y position. 334 | 335 | ; Write next character of message: 336 | ldx msg_ptr ; Get message offset. 337 | inc msg_ptr ; Increment message offset source. 338 | lda hello_msg,x ; Get message character. 339 | beq message_done ; A=0 => End of message. 340 | sta PPU_DATA ; Write the character. 341 | 342 | cmp #$20 343 | beq skip_click ; Don't make a click for space characters. 344 | 345 | ; Activate short one-shot noise effect here, by loading length counter: 346 | lda #%00111000 ; Length ID 7 is "6" (?) steps => 6/240 => 25ms?? 347 | sta APU_NOISE_TIMER 348 | 349 | skip_click: 350 | ; Wait for 50ms (3 frames at 60Hz): 351 | nmi_delay 3 352 | jmp char_loop ; Go process the next character. 353 | 354 | message_done: 355 | ; Message is done; wait half a second. 356 | nmi_delay 30 357 | ; Change the text colour of the 5th and 6th rows. 358 | lda #$23 ; Attribute table starts at $23C0. 359 | sta PPU_ADDR 360 | lda #$C8 ; Select 2nd metarow (rows 4, 5, 6, and 7). 361 | sta PPU_ADDR 362 | ldx #8 ; Fill just one metarow. 363 | lda #%01011111 ; Lower 2 rows (bits 4-7) get palette 1 (%01 x 2), upper 2 get palette 3 (%11 x 2). 364 | : sta PPU_DATA 365 | dex 366 | bne :- 367 | 368 | ; Fix scroll position: 369 | lda #0 370 | sta PPU_SCROLL ; Write X position first. 371 | sta PPU_SCROLL ; Then write Y position. 372 | 373 | ; Wait 1 sec: 374 | nmi_delay 60 375 | 376 | ; Scroll off screen: 377 | ldx #0 378 | scroll_loop: 379 | cpx #((6*8)<<1) ; Scroll by 56 scanlines (7 lines), using lower bit to halve the speed. 380 | beq repeat_message_loop ; Reached our target scroll limit. 381 | wait_for_nmi 382 | lda #0 383 | sta PPU_SCROLL ; X scroll is still 0. 384 | txa 385 | lsr a ; Discard lower bit. 386 | sta PPU_SCROLL ; Y scroll is upper 6 bits of X register. 387 | inx ; Increment scroll counter. 388 | jmp scroll_loop 389 | 390 | repeat_message_loop: 391 | jmp message_loop 392 | 393 | .endproc 394 | 395 | 396 | 397 | ; ===== CHR-ROM Pattern Tables ================================================= 398 | 399 | ; ----- Pattern Table 0 -------------------------------------------------------- 400 | 401 | .segment "PATTERN0" 402 | 403 | .incbin "anton.chr" 404 | 405 | .segment "PATTERN1" 406 | 407 | .repeat $100 408 | .byt %11111111 409 | .byt %10111011 410 | .byt %11010111 411 | .byt %11101111 412 | .byt %11010111 413 | .byt %10111011 414 | .byt %11111111 415 | .byt %11111111 416 | Repeat 8, .byt $FF 417 | .endrepeat 418 | --------------------------------------------------------------------------------